1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-04 21:15:19 +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

@ -20,8 +20,7 @@ class IncomeStatement
ScopeTotals.new(
transactions_count: result.sum(&:transactions_count),
income_money: Money.new(total_income, family.currency),
expense_money: Money.new(total_expense, family.currency),
missing_exchange_rates?: result.any?(&:missing_exchange_rates?)
expense_money: Money.new(total_expense, family.currency)
)
end
@ -53,16 +52,9 @@ class IncomeStatement
family_stats(interval: interval).find { |stat| stat.classification == "income" }&.median || 0
end
def warm_caches!(interval: "month")
totals
family_stats(interval: interval)
category_stats(interval: interval)
nil
end
private
ScopeTotals = Data.define(:transactions_count, :income_money, :expense_money, :missing_exchange_rates?)
PeriodTotal = Data.define(:classification, :total, :currency, :missing_exchange_rates?, :category_totals)
ScopeTotals = Data.define(:transactions_count, :income_money, :expense_money)
PeriodTotal = Data.define(:classification, :total, :currency, :category_totals)
CategoryTotal = Data.define(:category, :total, :currency, :weight)
def categories
@ -102,7 +94,6 @@ class IncomeStatement
classification: classification,
total: category_totals.reject { |ct| ct.category.subcategory? }.sum(&:total),
currency: family.currency,
missing_exchange_rates?: totals.any?(&:missing_exchange_rates?),
category_totals: category_totals
)
end
@ -110,14 +101,14 @@ class IncomeStatement
def family_stats(interval: "month")
@family_stats ||= {}
@family_stats[interval] ||= Rails.cache.fetch([
"income_statement", "family_stats", family.id, interval, entries_cache_version
"income_statement", "family_stats", family.id, interval, family.entries_cache_version
]) { FamilyStats.new(family, interval:).call }
end
def category_stats(interval: "month")
@category_stats ||= {}
@category_stats[interval] ||= Rails.cache.fetch([
"income_statement", "category_stats", family.id, interval, entries_cache_version
"income_statement", "category_stats", family.id, interval, family.entries_cache_version
]) { CategoryStats.new(family, interval:).call }
end
@ -125,24 +116,11 @@ class IncomeStatement
sql_hash = Digest::MD5.hexdigest(transactions_scope.to_sql)
Rails.cache.fetch([
"income_statement", "totals_query", family.id, sql_hash, entries_cache_version
"income_statement", "totals_query", family.id, sql_hash, family.entries_cache_version
]) { Totals.new(family, transactions_scope: transactions_scope).call }
end
def monetizable_currency
family.currency
end
# Returns a monotonically increasing integer based on the most recent
# update to any Entry that belongs to the family. Incorporated into cache
# keys so they expire automatically on data changes.
def entries_cache_version
@entries_cache_version ||= begin
ts = Entry.joins(:account)
.where(accounts: { family_id: family.id })
.maximum(:updated_at)
ts.present? ? ts.to_i : 0
end
end
end