diff --git a/app/Services/EDocument/Standards/Peppol.php b/app/Services/EDocument/Standards/Peppol.php index f453fdeff6..d4278cdbd4 100644 --- a/app/Services/EDocument/Standards/Peppol.php +++ b/app/Services/EDocument/Standards/Peppol.php @@ -1407,11 +1407,13 @@ class Peppol extends AbstractService $party->PartyIdentification[] = $pi; - $pts = new \InvoiceNinja\EInvoice\Models\Peppol\PartyTaxSchemeType\PartyTaxScheme(); + //// If this is intracommunity supply, ensure that the country prefix is on the party tax scheme + $pts = new \InvoiceNinja\EInvoice\Models\Peppol\PartyTaxSchemeType\PartyTaxScheme(); $companyID = new \InvoiceNinja\EInvoice\Models\Peppol\IdentifierType\CompanyID(); - $companyID->value = preg_replace("/[^a-zA-Z0-9]/", "", $this->invoice->client->vat_number); + $companyID->value = $this->ensureVatNumberPrefix($this->invoice->client->vat_number, $this->invoice->client->country->iso_3166_2); $pts->CompanyID = $companyID; + //// If this is intracommunity supply, ensure that the country prefix is on the party tax scheme $ts = new TaxScheme(); $id = new ID(); @@ -1920,6 +1922,30 @@ class Peppol extends AbstractService return '0037'; } + /** + * Ensures the VAT number has the correct country code prefix. + * + * @param string $vatNumber The raw VAT number. + * @param string $countryCode The 2-letter ISO country code. + * @return string The formatted VAT number with prefix. + */ + private function ensureVatNumberPrefix(string $vatNumber, string $countryCode): string + { + // Clean the VAT number by removing non-alphanumeric characters + $cleanedVat = preg_replace("/[^a-zA-Z0-9]/", "", $vatNumber); + + // Handle Greece special case + $prefix = ($countryCode === 'GR') ? 'EL' : $countryCode; + + // Check if the VAT number already starts with the correct prefix + if (str_starts_with(strtoupper($cleanedVat), $prefix)) { + return $cleanedVat; + } + + // Prepend the prefix if it's missing + return $prefix . $cleanedVat; + } + public function getErrors(): array { return $this->errors; diff --git a/app/Services/Template/TemplateService.php b/app/Services/Template/TemplateService.php index 6391d59e7c..77772bd60d 100644 --- a/app/Services/Template/TemplateService.php +++ b/app/Services/Template/TemplateService.php @@ -161,7 +161,7 @@ class TemplateService $this->twig->addFilter($filter); $allowedTags = ['if', 'for', 'set', 'filter']; - $allowedFilters = ['default', 'groupBy','capitalize', 'abs', 'date_modify', 'keys', 'join', 'reduce', 'format_date','json_decode','date_modify','trim','round','format_spellout_number','split', 'reduce','replace', 'escape', 'e', 'reverse', 'shuffle', 'slice', 'batch', 'title', 'sort', 'split', 'upper', 'lower', 'capitalize', 'filter', 'length', 'merge','format_currency', 'format_number','format_percent_number','map', 'join', 'first', 'date', 'sum', 'number_format','nl2br','striptags','markdown_to_html']; + $allowedFilters = ['url_encode','default', 'groupBy','capitalize', 'abs', 'date_modify', 'keys', 'join', 'reduce', 'format_date','json_decode','date_modify','trim','round','format_spellout_number','split', 'reduce','replace', 'escape', 'e', 'reverse', 'shuffle', 'slice', 'batch', 'title', 'sort', 'split', 'upper', 'lower', 'capitalize', 'filter', 'length', 'merge','format_currency', 'format_number','format_percent_number','map', 'join', 'first', 'date', 'sum', 'number_format','nl2br','striptags','markdown_to_html']; $allowedFunctions = ['range', 'cycle', 'constant', 'date','img','t']; $allowedProperties = ['type_id']; // $allowedMethods = ['img','t']; diff --git a/tests/Feature/EInvoice/PeppolXmlValidationTest.php b/tests/Feature/EInvoice/PeppolXmlValidationTest.php new file mode 100644 index 0000000000..41c1a64ed1 --- /dev/null +++ b/tests/Feature/EInvoice/PeppolXmlValidationTest.php @@ -0,0 +1,378 @@ + + + urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0 + urn:fdc:peppol.eu:2017:poacc:billing:01:1.0 + INV-20xx-0001 + 2026-01-23 + 2026-02-23 + 380 + Autoliquidation Following art. + EUR + REF-12345/001/0001 + + REF-12345/001/0001 + + + Invoice_INV-20xx-0001.pdf + + + + BE0123456789 + + BE0123456789 + + + Example Company S.A. + + + Example Street 123 + Brussels + 1000 + + BE + + + + BE0123456789 + + VAT + + + + Example Company S.A. + + + John Doe + +31 2 123 45 67 + contact@example.com + + + + + + 987654321 + + 987654321 + + + Customer Company GmbH + + + Customer Street 456 + Berlin + 10115 + + DE + + + + DE987654321 + + VAT + + + + Customer Company GmbH + + + contact@customer.com + + + + + 2026-01-21 + + + + BE + + + + + + 1 + + + 30 Days + + + 0 + + 10000 + 0 + + K + 0 + vatex-eu-ic + Intra-Community supply + + VAT + + + + + + 10000 + 10000 + 10000 + 0 + 0 + 10000 + + + 1 + 10 + 1000 + + Service Support Package A + SVC-001 + + K + 0 + + VAT + + + + + 100 + + + + 2 + 5 + 500 + + Service Support Package B + SVC-002 + + K + 0 + + VAT + + + + + 100 + + + + 3 + 20 + 2000 + + Service Support Package C + SVC-003 + + K + 0 + + VAT + + + + + 100 + + + + 4 + 8 + 800 + + Service Support D + SVC-004 + + K + 0 + + VAT + + + + + 100 + + + + 5 + 8 + 800 + + Service Support Package E + SVC-005 + + K + 0 + + VAT + + + + + 100 + + + + 6 + 2 + 1000 + + Software License A + SW-001 + + K + 0 + + VAT + + + + + 500 + + + + 7 + 2 + 1000 + + Software License B + SW-002 + + K + 0 + + VAT + + + + + 500 + + + + 8 + 5 + 1000 + + Software License C + SW-003 + + K + 0 + + VAT + + + + + 200 + + + + 9 + 10 + 500 + + Additional Service Package + SVC-006 + + K + 0 + + VAT + + + + + 50 + + + + 10 + 2 + 500 + + Additional Feature Package + SVC-007 + + K + 0 + + VAT + + + + + 250 + + + + 11 + 3 + 900 + + Professional Services - Remote + PS-001 + + K + 0 + + VAT + + + + + 300 + + + +'; + + +public function setUp(): void +{ + parent::setUp(); + + try { + $processor = new \Saxon\SaxonProcessor(); + } catch (\Throwable $e) { + $this->markTestSkipped('saxon not installed'); + } + +} + +public function testPeppolXmlValidation() +{ + + try { + $processor = new \Saxon\SaxonProcessor(); + } catch (\Throwable $e) { + $this->markTestSkipped('saxon not installed'); + } + + $validator = new XsltDocumentValidator($this->xml); + $validator->validate(); + + if (count($validator->getErrors()) > 0) { + // nlog($this->xml); + nlog($validator->getErrors()); + } + + $this->assertCount(0, $validator->getErrors()); + } +} \ No newline at end of file