Updates for Verifactu

This commit is contained in:
David Bomba 2025-08-10 15:32:35 +10:00
parent c02c87765b
commit 8a137329d4
11 changed files with 296 additions and 3025 deletions

View File

@ -104,16 +104,11 @@ class VerifactuDocumentValidator extends XsltDocumentValidator
$registroAlta = $xpath->query('//si:RegistroAlta | //sum1:RegistroAlta'); $registroAlta = $xpath->query('//si:RegistroAlta | //sum1:RegistroAlta');
if ($registroAlta->length > 0) { if ($registroAlta->length > 0) {
$tipoFactura = $xpath->query('.//si:TipoFactura | .//sum1:TipoFactura', $registroAlta->item(0)); $tipoFactura = $xpath->query('.//si:TipoFactura | .//sum1:TipoFactura', $registroAlta->item(0));
if ($tipoFactura->length > 0 && $tipoFactura->item(0)->textContent === 'R1') { if ($tipoFactura->length > 0 && in_array($tipoFactura->item(0)->textContent,['R1','F3'])) {
return 'modification'; return 'modification';
} }
} }
// Check for RegistroModificacion structure (legacy)
$registroModificacion = $xpath->query('//si:RegistroModificacion | //sum1:RegistroModificacion');
if ($registroModificacion->length > 0) {
return 'modification';
}
// Check for cancellation structure // Check for cancellation structure
$registroAnulacion = $xpath->query('//si:RegistroAnulacion | //sum1:RegistroAnulacion'); $registroAnulacion = $xpath->query('//si:RegistroAnulacion | //sum1:RegistroAnulacion');
@ -192,7 +187,7 @@ class VerifactuDocumentValidator extends XsltDocumentValidator
if ($tipoFactura === false || $tipoFactura->length === 0) { if ($tipoFactura === false || $tipoFactura->length === 0) {
$tipoFactura = $xpath->query('.//sum1:TipoFactura', $registroAlta->item(0)); $tipoFactura = $xpath->query('.//sum1:TipoFactura', $registroAlta->item(0));
} }
if ($tipoFactura !== false && $tipoFactura->length > 0 && $tipoFactura->item(0)->textContent !== 'R1') { if ($tipoFactura !== false && $tipoFactura->length > 0 && !in_array($tipoFactura->item(0)->textContent, ['R1','F3'])) {
$this->errors['structure'][] = "TipoFactura must be 'R1' for modifications, found: " . $tipoFactura->item(0)->textContent; $this->errors['structure'][] = "TipoFactura must be 'R1' for modifications, found: " . $tipoFactura->item(0)->textContent;
} }
} }

View File

@ -24,11 +24,9 @@ use App\Helpers\Invoice\InvoiceSumInclusive;
use App\Services\EDocument\Standards\Verifactu\AeatClient; use App\Services\EDocument\Standards\Verifactu\AeatClient;
use App\Services\EDocument\Standards\Verifactu\RegistroAlta; use App\Services\EDocument\Standards\Verifactu\RegistroAlta;
use App\Services\EDocument\Standards\Verifactu\Models\Desglose; use App\Services\EDocument\Standards\Verifactu\Models\Desglose;
use App\Services\EDocument\Standards\Verifactu\RegistroModificacion;
use App\Services\EDocument\Standards\Verifactu\Models\Encadenamiento; use App\Services\EDocument\Standards\Verifactu\Models\Encadenamiento;
use App\Services\EDocument\Standards\Verifactu\Models\RegistroAnterior; use App\Services\EDocument\Standards\Verifactu\Models\RegistroAnterior;
use App\Services\EDocument\Standards\Verifactu\Models\SistemaInformatico; use App\Services\EDocument\Standards\Verifactu\Models\SistemaInformatico;
use App\Services\EDocument\Standards\Verifactu\Models\InvoiceModification;
use App\Services\EDocument\Standards\Verifactu\Models\PersonaFisicaJuridica; use App\Services\EDocument\Standards\Verifactu\Models\PersonaFisicaJuridica;
use App\Services\EDocument\Standards\Verifactu\Models\Invoice as VerifactuInvoice; use App\Services\EDocument\Standards\Verifactu\Models\Invoice as VerifactuInvoice;
@ -62,8 +60,14 @@ class Verifactu extends AbstractService
$v_logs = $this->invoice->company->verifactu_logs; $v_logs = $this->invoice->company->verifactu_logs;
//determine the current status of the invoice. $i_logs = $this->invoice->verifactu_logs;
$document = (new RegistroAlta($this->invoice))->run()->getInvoice();
if($i_logs->count() >= 1){
$document = (new RegistroAlta($this->invoice))->run()->setRectification()->getInvoice();
}
else{
$document = (new RegistroAlta($this->invoice))->run()->getInvoice();
}
//keep this state for logging later on successful send //keep this state for logging later on successful send
$this->_document = $document; $this->_document = $document;
@ -74,7 +78,6 @@ class Verifactu extends AbstractService
if($v_logs->count() >= 1){ if($v_logs->count() >= 1){
$v_log = $v_logs->first(); $v_log = $v_logs->first();
$this->_previous_huella = $v_log->hash; $this->_previous_huella = $v_log->hash;
// $document = InvoiceModification::createFromInvoice($document, $v_log->deserialize());
} }
//3. cancelled => RegistroAnulacion //3. cancelled => RegistroAnulacion

View File

@ -21,6 +21,7 @@ class Invoice extends BaseXmlModel implements XmlModelInterface
// Constants for invoice types // Constants for invoice types
public const TIPO_FACTURA_NORMAL = 'F1'; public const TIPO_FACTURA_NORMAL = 'F1';
public const TIPO_FACTURA_RECTIFICATIVA = 'R1'; public const TIPO_FACTURA_RECTIFICATIVA = 'R1';
public const TIPO_FACTURA_SUSTITUIDA = 'F3';
// Constants for rectification types // Constants for rectification types
public const TIPO_RECTIFICATIVA_COMPLETA = 'I'; // Rectificación por diferencias (Complete rectification) public const TIPO_RECTIFICATIVA_COMPLETA = 'I'; // Rectificación por diferencias (Complete rectification)
@ -36,7 +37,7 @@ class Invoice extends BaseXmlModel implements XmlModelInterface
protected ?string $tipoRectificativa = null; protected ?string $tipoRectificativa = null;
protected ?array $facturasRectificadas = null; protected ?array $facturasRectificadas = null;
protected ?array $facturasSustituidas = null; protected ?array $facturasSustituidas = null;
protected ?float $importeRectificacion = null; protected ?array $importeRectificacion = null;
protected ?string $fechaOperacion = null; protected ?string $fechaOperacion = null;
protected string $descripcionOperacion; protected string $descripcionOperacion;
protected ?string $facturaSimplificadaArt7273 = null; protected ?string $facturaSimplificadaArt7273 = null;
@ -233,21 +234,17 @@ class Invoice extends BaseXmlModel implements XmlModelInterface
return $this->importeRectificacion; return $this->importeRectificacion;
} }
public function setImporteRectificacion(?float $importeRectificacion): self public function setImporteRectificacion(?array $importeRectificacion): self
{ {
if ($importeRectificacion !== null) {
// Validate that the amount is within reasonable bounds
if (abs($importeRectificacion) > 999999999.99) {
throw new \InvalidArgumentException('ImporteRectificacion must be between -999999999.99 and 999999999.99');
}
}
$this->importeRectificacion = $importeRectificacion; $this->importeRectificacion = $importeRectificacion;
return $this; return $this;
} }
public function setRectificationAmounts(array $amounts): self public function setRectificationAmounts(array $amounts): self
{ {
$this->importeRectificacion = $amounts; $this->importeRectificacion = $amounts;
return $this; return $this;
} }
@ -565,11 +562,11 @@ class Invoice extends BaseXmlModel implements XmlModelInterface
* Helper method to create a rectificative invoice with ImporteRectificacion * Helper method to create a rectificative invoice with ImporteRectificacion
* *
* @param string $tipoRectificativa The type of rectification ('I' for complete, 'S' for substitutive) * @param string $tipoRectificativa The type of rectification ('I' for complete, 'S' for substitutive)
* @param float $importeRectificacion The rectification amount * @param array $importeRectificacion The rectification amount
* @param string $descripcionOperacion Description of the rectification operation * @param string $descripcionOperacion Description of the rectification operation
* @return self * @return self
*/ */
public function makeRectificativeWithAmount(string $tipoRectificativa, float $importeRectificacion, string $descripcionOperacion = 'Rectificación de factura'): self public function makeRectificativeWithAmount(string $tipoRectificativa, array $importeRectificacion, string $descripcionOperacion = 'Rectificación de factura'): self
{ {
$this->setTipoFactura(self::TIPO_FACTURA_RECTIFICATIVA) $this->setTipoFactura(self::TIPO_FACTURA_RECTIFICATIVA)
->setTipoRectificativa($tipoRectificativa) ->setTipoRectificativa($tipoRectificativa)
@ -947,7 +944,7 @@ class Invoice extends BaseXmlModel implements XmlModelInterface
$root->appendChild($this->createElement($doc, 'TipoFactura', $this->tipoFactura)); $root->appendChild($this->createElement($doc, 'TipoFactura', $this->tipoFactura));
// 5. TipoRectificativa (only for R1 invoices) // 5. TipoRectificativa (only for R1 invoices)
if ($this->tipoFactura === self::TIPO_FACTURA_RECTIFICATIVA && $this->tipoRectificativa !== null) { if (in_array($this->tipoFactura, [self::TIPO_FACTURA_SUSTITUIDA, self::TIPO_FACTURA_RECTIFICATIVA]) && $this->tipoRectificativa !== null) {
$root->appendChild($this->createElement($doc, 'TipoRectificativa', $this->tipoRectificativa)); $root->appendChild($this->createElement($doc, 'TipoRectificativa', $this->tipoRectificativa));
} }
@ -973,8 +970,32 @@ class Invoice extends BaseXmlModel implements XmlModelInterface
$root->appendChild($facturasRectificadasElement); $root->appendChild($facturasRectificadasElement);
} }
if ($this->tipoFactura === self::TIPO_FACTURA_SUSTITUIDA && $this->facturasSustituidas !== null) {
$facturasSustituidasElement = $this->createElement($doc, 'FacturasSustituidas');
foreach ($this->facturasSustituidas as $facturaSustituidas) {
$idFacturaSustituidasElement = $this->createElement($doc, 'IDFacturaSustituida');
// Add IDEmisorFactura
$idFacturaSustituidasElement->appendChild($this->createElement($doc, 'IDEmisorFactura', $facturaSustituidas['IDEmisorFactura']));
// Add NumSerieFactura
$idFacturaSustituidasElement->appendChild($this->createElement($doc, 'NumSerieFactura', $facturaSustituidas['NumSerieFactura']));
// Add FechaExpedicionFactura
$idFacturaSustituidasElement->appendChild($this->createElement($doc, 'FechaExpedicionFactura', $facturaSustituidas['FechaExpedicionFactura']));
$facturasSustituidasElement->appendChild($idFacturaSustituidasElement);
}
$root->appendChild($facturasSustituidasElement);
}
// 7. ImporteRectificacion (only for R1 invoices with proper structure) // 7. ImporteRectificacion (only for R1 invoices with proper structure)
if ($this->tipoFactura === self::TIPO_FACTURA_RECTIFICATIVA && $this->importeRectificacion !== null) { if (in_array($this->tipoFactura, [self::TIPO_FACTURA_RECTIFICATIVA, self::TIPO_FACTURA_SUSTITUIDA]) && $this->importeRectificacion !== null) {
$importeRectificacionElement = $this->createElement($doc, 'ImporteRectificacion'); $importeRectificacionElement = $this->createElement($doc, 'ImporteRectificacion');
// Add BaseRectificada // Add BaseRectificada
@ -1442,14 +1463,6 @@ class Invoice extends BaseXmlModel implements XmlModelInterface
return $node ? $node->nodeValue : null; return $node ? $node->nodeValue : null;
} }
/**
* Create a modification from this invoice
*/
public function createModification(Invoice $modifiedInvoice): InvoiceModification
{
return InvoiceModification::createFromInvoice($this, $modifiedInvoice);
}
/** /**
* Create a cancellation record for this invoice * Create a cancellation record for this invoice
*/ */
@ -1468,47 +1481,6 @@ class Invoice extends BaseXmlModel implements XmlModelInterface
return $cancellation; return $cancellation;
} }
/**
* Create a modification record from this invoice
*/
public function createModificationRecord(): RegistroModificacion
{
$modificationRecord = new RegistroModificacion();
$modificationRecord
->setIdVersion($this->getIdVersion())
->setIdFactura($this->getIdFactura())
->setRefExterna($this->getRefExterna())
->setNombreRazonEmisor($this->getNombreRazonEmisor())
->setSubsanacion($this->getSubsanacion())
->setRechazoPrevio($this->getRechazoPrevio())
->setTipoFactura($this->getTipoFactura())
->setTipoRectificativa($this->getTipoRectificativa())
->setFacturasRectificadas($this->getFacturasRectificadas())
->setFacturasSustituidas($this->getFacturasSustituidas())
->setImporteRectificacion($this->getImporteRectificacion())
->setFechaOperacion($this->getFechaOperacion())
->setDescripcionOperacion($this->getDescripcionOperacion())
->setFacturaSimplificadaArt7273($this->getFacturaSimplificadaArt7273())
->setFacturaSinIdentifDestinatarioArt61d($this->getFacturaSinIdentifDestinatarioArt61d())
->setMacrodato($this->getMacrodato())
->setEmitidaPorTerceroODestinatario($this->getEmitidaPorTerceroODestinatario())
->setTercero($this->getTercero())
->setDestinatarios($this->getDestinatarios())
->setCupon($this->getCupon())
->setDesglose($this->getDesglose())
->setCuotaTotal($this->getCuotaTotal())
->setImporteTotal($this->getImporteTotal())
->setEncadenamiento($this->getEncadenamiento())
->setSistemaInformatico($this->getSistemaInformatico())
->setFechaHoraHusoGenRegistro($this->getFechaHoraHusoGenRegistro())
->setNumRegistroAcuerdoFacturacion($this->getNumRegistroAcuerdoFacturacion())
->setIdAcuerdoSistemaInformatico($this->getIdAcuerdoSistemaInformatico())
->setTipoHuella($this->getTipoHuella())
->setHuella($this->getHuella());
return $modificationRecord;
}
public function serialize() public function serialize()
{ {
return serialize($this); return serialize($this);

View File

@ -1,332 +0,0 @@
<?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(\Carbon\Carbon::parse($invoice->date)->format('d-m-Y'));
$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

@ -1,376 +0,0 @@
<?php
namespace App\Services\EDocument\Standards\Verifactu\Models;
/**
* InvoiceModification - Complete Invoice Modification Container
*
* This class represents the complete modification structure required for Verifactu e-invoicing
* modification operations. It contains both the cancellation record and the modification record.
*/
class InvoiceModification extends BaseXmlModel implements XmlModelInterface
{
protected RegistroAnulacion $registroAnulacion;
protected RegistroModificacion $registroModificacion;
protected SistemaInformatico $sistemaInformatico;
// @todo - in the UI we'll need additional logic to support these codes
private array $motivo_anulacion_codes = [
'1' => "Sustitución por otra factura", // Replacement by another invoice
'2' => "Error en facturación", // Billing error
'3' => "Anulación por devolución", // Cancellation due to return
'4' => "Anulación por insolvencia" // Cancellation due to insolvency
];
public function __construct()
{
$this->registroAnulacion = new RegistroAnulacion();
$this->registroModificacion = new RegistroModificacion();
$this->sistemaInformatico = new SistemaInformatico();
}
public function getRegistroAnulacion(): RegistroAnulacion
{
return $this->registroAnulacion;
}
public function setRegistroAnulacion(RegistroAnulacion $registroAnulacion): self
{
$this->registroAnulacion = $registroAnulacion;
return $this;
}
public function getRegistroModificacion(): RegistroModificacion
{
return $this->registroModificacion;
}
public function setRegistroModificacion(RegistroModificacion $registroModificacion): self
{
$this->registroModificacion = $registroModificacion;
return $this;
}
public function getSistemaInformatico(): SistemaInformatico
{
return $this->sistemaInformatico;
}
public function setSistemaInformatico(SistemaInformatico $sistemaInformatico): self
{
$this->sistemaInformatico = $sistemaInformatico;
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:sP');
$modification = new self();
// Set up cancellation record
$cancellation = new RegistroAnulacion();
$cancellation
->setIdEmisorFactura($originalInvoice->getTercero()?->getNif() ?? 'B12345678')
->setNumSerieFactura($originalInvoice->getIdFactura()->getNumSerieFactura())
->setFechaExpedicionFactura($originalInvoice->getFechaExpedicionFactura())
->setMotivoAnulacion('1'); // Sustitución por otra factura
$modification->setRegistroAnulacion($cancellation);
// Set up modification record
$modificationRecord = new RegistroModificacion();
$modificationRecord
->setIdVersion($modifiedInvoice->getIdVersion())
->setIdFactura($modifiedInvoice->getIdFactura())
->setRefExterna($modifiedInvoice->getRefExterna())
->setNombreRazonEmisor($modifiedInvoice->getNombreRazonEmisor())
->setSubsanacion($modifiedInvoice->getSubsanacion())
->setRechazoPrevio($modifiedInvoice->getRechazoPrevio())
->setTipoFactura('R1') // always R1 for rectification
->setTipoRectificativa('S') // always S for rectification
->setFacturasRectificadas($modifiedInvoice->getFacturasRectificadas())
->setFacturasSustituidas($modifiedInvoice->getFacturasSustituidas())
->setImporteRectificacion($modifiedInvoice->getImporteRectificacion())
->setFechaOperacion($modifiedInvoice->getFechaOperacion())
->setDescripcionOperacion($modifiedInvoice->getDescripcionOperacion())
->setFacturaSimplificadaArt7273($modifiedInvoice->getFacturaSimplificadaArt7273())
->setFacturaSinIdentifDestinatarioArt61d($modifiedInvoice->getFacturaSinIdentifDestinatarioArt61d())
->setMacrodato($modifiedInvoice->getMacrodato())
->setEmitidaPorTerceroODestinatario($modifiedInvoice->getEmitidaPorTerceroODestinatario())
->setTercero($modifiedInvoice->getTercero())
->setDestinatarios($modifiedInvoice->getDestinatarios())
->setCupon($modifiedInvoice->getCupon())
->setDesglose($modifiedInvoice->getDesglose())
->setCuotaTotal($modifiedInvoice->getCuotaTotal())
->setImporteTotal($modifiedInvoice->getImporteTotal())
->setEncadenamiento($modifiedInvoice->getEncadenamiento())
->setSistemaInformatico($modifiedInvoice->getSistemaInformatico())
->setFechaHoraHusoGenRegistro($currentTimestamp)
->setNumRegistroAcuerdoFacturacion($modifiedInvoice->getNumRegistroAcuerdoFacturacion())
->setIdAcuerdoSistemaInformatico($modifiedInvoice->getIdAcuerdoSistemaInformatico())
->setTipoHuella($modifiedInvoice->getTipoHuella())
->setHuella('PLACEHOLDER_HUELLA');
$modification->setRegistroModificacion($modificationRecord);
// Set up sistema informatico for the modification (only if not null)
if ($modifiedInvoice->getSistemaInformatico()) {
$modification->setSistemaInformatico($modifiedInvoice->getSistemaInformatico());
}
return $modification;
}
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
if ($this->sistemaInformatico) {
$obligadoEmision->appendChild($soapDoc->createElementNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd', 'sum1:NombreRazon', $this->sistemaInformatico->getNombreRazon()));
$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->sistemaInformatico->getNif()));
} else {
// Default values if no sistema informatico is available
$obligadoEmision->appendChild($soapDoc->createElementNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd', 'sum1:NombreRazon', 'CERTIFICADO FISICA PRUEBAS'));
$obligadoEmision->appendChild($soapDoc->createElementNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd', 'sum1:NIF', 'A39200019'));
}
// 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);
// Create RegistroAlta
$registroAlta = $soapDoc->createElementNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd', 'sum1:RegistroAlta');
$registroFactura->appendChild($registroAlta);
// Add IDVersion inside RegistroAlta
$registroAlta->appendChild($soapDoc->createElementNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd', 'sum1:IDVersion', '1.0'));
// Create IDFactura
$idFactura = $soapDoc->createElementNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd', 'sum1:IDFactura');
$registroAlta->appendChild($idFactura);
// Add IDFactura child elements
$idFactura->appendChild($soapDoc->createElementNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd', 'sum1:IDEmisorFactura', $this->registroModificacion->getIdFactura()->getIdEmisorFactura()));
$idFactura->appendChild($soapDoc->createElementNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd', 'sum1:NumSerieFactura', $this->registroModificacion->getIdFactura()->getNumSerieFactura()));
$idFactura->appendChild($soapDoc->createElementNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd', 'sum1:FechaExpedicionFactura', $this->registroModificacion->getIdFactura()->getFechaExpedicionFactura()));
// Add NombreRazonEmisor
$registroAlta->appendChild($soapDoc->createElementNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd', 'sum1:NombreRazonEmisor', $this->registroModificacion->getNombreRazonEmisor()));
// Add TipoFactura (R1 for rectificativa)
$registroAlta->appendChild($soapDoc->createElementNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd', 'sum1:TipoFactura', 'R1'));
// Add TipoRectificativa for R1 invoices (S for sustitutiva)
if ($this->registroModificacion->getTipoFactura() === 'R1') {
$registroAlta->appendChild($soapDoc->createElementNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd', 'sum1:TipoRectificativa', 'S'));
}
// Add DescripcionOperacion
$registroAlta->appendChild($soapDoc->createElementNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd', 'sum1:DescripcionOperacion', $this->registroModificacion->getDescripcionOperacion()));
// Create ModificacionFactura with correct namespace
$modificacionFactura = $soapDoc->createElementNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd', 'sum1:ModificacionFactura');
$registroAlta->appendChild($modificacionFactura);
// Add TipoRectificativa (S for sustitutiva)
$modificacionFactura->appendChild($soapDoc->createElementNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd', 'sum1:TipoRectificativa', 'S'));
// Create FacturasRectificadas
$facturasRectificadas = $soapDoc->createElementNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd', 'sum1:FacturasRectificadas');
$modificacionFactura->appendChild($facturasRectificadas);
// Add Factura (the original invoice being rectified)
$factura = $soapDoc->createElementNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd', 'sum1:Factura');
$facturasRectificadas->appendChild($factura);
// Add original invoice details
$factura->appendChild($soapDoc->createElementNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd', 'sum1:NumSerieFacturaEmisor', $this->registroAnulacion->getNumSerieFactura()));
$factura->appendChild($soapDoc->createElementNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd', 'sum1:FechaExpedicionFacturaEmisor', $this->registroAnulacion->getFechaExpedicionFactura()));
// Create Desglose
$desglose = $soapDoc->createElementNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd', 'sum1:Desglose');
$registroAlta->appendChild($desglose);
// Create DetalleDesglose
$detalleDesglose = $soapDoc->createElementNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd', 'sum1:DetalleDesglose');
$desglose->appendChild($detalleDesglose);
// Add DetalleDesglose child elements
$detalleDesglose->appendChild($soapDoc->createElementNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd', 'sum1:ClaveRegimen', '01'));
$detalleDesglose->appendChild($soapDoc->createElementNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd', 'sum1:CalificacionOperacion', 'S1'));
$detalleDesglose->appendChild($soapDoc->createElementNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd', 'sum1:TipoImpositivo', '21'));
$detalleDesglose->appendChild($soapDoc->createElementNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd', 'sum1:BaseImponibleOimporteNoSujeto', '200.00'));
$detalleDesglose->appendChild($soapDoc->createElementNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd', 'sum1:CuotaRepercutida', $this->registroModificacion->getCuotaTotal()));
// Add ImporteTotal
$registroAlta->appendChild($soapDoc->createElementNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd', 'sum1:ImporteTotal', $this->registroModificacion->getImporteTotal()));
return $soapDoc->saveXML();
}
public function toXmlString(): string
{
$doc = new \DOMDocument('1.0', 'UTF-8');
$doc->preserveWhiteSpace = false;
$doc->formatOutput = true;
// Create ModificacionFactura root
$root = $doc->createElementNS(self::XML_NAMESPACE, self::XML_NAMESPACE_PREFIX . ':ModificacionFactura');
$doc->appendChild($root);
// Add RegistroAnulacion
$registroAnulacionElement = $this->registroAnulacion->toXml($doc);
$root->appendChild($registroAnulacionElement);
// Add RegistroModificacion
$registroModificacionElement = $this->registroModificacion->toXml($doc);
$root->appendChild($registroModificacionElement);
return $doc->saveXML();
}
public function toXml(\DOMDocument $doc): \DOMElement
{
// Create ModificacionFactura root
$root = $doc->createElementNS(self::XML_NAMESPACE, self::XML_NAMESPACE_PREFIX . ':ModificacionFactura');
// Add RegistroAnulacion
$registroAnulacionElement = $this->registroAnulacion->toXml($doc);
$root->appendChild($registroAnulacionElement);
// Add RegistroModificacion
$registroModificacionElement = $this->registroModificacion->toXml($doc);
$root->appendChild($registroModificacionElement);
return $root;
}
public static function fromDOMElement(\DOMElement $element): self
{
$modification = new self();
// Handle RegistroAnulacion
$registroAnulacionElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'RegistroAnulacion')->item(0);
if ($registroAnulacionElement) {
$registroAnulacion = RegistroAnulacion::fromDOMElement($registroAnulacionElement);
$modification->setRegistroAnulacion($registroAnulacion);
}
// Handle RegistroModificacion
$registroModificacionElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'RegistroModificacion')->item(0);
if ($registroModificacionElement) {
$registroModificacion = RegistroModificacion::fromDOMElement($registroModificacionElement);
$modification->setRegistroModificacion($registroModificacion);
}
return $modification;
}
/**
* Create a proper RegistroAlta structure from the RegistroModificacion data
*/
// private function createRegistroAltaFromModificacion(\DOMDocument $doc): \DOMElement
// {
// $registroAlta = $doc->createElementNS(self::XML_NAMESPACE, self::XML_NAMESPACE_PREFIX . ':RegistroAlta');
// // Add IDVersion
// $registroAlta->appendChild($this->createElement($doc, 'IDVersion', $this->registroModificacion->getIdVersion()));
// // Create IDFactura structure
// $idFactura = $doc->createElementNS(self::XML_NAMESPACE, self::XML_NAMESPACE_PREFIX . ':IDFactura');
// $idFactura->appendChild($this->createElement($doc, 'IDEmisorFactura', $this->registroModificacion->getTercero()?->getNif() ?? 'B12345678'));
// $idFactura->appendChild($this->createElement($doc, 'NumSerieFactura', $this->registroModificacion->getIdFactura()));
// $idFactura->appendChild($this->createElement($doc, 'FechaExpedicionFactura', '2025-01-01'));
// $registroAlta->appendChild($idFactura);
// // Add other required elements
// if ($this->registroModificacion->getRefExterna()) {
// $registroAlta->appendChild($this->createElement($doc, 'RefExterna', $this->registroModificacion->getRefExterna()));
// }
// $registroAlta->appendChild($this->createElement($doc, 'NombreRazonEmisor', $this->registroModificacion->getNombreRazonEmisor()));
// $registroAlta->appendChild($this->createElement($doc, 'TipoFactura', $this->registroModificacion->getTipoFactura()));
// $registroAlta->appendChild($this->createElement($doc, 'DescripcionOperacion', $this->registroModificacion->getDescripcionOperacion()));
// // Add Desglose
// $desglose = $doc->createElementNS(self::XML_NAMESPACE, self::XML_NAMESPACE_PREFIX . ':Desglose');
// $desgloseFactura = $doc->createElementNS(self::XML_NAMESPACE, self::XML_NAMESPACE_PREFIX . ':DesgloseFactura');
// $desgloseFactura->appendChild($this->createElement($doc, 'Impuesto', '01'));
// $desgloseFactura->appendChild($this->createElement($doc, 'ClaveRegimen', '01'));
// $desgloseFactura->appendChild($this->createElement($doc, 'CalificacionOperacion', 'S1'));
// $desgloseFactura->appendChild($this->createElement($doc, 'TipoImpositivo', '21'));
// $desgloseFactura->appendChild($this->createElement($doc, 'BaseImponibleOimporteNoSujeto', '100.00'));
// $desgloseFactura->appendChild($this->createElement($doc, 'CuotaRepercutida', '21.00'));
// $desglose->appendChild($desgloseFactura);
// $registroAlta->appendChild($desglose);
// $registroAlta->appendChild($this->createElement($doc, 'CuotaTotal', $this->registroModificacion->getCuotaTotal()));
// $registroAlta->appendChild($this->createElement($doc, 'ImporteTotal', $this->registroModificacion->getImporteTotal()));
// // Add Encadenamiento
// $encadenamiento = $doc->createElementNS(self::XML_NAMESPACE, self::XML_NAMESPACE_PREFIX . ':Encadenamiento');
// $encadenamiento->appendChild($this->createElement($doc, 'PrimerRegistro', 'S'));
// $registroAlta->appendChild($encadenamiento);
// // Add SistemaInformatico
// $sistemaInformatico = $doc->createElementNS(self::XML_NAMESPACE, self::XML_NAMESPACE_PREFIX . ':SistemaInformatico');
// $sistemaInformatico->appendChild($this->createElement($doc, 'NombreRazon', 'Test System'));
// $sistemaInformatico->appendChild($this->createElement($doc, 'NIF', 'B12345678'));
// $sistemaInformatico->appendChild($this->createElement($doc, 'NombreSistemaInformatico', 'Test Software'));
// $sistemaInformatico->appendChild($this->createElement($doc, 'IdSistemaInformatico', '01'));
// $sistemaInformatico->appendChild($this->createElement($doc, 'Version', '1.0'));
// $sistemaInformatico->appendChild($this->createElement($doc, 'NumeroInstalacion', '001'));
// $sistemaInformatico->appendChild($this->createElement($doc, 'TipoUsoPosibleSoloVerifactu', 'S'));
// $sistemaInformatico->appendChild($this->createElement($doc, 'TipoUsoPosibleMultiOT', 'S'));
// $sistemaInformatico->appendChild($this->createElement($doc, 'IndicadorMultiplesOT', 'S'));
// $registroAlta->appendChild($sistemaInformatico);
// $registroAlta->appendChild($this->createElement($doc, 'FechaHoraHusoGenRegistro', $this->registroModificacion->getFechaHoraHusoGenRegistro()));
// $registroAlta->appendChild($this->createElement($doc, 'TipoHuella', $this->registroModificacion->getTipoHuella()));
// $registroAlta->appendChild($this->createElement($doc, 'Huella', $this->registroModificacion->getHuella()));
// return $registroAlta;
// }
}

View File

@ -1,679 +0,0 @@
<?php
namespace App\Services\EDocument\Standards\Verifactu\Models;
/**
* RegistroModificacion - Invoice Modification Record
*
* This class represents the modification record information required for Verifactu e-invoicing
* modification operations. It contains the new/modified invoice data.
*/
class RegistroModificacion extends BaseXmlModel
{
protected string $idVersion;
protected IDFactura $idFactura;
protected ?string $refExterna = null;
protected string $nombreRazonEmisor;
protected ?string $subsanacion = null;
protected ?string $rechazoPrevio = null;
protected string $tipoFactura;
protected ?string $tipoRectificativa = null;
protected ?array $facturasRectificadas = null;
protected ?array $facturasSustituidas = null;
protected ?float $importeRectificacion = null;
protected ?string $fechaOperacion = null;
protected string $descripcionOperacion;
protected ?string $facturaSimplificadaArt7273 = null;
protected ?string $facturaSinIdentifDestinatarioArt61d = null;
protected ?string $macrodato = null;
protected ?string $emitidaPorTerceroODestinatario = null;
protected ?PersonaFisicaJuridica $tercero = null;
protected ?array $destinatarios = null;
protected ?string $cupon = null;
protected Desglose $desglose;
protected float $cuotaTotal;
protected float $importeTotal;
protected Encadenamiento $encadenamiento;
protected SistemaInformatico $sistemaInformatico;
protected string $fechaHoraHusoGenRegistro;
protected ?string $numRegistroAcuerdoFacturacion = null;
protected ?string $idAcuerdoSistemaInformatico = null;
protected string $tipoHuella;
protected string $huella;
protected ?string $signature = null;
protected ?FacturaRectificativa $facturaRectificativa = null;
protected ?string $privateKeyPath = null;
protected ?string $publicKeyPath = null;
protected ?string $certificatePath = null;
protected ?string $fechaExpedicionFactura = null;
public function __construct()
{
// Initialize required properties
$this->desglose = new Desglose();
$this->encadenamiento = new Encadenamiento();
$this->sistemaInformatico = new SistemaInformatico();
$this->tipoFactura = 'F1'; // Default to normal invoice
}
// Getters and setters - same as Invoice model
public function getIdVersion(): string
{
return $this->idVersion;
}
public function setIdVersion(string $idVersion): self
{
$this->idVersion = $idVersion;
return $this;
}
public function getFechaExpedicionFactura(): string
{
return $this->fechaExpedicionFactura ?? now()->format('d-m-Y');
}
public function setFechaExpedicionFactura(string $fechaExpedicionFactura): self
{
$this->fechaExpedicionFactura = $fechaExpedicionFactura;
return $this;
}
public function getIdFactura(): IDFactura
{
return $this->idFactura;
}
public function setIdFactura(IDFactura $idFactura): self
{
$this->idFactura = $idFactura;
return $this;
}
public function getRefExterna(): ?string
{
return $this->refExterna;
}
public function setRefExterna(?string $refExterna): self
{
$this->refExterna = $refExterna;
return $this;
}
public function getNombreRazonEmisor(): string
{
return $this->nombreRazonEmisor;
}
public function setNombreRazonEmisor(string $nombreRazonEmisor): self
{
$this->nombreRazonEmisor = $nombreRazonEmisor;
return $this;
}
public function getSubsanacion(): ?string
{
return $this->subsanacion;
}
public function setSubsanacion(?string $subsanacion): self
{
$this->subsanacion = $subsanacion;
return $this;
}
public function getRechazoPrevio(): ?string
{
return $this->rechazoPrevio;
}
public function setRechazoPrevio(?string $rechazoPrevio): self
{
$this->rechazoPrevio = $rechazoPrevio;
return $this;
}
public function getTipoFactura(): string
{
return $this->tipoFactura;
}
public function setTipoFactura(string $tipoFactura): self
{
$this->tipoFactura = $tipoFactura;
return $this;
}
public function getTipoRectificativa(): ?string
{
return $this->tipoRectificativa;
}
public function setTipoRectificativa(?string $tipoRectificativa): self
{
$this->tipoRectificativa = $tipoRectificativa;
return $this;
}
public function getFacturasRectificadas(): ?array
{
return $this->facturasRectificadas;
}
public function setFacturasRectificadas(?array $facturasRectificadas): self
{
$this->facturasRectificadas = $facturasRectificadas;
return $this;
}
public function getFacturasSustituidas(): ?array
{
return $this->facturasSustituidas;
}
public function setFacturasSustituidas(?array $facturasSustituidas): self
{
$this->facturasSustituidas = $facturasSustituidas;
return $this;
}
public function getImporteRectificacion(): ?float
{
return $this->importeRectificacion;
}
public function setImporteRectificacion(?float $importeRectificacion): self
{
$this->importeRectificacion = $importeRectificacion;
return $this;
}
public function getFechaOperacion(): ?string
{
return $this->fechaOperacion;
}
public function setFechaOperacion(?string $fechaOperacion): self
{
$this->fechaOperacion = $fechaOperacion;
return $this;
}
public function getDescripcionOperacion(): string
{
return $this->descripcionOperacion;
}
public function setDescripcionOperacion(string $descripcionOperacion): self
{
$this->descripcionOperacion = $descripcionOperacion;
return $this;
}
public function getFacturaSimplificadaArt7273(): ?string
{
return $this->facturaSimplificadaArt7273;
}
public function setFacturaSimplificadaArt7273(?string $facturaSimplificadaArt7273): self
{
$this->facturaSimplificadaArt7273 = $facturaSimplificadaArt7273;
return $this;
}
public function getFacturaSinIdentifDestinatarioArt61d(): ?string
{
return $this->facturaSinIdentifDestinatarioArt61d;
}
public function setFacturaSinIdentifDestinatarioArt61d(?string $facturaSinIdentifDestinatarioArt61d): self
{
$this->facturaSinIdentifDestinatarioArt61d = $facturaSinIdentifDestinatarioArt61d;
return $this;
}
public function getMacrodato(): ?string
{
return $this->macrodato;
}
public function setMacrodato(?string $macrodato): self
{
$this->macrodato = $macrodato;
return $this;
}
public function getEmitidaPorTerceroODestinatario(): ?string
{
return $this->emitidaPorTerceroODestinatario;
}
public function setEmitidaPorTerceroODestinatario(?string $emitidaPorTerceroODestinatario): self
{
$this->emitidaPorTerceroODestinatario = $emitidaPorTerceroODestinatario;
return $this;
}
public function getTercero(): ?PersonaFisicaJuridica
{
return $this->tercero;
}
public function setTercero(?PersonaFisicaJuridica $tercero): self
{
$this->tercero = $tercero;
return $this;
}
public function getDestinatarios(): ?array
{
return $this->destinatarios;
}
public function setDestinatarios(?array $destinatarios): self
{
$this->destinatarios = $destinatarios;
return $this;
}
public function getCupon(): ?string
{
return $this->cupon;
}
public function setCupon(?string $cupon): self
{
$this->cupon = $cupon;
return $this;
}
public function getDesglose(): Desglose
{
return $this->desglose;
}
public function setDesglose(Desglose $desglose): self
{
$this->desglose = $desglose;
return $this;
}
public function getCuotaTotal(): float
{
return $this->cuotaTotal;
}
public function setCuotaTotal(float $cuotaTotal): self
{
$this->cuotaTotal = $cuotaTotal;
return $this;
}
public function getImporteTotal(): float
{
return $this->importeTotal;
}
public function setImporteTotal($importeTotal): self
{
if (!is_numeric($importeTotal)) {
throw new \InvalidArgumentException('ImporteTotal must be a numeric value');
}
$formatted = number_format((float)$importeTotal, 2, '.', '');
if (!preg_match('/^(\+|-)?\d{1,12}(\.\d{0,2})?$/', $formatted)) {
throw new \InvalidArgumentException('ImporteTotal must be a number with up to 12 digits and 2 decimal places');
}
$this->importeTotal = (float)$importeTotal;
return $this;
}
public function getEncadenamiento(): Encadenamiento
{
return $this->encadenamiento;
}
public function setEncadenamiento(Encadenamiento $encadenamiento): self
{
$this->encadenamiento = $encadenamiento;
return $this;
}
public function getSistemaInformatico(): SistemaInformatico
{
return $this->sistemaInformatico;
}
public function setSistemaInformatico(SistemaInformatico $sistemaInformatico): self
{
$this->sistemaInformatico = $sistemaInformatico;
return $this;
}
public function getFechaHoraHusoGenRegistro(): string
{
return $this->fechaHoraHusoGenRegistro;
}
public function setFechaHoraHusoGenRegistro(string $fechaHoraHusoGenRegistro): self
{
$this->fechaHoraHusoGenRegistro = $fechaHoraHusoGenRegistro;
return $this;
}
public function getNumRegistroAcuerdoFacturacion(): ?string
{
return $this->numRegistroAcuerdoFacturacion;
}
public function setNumRegistroAcuerdoFacturacion(?string $numRegistroAcuerdoFacturacion): self
{
$this->numRegistroAcuerdoFacturacion = $numRegistroAcuerdoFacturacion;
return $this;
}
public function getIdAcuerdoSistemaInformatico(): ?string
{
return $this->idAcuerdoSistemaInformatico;
}
public function setIdAcuerdoSistemaInformatico(?string $idAcuerdoSistemaInformatico): self
{
$this->idAcuerdoSistemaInformatico = $idAcuerdoSistemaInformatico;
return $this;
}
public function getTipoHuella(): string
{
return $this->tipoHuella;
}
public function setTipoHuella(string $tipoHuella): self
{
$this->tipoHuella = $tipoHuella;
return $this;
}
public function getHuella(): string
{
return $this->huella;
}
public function setHuella(string $huella): self
{
$this->huella = $huella;
return $this;
}
public function getSignature(): ?string
{
return $this->signature;
}
public function setSignature(?string $signature): self
{
$this->signature = $signature;
return $this;
}
public function getFacturaRectificativa(): ?FacturaRectificativa
{
return $this->facturaRectificativa;
}
public function setFacturaRectificativa(FacturaRectificativa $facturaRectificativa): void
{
$this->facturaRectificativa = $facturaRectificativa;
}
public function setPrivateKeyPath(string $path): self
{
$this->privateKeyPath = $path;
return $this;
}
public function setPublicKeyPath(string $path): self
{
$this->publicKeyPath = $path;
return $this;
}
public function setCertificatePath(string $path): self
{
$this->certificatePath = $path;
return $this;
}
public function toXml(\DOMDocument $doc): \DOMElement
{
// Create root element with proper namespaces
$root = $doc->createElementNS(self::XML_NAMESPACE, self::XML_NAMESPACE_PREFIX . ':RegistroModificacion');
// Add namespaces
$root->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:' . self::XML_NAMESPACE_PREFIX, self::XML_NAMESPACE);
$root->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:' . self::XML_DS_NAMESPACE_PREFIX, self::XML_DS_NAMESPACE);
// Add required elements in exact order according to schema
$root->appendChild($this->createElement($doc, 'IDVersion', $this->idVersion));
// Create IDFactura structure
$idFactura = $this->createElement($doc, 'IDFactura');
$idFactura->appendChild($this->createElement($doc, 'IDEmisorFactura', $this->tercero?->getNif() ?? 'B12345678'));
$idFactura->appendChild($this->createElement($doc, 'NumSerieFactura', $this->idFactura->getNumSerieFactura()));
$idFactura->appendChild($this->createElement($doc, 'FechaExpedicionFactura', $this->getFechaExpedicionFactura()));
$root->appendChild($idFactura);
if ($this->refExterna !== null) {
$root->appendChild($this->createElement($doc, 'RefExterna', $this->refExterna));
}
$root->appendChild($this->createElement($doc, 'NombreRazonEmisor', $this->nombreRazonEmisor));
if ($this->subsanacion !== null) {
$root->appendChild($this->createElement($doc, 'Subsanacion', $this->subsanacion));
}
if ($this->rechazoPrevio !== null) {
$root->appendChild($this->createElement($doc, 'RechazoPrevio', $this->rechazoPrevio));
}
// Create DatosFactura element
$datosFactura = $this->createElement($doc, 'DatosFactura');
// Add TipoFactura to DatosFactura
$datosFactura->appendChild($this->createElement($doc, 'TipoFactura', $this->tipoFactura));
if ($this->tipoFactura === 'R1' && $this->facturaRectificativa !== null) {
$datosFactura->appendChild($this->createElement($doc, 'TipoRectificativa', $this->facturaRectificativa->getTipoRectificativa()));
$facturasRectificadas = $this->createElement($doc, 'FacturasRectificadas');
$facturasRectificadas->appendChild($this->facturaRectificativa->toXml($doc));
$datosFactura->appendChild($facturasRectificadas);
if ($this->importeRectificacion !== null) {
$datosFactura->appendChild($this->createElement($doc, 'ImporteRectificacion', (string)$this->importeRectificacion));
}
}
if ($this->fechaOperacion) {
$datosFactura->appendChild($this->createElement($doc, 'FechaOperacion', date('d-m-Y', strtotime($this->fechaOperacion))));
}
$datosFactura->appendChild($this->createElement($doc, 'DescripcionOperacion', $this->descripcionOperacion));
if ($this->cupon !== null) {
$datosFactura->appendChild($this->createElement($doc, 'Cupon', $this->cupon));
}
if ($this->facturaSimplificadaArt7273 !== null) {
$datosFactura->appendChild($this->createElement($doc, 'FacturaSimplificadaArt7273', $this->facturaSimplificadaArt7273));
}
if ($this->facturaSinIdentifDestinatarioArt61d !== null) {
$datosFactura->appendChild($this->createElement($doc, 'FacturaSinIdentifDestinatarioArt61d', $this->facturaSinIdentifDestinatarioArt61d));
}
if ($this->macrodato !== null) {
$datosFactura->appendChild($this->createElement($doc, 'Macrodato', $this->macrodato));
}
if ($this->emitidaPorTerceroODestinatario !== null) {
$datosFactura->appendChild($this->createElement($doc, 'EmitidaPorTerceroODestinatario', $this->emitidaPorTerceroODestinatario));
}
if ($this->tercero !== null) {
$datosFactura->appendChild($this->tercero->toXml($doc));
}
if ($this->destinatarios !== null && count($this->destinatarios) > 0) {
$destinatariosElement = $this->createElement($doc, 'Destinatarios');
foreach ($this->destinatarios as $destinatario) {
$idDestinatarioElement = $this->createElement($doc, 'IDDestinatario');
// Add NombreRazon
$idDestinatarioElement->appendChild($this->createElement($doc, 'NombreRazon', $destinatario->getNombreRazon()));
// Add either NIF or IDOtro
if ($destinatario->getNif() !== null) {
$idDestinatarioElement->appendChild($this->createElement($doc, 'NIF', $destinatario->getNif()));
} else {
$idOtroElement = $this->createElement($doc, 'IDOtro');
$idOtroElement->appendChild($this->createElement($doc, 'CodigoPais', $destinatario->getPais()));
$idOtroElement->appendChild($this->createElement($doc, 'IDType', $destinatario->getTipoIdentificacion()));
$idOtroElement->appendChild($this->createElement($doc, 'ID', $destinatario->getIdOtro()));
$idDestinatarioElement->appendChild($idOtroElement);
}
$destinatariosElement->appendChild($idDestinatarioElement);
}
$datosFactura->appendChild($destinatariosElement);
}
// Add Desglose to DatosFactura
if ($this->desglose) {
$desgloseElement = $this->desglose->toXml($doc);
$datosFactura->appendChild($desgloseElement);
}
// Add CuotaTotal to DatosFactura
$datosFactura->appendChild($this->createElement($doc, 'CuotaTotal', (string)$this->cuotaTotal));
// Add ImporteTotal to DatosFactura
$datosFactura->appendChild($this->createElement($doc, 'ImporteTotal', (string)$this->importeTotal));
// Add Encadenamiento to DatosFactura
if ($this->encadenamiento) {
$encadenamientoElement = $this->encadenamiento->toXml($doc);
$datosFactura->appendChild($encadenamientoElement);
}
// Add SistemaInformatico to DatosFactura
if ($this->sistemaInformatico) {
$sistemaInformaticoElement = $this->sistemaInformatico->toXml($doc);
$datosFactura->appendChild($sistemaInformaticoElement);
}
// Add FechaHoraHusoGenRegistro to DatosFactura
$datosFactura->appendChild($this->createElement($doc, 'FechaHoraHusoGenRegistro', $this->fechaHoraHusoGenRegistro));
// Add TipoHuella and Huella to DatosFactura
$datosFactura->appendChild($this->createElement($doc, 'TipoHuella', $this->tipoHuella));
$datosFactura->appendChild($this->createElement($doc, 'Huella', $this->huella));
// Add DatosFactura to root
$root->appendChild($datosFactura);
// Add optional Signature
if ($this->signature !== null) {
$root->appendChild($this->createDsElement($doc, 'Signature', $this->signature));
}
return $root;
}
public function toXmlString(): string
{
// Validate required fields first, outside of try-catch
$requiredFields = [
'idVersion' => 'IDVersion',
'idFactura' => 'NumSerieFactura',
'nombreRazonEmisor' => 'NombreRazonEmisor',
'tipoFactura' => 'TipoFactura',
'descripcionOperacion' => 'DescripcionOperacion',
'cuotaTotal' => 'CuotaTotal',
'importeTotal' => 'ImporteTotal',
'fechaHoraHusoGenRegistro' => 'FechaHoraHusoGenRegistro',
'tipoHuella' => 'TipoHuella',
'huella' => 'Huella'
];
foreach ($requiredFields as $property => $fieldName) {
if (!isset($this->$property)) {
throw new \InvalidArgumentException("Missing required field: $fieldName");
}
}
// Enable user error handling for XML operations
$previousErrorSetting = libxml_use_internal_errors(true);
libxml_clear_errors();
try {
$doc = new \DOMDocument('1.0', 'UTF-8');
$doc->preserveWhiteSpace = false;
$doc->formatOutput = true;
// Create root element using toXml method
$root = $this->toXml($doc);
$doc->appendChild($root);
$xml = $doc->saveXML();
if ($xml === false) {
throw new \DOMException('Failed to generate XML');
}
return $xml;
} catch (\ErrorException $e) {
// Convert any libxml errors to DOMException
$errors = libxml_get_errors();
libxml_clear_errors();
if (!empty($errors)) {
throw new \DOMException($errors[0]->message);
}
throw new \DOMException($e->getMessage());
} finally {
// Restore previous error handling setting
libxml_use_internal_errors($previousErrorSetting);
libxml_clear_errors();
}
}
public static function fromDOMElement(\DOMElement $element): self
{
$registroModificacion = new self();
// Handle IDVersion
$idVersion = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'IDVersion')->item(0);
if ($idVersion) {
$registroModificacion->setIdVersion($idVersion->nodeValue);
}
// Handle IDFactura
$idFactura = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'IDFactura')->item(0);
if ($idFactura) {
$numSerieFactura = $idFactura->getElementsByTagNameNS(self::XML_NAMESPACE, 'NumSerieFactura')->item(0);
if ($numSerieFactura) {
$registroModificacion->setIdFactura($numSerieFactura->nodeValue);
}
$fechaExpedicionFactura = $idFactura->getElementsByTagNameNS(self::XML_NAMESPACE, 'FechaExpedicionFactura')->item(0);
if ($fechaExpedicionFactura) {
$registroModificacion->setFechaExpedicionFactura($fechaExpedicionFactura->nodeValue);
}
}
// Handle other fields similar to Invoice model
// ... (implement other field parsing as needed)
return $registroModificacion;
}
}

View File

@ -108,9 +108,6 @@ class RegistroAlta
$this->current_timestamp = now()->format('Y-m-d\TH:i:sP'); $this->current_timestamp = now()->format('Y-m-d\TH:i:sP');
// Determine if this is a rectification invoice
$isRectification = $this->invoice->status_id === 5; // Assuming status_id 5 is for rectification
$this->v_invoice $this->v_invoice
->setIdVersion('1.0') ->setIdVersion('1.0')
->setIdFactura((new IDFactura()) ->setIdFactura((new IDFactura())
@ -118,37 +115,14 @@ class RegistroAlta
->setNumSerieFactura($this->invoice->number) ->setNumSerieFactura($this->invoice->number)
->setFechaExpedicionFactura(\Carbon\Carbon::parse($this->invoice->date)->format('d-m-Y'))) ->setFechaExpedicionFactura(\Carbon\Carbon::parse($this->invoice->date)->format('d-m-Y')))
->setNombreRazonEmisor($this->company->present()->name()) //company name ->setNombreRazonEmisor($this->company->present()->name()) //company name
->setTipoFactura($isRectification ? 'R1' : 'F1') //invoice type ->setTipoFactura('F1') //invoice type
->setDescripcionOperacion($isRectification ? 'Rectificación por error en factura anterior' : 'Alta')// It IS! manadatory - max chars 500 ->setDescripcionOperacion('Alta')// It IS! manadatory - max chars 500
->setCuotaTotal($this->invoice->total_taxes) //total taxes ->setCuotaTotal($this->invoice->total_taxes) //total taxes
->setImporteTotal($this->invoice->amount) //total invoice amount ->setImporteTotal($this->invoice->amount) //total invoice amount
->setFechaHoraHusoGenRegistro($this->current_timestamp) //creation/submission timestamp ->setFechaHoraHusoGenRegistro($this->current_timestamp) //creation/submission timestamp
->setTipoHuella('01') //sha256 ->setTipoHuella('01') //sha256
->setHuella('PLACEHOLDER_HUELLA'); ->setHuella('PLACEHOLDER_HUELLA');
// Set up rectification details if this is a rectification invoice
if ($isRectification) {
$this->v_invoice->setTipoRectificativa('S'); // S for substitutive rectification
// Set up rectified invoice information
$facturasRectificadas = [
[
'IDEmisorFactura' => $this->company->settings->vat_number,
'NumSerieFactura' => $this->invoice->number,
'FechaExpedicionFactura' => \Carbon\Carbon::parse($this->invoice->date)->format('d-m-Y')
]
];
$this->v_invoice->setFacturasRectificadas($facturasRectificadas);
// Set up rectification amounts
$importeRectificacion = [
'BaseRectificada' => $this->calc->getNetSubtotal(),
'CuotaRectificada' => $this->invoice->total_taxes,
'CuotaRecargoRectificado' => 0.00
];
$this->v_invoice->setRectificationAmounts($importeRectificacion);
}
/** The business entity that is issuing the invoice */ /** The business entity that is issuing the invoice */
$emisor = new PersonaFisicaJuridica(); $emisor = new PersonaFisicaJuridica();
$emisor->setNif($this->company->settings->vat_number) $emisor->setNif($this->company->settings->vat_number)
@ -235,6 +209,34 @@ class RegistroAlta
return $this; return $this;
} }
public function setRectification(): self
{
$this->v_invoice->setTipoFactura('R1');
$this->v_invoice->setTipoRectificativa('S'); // S for substitutive rectification
// Set up rectified invoice information
$facturasRectificadas = [
[
'IDEmisorFactura' => $this->company->settings->vat_number,
'NumSerieFactura' => $this->invoice->number,
'FechaExpedicionFactura' => \Carbon\Carbon::parse($this->invoice->date)->format('d-m-Y')
]
];
$this->v_invoice->setFacturasRectificadas($facturasRectificadas);
// Set up rectification amounts
$importeRectificacion = [
'BaseRectificada' => $this->calc->getNetSubtotal(),
'CuotaRectificada' => $this->invoice->total_taxes,
'CuotaRecargoRectificado' => 0.00
];
$this->v_invoice->setRectificationAmounts($importeRectificacion);
return $this;
}
public function getInvoice(): VerifactuInvoice public function getInvoice(): VerifactuInvoice
{ {
return $this->v_invoice; return $this->v_invoice;

View File

@ -1,443 +0,0 @@
<?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('15-01-2024', $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('15-01-2024', $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>15-01-2024</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

@ -24,7 +24,6 @@ use App\Services\EDocument\Standards\Verifactu\Models\IDFactura;
use App\Services\EDocument\Standards\Verifactu\ResponseProcessor; use App\Services\EDocument\Standards\Verifactu\ResponseProcessor;
use App\Services\EDocument\Standards\Verifactu\Models\Encadenamiento; use App\Services\EDocument\Standards\Verifactu\Models\Encadenamiento;
use App\Services\EDocument\Standards\Verifactu\Models\RegistroAnterior; use App\Services\EDocument\Standards\Verifactu\Models\RegistroAnterior;
use App\Services\EDocument\Standards\Verifactu\Models\InvoiceModification;
use App\Services\EDocument\Standards\Validation\VerifactuDocumentValidator; use App\Services\EDocument\Standards\Validation\VerifactuDocumentValidator;
use App\Services\EDocument\Standards\Verifactu\Models\FacturaRectificativa; use App\Services\EDocument\Standards\Verifactu\Models\FacturaRectificativa;
use App\Services\EDocument\Standards\Verifactu\Models\Invoice as VerifactuInvoice; use App\Services\EDocument\Standards\Verifactu\Models\Invoice as VerifactuInvoice;
@ -195,7 +194,7 @@ class VerifactuFeatureTest extends TestCase
$invoice = $this->buildData(); $invoice = $this->buildData();
$invoice->number = 'TEST0033343459'; $invoice->number = 'TEST0033343460';
$invoice->save(); $invoice->save();
$this->assertNotNull($invoice); $this->assertNotNull($invoice);
@ -212,17 +211,17 @@ class VerifactuFeatureTest extends TestCase
$xx = VerifactuLog::create([ $xx = VerifactuLog::create([
'invoice_id' => $_inv->id, 'invoice_id' => $_inv->id,
'company_id' => $invoice->company_id, 'company_id' => $invoice->company_id,
'invoice_number' => 'TEST0033343458', 'invoice_number' => 'TEST0033343459',
'date' => '2025-08-10', 'date' => '2025-08-10',
'hash' => '71E0DB528B7D83CE44A1D9055FE814371D77A9291EB24B74043ACE639175CC3C', 'hash' => 'E5A23515881D696FCD1CA8EE4902632BFC6D892BA8EB79CB656A5F84963079D3',
'nif' => 'A39200019', 'nif' => 'A39200019',
'previous_hash' => '71E0DB528B7D83CE44A1D9055FE814371D77A9291EB24B74043ACE639175CC3C', 'previous_hash' => 'E5A23515881D696FCD1CA8EE4902632BFC6D892BA8EB79CB656A5F84963079D3',
]); ]);
$verifactu = new Verifactu($invoice); $verifactu = new Verifactu($invoice);
$verifactu->run(); $verifactu->run();
$verifactu->setTestMode() $verifactu->setTestMode()
->setPreviousHash('71E0DB528B7D83CE44A1D9055FE814371D77A9291EB24B74043ACE639175CC3C'); ->setPreviousHash('E5A23515881D696FCD1CA8EE4902632BFC6D892BA8EB79CB656A5F84963079D3');
$validator = new \App\Services\EDocument\Standards\Validation\VerifactuDocumentValidator($verifactu->getEnvelope()); $validator = new \App\Services\EDocument\Standards\Validation\VerifactuDocumentValidator($verifactu->getEnvelope());
$validator->validate(); $validator->validate();
@ -262,291 +261,245 @@ class VerifactuFeatureTest extends TestCase
$xx->forceDelete(); $xx->forceDelete();
} }
public function testBuildInvoiceCancellation()
{
$invoice = $this->buildData();
$invoice->number = 'TEST0033343459';
$invoice->save();
$_inv = Invoice::factory()->create([
'user_id' => $invoice->user_id,
'company_id' => $invoice->company_id,
'client_id' => $invoice->client_id,
'date' => '2025-08-10',
'status_id' => Invoice::STATUS_SENT,
'uses_inclusive_taxes' => false,
]);
$xx = VerifactuLog::create([
'invoice_id' => $_inv->id,
'company_id' => $invoice->company_id,
'invoice_number' => 'TEST0033343459',
'date' => '2025-08-10',
'hash' => 'CEF610A3C24D4106ABE4A836C48B0F5251600F44EEE05A90EBD7185FA753553F',
'nif' => 'A39200019',
'previous_hash' => 'CEF610A3C24D4106ABE4A836C48B0F5251600F44EEE05A90EBD7185FA753553F',
]);
$verifactu = new Verifactu($invoice);
$document = (new RegistroAlta($invoice))->run()->getInvoice();
$huella = $this->cancellationHash($document, $xx->hash);
$cancellation = $document->createCancellation();
// $cancellation->setFechaHoraHusoGenRegistro('2025-08-09T23:57:25+00:00');
$cancellation->setHuella($huella);
$soapXml = $cancellation->toSoapEnvelope();
$response = Http::withHeaders([
'Content-Type' => 'text/xml; charset=utf-8',
'SOAPAction' => '',
])
->withOptions([
'cert' => storage_path('aeat-cert5.pem'),
'ssl_key' => storage_path('aeat-key5.pem'),
'verify' => false,
'timeout' => 30,
])
->withBody($soapXml, 'text/xml')
->post('https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP');
nlog('Request with AEAT official test data:');
nlog($soapXml);
nlog('Response with AEAT official test data:');
nlog('Response Status: ' . $response->status());
nlog('Response Headers: ' . json_encode($response->headers()));
nlog('Response Body: ' . $response->body());
$r = new ResponseProcessor();
$rx = $r->processResponse($response->body());
$this->assertTrue($rx['success']);
$xx->forceDelete();
}
private function cancellationHash($document, $huella)
{
$idEmisorFacturaAnulada = $document->getIdFactura()->getIdEmisorFactura();
$numSerieFacturaAnulada = $document->getIdFactura()->getNumSerieFactura();
$fechaExpedicionFacturaAnulada = $document->getIdFactura()->getFechaExpedicionFactura();
$fechaHoraHusoGenRegistro = $document->getFechaHoraHusoGenRegistro();
$hashInput = "IDEmisorFacturaAnulada={$idEmisorFacturaAnulada}&" .
"NumSerieFacturaAnulada={$numSerieFacturaAnulada}&" .
"FechaExpedicionFacturaAnulada={$fechaExpedicionFacturaAnulada}&" .
"Huella={$huella}&" .
"FechaHoraHusoGenRegistro={$fechaHoraHusoGenRegistro}";
nlog("Cancellation Huella: " . $hashInput);
return strtoupper(hash('sha256', $hashInput));
public function testBuildInvoiceCancellation() }
{
$invoice = $this->buildData();
$invoice->number = 'TEST0033343459';
$invoice->save();
$_inv = Invoice::factory()->create([
'user_id' => $invoice->user_id,
'company_id' => $invoice->company_id,
'client_id' => $invoice->client_id,
'date' => '2025-08-10',
'status_id' => Invoice::STATUS_SENT,
'uses_inclusive_taxes' => false,
]);
$xx = VerifactuLog::create([
'invoice_id' => $_inv->id,
'company_id' => $invoice->company_id,
'invoice_number' => 'TEST0033343459',
'date' => '2025-08-10',
'hash' => 'CEF610A3C24D4106ABE4A836C48B0F5251600F44EEE05A90EBD7185FA753553F',
'nif' => 'A39200019',
'previous_hash' => 'CEF610A3C24D4106ABE4A836C48B0F5251600F44EEE05A90EBD7185FA753553F',
]);
$verifactu = new Verifactu($invoice);
$document = (new RegistroAlta($invoice))->run()->getInvoice();
$huella = $this->cancellationHash($document, $xx->hash);
$cancellation = $document->createCancellation();
// $cancellation->setFechaHoraHusoGenRegistro('2025-08-09T23:57:25+00:00');
$cancellation->setHuella($huella);
$soapXml = $cancellation->toSoapEnvelope();
$response = Http::withHeaders([
'Content-Type' => 'text/xml; charset=utf-8',
'SOAPAction' => '',
])
->withOptions([
'cert' => storage_path('aeat-cert5.pem'),
'ssl_key' => storage_path('aeat-key5.pem'),
'verify' => false,
'timeout' => 30,
])
->withBody($soapXml, 'text/xml')
->post('https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP');
nlog('Request with AEAT official test data:');
nlog($soapXml);
nlog('Response with AEAT official test data:');
nlog('Response Status: ' . $response->status());
nlog('Response Headers: ' . json_encode($response->headers()));
nlog('Response Body: ' . $response->body());
$r = new ResponseProcessor();
$rx = $r->processResponse($response->body());
$this->assertTrue($rx['success']);
$xx->forceDelete();
}
private function cancellationHash($document, $huella)
{
$idEmisorFacturaAnulada = $document->getIdFactura()->getIdEmisorFactura();
$numSerieFacturaAnulada = $document->getIdFactura()->getNumSerieFactura();
$fechaExpedicionFacturaAnulada = $document->getIdFactura()->getFechaExpedicionFactura();
$fechaHoraHusoGenRegistro = $document->getFechaHoraHusoGenRegistro();
$hashInput = "IDEmisorFacturaAnulada={$idEmisorFacturaAnulada}&" .
"NumSerieFacturaAnulada={$numSerieFacturaAnulada}&" .
"FechaExpedicionFacturaAnulada={$fechaExpedicionFacturaAnulada}&" .
"Huella={$huella}&" .
"FechaHoraHusoGenRegistro={$fechaHoraHusoGenRegistro}";
nlog("Cancellation Huella: " . $hashInput);
return strtoupper(hash('sha256', $hashInput));
// $hashInput = "IDEmisorFacturaAnulada={$document->getIdFactura()->getIdEmisorFactura()}&" .
// "NumSerieFacturaAnulada={$document->getIdFactura()->getNumSerieFactura()}&" .
// "FechaExpedicionFacturaAnulada={$document->getIdFactura()->getFechaExpedicionFactura()}&" .
// "Huella={$blank_huella}&" .
// "FechaHoraHusoGenRegistro={$document->getFechaHoraHusoGenRegistro()}";
// return strtoupper(hash('sha256', $hashInput));
}
public function test_invoice_invoice_modification() public function test_invoice_invoice_modification()
{ {
$invoice = $this->buildData(); $invoice = $this->buildData();
$invoice->number = 'TEST0033343444'; $invoice->number = 'TEST0033343460-R2';
$invoice->save(); $invoice->save();
$previous_huella = 'C9D10B1EE60CEE114B67CDF07F23487239B2A04A697BE2C4F67AC934B0553CF5'; $previous_huella = '1FB6B4EF72DD2A07CC23B3F9D74EE5749C8E86B34B9B1DFFFC8C3E46ACA87E21';
$xx = VerifactuLog::create([
'invoice_id' => $invoice->id,
'company_id' => $invoice->company_id,
'invoice_number' => 'TEST0033343459',
'date' => '2025-08-10',
'hash' => $previous_huella,
'nif' => 'A39200019',
'previous_hash' => $previous_huella,
]);
$verifactu = new Verifactu($invoice); $verifactu = new Verifactu($invoice);
$old_document = $verifactu->setTestMode() $document = $verifactu->setTestMode()
->setPreviousHash($previous_huella) ->setPreviousHash($previous_huella)
->run() ->run()
->getInvoice(); ->getInvoice();
nlog($document->toSoapEnvelope());
$_verifactu = (new Verifactu($invoice))->setTestMode()->run(); $response = $verifactu->send($document->toSoapEnvelope());
$new_document = $_verifactu->getInvoice();
$new_document->setTipoFactura('R1');
$new_document->setTipoRectificativa('S');
// For substitutive rectifications (S), ImporteRectificacion is mandatory
$new_document->setImporteRectificacion(100.00);
// Set a proper description for the rectification operation
$new_document->setDescripcionOperacion('Rectificación por error en factura anterior');
// Debug: Log the description to ensure it's set
\Log::info('DescripcionOperacion set to: ' . $new_document->getDescripcionOperacion());
// Set up the rectified invoice information with proper amounts
// For R1 invoices, we need BaseRectificada and CuotaRectificada
$new_document->setRectifiedInvoice(
'A39200019', // NIF of rectified invoice
'TEST0033343443', // Series number of rectified invoice
'09-08-2025' // Date of rectified invoice
);
// Set the rectified amounts (BaseRectificada and CuotaRectificada)
// These represent the amounts from the original invoice that are being rectified
$new_document->setRectifiedAmounts(
200.00, // BaseRectificada - base amount from original invoice
42.00 // CuotaRectificada - tax amount from original invoice
);
$response = $_verifactu->send($new_document->toSoapEnvelope());
// $_document = InvoiceModification::createFromInvoice($old_document, $new_document);
// $response = $_verifactu->send($_document->toSoapEnvelope());
// Debug: Log the XML being sent
$xmlString = $new_document->toXmlString();
\Log::info('Generated XML for R1 invoice:', ['xml' => $xmlString]);
// Debug: Log the SOAP envelope being sent
$soapEnvelope = $new_document->toSoapEnvelope();
\Log::info('Generated SOAP envelope for R1 invoice:', ['soap' => $soapEnvelope]);
// Debug: Log the response to see what's happening
\Log::info('Verifactu R1 invoice test response:', $response);
$this->assertNotNull($response); $this->assertNotNull($response);
$this->assertArrayHasKey('success', $response); $this->assertArrayHasKey('success', $response);
$this->assertTrue($response['success']); $this->assertTrue($response['success']);
} }
public function test_raw() public function test_rectification_invoice()
{ {
$soapXml = <<<XML $soapXml = <<<XML
<?xml version="1.0" encoding="UTF-8"?> <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:sum="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroLR.xsd" xmlns:sum1="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd">
<soapenv:Envelope <soapenv:Header/>
xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" <soapenv:Body>
xmlns:sum="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroLR.xsd" <sum:RegFactuSistemaFacturacion>
xmlns:sum1="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd"> <sum:Cabecera>
<soapenv:Header/> <sum1:ObligadoEmision>
<soapenv:Body> <sum1:NombreRazon>CERTIFICADO FISICA PRUEBAS</sum1:NombreRazon>
<sum:RegFactuSistemaFacturacion> <sum1:NIF>A39200019</sum1:NIF>
<sum:Cabecera> </sum1:ObligadoEmision>
<sum1:ObligadoEmision> </sum:Cabecera>
<sum1:NombreRazon>CERTIFICADO FISICA PRUEBAS</sum1:NombreRazon> <sum:RegistroFactura>
<sum1:NIF>A39200019</sum1:NIF> <sum1:RegistroAlta>
</sum1:ObligadoEmision> <sum1:IDVersion>1.0</sum1:IDVersion>
</sum:Cabecera> <sum1:IDFactura>
<sum:RegistroFactura> <sum1:IDEmisorFactura>A39200019</sum1:IDEmisorFactura>
<sum1:RegistroAlta> <sum1:NumSerieFactura>TEST0033343460-R1</sum1:NumSerieFactura>
<sum1:IDVersion>1.0</sum1:IDVersion> <sum1:FechaExpedicionFactura>10-08-2025</sum1:FechaExpedicionFactura>
<sum1:IDFactura> </sum1:IDFactura>
<sum1:IDEmisorFactura>A39200019</sum1:IDEmisorFactura> <sum1:NombreRazonEmisor>CERTIFICADO FISICA PRUEBAS</sum1:NombreRazonEmisor>
<sum1:NumSerieFactura>TEST0033343444</sum1:NumSerieFactura> <sum1:TipoFactura>F3</sum1:TipoFactura>
<sum1:FechaExpedicionFactura>09-08-2025</sum1:FechaExpedicionFactura> <sum1:FacturasSustituidas>
</sum1:IDFactura> <sum1:IDFacturaSustituida>
<sum1:NombreRazonEmisor>CERTIFICADO FISICA PRUEBAS</sum1:NombreRazonEmisor> <sum1:IDEmisorFactura>A39200019</sum1:IDEmisorFactura>
<sum1:TipoFactura>R1</sum1:TipoFactura> <sum1:NumSerieFactura>TEST0033343460</sum1:NumSerieFactura>
<sum1:TipoRectificativa>S</sum1:TipoRectificativa> <sum1:FechaExpedicionFactura>10-08-2025</sum1:FechaExpedicionFactura>
<sum1:FacturasRectificadas> </sum1:IDFacturaSustituida>
<sum1:IDFacturaRectificada> </sum1:FacturasSustituidas>
<sum1:IDEmisorFactura>A39200019</sum1:IDEmisorFactura> <sum1:DescripcionOperacion>Alta</sum1:DescripcionOperacion>
<sum1:NumSerieFactura>TEST0033343443</sum1:NumSerieFactura> <sum1:Destinatarios>
<sum1:FechaExpedicionFactura>09-08-2025</sum1:FechaExpedicionFactura> <sum1:IDDestinatario>
</sum1:IDFacturaRectificada> <sum1:NombreRazon>CERTIFICADO FISICA PRUEBAS</sum1:NombreRazon>
</sum1:FacturasRectificadas> <sum1:NIF>A39200019</sum1:NIF>
<sum1:ImporteRectificacion> </sum1:IDDestinatario>
<sum1:BaseRectificada>100.00</sum1:BaseRectificada> </sum1:Destinatarios>
<sum1:CuotaRectificada>21.00</sum1:CuotaRectificada> <sum1:Desglose>
<sum1:CuotaRecargoRectificado>0.00</sum1:CuotaRecargoRectificado> <sum1:DetalleDesglose>
</sum1:ImporteRectificacion> <sum1:Impuesto>01</sum1:Impuesto>
<sum1:DescripcionOperacion>Rectificación por error en factura anterior</sum1:DescripcionOperacion> <sum1:ClaveRegimen>01</sum1:ClaveRegimen>
<sum1:Destinatarios> <sum1:CalificacionOperacion>S1</sum1:CalificacionOperacion>
<sum1:IDDestinatario> <sum1:TipoImpositivo>21.00</sum1:TipoImpositivo>
<sum1:NombreRazon>Test Recipient Company</sum1:NombreRazon> <sum1:BaseImponibleOimporteNoSujeto>100.00</sum1:BaseImponibleOimporteNoSujeto>
<sum1:NIF>A39200019</sum1:NIF> <sum1:CuotaRepercutida>21.00</sum1:CuotaRepercutida>
</sum1:IDDestinatario> </sum1:DetalleDesglose>
</sum1:Destinatarios> </sum1:Desglose>
<sum1:Desglose> <sum1:CuotaTotal>21</sum1:CuotaTotal>
<sum1:DetalleDesglose> <sum1:ImporteTotal>121</sum1:ImporteTotal>
<sum1:Impuesto>01</sum1:Impuesto> <sum1:Encadenamiento>
<sum1:ClaveRegimen>01</sum1:ClaveRegimen> <sum1:RegistroAnterior>
<sum1:CalificacionOperacion>S1</sum1:CalificacionOperacion> <sum1:IDEmisorFactura>A39200019</sum1:IDEmisorFactura>
<sum1:TipoImpositivo>21.00</sum1:TipoImpositivo> <sum1:NumSerieFactura>TEST0033343459</sum1:NumSerieFactura>
<sum1:BaseImponibleOimporteNoSujeto>97.00</sum1:BaseImponibleOimporteNoSujeto> <sum1:FechaExpedicionFactura>10-08-2025</sum1:FechaExpedicionFactura>
<sum1:CuotaRepercutida>20.37</sum1:CuotaRepercutida> <sum1:Huella>1FB6B4EF72DD2A07CC23B3F9D74EE5749C8E86B34B9B1DFFFC8C3E46ACA87E21</sum1:Huella>
</sum1:DetalleDesglose> </sum1:RegistroAnterior>
</sum1:Desglose> </sum1:Encadenamiento>
<sum1:CuotaTotal>47.05</sum1:CuotaTotal> <sum1:SistemaInformatico>
<sum1:ImporteTotal>144.05</sum1:ImporteTotal> <sum1:NombreRazon>CERTIFICADO FISICA PRUEBAS</sum1:NombreRazon>
<sum1:Encadenamiento> <sum1:NIF>A39200019</sum1:NIF>
<sum1:PrimerRegistro>S</sum1:PrimerRegistro> <sum1:NombreSistemaInformatico>InvoiceNinja</sum1:NombreSistemaInformatico>
</sum1:Encadenamiento> <sum1:IdSistemaInformatico>77</sum1:IdSistemaInformatico>
<sum1:SistemaInformatico> <sum1:Version>1.0.03</sum1:Version>
<sum1:NombreRazon>CERTIFICADO FISICA PRUEBAS</sum1:NombreRazon> <sum1:NumeroInstalacion>383</sum1:NumeroInstalacion>
<sum1:NIF>A39200019</sum1:NIF> <sum1:TipoUsoPosibleSoloVerifactu>N</sum1:TipoUsoPosibleSoloVerifactu>
<sum1:NombreSistemaInformatico>InvoiceNinja</sum1:NombreSistemaInformatico> <sum1:TipoUsoPosibleMultiOT>S</sum1:TipoUsoPosibleMultiOT>
<sum1:IdSistemaInformatico>77</sum1:IdSistemaInformatico> <sum1:IndicadorMultiplesOT>S</sum1:IndicadorMultiplesOT>
<sum1:Version>1.0.03</sum1:Version> </sum1:SistemaInformatico>
<sum1:NumeroInstalacion>383</sum1:NumeroInstalacion> <sum1:FechaHoraHusoGenRegistro>2025-08-10T05:02:18+00:00</sum1:FechaHoraHusoGenRegistro>
<sum1:TipoUsoPosibleSoloVerifactu>N</sum1:TipoUsoPosibleSoloVerifactu> <sum1:TipoHuella>01</sum1:TipoHuella>
<sum1:TipoUsoPosibleMultiOT>S</sum1:TipoUsoPosibleMultiOT> <sum1:Huella>BC61C7CB7CB09917C076CAE7D066B3E2CF521A3B8B501D0C83250B5EB4A4B40D</sum1:Huella>
<sum1:IndicadorMultiplesOT>S</sum1:IndicadorMultiplesOT> </sum1:RegistroAlta>
</sum1:SistemaInformatico> </sum:RegistroFactura>
<sum1:FechaHoraHusoGenRegistro>2025-08-09T23:18:44+02:00</sum1:FechaHoraHusoGenRegistro> </sum:RegFactuSistemaFacturacion>
<sum1:TipoHuella>01</sum1:TipoHuella> </soapenv:Body>
<sum1:Huella>E7558C33FE3496551F38FEB582F4879B1D9F6C073489628A8DC275E12298941F</sum1:Huella> </soapenv:Envelope>
</sum1:RegistroAlta>
</sum:RegistroFactura> XML;
</sum:RegFactuSistemaFacturacion>
</soapenv:Body>
</soapenv:Envelope>
XML;
$xslt = new VerifactuDocumentValidator($soapXml); $xslt = new VerifactuDocumentValidator($soapXml);
$xslt->validate(); $xslt->validate();
$errors = $xslt->getVerifactuErrors(); $errors = $xslt->getVerifactuErrors();
if(count($errors) > 0) { if(count($errors) > 0) {
nlog('Errors:'); nlog('Errors:');
nlog($errors); nlog($errors);
nlog('Errors:'); nlog('Errors:');
} }
$this->assertCount(0, $errors); $this->assertCount(0, $errors);
$response = Http::withHeaders([ $response = Http::withHeaders([
'Content-Type' => 'text/xml; charset=utf-8', 'Content-Type' => 'text/xml; charset=utf-8',
'SOAPAction' => '', 'SOAPAction' => '',
]) ])
->withOptions([ ->withOptions([
'cert' => storage_path('aeat-cert5.pem'), 'cert' => storage_path('aeat-cert5.pem'),
'ssl_key' => storage_path('aeat-key5.pem'), 'ssl_key' => storage_path('aeat-key5.pem'),
'verify' => false, 'verify' => false,
'timeout' => 30, 'timeout' => 30,
]) ])
->withBody($soapXml, 'text/xml') ->withBody($soapXml, 'text/xml')
->post('https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP'); ->post('https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP');
nlog('Request with AEAT official test data:'); nlog('Request with AEAT official test data:');
nlog($soapXml); nlog($soapXml);
nlog('Response with AEAT official test data:'); nlog('Response with AEAT official test data:');
nlog('Response Status: ' . $response->status()); nlog('Response Status: ' . $response->status());
nlog('Response Headers: ' . json_encode($response->headers())); nlog('Response Headers: ' . json_encode($response->headers()));
nlog('Response Body: ' . $response->body()); nlog('Response Body: ' . $response->body());
$r = new ResponseProcessor(); $r = new ResponseProcessor();
$rx = $r->processResponse($response->body()); $rx = $r->processResponse($response->body());
nlog($rx); nlog($rx);
}
}
public function testInvoiceCancellation() public function testInvoiceCancellation()
@ -654,7 +607,15 @@ nlog($rx);
// Set up rectification details exactly as in the expected XML // Set up rectification details exactly as in the expected XML
$invoice->setRectifiedInvoice('A39200019', 'TEST0033343443', '09-08-2025'); $invoice->setRectifiedInvoice('A39200019', 'TEST0033343443', '09-08-2025');
$invoice->setRectificationAmounts(100.00, 21.00, 0.00);
$importeRectificacion = [
'BaseRectificada' => 100.00,
'CuotaRectificada' => 21.00,
'CuotaRecargoRectificado' => 0.00
];
$invoice->setRectificationAmounts($importeRectificacion);
// Set up desglose exactly as in the expected XML // Set up desglose exactly as in the expected XML
$desglose = new Desglose(); $desglose = new Desglose();

View File

@ -1,671 +0,0 @@
<?php
namespace Tests\Feature\EInvoice\Verifactu;
use Tests\TestCase;
use App\Services\EDocument\Standards\Verifactu\Models\Invoice;
use App\Services\EDocument\Standards\Verifactu\Models\Desglose;
use App\Services\EDocument\Standards\Verifactu\Models\Encadenamiento;
use App\Services\EDocument\Standards\Validation\XsltDocumentValidator;
use App\Services\EDocument\Standards\Verifactu\Models\RegistroAnulacion;
use App\Services\EDocument\Standards\Verifactu\Models\SistemaInformatico;
use App\Services\EDocument\Standards\Verifactu\Models\InvoiceModification;
use App\Services\EDocument\Standards\Verifactu\Models\RegistroModificacion;
use App\Services\EDocument\Standards\Verifactu\Models\PersonaFisicaJuridica;
class VerifactuModificationTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
}
public function test_can_create_registro_anulacion()
{
$cancellation = new RegistroAnulacion();
$cancellation
->setIdEmisorFactura('99999910G')
->setNumSerieFactura('TEST0033343436')
->setFechaExpedicionFactura('02-07-2025')
->setMotivoAnulacion('1');
$this->assertEquals('99999910G', $cancellation->getIdEmisorFactura());
$this->assertEquals('TEST0033343436', $cancellation->getNumSerieFactura());
$this->assertEquals('02-07-2025', $cancellation->getFechaExpedicionFactura());
$this->assertEquals('1', $cancellation->getMotivoAnulacion());
$xml = $cancellation->toXmlString();
$this->assertStringContainsString('RegistroAnulacion', $xml);
$this->assertStringContainsString('99999910G', $xml);
$this->assertStringContainsString('TEST0033343436', $xml);
$this->assertStringContainsString('02-07-2025', $xml);
$this->assertStringContainsString('1', $xml);
$validXml = $cancellation->toXmlString();
// Use the new VerifactuDocumentValidator
$validator = new \App\Services\EDocument\Standards\Validation\VerifactuDocumentValidator($validXml);
$validator->validate();
$errors = $validator->getVerifactuErrors();
if (!empty($errors)) {
nlog('Verifactu Validation Errors:');
nlog($validXml);
nlog($errors);
}
}
public function test_can_create_registro_modificacion()
{
$modification = new RegistroModificacion();
$modification
->setIdVersion('1.0')
->setIdFactura((new \App\Services\EDocument\Standards\Verifactu\Models\IDFactura())
->setIdEmisorFactura('99999910G')
->setNumSerieFactura('TEST0033343436')
->setFechaExpedicionFactura('01-01-2025'))
->setNombreRazonEmisor('CERTIFICADO FISICA PRUEBAS')
->setTipoFactura('F1')
->setDescripcionOperacion('Test invoice modification')
->setCuotaTotal(21.00)
->setImporteTotal(121.00)
->setFechaHoraHusoGenRegistro('2025-01-02T12:00:00')
->setTipoHuella('01')
->setHuella('TEST_HASH');
// Add sistema informatico
$sistema = new SistemaInformatico();
$sistema
->setNombreRazon('Sistema de Facturación')
->setNif('A39200019')
->setNombreSistemaInformatico('InvoiceNinja')
->setIdSistemaInformatico('77')
->setVersion('1.0.03')
->setNumeroInstalacion('383');
$modification->setSistemaInformatico($sistema);
// Add desglose
$desglose = new Desglose();
$desglose->setDesgloseFactura([
'Impuesto' => '01',
'ClaveRegimen' => '01',
'CalificacionOperacion' => 'S1',
'TipoImpositivo' => '21',
'BaseImponibleOimporteNoSujeto' => '100.00',
'CuotaRepercutida' => '21.00'
]);
$modification->setDesglose($desglose);
// Add encadenamiento
$encadenamiento = new Encadenamiento();
$encadenamiento->setPrimerRegistro('S');
$modification->setEncadenamiento($encadenamiento);
$this->assertEquals('1.0', $modification->getIdVersion());
$this->assertEquals('TEST0033343436', $modification->getIdFactura()->getNumSerieFactura());
$this->assertEquals('CERTIFICADO FISICA PRUEBAS', $modification->getNombreRazonEmisor());
$this->assertEquals('F1', $modification->getTipoFactura());
$this->assertEquals(21.00, $modification->getCuotaTotal());
$this->assertEquals(121.00, $modification->getImporteTotal());
$xml = $modification->toXmlString();
$this->assertStringContainsString('RegistroModificacion', $xml);
$this->assertStringContainsString('TEST0033343436', $xml);
$this->assertStringContainsString('CERTIFICADO FISICA PRUEBAS', $xml);
$this->assertStringContainsString('21', $xml);
$this->assertStringContainsString('121', $xml);
// Use the new VerifactuDocumentValidator
$validator = new \App\Services\EDocument\Standards\Validation\VerifactuDocumentValidator($xml);
$validator->validate();
$errors = $validator->getVerifactuErrors();
if (!empty($errors)) {
nlog('Verifactu Validation Errors:');
nlog($xml);
nlog($errors);
}
}
public function test_can_create_invoice_modification_from_invoices()
{
// Create original invoice
$originalInvoice = new Invoice();
$originalInvoice
->setIdVersion('1.0')
->setIdFactura((new \App\Services\EDocument\Standards\Verifactu\Models\IDFactura())
->setIdEmisorFactura('99999910G')
->setNumSerieFactura('TEST0033343436')
->setFechaExpedicionFactura('01-01-2025'))
->setNombreRazonEmisor('Original Company')
->setTipoFactura('F1')
->setDescripcionOperacion('Original invoice')
->setCuotaTotal(21.00)
->setImporteTotal(121.00)
->setFechaHoraHusoGenRegistro('2025-01-01T12:00:00')
->setTipoHuella('01')
->setHuella('ORIGINAL_HASH');
// Add emitter to original invoice
$emisor = new PersonaFisicaJuridica();
$emisor
->setNif('99999910G')
->setRazonSocial('Original Company');
$originalInvoice->setTercero($emisor);
// Add sistema informatico to original invoice
$sistema = new SistemaInformatico();
$sistema
->setNombreRazon('Sistema de Facturación')
->setNif('A39200019')
->setNombreSistemaInformatico('InvoiceNinja')
->setIdSistemaInformatico('77')
->setVersion('1.0.03')
->setNumeroInstalacion('383');
$originalInvoice->setSistemaInformatico($sistema);
// Create modified invoice
$modifiedInvoice = new Invoice();
$modifiedInvoice
->setIdVersion('1.0')
->setIdFactura((new \App\Services\EDocument\Standards\Verifactu\Models\IDFactura())
->setIdEmisorFactura('99999910G')
->setNumSerieFactura('TEST0033343436')
->setFechaExpedicionFactura('02-01-2025'))
->setNombreRazonEmisor('Modified Company')
->setTipoFactura('F1')
->setDescripcionOperacion('Modified invoice')
->setCuotaTotal(42.00)
->setImporteTotal(242.00)
->setFechaHoraHusoGenRegistro('2025-01-02T12:00:00')
->setTipoHuella('01')
->setHuella('MODIFIED_HASH');
// Add emitter to modified invoice
$emisorModificado = new PersonaFisicaJuridica();
$emisorModificado
->setNif('99999910G')
->setRazonSocial('Modified Company');
$modifiedInvoice->setTercero($emisorModificado);
// Add sistema informatico to modified invoice
$modifiedInvoice->setSistemaInformatico($sistema);
// Create modification
$modification = InvoiceModification::createFromInvoice($originalInvoice, $modifiedInvoice);
$this->assertInstanceOf(InvoiceModification::class, $modification);
$this->assertInstanceOf(RegistroAnulacion::class, $modification->getRegistroAnulacion());
$this->assertInstanceOf(RegistroModificacion::class, $modification->getRegistroModificacion());
// Test cancellation record
$cancellation = $modification->getRegistroAnulacion();
$this->assertEquals('99999910G', $cancellation->getIdEmisorFactura());
$this->assertEquals('TEST0033343436', $cancellation->getNumSerieFactura());
$this->assertEquals('1', $cancellation->getMotivoAnulacion());
// Test modification record
$modificationRecord = $modification->getRegistroModificacion();
$this->assertEquals('Modified Company', $modificationRecord->getNombreRazonEmisor());
$this->assertEquals(42.00, $modificationRecord->getCuotaTotal());
$this->assertEquals(242.00, $modificationRecord->getImporteTotal());
$validXml = $modification->toSoapEnvelope();
// Use the new VerifactuDocumentValidator
$validator = new \App\Services\EDocument\Standards\Validation\VerifactuDocumentValidator($validXml);
$validator->validate();
$errors = $validator->getVerifactuErrors();
if (!empty($errors)) {
nlog('Verifactu Validation Errors:');
nlog($validXml);
nlog($errors);
}
// Now that validation is working correctly, we can assert no errors
$this->assertCount(0, $errors);
}
public function test_can_generate_modification_soap_envelope()
{
// Create original invoice
$originalInvoice = new Invoice();
$originalInvoice
->setIdVersion('1.0')
->setIdFactura((new \App\Services\EDocument\Standards\Verifactu\Models\IDFactura())
->setIdEmisorFactura('99999910G')
->setNumSerieFactura('TEST0033343436')
->setFechaExpedicionFactura('01-01-2025'))
->setNombreRazonEmisor('Original Company')
->setTipoFactura('F1')
->setDescripcionOperacion('Original invoice')
->setCuotaTotal(21.00)
->setImporteTotal(121.00)
->setFechaHoraHusoGenRegistro('2025-01-01T12:00:00')
->setTipoHuella('01')
->setHuella('ORIGINAL_HASH');
// Add emitter to original invoice
$emisor = new PersonaFisicaJuridica();
$emisor
->setNif('99999910G')
->setRazonSocial('Original Company');
$originalInvoice->setTercero($emisor);
// Add sistema informatico to original invoice
$sistema = new SistemaInformatico();
$sistema
->setNombreRazon('Sistema de Facturación')
->setNif('A39200019')
->setNombreSistemaInformatico('InvoiceNinja')
->setIdSistemaInformatico('77')
->setVersion('1.0.03')
->setNumeroInstalacion('383');
$originalInvoice->setSistemaInformatico($sistema);
// Create modified invoice
$modifiedInvoice = new Invoice();
$modifiedInvoice
->setIdVersion('1.0')
->setIdFactura((new \App\Services\EDocument\Standards\Verifactu\Models\IDFactura())
->setIdEmisorFactura('99999910G')
->setNumSerieFactura('TEST0033343436')
->setFechaExpedicionFactura('02-01-2025'))
->setNombreRazonEmisor('Modified Company')
->setTipoFactura('F1')
->setDescripcionOperacion('Modified invoice')
->setCuotaTotal(42.00)
->setImporteTotal(242.00)
->setFechaHoraHusoGenRegistro('2025-01-02T12:00:00')
->setTipoHuella('01')
->setHuella('MODIFIED_HASH');
// Add emitter to modified invoice
$emisorModificado = new PersonaFisicaJuridica();
$emisorModificado
->setNif('99999910G')
->setRazonSocial('Modified Company');
$modifiedInvoice->setTercero($emisorModificado);
// Add sistema informatico to modified invoice
$modifiedInvoice->setSistemaInformatico($sistema);
// Create modification
$modification = InvoiceModification::createFromInvoice($originalInvoice, $modifiedInvoice);
// Generate SOAP envelope
$soapXml = $modification->toSoapEnvelope();
$this->assertStringContainsString('soapenv:Envelope', $soapXml);
$this->assertStringContainsString('sum:RegFactuSistemaFacturacion', $soapXml);
$this->assertStringContainsString('sum1:RegistroAlta', $soapXml);
$this->assertStringContainsString('sum1:TipoFactura>R1</sum1:TipoFactura>', $soapXml);
$this->assertStringContainsString('sum1:TipoRectificativa>S</sum1:TipoRectificativa>', $soapXml);
$this->assertStringContainsString('TEST0033343436', $soapXml);
$this->assertStringContainsString('Modified invoice', $soapXml);
$this->assertStringContainsString('42', $soapXml);
$this->assertStringContainsString('242', $soapXml);
// Verify that TipoRectificativa is present for R1 invoices
$this->assertStringContainsString('sum1:TipoRectificativa>S</sum1:TipoRectificativa>', $soapXml);
$validXml = $modification->toSoapEnvelope();
// Use the new VerifactuDocumentValidator
$validator = new \App\Services\EDocument\Standards\Validation\VerifactuDocumentValidator($validXml);
$validator->validate();
$errors = $validator->getVerifactuErrors();
if (!empty($errors)) {
nlog('Verifactu Validation Errors:');
nlog($validXml);
nlog($errors);
}
}
public function test_tipo_rectificativa_only_added_for_r1_invoices()
{
// Create original invoice
$originalInvoice = new Invoice();
$originalInvoice
->setIdVersion('1.0')
->setIdFactura((new \App\Services\EDocument\Standards\Verifactu\Models\IDFactura())
->setIdEmisorFactura('99999910G')
->setNumSerieFactura('TEST0033343436')
->setFechaExpedicionFactura('01-01-2025'))
->setNombreRazonEmisor('Original Company')
->setTipoFactura('F1')
->setDescripcionOperacion('Original invoice')
->setCuotaTotal(21.00)
->setImporteTotal(121.00)
->setFechaHoraHusoGenRegistro('2025-01-01T12:00:00')
->setTipoHuella('01')
->setHuella('ORIGINAL_HASH');
// Add emitter to original invoice
$emisor = new PersonaFisicaJuridica();
$emisor
->setNif('99999910G')
->setRazonSocial('Original Company');
$originalInvoice->setTercero($emisor);
// Create modified invoice with F1 type (not R1)
$modifiedInvoice = new Invoice();
$modifiedInvoice
->setIdVersion('1.0')
->setIdFactura((new \App\Services\EDocument\Standards\Verifactu\Models\IDFactura())
->setIdEmisorFactura('99999910G')
->setNumSerieFactura('TEST0033343436')
->setFechaExpedicionFactura('02-01-2025'))
->setNombreRazonEmisor('Modified Company')
->setTipoFactura('F1') // F1 instead of R1
->setDescripcionOperacion('Modified invoice')
->setCuotaTotal(42.00)
->setImporteTotal(242.00)
->setFechaHoraHusoGenRegistro('2025-01-02T12:00:00')
->setTipoHuella('01')
->setHuella('MODIFIED_HASH');
// Create modification
$modification = InvoiceModification::createFromInvoice($originalInvoice, $modifiedInvoice);
// Generate SOAP envelope
$soapXml = $modification->toSoapEnvelope();
// For InvoiceModification, TipoFactura is always R1 and TipoRectificativa is always S
// This is because InvoiceModification is specifically for rectifying invoices
$this->assertStringContainsString('sum1:TipoFactura>R1</sum1:TipoFactura>', $soapXml);
$this->assertStringContainsString('sum1:TipoRectificativa>S</sum1:TipoRectificativa>', $soapXml);
// Verify that the original F1 type from modifiedInvoice is not used in the SOAP envelope
// because InvoiceModification always converts to R1
$this->assertStringNotContainsString('sum1:TipoFactura>F1</sum1:TipoFactura>', $soapXml);
}
public function test_invoice_can_create_modification()
{
// Create original invoice
$originalInvoice = new Invoice();
$originalInvoice
->setIdVersion('1.0')
->setIdFactura((new \App\Services\EDocument\Standards\Verifactu\Models\IDFactura())
->setIdEmisorFactura('99999910G')
->setNumSerieFactura('TEST0033343436')
->setFechaExpedicionFactura('01-01-2025'))
->setNombreRazonEmisor('Original Company')
->setTipoFactura('F1')
->setDescripcionOperacion('Original invoice')
->setCuotaTotal(21.00)
->setImporteTotal(121.00)
->setFechaHoraHusoGenRegistro('2025-01-01T12:00:00')
->setTipoHuella('01')
->setHuella('ORIGINAL_HASH');
// Add emitter to original invoice
$emisor = new PersonaFisicaJuridica();
$emisor
->setNif('99999910G')
->setRazonSocial('Original Company');
$originalInvoice->setTercero($emisor);
// Add sistema informatico to original invoice
$sistema = new SistemaInformatico();
$sistema
->setNombreRazon('Sistema de Facturación')
->setNif('A39200019')
->setNombreSistemaInformatico('InvoiceNinja')
->setIdSistemaInformatico('77')
->setVersion('1.0.03')
->setNumeroInstalacion('383');
$originalInvoice->setSistemaInformatico($sistema);
// Create modified invoice
$modifiedInvoice = new Invoice();
$modifiedInvoice
->setIdVersion('1.0')
->setIdFactura((new \App\Services\EDocument\Standards\Verifactu\Models\IDFactura())
->setIdEmisorFactura('99999910G')
->setNumSerieFactura('TEST0033343436')
->setFechaExpedicionFactura('02-01-2025'))
->setNombreRazonEmisor('Modified Company')
->setTipoFactura('F1')
->setDescripcionOperacion('Modified invoice')
->setCuotaTotal(42.00)
->setImporteTotal(242.00)
->setFechaHoraHusoGenRegistro('2025-01-02T12:00:00')
->setTipoHuella('01')
->setHuella('MODIFIED_HASH');
// Add emitter to modified invoice
$emisorModificado = new PersonaFisicaJuridica();
$emisorModificado
->setNif('99999910G')
->setRazonSocial('Modified Company');
$modifiedInvoice->setTercero($emisorModificado);
// Add sistema informatico to modified invoice
$modifiedInvoice->setSistemaInformatico($sistema);
// Create modification using the invoice method
$modification = $originalInvoice->createModification($modifiedInvoice);
$this->assertInstanceOf(InvoiceModification::class, $modification);
// Test cancellation record
$cancellation = $modification->getRegistroAnulacion();
$this->assertEquals('99999910G', $cancellation->getIdEmisorFactura());
$this->assertEquals('TEST0033343436', $cancellation->getNumSerieFactura());
$this->assertEquals('1', $cancellation->getMotivoAnulacion());
// Test modification record
$modificationRecord = $modification->getRegistroModificacion();
$this->assertEquals('Modified Company', $modificationRecord->getNombreRazonEmisor());
$this->assertEquals(42.00, $modificationRecord->getCuotaTotal());
$this->assertEquals(242.00, $modificationRecord->getImporteTotal());
$validXml = $modification->toSoapEnvelope();
// Use the new VerifactuDocumentValidator
$validator = new \App\Services\EDocument\Standards\Validation\VerifactuDocumentValidator($validXml);
$validator->validate();
$errors = $validator->getVerifactuErrors();
if (!empty($errors)) {
nlog('Verifactu Validation Errors:');
nlog($validXml);
nlog($errors);
}
}
public function test_invoice_can_create_cancellation()
{
$invoice = new Invoice();
$invoice
->setIdVersion('1.0')
->setIdFactura((new \App\Services\EDocument\Standards\Verifactu\Models\IDFactura())
->setIdEmisorFactura('99999910G')
->setNumSerieFactura('TEST0033343436')
->setFechaExpedicionFactura('01-01-2025'))
->setNombreRazonEmisor('Test Company')
->setTipoFactura('F1')
->setDescripcionOperacion('Test invoice')
->setCuotaTotal(21.00)
->setImporteTotal(121.00)
->setFechaHoraHusoGenRegistro('2025-01-01T12:00:00')
->setTipoHuella('01')
->setHuella('TEST_HASH');
// Add emitter
$emisor = new PersonaFisicaJuridica();
$emisor
->setNif('99999910G')
->setRazonSocial('Test Company');
$invoice->setTercero($emisor);
$cancellation = $invoice->createCancellation();
$this->assertInstanceOf(RegistroAnulacion::class, $cancellation);
$this->assertEquals('99999910G', $cancellation->getIdEmisorFactura());
$this->assertEquals('TEST0033343436', $cancellation->getNumSerieFactura());
$this->assertEquals('1', $cancellation->getMotivoAnulacion());
}
public function test_invoice_can_create_modification_record()
{
$invoice = new Invoice();
$invoice
->setIdVersion('1.0')
->setIdFactura((new \App\Services\EDocument\Standards\Verifactu\Models\IDFactura())
->setIdEmisorFactura('99999910G')
->setNumSerieFactura('TEST0033343436')
->setFechaExpedicionFactura('01-01-2025'))
->setNombreRazonEmisor('Test Company')
->setTipoFactura('F1')
->setDescripcionOperacion('Test invoice')
->setCuotaTotal(21.00)
->setImporteTotal(121.00)
->setFechaHoraHusoGenRegistro('2025-01-01T12:00:00')
->setTipoHuella('01')
->setHuella('TEST_HASH');
// Add emitter
$emisor = new PersonaFisicaJuridica();
$emisor
->setNif('99999910G')
->setRazonSocial('Test Company');
$invoice->setTercero($emisor);
// Add sistema informatico
$sistema = new SistemaInformatico();
$sistema
->setNombreRazon('Sistema de Facturación')
->setNif('A39200019')
->setNombreSistemaInformatico('InvoiceNinja')
->setIdSistemaInformatico('77')
->setVersion('1.0.03')
->setNumeroInstalacion('383');
$invoice->setSistemaInformatico($sistema);
$modificationRecord = $invoice->createModificationRecord();
$this->assertInstanceOf(RegistroModificacion::class, $modificationRecord);
$this->assertEquals('1.0', $modificationRecord->getIdVersion());
$this->assertEquals('TEST0033343436', $modificationRecord->getIdFactura());
$this->assertEquals('Test Company', $modificationRecord->getNombreRazonEmisor());
$this->assertEquals('F1', $modificationRecord->getTipoFactura());
$this->assertEquals(21.00, $modificationRecord->getCuotaTotal());
$this->assertEquals(121.00, $modificationRecord->getImporteTotal());
}
public function test_modification_xml_structure_matches_aeat_requirements()
{
// Create original invoice
$originalInvoice = new Invoice();
$originalInvoice
->setIdVersion('1.0')
->setIdFactura((new \App\Services\EDocument\Standards\Verifactu\Models\IDFactura())
->setIdEmisorFactura('99999910G')
->setNumSerieFactura('TEST0033343436')
->setFechaExpedicionFactura('01-01-2025'))
->setNombreRazonEmisor('Original Company')
->setTipoFactura('F1')
->setDescripcionOperacion('Original invoice')
->setCuotaTotal(21.00)
->setImporteTotal(121.00)
->setFechaHoraHusoGenRegistro('2025-01-01T12:00:00')
->setTipoHuella('01')
->setHuella('ORIGINAL_HASH');
// Add emitter to original invoice
$emisor = new PersonaFisicaJuridica();
$emisor
->setNif('99999910G')
->setRazonSocial('Original Company');
$originalInvoice->setTercero($emisor);
// Add sistema informatico to original invoice
$sistema = new SistemaInformatico();
$sistema
->setNombreRazon('Sistema de Facturación')
->setNif('A39200019')
->setNombreSistemaInformatico('InvoiceNinja')
->setIdSistemaInformatico('77')
->setVersion('1.0.03')
->setNumeroInstalacion('383');
$originalInvoice->setSistemaInformatico($sistema);
// Create modified invoice
$modifiedInvoice = new Invoice();
$modifiedInvoice
->setIdVersion('1.0')
->setIdFactura((new \App\Services\EDocument\Standards\Verifactu\Models\IDFactura())
->setIdEmisorFactura('99999910G')
->setNumSerieFactura('TEST0033343436')
->setFechaExpedicionFactura('02-01-2025'))
->setNombreRazonEmisor('Modified Company')
->setTipoFactura('F1')
->setDescripcionOperacion('Modified invoice')
->setCuotaTotal(42.00)
->setImporteTotal(242.00)
->setFechaHoraHusoGenRegistro('2025-01-02T12:00:00')
->setTipoHuella('01')
->setHuella('MODIFIED_HASH');
// Add emitter to modified invoice
$emisorModificado = new PersonaFisicaJuridica();
$emisorModificado
->setNif('99999910G')
->setRazonSocial('Modified Company');
$modifiedInvoice->setTercero($emisorModificado);
// Add sistema informatico to modified invoice
$modifiedInvoice->setSistemaInformatico($sistema);
// Create modification
$modification = InvoiceModification::createFromInvoice($originalInvoice, $modifiedInvoice);
// Generate SOAP envelope
$soapXml = $modification->toSoapEnvelope();
nlog($soapXml);
// Verify the XML structure matches AEAT requirements
$this->assertStringContainsString('<soapenv:Envelope', $soapXml);
$this->assertStringContainsString('<soapenv:Header', $soapXml);
$this->assertStringContainsString('<soapenv:Body', $soapXml);
$this->assertStringContainsString('<lr:RegFactuSistemaFacturacion', $soapXml);
$this->assertStringContainsString('<si:RegistroAlta', $soapXml);
$this->assertStringContainsString('<si:TipoFactura>R1</si:TipoFactura>', $soapXml);
// Verify modification structure (no cancellation block needed)
$this->assertStringContainsString('<si:DescripcionOperacion>Modified invoice</si:DescripcionOperacion>', $soapXml);
$this->assertStringContainsString('<si:ImporteTotal>242</si:ImporteTotal>', $soapXml);
$this->assertStringContainsString('<si:CuotaTotal>42</si:CuotaTotal>', $soapXml);
$validXml = $modification->toSoapEnvelope();
// Use the new VerifactuDocumentValidator
$validator = new \App\Services\EDocument\Standards\Validation\VerifactuDocumentValidator($validXml);
$validator->validate();
$errors = $validator->getVerifactuErrors();
if (!empty($errors)) {
nlog('Verifactu Validation Errors:');
nlog($validXml);
nlog($errors);
}
}
}

View File

@ -12,7 +12,6 @@ use App\Services\EDocument\Standards\Verifactu\Models\Encadenamiento;
use App\Services\EDocument\Standards\Verifactu\Models\SistemaInformatico; use App\Services\EDocument\Standards\Verifactu\Models\SistemaInformatico;
use App\Services\EDocument\Standards\Verifactu\ResponseProcessor; use App\Services\EDocument\Standards\Verifactu\ResponseProcessor;
use App\Services\EDocument\Standards\Verifactu\Models\PersonaFisicaJuridica; use App\Services\EDocument\Standards\Verifactu\Models\PersonaFisicaJuridica;
use App\Services\EDocument\Standards\Verifactu\Models\InvoiceModification;
class WSTest extends TestCase class WSTest extends TestCase
@ -392,166 +391,6 @@ $invoice->setDestinatarios($destinatarios);
} }
//@todo - need to test that cancelling an invoice works.
public function test_cancel_existing_invoice()
{
//@todo - need to test that cancelling an invoice works.
}
//@todo - Need to test that modifying an invoice works.
public function test_cancel_and_modify_existing_invoice()
{
$currentTimestamp = now()->setTimezone('Europe/Madrid')->format('Y-m-d\TH:i:s');
$invoice_number = 'TEST0033343436';
$invoice_date = '02-07-2025';
$nif = '99999910G';
// Create original invoice (the one to be cancelled)
$originalInvoice = new Invoice();
$originalInvoice
->setIdVersion('1.0')
->setIdFactura($invoice_number)
->setNombreRazonEmisor('Original Company')
->setTipoFactura('F1')
->setDescripcionOperacion('Original invoice')
->setCuotaTotal(21.00)
->setImporteTotal(121.00)
->setFechaHoraHusoGenRegistro($currentTimestamp)
->setTipoHuella('01')
->setHuella('ORIGINAL_HASH');
// Add emitter to original invoice
$emisor = new PersonaFisicaJuridica();
$emisor
->setNif($nif)
->setRazonSocial('Original Company');
$originalInvoice->setTercero($emisor);
// Add sistema informatico to original invoice
$sistema = new SistemaInformatico();
$sistema
->setNombreRazon('Sistema de Facturación')
->setNif('A39200019')
->setNombreSistemaInformatico('InvoiceNinja')
->setIdSistemaInformatico('77')
->setVersion('1.0.03')
->setNumeroInstalacion('383');
$originalInvoice->setSistemaInformatico($sistema);
// Create modified invoice (the replacement)
$modifiedInvoice = new Invoice();
$modifiedInvoice
->setIdVersion('1.0')
->setIdFactura($invoice_number)
->setNombreRazonEmisor('CERTIFICADO FISICA PRUEBAS')
->setTipoFactura('F1')
->setDescripcionOperacion('Test invoice submitted by computer system on behalf of business')
->setCuotaTotal(21.00)
->setImporteTotal(121.00)
->setFechaHoraHusoGenRegistro($currentTimestamp)
->setTipoHuella('01')
->setHuella('PLACEHOLDER_HUELLA');
// Add emitter to modified invoice
$emisorModificado = new PersonaFisicaJuridica();
$emisorModificado
->setNif($nif)
->setRazonSocial('CERTIFICADO FISICA PRUEBAS');
$modifiedInvoice->setTercero($emisorModificado);
// Add sistema informatico to modified invoice
$modifiedInvoice->setSistemaInformatico($sistema);
// Add destinatarios to modified invoice
$destinatario = new PersonaFisicaJuridica();
$destinatario
->setNombreRazon('Test Recipient Company')
->setNif('A39200019');
$modifiedInvoice->setDestinatarios([$destinatario]);
// Add desglose to modified invoice
$desglose = new Desglose();
$desglose->setDesgloseFactura([
'Impuesto' => '01',
'ClaveRegimen' => '01',
'CalificacionOperacion' => 'S1',
'TipoImpositivo' => '21',
'BaseImponibleOimporteNoSujeto' => '100.00',
'CuotaRepercutida' => '21.00'
]);
$modifiedInvoice->setDesglose($desglose);
// Add encadenamiento to modified invoice
$encadenamiento = new Encadenamiento();
$encadenamiento->setPrimerRegistro('S');
$modifiedInvoice->setEncadenamiento($encadenamiento);
// Create modification using the new models
$modification = InvoiceModification::createFromInvoice($originalInvoice, $modifiedInvoice);
// Calculate the correct hash using AEAT's specified format
$correctHash = $this->calculateVerifactuHash(
$nif, // IDEmisorFactura
$invoice_number, // NumSerieFactura
$invoice_date, // FechaExpedicionFactura
'F1', // TipoFactura
'21.00', // CuotaTotal
'121.00', // ImporteTotal
'', // Huella (empty for first calculation)
$currentTimestamp // FechaHoraHusoGenRegistro (current time)
);
// Update the modification record with the correct hash
$modification->getRegistroModificacion()->setHuella($correctHash);
nlog('Calculated hash for XML: ' . $correctHash);
// Generate SOAP envelope
$soapXml = $modification->toSoapEnvelope();
// Sign the XML before sending
$certPath = storage_path('aeat-cert5.pem');
$keyPath = storage_path('aeat-key5.pem');
$signingService = new \App\Services\EDocument\Standards\Verifactu\Signing\SigningService($soapXml, file_get_contents($keyPath), file_get_contents($certPath));
$soapXml = $signingService->sign();
// Try direct HTTP approach instead of SOAP client
$response = Http::withHeaders([
'Content-Type' => 'text/xml; charset=utf-8',
'SOAPAction' => '',
])
->withOptions([
'cert' => storage_path('aeat-cert5.pem'),
'ssl_key' => storage_path('aeat-key5.pem'),
'verify' => false,
'timeout' => 30,
])
->withBody($soapXml, 'text/xml')
->post('https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP');
nlog('Request with AEAT official test data:');
nlog($soapXml);
nlog('Response with AEAT official test data:');
nlog('Response Status: ' . $response->status());
nlog('Response Headers: ' . json_encode($response->headers()));
nlog('Response Body: ' . $response->body());
if (!$response->successful()) {
\Log::error('Request failed with status: ' . $response->status());
\Log::error('Response body: ' . $response->body());
}
$this->assertTrue($response->successful());
$responseProcessor = new ResponseProcessor();
$responseProcessor->processResponse($response->body());
nlog($responseProcessor->getSummary());
$this->assertTrue($responseProcessor->getSummary()['success']);
}
/** /**
* Calculate Verifactu hash using AEAT's specified format * Calculate Verifactu hash using AEAT's specified format
* Based on AEAT response showing the exact format they use * Based on AEAT response showing the exact format they use