mirror of
https://github.com/we-promise/sure.git
synced 2026-06-07 19:59:00 +00:00
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:
@@ -52,11 +52,11 @@ class Balance::ChartSeriesBuilderTest < ActiveSupport::TestCase
|
||||
)
|
||||
|
||||
# Only 1 rate in DB. We'll be missing the first and last days in the series.
|
||||
# This rate should be applied to 1 day ago and today, but not 2 days ago (will fall back to 1)
|
||||
# This rate should be applied to all days: LOCF for future dates, nearest future rate for earlier dates.
|
||||
ExchangeRate.create!(date: 1.day.ago.to_date, from_currency: "USD", to_currency: "EUR", rate: 2)
|
||||
|
||||
expected = [
|
||||
1000, # No rate available, so fall back to 1:1 conversion (1000 USD = 1000 EUR)
|
||||
2000, # No prior rate, so use nearest future rate (2:1 from 1 day ago): 1000 * 2 = 2000
|
||||
2200, # Rate available, so use 2:1 conversion (1100 USD = 2200 EUR)
|
||||
2400 # Rate NOT available, but LOCF will use the last available rate, so use 2:1 conversion (1200 USD = 2400 EUR)
|
||||
]
|
||||
|
||||
@@ -67,4 +67,32 @@ class ExchangeRateTest < ActiveSupport::TestCase
|
||||
|
||||
assert_nil ExchangeRate.find_or_fetch_rate(from: "USD", to: "EUR", date: Date.current, cache: true)
|
||||
end
|
||||
|
||||
test "reuses nearest cached rate within lookback window instead of calling provider" do
|
||||
# Simulate a rate saved under Friday's date when Saturday is requested
|
||||
friday = 1.day.ago.to_date
|
||||
ExchangeRate.create!(from_currency: "USD", to_currency: "JPY", date: friday, rate: 150.5)
|
||||
|
||||
saturday = Date.current
|
||||
|
||||
@provider.expects(:fetch_exchange_rate).never
|
||||
|
||||
result = ExchangeRate.find_or_fetch_rate(from: "USD", to: "JPY", date: saturday)
|
||||
assert_equal 150.5, result.rate
|
||||
assert_equal friday, result.date
|
||||
end
|
||||
|
||||
test "does not reuse cached rate outside lookback window" do
|
||||
old_date = (ExchangeRate::NEAREST_RATE_LOOKBACK_DAYS + 1).days.ago.to_date
|
||||
ExchangeRate.create!(from_currency: "USD", to_currency: "JPY", date: old_date, rate: 140.0)
|
||||
|
||||
provider_response = provider_success_response(
|
||||
OpenStruct.new(from: "USD", to: "JPY", date: Date.current, rate: 155.0)
|
||||
)
|
||||
|
||||
@provider.expects(:fetch_exchange_rate).returns(provider_response)
|
||||
|
||||
result = ExchangeRate.find_or_fetch_rate(from: "USD", to: "JPY", date: Date.current)
|
||||
assert_equal 155.0, result.rate
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user