From 28288ee722cb2d2bc0f6cd996ffbac6640222ae2 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Wed, 28 Jan 2026 22:09:27 -0600 Subject: [PATCH] Show total payments and balance --- app/Models/Invoice.php | 31 +++++++++++++++---- app/Models/InvoiceLine.php | 4 +++ app/Models/Payment.php | 19 ++++++++++++ ...ents_and_balance_due_to_invoices_table.php | 29 +++++++++++++++++ .../components/⚡create-payment.blade.php | 2 +- .../views/components/⚡invoice-list.blade.php | 12 ++++++- 6 files changed, 89 insertions(+), 8 deletions(-) create mode 100644 database/migrations/2026_01_29_034256_add_total_payments_and_balance_due_to_invoices_table.php diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index 8b6b610..c0801b7 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -14,11 +14,12 @@ use Illuminate\Support\Str; class Invoice extends Model { use HasFactory; + public static function booted(): void { static::creating(function (Invoice $invoice) { $invoice->invoice_number ??= static::generateInvoiceNumber(); - $invoice->uuid = (string) Str::uuid(); + $invoice->uuid = (string) Str::uuid(); }); } @@ -45,11 +46,13 @@ class Invoice extends Model ]; protected $casts = [ - 'total' => MoneyCast::class, - 'status' => InvoiceStatus::class, - 'invoice_date' => 'date', - 'due_date' => 'date', - 'sent_at' => 'date', + 'total' => MoneyCast::class, + 'total_payments' => MoneyCast::class, + 'balance_due' => MoneyCast::class, + 'status' => InvoiceStatus::class, + 'invoice_date' => 'date', + 'due_date' => 'date', + 'sent_at' => 'date', ]; /** @@ -84,6 +87,22 @@ class Invoice extends Model $this->saveQuietly(); } + public function recalculateTotalPayments(): void + { + $this->attributes['total_payments'] = $this->payments()->sum('amount'); + $this->saveQuietly(); + + $this->refresh(); + + if ($this->status === InvoiceStatus::POSTED && $this->balance_due == 0) { + $this->status = InvoiceStatus::PAID; + $this->saveQuietly(); + } elseif ($this->status === InvoiceStatus::PAID && $this->balance_due != 0) { + $this->status = InvoiceStatus::POSTED; + $this->saveQuietly(); + } + } + public function isLocked(): bool { return in_array($this->status, [InvoiceStatus::POSTED, InvoiceStatus::PAID, InvoiceStatus::VOID]); diff --git a/app/Models/InvoiceLine.php b/app/Models/InvoiceLine.php index 797c389..2f968d2 100644 --- a/app/Models/InvoiceLine.php +++ b/app/Models/InvoiceLine.php @@ -33,6 +33,10 @@ class InvoiceLine extends Model throw new InvoiceLockedException; } + if ($line->exists && $line->isDirty('invoice_id')) { + throw new \RuntimeException('Cannot move invoice line to another invoice'); + } + $line->amount = $line->unit_price * $line->quantity; }); diff --git a/app/Models/Payment.php b/app/Models/Payment.php index 55b9b71..56d4f48 100644 --- a/app/Models/Payment.php +++ b/app/Models/Payment.php @@ -31,6 +31,25 @@ class Payment extends Model 'payment_method' => PaymentMethod::class, ]; + public static function booted(): void + { + static::saved(function (Payment $payment) { + // If invoice_id changed, recalculate the old invoice too + if ($payment->wasChanged('invoice_id')) { + $originalInvoiceId = $payment->getOriginal('invoice_id'); + if ($originalInvoiceId) { + Invoice::find($originalInvoiceId)?->recalculateTotalPayments(); + } + } + + $payment->invoice->recalculateTotalPayments(); + }); + + static::deleted(function (Payment $payment) { + $payment->invoice->recalculateTotalPayments(); + }); + } + public function invoice(): BelongsTo { return $this->belongsTo(Invoice::class); diff --git a/database/migrations/2026_01_29_034256_add_total_payments_and_balance_due_to_invoices_table.php b/database/migrations/2026_01_29_034256_add_total_payments_and_balance_due_to_invoices_table.php new file mode 100644 index 0000000..30d55fa --- /dev/null +++ b/database/migrations/2026_01_29_034256_add_total_payments_and_balance_due_to_invoices_table.php @@ -0,0 +1,29 @@ +bigInteger('total_payments')->default(0)->after('total'); + $table->bigInteger('balance_due')->storedAs('total - total_payments')->after('total_payments'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('invoices', function (Blueprint $table) { + $table->dropColumn(['balance_due', 'total_payments']); + }); + } +}; diff --git a/resources/views/components/⚡create-payment.blade.php b/resources/views/components/⚡create-payment.blade.php index 87d89a7..07e443c 100644 --- a/resources/views/components/⚡create-payment.blade.php +++ b/resources/views/components/⚡create-payment.blade.php @@ -104,7 +104,7 @@ new class extends Component { @foreach ($this->invoices as $invoice) + - {{ $invoice->invoice_number }} - Balance: {{ formatMoney($invoice->balance_due) }} @endforeach diff --git a/resources/views/components/⚡invoice-list.blade.php b/resources/views/components/⚡invoice-list.blade.php index 04e5ccc..05777ed 100644 --- a/resources/views/components/⚡invoice-list.blade.php +++ b/resources/views/components/⚡invoice-list.blade.php @@ -70,7 +70,15 @@ new class extends Component { - Total + Invoice Total + + + Total Payments + + + Balance Due @@ -104,6 +112,8 @@ new class extends Component { @endif {{ formatMoney($invoice->total) }} + {{ formatMoney($invoice->total_payments) }} + {{ formatMoney($invoice->balance_due) }}