additional tests

This commit is contained in:
David Bomba 2025-08-08 14:04:13 +10:00
parent b94316dbed
commit 442ff42ceb
5 changed files with 880 additions and 4 deletions

View File

@ -29,12 +29,16 @@ use App\Services\EDocument\Standards\Verifactu\RegistroAlta;
use App\Services\EDocument\Standards\Verifactu\RegistroModificacion; use App\Services\EDocument\Standards\Verifactu\RegistroModificacion;
use App\Services\EDocument\Standards\Verifactu\Models\Invoice as VerifactuInvoice; use App\Services\EDocument\Standards\Verifactu\Models\Invoice as VerifactuInvoice;
use App\Services\EDocument\Standards\Verifactu\Models\InvoiceModification; use App\Services\EDocument\Standards\Verifactu\Models\InvoiceModification;
use App\Services\EDocument\Standards\Verifactu\AeatClient;
class Verifactu extends AbstractService class Verifactu extends AbstractService
{ {
private AeatClient $aeat_client;
public function __construct(public Invoice $invoice) 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. //determine the current status of the invoice.
$document = new RegistroAlta($this->invoice); $document = new RegistroAlta($this->invoice);
$huella = '';
//1. new => RegistraAlta //1. new => RegistraAlta
if($v_logs->count() >= 1){ if($v_logs->count() >= 1){
$v_log = $v_logs->first(); $v_log = $v_logs->first();
$huella = $v_log->hash;
$document = InvoiceModification::fromInvoice($this->invoice, $v_log->deserialize()); $document = InvoiceModification::fromInvoice($this->invoice, $v_log->deserialize());
} }
//3. cancelled => RegistroAnulacion //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);
}
}

View File

@ -0,0 +1,332 @@
<?php
namespace App\Services\EDocument\Standards\Verifactu\Models;
use App\Models\Invoice;
/**
* InvoiceCancellation - Invoice Cancellation for Verifactu
*
* This class generates the XML structure for cancelling invoices in the Verifactu system.
* It follows the specific format required by the Spanish Tax Agency (AEAT).
*/
class InvoiceCancellation extends BaseXmlModel implements XmlModelInterface
{
protected string $idVersion = '1.1';
protected string $numSerieFacturaEmisor;
protected string $fechaExpedicionFacturaEmisor;
protected string $nifEmisor;
protected string $huellaFactura;
protected string $estado = '02'; // 02 means 'Invoice cancelled'
protected string $descripcionEstado = 'Factura anulada por error';
public function __construct()
{
// Default constructor
}
/**
* Create cancellation from an existing invoice
*/
public static function fromInvoice(Invoice $invoice, string $huella = ''): self
{
$cancellation = new self();
$cancellation->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;
}
}

View File

@ -62,11 +62,19 @@ class InvoiceModification extends BaseXmlModel implements XmlModelInterface
return $this; return $this;
} }
public function setHuella(string $huella): self
{
$this->getRegistroModificacion()->setHuella($huella);
return $this;
}
/** /**
* Create a modification from an existing invoice * Create a modification from an existing invoice
*/ */
public static function createFromInvoice(Invoice $originalInvoice, Invoice $modifiedInvoice): self public static function createFromInvoice(Invoice $originalInvoice, Invoice $modifiedInvoice): self
{ {
$currentTimestamp = now()->format('Y-m-d\TH:i:s');
$modification = new self(); $modification = new self();
// Set up cancellation record // Set up cancellation record
@ -107,11 +115,11 @@ class InvoiceModification extends BaseXmlModel implements XmlModelInterface
->setImporteTotal($modifiedInvoice->getImporteTotal()) ->setImporteTotal($modifiedInvoice->getImporteTotal())
->setEncadenamiento($modifiedInvoice->getEncadenamiento()) ->setEncadenamiento($modifiedInvoice->getEncadenamiento())
->setSistemaInformatico($modifiedInvoice->getSistemaInformatico()) ->setSistemaInformatico($modifiedInvoice->getSistemaInformatico())
->setFechaHoraHusoGenRegistro($modifiedInvoice->getFechaHoraHusoGenRegistro()) ->setFechaHoraHusoGenRegistro($currentTimestamp)
->setNumRegistroAcuerdoFacturacion($modifiedInvoice->getNumRegistroAcuerdoFacturacion()) ->setNumRegistroAcuerdoFacturacion($modifiedInvoice->getNumRegistroAcuerdoFacturacion())
->setIdAcuerdoSistemaInformatico($modifiedInvoice->getIdAcuerdoSistemaInformatico()) ->setIdAcuerdoSistemaInformatico($modifiedInvoice->getIdAcuerdoSistemaInformatico())
->setTipoHuella($modifiedInvoice->getTipoHuella()) ->setTipoHuella($modifiedInvoice->getTipoHuella())
->setHuella($modifiedInvoice->getHuella()); ->setHuella('PLACEHOLDER_HUELLA');
$modification->setRegistroModificacion($modificationRecord); $modification->setRegistroModificacion($modificationRecord);

View File

@ -0,0 +1,442 @@
<?php
namespace Tests\Feature\EInvoice\Verifactu;
use Tests\TestCase;
use App\Models\User;
use App\Models\Client;
use App\Models\Account;
use App\Models\Company;
use App\Models\Invoice;
use Faker\Factory as Faker;
use App\Models\CompanyToken;
use App\Models\ClientContact;
use App\DataMapper\InvoiceItem;
use App\DataMapper\ClientSettings;
use App\DataMapper\CompanySettings;
use App\Factory\CompanyUserFactory;
use App\Services\EDocument\Standards\Verifactu\Models\InvoiceCancellation;
class InvoiceCancellationTest extends TestCase
{
private $user;
private $company;
private $token;
private $client;
private $faker;
private string $test_company_nif = 'A39200019';
private string $test_client_nif = 'A39200019';
protected function setUp(): void
{
parent::setUp();
$this->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('<?xml version="1.0" encoding="UTF-8"?>', $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 = [
'<?xml version="1.0" encoding="UTF-8"?>',
'<SuministroLRFacturas',
'xmlns="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/suministro/FacturaLR.xsd"',
'xmlns:ds="http://www.w3.org/2000/09/xmldsig#"',
'Version="1.1"',
'<LRFacturaEntrada>',
'<IDFactura>',
'<IDEmisorFactura>',
'<NumSerieFacturaEmisor>INV-2024-001</NumSerieFacturaEmisor>',
'<FechaExpedicionFacturaEmisor>2024-01-15</FechaExpedicionFacturaEmisor>',
'<NIFEmisor>A39200019</NIFEmisor>',
'<HuellaFactura>ABCD1234EF5678901234567890ABCDEF1234567890ABCDEF1234567890ABCDEF12</HuellaFactura>',
'</IDEmisorFactura>',
'</IDFactura>',
'<EstadoFactura>',
'<Estado>02</Estado>',
'<DescripcionEstado>Factura anulada por error</DescripcionEstado>',
'</EstadoFactura>',
'</LRFacturaEntrada>',
'</SuministroLRFacturas>'
];
foreach ($expectedElements as $element) {
$this->assertStringContainsString($element, $xmlString, "XML should contain: $element");
}
// Verify XML is properly formatted and indented
$this->assertStringContainsString(' <LRFacturaEntrada>', $xmlString);
$this->assertStringContainsString(' <IDFactura>', $xmlString);
$this->assertStringContainsString(' <IDEmisorFactura>', $xmlString);
$this->assertStringContainsString(' <NumSerieFacturaEmisor>', $xmlString);
}
}

View File

@ -166,4 +166,54 @@ class VerifactuFeatureTest extends TestCase
$this->assertNotNull($invoice); $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());
}
} }