Skip to content

Add per-account toggle to disable automatic transaction categorization#1700

Open
mike-lloyd03 wants to merge 1 commit intowe-promise:mainfrom
mike-lloyd03:feature/per-account-category-matcher-toggle
Open

Add per-account toggle to disable automatic transaction categorization#1700
mike-lloyd03 wants to merge 1 commit intowe-promise:mainfrom
mike-lloyd03:feature/per-account-category-matcher-toggle

Conversation

@mike-lloyd03
Copy link
Copy Markdown

@mike-lloyd03 mike-lloyd03 commented May 7, 2026

This PR comes out of a discussion on Discord. I'd like to have transactions imported from Plaid not use the category matcher and just leave them uncategorized.

This PR (writted by Claude with me guiding and testing), adds a new toggle to disable this functionality on a per-account basis.

Summary

  • Adds enable_category_matcher boolean column to accounts (default true, preserving existing behavior) so users can opt out of Plaid's automatic category suggestions on a per-account basis
  • When disabled, newly synced transactions arrive uncategorized — leaving categories to rules or manual assignment
  • Toggle appears in the account edit modal (three-dot menu → Edit) for linked accounts only, saves via the normal "Update Account" submit button.
image

Details

The Plaid integration maps personal_finance_category.detailed to user-defined categories via PlaidEntry::Processor. This is unconditional today — the initial category assignment can't be prevented even if the user prefers to categorize manually. A per-account toggle is more flexible than a per-family setting since users may want auto-categorization on one account but not another.

The enable_category_matcher name is intentionally generic so it future-proofs for other providers (e.g. SimpleFIN) that may wire up category matching later.

What is not changing:

  • Existing categorized transactions are untouched — the toggle only affects future syncs
  • Rules, AI categorization, and merchant enrichment are unaffected
  • SimpleFIN's category matcher (currently dead code in its processor) is not modified

Test plan

  • Visit /accounts → three-dot menu → Edit on a Plaid-linked account → confirm toggle renders defaulting to on
  • Toggle off → click "Update Account" → reopen modal → confirm toggle is still off
  • Confirm toggle does not appear for manual (unlinked) accounts
  • bin/rails test test/models/plaid_entry/processor_test.rb passes
  • bin/rails test passes
  • bin/rubocop -f github -a clean
  • bundle exec erb_lint ./app/**/*.erb -a clean
  • bin/brakeman --no-pager clean

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features
    • Added category matcher toggle for linked accounts—users can now enable or disable automatic transaction categorization directly from account settings.

@brin-security-scanner brin-security-scanner Bot added contributor:verified Contributor passed trust analysis. pr:verified PR passed security analysis. labels May 7, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 7, 2026

Review Change Stack
No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 142d6bca-c50f-46f2-a290-f480065654f9

📥 Commits

Reviewing files that changed from the base of the PR and between 44d3a55 and 0badaa4.

📒 Files selected for processing (5)
  • app/controllers/accounts_controller.rb
  • app/controllers/concerns/accountable_resource.rb
  • app/models/plaid_entry/processor.rb
  • app/views/accounts/_form.html.erb
  • config/locales/views/accounts/en.yml
🚧 Files skipped from review as they are similar to previous changes (4)
  • app/controllers/concerns/accountable_resource.rb
  • app/models/plaid_entry/processor.rb
  • app/controllers/accounts_controller.rb
  • config/locales/views/accounts/en.yml

📝 Walkthrough

Walkthrough

This PR adds a per-account boolean flag to enable/disable automatic category matching, including DB column and migration, a protected PATCH route and controller action to toggle it, strong-param whitelist, processor guard to skip matching when disabled, UI toggle and locales, and updated tests.

Changes

Category Matcher Toggle Feature

Layer / File(s) Summary
Database Schema
db/migrate/20260429120000_add_enable_category_matcher_to_accounts.rb, db/schema.rb
Adds enable_category_matcher boolean column to accounts with default: true and null: false; updates schema version.
API Routing & Authorization
config/routes.rb, app/controllers/accounts_controller.rb
Adds PATCH /accounts/:id/toggle_category_matcher route and toggle_category_matcher action; action casts nested param to boolean, calls update!, and returns 204 No Content; action protected by existing set_manageable_account before_action.
Parameter Handling
app/controllers/concerns/accountable_resource.rb
Expands account_params to permit :enable_category_matcher.
Processing Logic Guard
app/models/plaid_entry/processor.rb
matched_category now returns early with nil if account absent or enable_category_matcher? is false; otherwise proceeds with existing matching logic.
Form UI & Localization
app/views/accounts/_form.html.erb, config/locales/views/accounts/en.yml
Adds conditional enable_category_matcher toggle shown when account is linked and persisted; adds label and hint translations and minor YAML formatting adjustments.
Test Coverage
test/models/plaid_entry/processor_test.rb, test/system/accounts_test.rb
Adds unit test asserting category matcher is not invoked when feature disabled and verifies resulting transaction has no category; adjusts system test to expect h3 heading after update.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • we-promise/sure#1130: Modifies AccountsController by adding a member action and updating the set_manageable_account before_action whitelist; closely related to routing/authorization changes.

Suggested reviewers

  • jjmata
  • sokie

🐰 A tiny toggle, tucked in the form so neat,
Flip on the matcher — transactions find their seat,
With schema and guard the rabbit hops along,
Tests hum a tune and the code sings a song,
Hooray for tidy flags and a job well complete!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding a per-account toggle to control automatic transaction categorization, which aligns perfectly with the primary objectives and all changes in the PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/controllers/concerns/accountable_resource.rb (1)

104-109: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Constrain enable_category_matcher in shared strong params

Line 108 permits this flag through generic account create/update params. That allows crafted requests to set it outside the linked-account toggle flow, which can produce unexpected behavior later (for example after linking a previously manual account). Prefer permitting this key only in toggle_category_matcher (or conditionally when the target account is linked).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/controllers/concerns/accountable_resource.rb` around lines 104 - 109, The
strong params block in params.require(:account).permit currently allows
:enable_category_matcher unconditionally, letting callers set it outside the
intended toggle flow; remove :enable_category_matcher from the generic permit
list in AccountableResource (the params.require(:account).permit call) and
instead allow it only in the dedicated toggle action (e.g.,
toggle_category_matcher) or conditionally when the account is linked (check
account.linked? or equivalent) before permitting or applying the change; update
the toggle_category_matcher controller method to accept and validate
enable_category_matcher and ensure other create/update flows never accept that
key.
🧹 Nitpick comments (1)
test/system/accounts_test.rb (1)

158-158: ⚡ Quick win

Prefer a less brittle assertion than fixed heading level

Line 158 couples this flow test to h3. Asserting text presence (or a stable test id) will reduce false failures from purely visual/semantic heading changes.

Suggested tweak
-      assert_selector "h3", text: "Updated account name"
+      assert_text "Updated account name"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/system/accounts_test.rb` at line 158, The test currently uses a brittle
selector assert_selector "h3", text: "Updated account name"; replace it with a
more stable assertion such as asserting the text exists (e.g., use assert_text
"Updated account name") or target a stable test id/data attribute (e.g.,
assert_selector "[data-test='account-name']", text: "Updated account name") so
the spec no longer depends on the heading level; update the assertion in the
accounts_test flow that references "Updated account name" accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/controllers/accounts_controller.rb`:
- Around line 98-100: The toggle_category_matcher action currently casts
params.dig(...) which treats a missing key as false; instead require and permit
the param so missing requests fail fast. Update toggle_category_matcher to pull
params via something like
params.require(:account).permit(:enable_category_matcher) (or use require +
fetch on :enable_category_matcher) and then cast the permitted value with
ActiveModel::Type::Boolean before calling `@account.update`! so malformed/partial
requests raise ParameterMissing rather than silently disabling the feature.

---

Outside diff comments:
In `@app/controllers/concerns/accountable_resource.rb`:
- Around line 104-109: The strong params block in
params.require(:account).permit currently allows :enable_category_matcher
unconditionally, letting callers set it outside the intended toggle flow; remove
:enable_category_matcher from the generic permit list in AccountableResource
(the params.require(:account).permit call) and instead allow it only in the
dedicated toggle action (e.g., toggle_category_matcher) or conditionally when
the account is linked (check account.linked? or equivalent) before permitting or
applying the change; update the toggle_category_matcher controller method to
accept and validate enable_category_matcher and ensure other create/update flows
never accept that key.

---

Nitpick comments:
In `@test/system/accounts_test.rb`:
- Line 158: The test currently uses a brittle selector assert_selector "h3",
text: "Updated account name"; replace it with a more stable assertion such as
asserting the text exists (e.g., use assert_text "Updated account name") or
target a stable test id/data attribute (e.g., assert_selector
"[data-test='account-name']", text: "Updated account name") so the spec no
longer depends on the heading level; update the assertion in the accounts_test
flow that references "Updated account name" accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ddfd8dcc-8684-47a1-9947-883bc11f452f

📥 Commits

Reviewing files that changed from the base of the PR and between 7e1de42 and 44d3a55.

📒 Files selected for processing (10)
  • app/controllers/accounts_controller.rb
  • app/controllers/concerns/accountable_resource.rb
  • app/models/plaid_entry/processor.rb
  • app/views/accounts/_form.html.erb
  • config/locales/views/accounts/en.yml
  • config/routes.rb
  • db/migrate/20260429120000_add_enable_category_matcher_to_accounts.rb
  • db/schema.rb
  • test/models/plaid_entry/processor_test.rb
  • test/system/accounts_test.rb

Comment on lines +98 to +100
def toggle_category_matcher
value = ActiveModel::Type::Boolean.new.cast(params.dig(:account, :enable_category_matcher))
@account.update!(enable_category_matcher: value)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Require the toggle param before casting to avoid accidental disable

At Line 99, cast(params.dig(...)) treats a missing key as false, so malformed/partial requests can silently disable category matching. Require + permit this field and fail fast when absent.

Proposed fix
 def toggle_category_matcher
-  value = ActiveModel::Type::Boolean.new.cast(params.dig(:account, :enable_category_matcher))
+  value = ActiveModel::Type::Boolean.new.cast(
+    params.require(:account).permit(:enable_category_matcher).fetch(:enable_category_matcher)
+  )
   `@account.update`!(enable_category_matcher: value)
   head :no_content
 end

As per coding guidelines: app/controllers/**/*.rb: “Implement strong parameters and CSRF protection throughout the application”.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def toggle_category_matcher
value = ActiveModel::Type::Boolean.new.cast(params.dig(:account, :enable_category_matcher))
@account.update!(enable_category_matcher: value)
def toggle_category_matcher
value = ActiveModel::Type::Boolean.new.cast(
params.require(:account).permit(:enable_category_matcher).fetch(:enable_category_matcher)
)
`@account.update`!(enable_category_matcher: value)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/controllers/accounts_controller.rb` around lines 98 - 100, The
toggle_category_matcher action currently casts params.dig(...) which treats a
missing key as false; instead require and permit the param so missing requests
fail fast. Update toggle_category_matcher to pull params via something like
params.require(:account).permit(:enable_category_matcher) (or use require +
fetch on :enable_category_matcher) and then cast the permitted value with
ActiveModel::Type::Boolean before calling `@account.update`! so malformed/partial
requests raise ParameterMissing rather than silently disabling the feature.

Adds an `enable_category_matcher` boolean (default: true) to accounts so
users can opt out of Plaid's automatic category suggestions on a per-account
basis. When disabled, newly synced transactions arrive uncategorized so
rules or manual assignment take precedence.

- New migration adds `enable_category_matcher` column (default true, null: false)
- `PlaidEntry::Processor#matched_category` gates the CategoryMatcher call on the account flag
- Toggle rendered in the account edit modal for linked accounts (saves via main form submit)
- `AccountableResource#account_params` permits the new field
- `AccountsController#toggle_category_matcher` action added for potential API use
- i18n strings added for label and hint text
- Unit test covers the disabled-matcher path

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
@mike-lloyd03 mike-lloyd03 force-pushed the feature/per-account-category-matcher-toggle branch from 44d3a55 to 0badaa4 Compare May 7, 2026 20:27
Copy link
Copy Markdown
Collaborator

jjmata commented May 8, 2026

Solid, well-scoped feature — good backward-compatible default and clean guard placement. A few things worth clarifying:

Turbo/response wiring for the toggle
toggle_category_matcher returns head :no_content (204), while the sibling toggle_active uses redirect_to. If form.toggle :enable_category_matcher in _form.html.erb is part of the regular "Update Account" form, the value will flow through account_params to the normal update action — and the dedicated toggle_category_matcher route would be unused from the current UI. If the toggle is instead meant to auto-save immediately via Turbo (e.g. with a data-action="change->..." attribute), a 204 response needs a corresponding Turbo stream or the toggle will appear to silently succeed with no UI acknowledgement. Worth confirming the wiring is complete end-to-end.

SimpleFIN parity
enable_category_matcher is intentionally named generically — nice forward-thinking. When SimpleFIN's category matcher is eventually wired up, the same account&.enable_category_matcher? guard will need to be applied in its processor too. A # TODO: apply enable_category_matcher? guard here comment near the SimpleFIN processor call site would make that easy to track down.

h2 → h3 in system test
The system test change (assert_selector "h3" replacing assert_selector "h2") looks unrelated to this feature. Is this catching a pre-existing heading level change in the account show view, or did something in this PR cause it? If it's pre-existing, it's fine to include but worth a note in the description so reviewers know it isn't a regression from this PR.

YAML multi-line reformatting
The confirm_body_html: and success_login: keys were reformatted from flow scalars to block scalars. Valid YAML, but confirm the rendered strings are byte-for-byte identical — block scalars handle trailing newlines and whitespace differently depending on whether | (literal) or > (folded) style is used. The current reformatting uses a plain block scalar (no |/> indicator), which should be fine, but worth a quick sanity-check render.

Reviewed by Claude Code


Generated by Claude Code

@jjmata jjmata self-requested a review May 8, 2026 18:45
Copy link
Copy Markdown
Collaborator

@jjmata jjmata left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please fix broken tests and address PR review comments, @mike-lloyd03. 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor:verified Contributor passed trust analysis. pr:verified PR passed security analysis.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants