mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-07 06:25:19 +02:00
Balance series uses new component fields
This commit is contained in:
parent
e795c4c9a3
commit
bf3ce5d79b
7 changed files with 191 additions and 124 deletions
|
@ -82,8 +82,8 @@ class Account::ReconciliationManager
|
||||||
balance_record = account.balances.find_by(date: date, currency: account.currency)
|
balance_record = account.balances.find_by(date: date, currency: account.currency)
|
||||||
|
|
||||||
{
|
{
|
||||||
cash_balance: balance_record&.cash_balance,
|
cash_balance: balance_record&.end_cash_balance,
|
||||||
balance: balance_record&.balance
|
balance: balance_record&.end_balance
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,21 +8,21 @@ class Balance::ChartSeriesBuilder
|
||||||
end
|
end
|
||||||
|
|
||||||
def balance_series
|
def balance_series
|
||||||
build_series_for(:balance)
|
build_series_for(:end_balance)
|
||||||
rescue => e
|
rescue => e
|
||||||
Rails.logger.error "Balance series error: #{e.message} for accounts #{@account_ids}"
|
Rails.logger.error "Balance series error: #{e.message} for accounts #{@account_ids}"
|
||||||
raise
|
raise
|
||||||
end
|
end
|
||||||
|
|
||||||
def cash_balance_series
|
def cash_balance_series
|
||||||
build_series_for(:cash_balance)
|
build_series_for(:end_cash_balance)
|
||||||
rescue => e
|
rescue => e
|
||||||
Rails.logger.error "Cash balance series error: #{e.message} for accounts #{@account_ids}"
|
Rails.logger.error "Cash balance series error: #{e.message} for accounts #{@account_ids}"
|
||||||
raise
|
raise
|
||||||
end
|
end
|
||||||
|
|
||||||
def holdings_balance_series
|
def holdings_balance_series
|
||||||
build_series_for(:holdings_balance)
|
build_series_for(:end_holdings_balance)
|
||||||
rescue => e
|
rescue => e
|
||||||
Rails.logger.error "Holdings balance series error: #{e.message} for accounts #{@account_ids}"
|
Rails.logger.error "Holdings balance series error: #{e.message} for accounts #{@account_ids}"
|
||||||
raise
|
raise
|
||||||
|
@ -37,13 +37,20 @@ class Balance::ChartSeriesBuilder
|
||||||
|
|
||||||
def build_series_for(column)
|
def build_series_for(column)
|
||||||
values = query_data.map do |datum|
|
values = query_data.map do |datum|
|
||||||
|
# Map column names to their start equivalents
|
||||||
|
previous_column = case column
|
||||||
|
when :end_balance then :start_balance
|
||||||
|
when :end_cash_balance then :start_cash_balance
|
||||||
|
when :end_holdings_balance then :start_holdings_balance
|
||||||
|
end
|
||||||
|
|
||||||
Series::Value.new(
|
Series::Value.new(
|
||||||
date: datum.date,
|
date: datum.date,
|
||||||
date_formatted: I18n.l(datum.date, format: :long),
|
date_formatted: I18n.l(datum.date, format: :long),
|
||||||
value: Money.new(datum.send(column), currency),
|
value: Money.new(datum.send(column), currency),
|
||||||
trend: Trend.new(
|
trend: Trend.new(
|
||||||
current: Money.new(datum.send(column), currency),
|
current: Money.new(datum.send(column), currency),
|
||||||
previous: Money.new(datum.send("previous_#{column}"), currency),
|
previous: Money.new(datum.send(previous_column), currency),
|
||||||
favorable_direction: favorable_direction
|
favorable_direction: favorable_direction
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -88,66 +95,57 @@ class Balance::ChartSeriesBuilder
|
||||||
WITH dates AS (
|
WITH dates AS (
|
||||||
SELECT generate_series(DATE :start_date, DATE :end_date, :interval::interval)::date AS date
|
SELECT generate_series(DATE :start_date, DATE :end_date, :interval::interval)::date AS date
|
||||||
UNION DISTINCT
|
UNION DISTINCT
|
||||||
SELECT :end_date::date -- Pass in date to ensure timezone-aware "today" date
|
SELECT :end_date::date -- Ensure end date is included
|
||||||
), aggregated_balances AS (
|
|
||||||
SELECT
|
|
||||||
d.date,
|
|
||||||
-- Total balance (assets positive, liabilities negative)
|
|
||||||
SUM(
|
|
||||||
CASE WHEN accounts.classification = 'asset'
|
|
||||||
THEN COALESCE(last_bal.balance, 0)
|
|
||||||
ELSE -COALESCE(last_bal.balance, 0)
|
|
||||||
END * COALESCE(er.rate, 1) * :sign_multiplier::integer
|
|
||||||
) AS balance,
|
|
||||||
-- Cash-only balance
|
|
||||||
SUM(
|
|
||||||
CASE WHEN accounts.classification = 'asset'
|
|
||||||
THEN COALESCE(last_bal.cash_balance, 0)
|
|
||||||
ELSE -COALESCE(last_bal.cash_balance, 0)
|
|
||||||
END * COALESCE(er.rate, 1) * :sign_multiplier::integer
|
|
||||||
) AS cash_balance,
|
|
||||||
-- Holdings value (balance ‑ cash)
|
|
||||||
SUM(
|
|
||||||
CASE WHEN accounts.classification = 'asset'
|
|
||||||
THEN COALESCE(last_bal.balance, 0) - COALESCE(last_bal.cash_balance, 0)
|
|
||||||
ELSE 0
|
|
||||||
END * COALESCE(er.rate, 1) * :sign_multiplier::integer
|
|
||||||
) AS holdings_balance
|
|
||||||
FROM dates d
|
|
||||||
JOIN accounts ON accounts.id = ANY(array[:account_ids]::uuid[])
|
|
||||||
|
|
||||||
-- Last observation carried forward (LOCF), use the most recent balance on or before the chart date
|
|
||||||
LEFT JOIN LATERAL (
|
|
||||||
SELECT b.balance, b.cash_balance
|
|
||||||
FROM balances b
|
|
||||||
WHERE b.account_id = accounts.id
|
|
||||||
AND b.date <= d.date
|
|
||||||
ORDER BY b.date DESC
|
|
||||||
LIMIT 1
|
|
||||||
) last_bal ON TRUE
|
|
||||||
|
|
||||||
-- Last observation carried forward (LOCF), use the most recent exchange rate on or before the chart date
|
|
||||||
LEFT JOIN LATERAL (
|
|
||||||
SELECT er.rate
|
|
||||||
FROM exchange_rates er
|
|
||||||
WHERE er.from_currency = accounts.currency
|
|
||||||
AND er.to_currency = :target_currency
|
|
||||||
AND er.date <= d.date
|
|
||||||
ORDER BY er.date DESC
|
|
||||||
LIMIT 1
|
|
||||||
) er ON TRUE
|
|
||||||
GROUP BY d.date
|
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
date,
|
d.date,
|
||||||
balance,
|
-- Use flows_factor: already handles asset (+1) vs liability (-1)
|
||||||
cash_balance,
|
COALESCE(SUM(last_bal.end_balance * last_bal.flows_factor * COALESCE(er.rate, 1) * :sign_multiplier::integer), 0) AS end_balance,
|
||||||
holdings_balance,
|
COALESCE(SUM(last_bal.end_cash_balance * last_bal.flows_factor * COALESCE(er.rate, 1) * :sign_multiplier::integer), 0) AS end_cash_balance,
|
||||||
COALESCE(LAG(balance) OVER (ORDER BY date), 0) AS previous_balance,
|
-- Holdings only for assets (flows_factor = 1)
|
||||||
COALESCE(LAG(cash_balance) OVER (ORDER BY date), 0) AS previous_cash_balance,
|
COALESCE(SUM(
|
||||||
COALESCE(LAG(holdings_balance) OVER (ORDER BY date), 0) AS previous_holdings_balance
|
CASE WHEN last_bal.flows_factor = 1
|
||||||
FROM aggregated_balances
|
THEN last_bal.end_non_cash_balance
|
||||||
ORDER BY date
|
ELSE 0
|
||||||
|
END * COALESCE(er.rate, 1) * :sign_multiplier::integer
|
||||||
|
), 0) AS end_holdings_balance,
|
||||||
|
-- Previous balances
|
||||||
|
COALESCE(SUM(last_bal.start_balance * last_bal.flows_factor * COALESCE(er.rate, 1) * :sign_multiplier::integer), 0) AS start_balance,
|
||||||
|
COALESCE(SUM(last_bal.start_cash_balance * last_bal.flows_factor * COALESCE(er.rate, 1) * :sign_multiplier::integer), 0) AS start_cash_balance,
|
||||||
|
COALESCE(SUM(
|
||||||
|
CASE WHEN last_bal.flows_factor = 1
|
||||||
|
THEN last_bal.start_non_cash_balance
|
||||||
|
ELSE 0
|
||||||
|
END * COALESCE(er.rate, 1) * :sign_multiplier::integer
|
||||||
|
), 0) AS start_holdings_balance
|
||||||
|
FROM dates d
|
||||||
|
CROSS JOIN accounts
|
||||||
|
LEFT JOIN LATERAL (
|
||||||
|
SELECT b.end_balance,
|
||||||
|
b.end_cash_balance,
|
||||||
|
b.end_non_cash_balance,
|
||||||
|
b.start_balance,
|
||||||
|
b.start_cash_balance,
|
||||||
|
b.start_non_cash_balance,
|
||||||
|
b.flows_factor
|
||||||
|
FROM balances b
|
||||||
|
WHERE b.account_id = accounts.id
|
||||||
|
AND b.date <= d.date
|
||||||
|
ORDER BY b.date DESC
|
||||||
|
LIMIT 1
|
||||||
|
) last_bal ON TRUE
|
||||||
|
LEFT JOIN LATERAL (
|
||||||
|
SELECT er.rate
|
||||||
|
FROM exchange_rates er
|
||||||
|
WHERE er.from_currency = accounts.currency
|
||||||
|
AND er.to_currency = :target_currency
|
||||||
|
AND er.date <= d.date
|
||||||
|
ORDER BY er.date DESC
|
||||||
|
LIMIT 1
|
||||||
|
) er ON TRUE
|
||||||
|
WHERE accounts.id = ANY(array[:account_ids]::uuid[])
|
||||||
|
GROUP BY d.date
|
||||||
|
ORDER BY d.date
|
||||||
SQL
|
SQL
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -28,9 +28,20 @@ class Balance::Materializer
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_account_info
|
def update_account_info
|
||||||
calculated_balance = @balances.sort_by(&:date).last&.balance || 0
|
# Query fresh balance from DB to get generated column values
|
||||||
calculated_holdings_value = @holdings.select { |h| h.date == Date.current }.sum(&:amount) || 0
|
current_balance = account.balances
|
||||||
calculated_cash_balance = calculated_balance - calculated_holdings_value
|
.where(currency: account.currency)
|
||||||
|
.order(date: :desc)
|
||||||
|
.first
|
||||||
|
|
||||||
|
if current_balance
|
||||||
|
calculated_balance = current_balance.end_balance
|
||||||
|
calculated_cash_balance = current_balance.end_cash_balance
|
||||||
|
else
|
||||||
|
# Fallback if no balance exists
|
||||||
|
calculated_balance = 0
|
||||||
|
calculated_cash_balance = 0
|
||||||
|
end
|
||||||
|
|
||||||
Rails.logger.info("Balance update: cash=#{calculated_cash_balance}, total=#{calculated_balance}")
|
Rails.logger.info("Balance update: cash=#{calculated_cash_balance}, total=#{calculated_balance}")
|
||||||
|
|
||||||
|
@ -61,7 +72,10 @@ class Balance::Materializer
|
||||||
end
|
end
|
||||||
|
|
||||||
def purge_stale_balances
|
def purge_stale_balances
|
||||||
deleted_count = account.balances.delete_by("date < ?", account.start_date)
|
sorted_balances = @balances.sort_by(&:date)
|
||||||
|
oldest_calculated_balance_date = sorted_balances.first&.date
|
||||||
|
newest_calculated_balance_date = sorted_balances.last&.date
|
||||||
|
deleted_count = account.balances.delete_by("date < ? OR date > ?", oldest_calculated_balance_date, newest_calculated_balance_date)
|
||||||
Rails.logger.info("Purged #{deleted_count} stale balances") if deleted_count > 0
|
Rails.logger.info("Purged #{deleted_count} stale balances") if deleted_count > 0
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,15 @@
|
||||||
require "test_helper"
|
require "test_helper"
|
||||||
|
|
||||||
class Account::ReconciliationManagerTest < ActiveSupport::TestCase
|
class Account::ReconciliationManagerTest < ActiveSupport::TestCase
|
||||||
|
include BalanceTestHelper
|
||||||
|
|
||||||
setup do
|
setup do
|
||||||
@account = accounts(:investment)
|
@account = accounts(:investment)
|
||||||
@manager = Account::ReconciliationManager.new(@account)
|
@manager = Account::ReconciliationManager.new(@account)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "new reconciliation" do
|
test "new reconciliation" do
|
||||||
@account.balances.create!(
|
create_balance(account: @account, date: Date.current, balance: 1000, cash_balance: 500)
|
||||||
date: Date.current,
|
|
||||||
balance: 1000,
|
|
||||||
cash_balance: 500,
|
|
||||||
currency: @account.currency
|
|
||||||
)
|
|
||||||
|
|
||||||
result = @manager.reconcile_balance(balance: 1200, date: Date.current)
|
result = @manager.reconcile_balance(balance: 1200, date: Date.current)
|
||||||
|
|
||||||
|
@ -24,7 +21,7 @@ class Account::ReconciliationManagerTest < ActiveSupport::TestCase
|
||||||
end
|
end
|
||||||
|
|
||||||
test "updates existing reconciliation without date change" do
|
test "updates existing reconciliation without date change" do
|
||||||
@account.balances.create!(date: Date.current, balance: 1000, cash_balance: 500, currency: @account.currency)
|
create_balance(account: @account, date: Date.current, balance: 1000, cash_balance: 500)
|
||||||
|
|
||||||
# Existing reconciliation entry
|
# Existing reconciliation entry
|
||||||
existing_entry = @account.entries.create!(name: "Test", amount: 1000, date: Date.current, entryable: Valuation.new(kind: "reconciliation"), currency: @account.currency)
|
existing_entry = @account.entries.create!(name: "Test", amount: 1000, date: Date.current, entryable: Valuation.new(kind: "reconciliation"), currency: @account.currency)
|
||||||
|
@ -39,8 +36,8 @@ class Account::ReconciliationManagerTest < ActiveSupport::TestCase
|
||||||
end
|
end
|
||||||
|
|
||||||
test "updates existing reconciliation with date and amount change" do
|
test "updates existing reconciliation with date and amount change" do
|
||||||
@account.balances.create!(date: 5.days.ago, balance: 1000, cash_balance: 500, currency: @account.currency)
|
create_balance(account: @account, date: 5.days.ago, balance: 1000, cash_balance: 500)
|
||||||
@account.balances.create!(date: Date.current, balance: 1200, cash_balance: 700, currency: @account.currency)
|
create_balance(account: @account, date: Date.current, balance: 1200, cash_balance: 700)
|
||||||
|
|
||||||
# Existing reconciliation entry (5 days ago)
|
# Existing reconciliation entry (5 days ago)
|
||||||
existing_entry = @account.entries.create!(name: "Test", amount: 1000, date: 5.days.ago, entryable: Valuation.new(kind: "reconciliation"), currency: @account.currency)
|
existing_entry = @account.entries.create!(name: "Test", amount: 1000, date: 5.days.ago, entryable: Valuation.new(kind: "reconciliation"), currency: @account.currency)
|
||||||
|
@ -63,12 +60,7 @@ class Account::ReconciliationManagerTest < ActiveSupport::TestCase
|
||||||
end
|
end
|
||||||
|
|
||||||
test "handles date conflicts" do
|
test "handles date conflicts" do
|
||||||
@account.balances.create!(
|
create_balance(account: @account, date: Date.current, balance: 1000, cash_balance: 1000)
|
||||||
date: Date.current,
|
|
||||||
balance: 1000,
|
|
||||||
cash_balance: 1000,
|
|
||||||
currency: @account.currency
|
|
||||||
)
|
|
||||||
|
|
||||||
# Existing reconciliation entry
|
# Existing reconciliation entry
|
||||||
@account.entries.create!(
|
@account.entries.create!(
|
||||||
|
@ -89,7 +81,7 @@ class Account::ReconciliationManagerTest < ActiveSupport::TestCase
|
||||||
end
|
end
|
||||||
|
|
||||||
test "dry run does not persist account" do
|
test "dry run does not persist account" do
|
||||||
@account.balances.create!(date: Date.current, balance: 1000, cash_balance: 500, currency: @account.currency)
|
create_balance(account: @account, date: Date.current, balance: 1000, cash_balance: 500)
|
||||||
|
|
||||||
assert_no_difference "Valuation.count" do
|
assert_no_difference "Valuation.count" do
|
||||||
@manager.reconcile_balance(balance: 1200, date: Date.current, dry_run: true)
|
@manager.reconcile_balance(balance: 1200, date: Date.current, dry_run: true)
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
require "test_helper"
|
require "test_helper"
|
||||||
|
|
||||||
class Balance::ChartSeriesBuilderTest < ActiveSupport::TestCase
|
class Balance::ChartSeriesBuilderTest < ActiveSupport::TestCase
|
||||||
|
include BalanceTestHelper
|
||||||
|
|
||||||
setup do
|
setup do
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -9,9 +11,9 @@ class Balance::ChartSeriesBuilderTest < ActiveSupport::TestCase
|
||||||
account.balances.destroy_all
|
account.balances.destroy_all
|
||||||
|
|
||||||
# With gaps
|
# With gaps
|
||||||
account.balances.create!(date: 3.days.ago.to_date, balance: 1000, currency: "USD")
|
create_balance(account: account, date: 3.days.ago.to_date, balance: 1000)
|
||||||
account.balances.create!(date: 1.day.ago.to_date, balance: 1100, currency: "USD")
|
create_balance(account: account, date: 1.day.ago.to_date, balance: 1100)
|
||||||
account.balances.create!(date: Date.current, balance: 1200, currency: "USD")
|
create_balance(account: account, date: Date.current, balance: 1200)
|
||||||
|
|
||||||
builder = Balance::ChartSeriesBuilder.new(
|
builder = Balance::ChartSeriesBuilder.new(
|
||||||
account_ids: [ account.id ],
|
account_ids: [ account.id ],
|
||||||
|
@ -38,9 +40,9 @@ class Balance::ChartSeriesBuilderTest < ActiveSupport::TestCase
|
||||||
account = accounts(:depository)
|
account = accounts(:depository)
|
||||||
account.balances.destroy_all
|
account.balances.destroy_all
|
||||||
|
|
||||||
account.balances.create!(date: 2.days.ago.to_date, balance: 1000, currency: "USD")
|
create_balance(account: account, date: 2.days.ago.to_date, balance: 1000)
|
||||||
account.balances.create!(date: 1.day.ago.to_date, balance: 1100, currency: "USD")
|
create_balance(account: account, date: 1.day.ago.to_date, balance: 1100)
|
||||||
account.balances.create!(date: Date.current, balance: 1200, currency: "USD")
|
create_balance(account: account, date: Date.current, balance: 1200)
|
||||||
|
|
||||||
builder = Balance::ChartSeriesBuilder.new(
|
builder = Balance::ChartSeriesBuilder.new(
|
||||||
account_ids: [ account.id ],
|
account_ids: [ account.id ],
|
||||||
|
@ -68,13 +70,13 @@ class Balance::ChartSeriesBuilderTest < ActiveSupport::TestCase
|
||||||
|
|
||||||
Balance.destroy_all
|
Balance.destroy_all
|
||||||
|
|
||||||
asset_account.balances.create!(date: 3.days.ago.to_date, balance: 500, currency: "USD")
|
create_balance(account: asset_account, date: 3.days.ago.to_date, balance: 500)
|
||||||
asset_account.balances.create!(date: 1.day.ago.to_date, balance: 1000, currency: "USD")
|
create_balance(account: asset_account, date: 1.day.ago.to_date, balance: 1000)
|
||||||
asset_account.balances.create!(date: Date.current, balance: 1000, currency: "USD")
|
create_balance(account: asset_account, date: Date.current, balance: 1000)
|
||||||
|
|
||||||
liability_account.balances.create!(date: 3.days.ago.to_date, balance: 200, currency: "USD")
|
create_balance(account: liability_account, date: 3.days.ago.to_date, balance: 200)
|
||||||
liability_account.balances.create!(date: 2.days.ago.to_date, balance: 200, currency: "USD")
|
create_balance(account: liability_account, date: 2.days.ago.to_date, balance: 200)
|
||||||
liability_account.balances.create!(date: Date.current, balance: 100, currency: "USD")
|
create_balance(account: liability_account, date: Date.current, balance: 100)
|
||||||
|
|
||||||
builder = Balance::ChartSeriesBuilder.new(
|
builder = Balance::ChartSeriesBuilder.new(
|
||||||
account_ids: [ asset_account.id, liability_account.id ],
|
account_ids: [ asset_account.id, liability_account.id ],
|
||||||
|
@ -98,8 +100,8 @@ class Balance::ChartSeriesBuilderTest < ActiveSupport::TestCase
|
||||||
account = accounts(:credit_card)
|
account = accounts(:credit_card)
|
||||||
account.balances.destroy_all
|
account.balances.destroy_all
|
||||||
|
|
||||||
account.balances.create!(date: 1.day.ago.to_date, balance: 1000, currency: "USD")
|
create_balance(account: account, date: 1.day.ago.to_date, balance: 1000)
|
||||||
account.balances.create!(date: Date.current, balance: 500, currency: "USD")
|
create_balance(account: account, date: Date.current, balance: 500)
|
||||||
|
|
||||||
builder = Balance::ChartSeriesBuilder.new(
|
builder = Balance::ChartSeriesBuilder.new(
|
||||||
account_ids: [ account.id ],
|
account_ids: [ account.id ],
|
||||||
|
|
|
@ -2,6 +2,7 @@ require "test_helper"
|
||||||
|
|
||||||
class Balance::MaterializerTest < ActiveSupport::TestCase
|
class Balance::MaterializerTest < ActiveSupport::TestCase
|
||||||
include EntriesTestHelper
|
include EntriesTestHelper
|
||||||
|
include BalanceTestHelper
|
||||||
|
|
||||||
setup do
|
setup do
|
||||||
@account = families(:empty).accounts.create!(
|
@account = families(:empty).accounts.create!(
|
||||||
|
@ -16,8 +17,6 @@ class Balance::MaterializerTest < ActiveSupport::TestCase
|
||||||
test "syncs balances" do
|
test "syncs balances" do
|
||||||
Holding::Materializer.any_instance.expects(:materialize_holdings).returns([]).once
|
Holding::Materializer.any_instance.expects(:materialize_holdings).returns([]).once
|
||||||
|
|
||||||
@account.expects(:start_date).returns(2.days.ago.to_date)
|
|
||||||
|
|
||||||
expected_balances = [
|
expected_balances = [
|
||||||
Balance.new(
|
Balance.new(
|
||||||
date: 1.day.ago.to_date,
|
date: 1.day.ago.to_date,
|
||||||
|
@ -62,29 +61,13 @@ class Balance::MaterializerTest < ActiveSupport::TestCase
|
||||||
assert_balance_fields_persisted(expected_balances)
|
assert_balance_fields_persisted(expected_balances)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "purges stale balances and holdings" do
|
test "purges stale balances outside calculated range" do
|
||||||
# Balance before start date is stale
|
# Create existing balances that will be stale
|
||||||
@account.expects(:start_date).returns(2.days.ago.to_date).twice
|
stale_old = create_balance(account: @account, date: 5.days.ago.to_date, balance: 5000)
|
||||||
|
stale_future = create_balance(account: @account, date: 2.days.from_now.to_date, balance: 15000)
|
||||||
stale_balance = Balance.new(
|
|
||||||
date: 3.days.ago.to_date,
|
|
||||||
balance: 10000,
|
|
||||||
cash_balance: 10000,
|
|
||||||
currency: "USD",
|
|
||||||
start_cash_balance: 0,
|
|
||||||
start_non_cash_balance: 0,
|
|
||||||
cash_inflows: 0,
|
|
||||||
cash_outflows: 0,
|
|
||||||
non_cash_inflows: 0,
|
|
||||||
non_cash_outflows: 0,
|
|
||||||
net_market_flows: 0,
|
|
||||||
cash_adjustments: 10000,
|
|
||||||
non_cash_adjustments: 0,
|
|
||||||
flows_factor: 1
|
|
||||||
)
|
|
||||||
|
|
||||||
|
# Calculator will return balances for only these dates
|
||||||
expected_balances = [
|
expected_balances = [
|
||||||
stale_balance,
|
|
||||||
Balance.new(
|
Balance.new(
|
||||||
date: 2.days.ago.to_date,
|
date: 2.days.ago.to_date,
|
||||||
balance: 10000,
|
balance: 10000,
|
||||||
|
@ -136,13 +119,19 @@ class Balance::MaterializerTest < ActiveSupport::TestCase
|
||||||
]
|
]
|
||||||
|
|
||||||
Balance::ForwardCalculator.any_instance.expects(:calculate).returns(expected_balances)
|
Balance::ForwardCalculator.any_instance.expects(:calculate).returns(expected_balances)
|
||||||
|
Holding::Materializer.any_instance.expects(:materialize_holdings).returns([]).once
|
||||||
|
|
||||||
assert_difference "@account.balances.count", 3 do
|
# Should end up with 3 balances (stale ones deleted, new ones created)
|
||||||
|
assert_difference "@account.balances.count", 1 do
|
||||||
Balance::Materializer.new(@account, strategy: :forward).materialize_balances
|
Balance::Materializer.new(@account, strategy: :forward).materialize_balances
|
||||||
end
|
end
|
||||||
|
|
||||||
# Only non-stale balances should be persisted and checked
|
# Verify stale balances were deleted
|
||||||
assert_balance_fields_persisted(expected_balances.reject { |b| b.date < 2.days.ago.to_date })
|
assert_nil @account.balances.find_by(id: stale_old.id)
|
||||||
|
assert_nil @account.balances.find_by(id: stale_future.id)
|
||||||
|
|
||||||
|
# Verify expected balances were persisted
|
||||||
|
assert_balance_fields_persisted(expected_balances)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
72
test/support/balance_test_helper.rb
Normal file
72
test/support/balance_test_helper.rb
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
module BalanceTestHelper
|
||||||
|
def create_balance(account:, date:, balance:, cash_balance: nil, **attributes)
|
||||||
|
# If cash_balance is not provided, default to entire balance being cash
|
||||||
|
cash_balance ||= balance
|
||||||
|
|
||||||
|
# Calculate non-cash balance
|
||||||
|
non_cash_balance = balance - cash_balance
|
||||||
|
|
||||||
|
# Set default component values that will generate the desired end_balance
|
||||||
|
# flows_factor should be 1 for assets, -1 for liabilities
|
||||||
|
flows_factor = account.classification == "liability" ? -1 : 1
|
||||||
|
|
||||||
|
defaults = {
|
||||||
|
date: date,
|
||||||
|
balance: balance,
|
||||||
|
cash_balance: cash_balance,
|
||||||
|
currency: account.currency,
|
||||||
|
start_cash_balance: cash_balance,
|
||||||
|
start_non_cash_balance: non_cash_balance,
|
||||||
|
cash_inflows: 0,
|
||||||
|
cash_outflows: 0,
|
||||||
|
non_cash_inflows: 0,
|
||||||
|
non_cash_outflows: 0,
|
||||||
|
net_market_flows: 0,
|
||||||
|
cash_adjustments: 0,
|
||||||
|
non_cash_adjustments: 0,
|
||||||
|
flows_factor: flows_factor
|
||||||
|
}
|
||||||
|
|
||||||
|
account.balances.create!(defaults.merge(attributes))
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_balance_with_flows(account:, date:, start_balance:, end_balance:,
|
||||||
|
cash_portion: 1.0, cash_flow: 0, non_cash_flow: 0,
|
||||||
|
market_flow: 0, **attributes)
|
||||||
|
# Calculate cash and non-cash portions
|
||||||
|
start_cash = start_balance * cash_portion
|
||||||
|
start_non_cash = start_balance * (1 - cash_portion)
|
||||||
|
|
||||||
|
# Calculate adjustments needed to reach end_balance
|
||||||
|
expected_end_cash = start_cash + cash_flow
|
||||||
|
expected_end_non_cash = start_non_cash + non_cash_flow + market_flow
|
||||||
|
expected_total = expected_end_cash + expected_end_non_cash
|
||||||
|
|
||||||
|
# Calculate adjustments if end_balance doesn't match expected
|
||||||
|
total_adjustment = end_balance - expected_total
|
||||||
|
cash_adjustment = cash_portion * total_adjustment
|
||||||
|
non_cash_adjustment = (1 - cash_portion) * total_adjustment
|
||||||
|
|
||||||
|
# flows_factor should be 1 for assets, -1 for liabilities
|
||||||
|
flows_factor = account.classification == "liability" ? -1 : 1
|
||||||
|
|
||||||
|
defaults = {
|
||||||
|
date: date,
|
||||||
|
balance: end_balance,
|
||||||
|
cash_balance: expected_end_cash + cash_adjustment,
|
||||||
|
currency: account.currency,
|
||||||
|
start_cash_balance: start_cash,
|
||||||
|
start_non_cash_balance: start_non_cash,
|
||||||
|
cash_inflows: cash_flow > 0 ? cash_flow : 0,
|
||||||
|
cash_outflows: cash_flow < 0 ? -cash_flow : 0,
|
||||||
|
non_cash_inflows: non_cash_flow > 0 ? non_cash_flow : 0,
|
||||||
|
non_cash_outflows: non_cash_flow < 0 ? -non_cash_flow : 0,
|
||||||
|
net_market_flows: market_flow,
|
||||||
|
cash_adjustments: cash_adjustment,
|
||||||
|
non_cash_adjustments: non_cash_adjustment,
|
||||||
|
flows_factor: flows_factor
|
||||||
|
}
|
||||||
|
|
||||||
|
account.balances.create!(defaults.merge(attributes))
|
||||||
|
end
|
||||||
|
end
|
Loading…
Add table
Add a link
Reference in a new issue