mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-05 05:25:24 +02:00
Balance sheet cache layer, non-blocking sync UI (#2356)
* Balance sheet cache layer with cache-busting * Update family cache timestamps during Sync * Less blocking sync loaders * Consolidate family data caching key logic * Fix turbo stream broadcasts * Remove dev delay * Add back account group sorting
This commit is contained in:
parent
dab693d74f
commit
10ce2c8e23
35 changed files with 529 additions and 466 deletions
|
@ -1,7 +1,7 @@
|
|||
class BalanceSheet
|
||||
include Monetizable
|
||||
|
||||
monetize :total_assets, :total_liabilities, :net_worth
|
||||
monetize :net_worth
|
||||
|
||||
attr_reader :family
|
||||
|
||||
|
@ -9,99 +9,36 @@ class BalanceSheet
|
|||
@family = family
|
||||
end
|
||||
|
||||
def total_assets
|
||||
totals_query.filter { |t| t.classification == "asset" }.sum(&:converted_balance)
|
||||
def assets
|
||||
@assets ||= ClassificationGroup.new(
|
||||
classification: "asset",
|
||||
currency: family.currency,
|
||||
accounts: account_totals.asset_accounts
|
||||
)
|
||||
end
|
||||
|
||||
def total_liabilities
|
||||
totals_query.filter { |t| t.classification == "liability" }.sum(&:converted_balance)
|
||||
end
|
||||
|
||||
def net_worth
|
||||
total_assets - total_liabilities
|
||||
def liabilities
|
||||
@liabilities ||= ClassificationGroup.new(
|
||||
classification: "liability",
|
||||
currency: family.currency,
|
||||
accounts: account_totals.liability_accounts
|
||||
)
|
||||
end
|
||||
|
||||
def classification_groups
|
||||
Rails.cache.fetch(family.build_cache_key("bs_classification_groups")) do
|
||||
asset_groups = account_groups("asset")
|
||||
liability_groups = account_groups("liability")
|
||||
|
||||
[
|
||||
ClassificationGroup.new(
|
||||
key: "asset",
|
||||
display_name: "Assets",
|
||||
icon: "plus",
|
||||
total_money: total_assets_money,
|
||||
account_groups: asset_groups,
|
||||
syncing?: asset_groups.any?(&:syncing?)
|
||||
),
|
||||
ClassificationGroup.new(
|
||||
key: "liability",
|
||||
display_name: "Debts",
|
||||
icon: "minus",
|
||||
total_money: total_liabilities_money,
|
||||
account_groups: liability_groups,
|
||||
syncing?: liability_groups.any?(&:syncing?)
|
||||
)
|
||||
]
|
||||
end
|
||||
[ assets, liabilities ]
|
||||
end
|
||||
|
||||
def account_groups(classification = nil)
|
||||
Rails.cache.fetch(family.build_cache_key("bs_account_groups_#{classification || 'all'}")) do
|
||||
classification_accounts = classification ? totals_query.filter { |t| t.classification == classification } : totals_query
|
||||
classification_total = classification_accounts.sum(&:converted_balance)
|
||||
def account_groups
|
||||
[ assets.account_groups, liabilities.account_groups ].flatten
|
||||
end
|
||||
|
||||
account_groups = classification_accounts.group_by(&:accountable_type)
|
||||
.transform_keys { |k| Accountable.from_type(k) }
|
||||
|
||||
groups = account_groups.map do |accountable, accounts|
|
||||
group_total = accounts.sum(&:converted_balance)
|
||||
|
||||
key = accountable.model_name.param_key
|
||||
|
||||
group = AccountGroup.new(
|
||||
id: classification ? "#{classification}_#{key}_group" : "#{key}_group",
|
||||
key: key,
|
||||
name: accountable.display_name,
|
||||
classification: accountable.classification,
|
||||
total: group_total,
|
||||
total_money: Money.new(group_total, currency),
|
||||
weight: classification_total.zero? ? 0 : group_total / classification_total.to_d * 100,
|
||||
missing_rates?: accounts.any? { |a| a.missing_rates? },
|
||||
color: accountable.color,
|
||||
syncing?: accounts.any?(&:is_syncing),
|
||||
accounts: accounts.map do |account|
|
||||
account
|
||||
end.sort_by(&:converted_balance).reverse
|
||||
)
|
||||
|
||||
group
|
||||
end
|
||||
|
||||
groups.sort_by do |group|
|
||||
manual_order = Accountable::TYPES
|
||||
type_name = group.key.camelize
|
||||
manual_order.index(type_name) || Float::INFINITY
|
||||
end
|
||||
end
|
||||
def net_worth
|
||||
assets.total - liabilities.total
|
||||
end
|
||||
|
||||
def net_worth_series(period: Period.last_30_days)
|
||||
memo_key = [ period.start_date, period.end_date ].compact.join("_")
|
||||
|
||||
@net_worth_series ||= {}
|
||||
|
||||
account_ids = active_accounts.pluck(:id)
|
||||
|
||||
builder = (@net_worth_series[memo_key] ||= Balance::ChartSeriesBuilder.new(
|
||||
account_ids: account_ids,
|
||||
currency: currency,
|
||||
period: period,
|
||||
favorable_direction: "up"
|
||||
))
|
||||
|
||||
builder.balance_series
|
||||
net_worth_series_builder.net_worth_series(period: period)
|
||||
end
|
||||
|
||||
def currency
|
||||
|
@ -109,32 +46,19 @@ class BalanceSheet
|
|||
end
|
||||
|
||||
def syncing?
|
||||
classification_groups.any? { |group| group.syncing? }
|
||||
sync_status_monitor.syncing?
|
||||
end
|
||||
|
||||
private
|
||||
ClassificationGroup = Struct.new(:key, :display_name, :icon, :total_money, :account_groups, :syncing?, keyword_init: true)
|
||||
AccountGroup = Struct.new(:id, :key, :name, :accountable_type, :classification, :total, :total_money, :weight, :accounts, :color, :missing_rates?, :syncing?, keyword_init: true)
|
||||
|
||||
def active_accounts
|
||||
family.accounts.active.with_attached_logo
|
||||
def sync_status_monitor
|
||||
@sync_status_monitor ||= SyncStatusMonitor.new(family)
|
||||
end
|
||||
|
||||
def totals_query
|
||||
@totals_query ||= active_accounts
|
||||
.joins(ActiveRecord::Base.sanitize_sql_array([ "LEFT JOIN exchange_rates ON exchange_rates.date = CURRENT_DATE AND accounts.currency = exchange_rates.from_currency AND exchange_rates.to_currency = ?", currency ]))
|
||||
.joins(ActiveRecord::Base.sanitize_sql_array([
|
||||
"LEFT JOIN syncs ON syncs.syncable_id = accounts.id AND syncs.syncable_type = 'Account' AND syncs.status IN (?) AND syncs.created_at > ?",
|
||||
%w[pending syncing],
|
||||
Sync::VISIBLE_FOR.ago
|
||||
]))
|
||||
.select(
|
||||
"accounts.*",
|
||||
"SUM(accounts.balance * COALESCE(exchange_rates.rate, 1)) as converted_balance",
|
||||
"COUNT(syncs.id) > 0 as is_syncing",
|
||||
ActiveRecord::Base.sanitize_sql_array([ "COUNT(CASE WHEN accounts.currency <> ? AND exchange_rates.rate IS NULL THEN 1 END) as missing_rates", currency ])
|
||||
)
|
||||
.group(:classification, :accountable_type, :id)
|
||||
.to_a
|
||||
def account_totals
|
||||
@account_totals ||= AccountTotals.new(family, sync_status_monitor: sync_status_monitor)
|
||||
end
|
||||
|
||||
def net_worth_series_builder
|
||||
@net_worth_series_builder ||= NetWorthSeriesBuilder.new(family)
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue