1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-07-24 15:49:39 +02:00
Maybe/app/models/account/chartable.rb

103 lines
3.1 KiB
Ruby
Raw Normal View History

module Account::Chartable
extend ActiveSupport::Concern
class_methods do
def balance_series(currency:, period: Period.last_30_days, favorable_direction: "up")
balances = Account::Balance.find_by_sql([
balance_series_query,
{
start_date: period.start_date,
end_date: period.end_date,
interval: period.interval,
target_currency: currency
}
])
balances = gapfill_balances(balances)
values = [ nil, *balances ].each_cons(2).map do |prev, curr|
Series::Value.new(
date: curr.date,
date_formatted: I18n.l(curr.date, format: :long),
trend: Trend.new(
current: Money.new(curr.balance, currency),
previous: prev.nil? ? nil : Money.new(prev.balance, currency),
favorable_direction: favorable_direction
)
)
end
Series.new(
start_date: period.start_date,
end_date: period.end_date,
interval: period.interval,
trend: Trend.new(
current: Money.new(balances.last&.balance || 0, currency),
previous: Money.new(balances.first&.balance || 0, currency),
favorable_direction: favorable_direction
),
values: values
)
end
private
def balance_series_query
<<~SQL
WITH dates as (
SELECT generate_series(DATE :start_date, DATE :end_date, :interval::interval)::date as date
UNION DISTINCT
SELECT CURRENT_DATE -- Ensures we always end on current date, regardless of interval
)
SELECT
d.date,
SUM(CASE WHEN accounts.classification = 'asset' THEN ab.balance ELSE -ab.balance END * COALESCE(er.rate, 1)) as balance,
COUNT(CASE WHEN accounts.currency <> :target_currency AND er.rate IS NULL THEN 1 END) as missing_rates
FROM dates d
LEFT JOIN accounts ON accounts.id IN (#{all.select(:id).to_sql})
LEFT JOIN account_balances ab ON (
ab.date = d.date AND
ab.currency = accounts.currency AND
ab.account_id = accounts.id
)
LEFT JOIN exchange_rates er ON (
er.date = ab.date AND
er.from_currency = accounts.currency AND
er.to_currency = :target_currency
)
GROUP BY d.date
ORDER BY d.date
SQL
end
def gapfill_balances(balances)
gapfilled = []
prev_balance = nil
[ nil, *balances ].each_cons(2).each_with_index do |(prev, curr), index|
if index == 0 && curr.balance.nil?
curr.balance = 0 # Ensure all series start with a non-nil balance
elsif curr.balance.nil?
curr.balance = prev.balance
end
gapfilled << curr
end
gapfilled
end
end
def favorable_direction
classification == "asset" ? "up" : "down"
end
def balance_series(period: Period.last_30_days)
self.class.where(id: self.id).balance_series(
currency: currency,
period: period,
favorable_direction: favorable_direction
)
end
end