609 lines
25 KiB
PHP
609 lines
25 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Invoice Ninja (https://invoiceninja.com).
|
|
*
|
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
|
*
|
|
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
|
|
*
|
|
* @license https://www.elastic.co/licensing/elastic-license
|
|
*/
|
|
|
|
namespace App\Services\EDocument\Standards\Validation;
|
|
|
|
/**
|
|
* VerifactuDocumentValidator - Validates Verifactu XML documents
|
|
*
|
|
* Extends the base XsltDocumentValidator but is configured specifically for Verifactu
|
|
* validation using the correct XSD schemas and namespaces.
|
|
*/
|
|
class VerifactuDocumentValidator extends XsltDocumentValidator
|
|
{
|
|
private array $verifactu_stylesheets = [
|
|
// Add any Verifactu-specific stylesheets here if needed
|
|
// '/Services/EDocument/Standards/Validation/Verifactu/Stylesheets/verifactu-validation.xslt',
|
|
];
|
|
|
|
private string $verifactu_xsd = 'Services/EDocument/Standards/Verifactu/xsd/SuministroLR.xsd';
|
|
private string $verifactu_informacion_xsd = 'Services/EDocument/Standards/Verifactu/xsd/SuministroInformacion.xsd';
|
|
|
|
public function __construct(public string $xml_document)
|
|
{
|
|
parent::__construct($xml_document);
|
|
|
|
// Override the base configuration for Verifactu
|
|
$this->setXsd($this->verifactu_xsd);
|
|
$this->setStyleSheets($this->verifactu_stylesheets);
|
|
}
|
|
|
|
/**
|
|
* Validate Verifactu XML document
|
|
*
|
|
* @return self
|
|
*/
|
|
public function validate(): self
|
|
{
|
|
$this->validateVerifactuXsd()
|
|
->validateVerifactuSchema();
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Validate against Verifactu XSD schemas
|
|
*/
|
|
private function validateVerifactuXsd(): self
|
|
{
|
|
libxml_use_internal_errors(true);
|
|
|
|
$xml = new \DOMDocument();
|
|
$xml->loadXML($this->xml_document);
|
|
|
|
// Extract business content from SOAP envelope if needed
|
|
$businessContent = $this->extractBusinessContent($xml);
|
|
|
|
// Detect document type to determine which validation to apply
|
|
$documentType = $this->detectDocumentType($businessContent);
|
|
|
|
nlog("Detected document type: " . $documentType);
|
|
|
|
// For modifications, we need to use a different validation approach
|
|
// since the standard XSD doesn't support modification structure
|
|
if ($documentType === 'modification') {
|
|
$this->validateModificationDocument($businessContent);
|
|
} else {
|
|
// For registration and cancellation, use standard XSD validation
|
|
if (!$businessContent->schemaValidate(app_path($this->verifactu_xsd))) {
|
|
$errors = libxml_get_errors();
|
|
libxml_clear_errors();
|
|
|
|
foreach ($errors as $error) {
|
|
$this->errors['xsd'][] = $this->formatXsdError($error);
|
|
}
|
|
}
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Format XSD validation errors to be more human-readable
|
|
*
|
|
* @param \LibXMLError $error The libxml error object
|
|
* @return string Formatted error message
|
|
*/
|
|
private function formatXsdError(\LibXMLError $error): string
|
|
{
|
|
$message = trim($error->message);
|
|
$line = $error->line;
|
|
|
|
// Remove long namespace URLs to make errors more readable
|
|
$message = preg_replace(
|
|
'/\{https:\/\/www2\.agenciatributaria\.gob\.es\/static_files\/common\/internet\/dep\/aplicaciones\/es\/aeat\/tike\/cont\/ws\/[^}]+\}/',
|
|
'',
|
|
$message
|
|
);
|
|
|
|
// Clean up the message and make it more user-friendly
|
|
$message = $this->translateXsdError($message);
|
|
|
|
return sprintf('Line %d: %s', $line, $message);
|
|
}
|
|
|
|
/**
|
|
* Translate XSD error messages to more user-friendly Spanish/English descriptions
|
|
*
|
|
* @param string $message The original XSD error message
|
|
* @return string Translated and improved error message
|
|
*/
|
|
private function translateXsdError(string $message): string
|
|
{
|
|
// Handle missing child element error specifically
|
|
if (preg_match('/Missing child element\(s\)\. Expected is \( ([^)]+) \)/', $message, $matches)) {
|
|
$expectedElement = trim($matches[1]);
|
|
$message = "Missing required child element: $expectedElement";
|
|
}
|
|
|
|
// Common error patterns and their translations
|
|
$errorTranslations = [
|
|
// Element not found
|
|
'/Element ([^:]+): ([^:]+) not found/' => 'Element not found: $2',
|
|
|
|
// Invalid content
|
|
'/Element ([^:]+): ([^:]+) has invalid content/' => 'Invalid content in element: $2',
|
|
|
|
// Required attribute missing
|
|
'/The attribute ([^:]+) is required/' => 'Required attribute missing: $1',
|
|
|
|
// Value not allowed
|
|
'/Value ([^:]+) is not allowed/' => 'Value not allowed: $1',
|
|
|
|
// Pattern validation failed
|
|
'/Element ([^:]+): ([^:]+) is not a valid value of the atomic type/' => 'Invalid value for element: $2',
|
|
];
|
|
|
|
// Apply translations
|
|
foreach ($errorTranslations as $pattern => $replacement) {
|
|
if (preg_match($pattern, $message, $matches)) {
|
|
$message = preg_replace($pattern, $replacement, $message);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Clean up common element names and make them more readable
|
|
$elementTranslations = [
|
|
'Desglose' => 'Desglose (Tax Breakdown)',
|
|
'DetalleDesglose' => 'DetalleDesglose (Tax Detail)',
|
|
'TipoFactura' => 'TipoFactura (Invoice Type)',
|
|
'DescripcionOperacion' => 'DescripcionOperacion (Operation Description)',
|
|
'ImporteTotal' => 'ImporteTotal (Total Amount)',
|
|
'RegistroAlta' => 'RegistroAlta (Registration Record)',
|
|
'RegistroAnulacion' => 'RegistroAnulacion (Cancellation Record)',
|
|
'FacturasRectificadas' => 'FacturasRectificadas (Corrected Invoices)',
|
|
'IDFacturaRectificada' => 'IDFacturaRectificada (Corrected Invoice ID)',
|
|
'IDEmisorFactura' => 'IDEmisorFactura (Invoice Emitter ID)',
|
|
'NumSerieFactura' => 'NumSerieFactura (Invoice Series Number)',
|
|
'FechaExpedicionFactura' => 'FechaExpedicionFactura (Invoice Issue Date)',
|
|
'Impuestos' => 'Impuestos (Taxes)',
|
|
'DetalleIVA' => 'DetalleIVA (VAT Detail)',
|
|
'CuotaRepercutida' => 'CuotaRepercutida (Recharged Tax Amount)',
|
|
'FechaExpedicionFacturaEmisor' => 'FechaExpedicionFacturaEmisor (Emitter Invoice Issue Date)',
|
|
];
|
|
|
|
// Apply element translations
|
|
foreach ($elementTranslations as $element => $translation) {
|
|
$message = str_replace($element, $translation, $message);
|
|
}
|
|
|
|
// Remove extra whitespace and clean up the message
|
|
$message = preg_replace('/\s+/', ' ', $message);
|
|
$message = trim($message);
|
|
|
|
return $message;
|
|
}
|
|
|
|
/**
|
|
* Detect the type of Verifactu document
|
|
*/
|
|
private function detectDocumentType(\DOMDocument $doc): string
|
|
{
|
|
$xpath = new \DOMXPath($doc);
|
|
$xpath->registerNamespace('si', 'https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd');
|
|
$xpath->registerNamespace('sum1', 'https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd');
|
|
|
|
// Check for modification structure - look for RegistroAlta with TipoFactura R1
|
|
$registroAlta = $xpath->query('//si:RegistroAlta | //sum1:RegistroAlta');
|
|
if ($registroAlta->length > 0) {
|
|
$tipoFactura = $xpath->query('.//si:TipoFactura | .//sum1:TipoFactura', $registroAlta->item(0));
|
|
if ($tipoFactura->length > 0 && in_array($tipoFactura->item(0)->textContent,['R1','F3'])) {
|
|
return 'modification';
|
|
}
|
|
}
|
|
|
|
|
|
// Check for cancellation structure
|
|
$registroAnulacion = $xpath->query('//si:RegistroAnulacion | //sum1:RegistroAnulacion');
|
|
if ($registroAnulacion->length > 0) {
|
|
return 'cancellation';
|
|
}
|
|
|
|
// Check for registration structure (RegistroAlta with TipoFactura not R1)
|
|
if ($registroAlta->length > 0) {
|
|
$tipoFactura = $xpath->query('.//si:TipoFactura | .//sum1:TipoFactura', $registroAlta->item(0));
|
|
if ($tipoFactura->length === 0 || $tipoFactura->item(0)->textContent !== 'R1') {
|
|
return 'registration';
|
|
}
|
|
}
|
|
|
|
return 'unknown';
|
|
}
|
|
|
|
/**
|
|
* Validate modification documents using business rules instead of strict XSD
|
|
*/
|
|
private function validateModificationDocument(\DOMDocument $doc): void
|
|
{
|
|
$xpath = new \DOMXPath($doc);
|
|
$xpath->registerNamespace('si', 'https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd');
|
|
$xpath->registerNamespace('sum1', 'https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd');
|
|
$xpath->registerNamespace('lr', 'https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroLR.xsd');
|
|
|
|
// Validate modification-specific structure
|
|
$this->validateModificationStructure($xpath);
|
|
|
|
// Validate required elements for modifications
|
|
$this->validateModificationRequiredElements($xpath);
|
|
|
|
// Validate business rules for modifications
|
|
$this->validateModificationBusinessRules($xpath);
|
|
}
|
|
|
|
/**
|
|
* Validate modification structure
|
|
*/
|
|
private function validateModificationStructure(\DOMXPath $xpath): void
|
|
{
|
|
// Check for RegistroAlta with TipoFactura R1
|
|
$registroAlta = $xpath->query('//si:RegistroAlta');
|
|
if ($registroAlta === false || $registroAlta->length === 0) {
|
|
// Try alternative namespace
|
|
$registroAlta = $xpath->query('//sum1:RegistroAlta');
|
|
if ($registroAlta === false || $registroAlta->length === 0) {
|
|
$this->errors['structure'][] = "RegistroAlta element not found for modification";
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Check for required modification elements within the RegistroAlta
|
|
$requiredElements = [
|
|
'.//si:TipoFactura' => 'TipoFactura',
|
|
'.//si:DescripcionOperacion' => 'DescripcionOperacion',
|
|
'.//si:ImporteTotal' => 'ImporteTotal'
|
|
];
|
|
|
|
foreach ($requiredElements as $xpathQuery => $elementName) {
|
|
$elements = $xpath->query($xpathQuery, $registroAlta->item(0));
|
|
if ($elements === false || $elements->length === 0) {
|
|
// Try alternative namespace
|
|
$altQuery = str_replace('si:', 'sum1:', $xpathQuery);
|
|
$elements = $xpath->query($altQuery, $registroAlta->item(0));
|
|
if ($elements === false || $elements->length === 0) {
|
|
$this->errors['structure'][] = "Required modification element not found: $elementName";
|
|
}
|
|
}
|
|
}
|
|
|
|
// Validate TipoFactura is R1 for modifications
|
|
$tipoFactura = $xpath->query('.//si:TipoFactura', $registroAlta->item(0));
|
|
if ($tipoFactura === false || $tipoFactura->length === 0) {
|
|
$tipoFactura = $xpath->query('.//sum1:TipoFactura', $registroAlta->item(0));
|
|
}
|
|
if ($tipoFactura !== false && $tipoFactura->length > 0 && !in_array($tipoFactura->item(0)->textContent, ['R1','F3'])) {
|
|
$this->errors['structure'][] = "TipoFactura must be 'R1' for modifications, found: " . $tipoFactura->item(0)->textContent;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate required elements for modifications
|
|
*/
|
|
private function validateModificationRequiredElements(\DOMXPath $xpath): void
|
|
{
|
|
// Check for required elements in FacturasRectificadas - look for both si: and sf: namespaces
|
|
$facturasRectificadas = $xpath->query('//si:FacturasRectificadas | //sf:FacturasRectificadas');
|
|
if ($facturasRectificadas !== false && $facturasRectificadas->length > 0) {
|
|
$idFacturasRectificadas = $xpath->query('//si:FacturasRectificadas/si:IDFacturaRectificada | //sf:FacturasRectificadas/sf:IDFacturaRectificada');
|
|
if ($idFacturasRectificadas === false || $idFacturasRectificadas->length === 0) {
|
|
$this->errors['structure'][] = "At least one IDFacturaRectificada is required in FacturasRectificadas";
|
|
} else {
|
|
// Validate each IDFacturaRectificada has required elements
|
|
foreach ($idFacturasRectificadas as $index => $idFacturaRectificada) {
|
|
$idEmisorFactura = $xpath->query('.//si:IDEmisorFactura | .//sf:IDEmisorFactura', $idFacturaRectificada);
|
|
$numSerieFactura = $xpath->query('.//si:NumSerieFactura | .//sf:NumSerieFactura', $idFacturaRectificada);
|
|
$fechaExpedicionFactura = $xpath->query('.//si:FechaExpedicionFactura | .//sf:FechaExpedicionFactura', $idFacturaRectificada);
|
|
|
|
if ($idEmisorFactura === false || $idEmisorFactura->length === 0) {
|
|
$this->errors['structure'][] = "IDEmisorFactura is required in IDFacturaRectificada " . ($index + 1);
|
|
}
|
|
if ($numSerieFactura === false || $numSerieFactura->length === 0) {
|
|
$this->errors['structure'][] = "NumSerieFactura is required in IDFacturaRectificada " . ($index + 1);
|
|
}
|
|
if ($fechaExpedicionFactura === false || $fechaExpedicionFactura->length === 0) {
|
|
$this->errors['structure'][] = "FechaExpedicionFactura is required in IDFacturaRectificada " . ($index + 1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for tax information - look for both si: and sf: namespaces
|
|
$impuestos = $xpath->query('//si:Impuestos | //sf:Impuestos');
|
|
if ($impuestos !== false && $impuestos->length > 0) {
|
|
$detalleIVA = $xpath->query('//si:Impuestos/si:DetalleIVA | //sf:Impuestos/sf:DetalleIVA');
|
|
if ($detalleIVA === false || $detalleIVA->length === 0) {
|
|
$this->errors['structure'][] = "DetalleIVA is required when Impuestos is present";
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate business rules for modifications
|
|
*/
|
|
private function validateModificationBusinessRules(\DOMXPath $xpath): void
|
|
{
|
|
// Validate ImporteTotal is numeric and positive
|
|
$importeTotal = $xpath->query('//si:ImporteTotal');
|
|
if ($importeTotal->length > 0) {
|
|
$value = $importeTotal->item(0)->textContent;
|
|
if (!is_numeric($value) || floatval($value) <= 0) {
|
|
$this->errors['business'][] = "ImporteTotal must be a positive number, found: $value";
|
|
}
|
|
}
|
|
|
|
// Validate tax amounts are consistent
|
|
$cuotaRepercutida = $xpath->query('//si:CuotaRepercutida');
|
|
if ($cuotaRepercutida->length > 0) {
|
|
$value = $cuotaRepercutida->item(0)->textContent;
|
|
if (!is_numeric($value)) {
|
|
$this->errors['business'][] = "CuotaRepercutida must be numeric, found: $value";
|
|
}
|
|
}
|
|
|
|
// Validate date formats
|
|
$fechaExpedicion = $xpath->query('//si:FechaExpedicionFacturaEmisor');
|
|
if ($fechaExpedicion->length > 0) {
|
|
$value = $fechaExpedicion->item(0)->textContent;
|
|
if (!preg_match('/^\d{2}-\d{2}-\d{4}$/', $value)) {
|
|
$this->errors['business'][] = "FechaExpedicionFacturaEmisor must be in DD-MM-YYYY format, found: $value";
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate against Verifactu-specific schema rules
|
|
*/
|
|
private function validateVerifactuSchema(): self
|
|
{
|
|
try {
|
|
// Add any Verifactu-specific validation logic here
|
|
// This could include business rule validation, format checks, etc.
|
|
|
|
// For now, we'll just do basic structure validation
|
|
$this->validateVerifactuStructure();
|
|
|
|
} catch (\Throwable $th) {
|
|
$this->errors['general'][] = $th->getMessage();
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Extract business content from SOAP envelope
|
|
*/
|
|
private function extractBusinessContent(\DOMDocument $doc): \DOMDocument
|
|
{
|
|
$xpath = new \DOMXPath($doc);
|
|
$xpath->registerNamespace('lr', 'https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroLR.xsd');
|
|
|
|
$regFactuElements = $xpath->query('//lr:RegFactuSistemaFacturacion');
|
|
|
|
if ($regFactuElements->length > 0) {
|
|
$businessContent = $regFactuElements->item(0);
|
|
|
|
$businessDoc = new \DOMDocument();
|
|
$businessDoc->appendChild($businessDoc->importNode($businessContent, true));
|
|
|
|
return $businessDoc;
|
|
}
|
|
|
|
// If no business content found, return the original document
|
|
return $doc;
|
|
}
|
|
|
|
/**
|
|
* Validate Verifactu-specific structure requirements
|
|
*/
|
|
private function validateVerifactuStructure(): void
|
|
{
|
|
$doc = new \DOMDocument();
|
|
$doc->loadXML($this->xml_document);
|
|
|
|
$xpath = new \DOMXPath($doc);
|
|
$xpath->registerNamespace('si', 'https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd');
|
|
|
|
// Check for required elements
|
|
$requiredElements = [
|
|
'//si:TipoFactura',
|
|
'//si:DescripcionOperacion',
|
|
'//si:ImporteTotal'
|
|
];
|
|
|
|
foreach ($requiredElements as $element) {
|
|
$nodes = $xpath->query($element);
|
|
if ($nodes->length === 0) {
|
|
$this->errors['structure'][] = "Required element not found: $element";
|
|
}
|
|
}
|
|
|
|
// Check for modification-specific elements
|
|
$modificationElements = $xpath->query('//si:ModificacionFactura');
|
|
if ($modificationElements->length > 0) {
|
|
// Validate modification structure
|
|
$tipoRectificativa = $xpath->query('//si:TipoRectificativa');
|
|
if ($tipoRectificativa->length === 0) {
|
|
$this->errors['structure'][] = "TipoRectificativa is required for modifications";
|
|
}
|
|
|
|
$facturasRectificadas = $xpath->query('//si:FacturasRectificadas');
|
|
if ($facturasRectificadas->length === 0) {
|
|
$this->errors['structure'][] = "FacturasRectificadas is required for modifications";
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get Verifactu-specific errors
|
|
*/
|
|
public function getVerifactuErrors(): array
|
|
{
|
|
return $this->getErrors();
|
|
}
|
|
|
|
/**
|
|
* Get detailed error information with suggestions for fixing common issues
|
|
*
|
|
* @return array Detailed error information with context and suggestions
|
|
*/
|
|
public function getDetailedErrors(): array
|
|
{
|
|
$detailedErrors = [];
|
|
|
|
foreach ($this->errors as $errorType => $errors) {
|
|
foreach ($errors as $error) {
|
|
$detailedErrors[] = [
|
|
'type' => $errorType,
|
|
'message' => $error,
|
|
'context' => $this->getErrorContext($error),
|
|
'suggestion' => $this->getErrorSuggestion($error),
|
|
'severity' => $this->getErrorSeverity($errorType)
|
|
];
|
|
}
|
|
}
|
|
|
|
return $detailedErrors;
|
|
}
|
|
|
|
/**
|
|
* Get context information for an error
|
|
*
|
|
* @param string $error The error message
|
|
* @return string Context information
|
|
*/
|
|
private function getErrorContext(string $error): string
|
|
{
|
|
if (strpos($error, 'Desglose') !== false) {
|
|
return 'The Desglose (Tax Breakdown) element requires a DetalleDesglose (Tax Detail) child element to specify the tax breakdown structure.';
|
|
}
|
|
|
|
if (strpos($error, 'TipoFactura') !== false) {
|
|
return 'The TipoFactura (Invoice Type) element specifies the type of invoice being processed (e.g., F1 for regular invoice, R1 for modification).';
|
|
}
|
|
|
|
if (strpos($error, 'DescripcionOperacion') !== false) {
|
|
return 'The DescripcionOperacion (Operation Description) element provides a description of the business operation being documented.';
|
|
}
|
|
|
|
if (strpos($error, 'ImporteTotal') !== false) {
|
|
return 'The ImporteTotal (Total Amount) element contains the total amount of the invoice including all taxes.';
|
|
}
|
|
|
|
if (strpos($error, 'FacturasRectificadas') !== false) {
|
|
return 'The FacturasRectificadas (Corrected Invoices) element is required for modification invoices to reference the original invoices being corrected.';
|
|
}
|
|
|
|
return 'This error indicates a structural issue with the XML document that prevents it from conforming to the Verifactu schema requirements.';
|
|
}
|
|
|
|
/**
|
|
* Get suggestions for fixing an error
|
|
*
|
|
* @param string $error The error message
|
|
* @return string Suggestion for fixing the error
|
|
*/
|
|
private function getErrorSuggestion(string $error): string
|
|
{
|
|
if (strpos($error, 'Missing child element') !== false && strpos($error, 'DetalleDesglose') !== false) {
|
|
return 'Add a DetalleDesglose element within the Desglose element to specify the tax breakdown details. Example: <DetalleDesglose><TipoImpositivo>21</TipoImpositivo><BaseImponible>100.00</BaseImponible><CuotaRepercutida>21.00</CuotaRepercutida></DetalleDesglose>';
|
|
}
|
|
|
|
if (strpos($error, 'TipoFactura') !== false) {
|
|
return 'Ensure the TipoFactura element contains a valid value: F1 (regular invoice), F2 (simplified invoice), F3 (modification), or R1 (modification).';
|
|
}
|
|
|
|
if (strpos($error, 'DescripcionOperacion') !== false) {
|
|
return 'Add a DescripcionOperacion element with a clear description of the business operation, such as "Venta de mercancías" or "Prestación de servicios".';
|
|
}
|
|
|
|
if (strpos($error, 'ImporteTotal') !== false) {
|
|
return 'Ensure the ImporteTotal element contains a valid numeric value representing the total invoice amount including taxes.';
|
|
}
|
|
|
|
if (strpos($error, 'FacturasRectificadas') !== false) {
|
|
return 'For modification invoices, add the FacturasRectificadas element with at least one IDFacturaRectificada containing the original invoice details.';
|
|
}
|
|
|
|
return 'Review the XML structure against the Verifactu schema requirements and ensure all required elements are present with valid content.';
|
|
}
|
|
|
|
/**
|
|
* Get error severity level
|
|
*
|
|
* @param string $errorType The type of error
|
|
* @return string Severity level
|
|
*/
|
|
private function getErrorSeverity(string $errorType): string
|
|
{
|
|
return match($errorType) {
|
|
'xsd' => 'high',
|
|
'structure' => 'medium',
|
|
'business' => 'low',
|
|
'general' => 'medium',
|
|
default => 'medium'
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get a user-friendly summary of validation errors
|
|
*
|
|
* @return string Summary of validation errors
|
|
*/
|
|
public function getErrorSummary(): string
|
|
{
|
|
if (empty($this->errors)) {
|
|
return 'Document validation passed successfully.';
|
|
}
|
|
|
|
$summary = [];
|
|
$totalErrors = 0;
|
|
|
|
foreach ($this->errors as $errorType => $errors) {
|
|
$count = count($errors);
|
|
$totalErrors += $count;
|
|
|
|
$typeLabel = match($errorType) {
|
|
'xsd' => 'Schema Validation Errors',
|
|
'structure' => 'Structural Errors',
|
|
'business' => 'Business Rule Violations',
|
|
'general' => 'General Errors',
|
|
default => ucfirst($errorType) . ' Errors'
|
|
};
|
|
|
|
$summary[] = "$typeLabel: $count";
|
|
}
|
|
|
|
$summaryText = "Validation failed with $totalErrors total error(s):\n";
|
|
$summaryText .= implode(', ', $summary);
|
|
|
|
return $summaryText;
|
|
}
|
|
|
|
/**
|
|
* Get errors formatted for display in logs or user interfaces
|
|
*
|
|
* @return array Formatted errors grouped by type
|
|
*/
|
|
public function getFormattedErrors(): array
|
|
{
|
|
$formatted = [];
|
|
|
|
foreach ($this->errors as $errorType => $errors) {
|
|
$formatted[$errorType] = [
|
|
'count' => count($errors),
|
|
'messages' => $errors,
|
|
'severity' => $this->getErrorSeverity($errorType)
|
|
];
|
|
}
|
|
|
|
return $formatted;
|
|
}
|
|
}
|