From 91d970c7fe1e126b07f13050c3af6930fdc11e77 Mon Sep 17 00:00:00 2001 From: Zach Gollwitzer Date: Sun, 20 Jul 2025 08:37:55 -0400 Subject: [PATCH] Update calculator test assertions for new balance components --- app/models/balance.rb | 10 +- app/models/balance/base_calculator.rb | 4 +- app/models/balance/forward_calculator.rb | 4 +- app/models/balance/reverse_calculator.rb | 8 +- .../models/balance/forward_calculator_test.rb | 272 ++++++++++++++---- .../models/balance/reverse_calculator_test.rb | 230 ++++++++++++--- test/support/ledger_testing_helper.rb | 90 +++++- 7 files changed, 503 insertions(+), 115 deletions(-) diff --git a/app/models/balance.rb b/app/models/balance.rb index ff28db90..dffc9f07 100644 --- a/app/models/balance.rb +++ b/app/models/balance.rb @@ -2,8 +2,16 @@ class Balance < ApplicationRecord include Monetizable belongs_to :account + validates :account, :date, :balance, presence: true - monetize :balance, :cash_balance + validates :flows_factor, inclusion: { in: [ -1, 1 ] } + + monetize :balance, :cash_balance, + :start_cash_balance, :start_non_cash_balance, :start_balance, + :cash_inflows, :cash_outflows, :non_cash_inflows, :non_cash_outflows, :net_market_flows, + :cash_adjustments, :non_cash_adjustments, + :end_cash_balance, :end_non_cash_balance, :end_balance + scope :in_period, ->(period) { period.nil? ? all : where(date: period.date_range) } scope :chronological, -> { order(:date) } end diff --git a/app/models/balance/base_calculator.rb b/app/models/balance/base_calculator.rb index 92ef5d3e..af95bef8 100644 --- a/app/models/balance/base_calculator.rb +++ b/app/models/balance/base_calculator.rb @@ -57,12 +57,14 @@ class Balance::BaseCalculator raise NotImplementedError, "Directional calculators must implement this method" end - def build_balance(date:, cash_balance:, non_cash_balance:) + def build_balance(date:, cash_balance:, non_cash_balance:, start_cash_balance: nil, start_non_cash_balance: nil) Balance.new( account_id: account.id, date: date, balance: non_cash_balance + cash_balance, cash_balance: cash_balance, + start_cash_balance: start_cash_balance || 0, + start_non_cash_balance: start_non_cash_balance || 0, currency: account.currency ) end diff --git a/app/models/balance/forward_calculator.rb b/app/models/balance/forward_calculator.rb index bd9272b7..8cf96fa5 100644 --- a/app/models/balance/forward_calculator.rb +++ b/app/models/balance/forward_calculator.rb @@ -24,7 +24,9 @@ class Balance::ForwardCalculator < Balance::BaseCalculator output_balance = build_balance( date: date, cash_balance: end_cash_balance, - non_cash_balance: end_non_cash_balance + non_cash_balance: end_non_cash_balance, + start_cash_balance: start_cash_balance, + start_non_cash_balance: start_non_cash_balance ) # Set values for the next iteration diff --git a/app/models/balance/reverse_calculator.rb b/app/models/balance/reverse_calculator.rb index 1e75d5e4..36106d2f 100644 --- a/app/models/balance/reverse_calculator.rb +++ b/app/models/balance/reverse_calculator.rb @@ -24,7 +24,9 @@ class Balance::ReverseCalculator < Balance::BaseCalculator build_balance( date: date, cash_balance: end_cash_balance, - non_cash_balance: end_non_cash_balance + non_cash_balance: end_non_cash_balance, + start_cash_balance: start_cash_balance, + start_non_cash_balance: start_non_cash_balance ) else start_cash_balance = derive_start_cash_balance(end_cash_balance: end_cash_balance, date: date) @@ -35,7 +37,9 @@ class Balance::ReverseCalculator < Balance::BaseCalculator output_balance = build_balance( date: date, cash_balance: end_cash_balance, - non_cash_balance: end_non_cash_balance + non_cash_balance: end_non_cash_balance, + start_cash_balance: start_cash_balance, + start_non_cash_balance: start_non_cash_balance ) end_cash_balance = start_cash_balance diff --git a/test/models/balance/forward_calculator_test.rb b/test/models/balance/forward_calculator_test.rb index b6eb2d11..98ee9b54 100644 --- a/test/models/balance/forward_calculator_test.rb +++ b/test/models/balance/forward_calculator_test.rb @@ -21,8 +21,12 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase assert_calculated_ledger_balances( calculated_data: calculated, - expected_balances: [ - [ Date.current, { balance: 0, cash_balance: 0 } ] + expected_data: [ + { + 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 } + } ] ) end @@ -41,9 +45,17 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase # Since we start at 0, this transaction (inflow) simply increases balance from 0 -> 1000 assert_calculated_ledger_balances( calculated_data: calculated, - expected_balances: [ - [ 3.days.ago.to_date, { balance: 0, cash_balance: 0 } ], - [ 2.days.ago.to_date, { balance: 1000, cash_balance: 1000 } ] + expected_data: [ + { + 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 } + }, + { + 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 } + } ] ) end @@ -62,9 +74,17 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase # First valuation sets balance to 18000, then transaction increases balance to 19000 assert_calculated_ledger_balances( calculated_data: calculated, - expected_balances: [ - [ 3.days.ago.to_date, { balance: 18000, cash_balance: 18000 } ], - [ 2.days.ago.to_date, { balance: 19000, cash_balance: 19000 } ] + expected_data: [ + { + 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 } + }, + { + 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 } + } ] ) end @@ -83,9 +103,17 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase assert_calculated_ledger_balances( calculated_data: calculated, - expected_balances: [ - [ 3.days.ago.to_date, { balance: 17000, cash_balance: 17000 } ], - [ 2.days.ago.to_date, { balance: 18000, cash_balance: 18000 } ] + expected_data: [ + { + 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 } + }, + { + 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 } + } ] ) end @@ -105,9 +133,17 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase assert_calculated_ledger_balances( calculated_data: calculated, - expected_balances: [ - [ 3.days.ago.to_date, { balance: 17000, cash_balance: 0.0 } ], - [ 2.days.ago.to_date, { balance: 18000, cash_balance: 0.0 } ] + expected_data: [ + { + 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 } + }, + { + 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 } + } ] ) end @@ -127,9 +163,17 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase assert_calculated_ledger_balances( calculated_data: calculated, - expected_balances: [ - [ 3.days.ago.to_date, { balance: 17000, cash_balance: 17000 } ], - [ 2.days.ago.to_date, { balance: 18000, cash_balance: 18000 } ] + expected_data: [ + { + 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 } + }, + { + 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 } + } ] ) end @@ -152,11 +196,27 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase assert_calculated_ledger_balances( calculated_data: calculated, - expected_balances: [ - [ 5.days.ago.to_date, { balance: 20000, cash_balance: 20000 } ], - [ 4.days.ago.to_date, { balance: 20500, cash_balance: 20500 } ], - [ 3.days.ago.to_date, { balance: 20500, cash_balance: 20500 } ], - [ 2.days.ago.to_date, { balance: 20400, cash_balance: 20400 } ] + expected_data: [ + { + 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 } + }, + { + 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 } + }, + { + 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 } + }, + { + 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 } + } ] ) end @@ -176,11 +236,27 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase assert_calculated_ledger_balances( calculated_data: calculated, - expected_balances: [ - [ 5.days.ago.to_date, { balance: 1000, cash_balance: 1000 } ], - [ 4.days.ago.to_date, { balance: 500, cash_balance: 500 } ], - [ 3.days.ago.to_date, { balance: 500, cash_balance: 500 } ], - [ 2.days.ago.to_date, { balance: 600, cash_balance: 600 } ] + expected_data: [ + { + 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 } + }, + { + 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 } + }, + { + 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 } + }, + { + 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 } + } ] ) end @@ -203,17 +279,57 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase assert_calculated_ledger_balances( calculated_data: calculated, - expected_balances: [ - [ 10.days.ago.to_date, { balance: 20000, cash_balance: 20000 } ], - [ 9.days.ago.to_date, { balance: 20000, cash_balance: 20000 } ], - [ 8.days.ago.to_date, { balance: 25000, cash_balance: 25000 } ], - [ 7.days.ago.to_date, { balance: 25000, cash_balance: 25000 } ], - [ 6.days.ago.to_date, { balance: 17000, cash_balance: 17000 } ], - [ 5.days.ago.to_date, { balance: 17000, cash_balance: 17000 } ], - [ 4.days.ago.to_date, { balance: 17500, cash_balance: 17500 } ], - [ 3.days.ago.to_date, { balance: 17000, cash_balance: 17000 } ], - [ 2.days.ago.to_date, { balance: 17000, cash_balance: 17000 } ], - [ 1.day.ago.to_date, { balance: 16900, cash_balance: 16900 } ] + 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 } + }, + { + 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 } + }, + { + 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 } + }, + { + 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 } + } ] ) end @@ -237,11 +353,27 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase assert_calculated_ledger_balances( calculated_data: calculated, - expected_balances: [ - [ 4.days.ago.to_date, { balance: 100, cash_balance: 100 } ], - [ 3.days.ago.to_date, { balance: 200, cash_balance: 200 } ], - [ 2.days.ago.to_date, { balance: 500, cash_balance: 500 } ], - [ 1.day.ago.to_date, { balance: 1100, cash_balance: 1100 } ] + expected_data: [ + { + 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 } + }, + { + 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 } + }, + { + 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 } + }, + { + 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 } + } ] ) end @@ -263,9 +395,17 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase assert_calculated_ledger_balances( calculated_data: calculated, - expected_balances: [ - [ 2.days.ago.to_date, { balance: 20000, cash_balance: 0 } ], - [ 1.day.ago.to_date, { balance: 18000, cash_balance: 0 } ] + expected_data: [ + { + 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 } + }, + { + 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 } + } ] ) end @@ -286,9 +426,17 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase assert_calculated_ledger_balances( calculated_data: calculated, - expected_balances: [ - [ 3.days.ago.to_date, { balance: 500000, cash_balance: 0 } ], - [ 2.days.ago.to_date, { balance: 500000, cash_balance: 0 } ] + expected_data: [ + { + 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 } + }, + { + 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 } + } ] ) end @@ -324,11 +472,27 @@ class Balance::ForwardCalculatorTest < ActiveSupport::TestCase assert_calculated_ledger_balances( calculated_data: calculated, - expected_balances: [ - [ 3.days.ago.to_date, { balance: 5000, cash_balance: 5000 } ], - [ 2.days.ago.to_date, { balance: 5000, cash_balance: 5000 } ], - [ 1.day.ago.to_date, { balance: 5000, cash_balance: 4000 } ], - [ Date.current, { balance: 5000, cash_balance: 4000 } ] + expected_data: [ + { + 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 } + }, + { + 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 } + }, + { + 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 } + }, + { + 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 } + } ] ) end diff --git a/test/models/balance/reverse_calculator_test.rb b/test/models/balance/reverse_calculator_test.rb index a9348220..58470395 100644 --- a/test/models/balance/reverse_calculator_test.rb +++ b/test/models/balance/reverse_calculator_test.rb @@ -16,8 +16,12 @@ class Balance::ReverseCalculatorTest < ActiveSupport::TestCase assert_calculated_ledger_balances( calculated_data: calculated, - expected_balances: [ - [ Date.current, { balance: 20000, cash_balance: 20000 } ] + expected_data: [ + { + date: Date.current, + 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 } + } ] ) end @@ -47,12 +51,32 @@ class Balance::ReverseCalculatorTest < ActiveSupport::TestCase # a 100% full entries history. assert_calculated_ledger_balances( calculated_data: calculated, - expected_balances: [ - [ Date.current, { balance: 20000, cash_balance: 20000 } ], # Current anchor - [ 1.day.ago, { balance: 20000, cash_balance: 20000 } ], - [ 2.days.ago, { balance: 20000, cash_balance: 20000 } ], - [ 3.days.ago, { balance: 20000, cash_balance: 20000 } ], - [ 4.days.ago, { balance: 15000, cash_balance: 15000 } ] # Opening anchor + expected_data: [ + { + date: Date.current, + 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 } + }, # Current anchor + { + date: 1.day.ago, + 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: 2.days.ago, + 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: 3.days.ago, + legacy_balances: { balance: 20000, cash_balance: 20000 }, + balances: { start: 15000, start_cash: 15000, start_non_cash: 0, end_cash: 20000, end_non_cash: 0, end: 20000 } + }, + { + date: 4.days.ago, + legacy_balances: { balance: 15000, cash_balance: 15000 }, + balances: { start: 15000, start_cash: 15000, start_non_cash: 0, end_cash: 15000, end_non_cash: 0, end: 15000 } + } # Opening anchor ] ) end @@ -75,9 +99,17 @@ class Balance::ReverseCalculatorTest < ActiveSupport::TestCase assert_calculated_ledger_balances( calculated_data: calculated, - expected_balances: [ - [ Date.current, { balance: 20000, cash_balance: 10000 } ], # Since $10,000 of holdings, cash has to be $10,000 to reach $20,000 total value - [ 1.day.ago, { balance: 15000, cash_balance: 5000 } ] # Since $10,000 of holdings, cash has to be $5,000 to reach $15,000 total value + expected_data: [ + { + date: Date.current, + legacy_balances: { balance: 20000, cash_balance: 10000 }, + balances: { start: 20000, start_cash: 10000, start_non_cash: 10000, end_cash: 10000, end_non_cash: 10000, end: 20000 } + }, # Since $10,000 of holdings, cash has to be $10,000 to reach $20,000 total value + { + date: 1.day.ago, + legacy_balances: { balance: 15000, cash_balance: 5000 }, + balances: { start: 15000, start_cash: 5000, start_non_cash: 10000, end_cash: 5000, end_non_cash: 10000, end: 15000 } + } # Since $10,000 of holdings, cash has to be $5,000 to reach $15,000 total value ] ) end @@ -96,13 +128,37 @@ class Balance::ReverseCalculatorTest < ActiveSupport::TestCase assert_calculated_ledger_balances( calculated_data: calculated, - expected_balances: [ - [ Date.current, { balance: 20000, cash_balance: 20000 } ], # Current balance - [ 1.day.ago, { balance: 20000, cash_balance: 20000 } ], # No change - [ 2.days.ago, { balance: 20000, cash_balance: 20000 } ], # After expense (+100) - [ 3.days.ago, { balance: 20100, cash_balance: 20100 } ], # Before expense - [ 4.days.ago, { balance: 20100, cash_balance: 20100 } ], # After income (-500) - [ 5.days.ago, { balance: 19600, cash_balance: 19600 } ] # After income (-500) + expected_data: [ + { + date: Date.current, + 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 } + }, # Current balance + { + date: 1.day.ago, + 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 } + }, # No change + { + date: 2.days.ago, + legacy_balances: { balance: 20000, cash_balance: 20000 }, + balances: { start: 20100, start_cash: 20100, start_non_cash: 0, end_cash: 20000, end_non_cash: 0, end: 20000 } + }, # After expense (+100) + { + date: 3.days.ago, + legacy_balances: { balance: 20100, cash_balance: 20100 }, + balances: { start: 20100, start_cash: 20100, start_non_cash: 0, end_cash: 20100, end_non_cash: 0, end: 20100 } + }, # Before expense + { + date: 4.days.ago, + legacy_balances: { balance: 20100, cash_balance: 20100 }, + balances: { start: 19600, start_cash: 19600, start_non_cash: 0, end_cash: 20100, end_non_cash: 0, end: 20100 } + }, # After income (-500) + { + date: 5.days.ago, + legacy_balances: { balance: 19600, cash_balance: 19600 }, + balances: { start: 19600, start_cash: 19600, start_non_cash: 0, end_cash: 19600, end_non_cash: 0, end: 19600 } + } # After income (-500) ] ) end @@ -122,13 +178,37 @@ class Balance::ReverseCalculatorTest < ActiveSupport::TestCase # Reversed order: showing how we work backwards assert_calculated_ledger_balances( calculated_data: calculated, - expected_balances: [ - [ Date.current, { balance: 2000, cash_balance: 2000 } ], # Current balance - [ 1.day.ago, { balance: 2000, cash_balance: 2000 } ], # No change - [ 2.days.ago, { balance: 2000, cash_balance: 2000 } ], # After expense (+100) - [ 3.days.ago, { balance: 1900, cash_balance: 1900 } ], # Before expense - [ 4.days.ago, { balance: 1900, cash_balance: 1900 } ], # After CC payment (-500) - [ 5.days.ago, { balance: 2400, cash_balance: 2400 } ] + expected_data: [ + { + date: Date.current, + legacy_balances: { balance: 2000, cash_balance: 2000 }, + balances: { start: 2000, start_cash: 2000, start_non_cash: 0, end_cash: 2000, end_non_cash: 0, end: 2000 } + }, # Current balance + { + date: 1.day.ago, + legacy_balances: { balance: 2000, cash_balance: 2000 }, + balances: { start: 2000, start_cash: 2000, start_non_cash: 0, end_cash: 2000, end_non_cash: 0, end: 2000 } + }, # No change + { + date: 2.days.ago, + legacy_balances: { balance: 2000, cash_balance: 2000 }, + balances: { start: 1900, start_cash: 1900, start_non_cash: 0, end_cash: 2000, end_non_cash: 0, end: 2000 } + }, # After expense (+100) + { + date: 3.days.ago, + legacy_balances: { balance: 1900, cash_balance: 1900 }, + balances: { start: 1900, start_cash: 1900, start_non_cash: 0, end_cash: 1900, end_non_cash: 0, end: 1900 } + }, # Before expense + { + date: 4.days.ago, + legacy_balances: { balance: 1900, cash_balance: 1900 }, + balances: { start: 2400, start_cash: 2400, start_non_cash: 0, end_cash: 1900, end_non_cash: 0, end: 1900 } + }, # After CC payment (-500) + { + date: 5.days.ago, + legacy_balances: { balance: 2400, cash_balance: 2400 }, + balances: { start: 2400, start_cash: 2400, start_non_cash: 0, end_cash: 2400, end_non_cash: 0, end: 2400 } + } ] ) end @@ -150,10 +230,22 @@ class Balance::ReverseCalculatorTest < ActiveSupport::TestCase assert_calculated_ledger_balances( calculated_data: calculated, - expected_balances: [ - [ Date.current, { balance: 198000, cash_balance: 0 } ], - [ 1.day.ago, { balance: 198000, cash_balance: 0 } ], - [ 2.days.ago, { balance: 200000, cash_balance: 0 } ] + expected_data: [ + { + date: Date.current, + legacy_balances: { balance: 198000, cash_balance: 0 }, + balances: { start: 198000, start_cash: 0, start_non_cash: 198000, end_cash: 0, end_non_cash: 198000, end: 198000 } + }, + { + date: 1.day.ago, + legacy_balances: { balance: 198000, cash_balance: 0 }, + balances: { start: 200000, start_cash: 0, start_non_cash: 200000, end_cash: 0, end_non_cash: 198000, end: 198000 } + }, + { + date: 2.days.ago, + legacy_balances: { balance: 200000, cash_balance: 0 }, + balances: { start: 200000, start_cash: 0, start_non_cash: 200000, end_cash: 0, end_non_cash: 200000, end: 200000 } + } ] ) end @@ -174,10 +266,22 @@ class Balance::ReverseCalculatorTest < ActiveSupport::TestCase assert_calculated_ledger_balances( calculated_data: calculated, - expected_balances: [ - [ Date.current, { balance: 1000, cash_balance: 0 } ], - [ 1.day.ago, { balance: 1000, cash_balance: 0 } ], - [ 2.days.ago, { balance: 1000, cash_balance: 0 } ] + expected_data: [ + { + date: Date.current, + legacy_balances: { balance: 1000, cash_balance: 0 }, + balances: { start: 1000, start_cash: 0, start_non_cash: 1000, end_cash: 0, end_non_cash: 1000, end: 1000 } + }, + { + date: 1.day.ago, + legacy_balances: { balance: 1000, cash_balance: 0 }, + balances: { start: 1000, start_cash: 0, start_non_cash: 1000, end_cash: 0, end_non_cash: 1000, end: 1000 } + }, + { + date: 2.days.ago, + legacy_balances: { balance: 1000, cash_balance: 0 }, + balances: { start: 1000, start_cash: 0, start_non_cash: 1000, end_cash: 0, end_non_cash: 1000, end: 1000 } + } ] ) end @@ -206,10 +310,22 @@ class Balance::ReverseCalculatorTest < ActiveSupport::TestCase # (the single trade doesn't affect balance; it just alters cash vs. holdings composition) assert_calculated_ledger_balances( calculated_data: calculated, - expected_balances: [ - [ Date.current, { balance: 20000, cash_balance: 19000 } ], # Current: $19k cash + $1k holdings (anchor) - [ 1.day.ago.to_date, { balance: 20000, cash_balance: 19000 } ], # After trade: $19k cash + $1k holdings - [ 2.days.ago.to_date, { balance: 20000, cash_balance: 20000 } ] # At first, account is 100% cash, no holdings (no trades) + expected_data: [ + { + date: Date.current, + legacy_balances: { balance: 20000, cash_balance: 19000 }, + balances: { start: 20000, start_cash: 19000, start_non_cash: 1000, end_cash: 19000, end_non_cash: 1000, end: 20000 } + }, # Current: $19k cash + $1k holdings (anchor) + { + date: 1.day.ago.to_date, + legacy_balances: { balance: 20000, cash_balance: 19000 }, + balances: { start: 20000, start_cash: 20000, start_non_cash: 0, end_cash: 19000, end_non_cash: 1000, end: 20000 } + }, # After trade: $19k cash + $1k holdings + { + date: 2.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 } + } # At first, account is 100% cash, no holdings (no trades) ] ) end @@ -240,10 +356,22 @@ class Balance::ReverseCalculatorTest < ActiveSupport::TestCase assert_calculated_ledger_balances( calculated_data: calculated, - expected_balances: [ - [ Date.current, { balance: 20000, cash_balance: 19000 } ], # Current: $19k cash + $1k holdings ($500 MSFT, $500 AAPL) - [ 1.day.ago.to_date, { balance: 20000, cash_balance: 19000 } ], # After AAPL trade: $19k cash + $1k holdings - [ 2.days.ago.to_date, { balance: 20000, cash_balance: 19500 } ] # Before AAPL trade: $19.5k cash + $500 MSFT + expected_data: [ + { + date: Date.current, + legacy_balances: { balance: 20000, cash_balance: 19000 }, + balances: { start: 20000, start_cash: 19000, start_non_cash: 1000, end_cash: 19000, end_non_cash: 1000, end: 20000 } + }, # Current: $19k cash + $1k holdings ($500 MSFT, $500 AAPL) + { + date: 1.day.ago.to_date, + legacy_balances: { balance: 20000, cash_balance: 19000 }, + balances: { start: 20000, start_cash: 19500, start_non_cash: 500, end_cash: 19000, end_non_cash: 1000, end: 20000 } + }, # After AAPL trade: $19k cash + $1k holdings + { + date: 2.days.ago.to_date, + legacy_balances: { balance: 20000, cash_balance: 19500 }, + balances: { start: 20000, start_cash: 19500, start_non_cash: 500, end_cash: 19500, end_non_cash: 500, end: 20000 } + } # Before AAPL trade: $19.5k cash + $500 MSFT ] ) end @@ -267,12 +395,24 @@ class Balance::ReverseCalculatorTest < ActiveSupport::TestCase assert_calculated_ledger_balances( calculated_data: calculated, - expected_balances: [ + expected_data: [ # No matter what, we force current day equal to the "anchor" balance (what provider gave us), and let "cash" float based on holdings value # This ensures the user sees the same top-line number reported by the provider (even if it creates a discrepancy in the cash balance) - [ Date.current, { balance: 20000, cash_balance: 18000 } ], - [ 1.day.ago, { balance: 20000, cash_balance: 18000 } ], - [ 2.days.ago, { balance: 15000, cash_balance: 15000 } ] # Opening anchor sets absolute balance + { + date: Date.current, + legacy_balances: { balance: 20000, cash_balance: 18000 }, + balances: { start: 20000, start_cash: 18000, start_non_cash: 2000, end_cash: 18000, end_non_cash: 2000, end: 20000 } + }, + { + date: 1.day.ago, + legacy_balances: { balance: 20000, cash_balance: 18000 }, + balances: { start: 20000, start_cash: 18000, start_non_cash: 2000, end_cash: 18000, end_non_cash: 2000, end: 20000 } + }, + { + date: 2.days.ago, + legacy_balances: { balance: 15000, cash_balance: 15000 }, + balances: { start: 15000, start_cash: 15000, start_non_cash: 0, end_cash: 15000, end_non_cash: 0, end: 15000 } + } # Opening anchor sets absolute balance ] ) end diff --git a/test/support/ledger_testing_helper.rb b/test/support/ledger_testing_helper.rb index 6ae71678..75c25f98 100644 --- a/test/support/ledger_testing_helper.rb +++ b/test/support/ledger_testing_helper.rb @@ -109,13 +109,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 +133,76 @@ 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 + legacy_balances = expected[:legacy_balances] + balances = expected[:balances] + flows = expected[:flows] + adjustments = expected[:adjustments] - if expected[:cash_balance] - assert_equal expected[:cash_balance], calculated_balance.cash_balance.to_d, + # Legacy balance assertions + if legacy_balances.any? + assert_equal legacy_balances[:balance], calculated_balance.balance.to_d, + "Balance mismatch for #{date}" + + assert_equal legacy_balances[:cash_balance], calculated_balance.cash_balance.to_d, "Cash balance mismatch for #{date}" end + + # Balance assertions + if balances.any? + assert_equal balances[:start_cash], calculated_balance.start_cash_balance.to_d, + "Start cash balance mismatch for #{date}" if balances.key?(:start_cash) + + assert_equal balances[:start_non_cash], calculated_balance.start_non_cash_balance.to_d, + "Start non-cash balance mismatch for #{date}" if balances.key?(:start_non_cash) + + assert_equal balances[:end_cash], calculated_balance.end_cash_balance.to_d, + "End cash balance mismatch for #{date}" if balances.key?(:end_cash) + + assert_equal balances[:end_non_cash], calculated_balance.end_non_cash_balance.to_d, + "End non-cash balance mismatch for #{date}" if balances.key?(:end_non_cash) + + # Generated column assertions + assert_equal balances[:start], calculated_balance.start_balance.to_d, + "Start balance mismatch for #{date}" if balances.key?(:start) + + assert_equal balances[:end], calculated_balance.end_balance.to_d, + "End balance mismatch for #{date}" if balances.key?(:end) + 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) + + assert_equal flows[:cash_outflows], calculated_balance.cash_outflows.to_d, + "Cash outflows mismatch for #{date}" if flows.key?(:cash_outflows) + + 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 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 flows[:net_market_flows], calculated_balance.net_market_flows.to_d, + "Net market flows mismatch for #{date}" if flows.key?(:net_market_flows) + end + + # Adjustment assertions + if adjustments.any? + assert_equal adjustments[:cash_adjustments], calculated_balance.cash_adjustments.to_d, + "Cash adjustments mismatch for #{date}" if adjustments.key?(:cash_adjustments) + + assert_equal adjustments[:non_cash_adjustments], calculated_balance.non_cash_adjustments.to_d, + "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 + assert_equal calculated_balance.cash_balance.to_d, calculated_balance.end_cash_balance.to_d, + "Temporary assertion failed: end_cash_balance should equal cash_balance for #{date}" + + assert_equal calculated_balance.balance.to_d, calculated_balance.end_balance.to_d, + "Temporary assertion failed: end_balance should equal balance for #{date}" else assert_nil calculated_balance, "Unexpected balance calculated for #{date}" end