Skip to content

Commit 79ef28b

Browse files
committed
feat(finance): Phase 80 — Bank Reconciliation with accounts and transaction matching
https://claude.ai/code/session_01RdUGwo74JXChRCF88Yu27d
1 parent c6748fb commit 79ef28b

20 files changed

Lines changed: 1070 additions & 114 deletions

erp/app/Modules/Finance/Http/Controllers/BankAccountController.php

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,7 @@ public function index(Request $request): Response
1717
$this->authorize('viewAny', BankAccount::class);
1818

1919
$accounts = BankAccount::where('tenant_id', $request->user()->tenant_id)
20-
->get()
21-
->map(function (BankAccount $account) {
22-
return array_merge($account->toArray(), [
23-
'balance' => $account->balance,
24-
'unreconciled_count' => $account->unreconciledCount,
25-
]);
26-
});
20+
->paginate(20);
2721

2822
return Inertia::render('Finance/BankAccounts/Index', [
2923
'accounts' => $accounts,
@@ -43,18 +37,21 @@ public function store(Request $request): RedirectResponse
4337

4438
$data = $request->validate([
4539
'name' => 'required|string|max:255',
46-
'bank_name' => 'nullable|string|max:255',
40+
'bank_name' => 'required|string|max:255',
4741
'account_number' => 'nullable|string|max:255',
48-
'currency_code' => 'required|string|size:3',
49-
'opening_balance' => 'required|numeric',
42+
'currency' => 'nullable|string|max:10',
43+
'opening_balance' => 'nullable|numeric',
5044
]);
5145

52-
BankAccount::create([
46+
$account = BankAccount::create([
5347
...$data,
54-
'tenant_id' => $request->user()->tenant_id,
48+
'tenant_id' => $request->user()->tenant_id,
49+
'currency' => $data['currency'] ?? 'USD',
50+
'opening_balance' => $data['opening_balance'] ?? 0,
5551
]);
52+
$account->updateBalance();
5653

57-
return redirect()->route('finance.bank-accounts.index')
54+
return redirect()->back()
5855
->with('success', 'Bank account created.');
5956
}
6057

@@ -90,15 +87,16 @@ public function update(Request $request, BankAccount $bankAccount): RedirectResp
9087

9188
$data = $request->validate([
9289
'name' => 'required|string|max:255',
93-
'bank_name' => 'nullable|string|max:255',
90+
'bank_name' => 'required|string|max:255',
9491
'account_number' => 'nullable|string|max:255',
95-
'currency_code' => 'required|string|size:3',
96-
'opening_balance' => 'required|numeric',
92+
'currency' => 'nullable|string|max:10',
93+
'opening_balance' => 'nullable|numeric',
9794
]);
9895

9996
$bankAccount->update($data);
97+
$bankAccount->updateBalance();
10098

101-
return redirect()->route('finance.bank-accounts.index')
99+
return redirect()->back()
102100
->with('success', 'Bank account updated.');
103101
}
104102

@@ -108,7 +106,7 @@ public function destroy(BankAccount $bankAccount): RedirectResponse
108106

109107
$bankAccount->delete();
110108

111-
return redirect()->route('finance.bank-accounts.index')
109+
return redirect()->back()
112110
->with('success', 'Bank account deleted.');
113111
}
114112
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<?php
2+
3+
namespace App\Modules\Finance\Http\Controllers;
4+
5+
use App\Http\Controllers\Controller;
6+
use App\Modules\Finance\Models\BankAccount;
7+
use App\Modules\Finance\Models\BankReconciliation;
8+
use App\Modules\Finance\Models\BankTransaction;
9+
use Illuminate\Http\RedirectResponse;
10+
use Illuminate\Http\Request;
11+
use Inertia\Inertia;
12+
use Inertia\Response;
13+
14+
class BankReconciliationController extends Controller
15+
{
16+
public function index(Request $request): Response
17+
{
18+
$this->authorize('viewAny', BankReconciliation::class);
19+
20+
$query = BankReconciliation::with('account')
21+
->where('tenant_id', $request->user()->tenant_id);
22+
23+
if ($request->filled('bank_account_id')) {
24+
$query->where('bank_account_id', $request->bank_account_id);
25+
}
26+
27+
$reconciliations = $query->orderByDesc('statement_date')->paginate(20);
28+
29+
$bankAccounts = BankAccount::where('tenant_id', $request->user()->tenant_id)
30+
->get(['id', 'name', 'bank_name']);
31+
32+
return Inertia::render('Finance/BankReconciliations/Index', [
33+
'reconciliations' => $reconciliations,
34+
'bankAccounts' => $bankAccounts,
35+
'filters' => $request->only(['bank_account_id']),
36+
]);
37+
}
38+
39+
public function create(): Response
40+
{
41+
$this->authorize('create', BankReconciliation::class);
42+
43+
$bankAccounts = BankAccount::where('tenant_id', auth()->user()->tenant_id)
44+
->get(['id', 'name', 'bank_name']);
45+
46+
return Inertia::render('Finance/BankReconciliations/Create', [
47+
'bankAccounts' => $bankAccounts,
48+
]);
49+
}
50+
51+
public function store(Request $request): RedirectResponse
52+
{
53+
$this->authorize('create', BankReconciliation::class);
54+
55+
$data = $request->validate([
56+
'bank_account_id' => 'required|exists:bank_accounts,id',
57+
'statement_date' => 'required|date',
58+
'statement_balance' => 'required|numeric',
59+
'notes' => 'nullable|string',
60+
]);
61+
62+
$reconciliation = BankReconciliation::create([
63+
...$data,
64+
'tenant_id' => $request->user()->tenant_id,
65+
]);
66+
67+
return redirect()->route('finance.bank-reconciliations.show', $reconciliation)
68+
->with('success', 'Reconciliation created.');
69+
}
70+
71+
public function show(BankReconciliation $bankReconciliation): Response
72+
{
73+
$this->authorize('view', $bankReconciliation);
74+
75+
$bankReconciliation->load('account');
76+
77+
$transactions = BankTransaction::where('bank_account_id', $bankReconciliation->bank_account_id)
78+
->where('tenant_id', $bankReconciliation->tenant_id)
79+
->orderByDesc('transaction_date')
80+
->get();
81+
82+
return Inertia::render('Finance/BankReconciliations/Show', [
83+
'reconciliation' => array_merge($bankReconciliation->toArray(), [
84+
'difference' => $bankReconciliation->difference,
85+
'is_balanced' => $bankReconciliation->is_balanced,
86+
]),
87+
'transactions' => $transactions,
88+
]);
89+
}
90+
91+
public function complete(Request $request, BankReconciliation $bankReconciliation): RedirectResponse
92+
{
93+
$this->authorize('update', $bankReconciliation);
94+
95+
$bankReconciliation->complete($request->user());
96+
97+
return redirect()->back()
98+
->with('success', 'Reconciliation completed.');
99+
}
100+
101+
public function destroy(BankReconciliation $bankReconciliation): RedirectResponse
102+
{
103+
$this->authorize('delete', $bankReconciliation);
104+
105+
$bankReconciliation->delete();
106+
107+
return redirect()->route('finance.bank-reconciliations.index')
108+
->with('success', 'Reconciliation deleted.');
109+
}
110+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<?php
2+
3+
namespace App\Modules\Finance\Http\Controllers;
4+
5+
use App\Http\Controllers\Controller;
6+
use App\Modules\Finance\Models\BankAccount;
7+
use App\Modules\Finance\Models\BankTransaction;
8+
use Illuminate\Http\RedirectResponse;
9+
use Illuminate\Http\Request;
10+
use Inertia\Inertia;
11+
use Inertia\Response;
12+
13+
class BankTransactionController extends Controller
14+
{
15+
public function index(Request $request): Response
16+
{
17+
$this->authorize('viewAny', BankTransaction::class);
18+
19+
$query = BankTransaction::with('account')
20+
->where('tenant_id', $request->user()->tenant_id);
21+
22+
if ($request->filled('bank_account_id')) {
23+
$query->where('bank_account_id', $request->bank_account_id);
24+
}
25+
26+
$transactions = $query->orderByDesc('transaction_date')->orderByDesc('id')->paginate(20);
27+
28+
$bankAccounts = BankAccount::where('tenant_id', $request->user()->tenant_id)
29+
->get(['id', 'name', 'bank_name']);
30+
31+
return Inertia::render('Finance/BankTransactions/Index', [
32+
'transactions' => $transactions,
33+
'bankAccounts' => $bankAccounts,
34+
'filters' => $request->only(['bank_account_id']),
35+
]);
36+
}
37+
38+
public function store(Request $request): RedirectResponse
39+
{
40+
$this->authorize('create', BankTransaction::class);
41+
42+
$data = $request->validate([
43+
'bank_account_id' => 'required|exists:bank_accounts,id',
44+
'transaction_date' => 'required|date',
45+
'description' => 'required|string|max:500',
46+
'amount' => 'required|numeric',
47+
'type' => 'required|in:credit,debit',
48+
'reference' => 'nullable|string|max:255',
49+
]);
50+
51+
// For debit transactions, ensure amount is stored as negative
52+
if ($data['type'] === 'debit' && $data['amount'] > 0) {
53+
$data['amount'] = -abs($data['amount']);
54+
}
55+
56+
$transaction = BankTransaction::create([
57+
...$data,
58+
'tenant_id' => $request->user()->tenant_id,
59+
]);
60+
61+
$transaction->account->updateBalance();
62+
63+
return redirect()->back()
64+
->with('success', 'Transaction added.');
65+
}
66+
67+
public function reconcile(Request $request, BankTransaction $bankTransaction): RedirectResponse
68+
{
69+
$this->authorize('update', $bankTransaction);
70+
71+
$bankTransaction->update([
72+
'is_reconciled' => !$bankTransaction->is_reconciled,
73+
]);
74+
75+
return redirect()->back()
76+
->with('success', 'Transaction reconcile status updated.');
77+
}
78+
79+
public function destroy(BankTransaction $bankTransaction): RedirectResponse
80+
{
81+
$this->authorize('delete', $bankTransaction);
82+
83+
$account = $bankTransaction->account;
84+
$bankTransaction->delete();
85+
$account->updateBalance();
86+
87+
return redirect()->back()
88+
->with('success', 'Transaction deleted.');
89+
}
90+
}

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,25 @@ class BankAccount extends Model
1515
protected $fillable = [
1616
'tenant_id', 'name', 'account_number', 'bank_name',
1717
'currency_code', 'opening_balance',
18+
'currency', 'current_balance', 'is_active',
1819
];
1920

2021
protected $casts = [
2122
'opening_balance' => 'float',
23+
'current_balance' => 'float',
24+
'is_active' => 'boolean',
2225
];
2326

2427
public function transactions(): HasMany
2528
{
2629
return $this->hasMany(BankTransaction::class);
2730
}
2831

32+
public function reconciliations(): HasMany
33+
{
34+
return $this->hasMany(BankReconciliation::class);
35+
}
36+
2937
public function getBalanceAttribute(): float
3038
{
3139
return $this->opening_balance + $this->transactions()->sum('amount');
@@ -35,4 +43,10 @@ public function getUnreconciledCountAttribute(): int
3543
{
3644
return $this->transactions()->where('reconciled', false)->count();
3745
}
46+
47+
public function updateBalance(): void
48+
{
49+
$this->current_balance = $this->opening_balance + $this->transactions()->sum('amount');
50+
$this->save();
51+
}
3852
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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\Relations\HasMany;
10+
11+
class BankReconciliation extends Model
12+
{
13+
use BelongsToTenant;
14+
15+
protected $fillable = [
16+
'tenant_id', 'bank_account_id', 'statement_date', 'statement_balance',
17+
'reconciled_balance', 'status', 'notes', 'completed_by', 'completed_at',
18+
];
19+
20+
protected $casts = [
21+
'statement_balance' => 'float',
22+
'reconciled_balance' => 'float',
23+
'statement_date' => 'date',
24+
'completed_at' => 'datetime',
25+
];
26+
27+
public function account(): BelongsTo
28+
{
29+
return $this->belongsTo(BankAccount::class, 'bank_account_id');
30+
}
31+
32+
public function completedBy(): BelongsTo
33+
{
34+
return $this->belongsTo(User::class, 'completed_by');
35+
}
36+
37+
public function transactions(): HasMany
38+
{
39+
return $this->hasMany(BankTransaction::class, 'reconciliation_id');
40+
}
41+
42+
public function getDifferenceAttribute(): float
43+
{
44+
return $this->statement_balance - $this->reconciled_balance;
45+
}
46+
47+
public function getIsBalancedAttribute(): bool
48+
{
49+
return abs($this->difference) < 0.01;
50+
}
51+
52+
public function complete(User $user): void
53+
{
54+
$this->reconciled_balance = $this->transactions()->sum('amount');
55+
$this->status = 'completed';
56+
$this->completed_by = $user->id;
57+
$this->completed_at = now();
58+
$this->save();
59+
60+
$this->transactions()->update(['is_reconciled' => true]);
61+
}
62+
}

0 commit comments

Comments
 (0)