mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-02 12:05:19 +02:00
Add the ability to "rollup" values in a time series (#554)
* Clean up time series models * Add value group rollup class for summarizing hierarchical data * Integrate new classes * Update UI to use new patterns * Update D3 charts to expect new data format * Clean up account model * More cleanup * Money improvements * Use new money fields * Remove invalid fixture data to avoid orphaned accountables * Update time series to work better with collections * Fix tests and UI bugs
This commit is contained in:
parent
0a8518506c
commit
f904d9d062
34 changed files with 687 additions and 391 deletions
|
@ -26,10 +26,8 @@ class Account < ApplicationRecord
|
|||
%w[name]
|
||||
end
|
||||
|
||||
def trend(period = Period.all)
|
||||
first = balances.in_period(period).order(:date).first
|
||||
last = balances.in_period(period).order(date: :desc).first
|
||||
Trend.new(current: last.balance, previous: first.balance, type: classification)
|
||||
def balance_on(date)
|
||||
balances.where("date <= ?", date).order(date: :desc).first&.balance
|
||||
end
|
||||
|
||||
def self.by_provider
|
||||
|
@ -41,55 +39,28 @@ class Account < ApplicationRecord
|
|||
exists?(status: "syncing")
|
||||
end
|
||||
|
||||
# TODO: We will need a better way to encapsulate large queries & transformation logic, but leaving all in one spot until
|
||||
# we have a better understanding of the requirements
|
||||
def self.by_group(period = Period.all)
|
||||
ranked_balances_cte = active.joins(:balances)
|
||||
.select("
|
||||
account_balances.account_id,
|
||||
account_balances.balance,
|
||||
account_balances.date,
|
||||
ROW_NUMBER() OVER (PARTITION BY account_balances.account_id ORDER BY date ASC) AS rn_asc,
|
||||
ROW_NUMBER() OVER (PARTITION BY account_balances.account_id ORDER BY date DESC) AS rn_desc
|
||||
")
|
||||
def series(period = Period.all)
|
||||
TimeSeries.from_collection(balances.in_period(period), :balance_money)
|
||||
end
|
||||
|
||||
if period.date_range
|
||||
ranked_balances_cte = ranked_balances_cte.where("account_balances.date BETWEEN ? AND ?", period.date_range.begin, period.date_range.end)
|
||||
def self.by_group(period = Period.all)
|
||||
grouped_accounts = { assets: ValueGroup.new("Assets"), liabilities: ValueGroup.new("Liabilities") }
|
||||
|
||||
Accountable.by_classification.each do |classification, types|
|
||||
types.each do |type|
|
||||
group = grouped_accounts[classification.to_sym].add_child_node(type)
|
||||
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))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
accounts_with_period_balances = AccountBalance.with(
|
||||
ranked_balances: ranked_balances_cte
|
||||
)
|
||||
.from("ranked_balances AS rb")
|
||||
.joins("JOIN accounts a ON a.id = rb.account_id")
|
||||
.select("
|
||||
a.name,
|
||||
a.accountable_type,
|
||||
a.classification,
|
||||
SUM(CASE WHEN rb.rn_asc = 1 THEN rb.balance ELSE 0 END) AS start_balance,
|
||||
MAX(CASE WHEN rb.rn_asc = 1 THEN rb.date ELSE NULL END) as start_date,
|
||||
SUM(CASE WHEN rb.rn_desc = 1 THEN rb.balance ELSE 0 END) AS end_balance,
|
||||
MAX(CASE WHEN rb.rn_desc = 1 THEN rb.date ELSE NULL END) as end_date
|
||||
")
|
||||
.where("rb.rn_asc = 1 OR rb.rn_desc = 1")
|
||||
.group("a.id")
|
||||
.order("end_balance")
|
||||
.to_a
|
||||
|
||||
assets = accounts_with_period_balances.select { |row| row.classification == "asset" }
|
||||
liabilities = accounts_with_period_balances.select { |row| row.classification == "liability" }
|
||||
|
||||
total_assets = assets.sum(&:end_balance)
|
||||
total_liabilities = liabilities.sum(&:end_balance)
|
||||
|
||||
{
|
||||
asset: build_group_summary(assets, "asset"),
|
||||
liability: build_group_summary(liabilities, "liability")
|
||||
}
|
||||
grouped_accounts
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_currency
|
||||
if self.currency == self.family.currency
|
||||
self.converted_balance = self.balance
|
||||
|
@ -99,34 +70,4 @@ class Account < ApplicationRecord
|
|||
self.converted_currency = self.family.currency
|
||||
end
|
||||
end
|
||||
|
||||
def self.build_group_summary(accounts, classification)
|
||||
total_balance = accounts.sum(&:end_balance)
|
||||
{
|
||||
total: total_balance,
|
||||
groups: accounts.group_by(&:accountable_type).transform_values do |rows|
|
||||
build_account_summary(rows, total_balance, classification)
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
def self.build_account_summary(accounts, total_balance, classification)
|
||||
end_balance = accounts.sum(&:end_balance)
|
||||
start_balance = accounts.sum(&:start_balance)
|
||||
{
|
||||
start_balance: start_balance,
|
||||
end_balance: end_balance,
|
||||
allocation: (end_balance / total_balance * 100).round(2),
|
||||
trend: Trend.new(current: end_balance, previous: start_balance, type: classification),
|
||||
accounts: accounts.map do |account|
|
||||
{
|
||||
name: account.name,
|
||||
start_balance: account.start_balance,
|
||||
end_balance: account.end_balance,
|
||||
allocation: (account.end_balance / total_balance * 100).round(2),
|
||||
trend: Trend.new(current: account.end_balance, previous: account.start_balance, type: classification)
|
||||
}
|
||||
end
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue