mirror of
https://github.com/we-promise/sure.git
synced 2026-04-07 22:34:47 +00:00
* Add tax-advantaged account exclusions and investment data warnings * Address PR review feedback: translations + cache key stability - Add proper translations for provider warnings in 8 locales (de, es, nb, pt-BR, ro, tr, zh-CN, zh-TW) - Fix cache key stability: use SHA256.hexdigest instead of Array#hash (randomized per process) --------- Co-authored-by: luckyPipewrench <luckypipewrench@proton.me>
561 lines
25 KiB
Ruby
561 lines
25 KiB
Ruby
require "test_helper"
|
|
|
|
class IncomeStatementTest < ActiveSupport::TestCase
|
|
include EntriesTestHelper
|
|
|
|
setup do
|
|
@family = families(:empty)
|
|
|
|
@income_category = @family.categories.create! name: "Income", classification: "income"
|
|
@food_category = @family.categories.create! name: "Food", classification: "expense"
|
|
@groceries_category = @family.categories.create! name: "Groceries", classification: "expense", parent: @food_category
|
|
|
|
@checking_account = @family.accounts.create! name: "Checking", currency: @family.currency, balance: 5000, accountable: Depository.new
|
|
@credit_card_account = @family.accounts.create! name: "Credit Card", currency: @family.currency, balance: 1000, accountable: CreditCard.new
|
|
@loan_account = @family.accounts.create! name: "Mortgage", currency: @family.currency, balance: 50000, accountable: Loan.new
|
|
|
|
create_transaction(account: @checking_account, amount: -1000, category: @income_category)
|
|
create_transaction(account: @checking_account, amount: 200, category: @groceries_category)
|
|
create_transaction(account: @credit_card_account, amount: 300, category: @groceries_category)
|
|
create_transaction(account: @credit_card_account, amount: 400, category: @groceries_category)
|
|
end
|
|
|
|
test "calculates totals for transactions" do
|
|
income_statement = IncomeStatement.new(@family)
|
|
totals = income_statement.totals(date_range: Period.last_30_days.date_range)
|
|
assert_equal Money.new(1000, @family.currency), totals.income_money
|
|
assert_equal Money.new(200 + 300 + 400, @family.currency), totals.expense_money
|
|
assert_equal 4, totals.transactions_count
|
|
end
|
|
|
|
test "calculates expenses for a period" do
|
|
income_statement = IncomeStatement.new(@family)
|
|
expense_totals = income_statement.expense_totals(period: Period.last_30_days)
|
|
|
|
expected_total_expense = 200 + 300 + 400
|
|
|
|
assert_equal expected_total_expense, expense_totals.total
|
|
assert_equal expected_total_expense, expense_totals.category_totals.find { |ct| ct.category.id == @groceries_category.id }.total
|
|
assert_equal expected_total_expense, expense_totals.category_totals.find { |ct| ct.category.id == @food_category.id }.total
|
|
end
|
|
|
|
test "calculates income for a period" do
|
|
income_statement = IncomeStatement.new(@family)
|
|
income_totals = income_statement.income_totals(period: Period.last_30_days)
|
|
|
|
expected_total_income = 1000
|
|
|
|
assert_equal expected_total_income, income_totals.total
|
|
assert_equal expected_total_income, income_totals.category_totals.find { |ct| ct.category.id == @income_category.id }.total
|
|
end
|
|
|
|
test "calculates median expense" do
|
|
income_statement = IncomeStatement.new(@family)
|
|
assert_equal 200 + 300 + 400, income_statement.expense_totals(period: Period.last_30_days).total
|
|
end
|
|
|
|
test "calculates median income" do
|
|
income_statement = IncomeStatement.new(@family)
|
|
assert_equal 1000, income_statement.income_totals(period: Period.last_30_days).total
|
|
end
|
|
|
|
# NEW TESTS: Statistical Methods
|
|
test "calculates median expense correctly with known dataset" do
|
|
# Clear existing transactions by deleting entries
|
|
Entry.joins(:account).where(accounts: { family_id: @family.id }).destroy_all
|
|
|
|
# Create expenses: 100, 200, 300, 400, 500 (median should be 300)
|
|
create_transaction(account: @checking_account, amount: 100, category: @groceries_category)
|
|
create_transaction(account: @checking_account, amount: 200, category: @groceries_category)
|
|
create_transaction(account: @checking_account, amount: 300, category: @groceries_category)
|
|
create_transaction(account: @checking_account, amount: 400, category: @groceries_category)
|
|
create_transaction(account: @checking_account, amount: 500, category: @groceries_category)
|
|
|
|
income_statement = IncomeStatement.new(@family)
|
|
# CORRECT BUSINESS LOGIC: Calculates median of time-period totals for budget planning
|
|
# All transactions in same month = monthly total of 1500, so median = 1500.0
|
|
assert_equal 1500.0, income_statement.median_expense(interval: "month")
|
|
end
|
|
|
|
test "calculates median income correctly with known dataset" do
|
|
# Clear existing transactions by deleting entries
|
|
Entry.joins(:account).where(accounts: { family_id: @family.id }).destroy_all
|
|
|
|
# Create income: -200, -300, -400, -500, -600 (median should be -400, displayed as 400)
|
|
create_transaction(account: @checking_account, amount: -200, category: @income_category)
|
|
create_transaction(account: @checking_account, amount: -300, category: @income_category)
|
|
create_transaction(account: @checking_account, amount: -400, category: @income_category)
|
|
create_transaction(account: @checking_account, amount: -500, category: @income_category)
|
|
create_transaction(account: @checking_account, amount: -600, category: @income_category)
|
|
|
|
income_statement = IncomeStatement.new(@family)
|
|
# CORRECT BUSINESS LOGIC: Calculates median of time-period totals for budget planning
|
|
# All transactions in same month = monthly total of -2000, so median = 2000.0
|
|
assert_equal 2000.0, income_statement.median_income(interval: "month")
|
|
end
|
|
|
|
test "calculates average expense correctly with known dataset" do
|
|
# Clear existing transactions by deleting entries
|
|
Entry.joins(:account).where(accounts: { family_id: @family.id }).destroy_all
|
|
|
|
# Create expenses: 100, 200, 300 (average should be 200)
|
|
create_transaction(account: @checking_account, amount: 100, category: @groceries_category)
|
|
create_transaction(account: @checking_account, amount: 200, category: @groceries_category)
|
|
create_transaction(account: @checking_account, amount: 300, category: @groceries_category)
|
|
|
|
income_statement = IncomeStatement.new(@family)
|
|
# CORRECT BUSINESS LOGIC: Calculates average of time-period totals for budget planning
|
|
# All transactions in same month = monthly total of 600, so average = 600.0
|
|
assert_equal 600.0, income_statement.avg_expense(interval: "month")
|
|
end
|
|
|
|
test "calculates category-specific median expense" do
|
|
# Clear existing transactions by deleting entries
|
|
Entry.joins(:account).where(accounts: { family_id: @family.id }).destroy_all
|
|
|
|
# Create different amounts for groceries vs other food
|
|
other_food_category = @family.categories.create! name: "Restaurants", classification: "expense", parent: @food_category
|
|
|
|
# Groceries: 100, 300, 500 (median = 300)
|
|
create_transaction(account: @checking_account, amount: 100, category: @groceries_category)
|
|
create_transaction(account: @checking_account, amount: 300, category: @groceries_category)
|
|
create_transaction(account: @checking_account, amount: 500, category: @groceries_category)
|
|
|
|
# Restaurants: 50, 150 (median = 100)
|
|
create_transaction(account: @checking_account, amount: 50, category: other_food_category)
|
|
create_transaction(account: @checking_account, amount: 150, category: other_food_category)
|
|
|
|
income_statement = IncomeStatement.new(@family)
|
|
# CORRECT BUSINESS LOGIC: Calculates median of time-period totals for budget planning
|
|
# All groceries in same month = monthly total of 900, so median = 900.0
|
|
assert_equal 900.0, income_statement.median_expense(interval: "month", category: @groceries_category)
|
|
# For restaurants: monthly total = 200, so median = 200.0
|
|
restaurants_median = income_statement.median_expense(interval: "month", category: other_food_category)
|
|
assert_equal 200.0, restaurants_median
|
|
end
|
|
|
|
test "calculates category-specific average expense" do
|
|
# Clear existing transactions by deleting entries
|
|
Entry.joins(:account).where(accounts: { family_id: @family.id }).destroy_all
|
|
|
|
# Create different amounts for groceries
|
|
# Groceries: 100, 200, 300 (average = 200)
|
|
create_transaction(account: @checking_account, amount: 100, category: @groceries_category)
|
|
create_transaction(account: @checking_account, amount: 200, category: @groceries_category)
|
|
create_transaction(account: @checking_account, amount: 300, category: @groceries_category)
|
|
|
|
income_statement = IncomeStatement.new(@family)
|
|
# CORRECT BUSINESS LOGIC: Calculates average of time-period totals for budget planning
|
|
# All transactions in same month = monthly total of 600, so average = 600.0
|
|
assert_equal 600.0, income_statement.avg_expense(interval: "month", category: @groceries_category)
|
|
end
|
|
|
|
# NEW TESTS: Transfer and Kind Filtering
|
|
# NOTE: These tests now pass because kind filtering is working after the refactoring!
|
|
test "excludes regular transfers from income statement calculations" do
|
|
# Create a regular transfer between accounts
|
|
outflow_transaction = create_transaction(account: @checking_account, amount: 500, kind: "funds_movement")
|
|
inflow_transaction = create_transaction(account: @credit_card_account, amount: -500, kind: "funds_movement")
|
|
|
|
income_statement = IncomeStatement.new(@family)
|
|
totals = income_statement.totals(date_range: Period.last_30_days.date_range)
|
|
|
|
# NOW WORKING: Excludes transfers correctly after refactoring
|
|
assert_equal 4, totals.transactions_count # Only original 4 transactions
|
|
assert_equal Money.new(1000, @family.currency), totals.income_money
|
|
assert_equal Money.new(900, @family.currency), totals.expense_money
|
|
end
|
|
|
|
test "includes loan payments as expenses in income statement" do
|
|
# Create a loan payment transaction
|
|
loan_payment = create_transaction(account: @checking_account, amount: 1000, category: nil, kind: "loan_payment")
|
|
|
|
income_statement = IncomeStatement.new(@family)
|
|
totals = income_statement.totals(date_range: Period.last_30_days.date_range)
|
|
|
|
# CONTINUES TO WORK: Includes loan payments as expenses (loan_payment not in exclusion list)
|
|
assert_equal 5, totals.transactions_count
|
|
assert_equal Money.new(1000, @family.currency), totals.income_money
|
|
assert_equal Money.new(1900, @family.currency), totals.expense_money # 900 + 1000
|
|
end
|
|
|
|
test "excludes one-time transactions from income statement calculations" do
|
|
# Create a one-time transaction
|
|
one_time_transaction = create_transaction(account: @checking_account, amount: 250, category: @groceries_category, kind: "one_time")
|
|
|
|
income_statement = IncomeStatement.new(@family)
|
|
totals = income_statement.totals(date_range: Period.last_30_days.date_range)
|
|
|
|
# NOW WORKING: Excludes one-time transactions correctly after refactoring
|
|
assert_equal 4, totals.transactions_count # Only original 4 transactions
|
|
assert_equal Money.new(1000, @family.currency), totals.income_money
|
|
assert_equal Money.new(900, @family.currency), totals.expense_money
|
|
end
|
|
|
|
test "excludes payment transactions from income statement calculations" do
|
|
# Create a payment transaction (credit card payment)
|
|
payment_transaction = create_transaction(account: @checking_account, amount: 300, category: nil, kind: "cc_payment")
|
|
|
|
income_statement = IncomeStatement.new(@family)
|
|
totals = income_statement.totals(date_range: Period.last_30_days.date_range)
|
|
|
|
# NOW WORKING: Excludes payment transactions correctly after refactoring
|
|
assert_equal 4, totals.transactions_count # Only original 4 transactions
|
|
assert_equal Money.new(1000, @family.currency), totals.income_money
|
|
assert_equal Money.new(900, @family.currency), totals.expense_money
|
|
end
|
|
|
|
test "excludes excluded transactions from income statement calculations" do
|
|
# Create an excluded transaction
|
|
excluded_transaction_entry = create_transaction(account: @checking_account, amount: 250, category: @groceries_category)
|
|
excluded_transaction_entry.update!(excluded: true)
|
|
|
|
income_statement = IncomeStatement.new(@family)
|
|
totals = income_statement.totals(date_range: Period.last_30_days.date_range)
|
|
|
|
# Should exclude excluded transactions
|
|
assert_equal 4, totals.transactions_count # Only original 4 transactions
|
|
assert_equal Money.new(1000, @family.currency), totals.income_money
|
|
assert_equal Money.new(900, @family.currency), totals.expense_money
|
|
end
|
|
|
|
# NEW TESTS: Interval-Based Calculations
|
|
test "different intervals return different statistical results with multi-period data" do
|
|
# Clear existing transactions
|
|
Entry.joins(:account).where(accounts: { family_id: @family.id }).destroy_all
|
|
|
|
# Create transactions across multiple weeks to test interval behavior
|
|
# Week 1: 100, 200 (total: 300, median: 150)
|
|
create_transaction(account: @checking_account, amount: 100, category: @groceries_category, date: 3.weeks.ago)
|
|
create_transaction(account: @checking_account, amount: 200, category: @groceries_category, date: 3.weeks.ago + 1.day)
|
|
|
|
# Week 2: 400, 600 (total: 1000, median: 500)
|
|
create_transaction(account: @checking_account, amount: 400, category: @groceries_category, date: 2.weeks.ago)
|
|
create_transaction(account: @checking_account, amount: 600, category: @groceries_category, date: 2.weeks.ago + 1.day)
|
|
|
|
# Week 3: 800 (total: 800, median: 800)
|
|
create_transaction(account: @checking_account, amount: 800, category: @groceries_category, date: 1.week.ago)
|
|
|
|
income_statement = IncomeStatement.new(@family)
|
|
|
|
month_median = income_statement.median_expense(interval: "month")
|
|
week_median = income_statement.median_expense(interval: "week")
|
|
|
|
# CRITICAL TEST: Different intervals should return different results
|
|
# Month interval: median of monthly totals (if all in same month) vs individual transactions
|
|
# Week interval: median of weekly totals [300, 1000, 800] = 800 vs individual transactions [100,200,400,600,800] = 400
|
|
refute_equal month_median, week_median, "Different intervals should return different statistical results when data spans multiple time periods"
|
|
|
|
# Both should still be numeric
|
|
assert month_median.is_a?(Numeric)
|
|
assert week_median.is_a?(Numeric)
|
|
assert month_median > 0
|
|
assert week_median > 0
|
|
end
|
|
|
|
# NEW TESTS: Edge Cases
|
|
test "handles empty dataset gracefully" do
|
|
# Create a truly empty family
|
|
empty_family = Family.create!(name: "Empty Test Family", currency: "USD")
|
|
income_statement = IncomeStatement.new(empty_family)
|
|
|
|
# Should return 0 for statistical measures
|
|
assert_equal 0, income_statement.median_expense(interval: "month")
|
|
assert_equal 0, income_statement.median_income(interval: "month")
|
|
assert_equal 0, income_statement.avg_expense(interval: "month")
|
|
end
|
|
|
|
test "handles category not found gracefully" do
|
|
nonexistent_category = Category.new(id: 99999, name: "Nonexistent")
|
|
|
|
income_statement = IncomeStatement.new(@family)
|
|
|
|
assert_equal 0, income_statement.median_expense(interval: "month", category: nonexistent_category)
|
|
assert_equal 0, income_statement.avg_expense(interval: "month", category: nonexistent_category)
|
|
end
|
|
|
|
test "handles transactions without categories" do
|
|
# Create transaction without category
|
|
create_transaction(account: @checking_account, amount: 150, category: nil)
|
|
|
|
income_statement = IncomeStatement.new(@family)
|
|
totals = income_statement.totals(date_range: Period.last_30_days.date_range)
|
|
|
|
# Should still include uncategorized transaction in totals
|
|
assert_equal 5, totals.transactions_count
|
|
assert_equal Money.new(1050, @family.currency), totals.expense_money # 900 + 150
|
|
end
|
|
|
|
test "includes investment_contribution transactions as expenses in income statement" do
|
|
# Create a transfer to investment account (marked as investment_contribution)
|
|
investment_contribution = create_transaction(
|
|
account: @checking_account,
|
|
amount: 1000,
|
|
category: nil,
|
|
kind: "investment_contribution"
|
|
)
|
|
|
|
income_statement = IncomeStatement.new(@family)
|
|
totals = income_statement.totals(date_range: Period.last_30_days.date_range)
|
|
|
|
# investment_contribution should be included as an expense (visible in cashflow)
|
|
assert_equal 5, totals.transactions_count # Original 4 + investment_contribution
|
|
assert_equal Money.new(1000, @family.currency), totals.income_money
|
|
assert_equal Money.new(1900, @family.currency), totals.expense_money # 900 + 1000 investment
|
|
end
|
|
|
|
test "includes provider-imported investment_contribution inflows as expenses" do
|
|
# Simulates a 401k contribution that was auto-deducted from payroll
|
|
# Provider imports this as an inflow to the investment account (negative amount)
|
|
# but it should still appear as an expense in cashflow
|
|
|
|
investment_account = @family.accounts.create!(
|
|
name: "401k",
|
|
currency: @family.currency,
|
|
balance: 10000,
|
|
accountable: Investment.new
|
|
)
|
|
|
|
# Provider-imported contribution shows as inflow (negative amount) to the investment account
|
|
# kind is investment_contribution, which should be treated as expense regardless of sign
|
|
provider_contribution = create_transaction(
|
|
account: investment_account,
|
|
amount: -500, # Negative = inflow to account
|
|
category: nil,
|
|
kind: "investment_contribution"
|
|
)
|
|
|
|
income_statement = IncomeStatement.new(@family)
|
|
totals = income_statement.totals(date_range: Period.last_30_days.date_range)
|
|
|
|
# The provider-imported contribution should appear as an expense
|
|
assert_equal 5, totals.transactions_count # Original 4 + provider contribution
|
|
assert_equal Money.new(1000, @family.currency), totals.income_money
|
|
assert_equal Money.new(1400, @family.currency), totals.expense_money # 900 + 500 (abs of -500)
|
|
end
|
|
|
|
# Tax-Advantaged Account Exclusion Tests
|
|
test "excludes transactions from tax-advantaged Roth IRA accounts" do
|
|
# Create a Roth IRA (tax-exempt) investment account
|
|
roth_ira = @family.accounts.create!(
|
|
name: "Roth IRA",
|
|
currency: @family.currency,
|
|
balance: 50000,
|
|
accountable: Investment.new(subtype: "roth_ira")
|
|
)
|
|
|
|
# Create a dividend transaction in the Roth IRA
|
|
# This should NOT appear in budget totals
|
|
create_transaction(account: roth_ira, amount: -200, category: @income_category)
|
|
|
|
income_statement = IncomeStatement.new(@family)
|
|
totals = income_statement.totals(date_range: Period.last_30_days.date_range)
|
|
|
|
# The Roth IRA dividend should be excluded
|
|
assert_equal 4, totals.transactions_count # Only original 4 transactions
|
|
assert_equal Money.new(1000, @family.currency), totals.income_money
|
|
assert_equal Money.new(900, @family.currency), totals.expense_money
|
|
end
|
|
|
|
test "excludes transactions from tax-deferred 401k accounts" do
|
|
# Create a 401k (tax-deferred) investment account
|
|
account_401k = @family.accounts.create!(
|
|
name: "Company 401k",
|
|
currency: @family.currency,
|
|
balance: 100000,
|
|
accountable: Investment.new(subtype: "401k")
|
|
)
|
|
|
|
# Create a dividend transaction in the 401k
|
|
# This should NOT appear in budget totals
|
|
create_transaction(account: account_401k, amount: -500, category: @income_category)
|
|
|
|
income_statement = IncomeStatement.new(@family)
|
|
totals = income_statement.totals(date_range: Period.last_30_days.date_range)
|
|
|
|
# The 401k dividend should be excluded
|
|
assert_equal 4, totals.transactions_count
|
|
assert_equal Money.new(1000, @family.currency), totals.income_money
|
|
assert_equal Money.new(900, @family.currency), totals.expense_money
|
|
end
|
|
|
|
test "includes transactions from taxable brokerage accounts" do
|
|
# Create a taxable brokerage account
|
|
brokerage = @family.accounts.create!(
|
|
name: "Brokerage",
|
|
currency: @family.currency,
|
|
balance: 25000,
|
|
accountable: Investment.new(subtype: "brokerage")
|
|
)
|
|
|
|
# Create a dividend transaction in the taxable account
|
|
# This SHOULD appear in budget totals
|
|
create_transaction(account: brokerage, amount: -300, category: @income_category)
|
|
|
|
income_statement = IncomeStatement.new(@family)
|
|
totals = income_statement.totals(date_range: Period.last_30_days.date_range)
|
|
|
|
# The brokerage dividend SHOULD be included
|
|
assert_equal 5, totals.transactions_count
|
|
assert_equal Money.new(1300, @family.currency), totals.income_money # 1000 + 300
|
|
assert_equal Money.new(900, @family.currency), totals.expense_money
|
|
end
|
|
|
|
test "includes transactions from default taxable crypto accounts" do
|
|
# Create a crypto account (default taxable)
|
|
crypto_account = @family.accounts.create!(
|
|
name: "Coinbase",
|
|
currency: @family.currency,
|
|
balance: 5000,
|
|
accountable: Crypto.new
|
|
)
|
|
|
|
# Create a transaction in the crypto account
|
|
create_transaction(account: crypto_account, amount: 100, category: @groceries_category)
|
|
|
|
income_statement = IncomeStatement.new(@family)
|
|
totals = income_statement.totals(date_range: Period.last_30_days.date_range)
|
|
|
|
# Crypto transaction SHOULD be included (default is taxable)
|
|
assert_equal 5, totals.transactions_count
|
|
assert_equal Money.new(1000, @family.currency), totals.expense_money # 900 + 100
|
|
end
|
|
|
|
test "excludes transactions from tax-deferred crypto accounts" do
|
|
# Create a crypto account in a tax-deferred retirement account
|
|
crypto_in_ira = @family.accounts.create!(
|
|
name: "Crypto IRA",
|
|
currency: @family.currency,
|
|
balance: 10000,
|
|
accountable: Crypto.new(tax_treatment: "tax_deferred")
|
|
)
|
|
|
|
# Create a transaction in the tax-deferred crypto account
|
|
create_transaction(account: crypto_in_ira, amount: 250, category: @groceries_category)
|
|
|
|
income_statement = IncomeStatement.new(@family)
|
|
totals = income_statement.totals(date_range: Period.last_30_days.date_range)
|
|
|
|
# The tax-deferred crypto transaction should be excluded
|
|
assert_equal 4, totals.transactions_count
|
|
assert_equal Money.new(900, @family.currency), totals.expense_money
|
|
end
|
|
|
|
test "family.tax_advantaged_account_ids returns correct accounts" do
|
|
# Create various accounts
|
|
roth_ira = @family.accounts.create!(
|
|
name: "Roth IRA",
|
|
currency: @family.currency,
|
|
balance: 50000,
|
|
accountable: Investment.new(subtype: "roth_ira")
|
|
)
|
|
|
|
traditional_ira = @family.accounts.create!(
|
|
name: "Traditional IRA",
|
|
currency: @family.currency,
|
|
balance: 30000,
|
|
accountable: Investment.new(subtype: "ira")
|
|
)
|
|
|
|
brokerage = @family.accounts.create!(
|
|
name: "Brokerage",
|
|
currency: @family.currency,
|
|
balance: 25000,
|
|
accountable: Investment.new(subtype: "brokerage")
|
|
)
|
|
|
|
crypto_taxable = @family.accounts.create!(
|
|
name: "Crypto Taxable",
|
|
currency: @family.currency,
|
|
balance: 5000,
|
|
accountable: Crypto.new
|
|
)
|
|
|
|
crypto_deferred = @family.accounts.create!(
|
|
name: "Crypto IRA",
|
|
currency: @family.currency,
|
|
balance: 10000,
|
|
accountable: Crypto.new(tax_treatment: "tax_deferred")
|
|
)
|
|
|
|
# Clear the memoized value
|
|
@family.instance_variable_set(:@tax_advantaged_account_ids, nil)
|
|
|
|
tax_advantaged_ids = @family.tax_advantaged_account_ids
|
|
|
|
# Should include Roth IRA, Traditional IRA, and tax-deferred Crypto
|
|
assert_includes tax_advantaged_ids, roth_ira.id
|
|
assert_includes tax_advantaged_ids, traditional_ira.id
|
|
assert_includes tax_advantaged_ids, crypto_deferred.id
|
|
|
|
# Should NOT include taxable accounts
|
|
refute_includes tax_advantaged_ids, brokerage.id
|
|
refute_includes tax_advantaged_ids, crypto_taxable.id
|
|
|
|
# Should NOT include non-investment accounts
|
|
refute_includes tax_advantaged_ids, @checking_account.id
|
|
refute_includes tax_advantaged_ids, @credit_card_account.id
|
|
end
|
|
|
|
test "returns zero totals when family has only tax-advantaged accounts" do
|
|
# Create a fresh family with ONLY tax-advantaged accounts
|
|
family_only_retirement = Family.create!(
|
|
name: "Retirement Only Family",
|
|
currency: "USD",
|
|
locale: "en",
|
|
date_format: "%Y-%m-%d"
|
|
)
|
|
|
|
# Create a 401k account (tax-deferred)
|
|
retirement_account = family_only_retirement.accounts.create!(
|
|
name: "401k",
|
|
currency: "USD",
|
|
balance: 100000,
|
|
accountable: Investment.new(subtype: "401k")
|
|
)
|
|
|
|
# Create a Roth IRA account (tax-exempt)
|
|
roth_account = family_only_retirement.accounts.create!(
|
|
name: "Roth IRA",
|
|
currency: "USD",
|
|
balance: 50000,
|
|
accountable: Investment.new(subtype: "roth_ira")
|
|
)
|
|
|
|
# Add transactions to these accounts (would normally be contributions/trades)
|
|
# Using standard kind to simulate transactions that would normally appear
|
|
Entry.create!(
|
|
account: retirement_account,
|
|
name: "401k Contribution",
|
|
date: 5.days.ago,
|
|
amount: 500,
|
|
currency: "USD",
|
|
entryable: Transaction.new(kind: "standard")
|
|
)
|
|
|
|
Entry.create!(
|
|
account: roth_account,
|
|
name: "Roth IRA Contribution",
|
|
date: 3.days.ago,
|
|
amount: 200,
|
|
currency: "USD",
|
|
entryable: Transaction.new(kind: "standard")
|
|
)
|
|
|
|
# Verify the accounts are correctly identified as tax-advantaged
|
|
tax_advantaged_ids = family_only_retirement.tax_advantaged_account_ids
|
|
assert_equal 2, tax_advantaged_ids.count
|
|
assert_includes tax_advantaged_ids, retirement_account.id
|
|
assert_includes tax_advantaged_ids, roth_account.id
|
|
|
|
# Get income statement totals
|
|
income_statement = IncomeStatement.new(family_only_retirement)
|
|
totals = income_statement.totals(date_range: Period.last_30_days.date_range)
|
|
|
|
# All transactions should be excluded, resulting in zero totals
|
|
assert_equal 0, totals.transactions_count
|
|
assert_equal Money.new(0, "USD"), totals.income_money
|
|
assert_equal Money.new(0, "USD"), totals.expense_money
|
|
end
|
|
end
|