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.");
|
$fail("Cannot create a modification invoice where a payment has been made.");
|
||||||
} elseif($invoice->status_id === Invoice::STATUS_CANCELLED ) {
|
} elseif($invoice->status_id === Invoice::STATUS_CANCELLED ) {
|
||||||
$fail("Cannot create a modification invoice for a cancelled invoice.");
|
$fail("Cannot create a modification invoice for a cancelled invoice.");
|
||||||
} elseif($invoice->status_id === Invoice::STATUS_REPLACED) {
|
// } elseif($invoice->status_id === Invoice::STATUS_REPLACED) {
|
||||||
$fail("Cannot create a modification invoice for a replaced invoice.");
|
// $fail("Cannot create a modification invoice for a replaced invoice.");
|
||||||
} elseif($invoice->status_id === Invoice::STATUS_REVERSED) {
|
} elseif($invoice->status_id === Invoice::STATUS_REVERSED) {
|
||||||
$fail("Cannot create a modification invoice for a reversed invoice.");
|
$fail("Cannot create a modification invoice for a reversed invoice.");
|
||||||
} elseif ($invoice->status_id !== Invoice::STATUS_SENT) {
|
// } elseif ($invoice->status_id !== Invoice::STATUS_SENT) {
|
||||||
$fail("Cannot create a modification invoice.");
|
// $fail("Cannot create a modification invoice.");
|
||||||
} elseif($invoice->amount <= 0){
|
// } elseif($invoice->amount <= 0){
|
||||||
$fail("Cannot create a modification invoice for an invoice with an amount less than 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 InvoiceNinja\EInvoice\EInvoice;
|
||||||
use App\Utils\Traits\NumberFormatter;
|
use App\Utils\Traits\NumberFormatter;
|
||||||
use App\Helpers\Invoice\InvoiceSumInclusive;
|
use App\Helpers\Invoice\InvoiceSumInclusive;
|
||||||
use App\Services\EDocument\Gateway\Qvalia\Qvalia;
|
|
||||||
use InvoiceNinja\EInvoice\Models\Peppol\ItemType\Item;
|
use InvoiceNinja\EInvoice\Models\Peppol\ItemType\Item;
|
||||||
use App\Services\EDocument\Gateway\Storecove\Storecove;
|
use App\Services\EDocument\Gateway\Storecove\Storecove;
|
||||||
use InvoiceNinja\EInvoice\Models\Peppol\PartyType\Party;
|
use InvoiceNinja\EInvoice\Models\Peppol\PartyType\Party;
|
||||||
|
|
@ -139,9 +138,9 @@ class Peppol extends AbstractService
|
||||||
|
|
||||||
private EInvoice $e;
|
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';
|
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;
|
$items = $replicated_invoice->line_items;
|
||||||
|
|
||||||
foreach($items as &$item) {
|
foreach($items as &$item) {
|
||||||
$item->quantity = $item->quantity * -1;
|
$item->quantity = $item->quantity * -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
$replicated_invoice->line_items = $items;
|
$replicated_invoice->line_items = $items;
|
||||||
|
$replicated_invoice->backup->cancelled_invoice_id = $this->invoice->hashed_id;
|
||||||
$backup = new \App\DataMapper\InvoiceBackup(
|
$replicated_invoice->backup->cancelled_invoice_number = $this->invoice->number;
|
||||||
cancelled_invoice_id: $this->invoice->hashed_id,
|
$replicated_invoice->backup->cancellation_reason = $this->reason ?? 'R3';
|
||||||
cancelled_invoice_number: $this->invoice->number,
|
|
||||||
cancellation_reason: $this->reason ?? 'R3'
|
|
||||||
);
|
|
||||||
|
|
||||||
$replicated_invoice->backup = $backup;
|
|
||||||
|
|
||||||
$invoice_repository = new InvoiceRepository();
|
$invoice_repository = new InvoiceRepository();
|
||||||
$replicated_invoice = $invoice_repository->save([], $replicated_invoice);
|
$replicated_invoice = $invoice_repository->save([], $replicated_invoice);
|
||||||
$replicated_invoice->service()->markSent()->sendVerifactu()->save();
|
$replicated_invoice->service()->markSent()->sendVerifactu()->save();
|
||||||
|
|
||||||
$old_backup = new \App\DataMapper\InvoiceBackup(
|
$this->invoice->backup->credit_invoice_id = $replicated_invoice->hashed_id;
|
||||||
credit_invoice_id: $replicated_invoice->hashed_id,
|
$this->invoice->backup->credit_invoice_number = $replicated_invoice->number;
|
||||||
credit_invoice_number: $replicated_invoice->number,
|
$this->invoice->backup->cancellation_reason = $this->reason ?? 'R3';
|
||||||
cancellation_reason: $this->reason ?? 'R3'
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->invoice->backup = $old_backup;
|
|
||||||
$this->invoice->saveQuietly();
|
$this->invoice->saveQuietly();
|
||||||
$this->invoice->fresh();
|
$this->invoice->fresh();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -729,7 +729,7 @@ class InvoiceService
|
||||||
$this->invoice->backup->replaced_invoice_id = $modified_invoice->hashed_id;
|
$this->invoice->backup->replaced_invoice_id = $modified_invoice->hashed_id;
|
||||||
$this->invoice->saveQuietly();
|
$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();
|
$this->sendVerifactu();
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
|
|
|
||||||
|
|
@ -255,8 +255,6 @@ return [
|
||||||
'upload_extensions' => env('ADDITIONAL_UPLOAD_EXTENSIONS', ''),
|
'upload_extensions' => env('ADDITIONAL_UPLOAD_EXTENSIONS', ''),
|
||||||
'storecove_api_key' => env('STORECOVE_API_KEY', false),
|
'storecove_api_key' => env('STORECOVE_API_KEY', false),
|
||||||
'storecove_email_catchall' => env('STORECOVE_CATCHALL_EMAIL',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_x_alignment' => env('PDF_PAGE_NUMBER_X', 0),
|
||||||
'pdf_page_numbering_y_alignment' => env('PDF_PAGE_NUMBER_Y', -6),
|
'pdf_page_numbering_y_alignment' => env('PDF_PAGE_NUMBER_Y', -6),
|
||||||
'hosted_einvoice_secret' => env('HOSTED_EINVOICE_SECRET', null),
|
'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()
|
public function test_create_modification_invoice_validation_fails()
|
||||||
{
|
{
|
||||||
$invoice = $this->buildData();;
|
$invoice = $this->buildData();;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue