TransactionsController#create looked up the account with
accessible_accounts.find(params.dig(:entry, :account_id)). With no
account selected the id is blank, find raises RecordNotFound, and
StoreLocation's rescue_from turns that into head :not_found — the 404
on /transactions the reporter saw.
Switch to find_by(id:) and, when it returns nil, rebuild the entry,
run validation, and re-render :new with 422, matching the existing
validation-failure branch. This covers a blank, missing, or invalid
account_id, so the user gets the form back with errors instead of a
dead button.
Fixes#2566
Co-authored-by: agentloop <agentloop@localhost>
* fix: disable "Mark as Recurring" button when a manual recurring transaction already exists
Previously the button was always clickable and only failed after a POST,
showing "A manual recurring transaction already exists for this pattern".
Extract the lookup into Transaction#existing_manual_recurring_transaction
(reused by the controller guard) so the view can disable the button ahead
of time and show the reason inline.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix: address CodeRabbit review feedback on PR #3103
Move the existing_manual_recurring lookup out of the show view and into
the controller so rendering no longer runs an Active Record query
in-template, and strengthen the "no match" model test with near-match
recurring transactions that individually differ by account, merchant,
amount, currency, and manual flag.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix: move mark-recurring presentation state fully into controller, fix stale state on failed update
Address CodeRabbit follow-up on PR #3103:
- Compute the mark-recurring button's subtitle text/class, href, disabled
state, title, and class entirely in TransactionsController (via a shared
assign_mark_recurring_state helper) instead of deriving them with
ternaries in the view.
- Populate that state before TransactionsController#update re-renders
:show on a failed entry update, so the button doesn't incorrectly appear
enabled when a matching manual recurring transaction exists.
- Add a controller test covering the failed-update render path.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix: use blank name instead of blank date to trigger validation failure in mark-recurring test
The CI test_unit run flagged a real bug in the test itself: TransactionsController#entry_params
strips blank :date/:amount before update, so date: "" never reached model validation and the
update succeeded (302) instead of failing (422) as the test expected. Use a blank :name instead,
which isn't stripped, and add DOM assertions (disabled button, no mark_as_recurring form action)
per CodeRabbit's follow-up review.
Verified against a live NAS Rails console reproduction (bypassing the test stack's broken
fixtures) that the failed-update render now correctly shows the button as disabled with the
"already exists" message and no action link.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix: use transaction id instead of entry id in mark-recurring route assertion
mark_as_recurring is a member route on the transactions resource keyed by the
Transaction's id, not the Entry's id (Entry uses delegated_type, so Entry and
its Transaction entryable have distinct ids). The prior assertion built the
path from `entry`, which could produce a different URL than the one actually
rendered, so the "no href" check could pass even if the button leaked a link.
Use entry.entryable so the assertion matches the real route.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix: refresh mark-recurring state on turbo_stream update, avoid unconditional query, keep DS::Button href
Addresses jjmata's review on PR #3103:
- Extract the "Mark as Recurring" block into a dom_id-wrapped partial and
replace it in the successful update turbo_stream response, so inline
edits that change whether the transaction matches an existing manual
recurring transaction are reflected immediately instead of only on the
next full page render.
- Skip the existing_manual_recurring_transaction lookup entirely when the
block won't be rendered (no edit permission, or split-child entry),
avoiding an unconditional extra query on every transaction show/failed
update.
- Keep href present on the DS::Button and only toggle disabled, matching
the established pattern elsewhere in the app, instead of nulling href
(which flips the component to a bare <button> and leaks a stray
method="post" attribute).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: Gerald <248542187+gfr-free@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* fix(accounts): remember transaction page size across account navigation
per_page was only ever read from the current request's query string, so
switching accounts always reset the activity feed back to 10 entries.
Reuses TransactionsController's existing prev_transaction_page_params
session store so the chosen page size applies consistently on both the
account detail page and the global transactions page.
Closes#3082
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(accounts): validate stored per_page and preserve it across filtered requests
Addresses review feedback on #3084: safe_per_page now validates the
stored default against the allowed values (a raw stored value like
"1" was previously passed through unchecked), TransactionsController
no longer wipes the remembered per_page when a request supplies
filters but omits per_page, and Session#prev_transaction_page_params
normalizes a NULL value to an empty hash.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(transactions): use stored per_page as pagy fallback on filtered requests
store_params! already preserved the previously-selected per_page in the
session when a filtered request (e.g. dashboard money-flow links) omitted
it, but TransactionsController#index still called safe_per_page with its
hardcoded default of 10, so the stored preference was never applied to the
actual page rendered.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: Gerald <248542187+gfr-free@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* fix(recurring): include amount in manual recurring duplicate check
TransactionsController#mark_as_recurring blocked a second manual
recurring transaction whenever an existing one shared the same
account + payee name/merchant + currency, even when the amount
differed -- stricter than the DB unique indexes
(idx_recurring_txns_acct_name / idx_recurring_txns_acct_merchant),
RecurringTransaction::Identifier's own grouping key, and the
equivalent check already used in TransfersController#mark_as_recurring.
Add amount to the duplicate lookup so two distinct recurring payments
to the same payee at different amounts are both allowed, while an
exact duplicate is still blocked. Also rescue
ActiveRecord::RecordNotUnique around the create call so a race between
the pre-check and the DB constraint (e.g. a double-submit) surfaces
the same friendly "already exists" message instead of a generic
error, mirroring the existing race-handling pattern in
RecurringTransaction::Identifier.
Fixes#2936
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(recurring): don't blend distinct charge amounts into variance band
Once two manual recurring rows with the same payee/different amounts
can coexist (this PR), RecurringTransaction.create_from_transaction's
variance-band discovery still matched historical entries only by
account/payee/currency/day-window -- never by amount -- so it could
blend genuinely unrelated charges (e.g. a fee + a due from the same
merchant, same day) into one row's expected_amount_min/max/avg.
Flagged by Codex review on this PR.
Confirmed this is not hypothetical: two real production transactions
(3.00 and 19.68, same merchant, same day) got blended into a single
recurring row showing a fabricated "11.34" projected amount that
matches neither real transaction.
The same unfiltered matching independently exists in
RecurringTransaction::Identifier#manual_recurring_matches_entry?,
which periodically re-derives every manual recurring row's variance
after each sync (via IdentifyRecurringTransactionsJob). Both call
sites needed the fix together, or the job would silently re-blend
amounts on the next sync.
Add RecurringTransaction.amount_within_variance_band?(candidate,
anchor, ratio: 2) -- a candidate only counts as "the same fluctuating
payment" if it's within 2x (double/half) of the anchor. Anchored on
the target amount (not pairwise) so unrelated charges can't chain
together; ratio-based (not %-of-target-with-floor) so it's
scale-invariant and handles signed (expense) amounts correctly.
Threshold checked against real data: existing variance test fixtures
sit at ~1.2-1.3x (must stay included), the real corrupted case sits
at ~6.6x (must be excluded) -- 2x leaves comfortable margin on both
sides.
Wire this into find_matching_transaction_entries/
find_matching_transaction_amounts (SQL-level filter, same pattern as
the existing day-of-month bounds) and into
manual_recurring_matches_entry?. amount_window_scope/
matching_transactions and create_from_transfer need no changes --
confirmed by reading: the former only consumes an already-computed
band, the latter never does variance discovery at all.
Does not touch any already-corrupted production data -- deliberately
out of scope, discussed separately.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* fix(ci): skip scheduled preview cleanup on forks
Only run the hourly Cloudflare preview cleanup on we-promise/sure,
where the required secrets exist.
* Preload transfer counterparty associations on transactions index
Transfer#categorizable? walks inflow_transaction.entry.account during list
render, which N+1'd transactions, entries, and accounts per transfer row.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Assert transfer rows render in transactions index N+1 test
* Broaden transactions index N+1 SQL matchers for lazy loads
* Drop unused outflow transfer preloads on transactions index
* Treat only equality SQL lookups as N+1 in index test
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(transactions): add inline tag creation and search in transaction forms
* fix(transactions): add tag-only update endpoint for edit drawer
* feat(transactions): implement TagSelectComponent for improved tag selection and management
* feat(tag-select): refactor tag selection component for improved functionality and accessibility
* feat(tag-select): implement inline tag rendering and error handling in tag selection component
* refactor(tag-select): remove unused list target from tag select controller
* fix: return forbidden JSON for denied tag updates
* fix: lock transaction tags when clearing them
* refactor: move tag select into DS namespace
* refactor: add multiselect trigger form field style
* fix: auto-position tag select dropdowns
* feat: add keyboard navigation to tag select
* feat: add create tag and search placeholder to transaction forms in multiple languages
* style: tighten tag select option spacing
* fix: align tag select spacing and focus behavior
* refactor: render tag badges with DS pill
---------
Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
* Show inflow/outflow totals when filtering by transfers
When filtering transactions by "Transfer" type, the summary bar previously
showed $0 for both Income and Expenses because transfers were excluded from
those sums. Now computes transfer inflow/outflow in the same SQL pass and
switches labels to "Inflow"/"Outflow" when transfer amounts are non-zero.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add mixed filter comment and transfer-only test coverage
Document the intentional mixed filter behavior where transfer amounts
are excluded from the summary bar when non-transfer types are present.
Add test exercising Inflow/Outflow label switching for transfer-only results.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add protection indicator to entries and unlock functionality
- Introduced protection indicator component rendering on hover and in detail views.
- Added support to unlock entries, clearing protection flags (`user_modified`, `import_locked`, and locked attributes).
- Updated routes, controllers, and models to enable unlock functionality for trades and transactions.
- Refactored views and localized content to support the new feature.
- Added relevant tests for unlocking functionality and attribute handling.
* feat: improve sync protection and turbo stream updates for entries
- Added tests for turbo stream updates reflecting protection indicators.
- Ensured user-modified entries lock specific attributes to prevent overwrites.
- Updated controllers to mark entries as user-modified and reload for accurate rendering.
- Enhanced protection indicator rendering using turbo frames.
- Applied consistent lock state handling across trades and transactions.
* Address PR review comments for protection indicator
---------
Co-authored-by: luckyPipewrench <luckypipewrench@proton.me>
* Change email address
* Email confirmation
* Email change test
* Lint
* Schema reset
* Set test email sender
* Select specific user fixture
* Refactor/cleanup
* Remove unused email_confirmation_token
* Current user would never be true
* Fix translation test failures
* Basic plaid data model and linking
* Remove institutions, add plaid items
* Improve schema and Plaid provider
* Add webhook verification sketch
* Webhook verification
* Item accounts and balances sync setup
* Provide test encryption keys
* Fix test
* Only provide encryption keys in prod
* Try defining keys in test env
* Consolidate account sync logic
* Add back plaid account initialization
* Plaid transaction sync
* Sync UI overhaul for Plaid
* Add liability and investment syncing
* Handle investment webhooks and process current day holdings
* Remove logs
* Remove "all" period select for performance
* fix amount calc
* Remove todo comment
* Coming soon for investment historical data
* Document Plaid configuration
* Listen for holding updates
* Add sync model
* Fresh fixtures for sync tests
* Sync tests overhaul
* Fix entry tests
* Complete remaining model test updates
* Update system tests
* Update demo data task
* Add system tests back to PR checks
* More simplifications, add empty family to fixtures for easier testing
* Initial entryable models
* Update transfer and tests
* Update transaction controllers and tests
* Update sync process to use new entries model
* Get dashboard working again
* Update transfers, imports, and accounts to use Account::Entry
* Update system tests
* Consolidate transaction management into entries controller
* Add permitted partial key helper
* Move account transactions list to entries controller
* Delegate transaction entries search
* Move transfer relation to entry
* Update bulk transaction management flows to use entries
* Remove test code
* Test fix attempt
* Update demo data script
* Consolidate remaining transaction partials to entries
* Consolidate valuations controller to entries controller
* Lint fix
* Remove unused files, additional cleanup
* Add back valuation creation
* Make migrations fully reversible
* Stale routes cleanup
* Migrations reversible fix
* Move types to entryable concern
* Fix search when no entries found
* Remove more unused code
* Move Valuation to Account namespace
* Move account history to controller
* Clean up valuation controller and views
* Translations and cleanup
* Remove unused scopes and methods
* Pass brakeman
* Add transfer model and clean up family snapshot fixtures
* Ignore transfers in income and expense snapshots
* Add transfer validations
* Implement basic transfer matching UI
* Fix merge conflicts
* Add missing translations
* Tweak selection states for transfer types
* Add missing i18n translation
* Clean up transaction show view, add delete button
* Clean up tailwind global styles, add switch
* Bulk deletion controller and tests
* Normalize translations
* Add bulk deletion button and form
An overhaul and cleanup of the transactions feature including:
- Simplification of transactions search and filtering
- Consolidation of account sync logic after transaction change
- Split sidebar modal and modal into "drawer" and "modal" concepts
- Refactor of transaction partials and folder organization
- Cleanup turbo frames and streams for transaction updates, including new Transactions::RowsController for inline updates
- Refactored and added several integration and systems tests
* Rename account balance field for clarity
`original_balance` and `original_currency` may infer that these values are "original" to the account. In reality, they represent the "current" balance and currency on the account.
* Prepare fixture data for account sync testing
* Update to new field
* Fix conflicts
* Remove local schema change
* Transaction scaffold
* Rough in transaction views
* Fix sort order
* Fix mass assignment issue
* Fix test
* Simplify CI workflow
* Don't seed db before test