Compare commits
78 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
29bfbaf644 | |
|
|
27dd9907f3 | |
|
|
5a188ac355 | |
|
|
cfc41eb29a | |
|
|
e58f19f593 | |
|
|
e8336c85d7 | |
|
|
117709e551 | |
|
|
70dea557f5 | |
|
|
084f0fea25 | |
|
|
256555c952 | |
|
|
9b98d45d3b | |
|
|
c3b5a972a1 | |
|
|
7e1a6bc1c7 | |
|
|
0a7744a70e | |
|
|
1252cdf7ae | |
|
|
af926a394c | |
|
|
c5c9c4325e | |
|
|
3d3b5f6938 | |
|
|
ff92756dbc | |
|
|
a141ca1549 | |
|
|
bf8041ab7c | |
|
|
8bc1513591 | |
|
|
63e6f75a24 | |
|
|
5482f44bea | |
|
|
81ec3986ca | |
|
|
1a3badf748 | |
|
|
c7e79fe673 | |
|
|
8d23ba14d4 | |
|
|
1836ccc434 | |
|
|
94b628b6eb | |
|
|
67df175525 | |
|
|
47f33c8691 | |
|
|
6a0fff10ae | |
|
|
a447b6a20b | |
|
|
f7961ecb61 | |
|
|
37c74ee18c | |
|
|
555eb80018 | |
|
|
1e8727c4a4 | |
|
|
74f71d61d6 | |
|
|
8a137329d4 | |
|
|
c02c87765b | |
|
|
7393360db3 | |
|
|
f7055b516e | |
|
|
ee775e58a0 | |
|
|
5ff70dbeae | |
|
|
3791469c31 | |
|
|
1a86d5445b | |
|
|
442ff42ceb | |
|
|
b94316dbed | |
|
|
14fd4063f5 | |
|
|
97f2e70f5d | |
|
|
edd0de38ca | |
|
|
d53e1012af | |
|
|
5895c1b0ed | |
|
|
aa918f7ec0 | |
|
|
33078ee86c | |
|
|
6c8c270c2f | |
|
|
5afd3b85bc | |
|
|
dab787c3ae | |
|
|
03a39f33b8 | |
|
|
cbc5cb5f9b | |
|
|
4127eb32f9 | |
|
|
bf5359cb72 | |
|
|
d42735f2ee | |
|
|
ea663394b1 | |
|
|
b93ec6dd93 | |
|
|
a431dd43d4 | |
|
|
1c5c568251 | |
|
|
604ba82f8f | |
|
|
c8e0cdd090 | |
|
|
54ba4349f6 | |
|
|
0865ab3c3e | |
|
|
fc8cb7af36 | |
|
|
46de411160 | |
|
|
ff50e3c9d9 | |
|
|
fdf7d2d3cf | |
|
|
dd60eb3b58 | |
|
|
1f4fae314c |
|
|
@ -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,
|
||||
])
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -41,7 +41,7 @@ class EInvoiceController extends BaseController
|
|||
*/
|
||||
public function validateEntity(ValidateEInvoiceRequest $request)
|
||||
{
|
||||
$el = new EntityLevel();
|
||||
$el = $request->getValidatorClass();
|
||||
|
||||
$data = [];
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'])) {
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
// }
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()],
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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'));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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}");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
}
|
||||
|
|
@ -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'
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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';
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -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>";
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 &&
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
],
|
||||
];
|
||||
|
|
@ -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
|
||||
{
|
||||
//
|
||||
}
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in New Issue