Transaction Filtering and Search#
Overview#
The transaction list (GET /transactions) supports multi-dimensional filtering via a form-based search backed by two model classes: Transaction::Search and EntrySearch. All filters are applied as composable ActiveRecord scope chains — there is no user-controlled sort order; the list is always ordered reverse-chronological .
The eight available filter dimensions are defined in TransactionsHelper#transaction_search_filters:
| Dimension | Key | Notes |
|---|---|---|
| Account | account_ids / accounts | By ID or name |
| Date | start_date / end_date | Inclusive range |
| Type | types[] | income, expense, transfer |
| Status | status[] | confirmed, pending |
| Amount | amount + amount_operator | equal, greater, less |
| Category | categories[] | Supports "Uncategorized" as a special value |
| Tag | tags[] | Exact name match |
| Merchant | merchants[] | Exact name match |
A free-text search field (entry name / notes ILIKE) is also available alongside the structured filters .
Key Source Files#
| File | Role |
|---|---|
app/models/transaction/search.rb | Core filter model; composes all filter methods into transactions_scope |
app/models/entry_search.rb | Reusable class methods for text search, date, amount, and account filters |
app/controllers/transactions_controller.rb | Instantiates Transaction::Search, paginates results |
app/helpers/transactions_helper.rb | Declares the ordered list of filter panels |
app/views/transactions/searches/_form.html.erb | Search bar + filter popover UI |
app/views/transactions/searches/filters/ | One partial per filter dimension (see below) |
Architecture#
Transaction::Search#
Transaction::Search is an ActiveModel::Model with one attribute per filter dimension. Its transactions_scope method chains filters in order:
- Scopes to
family.transactionsand strips split-parent rows - Restricts to
accessible_account_idsfor permission scoping - Applies
active_accounts_only(draft/active accounts by default) - Delegates to private
apply_*methods andEntrySearchclass methods
EntrySearch#
EntrySearch provides reusable class methods called directly by Transaction::Search:
apply_search_filter— ILIKE onentries.nameandentries.notes(SQL-injection-safe viasanitize_sql_like)apply_date_filters—entries.date >= start_date/<= end_dateapply_amount_filter— comparesABS(entries.amount)with ±0.01 tolerance forequalapply_accounts_filter— by account name or account ID
Transaction-specific filters#
Implemented privately in Transaction::Search:
- Category (
apply_category_filter) — matches by category name or parent ID;"Uncategorized"maps tocategories.id IS NULL AND kind NOT IN (TRANSFER_KINDS) - Type (
apply_type_filter) — maps user-facingincome/expense/transferto amount sign +TRANSFER_KINDSconditions; selecting all three is a no-op - Status (
apply_status_filter) — delegates toTransaction.pending/Transaction.excluding_pendingscopes sourced fromPENDING_PROVIDERS; selecting both is a no-op - Merchant (
apply_merchant_filter) — inner join onmerchants.name - Tag (
apply_tag_filter) — inner join ontags.name
Controller wiring#
TransactionsController#index builds @search, calls transactions_scope.reverse_chronological, and paginates via pagy. The search_params method allows the full set of filter keys. Filter state is persisted in Current.session.prev_transaction_page_params and restored on bare /transactions visits .
UI#
The search UI is a form (scope: :q, auto-submit via data-controller="auto-submit-form") at _form.html.erb. Structured filters live in a DS::Popover that renders transactions/searches/menu, which in turn renders individual partials via TransactionsHelper#get_transaction_search_filter_partial_path .
Each filter dimension has its own partial under app/views/transactions/searches/filters/:
- Multi-select (account, category, merchant, tag): searchable checkbox lists using a
list-filterJS controller - Checkboxes (type, status): hardcoded options — type:
income/expense/transfer; status:confirmed/pending - Date range:
start_date/end_datedate inputs - Amount: numeric input +
amount_operatorselect (equal,greater,less)
Totals Bar#
Transaction::Search#totals computes income, expense, transfer inflow/outflow, and transaction count for the current filter state . Results are cached under a SHA-256 key of the filter attributes + family.entries_cache_version . Tax-advantaged accounts (401k, IRA, etc.) are excluded from totals .
Related Topics#
- Dashboard Filtering and Drilldowns — chart-based drilldowns pass pre-built
q[types][],q[status][], and date params totransactions_path - Transaction Categorization —
apply_category_filterand the "Uncategorized" special case are documented in Transaction Categorization - Transaction Exclusion — the base scope does not filter by
excluded; there is no "excluded" filter option