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
This commit is contained in:
Víctor Falcón 2026-05-05 15:26:28 +01:00 committed by GitHub
parent 22043ced29
commit 0fe7bf7fc6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 47 additions and 2 deletions

View File

@ -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,

View File

@ -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]);