1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-07-24 15:49:39 +02:00

perf(transactions): add kind to Transaction model and remove expensive Transfer joins in aggregations (#2388)

* add kind to transaction model

* Basic transfer creator

* Fix method naming conflict

* Creator form pattern

* Remove stale methods

* Tweak migration

* Remove BaseQuery, write entire query in each class for clarity

* Query optimizations

* Remove unused exchange rate query lines

* Remove temporary cache-warming strategy

* Fix test

* Update transaction search

* Decouple transactions endpoint from IncomeStatement

* Clean up transactions controller

* Update cursor rules

* Cleanup comments, logic in search

* Fix totals logic on transactions view

* Fix pagination

* Optimize search totals query

* Default to last 30 days on transactions page if no filters

* Decouple transactions list from transfer details

* Revert transfer route

* Migration reset

* Bundle update

* Fix matching logic, tests

* Remove unused code
This commit is contained in:
Zach Gollwitzer 2025-06-20 13:31:58 -04:00 committed by GitHub
parent 7aca5a2277
commit 1aae00f586
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
49 changed files with 1749 additions and 705 deletions

View file

@ -313,13 +313,13 @@ end
accountable: Depository.new
)
transfer = Transfer.from_accounts(
from_account: from_account,
to_account: to_account,
transfer = Transfer::Creator.new(
family: @family,
source_account_id: from_account.id,
destination_account_id: to_account.id,
date: Date.current,
amount: 100
)
transfer.save!
).create
get api_v1_transaction_url(transfer.inflow_transaction), headers: api_headers(@api_key)
assert_response :success

View file

@ -39,9 +39,8 @@ class TradesControllerTest < ActionDispatch::IntegrationTest
assert_difference -> { Entry.count } => 2,
-> { Transaction.count } => 2,
-> { Transfer.count } => 1 do
post trades_url, params: {
entry: {
account_id: @entry.account_id,
post trades_url(account_id: @entry.account_id), params: {
model: {
type: "deposit",
date: Date.current,
amount: 10,
@ -60,9 +59,8 @@ class TradesControllerTest < ActionDispatch::IntegrationTest
assert_difference -> { Entry.count } => 2,
-> { Transaction.count } => 2,
-> { Transfer.count } => 1 do
post trades_url, params: {
entry: {
account_id: @entry.account_id,
post trades_url(account_id: @entry.account_id), params: {
model: {
type: "withdrawal",
date: Date.current,
amount: 10,
@ -79,9 +77,8 @@ class TradesControllerTest < ActionDispatch::IntegrationTest
assert_difference -> { Entry.count } => 1,
-> { Transaction.count } => 1,
-> { Transfer.count } => 0 do
post trades_url, params: {
entry: {
account_id: @entry.account_id,
post trades_url(account_id: @entry.account_id), params: {
model: {
type: "withdrawal",
date: Date.current,
amount: 10,
@ -98,9 +95,8 @@ class TradesControllerTest < ActionDispatch::IntegrationTest
test "creates interest entry" do
assert_difference [ "Entry.count", "Transaction.count" ], 1 do
post trades_url, params: {
entry: {
account_id: @entry.account_id,
post trades_url(account_id: @entry.account_id), params: {
model: {
type: "interest",
date: Date.current,
amount: 10,
@ -117,9 +113,8 @@ class TradesControllerTest < ActionDispatch::IntegrationTest
test "creates trade buy entry" do
assert_difference [ "Entry.count", "Trade.count", "Security.count" ], 1 do
post trades_url, params: {
entry: {
account_id: @entry.account_id,
post trades_url(account_id: @entry.account_id), params: {
model: {
type: "buy",
date: Date.current,
ticker: "NVDA (NASDAQ)",
@ -141,9 +136,8 @@ class TradesControllerTest < ActionDispatch::IntegrationTest
test "creates trade sell entry" do
assert_difference [ "Entry.count", "Trade.count" ], 1 do
post trades_url, params: {
entry: {
account_id: @entry.account_id,
post trades_url(account_id: @entry.account_id), params: {
model: {
type: "sell",
ticker: "AAPL (NYSE)",
date: Date.current,

View file

@ -97,31 +97,98 @@ 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::Search totals method 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(
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)
expected_filters = { "start_date" => 30.days.ago.to_date }
Transaction::Search.expects(:new).with(family, filters: expected_filters).returns(search)
search.expects(:totals).once.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::Search totals method 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(
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)
search.expects(:totals).once.returns(totals)
get transactions_url(q: { categories: [ "Food" ], types: [ "expense" ] })
assert_response :success
end
end