diff --git a/README.md b/README.md index 312d521..0de9371 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->isCorrection = 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..757d7b3 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 YesNoType|null + */ + public $isCorrection; + /** * Invoice type (TipoFactura). * @var InvoiceType @@ -442,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; @@ -557,7 +571,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..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; @@ -73,7 +74,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', (string) $invoice->isCorrection->value)); + } // TipoFactura (required) if ($invoice->invoiceType) { 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 eeba837..fe5c286 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(YesNoType::NO->value, $correction->item(0)->textContent); + // 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 = 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(YesNoType::YES->value, $subsanation->item(0)->textContent); + } + /** * Test that the InvoiceSerializer can generate XML for an InvoiceCancellation. */ @@ -309,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(); @@ -376,6 +398,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');