Tests for modified invoices
This commit is contained in:
parent
aa918f7ec0
commit
5895c1b0ed
|
|
@ -9,7 +9,7 @@ use Illuminate\Database\Eloquent\Model;
|
||||||
* @property int $company_id
|
* @property int $company_id
|
||||||
* @property int $invoice_id
|
* @property int $invoice_id
|
||||||
* @property string $nif
|
* @property string $nif
|
||||||
* @property Carbon $date
|
* @property \Carbon\Carbon $date
|
||||||
* @property string $invoice_number
|
* @property string $invoice_number
|
||||||
* @property string $hash
|
* @property string $hash
|
||||||
* @property string $previous_hash
|
* @property string $previous_hash
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,13 @@ use App\Services\AbstractService;
|
||||||
use App\Helpers\Invoice\InvoiceSum;
|
use App\Helpers\Invoice\InvoiceSum;
|
||||||
use App\Utils\Traits\NumberFormatter;
|
use App\Utils\Traits\NumberFormatter;
|
||||||
use App\Helpers\Invoice\InvoiceSumInclusive;
|
use App\Helpers\Invoice\InvoiceSumInclusive;
|
||||||
|
use App\Services\EDocument\Standards\Verifactu\Models\Desglose;
|
||||||
|
use App\Services\EDocument\Standards\Verifactu\Models\Encadenamiento;
|
||||||
use App\Services\EDocument\Standards\Verifactu\Models\RegistroAnterior;
|
use App\Services\EDocument\Standards\Verifactu\Models\RegistroAnterior;
|
||||||
use App\Services\EDocument\Standards\Verifactu\Models\SistemaInformatico;
|
use App\Services\EDocument\Standards\Verifactu\Models\SistemaInformatico;
|
||||||
|
use App\Services\EDocument\Standards\Verifactu\Models\PersonaFisicaJuridica;
|
||||||
use App\Services\EDocument\Standards\Verifactu\Models\Invoice as VerifactuInvoice;
|
use App\Services\EDocument\Standards\Verifactu\Models\Invoice as VerifactuInvoice;
|
||||||
|
use App\Models\VerifactuLog;
|
||||||
|
|
||||||
class Verifactu extends AbstractService
|
class Verifactu extends AbstractService
|
||||||
{
|
{
|
||||||
|
|
@ -101,7 +105,7 @@ class Verifactu extends AbstractService
|
||||||
->setIdVersion('1.0')
|
->setIdVersion('1.0')
|
||||||
->setIdFactura($this->invoice->number) //invoice number
|
->setIdFactura($this->invoice->number) //invoice number
|
||||||
->setNombreRazonEmisor($this->company->present()->name()) //company name
|
->setNombreRazonEmisor($this->company->present()->name()) //company name
|
||||||
->setTipoFactura($this->calculateInvoiceType()) //invoice type
|
->setTipoFactura('F1') //invoice type
|
||||||
->setDescripcionOperacion('')// Not manadatory - max chars 500
|
->setDescripcionOperacion('')// Not manadatory - max chars 500
|
||||||
->setCuotaTotal($this->invoice->total_taxes) //total taxes
|
->setCuotaTotal($this->invoice->total_taxes) //total taxes
|
||||||
->setImporteTotal($this->invoice->amount) //total invoice amount
|
->setImporteTotal($this->invoice->amount) //total invoice amount
|
||||||
|
|
@ -132,7 +136,7 @@ class Verifactu extends AbstractService
|
||||||
$desglose = new Desglose();
|
$desglose = new Desglose();
|
||||||
|
|
||||||
//Combine the line taxes with invoice taxes here to get a total tax amount
|
//Combine the line taxes with invoice taxes here to get a total tax amount
|
||||||
$taxes = $calc->getTaxMap();
|
$taxes = $this->calc->getTaxMap();
|
||||||
|
|
||||||
$desglose_iva = [];
|
$desglose_iva = [];
|
||||||
|
|
||||||
|
|
@ -157,6 +161,7 @@ class Verifactu extends AbstractService
|
||||||
$encadenamiento = new Encadenamiento();
|
$encadenamiento = new Encadenamiento();
|
||||||
|
|
||||||
// Get the previous invoice log
|
// Get the previous invoice log
|
||||||
|
/** @var ?VerifactuLog $v_log */
|
||||||
$v_log = $this->company->verifactu_logs()->first();
|
$v_log = $this->company->verifactu_logs()->first();
|
||||||
|
|
||||||
// We chain the previous hash to the current invoice to ensure consistency
|
// We chain the previous hash to the current invoice to ensure consistency
|
||||||
|
|
@ -219,10 +224,4 @@ class Verifactu extends AbstractService
|
||||||
return '01';
|
return '01';
|
||||||
}
|
}
|
||||||
|
|
||||||
private function calculateInvoiceType(): string
|
|
||||||
{
|
|
||||||
//tipofactua
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
namespace App\Services\EDocument\Standards\Verifactu\Models;
|
namespace App\Services\EDocument\Standards\Verifactu\Models;
|
||||||
|
|
||||||
|
use App\Services\EDocument\Standards\Verifactu\Models\RegistroAnterior;
|
||||||
|
|
||||||
class Encadenamiento extends BaseXmlModel
|
class Encadenamiento extends BaseXmlModel
|
||||||
{
|
{
|
||||||
protected ?string $primerRegistro = null;
|
protected ?string $primerRegistro = null;
|
||||||
|
|
@ -96,12 +98,12 @@ class Encadenamiento extends BaseXmlModel
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getRegistroAnterior(): ?EncadenamientoFacturaAnterior
|
public function getRegistroAnterior(): ?RegistroAnterior
|
||||||
{
|
{
|
||||||
return $this->registroAnterior;
|
return $this->registroAnterior;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setRegistroAnterior(?EncadenamientoFacturaAnterior $registroAnterior): self
|
public function setRegistroAnterior(?RegistroAnterior $registroAnterior): self
|
||||||
{
|
{
|
||||||
$this->registroAnterior = $registroAnterior;
|
$this->registroAnterior = $registroAnterior;
|
||||||
return $this;
|
return $this;
|
||||||
|
|
|
||||||
|
|
@ -1138,4 +1138,68 @@ class Invoice extends BaseXmlModel
|
||||||
$node = $element->getElementsByTagNameNS(self::XML_NAMESPACE, $tagName)->item(0);
|
$node = $element->getElementsByTagNameNS(self::XML_NAMESPACE, $tagName)->item(0);
|
||||||
return $node ? $node->nodeValue : null;
|
return $node ? $node->nodeValue : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a modification from this invoice
|
||||||
|
*/
|
||||||
|
public function createModification(Invoice $modifiedInvoice): InvoiceModification
|
||||||
|
{
|
||||||
|
return InvoiceModification::createFromInvoice($this, $modifiedInvoice);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a cancellation record for this invoice
|
||||||
|
*/
|
||||||
|
public function createCancellation(): RegistroAnulacion
|
||||||
|
{
|
||||||
|
$cancellation = new RegistroAnulacion();
|
||||||
|
$cancellation
|
||||||
|
->setIdEmisorFactura($this->getTercero()?->getNif() ?? 'B12345678')
|
||||||
|
->setNumSerieFactura($this->getIdFactura())
|
||||||
|
->setFechaExpedicionFactura($this->getFechaExpedicionFactura())
|
||||||
|
->setMotivoAnulacion('1'); // Sustitución por otra factura
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,221 @@
|
||||||
|
<?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
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a modification from an existing invoice
|
||||||
|
*/
|
||||||
|
public static function createFromInvoice(Invoice $originalInvoice, Invoice $modifiedInvoice): self
|
||||||
|
{
|
||||||
|
$modification = new self();
|
||||||
|
|
||||||
|
// Set up cancellation record
|
||||||
|
$cancellation = new RegistroAnulacion();
|
||||||
|
$cancellation
|
||||||
|
->setIdEmisorFactura($originalInvoice->getTercero()?->getNif() ?? 'B12345678')
|
||||||
|
->setNumSerieFactura($originalInvoice->getIdFactura())
|
||||||
|
->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($modifiedInvoice->getTipoFactura())
|
||||||
|
->setTipoRectificativa($modifiedInvoice->getTipoRectificativa())
|
||||||
|
->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($modifiedInvoice->getFechaHoraHusoGenRegistro())
|
||||||
|
->setNumRegistroAcuerdoFacturacion($modifiedInvoice->getNumRegistroAcuerdoFacturacion())
|
||||||
|
->setIdAcuerdoSistemaInformatico($modifiedInvoice->getIdAcuerdoSistemaInformatico())
|
||||||
|
->setTipoHuella($modifiedInvoice->getTipoHuella())
|
||||||
|
->setHuella($modifiedInvoice->getHuella());
|
||||||
|
|
||||||
|
$modification->setRegistroModificacion($modificationRecord);
|
||||||
|
|
||||||
|
// Set up sistema informatico for the modification
|
||||||
|
$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 ModificacionFactura
|
||||||
|
$modificacionFactura = $soapDoc->createElementNS('https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroLR.xsd', 'sum:ModificacionFactura');
|
||||||
|
$body->appendChild($modificacionFactura);
|
||||||
|
|
||||||
|
// Add RegistroAnulacion
|
||||||
|
$registroAnulacionElement = $this->registroAnulacion->toXml($soapDoc);
|
||||||
|
$importedRegistroAnulacion = $soapDoc->importNode($registroAnulacionElement, true);
|
||||||
|
$modificacionFactura->appendChild($importedRegistroAnulacion);
|
||||||
|
|
||||||
|
// Add RegistroModificacion
|
||||||
|
$registroModificacionElement = $this->registroModificacion->toXml($soapDoc);
|
||||||
|
$importedRegistroModificacion = $soapDoc->importNode($registroModificacionElement, true);
|
||||||
|
$modificacionFactura->appendChild($importedRegistroModificacion);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,149 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\EDocument\Standards\Verifactu\Models;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RegistroAnulacion - Invoice Cancellation Record
|
||||||
|
*
|
||||||
|
* This class represents the cancellation record information required for Verifactu e-invoicing
|
||||||
|
* modification operations. It contains the details of the invoice to be cancelled.
|
||||||
|
*/
|
||||||
|
class RegistroAnulacion extends BaseXmlModel
|
||||||
|
{
|
||||||
|
protected string $idVersion;
|
||||||
|
protected string $idEmisorFactura;
|
||||||
|
protected string $numSerieFactura;
|
||||||
|
protected string $fechaExpedicionFactura;
|
||||||
|
protected string $motivoAnulacion;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->idVersion = '1.0';
|
||||||
|
$this->motivoAnulacion = '1'; // Default: Sustitución por otra factura
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getIdVersion(): string
|
||||||
|
{
|
||||||
|
return $this->idVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setIdVersion(string $idVersion): self
|
||||||
|
{
|
||||||
|
$this->idVersion = $idVersion;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getIdEmisorFactura(): string
|
||||||
|
{
|
||||||
|
return $this->idEmisorFactura;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setIdEmisorFactura(string $idEmisorFactura): self
|
||||||
|
{
|
||||||
|
$this->idEmisorFactura = $idEmisorFactura;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getNumSerieFactura(): string
|
||||||
|
{
|
||||||
|
return $this->numSerieFactura;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setNumSerieFactura(string $numSerieFactura): self
|
||||||
|
{
|
||||||
|
$this->numSerieFactura = $numSerieFactura;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFechaExpedicionFactura(): string
|
||||||
|
{
|
||||||
|
return $this->fechaExpedicionFactura;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setFechaExpedicionFactura(string $fechaExpedicionFactura): self
|
||||||
|
{
|
||||||
|
$this->fechaExpedicionFactura = $fechaExpedicionFactura;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMotivoAnulacion(): string
|
||||||
|
{
|
||||||
|
return $this->motivoAnulacion;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMotivoAnulacion(string $motivoAnulacion): self
|
||||||
|
{
|
||||||
|
$this->motivoAnulacion = $motivoAnulacion;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toXml(\DOMDocument $doc): \DOMElement
|
||||||
|
{
|
||||||
|
$root = $doc->createElementNS(self::XML_NAMESPACE, self::XML_NAMESPACE_PREFIX . ':RegistroAnulacion');
|
||||||
|
|
||||||
|
// Add IDVersion
|
||||||
|
$root->appendChild($this->createElement($doc, 'IDVersion', $this->idVersion));
|
||||||
|
|
||||||
|
// Create IDFactura structure
|
||||||
|
$idFactura = $this->createElement($doc, 'IDFactura');
|
||||||
|
$idFactura->appendChild($this->createElement($doc, 'IDEmisorFactura', $this->idEmisorFactura));
|
||||||
|
$idFactura->appendChild($this->createElement($doc, 'NumSerieFactura', $this->numSerieFactura));
|
||||||
|
$idFactura->appendChild($this->createElement($doc, 'FechaExpedicionFactura', $this->fechaExpedicionFactura));
|
||||||
|
$root->appendChild($idFactura);
|
||||||
|
|
||||||
|
// Add MotivoAnulacion
|
||||||
|
$root->appendChild($this->createElement($doc, 'MotivoAnulacion', $this->motivoAnulacion));
|
||||||
|
|
||||||
|
return $root;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function fromDOMElement(\DOMElement $element): self
|
||||||
|
{
|
||||||
|
$registroAnulacion = new self();
|
||||||
|
|
||||||
|
// Handle IDVersion
|
||||||
|
$idVersion = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'IDVersion')->item(0);
|
||||||
|
if ($idVersion) {
|
||||||
|
$registroAnulacion->setIdVersion($idVersion->nodeValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle IDFactura
|
||||||
|
$idFactura = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'IDFactura')->item(0);
|
||||||
|
if ($idFactura) {
|
||||||
|
$idEmisorFactura = $idFactura->getElementsByTagNameNS(self::XML_NAMESPACE, 'IDEmisorFactura')->item(0);
|
||||||
|
if ($idEmisorFactura) {
|
||||||
|
$registroAnulacion->setIdEmisorFactura($idEmisorFactura->nodeValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
$numSerieFactura = $idFactura->getElementsByTagNameNS(self::XML_NAMESPACE, 'NumSerieFactura')->item(0);
|
||||||
|
if ($numSerieFactura) {
|
||||||
|
$registroAnulacion->setNumSerieFactura($numSerieFactura->nodeValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
$fechaExpedicionFactura = $idFactura->getElementsByTagNameNS(self::XML_NAMESPACE, 'FechaExpedicionFactura')->item(0);
|
||||||
|
if ($fechaExpedicionFactura) {
|
||||||
|
$registroAnulacion->setFechaExpedicionFactura($fechaExpedicionFactura->nodeValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle MotivoAnulacion
|
||||||
|
$motivoAnulacion = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'MotivoAnulacion')->item(0);
|
||||||
|
if ($motivoAnulacion) {
|
||||||
|
$registroAnulacion->setMotivoAnulacion($motivoAnulacion->nodeValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $registroAnulacion;
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,675 @@
|
||||||
|
<?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 string $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(): string
|
||||||
|
{
|
||||||
|
return $this->idFactura;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setIdFactura(string $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
|
||||||
|
{
|
||||||
|
if (!preg_match('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/', $fechaHoraHusoGenRegistro)) {
|
||||||
|
throw new \InvalidArgumentException('Invalid date format for FechaHoraHusoGenRegistro. Expected format: YYYY-MM-DDThh:mm:ss');
|
||||||
|
}
|
||||||
|
$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));
|
||||||
|
$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));
|
||||||
|
}
|
||||||
|
|
||||||
|
$root->appendChild($this->createElement($doc, 'TipoFactura', $this->tipoFactura));
|
||||||
|
|
||||||
|
if ($this->tipoFactura === 'R1' && $this->facturaRectificativa !== null) {
|
||||||
|
$root->appendChild($this->createElement($doc, 'TipoRectificativa', $this->facturaRectificativa->getTipoRectificativa()));
|
||||||
|
$facturasRectificadas = $this->createElement($doc, 'FacturasRectificadas');
|
||||||
|
$facturasRectificadas->appendChild($this->facturaRectificativa->toXml($doc));
|
||||||
|
$root->appendChild($facturasRectificadas);
|
||||||
|
if ($this->importeRectificacion !== null) {
|
||||||
|
$root->appendChild($this->createElement($doc, 'ImporteRectificacion', (string)$this->importeRectificacion));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->fechaOperacion) {
|
||||||
|
$root->appendChild($this->createElement($doc, 'FechaOperacion', date('d-m-Y', strtotime($this->fechaOperacion))));
|
||||||
|
}
|
||||||
|
|
||||||
|
$root->appendChild($this->createElement($doc, 'DescripcionOperacion', $this->descripcionOperacion));
|
||||||
|
|
||||||
|
if ($this->cupon !== null) {
|
||||||
|
$root->appendChild($this->createElement($doc, 'Cupon', $this->cupon));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->facturaSimplificadaArt7273 !== null) {
|
||||||
|
$root->appendChild($this->createElement($doc, 'FacturaSimplificadaArt7273', $this->facturaSimplificadaArt7273));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->facturaSinIdentifDestinatarioArt61d !== null) {
|
||||||
|
$root->appendChild($this->createElement($doc, 'FacturaSinIdentifDestinatarioArt61d', $this->facturaSinIdentifDestinatarioArt61d));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->macrodato !== null) {
|
||||||
|
$root->appendChild($this->createElement($doc, 'Macrodato', $this->macrodato));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->emitidaPorTerceroODestinatario !== null) {
|
||||||
|
$root->appendChild($this->createElement($doc, 'EmitidaPorTerceroODestinatario', $this->emitidaPorTerceroODestinatario));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->tercero !== null) {
|
||||||
|
$root->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);
|
||||||
|
}
|
||||||
|
$root->appendChild($destinatariosElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add Desglose
|
||||||
|
if ($this->desglose !== null) {
|
||||||
|
$root->appendChild($this->desglose->toXml($doc));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add CuotaTotal and ImporteTotal
|
||||||
|
$root->appendChild($this->createElement($doc, 'CuotaTotal', (string)$this->cuotaTotal));
|
||||||
|
$root->appendChild($this->createElement($doc, 'ImporteTotal', (string)$this->importeTotal));
|
||||||
|
|
||||||
|
// Add Encadenamiento
|
||||||
|
if ($this->encadenamiento !== null) {
|
||||||
|
$root->appendChild($this->encadenamiento->toXml($doc));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add SistemaInformatico
|
||||||
|
if ($this->sistemaInformatico !== null) {
|
||||||
|
$root->appendChild($this->sistemaInformatico->toXml($doc));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add FechaHoraHusoGenRegistro
|
||||||
|
$root->appendChild($this->createElement($doc, 'FechaHoraHusoGenRegistro', $this->fechaHoraHusoGenRegistro));
|
||||||
|
|
||||||
|
// Add NumRegistroAcuerdoFacturacion
|
||||||
|
if ($this->numRegistroAcuerdoFacturacion !== null) {
|
||||||
|
$root->appendChild($this->createElement($doc, 'NumRegistroAcuerdoFacturacion', $this->numRegistroAcuerdoFacturacion));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add IdAcuerdoSistemaInformatico
|
||||||
|
if ($this->idAcuerdoSistemaInformatico !== null) {
|
||||||
|
$root->appendChild($this->createElement($doc, 'IdAcuerdoSistemaInformatico', $this->idAcuerdoSistemaInformatico));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add TipoHuella and Huella
|
||||||
|
$root->appendChild($this->createElement($doc, 'TipoHuella', $this->tipoHuella));
|
||||||
|
$root->appendChild($this->createElement($doc, 'Huella', $this->huella));
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,489 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\EInvoice\Verifactu\Models;
|
||||||
|
|
||||||
|
use Tests\TestCase;
|
||||||
|
use App\Services\EDocument\Standards\Verifactu\Models\Invoice;
|
||||||
|
use App\Services\EDocument\Standards\Verifactu\Models\InvoiceModification;
|
||||||
|
use App\Services\EDocument\Standards\Verifactu\Models\RegistroAnulacion;
|
||||||
|
use App\Services\EDocument\Standards\Verifactu\Models\RegistroModificacion;
|
||||||
|
use App\Services\EDocument\Standards\Verifactu\Models\PersonaFisicaJuridica;
|
||||||
|
use App\Services\EDocument\Standards\Verifactu\Models\Desglose;
|
||||||
|
use App\Services\EDocument\Standards\Verifactu\Models\Encadenamiento;
|
||||||
|
use App\Services\EDocument\Standards\Verifactu\Models\SistemaInformatico;
|
||||||
|
|
||||||
|
class InvoiceModificationTest 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_can_create_registro_modificacion()
|
||||||
|
{
|
||||||
|
$modification = new RegistroModificacion();
|
||||||
|
$modification
|
||||||
|
->setIdVersion('1.0')
|
||||||
|
->setIdFactura('TEST0033343436')
|
||||||
|
->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());
|
||||||
|
$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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_can_create_invoice_modification_from_invoices()
|
||||||
|
{
|
||||||
|
// Create original invoice
|
||||||
|
$originalInvoice = new Invoice();
|
||||||
|
$originalInvoice
|
||||||
|
->setIdVersion('1.0')
|
||||||
|
->setIdFactura('TEST0033343436')
|
||||||
|
->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('TEST0033343436')
|
||||||
|
->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());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_can_generate_modification_soap_envelope()
|
||||||
|
{
|
||||||
|
// Create original invoice
|
||||||
|
$originalInvoice = new Invoice();
|
||||||
|
$originalInvoice
|
||||||
|
->setIdVersion('1.0')
|
||||||
|
->setIdFactura('TEST0033343436')
|
||||||
|
->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('TEST0033343436')
|
||||||
|
->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:ModificacionFactura', $soapXml);
|
||||||
|
$this->assertStringContainsString('sf:RegistroAnulacion', $soapXml);
|
||||||
|
$this->assertStringContainsString('sf:RegistroModificacion', $soapXml);
|
||||||
|
$this->assertStringContainsString('99999910G', $soapXml);
|
||||||
|
$this->assertStringContainsString('TEST0033343436', $soapXml);
|
||||||
|
$this->assertStringContainsString('Modified Company', $soapXml);
|
||||||
|
$this->assertStringContainsString('42', $soapXml);
|
||||||
|
$this->assertStringContainsString('242', $soapXml);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_invoice_can_create_modification()
|
||||||
|
{
|
||||||
|
// Create original invoice
|
||||||
|
$originalInvoice = new Invoice();
|
||||||
|
$originalInvoice
|
||||||
|
->setIdVersion('1.0')
|
||||||
|
->setIdFactura('TEST0033343436')
|
||||||
|
->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('TEST0033343436')
|
||||||
|
->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());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_invoice_can_create_cancellation()
|
||||||
|
{
|
||||||
|
$invoice = new Invoice();
|
||||||
|
$invoice
|
||||||
|
->setIdVersion('1.0')
|
||||||
|
->setIdFactura('TEST0033343436')
|
||||||
|
->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('TEST0033343436')
|
||||||
|
->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('TEST0033343436')
|
||||||
|
->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('TEST0033343436')
|
||||||
|
->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();
|
||||||
|
|
||||||
|
// Verify the XML structure matches AEAT requirements
|
||||||
|
$this->assertStringContainsString('<soapenv:Envelope', $soapXml);
|
||||||
|
$this->assertStringContainsString('<soapenv:Header', $soapXml);
|
||||||
|
$this->assertStringContainsString('<soapenv:Body', $soapXml);
|
||||||
|
$this->assertStringContainsString('<sum:ModificacionFactura', $soapXml);
|
||||||
|
$this->assertStringContainsString('<sf:RegistroAnulacion', $soapXml);
|
||||||
|
$this->assertStringContainsString('<sf:RegistroModificacion', $soapXml);
|
||||||
|
|
||||||
|
// Verify cancellation structure
|
||||||
|
$this->assertStringContainsString('<sf:IDFactura', $soapXml);
|
||||||
|
$this->assertStringContainsString('<sf:IDEmisorFactura>99999910G</sf:IDEmisorFactura>', $soapXml);
|
||||||
|
$this->assertStringContainsString('<sf:NumSerieFactura>TEST0033343436</sf:NumSerieFactura>', $soapXml);
|
||||||
|
$this->assertStringContainsString('<sf:MotivoAnulacion>1</sf:MotivoAnulacion>', $soapXml);
|
||||||
|
|
||||||
|
// Verify modification structure
|
||||||
|
$this->assertStringContainsString('<sf:NombreRazonEmisor>Modified Company</sf:NombreRazonEmisor>', $soapXml);
|
||||||
|
$this->assertStringContainsString('<sf:CuotaTotal>42</sf:CuotaTotal>', $soapXml);
|
||||||
|
$this->assertStringContainsString('<sf:ImporteTotal>242</sf:ImporteTotal>', $soapXml);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,6 +12,7 @@ use App\Services\EDocument\Standards\Verifactu\Models\Encadenamiento;
|
||||||
use App\Services\EDocument\Standards\Verifactu\Models\SistemaInformatico;
|
use App\Services\EDocument\Standards\Verifactu\Models\SistemaInformatico;
|
||||||
use App\Services\EDocument\Standards\Verifactu\Response\ResponseProcessor;
|
use App\Services\EDocument\Standards\Verifactu\Response\ResponseProcessor;
|
||||||
use App\Services\EDocument\Standards\Verifactu\Models\PersonaFisicaJuridica;
|
use App\Services\EDocument\Standards\Verifactu\Models\PersonaFisicaJuridica;
|
||||||
|
use App\Services\EDocument\Standards\Verifactu\Models\InvoiceModification;
|
||||||
|
|
||||||
|
|
||||||
class WSTest extends TestCase
|
class WSTest extends TestCase
|
||||||
|
|
@ -398,94 +399,93 @@ $invoice->setDestinatarios($destinatarios);
|
||||||
//@todo - Need to test that modifying an invoice works.
|
//@todo - Need to test that modifying an invoice works.
|
||||||
public function test_cancel_and_modify_existing_invoice()
|
public function test_cancel_and_modify_existing_invoice()
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|
||||||
$currentTimestamp = now()->setTimezone('Europe/Madrid')->format('Y-m-d\TH:i:sP');
|
$currentTimestamp = now()->setTimezone('Europe/Madrid')->format('Y-m-d\TH:i:sP');
|
||||||
$invoice_number = 'TEST0033343436';
|
$invoice_number = 'TEST0033343436';
|
||||||
$invoice_date = '02-07-2025';
|
$invoice_date = '02-07-2025';
|
||||||
$calc_hash = 'A0B4D14E6F7769860C8A4EAFFA3EEBF52B7044685BD69D1DB5BBD68EA0E2BA21';
|
|
||||||
$nif = '99999910G';
|
$nif = '99999910G';
|
||||||
|
|
||||||
$soapXml = <<<XML
|
// Create original invoice (the one to be cancelled)
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
$originalInvoice = new Invoice();
|
||||||
<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">
|
$originalInvoice
|
||||||
<soapenv:Header>
|
->setIdVersion('1.0')
|
||||||
<tik:ObligadoEmision xmlns:tik="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroLR.xsd">
|
->setIdFactura($invoice_number)
|
||||||
<tik:NIF>A39200019</tik:NIF>
|
->setNombreRazonEmisor('Original Company')
|
||||||
<tik:NombreRazon>Sistema de Facturación</tik:NombreRazon>
|
->setTipoFactura('F1')
|
||||||
</tik:ObligadoEmision>
|
->setDescripcionOperacion('Original invoice')
|
||||||
</soapenv:Header>
|
->setCuotaTotal(21.00)
|
||||||
|
->setImporteTotal(121.00)
|
||||||
|
->setFechaHoraHusoGenRegistro($currentTimestamp)
|
||||||
|
->setTipoHuella('01')
|
||||||
|
->setHuella('ORIGINAL_HASH');
|
||||||
|
|
||||||
<soapenv:Body>
|
// Add emitter to original invoice
|
||||||
<sum:ModificacionFactura>
|
$emisor = new PersonaFisicaJuridica();
|
||||||
|
$emisor
|
||||||
|
->setNif($nif)
|
||||||
|
->setRazonSocial('Original Company');
|
||||||
|
$originalInvoice->setTercero($emisor);
|
||||||
|
|
||||||
<sum1:RegistroAnulacion>
|
// Add sistema informatico to original invoice
|
||||||
<sum1:IDFactura>
|
$sistema = new SistemaInformatico();
|
||||||
<sum1:IDEmisorFactura>99999910G</sum1:IDEmisorFactura>
|
$sistema
|
||||||
<sum1:NumSerieFactura>TEST0033343436</sum1:NumSerieFactura>
|
->setNombreRazon('Sistema de Facturación')
|
||||||
<sum1:FechaExpedicionFactura>02-07-2025</sum1:FechaExpedicionFactura>
|
->setNif('A39200019')
|
||||||
</sum1:IDFactura>
|
->setNombreSistemaInformatico('InvoiceNinja')
|
||||||
<sum1:MotivoAnulacion>1</sum1:MotivoAnulacion> <!-- 1 = Sustitución por otra factura -->
|
->setIdSistemaInformatico('77')
|
||||||
</sum1:RegistroAnulacion>
|
->setVersion('1.0.03')
|
||||||
|
->setNumeroInstalacion('383');
|
||||||
|
$originalInvoice->setSistemaInformatico($sistema);
|
||||||
|
|
||||||
<sum1:RegistroModificacion>
|
// 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');
|
||||||
|
|
||||||
<sum1:IDVersion>1.0</sum1:IDVersion>
|
// Add emitter to modified invoice
|
||||||
<!-- IDFactura: The actual invoice issuer (using same test NIF) -->
|
$emisorModificado = new PersonaFisicaJuridica();
|
||||||
<sum1:IDFactura>
|
$emisorModificado
|
||||||
<sum1:IDEmisorFactura>99999910G</sum1:IDEmisorFactura>
|
->setNif($nif)
|
||||||
<sum1:NumSerieFactura>{$invoice_number}</sum1:NumSerieFactura>
|
->setRazonSocial('CERTIFICADO FISICA PRUEBAS');
|
||||||
<sum1:FechaExpedicionFactura>{$invoice_date}</sum1:FechaExpedicionFactura>
|
$modifiedInvoice->setTercero($emisorModificado);
|
||||||
</sum1:IDFactura>
|
|
||||||
<!-- NombreRazonEmisor: The actual business that issued the invoice -->
|
|
||||||
<sum1:NombreRazonEmisor>CERTIFICADO FISICA PRUEBAS</sum1:NombreRazonEmisor>
|
|
||||||
<sum1:TipoFactura>F1</sum1:TipoFactura>
|
|
||||||
<sum1:DescripcionOperacion>Test invoice submitted by computer system on behalf of business</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:ClaveRegimen>01</sum1:ClaveRegimen>
|
|
||||||
<sum1:CalificacionOperacion>S1</sum1:CalificacionOperacion>
|
|
||||||
<sum1:TipoImpositivo>21</sum1:TipoImpositivo>
|
|
||||||
<sum1:BaseImponibleOimporteNoSujeto>100.00</sum1:BaseImponibleOimporteNoSujeto>
|
|
||||||
<sum1:CuotaRepercutida>21.00</sum1:CuotaRepercutida>
|
|
||||||
</sum1:DetalleDesglose>
|
|
||||||
</sum1:Desglose>
|
|
||||||
<sum1:CuotaTotal>21.00</sum1:CuotaTotal>
|
|
||||||
<sum1:ImporteTotal>121.00</sum1:ImporteTotal>
|
|
||||||
<!-- Encadenamiento: Required chaining information -->
|
|
||||||
<sum1:Encadenamiento>
|
|
||||||
<sum1:PrimerRegistro>N</sum1:PrimerRegistro>
|
|
||||||
</sum1:Encadenamiento>
|
|
||||||
<!-- SistemaInformatico: The computer system details (same as ObligadoEmision) -->
|
|
||||||
<sum1:SistemaInformatico>
|
|
||||||
<sum1:NombreRazon>Sistema de Facturación</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>{$currentTimestamp}</sum1:FechaHoraHusoGenRegistro>
|
|
||||||
<sum1:TipoHuella>01</sum1:TipoHuella>
|
|
||||||
<sum1:Huella>PLACEHOLDER_HUELLA</sum1:Huella>
|
|
||||||
|
|
||||||
|
// Add sistema informatico to modified invoice
|
||||||
|
$modifiedInvoice->setSistemaInformatico($sistema);
|
||||||
|
|
||||||
</sum1:RegistroModificacion>
|
// Add destinatarios to modified invoice
|
||||||
|
$destinatario = new PersonaFisicaJuridica();
|
||||||
|
$destinatario
|
||||||
|
->setNombreRazon('Test Recipient Company')
|
||||||
|
->setNif('A39200019');
|
||||||
|
$modifiedInvoice->setDestinatarios([$destinatario]);
|
||||||
|
|
||||||
</sum:ModificacionFactura>
|
// Add desglose to modified invoice
|
||||||
</soapenv:Body>
|
$desglose = new Desglose();
|
||||||
</soapenv:Envelope>
|
$desglose->setDesgloseFactura([
|
||||||
XML;
|
'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
|
// Calculate the correct hash using AEAT's specified format
|
||||||
$correctHash = $this->calculateVerifactuHash(
|
$correctHash = $this->calculateVerifactuHash(
|
||||||
|
|
@ -499,11 +499,14 @@ $invoice->setDestinatarios($destinatarios);
|
||||||
$currentTimestamp // FechaHoraHusoGenRegistro (current time)
|
$currentTimestamp // FechaHoraHusoGenRegistro (current time)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Replace the placeholder with the correct hash
|
// Update the modification record with the correct hash
|
||||||
$soapXml = str_replace('PLACEHOLDER_HUELLA', $correctHash, $soapXml);
|
$modification->getRegistroModificacion()->setHuella($correctHash);
|
||||||
|
|
||||||
nlog('Calculated hash for XML: ' . $correctHash);
|
nlog('Calculated hash for XML: ' . $correctHash);
|
||||||
|
|
||||||
|
// Generate SOAP envelope
|
||||||
|
$soapXml = $modification->toSoapEnvelope();
|
||||||
|
|
||||||
// Sign the XML before sending
|
// Sign the XML before sending
|
||||||
$certPath = storage_path('aeat-cert5.pem');
|
$certPath = storage_path('aeat-cert5.pem');
|
||||||
$keyPath = storage_path('aeat-key5.pem');
|
$keyPath = storage_path('aeat-key5.pem');
|
||||||
|
|
@ -538,14 +541,12 @@ $invoice->setDestinatarios($destinatarios);
|
||||||
|
|
||||||
$this->assertTrue($response->successful());
|
$this->assertTrue($response->successful());
|
||||||
|
|
||||||
|
|
||||||
$responseProcessor = new ResponseProcessor();
|
$responseProcessor = new ResponseProcessor();
|
||||||
$responseProcessor->processResponse($response->body());
|
$responseProcessor->processResponse($response->body());
|
||||||
|
|
||||||
nlog($responseProcessor->getSummary());
|
nlog($responseProcessor->getSummary());
|
||||||
|
|
||||||
$this->assertTrue($responseProcessor->getSummary()['success']);
|
$this->assertTrue($responseProcessor->getSummary()['success']);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue