1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-06 05:55:21 +02:00

Update flow assertions for forward calculator

This commit is contained in:
Zach Gollwitzer 2025-07-20 09:17:59 -04:00
parent 91d970c7fe
commit 8616b2c0de
2 changed files with 178 additions and 103 deletions

View file

@ -11,7 +11,7 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase
# When syncing forwards, we don't care about the account balance. We generate everything based on entries, starting from 0.
test "no entries sync" do
account = create_account_with_ledger(
account: { type: Depository, balance: 20000, cash_balance: 20000, currency: "USD" },
account: { type: Depository, currency: "USD" },
entries: []
)
@ -25,7 +25,9 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase
{
date: Date.current,
legacy_balances: { balance: 0, cash_balance: 0 },
balances: { start: 0, start_cash: 0, start_non_cash: 0, end_cash: 0, end_non_cash: 0, end: 0 }
balances: { start: 0, start_cash: 0, start_non_cash: 0, end_cash: 0, end_non_cash: 0, end: 0 },
flows: 0,
adjustments: 0
}
]
)
@ -34,7 +36,7 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase
# Our system ensures all manual accounts have an opening anchor (for UX), but we should be able to handle a missing anchor by starting at 0 (i.e. "fresh account with no history")
test "account without opening anchor starts at zero balance" do
account = create_account_with_ledger(
account: { type: Depository, balance: 20000, cash_balance: 20000, currency: "USD" },
account: { type: Depository, currency: "USD" },
entries: [
{ type: "transaction", date: 2.days.ago.to_date, amount: -1000 }
]
@ -49,12 +51,16 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase
{
date: 3.days.ago.to_date,
legacy_balances: { balance: 0, cash_balance: 0 },
balances: { start: 0, start_cash: 0, start_non_cash: 0, end_cash: 0, end_non_cash: 0, end: 0 }
balances: { start: 0, start_cash: 0, start_non_cash: 0, end_cash: 0, end_non_cash: 0, end: 0 },
flows: 0,
adjustments: 0
},
{
date: 2.days.ago.to_date,
legacy_balances: { balance: 1000, cash_balance: 1000 },
balances: { start: 0, start_cash: 0, start_non_cash: 0, end_cash: 1000, end_non_cash: 0, end: 1000 }
balances: { start: 0, start_cash: 0, start_non_cash: 0, end_cash: 1000, end_non_cash: 0, end: 1000 },
flows: { cash_inflows: 1000, cash_outflows: 0 },
adjustments: 0
}
]
)
@ -62,7 +68,7 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase
test "reconciliation valuation sets absolute balance before applying subsequent transactions" do
account = create_account_with_ledger(
account: { type: Depository, balance: 20000, cash_balance: 20000, currency: "USD" },
account: { type: Depository, currency: "USD" },
entries: [
{ type: "reconciliation", date: 3.days.ago.to_date, balance: 18000 },
{ type: "transaction", date: 2.days.ago.to_date, amount: -1000 }
@ -78,12 +84,16 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase
{
date: 3.days.ago.to_date,
legacy_balances: { balance: 18000, cash_balance: 18000 },
balances: { start: 0, start_cash: 0, start_non_cash: 0, end_cash: 18000, end_non_cash: 0, end: 18000 }
balances: { start: 0, start_cash: 0, start_non_cash: 0, end_cash: 18000, end_non_cash: 0, end: 18000 },
flows: 0,
adjustments: { cash_adjustments: 18000, non_cash_adjustments: 0 }
},
{
date: 2.days.ago.to_date,
legacy_balances: { balance: 19000, cash_balance: 19000 },
balances: { start: 18000, start_cash: 18000, start_non_cash: 0, end_cash: 19000, end_non_cash: 0, end: 19000 }
balances: { start: 18000, start_cash: 18000, start_non_cash: 0, end_cash: 19000, end_non_cash: 0, end: 19000 },
flows: { cash_inflows: 1000, cash_outflows: 0 },
adjustments: 0
}
]
)
@ -92,7 +102,7 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase
test "cash-only accounts (depository, credit card) use valuations where cash balance equals total balance" do
[ Depository, CreditCard ].each do |account_type|
account = create_account_with_ledger(
account: { type: account_type, balance: 10000, cash_balance: 10000, currency: "USD" },
account: { type: account_type, currency: "USD" },
entries: [
{ type: "opening_anchor", date: 3.days.ago.to_date, balance: 17000 },
{ type: "reconciliation", date: 2.days.ago.to_date, balance: 18000 }
@ -107,12 +117,16 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase
{
date: 3.days.ago.to_date,
legacy_balances: { balance: 17000, cash_balance: 17000 },
balances: { start: 0, start_cash: 0, start_non_cash: 0, end_cash: 17000, end_non_cash: 0, end: 17000 }
balances: { start: 0, start_cash: 0, start_non_cash: 0, end_cash: 17000, end_non_cash: 0, end: 17000 },
flows: 0,
adjustments: { cash_adjustments: 17000, non_cash_adjustments: 0 }
},
{
date: 2.days.ago.to_date,
legacy_balances: { balance: 18000, cash_balance: 18000 },
balances: { start: 17000, start_cash: 17000, start_non_cash: 0, end_cash: 18000, end_non_cash: 0, end: 18000 }
balances: { start: 17000, start_cash: 17000, start_non_cash: 0, end_cash: 18000, end_non_cash: 0, end: 18000 },
flows: 0,
adjustments: { cash_adjustments: 1000, non_cash_adjustments: 0 }
}
]
)
@ -122,7 +136,7 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase
test "non-cash accounts (property, loan) use valuations where cash balance is always zero" do
[ Property, Loan ].each do |account_type|
account = create_account_with_ledger(
account: { type: account_type, balance: 10000, cash_balance: 10000, currency: "USD" },
account: { type: account_type, currency: "USD" },
entries: [
{ type: "opening_anchor", date: 3.days.ago.to_date, balance: 17000 },
{ type: "reconciliation", date: 2.days.ago.to_date, balance: 18000 }
@ -137,12 +151,16 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase
{
date: 3.days.ago.to_date,
legacy_balances: { balance: 17000, cash_balance: 0.0 },
balances: { start: 0, start_cash: 0, start_non_cash: 0, end_cash: 0, end_non_cash: 17000, end: 17000 }
balances: { start: 0, start_cash: 0, start_non_cash: 0, end_cash: 0, end_non_cash: 17000, end: 17000 },
flows: 0,
adjustments: { cash_adjustments: 0, non_cash_adjustments: 17000 }
},
{
date: 2.days.ago.to_date,
legacy_balances: { balance: 18000, cash_balance: 0.0 },
balances: { start: 17000, start_cash: 0, start_non_cash: 17000, end_cash: 0, end_non_cash: 18000, end: 18000 }
balances: { start: 17000, start_cash: 0, start_non_cash: 17000, end_cash: 0, end_non_cash: 18000, end: 18000 },
flows: 0,
adjustments: { cash_adjustments: 0, non_cash_adjustments: 1000 }
}
]
)
@ -151,7 +169,7 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase
test "mixed accounts (investment) use valuations where cash balance is total minus holdings" do
account = create_account_with_ledger(
account: { type: Investment, balance: 10000, cash_balance: 10000, currency: "USD" },
account: { type: Investment, currency: "USD" },
entries: [
{ type: "opening_anchor", date: 3.days.ago.to_date, balance: 17000 },
{ type: "reconciliation", date: 2.days.ago.to_date, balance: 18000 }
@ -167,12 +185,16 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase
{
date: 3.days.ago.to_date,
legacy_balances: { balance: 17000, cash_balance: 17000 },
balances: { start: 0, start_cash: 0, start_non_cash: 0, end_cash: 17000, end_non_cash: 0, end: 17000 }
balances: { start: 0, start_cash: 0, start_non_cash: 0, end_cash: 17000, end_non_cash: 0, end: 17000 },
flows: { market_flows: 0 },
adjustments: { cash_adjustments: 17000, non_cash_adjustments: 0 } # Since no holdings present, adjustment is all cash
},
{
date: 2.days.ago.to_date,
legacy_balances: { balance: 18000, cash_balance: 18000 },
balances: { start: 17000, start_cash: 17000, start_non_cash: 0, end_cash: 18000, end_non_cash: 0, end: 18000 }
balances: { start: 17000, start_cash: 17000, start_non_cash: 0, end_cash: 18000, end_non_cash: 0, end: 18000 },
flows: { market_flows: 0 },
adjustments: { cash_adjustments: 1000, non_cash_adjustments: 0 } # Since no holdings present, adjustment is all cash
}
]
)
@ -184,7 +206,7 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase
test "transactions on depository accounts affect cash balance" do
account = create_account_with_ledger(
account: { type: Depository, balance: 20000, cash_balance: 20000, currency: "USD" },
account: { type: Depository, currency: "USD" },
entries: [
{ type: "opening_anchor", date: 5.days.ago.to_date, balance: 20000 },
{ type: "transaction", date: 4.days.ago.to_date, amount: -500 }, # income
@ -200,22 +222,30 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase
{
date: 5.days.ago.to_date,
legacy_balances: { balance: 20000, cash_balance: 20000 },
balances: { start: 0, start_cash: 0, start_non_cash: 0, end_cash: 20000, end_non_cash: 0, end: 20000 }
balances: { start: 0, start_cash: 0, start_non_cash: 0, end_cash: 20000, end_non_cash: 0, end: 20000 },
flows: 0,
adjustments: { cash_adjustments: 20000, non_cash_adjustments: 0 }
},
{
date: 4.days.ago.to_date,
legacy_balances: { balance: 20500, cash_balance: 20500 },
balances: { start: 20000, start_cash: 20000, start_non_cash: 0, end_cash: 20500, end_non_cash: 0, end: 20500 }
balances: { start: 20000, start_cash: 20000, start_non_cash: 0, end_cash: 20500, end_non_cash: 0, end: 20500 },
flows: { cash_inflows: 500, cash_outflows: 0 },
adjustments: 0
},
{
date: 3.days.ago.to_date,
legacy_balances: { balance: 20500, cash_balance: 20500 },
balances: { start: 20500, start_cash: 20500, start_non_cash: 0, end_cash: 20500, end_non_cash: 0, end: 20500 }
balances: { start: 20500, start_cash: 20500, start_non_cash: 0, end_cash: 20500, end_non_cash: 0, end: 20500 },
flows: 0,
adjustments: 0
},
{
date: 2.days.ago.to_date,
legacy_balances: { balance: 20400, cash_balance: 20400 },
balances: { start: 20500, start_cash: 20500, start_non_cash: 0, end_cash: 20400, end_non_cash: 0, end: 20400 }
balances: { start: 20500, start_cash: 20500, start_non_cash: 0, end_cash: 20400, end_non_cash: 0, end: 20400 },
flows: { cash_inflows: 0, cash_outflows: 100 },
adjustments: 0
}
]
)
@ -224,7 +254,7 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase
test "transactions on credit card accounts affect cash balance inversely" do
account = create_account_with_ledger(
account: { type: CreditCard, balance: 10000, cash_balance: 10000, currency: "USD" },
account: { type: CreditCard, currency: "USD" },
entries: [
{ type: "opening_anchor", date: 5.days.ago.to_date, balance: 1000 },
{ type: "transaction", date: 4.days.ago.to_date, amount: -500 }, # CC payment
@ -240,22 +270,30 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase
{
date: 5.days.ago.to_date,
legacy_balances: { balance: 1000, cash_balance: 1000 },
balances: { start: 0, start_cash: 0, start_non_cash: 0, end_cash: 1000, end_non_cash: 0, end: 1000 }
balances: { start: 0, start_cash: 0, start_non_cash: 0, end_cash: 1000, end_non_cash: 0, end: 1000 },
flows: 0,
adjustments: { cash_adjustments: 1000, non_cash_adjustments: 0 }
},
{
date: 4.days.ago.to_date,
legacy_balances: { balance: 500, cash_balance: 500 },
balances: { start: 1000, start_cash: 1000, start_non_cash: 0, end_cash: 500, end_non_cash: 0, end: 500 }
balances: { start: 1000, start_cash: 1000, start_non_cash: 0, end_cash: 500, end_non_cash: 0, end: 500 },
flows: { cash_inflows: 0, cash_outflows: 500 },
adjustments: 0
},
{
date: 3.days.ago.to_date,
legacy_balances: { balance: 500, cash_balance: 500 },
balances: { start: 500, start_cash: 500, start_non_cash: 0, end_cash: 500, end_non_cash: 0, end: 500 }
balances: { start: 500, start_cash: 500, start_non_cash: 0, end_cash: 500, end_non_cash: 0, end: 500 },
flows: 0,
adjustments: 0
},
{
date: 2.days.ago.to_date,
legacy_balances: { balance: 600, cash_balance: 600 },
balances: { start: 500, start_cash: 500, start_non_cash: 0, end_cash: 600, end_non_cash: 0, end: 600 }
balances: { start: 500, start_cash: 500, start_non_cash: 0, end_cash: 600, end_non_cash: 0, end: 600 },
flows: { cash_inflows: 0, cash_outflows: 100 },
adjustments: 0
}
]
)
@ -263,15 +301,12 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase
test "depository account with transactions and balance reconciliations" do
account = create_account_with_ledger(
account: { type: Depository, balance: 20000, cash_balance: 20000, currency: "USD" },
account: { type: Depository, currency: "USD" },
entries: [
{ type: "opening_anchor", date: 10.days.ago.to_date, balance: 20000 },
{ type: "transaction", date: 8.days.ago.to_date, amount: -5000 },
{ type: "reconciliation", date: 6.days.ago.to_date, balance: 17000 },
{ type: "transaction", date: 6.days.ago.to_date, amount: -500 },
{ type: "transaction", date: 4.days.ago.to_date, amount: -500 },
{ type: "reconciliation", date: 3.days.ago.to_date, balance: 17000 },
{ type: "transaction", date: 1.day.ago.to_date, amount: 100 }
{ type: "opening_anchor", date: 4.days.ago.to_date, balance: 20000 },
{ type: "transaction", date: 3.days.ago.to_date, amount: -5000 },
{ type: "reconciliation", date: 2.days.ago.to_date, balance: 17000 },
{ type: "transaction", date: 1.day.ago.to_date, amount: -500 }
]
)
@ -280,63 +315,41 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase
assert_calculated_ledger_balances(
calculated_data: calculated,
expected_data: [
{
date: 10.days.ago.to_date,
legacy_balances: { balance: 20000, cash_balance: 20000 },
balances: { start: 0, start_cash: 0, start_non_cash: 0, end_cash: 20000, end_non_cash: 0, end: 20000 }
},
{
date: 9.days.ago.to_date,
legacy_balances: { balance: 20000, cash_balance: 20000 },
balances: { start: 20000, start_cash: 20000, start_non_cash: 0, end_cash: 20000, end_non_cash: 0, end: 20000 }
},
{
date: 8.days.ago.to_date,
legacy_balances: { balance: 25000, cash_balance: 25000 },
balances: { start: 20000, start_cash: 20000, start_non_cash: 0, end_cash: 25000, end_non_cash: 0, end: 25000 }
},
{
date: 7.days.ago.to_date,
legacy_balances: { balance: 25000, cash_balance: 25000 },
balances: { start: 25000, start_cash: 25000, start_non_cash: 0, end_cash: 25000, end_non_cash: 0, end: 25000 }
},
{
date: 6.days.ago.to_date,
legacy_balances: { balance: 17000, cash_balance: 17000 },
balances: { start: 25000, start_cash: 25000, start_non_cash: 0, end_cash: 17000, end_non_cash: 0, end: 17000 }
},
{
date: 5.days.ago.to_date,
legacy_balances: { balance: 17000, cash_balance: 17000 },
balances: { start: 17000, start_cash: 17000, start_non_cash: 0, end_cash: 17000, end_non_cash: 0, end: 17000 }
},
{
date: 4.days.ago.to_date,
legacy_balances: { balance: 17500, cash_balance: 17500 },
balances: { start: 17000, start_cash: 17000, start_non_cash: 0, end_cash: 17500, end_non_cash: 0, end: 17500 }
legacy_balances: { balance: 20000, cash_balance: 20000 },
balances: { start: 0, start_cash: 0, start_non_cash: 0, end_cash: 20000, end_non_cash: 0, end: 20000 },
flows: 0,
adjustments: { cash_adjustments: 20000, non_cash_adjustments: 0 }
},
{
date: 3.days.ago.to_date,
legacy_balances: { balance: 17000, cash_balance: 17000 },
balances: { start: 17500, start_cash: 17500, start_non_cash: 0, end_cash: 17000, end_non_cash: 0, end: 17000 }
legacy_balances: { balance: 25000, cash_balance: 25000 },
balances: { start: 20000, start_cash: 20000, start_non_cash: 0, end_cash: 25000, end_non_cash: 0, end: 25000 },
flows: { cash_inflows: 5000, cash_outflows: 0 },
adjustments: 0
},
{
date: 2.days.ago.to_date,
legacy_balances: { balance: 17000, cash_balance: 17000 },
balances: { start: 17000, start_cash: 17000, start_non_cash: 0, end_cash: 17000, end_non_cash: 0, end: 17000 }
balances: { start: 25000, start_cash: 25000, start_non_cash: 0, end_cash: 17000, end_non_cash: 0, end: 17000 },
flows: 0,
adjustments: { cash_adjustments: -8000, non_cash_adjustments: 0 }
},
{
date: 1.day.ago.to_date,
legacy_balances: { balance: 16900, cash_balance: 16900 },
balances: { start: 17000, start_cash: 17000, start_non_cash: 0, end_cash: 16900, end_non_cash: 0, end: 16900 }
legacy_balances: { balance: 17500, cash_balance: 17500 },
balances: { start: 17000, start_cash: 17000, start_non_cash: 0, end_cash: 17500, end_non_cash: 0, end: 17500 },
flows: { cash_inflows: 500, cash_outflows: 0 },
adjustments: 0
}
]
)
end
test "accounts with transactions in multiple currencies convert to the account currency" do
test "accounts with transactions in multiple currencies convert to the account currency and flows are stored in account currency" do
account = create_account_with_ledger(
account: { type: Depository, balance: 20000, cash_balance: 20000, currency: "USD" },
account: { type: Depository, currency: "USD" },
entries: [
{ type: "opening_anchor", date: 4.days.ago.to_date, balance: 100 },
{ type: "transaction", date: 3.days.ago.to_date, amount: -100 },
@ -357,22 +370,30 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase
{
date: 4.days.ago.to_date,
legacy_balances: { balance: 100, cash_balance: 100 },
balances: { start: 0, start_cash: 0, start_non_cash: 0, end_cash: 100, end_non_cash: 0, end: 100 }
balances: { start: 0, start_cash: 0, start_non_cash: 0, end_cash: 100, end_non_cash: 0, end: 100 },
flows: 0,
adjustments: { cash_adjustments: 100, non_cash_adjustments: 0 }
},
{
date: 3.days.ago.to_date,
legacy_balances: { balance: 200, cash_balance: 200 },
balances: { start: 100, start_cash: 100, start_non_cash: 0, end_cash: 200, end_non_cash: 0, end: 200 }
balances: { start: 100, start_cash: 100, start_non_cash: 0, end_cash: 200, end_non_cash: 0, end: 200 },
flows: { cash_inflows: 100, cash_outflows: 0 },
adjustments: 0
},
{
date: 2.days.ago.to_date,
legacy_balances: { balance: 500, cash_balance: 500 },
balances: { start: 200, start_cash: 200, start_non_cash: 0, end_cash: 500, end_non_cash: 0, end: 500 }
balances: { start: 200, start_cash: 200, start_non_cash: 0, end_cash: 500, end_non_cash: 0, end: 500 },
flows: { cash_inflows: 300, cash_outflows: 0 },
adjustments: 0
},
{
date: 1.day.ago.to_date,
legacy_balances: { balance: 1100, cash_balance: 1100 },
balances: { start: 500, start_cash: 500, start_non_cash: 0, end_cash: 1100, end_non_cash: 0, end: 1100 }
balances: { start: 500, start_cash: 500, start_non_cash: 0, end_cash: 1100, end_non_cash: 0, end: 1100 },
flows: { cash_inflows: 600, cash_outflows: 0 }, # Cash inflow is the USD equivalent of €500 (converted for balances table)
adjustments: 0
}
]
)
@ -381,7 +402,7 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase
# A loan is a special case where despite being a "non-cash" account, it is typical to have "payment" transactions that reduce the loan principal (non cash balance)
test "loan payment transactions affect non cash balance" do
account = create_account_with_ledger(
account: { type: Loan, balance: 10000, cash_balance: 0, currency: "USD" },
account: { type: Loan, currency: "USD" },
entries: [
{ type: "opening_anchor", date: 2.days.ago.to_date, balance: 20000 },
# "Loan payment" of $2000, which reduces the principal
@ -399,12 +420,16 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase
{
date: 2.days.ago.to_date,
legacy_balances: { balance: 20000, cash_balance: 0 },
balances: { start: 0, start_cash: 0, start_non_cash: 0, end_cash: 0, end_non_cash: 20000, end: 20000 }
balances: { start: 0, start_cash: 0, start_non_cash: 0, end_cash: 0, end_non_cash: 20000, end: 20000 },
flows: 0,
adjustments: { cash_adjustments: 0, non_cash_adjustments: 20000 } # Valuations adjust non-cash balance for non-cash accounts like Loans
},
{
date: 1.day.ago.to_date,
legacy_balances: { balance: 18000, cash_balance: 0 },
balances: { start: 20000, start_cash: 0, start_non_cash: 20000, end_cash: 0, end_non_cash: 18000, end: 18000 }
balances: { start: 20000, start_cash: 0, start_non_cash: 20000, end_cash: 0, end_non_cash: 18000, end: 18000 },
flows: { non_cash_inflows: 2000, non_cash_outflows: 0, cash_inflows: 0, cash_outflows: 0 }, # Loans are "special cases" where transactions do affect non-cash balance
adjustments: 0
}
]
)
@ -413,7 +438,7 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase
test "non cash accounts can only use valuations and transactions will be recorded but ignored for balance calculation" do
[ Property, Vehicle, OtherAsset, OtherLiability ].each do |account_type|
account = create_account_with_ledger(
account: { type: account_type, balance: 10000, cash_balance: 10000, currency: "USD" },
account: { type: account_type, currency: "USD" },
entries: [
{ type: "opening_anchor", date: 3.days.ago.to_date, balance: 500000 },
@ -430,12 +455,16 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase
{
date: 3.days.ago.to_date,
legacy_balances: { balance: 500000, cash_balance: 0 },
balances: { start: 0, start_cash: 0, start_non_cash: 0, end_cash: 0, end_non_cash: 500000, end: 500000 }
balances: { start: 0, start_cash: 0, start_non_cash: 0, end_cash: 0, end_non_cash: 500000, end: 500000 },
flows: 0,
adjustments: { cash_adjustments: 0, non_cash_adjustments: 500000 }
},
{
date: 2.days.ago.to_date,
legacy_balances: { balance: 500000, cash_balance: 0 },
balances: { start: 500000, start_cash: 0, start_non_cash: 500000, end_cash: 0, end_non_cash: 500000, end: 500000 }
balances: { start: 500000, start_cash: 0, start_non_cash: 500000, end_cash: 0, end_non_cash: 500000, end: 500000 },
flows: 0, # Despite having a transaction, non-cash accounts ignore it for balance calculation
adjustments: 0
}
]
)
@ -452,7 +481,7 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase
# Holdings are calculated separately and fed into the balance calculator; treated as "non-cash"
test "investment account calculates balance from transactions and trades and treats holdings as non-cash, additive to balance" do
account = create_account_with_ledger(
account: { type: Investment, balance: 10000, cash_balance: 10000, currency: "USD" },
account: { type: Investment, currency: "USD" },
entries: [
# Account starts with brokerage cash of $5000 and no holdings
{ type: "opening_anchor", date: 3.days.ago.to_date, balance: 5000 },
@ -462,7 +491,7 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase
holdings: [
# Holdings calculator will calculate $1000 worth of holdings
{ date: 1.day.ago.to_date, ticker: "AAPL", qty: 10, price: 100, amount: 1000 },
{ date: Date.current, ticker: "AAPL", qty: 10, price: 100, amount: 1000 }
{ date: Date.current, ticker: "AAPL", qty: 10, price: 110, amount: 1100 } # Price increased by 10%, so holdings value goes up by $100 without a trade
]
)
@ -476,29 +505,36 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase
{
date: 3.days.ago.to_date,
legacy_balances: { balance: 5000, cash_balance: 5000 },
balances: { start: 0, start_cash: 0, start_non_cash: 0, end_cash: 5000, end_non_cash: 0, end: 5000 }
balances: { start: 0, start_cash: 0, start_non_cash: 0, end_cash: 5000, end_non_cash: 0, end: 5000 },
flows: 0,
adjustments: { cash_adjustments: 5000, non_cash_adjustments: 0 }
},
{
date: 2.days.ago.to_date,
legacy_balances: { balance: 5000, cash_balance: 5000 },
balances: { start: 5000, start_cash: 5000, start_non_cash: 0, end_cash: 5000, end_non_cash: 0, end: 5000 }
balances: { start: 5000, start_cash: 5000, start_non_cash: 0, end_cash: 5000, end_non_cash: 0, end: 5000 },
flows: 0,
adjustments: 0
},
{
date: 1.day.ago.to_date,
legacy_balances: { balance: 5000, cash_balance: 4000 },
balances: { start: 5000, start_cash: 5000, start_non_cash: 0, end_cash: 4000, end_non_cash: 1000, end: 5000 }
balances: { start: 5000, start_cash: 5000, start_non_cash: 0, end_cash: 4000, end_non_cash: 1000, end: 5000 },
flows: { cash_inflows: 0, cash_outflows: 1000, non_cash_inflows: 1000, non_cash_outflows: 0, net_market_flows: 0 }, # Decrease cash by 1000, increase holdings by 1000 (i.e. "buy" of $1000 worth of AAPL)
adjustments: 0
},
{
date: Date.current,
legacy_balances: { balance: 5000, cash_balance: 4000 },
balances: { start: 5000, start_cash: 4000, start_non_cash: 1000, end_cash: 4000, end_non_cash: 1000, end: 5000 }
balances: { start: 5000, start_cash: 4000, start_non_cash: 1000, end_cash: 4000, end_non_cash: 1000, end: 5000 },
flows: { net_market_flows: 100 }, # Holdings value increased by 100, despite no change in portfolio quantities
adjustments: 0
}
]
)
end
private
def assert_balances(calculated_data:, expected_balances:)
# Sort calculated data by date to ensure consistent ordering
sorted_data = calculated_data.sort_by(&:date)

View file

@ -12,6 +12,8 @@ module LedgerTestingHelper
created_account = families(:empty).accounts.create!(
name: "Test Account",
accountable: account_type.new,
balance: 0, # Doesn't matter, ledger derives this
cash_balance: 0, # Doesn't matter, ledger derives this
**account_attrs
)
@ -170,25 +172,62 @@ module LedgerTestingHelper
end
# Flow assertions
if flows.any?
assert_equal flows[:cash_inflows], calculated_balance.cash_inflows.to_d,
"Cash inflows mismatch for #{date}" if flows.key?(:cash_inflows)
# 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.to_d,
"Cash inflows mismatch for #{date}"
assert_equal flows[:cash_outflows], calculated_balance.cash_outflows.to_d,
"Cash outflows mismatch for #{date}" if flows.key?(:cash_outflows)
assert_equal 0, calculated_balance.cash_outflows.to_d,
"Cash outflows mismatch for #{date}"
assert_equal flows[:non_cash_inflows], calculated_balance.non_cash_inflows.to_d,
"Non-cash inflows mismatch for #{date}" if flows.key?(:non_cash_inflows)
assert_equal 0, calculated_balance.non_cash_inflows.to_d,
"Non-cash inflows mismatch for #{date}"
assert_equal flows[:non_cash_outflows], calculated_balance.non_cash_outflows.to_d,
"Non-cash outflows mismatch for #{date}" if flows.key?(:non_cash_outflows)
assert_equal 0, calculated_balance.non_cash_outflows.to_d,
"Non-cash outflows mismatch for #{date}"
assert_equal flows[:net_market_flows], calculated_balance.net_market_flows.to_d,
"Net market flows mismatch for #{date}" if flows.key?(:net_market_flows)
assert_equal 0, calculated_balance.net_market_flows.to_d,
"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.to_d,
"Cash inflows mismatch for #{date}"
assert_equal flows[:cash_outflows], calculated_balance.cash_outflows.to_d,
"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.to_d,
"Non-cash inflows mismatch for #{date}"
assert_equal flows[:non_cash_outflows], calculated_balance.non_cash_outflows.to_d,
"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.to_d,
"Net market flows mismatch for #{date}"
end
end
# Adjustment assertions
if adjustments.any?
if adjustments.is_a?(Integer) && adjustments == 0
assert_equal 0, calculated_balance.cash_adjustments.to_d,
"Cash adjustments mismatch for #{date}"
assert_equal 0, calculated_balance.non_cash_adjustments.to_d,
"Non-cash adjustments mismatch for #{date}"
elsif adjustments.is_a?(Hash) && adjustments.any?
assert_equal adjustments[:cash_adjustments], calculated_balance.cash_adjustments.to_d,
"Cash adjustments mismatch for #{date}" if adjustments.key?(:cash_adjustments)