Update for ivnoice backup casting
This commit is contained in:
parent
67df175525
commit
94b628b6eb
|
|
@ -51,14 +51,14 @@ class CanGenerateModificationInvoice implements ValidationRule
|
|||
$fail("Cannot create a modification invoice where a payment has been made.");
|
||||
} elseif($invoice->status_id === Invoice::STATUS_CANCELLED ) {
|
||||
$fail("Cannot create a modification invoice for a cancelled invoice.");
|
||||
} elseif($invoice->status_id === Invoice::STATUS_REPLACED) {
|
||||
$fail("Cannot create a modification invoice for a replaced invoice.");
|
||||
// } elseif($invoice->status_id === Invoice::STATUS_REPLACED) {
|
||||
// $fail("Cannot create a modification invoice for a replaced invoice.");
|
||||
} elseif($invoice->status_id === Invoice::STATUS_REVERSED) {
|
||||
$fail("Cannot create a modification 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.");
|
||||
// } 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.");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,180 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\EDocument\Standards\Verifactu\Examples;
|
||||
|
||||
use App\Services\EDocument\Standards\Verifactu\Models\Invoice;
|
||||
use App\Services\EDocument\Standards\Verifactu\Models\IDFactura;
|
||||
use App\Services\EDocument\Standards\Verifactu\Models\FacturaRectificativa;
|
||||
|
||||
/**
|
||||
* Example demonstrating how to create R1 (rectificative) invoices
|
||||
* with proper conditional logic for ImporteRectificacion
|
||||
*/
|
||||
class R1InvoiceExample
|
||||
{
|
||||
/**
|
||||
* Example 1: Create a substitutive rectification invoice (R1 with TipoRectificativa = 'S')
|
||||
* This requires ImporteRectificacion to be set
|
||||
*/
|
||||
public static function createSubstitutiveRectification(): Invoice
|
||||
{
|
||||
$invoice = new Invoice();
|
||||
|
||||
// Set basic invoice information
|
||||
$invoice->setIdVersion('1.0')
|
||||
->setIdFactura(new IDFactura('A39200019', 'TEST0033343444', '09-08-2025'))
|
||||
->setNombreRazonEmisor('CERTIFICADO FISICA PRUEBAS')
|
||||
->setDescripcionOperacion('Rectificación sustitutiva de factura anterior')
|
||||
->setCuotaTotal(46.08)
|
||||
->setImporteTotal(141.08)
|
||||
->setFechaHoraHusoGenRegistro('2025-08-09T22:33:13+02:00')
|
||||
->setTipoHuella('01')
|
||||
->setHuella('C8053880DA04439862AEE429EB7AF6CF9F2D00141896B0646ED5BF7A2C482623');
|
||||
|
||||
// Make it a substitutive rectification (R1 with S type)
|
||||
// This automatically sets TipoFactura to 'R1' and TipoRectificativa to 'S'
|
||||
$invoice->makeSubstitutiveRectificationWithAmount(
|
||||
100.00, // ImporteRectificacion - required for substitutive rectifications
|
||||
'Rectificación sustitutiva de factura anterior'
|
||||
);
|
||||
|
||||
// Set up the rectified invoice information
|
||||
$invoice->setRectifiedInvoice(
|
||||
'A39200019', // NIF of rectified invoice
|
||||
'TEST0033343443', // Series number of rectified invoice
|
||||
'09-08-2025' // Date of rectified invoice
|
||||
);
|
||||
|
||||
return $invoice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Example 2: Create a complete rectification invoice (R1 with TipoRectificativa = 'I')
|
||||
* ImporteRectificacion is optional but recommended
|
||||
*/
|
||||
public static function createCompleteRectification(): Invoice
|
||||
{
|
||||
$invoice = new Invoice();
|
||||
|
||||
// Set basic invoice information
|
||||
$invoice->setIdVersion('1.0')
|
||||
->setIdFactura(new IDFactura('A39200019', 'TEST0033343445', '09-08-2025'))
|
||||
->setNombreRazonEmisor('CERTIFICADO FISICA PRUEBAS')
|
||||
->setDescripcionOperacion('Rectificación completa de factura anterior')
|
||||
->setCuotaTotal(46.08)
|
||||
->setImporteTotal(141.08)
|
||||
->setFechaHoraHusoGenRegistro('2025-08-09T22:33:13+02:00')
|
||||
->setTipoHuella('01')
|
||||
->setHuella('C8053880DA04439862AEE429EB7AF6CF9F2D00141896B0646ED5BF7A2C482623');
|
||||
|
||||
// Make it a complete rectification (R1 with I type)
|
||||
// ImporteRectificacion is optional for complete rectifications
|
||||
$invoice->makeCompleteRectification('Rectificación completa de factura anterior');
|
||||
|
||||
// Optionally set ImporteRectificacion (recommended but not mandatory)
|
||||
$invoice->setImporteRectificacion(50.00);
|
||||
|
||||
// Set up the rectified invoice information
|
||||
$invoice->setRectifiedInvoice(
|
||||
'A39200019', // NIF of rectified invoice
|
||||
'TEST0033343443', // Series number of rectified invoice
|
||||
'09-08-2025' // Date of rectified invoice
|
||||
);
|
||||
|
||||
return $invoice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Example 3: Create a substitutive rectification with automatic ImporteRectificacion calculation
|
||||
*/
|
||||
public static function createSubstitutiveRectificationWithAutoCalculation(): Invoice
|
||||
{
|
||||
$invoice = new Invoice();
|
||||
|
||||
// Set basic invoice information
|
||||
$invoice->setIdVersion('1.0')
|
||||
->setIdFactura(new IDFactura('A39200019', 'TEST0033343446', '09-08-2025'))
|
||||
->setNombreRazonEmisor('CERTIFICADO FISICA PRUEBAS')
|
||||
->setDescripcionOperacion('Rectificación sustitutiva con cálculo automático')
|
||||
->setCuotaTotal(46.08)
|
||||
->setImporteTotal(141.08)
|
||||
->setFechaHoraHusoGenRegistro('2025-08-09T22:33:13+02:00')
|
||||
->setTipoHuella('01')
|
||||
->setHuella('C8053880DA04439862AEE429EB7AF6CF9F2D00141896B0646ED5BF7A2C482623');
|
||||
|
||||
// Calculate ImporteRectificacion automatically from the difference
|
||||
$originalAmount = 200.00; // Original invoice amount
|
||||
$newAmount = 141.08; // New invoice amount
|
||||
$invoice->makeSubstitutiveRectificationFromDifference(
|
||||
$originalAmount,
|
||||
$newAmount,
|
||||
'Rectificación sustitutiva con cálculo automático'
|
||||
);
|
||||
|
||||
// Set up the rectified invoice information
|
||||
$invoice->setRectifiedInvoice(
|
||||
'A39200019', // NIF of rectified invoice
|
||||
'TEST0033343443', // Series number of rectified invoice
|
||||
'09-08-2025' // Date of rectified invoice
|
||||
);
|
||||
|
||||
return $invoice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Example 4: Step-by-step creation of a substitutive rectification
|
||||
*/
|
||||
public static function createSubstitutiveRectificationStepByStep(): Invoice
|
||||
{
|
||||
$invoice = new Invoice();
|
||||
|
||||
// Step 1: Set basic invoice information
|
||||
$invoice->setIdVersion('1.0')
|
||||
->setIdFactura(new IDFactura('A39200019', 'TEST0033343447', '09-08-2025'))
|
||||
->setNombreRazonEmisor('CERTIFICADO FISICA PRUEBAS')
|
||||
->setDescripcionOperacion('Rectificación sustitutiva paso a paso')
|
||||
->setCuotaTotal(46.08)
|
||||
->setImporteTotal(141.08)
|
||||
->setFechaHoraHusoGenRegistro('2025-08-09T22:33:13+02:00')
|
||||
->setTipoHuella('01')
|
||||
->setHuella('C8053880DA04439862AEE429EB7AF6CF9F2D00141896B0646ED5BF7A2C482623');
|
||||
|
||||
// Step 2: Set invoice type to rectificative
|
||||
$invoice->setTipoFactura(Invoice::TIPO_FACTURA_RECTIFICATIVA);
|
||||
|
||||
// Step 3: Set rectification type to substitutive
|
||||
$invoice->setTipoRectificativa(Invoice::TIPO_RECTIFICATIVA_SUSTITUTIVA);
|
||||
|
||||
// Step 4: Set ImporteRectificacion (mandatory for substitutive)
|
||||
$invoice->setImporteRectificacion(100.00);
|
||||
|
||||
// Step 5: Set up the rectified invoice information
|
||||
$invoice->setRectifiedInvoice(
|
||||
'A39200019', // NIF of rectified invoice
|
||||
'TEST0033343443', // Series number of rectified invoice
|
||||
'09-08-2025' // Date of rectified invoice
|
||||
);
|
||||
|
||||
return $invoice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and generate XML for an R1 invoice
|
||||
*/
|
||||
public static function generateXml(Invoice $invoice): string
|
||||
{
|
||||
try {
|
||||
// Validate the invoice first
|
||||
$invoice->validate();
|
||||
|
||||
// Generate XML
|
||||
$xml = $invoice->toXmlString();
|
||||
|
||||
return $xml;
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
throw new \RuntimeException('Invoice validation failed: ' . $e->getMessage());
|
||||
} catch (\Exception $e) {
|
||||
throw new \RuntimeException('XML generation failed: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
<?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\Utils\Ninja;
|
||||
use App\Models\Invoice;
|
||||
use App\Libraries\MultiDB;
|
||||
use App\Models\Company;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||
use Mail;
|
||||
use Illuminate\Mail\Mailables\Address;
|
||||
|
||||
class SendToAeat implements ShouldQueue
|
||||
{
|
||||
use Dispatchable;
|
||||
use InteractsWithQueue;
|
||||
use Queueable;
|
||||
use SerializesModels;
|
||||
|
||||
public $tries = 5;
|
||||
|
||||
public $deleteWhenMissingModels = true;
|
||||
|
||||
/**
|
||||
* Modification Invoices - (modify) Generates a F3 document which replaces the original invoice. And becomes the new 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()
|
||||
{
|
||||
MultiDB::setDB($this->company->db);
|
||||
|
||||
$invoice = Invoice::withTrashed()->find($this->invoice_id);
|
||||
|
||||
}
|
||||
|
||||
public function middleware()
|
||||
{
|
||||
return [new WithoutOverlapping("send_to_aeat_{$this->company->company_key}")];
|
||||
}
|
||||
|
||||
public function failed($exception = null)
|
||||
{
|
||||
nlog($exception);
|
||||
}
|
||||
}
|
||||
|
|
@ -81,31 +81,23 @@ class HandleCancellation extends AbstractService
|
|||
|
||||
$items = $replicated_invoice->line_items;
|
||||
|
||||
foreach($items as &$item) {
|
||||
$item->quantity = $item->quantity * -1;
|
||||
}
|
||||
foreach($items as &$item) {
|
||||
$item->quantity = $item->quantity * -1;
|
||||
}
|
||||
|
||||
$replicated_invoice->line_items = $items;
|
||||
|
||||
$backup = new \App\DataMapper\InvoiceBackup(
|
||||
cancelled_invoice_id: $this->invoice->hashed_id,
|
||||
cancelled_invoice_number: $this->invoice->number,
|
||||
cancellation_reason: $this->reason ?? 'R3'
|
||||
);
|
||||
|
||||
$replicated_invoice->backup = $backup;
|
||||
$replicated_invoice->backup->cancelled_invoice_id = $this->invoice->hashed_id;
|
||||
$replicated_invoice->backup->cancelled_invoice_number = $this->invoice->number;
|
||||
$replicated_invoice->backup->cancellation_reason = $this->reason ?? 'R3';
|
||||
|
||||
$invoice_repository = new InvoiceRepository();
|
||||
$replicated_invoice = $invoice_repository->save([], $replicated_invoice);
|
||||
$replicated_invoice->service()->markSent()->sendVerifactu()->save();
|
||||
|
||||
$old_backup = new \App\DataMapper\InvoiceBackup(
|
||||
credit_invoice_id: $replicated_invoice->hashed_id,
|
||||
credit_invoice_number: $replicated_invoice->number,
|
||||
cancellation_reason: $this->reason ?? 'R3'
|
||||
);
|
||||
$this->invoice->backup->credit_invoice_id = $replicated_invoice->hashed_id;
|
||||
$this->invoice->backup->credit_invoice_number = $replicated_invoice->number;
|
||||
$this->invoice->backup->cancellation_reason = $this->reason ?? 'R3';
|
||||
|
||||
$this->invoice->backup = $old_backup;
|
||||
$this->invoice->saveQuietly();
|
||||
$this->invoice->fresh();
|
||||
|
||||
|
|
|
|||
|
|
@ -729,7 +729,7 @@ class InvoiceService
|
|||
$this->invoice->backup->replaced_invoice_id = $modified_invoice->hashed_id;
|
||||
$this->invoice->saveQuietly();
|
||||
|
||||
$this->invoice->client->service()->updateBalance(round(($modified_invoice->amount - $this->invoice->amount), 2));
|
||||
$this->invoice->client->service()->updateBalance(round(($this->invoice->amount - $modified_invoice->amount), 2));
|
||||
$this->sendVerifactu();
|
||||
|
||||
return $this;
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -91,6 +91,84 @@ class VerifactuApiTest extends TestCase
|
|||
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
$this->assertNull($invoice->backup->modified_invoice_id);
|
||||
|
||||
$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(200);
|
||||
|
||||
$arr = $response->json();
|
||||
|
||||
$this->assertEquals($arr['data']['status_id'], Invoice::STATUS_SENT);
|
||||
$this->assertEquals($arr['data']['amount'], 242);
|
||||
$this->assertEquals($arr['data']['balance'], 242);
|
||||
$this->assertEquals($arr['data']['backup']['replaced_invoice_id'], $invoice->hashed_id);
|
||||
|
||||
$invoice = $invoice->fresh();
|
||||
|
||||
$this->assertEquals(Invoice::STATUS_REPLACED, $invoice->status_id);
|
||||
$this->assertEquals($arr['data']['id'], $invoice->backup->modified_invoice_id);
|
||||
|
||||
$this->assertEquals(615, $this->client->fresh()->balance);
|
||||
|
||||
//now create another modification invoice reducing the amounts
|
||||
|
||||
$data = $invoice2->toArray();
|
||||
$data['verifactu_modified'] = true;
|
||||
$data['modified_invoice_id'] = $arr['data']['id'];
|
||||
$data['number'] = null;
|
||||
$data['client_id'] = $this->client->hashed_id;
|
||||
$data['line_items'] = $invoice2->line_items;
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'X-API-SECRET' => config('ninja.api_secret'),
|
||||
'X-API-TOKEN' => $this->token,
|
||||
])->postJson('/api/v1/invoices', $data);
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
$this->assertEquals(494, $this->client->fresh()->balance);
|
||||
|
||||
}
|
||||
|
||||
public function test_create_modification_invoice_validation_fails()
|
||||
{
|
||||
$invoice = $this->buildData();;
|
||||
|
|
|
|||
Loading…
Reference in New Issue