mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-19 05:09:38 +02:00
Start and end balance anchors for historical account balances (#2455)
* Add kind field to valuation * Fix schema conflict * Add kind to valuation * Scaffold opening balance manager * Opening balance manager implementation * Update account import to use opening balance manager + tests * Update account to use opening balance manager * Fix test assertions, usage of current balance manager * Lint fixes * Add Opening Balance manager, add tests to forward calculator * Add credit card to "all cash" designation * Simplify valuation model * Add current balance manager with tests * Add current balance logic to reverse calculator and plaid sync * Tweaks to initial calc logic * Ledger testing helper, tweak assertions for reverse calculator * Update test assertions * Extract balance transformer, simplify calculators * Algo simplifications * Final tweaks to calculators * Cleanup * Fix error, propagate sync errors up to parent * Update migration script, valuation naming
This commit is contained in:
parent
9110ab27d2
commit
c1d98fe73b
35 changed files with 1903 additions and 355 deletions
152
test/support/ledger_testing_helper.rb
Normal file
152
test/support/ledger_testing_helper.rb
Normal file
|
@ -0,0 +1,152 @@
|
|||
module LedgerTestingHelper
|
||||
def create_account_with_ledger(account:, entries: [], exchange_rates: [], security_prices: [], holdings: [])
|
||||
# Clear all exchange rates and security prices to ensure clean test environment
|
||||
ExchangeRate.destroy_all
|
||||
Security::Price.destroy_all
|
||||
|
||||
# Create account with specified attributes
|
||||
account_attrs = account.except(:type)
|
||||
account_type = account[:type]
|
||||
|
||||
# Create the account
|
||||
created_account = families(:empty).accounts.create!(
|
||||
name: "Test Account",
|
||||
accountable: account_type.new,
|
||||
**account_attrs
|
||||
)
|
||||
|
||||
# Set up exchange rates if provided
|
||||
exchange_rates.each do |rate_data|
|
||||
ExchangeRate.create!(
|
||||
date: rate_data[:date],
|
||||
from_currency: rate_data[:from],
|
||||
to_currency: rate_data[:to],
|
||||
rate: rate_data[:rate]
|
||||
)
|
||||
end
|
||||
|
||||
# Set up security prices if provided
|
||||
security_prices.each do |price_data|
|
||||
security = Security.find_or_create_by!(ticker: price_data[:ticker]) do |s|
|
||||
s.name = price_data[:ticker]
|
||||
end
|
||||
|
||||
Security::Price.create!(
|
||||
security: security,
|
||||
date: price_data[:date],
|
||||
price: price_data[:price],
|
||||
currency: created_account.currency
|
||||
)
|
||||
end
|
||||
|
||||
# Create entries in the order they were specified
|
||||
entries.each do |entry_data|
|
||||
case entry_data[:type]
|
||||
when "current_anchor", "opening_anchor", "reconciliation"
|
||||
# Create valuation entry
|
||||
created_account.entries.create!(
|
||||
name: "Valuation",
|
||||
date: entry_data[:date],
|
||||
amount: entry_data[:balance],
|
||||
currency: entry_data[:currency] || created_account.currency,
|
||||
entryable: Valuation.new(kind: entry_data[:type])
|
||||
)
|
||||
when "transaction"
|
||||
# Use account currency if not specified
|
||||
currency = entry_data[:currency] || created_account.currency
|
||||
|
||||
created_account.entries.create!(
|
||||
name: "Transaction",
|
||||
date: entry_data[:date],
|
||||
amount: entry_data[:amount],
|
||||
currency: currency,
|
||||
entryable: Transaction.new
|
||||
)
|
||||
when "trade"
|
||||
# Find or create security
|
||||
security = Security.find_or_create_by!(ticker: entry_data[:ticker]) do |s|
|
||||
s.name = entry_data[:ticker]
|
||||
end
|
||||
|
||||
# Use account currency if not specified
|
||||
currency = entry_data[:currency] || created_account.currency
|
||||
|
||||
trade = Trade.new(
|
||||
qty: entry_data[:qty],
|
||||
security: security,
|
||||
price: entry_data[:price],
|
||||
currency: currency
|
||||
)
|
||||
|
||||
created_account.entries.create!(
|
||||
name: "Trade",
|
||||
date: entry_data[:date],
|
||||
amount: entry_data[:qty] * entry_data[:price],
|
||||
currency: currency,
|
||||
entryable: trade
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# Create holdings if provided
|
||||
holdings.each do |holding_data|
|
||||
# Find or create security
|
||||
security = Security.find_or_create_by!(ticker: holding_data[:ticker]) do |s|
|
||||
s.name = holding_data[:ticker]
|
||||
end
|
||||
|
||||
Holding.create!(
|
||||
account: created_account,
|
||||
security: security,
|
||||
date: holding_data[:date],
|
||||
qty: holding_data[:qty],
|
||||
price: holding_data[:price],
|
||||
amount: holding_data[:amount],
|
||||
currency: holding_data[:currency] || created_account.currency
|
||||
)
|
||||
end
|
||||
|
||||
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 ]
|
||||
end
|
||||
|
||||
# Get all unique dates from both calculated and expected data
|
||||
all_dates = (calculated_data.map(&:date) + expected_hash.keys).uniq.sort
|
||||
|
||||
# Check each date
|
||||
all_dates.each do |date|
|
||||
calculated_balance = calculated_data.find { |b| b.date == date }
|
||||
expected = expected_hash[date]
|
||||
|
||||
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
|
||||
|
||||
if expected[:cash_balance]
|
||||
assert_equal expected[:cash_balance], calculated_balance.cash_balance.to_d,
|
||||
"Cash balance mismatch for #{date}"
|
||||
end
|
||||
else
|
||||
assert_nil calculated_balance, "Unexpected balance calculated for #{date}"
|
||||
end
|
||||
end
|
||||
|
||||
# Verify we got all expected dates
|
||||
expected_dates = expected_hash.keys.sort
|
||||
calculated_dates = calculated_data.map(&:date).sort
|
||||
|
||||
expected_dates.each do |date|
|
||||
assert_includes calculated_dates, date,
|
||||
"Expected balance for #{date} was not in calculated data"
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Add table
Add a link
Reference in a new issue