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