Clean up transactions controller

This commit is contained in:
Zach Gollwitzer
2025-06-18 17:03:54 -04:00
parent b136787a6a
commit f7377e20a3
7 changed files with 190 additions and 238 deletions

View File

@@ -3,8 +3,6 @@ class TransactionsController < ApplicationController
before_action :store_params!, only: :index
require "digest/md5"
def new
super
@income_categories = Current.family.categories.incomes.alphabetically
@@ -13,95 +11,33 @@ class TransactionsController < ApplicationController
def index
@q = search_params
transactions_query = Transaction::Search.new(@q, family: Current.family).relation
search = Transaction::Search.new(Current.family, filters: @q)
@totals = Transaction::Totals.compute(search)
transactions_query = search.relation
set_focused_record(transactions_query, params[:focused_record_id], default_per_page: 50)
# ------------------------------------------------------------------
# Cache the expensive includes & pagination block so the DB work only
# runs when either the query params change *or* any entry has been
# updated for the current family.
# ------------------------------------------------------------------
items_per_page = params[:per_page].to_i.positive? ? params[:per_page].to_i : 50
current_page = params[:page].to_i.positive? ? params[:page].to_i : 1
latest_update_ts = Current.family.entries.maximum(:updated_at)&.utc&.to_i || 0
items_per_page = (params[:per_page].presence || default_params[:per_page]).to_i
items_per_page = 1 if items_per_page <= 0
current_page = (params[:page].presence || default_params[:page]).to_i
current_page = 1 if current_page <= 0
# Build a compact cache digest: sanitized filters + page info + a
# token that changes on updates *or* deletions.
entries_changed_token = [ latest_update_ts, Current.family.entries.count ].join(":")
digest_source = {
q: @q, # processed & sanitised search params
page: current_page, # requested page number
per: items_per_page, # page size
tok: entries_changed_token
}.to_json
cache_key = Current.family.build_cache_key(
"transactions_idx_#{Digest::MD5.hexdigest(digest_source)}"
)
cache_data = Rails.cache.fetch(cache_key, expires_in: 30.minutes) do
current_page_i = current_page
# Initial query
offset = (current_page_i - 1) * items_per_page
ids = transactions_query
.reverse_chronological
.limit(items_per_page)
.offset(offset)
.pluck(:id)
total_count = transactions_query.count
if ids.empty? && total_count.positive? && current_page_i > 1
current_page_i = (total_count.to_f / items_per_page).ceil
offset = (current_page_i - 1) * items_per_page
ids = transactions_query
.reverse_chronological
.limit(items_per_page)
.offset(offset)
.pluck(:id)
end
{ ids: ids, total_count: total_count, current_page: current_page_i }
end
ids = cache_data[:ids]
total_count = cache_data[:total_count]
current_page = cache_data[:current_page]
# Build Pagy object (this part is cheap done *after* potential
# page fallback so the pagination UI reflects the adjusted page
# number).
@pagy = Pagy.new(
count: total_count,
page: current_page,
items: items_per_page,
count: transactions_query.count,
page: current_page,
items: items_per_page,
params: ->(p) { p.except(:focused_record_id) }
)
# Fetch the transactions in the cached order
@transactions = Current.family.transactions
.active
.where(id: ids)
.includes(
{ entry: :account },
:category, :merchant, :tags,
transfer_as_outflow: { inflow_transaction: { entry: :account } },
transfer_as_inflow: { outflow_transaction: { entry: :account } }
)
# Preserve the order defined by `ids`
@transactions = ids.map { |id| @transactions.detect { |t| t.id == id } }.compact
@totals = Current.family.income_statement.totals(transactions_scope: transactions_query)
# Use Pagy's calculated page (which handles overflow) with our variables
@transactions = transactions_query
.reverse_chronological
.limit(items_per_page)
.offset((@pagy.page - 1) * items_per_page)
.includes(
{ entry: :account },
:category, :merchant, :tags,
transfer_as_outflow: { inflow_transaction: { entry: :account } },
transfer_as_inflow: { outflow_transaction: { entry: :account } }
)
end
def clear_filter
@@ -226,37 +162,6 @@ class TransactionsController < ApplicationController
cleaned_params.delete(:amount_operator) unless cleaned_params[:amount].present?
# -------------------------------------------------------------------
# Performance optimisation
# -------------------------------------------------------------------
# When a user lands on the Transactions page without an explicit date
# filter, the previous behaviour queried *all* historical transactions
# for the family. For large datasets this results in very expensive
# SQL (as shown in Skylight) particularly the aggregation queries
# used for @totals. To keep the UI responsive while still showing a
# sensible period of activity, we fall back to the user's preferred
# default period (stored on User#default_period, defaulting to
# "last_30_days") when **no** date filters have been supplied.
#
# This effectively changes the default view from "all-time" to a
# rolling window, dramatically reducing the rows scanned / grouped in
# Postgres without impacting the UX (the user can always clear the
# filter).
# -------------------------------------------------------------------
if cleaned_params[:start_date].blank? && cleaned_params[:end_date].blank?
period_key = Current.user&.default_period.presence || "last_30_days"
begin
period = Period.from_key(period_key)
cleaned_params[:start_date] = period.start_date
cleaned_params[:end_date] = period.end_date
rescue Period::InvalidKeyError
# Fallback should never happen but keeps things safe.
cleaned_params[:start_date] = 30.days.ago.to_date
cleaned_params[:end_date] = Date.current
end
end
cleaned_params
end
@@ -264,9 +169,9 @@ class TransactionsController < ApplicationController
if should_restore_params?
params_to_restore = {}
params_to_restore[:q] = stored_params["q"].presence || default_params[:q]
params_to_restore[:page] = stored_params["page"].presence || default_params[:page]
params_to_restore[:per_page] = stored_params["per_page"].presence || default_params[:per_page]
params_to_restore[:q] = stored_params["q"].presence || {}
params_to_restore[:page] = stored_params["page"].presence || 1
params_to_restore[:per_page] = stored_params["per_page"].presence || 50
redirect_to transactions_path(params_to_restore)
else
@@ -287,12 +192,4 @@ class TransactionsController < ApplicationController
def stored_params
Current.session.prev_transaction_page_params
end
def default_params
{
q: {},
page: 1,
per_page: 50
}
end
end

View File

@@ -18,9 +18,9 @@ class Transaction::Search
attr_reader :family
def initialize(attributes = {}, family:)
def initialize(family, filters: {})
@family = family
super(attributes)
super(filters)
end
# Build the complete filtered relation

View File

@@ -97,31 +97,97 @@ class TransactionsControllerTest < ActionDispatch::IntegrationTest
end
test "can paginate" do
family = families(:empty)
sign_in users(:empty)
# Clean up any existing entries to ensure clean test
family.accounts.each { |account| account.entries.delete_all }
account = family.accounts.create! name: "Test", balance: 0, currency: "USD", accountable: Depository.new
# Create multiple transactions for pagination
25.times do |i|
create_transaction(
account: account,
name: "Transaction #{i + 1}",
amount: 100 + i, # Different amounts to prevent transfer matching
date: Date.current - i.days # Different dates
)
end
total_transactions = family.entries.transactions.count
assert_operator total_transactions, :>=, 20, "Should have at least 20 transactions for testing"
# Test page 1 - should show limited transactions
get transactions_url(page: 1, per_page: 10)
assert_response :success
page_1_count = css_select("turbo-frame[id^='entry_']").count
assert_equal 10, page_1_count, "Page 1 should respect per_page limit"
# Test page 2 - should show different transactions
get transactions_url(page: 2, per_page: 10)
assert_response :success
page_2_count = css_select("turbo-frame[id^='entry_']").count
assert_operator page_2_count, :>, 0, "Page 2 should show some transactions"
assert_operator page_2_count, :<=, 10, "Page 2 should not exceed per_page limit"
# Test Pagy overflow handling - should redirect or handle gracefully
get transactions_url(page: 9999999, per_page: 10)
# Either success (if Pagy shows last page) or redirect (if Pagy redirects)
assert_includes [ 200, 302 ], response.status, "Pagy should handle overflow gracefully"
if response.status == 302
follow_redirect!
assert_response :success
end
overflow_count = css_select("turbo-frame[id^='entry_']").count
assert_operator overflow_count, :>, 0, "Overflow should show some transactions"
end
test "calls Transaction::Totals service with correct search parameters" do
family = families(:empty)
sign_in users(:empty)
account = family.accounts.create! name: "Test", balance: 0, currency: "USD", accountable: Depository.new
11.times do
create_transaction(account: account)
end
create_transaction(account: account, amount: 100)
sorted_transactions = family.entries.transactions.reverse_chronological.to_a
search = Transaction::Search.new(family)
totals = OpenStruct.new(
transactions_count: 1,
expense_money: Money.new(10000, "USD"),
income_money: Money.new(0, "USD")
)
assert_equal 11, sorted_transactions.count
get transactions_url(page: 1, per_page: 10)
Transaction::Search.expects(:new).with(family, filters: {}).returns(search)
Transaction::Totals.expects(:compute).once.with(search).returns(totals)
get transactions_url
assert_response :success
sorted_transactions.first(10).each do |transaction|
assert_dom "#" + dom_id(transaction), count: 1
end
end
get transactions_url(page: 2, per_page: 10)
test "calls Transaction::Totals service with filtered search parameters" do
family = families(:empty)
sign_in users(:empty)
account = family.accounts.create! name: "Test", balance: 0, currency: "USD", accountable: Depository.new
category = family.categories.create! name: "Food", color: "#ff0000"
assert_dom "#" + dom_id(sorted_transactions.last), count: 1
create_transaction(account: account, amount: 100, category: category)
get transactions_url(page: 9999999, per_page: 10) # out of range loads last page
search = Transaction::Search.new(family, filters: { "categories" => [ "Food" ], "types" => [ "expense" ] })
totals = OpenStruct.new(
transactions_count: 1,
expense_money: Money.new(10000, "USD"),
income_money: Money.new(0, "USD")
)
assert_dom "#" + dom_id(sorted_transactions.last), count: 1
Transaction::Search.expects(:new).with(family, filters: { "categories" => [ "Food" ], "types" => [ "expense" ] }).returns(search)
Transaction::Totals.expects(:compute).once.with(search).returns(totals)
get transactions_url(q: { categories: [ "Food" ], types: [ "expense" ] })
assert_response :success
end
end

View File

@@ -153,12 +153,8 @@ class IncomeStatementTest < ActiveSupport::TestCase
# NOTE: These tests now pass because kind filtering is working after the refactoring!
test "excludes regular transfers from income statement calculations" do
# Create a regular transfer between accounts
outflow_transaction = create_transaction(account: @checking_account, amount: 500)
inflow_transaction = create_transaction(account: @credit_card_account, amount: -500)
# Manually set transaction kinds to simulate transfer
outflow_transaction.entryable.update!(kind: "transfer")
inflow_transaction.entryable.update!(kind: "transfer")
outflow_transaction = create_transaction(account: @checking_account, amount: 500, kind: "transfer")
inflow_transaction = create_transaction(account: @credit_card_account, amount: -500, kind: "transfer")
income_statement = IncomeStatement.new(@family)
totals = income_statement.totals
@@ -171,8 +167,7 @@ class IncomeStatementTest < ActiveSupport::TestCase
test "includes loan payments as expenses in income statement" do
# Create a loan payment transaction
loan_payment = create_transaction(account: @checking_account, amount: 1000, category: nil)
loan_payment.entryable.update!(kind: "loan_payment")
loan_payment = create_transaction(account: @checking_account, amount: 1000, category: nil, kind: "loan_payment")
income_statement = IncomeStatement.new(@family)
totals = income_statement.totals
@@ -185,8 +180,7 @@ class IncomeStatementTest < ActiveSupport::TestCase
test "excludes one-time transactions from income statement calculations" do
# Create a one-time transaction
one_time_transaction = create_transaction(account: @checking_account, amount: 250, category: @groceries_category)
one_time_transaction.entryable.update!(kind: "one_time")
one_time_transaction = create_transaction(account: @checking_account, amount: 250, category: @groceries_category, kind: "one_time")
income_statement = IncomeStatement.new(@family)
totals = income_statement.totals
@@ -199,8 +193,7 @@ class IncomeStatementTest < ActiveSupport::TestCase
test "excludes payment transactions from income statement calculations" do
# Create a payment transaction (credit card payment)
payment_transaction = create_transaction(account: @checking_account, amount: 300, category: nil)
payment_transaction.entryable.update!(kind: "payment")
payment_transaction = create_transaction(account: @checking_account, amount: 300, category: nil, kind: "payment")
income_statement = IncomeStatement.new(@family)
totals = income_statement.totals

View File

@@ -1,6 +1,6 @@
require "test_helper"
class TransactionTest < ActiveSupport::TestCase
class Transaction::SearchTest < ActiveSupport::TestCase
include EntriesTestHelper
setup do
@@ -15,36 +15,36 @@ class TransactionTest < ActiveSupport::TestCase
standard_entry = create_transaction(
account: @checking_account,
amount: 100,
category: categories(:food_and_drink)
category: categories(:food_and_drink),
kind: "standard"
)
standard_entry.entryable.update!(kind: "standard")
transfer_entry = create_transaction(
account: @checking_account,
amount: 200
amount: 200,
kind: "transfer"
)
transfer_entry.entryable.update!(kind: "transfer")
payment_entry = create_transaction(
account: @credit_card_account,
amount: -300
amount: -300,
kind: "payment"
)
payment_entry.entryable.update!(kind: "payment")
loan_payment_entry = create_transaction(
account: @loan_account,
amount: 400
amount: 400,
kind: "loan_payment"
)
loan_payment_entry.entryable.update!(kind: "loan_payment")
one_time_entry = create_transaction(
account: @checking_account,
amount: 500
amount: 500,
kind: "one_time"
)
one_time_entry.entryable.update!(kind: "one_time")
# Test transfer type filter
transfer_results = Transaction::Search.new({ types: [ "transfer" ] }, family: @family).relation
transfer_results = Transaction::Search.new(@family, filters: { types: [ "transfer" ] }).relation
transfer_ids = transfer_results.pluck(:id)
assert_includes transfer_ids, transfer_entry.entryable.id
@@ -54,7 +54,7 @@ class TransactionTest < ActiveSupport::TestCase
assert_not_includes transfer_ids, loan_payment_entry.entryable.id
# Test expense type filter (should include loan_payment)
expense_results = Transaction::Search.new({ types: [ "expense" ] }, family: @family).relation
expense_results = Transaction::Search.new(@family, filters: { types: [ "expense" ] }).relation
expense_ids = expense_results.pluck(:id)
assert_includes expense_ids, standard_entry.entryable.id
@@ -66,11 +66,11 @@ class TransactionTest < ActiveSupport::TestCase
# Test income type filter
income_entry = create_transaction(
account: @checking_account,
amount: -600
amount: -600,
kind: "standard"
)
income_entry.entryable.update!(kind: "standard")
income_results = Transaction::Search.new({ types: [ "income" ] }, family: @family).relation
income_results = Transaction::Search.new(@family, filters: { types: [ "income" ] }).relation
income_ids = income_results.pluck(:id)
assert_includes income_ids, income_entry.entryable.id
@@ -79,7 +79,7 @@ class TransactionTest < ActiveSupport::TestCase
assert_not_includes income_ids, transfer_entry.entryable.id
# Test combined expense and income filter (excludes transfers)
non_transfer_results = Transaction::Search.new({ types: [ "expense", "income" ] }, family: @family).relation
non_transfer_results = Transaction::Search.new(@family, filters: { types: [ "expense", "income" ] }).relation
non_transfer_ids = non_transfer_results.pluck(:id)
assert_includes non_transfer_ids, standard_entry.entryable.id
@@ -94,24 +94,24 @@ class TransactionTest < ActiveSupport::TestCase
# Create uncategorized transactions of different kinds
uncategorized_standard = create_transaction(
account: @checking_account,
amount: 100
amount: 100,
kind: "standard"
)
uncategorized_standard.entryable.update!(kind: "standard")
uncategorized_transfer = create_transaction(
account: @checking_account,
amount: 200
amount: 200,
kind: "transfer"
)
uncategorized_transfer.entryable.update!(kind: "transfer")
uncategorized_loan_payment = create_transaction(
account: @loan_account,
amount: 300
amount: 300,
kind: "loan_payment"
)
uncategorized_loan_payment.entryable.update!(kind: "loan_payment")
# Search for uncategorized transactions
uncategorized_results = Transaction::Search.new({ categories: [ "Uncategorized" ] }, family: @family).relation
uncategorized_results = Transaction::Search.new(@family, filters: { categories: [ "Uncategorized" ] }).relation
uncategorized_ids = uncategorized_results.pluck(:id)
# Should include standard and loan_payment (budget-relevant) uncategorized transactions
@@ -127,18 +127,18 @@ class TransactionTest < ActiveSupport::TestCase
transaction1 = create_transaction(
account: @checking_account,
amount: 100,
category: categories(:food_and_drink)
category: categories(:food_and_drink),
kind: "standard"
)
transaction1.entryable.update!(kind: "standard")
transaction2 = create_transaction(
account: @checking_account,
amount: 200
amount: 200,
kind: "transfer"
)
transaction2.entryable.update!(kind: "transfer")
# Test new family-based API
search = Transaction::Search.new({ types: [ "expense" ] }, family: @family)
search = Transaction::Search.new(@family, filters: { types: [ "expense" ] })
results = search.relation
result_ids = results.pluck(:id)
@@ -154,8 +154,9 @@ class TransactionTest < ActiveSupport::TestCase
end
test "family-based API requires family parameter" do
assert_raises(ArgumentError, "missing keyword: :family") do
Transaction::Search.new({ types: [ "expense" ] })
assert_raises(NoMethodError) do
search = Transaction::Search.new({ types: [ "expense" ] })
search.relation # This will fail when trying to call .transactions on a Hash
end
end
end
end

View File

@@ -11,6 +11,8 @@ class Transaction::TotalsTest < ActiveSupport::TestCase
# Clean up existing entries/transactions from fixtures to ensure test isolation
@family.accounts.each { |account| account.entries.delete_all }
@search = Transaction::Search.new(@family)
end
test "computes basic expense and income totals" do
@@ -18,19 +20,18 @@ class Transaction::TotalsTest < ActiveSupport::TestCase
expense_entry = create_transaction(
account: @checking_account,
amount: 100,
category: categories(:food_and_drink)
category: categories(:food_and_drink),
kind: "standard"
)
expense_entry.entryable.update!(kind: "standard")
# Create income transaction
income_entry = create_transaction(
account: @checking_account,
amount: -200
amount: -200,
kind: "standard"
)
income_entry.entryable.update!(kind: "standard")
search = Transaction::Search.new({}, family: @family)
totals = Transaction::Totals.compute(search)
totals = Transaction::Totals.compute(@search)
assert_equal 2, totals.transactions_count
assert_equal Money.new(10000, "USD"), totals.expense_money # $100
@@ -41,19 +42,18 @@ class Transaction::TotalsTest < ActiveSupport::TestCase
# Create loan payment transaction
loan_payment_entry = create_transaction(
account: @loan_account,
amount: 500
amount: 500,
kind: "loan_payment"
)
loan_payment_entry.entryable.update!(kind: "loan_payment")
# Create regular expense
expense_entry = create_transaction(
account: @checking_account,
amount: 100
amount: 100,
kind: "standard"
)
expense_entry.entryable.update!(kind: "standard")
search = Transaction::Search.new({}, family: @family)
totals = Transaction::Totals.compute(search)
totals = Transaction::Totals.compute(@search)
assert_equal 2, totals.transactions_count
assert_equal Money.new(60000, "USD"), totals.expense_money # $500 + $100
@@ -64,31 +64,30 @@ class Transaction::TotalsTest < ActiveSupport::TestCase
# Create transactions that should be excluded
transfer_entry = create_transaction(
account: @checking_account,
amount: 100
amount: 100,
kind: "transfer"
)
transfer_entry.entryable.update!(kind: "transfer")
payment_entry = create_transaction(
account: @credit_card_account,
amount: -200
amount: -200,
kind: "payment"
)
payment_entry.entryable.update!(kind: "payment")
one_time_entry = create_transaction(
account: @checking_account,
amount: 300
amount: 300,
kind: "one_time"
)
one_time_entry.entryable.update!(kind: "one_time")
# Create transaction that should be included
standard_entry = create_transaction(
account: @checking_account,
amount: 50
amount: 50,
kind: "standard"
)
standard_entry.entryable.update!(kind: "standard")
search = Transaction::Search.new({}, family: @family)
totals = Transaction::Totals.compute(search)
totals = Transaction::Totals.compute(@search)
# Only the standard transaction should be counted
assert_equal 1, totals.transactions_count
@@ -101,9 +100,9 @@ class Transaction::TotalsTest < ActiveSupport::TestCase
eur_entry = create_transaction(
account: @checking_account,
amount: 100,
currency: "EUR"
currency: "EUR",
kind: "standard"
)
eur_entry.entryable.update!(kind: "standard")
# Create exchange rate EUR -> USD
ExchangeRate.create!(
@@ -117,12 +116,11 @@ class Transaction::TotalsTest < ActiveSupport::TestCase
usd_entry = create_transaction(
account: @checking_account,
amount: 50,
currency: "USD"
currency: "USD",
kind: "standard"
)
usd_entry.entryable.update!(kind: "standard")
search = Transaction::Search.new({}, family: @family)
totals = Transaction::Totals.compute(search)
totals = Transaction::Totals.compute(@search)
assert_equal 2, totals.transactions_count
# EUR 100 * 1.1 + USD 50 = 110 + 50 = 160
@@ -135,12 +133,11 @@ class Transaction::TotalsTest < ActiveSupport::TestCase
eur_entry = create_transaction(
account: @checking_account,
amount: 100,
currency: "EUR"
currency: "EUR",
kind: "standard"
)
eur_entry.entryable.update!(kind: "standard")
search = Transaction::Search.new({}, family: @family)
totals = Transaction::Totals.compute(search)
totals = Transaction::Totals.compute(@search)
assert_equal 1, totals.transactions_count
# Should use rate of 1 when exchange rate is missing
@@ -153,19 +150,19 @@ class Transaction::TotalsTest < ActiveSupport::TestCase
food_entry = create_transaction(
account: @checking_account,
amount: 100,
category: categories(:food_and_drink)
category: categories(:food_and_drink),
kind: "standard"
)
food_entry.entryable.update!(kind: "standard")
other_entry = create_transaction(
account: @checking_account,
amount: 50,
category: categories(:income)
category: categories(:income),
kind: "standard"
)
other_entry.entryable.update!(kind: "standard")
# Filter by food category only
search = Transaction::Search.new({ categories: [ "Food & Drink" ] }, family: @family)
search = Transaction::Search.new(@family, filters: { categories: [ "Food & Drink" ] })
totals = Transaction::Totals.compute(search)
assert_equal 1, totals.transactions_count
@@ -177,18 +174,18 @@ class Transaction::TotalsTest < ActiveSupport::TestCase
# Create expense and income transactions
expense_entry = create_transaction(
account: @checking_account,
amount: 100
amount: 100,
kind: "standard"
)
expense_entry.entryable.update!(kind: "standard")
income_entry = create_transaction(
account: @checking_account,
amount: -200
amount: -200,
kind: "standard"
)
income_entry.entryable.update!(kind: "standard")
# Filter by expense type only
search = Transaction::Search.new({ types: [ "expense" ] }, family: @family)
search = Transaction::Search.new(@family, filters: { types: [ "expense" ] })
totals = Transaction::Totals.compute(search)
assert_equal 1, totals.transactions_count
@@ -197,8 +194,7 @@ class Transaction::TotalsTest < ActiveSupport::TestCase
end
test "handles empty results" do
search = Transaction::Search.new({}, family: @family)
totals = Transaction::Totals.compute(search)
totals = Transaction::Totals.compute(@search)
assert_equal 0, totals.transactions_count
assert_equal Money.new(0, "USD"), totals.expense_money
@@ -209,27 +205,26 @@ class Transaction::TotalsTest < ActiveSupport::TestCase
# Create an excluded transaction (should be excluded by default)
excluded_entry = create_transaction(
account: @checking_account,
amount: 100
amount: 100,
kind: "standard"
)
excluded_entry.entryable.update!(kind: "standard")
excluded_entry.update!(excluded: true) # Marks it as excluded
# Create a normal transaction
normal_entry = create_transaction(
account: @checking_account,
amount: 50
amount: 50,
kind: "standard"
)
normal_entry.entryable.update!(kind: "standard")
# Default behavior should exclude excluded transactions
search = Transaction::Search.new({}, family: @family)
totals = Transaction::Totals.compute(search)
totals = Transaction::Totals.compute(@search)
assert_equal 1, totals.transactions_count
assert_equal Money.new(5000, "USD"), totals.expense_money # Only non-excluded transaction
# Explicitly include excluded transactions
search_with_excluded = Transaction::Search.new({ excluded_transactions: true }, family: @family)
search_with_excluded = Transaction::Search.new(@family, filters: { excluded_transactions: true })
totals_with_excluded = Transaction::Totals.compute(search_with_excluded)
assert_equal 2, totals_with_excluded.transactions_count

View File

@@ -1,7 +1,7 @@
module EntriesTestHelper
def create_transaction(attributes = {})
entry_attributes = attributes.except(:category, :tags, :merchant)
transaction_attributes = attributes.slice(:category, :tags, :merchant)
entry_attributes = attributes.except(:category, :tags, :merchant, :kind)
transaction_attributes = attributes.slice(:category, :tags, :merchant, :kind)
entry_defaults = {
account: accounts(:depository),