Files
sure/app/models/transaction/grouper/by_merchant_or_name.rb
Mikael Møller 5cb474d61c Quick Categorize Wizard — follow-up fixes (#1393)
* Extract Entry.uncategorized_transactions scope, remove Family#uncategorized_transaction_count

Adds a single Entry.uncategorized_transactions scope containing the
shared conditions (transactions join, active accounts, category nil,
not transfer kinds, not excluded). All callers now use this scope:

- Entry.uncategorized_matching builds on it
- Transaction::Grouper::ByMerchantOrName#uncategorized_entries uses it
- categorizes_controller#uncategorized_entries_for uses it (also fixes
  missing status/excluded filters that were silently absent before)
- Both controllers replace Current.family.uncategorized_transaction_count
  with Current.accessible_entries.uncategorized_transactions.count so
  the button count and wizard count both respect account sharing

Family#uncategorized_transaction_count removed as it is now unused and
was family-scoped rather than user-scoped.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Scope assign_entry write to Current.accessible_entries

Replaces unscoped Entry.where(id:) with Current.accessible_entries.where(id:)
so the write path is consistent with the find above it. Not exploitable
given the find would 404 first, but removes the pattern inconsistency.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Add privacy-sensitive class to amounts in categorize wizard

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Extract uncategorized_count helper in CategorizesController

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix comment on uncategorized_transactions scope to mention draft accounts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Use uncategorized_count helper in assign_entry action

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 13:17:37 +02:00

49 lines
1.2 KiB
Ruby

class Transaction::Grouper::ByMerchantOrName < Transaction::Grouper
def self.call(entries, limit: 20, offset: 0)
new(entries).call(limit: limit, offset: offset)
end
def initialize(entries)
@entries = entries
end
def call(limit: 20, offset: 0)
uncategorized_entries
.group_by { |entry| grouping_key_for(entry) }
.map { |key, entries| build_group(key, entries) }
.sort_by { |g| [ -g.entries.size, g.display_name ] }
.drop([ offset, 0 ].max)
.first(limit)
end
private
attr_reader :entries
def uncategorized_entries
entries
.uncategorized_transactions
.includes(entryable: :merchant)
.order(entries: { date: :desc })
end
def grouping_key_for(entry)
name = entry.entryable.merchant&.name.presence || entry.name
type = entry.amount.negative? ? "income" : "expense"
[ name, type ]
end
def build_group(key, entries)
name, type = key
merchant = entries.find { |e| e.entryable.merchant.present? }&.entryable&.merchant
Transaction::Grouper::Group.new(
grouping_key: name,
display_name: name,
entries: entries,
merchant: merchant,
transaction_type: type
)
end
end