Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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;

Copilot AI Mar 30, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The README example sets isCorrection to a boolean (true), but the model expects a YesNoType (and validation enforces that). Update the example to use YesNoType::YES (and optionally show null/unset for the default).

Suggested change
// $invoice->isCorrection = true;
// Default is null (no correction). To mark this invoice as a correction use:
// $invoice->isCorrection = YesNoType::YES;

Copilot uses AI. Check for mistakes.

// 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);
Expand Down
16 changes: 15 additions & 1 deletion src/models/InvoiceSubmission.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment on lines +56 to +61

Copilot AI Mar 30, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docblock wording “subsanate” is incorrect/unclear English. Consider rephrasing to something like “Indicates whether this submission corrects a previous one accepted with errors” (and keep terminology consistent with the XML element name Subsanacion).

Copilot uses AI. Check for mistakes.

/**
* Invoice type (TipoFactura).
* @var InvoiceType
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion src/services/InvoiceSerializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Copilot AI Mar 30, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The YesNoType import is unused in this file, which will be flagged by static analysis/linters. It can be removed unless you plan to reference the enum directly (e.g., for YesNoType::YES comparisons).

Suggested change
use eseperio\verifactu\models\enums\YesNoType;

Copilot uses AI. Check for mistakes.
use eseperio\verifactu\models\InvoiceCancellation;
use eseperio\verifactu\models\InvoiceId;
use eseperio\verifactu\models\InvoiceQuery;
Expand Down Expand Up @@ -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));
}
Comment on lines +77 to +80

Copilot AI Mar 30, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This block uses tabs and is missing spaces after if/before {, which is inconsistent with the surrounding style in this file (spaces indentation + if (...) {). Reformat to match the existing code style to keep diffs consistent and avoid formatter churn.

Suggested change
// Subsanacion (optional)
if($invoice->isCorrection) {
$root->appendChild($doc->createElementNS(self::SF_NAMESPACE, 'sf:Subsanacion', (string) $invoice->isCorrection->value));
}
// Subsanacion (optional)
if ($invoice->isCorrection) {
$root->appendChild($doc->createElementNS(self::SF_NAMESPACE, 'sf:Subsanacion', (string) $invoice->isCorrection->value));
}

Copilot uses AI. Check for mistakes.
Comment on lines +77 to +80

Copilot AI Mar 30, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if ($invoice->isCorrection) is effectively a null-check because YesNoType::NO is still truthy (it’s an enum object). That means Subsanacion will be emitted for both YES and NO whenever the property is set, which may not match the intended “only send for corrections” behavior. Make the intent explicit by checking !== null (emit both YES/NO) or comparing to YesNoType::YES (emit only for corrections).

Suggested change
// Subsanacion (optional)
if($invoice->isCorrection) {
$root->appendChild($doc->createElementNS(self::SF_NAMESPACE, 'sf:Subsanacion', (string) $invoice->isCorrection->value));
}
// Subsanacion (optional)
if ($invoice->isCorrection === YesNoType::YES) {
$root->appendChild($doc->createElementNS(self::SF_NAMESPACE, 'sf:Subsanacion', (string) $invoice->isCorrection->value));
}

Copilot uses AI. Check for mistakes.

// TipoFactura (required)
if ($invoice->invoiceType) {
Expand Down
1 change: 1 addition & 0 deletions tests/Unit/Models/InvoiceSubmissionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
25 changes: 25 additions & 0 deletions tests/Unit/Services/InvoiceSerializerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Comment on lines +59 to +60

Copilot AI Mar 30, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says “Subsanacion is not present”, but the assertion dereferences item(0) and checks for NO, which implies the node is present. Either assert $correction->length === 0 (if it should be omitted) or assert length === 1 and update the comment to reflect that it defaults to NO.

Suggested change
// Verify subsanation is not present
$correction = $dom->getElementsByTagNameNS(InvoiceSerializer::SF_NAMESPACE, 'Subsanacion');
// Verify subsanation element is present and defaults to NO
$correction = $dom->getElementsByTagNameNS(InvoiceSerializer::SF_NAMESPACE, 'Subsanacion');
$this->assertEquals(1, $correction->length);

Copilot uses AI. Check for mistakes.
$this->assertEquals(YesNoType::NO->value, $correction->item(0)->textContent);

// Verify invoice type
$tipoFactura = $dom->getElementsByTagNameNS(InvoiceSerializer::SF_NAMESPACE, 'TipoFactura');
$this->assertEquals(1, $tipoFactura->length);
Expand Down Expand Up @@ -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);
}
Comment on lines +103 to +118

Copilot AI Mar 30, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test method is inconsistent with the rest of the file: it lacks a : void return type, doesn’t assert the NodeList length before reading item(0), and the name/comment use “Subsanation” while the XML element is Subsanacion. Align the naming, add : void, and assert $subsanation->length === 1 before reading item(0) to avoid null dereferences.

Copilot uses AI. Check for mistakes.

/**
* Test that the InvoiceSerializer can generate XML for an InvoiceCancellation.
*/
Expand Down Expand Up @@ -309,6 +330,7 @@ private function createBasicInvoiceSubmission(): InvoiceSubmission
$invoice->operationDescription = 'Test operation';
$invoice->taxAmount = 21.00;
$invoice->totalAmount = 121.00;
$invoice->isCorrection = YesNoType::NO;

Copilot AI Mar 30, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createBasicInvoiceSubmission() now always sets $invoice->isCorrection = YesNoType::NO;. If the intention is that Subsanacion is optional and omitted unless explicitly requested, this should be left as null here so the “default invoice” path doesn’t force emission of the node.

Suggested change
$invoice->isCorrection = YesNoType::NO;

Copilot uses AI. Check for mistakes.

// Add a recipient
$recipient = new LegalPerson();
Expand Down Expand Up @@ -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');

Expand Down
Loading