1
0
Fork 0
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:
Zach Gollwitzer 2025-02-21 11:57:59 -05:00 committed by GitHub
parent 8539ac7dec
commit d75be2282b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
278 changed files with 3428 additions and 4354 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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"

View file

@ -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

View file

@ -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 ]

View 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

View file

@ -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

View file

@ -1,7 +0,0 @@
require "test_helper"
class Account::IssueTest < ActiveSupport::TestCase
test "the truth" do
assert true
end
end

View file

@ -1,4 +0,0 @@
require "test_helper"
class Account::TradeTest < ActiveSupport::TestCase
end

View file

@ -0,0 +1,5 @@
require "test_helper"
class Account::TransactionTest < ActiveSupport::TestCase
include Account::EntriesTestHelper
end

View file

@ -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

View 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

View file

@ -1,7 +0,0 @@
require "test_helper"
class CreditCardTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end

View file

@ -1,7 +0,0 @@
require "test_helper"
class CryptoTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end

View file

@ -1,7 +0,0 @@
require "test_helper"
class DepositoryTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end

View 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

View file

@ -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

View file

@ -1,7 +0,0 @@
require "test_helper"
class ImpersonationSessionLogTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end

View file

@ -1,7 +0,0 @@
require "test_helper"
class Import::MappingTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end

View file

@ -1,7 +0,0 @@
require "test_helper"
class Import::RowTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end

View 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

View file

@ -1,7 +0,0 @@
require "test_helper"
class InvestmentTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end

View file

@ -1,7 +0,0 @@
require "test_helper"
class IssueTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end

View file

@ -1,7 +0,0 @@
require "test_helper"
class OtherAssetTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end

View file

@ -1,7 +0,0 @@
require "test_helper"
class OtherLiabilityTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end

View 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

View file

@ -1,7 +0,0 @@
require "test_helper"
class PropertyTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end

View file

@ -1,7 +0,0 @@
require "test_helper"
class SecurityTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end

View file

@ -1,7 +0,0 @@
require "test_helper"
class SessionTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end

View file

@ -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

View file

@ -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
View 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

View file

@ -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

View file

@ -1,7 +0,0 @@
require "test_helper"
class VehicleTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end

View file

@ -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

View file

@ -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