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

Additional cache columns on balances for activity view breakdowns (#2505)

* Initial schema iteration

* Add new balance components

* Add existing data migrator to backfill components

* Update calculator test assertions for new balance components

* Update flow assertions for forward calculator

* Update reverse calculator flows assumptions

* Forward calculator tests passing

* Get all calculator tests passing

* Assert flows factor
This commit is contained in:
Zach Gollwitzer 2025-07-23 10:06:25 -04:00 committed by GitHub
parent 347c0a7906
commit da2045dbd8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 1159 additions and 177 deletions

View file

@ -12,6 +12,8 @@ module LedgerTestingHelper
created_account = families(:empty).accounts.create!(
name: "Test Account",
accountable: account_type.new,
balance: account[:balance] || 0, # Doesn't matter, ledger derives this
cash_balance: account[:cash_balance] || 0, # Doesn't matter, ledger derives this
**account_attrs
)
@ -109,13 +111,20 @@ module LedgerTestingHelper
created_account
end
def assert_calculated_ledger_balances(calculated_data:, expected_balances:)
# Convert expected balances to a hash for easier lookup
expected_hash = expected_balances.to_h do |date, balance_data|
[ date.to_date, balance_data ]
def assert_calculated_ledger_balances(calculated_data:, expected_data:)
# Convert expected data to a hash for easier lookup
# Structure: [ { date:, legacy_balances: { balance:, cash_balance: }, balances: { start:, start_cash:, etc... }, flows: { ... }, adjustments: { ... } } ]
expected_hash = {}
expected_data.each do |data|
expected_hash[data[:date].to_date] = {
legacy_balances: data[:legacy_balances] || {},
balances: data[:balances] || {},
flows: data[:flows] || {},
adjustments: data[:adjustments] || {}
}
end
# Get all unique dates from both calculated and expected data
# Get all unique dates from all data sources
all_dates = (calculated_data.map(&:date) + expected_hash.keys).uniq.sort
# Check each date
@ -126,15 +135,163 @@ module LedgerTestingHelper
if expected
assert calculated_balance, "Expected balance for #{date} but none was calculated"
if expected[:balance]
assert_equal expected[:balance], calculated_balance.balance.to_d,
"Balance mismatch for #{date}"
end
# Always assert flows_factor is correct based on account classification
expected_flows_factor = calculated_balance.account.classification == "asset" ? 1 : -1
assert_equal expected_flows_factor, calculated_balance.flows_factor,
"Flows factor mismatch for #{date}: expected #{expected_flows_factor} for #{calculated_balance.account.classification} account"
if expected[:cash_balance]
assert_equal expected[:cash_balance], calculated_balance.cash_balance.to_d,
legacy_balances = expected[:legacy_balances]
balances = expected[:balances]
flows = expected[:flows]
adjustments = expected[:adjustments]
# Legacy balance assertions
if legacy_balances.any?
assert_equal legacy_balances[:balance], calculated_balance.balance,
"Balance mismatch for #{date}"
assert_equal legacy_balances[:cash_balance], calculated_balance.cash_balance,
"Cash balance mismatch for #{date}"
end
# Balance assertions
if balances.any?
assert_equal balances[:start_cash], calculated_balance.start_cash_balance,
"Start cash balance mismatch for #{date}" if balances.key?(:start_cash)
assert_equal balances[:start_non_cash], calculated_balance.start_non_cash_balance,
"Start non-cash balance mismatch for #{date}" if balances.key?(:start_non_cash)
# Calculate end_cash_balance using the formula from the migration
if balances.key?(:end_cash)
# Determine flows_factor based on account classification
flows_factor = calculated_balance.account.classification == "asset" ? 1 : -1
expected_end_cash = calculated_balance.start_cash_balance +
((calculated_balance.cash_inflows - calculated_balance.cash_outflows) * flows_factor) +
calculated_balance.cash_adjustments
assert_equal balances[:end_cash], expected_end_cash,
"End cash balance mismatch for #{date}"
end
# Calculate end_non_cash_balance using the formula from the migration
if balances.key?(:end_non_cash)
# Determine flows_factor based on account classification
flows_factor = calculated_balance.account.classification == "asset" ? 1 : -1
expected_end_non_cash = calculated_balance.start_non_cash_balance +
((calculated_balance.non_cash_inflows - calculated_balance.non_cash_outflows) * flows_factor) +
calculated_balance.net_market_flows +
calculated_balance.non_cash_adjustments
assert_equal balances[:end_non_cash], expected_end_non_cash,
"End non-cash balance mismatch for #{date}"
end
# Calculate start_balance using the formula from the migration
if balances.key?(:start)
expected_start = calculated_balance.start_cash_balance + calculated_balance.start_non_cash_balance
assert_equal balances[:start], expected_start,
"Start balance mismatch for #{date}"
end
# Calculate end_balance using the formula from the migration since we're not persisting balances,
# and generated columns are not available until the record is persisted
if balances.key?(:end)
# Determine flows_factor based on account classification
flows_factor = calculated_balance.account.classification == "asset" ? 1 : -1
expected_end_cash_component = calculated_balance.start_cash_balance +
((calculated_balance.cash_inflows - calculated_balance.cash_outflows) * flows_factor) +
calculated_balance.cash_adjustments
expected_end_non_cash_component = calculated_balance.start_non_cash_balance +
((calculated_balance.non_cash_inflows - calculated_balance.non_cash_outflows) * flows_factor) +
calculated_balance.net_market_flows +
calculated_balance.non_cash_adjustments
expected_end = expected_end_cash_component + expected_end_non_cash_component
assert_equal balances[:end], expected_end,
"End balance mismatch for #{date}"
end
end
# Flow assertions
# If flows passed is 0, we assert all columns are 0
if flows.is_a?(Integer) && flows == 0
assert_equal 0, calculated_balance.cash_inflows,
"Cash inflows mismatch for #{date}"
assert_equal 0, calculated_balance.cash_outflows,
"Cash outflows mismatch for #{date}"
assert_equal 0, calculated_balance.non_cash_inflows,
"Non-cash inflows mismatch for #{date}"
assert_equal 0, calculated_balance.non_cash_outflows,
"Non-cash outflows mismatch for #{date}"
assert_equal 0, calculated_balance.net_market_flows,
"Net market flows mismatch for #{date}"
elsif flows.is_a?(Hash) && flows.any?
# Cash flows - must be asserted together
if flows.key?(:cash_inflows) || flows.key?(:cash_outflows)
assert flows.key?(:cash_inflows) && flows.key?(:cash_outflows),
"Cash inflows and outflows must be asserted together for #{date}"
assert_equal flows[:cash_inflows], calculated_balance.cash_inflows,
"Cash inflows mismatch for #{date}"
assert_equal flows[:cash_outflows], calculated_balance.cash_outflows,
"Cash outflows mismatch for #{date}"
end
# Non-cash flows - must be asserted together
if flows.key?(:non_cash_inflows) || flows.key?(:non_cash_outflows)
assert flows.key?(:non_cash_inflows) && flows.key?(:non_cash_outflows),
"Non-cash inflows and outflows must be asserted together for #{date}"
assert_equal flows[:non_cash_inflows], calculated_balance.non_cash_inflows,
"Non-cash inflows mismatch for #{date}"
assert_equal flows[:non_cash_outflows], calculated_balance.non_cash_outflows,
"Non-cash outflows mismatch for #{date}"
end
# Market flows - can be asserted independently
if flows.key?(:net_market_flows)
assert_equal flows[:net_market_flows], calculated_balance.net_market_flows,
"Net market flows mismatch for #{date}"
end
end
# Adjustment assertions
if adjustments.is_a?(Integer) && adjustments == 0
assert_equal 0, calculated_balance.cash_adjustments,
"Cash adjustments mismatch for #{date}"
assert_equal 0, calculated_balance.non_cash_adjustments,
"Non-cash adjustments mismatch for #{date}"
elsif adjustments.is_a?(Hash) && adjustments.any?
assert_equal adjustments[:cash_adjustments], calculated_balance.cash_adjustments,
"Cash adjustments mismatch for #{date}" if adjustments.key?(:cash_adjustments)
assert_equal adjustments[:non_cash_adjustments], calculated_balance.non_cash_adjustments,
"Non-cash adjustments mismatch for #{date}" if adjustments.key?(:non_cash_adjustments)
end
# Temporary assertions during migration (remove after migration complete)
# TODO: Remove these assertions after migration is complete
# Since we're not persisting balances, we calculate the end values
flows_factor = calculated_balance.account.classification == "asset" ? 1 : -1
expected_end_cash = calculated_balance.start_cash_balance +
((calculated_balance.cash_inflows - calculated_balance.cash_outflows) * flows_factor) +
calculated_balance.cash_adjustments
expected_end_balance = expected_end_cash +
calculated_balance.start_non_cash_balance +
((calculated_balance.non_cash_inflows - calculated_balance.non_cash_outflows) * flows_factor) +
calculated_balance.net_market_flows +
calculated_balance.non_cash_adjustments
assert_equal calculated_balance.cash_balance, expected_end_cash,
"Temporary assertion failed: end_cash_balance should equal cash_balance for #{date}"
assert_equal calculated_balance.balance, expected_end_balance,
"Temporary assertion failed: end_balance should equal balance for #{date}"
else
assert_nil calculated_balance, "Unexpected balance calculated for #{date}"
end