From aa918f7ec0c2e8fa69d68882ca0c25f46a5fe129 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 8 Aug 2025 09:01:31 +1000 Subject: [PATCH] Verifactu initial invoice creation --- app/Models/Company.php | 2 +- .../EDocument/Standards/Verifactu.php | 76 ++++++++- .../Verifactu/Models/RegistroAnterior.php | 157 ++++++++++++++++++ .../EInvoice/Verifactu/Models/WSTest.php | 92 ++++++++-- 4 files changed, 310 insertions(+), 17 deletions(-) create mode 100644 app/Services/EDocument/Standards/Verifactu/Models/RegistroAnterior.php diff --git a/app/Models/Company.php b/app/Models/Company.php index ff78383555..12548843e0 100644 --- a/app/Models/Company.php +++ b/app/Models/Company.php @@ -430,7 +430,7 @@ class Company extends BaseModel public function verifactu_logs(): HasMany { - return $this->hasMany(VerifactuLog::class); + return $this->hasMany(VerifactuLog::class)->orderBy('id', 'DESC'); } public function task_schedulers(): HasMany diff --git a/app/Services/EDocument/Standards/Verifactu.php b/app/Services/EDocument/Standards/Verifactu.php index 19fb41f5f0..06c31377e6 100644 --- a/app/Services/EDocument/Standards/Verifactu.php +++ b/app/Services/EDocument/Standards/Verifactu.php @@ -11,15 +11,17 @@ namespace App\Services\EDocument\Standards; -use App\DataMapper\Tax\BaseRule; use App\Models\Company; use App\Models\Invoice; use App\Models\Product; use App\Helpers\Invoice\Taxer; +use App\DataMapper\Tax\BaseRule; use App\Services\AbstractService; use App\Helpers\Invoice\InvoiceSum; use App\Utils\Traits\NumberFormatter; use App\Helpers\Invoice\InvoiceSumInclusive; +use App\Services\EDocument\Standards\Verifactu\Models\RegistroAnterior; +use App\Services\EDocument\Standards\Verifactu\Models\SistemaInformatico; use App\Services\EDocument\Standards\Verifactu\Models\Invoice as VerifactuInvoice; class Verifactu extends AbstractService @@ -98,7 +100,7 @@ class Verifactu extends AbstractService $this->v_invoice ->setIdVersion('1.0') ->setIdFactura($this->invoice->number) //invoice number - ->setNombreRazonEmisor($this->company->present()->name())) //company name + ->setNombreRazonEmisor($this->company->present()->name()) //company name ->setTipoFactura($this->calculateInvoiceType()) //invoice type ->setDescripcionOperacion('')// Not manadatory - max chars 500 ->setCuotaTotal($this->invoice->total_taxes) //total taxes @@ -130,14 +132,14 @@ class Verifactu extends AbstractService $desglose = new Desglose(); //Combine the line taxes with invoice taxes here to get a total tax amount - $taxes = array_merge($calc->getTaxMap()->merge($calc->getTotalTaxMap())->toArray()); + $taxes = $calc->getTaxMap(); $desglose_iva = []; foreach ($taxes as $tax) { $desglose_iva[] = [ - 'Impuesto' => '01' //tax type + 'Impuesto' => $this->calculateTaxType($tax['name']), //tax type 'ClaveRegimen' => '01', //tax regime classification code 'CalificacionOperacion' => 'S1', //operation classification code 'BaseImponibleOimporteNoSujeto' => $tax['base_amount'] ?? $this->calc->getNetSubtotal(), // taxable base amount @@ -148,9 +150,73 @@ class Verifactu extends AbstractService }; $desglose->setDesgloseIVA($desglose_iva); - $invoice->setDesglose($desglose); + + $this->v_invoice->setDesglose($desglose); + + // Encadenamiento + $encadenamiento = new Encadenamiento(); + + // Get the previous invoice log + $v_log = $this->company->verifactu_logs()->first(); + + // We chain the previous hash to the current invoice to ensure consistency + if($v_log){ + + $registro_anterior = new RegistroAnterior(); + $registro_anterior->setIDEmisorFactura($v_log->nif); + $registro_anterior->setNumSerieFactura($v_log->invoice_number); + $registro_anterior->setFechaExpedicionFactura($v_log->date->format('d-m-Y')); + $registro_anterior->setHuella($v_log->hash); + + $encadenamiento->setRegistroAnterior($registro_anterior); + + } + else { + + $encadenamiento->setPrimerRegistro('S'); + + } + + $this->v_invoice->setEncadenamiento($encadenamiento); + + //Sending system information - We automatically generate the obligado emision from this later + $sistema = new SistemaInformatico(); + $sistema + ->setNombreRazon('Invoice Ninja') + ->setNif(config('services.verifactu.sender_nif')) + ->setNombreSistemaInformatico('Invoice Ninja') + ->setIdSistemaInformatico('01') + ->setVersion('1.0') + ->setNumeroInstalacion('01') + ->setTipoUsoPosibleSoloVerifactu('S') + ->setTipoUsoPosibleMultiOT('S') + ->setIndicadorMultiplesOT('S'); + + $this->v_invoice->setSistemaInformatico($sistema); + return $this; + } + + private function calculateTaxType(string $tax_name): string + { + if(stripos($tax_name, 'iva') !== false) { + return '01'; + } + + if(stripos($tax_name, 'igic') !== false) { + return '03'; + } + + if(stripos($tax_name, 'ipsi') !== false) { + return '02'; + } + + if(stripos($tax_name, 'otros') !== false) { + return '05'; + } + + return '01'; } private function calculateInvoiceType(): string diff --git a/app/Services/EDocument/Standards/Verifactu/Models/RegistroAnterior.php b/app/Services/EDocument/Standards/Verifactu/Models/RegistroAnterior.php new file mode 100644 index 0000000000..082c4dbed8 --- /dev/null +++ b/app/Services/EDocument/Standards/Verifactu/Models/RegistroAnterior.php @@ -0,0 +1,157 @@ +createElement($doc, 'RegistroAnterior'); + + $root->appendChild($this->createElement($doc, 'IDEmisorFactura', $this->idEmisorFactura)); + $root->appendChild($this->createElement($doc, 'NumSerieFactura', $this->numSerieFactura)); + $root->appendChild($this->createElement($doc, 'FechaExpedicionFactura', $this->fechaExpedicionFactura)); + $root->appendChild($this->createElement($doc, 'Huella', $this->huella)); + + return $root; + } + + public static function fromDOMElement(\DOMElement $element): self + { + $registroAnterior = new self(); + + // Handle IDEmisorFactura + $idEmisorFactura = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'IDEmisorFactura')->item(0); + if ($idEmisorFactura) { + $registroAnterior->setIdEmisorFactura($idEmisorFactura->nodeValue); + } + + // Handle NumSerieFactura + $numSerieFactura = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'NumSerieFactura')->item(0); + if ($numSerieFactura) { + $registroAnterior->setNumSerieFactura($numSerieFactura->nodeValue); + } + + // Handle FechaExpedicionFactura + $fechaExpedicionFactura = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'FechaExpedicionFactura')->item(0); + if ($fechaExpedicionFactura) { + $registroAnterior->setFechaExpedicionFactura($fechaExpedicionFactura->nodeValue); + } + + // Handle Huella + $huella = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'Huella')->item(0); + if ($huella) { + $registroAnterior->setHuella($huella->nodeValue); + } + + return $registroAnterior; + } + + public static function fromXml($xml): self + { + if ($xml instanceof \DOMElement) { + return static::fromDOMElement($xml); + } + + if (!is_string($xml)) { + throw new \InvalidArgumentException('Input must be either a string or DOMElement'); + } + + // Enable user error handling for XML parsing + $previousErrorSetting = libxml_use_internal_errors(true); + + try { + $doc = new \DOMDocument(); + if (!$doc->loadXML($xml)) { + $errors = libxml_get_errors(); + libxml_clear_errors(); + throw new \DOMException('Failed to load XML: ' . ($errors ? $errors[0]->message : 'Invalid XML format')); + } + return static::fromDOMElement($doc->documentElement); + } finally { + // Restore previous error handling setting + libxml_use_internal_errors($previousErrorSetting); + } + } + + /** + * Get the NIF of the invoice issuer from the previous record + */ + public function getIdEmisorFactura(): string + { + return $this->idEmisorFactura; + } + + /** + * Set the NIF of the invoice issuer from the previous record + */ + public function setIdEmisorFactura(string $idEmisorFactura): self + { + $this->idEmisorFactura = $idEmisorFactura; + return $this; + } + + /** + * Get the invoice number from the previous record + */ + public function getNumSerieFactura(): string + { + return $this->numSerieFactura; + } + + /** + * Set the invoice number from the previous record + */ + public function setNumSerieFactura(string $numSerieFactura): self + { + $this->numSerieFactura = $numSerieFactura; + return $this; + } + + /** + * Get the invoice issue date from the previous record + */ + public function getFechaExpedicionFactura(): string + { + return $this->fechaExpedicionFactura; + } + + /** + * Set the invoice issue date from the previous record + * + * @param string $fechaExpedicionFactura Date in DD-MM-YYYY format + */ + public function setFechaExpedicionFactura(string $fechaExpedicionFactura): self + { + $this->fechaExpedicionFactura = $fechaExpedicionFactura; + return $this; + } + + /** + * Get the digital fingerprint/hash from the previous record + */ + public function getHuella(): string + { + return $this->huella; + } + + /** + * Set the digital fingerprint/hash from the previous record + */ + public function setHuella(string $huella): self + { + $this->huella = $huella; + return $this; + } +} \ No newline at end of file diff --git a/tests/Feature/EInvoice/Verifactu/Models/WSTest.php b/tests/Feature/EInvoice/Verifactu/Models/WSTest.php index 9bd07ed14d..998d2d1307 100644 --- a/tests/Feature/EInvoice/Verifactu/Models/WSTest.php +++ b/tests/Feature/EInvoice/Verifactu/Models/WSTest.php @@ -38,6 +38,84 @@ class WSTest extends TestCase $this->assertTrue($success); } + +//@todo - need to confirm that building the xml and sending works. + public function test_verifactu_invoice_model_can_build_xml() + { + + // Generate current timestamp in the correct format + $currentTimestamp = now()->setTimezone('Europe/Madrid')->format('Y-m-d\TH:i:s'); + + nlog($currentTimestamp); + + $invoice = new Invoice(); + $invoice + ->setIdVersion('1.0') + ->setIdFactura('FAC2023002') + ->setFechaExpedicionFactura('02-01-2025') + ->setRefExterna('REF-123') + ->setNombreRazonEmisor('Empresa Ejemplo SL') + ->setTipoFactura('F1') + ->setDescripcionOperacion('Venta de productos varios') + ->setCuotaTotal(210.00) + ->setImporteTotal(1000.00) + ->setFechaHoraHusoGenRegistro($currentTimestamp) + ->setTipoHuella('01') + ->setHuella('PLACEHOLDER_HUELLA'); + // Add emitter + $emisor = new PersonaFisicaJuridica(); + $emisor + ->setNif('A39200019') + ->setRazonSocial('Empresa Ejemplo SL'); + $invoice->setTercero($emisor); + + // Add breakdown + $desglose = new Desglose(); + $desglose->setDesgloseFactura([ + 'Impuesto' => '01', + 'ClaveRegimen' => '01', + 'CalificacionOperacion' => 'S1', + 'BaseImponibleOimporteNoSujeto' => 1000.00, + 'TipoImpositivo' => 21, + 'CuotaRepercutida' => 210.00 + ]); + $invoice->setDesglose($desglose); + + +$destinatarios = []; +$destinatario = new PersonaFisicaJuridica(); + +$destinatario + ->setNif('A39200020') + ->setNombreRazon('Empresa Ejemplo SL VV'); + +$destinatarios[] = $destinatario; + +$invoice->setDestinatarios($destinatarios); + + // Add information system + $sistema = new SistemaInformatico(); + $sistema + ->setNombreRazon('Sistema de Facturación') + ->setNif('A39200019') + ->setNombreSistemaInformatico('SistemaFacturacion') + ->setIdSistemaInformatico('01') + ->setVersion('1.0') + ->setNumeroInstalacion('INST-001'); + $invoice->setSistemaInformatico($sistema); + + // Add chain + $encadenamiento = new Encadenamiento(); + $encadenamiento->setPrimerRegistro('S'); + $invoice->setEncadenamiento($encadenamiento); + + $soapXml = $invoice->toSoapEnvelope(); + + $this->assertNotNull($soapXml); + + nlog($soapXml); + } + //@todo - need to confirm that building the xml and sending works. public function test_generated_invoice_xml_can_send_to_web_service() { @@ -65,17 +143,6 @@ class WSTest extends TestCase ->setTipoHuella('01') ->setHuella('PLACEHOLDER_HUELLA'); - - -// -// -// -// CERTIFICADO FISICA PRUEBAS -// 99999910G -// -// - - // Add emitter $emisor = new PersonaFisicaJuridica(); $emisor @@ -83,6 +150,9 @@ class WSTest extends TestCase ->setRazonSocial('Empresa Ejemplo SL'); $invoice->setTercero($emisor); + + + // Add breakdown $desglose = new Desglose(); $desglose->setDesgloseFactura([