Update for ivnoice backup casting

This commit is contained in:
David Bomba 2025-08-12 10:24:18 +10:00
parent 47f33c8691
commit 67df175525
11 changed files with 220 additions and 39 deletions

View File

@ -0,0 +1,60 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www/elastic.co/licensing/elastic-license
*/
namespace App\Casts;
use App\DataMapper\InvoiceBackup;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
class InvoiceBackupCast implements CastsAttributes
{
public function get($model, string $key, $value, array $attributes)
{
if (is_null($value)) {
return new InvoiceBackup();
}
$data = json_decode($value, true) ?? [];
return InvoiceBackup::fromArray($data);
}
public function set($model, string $key, $value, array $attributes)
{
if (is_null($value)) {
return [$key => null];
}
// Ensure we're dealing with our object type
if (! $value instanceof InvoiceBackup) {
throw new \InvalidArgumentException('Value must be an InvoiceBackup instance.');
}
return [
$key => json_encode([
'guid' => $value->guid,
'cancellation' => $value->cancellation ? [
'adjustment' => $value->cancellation->adjustment,
'status_id' => $value->cancellation->status_id,
] : [],
'cancelled_invoice_id' => $value->cancelled_invoice_id,
'cancelled_invoice_number' => $value->cancelled_invoice_number,
'cancellation_reason' => $value->cancellation_reason,
'credit_invoice_id' => $value->credit_invoice_id,
'credit_invoice_number' => $value->credit_invoice_number,
'redirect' => $value->redirect,
'modified_invoice_id' => $value->modified_invoice_id,
'replaced_invoice_id' => $value->replaced_invoice_id,
])
];
}
}

View File

@ -0,0 +1,32 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www/elastic.co/licensing/elastic-license
*/
namespace App\DataMapper;
/**
* Cancellation value object for invoice backup data.
*/
class Cancellation
{
public function __construct(
public float $adjustment = 0, // The cancellation adjustment amount
public int $status_id = 0 //The status id of the invoice when it was cancelled
) {}
public static function fromArray(array $data): self
{
return new self(
adjustment: $data['adjustment'] ?? 0,
status_id: $data['status_id'] ?? 0
);
}
}

View File

@ -0,0 +1,63 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www/elastic.co/licensing/elastic-license
*/
namespace App\DataMapper;
use App\Casts\InvoiceBackupCast;
use App\DataMapper\Cancellation;
use Illuminate\Contracts\Database\Eloquent\Castable;
/**
* InvoiceBackup.
*/
class InvoiceBackup implements Castable
{
public function __construct(
public string $guid = '', // The E-INVOICE SENT GUID reference
public Cancellation $cancellation = new Cancellation(0,0),
public ?string $cancelled_invoice_id = null, // The id of the invoice that was cancelled
public ?string $cancelled_invoice_number = null, // The number of the invoice that was cancelled
public ?string $cancellation_reason = null, // The reason for the cancellation
public ?string $credit_invoice_id = null, // The id of the credit invoice that was created
public ?string $credit_invoice_number = null, // The number of the credit invoice that was created
public ?string $redirect = null, // The redirect url for the invoice
public ?string $modified_invoice_id = null, // The id of the modified invoice (replaces the invoice with replaced_invoice_id)
public ?string $replaced_invoice_id = null // The id of the replaced invoice (The previous invoice that was replaced by the modified invoice)
) {}
/**
* Get the name of the caster class to use when casting from / to this cast target.
*
* @param array<string, mixed> $arguments
*/
public static function castUsing(array $arguments): string
{
return InvoiceBackupCast::class;
}
public static function fromArray(array $data): self
{
return new self(
guid: $data['guid'] ?? '',
cancellation: Cancellation::fromArray($data['cancellation'] ?? []),
cancelled_invoice_id: $data['cancelled_invoice_id'] ?? null,
cancelled_invoice_number: $data['cancelled_invoice_number'] ?? null,
cancellation_reason: $data['cancellation_reason'] ?? null,
credit_invoice_id: $data['credit_invoice_id'] ?? null,
credit_invoice_number: $data['credit_invoice_number'] ?? null,
redirect: $data['redirect'] ?? null,
modified_invoice_id: $data['modified_invoice_id'] ?? null,
replaced_invoice_id: $data['replaced_invoice_id'] ?? null
);
}
}

View File

@ -57,6 +57,8 @@ class CanGenerateModificationInvoice implements ValidationRule
$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.");
}
}

View File

@ -29,6 +29,7 @@ use App\Helpers\Invoice\InvoiceSumInclusive;
use App\Utils\Traits\Invoice\ActionsInvoice;
use Illuminate\Database\Eloquent\SoftDeletes;
use App\Events\Invoice\InvoiceReminderWasEmailed;
use App\DataMapper\InvoiceBackup;
use App\Utils\Number;
/**
@ -55,7 +56,7 @@ use App\Utils\Number;
* @property string|null $due_date
* @property bool $is_deleted
* @property object|array|string $line_items
* @property object|null $backup
* @property InvoiceBackup $backup
* @property object|null $sync
* @property string|null $footer
* @property string|null $public_notes
@ -207,7 +208,7 @@ class Invoice extends BaseModel
protected $casts = [
'line_items' => 'object',
'backup' => 'object',
'backup' => InvoiceBackup::class,
'updated_at' => 'timestamp',
'created_at' => 'timestamp',
'deleted_at' => 'timestamp',

View File

@ -328,6 +328,11 @@ class BaseRepository
nlog($e->getMessage());
}
}
/** Verifactu modified invoice check */
if(isset($data['verifactu_modified']) && $data['verifactu_modified']) {
$model->service()->modifyVerifactuWorkflow($data['modified_invoice_id'])->save();
}
}
if ($model instanceof Credit) {

View File

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

View File

@ -87,10 +87,11 @@ class HandleCancellation extends AbstractService
$replicated_invoice->line_items = $items;
$backup = new \stdClass();
$backup->cancelled_invoice_id = $this->invoice->hashed_id;
$backup->cancelled_invoice_number = $this->invoice->number;
$backup->cancellation_reason = $this->reason ?? 'R3';
$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;
@ -98,10 +99,11 @@ class HandleCancellation extends AbstractService
$replicated_invoice = $invoice_repository->save([], $replicated_invoice);
$replicated_invoice->service()->markSent()->sendVerifactu()->save();
$old_backup = new \stdClass();
$old_backup->credit_invoice_id = $replicated_invoice->hashed_id;
$old_backup->credit_invoice_number = $replicated_invoice->number;
$old_backup->cancellation_reason = $this->reason ?? 'R3';
$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 = $old_backup;
$this->invoice->saveQuietly();
@ -115,10 +117,9 @@ class HandleCancellation extends AbstractService
public function reverse()
{
/* The stored cancelled object - contains the adjustment and status*/
$cancellation = $this->invoice->backup->cancellation;
/* Will turn the negative cancellation amount to a positive adjustment*/
$cancellation = $this->invoice->backup->cancellation;
$adjustment = $cancellation->adjustment * -1;
$this->invoice->ledger()->updateInvoiceBalance($adjustment, "Invoice {$this->invoice->number} reversal");
@ -133,11 +134,9 @@ class HandleCancellation extends AbstractService
$this->invoice->client->service()->calculateBalance();
/* Pop the cancellation out of the backup*/
$backup = $this->invoice->backup;
unset($backup->cancellation);
$this->invoice->backup = $backup;
/* Clear the cancellation data */
$this->invoice->backup->cancellation->adjustment = 0;
$this->invoice->backup->cancellation->status_id = 0;
$this->invoice->saveQuietly();
$this->invoice->fresh();
@ -152,19 +151,11 @@ class HandleCancellation extends AbstractService
*/
private function backupCancellation($adjustment)
{
if (! is_object($this->invoice->backup)) {
$backup = new stdClass();
$this->invoice->backup = $backup;
}
$cancellation = new stdClass();
$cancellation->adjustment = $adjustment;
$cancellation->status_id = $this->invoice->status_id;
$invoice_backup = $this->invoice->backup;
$invoice_backup->cancellation = $cancellation;
$this->invoice->backup = $invoice_backup;
// Direct assignment to properties
$this->invoice->backup->cancellation->adjustment = $adjustment;
$this->invoice->backup->cancellation->status_id = $this->invoice->status_id;
$this->invoice->saveQuietly();
}
}

View File

@ -708,6 +708,33 @@ class InvoiceService
return $this;
}
/**
* modifyVerifactuWorkflow
* @todo - handle invoice modifications - ensure when we
* sent this to AEAT we reference the invoice that was replaced.
*
* @param string $modified_invoice_hashed_id
* @return self
*/
public function modifyVerifactuWorkflow(string $modified_invoice_hashed_id): self
{
$modified_invoice = Invoice::withTrashed()->find($this->decodePrimaryKey($modified_invoice_hashed_id));
$modified_invoice->status_id = Invoice::STATUS_REPLACED;
$modified_invoice->backup->modified_invoice_id = $this->invoice->hashed_id;
$modified_invoice->save();
$this->markSent();
//Update the client balance by the delta amount from the previous invoice to this one.
$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->sendVerifactu();
return $this;
}
/**
* Saves the invoice.
* @return Invoice object

View File

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

View File

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