1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-07 06:25:19 +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. # 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 test "no entries sync" do
account = create_account_with_ledger( account = create_account_with_ledger(
account: { type: Depository, balance: 20000, cash_balance: 20000, currency: "USD" }, account: { type: Depository, currency: "USD" },
entries: [] entries: []
) )
@ -25,7 +25,9 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase
{ {
date: Date.current, date: Date.current,
legacy_balances: { balance: 0, cash_balance: 0 }, 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") # 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 test "account without opening anchor starts at zero balance" do
account = create_account_with_ledger( account = create_account_with_ledger(
account: { type: Depository, balance: 20000, cash_balance: 20000, currency: "USD" }, account: { type: Depository, currency: "USD" },
entries: [ entries: [
{ type: "transaction", date: 2.days.ago.to_date, amount: -1000 } { 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, date: 3.days.ago.to_date,
legacy_balances: { balance: 0, cash_balance: 0 }, 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, date: 2.days.ago.to_date,
legacy_balances: { balance: 1000, cash_balance: 1000 }, 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 test "reconciliation valuation sets absolute balance before applying subsequent transactions" do
account = create_account_with_ledger( account = create_account_with_ledger(
account: { type: Depository, balance: 20000, cash_balance: 20000, currency: "USD" }, account: { type: Depository, currency: "USD" },
entries: [ entries: [
{ type: "reconciliation", date: 3.days.ago.to_date, balance: 18000 }, { type: "reconciliation", date: 3.days.ago.to_date, balance: 18000 },
{ type: "transaction", date: 2.days.ago.to_date, amount: -1000 } { 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, date: 3.days.ago.to_date,
legacy_balances: { balance: 18000, cash_balance: 18000 }, 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, date: 2.days.ago.to_date,
legacy_balances: { balance: 19000, cash_balance: 19000 }, 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 test "cash-only accounts (depository, credit card) use valuations where cash balance equals total balance" do
[ Depository, CreditCard ].each do |account_type| [ Depository, CreditCard ].each do |account_type|
account = create_account_with_ledger( account = create_account_with_ledger(
account: { type: account_type, balance: 10000, cash_balance: 10000, currency: "USD" }, account: { type: account_type, currency: "USD" },
entries: [ entries: [
{ type: "opening_anchor", date: 3.days.ago.to_date, balance: 17000 }, { type: "opening_anchor", date: 3.days.ago.to_date, balance: 17000 },
{ type: "reconciliation", date: 2.days.ago.to_date, balance: 18000 } { 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, date: 3.days.ago.to_date,
legacy_balances: { balance: 17000, cash_balance: 17000 }, 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, date: 2.days.ago.to_date,
legacy_balances: { balance: 18000, cash_balance: 18000 }, 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 test "non-cash accounts (property, loan) use valuations where cash balance is always zero" do
[ Property, Loan ].each do |account_type| [ Property, Loan ].each do |account_type|
account = create_account_with_ledger( account = create_account_with_ledger(
account: { type: account_type, balance: 10000, cash_balance: 10000, currency: "USD" }, account: { type: account_type, currency: "USD" },
entries: [ entries: [
{ type: "opening_anchor", date: 3.days.ago.to_date, balance: 17000 }, { type: "opening_anchor", date: 3.days.ago.to_date, balance: 17000 },
{ type: "reconciliation", date: 2.days.ago.to_date, balance: 18000 } { 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, date: 3.days.ago.to_date,
legacy_balances: { balance: 17000, cash_balance: 0.0 }, 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, date: 2.days.ago.to_date,
legacy_balances: { balance: 18000, cash_balance: 0.0 }, 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 test "mixed accounts (investment) use valuations where cash balance is total minus holdings" do
account = create_account_with_ledger( account = create_account_with_ledger(
account: { type: Investment, balance: 10000, cash_balance: 10000, currency: "USD" }, account: { type: Investment, currency: "USD" },
entries: [ entries: [
{ type: "opening_anchor", date: 3.days.ago.to_date, balance: 17000 }, { type: "opening_anchor", date: 3.days.ago.to_date, balance: 17000 },
{ type: "reconciliation", date: 2.days.ago.to_date, balance: 18000 } { 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, date: 3.days.ago.to_date,
legacy_balances: { balance: 17000, cash_balance: 17000 }, 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, date: 2.days.ago.to_date,
legacy_balances: { balance: 18000, cash_balance: 18000 }, 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 test "transactions on depository accounts affect cash balance" do
account = create_account_with_ledger( account = create_account_with_ledger(
account: { type: Depository, balance: 20000, cash_balance: 20000, currency: "USD" }, account: { type: Depository, currency: "USD" },
entries: [ entries: [
{ type: "opening_anchor", date: 5.days.ago.to_date, balance: 20000 }, { type: "opening_anchor", date: 5.days.ago.to_date, balance: 20000 },
{ type: "transaction", date: 4.days.ago.to_date, amount: -500 }, # income { 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, date: 5.days.ago.to_date,
legacy_balances: { balance: 20000, cash_balance: 20000 }, 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, date: 4.days.ago.to_date,
legacy_balances: { balance: 20500, cash_balance: 20500 }, 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, date: 3.days.ago.to_date,
legacy_balances: { balance: 20500, cash_balance: 20500 }, 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, date: 2.days.ago.to_date,
legacy_balances: { balance: 20400, cash_balance: 20400 }, 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 test "transactions on credit card accounts affect cash balance inversely" do
account = create_account_with_ledger( account = create_account_with_ledger(
account: { type: CreditCard, balance: 10000, cash_balance: 10000, currency: "USD" }, account: { type: CreditCard, currency: "USD" },
entries: [ entries: [
{ type: "opening_anchor", date: 5.days.ago.to_date, balance: 1000 }, { type: "opening_anchor", date: 5.days.ago.to_date, balance: 1000 },
{ type: "transaction", date: 4.days.ago.to_date, amount: -500 }, # CC payment { 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, date: 5.days.ago.to_date,
legacy_balances: { balance: 1000, cash_balance: 1000 }, 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, date: 4.days.ago.to_date,
legacy_balances: { balance: 500, cash_balance: 500 }, 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, date: 3.days.ago.to_date,
legacy_balances: { balance: 500, cash_balance: 500 }, 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, date: 2.days.ago.to_date,
legacy_balances: { balance: 600, cash_balance: 600 }, 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 test "depository account with transactions and balance reconciliations" do
account = create_account_with_ledger( account = create_account_with_ledger(
account: { type: Depository, balance: 20000, cash_balance: 20000, currency: "USD" }, account: { type: Depository, currency: "USD" },
entries: [ entries: [
{ type: "opening_anchor", date: 10.days.ago.to_date, balance: 20000 }, { type: "opening_anchor", date: 4.days.ago.to_date, balance: 20000 },
{ type: "transaction", date: 8.days.ago.to_date, amount: -5000 }, { type: "transaction", date: 3.days.ago.to_date, amount: -5000 },
{ type: "reconciliation", date: 6.days.ago.to_date, balance: 17000 }, { type: "reconciliation", date: 2.days.ago.to_date, balance: 17000 },
{ type: "transaction", date: 6.days.ago.to_date, amount: -500 }, { type: "transaction", date: 1.day.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 }
] ]
) )
@ -280,63 +315,41 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase
assert_calculated_ledger_balances( assert_calculated_ledger_balances(
calculated_data: calculated, calculated_data: calculated,
expected_data: [ 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, date: 4.days.ago.to_date,
legacy_balances: { balance: 17500, cash_balance: 17500 }, legacy_balances: { balance: 20000, cash_balance: 20000 },
balances: { start: 17000, start_cash: 17000, start_non_cash: 0, end_cash: 17500, end_non_cash: 0, end: 17500 } 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, date: 3.days.ago.to_date,
legacy_balances: { balance: 17000, cash_balance: 17000 }, legacy_balances: { balance: 25000, cash_balance: 25000 },
balances: { start: 17500, start_cash: 17500, start_non_cash: 0, end_cash: 17000, end_non_cash: 0, end: 17000 } 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, date: 2.days.ago.to_date,
legacy_balances: { balance: 17000, cash_balance: 17000 }, 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, date: 1.day.ago.to_date,
legacy_balances: { balance: 16900, cash_balance: 16900 }, legacy_balances: { balance: 17500, cash_balance: 17500 },
balances: { start: 17000, start_cash: 17000, start_non_cash: 0, end_cash: 16900, end_non_cash: 0, end: 16900 } 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 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 = create_account_with_ledger(
account: { type: Depository, balance: 20000, cash_balance: 20000, currency: "USD" }, account: { type: Depository, currency: "USD" },
entries: [ entries: [
{ type: "opening_anchor", date: 4.days.ago.to_date, balance: 100 }, { type: "opening_anchor", date: 4.days.ago.to_date, balance: 100 },
{ type: "transaction", date: 3.days.ago.to_date, amount: -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, date: 4.days.ago.to_date,
legacy_balances: { balance: 100, cash_balance: 100 }, 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, date: 3.days.ago.to_date,
legacy_balances: { balance: 200, cash_balance: 200 }, 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, date: 2.days.ago.to_date,
legacy_balances: { balance: 500, cash_balance: 500 }, 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, date: 1.day.ago.to_date,
legacy_balances: { balance: 1100, cash_balance: 1100 }, 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) # 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 test "loan payment transactions affect non cash balance" do
account = create_account_with_ledger( account = create_account_with_ledger(
account: { type: Loan, balance: 10000, cash_balance: 0, currency: "USD" }, account: { type: Loan, currency: "USD" },
entries: [ entries: [
{ type: "opening_anchor", date: 2.days.ago.to_date, balance: 20000 }, { type: "opening_anchor", date: 2.days.ago.to_date, balance: 20000 },
# "Loan payment" of $2000, which reduces the principal # "Loan payment" of $2000, which reduces the principal
@ -399,12 +420,16 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase
{ {
date: 2.days.ago.to_date, date: 2.days.ago.to_date,
legacy_balances: { balance: 20000, cash_balance: 0 }, 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, date: 1.day.ago.to_date,
legacy_balances: { balance: 18000, cash_balance: 0 }, 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 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| [ Property, Vehicle, OtherAsset, OtherLiability ].each do |account_type|
account = create_account_with_ledger( account = create_account_with_ledger(
account: { type: account_type, balance: 10000, cash_balance: 10000, currency: "USD" }, account: { type: account_type, currency: "USD" },
entries: [ entries: [
{ type: "opening_anchor", date: 3.days.ago.to_date, balance: 500000 }, { 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, date: 3.days.ago.to_date,
legacy_balances: { balance: 500000, cash_balance: 0 }, 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, date: 2.days.ago.to_date,
legacy_balances: { balance: 500000, cash_balance: 0 }, 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" # 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 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 = create_account_with_ledger(
account: { type: Investment, balance: 10000, cash_balance: 10000, currency: "USD" }, account: { type: Investment, currency: "USD" },
entries: [ entries: [
# Account starts with brokerage cash of $5000 and no holdings # Account starts with brokerage cash of $5000 and no holdings
{ type: "opening_anchor", date: 3.days.ago.to_date, balance: 5000 }, { type: "opening_anchor", date: 3.days.ago.to_date, balance: 5000 },
@ -462,7 +491,7 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase
holdings: [ holdings: [
# Holdings calculator will calculate $1000 worth of holdings # Holdings calculator will calculate $1000 worth of holdings
{ date: 1.day.ago.to_date, ticker: "AAPL", qty: 10, price: 100, amount: 1000 }, { 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, date: 3.days.ago.to_date,
legacy_balances: { balance: 5000, cash_balance: 5000 }, 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, date: 2.days.ago.to_date,
legacy_balances: { balance: 5000, cash_balance: 5000 }, 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, date: 1.day.ago.to_date,
legacy_balances: { balance: 5000, cash_balance: 4000 }, 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, date: Date.current,
legacy_balances: { balance: 5000, cash_balance: 4000 }, 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 end
private private
def assert_balances(calculated_data:, expected_balances:) def assert_balances(calculated_data:, expected_balances:)
# Sort calculated data by date to ensure consistent ordering # Sort calculated data by date to ensure consistent ordering
sorted_data = calculated_data.sort_by(&:date) sorted_data = calculated_data.sort_by(&:date)

View file

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