2025-02-21 11:57:59 -05:00
class BalanceSheet
include Monetizable
monetize :total_assets , :total_liabilities , :net_worth
attr_reader :family
def initialize ( family )
@family = family
end
def total_assets
totals_query . filter { | t | t . classification == " asset " } . sum ( & :converted_balance )
end
def total_liabilities
totals_query . filter { | t | t . classification == " liability " } . sum ( & :converted_balance )
end
def net_worth
total_assets - total_liabilities
end
def classification_groups
2025-05-23 11:46:12 -05:00
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
2025-02-21 11:57:59 -05:00
end
def account_groups ( classification = nil )
2025-05-23 11:46:12 -05:00
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 )
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
2025-05-23 12:25:18 -05:00
group = AccountGroup . new (
2025-05-23 11:46:12 -05:00
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
2025-05-23 12:25:18 -05:00
end . sort_by ( & :converted_balance ) . reverse
2025-05-23 11:46:12 -05:00
)
2025-05-23 12:25:18 -05:00
group
2025-05-23 11:46:12 -05:00
end
groups . sort_by do | group |
manual_order = Accountable :: TYPES
type_name = group . key . camelize
manual_order . index ( type_name ) || Float :: INFINITY
end
2025-05-15 10:19:56 -04:00
end
2025-02-21 11:57:59 -05:00
end
def net_worth_series ( period : Period . last_30_days )
2025-05-25 20:40:18 -04:00
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
2025-02-21 11:57:59 -05:00
end
def currency
family . currency
end
2025-05-15 10:19:56 -04:00
def syncing?
classification_groups . any? { | group | group . syncing? }
end
2025-02-21 11:57:59 -05:00
private
2025-05-15 10:19:56 -04:00
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 )
2025-02-21 11:57:59 -05:00
def active_accounts
family . accounts . active . with_attached_logo
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 ] ) )
2025-05-17 18:28:21 -04:00
. 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
] ) )
2025-02-21 11:57:59 -05:00
. select (
" accounts.* " ,
" SUM(accounts.balance * COALESCE(exchange_rates.rate, 1)) as converted_balance " ,
2025-05-15 10:19:56 -04:00
" COUNT(syncs.id) > 0 as is_syncing " ,
2025-02-21 11:57:59 -05:00
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
end
end