whisper-money/tests/Feature/OpenBanking/BalanceSyncServiceTest.php

184 lines
6.5 KiB
PHP

<?php
use App\Contracts\BankingProviderInterface;
use App\Models\Account;
use App\Models\AccountBalance;
use App\Models\BankingConnection;
use App\Models\Transaction;
use App\Models\User;
use App\Services\Banking\BalanceSyncService;
test('calculateHistoricalBalances derives balances from transactions', 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',
]);
// Reference balance: end of Feb 10, balance = 100000 (€1,000.00)
AccountBalance::factory()->create([
'account_id' => $account->id,
'balance_date' => '2026-02-10',
'balance' => 100000,
]);
// Transactions: Feb 10 had -5000 (debit), Feb 8 had +20000 (credit), Feb 5 had -10000 (debit)
Transaction::factory()->enableBanking()->create([
'user_id' => $user->id,
'account_id' => $account->id,
'transaction_date' => '2026-02-10',
'amount' => -5000,
]);
Transaction::factory()->enableBanking()->create([
'user_id' => $user->id,
'account_id' => $account->id,
'transaction_date' => '2026-02-08',
'amount' => 20000,
]);
Transaction::factory()->enableBanking()->create([
'user_id' => $user->id,
'account_id' => $account->id,
'transaction_date' => '2026-02-05',
'amount' => -10000,
]);
$service = new BalanceSyncService(Mockery::mock(BankingProviderInterface::class));
$service->calculateHistoricalBalances($account);
// End of Feb 10: 100000 (reference)
// End of Feb 8: 100000 - (-5000) = 105000 (before Feb 10 transactions)
// End of Feb 5: 105000 - 20000 = 85000 (before Feb 8 transactions)
expect($account->balances()->count())->toBe(3);
$feb8 = $account->balances()->where('balance_date', '2026-02-08')->first();
expect($feb8->balance)->toBe(105000);
$feb5 = $account->balances()->where('balance_date', '2026-02-05')->first();
expect($feb5->balance)->toBe(85000);
});
test('calculateHistoricalBalances handles multiple transactions per day', 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,
]);
// Two transactions on Feb 8: -3000 and -7000 = total -10000
Transaction::factory()->enableBanking()->create([
'user_id' => $user->id,
'account_id' => $account->id,
'transaction_date' => '2026-02-08',
'amount' => -3000,
]);
Transaction::factory()->enableBanking()->create([
'user_id' => $user->id,
'account_id' => $account->id,
'transaction_date' => '2026-02-08',
'amount' => -7000,
]);
$service = new BalanceSyncService(Mockery::mock(BankingProviderInterface::class));
$service->calculateHistoricalBalances($account);
// End of Feb 8: 100000 (no transactions between Feb 8 and Feb 10 on the reference date)
// Wait - there are no transactions on Feb 10, so running_balance stays 100000
// End of Feb 8: 100000
expect($account->balances()->count())->toBe(2);
$feb8 = $account->balances()->where('balance_date', '2026-02-08')->first();
expect($feb8->balance)->toBe(100000);
});
test('calculateHistoricalBalances skips dates with existing balances', 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,
]);
// Existing balance from balance_after_transaction (more accurate)
AccountBalance::factory()->create([
'account_id' => $account->id,
'balance_date' => '2026-02-05',
'balance' => 77777,
]);
Transaction::factory()->enableBanking()->create([
'user_id' => $user->id,
'account_id' => $account->id,
'transaction_date' => '2026-02-08',
'amount' => 20000,
]);
Transaction::factory()->enableBanking()->create([
'user_id' => $user->id,
'account_id' => $account->id,
'transaction_date' => '2026-02-05',
'amount' => -10000,
]);
$service = new BalanceSyncService(Mockery::mock(BankingProviderInterface::class));
$service->calculateHistoricalBalances($account);
// Feb 8 should be calculated, Feb 5 should NOT be overwritten
expect($account->balances()->count())->toBe(3);
$feb5 = $account->balances()->where('balance_date', '2026-02-05')->first();
expect($feb5->balance)->toBe(77777); // Preserved original value
});
test('calculateHistoricalBalances does nothing without reference balance', function () {
$user = User::factory()->onboarded()->create();
$account = Account::factory()->create([
'user_id' => $user->id,
]);
Transaction::factory()->enableBanking()->create([
'user_id' => $user->id,
'account_id' => $account->id,
'transaction_date' => '2026-02-08',
'amount' => -5000,
]);
$service = new BalanceSyncService(Mockery::mock(BankingProviderInterface::class));
$service->calculateHistoricalBalances($account);
expect($account->balances()->count())->toBe(0);
});
test('calculateHistoricalBalances does nothing without transactions', function () {
$user = User::factory()->onboarded()->create();
$account = Account::factory()->create([
'user_id' => $user->id,
]);
AccountBalance::factory()->create([
'account_id' => $account->id,
'balance_date' => '2026-02-10',
'balance' => 100000,
]);
$service = new BalanceSyncService(Mockery::mock(BankingProviderInterface::class));
$service->calculateHistoricalBalances($account);
expect($account->balances()->count())->toBe(1);
});