1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-02 20:15:22 +02:00

Multi-Currency Part 2 (#543)

* Support all currencies, handle outside DB

* Remove currencies from seed

* Fix account balance namespace

* Set default currency on authentication

* Cache currency instances

* Implement multi-currency syncs with tests

* Series fallback, passing tests

* Fix conflicts

* Make value group concrete class that works with currency values

* Fix migration conflict

* Update tests to expect multi-currency results

* Update account list to use group method

* Namespace updates

* Fetch unknown exchange rates from API

* Fix date range bug

* Ensure demo data works without external API

* Enforce cascades only at DB level
This commit is contained in:
Zach Gollwitzer 2024-03-21 13:39:10 -04:00 committed by GitHub
parent de0cba9fed
commit 110855d077
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
55 changed files with 1226 additions and 714 deletions

View file

@ -6,7 +6,7 @@ class Account < ApplicationRecord
broadcasts_refreshes
belongs_to :family
has_many :balances, class_name: "AccountBalance"
has_many :balances
has_many :valuations
has_many :transactions
@ -20,8 +20,6 @@ class Account < ApplicationRecord
delegated_type :accountable, types: Accountable::TYPES, dependent: :destroy
before_create :check_currency
def self.ransackable_attributes(auth_object = nil)
%w[name]
end
@ -30,6 +28,17 @@ class Account < ApplicationRecord
balances.where("date <= ?", date).order(date: :desc).first&.balance
end
# e.g. Wise, Revolut accounts that have transactions in multiple currencies
def multi_currency?
currencies = [ valuations.pluck(:currency), transactions.pluck(:currency) ].flatten.uniq
currencies.count > 1
end
# e.g. Accounts denominated in currency other than family currency
def foreign_currency?
currency != family.currency
end
def self.by_provider
# TODO: When 3rd party providers are supported, dynamically load all providers and their accounts
[ { name: "Manual accounts", accounts: all.order(balance: :desc).group_by(&:accountable_type) } ]
@ -39,35 +48,41 @@ class Account < ApplicationRecord
exists?(status: "syncing")
end
def series(period = Period.all)
TimeSeries.from_collection(balances.in_period(period), :balance_money)
def series(period: Period.all, currency: self.currency)
balance_series = balances.in_period(period).where(currency: Money::Currency.new(currency).iso_code)
if balance_series.empty? && period.date_range.end == Date.current
converted_balance = balance_money.exchange_to(currency)
if converted_balance
TimeSeries.new([ { date: Date.current, value: converted_balance } ])
else
TimeSeries.new([])
end
else
TimeSeries.from_collection(balance_series, :balance_money)
end
end
def self.by_group(period = Period.all)
grouped_accounts = { assets: ValueGroup.new("Assets"), liabilities: ValueGroup.new("Liabilities") }
def self.by_group(period: Period.all, currency: Money.default_currency)
grouped_accounts = { assets: ValueGroup.new("Assets", currency), liabilities: ValueGroup.new("Liabilities", currency) }
Accountable.by_classification.each do |classification, types|
types.each do |type|
group = grouped_accounts[classification.to_sym].add_child_node(type)
group = grouped_accounts[classification.to_sym].add_child_group(type, currency)
Accountable.from_type(type).includes(:account).each do |accountable|
account = accountable.account
value_node = group.add_value_node(account)
value_node.attach_series(account.series(period))
next unless account
value_node = group.add_value_node(
account,
account.balance_money.exchange_to(currency) || Money.new(0, currency),
account.series(period: period, currency: currency)
)
end
end
end
grouped_accounts
end
private
def check_currency
if self.currency == self.family.currency
self.converted_balance = self.balance
self.converted_currency = self.currency
else
self.converted_balance = ExchangeRate.convert(self.currency, self.family.currency, self.balance)
self.converted_currency = self.family.currency
end
end
end