diff --git a/app/models/account.rb b/app/models/account.rb index 0c037609..a50ef13a 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -1,5 +1,5 @@ class Account < ApplicationRecord - include Syncable, Monetizable, Issuable, Chartable, Enrichable, Linkable + include Syncable, Monetizable, Issuable, Chartable, Enrichable, Linkable, Convertible validates :name, :balance, :currency, presence: true diff --git a/app/models/account/balance/syncer.rb b/app/models/account/balance/syncer.rb index cc8ca68b..7aeaebda 100644 --- a/app/models/account/balance/syncer.rb +++ b/app/models/account/balance/syncer.rb @@ -19,6 +19,8 @@ class Account::Balance::Syncer if strategy == :forward update_account_info end + + account.sync_required_exchange_rates end end diff --git a/app/models/account/convertible.rb b/app/models/account/convertible.rb new file mode 100644 index 00000000..8f5a1199 --- /dev/null +++ b/app/models/account/convertible.rb @@ -0,0 +1,28 @@ +module Account::Convertible + extend ActiveSupport::Concern + + def sync_required_exchange_rates + unless requires_exchange_rates? + Rails.logger.info("No exchange rate sync needed for account #{id}") + return + end + + rates = ExchangeRate.find_rates( + from: currency, + to: target_currency, + start_date: start_date, + cache: true # caches from provider to DB + ) + + Rails.logger.info("Synced #{rates.count} exchange rates for account #{id}") + end + + private + def target_currency + family.currency + end + + def requires_exchange_rates? + currency != target_currency + end +end diff --git a/test/models/account/chartable_test.rb b/test/models/account/chartable_test.rb index 196feca6..a7d3acc5 100644 --- a/test/models/account/chartable_test.rb +++ b/test/models/account/chartable_test.rb @@ -35,4 +35,27 @@ class Account::ChartableTest < ActiveSupport::TestCase assert_equal 3000, series.values.find { |v| v.date == 20.days.ago.to_date }.trend.current.amount assert_equal 3500, series.values.last.trend.current.amount end + + test "generates correct totals for multi currency families" do + family = families(:empty) + family.update!(currency: "USD") + + usd_account = family.accounts.create!(name: "Asset", currency: "USD", balance: 5000, accountable: Depository.new) + eur_account = family.accounts.create!(name: "Asset", currency: "EUR", balance: 1000, accountable: Depository.new) + + usd_account.balances.create!(date: 3.days.ago.to_date, balance: 5000, currency: "USD") + eur_account.balances.create!(date: 3.days.ago.to_date, balance: 1000, currency: "EUR") + + # 1 EUR = 1.1 USD, so 1000 EUR = 1100 USD + ExchangeRate.create!(from_currency: "EUR", to_currency: "USD", date: 3.days.ago.to_date, rate: 1.1) + ExchangeRate.create!(from_currency: "EUR", to_currency: "USD", date: 2.days.ago.to_date, rate: 1.1) + ExchangeRate.create!(from_currency: "EUR", to_currency: "USD", date: 1.days.ago.to_date, rate: 1.1) + ExchangeRate.create!(from_currency: "EUR", to_currency: "USD", date: Date.current, rate: 1.1) + + series = family.accounts.balance_series(currency: "USD", period: Period.last_7_days) + + assert_equal 0, series.values.first.trend.current.amount + assert_equal 6100, series.values.find { |v| v.date == 3.days.ago.to_date }.trend.current.amount + assert_equal 6100, series.values.last.trend.current.amount + end end diff --git a/test/models/account/convertible_test.rb b/test/models/account/convertible_test.rb new file mode 100644 index 00000000..a142f881 --- /dev/null +++ b/test/models/account/convertible_test.rb @@ -0,0 +1,59 @@ +require "test_helper" +require "ostruct" + +class Account::ConvertibleTest < ActiveSupport::TestCase + include Account::EntriesTestHelper + + setup do + @family = families(:empty) + @family.update!(currency: "USD") + + # Foreign account (currency is not in the family's primary currency, so it will require exchange rates for net worth rollups) + @account = @family.accounts.create!(name: "Test Account", currency: "EUR", balance: 10000, accountable: Depository.new) + + @provider = mock + ExchangeRate.stubs(:provider).returns(@provider) + end + + test "syncs required exchange rates for an account" do + create_valuation(account: @account, date: 5.days.ago.to_date, amount: 9500, currency: "EUR") + + # Since we had a valuation 5 days ago, this account starts 6 days ago and needs daily exchange rates looking forward + assert_equal 6.days.ago.to_date, @account.start_date + + @provider.expects(:fetch_exchange_rates) + .with( + from: "EUR", + to: "USD", + start_date: 6.days.ago.to_date, + end_date: Date.current + ).returns( + OpenStruct.new( + success?: true, + rates: [ + OpenStruct.new(date: 6.days.ago.to_date, rate: 1.1), + OpenStruct.new(date: 5.days.ago.to_date, rate: 1.2), + OpenStruct.new(date: 4.days.ago.to_date, rate: 1.3), + OpenStruct.new(date: 3.days.ago.to_date, rate: 1.4), + OpenStruct.new(date: 2.days.ago.to_date, rate: 1.5), + OpenStruct.new(date: 1.day.ago.to_date, rate: 1.6), + OpenStruct.new(date: Date.current, rate: 1.7) + ] + ) + ) + + assert_difference "ExchangeRate.count", 7 do + @account.sync_required_exchange_rates + end + end + + test "does not sync rates for a domestic account" do + @account.update!(currency: "USD") + + @provider.expects(:fetch_exchange_rates).never + + assert_no_difference "ExchangeRate.count" do + @account.sync_required_exchange_rates + end + end +end