From 0fe7bf7fc6e4e5ffee4e96a48ce1831841bfbeb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Falc=C3=B3n?= Date: Tue, 5 May 2026 15:26:28 +0100 Subject: [PATCH] Batch historical balance writes (#356) ## Summary - Batch calculated historical account balances with one upsert - Keep existing balance dates untouched - Add regression coverage for one account_balances write query Fixes PHP-LARAVEL-1C ## Tests - php artisan test --compact tests/Feature/OpenBanking/BalanceSyncServiceTest.php - vendor/bin/pint --dirty --format agent --- app/Services/Banking/BalanceSyncService.php | 16 +++++++-- .../OpenBanking/BalanceSyncServiceTest.php | 33 +++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/app/Services/Banking/BalanceSyncService.php b/app/Services/Banking/BalanceSyncService.php index 321cc639..71f56369 100644 --- a/app/Services/Banking/BalanceSyncService.php +++ b/app/Services/Banking/BalanceSyncService.php @@ -4,8 +4,10 @@ namespace App\Services\Banking; use App\Contracts\BankingProviderInterface; use App\Models\Account; +use App\Models\AccountBalance; use Carbon\Carbon; use Illuminate\Support\Facades\Log; +use Illuminate\Support\Str; class BalanceSyncService { @@ -87,18 +89,28 @@ class BalanceSyncService $runningBalance = $referenceBalance->balance; $referenceDate = $referenceBalance->balance_date->toDateString(); + $now = now(); + $rows = []; foreach ($dailyTotals as $date => $sum) { if ($date < $referenceDate && ! isset($existingDates[$date])) { - $account->balances()->create([ + $rows[] = [ + 'id' => (string) Str::uuid(), + 'account_id' => $account->id, 'balance_date' => $date, 'balance' => $runningBalance, - ]); + 'created_at' => $now, + 'updated_at' => $now, + ]; } $runningBalance -= (int) $sum; } + if ($rows !== []) { + AccountBalance::upsert($rows, ['account_id', 'balance_date'], ['balance', 'updated_at']); + } + Log::info('Calculated historical balances', [ 'account_id' => $account->id, 'reference_date' => $referenceDate, diff --git a/tests/Feature/OpenBanking/BalanceSyncServiceTest.php b/tests/Feature/OpenBanking/BalanceSyncServiceTest.php index e5f77fcd..d904ac09 100644 --- a/tests/Feature/OpenBanking/BalanceSyncServiceTest.php +++ b/tests/Feature/OpenBanking/BalanceSyncServiceTest.php @@ -59,6 +59,39 @@ test('calculateHistoricalBalances derives balances from transactions', function expect($feb5->balance)->toBe(85000); }); +test('calculateHistoricalBalances writes missing balances in one query', function () { + $user = User::factory()->onboarded()->create(); + $connection = BankingConnection::factory()->create(['user_id' => $user->id]); + $account = Account::factory()->connected()->create([ + 'user_id' => $user->id, + 'banking_connection_id' => $connection->id, + 'external_account_id' => 'ext-123', + ]); + + AccountBalance::factory()->create([ + 'account_id' => $account->id, + 'balance_date' => '2026-02-10', + 'balance' => 100000, + ]); + + foreach (range(0, 5) as $daysBack) { + Transaction::factory()->enableBanking()->create([ + 'user_id' => $user->id, + 'account_id' => $account->id, + 'transaction_date' => sprintf('2026-02-%02d', 10 - $daysBack), + 'amount' => -1000, + ]); + } + + $service = new BalanceSyncService(Mockery::mock(BankingProviderInterface::class)); + + $result = countQueries(fn () => $service->calculateHistoricalBalances($account)); + $balanceWrites = collect($result['queries'])->filter(fn (string $query): bool => str_contains($query, 'account_balances') && str_contains(strtolower($query), 'insert')); + + expect($balanceWrites)->toHaveCount(1) + ->and($account->balances()->count())->toBe(6); +}); + test('calculateHistoricalBalances handles multiple transactions per day', function () { $user = User::factory()->onboarded()->create(); $connection = BankingConnection::factory()->create(['user_id' => $user->id]);