1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-02 20:15:22 +02:00

Account::Sync model and test fixture simplifications (#968)

* Add sync model

* Fresh fixtures for sync tests

* Sync tests overhaul

* Fix entry tests

* Complete remaining model test updates

* Update system tests

* Update demo data task

* Add system tests back to PR checks

* More simplifications, add empty family to fixtures for easier testing
This commit is contained in:
Zach Gollwitzer 2024-07-10 11:22:59 -04:00 committed by GitHub
parent de5a2e55b3
commit c6bdf49f10
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
60 changed files with 929 additions and 1353 deletions

View file

@ -1,101 +0,0 @@
require "test_helper"
require "csv"
class Account::Balance::CalculatorTest < ActiveSupport::TestCase
include FamilySnapshotTestHelper
test "syncs other asset balances" do
expected_balances = get_expected_balances_for(:collectable)
assert_account_balances calculated_balances_for(:collectable), expected_balances
end
test "syncs other liability balances" do
expected_balances = get_expected_balances_for(:iou)
assert_account_balances calculated_balances_for(:iou), expected_balances
end
test "syncs credit balances" do
expected_balances = get_expected_balances_for :credit_card
assert_account_balances calculated_balances_for(:credit_card), expected_balances
end
test "syncs checking account balances" do
expected_balances = get_expected_balances_for(:checking)
assert_account_balances calculated_balances_for(:checking), expected_balances
end
test "syncs foreign checking account balances" do
required_exchange_rates_for_sync = [
1.0834, 1.0845, 1.0819, 1.0872, 1.0788, 1.0743, 1.0755, 1.0774,
1.0778, 1.0783, 1.0773, 1.0709, 1.0729, 1.0773, 1.0778, 1.078,
1.0809, 1.0818, 1.0824, 1.0822, 1.0854, 1.0845, 1.0839, 1.0807,
1.084, 1.0856, 1.0858, 1.0898, 1.095, 1.094, 1.0926, 1.0986
]
required_exchange_rates_for_sync.each_with_index do |exchange_rate, idx|
ExchangeRate.create! date: idx.days.ago.to_date, from_currency: "EUR", to_currency: "USD", rate: exchange_rate
end
# Foreign accounts will generate balances for all currencies
expected_usd_balances = get_expected_balances_for(:eur_checking_usd)
expected_eur_balances = get_expected_balances_for(:eur_checking_eur)
calculated_balances = calculated_balances_for(:eur_checking)
calculated_usd_balances = calculated_balances.select { |b| b[:currency] == "USD" }
calculated_eur_balances = calculated_balances.select { |b| b[:currency] == "EUR" }
assert_account_balances calculated_usd_balances, expected_usd_balances
assert_account_balances calculated_eur_balances, expected_eur_balances
end
test "syncs multi-currency checking account balances" do
required_exchange_rates_for_sync = [
{ from_currency: "EUR", to_currency: "USD", date: 4.days.ago.to_date, rate: 1.0788 },
{ from_currency: "EUR", to_currency: "USD", date: 19.days.ago.to_date, rate: 1.0822 }
]
ExchangeRate.insert_all(required_exchange_rates_for_sync)
expected_balances = get_expected_balances_for(:multi_currency)
assert_account_balances calculated_balances_for(:multi_currency), expected_balances
end
test "syncs savings accounts balances" do
expected_balances = get_expected_balances_for(:savings)
assert_account_balances calculated_balances_for(:savings), expected_balances
end
test "syncs investment account balances" do
expected_balances = get_expected_balances_for(:brokerage)
assert_account_balances calculated_balances_for(:brokerage), expected_balances
end
test "syncs loan account balances" do
expected_balances = get_expected_balances_for(:mortgage_loan)
assert_account_balances calculated_balances_for(:mortgage_loan), expected_balances
end
test "syncs property account balances" do
expected_balances = get_expected_balances_for(:house)
assert_account_balances calculated_balances_for(:house), expected_balances
end
test "syncs vehicle account balances" do
expected_balances = get_expected_balances_for(:car)
assert_account_balances calculated_balances_for(:car), expected_balances
end
private
def assert_account_balances(actual_balances, expected_balances)
assert_equal expected_balances.count, actual_balances.count
actual_balances.each do |ab|
expected_balance = expected_balances.find { |eb| eb[:date] == ab[:date] }
assert_in_delta expected_balance[:balance], ab[:balance], 0.01, "Balance incorrect on date: #{ab[:date]}"
end
end
def calculated_balances_for(account_key)
Account::Balance::Calculator.new(accounts(account_key)).daily_balances
end
end

View file

@ -0,0 +1,133 @@
require "test_helper"
class Account::Balance::SyncerTest < ActiveSupport::TestCase
include Account::EntriesTestHelper
setup do
@account = families(:empty).accounts.create!(name: "Test", balance: 20000, currency: "USD", accountable: Depository.new)
end
test "syncs account with no entries" do
assert_equal 0, @account.balances.count
syncer = Account::Balance::Syncer.new(@account)
syncer.run
assert_equal [ @account.balance ], @account.balances.chronological.map(&:balance)
end
test "syncs account with valuations only" do
create_valuation(account: @account, date: 2.days.ago.to_date, amount: 22000)
syncer = Account::Balance::Syncer.new(@account)
syncer.run
assert_equal [ 22000, 22000, @account.balance ], @account.balances.chronological.map(&:balance)
end
test "syncs account with transactions only" do
create_transaction(account: @account, date: 4.days.ago.to_date, amount: 100)
create_transaction(account: @account, date: 2.days.ago.to_date, amount: -500)
syncer = Account::Balance::Syncer.new(@account)
syncer.run
assert_equal [ 19600, 19500, 19500, 20000, 20000, @account.balance ], @account.balances.chronological.map(&:balance)
end
test "syncs account with valuations and transactions" do
create_valuation(account: @account, date: 5.days.ago.to_date, amount: 20000)
create_transaction(account: @account, date: 3.days.ago.to_date, amount: -500)
create_transaction(account: @account, date: 2.days.ago.to_date, amount: 100)
create_valuation(account: @account, date: 1.day.ago.to_date, amount: 25000)
syncer = Account::Balance::Syncer.new(@account)
syncer.run
assert_equal [ 20000, 20000, 20500, 20400, 25000, @account.balance ], @account.balances.chronological.map(&:balance)
end
test "syncs account with transactions in multiple currencies" do
ExchangeRate.create! date: 1.day.ago.to_date, from_currency: "EUR", to_currency: "USD", rate: 1.2
create_transaction(account: @account, date: 3.days.ago.to_date, amount: 100, currency: "USD")
create_transaction(account: @account, date: 2.days.ago.to_date, amount: 300, currency: "USD")
create_transaction(account: @account, date: 1.day.ago.to_date, amount: 500, currency: "EUR") # €500 * 1.2 = $600
syncer = Account::Balance::Syncer.new(@account)
syncer.run
assert_equal [ 21000, 20900, 20600, 20000, @account.balance ], @account.balances.chronological.map(&:balance)
end
test "converts foreign account balances to family currency" do
@account.update! currency: "EUR"
create_transaction(date: 1.day.ago.to_date, amount: 1000, account: @account, currency: "EUR")
create_exchange_rate(2.days.ago.to_date, from: "EUR", to: "USD", rate: 2)
create_exchange_rate(1.day.ago.to_date, from: "EUR", to: "USD", rate: 2)
create_exchange_rate(Date.current, from: "EUR", to: "USD", rate: 2)
syncer = Account::Balance::Syncer.new(@account)
syncer.run
usd_balances = @account.balances.where(currency: "USD").chronological.map(&:balance)
eur_balances = @account.balances.where(currency: "EUR").chronological.map(&:balance)
assert_equal [ 21000, 20000, @account.balance ], eur_balances # native account balances
assert_equal [ 42000, 40000, @account.balance * 2 ], usd_balances # converted balances at rate of 2:1
end
test "fails with error if exchange rate not available for any entry" do
create_transaction(account: @account, currency: "EUR")
syncer = Account::Balance::Syncer.new(@account)
assert_raises Money::ConversionError do
syncer.run
end
end
# Account is able to calculate balances in its own currency (i.e. can still show a historical graph), but
# doesn't have exchange rates available to convert those calculated balances to the family currency
test "completes with warning if exchange rates not available to convert to family currency" do
@account.update! currency: "EUR"
syncer = Account::Balance::Syncer.new(@account)
syncer.run
assert_equal 1, syncer.warnings.count
end
test "overwrites existing balances and purges stale balances" do
assert_equal 0, @account.balances.size
@account.balances.create! date: Date.current, currency: "USD", balance: 30000 # incorrect balance, will be updated
@account.balances.create! date: 10.years.ago.to_date, currency: "USD", balance: 35000 # Out of range balance, will be deleted
assert_equal 2, @account.balances.size
syncer = Account::Balance::Syncer.new(@account)
syncer.run
assert_equal [ @account.balance ], @account.balances.chronological.map(&:balance)
end
test "partial sync does not affect balances prior to sync start date" do
existing_balance = @account.balances.create! date: 2.days.ago.to_date, currency: "USD", balance: 30000
transaction = create_transaction(account: @account, date: 1.day.ago.to_date, amount: 100, currency: "USD")
syncer = Account::Balance::Syncer.new(@account, start_date: 1.day.ago.to_date)
syncer.run
assert_equal [ existing_balance.balance, existing_balance.balance - transaction.amount, @account.balance ], @account.balances.chronological.map(&:balance)
end
private
def create_exchange_rate(date, from:, to:, rate:)
ExchangeRate.create! date: date, from_currency: from, to_currency: to, rate: rate
end
end

View file

@ -1,26 +1,29 @@
require "test_helper"
class Account::EntryTest < ActiveSupport::TestCase
include Account::EntriesTestHelper
setup do
@entry = account_entries :checking_one
@family = families :dylan_family
@entry = account_entries :transaction
end
test "valuations cannot have more than one entry per day" do
new_entry = Account::Entry.new \
entryable: Account::Valuation.new,
date: @entry.date, # invalid
currency: @entry.currency,
amount: @entry.amount
existing_valuation = account_entries :valuation
assert new_entry.invalid?
new_valuation = Account::Entry.new \
entryable: Account::Valuation.new,
date: existing_valuation.date, # invalid
currency: existing_valuation.currency,
amount: existing_valuation.amount
assert new_valuation.invalid?
end
test "triggers sync with correct start date when transaction is set to prior date" do
prior_date = @entry.date - 1
@entry.update! date: prior_date
@entry.account.expects(:sync_later).with(prior_date)
@entry.account.expects(:sync_later).with(start_date: prior_date)
@entry.sync_account_later
end
@ -28,48 +31,62 @@ class Account::EntryTest < ActiveSupport::TestCase
prior_date = @entry.date
@entry.update! date: @entry.date + 1
@entry.account.expects(:sync_later).with(prior_date)
@entry.account.expects(:sync_later).with(start_date: prior_date)
@entry.sync_account_later
end
test "triggers sync with correct start date when transaction deleted" do
prior_entry = account_entries(:checking_two) # 12 days ago
current_entry = account_entries(:checking_one) # 5 days ago
current_entry = create_transaction(date: 1.day.ago.to_date)
prior_entry = create_transaction(date: current_entry.date - 1.day)
current_entry.destroy!
current_entry.account.expects(:sync_later).with(prior_entry.date)
current_entry.account.expects(:sync_later).with(start_date: prior_entry.date)
current_entry.sync_account_later
end
test "can search entries" do
family = families(:empty)
account = family.accounts.create! name: "Test", balance: 0, accountable: Depository.new
category = family.categories.first
merchant = family.merchants.first
create_transaction(account: account, name: "a transaction")
create_transaction(account: account, name: "ignored")
create_transaction(account: account, name: "third transaction", category: category, merchant: merchant)
params = { search: "a" }
assert_equal 12, Account::Entry.search(params).size
assert_equal 2, family.entries.search(params).size
params = params.merge(categories: [ "Food & Drink" ]) # transaction specific search param
params = params.merge(categories: [ category.name ], merchants: [ merchant.name ]) # transaction specific search param
assert_equal 2, Account::Entry.search(params).size
assert_equal 1, family.entries.search(params).size
end
test "can calculate total spending for a group of transactions" do
assert_equal Money.new(2135), @family.entries.expense_total("USD")
assert_equal Money.new(1010.85, "EUR"), @family.entries.expense_total("EUR")
family = families(:empty)
account = family.accounts.create! name: "Test", balance: 0, accountable: Depository.new
create_transaction(account: account, amount: 100)
create_transaction(account: account, amount: 100)
create_transaction(account: account, amount: -500) # income, will be ignored
assert_equal Money.new(200), family.entries.expense_total("USD")
end
test "can calculate total income for a group of transactions" do
assert_equal -Money.new(2075), @family.entries.income_total("USD")
assert_equal -Money.new(250, "EUR"), @family.entries.income_total("EUR")
family = families(:empty)
account = family.accounts.create! name: "Test", balance: 0, accountable: Depository.new
create_transaction(account: account, amount: -100)
create_transaction(account: account, amount: -100)
create_transaction(account: account, amount: 500) # income, will be ignored
assert_equal Money.new(-200), family.entries.income_total("USD")
end
# See: https://github.com/maybe-finance/maybe/wiki/vision#signage-of-money
test "transactions with negative amounts are inflows, positive amounts are outflows to an account" do
inflow_transaction = account_entries(:checking_four)
outflow_transaction = account_entries(:checking_five)
assert inflow_transaction.amount < 0
assert inflow_transaction.inflow?
assert outflow_transaction.amount >= 0
assert outflow_transaction.outflow?
assert create_transaction(amount: -10).inflow?
assert create_transaction(amount: 10).outflow?
end
end

View file

@ -0,0 +1,36 @@
require "test_helper"
class Account::SyncTest < ActiveSupport::TestCase
setup do
@account = accounts(:depository)
@sync = Account::Sync.for(@account)
@balance_syncer = mock("Account::Balance::Syncer")
Account::Balance::Syncer.expects(:new).with(@account, start_date: nil).returns(@balance_syncer).once
end
test "runs sync" do
@balance_syncer.expects(:run).once
@balance_syncer.expects(:warnings).returns([ "test sync warning" ]).once
assert_equal "pending", @sync.status
assert_equal [], @sync.warnings
assert_nil @sync.last_ran_at
@sync.run
assert_equal "completed", @sync.status
assert_equal [ "test sync warning" ], @sync.warnings
assert @sync.last_ran_at
end
test "handles sync errors" do
@balance_syncer.expects(:run).raises(StandardError.new("test sync error"))
@sync.run
assert @sync.last_ran_at
assert_equal "failed", @sync.status
assert_equal "test sync error", @sync.error
end
end

View file

@ -1,124 +0,0 @@
require "test_helper"
class Account::SyncableTest < ActiveSupport::TestCase
include ActiveJob::TestHelper
setup do
@account = accounts(:savings)
end
test "calculates effective start date of an account" do
assert_equal 31.days.ago.to_date, accounts(:collectable).effective_start_date
assert_equal 31.days.ago.to_date, @account.effective_start_date
end
test "syncs regular account" do
@account.sync
assert_equal "ok", @account.status
assert_equal 32, @account.balances.count
end
test "syncs multi currency account" do
required_exchange_rates_for_sync = [
{ from_currency: "EUR", to_currency: "USD", date: 4.days.ago.to_date, rate: 1.0788 },
{ from_currency: "EUR", to_currency: "USD", date: 19.days.ago.to_date, rate: 1.0822 }
]
ExchangeRate.insert_all(required_exchange_rates_for_sync)
account = accounts(:multi_currency)
account.sync
assert_equal "ok", account.status
assert_equal 32, account.balances.where(currency: "USD").count
end
test "triggers sync job" do
assert_enqueued_with(job: AccountSyncJob, args: [ @account, Date.current ]) do
@account.sync_later(Date.current)
end
end
test "account has no balances until synced" do
account = accounts(:savings)
assert_equal 0, account.balances.count
end
test "account has balances after syncing" do
account = accounts(:savings)
account.sync
assert_equal 32, account.balances.count
end
test "partial sync with missing historical balances performs a full sync" do
account = accounts(:savings)
account.sync 10.days.ago.to_date
assert_equal 32, account.balances.count
end
test "balances are updated after syncing" do
account = accounts(:savings)
balance_date = 10.days.ago
account.balances.create!(date: balance_date, balance: 1000)
account.sync
assert_equal 19500, account.balances.find_by(date: balance_date)[:balance]
end
test "can perform a partial sync with a given sync start date" do
# Perform a full sync to populate all balances
@account.sync
# Perform partial sync
sync_start_date = 5.days.ago.to_date
balances_before_sync = @account.balances.to_a
@account.sync sync_start_date
balances_after_sync = @account.reload.balances.to_a
# Balances on or after should be updated
balances_after_sync.each do |balance_after_sync|
balance_before_sync = balances_before_sync.find { |b| b.date == balance_after_sync.date }
if balance_after_sync.date >= sync_start_date
assert balance_before_sync.updated_at < balance_after_sync.updated_at
else
assert_equal balance_before_sync.updated_at, balance_after_sync.updated_at
end
end
end
test "foreign currency account has balances in each currency after syncing" do
required_exchange_rates_for_sync = [
1.0834, 1.0845, 1.0819, 1.0872, 1.0788, 1.0743, 1.0755, 1.0774,
1.0778, 1.0783, 1.0773, 1.0709, 1.0729, 1.0773, 1.0778, 1.078,
1.0809, 1.0818, 1.0824, 1.0822, 1.0854, 1.0845, 1.0839, 1.0807,
1.084, 1.0856, 1.0858, 1.0898, 1.095, 1.094, 1.0926, 1.0986
]
required_exchange_rates_for_sync.each_with_index do |exchange_rate, idx|
ExchangeRate.create! date: idx.days.ago.to_date, from_currency: "EUR", to_currency: "USD", rate: exchange_rate
end
account = accounts(:eur_checking)
account.sync
assert_equal 64, account.balances.count
assert_equal 32, account.balances.where(currency: "EUR").count
assert_equal 32, account.balances.where(currency: "USD").count
end
test "stale balances are purged after syncing" do
account = accounts(:savings)
# Create old, stale balances that should be purged (since they are before account start date)
account.balances.create!(date: 1.year.ago, balance: 1000)
account.balances.create!(date: 2.years.ago, balance: 2000)
account.balances.create!(date: 3.years.ago, balance: 3000)
account.sync
assert_equal 32, account.balances.count
end
end

View file

@ -2,22 +2,8 @@ require "test_helper"
class Account::TransferTest < ActiveSupport::TestCase
setup do
# Transfers can be posted on different dates
@outflow = accounts(:checking).entries.create! \
date: 1.day.ago.to_date,
name: "Transfer to Savings",
amount: 100,
currency: "USD",
marked_as_transfer: true,
entryable: Account::Transaction.new
@inflow = accounts(:savings).entries.create! \
date: Date.current,
name: "Transfer from Savings",
amount: -100,
currency: "USD",
marked_as_transfer: true,
entryable: Account::Transaction.new
@outflow = account_entries(:transfer_out)
@inflow = account_entries(:transfer_in)
end
test "transfer valid if it has inflow and outflow from different accounts for the same amount" do
@ -28,14 +14,15 @@ class Account::TransferTest < ActiveSupport::TestCase
test "transfer must have 2 transactions" do
invalid_transfer_1 = Account::Transfer.new entries: [ @outflow ]
invalid_transfer_2 = Account::Transfer.new entries: [ @inflow, @outflow, account_entries(:savings_four) ]
invalid_transfer_2 = Account::Transfer.new entries: [ @inflow, @outflow, account_entries(:transaction) ]
assert invalid_transfer_1.invalid?
assert invalid_transfer_2.invalid?
end
test "transfer cannot have 2 transactions from the same account" do
account = accounts(:checking)
account = accounts(:depository)
inflow = account.entries.create! \
date: Date.current,
name: "Inflow",

View file

@ -1,38 +1,31 @@
require "test_helper"
require "csv"
class AccountTest < ActiveSupport::TestCase
def setup
@account = accounts(:checking)
include ActiveJob::TestHelper
setup do
@account = accounts(:depository)
@family = families(:dylan_family)
end
test "recognizes foreign currency account" do
regular_account = accounts(:checking)
foreign_account = accounts(:eur_checking)
assert_not regular_account.foreign_currency?
assert foreign_account.foreign_currency?
test "can sync later" do
assert_enqueued_with(job: AccountSyncJob, args: [ @account, start_date: Date.current ]) do
@account.sync_later start_date: Date.current
end
end
test "recognizes multi currency account" do
regular_account = accounts(:checking)
multi_currency_account = accounts(:multi_currency)
assert_not regular_account.multi_currency?
assert multi_currency_account.multi_currency?
end
test "can sync" do
start_date = 10.days.ago.to_date
test "multi currency and foreign currency are different concepts" do
multi_currency_account = accounts(:multi_currency)
assert_equal multi_currency_account.family.currency, multi_currency_account.currency
assert multi_currency_account.multi_currency?
assert_not multi_currency_account.foreign_currency?
mock_sync = mock("Account::Sync")
mock_sync.expects(:run).once
Account::Sync.expects(:for).with(@account, start_date: start_date).returns(mock_sync).once
@account.sync start_date: start_date
end
test "groups accounts by type" do
@family.accounts.each do |account|
account.sync
end
result = @family.accounts.by_group(period: Period.all)
assets = result[:assets]
liabilities = result[:liabilities]
@ -50,7 +43,7 @@ class AccountTest < ActiveSupport::TestCase
loans = liabilities.children.find { |group| group.name == "Loan" }
other_liabilities = liabilities.children.find { |group| group.name == "OtherLiability" }
assert_equal 4, depositories.children.count
assert_equal 1, depositories.children.count
assert_equal 1, properties.children.count
assert_equal 1, vehicles.children.count
assert_equal 1, investments.children.count
@ -61,38 +54,24 @@ class AccountTest < ActiveSupport::TestCase
assert_equal 1, other_liabilities.children.count
end
test "generates series with last balance equal to current account balance" do
# If account hasn't been synced, series falls back to a single point with the current balance
assert_equal @account.balance_money, @account.series.last.value
@account.sync
# Synced series will always have final balance equal to the current account balance
assert_equal @account.balance_money, @account.series.last.value
test "generates balance series" do
assert_equal 2, @account.series.values.count
end
test "generates empty series for foreign currency if no exchange rate" do
account = accounts(:eur_checking)
# We know EUR -> NZD exchange rate is not available in fixtures
assert_equal 0, account.series(currency: "NZD").values.count
test "generates balance series with single value if no balances" do
@account.balances.delete_all
assert_equal 1, @account.series.values.count
end
test "should destroy dependent transactions" do
assert_difference("Account::Transaction.count", -@account.transactions.count) do
@account.destroy
end
test "generates balance series in period" do
@account.balances.delete_all
@account.balances.create! date: 31.days.ago.to_date, balance: 5000, currency: "USD" # out of period range
@account.balances.create! date: 30.days.ago.to_date, balance: 5000, currency: "USD" # in range
assert_equal 1, @account.series(period: Period.last_30_days).values.count
end
test "should destroy dependent balances" do
assert_difference("Account::Balance.count", -@account.balances.count) do
@account.destroy
end
end
test "should destroy dependent valuations" do
assert_difference("Account::Valuation.count", -@account.valuations.count) do
@account.destroy
end
test "generates empty series if no balances and no exchange rate" do
assert_equal 0, @account.series(currency: "NZD").values.count
end
end

View file

@ -20,7 +20,7 @@ class CategoryTest < ActiveSupport::TestCase
end
test "updating name should clear the internal_category field" do
category = Category.take
category = categories(:income)
assert_changes "category.reload.internal_category", to: nil do
category.update_attribute(:name, "new name")
end

View file

@ -2,144 +2,148 @@ require "test_helper"
require "csv"
class FamilyTest < ActiveSupport::TestCase
include FamilySnapshotTestHelper
include Account::EntriesTestHelper
def setup
@family = families(:dylan_family)
required_exchange_rates_for_family = [
1.0834, 1.0845, 1.0819, 1.0872, 1.0788, 1.0743, 1.0755, 1.0774,
1.0778, 1.0783, 1.0773, 1.0709, 1.0729, 1.0773, 1.0778, 1.078,
1.0809, 1.0818, 1.0824, 1.0822, 1.0854, 1.0845, 1.0839, 1.0807,
1.084, 1.0856, 1.0858, 1.0898, 1.095, 1.094, 1.0926, 1.0986
]
required_exchange_rates_for_family.each_with_index do |exchange_rate, idx|
ExchangeRate.create! date: idx.days.ago.to_date, from_currency: "EUR", to_currency: "USD", rate: exchange_rate
end
@family.accounts.each do |account|
account.sync
end
@family = families :empty
end
test "should have many users" do
assert @family.users.size > 0
assert @family.users.include?(users(:family_admin))
test "calculates assets" do
assert_equal Money.new(0, @family.currency), @family.assets
@family.accounts.create!(balance: 1000, accountable: Depository.new)
@family.accounts.create!(balance: 5000, accountable: OtherAsset.new)
@family.accounts.create!(balance: 10000, accountable: CreditCard.new) # ignored
assert_equal Money.new(1000 + 5000, @family.currency), @family.assets
end
test "should have many accounts" do
assert @family.accounts.size > 0
test "calculates liabilities" do
assert_equal Money.new(0, @family.currency), @family.liabilities
@family.accounts.create!(balance: 1000, accountable: CreditCard.new)
@family.accounts.create!(balance: 5000, accountable: OtherLiability.new)
@family.accounts.create!(balance: 10000, accountable: Depository.new) # ignored
assert_equal Money.new(1000 + 5000, @family.currency), @family.liabilities
end
test "should destroy dependent users" do
assert_difference("User.count", -@family.users.count) do
@family.destroy
end
end
test "calculates net worth" do
assert_equal Money.new(0, @family.currency), @family.net_worth
test "should destroy dependent accounts" do
assert_difference("Account.count", -@family.accounts.count) do
@family.destroy
end
end
@family.accounts.create!(balance: 1000, accountable: CreditCard.new)
@family.accounts.create!(balance: 50000, accountable: Depository.new)
test "should destroy dependent transaction categories" do
assert_difference("Category.count", -@family.categories.count) do
@family.destroy
end
end
test "should destroy dependent merchants" do
assert_difference("Merchant.count", -@family.merchants.count) do
@family.destroy
end
end
test "should calculate total assets" do
expected = get_today_snapshot_value_for :assets
assert_in_delta expected, @family.assets.amount, 0.01
end
test "should calculate total liabilities" do
expected = get_today_snapshot_value_for :liabilities
assert_in_delta expected, @family.liabilities.amount, 0.01
end
test "should calculate net worth" do
expected = get_today_snapshot_value_for :net_worth
assert_in_delta expected, @family.net_worth.amount, 0.01
end
test "calculates asset time series" do
series = @family.snapshot[:asset_series]
expected_series = get_expected_balances_for :assets
assert_time_series_balances series, expected_series
end
test "calculates liability time series" do
series = @family.snapshot[:liability_series]
expected_series = get_expected_balances_for :liabilities
assert_time_series_balances series, expected_series
end
test "calculates net worth time series" do
series = @family.snapshot[:net_worth_series]
expected_series = get_expected_balances_for :net_worth
assert_time_series_balances series, expected_series
end
test "calculates rolling expenses" do
series = @family.snapshot_transactions[:spending_series]
expected_series = get_expected_balances_for :rolling_spend
assert_time_series_balances series, expected_series, ignore_count: true
end
test "calculates rolling income" do
series = @family.snapshot_transactions[:income_series]
expected_series = get_expected_balances_for :rolling_income
assert_time_series_balances series, expected_series, ignore_count: true
end
test "calculates savings rate series" do
series = @family.snapshot_transactions[:savings_rate_series]
expected_series = get_expected_balances_for :savings_rate
series.values.each do |tsb|
expected_balance = expected_series.find { |eb| eb[:date] == tsb.date }
assert_in_delta expected_balance[:balance], tsb.value, 0.0001, "Balance incorrect on date: #{tsb.date}"
end
assert_equal Money.new(50000 - 1000, @family.currency), @family.net_worth
end
test "should exclude disabled accounts from calculations" do
assets_before = @family.assets
liabilities_before = @family.liabilities
net_worth_before = @family.net_worth
cc = @family.accounts.create!(balance: 1000, accountable: CreditCard.new)
@family.accounts.create!(balance: 50000, accountable: Depository.new)
disabled_checking = accounts(:checking)
disabled_cc = accounts(:credit_card)
assert_equal Money.new(50000 - 1000, @family.currency), @family.net_worth
disabled_checking.update!(is_active: false)
disabled_cc.update!(is_active: false)
cc.update! is_active: false
assert_equal assets_before - disabled_checking.balance, @family.assets
assert_equal liabilities_before - disabled_cc.balance, @family.liabilities
assert_equal net_worth_before - disabled_checking.balance + disabled_cc.balance, @family.net_worth
assert_equal Money.new(50000, @family.currency), @family.net_worth
end
private
test "syncs active accounts" do
account = @family.accounts.create!(balance: 1000, accountable: CreditCard.new, is_active: false)
def assert_time_series_balances(time_series_balances, expected_balances, ignore_count: false)
assert_equal time_series_balances.values.count, expected_balances.count unless ignore_count
Account.any_instance.expects(:sync_later).never
time_series_balances.values.each do |tsb|
expected_balance = expected_balances.find { |eb| eb[:date] == tsb.date }
assert_in_delta expected_balance[:balance], tsb.value.amount, 0.01, "Balance incorrect on date: #{tsb.date}"
end
end
@family.sync
account.update! is_active: true
Account.any_instance.expects(:sync_later).with(start_date: nil).once
@family.sync
end
test "calculates snapshot" do
asset = @family.accounts.create!(balance: 500, accountable: Depository.new)
liability = @family.accounts.create!(balance: 100, accountable: CreditCard.new)
asset.balances.create! date: 1.day.ago.to_date, currency: "USD", balance: 450
asset.balances.create! date: Date.current, currency: "USD", balance: 500
liability.balances.create! date: 1.day.ago.to_date, currency: "USD", balance: 50
liability.balances.create! date: Date.current, currency: "USD", balance: 100
expected_asset_series = [
{ date: 1.day.ago.to_date, value: Money.new(450) },
{ date: Date.current, value: Money.new(500) }
]
expected_liability_series = [
{ date: 1.day.ago.to_date, value: Money.new(50) },
{ date: Date.current, value: Money.new(100) }
]
expected_net_worth_series = [
{ date: 1.day.ago.to_date, value: Money.new(450 - 50) },
{ date: Date.current, value: Money.new(500 - 100) }
]
assert_equal expected_asset_series, @family.snapshot[:asset_series].values.map { |v| { date: v.date, value: v.value } }
assert_equal expected_liability_series, @family.snapshot[:liability_series].values.map { |v| { date: v.date, value: v.value } }
assert_equal expected_net_worth_series, @family.snapshot[:net_worth_series].values.map { |v| { date: v.date, value: v.value } }
end
test "calculates top movers" do
checking_account = @family.accounts.create!(balance: 500, accountable: Depository.new)
savings_account = @family.accounts.create!(balance: 1000, accountable: Depository.new)
create_transaction(account: checking_account, date: 2.days.ago.to_date, amount: -1000)
create_transaction(account: checking_account, date: 1.day.ago.to_date, amount: 10)
create_transaction(account: savings_account, date: 2.days.ago.to_date, amount: -5000)
snapshot = @family.snapshot_account_transactions
top_spenders = snapshot[:top_spenders]
top_earners = snapshot[:top_earners]
top_savers = snapshot[:top_savers]
assert_equal 10, top_spenders.first.spending
assert_equal 5000, top_earners.first.income
assert_equal 1000, top_earners.second.income
assert_equal 1, top_savers.first.savings_rate
assert_equal ((1000 - 10).to_f / 1000), top_savers.second.savings_rate
end
test "calculates rolling transaction totals" do
account = @family.accounts.create!(balance: 1000, accountable: Depository.new)
create_transaction(account: account, date: 2.days.ago.to_date, amount: -500)
create_transaction(account: account, date: 1.day.ago.to_date, amount: 100)
create_transaction(account: account, date: Date.current, amount: 20)
snapshot = @family.snapshot_transactions
expected_income_series = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 500, 500, 500
]
assert_equal expected_income_series, snapshot[:income_series].values.map(&:value).map(&:amount)
expected_spending_series = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 100, 120
]
assert_equal expected_spending_series, snapshot[:spending_series].values.map(&:value).map(&:amount)
expected_savings_rate_series = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 1, 0.8, 0.76
]
assert_equal expected_savings_rate_series, snapshot[:savings_rate_series].values.map(&:value).map { |v| v.round(2) }
end
end

View file

@ -2,8 +2,8 @@ require "test_helper"
class TagTest < ActiveSupport::TestCase
test "replace and destroy" do
old_tag = tags(:hawaii_trip)
new_tag = tags(:trips)
old_tag = tags(:one)
new_tag = tags(:two)
assert_difference "Tag.count", -1 do
old_tag.replace_and_destroy!(new_tag)

View file

@ -2,10 +2,6 @@ require "test_helper"
require "ostruct"
class ValueGroupTest < ActiveSupport::TestCase
setup do
checking = accounts(:checking)
savings = accounts(:savings)
collectable = accounts(:collectable)
# Level 1
@assets = ValueGroup.new("Assets", :usd)