Skip to content

Commit d257d1e

Browse files
committed
feat(finance): Phase 137 — Finance Vendor Payments
Add vendor payment lifecycle: pending → approved → processed/rejected/cancelled, with soft-delete, policy-gated CRUD and action routes. https://claude.ai/code/session_01RdUGwo74JXChRCF88Yu27d
1 parent 2d3f857 commit d257d1e

11 files changed

Lines changed: 495 additions & 0 deletions

File tree

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
<?php
2+
3+
namespace App\Modules\Finance\Http\Controllers;
4+
5+
use App\Http\Controllers\Controller;
6+
use App\Modules\Finance\Models\VendorPayment;
7+
use Illuminate\Http\RedirectResponse;
8+
use Illuminate\Http\Request;
9+
use Inertia\Inertia;
10+
use Inertia\Response;
11+
12+
class VendorPaymentController extends Controller
13+
{
14+
public function index(Request $request): Response
15+
{
16+
$this->authorize('viewAny', VendorPayment::class);
17+
18+
$query = VendorPayment::orderByDesc('payment_date');
19+
20+
if ($request->filled('status')) {
21+
$query->where('status', $request->status);
22+
}
23+
24+
$vendorPayments = $query->paginate(20);
25+
26+
return Inertia::render('Finance/VendorPayments/Index', compact('vendorPayments'));
27+
}
28+
29+
public function create(): Response
30+
{
31+
$this->authorize('create', VendorPayment::class);
32+
33+
return Inertia::render('Finance/VendorPayments/Create');
34+
}
35+
36+
public function store(Request $request): RedirectResponse
37+
{
38+
$this->authorize('create', VendorPayment::class);
39+
40+
$data = $request->validate([
41+
'vendor_name' => 'required|string|max:255',
42+
'vendor_code' => 'nullable|string|max:255',
43+
'amount' => 'required|numeric|min:0',
44+
'currency' => 'nullable|string|max:3',
45+
'payment_method' => 'nullable|in:bank_transfer,cheque,cash,online',
46+
'reference' => 'nullable|string|max:255',
47+
'payment_date' => 'required|date',
48+
'due_date' => 'nullable|date',
49+
'notes' => 'nullable|string',
50+
]);
51+
52+
VendorPayment::create([
53+
'tenant_id' => app('tenant')->id,
54+
'vendor_name' => $data['vendor_name'],
55+
'vendor_code' => $data['vendor_code'] ?? null,
56+
'amount' => $data['amount'],
57+
'currency' => $data['currency'] ?? 'USD',
58+
'payment_method' => $data['payment_method'] ?? 'bank_transfer',
59+
'reference' => $data['reference'] ?? null,
60+
'payment_date' => $data['payment_date'],
61+
'due_date' => $data['due_date'] ?? null,
62+
'notes' => $data['notes'] ?? null,
63+
'created_by' => auth()->id(),
64+
]);
65+
66+
return redirect()->route('finance.vendor-payments.index')
67+
->with('success', 'Vendor payment created.');
68+
}
69+
70+
public function show(VendorPayment $vendorPayment): Response
71+
{
72+
$this->authorize('view', $vendorPayment);
73+
74+
$vendorPayment->load(['approvedBy', 'createdBy']);
75+
76+
return Inertia::render('Finance/VendorPayments/Show', compact('vendorPayment'));
77+
}
78+
79+
public function edit(VendorPayment $vendorPayment): Response
80+
{
81+
$this->authorize('update', $vendorPayment);
82+
83+
return Inertia::render('Finance/VendorPayments/Edit', compact('vendorPayment'));
84+
}
85+
86+
public function update(Request $request, VendorPayment $vendorPayment): RedirectResponse
87+
{
88+
$this->authorize('update', $vendorPayment);
89+
90+
$data = $request->validate([
91+
'vendor_name' => 'required|string|max:255',
92+
'vendor_code' => 'nullable|string|max:255',
93+
'amount' => 'required|numeric|min:0',
94+
'currency' => 'nullable|string|max:3',
95+
'payment_method' => 'nullable|in:bank_transfer,cheque,cash,online',
96+
'reference' => 'nullable|string|max:255',
97+
'payment_date' => 'required|date',
98+
'due_date' => 'nullable|date',
99+
'notes' => 'nullable|string',
100+
]);
101+
102+
$vendorPayment->update($data);
103+
104+
return redirect()->route('finance.vendor-payments.index')
105+
->with('success', 'Vendor payment updated.');
106+
}
107+
108+
public function destroy(VendorPayment $vendorPayment): RedirectResponse
109+
{
110+
$this->authorize('delete', $vendorPayment);
111+
112+
$vendorPayment->delete();
113+
114+
return redirect()->route('finance.vendor-payments.index')
115+
->with('success', 'Vendor payment deleted.');
116+
}
117+
118+
// ── Custom actions ────────────────────────────────────────────────────────
119+
120+
public function approve(VendorPayment $vendorPayment): RedirectResponse
121+
{
122+
$this->authorize('approve', $vendorPayment);
123+
124+
$vendorPayment->approve(auth()->id());
125+
126+
return redirect()->route('finance.vendor-payments.index')
127+
->with('success', 'Vendor payment approved.');
128+
}
129+
130+
public function process(VendorPayment $vendorPayment): RedirectResponse
131+
{
132+
$this->authorize('process', $vendorPayment);
133+
134+
$vendorPayment->process();
135+
136+
return redirect()->route('finance.vendor-payments.index')
137+
->with('success', 'Vendor payment processed.');
138+
}
139+
140+
public function reject(VendorPayment $vendorPayment): RedirectResponse
141+
{
142+
$this->authorize('reject', $vendorPayment);
143+
144+
$vendorPayment->reject();
145+
146+
return redirect()->route('finance.vendor-payments.index')
147+
->with('success', 'Vendor payment rejected.');
148+
}
149+
150+
public function cancel(VendorPayment $vendorPayment): RedirectResponse
151+
{
152+
$this->authorize('cancel', $vendorPayment);
153+
154+
$vendorPayment->cancel();
155+
156+
return redirect()->route('finance.vendor-payments.index')
157+
->with('success', 'Vendor payment cancelled.');
158+
}
159+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
<?php
2+
3+
namespace App\Modules\Finance\Models;
4+
5+
use App\Models\User;
6+
use App\Modules\Core\Traits\BelongsToTenant;
7+
use Illuminate\Database\Eloquent\Model;
8+
use Illuminate\Database\Eloquent\Relations\BelongsTo;
9+
use Illuminate\Database\Eloquent\SoftDeletes;
10+
11+
class VendorPayment extends Model
12+
{
13+
use BelongsToTenant, SoftDeletes;
14+
15+
protected $fillable = [
16+
'tenant_id',
17+
'payment_number',
18+
'vendor_name',
19+
'vendor_code',
20+
'amount',
21+
'currency',
22+
'payment_method',
23+
'reference',
24+
'payment_date',
25+
'due_date',
26+
'status',
27+
'notes',
28+
'approved_by',
29+
'approved_at',
30+
'processed_at',
31+
'created_by',
32+
];
33+
34+
protected $attributes = [
35+
'status' => 'pending',
36+
'currency' => 'USD',
37+
'payment_method' => 'bank_transfer',
38+
];
39+
40+
protected $casts = [
41+
'amount' => 'decimal:2',
42+
'payment_date' => 'date',
43+
'due_date' => 'date',
44+
'approved_at' => 'datetime',
45+
'processed_at' => 'datetime',
46+
];
47+
48+
// ── Relationships ─────────────────────────────────────────────────────────
49+
50+
public function approvedBy(): BelongsTo
51+
{
52+
return $this->belongsTo(User::class, 'approved_by');
53+
}
54+
55+
public function createdBy(): BelongsTo
56+
{
57+
return $this->belongsTo(User::class, 'created_by');
58+
}
59+
60+
// ── Status transitions ────────────────────────────────────────────────────
61+
62+
public function approve(int $userId): void
63+
{
64+
$this->status = 'approved';
65+
$this->approved_by = $userId;
66+
$this->approved_at = now();
67+
68+
if (is_null($this->payment_number)) {
69+
$this->payment_number = $this->generatePaymentNumber();
70+
}
71+
72+
$this->save();
73+
}
74+
75+
public function process(): void
76+
{
77+
$this->status = 'processed';
78+
$this->processed_at = now();
79+
$this->save();
80+
}
81+
82+
public function reject(): void
83+
{
84+
$this->status = 'rejected';
85+
$this->save();
86+
}
87+
88+
public function cancel(): void
89+
{
90+
$this->status = 'cancelled';
91+
$this->save();
92+
}
93+
94+
// ── Helpers ───────────────────────────────────────────────────────────────
95+
96+
public function generatePaymentNumber(): string
97+
{
98+
return 'VP-' . date('Y') . '-' . str_pad((string) $this->id, 5, '0', STR_PAD_LEFT);
99+
}
100+
101+
// ── Accessors ─────────────────────────────────────────────────────────────
102+
103+
public function getIsPendingAttribute(): bool
104+
{
105+
return $this->status === 'pending';
106+
}
107+
108+
public function getIsApprovedAttribute(): bool
109+
{
110+
return $this->status === 'approved';
111+
}
112+
113+
public function getIsProcessedAttribute(): bool
114+
{
115+
return $this->status === 'processed';
116+
}
117+
118+
public function getIsOverdueAttribute(): bool
119+
{
120+
return $this->is_pending
121+
&& $this->due_date !== null
122+
&& $this->due_date->lt(now()->startOfDay());
123+
}
124+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
namespace App\Modules\Finance\Policies;
4+
5+
use App\Models\User;
6+
use App\Modules\Finance\Models\VendorPayment;
7+
8+
class VendorPaymentPolicy
9+
{
10+
public function viewAny(User $user): bool
11+
{
12+
return $user->can('finance.view');
13+
}
14+
15+
public function view(User $user, VendorPayment $vendorPayment): 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, VendorPayment $vendorPayment): bool
26+
{
27+
return $user->can('finance.create');
28+
}
29+
30+
public function approve(User $user, VendorPayment $vendorPayment): bool
31+
{
32+
return $user->can('finance.create');
33+
}
34+
35+
public function process(User $user, VendorPayment $vendorPayment): bool
36+
{
37+
return $user->can('finance.create');
38+
}
39+
40+
public function reject(User $user, VendorPayment $vendorPayment): bool
41+
{
42+
return $user->can('finance.delete');
43+
}
44+
45+
public function cancel(User $user, VendorPayment $vendorPayment): bool
46+
{
47+
return $user->can('finance.delete');
48+
}
49+
50+
public function delete(User $user, VendorPayment $vendorPayment): bool
51+
{
52+
return $user->can('finance.delete');
53+
}
54+
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@
110110
use App\Modules\Finance\Policies\CashFlowForecastPolicy;
111111
use App\Modules\Finance\Models\RecurringExpense;
112112
use App\Modules\Finance\Policies\RecurringExpensePolicy;
113+
use App\Modules\Finance\Models\VendorPayment;
114+
use App\Modules\Finance\Policies\VendorPaymentPolicy;
113115
use Illuminate\Support\Facades\Gate;
114116
use Illuminate\Support\ServiceProvider;
115117

@@ -193,6 +195,7 @@ public function boot(): void
193195
Gate::policy(IntercompanyTransaction::class, IntercompanyPolicy::class);
194196
Gate::policy(CashFlowForecast::class, CashFlowForecastPolicy::class);
195197
Gate::policy(RecurringExpense::class, RecurringExpensePolicy::class);
198+
Gate::policy(VendorPayment::class, VendorPaymentPolicy::class);
196199
if ($this->app->runningInConsole()) {
197200
$this->commands([\App\Modules\Finance\Console\Commands\GenerateRecurringInvoices::class]);
198201
}

erp/app/Modules/Finance/routes/finance.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,3 +409,13 @@
409409
Route::post('recurring-expenses/{recurring_expense}/cancel', [RecurringExpenseController::class, 'cancel'])->name('recurring-expenses.cancel');
410410
Route::resource('recurring-expenses', RecurringExpenseController::class);
411411
});
412+
413+
// Vendor Payments
414+
use App\Modules\Finance\Http\Controllers\VendorPaymentController;
415+
Route::middleware(['web', 'auth', 'verified'])->prefix('finance')->name('finance.')->group(function () {
416+
Route::post('vendor-payments/{vendor_payment}/approve', [VendorPaymentController::class, 'approve'])->name('vendor-payments.approve');
417+
Route::post('vendor-payments/{vendor_payment}/process', [VendorPaymentController::class, 'process'])->name('vendor-payments.process');
418+
Route::post('vendor-payments/{vendor_payment}/reject', [VendorPaymentController::class, 'reject'])->name('vendor-payments.reject');
419+
Route::post('vendor-payments/{vendor_payment}/cancel', [VendorPaymentController::class, 'cancel'])->name('vendor-payments.cancel');
420+
Route::resource('vendor-payments', VendorPaymentController::class);
421+
});

0 commit comments

Comments
 (0)