Skip to content

Commit 196e519

Browse files
committed
feat: Phase 9 WIP — Quotes models, policy, and migrations
- Quote and QuoteItem models with HasLineItemTotals + HasStatusTransitions - QuotePolicy registered in FinanceServiceProvider - Migrations for quotes and quote_items tables - Lifecycle: draft → sent → accepted/declined/cancelled https://claude.ai/code/session_01RdUGwo74JXChRCF88Yu27d
1 parent 357e671 commit 196e519

6 files changed

Lines changed: 188 additions & 0 deletions

File tree

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
namespace App\Modules\Finance\Models;
4+
5+
use App\Models\User;
6+
use App\Modules\Core\Traits\BelongsToTenant;
7+
use App\Modules\Core\Traits\HasAuditLog;
8+
use App\Modules\Finance\Traits\HasLineItemTotals;
9+
use App\Modules\Finance\Traits\HasStatusTransitions;
10+
use Illuminate\Database\Eloquent\Model;
11+
use Illuminate\Database\Eloquent\Relations\BelongsTo;
12+
use Illuminate\Database\Eloquent\Relations\HasMany;
13+
use Illuminate\Database\Eloquent\SoftDeletes;
14+
15+
class Quote extends Model
16+
{
17+
use BelongsToTenant;
18+
use HasAuditLog;
19+
use SoftDeletes;
20+
use HasLineItemTotals;
21+
use HasStatusTransitions;
22+
23+
protected $fillable = [
24+
'tenant_id', 'contact_id', 'number',
25+
'issue_date', 'expiry_date', 'status', 'notes', 'created_by',
26+
];
27+
28+
protected $casts = [
29+
'issue_date' => 'date',
30+
'expiry_date' => 'date',
31+
];
32+
33+
protected $attributes = ['status' => 'draft'];
34+
35+
protected function getTransitions(): array
36+
{
37+
return [
38+
'draft' => ['sent', 'cancelled'],
39+
'sent' => ['accepted', 'declined', 'cancelled'],
40+
'accepted' => [],
41+
'declined' => [],
42+
'cancelled' => [],
43+
];
44+
}
45+
46+
public function contact(): BelongsTo
47+
{
48+
return $this->belongsTo(Contact::class);
49+
}
50+
51+
public function items(): HasMany
52+
{
53+
return $this->hasMany(QuoteItem::class);
54+
}
55+
56+
public function creator(): BelongsTo
57+
{
58+
return $this->belongsTo(User::class, 'created_by');
59+
}
60+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
namespace App\Modules\Finance\Models;
4+
5+
use Illuminate\Database\Eloquent\Model;
6+
use Illuminate\Database\Eloquent\Relations\BelongsTo;
7+
8+
class QuoteItem extends Model
9+
{
10+
protected $table = 'quote_items';
11+
12+
protected $fillable = [
13+
'quote_id', 'description', 'quantity', 'unit_price', 'tax_rate',
14+
];
15+
16+
protected $casts = [
17+
'quantity' => 'decimal:2',
18+
'unit_price' => 'decimal:2',
19+
'tax_rate' => 'decimal:2',
20+
];
21+
22+
public function quote(): BelongsTo
23+
{
24+
return $this->belongsTo(Quote::class);
25+
}
26+
27+
public function getLineTotalAttribute(): float
28+
{
29+
return (float) $this->quantity * (float) $this->unit_price * (1 + (float) $this->tax_rate / 100);
30+
}
31+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
namespace App\Modules\Finance\Policies;
4+
5+
use App\Models\User;
6+
use App\Modules\Finance\Models\Quote;
7+
8+
class QuotePolicy
9+
{
10+
public function viewAny(User $user): bool
11+
{
12+
return $user->can('finance.view');
13+
}
14+
15+
public function view(User $user, Quote $quote): bool
16+
{
17+
return $user->can('finance.view');
18+
}
19+
20+
public function create(User $user): bool
21+
{
22+
return $user->can('finance.create');
23+
}
24+
25+
public function update(User $user, Quote $quote): bool
26+
{
27+
return $user->can('finance.update');
28+
}
29+
30+
public function delete(User $user, Quote $quote): bool
31+
{
32+
return $user->can('finance.delete') && $quote->status === 'draft';
33+
}
34+
}

erp/app/Modules/Finance/Providers/FinanceServiceProvider.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@
77
use App\Modules\Finance\Models\Contact;
88
use App\Modules\Finance\Models\Invoice;
99
use App\Modules\Finance\Models\JournalEntry;
10+
use App\Modules\Finance\Models\Quote;
1011
use App\Modules\Finance\Policies\AccountPolicy;
1112
use App\Modules\Finance\Policies\BillPolicy;
1213
use App\Modules\Finance\Policies\ContactPolicy;
1314
use App\Modules\Finance\Policies\InvoicePolicy;
1415
use App\Modules\Finance\Policies\JournalEntryPolicy;
16+
use App\Modules\Finance\Policies\QuotePolicy;
1517
use Illuminate\Support\Facades\Gate;
1618
use Illuminate\Support\ServiceProvider;
1719

@@ -28,5 +30,6 @@ public function boot(): void
2830
Gate::policy(JournalEntry::class, JournalEntryPolicy::class);
2931
Gate::policy(Invoice::class, InvoicePolicy::class);
3032
Gate::policy(Bill::class, BillPolicy::class);
33+
Gate::policy(Quote::class, QuotePolicy::class);
3134
}
3235
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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('quotes', function (Blueprint $table) {
12+
$table->id();
13+
$table->unsignedBigInteger('tenant_id');
14+
$table->unsignedBigInteger('contact_id')->nullable();
15+
$table->string('number')->nullable();
16+
$table->date('issue_date');
17+
$table->date('expiry_date')->nullable();
18+
$table->enum('status', ['draft', 'sent', 'accepted', 'declined', 'cancelled'])->default('draft');
19+
$table->text('notes')->nullable();
20+
$table->unsignedBigInteger('created_by')->nullable();
21+
$table->timestamps();
22+
$table->softDeletes();
23+
$table->foreign('tenant_id')->references('id')->on('tenants')->cascadeOnDelete();
24+
$table->foreign('contact_id')->references('id')->on('contacts')->nullOnDelete();
25+
$table->unique(['tenant_id', 'number']);
26+
});
27+
}
28+
29+
public function down(): void
30+
{
31+
Schema::dropIfExists('quotes');
32+
}
33+
};
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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('quote_items', function (Blueprint $table) {
12+
$table->id();
13+
$table->unsignedBigInteger('quote_id');
14+
$table->text('description');
15+
$table->decimal('quantity', 12, 2)->default(1);
16+
$table->decimal('unit_price', 15, 2);
17+
$table->decimal('tax_rate', 5, 2)->default(0);
18+
$table->timestamps();
19+
$table->foreign('quote_id')->references('id')->on('quotes')->cascadeOnDelete();
20+
});
21+
}
22+
23+
public function down(): void
24+
{
25+
Schema::dropIfExists('quote_items');
26+
}
27+
};

0 commit comments

Comments
 (0)