Skip to content

Commit

Permalink
Enhance Bank Statement Reconciliation with Advanced Matching
Browse files Browse the repository at this point in the history
  • Loading branch information
sweep-ai[bot] authored Dec 24, 2024
1 parent 288e3e5 commit e52cb09
Show file tree
Hide file tree
Showing 4 changed files with 173 additions and 24 deletions.
59 changes: 54 additions & 5 deletions app/Filament/App/Resources/BankStatementResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@

use App\Filament\App\Resources\BankStatementResource\Pages;
use App\Models\BankStatement;
use App\Services\ReconciliationService;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables\Table;
use Filament\Tables;
use Filament\Forms\Components\FileUpload;
use Filament\Notifications\Notification;

class BankStatementResource extends Resource
{
Expand All @@ -34,24 +37,70 @@ public static function form(Form $form): Form
Forms\Components\TextInput::make('ending_balance')
->required()
->numeric(),
FileUpload::make('statement_file')
->label('Import Bank Statement')
->acceptedFileTypes(['text/csv', 'application/vnd.ms-excel'])
->visible(fn ($livewire) => $livewire instanceof Pages\CreateBankStatement)
->helperText('Upload a CSV file with your bank statement data'),
]);
}

public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('statement_date'),
Tables\Columns\TextColumn::make('statement_date')
->date(),
Tables\Columns\TextColumn::make('account.name'),
Tables\Columns\TextColumn::make('total_credits'),
Tables\Columns\TextColumn::make('total_debits'),
Tables\Columns\TextColumn::make('ending_balance'),
Tables\Columns\TextColumn::make('total_credits')
->money('USD'),
Tables\Columns\TextColumn::make('total_debits')
->money('USD'),
Tables\Columns\TextColumn::make('ending_balance')
->money('USD'),
Tables\Columns\IconColumn::make('reconciled')
->boolean()
->label('Reconciled'),
])
->filters([
//
Tables\Filters\Filter::make('date')
->form([
Forms\Components\DatePicker::make('from'),
Forms\Components\DatePicker::make('until'),
])
->query(function ($query, array $data) {
return $query
->when(
$data['from'],
fn($query) => $query->whereDate('statement_date', '>=', $data['from'])
)
->when(
$data['until'],
fn($query) => $query->whereDate('statement_date', '<=', $data['until'])
);
})
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\Action::make('reconcile')
->action(function (BankStatement $record) {
$reconciliationService = new ReconciliationService();
$result = $reconciliationService->reconcile($record);

Notification::make()
->title('Reconciliation Complete')
->body(view('bank-statements.reconciliation-result', [
'matched' => $result['matched_transactions']->count(),
'unmatched' => $result['unmatched_transactions']->count(),
'discrepancies' => $result['discrepancies'],
'balance_discrepancy' => $result['balance_discrepancy']
]))
->success()
->send();
})
->icon('heroicon-o-check-circle')
->color('success')
->requiresConfirmation(),
])
->bulkActions([
Tables\Actions\DeleteBulkAction::make(),
Expand Down
64 changes: 45 additions & 19 deletions app/Services/ReconciliationService.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,24 @@

use App\Models\BankStatement;
use App\Models\Transaction;
use Carbon\Carbon;

class ReconciliationService
{
public function reconcile(BankStatement $bankStatement)
{
$transactions = Transaction::where('account_id', $bankStatement->account_id)
->whereBetween('transaction_date', [$bankStatement->statement_date->startOfMonth(), $bankStatement->statement_date->endOfMonth()])
->whereBetween('transaction_date', [
$bankStatement->statement_date->startOfMonth(),
$bankStatement->statement_date->endOfMonth()
])
->get();

$totalCredits = 0;
$totalDebits = 0;
$matchedTransactions = collect();
$unmatchedTransactions = collect();
$discrepancies = collect();

foreach ($transactions as $transaction) {
if ($transaction->amount > 0) {
Expand All @@ -25,48 +30,69 @@ public function reconcile(BankStatement $bankStatement)
$totalDebits += abs($transaction->amount);
}

if ($this->matchTransaction($transaction, $bankStatement)) {
$matched = $this->matchTransaction($transaction, $bankStatement);

if ($matched) {
$matchedTransactions->push($transaction);
} else {
$unmatchedTransactions->push($transaction);
// Record discrepancy details
$discrepancies->push([
'transaction' => $transaction,
'type' => 'unmatched_transaction',
'amount' => $transaction->amount,
'date' => $transaction->transaction_date
]);
}
}

$discrepancy = ($totalCredits - $totalDebits) - ($bankStatement->total_credits - $bankStatement->total_debits);
$balanceDiscrepancy = ($totalCredits - $totalDebits) -
($bankStatement->total_credits - $bankStatement->total_debits);

if (abs($balanceDiscrepancy) > 0.01) {
$discrepancies->push([
'type' => 'balance_mismatch',
'amount' => $balanceDiscrepancy,
'expected' => $bankStatement->total_credits - $bankStatement->total_debits,
'actual' => $totalCredits - $totalDebits
]);
}

return [
'matched_transactions' => $matchedTransactions,
'unmatched_transactions' => $unmatchedTransactions,
'discrepancy' => $discrepancy,
'discrepancies' => $discrepancies,
'total_credits' => $totalCredits,
'total_debits' => $totalDebits,
'bank_statement_credits' => $bankStatement->total_credits,
'bank_statement_debits' => $bankStatement->total_debits,
'balance_discrepancy' => $balanceDiscrepancy
];
}

private function matchTransaction(Transaction $transaction, BankStatement $bankStatement)
{
// Implement more sophisticated matching logic
$matched = $bankStatement->transactions()
// Try exact match first
$exactMatch = $bankStatement->transactions()
->where('transaction_date', $transaction->transaction_date)
->where('amount', $transaction->amount)
->exists();

$transaction->update(['reconciled' => $matched]);

return $matched;
}

private function matchTransaction(Transaction $transaction, BankStatement $bankStatement)
{
// Implement matching logic here
// For example, match by date and amount
$matched = $bankStatement->transactions()
->where('transaction_date', $transaction->transaction_date)
if ($exactMatch) {
$transaction->update(['reconciled' => true]);
return true;
}

// Try fuzzy match within 2 days and same amount
$fuzzyMatch = $bankStatement->transactions()
->whereBetween('transaction_date', [
$transaction->transaction_date->subDays(2),
$transaction->transaction_date->addDays(2)
])
->where('amount', $transaction->amount)
->exists();

$transaction->update(['reconciled' => $matched]);
$transaction->update(['reconciled' => $fuzzyMatch]);
return $fuzzyMatch;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@


<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
public function up()
{
Schema::table('transactions', function (Blueprint $table) {
$table->boolean('reconciled')->default(false);
$table->text('discrepancy_notes')->nullable();
$table->timestamp('reconciled_at')->nullable();
$table->foreignId('reconciled_by_user_id')->nullable()->constrained('users')->nullOnDelete();
});
}

public function down()
{
Schema::table('transactions', function (Blueprint $table) {
$table->dropForeign(['reconciled_by_user_id']);
$table->dropColumn(['reconciled', 'discrepancy_notes', 'reconciled_at', 'reconciled_by_user_id']);
});
}
};
46 changes: 46 additions & 0 deletions resources/views/bank-statements/reconciliation-result.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@


<div class="space-y-4">
<div class="flex justify-between">
<div class="text-sm font-medium text-gray-500">Matched Transactions</div>
<div class="text-sm font-semibold text-green-600">{{ $matched }}</div>
</div>

<div class="flex justify-between">
<div class="text-sm font-medium text-gray-500">Unmatched Transactions</div>
<div class="text-sm font-semibold text-red-600">{{ $unmatched }}</div>
</div>

@if($discrepancies->isNotEmpty())
<div class="mt-4">
<div class="text-sm font-medium text-gray-500 mb-2">Discrepancies Found:</div>
<div class="space-y-2">
@foreach($discrepancies as $discrepancy)
<div class="bg-red-50 p-2 rounded">
@if($discrepancy['type'] === 'unmatched_transaction')
<div class="text-sm text-red-700">
Unmatched: {{ money($discrepancy['amount']) }} on {{ $discrepancy['date']->format('Y-m-d') }}
</div>
@elseif($discrepancy['type'] === 'balance_mismatch')
<div class="text-sm text-red-700">
Balance Mismatch: {{ money($discrepancy['amount']) }}
<div class="text-xs text-red-600">
Expected: {{ money($discrepancy['expected']) }}
Actual: {{ money($discrepancy['actual']) }}
</div>
</div>
@endif
</div>
@endforeach
</div>
</div>
@endif

@if($balance_discrepancy != 0)
<div class="mt-4 bg-yellow-50 p-3 rounded">
<div class="text-sm font-medium text-yellow-800">
Total Balance Discrepancy: {{ money($balance_discrepancy) }}
</div>
</div>
@endif
</div>

0 comments on commit e52cb09

Please sign in to comment.