From 04cedea2f18f7b20cb42d171e3b38084026a279b Mon Sep 17 00:00:00 2001 From: Alberto Contreras Date: Wed, 26 Nov 2025 21:54:21 +0100 Subject: [PATCH 1/3] UPDATE Invoice submission process to include an option to send corrections --- README.md | 9 ++++--- src/models/InvoiceSubmission.php | 9 ++++++- src/services/InvoiceSerializer.php | 5 +++- tests/Unit/Services/InvoiceSerializerTest.php | 24 +++++++++++++++++++ 4 files changed, 42 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 312d521..a1a2bef 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ composer require robrichards/xmlseclibs ### 1. **Configuration** Before using any service, you must configure the library with your certificate, password, certificate type, and -environment. +environment. Choose between certificate type (`certificate` or `seal`) and environment (`production` or `sandbox`) according to whether you'll be working in production or testing. @@ -145,14 +145,17 @@ $invoice->operationDescription = 'Venta de productos'; $invoice->taxAmount = 21.00; // Total tax amount $invoice->totalAmount = 121.00; // Total invoice amount $invoice->simplifiedInvoice = YesNoType::NO; -$invoice->invoiceWithoutRecipient = YesNoType::NO; +$invoice->invoiceWithoutRecipient = YesNoType::NO; + +// If providing a subsanation after receiving an "Accepted with error" message +// $invoice->isSubsanation = true; // Add tax breakdown (using object-oriented approach) $breakdown = new Breakdown(); $detail = new BreakdownDetail(); $detail->taxType = TaxType::IVA; $detail->taxRate = 21.00; -$detail->taxableBase = 100.00; +$detail->taxableBase = 100.00; $detail->taxAmount = 21.00; $detail->operationQualification = OperationQualificationType::SUBJECT_NO_EXEMPT_NO_REVERSE; $breakdown->addDetail($detail); diff --git a/src/models/InvoiceSubmission.php b/src/models/InvoiceSubmission.php index e5cf23f..29a2736 100644 --- a/src/models/InvoiceSubmission.php +++ b/src/models/InvoiceSubmission.php @@ -53,6 +53,13 @@ class InvoiceSubmission extends InvoiceRecord */ private $rectificationData = []; + /** + * Identifies if a submission is to subsanate a previous one accepted with errors + * + * @var bool + */ + public $isCorrection = false; + /** * Invoice type (TipoFactura). * @var InvoiceType @@ -557,7 +564,7 @@ public function rules(): array /** * Deprecated: Use InvoiceSerializer::toInvoiceXml() instead. - * + * * @deprecated This method has been replaced by InvoiceSerializer::toInvoiceXml() * @return \DOMDocument * @throws \Exception diff --git a/src/services/InvoiceSerializer.php b/src/services/InvoiceSerializer.php index ef3f8a8..cdbef2b 100644 --- a/src/services/InvoiceSerializer.php +++ b/src/services/InvoiceSerializer.php @@ -73,7 +73,10 @@ public static function toInvoiceXml(InvoiceSubmission $invoice, bool $validate = // NombreRazonEmisor (required) $root->appendChild($doc->createElementNS(self::SF_NAMESPACE, 'sf:NombreRazonEmisor', (string) $invoice->issuerName)); - + // Subsanacion (optional) + if($invoice->isCorrection) { + $root->appendChild($doc->createElementNS(self::SF_NAMESPACE, 'sf:Subsanacion', 'S')); + } // TipoFactura (required) if ($invoice->invoiceType) { diff --git a/tests/Unit/Services/InvoiceSerializerTest.php b/tests/Unit/Services/InvoiceSerializerTest.php index eeba837..781ac63 100644 --- a/tests/Unit/Services/InvoiceSerializerTest.php +++ b/tests/Unit/Services/InvoiceSerializerTest.php @@ -56,6 +56,10 @@ public function testToInvoiceXml(): void $this->assertEquals(1, $nombreRazon->length); $this->assertEquals('Test Company', $nombreRazon->item(0)->textContent); + // Verify subsanation is not present + $correction = $dom->getElementsByTagNameNS(InvoiceSerializer::SF_NAMESPACE, 'Subsanacion'); + $this->assertEquals(0, $correction->count()); + // Verify invoice type $tipoFactura = $dom->getElementsByTagNameNS(InvoiceSerializer::SF_NAMESPACE, 'TipoFactura'); $this->assertEquals(1, $tipoFactura->length); @@ -96,6 +100,23 @@ public function testToInvoiceXml(): void $this->assertEquals(str_repeat('a', 64), $huella->item(0)->textContent); } + /** + * Test that the InvoiceSerializer can generate a XML for InvoiceSubmission with the subsanation info + */ + public function testToInvoiceXmlWithIsSubsanation() + { + // Create a basic InvoiceSubmission object + $invoice = $this->createBasicInvoiceSubmission(); + $invoice->isCorrection = true; + + // Generate XML using the serializer + $dom = InvoiceSerializer::toInvoiceXml($invoice, false); // Skip validation + + // Verify subsanation is present + $subsanation = $dom->getElementsByTagNameNS(InvoiceSerializer::SF_NAMESPACE, 'Subsanacion'); + $this->assertEquals('S', $subsanation->item(0)->textContent); + } + /** * Test that the InvoiceSerializer can generate XML for an InvoiceCancellation. */ @@ -376,6 +397,9 @@ private function createBasicInvoiceQuery(): InvoiceQuery // Set counterparty $query->setCounterparty('87654321X', 'Test Counterparty'); + // Set issuerparty + $query->setIssuerparty('98765432M', 'Test Issuer'); + // Set system info $query->setSystemInfo('Test System', '1.0'); From d85ec3cd50011592c5742a4d7b83eab0fad466b1 Mon Sep 17 00:00:00 2001 From: Alberto Contreras Date: Wed, 26 Nov 2025 21:57:46 +0100 Subject: [PATCH 2/3] UPDATE Readme file to use the prover value for corrections --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a1a2bef..0de9371 100644 --- a/README.md +++ b/README.md @@ -148,7 +148,7 @@ $invoice->simplifiedInvoice = YesNoType::NO; $invoice->invoiceWithoutRecipient = YesNoType::NO; // If providing a subsanation after receiving an "Accepted with error" message -// $invoice->isSubsanation = true; +// $invoice->isCorrection = true; // Add tax breakdown (using object-oriented approach) $breakdown = new Breakdown(); From 3c7edd46a690209de84fe9dda85991ee8d5d9159 Mon Sep 17 00:00:00 2001 From: Alberto Contreras Date: Wed, 26 Nov 2025 22:34:40 +0100 Subject: [PATCH 3/3] UPDATE InvoiceSubmission to use YesNoType for the new isCorrection property --- src/models/InvoiceSubmission.php | 19 +++++++++++++------ src/services/InvoiceSerializer.php | 3 ++- tests/Unit/Models/InvoiceSubmissionTest.php | 1 + tests/Unit/Services/InvoiceSerializerTest.php | 7 ++++--- 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/models/InvoiceSubmission.php b/src/models/InvoiceSubmission.php index 29a2736..757d7b3 100644 --- a/src/models/InvoiceSubmission.php +++ b/src/models/InvoiceSubmission.php @@ -53,12 +53,12 @@ class InvoiceSubmission extends InvoiceRecord */ private $rectificationData = []; - /** - * Identifies if a submission is to subsanate a previous one accepted with errors - * - * @var bool - */ - public $isCorrection = false; + /** + * Identifies if a submission is to subsanate a previous one accepted with errors + * + * @var YesNoType|null + */ + public $isCorrection; /** * Invoice type (TipoFactura). @@ -449,6 +449,13 @@ public function rules(): array return ($value instanceof YesNoType) ? true : 'Must be an instance of YesNoType.'; }], + ['isCorrection', function ($value): bool|string { + if ($value === null) { + return true; + } + + return ($value instanceof YesNoType) ? true : 'Must be an instance of YesNoType.'; + }], ['invoiceWithoutRecipient', function ($value): bool|string { if ($value === null) { return true; diff --git a/src/services/InvoiceSerializer.php b/src/services/InvoiceSerializer.php index cdbef2b..b116a56 100644 --- a/src/services/InvoiceSerializer.php +++ b/src/services/InvoiceSerializer.php @@ -7,6 +7,7 @@ use eseperio\verifactu\models\Breakdown; use eseperio\verifactu\models\BreakdownDetail; use eseperio\verifactu\models\ComputerSystem; +use eseperio\verifactu\models\enums\YesNoType; use eseperio\verifactu\models\InvoiceCancellation; use eseperio\verifactu\models\InvoiceId; use eseperio\verifactu\models\InvoiceQuery; @@ -75,7 +76,7 @@ public static function toInvoiceXml(InvoiceSubmission $invoice, bool $validate = // Subsanacion (optional) if($invoice->isCorrection) { - $root->appendChild($doc->createElementNS(self::SF_NAMESPACE, 'sf:Subsanacion', 'S')); + $root->appendChild($doc->createElementNS(self::SF_NAMESPACE, 'sf:Subsanacion', (string) $invoice->isCorrection->value)); } // TipoFactura (required) diff --git a/tests/Unit/Models/InvoiceSubmissionTest.php b/tests/Unit/Models/InvoiceSubmissionTest.php index 63e98c7..260aeba 100644 --- a/tests/Unit/Models/InvoiceSubmissionTest.php +++ b/tests/Unit/Models/InvoiceSubmissionTest.php @@ -78,6 +78,7 @@ public function testToXmlMethodExists(): void $submission->invoiceType = 'F1'; $submission->taxAmount = 21.00; $submission->totalAmount = 121.00; + $submission->isCorrection = YesNoType::NO; // Set InvoiceId $invoiceId = new InvoiceId(); diff --git a/tests/Unit/Services/InvoiceSerializerTest.php b/tests/Unit/Services/InvoiceSerializerTest.php index 781ac63..fe5c286 100644 --- a/tests/Unit/Services/InvoiceSerializerTest.php +++ b/tests/Unit/Services/InvoiceSerializerTest.php @@ -58,7 +58,7 @@ public function testToInvoiceXml(): void // Verify subsanation is not present $correction = $dom->getElementsByTagNameNS(InvoiceSerializer::SF_NAMESPACE, 'Subsanacion'); - $this->assertEquals(0, $correction->count()); + $this->assertEquals(YesNoType::NO->value, $correction->item(0)->textContent); // Verify invoice type $tipoFactura = $dom->getElementsByTagNameNS(InvoiceSerializer::SF_NAMESPACE, 'TipoFactura'); @@ -107,14 +107,14 @@ public function testToInvoiceXmlWithIsSubsanation() { // Create a basic InvoiceSubmission object $invoice = $this->createBasicInvoiceSubmission(); - $invoice->isCorrection = true; + $invoice->isCorrection = YesNoType::YES; // Generate XML using the serializer $dom = InvoiceSerializer::toInvoiceXml($invoice, false); // Skip validation // Verify subsanation is present $subsanation = $dom->getElementsByTagNameNS(InvoiceSerializer::SF_NAMESPACE, 'Subsanacion'); - $this->assertEquals('S', $subsanation->item(0)->textContent); + $this->assertEquals(YesNoType::YES->value, $subsanation->item(0)->textContent); } /** @@ -330,6 +330,7 @@ private function createBasicInvoiceSubmission(): InvoiceSubmission $invoice->operationDescription = 'Test operation'; $invoice->taxAmount = 21.00; $invoice->totalAmount = 121.00; + $invoice->isCorrection = YesNoType::NO; // Add a recipient $recipient = new LegalPerson();