mirror of
https://github.com/we-promise/sure
synced 2026-04-25 17:15:07 +02:00
Ensure accessible_account_ids filtering is applied whenever account scope is provided, including empty arrays, so users with no shared accounts cannot see family-wide transactions. Also make totals robust when scoped queries return no rows and add regression tests for both visibility and totals behavior with empty accessible account lists.
650 lines
22 KiB
Ruby
650 lines
22 KiB
Ruby
require "test_helper"
|
|
|
|
class Transaction::SearchTest < ActiveSupport::TestCase
|
|
include EntriesTestHelper
|
|
|
|
setup do
|
|
@family = families(:dylan_family)
|
|
@checking_account = accounts(:depository)
|
|
@credit_card_account = accounts(:credit_card)
|
|
@loan_account = accounts(:loan)
|
|
|
|
# Clean up existing entries/transactions from fixtures to ensure test isolation
|
|
@family.accounts.each { |account| account.entries.delete_all }
|
|
end
|
|
|
|
test "search filters by transaction types using kind enum" do
|
|
# Create different types of transactions using the helper method
|
|
standard_entry = create_transaction(
|
|
account: @checking_account,
|
|
amount: 100,
|
|
category: categories(:food_and_drink),
|
|
kind: "standard"
|
|
)
|
|
|
|
transfer_entry = create_transaction(
|
|
account: @checking_account,
|
|
amount: 200,
|
|
kind: "funds_movement"
|
|
)
|
|
|
|
payment_entry = create_transaction(
|
|
account: @credit_card_account,
|
|
amount: -300,
|
|
kind: "cc_payment"
|
|
)
|
|
|
|
loan_payment_entry = create_transaction(
|
|
account: @loan_account,
|
|
amount: 400,
|
|
kind: "loan_payment"
|
|
)
|
|
|
|
one_time_entry = create_transaction(
|
|
account: @checking_account,
|
|
amount: 500,
|
|
kind: "one_time"
|
|
)
|
|
|
|
# Test transfer type filter (includes loan_payment)
|
|
transfer_results = Transaction::Search.new(@family, filters: { types: [ "transfer" ] }).transactions_scope
|
|
transfer_ids = transfer_results.pluck(:id)
|
|
|
|
assert_includes transfer_ids, transfer_entry.entryable.id
|
|
assert_includes transfer_ids, payment_entry.entryable.id
|
|
assert_includes transfer_ids, loan_payment_entry.entryable.id
|
|
assert_not_includes transfer_ids, one_time_entry.entryable.id
|
|
assert_not_includes transfer_ids, standard_entry.entryable.id
|
|
|
|
# Test expense type filter (excludes transfer kinds but includes one_time)
|
|
expense_results = Transaction::Search.new(@family, filters: { types: [ "expense" ] }).transactions_scope
|
|
expense_ids = expense_results.pluck(:id)
|
|
|
|
assert_includes expense_ids, standard_entry.entryable.id
|
|
assert_includes expense_ids, one_time_entry.entryable.id
|
|
assert_not_includes expense_ids, loan_payment_entry.entryable.id
|
|
assert_not_includes expense_ids, transfer_entry.entryable.id
|
|
assert_not_includes expense_ids, payment_entry.entryable.id
|
|
|
|
# Test income type filter
|
|
income_entry = create_transaction(
|
|
account: @checking_account,
|
|
amount: -600,
|
|
kind: "standard"
|
|
)
|
|
|
|
income_results = Transaction::Search.new(@family, filters: { types: [ "income" ] }).transactions_scope
|
|
income_ids = income_results.pluck(:id)
|
|
|
|
assert_includes income_ids, income_entry.entryable.id
|
|
assert_not_includes income_ids, standard_entry.entryable.id
|
|
assert_not_includes income_ids, loan_payment_entry.entryable.id
|
|
assert_not_includes income_ids, transfer_entry.entryable.id
|
|
|
|
# Test combined expense and income filter (excludes transfer kinds but includes one_time)
|
|
non_transfer_results = Transaction::Search.new(@family, filters: { types: [ "expense", "income" ] }).transactions_scope
|
|
non_transfer_ids = non_transfer_results.pluck(:id)
|
|
|
|
assert_includes non_transfer_ids, standard_entry.entryable.id
|
|
assert_includes non_transfer_ids, income_entry.entryable.id
|
|
assert_includes non_transfer_ids, one_time_entry.entryable.id
|
|
assert_not_includes non_transfer_ids, loan_payment_entry.entryable.id
|
|
assert_not_includes non_transfer_ids, transfer_entry.entryable.id
|
|
assert_not_includes non_transfer_ids, payment_entry.entryable.id
|
|
end
|
|
|
|
test "search category filter handles uncategorized transactions correctly with kind filtering" do
|
|
# Create uncategorized transactions of different kinds
|
|
uncategorized_standard = create_transaction(
|
|
account: @checking_account,
|
|
amount: 100,
|
|
kind: "standard"
|
|
)
|
|
|
|
uncategorized_transfer = create_transaction(
|
|
account: @checking_account,
|
|
amount: 200,
|
|
kind: "funds_movement"
|
|
)
|
|
|
|
uncategorized_loan_payment = create_transaction(
|
|
account: @loan_account,
|
|
amount: 300,
|
|
kind: "loan_payment"
|
|
)
|
|
|
|
# Search for uncategorized transactions
|
|
uncategorized_results = Transaction::Search.new(@family, filters: { categories: [ Category.uncategorized.name ] }).transactions_scope
|
|
uncategorized_ids = uncategorized_results.pluck(:id)
|
|
|
|
# Should include standard uncategorized transactions
|
|
assert_includes uncategorized_ids, uncategorized_standard.entryable.id
|
|
|
|
# Should exclude all transfer kinds (TRANSFER_KINDS) even if uncategorized
|
|
assert_not_includes uncategorized_ids, uncategorized_transfer.entryable.id
|
|
assert_not_includes uncategorized_ids, uncategorized_loan_payment.entryable.id
|
|
end
|
|
|
|
test "filtering for only Uncategorized returns only uncategorized transactions" do
|
|
# Create a mix of categorized and uncategorized transactions
|
|
categorized = create_transaction(
|
|
account: @checking_account,
|
|
amount: 100,
|
|
category: categories(:food_and_drink)
|
|
)
|
|
|
|
uncategorized = create_transaction(
|
|
account: @checking_account,
|
|
amount: 200
|
|
)
|
|
|
|
# Filter for only uncategorized
|
|
results = Transaction::Search.new(@family, filters: { categories: [ Category.uncategorized.name ] }).transactions_scope
|
|
result_ids = results.pluck(:id)
|
|
|
|
# Should only include uncategorized transaction
|
|
assert_includes result_ids, uncategorized.entryable.id
|
|
assert_not_includes result_ids, categorized.entryable.id
|
|
assert_equal 1, result_ids.size
|
|
end
|
|
|
|
test "filtering for Uncategorized plus a real category returns both" do
|
|
# Create a travel category for testing
|
|
travel_category = @family.categories.create!(
|
|
name: "Travel",
|
|
color: "#3b82f6"
|
|
)
|
|
|
|
# Create transactions with different categories
|
|
food_transaction = create_transaction(
|
|
account: @checking_account,
|
|
amount: 100,
|
|
category: categories(:food_and_drink)
|
|
)
|
|
|
|
travel_transaction = create_transaction(
|
|
account: @checking_account,
|
|
amount: 150,
|
|
category: travel_category
|
|
)
|
|
|
|
uncategorized = create_transaction(
|
|
account: @checking_account,
|
|
amount: 200
|
|
)
|
|
|
|
# Filter for food category + uncategorized
|
|
results = Transaction::Search.new(@family, filters: { categories: [ "Food & Drink", Category.uncategorized.name ] }).transactions_scope
|
|
result_ids = results.pluck(:id)
|
|
|
|
# Should include both food and uncategorized
|
|
assert_includes result_ids, food_transaction.entryable.id
|
|
assert_includes result_ids, uncategorized.entryable.id
|
|
# Should NOT include travel
|
|
assert_not_includes result_ids, travel_transaction.entryable.id
|
|
assert_equal 2, result_ids.size
|
|
end
|
|
|
|
test "filtering excludes uncategorized when not in filter" do
|
|
# Create a mix of transactions
|
|
categorized = create_transaction(
|
|
account: @checking_account,
|
|
amount: 100,
|
|
category: categories(:food_and_drink)
|
|
)
|
|
|
|
uncategorized = create_transaction(
|
|
account: @checking_account,
|
|
amount: 200
|
|
)
|
|
|
|
# Filter for only food category (without Uncategorized)
|
|
results = Transaction::Search.new(@family, filters: { categories: [ "Food & Drink" ] }).transactions_scope
|
|
result_ids = results.pluck(:id)
|
|
|
|
# Should only include categorized transaction
|
|
assert_includes result_ids, categorized.entryable.id
|
|
assert_not_includes result_ids, uncategorized.entryable.id
|
|
assert_equal 1, result_ids.size
|
|
end
|
|
|
|
test "new family-based API works correctly" do
|
|
# Create transactions for testing
|
|
transaction1 = create_transaction(
|
|
account: @checking_account,
|
|
amount: 100,
|
|
category: categories(:food_and_drink),
|
|
kind: "standard"
|
|
)
|
|
|
|
transaction2 = create_transaction(
|
|
account: @checking_account,
|
|
amount: 200,
|
|
kind: "funds_movement"
|
|
)
|
|
|
|
# Test new family-based API
|
|
search = Transaction::Search.new(@family, filters: { types: [ "expense" ] })
|
|
results = search.transactions_scope
|
|
result_ids = results.pluck(:id)
|
|
|
|
# Should include expense transactions
|
|
assert_includes result_ids, transaction1.entryable.id
|
|
# Should exclude transfer transactions
|
|
assert_not_includes result_ids, transaction2.entryable.id
|
|
|
|
# Test that the relation builds from family.transactions correctly
|
|
assert_equal @family.transactions.joins(entry: :account).where(
|
|
"entries.amount >= 0 AND NOT (transactions.kind IN (?))", Transaction::TRANSFER_KINDS
|
|
).count, results.count
|
|
end
|
|
|
|
test "transfer filter includes investment_contribution transactions" do
|
|
investment_contribution = create_transaction(
|
|
account: @checking_account,
|
|
amount: 500,
|
|
kind: "investment_contribution"
|
|
)
|
|
|
|
funds_movement = create_transaction(
|
|
account: @checking_account,
|
|
amount: 200,
|
|
kind: "funds_movement"
|
|
)
|
|
|
|
search = Transaction::Search.new(@family, filters: { types: [ "transfer" ] })
|
|
result_ids = search.transactions_scope.pluck(:id)
|
|
|
|
assert_includes result_ids, investment_contribution.entryable.id
|
|
assert_includes result_ids, funds_movement.entryable.id
|
|
end
|
|
|
|
test "expense filter excludes investment_contribution transactions" do
|
|
investment_contribution = create_transaction(
|
|
account: @checking_account,
|
|
amount: 500,
|
|
kind: "investment_contribution"
|
|
)
|
|
|
|
standard_expense = create_transaction(
|
|
account: @checking_account,
|
|
amount: 100,
|
|
kind: "standard"
|
|
)
|
|
|
|
search = Transaction::Search.new(@family, filters: { types: [ "expense" ] })
|
|
result_ids = search.transactions_scope.pluck(:id)
|
|
|
|
assert_not_includes result_ids, investment_contribution.entryable.id
|
|
assert_includes result_ids, standard_expense.entryable.id
|
|
end
|
|
|
|
test "family-based API requires family parameter" do
|
|
assert_raises(NoMethodError) do
|
|
search = Transaction::Search.new({ types: [ "expense" ] })
|
|
search.transactions_scope # This will fail when trying to call .transactions on a Hash
|
|
end
|
|
end
|
|
|
|
# Totals method tests (lifted from Transaction::TotalsTest)
|
|
|
|
test "totals computes basic expense and income totals" do
|
|
# Create expense transaction
|
|
expense_entry = create_transaction(
|
|
account: @checking_account,
|
|
amount: 100,
|
|
category: categories(:food_and_drink),
|
|
kind: "standard"
|
|
)
|
|
|
|
# Create income transaction
|
|
income_entry = create_transaction(
|
|
account: @checking_account,
|
|
amount: -200,
|
|
kind: "standard"
|
|
)
|
|
|
|
search = Transaction::Search.new(@family)
|
|
totals = search.totals
|
|
|
|
assert_equal 2, totals.count
|
|
assert_equal Money.new(100, "USD"), totals.expense_money # $100
|
|
assert_equal Money.new(200, "USD"), totals.income_money # $200
|
|
end
|
|
|
|
test "totals handles multi-currency transactions with exchange rates" do
|
|
# Create EUR transaction
|
|
eur_entry = create_transaction(
|
|
account: @checking_account,
|
|
amount: 100,
|
|
currency: "EUR",
|
|
kind: "standard"
|
|
)
|
|
|
|
# Create exchange rate EUR -> USD
|
|
ExchangeRate.create!(
|
|
from_currency: "EUR",
|
|
to_currency: "USD",
|
|
rate: 1.1,
|
|
date: eur_entry.date
|
|
)
|
|
|
|
# Create USD transaction
|
|
usd_entry = create_transaction(
|
|
account: @checking_account,
|
|
amount: 50,
|
|
currency: "USD",
|
|
kind: "standard"
|
|
)
|
|
|
|
search = Transaction::Search.new(@family)
|
|
totals = search.totals
|
|
|
|
assert_equal 2, totals.count
|
|
# EUR 100 * 1.1 + USD 50 = 110 + 50 = 160
|
|
assert_equal Money.new(160, "USD"), totals.expense_money
|
|
assert_equal Money.new(0, "USD"), totals.income_money
|
|
end
|
|
|
|
test "totals handles missing exchange rates gracefully" do
|
|
# Create EUR transaction without exchange rate
|
|
eur_entry = create_transaction(
|
|
account: @checking_account,
|
|
amount: 100,
|
|
currency: "EUR",
|
|
kind: "standard"
|
|
)
|
|
|
|
search = Transaction::Search.new(@family)
|
|
totals = search.totals
|
|
|
|
assert_equal 1, totals.count
|
|
# Should use rate of 1 when exchange rate is missing
|
|
assert_equal Money.new(100, "USD"), totals.expense_money # EUR 100 * 1
|
|
assert_equal Money.new(0, "USD"), totals.income_money
|
|
end
|
|
|
|
test "totals respects category filters" do
|
|
# Create transactions in different categories
|
|
food_entry = create_transaction(
|
|
account: @checking_account,
|
|
amount: 100,
|
|
category: categories(:food_and_drink),
|
|
kind: "standard"
|
|
)
|
|
|
|
other_entry = create_transaction(
|
|
account: @checking_account,
|
|
amount: 50,
|
|
category: categories(:income),
|
|
kind: "standard"
|
|
)
|
|
|
|
# Filter by food category only
|
|
search = Transaction::Search.new(@family, filters: { categories: [ "Food & Drink" ] })
|
|
totals = search.totals
|
|
|
|
assert_equal 1, totals.count
|
|
assert_equal Money.new(100, "USD"), totals.expense_money # Only food transaction
|
|
assert_equal Money.new(0, "USD"), totals.income_money
|
|
end
|
|
|
|
test "category filter includes subcategories" do
|
|
# Create a transaction with the parent category
|
|
parent_entry = create_transaction(
|
|
account: @checking_account,
|
|
amount: 100,
|
|
category: categories(:food_and_drink),
|
|
kind: "standard"
|
|
)
|
|
|
|
# Create a transaction with the subcategory (fixture :subcategory has name "Restaurants", parent "Food & Drink")
|
|
subcategory_entry = create_transaction(
|
|
account: @checking_account,
|
|
amount: 75,
|
|
category: categories(:subcategory),
|
|
kind: "standard"
|
|
)
|
|
|
|
# Create a transaction with a different category
|
|
other_entry = create_transaction(
|
|
account: @checking_account,
|
|
amount: 50,
|
|
category: categories(:income),
|
|
kind: "standard"
|
|
)
|
|
|
|
# Filter by parent category only - should include both parent and subcategory transactions
|
|
search = Transaction::Search.new(@family, filters: { categories: [ "Food & Drink" ] })
|
|
results = search.transactions_scope
|
|
result_ids = results.pluck(:id)
|
|
|
|
# Should include both parent and subcategory transactions
|
|
assert_includes result_ids, parent_entry.entryable.id
|
|
assert_includes result_ids, subcategory_entry.entryable.id
|
|
# Should not include transactions with different category
|
|
assert_not_includes result_ids, other_entry.entryable.id
|
|
|
|
# Verify totals also include subcategory transactions
|
|
totals = search.totals
|
|
assert_equal 2, totals.count
|
|
assert_equal Money.new(175, "USD"), totals.expense_money # 100 + 75
|
|
end
|
|
|
|
test "totals respects type filters" do
|
|
# Create expense and income transactions
|
|
expense_entry = create_transaction(
|
|
account: @checking_account,
|
|
amount: 100,
|
|
kind: "standard"
|
|
)
|
|
|
|
income_entry = create_transaction(
|
|
account: @checking_account,
|
|
amount: -200,
|
|
kind: "standard"
|
|
)
|
|
|
|
# Filter by expense type only
|
|
search = Transaction::Search.new(@family, filters: { types: [ "expense" ] })
|
|
totals = search.totals
|
|
|
|
assert_equal 1, totals.count
|
|
assert_equal Money.new(100, "USD"), totals.expense_money
|
|
assert_equal Money.new(0, "USD"), totals.income_money
|
|
end
|
|
|
|
test "totals handles empty results" do
|
|
search = Transaction::Search.new(@family)
|
|
totals = search.totals
|
|
|
|
assert_equal 0, totals.count
|
|
assert_equal Money.new(0, "USD"), totals.expense_money
|
|
assert_equal Money.new(0, "USD"), totals.income_money
|
|
end
|
|
|
|
test "category filter handles non-existent category names without SQL error" do
|
|
# Create a transaction with an existing category
|
|
existing_entry = create_transaction(
|
|
account: @checking_account,
|
|
amount: 100,
|
|
category: categories(:food_and_drink),
|
|
kind: "standard"
|
|
)
|
|
|
|
# Search for non-existent category names (parent_category_ids will be empty)
|
|
# This should not cause a SQL error with "IN ()"
|
|
search = Transaction::Search.new(@family, filters: { categories: [ "Non-Existent Category 1", "Non-Existent Category 2" ] })
|
|
results = search.transactions_scope
|
|
result_ids = results.pluck(:id)
|
|
|
|
# Should not include any transactions since categories don't exist
|
|
assert_not_includes result_ids, existing_entry.entryable.id
|
|
assert_equal 0, result_ids.length
|
|
|
|
# Verify totals also work without error
|
|
totals = search.totals
|
|
assert_equal 0, totals.count
|
|
assert_equal Money.new(0, "USD"), totals.expense_money
|
|
end
|
|
|
|
test "search matches entries name OR notes with ILIKE" do
|
|
# Transaction with matching text in name only
|
|
name_match = create_transaction(
|
|
account: @checking_account,
|
|
amount: 100,
|
|
kind: "standard",
|
|
name: "Grocery Store"
|
|
)
|
|
|
|
# Transaction with matching text in notes only
|
|
notes_match = create_transaction(
|
|
account: @checking_account,
|
|
amount: 50,
|
|
kind: "standard",
|
|
name: "Credit Card Payment",
|
|
notes: "Payment of 50 USD at Grocery Mart on 2026-11-01"
|
|
)
|
|
|
|
# Transaction with no matching text
|
|
no_match = create_transaction(
|
|
account: @checking_account,
|
|
amount: 75,
|
|
kind: "standard",
|
|
name: "Gas station",
|
|
notes: "Fuel refill"
|
|
)
|
|
|
|
search = Transaction::Search.new(
|
|
@family,
|
|
filters: { search: "grocery" }
|
|
)
|
|
|
|
results = search.transactions_scope
|
|
result_ids = results.pluck(:id)
|
|
|
|
# Should match name
|
|
assert_includes result_ids, name_match.entryable.id
|
|
|
|
# Should match notes
|
|
assert_includes result_ids, notes_match.entryable.id
|
|
|
|
# Should not match unrelated transactions
|
|
assert_not_includes result_ids, no_match.entryable.id
|
|
end
|
|
|
|
test "uncategorized filter returns same results across all supported locales" do
|
|
# Create uncategorized transactions
|
|
uncategorized1 = create_transaction(
|
|
account: @checking_account,
|
|
amount: 100,
|
|
kind: "standard"
|
|
)
|
|
|
|
uncategorized2 = create_transaction(
|
|
account: @checking_account,
|
|
amount: 200,
|
|
kind: "standard"
|
|
)
|
|
|
|
# Create a categorized transaction to ensure filter is working
|
|
categorized = create_transaction(
|
|
account: @checking_account,
|
|
amount: 300,
|
|
category: categories(:food_and_drink),
|
|
kind: "standard"
|
|
)
|
|
|
|
# Get the expected count using English locale (known working case)
|
|
I18n.with_locale(:en) do
|
|
english_uncategorized_name = Category.uncategorized.name
|
|
english_results = Transaction::Search.new(@family, filters: { categories: [ english_uncategorized_name ] }).transactions_scope
|
|
@expected_count = english_results.count
|
|
assert_equal 2, @expected_count, "English locale should return 2 uncategorized transactions"
|
|
end
|
|
|
|
# Test every supported locale returns the same count when filtering by that locale's uncategorized name
|
|
LanguagesHelper::SUPPORTED_LOCALES.each do |locale|
|
|
I18n.with_locale(locale) do
|
|
localized_uncategorized_name = Category.uncategorized.name
|
|
results = Transaction::Search.new(@family, filters: { categories: [ localized_uncategorized_name ] }).transactions_scope
|
|
result_count = results.count
|
|
|
|
assert_equal @expected_count, result_count,
|
|
"Locale '#{locale}' with uncategorized name '#{localized_uncategorized_name}' should return #{@expected_count} transactions but got #{result_count}"
|
|
end
|
|
end
|
|
end
|
|
|
|
test "uncategorized filter works with English parameter name regardless of current locale" do
|
|
# This tests the bug where URL contains English "Uncategorized" but user's locale is different
|
|
# Bug: /transactions/?q[categories][]=Uncategorized fails when locale is French
|
|
|
|
# Create uncategorized transactions
|
|
uncategorized1 = create_transaction(
|
|
account: @checking_account,
|
|
amount: 100,
|
|
kind: "standard"
|
|
)
|
|
|
|
uncategorized2 = create_transaction(
|
|
account: @checking_account,
|
|
amount: 200,
|
|
kind: "standard"
|
|
)
|
|
|
|
# Create a categorized transaction to ensure filter is working
|
|
categorized = create_transaction(
|
|
account: @checking_account,
|
|
amount: 300,
|
|
category: categories(:food_and_drink),
|
|
kind: "standard"
|
|
)
|
|
|
|
# Get the English uncategorized name (this is what URLs typically contain)
|
|
english_uncategorized_name = I18n.t("models.category.uncategorized", locale: :en)
|
|
|
|
# Get the expected count using English locale (known working case)
|
|
expected_count = nil
|
|
I18n.with_locale(:en) do
|
|
results = Transaction::Search.new(@family, filters: { categories: [ english_uncategorized_name ] }).transactions_scope
|
|
expected_count = results.count
|
|
assert_equal 2, expected_count, "English locale should return 2 uncategorized transactions"
|
|
end
|
|
|
|
# Test that using the English parameter name works in every supported locale
|
|
# This catches the bug where French locale fails with English "Uncategorized" parameter
|
|
LanguagesHelper::SUPPORTED_LOCALES.each do |locale|
|
|
I18n.with_locale(locale) do
|
|
# Simulate URL parameter: q[categories][]=Uncategorized (English, regardless of user's locale)
|
|
results = Transaction::Search.new(@family, filters: { categories: [ english_uncategorized_name ] }).transactions_scope
|
|
result_count = results.count
|
|
|
|
assert_equal expected_count, result_count,
|
|
"Locale '#{locale}' should return #{expected_count} transactions when filtering with English 'Uncategorized' parameter, but got #{result_count}"
|
|
end
|
|
end
|
|
end
|
|
|
|
test "empty accessible_account_ids yields no visible transactions" do
|
|
create_transaction(account: @checking_account, amount: 100)
|
|
|
|
search = Transaction::Search.new(@family, filters: {}, accessible_account_ids: [])
|
|
|
|
assert_empty search.transactions_scope
|
|
end
|
|
|
|
test "totals handles empty accessible_account_ids without raising" do
|
|
create_transaction(account: @checking_account, amount: 100)
|
|
|
|
search = Transaction::Search.new(@family, filters: {}, accessible_account_ids: [])
|
|
totals = search.totals
|
|
|
|
assert_equal 0, totals.count
|
|
assert_equal Money.new(0, @family.currency), totals.expense_money
|
|
assert_equal Money.new(0, @family.currency), totals.income_money
|
|
assert_equal Money.new(0, @family.currency), totals.transfer_inflow_money
|
|
assert_equal Money.new(0, @family.currency), totals.transfer_outflow_money
|
|
end
|
|
end
|