From e58f19f59372cedfb4bac9e8fee5f6063a217230 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 15 Aug 2025 13:13:51 +1000 Subject: [PATCH] Validation for verifactu documents --- .../Validation/Verifactu/EntityLevel.php | 20 ++++ .../Validation/VerifactuDocumentValidator.php | 104 +++++++++++++++++- .../Standards/Verifactu/RegistroAlta.php | 6 +- 3 files changed, 123 insertions(+), 7 deletions(-) diff --git a/app/Services/EDocument/Standards/Validation/Verifactu/EntityLevel.php b/app/Services/EDocument/Standards/Validation/Verifactu/EntityLevel.php index 79cf3f2e66..6b7ab1a34d 100644 --- a/app/Services/EDocument/Standards/Validation/Verifactu/EntityLevel.php +++ b/app/Services/EDocument/Standards/Validation/Verifactu/EntityLevel.php @@ -98,6 +98,26 @@ class EntityLevel implements EntityLevelInterface } + $_invoice = (new \App\Services\EDocument\Standards\Verifactu\RegistroAlta($invoice))->run()->getInvoice(); + $xml = $_invoice->toXmlString(); + + $xslt = new \App\Services\EDocument\Standards\Validation\VerifactuDocumentValidator($xml); + $xslt->validate(); + $errors = $xslt->getVerifactuErrors(); + nlog($errors); + + if (isset($errors['stylesheet']) && count($errors['stylesheet']) > 0) { + $this->errors['invoice'] = array_merge($this->errors['invoice'], $errors['stylesheet']); + } + + if (isset($errors['general']) && count($errors['general']) > 0) { + $this->errors['invoice'] = array_merge($this->errors['invoice'], $errors['general']); + } + + if (isset($errors['xsd']) && count($errors['xsd']) > 0) { + $this->errors['invoice'] = array_merge($this->errors['invoice'], $errors['xsd']); + } + // $this->errors['invoice'][] = 'test error'; $this->errors['passes'] = count($this->errors['invoice']) === 0 && count($this->errors['company']) === 0; //no need to check client as we are using client level settings diff --git a/app/Services/EDocument/Standards/Validation/VerifactuDocumentValidator.php b/app/Services/EDocument/Standards/Validation/VerifactuDocumentValidator.php index 8b6144adaf..c4c6842ed9 100644 --- a/app/Services/EDocument/Standards/Validation/VerifactuDocumentValidator.php +++ b/app/Services/EDocument/Standards/Validation/VerifactuDocumentValidator.php @@ -79,11 +79,7 @@ class VerifactuDocumentValidator extends XsltDocumentValidator libxml_clear_errors(); foreach ($errors as $error) { - $this->errors['xsd'][] = sprintf( - 'Line %d: %s', - $error->line, - trim($error->message) - ); + $this->errors['xsd'][] = $this->formatXsdError($error); } } } @@ -91,6 +87,104 @@ class VerifactuDocumentValidator extends XsltDocumentValidator 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 + { + // Common error patterns and their translations + $errorTranslations = [ + // Missing child elements + '/Missing child element\(s\)\. Expected is \( ([^)]+) \)/' => 'Faltan elementos requeridos: $1', + '/Missing child element\(s\)\. Expected is \( ([^)]+) \)/' => 'Missing required elements: $1', + + // Element not found + '/Element ([^:]+): ([^:]+) not found/' => 'Elemento no encontrado: $2', + '/Element ([^:]+): ([^:]+) not found/' => 'Element not found: $2', + + // Invalid content + '/Element ([^:]+): ([^:]+) has invalid content/' => 'Contenido inválido en elemento: $2', + '/Element ([^:]+): ([^:]+) has invalid content/' => 'Invalid content in element: $2', + + // Required attribute missing + '/The attribute ([^:]+) is required/' => 'Atributo requerido faltante: $1', + '/The attribute ([^:]+) is required/' => 'Required attribute missing: $1', + + // Value not allowed + '/Value ([^:]+) is not allowed/' => 'Valor no permitido: $1', + '/Value ([^:]+) is not allowed/' => 'Value not allowed: $1', + + // Pattern validation failed + '/Element ([^:]+): ([^:]+) is not a valid value of the atomic type/' => 'Valor inválido para el elemento: $2', + '/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)', + ]; + + 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 */ diff --git a/app/Services/EDocument/Standards/Verifactu/RegistroAlta.php b/app/Services/EDocument/Standards/Verifactu/RegistroAlta.php index 19727ba8b4..1f6b053e72 100644 --- a/app/Services/EDocument/Standards/Verifactu/RegistroAlta.php +++ b/app/Services/EDocument/Standards/Verifactu/RegistroAlta.php @@ -201,23 +201,25 @@ class RegistroAlta $client_country_code = $this->invoice->client->country->iso_3166_2; + /** By Default we assume a Spanish transaction */ $impuesto = 'S2'; $clave_regimen = '08'; $calificacion = 'S1'; $br = new \App\DataMapper\Tax\BaseRule(); + /** EU B2B */ if (in_array($client_country_code, $br->eu_country_codes) && $this->invoice->client->classification != 'individual') { $impuesto = '05'; $clave_regimen = '05'; $calificacion = 'N2'; - } + } /** EU B2C */ elseif (in_array($client_country_code, $br->eu_country_codes) && $this->invoice->client->classification == 'individual') { $impuesto = '08'; $clave_regimen = '05'; $calificacion = 'N2'; } - else{ //Non-EU + else { /** Non-EU */ $impuesto = '05'; $clave_regimen = '05'; $calificacion = 'N2';