Verifactu initial invoice creation

This commit is contained in:
David Bomba 2025-08-08 09:01:31 +10:00
parent 33078ee86c
commit aa918f7ec0
4 changed files with 310 additions and 17 deletions

View File

@ -430,7 +430,7 @@ class Company extends BaseModel
public function verifactu_logs(): HasMany public function verifactu_logs(): HasMany
{ {
return $this->hasMany(VerifactuLog::class); return $this->hasMany(VerifactuLog::class)->orderBy('id', 'DESC');
} }
public function task_schedulers(): HasMany public function task_schedulers(): HasMany

View File

@ -11,15 +11,17 @@
namespace App\Services\EDocument\Standards; namespace App\Services\EDocument\Standards;
use App\DataMapper\Tax\BaseRule;
use App\Models\Company; use App\Models\Company;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\Product; use App\Models\Product;
use App\Helpers\Invoice\Taxer; use App\Helpers\Invoice\Taxer;
use App\DataMapper\Tax\BaseRule;
use App\Services\AbstractService; use App\Services\AbstractService;
use App\Helpers\Invoice\InvoiceSum; use App\Helpers\Invoice\InvoiceSum;
use App\Utils\Traits\NumberFormatter; use App\Utils\Traits\NumberFormatter;
use App\Helpers\Invoice\InvoiceSumInclusive; use App\Helpers\Invoice\InvoiceSumInclusive;
use App\Services\EDocument\Standards\Verifactu\Models\RegistroAnterior;
use App\Services\EDocument\Standards\Verifactu\Models\SistemaInformatico;
use App\Services\EDocument\Standards\Verifactu\Models\Invoice as VerifactuInvoice; use App\Services\EDocument\Standards\Verifactu\Models\Invoice as VerifactuInvoice;
class Verifactu extends AbstractService class Verifactu extends AbstractService
@ -98,7 +100,7 @@ class Verifactu extends AbstractService
$this->v_invoice $this->v_invoice
->setIdVersion('1.0') ->setIdVersion('1.0')
->setIdFactura($this->invoice->number) //invoice number ->setIdFactura($this->invoice->number) //invoice number
->setNombreRazonEmisor($this->company->present()->name())) //company name ->setNombreRazonEmisor($this->company->present()->name()) //company name
->setTipoFactura($this->calculateInvoiceType()) //invoice type ->setTipoFactura($this->calculateInvoiceType()) //invoice type
->setDescripcionOperacion('')// Not manadatory - max chars 500 ->setDescripcionOperacion('')// Not manadatory - max chars 500
->setCuotaTotal($this->invoice->total_taxes) //total taxes ->setCuotaTotal($this->invoice->total_taxes) //total taxes
@ -130,14 +132,14 @@ class Verifactu extends AbstractService
$desglose = new Desglose(); $desglose = new Desglose();
//Combine the line taxes with invoice taxes here to get a total tax amount //Combine the line taxes with invoice taxes here to get a total tax amount
$taxes = array_merge($calc->getTaxMap()->merge($calc->getTotalTaxMap())->toArray()); $taxes = $calc->getTaxMap();
$desglose_iva = []; $desglose_iva = [];
foreach ($taxes as $tax) { foreach ($taxes as $tax) {
$desglose_iva[] = [ $desglose_iva[] = [
'Impuesto' => '01' //tax type 'Impuesto' => $this->calculateTaxType($tax['name']), //tax type
'ClaveRegimen' => '01', //tax regime classification code 'ClaveRegimen' => '01', //tax regime classification code
'CalificacionOperacion' => 'S1', //operation classification code 'CalificacionOperacion' => 'S1', //operation classification code
'BaseImponibleOimporteNoSujeto' => $tax['base_amount'] ?? $this->calc->getNetSubtotal(), // taxable base amount 'BaseImponibleOimporteNoSujeto' => $tax['base_amount'] ?? $this->calc->getNetSubtotal(), // taxable base amount
@ -148,9 +150,73 @@ class Verifactu extends AbstractService
}; };
$desglose->setDesgloseIVA($desglose_iva); $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 private function calculateInvoiceType(): string

View File

@ -0,0 +1,157 @@
<?php
namespace App\Services\EDocument\Standards\Verifactu\Models;
/**
* RegistroAnterior - Previous Record Information
*
* This class represents the previous record information required for Verifactu e-invoicing
* chain linking. It contains the details of the previous invoice in the chain.
*/
class RegistroAnterior extends BaseXmlModel
{
protected string $idEmisorFactura;
protected string $numSerieFactura;
protected string $fechaExpedicionFactura;
protected string $huella;
public function toXml(\DOMDocument $doc): \DOMElement
{
$root = $this->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;
}
}

View File

@ -38,6 +38,84 @@ class WSTest extends TestCase
$this->assertTrue($success); $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. //@todo - need to confirm that building the xml and sending works.
public function test_generated_invoice_xml_can_send_to_web_service() public function test_generated_invoice_xml_can_send_to_web_service()
{ {
@ -65,17 +143,6 @@ class WSTest extends TestCase
->setTipoHuella('01') ->setTipoHuella('01')
->setHuella('PLACEHOLDER_HUELLA'); ->setHuella('PLACEHOLDER_HUELLA');
// <sum:Cabecera>
// <!-- ObligadoEmision: The computer system submitting on behalf of the invoice issuer -->
// <sum1:ObligadoEmision>
// <sum1:NombreRazon>CERTIFICADO FISICA PRUEBAS</sum1:NombreRazon>
// <sum1:NIF>99999910G</sum1:NIF>
// </sum1:ObligadoEmision>
// </sum:Cabecera>
// Add emitter // Add emitter
$emisor = new PersonaFisicaJuridica(); $emisor = new PersonaFisicaJuridica();
$emisor $emisor
@ -83,6 +150,9 @@ class WSTest extends TestCase
->setRazonSocial('Empresa Ejemplo SL'); ->setRazonSocial('Empresa Ejemplo SL');
$invoice->setTercero($emisor); $invoice->setTercero($emisor);
// Add breakdown // Add breakdown
$desglose = new Desglose(); $desglose = new Desglose();
$desglose->setDesgloseFactura([ $desglose->setDesgloseFactura([