Fix foreign currency accounts using wrong exchange rate in balance sheet totals (#1010)

Balance sheet totals and accountable type summaries used a SQL JOIN on
exchange_rates matching only today's date, which returned NULL (defaulting
to 1:1) when no rate existed for that exact date. This caused foreign
currency accounts to show incorrect totals.

Changes:
- Refactor BalanceSheet::AccountTotals to batch-fetch exchange rates via
  ExchangeRate.rates_for, with provider fallback, instead of a SQL join
- Refactor Accountable.balance_money to use the same batch approach
- Add ExchangeRate.rates_for helper for deduplicated rate lookups
- Fix net worth chart query to fall back to the nearest future rate when
  no historical rate exists for a given date
- Add composite index on accounts (family_id, status, accountable_type)
- Reuse nearest cached exchange rate within a 5-day lookback window
  before calling the provider, preventing redundant API calls on
  weekends and holidays when providers return prior-day rates

https://claude.ai/code/session_01GyssBJxQqdWnuYofQRjUu8

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Mikael
2026-02-20 02:07:47 +09:00
committed by GitHub
parent a63e1c5a89
commit cc20e2c19c
8 changed files with 132 additions and 37 deletions

View File

@@ -62,15 +62,21 @@ module Accountable
self.name.pluralize.titleize
end
# Sums the balances of all active accounts of this type, converting foreign currencies to the family's currency.
# @return [BigDecimal] total balance in the family's currency
def balance_money(family)
family.accounts
.active
.joins(sanitize_sql_array([
"LEFT JOIN exchange_rates ON exchange_rates.date = :current_date AND accounts.currency = exchange_rates.from_currency AND exchange_rates.to_currency = :family_currency",
{ current_date: Date.current.to_s, family_currency: family.currency }
]))
.where(accountable_type: self.name)
.sum("accounts.balance * COALESCE(exchange_rates.rate, 1)")
accounts = family.accounts.active.where(accountable_type: self.name).to_a
foreign_currencies = accounts.filter_map { |a| a.currency if a.currency != family.currency }
rates = ExchangeRate.rates_for(foreign_currencies, to: family.currency, date: Date.current)
accounts.sum(BigDecimal(0)) { |account|
if account.currency == family.currency
account.balance
else
account.balance * (rates[account.currency] || 1)
end
}
end
end