diff --git a/app/Casts/InvoiceBackupCast.php b/app/Casts/InvoiceBackupCast.php new file mode 100644 index 0000000000..817ff2b489 --- /dev/null +++ b/app/Casts/InvoiceBackupCast.php @@ -0,0 +1,60 @@ + 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, + ]) + ]; + } +} diff --git a/app/DataMapper/Cancellation.php b/app/DataMapper/Cancellation.php new file mode 100644 index 0000000000..5d4fab7f8e --- /dev/null +++ b/app/DataMapper/Cancellation.php @@ -0,0 +1,32 @@ + $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 + ); + } +} + diff --git a/app/Http/ValidationRules/Invoice/CanGenerateModificationInvoice.php b/app/Http/ValidationRules/Invoice/CanGenerateModificationInvoice.php index 7b3cd22fd3..7c76655576 100644 --- a/app/Http/ValidationRules/Invoice/CanGenerateModificationInvoice.php +++ b/app/Http/ValidationRules/Invoice/CanGenerateModificationInvoice.php @@ -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."); } } diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index a83e15a471..4efcc8ca92 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -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', diff --git a/app/Repositories/BaseRepository.php b/app/Repositories/BaseRepository.php index 3b17647eeb..e7c253d61b 100644 --- a/app/Repositories/BaseRepository.php +++ b/app/Repositories/BaseRepository.php @@ -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) { diff --git a/app/Services/EDocument/Jobs/SendEDocument.php b/app/Services/EDocument/Jobs/SendEDocument.php index 794e660bbe..383667154b 100644 --- a/app/Services/EDocument/Jobs/SendEDocument.php +++ b/app/Services/EDocument/Jobs/SendEDocument.php @@ -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(); } diff --git a/app/Services/Invoice/HandleCancellation.php b/app/Services/Invoice/HandleCancellation.php index 480f80cfef..a84f08f372 100644 --- a/app/Services/Invoice/HandleCancellation.php +++ b/app/Services/Invoice/HandleCancellation.php @@ -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(); } } diff --git a/app/Services/Invoice/InvoiceService.php b/app/Services/Invoice/InvoiceService.php index 2d56cd823c..5eff5093a4 100644 --- a/app/Services/Invoice/InvoiceService.php +++ b/app/Services/Invoice/InvoiceService.php @@ -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 diff --git a/app/Services/Invoice/TriggeredActions.php b/app/Services/Invoice/TriggeredActions.php index 0721a6db03..54eda85dff 100644 --- a/app/Services/Invoice/TriggeredActions.php +++ b/app/Services/Invoice/TriggeredActions.php @@ -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(); } diff --git a/app/Transformers/InvoiceTransformer.php b/app/Transformers/InvoiceTransformer.php index 87c56c1f7c..253ebab520 100644 --- a/app/Transformers/InvoiceTransformer.php +++ b/app/Transformers/InvoiceTransformer.php @@ -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), ];