additional tests
This commit is contained in:
parent
b94316dbed
commit
442ff42ceb
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue