Update for ivnoice backup casting

This commit is contained in:
David Bomba 2025-08-12 11:59:44 +10:00
parent 67df175525
commit 94b628b6eb
12 changed files with 177 additions and 756 deletions

View File

@ -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.");
}
}

View File

@ -1,93 +0,0 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Services\EDocument\Gateway\Qvalia;
class Invoice
{
public function __construct(public Qvalia $qvalia)
{
}
// Methods
/**
* status
*
* @param string $legal_entity_id
* @param string $integration_id
* @return mixed
*/
// {
// "status": "",
// "data": {
// "message": "",
// "status": {
// "document_id": "",
// "order_number": "",
// "payment_reference": "",
// "credit_note": "",
// "reminder": "",
// "status": "",
// "sent_at": "",
// "paid_at": "",
// "cancelled_at": "",
// "send_method": ""
// }
// }
// }
/**
* status
*
* @param string $legal_entity_id
* @param string $integration_id
* @return mixed
*/
public function status(string $legal_entity_id, string $integration_id)
{
$uri = "/account/{$legal_entity_id}/action/invoice/outgoing/status/{$integration_id}";
$r = $this->qvalia->httpClient($uri, (\App\Enum\HttpVerb::GET)->value, []);
return $r->object();
}
/**
* send
*
* @param string $legal_entity_id
* @param string $document
* @return mixed
*/
public function send(string $legal_entity_id, string $document)
{
// Set Headers
// Either "application/json" (default) or "application/xml"
$headers = [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
// 'Content-Type' => 'application/xml',
];
$data = [
'Invoice' => $document
];
$uri = "/transaction/{$legal_entity_id}/invoices/outgoing";
$r = $this->qvalia->httpClient($uri, (\App\Enum\HttpVerb::POST)->value, $data, $headers);
return $r->object();
}
}

View File

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

View File

@ -1,143 +0,0 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Services\EDocument\Gateway\Qvalia;
class Partner
{
private string $partner_number;
public function __construct(public Qvalia $qvalia)
{
$this->partner_number = config('ninja.qvalia_partner_number');
}
/**
* getAccount
*
* Get Partner Account Object
* @return mixed
*/
public function getAccount()
{
$uri = "/partner/{$this->partner_number}/account";
$r = $this->qvalia->httpClient($uri, (\App\Enum\HttpVerb::GET)->value, []);
return $r->object();
}
/**
* getPeppolId
*
* Get information on a peppol ID
* @param string $id
* @return mixed
*/
public function getPeppolId(string $id)
{
$uri = "/partner/{$this->partner_number}/peppol/lookup/{$id}";
$uri = "/partner/{$this->partner_number}/account";
$r = $this->qvalia->httpClient($uri, (\App\Enum\HttpVerb::GET)->value, []);
return $r->object();
}
/**
* getAccountId
*
* Get information on a Invoice Ninja Peppol Client Account
* @param string $id
* @return mixed
*/
public function getAccountId(string $id)
{
$uri = "/partner/{$this->partner_number}/account/{$id}";
}
/**
* createAccount
*
* Create a new account for the partner
* @param array $data
* @return mixed
*/
public function createAccount(array $data)
{
$uri = "/partner/{$this->partner_number}/account";
return $this->qvalia->httpClient($uri, (\App\Enum\HttpVerb::POST)->value, $data)->object();
}
/**
* updateAccount
*
* Update an existing account for the partner
* @param string $accountRegNo
* @param array $data
* @return mixed
*/
public function updateAccount(string $accountRegNo, array $data)
{
$uri = "/partner/{$this->partner_number}/account/{$accountRegNo}";
return $this->qvalia->httpClient($uri, (\App\Enum\HttpVerb::PUT)->value, $data)->object();
}
/**
* deleteAccount
*
* Delete an account for the partner
* @param string $accountRegNo
* @return mixed
*/
public function deleteAccount(string $accountRegNo)
{
$uri = "/partner/{$this->partner_number}/account/{$accountRegNo}";
return $this->qvalia->httpClient($uri, (\App\Enum\HttpVerb::DELETE)->value, [])->object();
}
/**
* updatePeppolId
*
* Update a Peppol ID for an account
* @param string $accountRegNo
* @param string $peppolId
* @param array $data
* @return mixed
*/
public function updatePeppolId(string $accountRegNo, string $peppolId, array $data)
{
$uri = "/partner/{$this->partner_number}/account/{$accountRegNo}/peppol/{$peppolId}";
return $this->qvalia->httpClient($uri, (\App\Enum\HttpVerb::PUT)->value, $data)->object();
}
/**
* deletePeppolId
*
* Delete a Peppol ID for an account
* @param string $accountRegNo
* @param string $peppolId
* @return mixed
*/
public function deletePeppolId(string $accountRegNo, string $peppolId)
{
$uri = "/partner/{$this->partner_number}/account/{$accountRegNo}/peppol/{$peppolId}";
return $this->qvalia->httpClient($uri, (\App\Enum\HttpVerb::DELETE)->value, [])->object();
}
}

View File

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

View File

@ -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';

View File

@ -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());
}
}
}

View File

@ -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);
}
}

View File

@ -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();

View File

@ -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;

View File

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

View File

@ -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();;