$accounts * @return array}> */ public function getAccountMetrics(string $userCurrency, Collection $accounts): array { $now = Carbon::now(); $rangeStart = $now->copy()->subMonths(12)->startOfMonth(); $accountIds = $accounts->pluck('id'); $lookup = BalanceLookup::forAccounts($accountIds, $rangeStart, $now->copy()); $this->preloadExchangeRates($userCurrency, $accounts, $this->monthlyEndDateStrings($rangeStart, $now)); $metrics = []; foreach ($accounts as $account) { $history = []; $current = $rangeStart->copy(); $endMonth = $now->copy()->startOfMonth(); while ($current->lte($endMonth)) { $date = $current->copy()->endOfMonth(); $originalBalance = $lookup->getBalanceAt($account->id, $date); $convertedBalance = $this->convertBalance($originalBalance, $account->currency_code, $userCurrency, $date->toDateString()); $point = [ 'date' => $this->formatMonth($date), 'value' => $convertedBalance, ]; if ($account->type->supportsInvestedAmount()) { $investedAmount = $lookup->getInvestedAmountAt($account->id, $date); $point['investedAmount'] = $investedAmount; } $history[] = $point; $current->addMonth(); } $currentBalance = end($history)['value'] ?? 0; $previousBalance = count($history) > 1 ? $history[count($history) - 2]['value'] : 0; $investedAmount = null; if ($account->type->supportsInvestedAmount()) { $investedAmount = $lookup->getInvestedAmountAt($account->id, $now); } $metrics[$account->id] = [ 'currentBalance' => $currentBalance, 'previousBalance' => $previousBalance, 'diff' => $currentBalance - $previousBalance, 'investedAmount' => $investedAmount, 'history' => $history, ]; } return $metrics; } /** * Build monthly net worth evolution data points for all accounts. * * Each point contains the month, timestamp, and per-account converted balances. * Accounts with a different currency than the user also get `_original` entries. * Investment accounts get `_invested` entries. * * @param Collection $accounts * @return array{data: list>, accounts: mixed, currency_code: string} */ public function getNetWorthEvolution(string $userCurrency, Collection $accounts, Carbon $start, Carbon $end): array { $accountIds = $accounts->pluck('id'); $lookupEnd = Carbon::now()->gt($end) ? Carbon::now() : $end->copy(); $lookup = BalanceLookup::forAccounts($accountIds, $start->copy()->startOfMonth(), $lookupEnd); $this->preloadExchangeRates($userCurrency, $accounts, $this->monthlyEndDateStrings($start, $end)); $points = []; $current = $start->copy()->startOfMonth(); $endMonth = $end->copy()->startOfMonth(); while ($current->lte($endMonth)) { $date = $current->copy()->endOfMonth(); $point = [ 'month' => $date->format('Y-m'), 'timestamp' => $date->timestamp, ]; foreach ($accounts as $account) { $originalBalance = $lookup->getBalanceAt($account->id, $date); $convertedBalance = $this->convertBalance( $originalBalance, $account->currency_code, $userCurrency, $date->toDateString(), ); $point[$account->id] = $convertedBalance; if ($account->currency_code !== $userCurrency) { $point[$account->id.'_original'] = [ 'amount' => $originalBalance, 'currency_code' => $account->currency_code, ]; } if ($account->type->supportsInvestedAmount()) { $point[$account->id.'_invested'] = $lookup->getInvestedAmountAt($account->id, $date); } } $points[] = $point; $current->addMonth(); } $now = Carbon::now(); $accountsConfig = $accounts->mapWithKeys(function ($account) use ($lookup, $now) { $config = [ 'id' => $account->id, 'name' => $account->name, 'name_iv' => $account->name_iv, 'encrypted' => $account->encrypted, 'type' => $account->type, 'currency_code' => $account->currency_code, 'bank' => $account->bank, 'banking_connection_id' => $account->banking_connection_id, 'hidden_on_dashboard' => $account->hidden_on_dashboard, ]; if ($account->type === AccountType::RealEstate && $account->relationLoaded('realEstateDetail') && $account->realEstateDetail?->linked_loan_account_id) { $config['linked_loan_account_id'] = $account->realEstateDetail->linked_loan_account_id; } if ($account->type->supportsInvestedAmount()) { $config['invested_amount'] = $lookup->getInvestedAmountAt($account->id, $now); } return [$account->id => $config]; }); return [ 'data' => $points, 'accounts' => $accountsConfig, 'currency_code' => $userCurrency, ]; } /** * Build daily net worth evolution data points for all accounts. * * Each point contains the date, timestamp, and per-account converted balances. * Accounts with a different currency than the user also get `_original` entries. * * @param Collection $accounts * @return array{data: list>, accounts: mixed, currency_code: string} */ public function getNetWorthDailyEvolution(string $userCurrency, Collection $accounts, Carbon $start, Carbon $end): array { $accountIds = $accounts->pluck('id'); $lookup = BalanceLookup::forAccounts($accountIds, $start, $end); $this->preloadExchangeRates($userCurrency, $accounts, $this->dailyDateStrings($start, $end)); $points = []; $current = $start->copy(); while ($current->lte($end)) { $date = $current->copy(); $point = [ 'date' => $date->format('Y-m-d'), 'timestamp' => $date->endOfDay()->timestamp, ]; foreach ($accounts as $account) { $originalBalance = $lookup->getBalanceAt($account->id, $date); $convertedBalance = $this->convertBalance( $originalBalance, $account->currency_code, $userCurrency, $date->toDateString(), ); $point[$account->id] = $convertedBalance; if ($account->currency_code !== $userCurrency) { $point[$account->id.'_original'] = [ 'amount' => $originalBalance, 'currency_code' => $account->currency_code, ]; } } $points[] = $point; $current->addDay(); } $accountsConfig = $accounts->mapWithKeys(function ($account) { return [ $account->id => [ 'id' => $account->id, 'name' => $account->name, 'name_iv' => $account->name_iv, 'encrypted' => $account->encrypted, 'type' => $account->type, 'currency_code' => $account->currency_code, 'bank' => $account->bank, ], ]; }); return [ 'data' => $points, 'accounts' => $accountsConfig, 'currency_code' => $userCurrency, ]; } /** * Convert a balance from one currency to another. */ private function convertBalance(int $balance, string $sourceCurrency, string $targetCurrency, string $date): int { if (strtolower($sourceCurrency) === strtolower($targetCurrency)) { return $balance; } return $this->exchangeRateService->convert($sourceCurrency, $targetCurrency, $balance, $date); } /** * @param Collection $accounts * @param list $dates */ private function preloadExchangeRates(string $userCurrency, Collection $accounts, array $dates): void { $hasForeignCurrencyAccount = $accounts->contains( fn (Account $account): bool => strcasecmp($account->currency_code, $userCurrency) !== 0, ); if (! $hasForeignCurrencyAccount) { return; } $this->exchangeRateService->preloadRates($userCurrency, $dates); } /** * @return list */ private function monthlyEndDateStrings(Carbon $start, Carbon $end): array { $dates = []; $current = $start->copy()->startOfMonth(); $endMonth = $end->copy()->startOfMonth(); while ($current->lte($endMonth)) { $dates[] = $current->copy()->endOfMonth()->toDateString(); $current->addMonth(); } return $dates; } /** * @return list */ private function dailyDateStrings(Carbon $start, Carbon $end): array { $dates = []; $current = $start->copy(); while ($current->lte($end)) { $dates[] = $current->toDateString(); $current->addDay(); } return $dates; } /** * Format a month label: "M" for current year, "M 'y" for previous years. */ private function formatMonth(Carbon $date): string { $isCurrentYear = $date->year === Carbon::now()->year; return $isCurrentYear ? $date->format('M') : $date->format("M 'y"); } }