From 442ff42cebc77eaf059dd4d3a31939ee9618c149 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 8 Aug 2025 14:04:13 +1000 Subject: [PATCH] additional tests --- .../EDocument/Standards/Verifactu.php | 48 +- .../Verifactu/Models/InvoiceCancellation.php | 332 +++++++++++++ .../Verifactu/Models/InvoiceModification.php | 12 +- .../Verifactu/InvoiceCancellationTest.php | 442 ++++++++++++++++++ .../Verifactu/VerifactuFeatureTest.php | 50 ++ 5 files changed, 880 insertions(+), 4 deletions(-) create mode 100644 app/Services/EDocument/Standards/Verifactu/Models/InvoiceCancellation.php create mode 100644 tests/Feature/EInvoice/Verifactu/InvoiceCancellationTest.php diff --git a/app/Services/EDocument/Standards/Verifactu.php b/app/Services/EDocument/Standards/Verifactu.php index 6527205da9..d325ce9390 100644 --- a/app/Services/EDocument/Standards/Verifactu.php +++ b/app/Services/EDocument/Standards/Verifactu.php @@ -29,12 +29,16 @@ use App\Services\EDocument\Standards\Verifactu\RegistroAlta; use App\Services\EDocument\Standards\Verifactu\RegistroModificacion; use App\Services\EDocument\Standards\Verifactu\Models\Invoice as VerifactuInvoice; use App\Services\EDocument\Standards\Verifactu\Models\InvoiceModification; +use App\Services\EDocument\Standards\Verifactu\AeatClient; class Verifactu extends AbstractService { + private AeatClient $aeat_client; + public function __construct(public Invoice $invoice) { + $this->aeat_client = new AeatClient(); } /** @@ -50,15 +54,55 @@ class Verifactu extends AbstractService //determine the current status of the invoice. $document = new RegistroAlta($this->invoice); + $huella = ''; + //1. new => RegistraAlta if($v_logs->count() >= 1){ $v_log = $v_logs->first(); - + $huella = $v_log->hash; $document = InvoiceModification::fromInvoice($this->invoice, $v_log->deserialize()); } - //3. cancelled => RegistroAnulacion + + $new_huella = $this->calculateHash($document, $huella); // careful with this! we'll need to reference this later + $document->setHuella($new_huella); + + $soapXml = $document->toSoapEnvelope(); + + } + + /** + * calculateHash + * + * @param mixed $document + * @param string $huella + * @return string + */ + public function calculateHash($document, string $huella): string + { + $idEmisorFactura = $document->getIdEmisorFactura(); + $numSerieFactura = $document->getNumSerieFactura(); + $fechaExpedicionFactura = $document->getFechaExpedicionFactura(); + $tipoFactura = $document->getTipoFactura(); + $cuotaTotal = $document->getCuotaTotal(); + $importeTotal = $document->getImporteTotal(); + $fechaHoraHusoGenRegistro = $document->getFechaHoraHusoGenRegistro(); + + $hashInput = "IDEmisorFactura={$idEmisorFactura}&" . + "NumSerieFactura={$numSerieFactura}&" . + "FechaExpedicionFactura={$fechaExpedicionFactura}&" . + "TipoFactura={$tipoFactura}&" . + "CuotaTotal={$cuotaTotal}&" . + "ImporteTotal={$importeTotal}&" . + "Huella={$huella}&" . + "FechaHoraHusoGenRegistro={$fechaHoraHusoGenRegistro}"; + + return strtoupper(hash('sha256', $hashInput)); } + public function send(string $soapXml): array + { + return $this->aeat_client->send($soapXml); + } } \ No newline at end of file diff --git a/app/Services/EDocument/Standards/Verifactu/Models/InvoiceCancellation.php b/app/Services/EDocument/Standards/Verifactu/Models/InvoiceCancellation.php new file mode 100644 index 0000000000..0821fad575 --- /dev/null +++ b/app/Services/EDocument/Standards/Verifactu/Models/InvoiceCancellation.php @@ -0,0 +1,332 @@ +setNumSerieFacturaEmisor($invoice->number); + $cancellation->setFechaExpedicionFacturaEmisor($invoice->date); + $cancellation->setNifEmisor($invoice->company->settings->vat_number ?? 'B12345678'); + $cancellation->setHuellaFactura($huella); + + return $cancellation; + } + + // Getters and Setters + public function getIdVersion(): string + { + return $this->idVersion; + } + + public function setIdVersion(string $idVersion): self + { + $this->idVersion = $idVersion; + return $this; + } + + public function getNumSerieFacturaEmisor(): string + { + return $this->numSerieFacturaEmisor; + } + + public function setNumSerieFacturaEmisor(string $numSerieFacturaEmisor): self + { + $this->numSerieFacturaEmisor = $numSerieFacturaEmisor; + return $this; + } + + public function getFechaExpedicionFacturaEmisor(): string + { + return $this->fechaExpedicionFacturaEmisor; + } + + public function setFechaExpedicionFacturaEmisor(string $fechaExpedicionFacturaEmisor): self + { + $this->fechaExpedicionFacturaEmisor = $fechaExpedicionFacturaEmisor; + return $this; + } + + public function getNifEmisor(): string + { + return $this->nifEmisor; + } + + public function setNifEmisor(string $nifEmisor): self + { + $this->nifEmisor = $nifEmisor; + return $this; + } + + public function getHuellaFactura(): string + { + return $this->huellaFactura; + } + + public function setHuellaFactura(string $huellaFactura): self + { + $this->huellaFactura = $huellaFactura; + return $this; + } + + public function getEstado(): string + { + return $this->estado; + } + + public function setEstado(string $estado): self + { + $this->estado = $estado; + return $this; + } + + public function getDescripcionEstado(): string + { + return $this->descripcionEstado; + } + + public function setDescripcionEstado(string $descripcionEstado): self + { + $this->descripcionEstado = $descripcionEstado; + return $this; + } + + /** + * Generate the XML structure for the cancellation + */ + public function toXml(\DOMDocument $doc): \DOMElement + { + // Create root element with proper namespaces + $root = $doc->createElementNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/suministro/FacturaLR.xsd', 'SuministroLRFacturas'); + + // Add namespaces + $root->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:ds', 'http://www.w3.org/2000/09/xmldsig#'); + $root->setAttribute('Version', $this->idVersion); + + // Create LRFacturaEntrada + $lrFacturaEntrada = $doc->createElementNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/suministro/FacturaLR.xsd', 'LRFacturaEntrada'); + $root->appendChild($lrFacturaEntrada); + + // Create IDFactura + $idFactura = $doc->createElementNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/suministro/FacturaLR.xsd', 'IDFactura'); + $lrFacturaEntrada->appendChild($idFactura); + + // Create IDEmisorFactura + $idEmisorFactura = $doc->createElementNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/suministro/FacturaLR.xsd', 'IDEmisorFactura'); + $idFactura->appendChild($idEmisorFactura); + + // Add NumSerieFacturaEmisor + $idEmisorFactura->appendChild($doc->createElementNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/suministro/FacturaLR.xsd', 'NumSerieFacturaEmisor', $this->numSerieFacturaEmisor)); + + // Add FechaExpedicionFacturaEmisor + $idEmisorFactura->appendChild($doc->createElementNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/suministro/FacturaLR.xsd', 'FechaExpedicionFacturaEmisor', $this->fechaExpedicionFacturaEmisor)); + + // Add NIFEmisor + $idEmisorFactura->appendChild($doc->createElementNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/suministro/FacturaLR.xsd', 'NIFEmisor', $this->nifEmisor)); + + // Add HuellaFactura + $idEmisorFactura->appendChild($doc->createElementNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/suministro/FacturaLR.xsd', 'HuellaFactura', $this->huellaFactura)); + + // Create EstadoFactura + $estadoFactura = $doc->createElementNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/suministro/FacturaLR.xsd', 'EstadoFactura'); + $lrFacturaEntrada->appendChild($estadoFactura); + + // Add Estado + $estadoFactura->appendChild($doc->createElementNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/suministro/FacturaLR.xsd', 'Estado', $this->estado)); + + // Add DescripcionEstado + $estadoFactura->appendChild($doc->createElementNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/suministro/FacturaLR.xsd', 'DescripcionEstado', $this->descripcionEstado)); + + return $root; + } + + /** + * Generate XML string + */ + public function toXmlString(): string + { + $doc = new \DOMDocument('1.0', 'UTF-8'); + $doc->preserveWhiteSpace = false; + $doc->formatOutput = true; + + $root = $this->toXml($doc); + $doc->appendChild($root); + + return $doc->saveXML(); + } + + /** + * Generate SOAP envelope for web service communication + */ + public function toSoapEnvelope(): string + { + // Create the SOAP document + $soapDoc = new \DOMDocument('1.0', 'UTF-8'); + $soapDoc->preserveWhiteSpace = false; + $soapDoc->formatOutput = true; + + // Create SOAP envelope with namespaces + $envelope = $soapDoc->createElementNS('http://schemas.xmlsoap.org/soap/envelope/', 'soapenv:Envelope'); + $envelope->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:soapenv', 'http://schemas.xmlsoap.org/soap/envelope/'); + $envelope->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:sum', 'https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroLR.xsd'); + $envelope->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:sum1', 'https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd'); + + $soapDoc->appendChild($envelope); + + // Create Header + $header = $soapDoc->createElementNS('http://schemas.xmlsoap.org/soap/envelope/', 'soapenv:Header'); + $envelope->appendChild($header); + + // Create Body + $body = $soapDoc->createElementNS('http://schemas.xmlsoap.org/soap/envelope/', 'soapenv:Body'); + $envelope->appendChild($body); + + // Create RegFactuSistemaFacturacion + $regFactu = $soapDoc->createElementNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroLR.xsd', 'sum:RegFactuSistemaFacturacion'); + $body->appendChild($regFactu); + + // Create Cabecera + $cabecera = $soapDoc->createElementNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroLR.xsd', 'sum:Cabecera'); + $regFactu->appendChild($cabecera); + + // Create ObligadoEmision + $obligadoEmision = $soapDoc->createElementNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd', 'sum1:ObligadoEmision'); + $cabecera->appendChild($obligadoEmision); + + // Add ObligadoEmision content (using default values for now) + $obligadoEmision->appendChild($soapDoc->createElementNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd', 'sum1:NombreRazon', 'Test Company')); + $obligadoEmision->appendChild($soapDoc->createElementNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd', 'sum1:NIF', $this->nifEmisor)); + + // Create RegistroFactura + $registroFactura = $soapDoc->createElementNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroLR.xsd', 'sum:RegistroFactura'); + $regFactu->appendChild($registroFactura); + + // Import your existing XML into the RegistroFactura + $yourXmlDoc = new \DOMDocument(); + $yourXmlDoc->loadXML($this->toXmlString()); + + // Import the root element from your XML + $importedNode = $soapDoc->importNode($yourXmlDoc->documentElement, true); + $registroFactura->appendChild($importedNode); + + return $soapDoc->saveXML(); + } + + /** + * Parse from DOM element + */ + public static function fromDOMElement(\DOMElement $element): self + { + $cancellation = new self(); + + // Parse IDVersion + $idVersion = $element->getAttribute('Version'); + if ($idVersion) { + $cancellation->setIdVersion($idVersion); + } + + // Parse LRFacturaEntrada + $lrFacturaEntrada = $element->getElementsByTagNameNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/suministro/FacturaLR.xsd', 'LRFacturaEntrada')->item(0); + if ($lrFacturaEntrada) { + // Parse IDFactura + $idFactura = $lrFacturaEntrada->getElementsByTagNameNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/suministro/FacturaLR.xsd', 'IDFactura')->item(0); + if ($idFactura) { + $idEmisorFactura = $idFactura->getElementsByTagNameNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/suministro/FacturaLR.xsd', 'IDEmisorFactura')->item(0); + if ($idEmisorFactura) { + // Parse NumSerieFacturaEmisor + $numSerie = $cancellation->getElementValue($idEmisorFactura, 'NumSerieFacturaEmisor', 'https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/suministro/FacturaLR.xsd'); + if ($numSerie) { + $cancellation->setNumSerieFacturaEmisor($numSerie); + } + + // Parse FechaExpedicionFacturaEmisor + $fecha = $cancellation->getElementValue($idEmisorFactura, 'FechaExpedicionFacturaEmisor', 'https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/suministro/FacturaLR.xsd'); + if ($fecha) { + $cancellation->setFechaExpedicionFacturaEmisor($fecha); + } + + // Parse NIFEmisor + $nif = $cancellation->getElementValue($idEmisorFactura, 'NIFEmisor', 'https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/suministro/FacturaLR.xsd'); + if ($nif) { + $cancellation->setNifEmisor($nif); + } + + // Parse HuellaFactura + $huella = $cancellation->getElementValue($idEmisorFactura, 'HuellaFactura', 'https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/suministro/FacturaLR.xsd'); + if ($huella) { + $cancellation->setHuellaFactura($huella); + } + } + } + + // Parse EstadoFactura + $estadoFactura = $lrFacturaEntrada->getElementsByTagNameNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/suministro/FacturaLR.xsd', 'EstadoFactura')->item(0); + if ($estadoFactura) { + // Parse Estado + $estado = $cancellation->getElementValue($estadoFactura, 'Estado', 'https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/suministro/FacturaLR.xsd'); + if ($estado) { + $cancellation->setEstado($estado); + } + + // Parse DescripcionEstado + $descripcion = $cancellation->getElementValue($estadoFactura, 'DescripcionEstado', 'https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/suministro/FacturaLR.xsd'); + if ($descripcion) { + $cancellation->setDescripcionEstado($descripcion); + } + } + } + + return $cancellation; + } + + + + /** + * Serialize for storage + */ + public function serialize(): string + { + return serialize($this); + } + + /** + * Unserialize from storage + */ + public static function unserialize(string $data): self + { + $object = unserialize($data); + + if (!$object instanceof self) { + throw new \InvalidArgumentException('Invalid serialized data - not an InvoiceCancellation object'); + } + + return $object; + } +} \ No newline at end of file diff --git a/app/Services/EDocument/Standards/Verifactu/Models/InvoiceModification.php b/app/Services/EDocument/Standards/Verifactu/Models/InvoiceModification.php index d4905f2949..5b7d9c57bc 100644 --- a/app/Services/EDocument/Standards/Verifactu/Models/InvoiceModification.php +++ b/app/Services/EDocument/Standards/Verifactu/Models/InvoiceModification.php @@ -62,11 +62,19 @@ class InvoiceModification extends BaseXmlModel implements XmlModelInterface return $this; } + public function setHuella(string $huella): self + { + $this->getRegistroModificacion()->setHuella($huella); + return $this; + } + /** * Create a modification from an existing invoice */ public static function createFromInvoice(Invoice $originalInvoice, Invoice $modifiedInvoice): self { + $currentTimestamp = now()->format('Y-m-d\TH:i:s'); + $modification = new self(); // Set up cancellation record @@ -107,11 +115,11 @@ class InvoiceModification extends BaseXmlModel implements XmlModelInterface ->setImporteTotal($modifiedInvoice->getImporteTotal()) ->setEncadenamiento($modifiedInvoice->getEncadenamiento()) ->setSistemaInformatico($modifiedInvoice->getSistemaInformatico()) - ->setFechaHoraHusoGenRegistro($modifiedInvoice->getFechaHoraHusoGenRegistro()) + ->setFechaHoraHusoGenRegistro($currentTimestamp) ->setNumRegistroAcuerdoFacturacion($modifiedInvoice->getNumRegistroAcuerdoFacturacion()) ->setIdAcuerdoSistemaInformatico($modifiedInvoice->getIdAcuerdoSistemaInformatico()) ->setTipoHuella($modifiedInvoice->getTipoHuella()) - ->setHuella($modifiedInvoice->getHuella()); + ->setHuella('PLACEHOLDER_HUELLA'); $modification->setRegistroModificacion($modificationRecord); diff --git a/tests/Feature/EInvoice/Verifactu/InvoiceCancellationTest.php b/tests/Feature/EInvoice/Verifactu/InvoiceCancellationTest.php new file mode 100644 index 0000000000..8798eb2a9c --- /dev/null +++ b/tests/Feature/EInvoice/Verifactu/InvoiceCancellationTest.php @@ -0,0 +1,442 @@ +faker = Faker::create(); + } + + private function buildTestInvoice(): Invoice + { + $account = Account::factory()->create([ + 'hosted_client_count' => 1000, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $company = Company::factory()->create([ + 'account_id' => $account->id, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $company_settings = CompanySettings::defaults(); + $company_settings->currency_id = '3'; + $company_settings->country_id = '724'; + $company_settings->vat_number = $this->test_company_nif; + + $company->settings = $company_settings; + $company->save(); + + $this->company = $company; + + $user = User::factory()->create([ + 'account_id' => $account->id, + 'email' => $this->faker->unique()->safeEmail(), + 'confirmation_code' => $this->faker->unique()->uuid(), + ]); + + $this->user = $user; + + $user->companies()->attach($company->id, [ + 'account_id' => $account->id, + 'is_owner' => 1, + 'is_admin' => 1, + 'is_locked' => 0, + 'notifications' => CompanySettings::notificationDefaults(), + 'settings' => null, + ]); + + + $company_token = new CompanyToken(); + $company_token->user_id = $user->id; + $company_token->company_id = $company->id; + $company_token->account_id = $account->id; + $company_token->token = $this->faker->unique()->sha1(); + $company_token->name = $this->faker->word(); + $company_token->is_system = 0; + + $company_token->save(); + + $client_settings = ClientSettings::defaults(); + $client_settings->currency_id = '3'; + + $this->client = Client::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'name' => 'Test Client', + 'address1' => 'Calle Mayor 123', + 'city' => 'Madrid', + 'state' => 'Madrid', + 'postal_code' => '28001', + 'country_id' => 724, + 'vat_number' => $this->test_client_nif, + 'balance' => 0, + 'paid_to_date' => 0, + 'settings' => $client_settings, + ]); + + $line_items = []; + + $item = new InvoiceItem(); + $item->product_key = '1234567890'; + $item->qty = 1; + $item->cost = 100; + $item->notes = 'Test item'; + $item->tax_name1 = 'IVA'; + $item->tax_rate1 = 21; + + $line_items[] = $item; + + $invoice = Invoice::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'client_id' => $this->client->id, + 'number' => 'INV-2024-001', + 'date' => '2024-01-15', + 'due_date' => now()->addDays(5)->format('Y-m-d'), + 'status_id' => Invoice::STATUS_DRAFT, + 'amount' => 121.00, + 'balance' => 121.00, + 'line_items' => $line_items, + ]); + + $invoice = $invoice->calc() + ->getInvoice() + ->service() + ->markSent() + ->save(); + + return $invoice; + } + + public function testInvoiceCancellationCreation() + { + $invoice = $this->buildTestInvoice(); + + $huella = 'ABCD1234EF5678901234567890ABCDEF1234567890ABCDEF1234567890ABCDEF12'; + + $cancellation = InvoiceCancellation::fromInvoice($invoice, $huella); + + $this->assertInstanceOf(InvoiceCancellation::class, $cancellation); + $this->assertEquals('INV-2024-001', $cancellation->getNumSerieFacturaEmisor()); + $this->assertEquals('2024-01-15', $cancellation->getFechaExpedicionFacturaEmisor()); + $this->assertEquals($this->test_company_nif, $cancellation->getNifEmisor()); + $this->assertEquals($huella, $cancellation->getHuellaFactura()); + $this->assertEquals('02', $cancellation->getEstado()); + $this->assertEquals('Factura anulada por error', $cancellation->getDescripcionEstado()); + } + + public function testInvoiceCancellationXmlGeneration() + { + $invoice = $this->buildTestInvoice(); + + $huella = 'ABCD1234EF5678901234567890ABCDEF1234567890ABCDEF1234567890ABCDEF12'; + + $cancellation = InvoiceCancellation::fromInvoice($invoice, $huella); + + $xmlString = $cancellation->toXmlString(); + + // Verify XML structure + $this->assertNotEmpty($xmlString); + $this->assertStringContainsString('', $xmlString); + $this->assertStringContainsString('SuministroLRFacturas', $xmlString); + $this->assertStringContainsString('xmlns:ds="http://www.w3.org/2000/09/xmldsig#"', $xmlString); + $this->assertStringContainsString('Version="1.1"', $xmlString); + + // Verify required elements + $this->assertStringContainsString('LRFacturaEntrada', $xmlString); + $this->assertStringContainsString('IDFactura', $xmlString); + $this->assertStringContainsString('IDEmisorFactura', $xmlString); + $this->assertStringContainsString('NumSerieFacturaEmisor', $xmlString); + $this->assertStringContainsString('FechaExpedicionFacturaEmisor', $xmlString); + $this->assertStringContainsString('NIFEmisor', $xmlString); + $this->assertStringContainsString('HuellaFactura', $xmlString); + $this->assertStringContainsString('EstadoFactura', $xmlString); + $this->assertStringContainsString('Estado', $xmlString); + $this->assertStringContainsString('DescripcionEstado', $xmlString); + + // Verify specific values + $this->assertStringContainsString('INV-2024-001', $xmlString); + $this->assertStringContainsString('2024-01-15', $xmlString); + $this->assertStringContainsString($this->test_company_nif, $xmlString); + $this->assertStringContainsString($huella, $xmlString); + $this->assertStringContainsString('02', $xmlString); + $this->assertStringContainsString('Factura anulada por error', $xmlString); + } + + + public function testInvoiceCancellationSoapEnvelope() + { + $invoice = $this->buildTestInvoice(); + + $huella = 'ABCD1234EF5678901234567890ABCDEF1234567890ABCDEF1234567890ABCDEF12'; + + $cancellation = InvoiceCancellation::fromInvoice($invoice, $huella); + + $soapEnvelope = $cancellation->toSoapEnvelope(); + + // Verify SOAP structure + $this->assertNotEmpty($soapEnvelope); + $this->assertStringContainsString('soapenv:Envelope', $soapEnvelope); + $this->assertStringContainsString('xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"', $soapEnvelope); + $this->assertStringContainsString('xmlns:sum="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroLR.xsd"', $soapEnvelope); + $this->assertStringContainsString('xmlns:sum1="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd"', $soapEnvelope); + + // Verify SOAP body structure + $this->assertStringContainsString('soapenv:Header', $soapEnvelope); + $this->assertStringContainsString('soapenv:Body', $soapEnvelope); + $this->assertStringContainsString('sum:RegFactuSistemaFacturacion', $soapEnvelope); + $this->assertStringContainsString('sum:Cabecera', $soapEnvelope); + $this->assertStringContainsString('sum1:ObligadoEmision', $soapEnvelope); + $this->assertStringContainsString('sum:RegistroFactura', $soapEnvelope); + + // Verify the cancellation XML is embedded in SOAP + $this->assertStringContainsString('SuministroLRFacturas', $soapEnvelope); + $this->assertStringContainsString('LRFacturaEntrada', $soapEnvelope); + } + + + public function testInvoiceCancellationCustomValues() + { + $cancellation = new InvoiceCancellation(); + + $cancellation->setNumSerieFacturaEmisor('CUSTOM-INV-001') + ->setFechaExpedicionFacturaEmisor('2025-01-20') + ->setNifEmisor('B87654321') + ->setHuellaFactura('CUSTOM_HASH_1234567890ABCDEF') + ->setEstado('01') // Different status + ->setDescripcionEstado('Factura anulada por solicitud del cliente'); + + $xmlString = $cancellation->toXmlString(); + + // Verify custom values are in XML + $this->assertStringContainsString('CUSTOM-INV-001', $xmlString); + $this->assertStringContainsString('2025-01-20', $xmlString); + $this->assertStringContainsString('B87654321', $xmlString); + $this->assertStringContainsString('CUSTOM_HASH_1234567890ABCDEF', $xmlString); + $this->assertStringContainsString('01', $xmlString); + $this->assertStringContainsString('Factura anulada por solicitud del cliente', $xmlString); + + } + + public function testInvoiceCancellationSerialization() + { + $invoice = $this->buildTestInvoice(); + + $cancellation = InvoiceCancellation::fromInvoice($invoice, 'TEST_HASH'); + + // Serialize + $serialized = $cancellation->serialize(); + $this->assertNotEmpty($serialized); + $this->assertIsString($serialized); + + // Deserialize + $deserialized = InvoiceCancellation::unserialize($serialized); + $this->assertInstanceOf(InvoiceCancellation::class, $deserialized); + + // Verify all properties are preserved + $this->assertEquals($cancellation->getNumSerieFacturaEmisor(), $deserialized->getNumSerieFacturaEmisor()); + $this->assertEquals($cancellation->getFechaExpedicionFacturaEmisor(), $deserialized->getFechaExpedicionFacturaEmisor()); + $this->assertEquals($cancellation->getNifEmisor(), $deserialized->getNifEmisor()); + $this->assertEquals($cancellation->getHuellaFactura(), $deserialized->getHuellaFactura()); + $this->assertEquals($cancellation->getEstado(), $deserialized->getEstado()); + $this->assertEquals($cancellation->getDescripcionEstado(), $deserialized->getDescripcionEstado()); + + } + + public function testInvoiceCancellationFromXml() + { + $invoice = $this->buildTestInvoice(); + + $originalCancellation = InvoiceCancellation::fromInvoice($invoice, 'ORIGINAL_HASH'); + $originalCancellation->setEstado('03') + ->setDescripcionEstado('Factura anulada por duplicado'); + + $xmlString = $originalCancellation->toXmlString(); + + // Parse from XML + $parsedCancellation = InvoiceCancellation::fromXml($xmlString); + + // Verify all properties are correctly parsed + $this->assertEquals($originalCancellation->getNumSerieFacturaEmisor(), $parsedCancellation->getNumSerieFacturaEmisor()); + $this->assertEquals($originalCancellation->getFechaExpedicionFacturaEmisor(), $parsedCancellation->getFechaExpedicionFacturaEmisor()); + $this->assertEquals($originalCancellation->getNifEmisor(), $parsedCancellation->getNifEmisor()); + $this->assertEquals($originalCancellation->getHuellaFactura(), $parsedCancellation->getHuellaFactura()); + $this->assertEquals($originalCancellation->getEstado(), $parsedCancellation->getEstado()); + $this->assertEquals($originalCancellation->getDescripcionEstado(), $parsedCancellation->getDescripcionEstado()); + + } + + public function testInvoiceCancellationXmlValidation() + { + $invoice = $this->buildTestInvoice(); + + $cancellation = InvoiceCancellation::fromInvoice($invoice, 'VALIDATION_HASH'); + + $xmlString = $cancellation->toXmlString(); + + // Verify XML is well-formed + $doc = new \DOMDocument(); + $this->assertTrue($doc->loadXML($xmlString), 'Generated XML should be well-formed'); + + // Verify required namespaces + $doc->loadXML($xmlString); + $root = $doc->documentElement; + + $this->assertEquals('SuministroLRFacturas', $root->nodeName); + $this->assertEquals('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/suministro/FacturaLR.xsd', $root->getAttribute('xmlns')); + $this->assertEquals('http://www.w3.org/2000/09/xmldsig#', $root->getAttribute('xmlns:ds')); + $this->assertEquals('1.1', $root->getAttribute('Version')); + + } + + public function testInvoiceCancellationDifferentStatusCodes() + { + $invoice = $this->buildTestInvoice(); + + $statusCodes = [ + '01' => 'Factura anulada por solicitud del cliente', + '02' => 'Factura anulada por error', + '03' => 'Factura anulada por duplicado', + '04' => 'Factura anulada por otros motivos' + ]; + + foreach ($statusCodes as $code => $description) { + $cancellation = InvoiceCancellation::fromInvoice($invoice, 'STATUS_HASH_' . $code); + $cancellation->setEstado($code) + ->setDescripcionEstado($description); + + $xmlString = $cancellation->toXmlString(); + + $this->assertStringContainsString($code, $xmlString); + $this->assertStringContainsString($description, $xmlString); + } + + } + + public function testInvoiceCancellationWithNullValues() + { + $cancellation = new InvoiceCancellation(); + + // Test with minimal required values + $cancellation->setNumSerieFacturaEmisor('MINIMAL-INV') + ->setFechaExpedicionFacturaEmisor('2025-01-01') + ->setNifEmisor('B12345678') + ->setHuellaFactura('MINIMAL_HASH'); + + $xmlString = $cancellation->toXmlString(); + + // Should still generate valid XML with default values + $this->assertNotEmpty($xmlString); + $this->assertStringContainsString('MINIMAL-INV', $xmlString); + $this->assertStringContainsString('2025-01-01', $xmlString); + $this->assertStringContainsString('B12345678', $xmlString); + $this->assertStringContainsString('MINIMAL_HASH', $xmlString); + $this->assertStringContainsString('02', $xmlString); // Default estado + $this->assertStringContainsString('Factura anulada por error', $xmlString); // Default description + + } + + public function testInvoiceCancellationIntegrationWithVerifactu() + { + $invoice = $this->buildTestInvoice(); + + // Simulate the integration with the main Verifactu class + $cancellation = InvoiceCancellation::fromInvoice($invoice, 'INTEGRATION_HASH'); + + // Test XML generation + $xmlString = $cancellation->toXmlString(); + $this->assertNotEmpty($xmlString); + + // Test SOAP envelope generation + $soapEnvelope = $cancellation->toSoapEnvelope(); + $this->assertNotEmpty($soapEnvelope); + + // Test serialization for storage + $serialized = $cancellation->serialize(); + $this->assertNotEmpty($serialized); + + // Test that the cancellation can be stored and retrieved + $deserialized = InvoiceCancellation::unserialize($serialized); + $this->assertInstanceOf(InvoiceCancellation::class, $deserialized); + + // Verify the deserialized object can still generate XML + $newXmlString = $deserialized->toXmlString(); + $this->assertNotEmpty($newXmlString); + $this->assertEquals($xmlString, $newXmlString); + + } + + public function testInvoiceCancellationExactXmlFormat() + { + $invoice = $this->buildTestInvoice(); + + $cancellation = InvoiceCancellation::fromInvoice($invoice, 'ABCD1234EF5678901234567890ABCDEF1234567890ABCDEF1234567890ABCDEF12'); + + $xmlString = $cancellation->toXmlString(); + + // Verify the exact XML structure matches the required format + $expectedElements = [ + '', + '', + '', + '', + 'INV-2024-001', + '2024-01-15', + 'A39200019', + 'ABCD1234EF5678901234567890ABCDEF1234567890ABCDEF1234567890ABCDEF12', + '', + '', + '', + '02', + 'Factura anulada por error', + '', + '', + '' + ]; + + foreach ($expectedElements as $element) { + $this->assertStringContainsString($element, $xmlString, "XML should contain: $element"); + } + + // Verify XML is properly formatted and indented + $this->assertStringContainsString(' ', $xmlString); + $this->assertStringContainsString(' ', $xmlString); + $this->assertStringContainsString(' ', $xmlString); + $this->assertStringContainsString(' ', $xmlString); + } +} \ No newline at end of file diff --git a/tests/Feature/EInvoice/Verifactu/VerifactuFeatureTest.php b/tests/Feature/EInvoice/Verifactu/VerifactuFeatureTest.php index c30716a2ec..f7c49f4c9a 100644 --- a/tests/Feature/EInvoice/Verifactu/VerifactuFeatureTest.php +++ b/tests/Feature/EInvoice/Verifactu/VerifactuFeatureTest.php @@ -166,4 +166,54 @@ class VerifactuFeatureTest extends TestCase $this->assertNotNull($invoice); } + + public function testInvoiceCancellation() + { + // Create a sample invoice + $invoice = $this->buildData(); + + // Create cancellation from invoice + $cancellation = \App\Services\EDocument\Standards\Verifactu\Models\InvoiceCancellation::fromInvoice( + $invoice, + 'ABCD1234EF5678901234567890ABCDEF1234567890ABCDEF1234567890ABCDEF12' + ); + + // Set custom cancellation details + $cancellation->setEstado('02') // 02 = Invoice cancelled + ->setDescripcionEstado('Factura anulada por error'); + + // Generate XML + $xmlString = $cancellation->toXmlString(); + + // Verify XML structure + $this->assertNotEmpty($xmlString); + $this->assertStringContainsString('SuministroLRFacturas', $xmlString); + $this->assertStringContainsString('LRFacturaEntrada', $xmlString); + $this->assertStringContainsString('IDFactura', $xmlString); + $this->assertStringContainsString('EstadoFactura', $xmlString); + $this->assertStringContainsString('Estado', $xmlString); + $this->assertStringContainsString('02', $xmlString); // Cancelled status + + // Generate SOAP envelope + $soapEnvelope = $cancellation->toSoapEnvelope(); + + // Verify SOAP structure + $this->assertNotEmpty($soapEnvelope); + $this->assertStringContainsString('soapenv:Envelope', $soapEnvelope); + $this->assertStringContainsString('RegFactuSistemaFacturacion', $soapEnvelope); + + // Test serialization + $serialized = $cancellation->serialize(); + $this->assertNotEmpty($serialized); + + // Test deserialization + $deserialized = \App\Services\EDocument\Standards\Verifactu\Models\InvoiceCancellation::unserialize($serialized); + $this->assertEquals($cancellation->getNumSerieFacturaEmisor(), $deserialized->getNumSerieFacturaEmisor()); + $this->assertEquals($cancellation->getEstado(), $deserialized->getEstado()); + + // Test from XML + $fromXml = \App\Services\EDocument\Standards\Verifactu\Models\InvoiceCancellation::fromXml($xmlString); + $this->assertEquals($cancellation->getNumSerieFacturaEmisor(), $fromXml->getNumSerieFacturaEmisor()); + $this->assertEquals($cancellation->getEstado(), $fromXml->getEstado()); + } } \ No newline at end of file