Skip to content
Draft
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
5 changes: 3 additions & 2 deletions src/models/InvoiceId.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class InvoiceId extends Model

/**
* Issue date (FechaExpedicionFactura), format YYYY-MM-DD.
* The serializer will automatically convert this to DD-MM-YYYY as required by the AEAT schema.
* @var string
*/
public $issueDate;
Expand All @@ -39,8 +40,8 @@ public function rules(): array
[['issuerNif', 'seriesNumber', 'issueDate'], 'required'],
[['issuerNif', 'seriesNumber', 'issueDate'], 'string'],
['issueDate', fn($value): bool|string =>
// Checks for format DD-MM-YYYY (simple regex)
(preg_match('/^\\d{2}-\\d{2}-\\d{4}$/', (string) $value)) ? true : 'Must be a valid date (YYYY-MM-DD).'],
// Checks for format YYYY-MM-DD (ISO 8601)
(preg_match('/^\d{4}-\d{2}-\d{2}$/', (string) $value)) ? true : 'Must be a valid date (YYYY-MM-DD).'],
];
}

Expand Down
10 changes: 5 additions & 5 deletions src/models/InvoiceSubmission.php
Original file line number Diff line number Diff line change
Expand Up @@ -534,8 +534,8 @@ public function rules(): array
return true;
}

// Checks for format DD-MM-YYYY (simple regex)
return (preg_match('/^\\d{2}-\\d{2}-\\d{4}$/', $value)) ? true : 'Must be a valid date (YYYY-MM-DD).';
// Checks for format YYYY-MM-DD (ISO 8601)
return (preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) ? true : 'Must be a valid date (YYYY-MM-DD).';
}],
// New rule to validate the issue date format
['invoiceId', function ($value): bool|string {
Expand All @@ -547,10 +547,10 @@ public function rules(): array
return true;
}

// Verifica el formato DD-MM-YYYY
return (preg_match('/^\\d{2,2}-\\d{2,2}-\\d{4,4}$/', $value->issueDate))
// Verifica el formato YYYY-MM-DD (ISO 8601)
return (preg_match('/^\d{4}-\d{2}-\d{2}$/', $value->issueDate))
? true
: 'La fecha de expedición debe tener el formato DD-MM-YYYY.';
: 'The issue date must be in YYYY-MM-DD format.';
}],
]);
}
Expand Down
35 changes: 27 additions & 8 deletions src/services/InvoiceSerializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public static function toInvoiceXml(InvoiceSubmission $invoice, bool $validate =
$idFactura = $doc->createElementNS(self::SF_NAMESPACE, 'sf:IDFactura');
$idFactura->appendChild($doc->createElementNS(self::SF_NAMESPACE, 'sf:IDEmisorFactura', (string) $invoiceId->issuerNif));
$idFactura->appendChild($doc->createElementNS(self::SF_NAMESPACE, 'sf:NumSerieFactura', (string) $invoiceId->seriesNumber));
$idFactura->appendChild($doc->createElementNS(self::SF_NAMESPACE, 'sf:FechaExpedicionFactura', (string) $invoiceId->issueDate));
$idFactura->appendChild($doc->createElementNS(self::SF_NAMESPACE, 'sf:FechaExpedicionFactura', self::formatDate((string) $invoiceId->issueDate)));
$root->appendChild($idFactura);
}

Expand Down Expand Up @@ -91,7 +91,7 @@ public static function toInvoiceXml(InvoiceSubmission $invoice, bool $validate =
$idFacturaRectificada = $doc->createElementNS(self::SF_NAMESPACE, 'sf:IDFacturaRectificada');
$idFacturaRectificada->appendChild($doc->createElementNS(self::SF_NAMESPACE, 'sf:IDEmisorFactura', (string) $rect['issuerNif']));
$idFacturaRectificada->appendChild($doc->createElementNS(self::SF_NAMESPACE, 'sf:NumSerieFactura', (string) $rect['seriesNumber']));
$idFacturaRectificada->appendChild($doc->createElementNS(self::SF_NAMESPACE, 'sf:FechaExpedicionFactura', (string) $rect['issueDate']));
$idFacturaRectificada->appendChild($doc->createElementNS(self::SF_NAMESPACE, 'sf:FechaExpedicionFactura', self::formatDate((string) $rect['issueDate'])));
$facturasRectificadas->appendChild($idFacturaRectificada);
}
$root->appendChild($facturasRectificadas);
Expand All @@ -104,7 +104,7 @@ public static function toInvoiceXml(InvoiceSubmission $invoice, bool $validate =
$idFacturaSustituida = $doc->createElementNS(self::SF_NAMESPACE, 'sf:IDFacturaSustituida');
$idFacturaSustituida->appendChild($doc->createElementNS(self::SF_NAMESPACE, 'sf:IDEmisorFactura', (string) $subst['issuerNif']));
$idFacturaSustituida->appendChild($doc->createElementNS(self::SF_NAMESPACE, 'sf:NumSerieFactura', (string) $subst['seriesNumber']));
$idFacturaSustituida->appendChild($doc->createElementNS(self::SF_NAMESPACE, 'sf:FechaExpedicionFactura', (string) $subst['issueDate']));
$idFacturaSustituida->appendChild($doc->createElementNS(self::SF_NAMESPACE, 'sf:FechaExpedicionFactura', self::formatDate((string) $subst['issueDate'])));
$facturasSustituidas->appendChild($idFacturaSustituida);
}
$root->appendChild($facturasSustituidas);
Expand All @@ -123,7 +123,7 @@ public static function toInvoiceXml(InvoiceSubmission $invoice, bool $validate =
}

if (!empty($invoice->operationDate)) {
$root->appendChild($doc->createElementNS(self::SF_NAMESPACE, 'sf:FechaOperacion', (string) $invoice->operationDate));
$root->appendChild($doc->createElementNS(self::SF_NAMESPACE, 'sf:FechaOperacion', self::formatDate((string) $invoice->operationDate)));
}

$root->appendChild($doc->createElementNS(self::SF_NAMESPACE, 'sf:DescripcionOperacion', (string) $invoice->operationDescription));
Expand Down Expand Up @@ -292,7 +292,7 @@ public static function toInvoiceXml(InvoiceSubmission $invoice, bool $validate =
$registroAnterior = $doc->createElementNS(self::SF_NAMESPACE, 'sf:RegistroAnterior');
$registroAnterior->appendChild($doc->createElementNS(self::SF_NAMESPACE, 'sf:IDEmisorFactura', (string) $previousInvoice->issuerNif));
$registroAnterior->appendChild($doc->createElementNS(self::SF_NAMESPACE, 'sf:NumSerieFactura', (string) $previousInvoice->seriesNumber));
$registroAnterior->appendChild($doc->createElementNS(self::SF_NAMESPACE, 'sf:FechaExpedicionFactura', (string) $previousInvoice->issueDate));
$registroAnterior->appendChild($doc->createElementNS(self::SF_NAMESPACE, 'sf:FechaExpedicionFactura', self::formatDate((string) $previousInvoice->issueDate)));
$registroAnterior->appendChild($doc->createElementNS(self::SF_NAMESPACE, 'sf:Huella', (string) $previousInvoice->hash));
$encadenamiento->appendChild($registroAnterior);
} else {
Expand Down Expand Up @@ -353,7 +353,7 @@ public static function toCancellationXml(InvoiceCancellation $cancellation, bool
$idFactura = $doc->createElementNS(self::SF_NAMESPACE, 'sf:IDFactura');
$idFactura->appendChild($doc->createElementNS(self::SF_NAMESPACE, 'sf:IDEmisorFacturaAnulada', (string) $invoiceId->issuerNif));
$idFactura->appendChild($doc->createElementNS(self::SF_NAMESPACE, 'sf:NumSerieFacturaAnulada', (string) $invoiceId->seriesNumber));
$idFactura->appendChild($doc->createElementNS(self::SF_NAMESPACE, 'sf:FechaExpedicionFacturaAnulada', (string) $invoiceId->issueDate));
$idFactura->appendChild($doc->createElementNS(self::SF_NAMESPACE, 'sf:FechaExpedicionFacturaAnulada', self::formatDate((string) $invoiceId->issueDate)));
$root->appendChild($idFactura);
}

Expand Down Expand Up @@ -403,7 +403,7 @@ public static function toCancellationXml(InvoiceCancellation $cancellation, bool
$registroAnterior = $doc->createElementNS(self::SF_NAMESPACE, 'sf:RegistroAnterior');
$registroAnterior->appendChild($doc->createElementNS(self::SF_NAMESPACE, 'sf:IDEmisorFactura', (string) $previousInvoice->issuerNif));
$registroAnterior->appendChild($doc->createElementNS(self::SF_NAMESPACE, 'sf:NumSerieFactura', (string) $previousInvoice->seriesNumber));
$registroAnterior->appendChild($doc->createElementNS(self::SF_NAMESPACE, 'sf:FechaExpedicionFactura', (string) $previousInvoice->issueDate));
$registroAnterior->appendChild($doc->createElementNS(self::SF_NAMESPACE, 'sf:FechaExpedicionFactura', self::formatDate((string) $previousInvoice->issueDate)));
$registroAnterior->appendChild($doc->createElementNS(self::SF_NAMESPACE, 'sf:Huella', (string) $previousInvoice->hash));
$encadenamiento->appendChild($registroAnterior);
} else {
Expand Down Expand Up @@ -508,7 +508,7 @@ public static function toQueryXml(InvoiceQuery $query, bool $validate = true): \
// FechaExpedicionFactura (Consulta ns) wrapping SF choice type
if (!empty($query->issueDate)) {
$fechaWrapper = $doc->createElementNS(self::QUERY_NAMESPACE, 'sf:FechaExpedicionFactura');
$fechaWrapper->appendChild($doc->createElementNS(self::SF_NAMESPACE, 'sf:FechaExpedicionFactura', (string) $query->issueDate));
$fechaWrapper->appendChild($doc->createElementNS(self::SF_NAMESPACE, 'sf:FechaExpedicionFactura', self::formatDate((string) $query->issueDate)));
$filtro->appendChild($fechaWrapper);
}

Expand All @@ -525,6 +525,25 @@ public static function toQueryXml(InvoiceQuery $query, bool $validate = true): \
return $doc;
}

/**
* Converts a date string from YYYY-MM-DD to DD-MM-YYYY format as required by the AEAT schema.
* If the value is already in DD-MM-YYYY format, it is returned unchanged.
* Any other format is returned as-is; validation of the format should be done prior to calling this method.
*
* @param string $date Date string in YYYY-MM-DD or DD-MM-YYYY format
* @return string Date string in DD-MM-YYYY format
*/
public static function formatDate(string $date): string
{
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
[$year, $month, $day] = explode('-', $date);
return "{$day}-{$month}-{$year}";
}

// Already in DD-MM-YYYY format or unrecognized — return unchanged
return $date;
}

public static function validateXml(\DOMDocument $doc, string $schemaPath): void
{
libxml_use_internal_errors(true);
Expand Down
8 changes: 4 additions & 4 deletions tests/Unit/Services/InvoiceSerializerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,7 @@ public function testOtherIdNetherlandsCountryCode(): void
$invoiceId = new InvoiceId();
$invoiceId->issuerNif = '12345678Z';
$invoiceId->seriesNumber = 'TEST-NL-001';
$invoiceId->issueDate = '01-01-2023';
$invoiceId->issueDate = '2023-01-01';
$invoiceNL->setInvoiceId($invoiceId);
$invoiceNL->issuerName = 'Test Company';
$invoiceNL->invoiceType = InvoiceType::STANDARD;
Expand Down Expand Up @@ -413,7 +413,7 @@ private function createBasicInvoiceSubmission(): InvoiceSubmission
$invoiceId = new InvoiceId();
$invoiceId->issuerNif = '12345678Z';
$invoiceId->seriesNumber = 'TEST001';
$invoiceId->issueDate = '01-01-2023';
$invoiceId->issueDate = '2023-01-01';
$invoice->setInvoiceId($invoiceId);

$invoice->issuerName = 'Test Company';
Expand Down Expand Up @@ -452,7 +452,7 @@ private function createBasicInvoiceCancellation(): InvoiceCancellation
$invoiceId = new InvoiceId();
$invoiceId->issuerNif = '12345678Z';
$invoiceId->seriesNumber = 'TEST001';
$invoiceId->issueDate = '01-01-2023';
$invoiceId->issueDate = '2023-01-01';
$cancellation->setInvoiceId($invoiceId);

// Set hash-related properties
Expand Down Expand Up @@ -482,7 +482,7 @@ private function createBasicInvoiceQuery(): InvoiceQuery

// Set optional properties
$query->seriesNumber = 'TEST001';
$query->issueDate = '01-01-2023';
$query->issueDate = '2023-01-01';
$query->externalRef = 'TEST-REF-001';

// Set counterparty
Expand Down
12 changes: 6 additions & 6 deletions tests/Verifactu/ReadmeExamplesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public function testInvoiceRegistrationExample(): void
$invoiceId = new InvoiceId();
$invoiceId->issuerNif = 'B12345678';
$invoiceId->seriesNumber = 'FA2024/001';
$invoiceId->issueDate = '01-07-2024';
$invoiceId->issueDate = '2024-07-01';
$invoice->setInvoiceId($invoiceId);

// Set basic invoice data
Expand Down Expand Up @@ -83,7 +83,7 @@ public function testInvoiceRegistrationExample(): void
$invoice->hash = '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'; // Calculated hash

// Optional fields
$invoice->operationDate = '01-07-2024'; // Operation date
$invoice->operationDate = '2024-07-01'; // Operation date
$invoice->externalRef = 'REF123'; // External reference
$invoice->simplifiedInvoice = YesNoType::NO; // Is not a simplified invoice
$invoice->invoiceWithoutRecipient = YesNoType::NO; // Has identified recipient
Expand Down Expand Up @@ -113,7 +113,7 @@ public function testInvoiceRegistrationExample(): void
$this->assertInstanceOf(InvoiceId::class, $invoice->getInvoiceId());
$this->assertEquals('B12345678', $invoice->getInvoiceId()->issuerNif);
$this->assertEquals('FA2024/001', $invoice->getInvoiceId()->seriesNumber);
$this->assertEquals('01-07-2024', $invoice->getInvoiceId()->issueDate);
$this->assertEquals('2024-07-01', $invoice->getInvoiceId()->issueDate);
$this->assertEquals('Empresa Ejemplo SL', $invoice->issuerName);
$this->assertEquals(InvoiceType::STANDARD, $invoice->invoiceType);
$this->assertEquals('Venta de productos', $invoice->operationDescription);
Expand All @@ -133,7 +133,7 @@ public function testInvoiceCancellationExample(): void
$invoiceId = new InvoiceId();
$invoiceId->issuerNif = 'B12345678';
$invoiceId->seriesNumber = 'FA2024/001';
$invoiceId->issueDate = '01-07-2024';
$invoiceId->issueDate = '2024-07-01';
$cancellation->setInvoiceId($invoiceId);

// Set chaining data (using object-oriented approach)
Expand All @@ -142,7 +142,7 @@ public function testInvoiceCancellationExample(): void
$chaining->setPreviousInvoice([
'seriesNumber' => 'FA2024/000',
'issuerNif' => 'B12345678',
'issueDate' => '30-06-2024',
'issueDate' => '2024-06-30',
'hash' => '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
]);
$cancellation->setChaining($chaining);
Expand Down Expand Up @@ -193,7 +193,7 @@ public function testInvoiceCancellationExample(): void
$this->assertInstanceOf(InvoiceId::class, $cancellation->getInvoiceId());
$this->assertEquals('B12345678', $cancellation->getInvoiceId()->issuerNif);
$this->assertEquals('FA2024/001', $cancellation->getInvoiceId()->seriesNumber);
$this->assertEquals('01-07-2024', $cancellation->getInvoiceId()->issueDate);
$this->assertEquals('2024-07-01', $cancellation->getInvoiceId()->issueDate);
$this->assertEquals(YesNoType::NO, $cancellation->noPreviousRecord);
$this->assertEquals(YesNoType::NO, $cancellation->previousRejection);
$this->assertEquals(GeneratorType::ISSUER, $cancellation->generator);
Expand Down
4 changes: 2 additions & 2 deletions tests/Verifactu/VerifactuSandboxTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ private function createTestInvoice(): InvoiceSubmission
$invoiceId = new InvoiceId();
$invoiceId->issuerNif = $this->issuerNif; // was hardcoded before
$invoiceId->seriesNumber = 'TEST' . date('YmdHis');
$invoiceId->issueDate = date('d-m-Y');
$invoiceId->issueDate = date('Y-m-d');
$invoice->setInvoiceId($invoiceId);

// Set basic invoice data
Expand Down Expand Up @@ -168,7 +168,7 @@ private function createTestInvoice(): InvoiceSubmission
$invoice->recordTimestamp = date('Y-m-d\TH:i:sP');

// Optional fields
$invoice->operationDate = date('d-m-Y');
$invoice->operationDate = date('Y-m-d');
// Change to externalReference if you prefer not to depend on the alias:
// $invoice->externalReference = 'TEST-' . date('YmdHis');
$invoice->externalRef = 'TEST-' . date('YmdHis');
Expand Down