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

Update transaction search

This commit is contained in:
Zach Gollwitzer 2025-06-17 21:01:33 -04:00
parent e4a22f81a0
commit 415fd670b6
2 changed files with 131 additions and 23 deletions

View file

@ -16,7 +16,6 @@ class Transaction::Search
def build_query(scope)
query = scope.joins(entry: :account)
.joins(transfer_join)
query = apply_category_filter(query, categories)
query = apply_type_filter(query, types)
@ -31,27 +30,12 @@ class Transaction::Search
end
private
def transfer_join
<<~SQL
LEFT JOIN (
SELECT t.*, t.id as transfer_id, a.accountable_type
FROM transfers t
JOIN entries ae ON ae.entryable_id = t.inflow_transaction_id
AND ae.entryable_type = 'Transaction'
JOIN accounts a ON a.id = ae.account_id
) transfer_info ON (
transfer_info.inflow_transaction_id = transactions.id OR
transfer_info.outflow_transaction_id = transactions.id
)
SQL
end
def apply_category_filter(query, categories)
return query unless categories.present?
query = query.left_joins(:category).where(
"categories.name IN (?) OR (
categories.id IS NULL AND (transfer_info.transfer_id IS NULL OR transfer_info.accountable_type = 'Loan')
categories.id IS NULL AND (transactions.kind NOT IN ('transfer', 'payment', 'one_time') OR transactions.kind = 'loan_payment')
)",
categories
)
@ -67,7 +51,7 @@ class Transaction::Search
return query unless types.present?
return query if types.sort == [ "expense", "income", "transfer" ]
transfer_condition = "transfer_info.transfer_id IS NOT NULL"
transfer_condition = "transactions.kind IN ('transfer', 'payment', 'one_time')"
expense_condition = "entries.amount >= 0"
income_condition = "entries.amount <= 0"
@ -75,15 +59,15 @@ class Transaction::Search
when [ "transfer" ]
transfer_condition
when [ "expense" ]
Arel.sql("#{expense_condition} AND NOT (#{transfer_condition})")
Arel.sql("(#{expense_condition} AND transactions.kind NOT IN ('transfer', 'payment', 'one_time')) OR transactions.kind = 'loan_payment'")
when [ "income" ]
Arel.sql("#{income_condition} AND NOT (#{transfer_condition})")
Arel.sql("#{income_condition} AND transactions.kind NOT IN ('transfer', 'payment', 'one_time', 'loan_payment')")
when [ "expense", "transfer" ]
Arel.sql("#{expense_condition} OR #{transfer_condition}")
Arel.sql("(#{expense_condition} AND transactions.kind NOT IN ('transfer', 'payment', 'one_time')) OR transactions.kind = 'loan_payment' OR #{transfer_condition}")
when [ "income", "transfer" ]
Arel.sql("#{income_condition} OR #{transfer_condition}")
Arel.sql("(#{income_condition} AND transactions.kind NOT IN ('transfer', 'payment', 'one_time', 'loan_payment')) OR #{transfer_condition}")
when [ "expense", "income" ]
Arel.sql("NOT (#{transfer_condition})")
Arel.sql("transactions.kind NOT IN ('transfer', 'payment', 'one_time') OR transactions.kind = 'loan_payment'")
end
query.where(condition)

View file

@ -0,0 +1,124 @@
require "test_helper"
class TransactionTest < ActiveSupport::TestCase
include EntriesTestHelper
setup do
@family = families(:dylan_family)
@checking_account = accounts(:depository)
@credit_card_account = accounts(:credit_card)
@loan_account = accounts(:loan)
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)
)
standard_entry.entryable.update!(kind: "standard")
transfer_entry = create_transaction(
account: @checking_account,
amount: 200
)
transfer_entry.entryable.update!(kind: "transfer")
payment_entry = create_transaction(
account: @credit_card_account,
amount: -300
)
payment_entry.entryable.update!(kind: "payment")
loan_payment_entry = create_transaction(
account: @loan_account,
amount: 400
)
loan_payment_entry.entryable.update!(kind: "loan_payment")
one_time_entry = create_transaction(
account: @checking_account,
amount: 500
)
one_time_entry.entryable.update!(kind: "one_time")
# Test transfer type filter
transfer_results = Transaction.search(types: [ "transfer" ])
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, one_time_entry.entryable.id
assert_not_includes transfer_ids, standard_entry.entryable.id
assert_not_includes transfer_ids, loan_payment_entry.entryable.id
# Test expense type filter (should include loan_payment)
expense_results = Transaction.search(types: [ "expense" ])
expense_ids = expense_results.pluck(:id)
assert_includes expense_ids, standard_entry.entryable.id
assert_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
assert_not_includes expense_ids, one_time_entry.entryable.id
# Test income type filter
income_entry = create_transaction(
account: @checking_account,
amount: -600
)
income_entry.entryable.update!(kind: "standard")
income_results = Transaction.search(types: [ "income" ])
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 transfers)
non_transfer_results = Transaction.search(types: [ "expense", "income" ])
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, 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
assert_not_includes non_transfer_ids, one_time_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
)
uncategorized_standard.entryable.update!(kind: "standard")
uncategorized_transfer = create_transaction(
account: @checking_account,
amount: 200
)
uncategorized_transfer.entryable.update!(kind: "transfer")
uncategorized_loan_payment = create_transaction(
account: @loan_account,
amount: 300
)
uncategorized_loan_payment.entryable.update!(kind: "loan_payment")
# Search for uncategorized transactions
uncategorized_results = Transaction.search(categories: [ "Uncategorized" ])
uncategorized_ids = uncategorized_results.pluck(:id)
# Should include standard and loan_payment (budget-relevant) uncategorized transactions
assert_includes uncategorized_ids, uncategorized_standard.entryable.id
assert_includes uncategorized_ids, uncategorized_loan_payment.entryable.id
# Should exclude transfer transactions even if uncategorized
assert_not_includes uncategorized_ids, uncategorized_transfer.entryable.id
end
end