230 lines
7.6 KiB
PHP
230 lines
7.6 KiB
PHP
<?php
|
|
|
|
use App\Enums\AccountType;
|
|
use App\Models\Account;
|
|
use App\Models\AccountBalance;
|
|
use App\Models\User;
|
|
use App\Services\BalanceLookup;
|
|
use Carbon\Carbon;
|
|
|
|
beforeEach(function () {
|
|
$this->user = User::factory()->create();
|
|
$this->account = Account::factory()->create([
|
|
'user_id' => $this->user->id,
|
|
'type' => AccountType::Checking,
|
|
'currency_code' => 'USD',
|
|
]);
|
|
});
|
|
|
|
test('getBalanceAt returns carry-forward balance from before range', function () {
|
|
AccountBalance::factory()->create([
|
|
'account_id' => $this->account->id,
|
|
'balance_date' => '2025-12-15',
|
|
'balance' => 100000,
|
|
]);
|
|
|
|
$lookup = BalanceLookup::forAccounts(
|
|
[$this->account->id],
|
|
Carbon::parse('2026-01-01'),
|
|
Carbon::parse('2026-01-31'),
|
|
);
|
|
|
|
expect($lookup->getBalanceAt($this->account->id, Carbon::parse('2026-01-15')))->toBe(100000);
|
|
expect($lookup->getBalanceAt($this->account->id, Carbon::parse('2026-01-31')))->toBe(100000);
|
|
});
|
|
|
|
test('getBalanceAt returns zero when no balance exists', function () {
|
|
$lookup = BalanceLookup::forAccounts(
|
|
[$this->account->id],
|
|
Carbon::parse('2026-01-01'),
|
|
Carbon::parse('2026-01-31'),
|
|
);
|
|
|
|
expect($lookup->getBalanceAt($this->account->id, Carbon::parse('2026-01-15')))->toBe(0);
|
|
});
|
|
|
|
test('getBalanceAt returns latest balance on or before date within range', function () {
|
|
AccountBalance::factory()->create([
|
|
'account_id' => $this->account->id,
|
|
'balance_date' => '2026-01-05',
|
|
'balance' => 100000,
|
|
]);
|
|
AccountBalance::factory()->create([
|
|
'account_id' => $this->account->id,
|
|
'balance_date' => '2026-01-20',
|
|
'balance' => 200000,
|
|
]);
|
|
|
|
$lookup = BalanceLookup::forAccounts(
|
|
[$this->account->id],
|
|
Carbon::parse('2026-01-01'),
|
|
Carbon::parse('2026-01-31'),
|
|
);
|
|
|
|
expect($lookup->getBalanceAt($this->account->id, Carbon::parse('2026-01-03')))->toBe(0);
|
|
expect($lookup->getBalanceAt($this->account->id, Carbon::parse('2026-01-05')))->toBe(100000);
|
|
expect($lookup->getBalanceAt($this->account->id, Carbon::parse('2026-01-15')))->toBe(100000);
|
|
expect($lookup->getBalanceAt($this->account->id, Carbon::parse('2026-01-20')))->toBe(200000);
|
|
expect($lookup->getBalanceAt($this->account->id, Carbon::parse('2026-01-31')))->toBe(200000);
|
|
});
|
|
|
|
test('getBalanceAt works with multiple accounts', function () {
|
|
$account2 = Account::factory()->create([
|
|
'user_id' => $this->user->id,
|
|
'type' => AccountType::Savings,
|
|
'currency_code' => 'USD',
|
|
]);
|
|
|
|
AccountBalance::factory()->create([
|
|
'account_id' => $this->account->id,
|
|
'balance_date' => '2026-01-10',
|
|
'balance' => 100000,
|
|
]);
|
|
AccountBalance::factory()->create([
|
|
'account_id' => $account2->id,
|
|
'balance_date' => '2026-01-10',
|
|
'balance' => 500000,
|
|
]);
|
|
|
|
$lookup = BalanceLookup::forAccounts(
|
|
[$this->account->id, $account2->id],
|
|
Carbon::parse('2026-01-01'),
|
|
Carbon::parse('2026-01-31'),
|
|
);
|
|
|
|
expect($lookup->getBalanceAt($this->account->id, Carbon::parse('2026-01-15')))->toBe(100000);
|
|
expect($lookup->getBalanceAt($account2->id, Carbon::parse('2026-01-15')))->toBe(500000);
|
|
});
|
|
|
|
test('getInvestedAmountAt returns null when no invested data exists', function () {
|
|
AccountBalance::factory()->create([
|
|
'account_id' => $this->account->id,
|
|
'balance_date' => '2026-01-10',
|
|
'balance' => 100000,
|
|
'invested_amount' => null,
|
|
]);
|
|
|
|
$lookup = BalanceLookup::forAccounts(
|
|
[$this->account->id],
|
|
Carbon::parse('2026-01-01'),
|
|
Carbon::parse('2026-01-31'),
|
|
);
|
|
|
|
expect($lookup->getInvestedAmountAt($this->account->id, Carbon::parse('2026-01-15')))->toBeNull();
|
|
});
|
|
|
|
test('getInvestedAmountAt carries forward last known invested amount across null gaps', function () {
|
|
$account = Account::factory()->create([
|
|
'user_id' => $this->user->id,
|
|
'type' => AccountType::Investment,
|
|
'currency_code' => 'USD',
|
|
]);
|
|
|
|
AccountBalance::factory()->create([
|
|
'account_id' => $account->id,
|
|
'balance_date' => '2026-01-05',
|
|
'balance' => 500000,
|
|
'invested_amount' => 400000,
|
|
]);
|
|
AccountBalance::factory()->create([
|
|
'account_id' => $account->id,
|
|
'balance_date' => '2026-01-20',
|
|
'balance' => 550000,
|
|
'invested_amount' => null,
|
|
]);
|
|
|
|
$lookup = BalanceLookup::forAccounts(
|
|
[$account->id],
|
|
Carbon::parse('2026-01-01'),
|
|
Carbon::parse('2026-01-31'),
|
|
);
|
|
|
|
expect($lookup->getInvestedAmountAt($account->id, Carbon::parse('2026-01-05')))->toBe(400000);
|
|
// After the null entry on Jan 20, invested_amount should still carry forward from Jan 5
|
|
expect($lookup->getInvestedAmountAt($account->id, Carbon::parse('2026-01-25')))->toBe(400000);
|
|
});
|
|
|
|
test('getInvestedAmountAt carries forward from before range', function () {
|
|
$account = Account::factory()->create([
|
|
'user_id' => $this->user->id,
|
|
'type' => AccountType::Investment,
|
|
'currency_code' => 'USD',
|
|
]);
|
|
|
|
AccountBalance::factory()->create([
|
|
'account_id' => $account->id,
|
|
'balance_date' => '2025-12-15',
|
|
'balance' => 500000,
|
|
'invested_amount' => 400000,
|
|
]);
|
|
|
|
$lookup = BalanceLookup::forAccounts(
|
|
[$account->id],
|
|
Carbon::parse('2026-01-01'),
|
|
Carbon::parse('2026-01-31'),
|
|
);
|
|
|
|
expect($lookup->getInvestedAmountAt($account->id, Carbon::parse('2026-01-15')))->toBe(400000);
|
|
});
|
|
|
|
test('getInvestedAmountAt carry-forward seed uses latest non-null invested amount before range', function () {
|
|
$account = Account::factory()->create([
|
|
'user_id' => $this->user->id,
|
|
'type' => AccountType::Investment,
|
|
'currency_code' => 'USD',
|
|
]);
|
|
|
|
// Invested amount set early
|
|
AccountBalance::factory()->create([
|
|
'account_id' => $account->id,
|
|
'balance_date' => '2025-12-01',
|
|
'balance' => 400000,
|
|
'invested_amount' => 300000,
|
|
]);
|
|
// Latest balance before range has null invested_amount
|
|
AccountBalance::factory()->create([
|
|
'account_id' => $account->id,
|
|
'balance_date' => '2025-12-20',
|
|
'balance' => 450000,
|
|
'invested_amount' => null,
|
|
]);
|
|
|
|
$lookup = BalanceLookup::forAccounts(
|
|
[$account->id],
|
|
Carbon::parse('2026-01-01'),
|
|
Carbon::parse('2026-01-31'),
|
|
);
|
|
|
|
// Should carry forward balance from Dec 20 (latest before range)
|
|
expect($lookup->getBalanceAt($account->id, Carbon::parse('2026-01-10')))->toBe(450000);
|
|
// Should carry forward invested_amount from Dec 1 (latest non-null before range)
|
|
expect($lookup->getInvestedAmountAt($account->id, Carbon::parse('2026-01-10')))->toBe(300000);
|
|
});
|
|
|
|
test('forAccounts handles empty account list', function () {
|
|
$lookup = BalanceLookup::forAccounts(
|
|
[],
|
|
Carbon::parse('2026-01-01'),
|
|
Carbon::parse('2026-01-31'),
|
|
);
|
|
|
|
expect($lookup->getBalanceAt('nonexistent', Carbon::parse('2026-01-15')))->toBe(0);
|
|
expect($lookup->getInvestedAmountAt('nonexistent', Carbon::parse('2026-01-15')))->toBeNull();
|
|
});
|
|
|
|
test('forAccounts accepts a Collection of account IDs', function () {
|
|
AccountBalance::factory()->create([
|
|
'account_id' => $this->account->id,
|
|
'balance_date' => '2026-01-10',
|
|
'balance' => 100000,
|
|
]);
|
|
|
|
$lookup = BalanceLookup::forAccounts(
|
|
collect([$this->account->id]),
|
|
Carbon::parse('2026-01-01'),
|
|
Carbon::parse('2026-01-31'),
|
|
);
|
|
|
|
expect($lookup->getBalanceAt($this->account->id, Carbon::parse('2026-01-15')))->toBe(100000);
|
|
});
|