diff --git a/src/models/InvoiceId.php b/src/models/InvoiceId.php index 1bf609b..238022b 100644 --- a/src/models/InvoiceId.php +++ b/src/models/InvoiceId.php @@ -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; @@ -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).'], ]; } diff --git a/src/models/InvoiceSubmission.php b/src/models/InvoiceSubmission.php index e5cf23f..72a5cdf 100644 --- a/src/models/InvoiceSubmission.php +++ b/src/models/InvoiceSubmission.php @@ -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 { @@ -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.'; }], ]); } diff --git a/src/services/InvoiceSerializer.php b/src/services/InvoiceSerializer.php index e8615f7..01bbbcd 100644 --- a/src/services/InvoiceSerializer.php +++ b/src/services/InvoiceSerializer.php @@ -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); } @@ -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); @@ -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); @@ -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)); @@ -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 { @@ -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); } @@ -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 { @@ -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); } @@ -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); diff --git a/tests/Unit/Services/InvoiceSerializerTest.php b/tests/Unit/Services/InvoiceSerializerTest.php index 99dba7e..2a3621c 100644 --- a/tests/Unit/Services/InvoiceSerializerTest.php +++ b/tests/Unit/Services/InvoiceSerializerTest.php @@ -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; @@ -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'; @@ -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 @@ -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 diff --git a/tests/Verifactu/ReadmeExamplesTest.php b/tests/Verifactu/ReadmeExamplesTest.php index 3290861..4282262 100644 --- a/tests/Verifactu/ReadmeExamplesTest.php +++ b/tests/Verifactu/ReadmeExamplesTest.php @@ -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 @@ -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 @@ -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); @@ -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) @@ -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); @@ -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); diff --git a/tests/Verifactu/VerifactuSandboxTest.php b/tests/Verifactu/VerifactuSandboxTest.php index 7ec7e3b..e72ead1 100644 --- a/tests/Verifactu/VerifactuSandboxTest.php +++ b/tests/Verifactu/VerifactuSandboxTest.php @@ -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 @@ -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');