Skip to content

Commit a5988af

Browse files
committed
feat: Phase 7 partial — extract Invoice traits + bills migrations
- Extract HasLineItemTotals trait (subtotal/tax/total/amount_due/isOverdue) - Extract HasStatusTransitions trait (canTransitionTo/transitionTo/availableTransitions) - Refactor Invoice model to use both traits via getTransitions() - Add bills, bill_items, bill_payments migrations (timestamps 000003-000005) https://claude.ai/code/session_01RdUGwo74JXChRCF88Yu27d
1 parent 00ed26a commit a5988af

6 files changed

Lines changed: 168 additions & 62 deletions

File tree

erp/app/Modules/Finance/Models/Invoice.php

Lines changed: 13 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
use App\Models\User;
66
use App\Modules\Core\Traits\BelongsToTenant;
77
use App\Modules\Core\Traits\HasAuditLog;
8+
use App\Modules\Finance\Traits\HasLineItemTotals;
9+
use App\Modules\Finance\Traits\HasStatusTransitions;
810
use Illuminate\Database\Eloquent\Model;
911
use Illuminate\Database\Eloquent\Relations\BelongsTo;
1012
use Illuminate\Database\Eloquent\Relations\HasMany;
@@ -15,6 +17,8 @@ class Invoice extends Model
1517
use BelongsToTenant;
1618
use HasAuditLog;
1719
use SoftDeletes;
20+
use HasLineItemTotals;
21+
use HasStatusTransitions;
1822

1923
protected $fillable = [
2024
'tenant_id', 'contact_id', 'number',
@@ -28,12 +32,15 @@ class Invoice extends Model
2832

2933
protected $attributes = ['status' => 'draft'];
3034

31-
private const TRANSITIONS = [
32-
'draft' => ['sent', 'cancelled'],
33-
'sent' => ['paid', 'cancelled'],
34-
'paid' => [],
35-
'cancelled' => [],
36-
];
35+
protected function getTransitions(): array
36+
{
37+
return [
38+
'draft' => ['sent', 'cancelled'],
39+
'sent' => ['paid', 'cancelled'],
40+
'paid' => [],
41+
'cancelled' => [],
42+
];
43+
}
3744

3845
public function contact(): BelongsTo
3946
{
@@ -54,60 +61,4 @@ public function creator(): BelongsTo
5461
{
5562
return $this->belongsTo(User::class, 'created_by');
5663
}
57-
58-
public function getSubtotalAttribute(): float
59-
{
60-
return $this->items->sum(fn ($i) => (float) $i->quantity * (float) $i->unit_price);
61-
}
62-
63-
public function getTaxTotalAttribute(): float
64-
{
65-
return $this->items->sum(function ($i) {
66-
$sub = (float) $i->quantity * (float) $i->unit_price;
67-
return $sub * ((float) $i->tax_rate / 100);
68-
});
69-
}
70-
71-
public function getTotalAttribute(): float
72-
{
73-
return $this->subtotal + $this->tax_total;
74-
}
75-
76-
public function getAmountPaidAttribute(): float
77-
{
78-
return (float) $this->payments->sum('amount');
79-
}
80-
81-
public function getAmountDueAttribute(): float
82-
{
83-
return $this->total - $this->amount_paid;
84-
}
85-
86-
public function isOverdue(): bool
87-
{
88-
return $this->due_date !== null
89-
&& now()->startOfDay()->gt($this->due_date)
90-
&& ! in_array($this->status, ['paid', 'cancelled'], true);
91-
}
92-
93-
public function canTransitionTo(string $status): bool
94-
{
95-
return in_array($status, self::TRANSITIONS[$this->status] ?? [], true);
96-
}
97-
98-
public function availableTransitions(): array
99-
{
100-
return self::TRANSITIONS[$this->status] ?? [];
101-
}
102-
103-
public function transitionTo(string $status): void
104-
{
105-
if (! $this->canTransitionTo($status)) {
106-
throw new \DomainException(
107-
"Cannot transition invoice from '{$this->status}' to '{$status}'."
108-
);
109-
}
110-
111-
$this->update(['status' => $status]);
112-
}
11364
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
namespace App\Modules\Finance\Traits;
4+
5+
trait HasLineItemTotals
6+
{
7+
public function getSubtotalAttribute(): float
8+
{
9+
return $this->items->sum(fn ($i) => (float) $i->quantity * (float) $i->unit_price);
10+
}
11+
12+
public function getTaxTotalAttribute(): float
13+
{
14+
return $this->items->sum(function ($i) {
15+
$sub = (float) $i->quantity * (float) $i->unit_price;
16+
return $sub * ((float) $i->tax_rate / 100);
17+
});
18+
}
19+
20+
public function getTotalAttribute(): float
21+
{
22+
return $this->subtotal + $this->tax_total;
23+
}
24+
25+
public function getAmountPaidAttribute(): float
26+
{
27+
return (float) $this->payments->sum('amount');
28+
}
29+
30+
public function getAmountDueAttribute(): float
31+
{
32+
return $this->total - $this->amount_paid;
33+
}
34+
35+
public function isOverdue(): bool
36+
{
37+
return $this->due_date !== null
38+
&& now()->startOfDay()->gt($this->due_date)
39+
&& ! in_array($this->status, ['paid', 'cancelled'], true);
40+
}
41+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
namespace App\Modules\Finance\Traits;
4+
5+
trait HasStatusTransitions
6+
{
7+
abstract protected function getTransitions(): array;
8+
9+
public function canTransitionTo(string $status): bool
10+
{
11+
return in_array($status, $this->getTransitions()[$this->status] ?? [], true);
12+
}
13+
14+
public function availableTransitions(): array
15+
{
16+
return $this->getTransitions()[$this->status] ?? [];
17+
}
18+
19+
public function transitionTo(string $status): void
20+
{
21+
if (! $this->canTransitionTo($status)) {
22+
throw new \DomainException(
23+
"Cannot transition from '{$this->status}' to '{$status}'."
24+
);
25+
}
26+
27+
$this->update(['status' => $status]);
28+
}
29+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
return new class extends Migration
8+
{
9+
public function up(): void
10+
{
11+
Schema::create('bills', function (Blueprint $table) {
12+
$table->id();
13+
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
14+
$table->foreignId('contact_id')->nullable()->constrained()->nullOnDelete();
15+
$table->string('number', 50)->nullable();
16+
$table->date('issue_date');
17+
$table->date('due_date')->nullable();
18+
$table->enum('status', ['draft', 'received', 'paid', 'cancelled'])->default('draft');
19+
$table->text('notes')->nullable();
20+
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
21+
$table->timestamps();
22+
$table->softDeletes();
23+
$table->unique(['tenant_id', 'number']);
24+
});
25+
}
26+
27+
public function down(): void
28+
{
29+
Schema::dropIfExists('bills');
30+
}
31+
};
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
return new class extends Migration
8+
{
9+
public function up(): void
10+
{
11+
Schema::create('bill_items', function (Blueprint $table) {
12+
$table->id();
13+
$table->foreignId('bill_id')->constrained()->cascadeOnDelete();
14+
$table->string('description');
15+
$table->decimal('quantity', 12, 2);
16+
$table->decimal('unit_price', 15, 2);
17+
$table->decimal('tax_rate', 5, 2)->default(0);
18+
$table->timestamps();
19+
});
20+
}
21+
22+
public function down(): void
23+
{
24+
Schema::dropIfExists('bill_items');
25+
}
26+
};
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
return new class extends Migration
8+
{
9+
public function up(): void
10+
{
11+
Schema::create('bill_payments', function (Blueprint $table) {
12+
$table->id();
13+
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
14+
$table->foreignId('bill_id')->constrained()->cascadeOnDelete();
15+
$table->decimal('amount', 15, 2);
16+
$table->date('payment_date');
17+
$table->enum('method', ['cash', 'bank_transfer', 'cheque', 'card', 'other']);
18+
$table->string('reference', 100)->nullable();
19+
$table->text('notes')->nullable();
20+
$table->timestamps();
21+
});
22+
}
23+
24+
public function down(): void
25+
{
26+
Schema::dropIfExists('bill_payments');
27+
}
28+
};

0 commit comments

Comments
 (0)