Compare commits

...

78 Commits

Author SHA1 Message Date
David Bomba 29bfbaf644
Merge pull request #11185 from turbo124/verifactu
Verifactu
2025-08-15 13:28:44 +10:00
David Bomba 27dd9907f3 Cleanup for tests 2025-08-15 13:26:38 +10:00
David Bomba 5a188ac355 Fixes for tests 2025-08-15 13:22:29 +10:00
David Bomba cfc41eb29a Document validation for verifactu 2025-08-15 13:19:58 +10:00
David Bomba e58f19f593 Validation for verifactu documents 2025-08-15 13:13:51 +10:00
David Bomba e8336c85d7 Update rules for setting Impuesto values 2025-08-15 13:02:59 +10:00
David Bomba 117709e551 Validation checks 2025-08-15 12:26:40 +10:00
David Bomba 70dea557f5 Ensure tax codes that are sent are correct 2025-08-14 14:41:21 +10:00
David Bomba 084f0fea25 Activity translations for verifactu 2025-08-14 12:17:03 +10:00
David Bomba 256555c952 Add AEAT response to logs 2025-08-14 12:08:17 +10:00
David Bomba 9b98d45d3b Align tests with new workflow 2025-08-14 11:28:02 +10:00
David Bomba c3b5a972a1 Align tests with new workflow 2025-08-14 10:47:18 +10:00
David Bomba 7e1a6bc1c7 Align tests with new workflow 2025-08-14 10:38:51 +10:00
David Bomba 0a7744a70e Add IDOtro class 2025-08-14 10:07:32 +10:00
David Bomba 1252cdf7ae Integration of Verifactu with UI 2025-08-14 08:36:55 +10:00
David Bomba af926a394c Wiring up verifactu sending 2025-08-13 14:39:50 +10:00
David Bomba c5c9c4325e Wiring up verifactu sending 2025-08-13 14:26:03 +10:00
David Bomba 3d3b5f6938 Integration works for Verifactu 2025-08-13 13:15:51 +10:00
David Bomba ff92756dbc Verifactu api tests 2025-08-13 11:48:18 +10:00
David Bomba a141ca1549 Refactor tests to remove modifications of existing invoices 2025-08-13 11:39:11 +10:00
David Bomba bf8041ab7c Working on verifactu document mutations 2025-08-13 11:05:02 +10:00
David Bomba 8bc1513591 Wire up AEAT for processing 2025-08-13 10:28:10 +10:00
David Bomba 63e6f75a24 Additional rules around tests 2025-08-12 18:34:38 +10:00
David Bomba 5482f44bea Additional rules around tests 2025-08-12 14:42:32 +10:00
David Bomba 81ec3986ca Tests around the handling of verifactu credit amounts 2025-08-12 14:33:52 +10:00
David Bomba 1a3badf748 Tests around the handling of verifactu credit amounts 2025-08-12 13:52:05 +10:00
David Bomba c7e79fe673 Refactor to use generate parent/child ids 2025-08-12 13:41:11 +10:00
David Bomba 8d23ba14d4 Refactor to use generate parent/child ids 2025-08-12 12:29:36 +10:00
David Bomba 1836ccc434 Update for ivnoice backup casting 2025-08-12 12:13:44 +10:00
David Bomba 94b628b6eb Update for ivnoice backup casting 2025-08-12 11:59:44 +10:00
David Bomba 67df175525 Update for ivnoice backup casting 2025-08-12 10:24:18 +10:00
David Bomba 47f33c8691 Validation rules for modification invoice for Verifactu 2025-08-12 09:17:57 +10:00
David Bomba 6a0fff10ae Tests around handling cancellations in verifactu 2025-08-11 11:37:21 +10:00
David Bomba a447b6a20b Tests around handling cancellations in verifactu 2025-08-11 11:00:30 +10:00
David Bomba f7961ecb61 Additional tests for verifactu restore/archive logic 2025-08-11 10:10:33 +10:00
David Bomba 37c74ee18c Functional tests of spanish environment 2025-08-11 10:09:12 +10:00
David Bomba 555eb80018 Functional tests of spanish environment 2025-08-11 09:54:53 +10:00
David Bomba 1e8727c4a4 Force locking for spanish users 2025-08-11 09:25:48 +10:00
David Bomba 74f71d61d6 Padding settings 2025-08-11 09:14:51 +10:00
David Bomba 8a137329d4 Updates for Verifactu 2025-08-10 15:32:35 +10:00
David Bomba c02c87765b Create and cancel invoices working as expected 2025-08-10 13:03:51 +10:00
David Bomba 7393360db3 Working on feature flow and tests for verifactu submissions 2025-08-10 11:25:36 +10:00
David Bomba f7055b516e Working of feature flow of verifactu 2025-08-09 10:22:14 +10:00
David Bomba ee775e58a0 logging 2025-08-09 09:57:02 +10:00
David Bomba 5ff70dbeae Static analysis cleanup 2025-08-08 15:14:21 +10:00
David Bomba 3791469c31 Static analysis cleanup 2025-08-08 15:12:24 +10:00
David Bomba 1a86d5445b Updates for correct date formats for invoice cancellation tests 2025-08-08 14:07:11 +10:00
David Bomba 442ff42ceb additional tests 2025-08-08 14:04:13 +10:00
David Bomba b94316dbed XmlInterface 2025-08-08 13:17:06 +10:00
David Bomba 14fd4063f5 Verifactu feature tests 2025-08-08 12:43:23 +10:00
David Bomba 97f2e70f5d Expand tests to cancellation and modifications 2025-08-08 12:17:18 +10:00
David Bomba edd0de38ca Fixes for validation tests 2025-08-08 11:26:40 +10:00
David Bomba d53e1012af Add XSD validator for Verifactu 2025-08-08 11:23:03 +10:00
David Bomba 5895c1b0ed Tests for modified invoices 2025-08-08 10:40:38 +10:00
David Bomba aa918f7ec0 Verifactu initial invoice creation 2025-08-08 09:01:31 +10:00
David Bomba 33078ee86c Verifactu invoice generation 2025-08-07 21:56:11 +10:00
David Bomba 6c8c270c2f Verifactu invoice generation 2025-08-07 21:53:13 +10:00
David Bomba 5afd3b85bc additional verifactu validation tests 2025-08-07 21:26:30 +10:00
David Bomba dab787c3ae Validation rules for verifactu invoices 2025-08-07 19:42:18 +10:00
David Bomba 03a39f33b8 Add Entity Level Validation for Verifactu 2025-08-07 19:34:23 +10:00
David Bomba cbc5cb5f9b Skip WSTest by default 2025-08-07 18:45:33 +10:00
David Bomba 4127eb32f9 Add logging to wstest for requirements 2025-08-07 17:12:57 +10:00
David Bomba bf5359cb72 Add VerifactuLog 2025-08-07 15:16:04 +10:00
David Bomba d42735f2ee Tests 2025-08-07 14:36:13 +10:00
David Bomba ea663394b1 Working on ws tests 2025-08-07 14:15:17 +10:00
David Bomba b93ec6dd93 Additional tests for verifactu 2025-08-04 08:09:08 +10:00
David Bomba a431dd43d4 Fixes for verifactu WS 2025-06-26 17:29:48 +10:00
David Bomba 1c5c568251 WS Tests 2025-06-25 11:09:15 +10:00
David Bomba 604ba82f8f WS Tests 2025-06-25 08:52:37 +10:00
David Bomba c8e0cdd090 WS Tests 2025-06-25 08:50:56 +10:00
David Bomba 54ba4349f6 Tests for verifactu 2025-06-22 10:33:26 +10:00
David Bomba 0865ab3c3e Clean up for verifactu tests 2025-04-25 17:08:46 +10:00
David Bomba fc8cb7af36 Clean up for verifactu tests 2025-04-25 17:07:49 +10:00
David Bomba 46de411160 Finish test suite 2025-04-25 17:03:16 +10:00
David Bomba ff50e3c9d9 Finish test suite 2025-04-25 17:02:22 +10:00
David Bomba fdf7d2d3cf Refactor for additional test cases 2025-04-25 16:09:40 +10:00
David Bomba dd60eb3b58 Up to signing xml 2025-04-25 14:52:07 +10:00
David Bomba 1f4fae314c Stubs for generating Verifactu Standard Invoices 2025-04-25 14:03:26 +10:00
115 changed files with 17892 additions and 1555 deletions

View File

@ -0,0 +1,58 @@
<?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\Casts;
use App\DataMapper\InvoiceBackup;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
class InvoiceBackupCast implements CastsAttributes
{
public function get($model, string $key, $value, array $attributes)
{
if (is_null($value)) {
return new InvoiceBackup();
}
$data = json_decode($value, true) ?? [];
return InvoiceBackup::fromArray($data);
}
public function set($model, string $key, $value, array $attributes)
{
if (is_null($value)) {
return [$key => null];
}
// Ensure we're dealing with our object type
if (! $value instanceof InvoiceBackup) {
throw new \InvalidArgumentException('Value must be an InvoiceBackup instance.');
}
return [
$key => json_encode([
'guid' => $value->guid,
'cancellation' => $value->cancellation ? [
'adjustment' => $value->cancellation->adjustment,
'status_id' => $value->cancellation->status_id,
] : [],
'parent_invoice_id' => $value->parent_invoice_id,
'parent_invoice_number' => $value->parent_invoice_number,
'document_type' => $value->document_type,
'child_invoice_ids' => $value->child_invoice_ids->toArray(),
'redirect' => $value->redirect,
'adjustable_amount' => $value->adjustable_amount,
])
];
}
}

View File

@ -0,0 +1,32 @@
<?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\DataMapper;
/**
* Cancellation value object for invoice backup data.
*/
class Cancellation
{
public function __construct(
public float $adjustment = 0, // The cancellation adjustment amount
public int $status_id = 0 //The status id of the invoice when it was cancelled
) {}
public static function fromArray(array $data): self
{
return new self(
adjustment: $data['adjustment'] ?? 0,
status_id: $data['status_id'] ?? 0
);
}
}

View File

@ -477,7 +477,7 @@ class CompanySettings extends BaseSettings
public $sync_invoice_quote_columns = true;
public $e_invoice_type = 'EN16931';
public $e_invoice_type = 'EN16931'; //verifactu
public $e_quote_type = 'OrderX_Comfort';

View File

@ -0,0 +1,92 @@
<?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\DataMapper;
use App\Casts\InvoiceBackupCast;
use App\DataMapper\Cancellation;
use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Support\Collection;
/**
* InvoiceBackup.
*/
class InvoiceBackup implements Castable
{
public function __construct(
public string $guid = '', // The E-INVOICE SENT GUID reference - or enum to advise the document has been successfully sent.
public Cancellation $cancellation = new Cancellation(0,0),
public ?string $parent_invoice_id = null, // The id of the invoice that was cancelled
public ?string $parent_invoice_number = null, // The number of the invoice that was cancelled
public ?string $document_type = null, // F1, R2
public Collection $child_invoice_ids = new Collection(), // Collection of child invoice IDs
public ?string $redirect = null, // The redirect url for the invoice
public float $adjustable_amount = 0,
) {}
/**
* Get the name of the caster class to use when casting from / to this cast target.
*
* @param array<string, mixed> $arguments
*/
public static function castUsing(array $arguments): string
{
return InvoiceBackupCast::class;
}
public static function fromArray(array $data): self
{
return new self(
guid: $data['guid'] ?? '',
cancellation: Cancellation::fromArray($data['cancellation'] ?? []),
parent_invoice_id: $data['parent_invoice_id'] ?? null,
parent_invoice_number: $data['parent_invoice_number'] ?? null,
document_type: $data['document_type'] ?? null,
child_invoice_ids: isset($data['child_invoice_ids']) ? collect($data['child_invoice_ids']) : new Collection(),
redirect: $data['redirect'] ?? null,
adjustable_amount: $data['adjustable_amount'] ?? 0,
);
}
/**
* Add a child invoice ID to the collection
*/
public function addChildInvoiceId(string $invoiceId): void
{
$this->child_invoice_ids->push($invoiceId);
}
/**
* Remove a child invoice ID from the collection
*/
public function removeChildInvoiceId(string $invoiceId): void
{
$this->child_invoice_ids = $this->child_invoice_ids->reject($invoiceId);
}
/**
* Check if a child invoice ID exists
*/
public function hasChildInvoiceId(string $invoiceId): bool
{
return $this->child_invoice_ids->contains($invoiceId);
}
/**
* Get all child invoice IDs as an array
*/
public function getChildInvoiceIds(): array
{
return $this->child_invoice_ids->toArray();
}
}

View File

@ -41,7 +41,7 @@ class EInvoiceController extends BaseController
*/
public function validateEntity(ValidateEInvoiceRequest $request)
{
$el = new EntityLevel();
$el = $request->getValidatorClass();
$data = [];

View File

@ -11,17 +11,18 @@
namespace App\Http\Controllers;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Response;
use App\Http\Requests\EInvoice\Peppol\StoreEntityRequest;
use Illuminate\Http\JsonResponse;
use App\Services\EDocument\Jobs\SendEDocument;
use App\Http\Requests\EInvoice\Peppol\RetrySendRequest;
use App\Services\EDocument\Gateway\Storecove\Storecove;
use App\Http\Requests\EInvoice\Peppol\DisconnectRequest;
use App\Http\Requests\EInvoice\Peppol\ShowEntityRequest;
use App\Http\Requests\EInvoice\Peppol\StoreEntityRequest;
use App\Http\Requests\EInvoice\Peppol\UpdateEntityRequest;
use App\Services\EDocument\Standards\Verifactu\SendToAeat;
use App\Http\Requests\EInvoice\Peppol\AddTaxIdentifierRequest;
use App\Http\Requests\EInvoice\Peppol\RemoveTaxIdentifierRequest;
use App\Http\Requests\EInvoice\Peppol\RetrySendRequest;
use App\Http\Requests\EInvoice\Peppol\ShowEntityRequest;
use App\Http\Requests\EInvoice\Peppol\UpdateEntityRequest;
use App\Services\EDocument\Jobs\SendEDocument;
class EInvoicePeppolController extends BaseController
{
@ -264,8 +265,12 @@ class EInvoicePeppolController extends BaseController
public function retrySend(RetrySendRequest $request)
{
if(auth()->user()->company()->verifactuEnabled()) {
SendToAeat::dispatch($request->entity_id, auth()->user()->company(), 'create');
}
else {
SendEDocument::dispatch($request->entity, $request->entity_id, auth()->user()->company()->db);
}
return response()->json(['message' => 'trying....'], 200);
}

View File

@ -785,7 +785,7 @@ class InvoiceController extends BaseController
}
break;
case 'cancel':
$invoice = $invoice->service()->handleCancellation()->save();
$invoice = $invoice->service()->handleCancellation(request()->input('reason'))->save();
if (! $bulk) {
$this->itemResponse($invoice);
}

View File

@ -204,6 +204,12 @@ class UpdateCompanyRequest extends Request
$settings[$protected_var] = str_replace("script", "", $settings[$protected_var]);
}
}
if($this->company->settings->e_invoice_type == 'VERIFACTU') {
$settings['lock_invoices'] = 'when_sent';
$settings['e_invoice_type'] = 'VERIFACTU';
}
}
if (isset($settings['email_style_custom'])) {

View File

@ -29,7 +29,7 @@ class RetrySendRequest extends Request
return true;
}
return $user->account->isPaid() && $user->isAdmin() && $user->company()->legal_entity_id != null;
return $user->account->isPaid() && $user->isAdmin() && ($user->company()->legal_entity_id != null || $user->company()->verifactuEnabled());
}
/**

View File

@ -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
@ -75,7 +76,6 @@ class ValidateEInvoiceRequest extends Request
return false;
}
$class = Invoice::class;
match ($this->entity) {
@ -92,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();
// }
}
}

View File

@ -12,6 +12,7 @@
namespace App\Http\Requests\Invoice;
use App\Http\Requests\Request;
use App\Http\ValidationRules\Invoice\RestoreDisabledRule;
use App\Utils\Traits\Invoice\ActionsInvoice;
use App\Utils\Traits\MakesHash;
@ -37,7 +38,7 @@ class ActionInvoiceRequest extends Request
public function rules()
{
return [
'action' => 'required',
'action' => ['required', new RestoreDisabledRule()],
];
}

View File

@ -13,6 +13,8 @@ namespace App\Http\Requests\Invoice;
use App\Http\Requests\Request;
use App\Exceptions\DuplicatePaymentException;
use App\Http\ValidationRules\Invoice\RestoreDisabledRule;
use Illuminate\Validation\Rule;
class BulkInvoiceRequest extends Request
{
@ -23,9 +25,12 @@ class BulkInvoiceRequest extends Request
public function rules()
{
/** @var \App\Models\User $user */
$user = auth()->user();
return [
'action' => 'required|string',
'ids' => 'required|array',
'action' => ['required', 'bail','string', new RestoreDisabledRule()],
'ids' => ['required', 'bail', 'array'],
'email_type' => 'sometimes|in:reminder1,reminder2,reminder3,reminder_endless,custom1,custom2,custom3,invoice,quote,credit,payment,payment_partial,statement,purchase_order',
'template' => 'sometimes|string',
'template_id' => 'sometimes|string',

View File

@ -11,12 +11,14 @@
namespace App\Http\Requests\Invoice;
use App\Http\Requests\Request;
use App\Http\ValidationRules\Project\ValidProjectForClient;
use App\Models\Invoice;
use App\Utils\Traits\CleanLineItems;
use App\Http\Requests\Request;
use App\Utils\Traits\MakesHash;
use Illuminate\Validation\Rule;
use App\Utils\Traits\CleanLineItems;
use App\Http\ValidationRules\Project\ValidProjectForClient;
use App\Http\ValidationRules\Invoice\CanGenerateModificationInvoice;
use App\Http\ValidationRules\Invoice\VerifactuAmountCheck;
class StoreInvoiceRequest extends Request
{
@ -44,7 +46,7 @@ class StoreInvoiceRequest extends Request
$rules = [];
$rules['client_id'] = ['required', 'bail', Rule::exists('clients', 'id')->where('company_id', $user->company()->id)->where('is_deleted', 0)];
$rules['client_id'] = ['required', 'bail', new VerifactuAmountCheck($this->all()) , Rule::exists('clients', 'id')->where('company_id', $user->company()->id)->where('is_deleted', 0)];
if ($this->file('documents') && is_array($this->file('documents'))) {
$rules['documents.*'] = $this->fileValidation();
@ -71,7 +73,7 @@ class StoreInvoiceRequest extends Request
$rules['date'] = 'bail|sometimes|date:Y-m-d';
$rules['due_date'] = ['bail', 'sometimes', 'nullable', 'after:partial_due_date', Rule::requiredIf(fn () => strlen($this->partial_due_date ?? '') > 1), 'date'];
$rules['line_items'] = 'array';
$rules['line_items'] = ['bail', 'array'];
$rules['discount'] = 'sometimes|numeric|max:99999999999999';
$rules['tax_rate1'] = 'bail|sometimes|numeric';
$rules['tax_rate2'] = 'bail|sometimes|numeric';
@ -89,6 +91,8 @@ class StoreInvoiceRequest extends Request
$rules['custom_surcharge4'] = ['sometimes', 'nullable', 'bail', 'numeric', 'max:99999999999999'];
$rules['location_id'] = ['nullable', 'sometimes','bail', Rule::exists('locations', 'id')->where('company_id', $user->company()->id)->where('client_id', $this->client_id)];
$rules['modified_invoice_id'] = ['bail', 'sometimes', 'nullable', new CanGenerateModificationInvoice()];
return $rules;
}

View File

@ -0,0 +1,66 @@
<?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\Http\ValidationRules\Invoice;
use Closure;
use App\Models\Invoice;
use App\Utils\Traits\MakesHash;
use Illuminate\Contracts\Validation\ValidationRule;
/**
* Class CanGenerateModificationInvoice.
*/
class CanGenerateModificationInvoice implements ValidationRule
{
use MakesHash;
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (empty($value)) {
return;
}
$user = auth()->user();
$company = $user->company();
/** For verifactu, we do not allow restores of deleted invoices */
if (!$company->verifactuEnabled())
$fail("Verifactu no está habilitado para esta empresa"); // Verifactu is not enabled for this company
$invoice = Invoice::withTrashed()->find($this->decodePrimaryKey($value));
if (is_null($invoice)) {
$fail("Factura no encontrada."); // Invoice not found
} elseif($invoice->is_deleted) {
$fail("No se puede crear una factura de rectificación para una factura eliminada."); // Cannot create a rectification invoice for a deleted invoice
} elseif($invoice->backup->document_type !== 'F1') {
$fail("Solo las facturas originales F1 pueden ser rectificadas."); // Only original F1 invoices can be rectified
} elseif($invoice->status_id === Invoice::STATUS_DRAFT){
$fail("No se puede crear una factura de rectificación para una factura en borrador."); // Cannot create a rectification invoice for a draft invoice
} elseif(in_array($invoice->status_id, [Invoice::STATUS_PARTIAL, Invoice::STATUS_PAID])) {
$fail("No se puede crear una factura de rectificación cuando se ha realizado un pago."); // Cannot create a rectification invoice where a payment has been made
} elseif($invoice->status_id === Invoice::STATUS_CANCELLED ) {
$fail("No se puede crear una factura de rectificación para una factura cancelada."); // Cannot create a rectification invoice for a cancelled invoice
} elseif($invoice->status_id === Invoice::STATUS_REVERSED) {
$fail("No se puede crear una factura de rectificación para una factura revertida."); // Cannot create a rectification invoice for a reversed invoice
}
// } elseif ($invoice->status_id !== Invoice::STATUS_SENT) {
// $fail("Cannot create a modification invoice.");
// } elseif($invoice->amount <= 0){
// $fail("Cannot create a modification invoice for an invoice with an amount less than 0.");
}
}

View File

@ -0,0 +1,58 @@
<?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\Http\ValidationRules\Invoice;
use Closure;
use App\Models\Invoice;
use App\Utils\Traits\MakesHash;
use Illuminate\Contracts\Validation\ValidationRule;
/**
* Class RestoreDisabledRule.
*/
class RestoreDisabledRule implements ValidationRule
{
use MakesHash;
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$user = auth()->user();
$company = $user->company();
if (empty($value) || !$company->verifactuEnabled()) {
return;
}
$base_query = Invoice::withTrashed()
->whereIn('id', $this->transformKeys(request()->ids))
->company();
$restore_query = clone $base_query;
$delete_query = clone $base_query;
$mutated_query = $delete_query->where(function ($q){
$q->where('backup->document_type', 'F1')->where('backup->child_invoice_ids', '!=', '[]');
});
/** For verifactu, we do not allow restores of deleted invoices */
if($value == 'restore' && $restore_query->where('is_deleted', true)->exists()) {
$fail(ctrans('texts.restore_disabled_verifactu'));
}
elseif(in_array($value, ['delete', 'cancel']) && $mutated_query->exists()) {
$fail(ctrans('texts.delete_disabled_verifactu'));
}
}
}

View File

@ -0,0 +1,109 @@
<?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\Http\ValidationRules\Invoice;
use Closure;
use App\Models\Client;
use App\Models\Invoice;
use App\Utils\Traits\MakesHash;
use Illuminate\Contracts\Validation\ValidationRule;
/**
* Class VerifactuAmountCheck.
*/
class VerifactuAmountCheck implements ValidationRule
{
use MakesHash;
public function __construct(private array $input){}
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (empty($value)) {
return;
}
$user = auth()->user();
$company = $user->company();
if ($company->verifactuEnabled()) { // Company level check if Verifactu is enabled
$client = Client::withTrashed()->find($this->input['client_id']);
$invoice = false;
$child_invoices = false;
$child_invoice_totals = 0;
$child_invoice_count = 0;
if(isset($this->input['modified_invoice_id'])) {
$invoice = Invoice::withTrashed()->where('id', $this->decodePrimaryKey($this->input['modified_invoice_id']))->company()->firstOrFail();
if ($invoice->backup->adjustable_amount <= 0) {
$fail("Invoice already credited in full");
}
$child_invoices = Invoice::withTrashed()
->whereIn('id', $this->transformKeys($invoice->backup->child_invoice_ids->toArray()))
->get();
$child_invoice_totals = round($child_invoices->sum('amount'), 2);
$child_invoice_count = $child_invoices->count();
}
$items = collect($this->input['line_items'])->map(function ($item) use($company){
$discount = $item['discount'] ?? 0;
$is_amount_discount = $this->input['is_amount_discount'] ?? true;
if(!$is_amount_discount && $discount > 0) {
$discount = $item['quantity'] * $item['cost'] * ($discount / 100);
}
$line_total = ($item['quantity'] * $item['cost']) - $discount;
if(!$company->settings->inclusive_taxes) {
$tax = ($item['tax_rate1'] ?? 0) + ($item['tax_rate2'] ?? 0) + ($item['tax_rate3'] ?? 0);
$tax_amount = $line_total * ($tax / 100);
$line_total += $tax_amount;
}
return $line_total;
});
$total_discount = $this->input['discount'] ?? 0;
$is_amount_discount = $this->input['is_amount_discount'] ?? true;
if(!$is_amount_discount) {
$total_discount = $items->sum() * ($total_discount / 100);
}
$total = $items->sum() - $total_discount;
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) {
$total_adjustments = $total + $child_invoice_totals;
$fail("Total Adjustment {$total_adjustments} cannot exceed the original invoice amount {$invoice->amount}");
}
}
}
}

View File

@ -17,6 +17,7 @@ use App\DataMapper\Tax\TaxModel;
use App\Libraries\MultiDB;
use App\Models\Company;
use App\Models\Country;
use App\Models\TaxRate;
use App\Utils\Ninja;
use App\Utils\Traits\MakesHash;
use Illuminate\Foundation\Bus\Dispatchable;
@ -163,9 +164,10 @@ class CreateCompany
$settings = $company->settings;
$settings->language_id = '7';
$settings->e_invoice_type = 'Facturae_3.2.2';
$settings->e_invoice_type = 'Facturae_3.2.2'; //change this to verifactu
$settings->currency_id = '3';
$settings->timezone_id = '42';
$settings->lock_invoices = 'when_sent';
$company->settings = $settings;

View File

@ -39,6 +39,7 @@ use Laracasts\Presenter\PresentableTrait;
* @property string|null $plan_expires
* @property string|null $user_agent
* @property string|null $key
* @property string|null $e_invoice_token
* @property int|null $payment_id
* @property int $default_company_id
* @property string|null $trial_started
@ -71,6 +72,7 @@ use Laracasts\Presenter\PresentableTrait;
* @property string|null $account_sms_verification_number
* @property bool $account_sms_verified
* @property string|null $bank_integration_account_id
* @property string|null $e_invoicing_token
* @property bool $is_trial
* @property int $e_invoice_quota
* @property-read int|null $bank_integrations_count

View File

@ -279,6 +279,14 @@ class Activity extends StaticModel
public const EMAIL_CREDIT = 149;
public const VERIFACTU_INVOICE_SENT = 150;
public const VERIFACTU_INVOICE_SENT_FAILURE = 151;
public const VERIFACTU_CANCELLATION_SENT = 152;
public const VERIFACTU_CANCELLATION_SENT_FAILURE = 153;
protected $casts = [
'is_system' => 'boolean',
'updated_at' => 'timestamp',

View File

@ -330,9 +330,14 @@ class BaseModel extends Model
}
// special catch here for einvoicing eventing
if ($event_id == Webhook::EVENT_SENT_INVOICE && ($this instanceof Invoice) && is_null($this->backup) && $this->client->peppolSendingEnabled()) {
if ($event_id == Webhook::EVENT_SENT_INVOICE && ($this instanceof Invoice) && $this->backup->guid == "") {
if($this->client->peppolSendingEnabled()) {
\App\Services\EDocument\Jobs\SendEDocument::dispatch(get_class($this), $this->id, $this->company->db);
}
elseif($this->company->verifactuEnabled()) {
$this->service()->sendVerifactu();
}
}
}

View File

@ -37,6 +37,7 @@ use Illuminate\Contracts\Translation\HasLocalePreference;
* @property int $id
* @property int $company_id
* @property int $user_id
* @property int|null $location_id
* @property int|null $assigned_user_id
* @property string|null $name
* @property string|null $website
@ -80,12 +81,17 @@ use Illuminate\Contracts\Translation\HasLocalePreference;
* @property int|null $updated_at
* @property int|null $deleted_at
* @property string|null $id_number
* @property string|null $classification
* @property-read mixed $hashed_id
* @property-read \App\Models\User|null $assigned_user
* @property-read \App\Models\User $user
* @property-read \App\Models\Company $company
* @property-read \App\Models\Country|null $country
* @property-read \App\Models\Country|null $shipping_country
* @property-read \App\Models\Industry|null $industry
* @property-read \App\Models\Size|null $size
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Activity> $activities
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Location> $locations
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\CompanyLedger> $company_ledger
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ClientContact> $contacts
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Credit> $credits

View File

@ -93,6 +93,7 @@ use Laracasts\Presenter\PresentableTrait;
* @property bool $markdown_enabled
* @property bool $use_comma_as_decimal_place
* @property bool $report_include_drafts
* @property bool $invoice_task_project_header
* @property array|null $client_registration_fields
* @property bool $convert_rate_to_client
* @property bool $markdown_email_enabled
@ -129,11 +130,15 @@ use Laracasts\Presenter\PresentableTrait;
* @property int|null $smtp_port
* @property string|null $smtp_encryption
* @property string|null $smtp_local_domain
* @property boolean $invoice_task_item_description
* @property \App\DataMapper\QuickbooksSettings|null $quickbooks
* @property boolean $smtp_verify_peer
* @property object|null $origin_tax_data
* @property int|null $legal_entity_id
* @property-read \App\Models\Account $account
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Activity> $activities
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Location> $locations
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\VerifactuLog> $verifactu_logs
* @property-read int|null $activities_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Activity> $all_activities
* @property-read int|null $all_activities_count
@ -428,6 +433,11 @@ class Company extends BaseModel
return $this->hasMany(Scheduler::class);
}
public function verifactu_logs(): HasMany
{
return $this->hasMany(VerifactuLog::class)->orderBy('id', 'DESC');
}
public function task_schedulers(): HasMany
{
return $this->hasMany(Scheduler::class);
@ -1020,4 +1030,18 @@ class Company extends BaseModel
{
return !$this->account->is_flagged && $this->account->e_invoice_quota > 0 && isset($this->legal_entity_id) && isset($this->tax_data->acts_as_sender) && $this->tax_data->acts_as_sender;
}
/**
* verifactuEnabled
*
* Returns a flag if the current company is using verifactu as the e-invoice provider
*
* @return bool
*/
public function verifactuEnabled(): bool
{
return once(function () {
return $this->getSetting('e_invoice_type') == 'VERIFACTU';
});
}
}

View File

@ -91,6 +91,9 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property string|null $reminder2_sent
* @property string|null $reminder3_sent
* @property string|null $reminder_last_sent
* @property object|null $tax_data
* @property object|null $e_invoice
* @property int|null $location_id
* @property float $paid_to_date
* @property int|null $subscription_id
* @property \Illuminate\Database\Eloquent\Collection<int, \App\Models\Activity> $activities
@ -118,6 +121,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property \App\Models\Client $client
* @property \App\Models\Vendor|null $vendor
* @property-read mixed $pivot
* @property-read \App\Models\Location|null $location
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Activity> $activities
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\CompanyLedger> $company_ledger
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Document> $documents

View File

@ -26,6 +26,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property object|null $design
* @property bool $is_deleted
* @property bool $is_template
* @property string|null $entities
* @property int|null $created_at
* @property int|null $updated_at
* @property int|null $deleted_at

View File

@ -74,6 +74,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property-read int|null $documents_count
* @property-read mixed $hashed_id
* @property-read \App\Models\PaymentType|null $payment_type
* @property-read \App\Models\Currency|null $invoice_currency
* @property-read \App\Models\Project|null $project
* @property-read \App\Models\PurchaseOrder|null $purchase_order
* @property-read \App\Models\User $user

View File

@ -29,6 +29,7 @@ use App\Helpers\Invoice\InvoiceSumInclusive;
use App\Utils\Traits\Invoice\ActionsInvoice;
use Illuminate\Database\Eloquent\SoftDeletes;
use App\Events\Invoice\InvoiceReminderWasEmailed;
use App\DataMapper\InvoiceBackup;
use App\Utils\Number;
/**
@ -38,6 +39,7 @@ use App\Utils\Number;
* @property object|null $e_invoice
* @property int $client_id
* @property int $user_id
* @property int|null $location_id
* @property int|null $assigned_user_id
* @property int $company_id
* @property int $status_id
@ -54,7 +56,7 @@ use App\Utils\Number;
* @property string|null $due_date
* @property bool $is_deleted
* @property object|array|string $line_items
* @property object|null $backup
* @property InvoiceBackup $backup
* @property object|null $sync
* @property string|null $footer
* @property string|null $public_notes
@ -125,6 +127,9 @@ use App\Utils\Number;
* @property-read int|null $tasks_count
* @property-read \App\Models\User $user
* @property-read \App\Models\Vendor|null $vendor
* @property-read \App\Models\Location|null $location
* @property-read \App\Models\Quote|null $quote
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\VerifactuLog> $verifactu_logs
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Activity> $activities
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\CompanyLedger> $company_ledger
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Credit> $credits
@ -203,7 +208,7 @@ class Invoice extends BaseModel
protected $casts = [
'line_items' => 'object',
'backup' => 'object',
'backup' => InvoiceBackup::class,
'updated_at' => 'timestamp',
'created_at' => 'timestamp',
'deleted_at' => 'timestamp',
@ -239,9 +244,9 @@ class Invoice extends BaseModel
public const STATUS_REVERSED = 6;
public const STATUS_OVERDUE = -1; //status < 4 || < 3 && !is_deleted && !trashed() && due_date < now()
public const STATUS_OVERDUE = -1; // status < 4 || < 3 && !is_deleted && !trashed() && due_date < now()
public const STATUS_UNPAID = -2; //status < 4 || < 3 && !is_deleted && !trashed()
public const STATUS_UNPAID = -2; // status < 4 || < 3 && !is_deleted && !trashed()
public function toSearchableArray()
{
@ -400,6 +405,11 @@ class Invoice extends BaseModel
return $this->hasMany(Credit::class);
}
public function verifactu_logs(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(VerifactuLog::class)->orderBy('id', 'desc');
}
public function tasks(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(Task::class);

View File

@ -22,6 +22,8 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property int $id
* @property int $company_id
* @property int $user_id
* @property int $client_id
* @property int $vendor_id
* @property int|null $assigned_user_id
* @property string|null $name
* @property string|null $website

View File

@ -1,4 +1,13 @@
<?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\Models;
@ -31,6 +40,7 @@ use Laracasts\Presenter\PresentableTrait;
* @property bool $is_deleted
* @property string|null $number
* @property string $color
* @property int|null $current_hours
* @property-read \App\Models\Client|null $client
* @property-read \App\Models\Company $company
* @property-read int|null $documents_count
@ -53,6 +63,9 @@ use Laracasts\Presenter\PresentableTrait;
* @method static \Illuminate\Database\Eloquent\Builder|Project withoutTrashed()
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Document> $documents
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Task> $tasks
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Invoice> $invoices
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Quote> $quotes
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Expense> $expenses
* @mixin \Eloquent
*/
class Project extends BaseModel
@ -129,17 +142,17 @@ class Project extends BaseModel
return $this->hasMany(Task::class);
}
public function expenses(): HasMany
public function expenses(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(Expense::class);
}
public function invoices(): HasMany
public function invoices(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(Invoice::class)->withTrashed();
}
public function quotes(): HasMany
public function quotes(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(Quote::class);
}

View File

@ -59,6 +59,7 @@ use App\Events\PurchaseOrder\PurchaseOrderWasEmailed;
* @property float $tax_rate3
* @property float $total_taxes
* @property bool $uses_inclusive_taxes
* @property int|null $location_id
* @property string|null $reminder1_sent
* @property string|null $reminder2_sent
* @property string|null $reminder3_sent
@ -100,6 +101,10 @@ use App\Events\PurchaseOrder\PurchaseOrderWasEmailed;
* @property \App\Models\User $user
* @property \App\Models\Vendor $vendor
* @property \App\Models\PurchaseOrderInvitation $invitation
* @property \App\Models\Currency|null $currency
* @property \App\Models\Location|null $location
* @property object|null $tax_data
* @property object|null $e_invoice
* @method static \Illuminate\Database\Eloquent\Builder|PurchaseOrder exclude($columns)
* @method static \Database\Factories\PurchaseOrderFactory factory($count = null, $state = [])
* @method static \Illuminate\Database\Eloquent\Builder|PurchaseOrder filter(\App\Filters\QueryFilters $filters)

View File

@ -81,6 +81,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property int $custom_surcharge_tax2
* @property int $custom_surcharge_tax3
* @property int $custom_surcharge_tax4
* @property int|null $location_id
* @property float $exchange_rate
* @property float $amount
* @property float $balance
@ -94,6 +95,9 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property string|null $reminder2_sent
* @property string|null $reminder3_sent
* @property string|null $reminder_last_sent
* @property int|null $location_id
* @property object|null $tax_data
* @property object|null $e_invoice
* @property float $paid_to_date
* @property int|null $subscription_id
* @property \App\Models\User|null $assigned_user
@ -110,6 +114,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property-read \App\Models\Project|null $project
* @property-read \App\Models\User $user
* @property-read \App\Models\Vendor|null $vendor
* @property-read \App\Models\Location|null $location
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Activity> $activities
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Document> $documents
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Backup> $history

View File

@ -83,6 +83,7 @@ use App\Models\Presenters\RecurringInvoicePresenter;
* @property bool $custom_surcharge_tax3
* @property bool $custom_surcharge_tax4
* @property string|null $due_date_days
* @property int|null $location_id
* @property string|null $partial_due_date
* @property float $exchange_rate
* @property float $paid_to_date
@ -108,6 +109,7 @@ use App\Models\Presenters\RecurringInvoicePresenter;
* @property-read \App\Models\Subscription|null $subscription
* @property-read \App\Models\User $user
* @property-read \App\Models\Vendor|null $vendor
* @property-read \App\Models\Location|null $location
* @method static \Illuminate\Database\Eloquent\Builder|BaseModel exclude($columns)
* @method static \Database\Factories\RecurringInvoiceFactory factory($count = null, $state = [])
* @method static \Illuminate\Database\Eloquent\Builder|RecurringInvoice filter(\App\Filters\QueryFilters $filters)

View File

@ -57,6 +57,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property int $use_inventory_management
* @property string|null $optional_product_ids
* @property string|null $optional_recurring_product_ids
* @property string|null $steps
* @property-read \App\Models\Company $company
* @property-read mixed $hashed_id
* @property-read \App\Models\GroupSetting|null $group_settings

View File

@ -78,6 +78,8 @@ class SystemLog extends Model
public const CATEGORY_LOG = 6;
public const CATEGORY_VERIFACTU = 7;
/* Event IDs*/
public const EVENT_PAYMENT_RECONCILIATION_FAILURE = 10;
@ -115,6 +117,10 @@ class SystemLog extends Model
public const EVENT_INBOUND_MAIL_BLOCKED = 62;
public const EVENT_VERIFACTU_FAILURE = 70;
public const EVENT_VERIFACTU_SUCCESS = 71;
/*Type IDs*/
public const TYPE_PAYPAL = 300;
@ -180,6 +186,12 @@ class SystemLog extends Model
public const TYPE_GENERIC = 900;
public const TYPE_VERIFACTU_CANCELLATION = 1000;
public const TYPE_VERIFACTU_INVOICE = 1001;
public const TYPE_VERIFACTU_RECTIFICATION = 1002;
protected $fillable = [
'client_id',
'company_id',

View File

@ -570,8 +570,8 @@ class User extends Authenticatable implements MustVerifyEmail
*
* Note, returning FALSE here means the user does NOT have the permission we want to exclude
*
* @param array $matched_permission
* @param array $excluded_permissions
* @param array $matched_permission = []
* @param array $excluded_permissions = []
* @return bool
*/
public function hasExcludedPermissions(array $matched_permission = [], array $excluded_permissions = []): bool

View File

@ -54,6 +54,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property string|null $custom_value4
* @property string|null $vendor_hash
* @property string|null $public_notes
* @property string|null $classification
* @property string|null $id_number
* @property int|null $language_id
* @property int|null $last_login
@ -71,6 +72,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\VendorContact> $primary_contact
* @property-read int|null $primary_contact_count
* @property-read \App\Models\User $user
* @property-read \App\Models\Language|null $language
* @method static \Illuminate\Database\Eloquent\Builder|BaseModel exclude($columns)
* @method static \Database\Factories\VendorFactory factory($count = null, $state = [])
* @method static \Illuminate\Database\Eloquent\Builder|Vendor filter(\App\Filters\QueryFilters $filters)
@ -85,6 +87,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\VendorContact> $contacts
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Document> $documents
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\VendorContact> $primary_contact
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Location> $locations
* @mixin \Eloquent
*/
class Vendor extends BaseModel

View File

@ -0,0 +1,60 @@
<?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\Models;
use App\Models\Company;
use Illuminate\Database\Eloquent\Model;
use App\Models\Invoice;
/**
* @property int $id
* @property int $company_id
* @property int $invoice_id
* @property string $nif
* @property \Carbon\Carbon $date
* @property string $invoice_number
* @property string $hash
* @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
* @property-read \App\Models\Invoice $invoice
*/
class VerifactuLog extends Model
{
public $timestamps = true;
protected $casts = [
'date' => 'date',
'response' => 'object',
];
protected $guarded = ['id'];
public function company()
{
return $this->belongsTo(Company::class);
}
public function invoice()
{
return $this->belongsTo(Invoice::class);
}
public function deserialize()
{
return \App\Services\EDocument\Standards\Verifactu\Models\Invoice::unserialize($this->state);
}
}

View File

@ -108,6 +108,7 @@ class ACH implements MethodInterface, LivewireMethodInterface
return redirect()->route('client.payment_methods.index')->withMessage(ctrans('texts.payment_method_added'));
} catch (\Exception $e) {
nlog($e->getMessage());
return $this->braintree->processInternallyFailedPayment($this->braintree, $e);
}
}

View File

@ -328,6 +328,11 @@ class BaseRepository
nlog($e->getMessage());
}
}
/** Verifactu modified invoice check */
if($model->company->verifactuEnabled()) {
$model->service()->modifyVerifactuWorkflow($data, $this->new_model)->save();
}
}
if ($model instanceof Credit) {

View File

@ -1,93 +0,0 @@
<?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\Gateway\Qvalia;
class Invoice
{
public function __construct(public Qvalia $qvalia)
{
}
// Methods
/**
* status
*
* @param string $legal_entity_id
* @param string $integration_id
* @return mixed
*/
// {
// "status": "",
// "data": {
// "message": "",
// "status": {
// "document_id": "",
// "order_number": "",
// "payment_reference": "",
// "credit_note": "",
// "reminder": "",
// "status": "",
// "sent_at": "",
// "paid_at": "",
// "cancelled_at": "",
// "send_method": ""
// }
// }
// }
/**
* status
*
* @param string $legal_entity_id
* @param string $integration_id
* @return mixed
*/
public function status(string $legal_entity_id, string $integration_id)
{
$uri = "/account/{$legal_entity_id}/action/invoice/outgoing/status/{$integration_id}";
$r = $this->qvalia->httpClient($uri, (\App\Enum\HttpVerb::GET)->value, []);
return $r->object();
}
/**
* send
*
* @param string $legal_entity_id
* @param string $document
* @return mixed
*/
public function send(string $legal_entity_id, string $document)
{
// Set Headers
// Either "application/json" (default) or "application/xml"
$headers = [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
// 'Content-Type' => 'application/xml',
];
$data = [
'Invoice' => $document
];
$uri = "/transaction/{$legal_entity_id}/invoices/outgoing";
$r = $this->qvalia->httpClient($uri, (\App\Enum\HttpVerb::POST)->value, $data, $headers);
return $r->object();
}
}

View File

@ -1,186 +0,0 @@
<?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\Gateway\Qvalia;
use App\Services\EDocument\Gateway\MutatorUtil;
use App\Services\EDocument\Gateway\MutatorInterface;
class Mutator implements MutatorInterface
{
private \InvoiceNinja\EInvoice\Models\Peppol\Invoice $p_invoice;
private ?\InvoiceNinja\EInvoice\Models\Peppol\Invoice $_client_settings;
private ?\InvoiceNinja\EInvoice\Models\Peppol\Invoice $_company_settings;
private $invoice;
private MutatorUtil $mutator_util;
public function __construct(public Qvalia $qvalia)
{
$this->mutator_util = new MutatorUtil($this);
}
public function setInvoice($invoice): self
{
$this->invoice = $invoice;
return $this;
}
public function setPeppol($p_invoice): self
{
$this->p_invoice = $p_invoice;
return $this;
}
public function getPeppol(): mixed
{
return $this->p_invoice;
}
public function getClientSettings(): mixed
{
return $this->_client_settings;
}
public function getCompanySettings(): mixed
{
return $this->_company_settings;
}
public function setClientSettings($client_settings): self
{
$this->_client_settings = $client_settings;
return $this;
}
public function setCompanySettings($company_settings): self
{
$this->_company_settings = $company_settings;
return $this;
}
public function getInvoice(): mixed
{
return $this->invoice;
}
public function getSetting(string $property_path): mixed
{
return $this->mutator_util->getSetting($property_path);
}
/**
* senderSpecificLevelMutators
*
* Runs sender level specific requirements for the e-invoice,
*
* ie, mutations that are required by the senders country.
*
* @return self
*/
public function senderSpecificLevelMutators(): self
{
if (method_exists($this, $this->invoice->company->country()->iso_3166_2)) {
$this->{$this->invoice->company->country()->iso_3166_2}();
}
return $this;
}
/**
* receiverSpecificLevelMutators
*
* Runs receiver level specific requirements for the e-invoice
*
* ie mutations that are required by the receiving country
* @return self
*/
public function receiverSpecificLevelMutators(): self
{
if (method_exists($this, "client_{$this->invoice->company->country()->iso_3166_2}")) {
$this->{"client_{$this->invoice->company->country()->iso_3166_2}"}();
}
return $this;
}
// Country-specific methods
public function DE(): self
{
return $this;
}
public function CH(): self
{
return $this;
}
public function AT(): self
{
return $this;
}
public function AU(): self
{
return $this;
}
public function ES(): self
{
return $this;
}
public function FI(): self
{
return $this;
}
public function FR(): self
{
return $this;
}
public function IT(): self
{
return $this;
}
public function client_IT(): self
{
return $this;
}
public function MY(): self
{
return $this;
}
public function NL(): self
{
return $this;
}
public function NZ(): self
{
return $this;
}
public function PL(): self
{
return $this;
}
public function RO(): self
{
return $this;
}
public function SG(): self
{
return $this;
}
public function SE(): self
{
return $this;
}
}

View File

@ -1,143 +0,0 @@
<?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\Gateway\Qvalia;
class Partner
{
private string $partner_number;
public function __construct(public Qvalia $qvalia)
{
$this->partner_number = config('ninja.qvalia_partner_number');
}
/**
* getAccount
*
* Get Partner Account Object
* @return mixed
*/
public function getAccount()
{
$uri = "/partner/{$this->partner_number}/account";
$r = $this->qvalia->httpClient($uri, (\App\Enum\HttpVerb::GET)->value, []);
return $r->object();
}
/**
* getPeppolId
*
* Get information on a peppol ID
* @param string $id
* @return mixed
*/
public function getPeppolId(string $id)
{
$uri = "/partner/{$this->partner_number}/peppol/lookup/{$id}";
$uri = "/partner/{$this->partner_number}/account";
$r = $this->qvalia->httpClient($uri, (\App\Enum\HttpVerb::GET)->value, []);
return $r->object();
}
/**
* getAccountId
*
* Get information on a Invoice Ninja Peppol Client Account
* @param string $id
* @return mixed
*/
public function getAccountId(string $id)
{
$uri = "/partner/{$this->partner_number}/account/{$id}";
}
/**
* createAccount
*
* Create a new account for the partner
* @param array $data
* @return mixed
*/
public function createAccount(array $data)
{
$uri = "/partner/{$this->partner_number}/account";
return $this->qvalia->httpClient($uri, (\App\Enum\HttpVerb::POST)->value, $data)->object();
}
/**
* updateAccount
*
* Update an existing account for the partner
* @param string $accountRegNo
* @param array $data
* @return mixed
*/
public function updateAccount(string $accountRegNo, array $data)
{
$uri = "/partner/{$this->partner_number}/account/{$accountRegNo}";
return $this->qvalia->httpClient($uri, (\App\Enum\HttpVerb::PUT)->value, $data)->object();
}
/**
* deleteAccount
*
* Delete an account for the partner
* @param string $accountRegNo
* @return mixed
*/
public function deleteAccount(string $accountRegNo)
{
$uri = "/partner/{$this->partner_number}/account/{$accountRegNo}";
return $this->qvalia->httpClient($uri, (\App\Enum\HttpVerb::DELETE)->value, [])->object();
}
/**
* updatePeppolId
*
* Update a Peppol ID for an account
* @param string $accountRegNo
* @param string $peppolId
* @param array $data
* @return mixed
*/
public function updatePeppolId(string $accountRegNo, string $peppolId, array $data)
{
$uri = "/partner/{$this->partner_number}/account/{$accountRegNo}/peppol/{$peppolId}";
return $this->qvalia->httpClient($uri, (\App\Enum\HttpVerb::PUT)->value, $data)->object();
}
/**
* deletePeppolId
*
* Delete a Peppol ID for an account
* @param string $accountRegNo
* @param string $peppolId
* @return mixed
*/
public function deletePeppolId(string $accountRegNo, string $peppolId)
{
$uri = "/partner/{$this->partner_number}/account/{$accountRegNo}/peppol/{$peppolId}";
return $this->qvalia->httpClient($uri, (\App\Enum\HttpVerb::DELETE)->value, [])->object();
}
}

View File

@ -1,125 +0,0 @@
<?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\Gateway\Qvalia;
use App\DataMapper\Analytics\LegalEntityCreated;
use App\Models\Company;
use Illuminate\Support\Facades\Http;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\ServerException;
use Illuminate\Http\Client\RequestException;
use Turbo124\Beacon\Facades\LightLogs;
class Qvalia
{
/** @var string $base_url */
private string $base_url = 'https://api.qvalia.com';
/** @var string $sandbox_base_url */
private string $sandbox_base_url = 'https://api-qa.qvalia.com';
private bool $test_mode = true;
/** @var array $peppol_discovery */
private array $peppol_discovery = [
"documentTypes" => ["invoice"],
"network" => "peppol",
"metaScheme" => "iso6523-actorid-upis",
"scheme" => "de:lwid",
"identifier" => "DE:VAT"
];
/** @var array $dbn_discovery */
private array $dbn_discovery = [
"documentTypes" => ["invoice"],
"network" => "dbnalliance",
"metaScheme" => "iso6523-actorid-upis",
"scheme" => "gln",
"identifier" => "1200109963131"
];
private ?int $legal_entity_id;
public Partner $partner;
public Invoice $invoice;
public Mutator $mutator;
//integrationid - returned in headers
public function __construct()
{
$this->init();
$this->partner = new Partner($this);
$this->invoice = new Invoice($this);
$this->mutator = new Mutator($this);
}
private function init(): self
{
if ($this->test_mode) {
$this->base_url = $this->sandbox_base_url;
}
return $this;
}
public function sendDocument($legal_entity_id)
{
$uri = "/transaction/{$legal_entity_id}/invoices/outgoing";
$verb = 'POST';
}
/**
* httpClient
*
* @param string $uri
* @param string $verb
* @param array $data
* @param array $headers
* @return \Illuminate\Http\Client\Response
*/
public function httpClient(string $uri, string $verb, array $data, ?array $headers = [])
{
try {
$r = Http::withToken(config('ninja.qvalia_api_key'))
->withHeaders($this->getHeaders($headers))
->{$verb}("{$this->base_url}{$uri}", $data)->throw();
} catch (ClientException $e) {
// 4xx errors
nlog("LEI:: {$this->legal_entity_id}");
nlog("Client error: " . $e->getMessage());
nlog("Response body: " . $e->getResponse()->getBody()->getContents());
} catch (ServerException $e) {
// 5xx errors
nlog("LEI:: {$this->legal_entity_id}");
nlog("Server error: " . $e->getMessage());
nlog("Response body: " . $e->getResponse()->getBody()->getContents());
} catch (\Illuminate\Http\Client\RequestException $e) {
nlog("LEI:: {$this->legal_entity_id}");
nlog("Request error: {$e->getCode()}: " . $e->getMessage());
$responseBody = $e->response->body();
nlog("Response body: " . $responseBody);
return $e->response;
}
return $r; // @phpstan-ignore-line
}
}

View File

@ -59,7 +59,7 @@ class SendEDocument implements ShouldQueue
$model = $this->entity::withTrashed()->find($this->id);
if(isset($model->backup->guid) && is_string($model->backup->guid)){
if(isset($model->backup->guid) && is_string($model->backup->guid) && strlen($model->backup->guid) > 3){
nlog("already sent!");
return;
}
@ -217,9 +217,9 @@ class SendEDocument implements ShouldQueue
if($activity_id == Activity::EINVOICE_DELIVERY_SUCCESS){
$backup = ($model->backup && is_object($model->backup)) ? $model->backup : new \stdClass();
$backup->guid = str_replace('"', '', $notes);
$model->backup = $backup;
// $backup = ($model->backup && is_object($model->backup)) ? $model->backup : new \stdClass();
// $backup->guid = str_replace('"', '', $notes);
$model->backup->guid = str_replace('"', '', $notes);
$model->saveQuietly();
}

View File

@ -21,7 +21,6 @@ use App\Helpers\Invoice\InvoiceSum;
use InvoiceNinja\EInvoice\EInvoice;
use App\Utils\Traits\NumberFormatter;
use App\Helpers\Invoice\InvoiceSumInclusive;
use App\Services\EDocument\Gateway\Qvalia\Qvalia;
use InvoiceNinja\EInvoice\Models\Peppol\ItemType\Item;
use App\Services\EDocument\Gateway\Storecove\Storecove;
use InvoiceNinja\EInvoice\Models\Peppol\PartyType\Party;
@ -139,9 +138,9 @@ class Peppol extends AbstractService
private EInvoice $e;
private string $api_network = Storecove::class; // Storecove::class; // Qvalia::class;
private string $api_network = Storecove::class; // Storecove::class;
public Qvalia | Storecove $gateway;
public Storecove $gateway;
private string $customizationID = 'urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0';
@ -159,6 +158,7 @@ class Peppol extends AbstractService
public function __construct(public Invoice $invoice)
{
$this->company = $invoice->company;
$this->calc = $this->invoice->calc();
$this->e = new EInvoice();

View File

@ -0,0 +1,27 @@
<?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;
use App\Models\Client;
use App\Models\Company;
use App\Models\Invoice;
interface EntityLevelInterface
{
public function checkClient(Client $client): array;
public function checkCompany(Company $company): array;
public function checkInvoice(Invoice $invoice): array;
}

View File

@ -21,10 +21,11 @@ use App\Models\Invoice;
use App\Models\PurchaseOrder;
use App\Services\EDocument\Standards\Peppol;
use App\Services\EDocument\Standards\Validation\XsltDocumentValidator;
use App\Services\EDocument\Standards\Validation\EntityLevelInterface;
use Illuminate\Support\Facades\App;
use XSLTProcessor;
class EntityLevel
class EntityLevel implements EntityLevelInterface
{
private array $eu_country_codes = [
'AT', // Austria

View File

@ -0,0 +1,347 @@
<?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\Verifactu;
use App\Models\Quote;
use App\Models\Client;
use App\Models\Credit;
use App\Models\Vendor;
use App\Models\Company;
use App\Models\Invoice;
use App\Models\PurchaseOrder;
use Illuminate\Support\Facades\App;
use App\Services\EDocument\Standards\Validation\EntityLevelInterface;
//@todo - need to implement a rule set for verifactu for validation
class EntityLevel implements EntityLevelInterface
{
private array $errors = [];
private array $client_fields = [
// 'address1',
// 'city',
// 'state',
// 'postal_code',
// 'vat_number',
'country_id',
];
private array $company_settings_fields = [
// 'address1',
// 'city',
// 'state',
// 'postal_code',
'vat_number',
'country_id',
];
public function __construct(){}
private function init(string $locale): self
{
App::forgetInstance('translator');
$t = app('translator');
App::setLocale($locale);
return $this;
}
public function checkClient(Client $client): array
{
$this->init($client->locale());
$this->errors['client'] = $this->testClientState($client);
$this->errors['passes'] = count($this->errors['client']) == 0;
return $this->errors;
}
public function checkCompany(Company $company): array
{
$this->init($company->locale());
$this->errors['company'] = $this->testCompanyState($company);
$this->errors['passes'] = count($this->errors['company']) == 0;
return $this->errors;
}
public function checkInvoice(Invoice $invoice): array
{
$this->init($invoice->client->locale());
$this->errors['invoice'] = [];
$this->errors['client'] = $this->testClientState($invoice->client);
$this->errors['company'] = $this->testCompanyState($invoice->client); // uses client level settings which is what we want
if (count($this->errors['client']) > 0) {
$this->errors['passes'] = false;
return $this->errors;
}
$_invoice = (new \App\Services\EDocument\Standards\Verifactu\RegistroAlta($invoice))->run()->getInvoice();
$xml = $_invoice->toXmlString();
$xslt = new \App\Services\EDocument\Standards\Validation\VerifactuDocumentValidator($xml);
$xslt->validate();
$errors = $xslt->getVerifactuErrors();
nlog($errors);
if (isset($errors['stylesheet']) && count($errors['stylesheet']) > 0) {
$this->errors['invoice'] = array_merge($this->errors['invoice'], $errors['stylesheet']);
}
if (isset($errors['general']) && count($errors['general']) > 0) {
$this->errors['invoice'] = array_merge($this->errors['invoice'], $errors['general']);
}
if (isset($errors['xsd']) && count($errors['xsd']) > 0) {
$this->errors['invoice'] = array_merge($this->errors['invoice'], $errors['xsd']);
}
// $this->errors['invoice'][] = 'test error';
$this->errors['passes'] = count($this->errors['invoice']) === 0 && count($this->errors['company']) === 0; //no need to check client as we are using client level settings
return $this->errors;
// $p = new Peppol($invoice);
// $xml = false;
// try {
// $xml = $p->run()->toXml();
// if (count($p->getErrors()) >= 1) {
// foreach ($p->getErrors() as $error) {
// $this->errors['invoice'][] = $error;
// }
// }
// } catch (PeppolValidationException $e) {
// $this->errors['invoice'] = ['field' => $e->getInvalidField(), 'label' => $e->getInvalidField()];
// } catch (\Throwable $th) {
// }
// if ($xml) {
// // Second pass through the XSLT validator
// $xslt = new XsltDocumentValidator($xml);
// $errors = $xslt->validate()->getErrors();
// if (isset($errors['stylesheet']) && count($errors['stylesheet']) > 0) {
// $this->errors['invoice'] = array_merge($this->errors['invoice'], $errors['stylesheet']);
// }
// if (isset($errors['general']) && count($errors['general']) > 0) {
// $this->errors['invoice'] = array_merge($this->errors['invoice'], $errors['general']);
// }
// if (isset($errors['xsd']) && count($errors['xsd']) > 0) {
// $this->errors['invoice'] = array_merge($this->errors['invoice'], $errors['xsd']);
// }
// }
// $this->checkNexus($invoice->client);
}
private function testClientState(Client $client): array
{
$errors = [];
foreach ($this->client_fields as $field) {
if ($this->validString($client->{$field})) {
continue;
}
if ($field == 'country_id' && $client->country_id >= 1) {
continue;
}
// if($field == 'vat_number' && $client->classification == 'individual') {
// continue;
// }
$errors[] = ['field' => $field, 'label' => ctrans("texts.{$field}")];
}
/** Spanish Client Validation requirements */
if ($client->country_id == 724) {
if (in_array($client->classification, ['','individual']) && strlen($client->id_number ?? '') == 0 && strlen($client->vat_number ?? '') == 0) {
$errors[] = ['field' => 'id_number', 'label' => ctrans("texts.id_number")];
} elseif (!in_array($client->classification, ['','individual']) && strlen($client->vat_number ?? '')) {
$errors[] = ['field' => 'vat_number', 'label' => ctrans("texts.vat_number")];
}
}
// else{
// //If not an individual, you MUST have a VAT number if you are in the EU
// if (!in_array($client->classification,['','individual']) && !$this->validString($client->vat_number)) {
// $errors[] = ['field' => 'vat_number', 'label' => ctrans("texts.vat_number")];
// }
// }
return $errors;
}
private function testCompanyState(mixed $entity): array
{
$client = false;
$vendor = false;
$settings_object = false;
$company = false;
if ($entity instanceof Client) {
$client = $entity;
$company = $entity->company;
$settings_object = $client;
} elseif ($entity instanceof Company) {
$company = $entity;
$settings_object = $company;
} elseif ($entity instanceof Vendor) {
$vendor = $entity;
$company = $entity->company;
$settings_object = $company;
} elseif ($entity instanceof Invoice || $entity instanceof Credit || $entity instanceof Quote) {
$client = $entity->client;
$company = $entity->company;
$settings_object = $entity->client;
} elseif ($entity instanceof PurchaseOrder) {
$vendor = $entity->vendor;
$company = $entity->company;
$settings_object = $company;
}
$errors = [];
foreach ($this->company_settings_fields as $field) {
if ($this->validString($settings_object->getSetting($field))) {
continue;
}
$errors[] = ['field' => $field, 'label' => ctrans("texts.{$field}")];
}
//If not an individual, you MUST have a VAT number
if ($company->getSetting('classification') != 'individual' && !$this->validString($company->getSetting('vat_number'))) {
$errors[] = ['field' => 'vat_number', 'label' => ctrans("texts.vat_number")];
} elseif ($company->getSetting('classification') == 'individual' && !$this->validString($company->getSetting('id_number'))) {
$errors[] = ['field' => 'id_number', 'label' => ctrans("texts.id_number")];
}
if(!$this->isValidSpanishVAT($company->getSetting('vat_number'))) {
$errors[] = ['field' => 'vat_number', 'label' => ctrans("texts.vat_number")];
}
return $errors;
}
private function validString(?string $string): bool
{
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)
}

View File

@ -0,0 +1,207 @@
<?php
namespace App\Services\EDocument\Standards\Validation\Verifactu;
use App\Services\EDocument\Standards\Verifactu\Models\Invoice;
use InvalidArgumentException;
class InvoiceValidator
{
/**
* Validate an invoice against AEAT business rules
*/
public function validate(Invoice $invoice): array
{
$errors = [];
// Validate NIF format
$errors = array_merge($errors, $this->validateNif($invoice));
// Validate date formats
$errors = array_merge($errors, $this->validateDates($invoice));
// Validate invoice numbers
$errors = array_merge($errors, $this->validateInvoiceNumbers($invoice));
// Validate amounts
$errors = array_merge($errors, $this->validateAmounts($invoice));
// Validate tax rates
$errors = array_merge($errors, $this->validateTaxRates($invoice));
// Validate business logic
$errors = array_merge($errors, $this->validateBusinessLogic($invoice));
return $errors;
}
/**
* Validate NIF format (Spanish tax identification)
*/
private function validateNif(Invoice $invoice): array
{
$errors = [];
// Check emitter NIF
if ($invoice->getTercero() && $invoice->getTercero()->getNif()) {
$nif = $invoice->getTercero()->getNif();
if (!$this->isValidNif($nif)) {
$errors[] = "Invalid emitter NIF format: {$nif}";
}
}
// Check system NIF
if ($invoice->getSistemaInformatico() && $invoice->getSistemaInformatico()->getNif()) {
$nif = $invoice->getSistemaInformatico()->getNif();
if (!$this->isValidNif($nif)) {
$errors[] = "Invalid system NIF format: {$nif}";
}
}
return $errors;
}
/**
* Validate date formats
*/
private function validateDates(Invoice $invoice): array
{
$errors = [];
// Validate FechaHoraHusoGenRegistro format (YYYY-MM-DDTHH:MM:SS+HH:MM)
$fechaHora = $invoice->getFechaHoraHusoGenRegistro();
if ($fechaHora && !preg_match('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2}$/', $fechaHora)) {
$errors[] = "Invalid FechaHoraHusoGenRegistro format. Expected: YYYY-MM-DDTHH:MM:SS+HH:MM, Got: {$fechaHora}";
}
return $errors;
}
/**
* Validate amounts
*/
private function validateAmounts(Invoice $invoice): array
{
$errors = [];
// Validate total amounts
if ($invoice->getImporteTotal() <= 0) {
$errors[] = "ImporteTotal must be greater than 0";
}
if ($invoice->getCuotaTotal() < 0) {
$errors[] = "CuotaTotal cannot be negative (use rectification invoice for negative amounts)";
}
// Validate decimal places (AEAT expects 2 decimal places)
if (fmod($invoice->getImporteTotal() * 100, 1) !== 0.0) {
$errors[] = "ImporteTotal must have maximum 2 decimal places";
}
if (fmod($invoice->getCuotaTotal() * 100, 1) !== 0.0) {
$errors[] = "CuotaTotal must have maximum 2 decimal places";
}
return $errors;
}
/**
* Validate tax rates
*/
private function validateTaxRates(Invoice $invoice): array
{
$errors = [];
// Check if desglose exists and has valid tax rates
// if ($invoice->getDesglose()) {
// $desglose = $invoice->getDesglose();
// // Validate tax rates are standard Spanish rates
// $validRates = [0, 4, 10, 21];
// // This would need to be implemented based on your Desglose structure
// // $taxRate = $desglose->getTipoImpositivo();
// // if (!in_array($taxRate, $validRates)) {
// // $errors[] = "Invalid tax rate: {$taxRate}. Valid rates are: " . implode(', ', $validRates);
// // }
// }
return $errors;
}
/**
* Validate business logic rules
*/
private function validateBusinessLogic(Invoice $invoice): array
{
$errors = [];
// Check for required fields based on invoice type
if ($invoice->getTipoFactura() === 'R2' && !$invoice->getTipoRectificativa()) {
$errors[] = "Rectification invoices (R2) must specify TipoRectificativa";
}
// Check for simplified invoice requirements
if ($invoice->getTipoFactura() === 'F2' && !$invoice->getFacturaSimplificadaArt7273()) {
$errors[] = "Simplified invoices (F2) must specify FacturaSimplificadaArt7273";
}
// Check for system information requirements
if (!$invoice->getSistemaInformatico()) {
$errors[] = "SistemaInformatico is required for all invoices";
}
// Check for encadenamiento requirements
if (!$invoice->getEncadenamiento()) {
$errors[] = "Encadenamiento is required for all invoices";
}
return $errors;
}
/**
* Check if NIF format is valid for Spanish tax identification
*/
private function isValidNif(string $nif): bool
{
// Basic format validation for Spanish NIFs
// Company NIFs: Letter + 8 digits (e.g., B12345678)
// Individual NIFs: 8 digits + letter (e.g., 12345678A)
$pattern = '/^([A-Z]\d{8}|\d{8}[A-Z])$/';
return preg_match($pattern, $nif) === 1;
}
/**
* Get validation rules as array for documentation
*/
public function getValidationRules(): array
{
return [
'nif' => [
'format' => 'Company: Letter + 8 digits (B12345678), Individual: 8 digits + letter (12345678A)',
'required' => true
],
'dates' => [
'FechaHoraHusoGenRegistro' => 'YYYY-MM-DDTHH:MM:SS+HH:MM',
'FechaExpedicionFactura' => 'YYYY-MM-DD'
],
'amounts' => [
'decimal_places' => 'Maximum 2 decimal places',
'positive' => 'ImporteTotal must be positive',
'tax_rates' => 'Valid rates: 0%, 4%, 10%, 21%'
],
'invoice_numbers' => [
'min_length' => 'Test numbers should be at least 10 characters',
'characters' => 'Only letters, numbers, hyphens, underscores'
],
'business_logic' => [
'R1_invoices' => 'Must specify TipoRectificativa',
'F2_invoices' => 'Must specify FacturaSimplificadaArt7273',
'required_fields' => 'SistemaInformatico and Encadenamiento are required'
]
];
}
}

View File

@ -0,0 +1,609 @@
<?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;
/**
* VerifactuDocumentValidator - Validates Verifactu XML documents
*
* Extends the base XsltDocumentValidator but is configured specifically for Verifactu
* validation using the correct XSD schemas and namespaces.
*/
class VerifactuDocumentValidator extends XsltDocumentValidator
{
private array $verifactu_stylesheets = [
// Add any Verifactu-specific stylesheets here if needed
// '/Services/EDocument/Standards/Validation/Verifactu/Stylesheets/verifactu-validation.xslt',
];
private string $verifactu_xsd = 'Services/EDocument/Standards/Verifactu/xsd/SuministroLR.xsd';
private string $verifactu_informacion_xsd = 'Services/EDocument/Standards/Verifactu/xsd/SuministroInformacion.xsd';
public function __construct(public string $xml_document)
{
parent::__construct($xml_document);
// Override the base configuration for Verifactu
$this->setXsd($this->verifactu_xsd);
$this->setStyleSheets($this->verifactu_stylesheets);
}
/**
* Validate Verifactu XML document
*
* @return self
*/
public function validate(): self
{
$this->validateVerifactuXsd()
->validateVerifactuSchema();
return $this;
}
/**
* Validate against Verifactu XSD schemas
*/
private function validateVerifactuXsd(): self
{
libxml_use_internal_errors(true);
$xml = new \DOMDocument();
$xml->loadXML($this->xml_document);
// Extract business content from SOAP envelope if needed
$businessContent = $this->extractBusinessContent($xml);
// Detect document type to determine which validation to apply
$documentType = $this->detectDocumentType($businessContent);
nlog("Detected document type: " . $documentType);
// For modifications, we need to use a different validation approach
// since the standard XSD doesn't support modification structure
if ($documentType === 'modification') {
$this->validateModificationDocument($businessContent);
} else {
// For registration and cancellation, use standard XSD validation
if (!$businessContent->schemaValidate(app_path($this->verifactu_xsd))) {
$errors = libxml_get_errors();
libxml_clear_errors();
foreach ($errors as $error) {
$this->errors['xsd'][] = $this->formatXsdError($error);
}
}
}
return $this;
}
/**
* Format XSD validation errors to be more human-readable
*
* @param \LibXMLError $error The libxml error object
* @return string Formatted error message
*/
private function formatXsdError(\LibXMLError $error): string
{
$message = trim($error->message);
$line = $error->line;
// Remove long namespace URLs to make errors more readable
$message = preg_replace(
'/\{https:\/\/www2\.agenciatributaria\.gob\.es\/static_files\/common\/internet\/dep\/aplicaciones\/es\/aeat\/tike\/cont\/ws\/[^}]+\}/',
'',
$message
);
// Clean up the message and make it more user-friendly
$message = $this->translateXsdError($message);
return sprintf('Line %d: %s', $line, $message);
}
/**
* Translate XSD error messages to more user-friendly Spanish/English descriptions
*
* @param string $message The original XSD error message
* @return string Translated and improved error message
*/
private function translateXsdError(string $message): string
{
// Handle missing child element error specifically
if (preg_match('/Missing child element\(s\)\. Expected is \( ([^)]+) \)/', $message, $matches)) {
$expectedElement = trim($matches[1]);
$message = "Missing required child element: $expectedElement";
}
// Common error patterns and their translations
$errorTranslations = [
// Element not found
'/Element ([^:]+): ([^:]+) not found/' => 'Element not found: $2',
// Invalid content
'/Element ([^:]+): ([^:]+) has invalid content/' => 'Invalid content in element: $2',
// Required attribute missing
'/The attribute ([^:]+) is required/' => 'Required attribute missing: $1',
// Value not allowed
'/Value ([^:]+) is not allowed/' => 'Value not allowed: $1',
// Pattern validation failed
'/Element ([^:]+): ([^:]+) is not a valid value of the atomic type/' => 'Invalid value for element: $2',
];
// Apply translations
foreach ($errorTranslations as $pattern => $replacement) {
if (preg_match($pattern, $message, $matches)) {
$message = preg_replace($pattern, $replacement, $message);
break;
}
}
// Clean up common element names and make them more readable
$elementTranslations = [
'Desglose' => 'Desglose (Tax Breakdown)',
'DetalleDesglose' => 'DetalleDesglose (Tax Detail)',
'TipoFactura' => 'TipoFactura (Invoice Type)',
'DescripcionOperacion' => 'DescripcionOperacion (Operation Description)',
'ImporteTotal' => 'ImporteTotal (Total Amount)',
'RegistroAlta' => 'RegistroAlta (Registration Record)',
'RegistroAnulacion' => 'RegistroAnulacion (Cancellation Record)',
'FacturasRectificadas' => 'FacturasRectificadas (Corrected Invoices)',
'IDFacturaRectificada' => 'IDFacturaRectificada (Corrected Invoice ID)',
'IDEmisorFactura' => 'IDEmisorFactura (Invoice Emitter ID)',
'NumSerieFactura' => 'NumSerieFactura (Invoice Series Number)',
'FechaExpedicionFactura' => 'FechaExpedicionFactura (Invoice Issue Date)',
'Impuestos' => 'Impuestos (Taxes)',
'DetalleIVA' => 'DetalleIVA (VAT Detail)',
'CuotaRepercutida' => 'CuotaRepercutida (Recharged Tax Amount)',
'FechaExpedicionFacturaEmisor' => 'FechaExpedicionFacturaEmisor (Emitter Invoice Issue Date)',
];
// Apply element translations
foreach ($elementTranslations as $element => $translation) {
$message = str_replace($element, $translation, $message);
}
// Remove extra whitespace and clean up the message
$message = preg_replace('/\s+/', ' ', $message);
$message = trim($message);
return $message;
}
/**
* Detect the type of Verifactu document
*/
private function detectDocumentType(\DOMDocument $doc): string
{
$xpath = new \DOMXPath($doc);
$xpath->registerNamespace('si', 'https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd');
$xpath->registerNamespace('sum1', 'https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd');
// Check for modification structure - look for RegistroAlta with TipoFactura R1
$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 && in_array($tipoFactura->item(0)->textContent,['R1','F3'])) {
return 'modification';
}
}
// Check for cancellation structure
$registroAnulacion = $xpath->query('//si:RegistroAnulacion | //sum1:RegistroAnulacion');
if ($registroAnulacion->length > 0) {
return 'cancellation';
}
// Check for registration structure (RegistroAlta with TipoFactura not R1)
if ($registroAlta->length > 0) {
$tipoFactura = $xpath->query('.//si:TipoFactura | .//sum1:TipoFactura', $registroAlta->item(0));
if ($tipoFactura->length === 0 || $tipoFactura->item(0)->textContent !== 'R1') {
return 'registration';
}
}
return 'unknown';
}
/**
* Validate modification documents using business rules instead of strict XSD
*/
private function validateModificationDocument(\DOMDocument $doc): void
{
$xpath = new \DOMXPath($doc);
$xpath->registerNamespace('si', 'https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd');
$xpath->registerNamespace('sum1', 'https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd');
$xpath->registerNamespace('lr', 'https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroLR.xsd');
// Validate modification-specific structure
$this->validateModificationStructure($xpath);
// Validate required elements for modifications
$this->validateModificationRequiredElements($xpath);
// Validate business rules for modifications
$this->validateModificationBusinessRules($xpath);
}
/**
* Validate modification structure
*/
private function validateModificationStructure(\DOMXPath $xpath): void
{
// Check for RegistroAlta with TipoFactura R1
$registroAlta = $xpath->query('//si:RegistroAlta');
if ($registroAlta === false || $registroAlta->length === 0) {
// Try alternative namespace
$registroAlta = $xpath->query('//sum1:RegistroAlta');
if ($registroAlta === false || $registroAlta->length === 0) {
$this->errors['structure'][] = "RegistroAlta element not found for modification";
return;
}
}
// Check for required modification elements within the RegistroAlta
$requiredElements = [
'.//si:TipoFactura' => 'TipoFactura',
'.//si:DescripcionOperacion' => 'DescripcionOperacion',
'.//si:ImporteTotal' => 'ImporteTotal'
];
foreach ($requiredElements as $xpathQuery => $elementName) {
$elements = $xpath->query($xpathQuery, $registroAlta->item(0));
if ($elements === false || $elements->length === 0) {
// Try alternative namespace
$altQuery = str_replace('si:', 'sum1:', $xpathQuery);
$elements = $xpath->query($altQuery, $registroAlta->item(0));
if ($elements === false || $elements->length === 0) {
$this->errors['structure'][] = "Required modification element not found: $elementName";
}
}
}
// Validate TipoFactura is R1 for modifications
$tipoFactura = $xpath->query('.//si:TipoFactura', $registroAlta->item(0));
if ($tipoFactura === false || $tipoFactura->length === 0) {
$tipoFactura = $xpath->query('.//sum1:TipoFactura', $registroAlta->item(0));
}
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;
}
}
/**
* Validate required elements for modifications
*/
private function validateModificationRequiredElements(\DOMXPath $xpath): void
{
// Check for required elements in FacturasRectificadas - look for both si: and sf: namespaces
$facturasRectificadas = $xpath->query('//si:FacturasRectificadas | //sf:FacturasRectificadas');
if ($facturasRectificadas !== false && $facturasRectificadas->length > 0) {
$idFacturasRectificadas = $xpath->query('//si:FacturasRectificadas/si:IDFacturaRectificada | //sf:FacturasRectificadas/sf:IDFacturaRectificada');
if ($idFacturasRectificadas === false || $idFacturasRectificadas->length === 0) {
$this->errors['structure'][] = "At least one IDFacturaRectificada is required in FacturasRectificadas";
} else {
// Validate each IDFacturaRectificada has required elements
foreach ($idFacturasRectificadas as $index => $idFacturaRectificada) {
$idEmisorFactura = $xpath->query('.//si:IDEmisorFactura | .//sf:IDEmisorFactura', $idFacturaRectificada);
$numSerieFactura = $xpath->query('.//si:NumSerieFactura | .//sf:NumSerieFactura', $idFacturaRectificada);
$fechaExpedicionFactura = $xpath->query('.//si:FechaExpedicionFactura | .//sf:FechaExpedicionFactura', $idFacturaRectificada);
if ($idEmisorFactura === false || $idEmisorFactura->length === 0) {
$this->errors['structure'][] = "IDEmisorFactura is required in IDFacturaRectificada " . ($index + 1);
}
if ($numSerieFactura === false || $numSerieFactura->length === 0) {
$this->errors['structure'][] = "NumSerieFactura is required in IDFacturaRectificada " . ($index + 1);
}
if ($fechaExpedicionFactura === false || $fechaExpedicionFactura->length === 0) {
$this->errors['structure'][] = "FechaExpedicionFactura is required in IDFacturaRectificada " . ($index + 1);
}
}
}
}
// Check for tax information - look for both si: and sf: namespaces
$impuestos = $xpath->query('//si:Impuestos | //sf:Impuestos');
if ($impuestos !== false && $impuestos->length > 0) {
$detalleIVA = $xpath->query('//si:Impuestos/si:DetalleIVA | //sf:Impuestos/sf:DetalleIVA');
if ($detalleIVA === false || $detalleIVA->length === 0) {
$this->errors['structure'][] = "DetalleIVA is required when Impuestos is present";
}
}
}
/**
* Validate business rules for modifications
*/
private function validateModificationBusinessRules(\DOMXPath $xpath): void
{
// Validate ImporteTotal is numeric and positive
$importeTotal = $xpath->query('//si:ImporteTotal');
if ($importeTotal->length > 0) {
$value = $importeTotal->item(0)->textContent;
if (!is_numeric($value) || floatval($value) <= 0) {
$this->errors['business'][] = "ImporteTotal must be a positive number, found: $value";
}
}
// Validate tax amounts are consistent
$cuotaRepercutida = $xpath->query('//si:CuotaRepercutida');
if ($cuotaRepercutida->length > 0) {
$value = $cuotaRepercutida->item(0)->textContent;
if (!is_numeric($value)) {
$this->errors['business'][] = "CuotaRepercutida must be numeric, found: $value";
}
}
// Validate date formats
$fechaExpedicion = $xpath->query('//si:FechaExpedicionFacturaEmisor');
if ($fechaExpedicion->length > 0) {
$value = $fechaExpedicion->item(0)->textContent;
if (!preg_match('/^\d{2}-\d{2}-\d{4}$/', $value)) {
$this->errors['business'][] = "FechaExpedicionFacturaEmisor must be in DD-MM-YYYY format, found: $value";
}
}
}
/**
* Validate against Verifactu-specific schema rules
*/
private function validateVerifactuSchema(): self
{
try {
// Add any Verifactu-specific validation logic here
// This could include business rule validation, format checks, etc.
// For now, we'll just do basic structure validation
$this->validateVerifactuStructure();
} catch (\Throwable $th) {
$this->errors['general'][] = $th->getMessage();
}
return $this;
}
/**
* Extract business content from SOAP envelope
*/
private function extractBusinessContent(\DOMDocument $doc): \DOMDocument
{
$xpath = new \DOMXPath($doc);
$xpath->registerNamespace('lr', 'https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroLR.xsd');
$regFactuElements = $xpath->query('//lr:RegFactuSistemaFacturacion');
if ($regFactuElements->length > 0) {
$businessContent = $regFactuElements->item(0);
$businessDoc = new \DOMDocument();
$businessDoc->appendChild($businessDoc->importNode($businessContent, true));
return $businessDoc;
}
// If no business content found, return the original document
return $doc;
}
/**
* Validate Verifactu-specific structure requirements
*/
private function validateVerifactuStructure(): void
{
$doc = new \DOMDocument();
$doc->loadXML($this->xml_document);
$xpath = new \DOMXPath($doc);
$xpath->registerNamespace('si', 'https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd');
// Check for required elements
$requiredElements = [
'//si:TipoFactura',
'//si:DescripcionOperacion',
'//si:ImporteTotal'
];
foreach ($requiredElements as $element) {
$nodes = $xpath->query($element);
if ($nodes->length === 0) {
$this->errors['structure'][] = "Required element not found: $element";
}
}
// Check for modification-specific elements
$modificationElements = $xpath->query('//si:ModificacionFactura');
if ($modificationElements->length > 0) {
// Validate modification structure
$tipoRectificativa = $xpath->query('//si:TipoRectificativa');
if ($tipoRectificativa->length === 0) {
$this->errors['structure'][] = "TipoRectificativa is required for modifications";
}
$facturasRectificadas = $xpath->query('//si:FacturasRectificadas');
if ($facturasRectificadas->length === 0) {
$this->errors['structure'][] = "FacturasRectificadas is required for modifications";
}
}
}
/**
* Get Verifactu-specific errors
*/
public function getVerifactuErrors(): array
{
return $this->getErrors();
}
/**
* Get detailed error information with suggestions for fixing common issues
*
* @return array Detailed error information with context and suggestions
*/
public function getDetailedErrors(): array
{
$detailedErrors = [];
foreach ($this->errors as $errorType => $errors) {
foreach ($errors as $error) {
$detailedErrors[] = [
'type' => $errorType,
'message' => $error,
'context' => $this->getErrorContext($error),
'suggestion' => $this->getErrorSuggestion($error),
'severity' => $this->getErrorSeverity($errorType)
];
}
}
return $detailedErrors;
}
/**
* Get context information for an error
*
* @param string $error The error message
* @return string Context information
*/
private function getErrorContext(string $error): string
{
if (strpos($error, 'Desglose') !== false) {
return 'The Desglose (Tax Breakdown) element requires a DetalleDesglose (Tax Detail) child element to specify the tax breakdown structure.';
}
if (strpos($error, 'TipoFactura') !== false) {
return 'The TipoFactura (Invoice Type) element specifies the type of invoice being processed (e.g., F1 for regular invoice, R1 for modification).';
}
if (strpos($error, 'DescripcionOperacion') !== false) {
return 'The DescripcionOperacion (Operation Description) element provides a description of the business operation being documented.';
}
if (strpos($error, 'ImporteTotal') !== false) {
return 'The ImporteTotal (Total Amount) element contains the total amount of the invoice including all taxes.';
}
if (strpos($error, 'FacturasRectificadas') !== false) {
return 'The FacturasRectificadas (Corrected Invoices) element is required for modification invoices to reference the original invoices being corrected.';
}
return 'This error indicates a structural issue with the XML document that prevents it from conforming to the Verifactu schema requirements.';
}
/**
* Get suggestions for fixing an error
*
* @param string $error The error message
* @return string Suggestion for fixing the error
*/
private function getErrorSuggestion(string $error): string
{
if (strpos($error, 'Missing child element') !== false && strpos($error, 'DetalleDesglose') !== false) {
return 'Add a DetalleDesglose element within the Desglose element to specify the tax breakdown details. Example: <DetalleDesglose><TipoImpositivo>21</TipoImpositivo><BaseImponible>100.00</BaseImponible><CuotaRepercutida>21.00</CuotaRepercutida></DetalleDesglose>';
}
if (strpos($error, 'TipoFactura') !== false) {
return 'Ensure the TipoFactura element contains a valid value: F1 (regular invoice), F2 (simplified invoice), F3 (modification), or R1 (modification).';
}
if (strpos($error, 'DescripcionOperacion') !== false) {
return 'Add a DescripcionOperacion element with a clear description of the business operation, such as "Venta de mercancías" or "Prestación de servicios".';
}
if (strpos($error, 'ImporteTotal') !== false) {
return 'Ensure the ImporteTotal element contains a valid numeric value representing the total invoice amount including taxes.';
}
if (strpos($error, 'FacturasRectificadas') !== false) {
return 'For modification invoices, add the FacturasRectificadas element with at least one IDFacturaRectificada containing the original invoice details.';
}
return 'Review the XML structure against the Verifactu schema requirements and ensure all required elements are present with valid content.';
}
/**
* Get error severity level
*
* @param string $errorType The type of error
* @return string Severity level
*/
private function getErrorSeverity(string $errorType): string
{
return match($errorType) {
'xsd' => 'high',
'structure' => 'medium',
'business' => 'low',
'general' => 'medium',
default => 'medium'
};
}
/**
* Get a user-friendly summary of validation errors
*
* @return string Summary of validation errors
*/
public function getErrorSummary(): string
{
if (empty($this->errors)) {
return 'Document validation passed successfully.';
}
$summary = [];
$totalErrors = 0;
foreach ($this->errors as $errorType => $errors) {
$count = count($errors);
$totalErrors += $count;
$typeLabel = match($errorType) {
'xsd' => 'Schema Validation Errors',
'structure' => 'Structural Errors',
'business' => 'Business Rule Violations',
'general' => 'General Errors',
default => ucfirst($errorType) . ' Errors'
};
$summary[] = "$typeLabel: $count";
}
$summaryText = "Validation failed with $totalErrors total error(s):\n";
$summaryText .= implode(', ', $summary);
return $summaryText;
}
/**
* Get errors formatted for display in logs or user interfaces
*
* @return array Formatted errors grouped by type
*/
public function getFormattedErrors(): array
{
$formatted = [];
foreach ($this->errors as $errorType => $errors) {
$formatted[$errorType] = [
'count' => count($errors),
'messages' => $errors,
'severity' => $this->getErrorSeverity($errorType)
];
}
return $formatted;
}
}

View File

@ -26,7 +26,7 @@ class XsltDocumentValidator
// private string $peppol_stylesheetx = 'Services/EDocument/Standards/Validation/Peppol/Stylesheets/ubl_stylesheet.xslt';
// private string $peppol_stylesheet = 'Services/EDocument/Standards/Validation/Peppol/Stylesheets/ci_to_ubl_stylesheet.xslt';
private array $errors = [];
public array $errors = [];
public function __construct(public string $xml_document)
{

View File

@ -0,0 +1,202 @@
<?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;
use App\Models\Company;
use App\Models\Invoice;
use App\Models\Product;
use App\Models\VerifactuLog;
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\AeatClient;
use App\Services\EDocument\Standards\Verifactu\RegistroAlta;
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;
class Verifactu extends AbstractService
{
private AeatClient $aeat_client;
private string $soapXml;
//store the current document state
private VerifactuInvoice $_document;
//store the current huella
private string $_huella;
private string $_previous_huella;
public function __construct(public Invoice $invoice)
{
$this->aeat_client = new AeatClient();
}
/**
* Entry point for building document
*
* @return self
*/
public function run(): self
{
$v_logs = $this->invoice->company->verifactu_logs;
$i_logs = $this->invoice->verifactu_logs;
$document = (new RegistroAlta($this->invoice))->run();
if($this->invoice->amount < 0) {
$document = $document->setRectification();
}
$document = $document->getInvoice();
//keep this state for logging later on successful send
$this->_document = $document;
$this->_previous_huella = '';
if($v_logs->count() >= 1){
$v_log = $v_logs->first();
$this->_previous_huella = $v_log->hash;
}
$this->_huella = $this->calculateHash($document, $this->_previous_huella); // careful with this! we'll need to reference this later
$document->setHuella($this->_huella);
$this->setEnvelope($document->toSoapEnvelope());
return $this;
}
/**
* setHuella
* We need this for cancellation documents.
*
* @param string $huella
* @return self
*/
public function setHuella(string $huella): self
{
$this->_huella = $huella;
return $this;
}
public function getInvoice()
{
return $this->_document;
}
public function setInvoice(VerifactuInvoice $invoice): self
{
$this->_document = $invoice;
return $this;
}
public function getEnvelope(): string
{
return $this->soapXml;
}
public function setTestMode(): self
{
$this->aeat_client->setTestMode();
return $this;
}
/**
* setPreviousHash
*
* **only used for testing**
* @param string $previous_hash
* @return self
*/
public function setPreviousHash(string $previous_hash): self
{
$this->_previous_huella = $previous_hash;
return $this;
}
private function setEnvelope(string $soapXml): self
{
$this->soapXml = $soapXml;
return $this;
}
public function writeLog(array $response)
{
VerifactuLog::create([
'invoice_id' => $this->invoice->id,
'company_id' => $this->invoice->company_id,
'invoice_number' => $this->invoice->number,
'date' => $this->invoice->date,
'hash' => $this->_huella,
'nif' => $this->_document->getIdFactura()->getIdEmisorFactura(),
'previous_hash' => $this->_previous_huella,
'state' => $this->_document->serialize(),
'response' => $response,
'status' => $response['guid'],
]);
}
/**
* calculateHash
*
* @param mixed $document
* @param string $huella
* @return string
*/
public function calculateHash($document, string $huella): string
{
$idEmisorFactura = $document->getIdFactura()->getIdEmisorFactura();
$numSerieFactura = $document->getIdFactura()->getNumSerieFactura();
$fechaExpedicionFactura = $document->getIdFactura()->getFechaExpedicionFactura();
$tipoFactura = $document->getTipoFactura();
$cuotaTotal = $document->getCuotaTotal();
$importeTotal = $document->getImporteTotal();
$fechaHoraHusoGenRegistro = $document->getFechaHoraHusoGenRegistro();
$hashInput = "IDEmisorFactura={$idEmisorFactura}&" .
"NumSerieFactura={$numSerieFactura}&" .
"FechaExpedicionFactura={$fechaExpedicionFactura}&" .
"TipoFactura={$tipoFactura}&" .
"CuotaTotal={$cuotaTotal}&" .
"ImporteTotal={$importeTotal}&" .
"Huella={$huella}&" .
"FechaHoraHusoGenRegistro={$fechaHoraHusoGenRegistro}";
return strtoupper(hash('sha256', $hashInput));
}
public function send(string $soapXml): array
{
nlog(["sending", $soapXml]);
$response = $this->aeat_client->send($soapXml);
if($response['success'] || $response['status'] == 'ParcialmenteCorrecto'){
$this->writeLog($response);
}
return $response;
}
}

View File

@ -0,0 +1,100 @@
<?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\Services\EDocument\Standards\Verifactu\ResponseProcessor;
use Illuminate\Support\Facades\Http;
class AeatAuthority
{
// @todo - in the UI, the user must navigate to AEAT link, and add Invoice Ninja as a third party. We cannot send without this.
// @todo - need to store the verification of this in the company
// https://sede.agenciatributaria.gob.es/Sede/ayuda/consultas-informaticas/otros-servicios-ayuda-tecnica/consultar-confirmar-renunciar-apoderamiento-recibido.html
// @todo - register with AEAT as a third party - power of attorney
// Log in with their certificate, DNIe, or Cl@ve PIN.
// Select: "Otorgar poder a un tercero"
// Enter:
// Your SaaS company's NIF as the authorized party
// Power code: LGTINVDI (or GENERALDATPE)
// Confirm
// https://sede.agenciatributaria.gob.es/wlpl/BDC/conapoderWS
private string $base_url = 'https://sede.agenciatributaria.gob.es/wlpl/BDC/conapoderWS';
private string $sandbox_url = 'https://prewww1.aeat.es/wlpl/BDC/conapoderWS';
public function __construct()
{
}
public function setTestMode(): self
{
$this->base_url = $this->sandbox_url;
return $this;
}
public function run(string $client_nif): array
{
$sender_nif = config('services.verifactu.sender_nif');
$certificate = config('services.verifactu.certificate');
$ssl_key = config('services.verifactu.ssl_key');
$xml = <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:apod="http://www2.agenciatributaria.gob.es/apoderamiento/ws/apoderamientos">
<soapenv:Header/>
<soapenv:Body>
<apod:ConsultaApoderamiento>
<apod:identificadorApoderado>
<apod:nifRepresentante>{$sender_nif}</apod:nifRepresentante>
</apod:identificadorApoderado>
<apod:identificadorPoderdante>
<apod:nifPoderdante>{$client_nif}</apod:nifPoderdante>
</apod:identificadorPoderdante>
<apod:codigoPoder>LGTINVDI</apod:codigoPoder>
</apod:ConsultaApoderamiento>
</soapenv:Body>
</soapenv:Envelope>
XML;
$signingService = new \App\Services\EDocument\Standards\Verifactu\Signing\SigningService($xml, file_get_contents($ssl_key), file_get_contents($certificate));
$soapXml = $signingService->sign();
$response = Http::withHeaders([
'Content-Type' => 'text/xml; charset=utf-8',
'SOAPAction' => '',
])
->withOptions([
'cert' => $certificate,
'ssl_key' => $ssl_key,
'verify' => false,
'timeout' => 30,
])
->withBody($soapXml, 'text/xml')
->post($this->base_url);
$success = $response->successful();
$responseProcessor = new ResponseProcessor();
$parsedResponse = $responseProcessor->processResponse($response->body());
nlog($response->body());
nlog($parsedResponse);
return $parsedResponse;
}
}

View File

@ -0,0 +1,84 @@
<?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 Illuminate\Support\Facades\Http;
use App\Services\EDocument\Standards\Verifactu\ResponseProcessor;
class AeatClient
{
private string $base_url = 'https://www1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP';
private string $sandbox_url = 'https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP';
public function __construct(private ?string $certificate = null, private ?string $ssl_key = null)
{
$this->init();
}
/**
* initialize the certificates
*
* @return self
*/
private function init(): self
{
$this->certificate = $this->certificate ?? config('services.verifactu.certificate');
$this->ssl_key = $this->ssl_key ?? config('services.verifactu.ssl_key');
if(config('services.verifactu.test_mode')) {
$this->setTestMode();
}
return $this;
}
/**
* setTestMode
*
* @return self
*/
public function setTestMode(?string $base_url = null): self
{
$this->base_url = $base_url ?? $this->sandbox_url;
return $this;
}
public function send($xml): array
{
$response = Http::withHeaders([
'Content-Type' => 'text/xml; charset=utf-8',
'SOAPAction' => '',
])
->withOptions([
'cert' => $this->certificate,
'ssl_key' => $this->ssl_key,
'verify' => false,
'timeout' => 30,
])
->withBody($xml, 'text/xml')
->post($this->base_url);
$success = $response->successful();
$responseProcessor = new ResponseProcessor();
$parsedResponse = $responseProcessor->processResponse($response->body());
nlog($parsedResponse);
return $parsedResponse;
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Services\EDocument\Standards\Verifactu\Models;
class BaseTypes
{
// Common types
public const VERSION_TYPE = 'string';
public const NIF_TYPE = 'string';
public const FECHA_TYPE = 'string'; // ISO 8601 date
public const TEXT_MAX_18_TYPE = 'string';
public const TEXT_MAX_20_TYPE = 'string';
public const TEXT_MAX_30_TYPE = 'string';
public const TEXT_MAX_50_TYPE = 'string';
public const TEXT_MAX_60_TYPE = 'string';
public const TEXT_MAX_64_TYPE = 'string';
public const TEXT_MAX_70_TYPE = 'string';
public const TEXT_MAX_100_TYPE = 'string';
public const TEXT_MAX_120_TYPE = 'string';
public const TEXT_MAX_500_TYPE = 'string';
public const IMPORTE_SGN_12_2_TYPE = 'float';
public const TIPO_HUELLA_TYPE = 'string';
public const TIPO_PERIODO_TYPE = 'string';
public const CLAVE_TIPO_FACTURA_TYPE = 'string';
public const CLAVE_TIPO_RECTIFICATIVA_TYPE = 'string';
public const SIMPLIFICADA_CUALIFICADA_TYPE = 'string';
public const COMPLETA_SIN_DESTINATARIO_TYPE = 'string';
public const MACRODATO_TYPE = 'string';
public const TERCEROS_O_DESTINATARIO_TYPE = 'string';
public const GENERADO_POR_TYPE = 'string';
public const SIN_REGISTRO_PREVIO_TYPE = 'string';
public const SUBSANACION_TYPE = 'string';
public const RECHAZO_PREVIO_TYPE = 'string';
public const FIN_REQUERIMIENTO_TYPE = 'string';
public const INCIDENCIA_TYPE = 'string';
}

View File

@ -0,0 +1,69 @@
<?php
namespace App\Services\EDocument\Standards\Verifactu\Models;
abstract class BaseXmlModel
{
public const XML_NAMESPACE = 'https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd';
protected const XML_NAMESPACE_PREFIX = 'sum1';
protected const XML_DS_NAMESPACE = 'http://www.w3.org/2000/09/xmldsig#';
protected const XML_DS_NAMESPACE_PREFIX = 'ds';
protected function createElement(\DOMDocument $doc, string $name, ?string $value = null, array $attributes = []): \DOMElement
{
$element = $doc->createElementNS(self::XML_NAMESPACE, self::XML_NAMESPACE_PREFIX . ':' . $name);
if ($value !== null) {
$textNode = $doc->createTextNode($value);
$element->appendChild($textNode);
}
foreach ($attributes as $attrName => $attrValue) {
$element->setAttribute($attrName, $attrValue);
}
return $element;
}
protected function createDsElement(\DOMDocument $doc, string $name, ?string $value = null, array $attributes = []): \DOMElement
{
$element = $doc->createElementNS(self::XML_DS_NAMESPACE, self::XML_DS_NAMESPACE_PREFIX . ':' . $name);
if ($value !== null) {
$textNode = $doc->createTextNode($value);
$element->appendChild($textNode);
}
foreach ($attributes as $attrName => $attrValue) {
$element->setAttribute($attrName, $attrValue);
}
return $element;
}
protected function getElementValue(\DOMElement $parent, string $name, string $namespace = self::XML_NAMESPACE): ?string
{
$elements = $parent->getElementsByTagNameNS($namespace, $name);
if ($elements->length > 0) {
return $elements->item(0)->textContent;
}
return null;
}
abstract public function toXml(\DOMDocument $doc): \DOMElement;
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');
}
$doc = new \DOMDocument();
$doc->formatOutput = true;
$doc->preserveWhiteSpace = false;
if (!$doc->loadXML($xml)) {
throw new \DOMException('Failed to load XML: Invalid XML format');
}
return static::fromDOMElement($doc->documentElement);
}
abstract public static function fromDOMElement(\DOMElement $element): self;
}

View File

@ -0,0 +1,87 @@
<?php
namespace App\Services\EDocument\Standards\Verifactu\Models;
class Cupon extends BaseXmlModel
{
protected string $idCupon;
protected string $fechaExpedicionCupon;
protected float $importeCupon;
protected ?string $descripcionCupon = null;
public function toXml(\DOMDocument $doc): \DOMElement
{
$root = $this->createElement($doc, 'Cupon');
// Add required elements
$root->appendChild($this->createElement($doc, 'IDCupon', $this->idCupon));
$root->appendChild($this->createElement($doc, 'FechaExpedicionCupon', $this->fechaExpedicionCupon));
$root->appendChild($this->createElement($doc, 'ImporteCupon', (string)$this->importeCupon));
// Add optional description
if ($this->descripcionCupon !== null) {
$root->appendChild($this->createElement($doc, 'DescripcionCupon', $this->descripcionCupon));
}
return $root;
}
public static function fromDOMElement(\DOMElement $element): self
{
$cupon = new self();
$cupon->setIdCupon($cupon->getElementValue($element, 'IDCupon'));
$cupon->setFechaExpedicionCupon($cupon->getElementValue($element, 'FechaExpedicionCupon'));
$cupon->setImporteCupon((float)$cupon->getElementValue($element, 'ImporteCupon'));
$descripcionCupon = $cupon->getElementValue($element, 'DescripcionCupon');
if ($descripcionCupon !== null) {
$cupon->setDescripcionCupon($descripcionCupon);
}
return $cupon;
}
public function getIdCupon(): string
{
return $this->idCupon;
}
public function setIdCupon(string $idCupon): self
{
$this->idCupon = $idCupon;
return $this;
}
public function getFechaExpedicionCupon(): string
{
return $this->fechaExpedicionCupon;
}
public function setFechaExpedicionCupon(string $fechaExpedicionCupon): self
{
$this->fechaExpedicionCupon = $fechaExpedicionCupon;
return $this;
}
public function getImporteCupon(): float
{
return $this->importeCupon;
}
public function setImporteCupon(float $importeCupon): self
{
$this->importeCupon = $importeCupon;
return $this;
}
public function getDescripcionCupon(): ?string
{
return $this->descripcionCupon;
}
public function setDescripcionCupon(?string $descripcionCupon): self
{
$this->descripcionCupon = $descripcionCupon;
return $this;
}
}

View File

@ -0,0 +1,352 @@
<?php
namespace App\Services\EDocument\Standards\Verifactu\Models;
class Desglose extends BaseXmlModel
{
protected ?array $desgloseFactura = null;
protected ?array $desgloseTipoOperacion = null;
protected ?array $desgloseIVA = null;
protected ?array $desgloseIGIC = null;
protected ?array $desgloseIRPF = null;
protected ?array $desgloseIS = null;
protected ?DetalleDesglose $detalleDesglose = null;
public function toXml(\DOMDocument $doc): \DOMElement
{
$root = $this->createElement($doc, 'Desglose');
// If we have DetalleDesglose objects in the desgloseIVA array, use them
if ($this->desgloseIVA !== null && is_array($this->desgloseIVA) && count($this->desgloseIVA) > 0) {
foreach ($this->desgloseIVA as $detalleDesglose) {
if ($detalleDesglose instanceof DetalleDesglose) {
$root->appendChild($detalleDesglose->toXml($doc));
}
}
return $root;
}
// If we have a single DetalleDesglose object, use it
if ($this->detalleDesglose !== null) {
$root->appendChild($this->detalleDesglose->toXml($doc));
return $root;
}
// Always create a DetalleDesglose element if we have any data
$detalleDesglose = $this->createElement($doc, 'DetalleDesglose');
// Handle regular invoice desglose
if ($this->desgloseFactura !== null) {
// Add Impuesto if present
if (isset($this->desgloseFactura['Impuesto'])) {
$detalleDesglose->appendChild($this->createElement($doc, 'Impuesto', $this->desgloseFactura['Impuesto']));
} else {
// Default Impuesto for IVA
$detalleDesglose->appendChild($this->createElement($doc, 'Impuesto', '01'));
}
// Add ClaveRegimen if present
if (isset($this->desgloseFactura['ClaveRegimen']) ) {
$detalleDesglose->appendChild($this->createElement($doc, 'ClaveRegimen', $this->desgloseFactura['ClaveRegimen']));
} else {
// Default ClaveRegimen
$detalleDesglose->appendChild($this->createElement($doc, 'ClaveRegimen', '01'));
}
// Add CalificacionOperacion
$detalleDesglose->appendChild($this->createElement($doc, 'CalificacionOperacion',
$this->desgloseFactura['CalificacionOperacion'] ?? 'S1'));
// Add TipoImpositivo if present
if (isset($this->desgloseFactura['TipoImpositivo']) && $this->desgloseFactura['CalificacionOperacion'] == 'S1') {
$detalleDesglose->appendChild($this->createElement($doc, 'TipoImpositivo',
number_format((float)$this->desgloseFactura['TipoImpositivo'], 2, '.', '')));
}
// else {
// // Default TipoImpositivo
// $detalleDesglose->appendChild($this->createElement($doc, 'TipoImpositivo', '0'));
// }
// Convert BaseImponible to BaseImponibleOimporteNoSujeto if needed
$baseImponible = isset($this->desgloseFactura['BaseImponible'])
? $this->desgloseFactura['BaseImponible']
: ($this->desgloseFactura['BaseImponibleOimporteNoSujeto'] ?? '0');
$detalleDesglose->appendChild($this->createElement($doc, 'BaseImponibleOimporteNoSujeto',
number_format((float)$baseImponible, 2, '.', '')));
if(isset($this->desgloseFactura['Cuota']) && $this->desgloseFactura['CalificacionOperacion'] == 'S1'){
$detalleDesglose->appendChild($this->createElement($doc, 'CuotaRepercutida',
number_format((float)$this->desgloseFactura['Cuota'], 2, '.', '')));
}
// Add TipoRecargoEquivalencia if present
if (isset($this->desgloseFactura['TipoRecargoEquivalencia'])) {
$detalleDesglose->appendChild($this->createElement($doc, 'TipoRecargoEquivalencia',
number_format((float)$this->desgloseFactura['TipoRecargoEquivalencia'], 2, '.', '')));
}
// Add CuotaRecargoEquivalencia if present
if (isset($this->desgloseFactura['CuotaRecargoEquivalencia'])) {
$detalleDesglose->appendChild($this->createElement($doc, 'CuotaRecargoEquivalencia',
number_format((float)$this->desgloseFactura['CuotaRecargoEquivalencia'], 2, '.', '')));
}
}
// Handle simplified invoice desglose (IVA)
if ($this->desgloseIVA !== null) {
// If desgloseIVA is an array of arrays, handle multiple tax rates
if (is_array(reset($this->desgloseIVA))) {
foreach ($this->desgloseIVA as $desglose) {
$detalleDesglose = $this->createElement($doc, 'DetalleDesglose');
// Add Impuesto (required for IVA)
$detalleDesglose->appendChild($this->createElement($doc, 'Impuesto', $desglose['Impuesto'] ?? '01'));
// Add ClaveRegimen
$detalleDesglose->appendChild($this->createElement($doc, 'ClaveRegimen', $desglose['ClaveRegimen'] ?? '01'));
// Add CalificacionOperacion
$detalleDesglose->appendChild($this->createElement($doc, 'CalificacionOperacion', $desglose['CalificacionOperacion'] ?? 'S1'));
// Add TipoImpositivo if present
if (isset($desglose['TipoImpositivo'])) {
$detalleDesglose->appendChild($this->createElement($doc, 'TipoImpositivo',
number_format((float)$desglose['TipoImpositivo'], 2, '.', '')));
}
// Convert BaseImponible to BaseImponibleOimporteNoSujeto if needed
$baseImponible = isset($desglose['BaseImponible'])
? $desglose['BaseImponible']
: ($desglose['BaseImponibleOimporteNoSujeto'] ?? '0');
$detalleDesglose->appendChild($this->createElement($doc, 'BaseImponibleOimporteNoSujeto',
number_format((float)$baseImponible, 2, '.', '')));
// Convert Cuota to CuotaRepercutida if needed
$cuota = isset($desglose['Cuota'])
? $desglose['Cuota']
: ($desglose['CuotaRepercutida'] ?? '0');
$detalleDesglose->appendChild($this->createElement($doc, 'CuotaRepercutida',
number_format((float)$cuota, 2, '.', '')));
$root->appendChild($detalleDesglose);
}
} else {
// Single tax rate
$detalleDesglose = $this->createElement($doc, 'DetalleDesglose');
// Add Impuesto (required for IVA)
$detalleDesglose->appendChild($this->createElement($doc, 'Impuesto', $this->desgloseIVA['Impuesto'] ?? '01'));
// Add ClaveRegimen
$detalleDesglose->appendChild($this->createElement($doc, 'ClaveRegimen', $this->desgloseIVA['ClaveRegimen'] ?? '01'));
// Add CalificacionOperacion
$detalleDesglose->appendChild($this->createElement($doc, 'CalificacionOperacion', $this->desgloseIVA['CalificacionOperacion'] ?? 'S1'));
// Add TipoImpositivo if present
if (isset($this->desgloseIVA['TipoImpositivo'])) {
$detalleDesglose->appendChild($this->createElement($doc, 'TipoImpositivo',
number_format((float)$this->desgloseIVA['TipoImpositivo'], 2, '.', '')));
}
// Convert BaseImponible to BaseImponibleOimporteNoSujeto if needed
$baseImponible = isset($this->desgloseIVA['BaseImponible'])
? $this->desgloseIVA['BaseImponible']
: ($this->desgloseIVA['BaseImponibleOimporteNoSujeto'] ?? '');
$detalleDesglose->appendChild($this->createElement($doc, 'BaseImponibleOimporteNoSujeto',
number_format((float)$baseImponible, 2, '.', '')));
// Convert Cuota to CuotaRepercutida if needed
$cuota = isset($this->desgloseIVA['Cuota'])
? $this->desgloseIVA['Cuota']
: ($this->desgloseIVA['CuotaRepercutida'] ?? '0');
$detalleDesglose->appendChild($this->createElement($doc, 'CuotaRepercutida',
number_format((float)$cuota, 2, '.', '')));
$root->appendChild($detalleDesglose);
}
}
// // If we still don't have any data, create a default DetalleDesglose
// if (!$detalleDesglose->hasChildNodes()) {
// // Create a default DetalleDesglose with basic IVA information
// $detalleDesglose->appendChild($this->createElement($doc, 'Impuesto', '01'));
// $detalleDesglose->appendChild($this->createElement($doc, 'ClaveRegimen', '01'));
// $detalleDesglose->appendChild($this->createElement($doc, 'CalificacionOperacion', 'S1'));
// $detalleDesglose->appendChild($this->createElement($doc, 'TipoImpositivo', '0'));
// $detalleDesglose->appendChild($this->createElement($doc, 'BaseImponibleOimporteNoSujeto', '0'));
// $detalleDesglose->appendChild($this->createElement($doc, 'CuotaRepercutida', '0'));
// }
$root->appendChild($detalleDesglose);
return $root;
}
public static function fromDOMElement(\DOMElement $element): self
{
$desglose = new self();
// Parse DesgloseFactura
$desgloseFacturaElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'DesgloseFactura')->item(0);
if ($desgloseFacturaElement) {
$desgloseFactura = [];
foreach ($desgloseFacturaElement->childNodes as $child) {
if ($child instanceof \DOMElement) {
$desgloseFactura[$child->localName] = $child->nodeValue;
}
}
$desglose->setDesgloseFactura($desgloseFactura);
}
// Parse DesgloseTipoOperacion
$desgloseTipoOperacionElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'DesgloseTipoOperacion')->item(0);
if ($desgloseTipoOperacionElement) {
$desgloseTipoOperacion = [];
foreach ($desgloseTipoOperacionElement->childNodes as $child) {
if ($child instanceof \DOMElement) {
$desgloseTipoOperacion[$child->localName] = $child->nodeValue;
}
}
$desglose->setDesgloseTipoOperacion($desgloseTipoOperacion);
}
// Parse DesgloseIVA
$desgloseIvaElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'DesgloseIVA')->item(0);
if ($desgloseIvaElement) {
$desgloseIva = [];
foreach ($desgloseIvaElement->childNodes as $child) {
if ($child instanceof \DOMElement) {
$desgloseIva[$child->localName] = $child->nodeValue;
}
}
$desglose->setDesgloseIVA($desgloseIva);
}
// Parse DesgloseIGIC
$desgloseIgicElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'DesgloseIGIC')->item(0);
if ($desgloseIgicElement) {
$desgloseIgic = [];
foreach ($desgloseIgicElement->childNodes as $child) {
if ($child instanceof \DOMElement) {
$desgloseIgic[$child->localName] = $child->nodeValue;
}
}
$desglose->setDesgloseIGIC($desgloseIgic);
}
// Parse DesgloseIRPF
$desgloseIrpfElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'DesgloseIRPF')->item(0);
if ($desgloseIrpfElement) {
$desgloseIrpf = [];
foreach ($desgloseIrpfElement->childNodes as $child) {
if ($child instanceof \DOMElement) {
$desgloseIrpf[$child->localName] = $child->nodeValue;
}
}
$desglose->setDesgloseIRPF($desgloseIrpf);
}
// Parse DesgloseIS
$desgloseIsElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'DesgloseIS')->item(0);
if ($desgloseIsElement) {
$desgloseIs = [];
foreach ($desgloseIsElement->childNodes as $child) {
if ($child instanceof \DOMElement) {
$desgloseIs[$child->localName] = $child->nodeValue;
}
}
$desglose->setDesgloseIS($desgloseIs);
}
return $desglose;
}
public function getDesgloseFactura(): ?array
{
return $this->desgloseFactura;
}
public function setDesgloseFactura(?array $desgloseFactura): self
{
$this->desgloseFactura = $desgloseFactura;
return $this;
}
public function getDesgloseTipoOperacion(): ?array
{
return $this->desgloseTipoOperacion;
}
public function setDesgloseTipoOperacion(?array $desgloseTipoOperacion): self
{
$this->desgloseTipoOperacion = $desgloseTipoOperacion;
return $this;
}
public function getDesgloseIVA(): ?array
{
return $this->desgloseIVA;
}
public function setDesgloseIVA(?array $desgloseIVA): self
{
$this->desgloseIVA = $desgloseIVA;
return $this;
}
public function addDesgloseIVA(DetalleDesglose $desgloseIVA): self
{
$this->desgloseIVA[] = $desgloseIVA;
return $this;
}
public function getDesgloseIGIC(): ?array
{
return $this->desgloseIGIC;
}
public function setDesgloseIGIC(?array $desgloseIGIC): self
{
$this->desgloseIGIC = $desgloseIGIC;
return $this;
}
public function getDesgloseIRPF(): ?array
{
return $this->desgloseIRPF;
}
public function setDesgloseIRPF(?array $desgloseIRPF): self
{
$this->desgloseIRPF = $desgloseIRPF;
return $this;
}
public function getDesgloseIS(): ?array
{
return $this->desgloseIS;
}
public function setDesgloseIS(?array $desgloseIS): self
{
$this->desgloseIS = $desgloseIS;
return $this;
}
public function setDetalleDesglose(?DetalleDesglose $detalleDesglose): self
{
$this->detalleDesglose = $detalleDesglose;
return $this;
}
public function getDetalleDesglose(): ?DetalleDesglose
{
return $this->detalleDesglose;
}
}

View File

@ -0,0 +1,93 @@
<?php
namespace App\Services\EDocument\Standards\Verifactu\Models;
/**
* DesgloseRectificacion - Rectification Breakdown
*
* This class represents the DesgloseRectificacionType from the Spanish tax authority schema.
* It contains the breakdown of base and tax amounts for rectified invoices.
*/
class DesgloseRectificacion extends BaseXmlModel
{
protected float $baseRectificada;
protected float $cuotaRectificada;
protected ?float $cuotaRecargoRectificado = null;
public function __construct(float $baseRectificada, float $cuotaRectificada, ?float $cuotaRecargoRectificado = null)
{
$this->baseRectificada = $baseRectificada;
$this->cuotaRectificada = $cuotaRectificada;
$this->cuotaRecargoRectificado = $cuotaRecargoRectificado;
}
public function getBaseRectificada(): float
{
return $this->baseRectificada;
}
public function setBaseRectificada(float $baseRectificada): self
{
$this->baseRectificada = $baseRectificada;
return $this;
}
public function getCuotaRectificada(): float
{
return $this->cuotaRectificada;
}
public function setCuotaRectificada(float $cuotaRectificada): self
{
$this->cuotaRectificada = $cuotaRectificada;
return $this;
}
public function getCuotaRecargoRectificado(): ?float
{
return $this->cuotaRecargoRectificado;
}
public function setCuotaRecargoRectificado(?float $cuotaRecargoRectificado): self
{
$this->cuotaRecargoRectificado = $cuotaRecargoRectificado;
return $this;
}
public function toXml(\DOMDocument $doc): \DOMElement
{
$root = $doc->createElementNS(self::XML_NAMESPACE, self::XML_NAMESPACE_PREFIX . ':ImporteRectificacion');
// Add BaseRectificada (required)
$root->appendChild($this->createElement($doc, 'BaseRectificada', number_format($this->baseRectificada, 2, '.', '')));
// Add CuotaRectificada (required)
$root->appendChild($this->createElement($doc, 'CuotaRectificada', number_format($this->cuotaRectificada, 2, '.', '')));
// Add CuotaRecargoRectificado (optional)
if ($this->cuotaRecargoRectificado !== null) {
$root->appendChild($this->createElement($doc, 'CuotaRecargoRectificado', number_format($this->cuotaRecargoRectificado, 2, '.', '')));
}
return $root;
}
public static function fromDOMElement(\DOMElement $element): self
{
$baseRectificada = (float)self::getElementText($element, 'BaseRectificada');
$cuotaRectificada = (float)self::getElementText($element, 'CuotaRectificada');
$cuotaRecargoRectificado = self::getElementText($element, 'CuotaRecargoRectificado');
return new self(
$baseRectificada,
$cuotaRectificada,
$cuotaRecargoRectificado ? (float)$cuotaRecargoRectificado : null
);
}
protected static function getElementText(\DOMElement $element, string $tagName): ?string
{
$node = $element->getElementsByTagNameNS(self::XML_NAMESPACE, $tagName)->item(0);
return $node ? $node->nodeValue : null;
}
}

View File

@ -0,0 +1,67 @@
<?php
namespace App\Services\EDocument\Standards\Verifactu\Models;
class DetalleDesglose extends BaseXmlModel
{
protected array $desgloseIVA = [];
public function setDesgloseIVA(array $desglose): self
{
$this->desgloseIVA = $desglose;
return $this;
}
public function getDesgloseIVA(): array
{
return $this->desgloseIVA;
}
public function toXml(\DOMDocument $doc): \DOMElement
{
$root = $this->createElement($doc, 'DetalleDesglose');
// Add IVA details directly under DetalleDesglose
$root->appendChild($this->createElement($doc, 'Impuesto', $this->desgloseIVA['Impuesto']));
if(isset($this->desgloseIVA['ClaveRegimen']) && in_array($this->desgloseIVA['ClaveRegimen'], ['01','03'])){
$root->appendChild($this->createElement($doc, 'ClaveRegimen', $this->desgloseIVA['ClaveRegimen']));
}
$root->appendChild($this->createElement($doc, 'CalificacionOperacion', $this->desgloseIVA['CalificacionOperacion']));
if(isset($this->desgloseIVA['TipoImpositivo']) && $this->desgloseIVA['CalificacionOperacion'] == 'S1') {
$root->appendChild($this->createElement($doc, 'TipoImpositivo', (string)$this->desgloseIVA['TipoImpositivo']));
}
$root->appendChild($this->createElement($doc, 'BaseImponibleOimporteNoSujeto', (string)$this->desgloseIVA['BaseImponible']));
if(isset($this->desgloseIVA['Cuota']) && $this->desgloseIVA['CalificacionOperacion'] == 'S1') {
$root->appendChild($this->createElement($doc, 'CuotaRepercutida', (string)$this->desgloseIVA['Cuota']));
}
return $root;
}
public static function fromDOMElement(\DOMElement $element): self
{
$detalleDesglose = new self();
$desglose = [
'Impuesto' => self::getElementText($element, 'Impuesto'),
'ClaveRegimen' => self::getElementText($element, 'ClaveRegimen'),
'CalificacionOperacion' => self::getElementText($element, 'CalificacionOperacion'),
'BaseImponible' => (float)self::getElementText($element, 'BaseImponibleOimporteNoSujeto'),
'TipoImpositivo' => (float)self::getElementText($element, 'TipoImpositivo'),
'Cuota' => (float)self::getElementText($element, 'CuotaRepercutida')
];
$detalleDesglose->setDesgloseIVA($desglose);
return $detalleDesglose;
}
protected static function getElementText(\DOMElement $element, string $tagName): ?string
{
$node = $element->getElementsByTagNameNS(self::XML_NAMESPACE, $tagName)->item(0);
return $node ? $node->nodeValue : null;
}
}

View File

@ -0,0 +1,121 @@
<?php
namespace App\Services\EDocument\Standards\Verifactu\Models;
use App\Services\EDocument\Standards\Verifactu\Models\RegistroAnterior;
class Encadenamiento extends BaseXmlModel
{
protected ?string $primerRegistro = null;
protected ?RegistroAnterior $registroAnterior = null;
protected ?RegistroAnterior $registroPosterior = null;
public function toXml(\DOMDocument $doc): \DOMElement
{
$root = $this->createElement($doc, 'Encadenamiento');
if ($this->registroAnterior !== null) {
$root->appendChild($this->registroAnterior->toXml($doc));
} else {
// Always include PrimerRegistro if no RegistroAnterior is set
$root->appendChild($this->createElement($doc, 'PrimerRegistro', 'S'));
}
if ($this->registroPosterior !== null) {
$root->appendChild($this->registroPosterior->toXml($doc));
}
return $root;
}
public static function fromXml($xml): BaseXmlModel
{
$encadenamiento = new self();
if (is_string($xml)) {
error_log("Loading XML in Encadenamiento::fromXml: " . $xml);
$dom = new \DOMDocument();
if (!$dom->loadXML($xml)) {
error_log("Failed to load XML in Encadenamiento::fromXml");
throw new \DOMException('Invalid XML');
}
$element = $dom->documentElement;
} else {
$element = $xml;
}
try {
// Handle PrimerRegistro
$primerRegistro = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'PrimerRegistro')->item(0);
if ($primerRegistro) {
$encadenamiento->setPrimerRegistro($primerRegistro->nodeValue);
}
// Handle RegistroAnterior
$registroAnterior = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'RegistroAnterior')->item(0);
if ($registroAnterior) {
$encadenamiento->setRegistroAnterior(RegistroAnterior::fromDOMElement($registroAnterior));
}
return $encadenamiento;
} catch (\Exception $e) {
error_log("Error parsing XML in Encadenamiento::fromXml: " . $e->getMessage());
throw new \InvalidArgumentException('Error parsing XML: ' . $e->getMessage());
}
}
public static function fromDOMElement(\DOMElement $element): self
{
$encadenamiento = new self();
// Handle PrimerRegistro
$primerRegistro = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'PrimerRegistro')->item(0);
if ($primerRegistro) {
$encadenamiento->setPrimerRegistro($primerRegistro->nodeValue);
}
// Handle RegistroAnterior
$registroAnterior = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'RegistroAnterior')->item(0);
if ($registroAnterior) {
$encadenamiento->setRegistroAnterior(RegistroAnterior::fromDOMElement($registroAnterior));
}
return $encadenamiento;
}
public function getPrimerRegistro(): ?string
{
return $this->primerRegistro;
}
public function setPrimerRegistro(?string $primerRegistro): self
{
if ($primerRegistro !== null && $primerRegistro !== 'S') {
throw new \InvalidArgumentException('PrimerRegistro must be "S" or null');
}
$this->primerRegistro = $primerRegistro;
return $this;
}
public function getRegistroAnterior(): ?RegistroAnterior
{
return $this->registroAnterior;
}
public function setRegistroAnterior(?RegistroAnterior $registroAnterior): self
{
$this->registroAnterior = $registroAnterior;
return $this;
}
public function getRegistroPosterior(): ?RegistroAnterior
{
return $this->registroPosterior;
}
public function setRegistroPosterior(?RegistroAnterior $registroPosterior): self
{
$this->registroPosterior = $registroPosterior;
return $this;
}
}

View File

@ -0,0 +1,106 @@
<?php
namespace App\Services\EDocument\Standards\Verifactu\Models;
use DOMDocument;
use DOMElement;
class IDFactura extends BaseXmlModel
{
protected string $idEmisorFactura;
protected string $numSerieFactura;
protected string $fechaExpedicionFactura;
public function __construct()
{
// Initialize with default values
$this->idEmisorFactura = 'B12345678';
$this->numSerieFactura = '';
$this->fechaExpedicionFactura = now()->format('d-m-Y');
}
public function getIdEmisorFactura(): string
{
return $this->idEmisorFactura;
}
public function setIdEmisorFactura(string $idEmisorFactura): self
{
$this->idEmisorFactura = $idEmisorFactura;
return $this;
}
public function getNumSerieFactura(): string
{
return $this->numSerieFactura;
}
public function setNumSerieFactura(string $numSerieFactura): self
{
$this->numSerieFactura = $numSerieFactura;
return $this;
}
public function getFechaExpedicionFactura(): string
{
return $this->fechaExpedicionFactura;
}
public function setFechaExpedicionFactura(string $fechaExpedicionFactura): self
{
$this->fechaExpedicionFactura = $fechaExpedicionFactura;
return $this;
}
public function toXml(DOMDocument $doc): DOMElement
{
$idFactura = $this->createElement($doc, 'IDFactura');
$idFactura->appendChild($this->createElement($doc, 'IDEmisorFactura', $this->idEmisorFactura));
$idFactura->appendChild($this->createElement($doc, 'NumSerieFactura', $this->numSerieFactura));
$idFactura->appendChild($this->createElement($doc, 'FechaExpedicionFactura', $this->fechaExpedicionFactura));
return $idFactura;
}
public static function fromDOMElement(DOMElement $element): self
{
$idFactura = new self();
// Parse IDEmisorFactura
$idEmisorFactura = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'IDEmisorFactura')->item(0);
if ($idEmisorFactura) {
$idFactura->setIdEmisorFactura($idEmisorFactura->nodeValue);
}
// Parse NumSerieFactura
$numSerieFactura = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'NumSerieFactura')->item(0);
if ($numSerieFactura) {
$idFactura->setNumSerieFactura($numSerieFactura->nodeValue);
}
// Parse FechaExpedicionFactura
$fechaExpedicionFactura = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'FechaExpedicionFactura')->item(0);
if ($fechaExpedicionFactura) {
$idFactura->setFechaExpedicionFactura($fechaExpedicionFactura->nodeValue);
}
return $idFactura;
}
public function serialize(): string
{
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 IDFactura object');
}
return $object;
}
}

View File

@ -0,0 +1,153 @@
<?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 App\Services\EDocument\Standards\Verifactu\Models\BaseXmlModel;
class IDOtro extends BaseXmlModel
{
private const VALID_ID_TYPES = [
'01', // NIF IVA (EU operator with VAT number, non-Spanish)
'02', // NIF in Spain
'03', // VAT number (EU operator without Spanish NIF)
'04', // Passport
'05', // Official ID document
'06', // Residence certificate
'07', // Person without identification code
'08', // Other supporting document
'09', // Tax ID from third country
];
private ?string $nombreRazon = '';
/**
* __construct
*
* @param string $codigoPais ISO 3166-1 alpha-2 country code (e.g., ES, FR, US)
* @param string $idType AEAT ID type code (e.g., '07' = Person without identification code)
* @param string $id Identifier value, e.g., passport number, tax ID, or placeholder
* @return void
*/
public function __construct(private string $codigoPais = 'ES', private string $idType = '06', private string $id = 'NO_DISPONIBLE')
{
}
public function getNombreRazon(): string
{
return $this->nombreRazon;
}
public function getCodigoPais(): string
{
return $this->codigoPais;
}
public function getIdType(): string
{
return $this->idType;
}
public function getId(): string
{
return $this->id;
}
public function setNombreRazon(string $nombreRazon): self
{
$this->nombreRazon = $nombreRazon;
return $this;
}
public function setCodigoPais(string $codigoPais): self
{
$this->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;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,220 @@
<?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 App\Services\EDocument\Standards\Verifactu\Models\BaseXmlModel;
class PersonaFisicaJuridica extends BaseXmlModel
{
protected ?string $nif = null;
protected ?string $nombreRazon = null;
protected ?string $apellidos = null;
protected ?string $nombre = null;
protected ?string $razonSocial = null;
protected ?string $tipoIdentificacion = null;
protected ?IDOtro $idOtro = null;
protected ?string $pais = null;
public function getNif(): ?string
{
return $this->nif;
}
public function setNif(?string $nif): self
{
if ($nif !== null && strlen($nif) !== 9) {
throw new \InvalidArgumentException('NIF must be exactly 9 characters long');
}
$this->nif = $nif;
return $this;
}
public function getNombreRazon(): ?string
{
return $this->nombreRazon;
}
public function setNombreRazon(?string $nombreRazon): self
{
$this->nombreRazon = $nombreRazon;
return $this;
}
public function getApellidos(): ?string
{
return $this->apellidos;
}
public function setApellidos(?string $apellidos): self
{
$this->apellidos = $apellidos;
return $this;
}
public function getNombre(): ?string
{
return $this->nombre;
}
public function setNombre(?string $nombre): self
{
$this->nombre = $nombre;
return $this;
}
public function getRazonSocial(): ?string
{
return $this->razonSocial;
}
public function setRazonSocial(?string $razonSocial): self
{
$this->razonSocial = $razonSocial;
return $this;
}
public function getTipoIdentificacion(): ?string
{
return $this->tipoIdentificacion;
}
public function setTipoIdentificacion(?string $tipoIdentificacion): self
{
$this->tipoIdentificacion = $tipoIdentificacion;
return $this;
}
public function getIdOtro(): IDOtro
{
return $this->idOtro;
}
public function setIdOtro(IDOtro $idOtro): self
{
$this->idOtro = $idOtro;
return $this;
}
public function getPais(): ?string
{
return $this->pais;
}
public function setPais(?string $pais): self
{
$this->pais = $pais;
return $this;
}
public function toXml(\DOMDocument $doc): \DOMElement
{
$root = $this->createElement($doc, 'PersonaFisicaJuridica');
if ($this->nif !== null) {
$root->appendChild($this->createElement($doc, 'NIF', $this->nif));
}
if ($this->nombreRazon !== null) {
$root->appendChild($this->createElement($doc, 'NombreRazon', $this->nombreRazon));
}
if ($this->apellidos !== null) {
$root->appendChild($this->createElement($doc, 'Apellidos', $this->apellidos));
}
if ($this->nombre !== null) {
$root->appendChild($this->createElement($doc, 'Nombre', $this->nombre));
}
if ($this->razonSocial !== null) {
$root->appendChild($this->createElement($doc, 'RazonSocial', $this->razonSocial));
}
if ($this->tipoIdentificacion !== null) {
$root->appendChild($this->createElement($doc, 'TipoIdentificacion', $this->tipoIdentificacion));
}
if ($this->idOtro !== null) {
$root->appendChild($this->idOtro->toXml($doc));
}
if ($this->pais !== null) {
$root->appendChild($this->createElement($doc, 'Pais', $this->pais));
}
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
{
$persona = new self();
$nifElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'NIF')->item(0);
if ($nifElement) {
$persona->setNif($nifElement->nodeValue);
}
$nombreRazonElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'NombreRazon')->item(0);
if ($nombreRazonElement) {
$persona->setNombreRazon($nombreRazonElement->nodeValue);
}
$apellidosElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'Apellidos')->item(0);
if ($apellidosElement) {
$persona->setApellidos($apellidosElement->nodeValue);
}
$nombreElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'Nombre')->item(0);
if ($nombreElement) {
$persona->setNombre($nombreElement->nodeValue);
}
$razonSocialElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'RazonSocial')->item(0);
if ($razonSocialElement) {
$persona->setRazonSocial($razonSocialElement->nodeValue);
}
$tipoIdentificacionElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'TipoIdentificacion')->item(0);
if ($tipoIdentificacionElement) {
$persona->setTipoIdentificacion($tipoIdentificacionElement->nodeValue);
}
$idOtroElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'IDOtro')->item(0);
if ($idOtroElement) {
$persona->setIdOtro($idOtroElement->nodeValue);
}
$paisElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'Pais')->item(0);
if ($paisElement) {
$persona->setPais($paisElement->nodeValue);
}
return $persona;
}
}

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

@ -0,0 +1,454 @@
<?php
namespace App\Services\EDocument\Standards\Verifactu\Models;
/**
* RegistroAnulacion - Invoice Cancellation Record
*
* This class represents the cancellation record information required for Verifactu e-invoicing
* modification operations. It contains the details of the invoice to be cancelled.
*/
class RegistroAnulacion extends BaseXmlModel
{
protected string $idVersion;
protected string $idEmisorFactura;
protected string $numSerieFactura;
protected string $fechaExpedicionFactura;
protected string $motivoAnulacion;
protected string $nombreRazonEmisor;
// Additional properties required by XSD schema
protected ?string $refExterna = null;
protected ?string $sinRegistroPrevio = null;
protected ?string $rechazoPrevio = null;
protected ?string $generadoPor = null;
protected ?PersonaFisicaJuridica $generador = null;
protected Encadenamiento $encadenamiento;
protected SistemaInformatico $sistemaInformatico;
protected string $fechaHoraHusoGenRegistro;
protected string $tipoHuella;
protected string $huella;
protected ?string $signature = null;
public function __construct()
{
$this->idVersion = '1.0';
$this->motivoAnulacion = '1'; // Default: Sustitución por otra factura
$this->encadenamiento = new Encadenamiento();
$this->sistemaInformatico = new SistemaInformatico();
$this->fechaHoraHusoGenRegistro = now()->format('Y-m-d\TH:i:sP');
$this->tipoHuella = '01';
$this->huella = '';
}
public function getIdVersion(): string
{
return $this->idVersion;
}
public function setIdVersion(string $idVersion): self
{
$this->idVersion = $idVersion;
return $this;
}
public function getIdEmisorFactura(): string
{
return $this->idEmisorFactura;
}
public function setIdEmisorFactura(string $idEmisorFactura): self
{
$this->idEmisorFactura = $idEmisorFactura;
return $this;
}
public function getNumSerieFactura(): string
{
return $this->numSerieFactura;
}
public function setNumSerieFactura(string $numSerieFactura): self
{
$this->numSerieFactura = $numSerieFactura;
return $this;
}
public function getFechaExpedicionFactura(): string
{
return $this->fechaExpedicionFactura;
}
public function setFechaExpedicionFactura(string $fechaExpedicionFactura): self
{
$this->fechaExpedicionFactura = $fechaExpedicionFactura;
return $this;
}
public function getMotivoAnulacion(): string
{
return $this->motivoAnulacion;
}
public function setMotivoAnulacion(string $motivoAnulacion): self
{
$this->motivoAnulacion = $motivoAnulacion;
return $this;
}
public function getRefExterna(): ?string
{
return $this->refExterna;
}
public function setRefExterna(?string $refExterna): self
{
$this->refExterna = $refExterna;
return $this;
}
public function getSinRegistroPrevio(): ?string
{
return $this->sinRegistroPrevio;
}
public function setSinRegistroPrevio(?string $sinRegistroPrevio): self
{
$this->sinRegistroPrevio = $sinRegistroPrevio;
return $this;
}
public function getRechazoPrevio(): ?string
{
return $this->rechazoPrevio;
}
public function setRechazoPrevio(?string $rechazoPrevio): self
{
$this->rechazoPrevio = $rechazoPrevio;
return $this;
}
public function getGeneradoPor(): ?string
{
return $this->generadoPor;
}
public function setGeneradoPor(?string $generadoPor): self
{
$this->generadoPor = $generadoPor;
return $this;
}
public function getGenerador(): ?PersonaFisicaJuridica
{
return $this->generador;
}
public function setGenerador(?PersonaFisicaJuridica $generador): self
{
$this->generador = $generador;
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 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 getNombreRazonEmisor(): string
{
return $this->nombreRazonEmisor;
}
public function setNombreRazonEmisor(string $nombreRazonEmisor): self
{
$this->nombreRazonEmisor = $nombreRazonEmisor;
return $this;
}
public function toXml(\DOMDocument $doc): \DOMElement
{
$root = $doc->createElementNS(self::XML_NAMESPACE, self::XML_NAMESPACE_PREFIX . ':RegistroAnulacion');
// Add IDVersion
$root->appendChild($this->createElement($doc, 'IDVersion', $this->idVersion));
// Create IDFactura structure
$idFactura = $this->createElement($doc, 'IDFactura');
$idFactura->appendChild($this->createElement($doc, 'IDEmisorFacturaAnulada', $this->idEmisorFactura));
$idFactura->appendChild($this->createElement($doc, 'NumSerieFacturaAnulada', $this->numSerieFactura));
$idFactura->appendChild($this->createElement($doc, 'FechaExpedicionFacturaAnulada', $this->fechaExpedicionFactura));
$root->appendChild($idFactura);
// Add optional RefExterna
if ($this->refExterna !== null) {
$root->appendChild($this->createElement($doc, 'RefExterna', $this->refExterna));
}
// Add optional SinRegistroPrevio
if ($this->sinRegistroPrevio !== null) {
$root->appendChild($this->createElement($doc, 'SinRegistroPrevio', $this->sinRegistroPrevio));
}
// Add optional RechazoPrevio
if ($this->rechazoPrevio !== null) {
$root->appendChild($this->createElement($doc, 'RechazoPrevio', $this->rechazoPrevio));
}
// Add optional GeneradoPor
if ($this->generadoPor !== null) {
$root->appendChild($this->createElement($doc, 'GeneradoPor', $this->generadoPor));
}
// Add optional Generador
if ($this->generador !== null) {
$root->appendChild($this->generador->toXml($doc));
}
// Add Encadenamiento using actual property
$encadenamientoElement = $this->encadenamiento->toXml($doc);
$root->appendChild($encadenamientoElement);
// Add SistemaInformatico using actual property
$sistemaInformaticoElement = $this->sistemaInformatico->toXml($doc);
$root->appendChild($sistemaInformaticoElement);
// Add FechaHoraHusoGenRegistro using actual property
$root->appendChild($this->createElement($doc, 'FechaHoraHusoGenRegistro', $this->fechaHoraHusoGenRegistro));
// Add TipoHuella using actual property
$root->appendChild($this->createElement($doc, 'TipoHuella', $this->tipoHuella));
// Add Huella using actual property
$root->appendChild($this->createElement($doc, 'Huella', $this->huella));
// Add optional Signature
if ($this->signature !== null) {
$root->appendChild($this->createDsElement($doc, 'Signature', $this->signature));
}
return $root;
}
public static function fromDOMElement(\DOMElement $element): self
{
$registroAnulacion = new self();
// Handle IDVersion
$idVersion = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'IDVersion')->item(0);
if ($idVersion) {
$registroAnulacion->setIdVersion($idVersion->nodeValue);
}
// Handle IDFactura
$idFactura = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'IDFactura')->item(0);
if ($idFactura) {
$idEmisorFactura = $idFactura->getElementsByTagNameNS(self::XML_NAMESPACE, 'IDEmisorFacturaAnulada')->item(0);
if ($idEmisorFactura) {
$registroAnulacion->setIdEmisorFactura($idEmisorFactura->nodeValue);
}
$numSerieFactura = $idFactura->getElementsByTagNameNS(self::XML_NAMESPACE, 'NumSerieFacturaAnulada')->item(0);
if ($numSerieFactura) {
$registroAnulacion->setNumSerieFactura($numSerieFactura->nodeValue);
}
$fechaExpedicionFactura = $idFactura->getElementsByTagNameNS(self::XML_NAMESPACE, 'FechaExpedicionFacturaAnulada')->item(0);
if ($fechaExpedicionFactura) {
$registroAnulacion->setFechaExpedicionFactura($fechaExpedicionFactura->nodeValue);
}
}
// Handle optional elements
$refExterna = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'RefExterna')->item(0);
if ($refExterna) {
$registroAnulacion->setRefExterna($refExterna->nodeValue);
}
$sinRegistroPrevio = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'SinRegistroPrevio')->item(0);
if ($sinRegistroPrevio) {
$registroAnulacion->setSinRegistroPrevio($sinRegistroPrevio->nodeValue);
}
$rechazoPrevio = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'RechazoPrevio')->item(0);
if ($rechazoPrevio) {
$registroAnulacion->setRechazoPrevio($rechazoPrevio->nodeValue);
}
$generadoPor = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'GeneradoPor')->item(0);
if ($generadoPor) {
$registroAnulacion->setGeneradoPor($generadoPor->nodeValue);
}
// Handle Generador
$generador = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'Generador')->item(0);
if ($generador) {
$registroAnulacion->setGenerador(PersonaFisicaJuridica::fromDOMElement($generador));
}
// Handle Encadenamiento
$encadenamiento = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'Encadenamiento')->item(0);
if ($encadenamiento) {
$registroAnulacion->setEncadenamiento(Encadenamiento::fromDOMElement($encadenamiento));
}
// Handle SistemaInformatico
$sistemaInformatico = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'SistemaInformatico')->item(0);
if ($sistemaInformatico) {
$registroAnulacion->setSistemaInformatico(SistemaInformatico::fromDOMElement($sistemaInformatico));
}
// Handle FechaHoraHusoGenRegistro
$fechaHoraHusoGenRegistro = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'FechaHoraHusoGenRegistro')->item(0);
if ($fechaHoraHusoGenRegistro) {
$registroAnulacion->setFechaHoraHusoGenRegistro($fechaHoraHusoGenRegistro->nodeValue);
}
// Handle TipoHuella
$tipoHuella = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'TipoHuella')->item(0);
if ($tipoHuella) {
$registroAnulacion->setTipoHuella($tipoHuella->nodeValue);
}
// Handle Huella
$huella = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'Huella')->item(0);
if ($huella) {
$registroAnulacion->setHuella($huella->nodeValue);
}
// Handle MotivoAnulacion
$motivoAnulacion = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'MotivoAnulacion')->item(0);
if ($motivoAnulacion) {
$registroAnulacion->setMotivoAnulacion($motivoAnulacion->nodeValue);
}
return $registroAnulacion;
}
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();
}
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', $this->getNombreRazonEmisor()));
$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->getIdEmisorFactura()));
// 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();
}
}

View File

@ -0,0 +1,243 @@
<?php
namespace App\Services\EDocument\Standards\Verifactu\Models;
class SistemaInformatico extends BaseXmlModel
{
protected string $nombreRazon;
protected ?string $nif = null;
protected ?string $idOtro = null;
protected string $nombreSistemaInformatico;
protected string $idSistemaInformatico;
protected string $version;
protected string $numeroInstalacion;
protected string $tipoUsoPosibleSoloVerifactu = 'S';
protected string $tipoUsoPosibleMultiOT = 'S';
protected string $indicadorMultiplesOT = 'S';
public function __construct()
{
// Initialize required properties with default values
$this->nombreRazon = 'InvoiceNinja System';
$this->nombreSistemaInformatico = 'InvoiceNinja';
$this->idSistemaInformatico = '01';
$this->version = '1.0.0';
$this->numeroInstalacion = '001';
$this->nif = 'B12345678'; // Default NIF
}
public function toXml(\DOMDocument $doc): \DOMElement
{
$root = $this->createElement($doc, 'SistemaInformatico');
// Add nombreRazon (first element in nested sequence)
$root->appendChild($this->createElement($doc, 'NombreRazon', $this->nombreRazon));
// Add either NIF or IDOtro (second element in nested sequence)
if ($this->nif !== null) {
$root->appendChild($this->createElement($doc, 'NIF', $this->nif));
} elseif ($this->idOtro !== null) {
$root->appendChild($this->createElement($doc, 'IDOtro', $this->idOtro));
} else {
// If neither NIF nor IDOtro is set, we need to set a default NIF
$root->appendChild($this->createElement($doc, 'NIF', 'B12345678'));
}
// Add remaining elements (outside the nested sequence)
$root->appendChild($this->createElement($doc, 'NombreSistemaInformatico', $this->nombreSistemaInformatico));
$root->appendChild($this->createElement($doc, 'IdSistemaInformatico', $this->idSistemaInformatico));
$root->appendChild($this->createElement($doc, 'Version', $this->version));
$root->appendChild($this->createElement($doc, 'NumeroInstalacion', $this->numeroInstalacion));
$root->appendChild($this->createElement($doc, 'TipoUsoPosibleSoloVerifactu', $this->tipoUsoPosibleSoloVerifactu));
$root->appendChild($this->createElement($doc, 'TipoUsoPosibleMultiOT', $this->tipoUsoPosibleMultiOT));
$root->appendChild($this->createElement($doc, 'IndicadorMultiplesOT', $this->indicadorMultiplesOT));
return $root;
}
/**
* Create a SistemaInformatico instance from XML string
*/
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
{
$sistemaInformatico = new self();
// Parse NombreRazon
$nombreRazonElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'NombreRazon')->item(0);
if ($nombreRazonElement) {
$sistemaInformatico->setNombreRazon($nombreRazonElement->nodeValue);
}
// Parse NIF or IDOtro
$nifElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'NIF')->item(0);
if ($nifElement) {
$sistemaInformatico->setNif($nifElement->nodeValue);
} else {
$idOtroElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'IDOtro')->item(0);
if ($idOtroElement) {
$sistemaInformatico->setIdOtro($idOtroElement->nodeValue);
}
}
// Parse remaining elements
$nombreSistemaElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'NombreSistemaInformatico')->item(0);
if ($nombreSistemaElement) {
$sistemaInformatico->setNombreSistemaInformatico($nombreSistemaElement->nodeValue);
}
$idSistemaElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'IdSistemaInformatico')->item(0);
if ($idSistemaElement) {
$sistemaInformatico->setIdSistemaInformatico($idSistemaElement->nodeValue);
}
$versionElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'Version')->item(0);
if ($versionElement) {
$sistemaInformatico->setVersion($versionElement->nodeValue);
}
$numeroInstalacionElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'NumeroInstalacion')->item(0);
if ($numeroInstalacionElement) {
$sistemaInformatico->setNumeroInstalacion($numeroInstalacionElement->nodeValue);
}
$tipoUsoPosibleSoloVerifactuElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'TipoUsoPosibleSoloVerifactu')->item(0);
if ($tipoUsoPosibleSoloVerifactuElement) {
$sistemaInformatico->setTipoUsoPosibleSoloVerifactu($tipoUsoPosibleSoloVerifactuElement->nodeValue);
}
$tipoUsoPosibleMultiOTElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'TipoUsoPosibleMultiOT')->item(0);
if ($tipoUsoPosibleMultiOTElement) {
$sistemaInformatico->setTipoUsoPosibleMultiOT($tipoUsoPosibleMultiOTElement->nodeValue);
}
$indicadorMultiplesOTElement = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'IndicadorMultiplesOT')->item(0);
if ($indicadorMultiplesOTElement) {
$sistemaInformatico->setIndicadorMultiplesOT($indicadorMultiplesOTElement->nodeValue);
}
return $sistemaInformatico;
}
public function getNombreRazon(): string
{
return $this->nombreRazon;
}
public function setNombreRazon(string $nombreRazon): self
{
$this->nombreRazon = $nombreRazon;
return $this;
}
public function getNif(): ?string
{
return $this->nif;
}
public function setNif(?string $nif): self
{
$this->nif = $nif;
return $this;
}
public function getIdOtro(): ?string
{
return $this->idOtro;
}
public function setIdOtro(?string $idOtro): self
{
$this->idOtro = $idOtro;
return $this;
}
public function getNombreSistemaInformatico(): string
{
return $this->nombreSistemaInformatico;
}
public function setNombreSistemaInformatico(string $nombreSistemaInformatico): self
{
$this->nombreSistemaInformatico = $nombreSistemaInformatico;
return $this;
}
public function getIdSistemaInformatico(): string
{
return $this->idSistemaInformatico;
}
public function setIdSistemaInformatico(string $idSistemaInformatico): self
{
$this->idSistemaInformatico = $idSistemaInformatico;
return $this;
}
public function getVersion(): string
{
return $this->version;
}
public function setVersion(string $version): self
{
$this->version = $version;
return $this;
}
public function getNumeroInstalacion(): string
{
return $this->numeroInstalacion;
}
public function setNumeroInstalacion(string $numeroInstalacion): self
{
$this->numeroInstalacion = $numeroInstalacion;
return $this;
}
public function getTipoUsoPosibleSoloVerifactu(): string
{
return $this->tipoUsoPosibleSoloVerifactu;
}
public function setTipoUsoPosibleSoloVerifactu(string $tipoUsoPosibleSoloVerifactu): self
{
$this->tipoUsoPosibleSoloVerifactu = $tipoUsoPosibleSoloVerifactu;
return $this;
}
public function getTipoUsoPosibleMultiOT(): string
{
return $this->tipoUsoPosibleMultiOT;
}
public function setTipoUsoPosibleMultiOT(string $tipoUsoPosibleMultiOT): self
{
$this->tipoUsoPosibleMultiOT = $tipoUsoPosibleMultiOT;
return $this;
}
public function getIndicadorMultiplesOT(): string
{
return $this->indicadorMultiplesOT;
}
public function setIndicadorMultiplesOT(string $indicadorMultiplesOT): self
{
$this->indicadorMultiplesOT = $indicadorMultiplesOT;
return $this;
}
}

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,402 @@
<?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\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\IDFactura;
use App\Services\EDocument\Standards\Verifactu\Models\Encadenamiento;
use App\Services\EDocument\Standards\Verifactu\Models\DetalleDesglose;
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;
class RegistroAlta
{
use Taxer;
use NumberFormatter;
use MakesHash;
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
'06' => 'IAE', //local taxes - rarely used
'07' => 'Non-Vat / Exempt operations'
];
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
$this->v_log = $this->company->verifactu_logs()->first();
$this->current_timestamp = now()->format('Y-m-d\TH:i:sP');
$this->v_invoice
->setIdVersion('1.0')
->setIdFactura((new IDFactura())
->setIdEmisorFactura($this->company->settings->vat_number)
->setNumSerieFactura($this->invoice->number)
->setFechaExpedicionFactura(\Carbon\Carbon::parse($this->invoice->date)->format('d-m-Y')))
->setNombreRazonEmisor($this->company->present()->name()) //company name
->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');
/** The business entity that is issuing the invoice */
$emisor = new PersonaFisicaJuridica();
$emisor->setNif($this->company->settings->vat_number)
->setNombreRazon($this->invoice->company->present()->name());
/** The business entity (Client) that is receiving the invoice */
$destinatarios = [];
$destinatario = new PersonaFisicaJuridica();
//Spanish NIF/VAT
if($this->invoice->client->country_id == 724 && strlen($this->invoice->client->vat_number ?? '') > 5) {
$destinatario
->setNif($this->invoice->client->vat_number)
->setNombreRazon($this->invoice->client->present()->name());
}
elseif($this->invoice->client->country_id == 724) { // Spanish Passport
$destinatario = new IDOtro();
$destinatario->setNombreRazon($this->invoice->client->present()->name());
$destinatario->setCodigoPais('ES')
->setIdType('03')
->setId($this->invoice->client->id_number);
}
else {
$locationData = $this->invoice->service()->location();
$destinatario = new IDOtro();
$destinatario->setNombreRazon($this->invoice->client->present()->name());
$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;
$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' => $this->calculateRegimeClassification($tax['name']), //tax regime classification code
'CalificacionOperacion' => $this->calculateOperationClassification($tax['name']), //operation classification code
'BaseImponible' => $tax['base_amount'] ?? $this->calc->getNetSubtotal(), // taxable base amount - fixed: key matches DetalleDesglose::toXml()
'TipoImpositivo' => $tax['tax_rate'], // Tax Rate
'Cuota' => $tax['total'] // Tax Amount - fixed: key matches DetalleDesglose::toXml()
];
$detalle_desglose = new DetalleDesglose();
$detalle_desglose->setDesgloseIVA($desglose_iva);
$desglose->addDesgloseIVA($detalle_desglose);
};
if(count($taxes) == 0) {
$client_country_code = $this->invoice->client->country->iso_3166_2;
/** By Default we assume a Spanish transaction */
$impuesto = 'S2';
$clave_regimen = '08';
$calificacion = 'S1';
$br = new \App\DataMapper\Tax\BaseRule();
/** EU B2B */
if (in_array($client_country_code, $br->eu_country_codes) && $this->invoice->client->classification != 'individual') {
$impuesto = '05';
$clave_regimen = '05';
$calificacion = 'N2';
} /** EU B2C */
elseif (in_array($client_country_code, $br->eu_country_codes) && $this->invoice->client->classification == 'individual') {
$impuesto = '08';
$clave_regimen = '05';
$calificacion = 'N2';
}
else { /** Non-EU */
$impuesto = '05';
$clave_regimen = '05';
$calificacion = 'N2';
}
$desglose_iva = [
'Impuesto' => $impuesto, //tax type
'ClaveRegimen' => $clave_regimen, //tax regime classification code
'CalificacionOperacion' => $calificacion, //operation classification code
'BaseImponible' => $this->calc->getNetSubtotal(), // taxable base amount - fixed: key matches DetalleDesglose::toXml()
];
$detalle_desglose = new DetalleDesglose();
$detalle_desglose->setDesgloseIVA($desglose_iva);
$desglose->addDesgloseIVA($detalle_desglose);
}
$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($this->v_log->nif);
$registro_anterior->setNumSerieFactura($this->v_log->invoice_number);
$registro_anterior->setFechaExpedicionFactura($this->v_log->date->format('d-m-Y'));
$registro_anterior->setHuella($this->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('Sistema de Facturación')
->setNombreRazon(config('services.verifactu.sender_name')) //must match the cert name
->setNif(config('services.verifactu.sender_nif'))
->setNombreSistemaInformatico('InvoiceNinja')
->setIdSistemaInformatico('77')
->setVersion('1.0.03')
->setNumeroInstalacion('383')
->setTipoUsoPosibleSoloVerifactu('N')
->setTipoUsoPosibleMultiOT('S')
->setIndicadorMultiplesOT('S');
$this->v_invoice->setSistemaInformatico($sistema);
return $this;
}
public function setRectification(): self
{
$this->v_invoice->setTipoFactura('R2');
$this->v_invoice->setTipoRectificativa('I'); // S for substitutive rectification
//need to harvest the parent invoice!!
$_i = Invoice::withTrashed()->find($this->decodePrimaryKey($this->invoice->backup->parent_invoice_id));
if(!$_i) {
throw new \Exception('Parent invoice not found');
}
// Set up rectified invoice information
$facturasRectificadas = [
[
'IDEmisorFactura' => $this->company->settings->vat_number,
'NumSerieFactura' => $_i->number,
'FechaExpedicionFactura' => \Carbon\Carbon::parse($_i->date)->format('d-m-Y')
]
];
$this->v_invoice->setFacturasRectificadas($facturasRectificadas);
$this->invoice->backup->document_type = 'R2';
$this->invoice->save();
return $this;
}
public function getInvoice(): VerifactuInvoice
{
return $this->v_invoice;
}
private function calculateRegimeClassification(string $tax_name): string
{
$client_country_code = $this->invoice->client->country->iso_3166_2;
if($client_country_code == 'ES') {
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';
}
$br = new \App\DataMapper\Tax\BaseRule();
if (in_array($client_country_code, $br->eu_country_codes) && $this->invoice->client->classification != 'individual') {
return '08';
} elseif (in_array($client_country_code, $br->eu_country_codes) && $this->invoice->client->classification == 'individual') {
return '05';
}
return '07';
}
private function calculateTaxType(string $tax_name): string
{
$client_country_code = $this->invoice->client->country->iso_3166_2;
if($client_country_code == 'ES') {
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';
}
$br = new \App\DataMapper\Tax\BaseRule();
if (in_array($client_country_code, $br->eu_country_codes) && $this->invoice->client->classification != 'individual') {
return '08';
}
elseif (in_array($client_country_code, $br->eu_country_codes) && $this->invoice->client->classification == 'individual') {
return '05';
}
return '07';
}
private function calculateOperationClassification(string $tax_name): string
{
if($this->invoice->client->country_id == 724 || stripos($tax_name, 'iva') !== false) {
return 'S1';
}
return 'N2';
}
}

View File

@ -0,0 +1,340 @@
<?php
namespace App\Services\EDocument\Standards\Verifactu;
use DOMDocument;
use DOMElement;
use DOMNodeList;
use Exception;
use Illuminate\Support\Facades\Log;
class ResponseProcessor
{
private DOMDocument $dom;
private ?DOMElement $root = null;
public function __construct()
{
$this->dom = new DOMDocument();
}
/**
* Process AEAT XML response and return structured array
*/
public function processResponse(string $xmlResponse): array
{
try {
$this->loadXml($xmlResponse);
nlog($this->dom->saveXML());
return [
'success' => $this->isSuccessful(),
'status' => $this->getStatus(),
'errors' => $this->getErrors(),
'warnings' => $this->getWarnings(),
'data' => $this->getResponseData(),
'metadata' => $this->getMetadata(),
'guid' => $this->getGuid(),
'raw_response' => $xmlResponse
];
} catch (Exception $e) {
Log::error('Error processing AEAT response', [
'error' => $e->getMessage(),
'xml' => $xmlResponse
]);
return [
'success' => false,
'error' => 'Failed to process response: ' . $e->getMessage(),
'raw_response' => $xmlResponse
];
}
}
/**
* Load XML into DOM
*/
private function loadXml(string $xml): void
{
libxml_use_internal_errors(true);
libxml_clear_errors();
if (!$this->dom->loadXML($xml)) {
$errors = libxml_get_errors();
libxml_clear_errors();
throw new Exception('Invalid XML: ' . ($errors[0]->message ?? 'Unknown error'));
}
$this->root = $this->dom->documentElement;
}
private function getGuid(): ?string
{
return $this->getElementText('.//tikR:CSV') ?? null;
}
/**
* Check if response indicates success
*/
private function isSuccessful(): bool
{
$estadoEnvio = $this->getElementText('//tikR:EstadoEnvio');
return $estadoEnvio === 'Correcto';
}
/**
* Get response status
*/
private function getStatus(): string
{
return $this->getElementText('//tikR:EstadoEnvio') ?? 'Unknown';
}
/**
* Get all errors from response
*/
private function getErrors(): array
{
$errors = [];
// Check for SOAP faults
$fault = $this->getElementText('//env:Fault/faultstring');
if ($fault) {
$errors[] = [
'type' => 'SOAP_Fault',
'code' => $this->getElementText('//env:Fault/faultcode'),
'message' => $fault,
'details' => $this->getElementText('//env:Fault/detail/callstack')
];
}
// Check for business logic errors
$respuestaLineas = $this->dom->getElementsByTagNameNS(
'https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/RespuestaSuministro.xsd',
'RespuestaLinea'
);
foreach ($respuestaLineas as $linea) {
$estadoRegistro = $this->getElementText('.//tikR:EstadoRegistro', $linea);
if ($estadoRegistro === 'Incorrecto') {
$errors[] = [
'type' => 'Business_Error',
'code' => $this->getElementText('.//tikR:CodigoErrorRegistro', $linea),
'message' => $this->getElementText('.//tikR:DescripcionErrorRegistro', $linea),
'invoice_data' => $this->getInvoiceData($linea)
];
}
}
return $errors;
}
/**
* Get warnings from response
*/
private function getWarnings(): array
{
$warnings = [];
// Check for subsanacion (correction) messages
$subsanacion = $this->getElementText('//tikR:RespuestaLinea/tikR:Subsanacion');
if ($subsanacion) {
$warnings[] = [
'type' => 'Subsanacion',
'message' => $subsanacion
];
}
return $warnings;
}
/**
* Get response data
*/
private function getResponseData(): array
{
$data = [];
// Get header information
$cabecera = $this->getElement('//tikR:Cabecera');
if ($cabecera) {
$data['header'] = [
'obligado_emision' => [
'nombre_razon' => $this->getElementText('.//tik:NombreRazon', $cabecera),
'nif' => $this->getElementText('.//tik:NIF', $cabecera)
]
];
}
// Get processing information
$data['processing'] = [
'tiempo_espera_envio' => $this->getElementText('//tikR:TiempoEsperaEnvio'),
'estado_envio' => $this->getElementText('//tikR:EstadoEnvio')
];
// Get invoice responses
$data['invoices'] = $this->getInvoiceResponses();
return $data;
}
/**
* Get metadata from response
*/
private function getMetadata(): array
{
return [
'request_id' => $this->getElementText('//tikR:RespuestaLinea/tikR:IDFactura/tik:IDEmisorFactura'),
'invoice_series' => $this->getElementText('//tikR:RespuestaLinea/tikR:IDFactura/tik:NumSerieFactura'),
'invoice_date' => $this->getElementText('//tikR:RespuestaLinea/tikR:IDFactura/tik:FechaExpedicionFactura'),
'operation_type' => $this->getElementText('//tikR:RespuestaLinea/tikR:Operacion/tik:TipoOperacion'),
'external_reference' => $this->getElementText('//tikR:RespuestaLinea/tikR:RefExterna')
];
}
/**
* Get invoice responses
*/
private function getInvoiceResponses(): array
{
$invoices = [];
$respuestaLineas = $this->dom->getElementsByTagNameNS(
'https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/RespuestaSuministro.xsd',
'RespuestaLinea'
);
foreach ($respuestaLineas as $linea) {
$invoices[] = [
'id_emisor' => $this->getElementText('.//tikR:IDFactura/tik:IDEmisorFactura', $linea),
'num_serie' => $this->getElementText('.//tikR:IDFactura/tik:NumSerieFactura', $linea),
'fecha_expedicion' => $this->getElementText('.//tikR:IDFactura/tik:FechaExpedicionFactura', $linea),
'tipo_operacion' => $this->getElementText('.//tikR:Operacion/tik:TipoOperacion', $linea),
'ref_externa' => $this->getElementText('.//tikR:RefExterna', $linea),
'estado_registro' => $this->getElementText('.//tikR:EstadoRegistro', $linea),
'codigo_error' => $this->getElementText('.//tikR:CodigoErrorRegistro', $linea),
'descripcion_error' => $this->getElementText('.//tikR:DescripcionErrorRegistro', $linea),
'subsanacion' => $this->getElementText('.//tikR:Subsanacion', $linea)
];
}
return $invoices;
}
/**
* Get invoice data from response line
*/
private function getInvoiceData(DOMElement $linea): array
{
return [
'id_emisor' => $this->getElementText('.//tikR:IDFactura/tik:IDEmisorFactura', $linea),
'num_serie' => $this->getElementText('.//tikR:IDFactura/tik:NumSerieFactura', $linea),
'fecha_expedicion' => $this->getElementText('.//tikR:IDFactura/tik:FechaExpedicionFactura', $linea),
'tipo_operacion' => $this->getElementText('.//tikR:Operacion/tik:TipoOperacion', $linea),
'ref_externa' => $this->getElementText('.//tikR:RefExterna', $linea)
];
}
/**
* Get element text by XPath
*/
private function getElementText(string $xpath, ?DOMElement $context = null): ?string
{
$xpathObj = new \DOMXPath($this->dom);
// Register namespaces
$xpathObj->registerNamespace('env', 'http://schemas.xmlsoap.org/soap/envelope/');
$xpathObj->registerNamespace('tikR', 'https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/RespuestaSuministro.xsd');
$xpathObj->registerNamespace('tik', 'https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd');
$nodeList = $context ? $xpathObj->query($xpath, $context) : $xpathObj->query($xpath);
if ($nodeList && $nodeList->length > 0) {
return trim($nodeList->item(0)->nodeValue);
}
return null;
}
/**
* Get element by XPath
*/
private function getElement(string $xpath): ?DOMElement
{
$xpathObj = new \DOMXPath($this->dom);
// Register namespaces
$xpathObj->registerNamespace('env', 'http://schemas.xmlsoap.org/soap/envelope/');
$xpathObj->registerNamespace('tikR', 'https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/RespuestaSuministro.xsd');
$xpathObj->registerNamespace('tik', 'https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd');
$nodeList = $xpathObj->query($xpath);
if ($nodeList && $nodeList->length > 0) {
$node = $nodeList->item(0);
return $node instanceof DOMElement ? $node : null;
}
return null;
}
/**
* Check if response has errors
*/
public function hasErrors(): bool
{
return !empty($this->getErrors());
}
/**
* Get first error message
*/
public function getFirstError(): ?string
{
$errors = $this->getErrors();
return $errors[0]['message'] ?? null;
}
/**
* Get error codes
*/
public function getErrorCodes(): array
{
$codes = [];
$errors = $this->getErrors();
foreach ($errors as $error) {
if (isset($error['code'])) {
$codes[] = $error['code'];
}
}
return $codes;
}
/**
* Check if specific error code exists
*/
public function hasErrorCode(string $code): bool
{
return in_array($code, $this->getErrorCodes());
}
/**
* Get summary of response
*/
public function getSummary(): array
{
return [
'success' => $this->isSuccessful(),
'status' => $this->getStatus(),
'error_count' => count($this->getErrors()),
'warning_count' => count($this->getWarnings()),
'invoice_count' => count($this->getInvoiceResponses()),
'first_error' => $this->getFirstError(),
'error_codes' => $this->getErrorCodes()
];
}
}

View File

@ -0,0 +1,236 @@
<?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 Mail;
use App\Utils\Ninja;
use App\Models\Company;
use App\Models\Invoice;
use App\Models\Activity;
use App\Models\SystemLog;
use App\Libraries\MultiDB;
use Illuminate\Bus\Queueable;
use App\Jobs\Util\SystemLogger;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Cache;
use Illuminate\Mail\Mailables\Address;
use Illuminate\Queue\SerializesModels;
use App\Repositories\ActivityRepository;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use App\Services\EDocument\Standards\Verifactu;
use Illuminate\Queue\Middleware\WithoutOverlapping;
class SendToAeat implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
public $tries = 5;
public $deleteWhenMissingModels = true;
/**
* Modification Invoices - (modify)
* - If Amount < 0 - We generate a R2 document which is a negative modification on the original invoice.
* Create Invoices - (create) Generates a F1 document.
* Cancellation Invoices - (cancel) Generates a R3 document with full negative values of the original invoice.
*/
/**
* __construct
*
* @param int $invoice_id
* @param Company $company
* @param string $action create, modify, cancel
* @return void
*/
public function __construct(private int $invoice_id, private Company $company, private string $action)
{
}
public function backoff()
{
return [5, 30, 240, 3600, 7200];
}
public function handle(ActivityRepository $activity_repository)
{
MultiDB::setDB($this->company->db);
$invoice = Invoice::withTrashed()->find($this->invoice_id);
$invoice = $invoice->service()->markSent()->save();
switch($this->action) {
case 'create':
$this->createInvoice($invoice);
break;
case 'cancel':
$this->cancelInvoice($invoice);
break;
}
}
/**
* modifyInvoice
*
* Two code paths here:
* 1. F3 - we are replacing the invoice with a new one: ie. invoice->amount >=0
* 2. R2 - we are modifying the invoice with a negative amount: ie. invoice->amount < 0
* @param Invoice $invoice
* @return void
*/
public function createInvoice(Invoice $invoice)
{
$verifactu = new Verifactu($invoice);
$verifactu->run();
$envelope = $verifactu->getEnvelope();
$response = $verifactu->send($envelope);
nlog($response);
$message = '';
if (isset($response['errors'][0]['message'])) {
$message = $response['errors'][0]['message'];
}
if($response['success']) {
$invoice->backup->guid = $response['guid'];
$invoice->saveQuietly();
}
$this->writeActivity($invoice, $response['success'] ? Activity::VERIFACTU_INVOICE_SENT : Activity::VERIFACTU_INVOICE_SENT_FAILURE, $message);
$this->systemLog($invoice, $response, $response['success'] ? SystemLog::EVENT_VERIFACTU_SUCCESS : SystemLog::EVENT_VERIFACTU_FAILURE, SystemLog::TYPE_VERIFACTU_INVOICE);
}
public function cancelInvoice(Invoice $invoice)
{
$verifactu = new Verifactu($invoice);
$document = (new RegistroAlta($invoice))->run()->getInvoice();
$document->setNumSerieFactura($invoice->backup->parent_invoice_number);
$last_hash = $invoice->company->verifactu_logs()->first();
$huella = $this->cancellationHash($document, $last_hash->hash);
$cancellation = $document->createCancellation();
$cancellation->setHuella($huella);
$soapXml = $cancellation->toSoapEnvelope();
$response = $verifactu->setInvoice($document)
->setHuella($huella)
->setPreviousHash($last_hash->hash)
->send($soapXml);
nlog($response);
$message = '';
if($response['success']) {
//if successful, we need to pop this invoice from the child array of the parent invoice!
$parent = Invoice::withTrashed()->find($invoice->backup->parent_invoice_id);
if($parent) {
$parent->backup->child_invoice_ids = $parent->backup->child_invoice_ids->reject(fn($id) => $id === $invoice->hashed_id);
$parent->saveQuietly();
}
$invoice->backup->guid = $response['guid'];
$invoice->saveQuietly();
}
if(isset($response['errors'][0]['message'])){
$message = $response['errors'][0]['message'];
}
//@todo - verifactu logging
$this->writeActivity($invoice, $response['success'] ? Activity::VERIFACTU_CANCELLATION_SENT : Activity::VERIFACTU_CANCELLATION_SENT_FAILURE, $message);
$this->systemLog($invoice, $response, $response['success'] ? SystemLog::EVENT_VERIFACTU_SUCCESS : SystemLog::EVENT_VERIFACTU_FAILURE, SystemLog::TYPE_VERIFACTU_CANCELLATION);
}
public function middleware()
{
return [(new WithoutOverlapping("send_to_aeat_{$this->company->company_key}"))->releaseAfter(30)->expireAfter(30)];
}
public function failed($exception = null)
{
nlog($exception);
}
private function writeActivity(Invoice $invoice, int $activity_id, string $notes = ''): void
{
$activity = new Activity();
$activity->user_id = $invoice->user_id;
$activity->client_id = $invoice->client_id;
$activity->company_id = $invoice->company_id;
$activity->account_id = $invoice->company->account_id;
$activity->activity_type_id = $activity_id;
$activity->invoice_id = $invoice->id;
$activity->notes = str_replace('"', '', $notes);
$activity->is_system = true;
$activity->save();
}
private function systemLog(Invoice $invoice, array $data, int $event_id, int $type_id): void
{
(new SystemLogger(
$data,
SystemLog::CATEGORY_VERIFACTU,
$event_id,
$type_id,
$invoice->client,
$invoice->company
)
)->handle();
}
/**
* cancellationHash
*
* @param mixed $document
* @param string $huella
* @return string
*/
private function cancellationHash($document, string $huella): string
{
$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}";
return strtoupper(hash('sha256', $hashInput));
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace App\Services\EDocument\Standards\Verifactu\Signing;
use RobRichards\XMLSecLibs\XMLSecurityDSig;
use RobRichards\XMLSecLibs\XMLSecurityKey;
class SigningService
{
public function __construct(
private string $xml,
private string $private_key,
private string $certificate
) {
}
public function sign()
{
$doc = new \DOMDocument();
$doc->loadXML($this->xml);
$objDSig = new XMLSecurityDSig();
$objDSig->setCanonicalMethod(XMLSecurityDSig::EXC_C14N);
$objDSig->addReference(
$doc,
XMLSecurityDSig::SHA256,
['http://www.w3.org/2000/09/xmldsig#enveloped-signature'],
['force_uri' => true]
);
$objKey = new XMLSecurityKey(XMLSecurityKey::RSA_SHA256, ['type' => 'private']);
$objKey->loadKey($this->private_key, false);
// Attach the certificate (public) to the KeyInfo
$objDSig->add509Cert($this->certificate, true, false, ['subjectName' => true]);
$objDSig->sign($objKey);
$objDSig->appendSignature($doc->documentElement);
// --- 3. Return signed XML as string ---
return $doc->saveXML();
}
}

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- editado con XMLSpy v2019 sp1 (x64) (http://www.altova.com) por AEAT (Agencia Estatal de Administracion Tributaria ((AEAT))) -->
<!-- edited with XMLSpy v2009 sp1 (http://www.altova.com) by PC Corporativo (AGENCIA TRIBUTARIA) -->
<schema xmlns="http://www.w3.org/2001/XMLSchema" xmlns:sfLRC="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/ConsultaLR.xsd" xmlns:sf="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd" targetNamespace="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/ConsultaLR.xsd" elementFormDefault="qualified">
<import namespace="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd" schemaLocation="SuministroInformacion.xsd"/>
<!-- edited with XMLSpy v2009 sp1 (http://www.altova.com) by PC Corporativo (AGENCIA TRIBUTARIA) -->
<element name="ConsultaFactuSistemaFacturacion" type="sfLRC:ConsultaFactuSistemaFacturacionType">
<annotation>
<documentation>Servicio de consulta Registros Facturacion</documentation>
</annotation>
</element>
<complexType name="ConsultaFactuSistemaFacturacionType">
<sequence>
<element name="Cabecera" type="sf:CabeceraConsultaSf"/>
<element name="FiltroConsulta" type="sfLRC:LRFiltroRegFacturacionType"/>
</sequence>
</complexType>
<complexType name="LRFiltroRegFacturacionType">
<sequence>
<!-- <element name="PeriodoImputacion" type="sf:PeriodoImputacionType"/> -->
<element name="NumSerieFactura" type="sf:TextoIDFacturaType" minOccurs="0">
<annotation>
<documentation xml:lang="es"> Nº Serie+Nº Factura de la Factura del Emisor.</documentation>
</annotation>
</element>
<element name="Contraparte" type="sf:ContraparteConsultaType" minOccurs="0">
<annotation>
<documentation xml:lang="es">Contraparte del NIF de la cabecera que realiza la consulta.
Obligado si la cosulta la realiza el Destinatario de los registros de facturacion.
Destinatario si la cosulta la realiza el Obligado dde los registros de facturacion.</documentation>
</annotation>
</element>
<element name="FechaExpedicionFactura" type="sf:FechaExpedicionConsultaType" minOccurs="0"/>
<element name="SistemaInformatico" type="sf:SistemaInformaticoType" minOccurs="0"/>
<element name="ClavePaginacion" type="sf:IDFacturaExpedidaBCType" minOccurs="0"/>
</sequence>
</complexType>
</schema>

View File

@ -0,0 +1,823 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- editado con XMLSpy v2019 sp1 (x64) (http://www.altova.com) por AEAT (Agencia Estatal de Administracion Tributaria ((AEAT))) -->
<!-- edited with XMLSpy v2009 sp1 (http://www.altova.com) by PC Corporativo (AGENCIA TRIBUTARIA) -->
<schema xmlns="http://www.w3.org/2001/XMLSchema" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:sf="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/EventosSIF.xsd" targetNamespace="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/EventosSIF.xsd" elementFormDefault="qualified">
<import namespace="http://www.w3.org/2000/09/xmldsig#" schemaLocation="http://www.w3.org/TR/xmldsig-core/xmldsig-core-schema.xsd"/>
<element name="RegistroEvento">
<complexType>
<sequence>
<element name="IDVersion" type="sf:VersionType"/>
<element name="Evento" type="sf:EventoType"/>
</sequence>
</complexType>
</element>
<complexType name="EventoType">
<sequence>
<element name="SistemaInformatico" type="sf:SistemaInformaticoType"/>
<element name="ObligadoEmision" type="sf:PersonaFisicaJuridicaESType">
<annotation>
<documentation xml:lang="es"> Obligado a expedir la factura. </documentation>
</annotation>
</element>
<element name="EmitidaPorTerceroODestinatario" type="sf:TercerosODestinatarioType" minOccurs="0"/>
<element name="TerceroODestinatario" type="sf:PersonaFisicaJuridicaType" minOccurs="0"/>
<element name="FechaHoraHusoGenEvento" type="dateTime">
<annotation>
<documentation xml:lang="es">Formato: YYYY-MM-DDThh:mm:ssTZD (ej: 2024-01-01T19:20:30+01:00) (ISO 8601)</documentation>
</annotation>
</element>
<element name="TipoEvento" type="sf:TipoEventoType"/>
<element name="DatosPropiosEvento" type="sf:DatosPropiosEventoType" minOccurs="0"/>
<element name="OtrosDatosEvento" type="sf:TextMax100Type" minOccurs="0"/>
<element name="Encadenamiento" type="sf:EncadenamientoType"/>
<element name="TipoHuella" type="sf:TipoHuellaType"/>
<element name="HuellaEvento" type="sf:TextMax64Type"/>
<element ref="ds:Signature"/>
</sequence>
</complexType>
<complexType name="SistemaInformaticoType">
<sequence>
<sequence>
<element name="NombreRazon" type="sf:TextMax120Type"/>
<choice>
<element name="NIF" type="sf:NIFType"/>
<element name="IDOtro" type="sf:IDOtroType"/>
</choice>
</sequence>
<element name="NombreSistemaInformatico" type="sf:TextMax30Type" minOccurs="0"/>
<element name="IdSistemaInformatico" type="sf:TextMax2Type"/>
<element name="Version" type="sf:TextMax50Type"/>
<element name="NumeroInstalacion" type="sf:TextMax100Type"/>
<element name="TipoUsoPosibleSoloVerifactu" type="sf:SiNoType" minOccurs="0"/>
<element name="TipoUsoPosibleMultiOT" type="sf:SiNoType" minOccurs="0"/>
<element name="IndicadorMultiplesOT" type="sf:SiNoType" minOccurs="0"/>
</sequence>
</complexType>
<complexType name="DatosPropiosEventoType">
<choice>
<element name="LanzamientoProcesoDeteccionAnomaliasRegFacturacion" type="sf:LanzamientoProcesoDeteccionAnomaliasRegFacturacionType"/>
<element name="DeteccionAnomaliasRegFacturacion" type="sf:DeteccionAnomaliasRegFacturacionType"/>
<element name="LanzamientoProcesoDeteccionAnomaliasRegEvento" type="sf:LanzamientoProcesoDeteccionAnomaliasRegEventoType"/>
<element name="DeteccionAnomaliasRegEvento" type="sf:DeteccionAnomaliasRegEventoType"/>
<element name="ExportacionRegFacturacionPeriodo" type="sf:ExportacionRegFacturacionPeriodoType"/>
<element name="ExportacionRegEventoPeriodo" type="sf:ExportacionRegEventoPeriodoType"/>
<element name="ResumenEventos" type="sf:ResumenEventosType"/>
</choice>
</complexType>
<complexType name="EncadenamientoType">
<choice>
<element name="PrimerEvento" type="sf:TextMax1Type"/>
<element name="EventoAnterior" type="sf:RegEventoAntType"/>
</choice>
</complexType>
<complexType name="LanzamientoProcesoDeteccionAnomaliasRegFacturacionType">
<sequence>
<element name="RealizadoProcesoSobreIntegridadHuellasRegFacturacion" type="sf:SiNoType"/>
<element name="NumeroDeRegistrosFacturacionProcesadosSobreIntegridadHuellas" type="sf:DigitosMax7Type" minOccurs="0"/>
<element name="RealizadoProcesoSobreIntegridadFirmasRegFacturacion" type="sf:SiNoType"/>
<element name="NumeroDeRegistrosFacturacionProcesadosSobreIntegridadFirmas" type="sf:DigitosMax7Type" minOccurs="0"/>
<element name="RealizadoProcesoSobreTrazabilidadCadenaRegFacturacion" type="sf:SiNoType"/>
<element name="NumeroDeRegistrosFacturacionProcesadosSobreTrazabilidadCadena" type="sf:DigitosMax7Type" minOccurs="0"/>
<element name="RealizadoProcesoSobreTrazabilidadFechasRegFacturacion" type="sf:SiNoType"/>
<element name="NumeroDeRegistrosFacturacionProcesadosSobreTrazabilidadFechas" type="sf:DigitosMax7Type" minOccurs="0"/>
</sequence>
</complexType>
<complexType name="DeteccionAnomaliasRegFacturacionType">
<sequence>
<element name="TipoAnomalia" type="sf:TipoAnomaliaType"/>
<element name="OtrosDatosAnomalia" type="sf:TextMax100Type" minOccurs="0"/>
<element name="RegistroFacturacionAnomalo" type="sf:IDFacturaExpedidaType" minOccurs="0"/>
</sequence>
</complexType>
<complexType name="LanzamientoProcesoDeteccionAnomaliasRegEventoType">
<sequence>
<element name="RealizadoProcesoSobreIntegridadHuellasRegEvento" type="sf:SiNoType"/>
<element name="NumeroDeRegistrosEventoProcesadosSobreIntegridadHuellas" type="sf:DigitosMax5Type" minOccurs="0"/>
<element name="RealizadoProcesoSobreIntegridadFirmasRegEvento" type="sf:SiNoType"/>
<element name="NumeroDeRegistrosEventoProcesadosSobreIntegridadFirmas" type="sf:DigitosMax5Type" minOccurs="0"/>
<element name="RealizadoProcesoSobreTrazabilidadCadenaRegEvento" type="sf:SiNoType"/>
<element name="NumeroDeRegistrosEventoProcesadosSobreTrazabilidadCadena" type="sf:DigitosMax5Type" minOccurs="0"/>
<element name="RealizadoProcesoSobreTrazabilidadFechasRegEvento" type="sf:SiNoType"/>
<element name="NumeroDeRegistrosEventoProcesadosSobreTrazabilidadFechas" type="sf:DigitosMax5Type" minOccurs="0"/>
</sequence>
</complexType>
<complexType name="DeteccionAnomaliasRegEventoType">
<sequence>
<element name="TipoAnomalia" type="sf:TipoAnomaliaType"/>
<element name="OtrosDatosAnomalia" type="sf:TextMax100Type" minOccurs="0"/>
<element name="RegEventoAnomalo" type="sf:RegEventoType" minOccurs="0"/>
</sequence>
</complexType>
<complexType name="ExportacionRegFacturacionPeriodoType">
<sequence>
<element name="FechaHoraHusoInicioPeriodoExport" type="dateTime">
<annotation>
<documentation xml:lang="es">Formato: YYYY-MM-DDThh:mm:ssTZD (ej: 2024-01-01T19:20:30+01:00) (ISO 8601)</documentation>
</annotation>
</element>
<element name="FechaHoraHusoFinPeriodoExport" type="dateTime">
<annotation>
<documentation xml:lang="es">Formato: YYYY-MM-DDThh:mm:ssTZD (ej: 2024-01-01T19:20:30+01:00) (ISO 8601)</documentation>
</annotation>
</element>
<element name="RegistroFacturacionInicialPeriodo" type="sf:IDFacturaExpedidaHuellaType"/>
<element name="RegistroFacturacionFinalPeriodo" type="sf:IDFacturaExpedidaHuellaType"/>
<element name="NumeroDeRegistrosFacturacionAltaExportados" type="sf:DigitosMax9Type"/>
<element name="SumaCuotaTotalAlta" type="sf:ImporteSgn12.2Type"/>
<element name="SumaImporteTotalAlta" type="sf:ImporteSgn12.2Type"/>
<element name="NumeroDeRegistrosFacturacionAnulacionExportados" type="sf:DigitosMax9Type"/>
<element name="RegistrosFacturacionExportadosDejanDeConservarse" type="sf:SiNoType"/>
</sequence>
</complexType>
<complexType name="ExportacionRegEventoPeriodoType">
<sequence>
<element name="FechaHoraHusoInicioPeriodoExport" type="dateTime">
<annotation>
<documentation xml:lang="es">Formato: YYYY-MM-DDThh:mm:ssTZD (ej: 2024-01-01T19:20:30+01:00) (ISO 8601)</documentation>
</annotation>
</element>
<element name="FechaHoraHusoFinPeriodoExport" type="dateTime">
<annotation>
<documentation xml:lang="es">Formato: YYYY-MM-DDThh:mm:ssTZD (ej: 2024-01-01T19:20:30+01:00) (ISO 8601)</documentation>
</annotation>
</element>
<element name="RegistroEventoInicialPeriodo" type="sf:RegEventoType"/>
<element name="RegistroEventoFinalPeriodo" type="sf:RegEventoType"/>
<element name="NumeroDeRegEventoExportados" type="sf:DigitosMax7Type"/>
<element name="RegEventoExportadosDejanDeConservarse" type="sf:SiNoType"/>
</sequence>
</complexType>
<complexType name="ResumenEventosType">
<sequence>
<element name="TipoEvento" type="sf:TipoEventoAgrType" maxOccurs="20"/>
<element name="RegistroFacturacionInicialPeriodo" type="sf:IDFacturaExpedidaHuellaType" minOccurs="0"/>
<element name="RegistroFacturacionFinalPeriodo" type="sf:IDFacturaExpedidaHuellaType" minOccurs="0"/>
<element name="NumeroDeRegistrosFacturacionAltaGenerados" type="sf:DigitosMax6Type"/>
<element name="SumaCuotaTotalAlta" type="sf:ImporteSgn12.2Type"/>
<element name="SumaImporteTotalAlta" type="sf:ImporteSgn12.2Type"/>
<element name="NumeroDeRegistrosFacturacionAnulacionGenerados" type="sf:DigitosMax6Type"/>
</sequence>
</complexType>
<complexType name="RegEventoType">
<sequence>
<element name="TipoEvento" type="sf:TipoEventoType"/>
<element name="FechaHoraHusoEvento" type="dateTime">
<annotation>
<documentation xml:lang="es">Formato: YYYY-MM-DDThh:mm:ssTZD (ej: 2024-01-01T19:20:30+01:00) (ISO 8601)</documentation>
</annotation>
</element>
<element name="HuellaEvento" type="sf:TextMax64Type"/>
</sequence>
</complexType>
<complexType name="RegEventoAntType">
<sequence>
<element name="TipoEvento" type="sf:TipoEventoType"/>
<element name="FechaHoraHusoGenEvento" type="dateTime">
<annotation>
<documentation xml:lang="es">Formato: YYYY-MM-DDThh:mm:ssTZD (ej: 2024-01-01T19:20:30+01:00) (ISO 8601)</documentation>
</annotation>
</element>
<element name="HuellaEvento" type="sf:TextMax64Type"/>
</sequence>
</complexType>
<complexType name="TipoEventoAgrType">
<sequence>
<element name="TipoEvento" type="sf:TipoEventoType"/>
<element name="NumeroDeEventos" type="sf:DigitosMax4Type"/>
</sequence>
</complexType>
<!-- Datos de persona Física o jurídica : Denominación, representación, identificación (NIF) -->
<complexType name="PersonaFisicaJuridicaESType">
<annotation>
<documentation xml:lang="es">Datos de una persona física o jurídica Española con un NIF asociado</documentation>
</annotation>
<sequence>
<element name="NombreRazon" type="sf:TextMax120Type"/>
<element name="NIF" type="sf:NIFType"/>
</sequence>
</complexType>
<simpleType name="NIFType">
<annotation>
<documentation xml:lang="es">NIF</documentation>
</annotation>
<restriction base="string">
<length value="9"/>
</restriction>
</simpleType>
<!-- Datos de persona Física o jurídica : Denominación, representación, identificación (NIF/Otro) -->
<complexType name="PersonaFisicaJuridicaType">
<annotation>
<documentation xml:lang="es">Datos de una persona física o jurídica Española o Extranjera</documentation>
</annotation>
<sequence>
<element name="NombreRazon" type="sf:TextMax120Type"/>
<choice>
<element name="NIF" type="sf:NIFType"/>
<element name="IDOtro" type="sf:IDOtroType"/>
</choice>
</sequence>
</complexType>
<!-- Datos de persona Física o jurídica : Denominación, representación, identificación (NIF/Otro) -->
<complexType name="IDOtroType">
<annotation>
<documentation xml:lang="es">Identificador de persona Física o jurídica distinto del NIF
(Código pais, Tipo de Identificador, y hasta 15 caractéres)
No se permite CodigoPais=ES e IDType=01-NIFContraparte
para ese caso, debe utilizarse NIF en lugar de IDOtro.
</documentation>
</annotation>
<sequence>
<element name="CodigoPais" type="sf:CountryType2" minOccurs="0"/>
<element name="IDType" type="sf:PersonaFisicaJuridicaIDTypeType"/>
<element name="ID" type="sf:TextMax20Type"/>
</sequence>
</complexType>
<!-- Tercero o Destinatario -->
<simpleType name="TercerosODestinatarioType">
<restriction base="string">
<enumeration value="D">
<annotation>
<documentation xml:lang="es">Destinatario</documentation>
</annotation>
</enumeration>
<enumeration value="T">
<annotation>
<documentation xml:lang="es">Tercero</documentation>
</annotation>
</enumeration>
</restriction>
</simpleType>
<simpleType name="SiNoType">
<restriction base="string">
<enumeration value="S"/>
<enumeration value="N"/>
</restriction>
</simpleType>
<simpleType name="VersionType">
<restriction base="string">
<enumeration value="1.0"/>
</restriction>
</simpleType>
<!-- Cadena de 120 caracteres -->
<simpleType name="TextMax120Type">
<restriction base="string">
<maxLength value="120"/>
</restriction>
</simpleType>
<!-- Cadena de 100 caracteres -->
<simpleType name="TextMax100Type">
<restriction base="string">
<maxLength value="100"/>
</restriction>
</simpleType>
<!-- Cadena de 64 caracteres -->
<simpleType name="TextMax64Type">
<restriction base="string">
<maxLength value="64"/>
</restriction>
</simpleType>
<!-- Cadena de 60 caracteres -->
<simpleType name="TextMax60Type">
<restriction base="string">
<maxLength value="60"/>
</restriction>
</simpleType>
<!-- Cadena de 50 caracteres -->
<simpleType name="TextMax50Type">
<restriction base="string">
<maxLength value="50"/>
</restriction>
</simpleType>
<!-- Cadena de 30 caracteres -->
<simpleType name="TextMax30Type">
<restriction base="string">
<maxLength value="30"/>
</restriction>
</simpleType>
<!-- Cadena de 20 caracteres -->
<simpleType name="TextMax20Type">
<restriction base="string">
<maxLength value="20"/>
</restriction>
</simpleType>
<!-- Cadena de 2 caracteres -->
<simpleType name="TextMax2Type">
<restriction base="string">
<maxLength value="2"/>
</restriction>
</simpleType>
<!-- Cadena de 1 caracteres -->
<simpleType name="TextMax1Type">
<restriction base="string">
<maxLength value="1"/>
</restriction>
</simpleType>
<!-- Definición de un tipo simple restringido a 9 dígitos -->
<simpleType name="DigitosMax9Type">
<restriction base="string">
<maxLength value="9"/>
<pattern value="\d{1,9}"/>
</restriction>
</simpleType>
<!-- Definición de un tipo simple restringido a 7 dígitos -->
<simpleType name="DigitosMax7Type">
<restriction base="string">
<maxLength value="7"/>
<pattern value="\d{1,7}"/>
</restriction>
</simpleType>
<!-- Definición de un tipo simple restringido a 6 dígitos -->
<simpleType name="DigitosMax6Type">
<restriction base="string">
<maxLength value="6"/>
<pattern value="\d{1,6}"/>
</restriction>
</simpleType>
<!-- Definición de un tipo simple restringido a 5 dígitos -->
<simpleType name="DigitosMax5Type">
<restriction base="string">
<maxLength value="5"/>
<pattern value="\d{1,5}"/>
</restriction>
</simpleType>
<!-- Definición de un tipo simple restringido a 4 dígitos -->
<simpleType name="DigitosMax4Type">
<restriction base="string">
<maxLength value="4"/>
<pattern value="\d{1,4}"/>
</restriction>
</simpleType>
<!-- Fecha (dd-mm-yyyy) -->
<simpleType name="fecha">
<restriction base="string">
<length value="10"/>
<pattern value="\d{2,2}-\d{2,2}-\d{4,4}"/>
</restriction>
</simpleType>
<!-- Importe de 15 dígitos (12+2) "." como separador decimal -->
<simpleType name="ImporteSgn12.2Type">
<restriction base="string">
<pattern value="(\+|-)?\d{1,12}(\.\d{0,2})?"/>
</restriction>
</simpleType>
<!-- Tipo de identificador fiscal de persona Física o jurídica -->
<simpleType name="PersonaFisicaJuridicaIDTypeType">
<restriction base="string">
<enumeration value="02">
<annotation>
<documentation xml:lang="es">NIF-IVA</documentation>
</annotation>
</enumeration>
<enumeration value="03">
<annotation>
<documentation xml:lang="es">Pasaporte</documentation>
</annotation>
</enumeration>
<enumeration value="04">
<annotation>
<documentation xml:lang="es">IDEnPaisResidencia</documentation>
</annotation>
</enumeration>
<enumeration value="05">
<annotation>
<documentation xml:lang="es">Certificado Residencia</documentation>
</annotation>
</enumeration>
<enumeration value="06">
<annotation>
<documentation xml:lang="es">Otro documento Probatorio</documentation>
</annotation>
</enumeration>
<enumeration value="07">
<annotation>
<documentation xml:lang="es">No Censado</documentation>
</annotation>
</enumeration>
</restriction>
</simpleType>
<!-- Tipo Hash -->
<simpleType name="TipoHuellaType">
<restriction base="string">
<enumeration value="01">
<annotation>
<documentation xml:lang="es">SHA-256</documentation>
</annotation>
</enumeration>
</restriction>
</simpleType>
<simpleType name="TipoEventoType">
<restriction base="string">
<enumeration value="01">
<annotation>
<documentation xml:lang="es">Inicio del funcionamiento del sistema informático como «NO VERI*FACTU».</documentation>
</annotation>
</enumeration>
<enumeration value="02">
<annotation>
<documentation xml:lang="es">Fin del funcionamiento del sistema informático como «NO VERI*FACTU».</documentation>
</annotation>
</enumeration>
<enumeration value="03">
<annotation>
<documentation xml:lang="es">Lanzamiento del proceso de detección de anomalías en los registros de facturación.</documentation>
</annotation>
</enumeration>
<enumeration value="04">
<annotation>
<documentation xml:lang="es">Detección de anomalías en la integridad, inalterabilidad y trazabilidad de registros de facturación.</documentation>
</annotation>
</enumeration>
<enumeration value="05">
<annotation>
<documentation xml:lang="es">Lanzamiento del proceso de detección de anomalías en los registros de evento.</documentation>
</annotation>
</enumeration>
<enumeration value="06">
<annotation>
<documentation xml:lang="es">Detección de anomalías en la integridad, inalterabilidad y trazabilidad de registros de evento.</documentation>
</annotation>
</enumeration>
<enumeration value="07">
<annotation>
<documentation xml:lang="es">Restauración de copia de seguridad, cuando ésta se gestione desde el propio sistema informático de facturación.</documentation>
</annotation>
</enumeration>
<enumeration value="08">
<annotation>
<documentation xml:lang="es">Exportación de registros de facturación generados en un periodo.</documentation>
</annotation>
</enumeration>
<enumeration value="09">
<annotation>
<documentation xml:lang="es">Exportación de registros de evento generados en un periodo.</documentation>
</annotation>
</enumeration>
<enumeration value="10">
<annotation>
<documentation xml:lang="es">Registro resumen de eventos</documentation>
</annotation>
</enumeration>
<enumeration value="90">
<annotation>
<documentation xml:lang="es">Otros tipos de eventos a registrar voluntariamente por la persona o entidad productora del sistema informático.
</documentation>
</annotation>
</enumeration>
</restriction>
</simpleType>
<simpleType name="TipoAnomaliaType">
<restriction base="string">
<enumeration value="01">
<annotation>
<documentation xml:lang="es">Integridad-huella</documentation>
</annotation>
</enumeration>
<enumeration value="02">
<annotation>
<documentation xml:lang="es">Integridad-firma</documentation>
</annotation>
</enumeration>
<enumeration value="03">
<annotation>
<documentation xml:lang="es">Integridad - Otros</documentation>
</annotation>
</enumeration>
<enumeration value="04">
<annotation>
<documentation xml:lang="es">Trazabilidad-cadena-registro - Reg. no primero pero con reg. anterior no anotado o inexistente</documentation>
</annotation>
</enumeration>
<enumeration value="05">
<annotation>
<documentation xml:lang="es">Trazabilidad-cadena-registro - Reg. no último pero con reg. posterior no anotado o inexistente</documentation>
</annotation>
</enumeration>
<enumeration value="06">
<annotation>
<documentation xml:lang="es">Trazabilidad-cadena-registro - Otros</documentation>
</annotation>
</enumeration>
<enumeration value="07">
<annotation>
<documentation xml:lang="es">Trazabilidad-cadena-huella - Huella del reg. no se corresponde con la 'huella del reg. anterior' almacenada en el registro posterior</documentation>
</annotation>
</enumeration>
<enumeration value="08">
<annotation>
<documentation xml:lang="es">Trazabilidad-cadena-huella - Campo 'huella del reg. anterior' no se corresponde con la huella del reg. anterior</documentation>
</annotation>
</enumeration>
<enumeration value="09">
<annotation>
<documentation xml:lang="es">Trazabilidad-cadena-huella - Otros</documentation>
</annotation>
</enumeration>
<enumeration value="10">
<annotation>
<documentation xml:lang="es">Trazabilidad-cadena - Otros</documentation>
</annotation>
</enumeration>
<enumeration value="11">
<annotation>
<documentation xml:lang="es">Trazabilidad-fechas - Fecha-hora anterior a la fecha del reg. anterior</documentation>
</annotation>
</enumeration>
<enumeration value="12">
<annotation>
<documentation xml:lang="es">Trazabilidad-fechas - Fecha-hora posterior a la fecha del reg. posterior</documentation>
</annotation>
</enumeration>
<enumeration value="13">
<annotation>
<documentation xml:lang="es">Trazabilidad-fechas - Reg. con fecha-hora de generación posterior a la fecha-hora actual del sistema</documentation>
</annotation>
</enumeration>
<enumeration value="14">
<annotation>
<documentation xml:lang="es">Trazabilidad-fechas - Otros</documentation>
</annotation>
</enumeration>
<enumeration value="15">
<annotation>
<documentation xml:lang="es">Trazabilidad - Otros</documentation>
</annotation>
</enumeration>
<enumeration value="90">
<annotation>
<documentation xml:lang="es">Otros</documentation>
</annotation>
</enumeration>
</restriction>
</simpleType>
<complexType name="IDFacturaExpedidaType">
<annotation>
<documentation xml:lang="es"> Datos de identificación de factura expedida para operaciones de consulta</documentation>
</annotation>
<sequence>
<element name="IDEmisorFactura" type="sf:NIFType"/>
<element name="NumSerieFactura" type="sf:TextMax60Type"/>
<element name="FechaExpedicionFactura" type="sf:fecha"/>
</sequence>
</complexType>
<complexType name="IDFacturaExpedidaHuellaType">
<annotation>
<documentation xml:lang="es">Datos de encadenamiento </documentation>
</annotation>
<sequence>
<element name="IDEmisorFactura" type="sf:NIFType"/>
<element name="NumSerieFactura" type="sf:TextMax60Type"/>
<element name="FechaExpedicionFactura" type="sf:fecha"/>
<element name="Huella" type="sf:TextMax64Type"/>
</sequence>
</complexType>
<!-- ISO 3166-1 alpha-2 codes -->
<simpleType name="CountryType2">
<restriction base="string">
<enumeration value="AF"/>
<enumeration value="AL"/>
<enumeration value="DE"/>
<enumeration value="AD"/>
<enumeration value="AO"/>
<enumeration value="AI"/>
<enumeration value="AQ"/>
<enumeration value="AG"/>
<enumeration value="SA"/>
<enumeration value="DZ"/>
<enumeration value="AR"/>
<enumeration value="AM"/>
<enumeration value="AW"/>
<enumeration value="AU"/>
<enumeration value="AT"/>
<enumeration value="AZ"/>
<enumeration value="BS"/>
<enumeration value="BH"/>
<enumeration value="BD"/>
<enumeration value="BB"/>
<enumeration value="BE"/>
<enumeration value="BZ"/>
<enumeration value="BJ"/>
<enumeration value="BM"/>
<enumeration value="BY"/>
<enumeration value="BO"/>
<enumeration value="BA"/>
<enumeration value="BW"/>
<enumeration value="BV"/>
<enumeration value="BR"/>
<enumeration value="BN"/>
<enumeration value="BG"/>
<enumeration value="BF"/>
<enumeration value="BI"/>
<enumeration value="BT"/>
<enumeration value="CV"/>
<enumeration value="KY"/>
<enumeration value="KH"/>
<enumeration value="CM"/>
<enumeration value="CA"/>
<enumeration value="CF"/>
<enumeration value="CC"/>
<enumeration value="CO"/>
<enumeration value="KM"/>
<enumeration value="CG"/>
<enumeration value="CD"/>
<enumeration value="CK"/>
<enumeration value="KP"/>
<enumeration value="KR"/>
<enumeration value="CI"/>
<enumeration value="CR"/>
<enumeration value="HR"/>
<enumeration value="CU"/>
<enumeration value="TD"/>
<enumeration value="CZ"/>
<enumeration value="CL"/>
<enumeration value="CN"/>
<enumeration value="CY"/>
<enumeration value="CW"/>
<enumeration value="DK"/>
<enumeration value="DM"/>
<enumeration value="DO"/>
<enumeration value="EC"/>
<enumeration value="EG"/>
<enumeration value="AE"/>
<enumeration value="ER"/>
<enumeration value="SK"/>
<enumeration value="SI"/>
<enumeration value="ES"/>
<enumeration value="US"/>
<enumeration value="EE"/>
<enumeration value="ET"/>
<enumeration value="FO"/>
<enumeration value="PH"/>
<enumeration value="FI"/>
<enumeration value="FJ"/>
<enumeration value="FR"/>
<enumeration value="GA"/>
<enumeration value="GM"/>
<enumeration value="GE"/>
<enumeration value="GS"/>
<enumeration value="GH"/>
<enumeration value="GI"/>
<enumeration value="GD"/>
<enumeration value="GR"/>
<enumeration value="GL"/>
<enumeration value="GU"/>
<enumeration value="GT"/>
<enumeration value="GG"/>
<enumeration value="GN"/>
<enumeration value="GQ"/>
<enumeration value="GW"/>
<enumeration value="GY"/>
<enumeration value="HT"/>
<enumeration value="HM"/>
<enumeration value="HN"/>
<enumeration value="HK"/>
<enumeration value="HU"/>
<enumeration value="IN"/>
<enumeration value="ID"/>
<enumeration value="IR"/>
<enumeration value="IQ"/>
<enumeration value="IE"/>
<enumeration value="IM"/>
<enumeration value="IS"/>
<enumeration value="IL"/>
<enumeration value="IT"/>
<enumeration value="JM"/>
<enumeration value="JP"/>
<enumeration value="JE"/>
<enumeration value="JO"/>
<enumeration value="KZ"/>
<enumeration value="KE"/>
<enumeration value="KG"/>
<enumeration value="KI"/>
<enumeration value="KW"/>
<enumeration value="LA"/>
<enumeration value="LS"/>
<enumeration value="LV"/>
<enumeration value="LB"/>
<enumeration value="LR"/>
<enumeration value="LY"/>
<enumeration value="LI"/>
<enumeration value="LT"/>
<enumeration value="LU"/>
<enumeration value="XG"/>
<enumeration value="MO"/>
<enumeration value="MK"/>
<enumeration value="MG"/>
<enumeration value="MY"/>
<enumeration value="MW"/>
<enumeration value="MV"/>
<enumeration value="ML"/>
<enumeration value="MT"/>
<enumeration value="FK"/>
<enumeration value="MP"/>
<enumeration value="MA"/>
<enumeration value="MH"/>
<enumeration value="MU"/>
<enumeration value="MR"/>
<enumeration value="YT"/>
<enumeration value="UM"/>
<enumeration value="MX"/>
<enumeration value="FM"/>
<enumeration value="MD"/>
<enumeration value="MC"/>
<enumeration value="MN"/>
<enumeration value="ME"/>
<enumeration value="MS"/>
<enumeration value="MZ"/>
<enumeration value="MM"/>
<enumeration value="NA"/>
<enumeration value="NR"/>
<enumeration value="CX"/>
<enumeration value="NP"/>
<enumeration value="NI"/>
<enumeration value="NE"/>
<enumeration value="NG"/>
<enumeration value="NU"/>
<enumeration value="NF"/>
<enumeration value="NO"/>
<enumeration value="NC"/>
<enumeration value="NZ"/>
<enumeration value="IO"/>
<enumeration value="OM"/>
<enumeration value="NL"/>
<enumeration value="BQ"/>
<enumeration value="PK"/>
<enumeration value="PW"/>
<enumeration value="PA"/>
<enumeration value="PG"/>
<enumeration value="PY"/>
<enumeration value="PE"/>
<enumeration value="PN"/>
<enumeration value="PF"/>
<enumeration value="PL"/>
<enumeration value="PT"/>
<enumeration value="PR"/>
<enumeration value="QA"/>
<enumeration value="GB"/>
<enumeration value="RW"/>
<enumeration value="RO"/>
<enumeration value="RU"/>
<enumeration value="SB"/>
<enumeration value="SV"/>
<enumeration value="WS"/>
<enumeration value="AS"/>
<enumeration value="KN"/>
<enumeration value="SM"/>
<enumeration value="SX"/>
<enumeration value="PM"/>
<enumeration value="VC"/>
<enumeration value="SH"/>
<enumeration value="LC"/>
<enumeration value="ST"/>
<enumeration value="SN"/>
<enumeration value="RS"/>
<enumeration value="SC"/>
<enumeration value="SL"/>
<enumeration value="SG"/>
<enumeration value="SY"/>
<enumeration value="SO"/>
<enumeration value="LK"/>
<enumeration value="SZ"/>
<enumeration value="ZA"/>
<enumeration value="SD"/>
<enumeration value="SS"/>
<enumeration value="SE"/>
<enumeration value="CH"/>
<enumeration value="SR"/>
<enumeration value="TH"/>
<enumeration value="TW"/>
<enumeration value="TZ"/>
<enumeration value="TJ"/>
<enumeration value="PS"/>
<enumeration value="TF"/>
<enumeration value="TL"/>
<enumeration value="TG"/>
<enumeration value="TK"/>
<enumeration value="TO"/>
<enumeration value="TT"/>
<enumeration value="TN"/>
<enumeration value="TC"/>
<enumeration value="TM"/>
<enumeration value="TR"/>
<enumeration value="TV"/>
<enumeration value="UA"/>
<enumeration value="UG"/>
<enumeration value="UY"/>
<enumeration value="UZ"/>
<enumeration value="VU"/>
<enumeration value="VA"/>
<enumeration value="VE"/>
<enumeration value="VN"/>
<enumeration value="VG"/>
<enumeration value="VI"/>
<enumeration value="WF"/>
<enumeration value="YE"/>
<enumeration value="DJ"/>
<enumeration value="ZM"/>
<enumeration value="ZW"/>
<enumeration value="QU"/>
<enumeration value="XB"/>
<enumeration value="XU"/>
<enumeration value="XN"/>
</restriction>
</simpleType>
</schema>

View File

@ -0,0 +1,195 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- editado con XMLSpy v2019 sp1 (x64) (http://www.altova.com) por AEAT (Agencia Estatal de Administracion Tributaria ((AEAT))) -->
<!-- edited with XMLSpy v2009 sp1 (http://www.altova.com) by PC Corporativo (AGENCIA TRIBUTARIA) -->
<schema xmlns="http://www.w3.org/2001/XMLSchema" xmlns:sfLRRC="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/RespuestaConsultaLR.xsd" xmlns:sf="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd" targetNamespace="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/RespuestaConsultaLR.xsd" elementFormDefault="qualified">
<import namespace="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd" schemaLocation="SuministroInformacion.xsd"/>
<!-- edited with XMLSpy v2009 sp1 (http://www.altova.com) by PC Corporativo (AGENCIA TRIBUTARIA) -->
<element name="RespuestaConsultaFactuSistemaFacturacion" type="sfLRRC:RespuestaConsultaFactuSistemaFacturacionType">
<annotation>
<documentation>Servicio de consulta de regIstros de facturacion</documentation>
</annotation>
</element>
<complexType name="RespuestaConsultaFactuSistemaFacturacionType">
<complexContent>
<extension base="sfLRRC:RespuestaConsultaType">
<sequence>
<element name="RegistroRespuestaConsultaFactuSistemaFacturacion" type="sfLRRC:RegistroRespuestaConsultaRegFacturacionType" minOccurs="0" maxOccurs="10000"/>
<element name="ClavePaginacion" type="sf:IDFacturaExpedidaBCType" minOccurs="0"/>
</sequence>
</extension>
</complexContent>
</complexType>
<complexType name="EstadoRegFactuType">
<sequence>
<element name="TimestampUltimaModificacion" type="dateTime"/>
<element name="EstadoRegistro" type="sfLRRC:EstadoRegistroType">
<annotation>
<documentation xml:lang="es">
Estado del registro almacenado en el sistema. Los estados posibles son: Correcta, AceptadaConErrores y Anulada
</documentation>
</annotation>
</element>
<element name="CodigoErrorRegistro" type="sfLRRC:ErrorDetalleType" minOccurs="0">
<annotation>
<documentation xml:lang="es">
Código del error de registro, en su caso.
</documentation>
</annotation>
</element>
<element name="DescripcionErrorRegistro" type="sf:TextMax500Type" minOccurs="0">
<annotation>
<documentation xml:lang="es">
Descripción detallada del error de registro, en su caso.
</documentation>
</annotation>
</element>
</sequence>
</complexType>
<complexType name="RegistroRespuestaConsultaRegFacturacionType">
<sequence>
<element name="IDFactura" type="sf:IDFacturaExpedidaType"/>
<element name="DatosRegistroFacturacion" type="sfLRRC:RespuestaDatosRegistroFacturacionType"/>
<element name="DatosPresentacion" type="sf:DatosPresentacion2Type" minOccurs="0"/>
<element name="EstadoRegistro" type="sfLRRC:EstadoRegFactuType" />
</sequence>
</complexType>
<complexType name="RespuestaConsultaType">
<sequence>
<element name="Cabecera" type="sf:CabeceraConsultaSf"/>
<element name="PeriodoImputacion">
<complexType>
<annotation>
<documentation xml:lang="es"> Período al que corresponden los apuntes. todos los apuntes deben corresponder al mismo período impositivo </documentation>
</annotation>
<sequence>
<element name="Ejercicio" type="sf:YearType"/>
<element name="Periodo" type="sf:TipoPeriodoType"/>
</sequence>
</complexType>
</element>
<element name="IndicadorPaginacion" type="sfLRRC:IndicadorPaginacionType"/>
<element name="ResultadoConsulta" type="sfLRRC:ResultadoConsultaType"/>
</sequence>
</complexType>
<!-- Datos del registro de facturacion -->
<complexType name="RespuestaDatosRegistroFacturacionType">
<annotation>
<documentation xml:lang="es"> Apunte correspondiente al libro de facturas expedidas. </documentation>
</annotation>
<sequence>
<element name="RefExterna" type="sf:TextMax70Type" minOccurs="0"/>
<element name="Subsanacion" type="sf:SubsanacionType" minOccurs="0"/>
<element name="RechazoPrevio" type="sf:RechazoPrevioType" minOccurs="0"/>
<element name="SinRegistroPrevio" type="sf:SinRegistroPrevioType" minOccurs="0"/>
<element name="GeneradoPor" type="sf:GeneradoPorType" minOccurs="0"/>
<element name="Generador" type="sf:PersonaFisicaJuridicaType" minOccurs="0"/>
<element name="TipoFactura" type="sf:ClaveTipoFacturaType" minOccurs="0">
<annotation>
<documentation xml:lang="es"> Clave del tipo de factura </documentation>
</annotation>
</element>
<element name="TipoRectificativa" type="sf:ClaveTipoRectificativaType" minOccurs="0">
<annotation>
<documentation xml:lang="es"> Identifica si el tipo de factura rectificativa es por sustitución o por diferencia </documentation>
</annotation>
</element>
<element name="FacturasRectificadas" minOccurs="0">
<complexType>
<annotation>
<documentation xml:lang="es">El ID de las facturas rectificadas, únicamente se rellena en el caso de rectificación de facturas</documentation>
</annotation>
<sequence>
<element name="IDFacturaRectificada" type="sf:IDFacturaARType" maxOccurs="1000"/>
</sequence>
</complexType>
</element>
<element name="FacturasSustituidas" minOccurs="0">
<complexType>
<annotation>
<documentation xml:lang="es">El ID de las facturas sustituidas, únicamente se rellena en el caso de facturas sustituidas</documentation>
</annotation>
<sequence>
<element name="IDFacturaSustituida" type="sf:IDFacturaARType" maxOccurs="1000"/>
</sequence>
</complexType>
</element>
<element name="ImporteRectificacion" type="sf:DesgloseRectificacionType" minOccurs="0"/>
<element name="FechaOperacion" type="sf:fecha" minOccurs="0"/>
<element name="DescripcionOperacion" type="sf:TextMax500Type" minOccurs="0"/>
<element name="FacturaSimplificadaArt7273" type="sf:SimplificadaCualificadaType" minOccurs="0"/>
<element name="FacturaSinIdentifDestinatarioArt61d" type="sf:CompletaSinDestinatarioType" minOccurs="0"/>
<element name="Macrodato" type="sf:MacrodatoType" minOccurs="0"/>
<element name="EmitidaPorTerceroODestinatario" type="sf:TercerosODestinatarioType" minOccurs="0"/>
<element name="Tercero" type="sf:PersonaFisicaJuridicaType" minOccurs="0">
<annotation>
<documentation xml:lang="es"> Tercero que expida la factura y/o genera el registro de alta. </documentation>
</annotation>
</element>
<element name="Destinatarios" minOccurs="0">
<complexType>
<annotation>
<documentation xml:lang="es">Contraparte de la operación. Cliente</documentation>
</annotation>
<sequence>
<element name="IDDestinatario" type="sf:PersonaFisicaJuridicaType" maxOccurs="1000"/>
</sequence>
</complexType>
</element>
<element name="Cupon" type="sf:CuponType" minOccurs="0"/>
<element name="Desglose" type="sf:DesgloseType" minOccurs="0"/>
<element name="CuotaTotal" type="sf:ImporteSgn12.2Type" minOccurs="0"/>
<element name="ImporteTotal" type="sf:ImporteSgn12.2Type" minOccurs="0"/>
<element name="Encadenamiento" minOccurs="0">
<complexType>
<choice>
<element name="PrimerRegistro" type="sf:PrimerRegistroCadenaType"/>
<element name="RegistroAnterior" type="sf:EncadenamientoFacturaAnteriorType"/>
</choice>
</complexType>
</element>
<element name="FechaHoraHusoGenRegistro" type="dateTime" minOccurs="0"/>
<element name="NumRegistroAcuerdoFacturacion" type="sf:TextMax15Type" minOccurs="0"/>
<element name="IdAcuerdoSistemaInformatico" type="sf:TextMax16Type" minOccurs="0"/>
<element name="TipoHuella" type="sf:TipoHuellaType" minOccurs="0"/>
<element name="Huella" type="sf:TextMax64Type" minOccurs="0"/>
<element name="NifRepresentante" type="sf:NIFType" minOccurs="0"/>
<element name="FechaFinVeriFactu" type="sf:fecha" minOccurs="0"/>
<element name="Incidencia" type="sf:IncidenciaType" minOccurs="0"/>
</sequence>
</complexType>
<simpleType name="IndicadorPaginacionType">
<restriction base="string">
<enumeration value="S"/>
<enumeration value="N"/>
</restriction>
</simpleType>
<simpleType name="ResultadoConsultaType">
<restriction base="string">
<enumeration value="ConDatos"/>
<enumeration value="SinDatos"/>
</restriction>
</simpleType>
<simpleType name="ErrorDetalleType">
<restriction base="integer"/>
</simpleType>
<!-- Estado del registro almacenado en el sistema -->
<simpleType name="EstadoRegistroType">
<restriction base="string">
<enumeration value="Correcta">
<annotation>
<documentation xml:lang="es">El registro se almacenado sin errores</documentation>
</annotation>
</enumeration>
<enumeration value="AceptadaConErrores">
<annotation>
<documentation xml:lang="es">El registro se almacenado tiene algunos errores. Ver detalle del error</documentation>
</annotation>
</enumeration>
<enumeration value="Anulada">
<annotation>
<documentation xml:lang="es">El registro almacenado ha sido anulado</documentation>
</annotation>
</enumeration>
</restriction>
</simpleType>
</schema>

View File

@ -0,0 +1,139 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- editado con XMLSpy v2019 sp1 (x64) (http://www.altova.com) por Puesto de Trabajo (Agencia Estatal de Administracion Tributaria ((AEAT))) -->
<!-- edited with XMLSpy v2009 sp1 (http://www.altova.com) by PC Corporativo (AGENCIA TRIBUTARIA) -->
<schema xmlns="http://www.w3.org/2001/XMLSchema" xmlns:sfR="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/RespuestaSuministro.xsd" xmlns:sf="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd" xmlns:sfLR="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroLR.xsd" targetNamespace="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/RespuestaSuministro.xsd" elementFormDefault="qualified">
<import namespace="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd" schemaLocation="SuministroInformacion.xsd"/>
<import namespace="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroLR.xsd" schemaLocation="SuministroLR.xsd"/>
<element name="RespuestaRegFactuSistemaFacturacion" type="sfR:RespuestaRegFactuSistemaFacturacionType"/>
<complexType name="RespuestaBaseType">
<sequence>
<element name="CSV" type="string" minOccurs="0">
<annotation>
<documentation xml:lang="es"> CSV asociado al envío generado por AEAT. Solo se genera si no hay rechazo del envio</documentation>
</annotation>
</element>
<element name="DatosPresentacion" type="sf:DatosPresentacionType" minOccurs="0">
<annotation>
<documentation xml:lang="es"> Se devuelven datos de la presentacion realizada. Solo se genera si no hay rechazo del envio </documentation>
</annotation>
</element>
<element name="Cabecera" type="sf:CabeceraType">
<annotation>
<documentation xml:lang="es"> Se devuelve la cabecera que se incluyó en el envío. </documentation>
</annotation>
</element>
<element name="TiempoEsperaEnvio" type="sf:Tipo6Type"/>
<element name="EstadoEnvio" type="sfR:EstadoEnvioType">
<annotation>
<documentation xml:lang="es">
Estado del envío en conjunto.
Si los datos de cabecera y todos los registros son correctos,el estado es correcto.
En caso de estructura y cabecera correctos donde todos los registros son incorrectos, el estado es incorrecto
En caso de estructura y cabecera correctos con al menos un registro incorrecto, el estado global es parcialmente correcto.
</documentation>
</annotation>
</element>
</sequence>
</complexType>
<complexType name="RespuestaRegFactuSistemaFacturacionType">
<annotation>
<documentation xml:lang="es"> Respuesta a un envío de registro de facturacion</documentation>
</annotation>
<complexContent>
<extension base="sfR:RespuestaBaseType">
<sequence>
<element name="RespuestaLinea" type="sfR:RespuestaExpedidaType" minOccurs="0" maxOccurs="1000">
<annotation>
<documentation xml:lang="es">
Estado detallado de cada línea del suministro.
</documentation>
</annotation>
</element>
</sequence>
</extension>
</complexContent>
</complexType>
<complexType name="RespuestaExpedidaType">
<annotation>
<documentation xml:lang="es"> Respuesta a un envío </documentation>
</annotation>
<sequence>
<element name="IDFactura" type="sf:IDFacturaExpedidaType">
<annotation>
<documentation xml:lang="es"> ID Factura Expedida </documentation>
</annotation>
</element>
<element name="Operacion" type="sf:OperacionType"/>
<element name="RefExterna" type="sf:TextMax70Type" minOccurs="0"/>
<element name="EstadoRegistro" type="sfR:EstadoRegistroType">
<annotation>
<documentation xml:lang="es">
Estado del registro. Correcto o Incorrecto
</documentation>
</annotation>
</element>
<element name="CodigoErrorRegistro" type="sfR:ErrorDetalleType" minOccurs="0">
<annotation>
<documentation xml:lang="es">
Código del error de registro, en su caso.
</documentation>
</annotation>
</element>
<element name="DescripcionErrorRegistro" type="sf:TextMax1500Type" minOccurs="0">
<annotation>
<documentation xml:lang="es">
Descripción detallada del error de registro, en su caso.
</documentation>
</annotation>
</element>
<element name="RegistroDuplicado" type="sf:RegistroDuplicadoType" minOccurs="0">
<annotation>
<documentation xml:lang="es">
Solo en el caso de que se rechace el registro por duplicado se devuelve este nodo con la informacion registrada en el sistema para este registro
</documentation>
</annotation>
</element>
</sequence>
</complexType>
<simpleType name="EstadoEnvioType">
<restriction base="string">
<enumeration value="Correcto">
<annotation>
<documentation xml:lang="es">Correcto</documentation>
</annotation>
</enumeration>
<enumeration value="ParcialmenteCorrecto">
<annotation>
<documentation xml:lang="es">Parcialmente correcto. Ver detalle de errores</documentation>
</annotation>
</enumeration>
<enumeration value="Incorrecto">
<annotation>
<documentation xml:lang="es">Incorrecto</documentation>
</annotation>
</enumeration>
</restriction>
</simpleType>
<simpleType name="EstadoRegistroType">
<restriction base="string">
<enumeration value="Correcto">
<annotation>
<documentation xml:lang="es">Correcto</documentation>
</annotation>
</enumeration>
<enumeration value="AceptadoConErrores">
<annotation>
<documentation xml:lang="es">Aceptado con Errores. Ver detalle del error</documentation>
</annotation>
</enumeration>
<enumeration value="Incorrecto">
<annotation>
<documentation xml:lang="es">Incorrecto</documentation>
</annotation>
</enumeration>
</restriction>
</simpleType>
<simpleType name="ErrorDetalleType">
<restriction base="integer"/>
</simpleType>
</schema>

View File

@ -0,0 +1,110 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- editado con XMLSpy v2019 sp1 (x64) (http://www.altova.com) por AEAT (Agencia Estatal de Administracion Tributaria ((AEAT))) -->
<wsdl:definitions xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:http="http://schemas.xmlsoap.org/wsdl/http/" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:mime="http://schemas.xmlsoap.org/wsdl/mime/" xmlns:sfLR="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroLR.xsd" xmlns:sf="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd" xmlns:sfR="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/RespuestaSuministro.xsd" xmlns:sfLRC="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/ConsultaLR.xsd" xmlns:sfLRRC="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/RespuestaConsultaLR.xsd" xmlns:sfWdsl="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SistemaFacturacion.wsdl" xmlns:ns="http://www.w3.org/2000/09/xmldsig#" targetNamespace="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SistemaFacturacion.wsdl">
<wsdl:types>
<xs:schema targetNamespace="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SistemaFacturacion.wsdl" elementFormDefault="qualified" xmlns:sfWdsl="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SistemaFacturacion.wsdl" xmlns:sf="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd" xmlns:sfLR="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroLR.xsd" xmlns:sfLRC="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/ConsultaLR.xsd" xmlns:sfLRRC="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/RespuestaConsultaLR.xsd">
<xs:import namespace="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd" schemaLocation="SuministroInformacion.xsd"/>
<xs:import namespace="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroLR.xsd" schemaLocation="SuministroLR.xsd"/>
<xs:import namespace="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/ConsultaLR.xsd" schemaLocation="ConsultaLR.xsd"/>
<xs:import namespace="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/RespuestaConsultaLR.xsd" schemaLocation="RespuestaConsultaLR.xsd"/>
<xs:import namespace="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/RespuestaSuministro.xsd" schemaLocation="RespuestaSuministro.xsd"/>
</xs:schema>
</wsdl:types>
<wsdl:message name="EntradaRegFactuSistemaFacturacion">
<wsdl:part name="RegFactuSistemaFacturacion" element="sfLR:RegFactuSistemaFacturacion"/>
</wsdl:message>
<wsdl:message name="EntradaConsultaFactuSistemaFacturacion">
<wsdl:part name="ConsultaFactuSistemaFacturacion" element="sfLRC:ConsultaFactuSistemaFacturacion"/>
</wsdl:message>
<wsdl:message name="RespuestaRegFactuSistemaFacturacion">
<wsdl:part name="RespuestaRegFactuSistemaFacturacion" element="sfR:RespuestaRegFactuSistemaFacturacion"/>
</wsdl:message>
<wsdl:message name="RespuestaConsultaFactuSistemaFacturacion">
<wsdl:part name="RespuestaConsultaFactuSistemaFacturacion" element="sfLRRC:RespuestaConsultaFactuSistemaFacturacion"/>
</wsdl:message>
<wsdl:portType name="sfPortTypeVerifactu">
<wsdl:operation name="RegFactuSistemaFacturacion">
<wsdl:input message="sfWdsl:EntradaRegFactuSistemaFacturacion"/>
<wsdl:output message="sfWdsl:RespuestaRegFactuSistemaFacturacion"/>
</wsdl:operation>
<wsdl:operation name="ConsultaFactuSistemaFacturacion">
<wsdl:input message="sfWdsl:EntradaConsultaFactuSistemaFacturacion"/>
<wsdl:output message="sfWdsl:RespuestaConsultaFactuSistemaFacturacion"/>
</wsdl:operation>
</wsdl:portType>
<wsdl:portType name="sfPortTypePorRequerimiento">
<wsdl:operation name="RegFactuSistemaFacturacion">
<wsdl:input message="sfWdsl:EntradaRegFactuSistemaFacturacion"/>
<wsdl:output message="sfWdsl:RespuestaRegFactuSistemaFacturacion"/>
</wsdl:operation>
</wsdl:portType>
<wsdl:binding name="sfVerifactu" type="sfWdsl:sfPortTypeVerifactu">
<soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
<wsdl:operation name="RegFactuSistemaFacturacion">
<soap:operation soapAction=""/>
<wsdl:input>
<soap:body use="literal"/>
</wsdl:input>
<wsdl:output>
<soap:body use="literal"/>
</wsdl:output>
</wsdl:operation>
<wsdl:operation name="ConsultaFactuSistemaFacturacion">
<soap:operation soapAction=""/>
<wsdl:input>
<soap:body use="literal"/>
</wsdl:input>
<wsdl:output>
<soap:body use="literal"/>
</wsdl:output>
</wsdl:operation>
</wsdl:binding>
<wsdl:binding name="sfRequerimiento" type="sfWdsl:sfPortTypePorRequerimiento">
<soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
<wsdl:operation name="RegFactuSistemaFacturacion">
<soap:operation soapAction=""/>
<wsdl:input>
<soap:body use="literal"/>
</wsdl:input>
<wsdl:output>
<soap:body use="literal"/>
</wsdl:output>
</wsdl:operation>
</wsdl:binding>
<wsdl:service name="sfVerifactu">
<!-- Sistemas que emiten facturas verificables. Entorno de PRODUCCION -->
<wsdl:port name="SistemaVerifactu" binding="sfWdsl:sfVerifactu">
<soap:address location="https://www1.agenciatributaria.gob.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP"/>
</wsdl:port>
<!-- Sistemas que emiten facturas verificables. Entorno de PRODUCCION para acceso con certificado de sello -->
<wsdl:port name="SistemaVerifactuSello" binding="sfWdsl:sfVerifactu">
<soap:address location="https://www10.agenciatributaria.gob.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP"/>
</wsdl:port>
<!-- Sistemas que emiten facturas verificables. Entorno de PRUEBAS -->
<wsdl:port name="SistemaVerifactuPruebas" binding="sfWdsl:sfVerifactu">
<soap:address location="https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP"/>
</wsdl:port>
<!-- Sistemas que emiten facturas verificables. Entorno de PRUEBAS para acceso con certificado de sello -->
<wsdl:port name="SistemaVerifactuSelloPruebas" binding="sfWdsl:sfVerifactu">
<soap:address location="https://prewww10.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP"/>
</wsdl:port>
</wsdl:service>
<wsdl:service name="sfRequerimiento">
<!-- Sistemas que emiten facturas NO verificables. (Remision bajo requerimiento). Entorno de PRODUCCION -->
<wsdl:port name="SistemaRequerimiento" binding="sfWdsl:sfRequerimiento">
<soap:address location="https://www1.agenciatributaria.gob.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/RequerimientoSOAP"/>
</wsdl:port>
<!-- Sistemas que emiten facturas NO verificables. (Remision bajo requerimiento). Entorno de PRODUCCION para acceso con certificado de sello -->
<wsdl:port name="SistemaRequerimientoSello" binding="sfWdsl:sfRequerimiento">
<soap:address location="https://www10.agenciatributaria.gob.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/RequerimientoSOAP"/>
</wsdl:port>
<!-- Sistemas que emiten facturas NO verificables. (Remision bajo requerimiento). Entorno de PRUEBAS -->
<wsdl:port name="SistemaRequerimientoPruebas" binding="sfWdsl:sfRequerimiento">
<soap:address location="https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/RequerimientoSOAP"/>
</wsdl:port>
<!-- Sistemas que emiten facturas NO verificables. (Remision bajo requerimiento). Entorno de PRUEBAS para acceso con certificado de sello -->
<wsdl:port name="SistemaRequerimientoSelloPruebas" binding="sfWdsl:sfRequerimiento">
<soap:address location="https://prewww10.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/RequerimientoSOAP"/>
</wsdl:port>
</wsdl:service>
</wsdl:definitions>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- editado con XMLSpy v2019 sp1 (x64) (http://www.altova.com) por Puesto de Trabajo (Agencia Estatal de Administracion Tributaria ((AEAT))) -->
<!-- edited with XMLSpy v2009 sp1 (http://www.altova.com) by PC Corporativo (AGENCIA TRIBUTARIA) -->
<schema xmlns="http://www.w3.org/2001/XMLSchema" xmlns:sfLR="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroLR.xsd" xmlns:sf="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd" targetNamespace="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroLR.xsd" elementFormDefault="qualified">
<import namespace="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd" schemaLocation="SuministroInformacion.xsd"/>
<element name="RegFactuSistemaFacturacion">
<complexType>
<sequence>
<element name="Cabecera" type="sf:CabeceraType"/>
<element name="RegistroFactura" type="sfLR:RegistroFacturaType" maxOccurs="1000"/>
</sequence>
</complexType>
</element>
<complexType name="RegistroFacturaType">
<annotation>
<documentation xml:lang="es">Datos correspondientes a los registros de facturacion</documentation>
</annotation>
<sequence>
<choice>
<element ref="sf:RegistroAlta"/>
<element ref="sf:RegistroAnulacion"/>
</choice>
</sequence>
</complexType>
</schema>

View File

@ -0,0 +1,77 @@
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:sum="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroL
R.xsd"
xmlns:sum1="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/Suministro
Informacion.xsd"
xmlns:xd="http://www.w3.org/2000/09/xmldsig#">
<soapenv:Header />
<soapenv:Body>
<sum:RegFactuSistemaFacturacion>
<sum:Cabecera>
<sum1:ObligadoEmision>
<sum1:NombreRazon>XXXXX</sum1:NombreRazon>
<sum1:NIF>AAAA</sum1:NIF>
</sum1:ObligadoEmision>
</sum:Cabecera>
<sum:RegistroFactura>
<sum1:RegistroAlta>
<sum1:IDVersion>1.0</sum1:IDVersion>
<sum1:IDFactura>
<sum1:IDEmisorFactura>AAAA</sum1:IDEmisorFactura>
<sum1:NumSerieFactura>12345</sum1:NumSerieFactura>
<sum1:FechaExpedicionFactura>13-09-2024</sum1:FechaExpedicionFactura>
</sum1:IDFactura>
<sum1:NombreRazonEmisor>XXXXX</sum1:NombreRazonEmisor>
<sum1:TipoFactura>F1</sum1:TipoFactura>
<sum1:DescripcionOperacion>Descripc</sum1:DescripcionOperacion>
<sum1:Destinatarios>
<sum1:IDDestinatario>
<sum1:NombreRazon>YYYY</sum1:NombreRazon>
<sum1:NIF>BBBB</sum1:NIF>
</sum1:IDDestinatario>
</sum1:Destinatarios>
<sum1:Desglose>
<sum1:DetalleDesglose>
<sum1:ClaveRegimen>01</sum1:ClaveRegimen>
<sum1:CalificacionOperacion>S1</sum1:CalificacionOperacion>
<sum1:TipoImpositivo>4</sum1:TipoImpositivo>
<sum1:BaseImponibleOimporteNoSujeto>10</sum1:BaseImponibleOimporteNoSujeto>
<sum1:CuotaRepercutida>0.4</sum1:CuotaRepercutida>
</sum1:DetalleDesglose>
<sum1:DetalleDesglose>
<sum1:ClaveRegimen>01</sum1:ClaveRegimen>
<sum1:CalificacionOperacion>S1</sum1:CalificacionOperacion>
<sum1:TipoImpositivo>21</sum1:TipoImpositivo>
<sum1:BaseImponibleOimporteNoSujeto>100</sum1:BaseImponibleOimporteNoSujeto>
<sum1:CuotaRepercutida>21</sum1:CuotaRepercutida>
</sum1:DetalleDesglose>
</sum1:Desglose>
<sum1:CuotaTotal>21.4</sum1:CuotaTotal>
<sum1:ImporteTotal>131.4</sum1:ImporteTotal>
<sum1:Encadenamiento>
<sum1:RegistroAnterior>
<sum1:IDEmisorFactura>AAAA</sum1:IDEmisorFactura>
<sum1:NumSerieFactura>44</sum1:NumSerieFactura>
<sum1:FechaExpedicionFactura>13-09-2024</sum1:FechaExpedicionFactura>
<sum1:Huella>HuellaRegistroAnterior</sum1:Huella>
</sum1:RegistroAnterior>
</sum1:Encadenamiento>
<sum1:SistemaInformatico>
<sum1:NombreRazon>SSSS</sum1:NombreRazon>
<sum1:NIF>NNNN</sum1:NIF>
<sum1:NombreSistemaInformatico>NombreSistemaInformatico</sum1:NombreSistemaInformatico>
<sum1:IdSistemaInformatico>77</sum1:IdSistemaInformatico>
<sum1:Version>1.0.03</sum1:Version>
<sum1:NumeroInstalacion>383</sum1:NumeroInstalacion>
<sum1:TipoUsoPosibleSoloVerifactu>N</sum1:TipoUsoPosibleSoloVerifactu>
<sum1:TipoUsoPosibleMultiOT>S</sum1:TipoUsoPosibleMultiOT>
<sum1:IndicadorMultiplesOT>S</sum1:IndicadorMultiplesOT>
</sum1:SistemaInformatico>
<sum1:FechaHoraHusoGenRegistro>2024-09-13T19:20:30+01:00</sum1:FechaHoraHusoGenRegistro>
<sum1:TipoHuella>01</sum1:TipoHuella>
<sum1:Huella>Huella</sum1:Huella>
</sum1:RegistroAlta>
</sum:RegistroFactura>
</sum:RegFactuSistemaFacturacion>
</soapenv:Body>
</soapenv:Envelope>

View File

@ -0,0 +1,50 @@
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:sum="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroL
R.xsd"
xmlns:sum1="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/Suministro
Informacion.xsd"
xmlns:xd="http://www.w3.org/2000/09/xmldsig#">
<soapenv:Header />
<soapenv:Body>
<sum:RegFactuSistemaFacturacion>
<sum:Cabecera>
<sum1:ObligadoEmision>
<sum1:NombreRazon>XXXXX</sum1:NombreRazon>
<sum1:NIF>AAAA</sum1:NIF>
</sum1:ObligadoEmision>
</sum:Cabecera>
<sum:RegistroFactura>
<sum1:RegistroAnulacion>
<sum1:IDVersion>1.0</sum1:IDVersion>
<sum1:IDFactura>
<sum1:IDEmisorFacturaAnulada>AAAA</sum1:IDEmisorFacturaAnulada>
<sum1:NumSerieFacturaAnulada>12345</sum1:NumSerieFacturaAnulada>
<sum1:FechaExpedicionFacturaAnulada>13-09-2024</sum1:FechaExpedicionFacturaAnulada>
</sum1:IDFactura>
<sum1:Encadenamiento>
<sum1:RegistroAnterior>
<sum1:IDEmisorFactura>AAAA</sum1:IDEmisorFactura>
<sum1:NumSerieFactura>44</sum1:NumSerieFactura>
<sum1:FechaExpedicionFactura>13-09-2024</sum1:FechaExpedicionFactura>
<sum1:Huella>HuellaRegistroAnterior</sum1:Huella>
</sum1:RegistroAnterior>
</sum1:Encadenamiento>
<sum1:SistemaInformatico>
<sum1:NombreRazon>SSSS</sum1:NombreRazon>
<sum1:NIF>NNNN</sum1:NIF>
<sum1:NombreSistemaInformatico>NombreSistemaInformatico</sum1:NombreSistemaInformatico>
<sum1:IdSistemaInformatico>77</sum1:IdSistemaInformatico>
<sum1:Version>1.0.03</sum1:Version>
<sum1:NumeroInstalacion>383</sum1:NumeroInstalacion>
<sum1:TipoUsoPosibleSoloVerifactu>N</sum1:TipoUsoPosibleSoloVerifactu>
<sum1:TipoUsoPosibleMultiOT>S</sum1:TipoUsoPosibleMultiOT>
<sum1:IndicadorMultiplesOT>S</sum1:IndicadorMultiplesOT>
</sum1:SistemaInformatico>
<sum1:FechaHoraHusoGenRegistro>2024-09-13T19:20:30+01:00</sum1:FechaHoraHusoGenRegistro>
<sum1:TipoHuella>01</sum1:TipoHuella>
<sum1:Huella>Huella</sum1:Huella>
</sum1:RegistroAnulacion>
</sum:RegistroFactura>
</sum:RegFactuSistemaFacturacion>
</soapenv:Body>
</soapenv:Envelope>

View File

@ -0,0 +1,78 @@
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:sum="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroL
R.xsd"
xmlns:sum1="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/Suministro
Informacion.xsd"
xmlns:xd="http://www.w3.org/2000/09/xmldsig#">
<soapenv:Header />
<soapenv:Body>
<sum:RegFactuSistemaFacturacion>
<sum:Cabecera>
<sum1:ObligadoEmision>
<sum1:NombreRazon>XXXXX</sum1:NombreRazon>
<sum1:NIF>AAAA</sum1:NIF>
</sum1:ObligadoEmision>
</sum:Cabecera>
<sum:RegistroFactura>
<sum1:RegistroAlta>
<sum1:IDVersion>1.0</sum1:IDVersion>
<sum1:IDFactura>
<sum1:IDEmisorFactura>AAAA</sum1:IDEmisorFactura>
<sum1:NumSerieFactura>12345</sum1:NumSerieFactura>
<sum1:FechaExpedicionFactura>13-09-2024</sum1:FechaExpedicionFactura>
</sum1:IDFactura>
<sum1:NombreRazonEmisor>XXXXX</sum1:NombreRazonEmisor>
<sum1:Subsanacion>S</sum1:Subsanacion>
<sum1:TipoFactura>F1</sum1:TipoFactura>
<sum1:DescripcionOperacion>Descripc</sum1:DescripcionOperacion>
<sum1:Destinatarios>
<sum1:IDDestinatario>
<sum1:NombreRazon>YYYY</sum1:NombreRazon>
<sum1:NIF>BBBB</sum1:NIF>
</sum1:IDDestinatario>
</sum1:Destinatarios>
<sum1:Desglose>
<sum1:DetalleDesglose>
<sum1:ClaveRegimen>01</sum1:ClaveRegimen>
<sum1:CalificacionOperacion>S1</sum1:CalificacionOperacion>
<sum1:TipoImpositivo>4</sum1:TipoImpositivo>
<sum1:BaseImponibleOimporteNoSujeto>10</sum1:BaseImponibleOimporteNoSujeto>
<sum1:CuotaRepercutida>0.4</sum1:CuotaRepercutida>
</sum1:DetalleDesglose>
<sum1:DetalleDesglose>
<sum1:ClaveRegimen>01</sum1:ClaveRegimen>
<sum1:CalificacionOperacion>S1</sum1:CalificacionOperacion>
<sum1:TipoImpositivo>21</sum1:TipoImpositivo>
<sum1:BaseImponibleOimporteNoSujeto>100</sum1:BaseImponibleOimporteNoSujeto>
<sum1:CuotaRepercutida>21</sum1:CuotaRepercutida>
</sum1:DetalleDesglose>
</sum1:Desglose>
<sum1:CuotaTotal>21.4</sum1:CuotaTotal>
<sum1:ImporteTotal>131.4</sum1:ImporteTotal>
<sum1:Encadenamiento>
<sum1:RegistroAnterior>
<sum1:IDEmisorFactura>AAAA</sum1:IDEmisorFactura>
<sum1:NumSerieFactura>44</sum1:NumSerieFactura>
<sum1:FechaExpedicionFactura>13-09-2024</sum1:FechaExpedicionFactura>
<sum1:Huella>HuellaRegistroAnterior</sum1:Huella>
</sum1:RegistroAnterior>
</sum1:Encadenamiento>
<sum1:SistemaInformatico>
<sum1:NombreRazon>SSSS</sum1:NombreRazon>
<sum1:NIF>NNNN</sum1:NIF>
<sum1:NombreSistemaInformatico>NombreSistemaInformatico</sum1:NombreSistemaInformatico>
<sum1:IdSistemaInformatico>77</sum1:IdSistemaInformatico>
<sum1:Version>1.0.03</sum1:Version>
<sum1:NumeroInstalacion>383</sum1:NumeroInstalacion>
<sum1:TipoUsoPosibleSoloVerifactu>N</sum1:TipoUsoPosibleSoloVerifactu>
<sum1:TipoUsoPosibleMultiOT>S</sum1:TipoUsoPosibleMultiOT>
<sum1:IndicadorMultiplesOT>S</sum1:IndicadorMultiplesOT>
</sum1:SistemaInformatico>
<sum1:FechaHoraHusoGenRegistro>2024-09-13T19:20:30+01:00</sum1:FechaHoraHusoGenRegistro>
<sum1:TipoHuella>01</sum1:TipoHuella>
<sum1:Huella>Huella</sum1:Huella>
</sum1:RegistroAlta>
</sum:RegistroFactura>
</sum:RegFactuSistemaFacturacion>
</soapenv:Body>
</soapenv:Envelope>

View File

@ -13,18 +13,20 @@ namespace App\Services\Invoice;
use App\Events\Invoice\InvoiceWasCancelled;
use App\Models\Invoice;
use App\Repositories\InvoiceRepository;
use App\Services\AbstractService;
use App\Utils\Ninja;
use App\Utils\Traits\GeneratesCounter;
use App\Utils\Traits\MakesHash;
use stdClass;
class HandleCancellation extends AbstractService
{
use GeneratesCounter;
use MakesHash;
public function __construct(private Invoice $invoice)
public function __construct(private Invoice $invoice, private ?string $reason = null)
{
$this->invoice = $invoice;
}
public function run()
@ -34,6 +36,10 @@ class HandleCancellation extends AbstractService
return $this->invoice;
}
if($this->invoice->company->verifactuEnabled()) {
return $this->verifactuCancellation();
}
$adjustment = ($this->invoice->balance < 0) ? abs($this->invoice->balance) : $this->invoice->balance * -1;
$this->backupCancellation($adjustment);
@ -56,12 +62,85 @@ class HandleCancellation extends AbstractService
return $this->invoice;
}
/**
* verifactuCancellation
* @todo we must ensure that if there have been previous credit notes attached to the invoice,
* that the credit notes are not exceeded by the cancellation amount.
* This is because the credit notes are not linked to the invoice, but are linked to the
* invoice's backup.
* So we need to check the backup for the credit notes and ensure that the cancellation amount
* does not exceed the credit notes.
* If it does, we need to create a new credit note with the remaining amount.
* This is because the credit notes are not linked to the invoice, but are linked to the
* @return Invoice
*/
private function verifactuCancellation(): Invoice
{
$this->invoice = $this->invoice->service()->setStatus(Invoice::STATUS_CANCELLED)->save();
$this->invoice->service()->workFlow()->save();
// R2 Cancellation - do not create a separate document
if($this->invoice->backup->document_type === 'R2'){
$parent = Invoice::withTrashed()->find($this->decodePrimaryKey($this->invoice->backup->parent_invoice_id));
if(!$parent) {
return $this->invoice;
}
$parent->backup->adjustable_amount -= $this->invoice->amount;
$parent->backup->child_invoice_ids->reject(fn($id) => $id === $this->invoice->hashed_id);
$parent->save();
$this->invoice->service()->cancelVerifactu();
}
else {
$replicated_invoice = $this->invoice->replicate();
$replicated_invoice->status_id = Invoice::STATUS_DRAFT;
$replicated_invoice->date = now()->format('Y-m-d');
$replicated_invoice->due_date = null;
$replicated_invoice->partial = 0;
$replicated_invoice->partial_due_date = null;
$replicated_invoice->number = null;
$replicated_invoice->amount = 0;
$replicated_invoice->balance = 0;
$replicated_invoice->paid_to_date = 0;
$items = $replicated_invoice->line_items;
foreach($items as &$item) {
$item->quantity = $item->quantity * -1;
}
$replicated_invoice->line_items = $items;
$replicated_invoice->backup->parent_invoice_id = $this->invoice->hashed_id;
$replicated_invoice->backup->parent_invoice_number = $this->invoice->number;
$replicated_invoice->backup->document_type = 'R2'; // Full Credit Note Generated for the invoice
$invoice_repository = new InvoiceRepository();
$replicated_invoice = $invoice_repository->save([], $replicated_invoice);
$replicated_invoice->service()->markSent()->sendVerifactu()->save();
$this->invoice->backup->child_invoice_ids->push($replicated_invoice->hashed_id);
$this->invoice->saveQuietly();
}
$this->invoice->fresh();
event(new InvoiceWasCancelled($this->invoice, $this->invoice->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
event('eloquent.updated: App\Models\Invoice', $this->invoice);
return $this->invoice;
}
public function reverse()
{
/* The stored cancelled object - contains the adjustment and status*/
$cancellation = $this->invoice->backup->cancellation;
/* Will turn the negative cancellation amount to a positive adjustment*/
$cancellation = $this->invoice->backup->cancellation;
$adjustment = $cancellation->adjustment * -1;
$this->invoice->ledger()->updateInvoiceBalance($adjustment, "Invoice {$this->invoice->number} reversal");
@ -76,11 +155,9 @@ class HandleCancellation extends AbstractService
$this->invoice->client->service()->calculateBalance();
/* Pop the cancellation out of the backup*/
$backup = $this->invoice->backup;
unset($backup->cancellation);
$this->invoice->backup = $backup;
/* Clear the cancellation data */
$this->invoice->backup->cancellation->adjustment = 0;
$this->invoice->backup->cancellation->status_id = 0;
$this->invoice->saveQuietly();
$this->invoice->fresh();
@ -95,19 +172,11 @@ class HandleCancellation extends AbstractService
*/
private function backupCancellation($adjustment)
{
if (! is_object($this->invoice->backup)) {
$backup = new stdClass();
$this->invoice->backup = $backup;
}
$cancellation = new stdClass();
$cancellation->adjustment = $adjustment;
$cancellation->status_id = $this->invoice->status_id;
// Direct assignment to properties
$this->invoice->backup->cancellation->adjustment = $adjustment;
$this->invoice->backup->cancellation->status_id = $this->invoice->status_id;
$invoice_backup = $this->invoice->backup;
$invoice_backup->cancellation = $cancellation;
$this->invoice->backup = $invoice_backup;
$this->invoice->saveQuietly();
}
}

View File

@ -27,6 +27,7 @@ use Illuminate\Support\Facades\Storage;
use App\Events\Invoice\InvoiceWasArchived;
use App\Jobs\Inventory\AdjustProductInventory;
use App\Libraries\Currency\Conversion\CurrencyApi;
use App\Services\EDocument\Standards\Verifactu\SendToAeat;
class InvoiceService
{
@ -233,21 +234,24 @@ class InvoiceService
return $this;
}
public function handleCancellation()
public function handleCancellation(?string $reason = null)
{
$this->removeUnpaidGatewayFees();
$this->invoice = (new HandleCancellation($this->invoice))->run();
$this->invoice = (new HandleCancellation($this->invoice, $reason))->run();
return $this;
}
public function markDeleted()
{
// $this->removeUnpaidGatewayFees();
$this->invoice = (new MarkInvoiceDeleted($this->invoice))->run();
if($this->invoice->company->verifactuEnabled()) {
$this->cancelVerifactu();
}
return $this;
}
@ -674,6 +678,65 @@ class InvoiceService
}
/**
* sendVerifactu
*
* @return self
*/
public function sendVerifactu(): self
{
SendToAeat::dispatch($this->invoice->id, $this->invoice->company, 'create');
return $this;
}
/**
* cancelVerifactu
*
* @return self
*/
public function cancelVerifactu(): self
{
SendToAeat::dispatch($this->invoice->id, $this->invoice->company, 'cancel');
return $this;
}
/**
* Handles all requirements for verifactu saves
*
* @param array $invoice_array
* @param bool $new_model
* @return self
*/
public function modifyVerifactuWorkflow(array $invoice_array, bool $new_model): self
{
if($new_model && $this->invoice->amount >= 0) {
$this->invoice->backup->document_type = 'F1';
$this->invoice->backup->adjustable_amount = $this->invoice->amount;
$this->invoice->backup->parent_invoice_number = $this->invoice->number;
$this->invoice->saveQuietly();
}
elseif(isset($invoice_array['modified_invoice_id'])) {
$modified_invoice = Invoice::withTrashed()->find($this->decodePrimaryKey($invoice_array['modified_invoice_id']));
$modified_invoice->backup->child_invoice_ids->push($this->invoice->hashed_id);
$modified_invoice->backup->adjustable_amount += $this->invoice->amount;
$modified_invoice->save();
$this->markSent();
//Update the client balance by the delta amount from the previous invoice to this one.
$this->invoice->backup->parent_invoice_id = $modified_invoice->hashed_id;
$this->invoice->backup->document_type = 'R2';
$this->invoice->backup->parent_invoice_number = $modified_invoice->number;
$this->invoice->saveQuietly();
$this->invoice->client->service()->updateBalance(round(($this->invoice->amount - $modified_invoice->amount), 2));
$this->sendVerifactu();
}
return $this;
}
/**
* Saves the invoice.
* @return Invoice object

View File

@ -81,18 +81,23 @@ class TriggeredActions extends AbstractService
$company->save();
}
if($this->request->has('retry_e_send') && $this->request->input('retry_e_send') == 'true' && !isset($this->invoice->backup->guid) && $this->invoice->client->peppolSendingEnabled()) {
if($this->request->has('retry_e_send') && $this->request->input('retry_e_send') == 'true' && strlen($this->invoice->backup->guid ?? '') == 0) {
if($this->invoice->client->peppolSendingEnabled()) {
\App\Services\EDocument\Jobs\SendEDocument::dispatch(get_class($this->invoice), $this->invoice->id, $this->invoice->company->db);
}
elseif($this->invoice->company->verifactuEnabled()) {
$this->invoice->service()->sendVerifactu();
}
}
if($this->request->has('redirect')) {
$redirectUrl = urldecode($this->request->input('redirect'));
if (filter_var($redirectUrl, FILTER_VALIDATE_URL)) {
$backup = ($this->invoice->backup && is_object($this->invoice->backup)) ? $this->invoice->backup : new \stdClass();
$backup->redirect = $redirectUrl;
$this->invoice->backup = $backup;
// $backup = ($this->invoice->backup && is_object($this->invoice->backup)) ? $this->invoice->backup : new \stdClass();
// $backup->redirect = $redirectUrl;
$this->invoice->backup->redirect = $redirectUrl;
$this->invoice->saveQuietly();
}

View File

@ -97,38 +97,38 @@ class QuickbooksService
return $this;
}
private function checkDefaultAccounts(): self
{
// private function checkDefaultAccounts(): self
// {
$accountQuery = "SELECT * FROM Account WHERE AccountType IN ('Income', 'Cost of Goods Sold')";
// $accountQuery = "SELECT * FROM Account WHERE AccountType IN ('Income', 'Cost of Goods Sold')";
if (strlen($this->settings->default_income_account) == 0 || strlen($this->settings->default_expense_account) == 0) {
// if (strlen($this->settings->default_income_account) == 0 || strlen($this->settings->default_expense_account) == 0) {
nlog("Checking default accounts for company {$this->company->company_key}");
$accounts = $this->sdk->Query($accountQuery);
// nlog("Checking default accounts for company {$this->company->company_key}");
// $accounts = $this->sdk->Query($accountQuery);
$find_income_account = true;
$find_expense_account = true;
// $find_income_account = true;
// $find_expense_account = true;
foreach ($accounts as $account) {
if ($account->AccountType->value == 'Income' && $find_income_account) {
$this->settings->default_income_account = $account->Id->value;
$find_income_account = false;
} elseif ($account->AccountType->value == 'Cost of Goods Sold' && $find_expense_account) {
$this->settings->default_expense_account = $account->Id->value;
$find_expense_account = false;
}
}
// foreach ($accounts as $account) {
// if ($account->AccountType->value == 'Income' && $find_income_account) {
// $this->settings->default_income_account = $account->Id->value;
// $find_income_account = false;
// } elseif ($account->AccountType->value == 'Cost of Goods Sold' && $find_expense_account) {
// $this->settings->default_expense_account = $account->Id->value;
// $find_expense_account = false;
// }
// }
nlog($this->settings);
// nlog($this->settings);
$this->company->quickbooks->settings = $this->settings;
$this->company->save();
}
// $this->company->quickbooks->settings = $this->settings;
// $this->company->save();
// }
return $this;
}
// return $this;
// }
private function checkToken(): self
{

View File

@ -130,25 +130,25 @@ class QuoteTransformer extends BaseTransformer
}
private function getPayments(mixed $qb_data)
{
$payments = [];
// private function getPayments(mixed $qb_data)
// {
// $payments = [];
$qb_payments = data_get($qb_data, 'LinkedTxn', false) ?? [];
// $qb_payments = data_get($qb_data, 'LinkedTxn', false) ?? [];
if (!empty($qb_payments) && !isset($qb_payments[0])) {
$qb_payments = [$qb_payments];
}
// if (!empty($qb_payments) && !isset($qb_payments[0])) {
// $qb_payments = [$qb_payments];
// }
foreach ($qb_payments as $payment) {
if (data_get($payment, 'TxnType', false) == 'Payment') {
$payments[] = data_get($payment, 'TxnId', false);
}
}
// foreach ($qb_payments as $payment) {
// if (data_get($payment, 'TxnType', false) == 'Payment') {
// $payments[] = data_get($payment, 'TxnId', false);
// }
// }
return $payments;
// return $payments;
}
// }
private function getLineItems(mixed $qb_data, array $tax_array)
{

View File

@ -171,7 +171,7 @@ class InvoiceTransformer extends EntityTransformer
'auto_bill_enabled' => (bool) $invoice->auto_bill_enabled,
'tax_info' => $invoice->tax_data ?: new \stdClass(),
'e_invoice' => $invoice->e_invoice ?: new \stdClass(),
'backup' => $invoice->backup ?: new \stdClass(),
'backup' => $invoice->backup,
'location_id' => $this->encodePrimaryKey($invoice->location_id),
];

View File

@ -757,7 +757,7 @@ class HtmlEngine
if ($this->entity_string == 'invoice' && $this->entity->net_payments()->exists()) {
$payment_list = '<br><br>';
foreach ($this->entity->net_payments as $payment) {
foreach ($this->entity->net_payments as $payment) { //@phpstan-ignore-line
$payment_list .= ctrans('texts.payment_subject') . ": " . $this->formatDate($payment->date, $this->client->date_format()) . " :: " . Number::formatMoney($payment->amount, $this->client) ." :: ". $payment->translatedType() . "<br>";
}

View File

@ -17,6 +17,11 @@ trait ActionsInvoice
{
public function invoiceDeletable($invoice): bool
{
//Cancelled invoices are not deletable if verifactu is enabled
if($invoice->company->verifactuEnabled() && $invoice->status_id == Invoice::STATUS_CANCELLED) {
return false;
}
if ($invoice->status_id <= Invoice::STATUS_SENT &&
$invoice->is_deleted == false &&
$invoice->deleted_at == null &&

View File

@ -94,6 +94,7 @@
"pusher/pusher-php-server": "^7.2",
"quickbooks/v3-php-sdk": "^6.2",
"razorpay/razorpay": "2.*",
"robrichards/xmlseclibs": "^3.1",
"sentry/sentry-laravel": "^4",
"setasign/fpdf": "^1.8",
"setasign/fpdi": "^2.3",
@ -124,6 +125,7 @@
"friendsofphp/php-cs-fixer": "^3.14",
"laracasts/cypress": "^3.0",
"larastan/larastan": "^2",
"laravel/boost": "^1.0",
"mockery/mockery": "^1.4.4",
"nunomaduro/collision": "^8.1",
"phpstan/phpstan": "^1.9",

2127
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -255,8 +255,6 @@ return [
'upload_extensions' => env('ADDITIONAL_UPLOAD_EXTENSIONS', ''),
'storecove_api_key' => env('STORECOVE_API_KEY', false),
'storecove_email_catchall' => env('STORECOVE_CATCHALL_EMAIL',false),
'qvalia_api_key' => env('QVALIA_API_KEY', false),
'qvalia_partner_number' => env('QVALIA_PARTNER_NUMBER', false),
'pdf_page_numbering_x_alignment' => env('PDF_PAGE_NUMBER_X', 0),
'pdf_page_numbering_y_alignment' => env('PDF_PAGE_NUMBER_Y', -6),
'hosted_einvoice_secret' => env('HOSTED_EINVOICE_SECRET', null),

View File

@ -141,19 +141,16 @@ return [
'gocardless' => [
'client_id' => env('GOCARDLESS_CLIENT_ID', null),
'client_secret' => env('GOCARDLESS_CLIENT_SECRET', null),
'environment' => env('GOCARDLESS_ENVIRONMENT', 'production'),
'redirect_uri' => env('GOCARDLESS_REDIRECT_URI', 'https://invoicing.co/gocardless/oauth/connect/confirm'),
'testing_company' => env('GOCARDLESS_TESTING_COMPANY', null),
'webhook_secret' => env('GOCARDLESS_WEBHOOK_SECRET', null),
],
'quickbooks' => [
'client_id' => env('QUICKBOOKS_CLIENT_ID', false),
'client_secret' => env('QUICKBOOKS_CLIENT_SECRET', false),
'redirect' => env('QUICKBOOKS_REDIRECT_URI'),
'env' => env('QUICKBOOKS_ENV'),
'debug' => env('APP_DEBUG',false)
],
'quickbooks_webhook' => [
'verifier_token' => env('QUICKBOOKS_VERIFIER_TOKEN', false),
],
'verifactu' => [
'sender_nif' => env('VERIFACTU_SENDER_NIF', ''),
'certificate' => env('VERIFACTU_CERTIFICATE', ''),
'ssl_key' => env('VERIFACTU_SSL_KEY', ''),
'sender_name' => env('VERIFACTU_SENDER_NAME', 'CERTIFICADO FISICA PRUEBAS'),
'test_mode' => env('VERIFACTU_TEST_MODE', false),
],
];

View File

@ -0,0 +1,43 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('verifactu_logs', function (Blueprint $table) {
$table->id();
$table->unsignedInteger('company_id')->index();
$table->unsignedInteger('invoice_id')->index();
$table->string('nif');
$table->string('date');
$table->string('invoice_number');
$table->string('hash');
$table->string('previous_hash')->nullable();
$table->string('status');
$table->json('response')->nullable();
$table->text('state')->nullable();
$table->timestamps();
$table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade')->onUpdate('cascade');
$table->foreign('invoice_id')->references('id')->on('invoices')->onDelete('cascade')->onUpdate('cascade');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
//
}
};

View File

@ -5573,6 +5573,18 @@ $lang = array(
'invalid_csv_data' => 'Invalid CSV data, your import was cancelled.',
'selected_products' => 'Selected Products',
'create_company_error_unauthorized' => 'You are not authorized to create a company. Only the account owner can create a company.',
'restore_disabled_verifactu' => 'You cannot restore an invoice once it has been deleted',
'delete_disabled_verifactu' => 'You cannot delete an invoice once it has been cancelled or modified',
'rectify' => 'Rectificar',
'verifactu_invoice_send_success' => 'Invoice :invoice for :client sent to AEAT successfully',
'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',
'activity_150' => 'E-Invoice :invoice for :client sent to AEAT successfully',
'activity_151' => 'E-Invoice :invoice for :client failed to send to AEAT :notes',
'activity_152' => 'Invoice cancellation for :invoice sent to AEAT successfully',
'activity_153' => 'Invoice cancellation for :invoice failed to send to AEAT :notes',
);
return $lang;

View File

@ -20,6 +20,7 @@ parameters:
- 'app/PaymentDrivers/AuthorizePaymentDriver.php'
- 'app/Http/Middleware/ThrottleRequestsWithPredis.php'
- 'app/Utils/Traits/*'
- 'Modules/Accounting/*'
universalObjectCratesClasses:
- App\DataMapper\Tax\RuleInterface
- App\DataMapper\FeesAndLimits

View File

@ -98,12 +98,14 @@ class CreditTest extends TestCase
$ii->product_key = 'xx';
$ii->notes = 'yy';
$credit_array['line_items'] = [$ii];
$credit_array['line_items'] = [];
$credit_array['line_items'][] = (array)$ii;
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/credits', $credit_array);
])->postJson('/api/v1/credits', $credit_array);
$response->assertStatus(200);
$arr = $response->json();
@ -176,12 +178,14 @@ class CreditTest extends TestCase
$ii->product_key = 'xx';
$ii->notes = 'yy';
$credit_array['line_items'] = [$ii];
$credit_array['line_items'] = [];
$credit_array['line_items'][] = (array)$ii;
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/credits', $credit_array);
])->postJson('/api/v1/credits', $credit_array);
$response->assertStatus(200);
$arr = $response->json();
@ -581,7 +585,7 @@ class CreditTest extends TestCase
$response = $this->withHeaders([
'X-API-TOKEN' => $this->token,
])->post('/api/v1/credits/bulk', $data)
])->postJson('/api/v1/credits/bulk', $data)
->assertStatus(200);
@ -593,7 +597,7 @@ class CreditTest extends TestCase
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/credits/bulk', $data)
])->postJson('/api/v1/credits/bulk', $data)
->assertStatus(200);
$data = [
@ -604,7 +608,7 @@ class CreditTest extends TestCase
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/credits/bulk', $data)
])->postJson('/api/v1/credits/bulk', $data)
->assertStatus(200);
}
@ -698,7 +702,7 @@ class CreditTest extends TestCase
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/credits/', $credit)
])->postJson('/api/v1/credits/', $credit)
->assertStatus(200);
}
@ -733,16 +737,16 @@ class CreditTest extends TestCase
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/credits', $data);
])->postJson('/api/v1/credits', $data);
$response->assertStatus(200);
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/credits', $data);
])->postJson('/api/v1/credits', $data);
$response->assertStatus(302);
$response->assertStatus(422);
}
public function testCreditPut()
@ -780,8 +784,8 @@ class CreditTest extends TestCase
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/credits/', $data);
])->postJson('/api/v1/credits/', $data);
$response->assertStatus(302);
$response->assertStatus(422);
}
}

View File

@ -11,6 +11,7 @@
namespace Tests\Feature\EInvoice;
use Faker\Factory;
use Tests\TestCase;
use App\Models\Client;
use App\Models\Company;
@ -23,6 +24,7 @@ use App\DataMapper\Tax\TaxModel;
use App\DataMapper\ClientSettings;
use App\DataMapper\CompanySettings;
use App\Factory\CompanyUserFactory;
use App\Repositories\InvoiceRepository;
use InvoiceNinja\EInvoice\EInvoice;
use InvoiceNinja\EInvoice\Symfony\Encode;
use App\Services\EDocument\Standards\Peppol;
@ -46,6 +48,9 @@ class PeppolTest extends TestCase
protected int $iterations = 10;
public $faker;
protected function setUp(): void
{
parent::setUp();
@ -54,6 +59,8 @@ class PeppolTest extends TestCase
$this->markTestSkipped('Skip test for GH Actions');
}
$this->faker = Factory::create();
$this->makeTestData();
$this->withoutMiddleware(
@ -109,6 +116,7 @@ class PeppolTest extends TestCase
$this->company->save();
$company = $this->company;
/** @var Client $client */
$client = Client::factory()->create([
'user_id' => $this->user->id,
'company_id' => $this->company->id,
@ -121,15 +129,23 @@ class PeppolTest extends TestCase
'id_number' => $params['client_id_number'] ?? '',
]);
$client->setRelation('company', $company);
/** @var ClientContact $contact */
$contact = ClientContact::factory()->create([
'client_id' => $client->id,
'company_id' =>$client->company_id,
'user_id' => $client->user_id,
'first_name' => $this->faker->firstName(),
'last_name' => $this->faker->lastName(),
'email' => $this->faker->safeEmail()
'email' => $this->faker->safeEmail(),
'is_primary' => true,
'send_email' => true,
]);
$client->setRelation('contacts', [$contact]);
/** @var Invoice $invoice */
$invoice = \App\Models\Invoice::factory()->create([
'client_id' => $client->id,
'company_id' => $this->company->id,
@ -143,8 +159,10 @@ class PeppolTest extends TestCase
'tax_name2' => '',
'tax_rate3' => 0,
'tax_name3' => '',
'status_id' => Invoice::STATUS_DRAFT,
]);
$items = $invoice->line_items;
foreach($items as &$item)
@ -160,6 +178,9 @@ class PeppolTest extends TestCase
$invoice->line_items = array_values($items);
$invoice = $invoice->calc()->getInvoice();
$invoice->setRelation('client', $client);
$invoice->setRelation('company', $company);
return compact('company', 'client', 'invoice');
}
@ -179,7 +200,6 @@ class PeppolTest extends TestCase
'is_tax_exempt' => false,
];
$entity_data = $this->setupTestData($scenario);
$invoice = $entity_data['invoice'];
@ -257,6 +277,17 @@ class PeppolTest extends TestCase
$company->settings = $settings;
$company->save();
$invoice->setRelation('company', $company);
$invoice->setRelation('client', $entity_data['client']);
$invoice->save();
$repo = new InvoiceRepository();
$invoice = $repo->save([], $invoice);
$invoice = $invoice->service()->markSent()->save();
$this->assertGreaterThan(0, $invoice->invitations()->count());
$data = [
'entity' => 'invoices',
'entity_id' => $invoice->hashed_id
@ -511,6 +542,10 @@ class PeppolTest extends TestCase
];
$invoice->save();
$repo = new InvoiceRepository();
$invoice = $repo->save([], $invoice);
$invoice = $invoice->service()->markSent()->save();
$company = $entity_data['company'];
$settings = $company->settings;

View File

@ -0,0 +1,952 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace Tests\Feature\EInvoice\Verifactu;
use App\DataMapper\InvoiceItem;
use Tests\TestCase;
use App\Models\Client;
use App\Models\Invoice;
use App\Models\Project;
use Tests\MockAccountData;
use App\Models\Subscription;
use App\Models\ClientContact;
use App\Utils\Traits\MakesHash;
use App\Models\RecurringInvoice;
use App\Factory\InvoiceItemFactory;
use App\Helpers\Invoice\InvoiceSum;
use Illuminate\Support\Facades\Config;
use App\Repositories\InvoiceRepository;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Str;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class VerifactuApiTest extends TestCase
{
use MakesHash;
use DatabaseTransactions;
use MockAccountData;
public $faker;
protected function setUp(): void
{
parent::setUp();
$this->faker = \Faker\Factory::create();
$this->makeTestData();
}
private function buildData()
{
$item = new InvoiceItem();
$item->quantity = 1;
$item->product_key = 'product_1';
$item->notes = 'Product 1';
$item->cost = 100;
$item->discount = 0;
$item->tax_rate1 = 21;
$item->tax_name1 = 'IVA';
/** @var \App\Models\Invoice $invoice */
$invoice = Invoice::factory()->create([
'company_id' => $this->company->id,
'client_id' => $this->client->id,
'user_id' => $this->user->id,
'number' => Str::random(32),
'date' => now()->format('Y-m-d'),
'due_date' => now()->addDays(100)->format('Y-m-d'),
'status_id' => Invoice::STATUS_DRAFT,
'is_deleted' => false,
'tax_rate1' => 0,
'tax_name1' => '',
'tax_rate2' => 0,
'tax_name2' => '',
'tax_rate3' => 0,
'tax_name3' => '',
'line_items' => [$item],
'discount' => 0,
'uses_inclusive_taxes' => false,
'exchange_rate' => 1,
'partial' => 0,
'partial_due_date' => null,
'footer' => '',
]);
$invoice->backup->document_type = 'F1';
$invoice->backup->adjustable_amount = 121;
$repo = new InvoiceRepository();
$invoice = $repo->save([], $invoice);
return $invoice;
}
public function test_delete_validation_for_parent_fails_correctly()
{
$settings = $this->company->settings;
$settings->e_invoice_type = 'VERIFACTU';
$settings->is_locked = 'when_sent';
$this->company->settings = $settings;
$this->company->save();
$invoice = $this->buildData();
$invoice->service()->markSent()->save();
$invoice2 = $this->buildData();
$invoice2->backup->document_type = 'R2';
$invoice2->backup->parent_invoice_id = $invoice->hashed_id;
$invoice2->save();
$invoice2->service()->markSent()->save();
$invoice->backup->child_invoice_ids->push($invoice2->hashed_id);
$invoice->save();
$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 = [
'action' => 'delete',
'ids' => [$invoice->hashed_id]
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/invoices/bulk', $data);
$response->assertStatus(422);
$data = [
'action' => 'delete',
'ids' => [$invoice2->hashed_id]
];
sleep(1);
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/invoices/bulk', $data);
$response->assertStatus(200);
}
public function test_archive_invoice_with_no_parent()
{
$settings = $this->company->settings;
$settings->e_invoice_type = 'VERIFACTU';
$settings->is_locked = 'when_sent';
$this->company->settings = $settings;
$this->company->save();
$invoice = $this->buildData();
$invoice->service()->markSent()->save();
$data = [
'action' => 'archive',
'ids' => [$invoice->hashed_id]
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/invoices/bulk', $data);
$response->assertStatus(200);
$data = [
'action' => 'restore',
'ids' => [$invoice->hashed_id]
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/invoices/bulk', $data);
$response->assertStatus(200);
}
public function test_delete_invoice_with_parent()
{
$settings = $this->company->settings;
$settings->e_invoice_type = 'VERIFACTU';
$this->company->settings = $settings;
$this->company->save();
$invoice = $this->buildData();
$invoice->service()->markSent()->save();
$this->assertEquals(121, $invoice->amount);
$data = $invoice->toArray();
unset($data['client']);
unset($data['invitations']);
$data['client_id'] = $this->client->hashed_id;
$data['verifactu_modified'] = true;
$data['modified_invoice_id'] = $invoice->hashed_id;
$data['number'] = null;
$data['discount'] = 121;
$data['is_amount_discount'] = true;
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/invoices', $data);
$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]
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/invoices/bulk', $data);
$response->assertStatus(422);
}
public function test_delete_invoice_with_no_parent()
{
$settings = $this->company->settings;
$settings->e_invoice_type = 'VERIFACTU';
$this->company->settings = $settings;
$this->company->save();
$invoice = $this->buildData();
$invoice = $invoice->service()->markSent()->save();
$this->assertEquals('F1', $invoice->backup->document_type);
$this->assertFalse($invoice->is_deleted);
$data = [
'action' => 'delete',
'ids' => [$invoice->hashed_id]
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/invoices/bulk', $data);
$response->assertStatus(200);
$data = [
'action' => 'restore',
'ids' => [$invoice->hashed_id]
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/invoices/bulk', $data);
$response->assertStatus(422);
}
public function test_credits_never_exceed_original_invoice9()
{
$settings = $this->company->settings;
$settings->e_invoice_type = 'VERIFACTU';
$this->company->settings = $settings;
$this->company->save();
$invoice = $this->buildData();
$invoice->service()->markSent()->save();
$this->assertEquals(121, $invoice->amount);
$data = $invoice->toArray();
unset($data['client']);
unset($data['invitations']);
$data['client_id'] = $this->client->hashed_id;
$data['verifactu_modified'] = true;
$data['modified_invoice_id'] = $invoice->hashed_id;
$data['number'] = null;
$data['line_items'] = [];
$data['discount'] = 122;
$data['is_amount_discount'] = true;
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/invoices', $data);
$response->assertStatus(422);
}
public function test_credits_never_exceed_original_invoice8()
{
$settings = $this->company->settings;
$settings->e_invoice_type = 'VERIFACTU';
$this->company->settings = $settings;
$this->company->save();
$invoice = $this->buildData();
$invoice->service()->markSent()->save();
$this->assertEquals(121, $invoice->amount);
$data = $invoice->toArray();
unset($data['client']);
unset($data['invitations']);
$data['client_id'] = $this->client->hashed_id;
$data['verifactu_modified'] = true;
$data['modified_invoice_id'] = $invoice->hashed_id;
$data['number'] = null;
$data['discount'] = 121;
$data['is_amount_discount'] = true;
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/invoices', $data);
$response->assertStatus(200);
}
public function test_credits_never_exceed_original_invoice7()
{
$settings = $this->company->settings;
$settings->e_invoice_type = 'VERIFACTU';
$this->company->settings = $settings;
$this->company->save();
$invoice = $this->buildData();
$invoice->service()->markSent()->save();
$this->assertEquals(121, $invoice->amount);
$data = $invoice->toArray();
unset($data['client']);
unset($data['invitations']);
$data['client_id'] = $this->client->hashed_id;
$data['verifactu_modified'] = true;
$data['modified_invoice_id'] = $invoice->hashed_id;
$data['number'] = null;
$data['discount'] = 120;
$data['is_amount_discount'] = true;
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/invoices', $data);
$response->assertStatus(422);
}
public function test_credits_never_exceed_original_invoice6()
{
$settings = $this->company->settings;
$settings->e_invoice_type = 'VERIFACTU';
$this->company->settings = $settings;
$this->company->save();
$invoice = $this->buildData();
$invoice->service()->markSent()->save();
$this->assertEquals(121, $invoice->amount);
$invoice->line_items = [[
'quantity' => -1,
'cost' => 10,
'discount' => 0,
'tax_rate1' => 21,
'tax_name1' => 'IVA',
]];
$invoice->discount = 0;
$invoice->is_amount_discount = false;
$data = $invoice->toArray();
unset($data['client']);
unset($data['invitations']);
$data['client_id'] = $this->client->hashed_id;
$data['verifactu_modified'] = true;
$data['modified_invoice_id'] = $invoice->hashed_id;
$data['number'] = null;
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/invoices', $data);
$response->assertStatus(200);
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/invoices', $data);
$response->assertStatus(200);
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/invoices', $data);
$response->assertStatus(200);
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/invoices', $data);
$response->assertStatus(200);
}
public function test_credits_never_exceed_original_invoice5()
{
$settings = $this->company->settings;
$settings->e_invoice_type = 'VERIFACTU';
$this->company->settings = $settings;
$this->company->save();
$invoice = $this->buildData();
$invoice->service()->markSent()->save();
$this->assertEquals(121, $invoice->amount);
$invoice->line_items = [[
'quantity' => -5,
'cost' => 100,
'discount' => 0,
'tax_rate1' => 21,
'tax_name1' => 'IVA',
]];
$invoice->discount = 0;
$invoice->is_amount_discount = false;
$data = $invoice->toArray();
unset($data['client']);
$data['client_id'] = $this->client->hashed_id;
$data['verifactu_modified'] = true;
$data['modified_invoice_id'] = $invoice->hashed_id;
$data['number'] = null;
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/invoices', $data);
$response->assertStatus(422);
}
public function test_credits_never_exceed_original_invoice4()
{
$settings = $this->company->settings;
$settings->e_invoice_type = 'VERIFACTU';
$this->company->settings = $settings;
$this->company->save();
$invoice = $this->buildData();
$invoice->service()->markSent()->save();
$this->assertEquals(121, $invoice->amount);
$data = $invoice->toArray();
unset($data['client']);
unset($data['company']);
unset($data['invitations']);
$data['client_id'] = $this->client->hashed_id;
$data['verifactu_modified'] = true;
$data['modified_invoice_id'] = $invoice->hashed_id;
$data['number'] = null;
$data['line_items'] = [[
'quantity' => -1,
'cost' => 100,
'discount' => 0,
'tax_rate1' => 21,
'tax_name1' => 'IVA',
]];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/invoices', $data);
$response->assertStatus(200);
}
public function test_credits_never_exceed_original_invoice3()
{
$settings = $this->company->settings;
$settings->e_invoice_type = 'VERIFACTU';
$this->company->settings = $settings;
$this->company->save();
$invoice = $this->buildData();
$invoice->service()->markSent()->save();
$invoice->line_items = [];
$invoice->discount = 500;
$invoice->is_amount_discount = true;
$data = $invoice->toArray();
$data['client_id'] = $this->client->hashed_id;
$data['verifactu_modified'] = true;
$data['modified_invoice_id'] = $invoice->hashed_id;
$data['number'] = null;
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/invoices', $data);
$response->assertStatus(422);
}
public function test_credits_never_exceed_original_invoice2()
{
$settings = $this->company->settings;
$settings->e_invoice_type = 'VERIFACTU';
$this->company->settings = $settings;
$this->company->save();
$invoice = $this->buildData();
$invoice->service()->markSent()->save();
$invoice->line_items = [];
$invoice->discount = 500;
$invoice->is_amount_discount = true;
$data = $invoice->toArray();
$data['client_id'] = $this->client->hashed_id;
$data['verifactu_modified'] = true;
$data['modified_invoice_id'] = $invoice->hashed_id;
$data['number'] = null;
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/invoices', $data);
$response->assertStatus(422);
}
public function test_credits_never_exceed_original_invoice()
{
$settings = $this->company->settings;
$settings->e_invoice_type = 'VERIFACTU';
$this->company->settings = $settings;
$this->company->save();
$invoice = $this->buildData();
$invoice->service()->markSent()->save();
// $invoice->line_items = [];
$invoice->discount = 5;
$invoice->is_amount_discount = true;
$data = $invoice->toArray();
$data['client_id'] = $this->client->hashed_id;
$data['verifactu_modified'] = true;
$data['modified_invoice_id'] = $invoice->hashed_id;
$data['number'] = null;
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/invoices', $data);
$response->assertStatus(422);
}
public function test_verifactu_amount_check()
{
$settings = $this->company->settings;
$settings->e_invoice_type = 'VERIFACTU';
$this->company->settings = $settings;
$this->company->save();
$invoice = $this->buildData();
$invoice->line_items = [];
$invoice->discount = 500;
$invoice->is_amount_discount = true;
$data = $invoice->toArray();
$data['client_id'] = $this->client->hashed_id;
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/invoices', $data);
$response->assertStatus(422);
}
public function test_create_modification_invoice()
{
$this->assertEquals(10, $this->client->balance);
$settings = $this->company->settings;
$settings->e_invoice_type = 'VERIFACTU';
$this->company->settings = $settings;
$this->company->save();
$invoice = $this->buildData();
$invoice->service()->markSent()->save();
$this->assertEquals(121, $invoice->amount);
$this->assertEquals(121, $invoice->balance);
$this->assertEquals(131, $this->client->fresh()->balance);
$invoice2 = $this->buildData();
$items = $invoice2->line_items;
$items[] = $items[0];
$invoice2->line_items = $items;
$invoice2 = $invoice2->calc()->getInvoice();
$invoice2->service()->markSent()->save();
$this->assertEquals(373, $this->client->fresh()->balance);
$data = $invoice2->toArray();
$data['verifactu_modified'] = true;
$data['modified_invoice_id'] = $invoice->hashed_id;
$data['number'] = null;
$data['client_id'] = $this->client->hashed_id;
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/invoices', $data);
$response->assertStatus(422);
}
public function test_create_modification_invoice_validation_fails()
{
$invoice = $this->buildData();;
$data = $invoice->toArray();
$data['verifactu_modified'] = true;
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/invoices', $data);
$response->assertStatus(422);
}
public function test_create_modification_invoice_validation_fails2()
{
$invoice = $this->buildData();;
$data = $invoice->toArray();
$data['verifactu_modified'] = true;
$data['modified_invoice_id'] = "XXX";
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/invoices', $data);
$response->assertStatus(422);
}
public function test_create_modification_invoice_validation_fails3()
{
$invoice = $this->buildData();;
$invoice2 = $this->buildData();
$invoice2->service()->markPaid()->save();
$data = $invoice->toArray();
$data['verifactu_modified'] = true;
$data['modified_invoice_id'] = $invoice2->hashed_id;
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/invoices', $data);
$response->assertStatus(422);
}
public function test_create_modification_invoice_validation_fails4()
{
$settings = $this->company->settings;
$settings->e_invoice_type = 'VERIFACTU';
$this->company->settings = $settings;
$this->company->save();
$invoice = $this->buildData();;
$invoice2 = $this->buildData();
$invoice2->service()->markSent()->save();
$data = $invoice->toArray();
$data['verifactu_modified'] = true;
$data['modified_invoice_id'] = $invoice2->hashed_id;
$data['client_id'] = $this->client->hashed_id;
$data['number'] = null;
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/invoices', $data);
$response->assertStatus(422);
}
public function test_cancel_invoice_response()
{
$invoice = $this->buildData();
$invoice->service()->markSent()->save();
$this->assertEquals($invoice->status_id, Invoice::STATUS_SENT);
$this->assertEquals($invoice->balance, 121);
$this->assertEquals($invoice->amount, 121);
$settings = $this->company->settings;
$settings->e_invoice_type = 'VERIFACTU';
$this->company->settings = $settings;
$this->company->save();
$data = [
'action' => 'cancel',
'ids' => [$invoice->hashed_id],
'reason' => 'R3'
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/invoices/bulk', $data);
$response->assertStatus(200);
$arr = $response->json();
$this->assertEquals($arr['data'][0]['status_id'], Invoice::STATUS_CANCELLED);
$this->assertEquals($arr['data'][0]['balance'], 121);
$this->assertEquals($arr['data'][0]['amount'], 121);
$this->assertNotNull($arr['data'][0]['backup']['child_invoice_ids'][0]);
$credit_invoice = Invoice::find($this->decodePrimaryKey($arr['data'][0]['backup']['child_invoice_ids'][0]));
$this->assertNotNull($credit_invoice);
$this->assertEquals($credit_invoice->status_id, Invoice::STATUS_SENT);
$this->assertEquals($credit_invoice->balance, -121);
$this->assertEquals($credit_invoice->amount, -121);
$this->assertEquals($credit_invoice->backup->parent_invoice_id, $invoice->hashed_id);
$this->assertEquals($credit_invoice->backup->parent_invoice_number, $invoice->number);
}
public function test_restore_invoice_validation()
{
$settings = $this->company->settings;
$settings->e_invoice_type = 'VERIFACTU';
$this->company->settings = $settings;
$this->company->save();
$data = [
'action' => 'delete',
'ids' => [$this->invoice->hashed_id]
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/invoices/bulk', $data);
$response->assertStatus(200);
$arr = $response->json();
$this->assertTrue($arr['data'][0]['is_deleted']);
$data = [
'action' => 'restore',
'ids' => [$this->invoice->hashed_id]
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/invoices/bulk', $data);
$response->assertStatus(422);
}
public function test_restore_invoice_that_is_archived()
{
$settings = $this->company->settings;
$settings->e_invoice_type = 'VERIFACTU';
$this->company->settings = $settings;
$this->company->save();
$data = [
'action' => 'archive',
'ids' => [$this->invoice->hashed_id]
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/invoices/bulk', $data);
$response->assertStatus(200);
$arr = $response->json();
$this->assertFalse($arr['data'][0]['is_deleted']);
$data = [
'action' => 'restore',
'ids' => [$this->invoice->hashed_id]
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/invoices/bulk', $data);
$response->assertStatus(200);
}
/**
* test_update_company_settings
*
* Verifactu we do not allow the user to change from the verifactu system nor, do we allow changing the locking feature of invoices
* @return void
*/
public function test_update_company_settings()
{
// Ensure LARAVEL_START is defined for the middleware
if (!defined('LARAVEL_START')) {
define('LARAVEL_START', microtime(true));
}
Config::set('ninja.environment', 'hosted');
$settings = $this->company->settings;
$settings->e_invoice_type = 'VERIFACTU';
$this->company->settings = $settings;
$this->company->save();
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->putJson('/api/v1/companies/'.$this->company->hashed_id, $this->company->toArray())
->assertStatus(200);
$settings = $this->company->settings;
$settings->e_invoice_type = 'Facturae_3.2.2';
$this->company->settings = $settings;
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->putJson('/api/v1/companies/'.$this->company->hashed_id, $this->company->toArray())
->assertStatus(200);
$arr = $response->json();
$this->assertEquals($arr['data']['settings']['e_invoice_type'], 'VERIFACTU');
$this->assertEquals($arr['data']['settings']['lock_invoices'], 'when_sent');
}
}

Some files were not shown because too many files have changed in this diff Show More