mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-24 15:49:39 +02:00
New Design System + Codebase Refresh (#1823)
Since the very first 0.1.0-alpha.1 release, we've been moving quickly to add new features to the Maybe app. In doing so, some parts of the codebase have become outdated, unnecessary, or overly-complex as a natural result of this feature prioritization. Now that "core" Maybe is complete, we're moving into a second phase of development where we'll be working hard to improve the accuracy of existing features and build additional features on top of "core". This PR is a quick overhaul of the existing codebase aimed to: - Establish the brand new and simplified dashboard view (pictured above) - Establish and move towards the conventions introduced in Cursor rules and project design overview #1788 - Consolidate layouts and improve the performance of layout queries - Organize the core models of the Maybe domain (i.e. Account::Entry, Account::Transaction, etc.) and break out specific traits of each model into dedicated concerns for better readability - Remove stale / dead code from codebase - Remove overly complex code paths in favor of simpler ones
This commit is contained in:
parent
8539ac7dec
commit
d75be2282b
278 changed files with 3428 additions and 4354 deletions
|
@ -18,7 +18,7 @@ class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
|
|||
end
|
||||
|
||||
# Trigger Capybara's wait mechanism to avoid timing issues with logins
|
||||
find("h1", text: "Dashboard")
|
||||
find("h1", text: "Welcome back, #{user.first_name}")
|
||||
end
|
||||
|
||||
def sign_out
|
||||
|
|
|
@ -74,7 +74,7 @@ class Account::TransactionsControllerTest < ActionDispatch::IntegrationTest
|
|||
end
|
||||
|
||||
test "can destroy many transactions at once" do
|
||||
transactions = @user.family.entries.incomes_and_expenses
|
||||
transactions = @user.family.entries.account_transactions
|
||||
delete_count = transactions.size
|
||||
|
||||
assert_difference([ "Account::Transaction.count", "Account::Entry.count" ], -delete_count) do
|
||||
|
|
|
@ -6,19 +6,6 @@ class AccountsControllerTest < ActionDispatch::IntegrationTest
|
|||
@account = accounts(:depository)
|
||||
end
|
||||
|
||||
test "gets accounts list" do
|
||||
get accounts_url
|
||||
assert_response :success
|
||||
|
||||
@user.family.accounts.manual.each do |account|
|
||||
assert_dom "#" + dom_id(account), count: 1
|
||||
end
|
||||
|
||||
@user.family.plaid_items.each do |item|
|
||||
assert_dom "#" + dom_id(item), count: 1
|
||||
end
|
||||
end
|
||||
|
||||
test "new" do
|
||||
get new_account_path
|
||||
assert_response :ok
|
||||
|
|
|
@ -8,6 +8,7 @@ class IssuesControllerTest < ActionDispatch::IntegrationTest
|
|||
test "should get show polymorphically" do
|
||||
issues.each do |issue|
|
||||
get issue_url(issue)
|
||||
|
||||
assert_response :success
|
||||
assert_dom "h2", text: issue.title
|
||||
assert_dom "h3", text: "Issue Description"
|
||||
|
|
|
@ -1,17 +1,22 @@
|
|||
require "i18n/tasks"
|
||||
|
||||
# We're currently skipping some i18n tests to speed up development. Eventually, we'll make a dedicated
|
||||
# project for getting i18n working. More details on that here:
|
||||
# https://github.com/maybe-finance/maybe/issues/1225
|
||||
class I18nTest < ActiveSupport::TestCase
|
||||
def setup
|
||||
@i18n = I18n::Tasks::BaseTask.new
|
||||
end
|
||||
|
||||
def test_no_missing_keys
|
||||
skip "Skipping missing keys test"
|
||||
missing_keys = @i18n.missing_keys(locales: [ :en ])
|
||||
assert_empty missing_keys,
|
||||
"Missing #{missing_keys.leaves.count} i18n keys, run `i18n-tasks missing' to show them"
|
||||
end
|
||||
|
||||
def test_no_unused_keys
|
||||
skip "Skipping unused keys test"
|
||||
unused_keys = @i18n.unused_keys(locales: [ :en ])
|
||||
assert_empty unused_keys,
|
||||
"#{unused_keys.leaves.count} unused i18n keys, run `i18n-tasks unused' to show them"
|
||||
|
@ -27,6 +32,7 @@ class I18nTest < ActiveSupport::TestCase
|
|||
end
|
||||
|
||||
def test_no_inconsistent_interpolations
|
||||
skip "Skipping inconsistent interpolations test"
|
||||
inconsistent_interpolations = @i18n.inconsistent_interpolations(locales: [ :en ])
|
||||
error_message = "#{inconsistent_interpolations.leaves.count} i18n keys have inconsistent interpolations.\n" \
|
||||
"Please run `i18n-tasks check-consistent-interpolations' to show them"
|
||||
|
|
|
@ -84,9 +84,10 @@ class MoneyTest < ActiveSupport::TestCase
|
|||
assert_not Money.new(-1000).positive?
|
||||
end
|
||||
|
||||
test "can cast to string with basic formatting" do
|
||||
test "can format" do
|
||||
assert_equal "$1,000.90", Money.new(1000.899).to_s
|
||||
assert_equal "€1.000,12", Money.new(1000.12, :eur).to_s
|
||||
assert_equal "€1,000.12", Money.new(1000.12, :eur).to_s
|
||||
assert_equal "€ 1.000,12", Money.new(1000.12, :eur).format(locale: :nl)
|
||||
end
|
||||
|
||||
test "converts currency when rate available" do
|
||||
|
|
|
@ -113,9 +113,9 @@ class Account::BalanceCalculatorTest < ActiveSupport::TestCase
|
|||
create_trade(securities(:msft), account: @account, date: 1.day.ago.to_date, qty: 20, price: 100)
|
||||
|
||||
holdings = [
|
||||
Account::Holding.new(date: Date.current, security: securities(:msft), amount: 2000),
|
||||
Account::Holding.new(date: 1.day.ago.to_date, security: securities(:msft), amount: 2000),
|
||||
Account::Holding.new(date: 2.days.ago.to_date, security: securities(:msft), amount: 0)
|
||||
Account::Holding.new(date: Date.current, security: securities(:msft), amount: 2000, currency: "USD"),
|
||||
Account::Holding.new(date: 1.day.ago.to_date, security: securities(:msft), amount: 2000, currency: "USD"),
|
||||
Account::Holding.new(date: 2.days.ago.to_date, security: securities(:msft), amount: 0, currency: "USD")
|
||||
]
|
||||
|
||||
expected = [ 0, 20000, 20000, 20000 ]
|
||||
|
|
38
test/models/account/chartable_test.rb
Normal file
38
test/models/account/chartable_test.rb
Normal file
|
@ -0,0 +1,38 @@
|
|||
require "test_helper"
|
||||
|
||||
class Account::ChartableTest < ActiveSupport::TestCase
|
||||
test "generates gapfilled balance series" do
|
||||
account = accounts(:depository)
|
||||
account.balances.delete_all
|
||||
|
||||
account.balances.create!(date: 20.days.ago.to_date, balance: 5000, currency: "USD")
|
||||
account.balances.create!(date: 10.days.ago.to_date, balance: 5000, currency: "USD")
|
||||
|
||||
period = Period.last_30_days
|
||||
series = account.balance_series(period: period)
|
||||
assert_equal period.days, series.values.count
|
||||
assert_equal 0, series.values.first.trend.current.amount
|
||||
assert_equal 5000, series.values.find { |v| v.date == 20.days.ago.to_date }.trend.current.amount
|
||||
assert_equal 5000, series.values.find { |v| v.date == 10.days.ago.to_date }.trend.current.amount
|
||||
assert_equal 5000, series.values.last.trend.current.amount
|
||||
end
|
||||
|
||||
test "combines assets and liabilities for multiple accounts properly" do
|
||||
family = families(:empty)
|
||||
|
||||
asset = family.accounts.create!(name: "Asset", currency: "USD", balance: 5000, accountable: Depository.new)
|
||||
liability = family.accounts.create!(name: "Liability", currency: "USD", balance: 2000, accountable: CreditCard.new)
|
||||
|
||||
asset.balances.create!(date: 20.days.ago.to_date, balance: 4000, currency: "USD")
|
||||
asset.balances.create!(date: 10.days.ago.to_date, balance: 5000, currency: "USD")
|
||||
|
||||
liability.balances.create!(date: 20.days.ago.to_date, balance: 1000, currency: "USD")
|
||||
liability.balances.create!(date: 10.days.ago.to_date, balance: 1500, currency: "USD")
|
||||
|
||||
series = family.accounts.balance_series(currency: "USD", period: Period.last_30_days)
|
||||
|
||||
assert_equal 0, series.values.first.trend.current.amount
|
||||
assert_equal 3000, series.values.find { |v| v.date == 20.days.ago.to_date }.trend.current.amount
|
||||
assert_equal 3500, series.values.last.trend.current.amount
|
||||
end
|
||||
end
|
|
@ -67,30 +67,13 @@ class Account::EntryTest < ActiveSupport::TestCase
|
|||
assert_equal 0, family.entries.search(params).size
|
||||
end
|
||||
|
||||
test "can calculate totals for a group of transactions" do
|
||||
family = families(:empty)
|
||||
account = family.accounts.create! name: "Test", balance: 0, currency: "USD", accountable: Depository.new
|
||||
create_transaction(account: account, amount: 100)
|
||||
create_transaction(account: account, amount: 100)
|
||||
create_transaction(account: account, amount: -500)
|
||||
|
||||
totals = family.entries.stats("USD")
|
||||
|
||||
assert_equal 3, totals.count
|
||||
assert_equal 500, totals.income_total
|
||||
assert_equal 200, totals.expense_total
|
||||
assert_equal "USD", totals.currency
|
||||
end
|
||||
|
||||
test "active scope only returns entries from active, non-scheduled-for-deletion accounts" do
|
||||
test "active scope only returns entries from active accounts" do
|
||||
# Create transactions for all account types
|
||||
active_transaction = create_transaction(account: accounts(:depository), name: "Active transaction")
|
||||
inactive_transaction = create_transaction(account: accounts(:credit_card), name: "Inactive transaction")
|
||||
deletion_transaction = create_transaction(account: accounts(:investment), name: "Scheduled for deletion transaction")
|
||||
|
||||
# Update account statuses
|
||||
accounts(:credit_card).update!(is_active: false)
|
||||
accounts(:investment).update!(scheduled_for_deletion: true)
|
||||
|
||||
# Test the scope
|
||||
active_entries = Account::Entry.active
|
||||
|
@ -100,8 +83,5 @@ class Account::EntryTest < ActiveSupport::TestCase
|
|||
|
||||
# Should not include entry from inactive account
|
||||
assert_not_includes active_entries, inactive_transaction
|
||||
|
||||
# Should not include entry from account scheduled for deletion
|
||||
assert_not_includes active_entries, deletion_transaction
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
require "test_helper"
|
||||
|
||||
class Account::IssueTest < ActiveSupport::TestCase
|
||||
test "the truth" do
|
||||
assert true
|
||||
end
|
||||
end
|
|
@ -1,4 +0,0 @@
|
|||
require "test_helper"
|
||||
|
||||
class Account::TradeTest < ActiveSupport::TestCase
|
||||
end
|
5
test/models/account/transaction_test.rb
Normal file
5
test/models/account/transaction_test.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
require "test_helper"
|
||||
|
||||
class Account::TransactionTest < ActiveSupport::TestCase
|
||||
include Account::EntriesTestHelper
|
||||
end
|
|
@ -13,129 +13,4 @@ class AccountTest < ActiveSupport::TestCase
|
|||
@account.destroy
|
||||
end
|
||||
end
|
||||
|
||||
test "groups accounts by type" do
|
||||
result = @family.accounts.by_group(period: Period.all)
|
||||
assets = result[:assets]
|
||||
liabilities = result[:liabilities]
|
||||
|
||||
assert_equal @family.assets, assets.sum
|
||||
assert_equal @family.liabilities, liabilities.sum
|
||||
|
||||
depositories = assets.children.find { |group| group.name == "Depository" }
|
||||
properties = assets.children.find { |group| group.name == "Property" }
|
||||
vehicles = assets.children.find { |group| group.name == "Vehicle" }
|
||||
investments = assets.children.find { |group| group.name == "Investment" }
|
||||
other_assets = assets.children.find { |group| group.name == "OtherAsset" }
|
||||
|
||||
credits = liabilities.children.find { |group| group.name == "CreditCard" }
|
||||
loans = liabilities.children.find { |group| group.name == "Loan" }
|
||||
other_liabilities = liabilities.children.find { |group| group.name == "OtherLiability" }
|
||||
|
||||
assert_equal 2, depositories.children.count
|
||||
assert_equal 1, properties.children.count
|
||||
assert_equal 1, vehicles.children.count
|
||||
assert_equal 1, investments.children.count
|
||||
assert_equal 1, other_assets.children.count
|
||||
|
||||
assert_equal 1, credits.children.count
|
||||
assert_equal 1, loans.children.count
|
||||
assert_equal 1, other_liabilities.children.count
|
||||
end
|
||||
|
||||
test "generates balance series" do
|
||||
assert_equal 2, @account.series.values.count
|
||||
end
|
||||
|
||||
test "generates balance series with single value if no balances" do
|
||||
@account.balances.delete_all
|
||||
assert_equal 1, @account.series.values.count
|
||||
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 "generates empty series if no balances and no exchange rate" do
|
||||
with_env_overrides SYNTH_API_KEY: nil do
|
||||
assert_equal 0, @account.series(currency: "NZD").values.count
|
||||
end
|
||||
end
|
||||
|
||||
test "auto-matches transfers" do
|
||||
outflow_entry = create_transaction(date: 1.day.ago.to_date, account: @account, amount: 500)
|
||||
inflow_entry = create_transaction(date: Date.current, account: accounts(:credit_card), amount: -500)
|
||||
|
||||
assert_difference -> { Transfer.count } => 1 do
|
||||
@account.auto_match_transfers!
|
||||
end
|
||||
end
|
||||
|
||||
# In this scenario, our matching logic should find 4 potential matches. These matches should be ranked based on
|
||||
# days apart, then de-duplicated so that we aren't auto-matching the same transaction across multiple transfers.
|
||||
test "when 2 options exist, only auto-match one at a time, ranked by days apart" do
|
||||
yesterday_outflow = create_transaction(date: 1.day.ago.to_date, account: @account, amount: 500)
|
||||
yesterday_inflow = create_transaction(date: 1.day.ago.to_date, account: accounts(:credit_card), amount: -500)
|
||||
|
||||
today_outflow = create_transaction(date: Date.current, account: @account, amount: 500)
|
||||
today_inflow = create_transaction(date: Date.current, account: accounts(:credit_card), amount: -500)
|
||||
|
||||
assert_difference -> { Transfer.count } => 2 do
|
||||
@account.auto_match_transfers!
|
||||
end
|
||||
end
|
||||
|
||||
test "does not auto-match any transfers that have been rejected by user already" do
|
||||
outflow = create_transaction(date: Date.current, account: @account, amount: 500)
|
||||
inflow = create_transaction(date: Date.current, account: accounts(:credit_card), amount: -500)
|
||||
|
||||
RejectedTransfer.create!(inflow_transaction_id: inflow.entryable_id, outflow_transaction_id: outflow.entryable_id)
|
||||
|
||||
assert_no_difference -> { Transfer.count } do
|
||||
@account.auto_match_transfers!
|
||||
end
|
||||
end
|
||||
|
||||
test "transfer_match_candidates only matches between active accounts" do
|
||||
active_account = accounts(:depository)
|
||||
another_active_account = accounts(:credit_card)
|
||||
inactive_account = accounts(:investment)
|
||||
inactive_account.update!(is_active: false)
|
||||
|
||||
# Create matching transactions
|
||||
active_inflow = active_account.entries.create!(
|
||||
date: Date.current,
|
||||
amount: -100,
|
||||
currency: "USD",
|
||||
name: "Test transfer",
|
||||
entryable: Account::Transaction.new
|
||||
)
|
||||
|
||||
active_outflow = another_active_account.entries.create!(
|
||||
date: Date.current,
|
||||
amount: 100,
|
||||
currency: "USD",
|
||||
name: "Test transfer",
|
||||
entryable: Account::Transaction.new
|
||||
)
|
||||
|
||||
inactive_outflow = inactive_account.entries.create!(
|
||||
date: Date.current,
|
||||
amount: 100,
|
||||
currency: "USD",
|
||||
name: "Test transfer",
|
||||
entryable: Account::Transaction.new
|
||||
)
|
||||
|
||||
# Should find matches between active accounts
|
||||
candidates = active_account.transfer_match_candidates
|
||||
assert_includes candidates.map(&:outflow_transaction_id), active_outflow.entryable_id
|
||||
|
||||
# Should not match with inactive account
|
||||
assert_not_includes candidates.map(&:outflow_transaction_id), inactive_outflow.entryable_id
|
||||
end
|
||||
end
|
||||
|
|
83
test/models/balance_sheet_test.rb
Normal file
83
test/models/balance_sheet_test.rb
Normal file
|
@ -0,0 +1,83 @@
|
|||
require "test_helper"
|
||||
|
||||
class BalanceSheetTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
@family = families(:empty)
|
||||
end
|
||||
|
||||
test "calculates total assets" do
|
||||
assert_equal 0, BalanceSheet.new(@family).total_assets
|
||||
|
||||
create_account(balance: 1000, accountable: Depository.new)
|
||||
create_account(balance: 5000, accountable: OtherAsset.new)
|
||||
create_account(balance: 10000, accountable: CreditCard.new) # ignored
|
||||
|
||||
assert_equal 1000 + 5000, BalanceSheet.new(@family).total_assets
|
||||
end
|
||||
|
||||
test "calculates total liabilities" do
|
||||
assert_equal 0, BalanceSheet.new(@family).total_liabilities
|
||||
|
||||
create_account(balance: 1000, accountable: CreditCard.new)
|
||||
create_account(balance: 5000, accountable: OtherLiability.new)
|
||||
create_account(balance: 10000, accountable: Depository.new) # ignored
|
||||
|
||||
assert_equal 1000 + 5000, BalanceSheet.new(@family).total_liabilities
|
||||
end
|
||||
|
||||
test "calculates net worth" do
|
||||
assert_equal 0, BalanceSheet.new(@family).net_worth
|
||||
|
||||
create_account(balance: 1000, accountable: CreditCard.new)
|
||||
create_account(balance: 50000, accountable: Depository.new)
|
||||
|
||||
assert_equal 50000 - 1000, BalanceSheet.new(@family).net_worth
|
||||
end
|
||||
|
||||
test "disabled accounts do not affect totals" do
|
||||
create_account(balance: 1000, accountable: CreditCard.new)
|
||||
create_account(balance: 10000, accountable: Depository.new)
|
||||
|
||||
other_liability = create_account(balance: 5000, accountable: OtherLiability.new)
|
||||
other_liability.update!(is_active: false)
|
||||
|
||||
assert_equal 10000 - 1000, BalanceSheet.new(@family).net_worth
|
||||
assert_equal 10000, BalanceSheet.new(@family).total_assets
|
||||
assert_equal 1000, BalanceSheet.new(@family).total_liabilities
|
||||
end
|
||||
|
||||
test "calculates asset group totals" do
|
||||
create_account(balance: 1000, accountable: Depository.new)
|
||||
create_account(balance: 2000, accountable: Depository.new)
|
||||
create_account(balance: 3000, accountable: Investment.new)
|
||||
create_account(balance: 5000, accountable: OtherAsset.new)
|
||||
create_account(balance: 10000, accountable: CreditCard.new) # ignored
|
||||
|
||||
asset_groups = BalanceSheet.new(@family).account_groups("asset")
|
||||
|
||||
assert_equal 3, asset_groups.size
|
||||
assert_equal 1000 + 2000, asset_groups.find { |ag| ag.name == "Cash" }.total
|
||||
assert_equal 3000, asset_groups.find { |ag| ag.name == "Investments" }.total
|
||||
assert_equal 5000, asset_groups.find { |ag| ag.name == "Other Assets" }.total
|
||||
end
|
||||
|
||||
test "calculates liability group totals" do
|
||||
create_account(balance: 1000, accountable: CreditCard.new)
|
||||
create_account(balance: 2000, accountable: CreditCard.new)
|
||||
create_account(balance: 3000, accountable: OtherLiability.new)
|
||||
create_account(balance: 5000, accountable: OtherLiability.new)
|
||||
create_account(balance: 10000, accountable: Depository.new) # ignored
|
||||
|
||||
liability_groups = BalanceSheet.new(@family).account_groups("liability")
|
||||
|
||||
assert_equal 2, liability_groups.size
|
||||
assert_equal 1000 + 2000, liability_groups.find { |ag| ag.name == "Credit Cards" }.total
|
||||
assert_equal 3000 + 5000, liability_groups.find { |ag| ag.name == "Other Liabilities" }.total
|
||||
end
|
||||
|
||||
private
|
||||
def create_account(attributes = {})
|
||||
account = @family.accounts.create! name: "Test", currency: "USD", **attributes
|
||||
account
|
||||
end
|
||||
end
|
|
@ -1,7 +0,0 @@
|
|||
require "test_helper"
|
||||
|
||||
class CreditCardTest < ActiveSupport::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
|
@ -1,7 +0,0 @@
|
|||
require "test_helper"
|
||||
|
||||
class CryptoTest < ActiveSupport::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
|
@ -1,7 +0,0 @@
|
|||
require "test_helper"
|
||||
|
||||
class DepositoryTest < ActiveSupport::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
56
test/models/family/auto_transfer_matchable_test.rb
Normal file
56
test/models/family/auto_transfer_matchable_test.rb
Normal file
|
@ -0,0 +1,56 @@
|
|||
require "test_helper"
|
||||
|
||||
class Family::AutoTransferMatchableTest < ActiveSupport::TestCase
|
||||
include Account::EntriesTestHelper
|
||||
|
||||
setup do
|
||||
@family = families(:dylan_family)
|
||||
@depository = accounts(:depository)
|
||||
@credit_card = accounts(:credit_card)
|
||||
end
|
||||
|
||||
test "auto-matches transfers" do
|
||||
outflow_entry = create_transaction(date: 1.day.ago.to_date, account: @depository, amount: 500)
|
||||
inflow_entry = create_transaction(date: Date.current, account: @credit_card, amount: -500)
|
||||
|
||||
assert_difference -> { Transfer.count } => 1 do
|
||||
@family.auto_match_transfers!
|
||||
end
|
||||
end
|
||||
|
||||
# In this scenario, our matching logic should find 4 potential matches. These matches should be ranked based on
|
||||
# days apart, then de-duplicated so that we aren't auto-matching the same transaction across multiple transfers.
|
||||
test "when 2 options exist, only auto-match one at a time, ranked by days apart" do
|
||||
yesterday_outflow = create_transaction(date: 1.day.ago.to_date, account: @depository, amount: 500)
|
||||
yesterday_inflow = create_transaction(date: 1.day.ago.to_date, account: @credit_card, amount: -500)
|
||||
|
||||
today_outflow = create_transaction(date: Date.current, account: @depository, amount: 500)
|
||||
today_inflow = create_transaction(date: Date.current, account: @credit_card, amount: -500)
|
||||
|
||||
assert_difference -> { Transfer.count } => 2 do
|
||||
@family.auto_match_transfers!
|
||||
end
|
||||
end
|
||||
|
||||
test "does not auto-match any transfers that have been rejected by user already" do
|
||||
outflow = create_transaction(date: Date.current, account: @depository, amount: 500)
|
||||
inflow = create_transaction(date: Date.current, account: @credit_card, amount: -500)
|
||||
|
||||
RejectedTransfer.create!(inflow_transaction_id: inflow.entryable_id, outflow_transaction_id: outflow.entryable_id)
|
||||
|
||||
assert_no_difference -> { Transfer.count } do
|
||||
@family.auto_match_transfers!
|
||||
end
|
||||
end
|
||||
|
||||
test "does not consider inactive accounts when matching transfers" do
|
||||
@depository.update!(is_active: false)
|
||||
|
||||
outflow = create_transaction(date: Date.current, account: @depository, amount: 500)
|
||||
inflow = create_transaction(date: Date.current, account: @credit_card, amount: -500)
|
||||
|
||||
assert_no_difference -> { Transfer.count } do
|
||||
@family.auto_match_transfers!
|
||||
end
|
||||
end
|
||||
end
|
|
@ -26,135 +26,4 @@ class FamilyTest < ActiveSupport::TestCase
|
|||
|
||||
@syncable.sync_data(start_date: family_sync.start_date)
|
||||
end
|
||||
|
||||
test "calculates assets" do
|
||||
assert_equal Money.new(0, @family.currency), @family.assets
|
||||
|
||||
create_account(balance: 1000, accountable: Depository.new)
|
||||
create_account(balance: 5000, accountable: OtherAsset.new)
|
||||
create_account(balance: 10000, accountable: CreditCard.new) # ignored
|
||||
|
||||
assert_equal Money.new(1000 + 5000, @family.currency), @family.assets
|
||||
end
|
||||
|
||||
test "calculates liabilities" do
|
||||
assert_equal Money.new(0, @family.currency), @family.liabilities
|
||||
|
||||
create_account(balance: 1000, accountable: CreditCard.new)
|
||||
create_account(balance: 5000, accountable: OtherLiability.new)
|
||||
create_account(balance: 10000, accountable: Depository.new) # ignored
|
||||
|
||||
assert_equal Money.new(1000 + 5000, @family.currency), @family.liabilities
|
||||
end
|
||||
|
||||
test "calculates net worth" do
|
||||
assert_equal Money.new(0, @family.currency), @family.net_worth
|
||||
|
||||
create_account(balance: 1000, accountable: CreditCard.new)
|
||||
create_account(balance: 50000, accountable: Depository.new)
|
||||
|
||||
assert_equal Money.new(50000 - 1000, @family.currency), @family.net_worth
|
||||
end
|
||||
|
||||
test "should exclude disabled accounts from calculations" do
|
||||
cc = create_account(balance: 1000, accountable: CreditCard.new)
|
||||
create_account(balance: 50000, accountable: Depository.new)
|
||||
|
||||
assert_equal Money.new(50000 - 1000, @family.currency), @family.net_worth
|
||||
|
||||
cc.update! is_active: false
|
||||
|
||||
assert_equal Money.new(50000, @family.currency), @family.net_worth
|
||||
end
|
||||
|
||||
test "calculates snapshot" do
|
||||
asset = create_account(balance: 500, accountable: Depository.new)
|
||||
liability = create_account(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 = create_account(balance: 500, accountable: Depository.new)
|
||||
savings_account = create_account(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)
|
||||
|
||||
zero_income_zero_expense_account = create_account(balance: 200, accountable: Depository.new)
|
||||
create_transaction(account: zero_income_zero_expense_account, amount: 0)
|
||||
|
||||
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.map(&:spending)
|
||||
assert_equal [ 5000, 1000 ], top_earners.map(&:income)
|
||||
assert_equal [ 1, 0.99 ], top_savers.map(&:savings_rate)
|
||||
end
|
||||
|
||||
|
||||
test "calculates rolling transaction totals" do
|
||||
account = create_account(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
|
||||
|
||||
private
|
||||
|
||||
def create_account(attributes = {})
|
||||
account = @family.accounts.create! name: "Test", currency: "USD", **attributes
|
||||
account
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
require "test_helper"
|
||||
|
||||
class ImpersonationSessionLogTest < ActiveSupport::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
|
@ -1,7 +0,0 @@
|
|||
require "test_helper"
|
||||
|
||||
class Import::MappingTest < ActiveSupport::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
|
@ -1,7 +0,0 @@
|
|||
require "test_helper"
|
||||
|
||||
class Import::RowTest < ActiveSupport::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
48
test/models/income_statement_test.rb
Normal file
48
test/models/income_statement_test.rb
Normal file
|
@ -0,0 +1,48 @@
|
|||
require "test_helper"
|
||||
|
||||
class IncomeStatementTest < ActiveSupport::TestCase
|
||||
include Account::EntriesTestHelper
|
||||
|
||||
setup do
|
||||
@family = families(:empty)
|
||||
|
||||
@income_category = @family.categories.create! name: "Income", classification: "income"
|
||||
@food_category = @family.categories.create! name: "Food", classification: "expense"
|
||||
@shopping_category = @family.categories.create! name: "Shopping", 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
|
||||
|
||||
create_transaction(account: @checking_account, amount: -1000, category: @food_category)
|
||||
create_transaction(account: @checking_account, amount: 200, category: @shopping_category)
|
||||
create_transaction(account: @credit_card_account, amount: 300, category: @food_category)
|
||||
create_transaction(account: @credit_card_account, amount: 400, category: @shopping_category)
|
||||
end
|
||||
|
||||
test "calculates totals for transactions" do
|
||||
income_statement = IncomeStatement.new(@family)
|
||||
assert_equal Money.new(1000, @family.currency), income_statement.totals.income_money
|
||||
assert_equal Money.new(200 + 300 + 400, @family.currency), income_statement.totals.expense_money
|
||||
assert_equal 4, income_statement.totals.transactions_count
|
||||
end
|
||||
|
||||
test "calculates expenses for a period" do
|
||||
income_statement = IncomeStatement.new(@family)
|
||||
assert_equal 200 + 300 + 400, income_statement.expense_totals(period: Period.last_30_days).total
|
||||
end
|
||||
|
||||
test "calculates income for a period" do
|
||||
income_statement = IncomeStatement.new(@family)
|
||||
assert_equal 1000, income_statement.income_totals(period: Period.last_30_days).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
|
||||
end
|
|
@ -1,7 +0,0 @@
|
|||
require "test_helper"
|
||||
|
||||
class InvestmentTest < ActiveSupport::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
|
@ -1,7 +0,0 @@
|
|||
require "test_helper"
|
||||
|
||||
class IssueTest < ActiveSupport::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
|
@ -1,7 +0,0 @@
|
|||
require "test_helper"
|
||||
|
||||
class OtherAssetTest < ActiveSupport::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
|
@ -1,7 +0,0 @@
|
|||
require "test_helper"
|
||||
|
||||
class OtherLiabilityTest < ActiveSupport::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
61
test/models/period_test.rb
Normal file
61
test/models/period_test.rb
Normal file
|
@ -0,0 +1,61 @@
|
|||
require "test_helper"
|
||||
|
||||
class PeriodTest < ActiveSupport::TestCase
|
||||
test "raises validation error when start_date or end_date is missing" do
|
||||
error = assert_raises(ActiveModel::ValidationError) do
|
||||
Period.new(start_date: nil, end_date: nil)
|
||||
end
|
||||
|
||||
assert_includes error.message, "Start date can't be blank"
|
||||
assert_includes error.message, "End date can't be blank"
|
||||
end
|
||||
|
||||
test "raises validation error when start_date is not before end_date" do
|
||||
error = assert_raises(ActiveModel::ValidationError) do
|
||||
Period.new(start_date: Date.current, end_date: Date.current - 1.day)
|
||||
end
|
||||
|
||||
assert_includes error.message, "Start date must be before end date"
|
||||
end
|
||||
|
||||
test "from_key returns period for valid key" do
|
||||
period = Period.from_key("last_30_days")
|
||||
assert_equal 30.days.ago.to_date, period.start_date
|
||||
assert_equal Date.current, period.end_date
|
||||
end
|
||||
|
||||
test "from_key with invalid key and fallback returns default period" do
|
||||
period = Period.from_key("invalid_key", fallback: true)
|
||||
assert_equal 30.days.ago.to_date, period.start_date
|
||||
assert_equal Date.current, period.end_date
|
||||
end
|
||||
|
||||
test "from_key with invalid key and no fallback raises error" do
|
||||
assert_raises ArgumentError do
|
||||
Period.from_key("invalid_key")
|
||||
end
|
||||
end
|
||||
|
||||
test "label returns correct label for known period" do
|
||||
period = Period.from_key("last_30_days")
|
||||
assert_equal "Last 30 Days", period.label
|
||||
end
|
||||
|
||||
test "label returns Custom Period for unknown period" do
|
||||
period = Period.new(start_date: Date.current - 15.days, end_date: Date.current)
|
||||
assert_equal "Custom Period", period.label
|
||||
end
|
||||
|
||||
test "comparison_label returns correct label for known period" do
|
||||
period = Period.from_key("last_30_days")
|
||||
assert_equal "vs. last month", period.comparison_label
|
||||
end
|
||||
|
||||
test "comparison_label returns date range for unknown period" do
|
||||
start_date = Date.current - 15.days
|
||||
end_date = Date.current
|
||||
period = Period.new(start_date: start_date, end_date: end_date)
|
||||
expected = "#{start_date.strftime("%b %d, %Y")} to #{end_date.strftime("%b %d, %Y")}"
|
||||
assert_equal expected, period.comparison_label
|
||||
end
|
||||
end
|
|
@ -1,7 +0,0 @@
|
|||
require "test_helper"
|
||||
|
||||
class PropertyTest < ActiveSupport::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
|
@ -1,7 +0,0 @@
|
|||
require "test_helper"
|
||||
|
||||
class SecurityTest < ActiveSupport::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
|
@ -1,7 +0,0 @@
|
|||
require "test_helper"
|
||||
|
||||
class SessionTest < ActiveSupport::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
|
@ -1,49 +0,0 @@
|
|||
require "test_helper"
|
||||
|
||||
class TimeSeries::TrendTest < ActiveSupport::TestCase
|
||||
test "handles money trend" do
|
||||
trend = TimeSeries::Trend.new(current: Money.new(100), previous: Money.new(50))
|
||||
assert_equal "up", trend.direction
|
||||
assert_equal Money.new(50), trend.value
|
||||
assert_equal 100.0, trend.percent
|
||||
end
|
||||
|
||||
test "up" do
|
||||
trend = TimeSeries::Trend.new(current: 100, previous: 50)
|
||||
assert_equal "up", trend.direction
|
||||
assert_equal "#10A861", trend.color
|
||||
end
|
||||
|
||||
test "down" do
|
||||
trend = TimeSeries::Trend.new(current: 50, previous: 100)
|
||||
assert_equal "down", trend.direction
|
||||
assert_equal "#F13636", trend.color
|
||||
end
|
||||
|
||||
test "flat" do
|
||||
trend1 = TimeSeries::Trend.new(current: 100, previous: 100)
|
||||
trend2 = TimeSeries::Trend.new(current: 100, previous: nil)
|
||||
trend3 = TimeSeries::Trend.new(current: nil, previous: nil)
|
||||
assert_equal "flat", trend1.direction
|
||||
assert_equal "flat", trend2.direction
|
||||
assert_equal "flat", trend3.direction
|
||||
assert_equal "#737373", trend1.color
|
||||
end
|
||||
|
||||
test "infinitely up" do
|
||||
trend = TimeSeries::Trend.new(current: 100, previous: 0)
|
||||
assert_equal "up", trend.direction
|
||||
end
|
||||
|
||||
test "infinitely down" do
|
||||
trend1 = TimeSeries::Trend.new(current: nil, previous: 100)
|
||||
trend2 = TimeSeries::Trend.new(current: 0, previous: 100)
|
||||
assert_equal "down", trend1.direction
|
||||
assert_equal "down", trend2.direction
|
||||
end
|
||||
|
||||
test "empty" do
|
||||
trend = TimeSeries::Trend.new(current: nil, previous: nil)
|
||||
assert_equal "flat", trend.direction
|
||||
end
|
||||
end
|
|
@ -1,102 +0,0 @@
|
|||
require "test_helper"
|
||||
|
||||
class TimeSeriesTest < ActiveSupport::TestCase
|
||||
test "it can accept array of money values" do
|
||||
series = TimeSeries.new([ { date: 1.day.ago.to_date, value: Money.new(100) }, { date: Date.current, value: Money.new(200) } ])
|
||||
|
||||
assert_equal Money.new(100), series.first.value
|
||||
assert_equal Money.new(200), series.last.value
|
||||
assert_equal "up", series.favorable_direction
|
||||
assert_equal "up", series.trend.direction
|
||||
assert_equal Money.new(100), series.trend.value
|
||||
assert_equal 100.0, series.trend.percent
|
||||
end
|
||||
|
||||
test "it can accept array of numeric values" do
|
||||
series = TimeSeries.new([ { date: 1.day.ago.to_date, value: 100 }, { date: Date.current, value: 200 } ])
|
||||
|
||||
assert_equal 100, series.first.value
|
||||
assert_equal 200, series.last.value
|
||||
assert_equal 100, series.on(1.day.ago.to_date).value
|
||||
assert_equal "up", series.favorable_direction
|
||||
assert_equal "up", series.trend.direction
|
||||
assert_equal 100, series.trend.value
|
||||
assert_equal 100.0, series.trend.percent
|
||||
end
|
||||
|
||||
test "when empty array passed, it returns empty series" do
|
||||
series = TimeSeries.new([])
|
||||
|
||||
assert_nil series.first
|
||||
assert_nil series.last
|
||||
assert_equal({ values: [], trend: { favorable_direction: "up", direction: "flat", value: 0, percent: 0.0 }, favorable_direction: "up" }.to_json, series.to_json)
|
||||
end
|
||||
|
||||
test "money series can be serialized to json" do
|
||||
expected_values = {
|
||||
values: [
|
||||
{
|
||||
date: 1.day.ago.to_date,
|
||||
value: { amount: "100.0", currency: "USD" },
|
||||
trend: { favorable_direction: "up", direction: "flat", value: { amount: "0.0", currency: "USD" }, percent: 0.0 }
|
||||
},
|
||||
{
|
||||
date: Date.current,
|
||||
value: { amount: "200.0", currency: "USD" },
|
||||
trend: { favorable_direction: "up", direction: "up", value: { amount: "100.0", currency: "USD" }, percent: 100.0 }
|
||||
}
|
||||
],
|
||||
trend: { favorable_direction: "up", direction: "up", value: { amount: "100.0", currency: "USD" }, percent: 100.0 },
|
||||
favorable_direction: "up"
|
||||
}.to_json
|
||||
|
||||
series = TimeSeries.new([ { date: 1.day.ago.to_date, value: Money.new(100) }, { date: Date.current, value: Money.new(200) } ])
|
||||
|
||||
assert_equal expected_values, series.to_json
|
||||
end
|
||||
|
||||
test "numeric series can be serialized to json" do
|
||||
expected_values = {
|
||||
values: [
|
||||
{ date: 1.day.ago.to_date, value: 100, trend: { favorable_direction: "up", direction: "flat", value: 0, percent: 0.0 } },
|
||||
{ date: Date.current, value: 200, trend: { favorable_direction: "up", direction: "up", value: 100, percent: 100.0 } }
|
||||
],
|
||||
trend: { favorable_direction: "up", direction: "up", value: 100, percent: 100.0 },
|
||||
favorable_direction: "up"
|
||||
}.to_json
|
||||
|
||||
series = TimeSeries.new([ { date: 1.day.ago.to_date, value: 100 }, { date: Date.current, value: 200 } ])
|
||||
|
||||
assert_equal expected_values, series.to_json
|
||||
end
|
||||
|
||||
test "it does not accept invalid values in Time Series Trend" do
|
||||
error = assert_raises(ActiveModel::ValidationError) do
|
||||
TimeSeries.new(
|
||||
[
|
||||
{ date: 1.day.ago.to_date, value: 100 },
|
||||
{ date: Date.current, value: "two hundred" }
|
||||
]
|
||||
)
|
||||
end
|
||||
assert_match(/Current must be of the same type as previous/, error.message)
|
||||
assert_match(/Previous must be of the same type as current/, error.message)
|
||||
assert_match(/Current must be of type Money, Numeric, or nil/, error.message)
|
||||
end
|
||||
|
||||
|
||||
test "it does not accept invalid values in Time Series Value" do
|
||||
# We need to stub trend otherwise an error is raised before TimeSeries::Value validation
|
||||
TimeSeries::Trend.stub(:new, nil) do
|
||||
error = assert_raises(ActiveModel::ValidationError) do
|
||||
TimeSeries.new(
|
||||
[
|
||||
{ date: 1.day.ago.to_date, value: 100 },
|
||||
{ date: Date.current, value: "two hundred" }
|
||||
]
|
||||
)
|
||||
end
|
||||
assert_equal "Validation failed: Value must be a Money or Numeric", error.message
|
||||
end
|
||||
end
|
||||
end
|
40
test/models/trend_test.rb
Normal file
40
test/models/trend_test.rb
Normal file
|
@ -0,0 +1,40 @@
|
|||
require "test_helper"
|
||||
|
||||
class TrendTest < ActiveSupport::TestCase
|
||||
test "handles money trend" do
|
||||
trend = Trend.new(current: Money.new(100), previous: Money.new(50))
|
||||
assert_equal "up", trend.direction
|
||||
assert_equal Money.new(50), trend.value
|
||||
assert_equal 100.0, trend.percent
|
||||
end
|
||||
|
||||
test "up" do
|
||||
trend = Trend.new(current: 100, previous: 50)
|
||||
assert_equal "up", trend.direction
|
||||
assert_equal "var(--color-success)", trend.color
|
||||
end
|
||||
|
||||
test "down" do
|
||||
trend = Trend.new(current: 50, previous: 100)
|
||||
assert_equal "down", trend.direction
|
||||
assert_equal "var(--color-destructive)", trend.color
|
||||
end
|
||||
|
||||
test "flat" do
|
||||
trend1 = Trend.new(current: 100, previous: 100)
|
||||
trend2 = Trend.new(current: 100, previous: nil)
|
||||
assert_equal "flat", trend1.direction
|
||||
assert_equal "up", trend2.direction
|
||||
assert_equal "var(--color-gray)", trend1.color
|
||||
end
|
||||
|
||||
test "infinitely up" do
|
||||
trend = Trend.new(current: 100, previous: 0)
|
||||
assert_equal "up", trend.direction
|
||||
end
|
||||
|
||||
test "infinitely down" do
|
||||
trend = Trend.new(current: 0, previous: 100)
|
||||
assert_equal "down", trend.direction
|
||||
end
|
||||
end
|
|
@ -1,141 +0,0 @@
|
|||
require "test_helper"
|
||||
require "ostruct"
|
||||
class ValueGroupTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
# Level 1
|
||||
@assets = ValueGroup.new("Assets", :usd)
|
||||
|
||||
# Level 2
|
||||
@depositories = @assets.add_child_group("Depositories", :usd)
|
||||
@other_assets = @assets.add_child_group("Other Assets", :usd)
|
||||
|
||||
# Level 3 (leaf/value nodes)
|
||||
@checking_node = @depositories.add_value_node(OpenStruct.new({ name: "Checking", value: Money.new(5000) }), Money.new(5000))
|
||||
@savings_node = @depositories.add_value_node(OpenStruct.new({ name: "Savings", value: Money.new(20000) }), Money.new(20000))
|
||||
@collectable_node = @other_assets.add_value_node(OpenStruct.new({ name: "Collectable", value: Money.new(550) }), Money.new(550))
|
||||
end
|
||||
|
||||
test "empty group works" do
|
||||
group = ValueGroup.new("Root", :usd)
|
||||
|
||||
assert_equal "Root", group.name
|
||||
assert_equal [], group.children
|
||||
assert_equal 0, group.sum
|
||||
assert_equal 0, group.avg
|
||||
assert_equal 100, group.percent_of_total
|
||||
assert_nil group.parent
|
||||
end
|
||||
|
||||
test "group without value nodes has no value" do
|
||||
assets = ValueGroup.new("Assets")
|
||||
depositories = assets.add_child_group("Depositories")
|
||||
|
||||
assert_equal 0, assets.sum
|
||||
assert_equal 0, depositories.sum
|
||||
end
|
||||
|
||||
test "sum equals value at leaf level" do
|
||||
assert_equal @checking_node.value, @checking_node.sum
|
||||
assert_equal @savings_node.value, @savings_node.sum
|
||||
assert_equal @collectable_node.value, @collectable_node.sum
|
||||
end
|
||||
|
||||
test "value is nil at rollup levels" do
|
||||
assert_not_equal @depositories.value, @depositories.sum
|
||||
assert_nil @depositories.value
|
||||
assert_nil @other_assets.value
|
||||
end
|
||||
|
||||
test "generates list of value nodes regardless of level in hierarchy" do
|
||||
assert_equal [ @checking_node, @savings_node, @collectable_node ], @assets.value_nodes
|
||||
assert_equal [ @checking_node, @savings_node ], @depositories.value_nodes
|
||||
assert_equal [ @collectable_node ], @other_assets.value_nodes
|
||||
end
|
||||
|
||||
test "group with value nodes aggregates totals correctly" do
|
||||
assert_equal Money.new(5000), @checking_node.sum
|
||||
assert_equal Money.new(20000), @savings_node.sum
|
||||
assert_equal Money.new(550), @collectable_node.sum
|
||||
|
||||
assert_equal Money.new(25000), @depositories.sum
|
||||
assert_equal Money.new(550), @other_assets.sum
|
||||
|
||||
assert_equal Money.new(25550), @assets.sum
|
||||
end
|
||||
|
||||
test "group averages leaf nodes" do
|
||||
assert_equal Money.new(5000), @checking_node.avg
|
||||
assert_equal Money.new(20000), @savings_node.avg
|
||||
assert_equal Money.new(550), @collectable_node.avg
|
||||
|
||||
assert_in_delta 12500, @depositories.avg.amount, 0.01
|
||||
assert_in_delta 550, @other_assets.avg.amount, 0.01
|
||||
assert_in_delta 8516.67, @assets.avg.amount, 0.01
|
||||
end
|
||||
|
||||
# Percentage of parent group (i.e. collectable is 100% of "Other Assets" group)
|
||||
test "group calculates percent of parent total" do
|
||||
assert_equal 100, @assets.percent_of_total
|
||||
assert_in_delta 97.85, @depositories.percent_of_total, 0.1
|
||||
assert_in_delta 2.15, @other_assets.percent_of_total, 0.1
|
||||
assert_in_delta 80.0, @savings_node.percent_of_total, 0.1
|
||||
assert_in_delta 20.0, @checking_node.percent_of_total, 0.1
|
||||
assert_equal 100, @collectable_node.percent_of_total
|
||||
end
|
||||
|
||||
test "handles unbalanced tree" do
|
||||
vehicles = @assets.add_child_group("Vehicles")
|
||||
|
||||
# Since we didn't add any value nodes to vehicles, shouldn't affect rollups
|
||||
assert_equal Money.new(25550), @assets.sum
|
||||
end
|
||||
|
||||
|
||||
test "can attach and aggregate time series" do
|
||||
checking_series = TimeSeries.new([ { date: 1.day.ago.to_date, value: Money.new(4000) }, { date: Date.current, value: Money.new(5000) } ])
|
||||
savings_series = TimeSeries.new([ { date: 1.day.ago.to_date, value: Money.new(19000) }, { date: Date.current, value: Money.new(20000) } ])
|
||||
|
||||
@checking_node.series = checking_series
|
||||
@savings_node.series = savings_series
|
||||
|
||||
assert_not_nil @checking_node.series
|
||||
assert_not_nil @savings_node.series
|
||||
|
||||
assert_equal @checking_node.sum, @checking_node.series.last.value
|
||||
assert_equal @savings_node.sum, @savings_node.series.last.value
|
||||
|
||||
aggregated_depository_series = TimeSeries.new([ { date: 1.day.ago.to_date, value: Money.new(23000) }, { date: Date.current, value: Money.new(25000) } ])
|
||||
aggregated_assets_series = TimeSeries.new([ { date: 1.day.ago.to_date, value: Money.new(23000) }, { date: Date.current, value: Money.new(25000) } ])
|
||||
|
||||
assert_equal aggregated_depository_series.values, @depositories.series.values
|
||||
assert_equal aggregated_assets_series.values, @assets.series.values
|
||||
end
|
||||
|
||||
test "attached series must be a TimeSeries" do
|
||||
assert_raises(RuntimeError) do
|
||||
@checking_node.series = []
|
||||
end
|
||||
end
|
||||
|
||||
test "cannot add time series to non-leaf node" do
|
||||
assert_raises(RuntimeError) do
|
||||
@assets.series = TimeSeries.new([])
|
||||
end
|
||||
end
|
||||
|
||||
test "can only add value node at leaf level of tree" do
|
||||
root = ValueGroup.new("Root Level")
|
||||
grandparent = root.add_child_group("Grandparent")
|
||||
parent = grandparent.add_child_group("Parent")
|
||||
|
||||
value_node = parent.add_value_node(OpenStruct.new({ name: "Value Node", value: Money.new(100) }), Money.new(100))
|
||||
|
||||
assert_raises(RuntimeError) do
|
||||
value_node.add_value_node(OpenStruct.new({ name: "Value Node", value: Money.new(100) }), Money.new(100))
|
||||
end
|
||||
|
||||
assert_raises(RuntimeError) do
|
||||
grandparent.add_value_node(OpenStruct.new({ name: "Value Node", value: Money.new(100) }), Money.new(100))
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,7 +0,0 @@
|
|||
require "test_helper"
|
||||
|
||||
class VehicleTest < ActiveSupport::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
|
@ -72,11 +72,14 @@ class AccountsTest < ApplicationSystemTestCase
|
|||
private
|
||||
|
||||
def open_new_account_modal
|
||||
click_link "sidebar-new-account"
|
||||
within "[data-controller='tabs']" do
|
||||
click_button "All"
|
||||
click_link "New account"
|
||||
end
|
||||
end
|
||||
|
||||
def assert_account_created(accountable_type, &block)
|
||||
click_link humanized_accountable(accountable_type)
|
||||
click_link Accountable.from_type(accountable_type).display_name.singularize
|
||||
click_link "Enter account balance" if accountable_type.in?(%w[Depository Investment Crypto Loan CreditCard])
|
||||
|
||||
account_name = "[system test] #{accountable_type} Account"
|
||||
|
@ -88,8 +91,10 @@ class AccountsTest < ApplicationSystemTestCase
|
|||
|
||||
click_button "Create Account"
|
||||
|
||||
find("details", text: humanized_accountable(accountable_type)).click
|
||||
assert_text account_name
|
||||
within "[data-controller='tabs']" do
|
||||
find("details", text: Accountable.from_type(accountable_type).display_name).click
|
||||
assert_text account_name
|
||||
end
|
||||
|
||||
visit accounts_url
|
||||
assert_text account_name
|
||||
|
@ -109,6 +114,6 @@ class AccountsTest < ApplicationSystemTestCase
|
|||
end
|
||||
|
||||
def humanized_accountable(accountable_type)
|
||||
accountable_type.constantize.model_name.human
|
||||
Accountable.from_type(accountable_type).display_name.singularize
|
||||
end
|
||||
end
|
||||
|
|
|
@ -201,7 +201,7 @@ class TransactionsTest < ApplicationSystemTestCase
|
|||
investment_account = accounts(:investment)
|
||||
outflow_entry = create_transaction("outflow", Date.current, 500, account: asset_account)
|
||||
inflow_entry = create_transaction("inflow", 1.day.ago.to_date, -500, account: investment_account)
|
||||
asset_account.auto_match_transfers!
|
||||
@user.family.auto_match_transfers!
|
||||
visit transactions_url
|
||||
within "#entry-group-" + Date.current.to_s + "-totals" do
|
||||
assert_text "-$100.00" # transaction eleven from setup
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue