From 0a7744a70e0564034237ecd7cadc035bade7cc58 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 14 Aug 2025 10:07:32 +1000 Subject: [PATCH] Add IDOtro class --- app/Http/Controllers/EInvoiceController.php | 2 +- .../Requests/Company/UpdateCompanyRequest.php | 4 +- .../EInvoice/ValidateEInvoiceRequest.php | 22 +++ .../Invoice/VerifactuAmountCheck.php | 9 +- app/Models/Company.php | 2 +- app/Repositories/BaseRepository.php | 2 +- .../Validation/Verifactu/EntityLevel.php | 82 +++++++++++- .../Standards/Verifactu/Models/IDOtro.php | 125 ++++++++++++++++++ .../Models/PersonaFisicaJuridica.php | 18 ++- .../Standards/Verifactu/RegistroAlta.php | 30 +++-- .../Standards/Verifactu/SendToAeat.php | 10 +- lang/en/texts.php | 1 + .../EInvoice/Verifactu/VerifactuApiTest.php | 57 ++++---- 13 files changed, 308 insertions(+), 56 deletions(-) create mode 100644 app/Services/EDocument/Standards/Verifactu/Models/IDOtro.php diff --git a/app/Http/Controllers/EInvoiceController.php b/app/Http/Controllers/EInvoiceController.php index eaebed3b99..c522568611 100644 --- a/app/Http/Controllers/EInvoiceController.php +++ b/app/Http/Controllers/EInvoiceController.php @@ -41,7 +41,7 @@ class EInvoiceController extends BaseController */ public function validateEntity(ValidateEInvoiceRequest $request) { - $el = new EntityLevel(); + $el = $request->getValidatorClass(); $data = []; diff --git a/app/Http/Requests/Company/UpdateCompanyRequest.php b/app/Http/Requests/Company/UpdateCompanyRequest.php index b805d46153..8a22dc400c 100644 --- a/app/Http/Requests/Company/UpdateCompanyRequest.php +++ b/app/Http/Requests/Company/UpdateCompanyRequest.php @@ -205,9 +205,9 @@ class UpdateCompanyRequest extends Request } } - if($this->company->settings->e_invoice_type == 'verifactu') { + if($this->company->settings->e_invoice_type == 'VERIFACTU') { $settings['lock_invoices'] = 'when_sent'; - $settings['e_invoice_type'] = 'verifactu'; + $settings['e_invoice_type'] = 'VERIFACTU'; } } diff --git a/app/Http/Requests/EInvoice/ValidateEInvoiceRequest.php b/app/Http/Requests/EInvoice/ValidateEInvoiceRequest.php index db7455b09f..ac14330549 100644 --- a/app/Http/Requests/EInvoice/ValidateEInvoiceRequest.php +++ b/app/Http/Requests/EInvoice/ValidateEInvoiceRequest.php @@ -16,6 +16,7 @@ use App\Models\Client; use App\Models\Company; use App\Models\Invoice; use App\Http\Requests\Request; +use App\Services\EDocument\Standards\Validation\Peppol\EntityLevel; use Illuminate\Validation\Rule; class ValidateEInvoiceRequest extends Request @@ -91,4 +92,25 @@ class ValidateEInvoiceRequest extends Request return $class::withTrashed()->find(is_string($this->entity_id) ? $this->decodePrimaryKey($this->entity_id) : $this->entity_id); } + + /** + * getValidatorClass + * + * Return the validator class based on the EInvoicing Standard + * + * @return \App\Services\EDocument\Standards\Validation\EntityLevelInterface + */ + public function getValidatorClass() + { + $user = auth()->user(); + + if($user->company()->settings->e_invoice_type == 'VERIFACTU') { + return new \App\Services\EDocument\Standards\Validation\Verifactu\EntityLevel(); + } + + // if($user->company()->settings->e_invoice_type == 'PEPPOL') { + return new \App\Services\EDocument\Standards\Validation\Peppol\EntityLevel(); + // } + + } } diff --git a/app/Http/ValidationRules/Invoice/VerifactuAmountCheck.php b/app/Http/ValidationRules/Invoice/VerifactuAmountCheck.php index 7e40828a18..c14df5d3e6 100644 --- a/app/Http/ValidationRules/Invoice/VerifactuAmountCheck.php +++ b/app/Http/ValidationRules/Invoice/VerifactuAmountCheck.php @@ -43,10 +43,6 @@ class VerifactuAmountCheck implements ValidationRule $client = Client::withTrashed()->find($this->input['client_id']); - if($client->country->iso_3166_2 !== 'ES') { // Client level check if client is in Spain - return; - } - $invoice = false; $child_invoices = false; $child_invoice_totals = 0; @@ -97,7 +93,10 @@ class VerifactuAmountCheck implements ValidationRule $total = $items->sum() - $total_discount; - if($total < 0 && !$invoice) { + if($total > 0 && $invoice) { + $fail("Only negative invoices can be linked to existing invoices {$total}"); + } + elseif($total < 0 && !$invoice) { $fail("Negative invoices {$total} can only be linked to existing invoices"); } elseif($invoice && ($total + $child_invoice_totals + $invoice->amount) < 0) { diff --git a/app/Models/Company.php b/app/Models/Company.php index a1ce95e618..9b16b123d6 100644 --- a/app/Models/Company.php +++ b/app/Models/Company.php @@ -1041,7 +1041,7 @@ class Company extends BaseModel public function verifactuEnabled(): bool { return once(function () { - return $this->getSetting('e_invoice_type') == 'verifactu'; + return $this->getSetting('e_invoice_type') == 'VERIFACTU'; }); } } diff --git a/app/Repositories/BaseRepository.php b/app/Repositories/BaseRepository.php index c7a316aefe..02b3b6b133 100644 --- a/app/Repositories/BaseRepository.php +++ b/app/Repositories/BaseRepository.php @@ -330,7 +330,7 @@ class BaseRepository } /** Verifactu modified invoice check */ - if($model->company->verifactuEnabled() && $client->country->iso_3166_2 === 'ES') { + if($model->company->verifactuEnabled()) { $model->service()->modifyVerifactuWorkflow($data, $this->new_model)->save(); } } diff --git a/app/Services/EDocument/Standards/Validation/Verifactu/EntityLevel.php b/app/Services/EDocument/Standards/Validation/Verifactu/EntityLevel.php index 383cb82820..9b6aadd6b5 100644 --- a/app/Services/EDocument/Standards/Validation/Verifactu/EntityLevel.php +++ b/app/Services/EDocument/Standards/Validation/Verifactu/EntityLevel.php @@ -23,18 +23,20 @@ class EntityLevel implements EntityLevelInterface private array $errors = []; private array $client_fields = [ - 'address1', - 'city', + // 'address1', + // 'city', // 'state', - 'postal_code', + // 'postal_code', + 'vat_number', 'country_id', ]; private array $company_settings_fields = [ - 'address1', - 'city', + // 'address1', + // 'city', // 'state', - 'postal_code', + // 'postal_code', + 'vat_number', 'country_id', ]; @@ -181,4 +183,72 @@ class EntityLevel implements EntityLevelInterface return iconv_strlen($string) >= 1; } + public function isValidSpanishVAT(string $vat): bool + { + $vat = strtoupper(trim($vat)); + + // Quick format check + if (!preg_match('/^[A-Z]\d{7}[A-Z0-9]$|^\d{8}[A-Z]$|^[XYZ]\d{7}[A-Z]$/', $vat)) { + return false; + } + + // NIF (individuals) + if (preg_match('/^\d{8}[A-Z]$/', $vat)) { + $number = (int)substr($vat, 0, 8); + $letter = substr($vat, -1); + $letters = 'TRWAGMYFPDXBNJZSQVHLCKE'; + return $letter === $letters[$number % 23]; + } + + // NIE (foreigners) + if (preg_match('/^[XYZ]\d{7}[A-Z]$/', $vat)) { + $replace = ['X' => '0', 'Y' => '1', 'Z' => '2']; + $number = (int)($replace[$vat[0]] . substr($vat, 1, 7)); + $letter = substr($vat, -1); + $letters = 'TRWAGMYFPDXBNJZSQVHLCKE'; + return $letter === $letters[$number % 23]; + } + + // CIF (companies) + if (preg_match('/^[ABCDEFGHJKLMNPQRSUVW]\d{7}[0-9A-J]$/', $vat)) { + $controlLetter = substr($vat, -1); + $digits = substr($vat, 1, 7); + + $sumEven = 0; + $sumOdd = 0; + for ($i = 0; $i < 7; $i++) { + $n = (int)$digits[$i]; + if ($i % 2 === 0) { // Odd positions (0-based index) + $n = $n * 2; + if ($n > 9) { + $n = floor($n / 10) + ($n % 10); + } + $sumOdd += $n; + } else { + $sumEven += $n; + } + } + + $total = $sumEven + $sumOdd; + $controlDigit = (10 - ($total % 10)) % 10; + $controlChar = 'JABCDEFGHI'[$controlDigit]; + + $firstLetter = $vat[0]; + if (strpos('PQRSW', $firstLetter) !== false) { + return $controlLetter === $controlChar; // Must be letter + } elseif (strpos('ABEH', $firstLetter) !== false) { + return $controlLetter == $controlDigit; // Must be digit + } else { + return ($controlLetter == $controlDigit || $controlLetter === $controlChar); + } + } + + return false; + } + +// // Example usage: +// var_dump(isValidSpanishVAT("12345678Z")); // true +// var_dump(isValidSpanishVAT("B12345674")); // true (CIF example) +// var_dump(isValidSpanishVAT("X1234567L")); // true (NIE) + } \ No newline at end of file diff --git a/app/Services/EDocument/Standards/Verifactu/Models/IDOtro.php b/app/Services/EDocument/Standards/Verifactu/Models/IDOtro.php new file mode 100644 index 0000000000..838c765e1f --- /dev/null +++ b/app/Services/EDocument/Standards/Verifactu/Models/IDOtro.php @@ -0,0 +1,125 @@ +codigoPais = strtoupper($codigoPais); + return $this; + } + + public function setIdType(string $idType): self + { + $this->idType = $idType; + return $this; + } + + public function setId(string $id): self + { + $this->id = $id; + return $this; + } + + /** + * Returns the array structure for serialization to XML + */ + public function toArray(): array + { + return [ + 'CodigoPais' => $this->codigoPais, + 'IDType' => $this->idType, + 'ID' => $this->id, + ]; + } + + /** + * Returns the XML fragment for IDOtro + */ + public function toXml(\DOMDocument $doc): \DOMElement + { + $root = $this->createElement($doc, 'IDOtro'); + + $root->appendChild($this->createElement($doc, 'CodigoPais', $this->codigoPais)); + $root->appendChild($this->createElement($doc, 'IDType', $this->idType)); + $root->appendChild($this->createElement($doc, 'ID', $this->id)); + + return $root; + } + + /** + * Create a PersonaFisicaJuridica instance from XML string or DOMElement + */ + public static function fromXml($xml): BaseXmlModel + { + if (is_string($xml)) { + $doc = new \DOMDocument(); + $doc->loadXML($xml); + $element = $doc->documentElement; + } else { + $element = $xml; + } + + return self::fromDOMElement($element); + } + + public static function fromDOMElement(\DOMElement $element): self + { + $idOtro = new self(); + + $codigoPaisElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'CodigoPais')->item(0); + if ($codigoPaisElement) { + $idOtro->setCodigoPais($codigoPaisElement->nodeValue); + } + + $idTypeElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'IDType')->item(0); + if ($idTypeElement) { + $idOtro->setIdType($idTypeElement->nodeValue); + } + + $idElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'ID')->item(0); + if ($idElement) { + $idOtro->setId($idElement->nodeValue); + } + + return $idOtro; + } +} diff --git a/app/Services/EDocument/Standards/Verifactu/Models/PersonaFisicaJuridica.php b/app/Services/EDocument/Standards/Verifactu/Models/PersonaFisicaJuridica.php index 26ca2f46bd..82dfe819c1 100644 --- a/app/Services/EDocument/Standards/Verifactu/Models/PersonaFisicaJuridica.php +++ b/app/Services/EDocument/Standards/Verifactu/Models/PersonaFisicaJuridica.php @@ -1,5 +1,15 @@ idOtro; } - public function setIdOtro(?string $idOtro): self + public function setIdOtro(IDOtro $idOtro): self { $this->idOtro = $idOtro; return $this; @@ -135,7 +145,7 @@ class PersonaFisicaJuridica extends BaseXmlModel } if ($this->idOtro !== null) { - $root->appendChild($this->createElement($doc, 'IDOtro', $this->idOtro)); + $root->appendChild($this->idOtro->toXml($doc)); } if ($this->pais !== null) { diff --git a/app/Services/EDocument/Standards/Verifactu/RegistroAlta.php b/app/Services/EDocument/Standards/Verifactu/RegistroAlta.php index 34ee15a661..9c7f4aa764 100644 --- a/app/Services/EDocument/Standards/Verifactu/RegistroAlta.php +++ b/app/Services/EDocument/Standards/Verifactu/RegistroAlta.php @@ -15,21 +15,22 @@ namespace App\Services\EDocument\Standards\Verifactu; use App\Models\Company; use App\Models\Invoice; use App\Models\Product; +use App\Models\VerifactuLog; use App\Helpers\Invoice\Taxer; +use App\Utils\Traits\MakesHash; 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\IDOtro; use App\Services\EDocument\Standards\Verifactu\Models\Desglose; -use App\Services\EDocument\Standards\Verifactu\Models\Encadenamiento; use App\Services\EDocument\Standards\Verifactu\Models\IDFactura; +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; -use App\Utils\Traits\MakesHash; class RegistroAlta { @@ -130,15 +131,28 @@ class RegistroAlta $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()); + if($this->invoice->client->country_id == 724) { + $destinatario + ->setNif($this->invoice->client->vat_number) + ->setNombreRazon($this->invoice->client->present()->name()); + } + else { + $locationData = $this->invoice->location(); + + $destinatario = new IDOtro(); + $destinatario->setCodigoPais($locationData['country_code']); + + $br = new \App\DataMapper\Tax\BaseRule(); + + if(in_array($locationData['country_code'], $br->eu_country_codes) && strlen($this->invoice->client->vat_number ?? '') > 0) { + $destinatario->setIdType('03'); + $destinatario->setId($this->invoice->client->vat_number); + } + } $destinatarios[] = $destinatario; diff --git a/app/Services/EDocument/Standards/Verifactu/SendToAeat.php b/app/Services/EDocument/Standards/Verifactu/SendToAeat.php index fafb741e89..bf90d29c07 100644 --- a/app/Services/EDocument/Standards/Verifactu/SendToAeat.php +++ b/app/Services/EDocument/Standards/Verifactu/SendToAeat.php @@ -72,11 +72,11 @@ class SendToAeat implements ShouldQueue $invoice = Invoice::withTrashed()->find($this->invoice_id); - if($invoice->client->country->iso_3166_2 != 'ES') { - $invoice->backup->guid = 'NOT_ES'; - $invoice->saveQuietly(); - return; - } + // if($invoice->client->country->iso_3166_2 != 'ES') { + // $invoice->backup->guid = 'NOT_ES'; + // $invoice->saveQuietly(); + // return; + // } switch($this->action) { case 'create': diff --git a/lang/en/texts.php b/lang/en/texts.php index 7a761bfb45..a60d6ddf59 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -5580,6 +5580,7 @@ $lang = array( 'verifactu_invoice_sent_failure' => 'Invoice :invoice for :client failed to send to AEAT :notes', 'verifactu_cancellation_send_success' => 'Invoice cancellation for :invoice sent to AEAT successfully', 'verifactu_cancellation_send_failure' => 'Invoice cancellation for :invoice failed to send to AEAT :notes', + 'verifactu' => 'Verifactu', ); return $lang; diff --git a/tests/Feature/EInvoice/Verifactu/VerifactuApiTest.php b/tests/Feature/EInvoice/Verifactu/VerifactuApiTest.php index 8840e94286..13e05f1981 100644 --- a/tests/Feature/EInvoice/Verifactu/VerifactuApiTest.php +++ b/tests/Feature/EInvoice/Verifactu/VerifactuApiTest.php @@ -85,7 +85,8 @@ class VerifactuApiTest extends TestCase ]); $invoice->backup->document_type = 'F1'; - + $invoice->backup->adjustable_amount = 121; + $repo = new InvoiceRepository(); $invoice = $repo->save([], $invoice); @@ -97,7 +98,7 @@ class VerifactuApiTest extends TestCase { $settings = $this->company->settings; - $settings->e_invoice_type = 'verifactu'; + $settings->e_invoice_type = 'VERIFACTU'; $settings->is_locked = 'when_sent'; $this->company->settings = $settings; @@ -117,6 +118,7 @@ class VerifactuApiTest extends TestCase $this->assertEquals('F1', $invoice->backup->document_type); $this->assertEquals('R2', $invoice2->backup->document_type); + $this->assertEquals($invoice->hashed_id, $invoice2->backup->parent_invoice_id); $this->assertCount(1, $invoice->backup->child_invoice_ids); $data = [ @@ -153,7 +155,7 @@ class VerifactuApiTest extends TestCase { $settings = $this->company->settings; - $settings->e_invoice_type = 'verifactu'; + $settings->e_invoice_type = 'VERIFACTU'; $settings->is_locked = 'when_sent'; $this->company->settings = $settings; @@ -192,7 +194,7 @@ class VerifactuApiTest extends TestCase { $settings = $this->company->settings; - $settings->e_invoice_type = 'verifactu'; + $settings->e_invoice_type = 'VERIFACTU'; $this->company->settings = $settings; $this->company->save(); @@ -219,6 +221,16 @@ class VerifactuApiTest extends TestCase $response->assertStatus(200); + $arr = $response->json(); + + $this->assertEquals('R2', $arr['data']['backup']['document_type']); + $this->assertEquals($invoice->hashed_id, $arr['data']['backup']['parent_invoice_id']); + + $invoice = $invoice->fresh(); + + $this->assertEquals('F1', $invoice->backup->document_type); + $this->assertCount(1, $invoice->backup->child_invoice_ids); + $data = [ 'action' => 'delete', 'ids' => [$invoice->hashed_id] @@ -237,7 +249,7 @@ class VerifactuApiTest extends TestCase { $settings = $this->company->settings; - $settings->e_invoice_type = 'verifactu'; + $settings->e_invoice_type = 'VERIFACTU'; $this->company->settings = $settings; $this->company->save(); @@ -279,7 +291,7 @@ class VerifactuApiTest extends TestCase { $settings = $this->company->settings; - $settings->e_invoice_type = 'verifactu'; + $settings->e_invoice_type = 'VERIFACTU'; $this->company->settings = $settings; $this->company->save(); @@ -313,7 +325,7 @@ class VerifactuApiTest extends TestCase { $settings = $this->company->settings; - $settings->e_invoice_type = 'verifactu'; + $settings->e_invoice_type = 'VERIFACTU'; $this->company->settings = $settings; $this->company->save(); @@ -346,7 +358,7 @@ class VerifactuApiTest extends TestCase { $settings = $this->company->settings; - $settings->e_invoice_type = 'verifactu'; + $settings->e_invoice_type = 'VERIFACTU'; $this->company->settings = $settings; $this->company->save(); @@ -355,7 +367,6 @@ class VerifactuApiTest extends TestCase $invoice->service()->markSent()->save(); $this->assertEquals(121, $invoice->amount); - $data = $invoice->toArray(); unset($data['client']); @@ -381,7 +392,7 @@ class VerifactuApiTest extends TestCase { $settings = $this->company->settings; - $settings->e_invoice_type = 'verifactu'; + $settings->e_invoice_type = 'VERIFACTU'; $this->company->settings = $settings; $this->company->save(); @@ -445,7 +456,7 @@ class VerifactuApiTest extends TestCase { $settings = $this->company->settings; - $settings->e_invoice_type = 'verifactu'; + $settings->e_invoice_type = 'VERIFACTU'; $this->company->settings = $settings; $this->company->save(); @@ -486,7 +497,7 @@ class VerifactuApiTest extends TestCase { $settings = $this->company->settings; - $settings->e_invoice_type = 'verifactu'; + $settings->e_invoice_type = 'VERIFACTU'; $this->company->settings = $settings; $this->company->save(); @@ -526,7 +537,7 @@ class VerifactuApiTest extends TestCase { $settings = $this->company->settings; - $settings->e_invoice_type = 'verifactu'; + $settings->e_invoice_type = 'VERIFACTU'; $this->company->settings = $settings; $this->company->save(); @@ -556,7 +567,7 @@ class VerifactuApiTest extends TestCase { $settings = $this->company->settings; - $settings->e_invoice_type = 'verifactu'; + $settings->e_invoice_type = 'VERIFACTU'; $this->company->settings = $settings; $this->company->save(); @@ -589,7 +600,7 @@ class VerifactuApiTest extends TestCase { $settings = $this->company->settings; - $settings->e_invoice_type = 'verifactu'; + $settings->e_invoice_type = 'VERIFACTU'; $this->company->settings = $settings; $this->company->save(); @@ -619,7 +630,7 @@ class VerifactuApiTest extends TestCase { $settings = $this->company->settings; - $settings->e_invoice_type = 'verifactu'; + $settings->e_invoice_type = 'VERIFACTU'; $this->company->settings = $settings; $this->company->save(); @@ -647,7 +658,7 @@ class VerifactuApiTest extends TestCase $this->assertEquals(10, $this->client->balance); $settings = $this->company->settings; - $settings->e_invoice_type = 'verifactu'; + $settings->e_invoice_type = 'VERIFACTU'; $this->company->settings = $settings; $this->company->save(); @@ -743,7 +754,7 @@ class VerifactuApiTest extends TestCase { $settings = $this->company->settings; - $settings->e_invoice_type = 'verifactu'; + $settings->e_invoice_type = 'VERIFACTU'; $this->company->settings = $settings; $this->company->save(); @@ -780,7 +791,7 @@ class VerifactuApiTest extends TestCase $this->assertEquals($invoice->amount, 121); $settings = $this->company->settings; - $settings->e_invoice_type = 'verifactu'; + $settings->e_invoice_type = 'VERIFACTU'; $this->company->settings = $settings; $this->company->save(); @@ -819,7 +830,7 @@ class VerifactuApiTest extends TestCase { $settings = $this->company->settings; - $settings->e_invoice_type = 'verifactu'; + $settings->e_invoice_type = 'VERIFACTU'; $this->company->settings = $settings; $this->company->save(); @@ -859,7 +870,7 @@ class VerifactuApiTest extends TestCase { $settings = $this->company->settings; - $settings->e_invoice_type = 'verifactu'; + $settings->e_invoice_type = 'VERIFACTU'; $this->company->settings = $settings; $this->company->save(); @@ -910,7 +921,7 @@ class VerifactuApiTest extends TestCase Config::set('ninja.environment', 'hosted'); $settings = $this->company->settings; - $settings->e_invoice_type = 'verifactu'; + $settings->e_invoice_type = 'VERIFACTU'; $this->company->settings = $settings; $this->company->save(); @@ -935,7 +946,7 @@ class VerifactuApiTest extends TestCase $arr = $response->json(); - $this->assertEquals($arr['data']['settings']['e_invoice_type'], 'verifactu'); + $this->assertEquals($arr['data']['settings']['e_invoice_type'], 'VERIFACTU'); $this->assertEquals($arr['data']['settings']['lock_invoices'], 'when_sent'); } } \ No newline at end of file