diff --git a/app/Services/Banking/IndexaCapitalBalanceSyncService.php b/app/Services/Banking/IndexaCapitalBalanceSyncService.php index 118d00f1..09e25744 100644 --- a/app/Services/Banking/IndexaCapitalBalanceSyncService.php +++ b/app/Services/Banking/IndexaCapitalBalanceSyncService.php @@ -20,6 +20,7 @@ class IndexaCapitalBalanceSyncService $performance = $client->getPerformance($account->external_account_id); $portfolios = $performance['portfolios'] ?? []; + $netAmounts = $performance['net_amounts'] ?? []; if (empty($portfolios)) { Log::warning('No portfolio data from Indexa Capital', [ @@ -55,7 +56,7 @@ class IndexaCapitalBalanceSyncService } $balanceCents = (int) round(floatval($value) * 100); - $investedAmountCents = $this->calculateInvestedAmount($entry); + $investedAmountCents = $this->calculateInvestedAmount($entry, $netAmounts); $account->balances()->updateOrCreate( ['balance_date' => $date], @@ -76,20 +77,27 @@ class IndexaCapitalBalanceSyncService } /** - * Calculate invested amount from portfolio entry data. + * Calculate invested amount from the net_amounts data. * - * Uses instruments_cost + cash_amount when available (cost basis approach). - * Falls back to total_amount - return if the return field is present. + * Uses net_amounts (cumulative net inflows keyed by YYYYMMDD) which represents + * the actual money invested (inflows - outflows - tax_outflows), matching + * what Indexa Capital shows as "investment" on their dashboard. + * + * Falls back to total_amount - return if net_amounts is unavailable. * * @param array $entry + * @param array $netAmounts */ - private function calculateInvestedAmount(array $entry): ?int + private function calculateInvestedAmount(array $entry, array $netAmounts): ?int { - $instrumentsCost = $entry['instruments_cost'] ?? null; - $cashAmount = $entry['cash_amount'] ?? null; + $date = $entry['date'] ?? null; - if ($instrumentsCost !== null && $cashAmount !== null) { - return (int) round((floatval($instrumentsCost) + floatval($cashAmount)) * 100); + if ($date !== null && ! empty($netAmounts)) { + $dateKey = str_replace('-', '', $date); + + if (isset($netAmounts[$dateKey])) { + return (int) round(floatval($netAmounts[$dateKey]) * 100); + } } $totalAmount = $entry['total_amount'] ?? null; diff --git a/tests/Feature/OpenBanking/IndexaCapitalBalanceSyncTest.php b/tests/Feature/OpenBanking/IndexaCapitalBalanceSyncTest.php index e8f5db9c..4e767db5 100644 --- a/tests/Feature/OpenBanking/IndexaCapitalBalanceSyncTest.php +++ b/tests/Feature/OpenBanking/IndexaCapitalBalanceSyncTest.php @@ -164,7 +164,7 @@ test('handles empty portfolios array gracefully', function () { expect($account->balances()->count())->toBe(0); }); -test('stores invested_amount from instruments_cost and cash_amount', function () { +test('stores invested_amount from net_amounts data', function () { $user = User::factory()->onboarded()->create(); $connection = BankingConnection::factory()->indexaCapital()->create([ 'user_id' => $user->id, @@ -175,24 +175,33 @@ test('stores invested_amount from instruments_cost and cash_amount', function () 'external_account_id' => 'IC-001', ]); + $today = now()->toDateString(); + $yesterday = now()->subDay()->toDateString(); + $todayKey = str_replace('-', '', $today); + $yesterdayKey = str_replace('-', '', $yesterday); + Http::fake([ 'api.indexacapital.com/accounts/IC-001/performance' => Http::response([ 'portfolios' => [ [ - 'date' => now()->toDateString(), + 'date' => $today, 'total_amount' => 15000.00, 'instruments_cost' => 12000.00, 'instruments_amount' => 14700.00, 'cash_amount' => 300.00, ], [ - 'date' => now()->subDay()->toDateString(), + 'date' => $yesterday, 'total_amount' => 14500.00, 'instruments_cost' => 12000.00, 'instruments_amount' => 14200.00, 'cash_amount' => 300.00, ], ], + 'net_amounts' => [ + $todayKey => 11000.00, + $yesterdayKey => 10800.00, + ], ]), ]); @@ -203,16 +212,15 @@ test('stores invested_amount from instruments_cost and cash_amount', function () expect($account->balances()->count())->toBe(2); $latest = $account->balances()->orderBy('balance_date', 'desc')->first(); - // invested_amount = instruments_cost + cash_amount = 12000 + 300 = 12300 → 1230000 cents + // invested_amount comes from net_amounts, not instruments_cost + cash_amount expect($latest->balance)->toBe(1500000); - expect($latest->invested_amount)->toBe(1230000); + expect($latest->invested_amount)->toBe(1100000); $previous = $account->balances()->orderBy('balance_date', 'asc')->first(); - // invested_amount = 12000 + 300 = 12300 → 1230000 cents - expect($previous->invested_amount)->toBe(1230000); + expect($previous->invested_amount)->toBe(1080000); }); -test('stores null invested_amount when cost fields are missing', function () { +test('stores null invested_amount when net_amounts is missing', function () { $user = User::factory()->onboarded()->create(); $connection = BankingConnection::factory()->indexaCapital()->create([ 'user_id' => $user->id, @@ -242,7 +250,7 @@ test('stores null invested_amount when cost fields are missing', function () { expect($balance->invested_amount)->toBeNull(); }); -test('falls back to total_amount minus return when instruments_cost is missing', function () { +test('falls back to total_amount minus return when net_amounts is missing', function () { $user = User::factory()->onboarded()->create(); $connection = BankingConnection::factory()->indexaCapital()->create([ 'user_id' => $user->id, @@ -256,7 +264,7 @@ test('falls back to total_amount minus return when instruments_cost is missing', Http::fake([ 'api.indexacapital.com/accounts/IC-001/performance' => Http::response([ 'portfolios' => [ - // No instruments_cost/cash_amount, but has return field + // No net_amounts in response, but entry has return field ['date' => now()->toDateString(), 'total_amount' => 8000.00, 'return' => -500.00], ], ]),