XmlInterface

This commit is contained in:
David Bomba 2025-08-08 13:17:06 +10:00
parent 14fd4063f5
commit b94316dbed
12 changed files with 337 additions and 192 deletions

View File

@ -402,7 +402,7 @@ class Invoice extends BaseModel
public function verifactu_logs(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(VerifactuLog::class);
return $this->hasMany(VerifactuLog::class)->orderBy('id', 'desc');
}
public function tasks(): \Illuminate\Database\Eloquent\Relations\HasMany

View File

@ -3,6 +3,7 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Services\EDocument\Standards\Verifactu\Models\Invoice;
/**
* @property int $id
@ -15,6 +16,7 @@ use Illuminate\Database\Eloquent\Model;
* @property string $previous_hash
* @property string $status
* @property object|null $response
* @property string $state
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property-read \App\Models\Company $company
@ -38,4 +40,9 @@ class VerifactuLog extends Model
{
return $this->belongsTo(Invoice::class);
}
public function deserialize()
{
return Invoice::unserialize($this->state);
}
}

View File

@ -1,5 +1,15 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Services\EDocument\Standards\Validation;
/**

View File

@ -25,70 +25,16 @@ 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\PersonaFisicaJuridica;
use App\Services\EDocument\Standards\Verifactu\RegistroAlta;
use App\Services\EDocument\Standards\Verifactu\RegistroModificacion;
use App\Services\EDocument\Standards\Verifactu\Models\Invoice as VerifactuInvoice;
use App\Models\VerifactuLog;
use App\Services\EDocument\Standards\Verifactu\Models\InvoiceModification;
class Verifactu extends AbstractService
{
use Taxer;
use NumberFormatter;
private Company $company;
private InvoiceSum | InvoiceSumInclusive $calc;
private VerifactuInvoice $v_invoice;
private array $tax_map = [];
private float $allowance_total = 0;
private array $errors = [];
private string $current_timestamp;
private array $impuesto_codes = [
'01' => 'IVA (Impuesto sobre el Valor Añadido)', // Value Added Tax - Standard Spanish VAT
'02' => 'IPSI (Impuesto sobre la Producción, los Servicios y la Importación)', // Production, Services and Import Tax - Ceuta and Melilla
'03' => 'IGIC (Impuesto General Indirecto Canario)', // Canary Islands General Indirect Tax
'05' => 'Otros (Others)' // Other taxes
];
private array $clave_regimen_codes = [
'01' => 'Régimen General', // General Regime - Standard VAT regime for most businesses
'02' => 'Régimen Simplificado', // Simplified Regime - For small businesses with simplified accounting
'03' => 'Régimen Especial de Agrupaciones de Módulos', // Special Module Grouping Regime - For agricultural activities
'04' => 'Régimen Especial del Recargo de Equivalencia', // Special Equivalence Surcharge Regime - For retailers
'05' => 'Régimen Especial de las Agencias de Viajes', // Special Travel Agencies Regime
'06' => 'Régimen Especial de los Bienes Usados', // Special Used Goods Regime
'07' => 'Régimen Especial de los Objetos de Arte', // Special Art Objects Regime
'08' => 'Régimen Especial de las Antigüedades', // Special Antiques Regime
'09' => 'Régimen Especial de los Objetos de Colección', // Special Collectibles Regime
'10' => 'Régimen Especial de los Bienes de Inversión', // Special Investment Goods Regime
'11' => 'Régimen Especial de los Servicios', // Special Services Regime
'12' => 'Régimen Especial de los Bienes de Inversión y Servicios', // Special Investment Goods and Services Regime
'13' => 'Régimen Especial de los Bienes de Inversión y Servicios (Inversión del Sujeto Pasivo)', // Special Investment Goods and Services Regime (Reverse Charge)
'14' => 'Régimen Especial de los Bienes de Inversión y Servicios (Inversión del Sujeto Pasivo - Bienes de Inversión)', // Special Investment Goods and Services Regime (Reverse Charge - Investment Goods)
'15' => 'Régimen Especial de los Bienes de Inversión y Servicios (Inversión del Sujeto Pasivo - Servicios)', // Special Investment Goods and Services Regime (Reverse Charge - Services)
'16' => 'Régimen Especial de los Bienes de Inversión y Servicios (Inversión del Sujeto Pasivo - Bienes de Inversión y Servicios)', // Special Investment Goods and Services Regime (Reverse Charge - Investment Goods and Services)
'17' => 'Régimen Especial de los Bienes de Inversión y Servicios (Inversión del Sujeto Pasivo - Bienes de Inversión y Servicios - Inversión del Sujeto Pasivo)', // Special Investment Goods and Services Regime (Reverse Charge - Investment Goods and Services - Reverse Charge)
'18' => 'Régimen Especial de los Bienes de Inversión y Servicios (Inversión del Sujeto Pasivo - Bienes de Inversión y Servicios - Inversión del Sujeto Pasivo - Bienes de Inversión)', // Special Investment Goods and Services Regime (Reverse Charge - Investment Goods and Services - Reverse Charge - Investment Goods)
'19' => 'Régimen Especial de los Bienes de Inversión y Servicios (Inversión del Sujeto Pasivo - Bienes de Inversión y Servicios - Inversión del Sujeto Pasivo - Servicios)', // Special Investment Goods and Services Regime (Reverse Charge - Investment Goods and Services - Reverse Charge - Services)
'20' => 'Régimen Especial de los Bienes de Inversión y Servicios (Inversión del Sujeto Pasivo - Bienes de Inversión y Servicios - Inversión del Sujeto Pasivo - Bienes de Inversión y Servicios)' // Special Investment Goods and Services Regime (Reverse Charge - Investment Goods and Services - Reverse Charge - Investment Goods and Services)
];
private array $calificacion_operacion_codes = [
'S1' => 'OPERACIÓN SUJETA Y NO EXENTA - SIN INVERSIÓN DEL SUJETO PASIVO', // Subject and Non-Exempt Operation - Without Reverse Charge
'S2' => 'OPERACIÓN SUJETA Y NO EXENTA - CON INVERSIÓN DEL SUJETO PASIVO', // Subject and Non-Exempt Operation - With Reverse Charge
'N1' => 'OPERACIÓN NO SUJETA ARTÍCULO 7, 14, OTROS', // Non-Subject Operation Article 7, 14, Others
'N2' => 'OPERACIÓN NO SUJETA POR REGLAS DE LOCALIZACIÓN' // Non-Subject Operation by Location Rules
];
public function __construct(public Invoice $invoice)
{
$this->company = $invoice->company;
$this->calc = $this->invoice->calc();
$this->v_invoice = new VerifactuInvoice();
{
}
/**
@ -99,129 +45,20 @@ class Verifactu extends AbstractService
public function run(): self
{
$this->current_timestamp = now()->setTimezone('Europe/Madrid')->format('Y-m-d\TH:i:s');
$v_logs = $this->invoice->verifactu_logs;
$this->v_invoice
->setIdVersion('1.0')
->setIdFactura($this->invoice->number) //invoice number
->setNombreRazonEmisor($this->company->present()->name()) //company name
->setTipoFactura('F1') //invoice type
->setDescripcionOperacion('')// Not 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');
//determine the current status of the invoice.
$document = new RegistroAlta($this->invoice);
/** The business entity that is issuing the invoice */
$emisor = new PersonaFisicaJuridica();
$emisor->setNif($this->company->settings->vat_number)
->setNombreRazon($this->invoice->company->present()->name());
$this->v_invoice->setTercero($emisor);
/** The business entity (Client) that is receiving the invoice */
$destinatarios = [];
$destinatario = new PersonaFisicaJuridica();
$destinatario
->setNif($this->invoice->client->vat_number)
->setNombreRazon($this->invoice->client->present()->name());
$destinatarios[] = $destinatario;
$this->v_invoice->setDestinatarios($destinatarios);
// The tax breakdown
$desglose = new Desglose();
//Combine the line taxes with invoice taxes here to get a total tax amount
$taxes = $this->calc->getTaxMap();
$desglose_iva = [];
foreach ($taxes as $tax) {
$desglose_iva[] = [
'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
'TipoImpositivo' => $tax['tax_rate'], // Tax Rate
'CuotaRepercutida' => $tax['total'] // Tax Amount
];
};
$desglose->setDesgloseIVA($desglose_iva);
$this->v_invoice->setDesglose($desglose);
// Encadenamiento
$encadenamiento = new Encadenamiento();
// Get the previous invoice log
/** @var ?VerifactuLog $v_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');
//1. new => RegistraAlta
if($v_logs->count() >= 1){
$v_log = $v_logs->first();
$document = InvoiceModification::fromInvoice($this->invoice, $v_log->deserialize());
}
$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';
//3. cancelled => RegistroAnulacion
}
}

View File

@ -1,12 +1,22 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Services\EDocument\Standards\Verifactu\Models;
use RobRichards\XMLSecLibs\XMLSecurityDSig;
use RobRichards\XMLSecLibs\XMLSecurityKey;
use Illuminate\Support\Facades\Log;
class Invoice extends BaseXmlModel
class Invoice extends BaseXmlModel implements XmlModelInterface
{
protected string $idVersion;
protected string $idFactura;
@ -1202,4 +1212,20 @@ class Invoice extends BaseXmlModel
return $modificationRecord;
}
public function serialize()
{
return serialize($this);
}
public static function unserialize(string $data): self
{
$object = unserialize($data);
if (!$object instanceof self) {
throw new \InvalidArgumentException('Invalid serialized data - not an Invoice object');
}
return $object;
}
}

View File

@ -8,7 +8,7 @@ namespace App\Services\EDocument\Standards\Verifactu\Models;
* This class represents the complete modification structure required for Verifactu e-invoicing
* modification operations. It contains both the cancellation record and the modification record.
*/
class InvoiceModification extends BaseXmlModel
class InvoiceModification extends BaseXmlModel implements XmlModelInterface
{
protected RegistroAnulacion $registroAnulacion;
protected RegistroModificacion $registroModificacion;

View File

@ -0,0 +1,22 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Services\EDocument\Standards\Verifactu\Models;
interface XmlModelInterface
{
public function toXmlString(): string;
public function toXml(\DOMDocument $doc): \DOMElement;
public function toSoapEnvelope(): string;
}

View File

@ -0,0 +1,230 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Services\EDocument\Standards\Verifactu;
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\Desglose;
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\PersonaFisicaJuridica;
use App\Services\EDocument\Standards\Verifactu\Models\Invoice as VerifactuInvoice;
use App\Models\VerifactuLog;
class RegistroAlta
{
use Taxer;
use NumberFormatter;
private Company $company;
private InvoiceSum | InvoiceSumInclusive $calc;
private VerifactuInvoice $v_invoice;
private ?VerifactuLog $v_log;
private array $tax_map = [];
private float $allowance_total = 0;
private array $errors = [];
private string $current_timestamp;
private array $impuesto_codes = [
'01' => 'IVA (Impuesto sobre el Valor Añadido)', // Value Added Tax - Standard Spanish VAT
'02' => 'IPSI (Impuesto sobre la Producción, los Servicios y la Importación)', // Production, Services and Import Tax - Ceuta and Melilla
'03' => 'IGIC (Impuesto General Indirecto Canario)', // Canary Islands General Indirect Tax
'05' => 'Otros (Others)' // Other taxes
];
private array $clave_regimen_codes = [
'01' => 'Régimen General', // General Regime - Standard VAT regime for most businesses
'02' => 'Régimen Simplificado', // Simplified Regime - For small businesses with simplified accounting
'03' => 'Régimen Especial de Agrupaciones de Módulos', // Special Module Grouping Regime - For agricultural activities
'04' => 'Régimen Especial del Recargo de Equivalencia', // Special Equivalence Surcharge Regime - For retailers
'05' => 'Régimen Especial de las Agencias de Viajes', // Special Travel Agencies Regime
'06' => 'Régimen Especial de los Bienes Usados', // Special Used Goods Regime
'07' => 'Régimen Especial de los Objetos de Arte', // Special Art Objects Regime
'08' => 'Régimen Especial de las Antigüedades', // Special Antiques Regime
'09' => 'Régimen Especial de los Objetos de Colección', // Special Collectibles Regime
'10' => 'Régimen Especial de los Bienes de Inversión', // Special Investment Goods Regime
'11' => 'Régimen Especial de los Servicios', // Special Services Regime
'12' => 'Régimen Especial de los Bienes de Inversión y Servicios', // Special Investment Goods and Services Regime
'13' => 'Régimen Especial de los Bienes de Inversión y Servicios (Inversión del Sujeto Pasivo)', // Special Investment Goods and Services Regime (Reverse Charge)
'14' => 'Régimen Especial de los Bienes de Inversión y Servicios (Inversión del Sujeto Pasivo - Bienes de Inversión)', // Special Investment Goods and Services Regime (Reverse Charge - Investment Goods)
'15' => 'Régimen Especial de los Bienes de Inversión y Servicios (Inversión del Sujeto Pasivo - Servicios)', // Special Investment Goods and Services Regime (Reverse Charge - Services)
'16' => 'Régimen Especial de los Bienes de Inversión y Servicios (Inversión del Sujeto Pasivo - Bienes de Inversión y Servicios)', // Special Investment Goods and Services Regime (Reverse Charge - Investment Goods and Services)
'17' => 'Régimen Especial de los Bienes de Inversión y Servicios (Inversión del Sujeto Pasivo - Bienes de Inversión y Servicios - Inversión del Sujeto Pasivo)', // Special Investment Goods and Services Regime (Reverse Charge - Investment Goods and Services - Reverse Charge)
'18' => 'Régimen Especial de los Bienes de Inversión y Servicios (Inversión del Sujeto Pasivo - Bienes de Inversión y Servicios - Inversión del Sujeto Pasivo - Bienes de Inversión)', // Special Investment Goods and Services Regime (Reverse Charge - Investment Goods and Services - Reverse Charge - Investment Goods)
'19' => 'Régimen Especial de los Bienes de Inversión y Servicios (Inversión del Sujeto Pasivo - Bienes de Inversión y Servicios - Inversión del Sujeto Pasivo - Servicios)', // Special Investment Goods and Services Regime (Reverse Charge - Investment Goods and Services - Reverse Charge - Services)
'20' => 'Régimen Especial de los Bienes de Inversión y Servicios (Inversión del Sujeto Pasivo - Bienes de Inversión y Servicios - Inversión del Sujeto Pasivo - Bienes de Inversión y Servicios)' // Special Investment Goods and Services Regime (Reverse Charge - Investment Goods and Services - Reverse Charge - Investment Goods and Services)
];
private array $calificacion_operacion_codes = [
'S1' => 'OPERACIÓN SUJETA Y NO EXENTA - SIN INVERSIÓN DEL SUJETO PASIVO', // Subject and Non-Exempt Operation - Without Reverse Charge
'S2' => 'OPERACIÓN SUJETA Y NO EXENTA - CON INVERSIÓN DEL SUJETO PASIVO', // Subject and Non-Exempt Operation - With Reverse Charge
'N1' => 'OPERACIÓN NO SUJETA ARTÍCULO 7, 14, OTROS', // Non-Subject Operation Article 7, 14, Others
'N2' => 'OPERACIÓN NO SUJETA POR REGLAS DE LOCALIZACIÓN' // Non-Subject Operation by Location Rules
];
public function __construct(public Invoice $invoice)
{
$this->company = $invoice->company;
$this->calc = $this->invoice->calc();
$this->v_invoice = new VerifactuInvoice();
}
/**
* Entry point for building document
*
* @return self
*/
public function run(): self
{
// Get the previous invoice log
/** @var ?VerifactuLog $v_log */
$this->v_log = $this->company->verifactu_logs()->first();
$this->current_timestamp = now()->setTimezone('Europe/Madrid')->format('Y-m-d\TH:i:s');
$this->v_invoice
->setIdVersion('1.0')
->setIdFactura($this->invoice->number) //invoice number
->setNombreRazonEmisor($this->company->present()->name()) //company name
->setTipoFactura('F1') //invoice type
->setDescripcionOperacion('')// Not 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');
/** The business entity that is issuing the invoice */
$emisor = new PersonaFisicaJuridica();
$emisor->setNif($this->company->settings->vat_number)
->setNombreRazon($this->invoice->company->present()->name());
$this->v_invoice->setTercero($emisor);
/** The business entity (Client) that is receiving the invoice */
$destinatarios = [];
$destinatario = new PersonaFisicaJuridica();
$destinatario
->setNif($this->invoice->client->vat_number)
->setNombreRazon($this->invoice->client->present()->name());
$destinatarios[] = $destinatario;
$this->v_invoice->setDestinatarios($destinatarios);
// The tax breakdown
$desglose = new Desglose();
//Combine the line taxes with invoice taxes here to get a total tax amount
$taxes = $this->calc->getTaxMap();
$desglose_iva = [];
foreach ($taxes as $tax) {
$desglose_iva[] = [
'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
'TipoImpositivo' => $tax['tax_rate'], // Tax Rate
'CuotaRepercutida' => $tax['total'] // Tax Amount
];
};
$desglose->setDesgloseIVA($desglose_iva);
$this->v_invoice->setDesglose($desglose);
// Encadenamiento
$encadenamiento = new Encadenamiento();
// We chain the previous hash to the current invoice to ensure consistency
if($this->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';
}
}

View File

@ -23,7 +23,8 @@ return new class extends Migration
$table->string('previous_hash')->nullable();
$table->string('status');
$table->json('response')->nullable();;
$table->json('response')->nullable();
$table->text('state')->nullable();
$table->timestamps();
$table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade')->onUpdate('cascade');

View File

@ -26,6 +26,9 @@ class VerifactuFeatureTest extends TestCase
private $client;
private $faker;
private string $test_company_nif = 'A39200019';
private string $test_client_nif = 'A39200019';
protected function setUp(): void
{
parent::setUp();
@ -61,6 +64,7 @@ class VerifactuFeatureTest extends TestCase
$settings->postal_code = '28001';
$settings->vat_number = 'B12345678'; // Spanish VAT number format
$settings->payment_terms = '10';
$settings->vat_number = $this->test_company_nif;
}
$this->company = Company::factory()->create([
@ -102,7 +106,7 @@ class VerifactuFeatureTest extends TestCase
'state' => 'Madrid',
'postal_code' => '28001',
'country_id' => 724,
'vat_number' => 'B12545678',
'vat_number' => $this->test_client_nif,
'balance' => 0,
'paid_to_date' => 0,
'settings' => $client_settings,
@ -146,7 +150,6 @@ class VerifactuFeatureTest extends TestCase
'line_items' => $line_items,
]);
$invoice = $invoice->calc()
->getInvoice()
->service()

View File

@ -1,5 +1,15 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace Tests\Feature\EInvoice\Verifactu;
use App\Services\EDocument\Standards\Verifactu\Models\Cupon;

View File

@ -301,20 +301,19 @@ class VerifactuModificationTest extends TestCase
$this->assertStringContainsString('42', $soapXml);
$this->assertStringContainsString('242', $soapXml);
$validXml = $modification->toSoapEnvelope();
$validXml = $modification->toSoapEnvelope();
// Use the new VerifactuDocumentValidator
$validator = new \App\Services\EDocument\Standards\Validation\VerifactuDocumentValidator($validXml);
$validator->validate();
$errors = $validator->getVerifactuErrors();
// Use the new VerifactuDocumentValidator
$validator = new \App\Services\EDocument\Standards\Validation\VerifactuDocumentValidator($validXml);
$validator->validate();
$errors = $validator->getVerifactuErrors();
if (!empty($errors)) {
if (!empty($errors)) {
nlog('Verifactu Validation Errors:');
nlog($validXml);
nlog($errors);
}
nlog('Verifactu Validation Errors:');
nlog($validXml);
nlog($errors);
}
}