diff --git a/app/Services/EDocument/Standards/Validation/VerifactuDocumentValidator.php b/app/Services/EDocument/Standards/Validation/VerifactuDocumentValidator.php index 284767b459..8b6144adaf 100644 --- a/app/Services/EDocument/Standards/Validation/VerifactuDocumentValidator.php +++ b/app/Services/EDocument/Standards/Validation/VerifactuDocumentValidator.php @@ -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; } } diff --git a/app/Services/EDocument/Standards/Verifactu.php b/app/Services/EDocument/Standards/Verifactu.php index 7d4b9fe5fd..6f55a7561a 100644 --- a/app/Services/EDocument/Standards/Verifactu.php +++ b/app/Services/EDocument/Standards/Verifactu.php @@ -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 diff --git a/app/Services/EDocument/Standards/Verifactu/Models/Invoice.php b/app/Services/EDocument/Standards/Verifactu/Models/Invoice.php index df110dff9f..2211997c09 100644 --- a/app/Services/EDocument/Standards/Verifactu/Models/Invoice.php +++ b/app/Services/EDocument/Standards/Verifactu/Models/Invoice.php @@ -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,10 +944,10 @@ 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)); } - + // 6. FacturasRectificadas (only for R1 invoices) if ($this->tipoFactura === self::TIPO_FACTURA_RECTIFICATIVA && $this->facturasRectificadas !== null) { $facturasRectificadasElement = $this->createElement($doc, 'FacturasRectificadas'); @@ -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); diff --git a/app/Services/EDocument/Standards/Verifactu/Models/InvoiceCancellation.php b/app/Services/EDocument/Standards/Verifactu/Models/InvoiceCancellation.php deleted file mode 100644 index efa824ac16..0000000000 --- a/app/Services/EDocument/Standards/Verifactu/Models/InvoiceCancellation.php +++ /dev/null @@ -1,332 +0,0 @@ -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; - } -} \ No newline at end of file diff --git a/app/Services/EDocument/Standards/Verifactu/Models/InvoiceModification.php b/app/Services/EDocument/Standards/Verifactu/Models/InvoiceModification.php deleted file mode 100644 index 43d0c9eac7..0000000000 --- a/app/Services/EDocument/Standards/Verifactu/Models/InvoiceModification.php +++ /dev/null @@ -1,376 +0,0 @@ - "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; - // } -} diff --git a/app/Services/EDocument/Standards/Verifactu/Models/RegistroModificacion.php b/app/Services/EDocument/Standards/Verifactu/Models/RegistroModificacion.php deleted file mode 100644 index 384a63b77a..0000000000 --- a/app/Services/EDocument/Standards/Verifactu/Models/RegistroModificacion.php +++ /dev/null @@ -1,679 +0,0 @@ -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; - } -} \ No newline at end of file diff --git a/app/Services/EDocument/Standards/Verifactu/RegistroAlta.php b/app/Services/EDocument/Standards/Verifactu/RegistroAlta.php index 55c1421136..7df104dd8a 100644 --- a/app/Services/EDocument/Standards/Verifactu/RegistroAlta.php +++ b/app/Services/EDocument/Standards/Verifactu/RegistroAlta.php @@ -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; diff --git a/tests/Feature/EInvoice/Verifactu/InvoiceCancellationTest.php b/tests/Feature/EInvoice/Verifactu/InvoiceCancellationTest.php deleted file mode 100644 index 3b6bb2a5ab..0000000000 --- a/tests/Feature/EInvoice/Verifactu/InvoiceCancellationTest.php +++ /dev/null @@ -1,443 +0,0 @@ -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('', $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 = [ - '', - '', - '', - '', - 'INV-2024-001', - '15-01-2024', - 'A39200019', - 'ABCD1234EF5678901234567890ABCDEF1234567890ABCDEF1234567890ABCDEF12', - '', - '', - '', - '02', - 'Factura anulada por error', - '', - '', - '' - ]; - - foreach ($expectedElements as $element) { - $this->assertStringContainsString($element, $xmlString, "XML should contain: $element"); - } - - // Verify XML is properly formatted and indented - $this->assertStringContainsString(' ', $xmlString); - $this->assertStringContainsString(' ', $xmlString); - $this->assertStringContainsString(' ', $xmlString); - $this->assertStringContainsString(' ', $xmlString); - } -} \ No newline at end of file diff --git a/tests/Feature/EInvoice/Verifactu/VerifactuFeatureTest.php b/tests/Feature/EInvoice/Verifactu/VerifactuFeatureTest.php index 9fe746a196..762fde4614 100644 --- a/tests/Feature/EInvoice/Verifactu/VerifactuFeatureTest.php +++ b/tests/Feature/EInvoice/Verifactu/VerifactuFeatureTest.php @@ -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 = << - - - - - - - CERTIFICADO FISICA PRUEBAS - A39200019 - - - - - 1.0 - - A39200019 - TEST0033343444 - 09-08-2025 - - CERTIFICADO FISICA PRUEBAS - R1 - S - - - A39200019 - TEST0033343443 - 09-08-2025 - - - - 100.00 - 21.00 - 0.00 - - Rectificación por error en factura anterior - - - Test Recipient Company - A39200019 - - - - - 01 - 01 - S1 - 21.00 - 97.00 - 20.37 - - - 47.05 - 144.05 - - S - - - CERTIFICADO FISICA PRUEBAS - A39200019 - InvoiceNinja - 77 - 1.0.03 - 383 - N - S - S - - 2025-08-09T23:18:44+02:00 - 01 - E7558C33FE3496551F38FEB582F4879B1D9F6C073489628A8DC275E12298941F - - - - - -XML; + public function test_rectification_invoice() + { + $soapXml = << + + + + + + CERTIFICADO FISICA PRUEBAS + A39200019 + + + + + 1.0 + + A39200019 + TEST0033343460-R1 + 10-08-2025 + + CERTIFICADO FISICA PRUEBAS + F3 + + + A39200019 + TEST0033343460 + 10-08-2025 + + + Alta + + + CERTIFICADO FISICA PRUEBAS + A39200019 + + + + + 01 + 01 + S1 + 21.00 + 100.00 + 21.00 + + + 21 + 121 + + + A39200019 + TEST0033343459 + 10-08-2025 + 1FB6B4EF72DD2A07CC23B3F9D74EE5749C8E86B34B9B1DFFFC8C3E46ACA87E21 + + + + CERTIFICADO FISICA PRUEBAS + A39200019 + InvoiceNinja + 77 + 1.0.03 + 383 + N + S + S + + 2025-08-10T05:02:18+00:00 + 01 + BC61C7CB7CB09917C076CAE7D066B3E2CF521A3B8B501D0C83250B5EB4A4B40D + + + + + + + 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(); diff --git a/tests/Feature/EInvoice/Verifactu/VerifactuModificationTest.php b/tests/Feature/EInvoice/Verifactu/VerifactuModificationTest.php deleted file mode 100644 index 0582dd1f9f..0000000000 --- a/tests/Feature/EInvoice/Verifactu/VerifactuModificationTest.php +++ /dev/null @@ -1,671 +0,0 @@ -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', $soapXml); - $this->assertStringContainsString('sum1:TipoRectificativa>S', $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', $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', $soapXml); - $this->assertStringContainsString('sum1:TipoRectificativa>S', $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', $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('assertStringContainsString('assertStringContainsString('assertStringContainsString('assertStringContainsString('assertStringContainsString('R1', $soapXml); - - // Verify modification structure (no cancellation block needed) - $this->assertStringContainsString('Modified invoice', $soapXml); - $this->assertStringContainsString('242', $soapXml); - $this->assertStringContainsString('42', $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); - } - - } -} \ No newline at end of file diff --git a/tests/Feature/EInvoice/Verifactu/WSTest.php b/tests/Feature/EInvoice/Verifactu/WSTest.php index a97b4bde23..85f899fafe 100644 --- a/tests/Feature/EInvoice/Verifactu/WSTest.php +++ b/tests/Feature/EInvoice/Verifactu/WSTest.php @@ -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