Updates for Verifactu
This commit is contained in:
parent
c02c87765b
commit
8a137329d4
|
|
@ -104,16 +104,11 @@ class VerifactuDocumentValidator extends XsltDocumentValidator
|
|||
$registroAlta = $xpath->query('//si:RegistroAlta | //sum1:RegistroAlta');
|
||||
if ($registroAlta->length > 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';
|
||||
}
|
||||
}
|
||||
|
||||
// Check for RegistroModificacion structure (legacy)
|
||||
$registroModificacion = $xpath->query('//si:RegistroModificacion | //sum1:RegistroModificacion');
|
||||
if ($registroModificacion->length > 0) {
|
||||
return 'modification';
|
||||
}
|
||||
|
||||
// Check for cancellation structure
|
||||
$registroAnulacion = $xpath->query('//si:RegistroAnulacion | //sum1:RegistroAnulacion');
|
||||
|
|
@ -192,7 +187,7 @@ class VerifactuDocumentValidator extends XsltDocumentValidator
|
|||
if ($tipoFactura === false || $tipoFactura->length === 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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,11 +24,9 @@ use App\Helpers\Invoice\InvoiceSumInclusive;
|
|||
use App\Services\EDocument\Standards\Verifactu\AeatClient;
|
||||
use App\Services\EDocument\Standards\Verifactu\RegistroAlta;
|
||||
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\RegistroAnterior;
|
||||
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\Invoice as VerifactuInvoice;
|
||||
|
||||
|
|
@ -62,8 +60,14 @@ class Verifactu extends AbstractService
|
|||
|
||||
$v_logs = $this->invoice->company->verifactu_logs;
|
||||
|
||||
//determine the current status of the invoice.
|
||||
$document = (new RegistroAlta($this->invoice))->run()->getInvoice();
|
||||
$i_logs = $this->invoice->verifactu_logs;
|
||||
|
||||
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
|
||||
$this->_document = $document;
|
||||
|
|
@ -74,7 +78,6 @@ class Verifactu extends AbstractService
|
|||
if($v_logs->count() >= 1){
|
||||
$v_log = $v_logs->first();
|
||||
$this->_previous_huella = $v_log->hash;
|
||||
// $document = InvoiceModification::createFromInvoice($document, $v_log->deserialize());
|
||||
}
|
||||
|
||||
//3. cancelled => RegistroAnulacion
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ class Invoice extends BaseXmlModel implements XmlModelInterface
|
|||
// Constants for invoice types
|
||||
public const TIPO_FACTURA_NORMAL = 'F1';
|
||||
public const TIPO_FACTURA_RECTIFICATIVA = 'R1';
|
||||
public const TIPO_FACTURA_SUSTITUIDA = 'F3';
|
||||
|
||||
// Constants for rectification types
|
||||
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 ?array $facturasRectificadas = null;
|
||||
protected ?array $facturasSustituidas = null;
|
||||
protected ?float $importeRectificacion = null;
|
||||
protected ?array $importeRectificacion = null;
|
||||
protected ?string $fechaOperacion = null;
|
||||
protected string $descripcionOperacion;
|
||||
protected ?string $facturaSimplificadaArt7273 = null;
|
||||
|
|
@ -233,21 +234,17 @@ class Invoice extends BaseXmlModel implements XmlModelInterface
|
|||
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;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setRectificationAmounts(array $amounts): self
|
||||
{
|
||||
|
||||
$this->importeRectificacion = $amounts;
|
||||
return $this;
|
||||
}
|
||||
|
|
@ -565,11 +562,11 @@ class Invoice extends BaseXmlModel implements XmlModelInterface
|
|||
* Helper method to create a rectificative invoice with ImporteRectificacion
|
||||
*
|
||||
* @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
|
||||
* @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)
|
||||
->setTipoRectificativa($tipoRectificativa)
|
||||
|
|
@ -947,7 +944,7 @@ class Invoice extends BaseXmlModel implements XmlModelInterface
|
|||
$root->appendChild($this->createElement($doc, 'TipoFactura', $this->tipoFactura));
|
||||
|
||||
// 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));
|
||||
}
|
||||
|
||||
|
|
@ -973,8 +970,32 @@ class Invoice extends BaseXmlModel implements XmlModelInterface
|
|||
$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)
|
||||
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');
|
||||
|
||||
// Add BaseRectificada
|
||||
|
|
@ -1442,14 +1463,6 @@ class Invoice extends BaseXmlModel implements XmlModelInterface
|
|||
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
|
||||
*/
|
||||
|
|
@ -1468,47 +1481,6 @@ class Invoice extends BaseXmlModel implements XmlModelInterface
|
|||
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()
|
||||
{
|
||||
return serialize($this);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
// }
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -108,9 +108,6 @@ class RegistroAlta
|
|||
|
||||
$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
|
||||
->setIdVersion('1.0')
|
||||
->setIdFactura((new IDFactura())
|
||||
|
|
@ -118,37 +115,14 @@ class RegistroAlta
|
|||
->setNumSerieFactura($this->invoice->number)
|
||||
->setFechaExpedicionFactura(\Carbon\Carbon::parse($this->invoice->date)->format('d-m-Y')))
|
||||
->setNombreRazonEmisor($this->company->present()->name()) //company name
|
||||
->setTipoFactura($isRectification ? 'R1' : 'F1') //invoice type
|
||||
->setDescripcionOperacion($isRectification ? 'Rectificación por error en factura anterior' : 'Alta')// It IS! manadatory - max chars 500
|
||||
->setTipoFactura('F1') //invoice type
|
||||
->setDescripcionOperacion('Alta')// It IS! manadatory - max chars 500
|
||||
->setCuotaTotal($this->invoice->total_taxes) //total taxes
|
||||
->setImporteTotal($this->invoice->amount) //total invoice amount
|
||||
->setFechaHoraHusoGenRegistro($this->current_timestamp) //creation/submission timestamp
|
||||
->setTipoHuella('01') //sha256
|
||||
->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 */
|
||||
$emisor = new PersonaFisicaJuridica();
|
||||
$emisor->setNif($this->company->settings->vat_number)
|
||||
|
|
@ -235,6 +209,34 @@ class RegistroAlta
|
|||
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
|
||||
{
|
||||
return $this->v_invoice;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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\Models\Encadenamiento;
|
||||
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\Verifactu\Models\FacturaRectificativa;
|
||||
use App\Services\EDocument\Standards\Verifactu\Models\Invoice as VerifactuInvoice;
|
||||
|
|
@ -195,7 +194,7 @@ class VerifactuFeatureTest extends TestCase
|
|||
|
||||
$invoice = $this->buildData();
|
||||
|
||||
$invoice->number = 'TEST0033343459';
|
||||
$invoice->number = 'TEST0033343460';
|
||||
$invoice->save();
|
||||
|
||||
$this->assertNotNull($invoice);
|
||||
|
|
@ -212,17 +211,17 @@ class VerifactuFeatureTest extends TestCase
|
|||
$xx = VerifactuLog::create([
|
||||
'invoice_id' => $_inv->id,
|
||||
'company_id' => $invoice->company_id,
|
||||
'invoice_number' => 'TEST0033343458',
|
||||
'invoice_number' => 'TEST0033343459',
|
||||
'date' => '2025-08-10',
|
||||
'hash' => '71E0DB528B7D83CE44A1D9055FE814371D77A9291EB24B74043ACE639175CC3C',
|
||||
'hash' => 'E5A23515881D696FCD1CA8EE4902632BFC6D892BA8EB79CB656A5F84963079D3',
|
||||
'nif' => 'A39200019',
|
||||
'previous_hash' => '71E0DB528B7D83CE44A1D9055FE814371D77A9291EB24B74043ACE639175CC3C',
|
||||
'previous_hash' => 'E5A23515881D696FCD1CA8EE4902632BFC6D892BA8EB79CB656A5F84963079D3',
|
||||
]);
|
||||
|
||||
$verifactu = new Verifactu($invoice);
|
||||
$verifactu->run();
|
||||
$verifactu->setTestMode()
|
||||
->setPreviousHash('71E0DB528B7D83CE44A1D9055FE814371D77A9291EB24B74043ACE639175CC3C');
|
||||
->setPreviousHash('E5A23515881D696FCD1CA8EE4902632BFC6D892BA8EB79CB656A5F84963079D3');
|
||||
|
||||
$validator = new \App\Services\EDocument\Standards\Validation\VerifactuDocumentValidator($verifactu->getEnvelope());
|
||||
$validator->validate();
|
||||
|
|
@ -262,291 +261,245 @@ class VerifactuFeatureTest extends TestCase
|
|||
$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()
|
||||
{
|
||||
$invoice = $this->buildData();
|
||||
$invoice->number = 'TEST0033343444';
|
||||
$invoice->number = 'TEST0033343460-R2';
|
||||
$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);
|
||||
$old_document = $verifactu->setTestMode()
|
||||
$document = $verifactu->setTestMode()
|
||||
->setPreviousHash($previous_huella)
|
||||
->run()
|
||||
->getInvoice();
|
||||
|
||||
nlog($document->toSoapEnvelope());
|
||||
|
||||
$_verifactu = (new Verifactu($invoice))->setTestMode()->run();
|
||||
$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);
|
||||
$response = $verifactu->send($document->toSoapEnvelope());
|
||||
|
||||
$this->assertNotNull($response);
|
||||
$this->assertArrayHasKey('success', $response);
|
||||
$this->assertTrue($response['success']);
|
||||
}
|
||||
|
||||
public function test_raw()
|
||||
{
|
||||
$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:Header/>
|
||||
<soapenv:Body>
|
||||
<sum:RegFactuSistemaFacturacion>
|
||||
<sum:Cabecera>
|
||||
<sum1:ObligadoEmision>
|
||||
<sum1:NombreRazon>CERTIFICADO FISICA PRUEBAS</sum1:NombreRazon>
|
||||
<sum1:NIF>A39200019</sum1:NIF>
|
||||
</sum1:ObligadoEmision>
|
||||
</sum:Cabecera>
|
||||
<sum:RegistroFactura>
|
||||
<sum1:RegistroAlta>
|
||||
<sum1:IDVersion>1.0</sum1:IDVersion>
|
||||
<sum1:IDFactura>
|
||||
<sum1:IDEmisorFactura>A39200019</sum1:IDEmisorFactura>
|
||||
<sum1:NumSerieFactura>TEST0033343444</sum1:NumSerieFactura>
|
||||
<sum1:FechaExpedicionFactura>09-08-2025</sum1:FechaExpedicionFactura>
|
||||
</sum1:IDFactura>
|
||||
<sum1:NombreRazonEmisor>CERTIFICADO FISICA PRUEBAS</sum1:NombreRazonEmisor>
|
||||
<sum1:TipoFactura>R1</sum1:TipoFactura>
|
||||
<sum1:TipoRectificativa>S</sum1:TipoRectificativa>
|
||||
<sum1:FacturasRectificadas>
|
||||
<sum1:IDFacturaRectificada>
|
||||
<sum1:IDEmisorFactura>A39200019</sum1:IDEmisorFactura>
|
||||
<sum1:NumSerieFactura>TEST0033343443</sum1:NumSerieFactura>
|
||||
<sum1:FechaExpedicionFactura>09-08-2025</sum1:FechaExpedicionFactura>
|
||||
</sum1:IDFacturaRectificada>
|
||||
</sum1:FacturasRectificadas>
|
||||
<sum1:ImporteRectificacion>
|
||||
<sum1:BaseRectificada>100.00</sum1:BaseRectificada>
|
||||
<sum1:CuotaRectificada>21.00</sum1:CuotaRectificada>
|
||||
<sum1:CuotaRecargoRectificado>0.00</sum1:CuotaRecargoRectificado>
|
||||
</sum1:ImporteRectificacion>
|
||||
<sum1:DescripcionOperacion>Rectificación por error en factura anterior</sum1:DescripcionOperacion>
|
||||
<sum1:Destinatarios>
|
||||
<sum1:IDDestinatario>
|
||||
<sum1:NombreRazon>Test Recipient Company</sum1:NombreRazon>
|
||||
<sum1:NIF>A39200019</sum1:NIF>
|
||||
</sum1:IDDestinatario>
|
||||
</sum1:Destinatarios>
|
||||
<sum1:Desglose>
|
||||
<sum1:DetalleDesglose>
|
||||
<sum1:Impuesto>01</sum1:Impuesto>
|
||||
<sum1:ClaveRegimen>01</sum1:ClaveRegimen>
|
||||
<sum1:CalificacionOperacion>S1</sum1:CalificacionOperacion>
|
||||
<sum1:TipoImpositivo>21.00</sum1:TipoImpositivo>
|
||||
<sum1:BaseImponibleOimporteNoSujeto>97.00</sum1:BaseImponibleOimporteNoSujeto>
|
||||
<sum1:CuotaRepercutida>20.37</sum1:CuotaRepercutida>
|
||||
</sum1:DetalleDesglose>
|
||||
</sum1:Desglose>
|
||||
<sum1:CuotaTotal>47.05</sum1:CuotaTotal>
|
||||
<sum1:ImporteTotal>144.05</sum1:ImporteTotal>
|
||||
<sum1:Encadenamiento>
|
||||
<sum1:PrimerRegistro>S</sum1:PrimerRegistro>
|
||||
</sum1:Encadenamiento>
|
||||
<sum1:SistemaInformatico>
|
||||
<sum1:NombreRazon>CERTIFICADO FISICA PRUEBAS</sum1:NombreRazon>
|
||||
<sum1:NIF>A39200019</sum1:NIF>
|
||||
<sum1:NombreSistemaInformatico>InvoiceNinja</sum1:NombreSistemaInformatico>
|
||||
<sum1:IdSistemaInformatico>77</sum1:IdSistemaInformatico>
|
||||
<sum1:Version>1.0.03</sum1:Version>
|
||||
<sum1:NumeroInstalacion>383</sum1:NumeroInstalacion>
|
||||
<sum1:TipoUsoPosibleSoloVerifactu>N</sum1:TipoUsoPosibleSoloVerifactu>
|
||||
<sum1:TipoUsoPosibleMultiOT>S</sum1:TipoUsoPosibleMultiOT>
|
||||
<sum1:IndicadorMultiplesOT>S</sum1:IndicadorMultiplesOT>
|
||||
</sum1:SistemaInformatico>
|
||||
<sum1:FechaHoraHusoGenRegistro>2025-08-09T23:18:44+02:00</sum1:FechaHoraHusoGenRegistro>
|
||||
<sum1:TipoHuella>01</sum1:TipoHuella>
|
||||
<sum1:Huella>E7558C33FE3496551F38FEB582F4879B1D9F6C073489628A8DC275E12298941F</sum1:Huella>
|
||||
</sum1:RegistroAlta>
|
||||
</sum:RegistroFactura>
|
||||
</sum:RegFactuSistemaFacturacion>
|
||||
</soapenv:Body>
|
||||
</soapenv:Envelope>
|
||||
XML;
|
||||
public function test_rectification_invoice()
|
||||
{
|
||||
$soapXml = <<<XML
|
||||
<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:Header/>
|
||||
<soapenv:Body>
|
||||
<sum:RegFactuSistemaFacturacion>
|
||||
<sum:Cabecera>
|
||||
<sum1:ObligadoEmision>
|
||||
<sum1:NombreRazon>CERTIFICADO FISICA PRUEBAS</sum1:NombreRazon>
|
||||
<sum1:NIF>A39200019</sum1:NIF>
|
||||
</sum1:ObligadoEmision>
|
||||
</sum:Cabecera>
|
||||
<sum:RegistroFactura>
|
||||
<sum1:RegistroAlta>
|
||||
<sum1:IDVersion>1.0</sum1:IDVersion>
|
||||
<sum1:IDFactura>
|
||||
<sum1:IDEmisorFactura>A39200019</sum1:IDEmisorFactura>
|
||||
<sum1:NumSerieFactura>TEST0033343460-R1</sum1:NumSerieFactura>
|
||||
<sum1:FechaExpedicionFactura>10-08-2025</sum1:FechaExpedicionFactura>
|
||||
</sum1:IDFactura>
|
||||
<sum1:NombreRazonEmisor>CERTIFICADO FISICA PRUEBAS</sum1:NombreRazonEmisor>
|
||||
<sum1:TipoFactura>F3</sum1:TipoFactura>
|
||||
<sum1:FacturasSustituidas>
|
||||
<sum1:IDFacturaSustituida>
|
||||
<sum1:IDEmisorFactura>A39200019</sum1:IDEmisorFactura>
|
||||
<sum1:NumSerieFactura>TEST0033343460</sum1:NumSerieFactura>
|
||||
<sum1:FechaExpedicionFactura>10-08-2025</sum1:FechaExpedicionFactura>
|
||||
</sum1:IDFacturaSustituida>
|
||||
</sum1:FacturasSustituidas>
|
||||
<sum1:DescripcionOperacion>Alta</sum1:DescripcionOperacion>
|
||||
<sum1:Destinatarios>
|
||||
<sum1:IDDestinatario>
|
||||
<sum1:NombreRazon>CERTIFICADO FISICA PRUEBAS</sum1:NombreRazon>
|
||||
<sum1:NIF>A39200019</sum1:NIF>
|
||||
</sum1:IDDestinatario>
|
||||
</sum1:Destinatarios>
|
||||
<sum1:Desglose>
|
||||
<sum1:DetalleDesglose>
|
||||
<sum1:Impuesto>01</sum1:Impuesto>
|
||||
<sum1:ClaveRegimen>01</sum1:ClaveRegimen>
|
||||
<sum1:CalificacionOperacion>S1</sum1:CalificacionOperacion>
|
||||
<sum1:TipoImpositivo>21.00</sum1:TipoImpositivo>
|
||||
<sum1:BaseImponibleOimporteNoSujeto>100.00</sum1:BaseImponibleOimporteNoSujeto>
|
||||
<sum1:CuotaRepercutida>21.00</sum1:CuotaRepercutida>
|
||||
</sum1:DetalleDesglose>
|
||||
</sum1:Desglose>
|
||||
<sum1:CuotaTotal>21</sum1:CuotaTotal>
|
||||
<sum1:ImporteTotal>121</sum1:ImporteTotal>
|
||||
<sum1:Encadenamiento>
|
||||
<sum1:RegistroAnterior>
|
||||
<sum1:IDEmisorFactura>A39200019</sum1:IDEmisorFactura>
|
||||
<sum1:NumSerieFactura>TEST0033343459</sum1:NumSerieFactura>
|
||||
<sum1:FechaExpedicionFactura>10-08-2025</sum1:FechaExpedicionFactura>
|
||||
<sum1:Huella>1FB6B4EF72DD2A07CC23B3F9D74EE5749C8E86B34B9B1DFFFC8C3E46ACA87E21</sum1:Huella>
|
||||
</sum1:RegistroAnterior>
|
||||
</sum1:Encadenamiento>
|
||||
<sum1:SistemaInformatico>
|
||||
<sum1:NombreRazon>CERTIFICADO FISICA PRUEBAS</sum1:NombreRazon>
|
||||
<sum1:NIF>A39200019</sum1:NIF>
|
||||
<sum1:NombreSistemaInformatico>InvoiceNinja</sum1:NombreSistemaInformatico>
|
||||
<sum1:IdSistemaInformatico>77</sum1:IdSistemaInformatico>
|
||||
<sum1:Version>1.0.03</sum1:Version>
|
||||
<sum1:NumeroInstalacion>383</sum1:NumeroInstalacion>
|
||||
<sum1:TipoUsoPosibleSoloVerifactu>N</sum1:TipoUsoPosibleSoloVerifactu>
|
||||
<sum1:TipoUsoPosibleMultiOT>S</sum1:TipoUsoPosibleMultiOT>
|
||||
<sum1:IndicadorMultiplesOT>S</sum1:IndicadorMultiplesOT>
|
||||
</sum1:SistemaInformatico>
|
||||
<sum1:FechaHoraHusoGenRegistro>2025-08-10T05:02:18+00:00</sum1:FechaHoraHusoGenRegistro>
|
||||
<sum1:TipoHuella>01</sum1:TipoHuella>
|
||||
<sum1:Huella>BC61C7CB7CB09917C076CAE7D066B3E2CF521A3B8B501D0C83250B5EB4A4B40D</sum1:Huella>
|
||||
</sum1:RegistroAlta>
|
||||
</sum:RegistroFactura>
|
||||
</sum:RegFactuSistemaFacturacion>
|
||||
</soapenv:Body>
|
||||
</soapenv:Envelope>
|
||||
|
||||
XML;
|
||||
|
||||
|
||||
$xslt = new VerifactuDocumentValidator($soapXml);
|
||||
$xslt->validate();
|
||||
$errors = $xslt->getVerifactuErrors();
|
||||
$xslt = new VerifactuDocumentValidator($soapXml);
|
||||
$xslt->validate();
|
||||
$errors = $xslt->getVerifactuErrors();
|
||||
|
||||
if(count($errors) > 0) {
|
||||
nlog('Errors:');
|
||||
nlog($errors);
|
||||
nlog('Errors:');
|
||||
}
|
||||
if(count($errors) > 0) {
|
||||
nlog('Errors:');
|
||||
nlog($errors);
|
||||
nlog('Errors:');
|
||||
}
|
||||
|
||||
$this->assertCount(0, $errors);
|
||||
$this->assertCount(0, $errors);
|
||||
|
||||
$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');
|
||||
$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());
|
||||
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());
|
||||
$r = new ResponseProcessor();
|
||||
$rx = $r->processResponse($response->body());
|
||||
|
||||
nlog($rx);
|
||||
nlog($rx);
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function testInvoiceCancellation()
|
||||
|
|
@ -654,7 +607,15 @@ nlog($rx);
|
|||
|
||||
// Set up rectification details exactly as in the expected XML
|
||||
$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
|
||||
$desglose = new Desglose();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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\ResponseProcessor;
|
||||
use App\Services\EDocument\Standards\Verifactu\Models\PersonaFisicaJuridica;
|
||||
use App\Services\EDocument\Standards\Verifactu\Models\InvoiceModification;
|
||||
|
||||
|
||||
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
|
||||
* Based on AEAT response showing the exact format they use
|
||||
|
|
|
|||
Loading…
Reference in New Issue