Skip to content

feat(settings): add reviewed bulk cleanup flows#1630

Open
JSONbored wants to merge 23 commits intowe-promise:mainfrom
JSONbored:codex/feat-settings-bulk-cleanup
Open

feat(settings): add reviewed bulk cleanup flows#1630
JSONbored wants to merge 23 commits intowe-promise:mainfrom
JSONbored:codex/feat-settings-bulk-cleanup

Conversation

@JSONbored
Copy link
Copy Markdown
Contributor

@JSONbored JSONbored commented May 1, 2026

Summary

  • Adds reviewed bulk cleanup flows for merchants, categories, and account metadata.
  • Keeps suggestions human-reviewed before applying changes.
  • Adds safer validation around merge targets and reviewed website/domain input.

Feature objective

Large imported or synced datasets can create duplicate merchants/categories and incomplete account/provider metadata. This PR adds reviewed, family-scoped bulk cleanup flows so users can clean up common data issues faster without auto-applying destructive changes.

What changed

  • Adds family-scoped bulk merge/update flows.
  • Preserves provider merchant records while allowing local cleanup.
  • Supports bulk website/domain updates for better logo and institution metadata.
  • Adds merchant merge UI behavior so users choose either an existing target or create a new target, not both.
  • Rejects invalid reviewed website input instead of silently dropping it.

Screenshots

Bulk update institution domains Bulk update merchant websites Merchant merge mutual exclusion Category merge

Why

Large imported or synced datasets often create duplicate merchants, categories, and incomplete account/provider metadata. Reviewed bulk cleanup makes Sure easier to maintain while keeping changes scoped, transactional, and reviewable before applying.

Validation

  • bin/rails test test/controllers/accounts_controller_test.rb test/controllers/categories_controller_test.rb test/controllers/family_merchants_controller_test.rb test/models/account_test.rb test/models/merchant_test.rb
  • bin/rails test test/controllers/family_merchants_controller_test.rb
  • npm run lint
  • scoped bin/rubocop
  • browser verification for the new cleanup dialogs/merchant merge behavior
  • git diff --check

Notes

  • Screenshots added for the new user-facing flows.

Summary by CodeRabbit

  • New Features

    • Bulk edit institution domains (modal) with domain normalization, selection validation, and success/failure messaging
    • Bulk update merchant websites (modal) with domain extraction, logo refresh, and failure handling
    • Merge categories (modal) supporting create-or-select target, validations, and merge reporting
    • Client-side merge UI syncing for existing vs new targets
  • Other

    • Deterministic default merchant color and centralized Brandfetch-based logo URLs
    • Accounts persist normalized institution domains
  • Tests

    • Added integration and model tests for bulk flows, merges, and domain/logo normalization

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 1, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds bulk-edit UIs and controller actions for account institution domains and family-merchant websites; centralizes domain extraction and Brandfetch logo URL generation on Merchant; normalizes account institution domains; implements Category::Merger with controller wiring, views, routes, locales, Stimulus, and expanded tests.

Changes

Account Bulk Domains & Institution Domain Normalization

Layer / File(s) Summary
Data Shape & Normalization
app/models/account.rb
Added Account.normalize_institution_domain(value), before_validation hook when institution_domain will change, and normalize_institution_domain_value; logo_url now uses normalized domain.
Bulk Update Action
app/controllers/accounts_controller.rb
Added bulk_domains and bulk_update_domains actions, bulk_domain_params, domain normalization, scoped writable accounts via bulk_domain_accounts, transactional updates, and RecordInvalid rescue with alerts.
UI & Routes
app/views/accounts/bulk_domains.html.erb, app/views/accounts/index.html.erb, config/routes.rb
Added bulk domains dialog view, index link, and collection routes GET :bulk_domains / POST :bulk_update_domains.
Localization & Tests
config/locales/views/accounts/en.yml, test/controllers/accounts_controller_test.rb, test/models/account_test.rb
Added i18n keys and tests for modal rendering, scoped writable updates, normalization persistence, and failure redirects.

Category Merge Workflow

Layer / File(s) Summary
Merge Logic & Validation
app/models/category/merger.rb
New Category::Merger with family-membership checks, ancestor-cycle protection, reparenting validation, methods to merge transactions, budget_categories, reparent children, destroy sources, and expose merged_count; defines UnauthorizedCategoryError.
Controller Wiring
app/controllers/categories_controller.rb
Added merge and perform_merge actions, merge param handling, conflict/selection guards, target resolution/creation, transactional merge_categories!, and rescues for MergeTargetNotFound, EmptyCategoryMerge, Category::Merger::UnauthorizedCategoryError, and ActiveRecord errors.
Helpers
app/controllers/categories_controller.rb
Added category_merge_params, conflicting_merge_target?, target_selected_as_source?, merge_target_category, merge_categories!, and record_error_message.
UI & Routes
app/views/categories/merge.html.erb, app/views/categories/index.html.erb, config/routes.rb
Added merge dialog view, index action link, and collection routes GET :merge / POST :perform_merge.
Localization & Tests
config/locales/views/categories/en.yml, test/controllers/categories_controller_test.rb
Added i18n strings and extensive tests covering success, rollback, conflicting inputs, hierarchy validation, reparenting, and cross-family ignores.

Family Merchant Bulk Websites & Merchant URL/Logo Centralization

Layer / File(s) Summary
URL & Logo Helpers
app/models/merchant.rb
Added Merchant.extract_domain(url) with URI-safe sanitizer fallback and Merchant.brandfetch_logo_url_for(url) that builds Brandfetch CDN URL guarded by settings.
Model Refactor & Defaults
app/models/family_merchant.rb, app/models/provider_merchant.rb
Removed per-class extract_domain helpers; generate_logo_url_from_website / generate_logo_url_from_website! now call Merchant.brandfetch_logo_url_for; FamilyMerchant.default_color added and used.
Bulk Website Actions
app/controllers/family_merchants_controller.rb
Added bulk_websites and bulk_update_websites: strong params, domain extraction/validation, family scoping, transactional updates, regenerate ProviderMerchant logos, and ActiveRecord error handling using InvalidMerchantWebsite.
Merge UI & JS Wiring
app/views/family_merchants/merge.html.erb, app/javascript/controllers/merchant_merge_target_controller.js
Refactored merge form to include new-target inputs and merchant-merge-target Stimulus controller; JS syncs/disables sections and intercepts submit.
UI & Routes
app/views/family_merchants/bulk_websites.html.erb, app/views/family_merchants/index.html.erb, config/routes.rb
Added bulk websites dialog view, index link, and collection routes GET :bulk_websites / POST :bulk_update_websites.
Localization & Tests
config/locales/views/merchants/en.yml, test/controllers/family_merchants_controller_test.rb, test/models/merchant_test.rb
Added i18n keys and tests for merge-with-new-target, new-target domain normalization/validation, scoped bulk updates refreshing Brandfetch logos, malformed-URL rejection, ActiveRecord-error handling, and logo clearing when Brandfetch is unavailable.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant AC as AccountsController
    participant Account as Account
    participant DB as Database

    User->>AC: POST bulk_update_domains (institution_domain, account_ids)
    AC->>AC: normalize_institution_domain(domain)
    AC->>AC: scope writable accounts for family
    AC->>DB: begin transaction
    loop per selected writable account
        AC->>Account: update!(institution_domain: normalized)
        Account-->>DB: write
    end
    DB->>AC: commit
    AC->>User: redirect with success or alert on failure
Loading
sequenceDiagram
    participant User as User
    participant CC as CategoriesController
    participant Merger as Category::Merger
    participant DB as Database

    User->>CC: POST perform_merge (target/new_target, source_ids)
    CC->>CC: validate params & resolve/create target
    CC->>Merger: new(family, target, sources)
    Merger->>Merger: validate ownership & hierarchy
    Merger->>DB: begin transaction
    Merger->>DB: update transactions to target
    Merger->>DB: merge budget_categories
    Merger->>DB: reparent children & destroy sources
    DB->>Merger: commit
    Merger->>CC: return merged_count
    CC->>User: redirect with success or alert on failure
Loading
sequenceDiagram
    participant User as User
    participant FMC as FamilyMerchantsController
    participant Merchant as Merchant
    participant Prov as ProviderMerchant
    participant DB as Database

    User->>FMC: POST bulk_update_websites (website_url, merchant_ids)
    FMC->>Merchant: extract_domain(website_url)
    Merchant-->>FMC: normalized domain or nil
    FMC->>FMC: validate selection & domain
    FMC->>DB: begin transaction
    loop per selected merchant in family
        FMC->>DB: update merchant.website_url
        alt merchant is ProviderMerchant
            Prov->>Merchant: brandfetch_logo_url_for(website_url)
            Merchant-->>Prov: brandfetch url or nil
            Prov->>DB: update logo_url
        end
    end
    DB->>FMC: commit
    FMC->>User: redirect with success or alert on failure
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

codex

Suggested reviewers

  • jjmata
  • michelroegl-brunner

"🐰
I hopped through code and trimmed each domain,
Merged tiny categories with a careful hop,
Bulk edits ran in transactions—no stop,
Logos fetched and set, defaults neat and true,
A rabbit cheers: tidy changes, all anew."

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.92% 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 'feat(settings): add reviewed bulk cleanup flows' clearly summarizes the main changes, which involve adding bulk cleanup/merge features for merchants, categories, and accounts with human review.
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

Warning

Review ran into problems

🔥 Problems

Timed out fetching pipeline failures after 30000ms


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.

@JSONbored JSONbored force-pushed the codex/feat-settings-bulk-cleanup branch from 366477a to 66b8e96 Compare May 2, 2026 00:52
@JSONbored JSONbored marked this pull request as ready for review May 2, 2026 06:08
@brin-security-scanner brin-security-scanner Bot added contributor:flagged Contributor flagged for review by trust analysis. pr:verified PR passed security analysis. and removed contributor:flagged Contributor flagged for review by trust analysis. labels May 2, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 66b8e96c82

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread app/controllers/accounts_controller.rb Outdated
Copy link
Copy Markdown
Contributor

@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: 9

🧹 Nitpick comments (1)
test/controllers/family_merchants_controller_test.rb (1)

48-62: ⚡ Quick win

Make the merge preconditions explicit in this test.

This currently depends on merchants(:amazon) being a current-family FamilyMerchant and on transactions(:one) already pointing at it. If either fixture changes, the assertion stops proving the merge behavior. Create the source merchant here and assign the transaction before posting so the test remains self-contained.

As per coding guidelines, create edge cases on-the-fly within test context instead of in fixtures; keep fixtures minimal (2-3 per model).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/controllers/family_merchants_controller_test.rb` around lines 48 - 62,
The test "should merge selected merchants into a new family merchant" relies on
fixtures (merchants(:amazon) and transactions(:one)); make it self-contained by
creating the source FamilyMerchant and reassigning the transaction within the
test before calling post perform_merge_family_merchants_path: instantiate a new
FamilyMerchant (associated with `@user.family`) to act as source instead of
merchants(:amazon), set transactions(:one).merchant = source and save (or create
a new transaction tied to that merchant), then perform the post with source_ids:
[source.id] and assert the merge results (FamilyMerchant.find_by!(family:
`@user.family`, name: "Streaming"), reload the transaction and check merchant, and
assert_not FamilyMerchant.exists?(source.id)).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/controllers/accounts_controller.rb`:
- Around line 45-57: The bulk_update_domains action reads params directly;
create and use a strong-params helper (e.g., bulk_domain_params) to permit
:institution_domain (string) and account_ids: [] and replace direct uses of
params[:institution_domain] and params[:account_ids] with values from
bulk_domain_params; ensure you call
Account.normalize_institution_domain(bulk_domain_params[:institution_domain])
and query bulk_domain_accounts.where(id: bulk_domain_params[:account_ids]) so
the controller uses permitted input before validation and the transactional
update (accounts.each { |account| account.update!(institution_domain: domain) })
runs only on whitelisted attributes.

In `@app/controllers/categories_controller.rb`:
- Around line 80-83: merge_target_category currently creates a new category when
both target_id and new_target_name are provided, which wrongly ignores the
explicit target; update the validation so that when both params[:target_id] and
params[:new_target_name] are present (in the flow around merge_target_category
and the similar block at lines 125-135), treat this as invalid input and
redirect back with an alert (e.g., t(".conflicting_target_params")) instead of
creating a new category; adjust merge_target_category (and the controller action
that assigns target = merge_target_category) to first check for the conflict and
return nil or raise a controlled error so the existing redirect_to
merge_categories_path, alert: ... path is used.
- Around line 74-100: Wrap the incoming merge payload in a strong-params helper
and use it in perform_merge and the merge_target_category helper: implement a
private method merge_params that does params.require(:merge).permit(source_ids:
[], :target_id, :new_target_name, :new_target_description,
:new_target_other_allowed_fields) (include the actual new_target_* names your
form sends), then replace direct uses of params[:source_ids],
params[:target_id], and params[:new_target_*] in perform_merge and in
merge_target_category with values from merge_params (e.g.
merge_params[:source_ids], merge_params[:target_id],
merge_params[:new_target_name]); ensure perform_merge still looks up sources via
Current.family.categories.where(id: merge_params[:source_ids]) and builds the
Category::Merger with the permitted target and source values.

In `@app/controllers/family_merchants_controller.rb`:
- Around line 109-124: The bulk_update_websites and related actions currently
read merchant_ids, source_ids, target_id and new_target_* directly from params;
create private strong-parameter helper methods (e.g. permitted_bulk_params and
permitted_merge_params) that use params.require(...).permit(merchant_ids: [],
source_ids: [], :target_id, :new_target_name, :new_target_field_x, ...) and
replace direct params access in bulk_update_websites and the merge/similar
methods so only permitted values are consumed; ensure array params use the []
form and that you call the helper (e.g. ids =
permitted_bulk_params[:merchant_ids]) before using the values.
- Around line 136-139: The controller currently lets merge_target_merchant
prefer new_target_name over target_id, which allows both inputs to be submitted
and causes ambiguous/destructive merges; update the controller actions that call
merge_target_merchant (the merge submission endpoints around the current target
resolution and the second similar block later) to validate params and reject
requests that provide both target_id and new_target_name: if both are present,
redirect_to merge_family_merchants_path (or the relevant form path) with an
alert (e.g. t(".ambiguous_target")) and do not proceed to merge_target_merchant;
ensure the check is performed before calling merge_target_merchant so exactly
one mode is supplied.

In `@app/models/account.rb`:
- Around line 141-149: The normalize_institution_domain logic is only a
controller/helper and imports (app/models/family/data_importer.rb) still write
raw values; move the normalization into the Account model so all writes are
canonical: add a before_validation callback (e.g., before_validation
:normalize_institution_domain!) or implement an institution_domain= setter in
Account that calls the existing normalize_institution_domain method, ensure
normalize_institution_domain is instance-level and used by that callback/setter,
and remove direct normalization from controllers/services so every write path
(including Family::DataImporter) uses the model-level normalization.
- Around line 141-149: The rescue in normalize_institution_domain currently
returns the raw URL string (value.to_s.strip), which preserves paths/invalid
chars; change the rescue to produce a host-only, sanitized domain: take s =
value.to_s.strip, remove any leading scheme via s.sub(/\Ahttps?:\/\//i, ""),
then split on / or : and keep the first segment, strip and remove leading "www."
(sub(/\Awww\./, "")), then return presence; update the rescue in
normalize_institution_domain to implement this host-extraction instead of
returning the original string.

In `@app/models/category/merger.rb`:
- Around line 43-49: validate_hierarchy! only checks the immediate parent
(target_category.parent_id == source.id) and misses ancestors, allowing merges
that would create cycles; update validate_hierarchy! to walk the full ancestor
chain of target_category (follow parent_id repeatedly or collect ancestor ids
via target_category.ancestors or a loop) and if any ancestor id equals any
source.id raise UnauthorizedCategoryError, ensuring reparent_subcategories
cannot set a category as its own parent; reference validate_hierarchy!,
source_categories, target_category.parent_id and reparent_subcategories when
making the change.

In `@app/models/merchant.rb`:
- Around line 13-20: The rescue branch in extract_domain currently returns the
raw input; change it to extract and return only a domain (or nil) instead of the
malformed URL. In the extract_domain method, after normalizing the string
(normalized_url), replace the rescue body with logic that strips any leading
scheme (normalized_url.sub(/\Ahttps?:\/\//, "")), takes the first path segment
(split("/").first), removes a leading "www." (sub(/\Awww\./, "")) and then
return that value only if it looks like a host (presence and optionally contains
a dot), otherwise return nil — do not return the original URL string.

---

Nitpick comments:
In `@test/controllers/family_merchants_controller_test.rb`:
- Around line 48-62: The test "should merge selected merchants into a new family
merchant" relies on fixtures (merchants(:amazon) and transactions(:one)); make
it self-contained by creating the source FamilyMerchant and reassigning the
transaction within the test before calling post
perform_merge_family_merchants_path: instantiate a new FamilyMerchant
(associated with `@user.family`) to act as source instead of merchants(:amazon),
set transactions(:one).merchant = source and save (or create a new transaction
tied to that merchant), then perform the post with source_ids: [source.id] and
assert the merge results (FamilyMerchant.find_by!(family: `@user.family`, name:
"Streaming"), reload the transaction and check merchant, and assert_not
FamilyMerchant.exists?(source.id)).
🪄 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: 7be46bdc-f14b-4d5c-a378-a1c099457660

📥 Commits

Reviewing files that changed from the base of the PR and between 268f5ae and 66b8e96c82aa6e0e3b5fc1288b9f4698e23ada73.

📒 Files selected for processing (22)
  • app/controllers/accounts_controller.rb
  • app/controllers/categories_controller.rb
  • app/controllers/family_merchants_controller.rb
  • app/models/account.rb
  • app/models/category/merger.rb
  • app/models/family_merchant.rb
  • app/models/merchant.rb
  • app/models/provider_merchant.rb
  • app/views/accounts/bulk_domains.html.erb
  • app/views/accounts/index.html.erb
  • app/views/categories/index.html.erb
  • app/views/categories/merge.html.erb
  • app/views/family_merchants/bulk_websites.html.erb
  • app/views/family_merchants/index.html.erb
  • app/views/family_merchants/merge.html.erb
  • config/locales/views/accounts/en.yml
  • config/locales/views/categories/en.yml
  • config/locales/views/merchants/en.yml
  • config/routes.rb
  • test/controllers/accounts_controller_test.rb
  • test/controllers/categories_controller_test.rb
  • test/controllers/family_merchants_controller_test.rb

Comment thread app/controllers/accounts_controller.rb
Comment thread app/controllers/categories_controller.rb
Comment thread app/controllers/categories_controller.rb Outdated
Comment thread app/controllers/family_merchants_controller.rb
Comment thread app/controllers/family_merchants_controller.rb Outdated
Comment thread app/models/account.rb
Comment thread app/models/category/merger.rb
Comment thread app/models/merchant.rb Outdated
@brin-security-scanner brin-security-scanner Bot added the contributor:flagged Contributor flagged for review by trust analysis. label May 2, 2026
Copy link
Copy Markdown
Contributor

@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: 2

♻️ Duplicate comments (1)
app/models/account.rb (1)

148-149: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Rescue path still returns a raw URL string instead of a domain (or nil).

For an input like "https://bank.com/%", URI.parse raises URI::InvalidURIError and the rescue path produces "https://bank.com/%" — a full URL with scheme and path — not a bare domain. That value passes the present? check in logo_url and is embedded directly into a Brandfetch CDN URL, producing a broken string (cdn.brandfetch.io/https://bank.com/%/...).

app/models/merchant.rb's extract_domain (line 22-23) already fixes this by returning nil on error — normalize_institution_domain should do the same.

🐛 Proposed fix
 rescue URI::InvalidURIError
-  value.to_s.strip.sub(/\Awww\./, "").presence&.downcase
+  nil
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/models/account.rb` around lines 148 - 149, The rescue in
normalize_institution_domain currently returns the original value string (e.g.
"https://bank.com/%") which yields a full URL instead of a bare domain; change
normalize_institution_domain so that on URI::InvalidURIError it returns nil (or
delegates to the same behavior as Merchant.extract_domain) rather than
value.to_s..., ensuring callers like logo_url won't embed an invalid full URL
into the Brandfetch CDN path.
🧹 Nitpick comments (2)
app/views/accounts/bulk_domains.html.erb (1)

16-16: ⚡ Quick win

Same border-gray-300 raw color class — use a design token.

Mirrors the issue in app/views/family_merchants/merge.html.erb line 24. The surrounding code already uses border-secondary (line 13); the checkbox should match.

🎨 Proposed fix
-              <%= check_box_tag "account_ids[]", account.id, false, class: "rounded border-gray-300" %>
+              <%= check_box_tag "account_ids[]", account.id, false, class: "rounded border-primary" %>

As per coding guidelines: "Use Tailwind CSS design tokens (e.g., text-primary, bg-container) instead of raw Tailwind classes."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/views/accounts/bulk_domains.html.erb` at line 16, The checkbox uses a raw
Tailwind color class; change the class on the check_box_tag call (the line
rendering check_box_tag "account_ids[]", account.id, false) to use the design
token used elsewhere (e.g., replace "border-gray-300" with "border-secondary")
so the checkbox matches the surrounding styles; update only the class string for
that check_box_tag to include "rounded border-secondary" (and keep any existing
classes) to conform to the design-token guideline.
app/views/family_merchants/merge.html.erb (1)

24-24: ⚡ Quick win

Replace border-gray-300 with a design token.

border-gray-300 is a raw Tailwind color utility; the design system already provides border-primary (or border-secondary to match the adjacent container border on line 21). Mixing raw color classes breaks theming.

🎨 Proposed fix
-              <%= check_box_tag "source_ids[]", merchant.id, false, class: "rounded border-gray-300" %>
+              <%= check_box_tag "source_ids[]", merchant.id, false, class: "rounded border-primary" %>

As per coding guidelines: "Use functional tokens defined in design system such as text-primary instead of text-white, bg-container instead of bg-white, border border-primary instead of border border-gray-200."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/views/family_merchants/merge.html.erb` at line 24, The checkbox currently
uses a raw Tailwind color utility ("border-gray-300")—update the check_box_tag
call so its class uses the design token instead (e.g., replace "border-gray-300"
with "border-primary" or "border-secondary" to match the adjacent container
border), preserving the other classes like "rounded"; locate the check_box_tag
for "source_ids[]" (merchant.id) and change only the border utility to the
appropriate token.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/controllers/accounts_controller.rb`:
- Around line 54-58: The bulk update inside Account.transaction using
accounts.each { |account| account.update!(institution_domain: domain) } can
raise ActiveRecord::RecordInvalid and cause a 500; wrap the transaction in a
begin/rescue that rescues ActiveRecord::RecordInvalid (or
ActiveRecord::RecordInvalid => e), let the transaction rollback, and then
redirect_to accounts_path with an alert (use i18n like t(".failure") and include
e.record.errors.full_messages.join(", ") or e.message) instead of allowing the
exception to bubble; keep using update! so validations still raise but are now
handled in the AccountsController action around the Account.transaction block.

In `@app/controllers/categories_controller.rb`:
- Around line 86-101: The target category is created with merge_target_category
(which calls Current.family.categories.create!) outside the merger's
transaction, so if Category::Merger#merge! later raises (e.g. RecordInvalid) the
newly created target can be left orphaned; fix by wrapping both the creation and
the merge call in a single ActiveRecord transaction: call Category.transaction
do … end around the call to merge_target_category and the subsequent
merger.merge! so the create! and all operations inside Category::Merger#merge!
participate in the same transaction and roll back together on error.

---

Duplicate comments:
In `@app/models/account.rb`:
- Around line 148-149: The rescue in normalize_institution_domain currently
returns the original value string (e.g. "https://bank.com/%") which yields a
full URL instead of a bare domain; change normalize_institution_domain so that
on URI::InvalidURIError it returns nil (or delegates to the same behavior as
Merchant.extract_domain) rather than value.to_s..., ensuring callers like
logo_url won't embed an invalid full URL into the Brandfetch CDN path.

---

Nitpick comments:
In `@app/views/accounts/bulk_domains.html.erb`:
- Line 16: The checkbox uses a raw Tailwind color class; change the class on the
check_box_tag call (the line rendering check_box_tag "account_ids[]",
account.id, false) to use the design token used elsewhere (e.g., replace
"border-gray-300" with "border-secondary") so the checkbox matches the
surrounding styles; update only the class string for that check_box_tag to
include "rounded border-secondary" (and keep any existing classes) to conform to
the design-token guideline.

In `@app/views/family_merchants/merge.html.erb`:
- Line 24: The checkbox currently uses a raw Tailwind color utility
("border-gray-300")—update the check_box_tag call so its class uses the design
token instead (e.g., replace "border-gray-300" with "border-primary" or
"border-secondary" to match the adjacent container border), preserving the other
classes like "rounded"; locate the check_box_tag for "source_ids[]"
(merchant.id) and change only the border utility to the appropriate token.
🪄 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: feb9a908-fa61-4c75-b9c1-5aba4a17dc27

📥 Commits

Reviewing files that changed from the base of the PR and between 66b8e96 and 4eaf193.

📒 Files selected for processing (16)
  • app/controllers/accounts_controller.rb
  • app/controllers/categories_controller.rb
  • app/controllers/family_merchants_controller.rb
  • app/models/account.rb
  • app/models/category/merger.rb
  • app/models/merchant.rb
  • app/views/accounts/bulk_domains.html.erb
  • app/views/categories/merge.html.erb
  • app/views/family_merchants/bulk_websites.html.erb
  • app/views/family_merchants/merge.html.erb
  • config/locales/views/categories/en.yml
  • config/locales/views/merchants/en.yml
  • test/controllers/categories_controller_test.rb
  • test/controllers/family_merchants_controller_test.rb
  • test/models/account_test.rb
  • test/models/merchant_test.rb
✅ Files skipped from review due to trivial changes (5)
  • app/views/categories/merge.html.erb
  • test/models/account_test.rb
  • config/locales/views/merchants/en.yml
  • app/views/family_merchants/bulk_websites.html.erb
  • config/locales/views/categories/en.yml
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/controllers/family_merchants_controller.rb

Comment thread app/controllers/accounts_controller.rb
Comment thread app/controllers/categories_controller.rb Outdated
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.

Review: feat(settings) — reviewed bulk cleanup flows

Good overall direction. The domain-extraction consolidation (Merchant.extract_domain / brandfetch_logo_url_for) cleans up a real duplication, the scoping is correct throughout, and the test coverage for the happy/sad paths is solid.

A few things need attention before merge:

Must fix

  • bulk_update_websites is missing a rescue ActiveRecord::RecordInvalid block — a validation failure will 500 (see inline comment).
  • reparent_subcategories moves orphaned source children to the target's parent, not to the target itself — needs a decision on intended behaviour and likely a fix (see inline comment).

Should fix

  • Three templates use border-gray-300 directly, violating the project's design-system token rule.
  • COLORS.sample fallback is non-deterministic; use COLORS.first to match the form default.

Nits / suggestions

  • perform_merge in CategoriesController uses a confusing variable-mutation-across-rollback pattern; a simpler control flow is suggested inline.
  • Category::Merger#merge! wraps itself in a transaction that's already opened by the controller — pick one owner.
  • The target dropdown and sources checklist share the same @categories list with no UX indication that selecting the same item in both is a no-op.

Generated by Claude Code

unless target
return redirect_to merge_family_merchants_path, alert: t(".target_not_found")
if conflicting_merge_target?(permitted_params)
return redirect_to merge_family_merchants_path, alert: t(".conflicting_target")
end
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Missing error handling — potential 500.

bulk_update_websites has no rescue ActiveRecord::RecordInvalid block. If a merchant.update! call fails validation, the exception will bubble up and produce a 500. The two other bulk operations (bulk_update_domains, perform_merge) both rescue this; this one should too:

redirect_to family_merchants_path, notice: t(".success", count: merchants.count)
rescue ActiveRecord::RecordInvalid => e
  redirect_to bulk_websites_family_merchants_path, alert: e.record.errors.full_messages.to_sentence
end

Generated by Claude Code

source_budget_category.update!(category_id: target_category.id)
end
end
end
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

reparent_subcategories moves children to the target's parent, not to the target itself.

When the target is a subcategory (has a parent_id), the source's children get reassigned to the target's parent, making them siblings of the target rather than children of it. For example: merging Food > Coffee (source) into Food > Restaurants (target) would move Coffee's subcategories up to Food, not down into Restaurants.

Is this intentional? If so, it's worth documenting. If not, the condition should likely be:

def reparent_subcategories(source)
  family.categories.where(parent_id: source.id).where.not(id: target_category.id).update_all(parent_id: target_category.id)
end

Note also that dependent: :nullify is set on Category#subcategories, so source.destroy! will nullify parent_id on any subcategories not explicitly reparented here — you'd want to ensure the update_all runs before destroy!, which it does in the current code.


Generated by Claude Code

<p class="text-sm font-medium text-primary"><%= t(".accounts_label") %></p>
<div class="max-h-64 overflow-y-auto space-y-1 border border-secondary rounded-lg p-2">
<% @accounts.each do |account| %>
<label class="flex items-center gap-2 p-2 hover:bg-surface-hover rounded cursor-pointer">
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Design system violation: border-gray-300.

Per the project conventions, raw Tailwind primitives should not be used directly — functional tokens from the design system are required. The same raw class appears in categories/merge.html.erb and family_merchants/bulk_websites.html.erb as well.

Existing usage elsewhere in the app (e.g. family_merchants/merge.html.erb) doesn't use border-gray-300 on checkboxes. The simplest fix is to drop the border class entirely and rely on the browser/OS default checkbox styling, or use whichever design-system utility is appropriate (e.g. border-secondary).


Generated by Claude Code

def merge_target_category(permitted_params)
if permitted_params[:new_target_name].present?
Current.family.categories.create!(
name: permitted_params[:new_target_name],
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

COLORS.sample fallback picks a random color non-deterministically.

The form pre-populates the color picker with Category::COLORS.first, so the user always sees a chosen color. But if the field arrives empty server-side (e.g. a crafted request), the saved color would be random, producing a mismatch from what was shown.

A safer fallback is the same default the form uses:

color: permitted_params[:new_target_color].presence || Category::COLORS.first,

The same issue exists in merge_target_merchant for FamilyMerchant::COLORS.sample.


Generated by Claude Code

Comment on lines +83 to +113
return redirect_to merge_categories_path, alert: t(".invalid_categories")
end

target = nil
merger = nil
merge_succeeded = false

Category.transaction do
target = merge_target_category(permitted_params)
raise ActiveRecord::Rollback unless target

merger = Category::Merger.new(
family: Current.family,
target_category: target,
source_categories: sources
)

merge_succeeded = merger.merge!
raise ActiveRecord::Rollback unless merge_succeeded
end

unless target
return redirect_to merge_categories_path, alert: t(".target_not_found")
end

if merge_succeeded
redirect_to categories_path, notice: t(".success", count: merger.merged_count)
else
redirect_to merge_categories_path, alert: t(".no_categories_selected")
end
rescue Category::Merger::UnauthorizedCategoryError => e
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Controller perform_merge is harder to follow than necessary.

Initializing target, merger, and merge_succeeded as nil/false outside the transaction block, then checking them again after the block, is a pattern that relies on Ruby variable mutation across a raise ActiveRecord::Rollback — which is non-obvious and could surprise maintainers who don't know that Rollback silently absorbs the exception rather than propagating it.

A cleaner approach: move the redirect logic so that all branching happens inside the transaction for the happy path, and only rescue/redirect outside. For example:

def perform_merge
  permitted_params = category_merge_params
  return redirect_to merge_categories_path, alert: t(".conflicting_target") if conflicting_merge_target?(permitted_params)

  sources = Current.family.categories.where(id: permitted_params[:source_ids])
  return redirect_to merge_categories_path, alert: t(".invalid_categories") unless sources.any?

  ActiveRecord::Base.transaction do
    target = merge_target_category(permitted_params) or raise ActiveRecord::Rollback
    merger = Category::Merger.new(family: Current.family, target_category: target, source_categories: sources)
    merger.merge! or raise ActiveRecord::Rollback
    # if we reach here, commit and redirect
    return redirect_to categories_path, notice: t(".success", count: merger.merged_count)
  end

  redirect_to merge_categories_path, alert: t(".no_categories_selected")
rescue Category::Merger::UnauthorizedCategoryError => e
  redirect_to merge_categories_path, alert: e.message
rescue ActiveRecord::RecordInvalid => e
  redirect_to merge_categories_path, alert: e.record.errors.full_messages.to_sentence
end

This makes the control flow explicit: return from inside the transaction on success, fall through to an alert redirect if the transaction was rolled back.


Generated by Claude Code


def merge!
return false if source_categories.empty?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Category::Merger#merge! opens its own transaction, but the controller already wraps the call in one.

Nested ActiveRecord::Base.transaction blocks use savepoints in PostgreSQL (and are no-ops in SQLite unless requires_new: true is set). The inner transaction here doesn't add real isolation — the outer transaction in the controller is what ultimately governs rollback. This double-wrapping is a code smell and could cause confusion if the behaviour ever needs to differ between the two.

Consider either:

  • having the controller manage the transaction and merge! operate without one (caller's responsibility), or
  • having merge! own the transaction and the controller not wrap it.

Since Category::Merger is a domain object that may be called from contexts other than this controller, having it own the transaction (option 2) is probably the cleaner long-term design — but then the controller shouldn't open its own outer transaction.


Generated by Claude Code

{} %>

<div class="space-y-3 rounded-lg border border-secondary p-3">
<p class="text-sm font-medium text-primary"><%= t(".new_target_label") %></p>
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Same @categories list is used for both the target dropdown and the sources checklist — a user can select the same category as both.

Category::Merger silently filters out the target from the sources (sources.reject { |c| c.id == target_category.id }), so there's no data hazard. But from a UX perspective, a user who selects "Dining" as target and also checks "Dining" as a source will see it silently ignored with no feedback — the success message will report one fewer merge than they expected.

A small JS enhancement (or even just a note in the hint text) to clarify that selecting a category as target automatically excludes it from the source list would prevent confusion.


Generated by Claude Code

@jjmata jjmata removed the contributor:flagged Contributor flagged for review by trust analysis. label May 2, 2026
@jjmata jjmata added this to the v0.7.1 milestone May 2, 2026
@JSONbored JSONbored force-pushed the codex/feat-settings-bulk-cleanup branch from 770b17f to ccc8fcb Compare May 3, 2026 01:26
@brin-security-scanner brin-security-scanner Bot added the contributor:flagged Contributor flagged for review by trust analysis. label May 3, 2026
Copy link
Copy Markdown
Contributor

@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: 3

🧹 Nitpick comments (1)
test/controllers/accounts_controller_test.rb (1)

17-31: ⚡ Quick win

Make the writable vs. read-only setup explicit in the test.

This scenario currently passes or fails based on whatever permissions the fixtures happen to give accounts(:depository) and accounts(:credit_card). Since the controller scopes through writable_by, it’s safer to create or override the relevant AccountShare records inline so the permission boundary is deterministic.

As per coding guidelines, “Create edge cases on-the-fly within test context instead of in fixtures; keep fixtures minimal (2-3 per model for base cases).”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/controllers/accounts_controller_test.rb` around lines 17 - 31, The test
"bulk domain update only changes writable accounts" relies on fixture
permissions; instead explicitly set up AccountShare records so permissions are
deterministic: after signing in users(:family_member) create or update
AccountShare for accounts(:depository) to grant writable access for that user
(so it appears in writable_by scope) and ensure accounts(:credit_card) either
has an AccountShare with read-only permissions or no writable share; then run
the same POST to bulk_update_domains_accounts_path and assert the depository's
domain changed while the credit_card's did not. Make sure to reference
AccountShare and the controller's writable_by scope when creating/updating these
shares so the test no longer depends on fixtures.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/controllers/family_merchants_controller.rb`:
- Around line 203-209: In merge_target_merchant, normalize
permitted_params[:new_target_website_url] using the same canonicalization as
bulk_update_websites before creating the FamilyMerchant: call
Merchant.extract_domain(permitted_params[:new_target_website_url]) (or
equivalent helper) and pass that canonicalized value to website_url (respecting
blank -> nil), while keeping existing color/name handling in the
Current.family.merchants.create! call.

In `@app/models/category/merger.rb`:
- Around line 87-89: The current reparent_subcategories method uses update_all
which bypasses validations and can create an invalid 3-level hierarchy when
target_category already has a parent and source has children; change the flow so
you validate reparenting before performing the bulk update: add a
validate_reparenting! method that raises if target_category.parent_id.present?
AND source.children.exists? (or equivalently source has subcategories), call
validate_reparenting! from initialize (after validate_hierarchy!), and only then
perform the update_all in reparent_subcategories; keep the method names
reparent_subcategories, validate_reparenting!, initialize, validate_hierarchy!,
target_category and source to locate the changes.

In `@app/models/provider_merchant.rb`:
- Around line 27-30: The current guard using
Setting.brand_fetch_client_id.present? prevents clearing an old logo because
Merchant.brandfetch_logo_url_for already returns nil when the client id is
missing; remove that guard so we always call Merchant.brandfetch_logo_url_for
when website_url is present and persist its result (which may be nil) to
logo_url. In other words, change the branch that references website_url.present?
to always run update!(logo_url: Merchant.brandfetch_logo_url_for(website_url))
(and keep the website_url.blank? branch calling update!(logo_url: nil)), so
logo_url is cleared when Brandfetch is unavailable or returns nil.

---

Nitpick comments:
In `@test/controllers/accounts_controller_test.rb`:
- Around line 17-31: The test "bulk domain update only changes writable
accounts" relies on fixture permissions; instead explicitly set up AccountShare
records so permissions are deterministic: after signing in users(:family_member)
create or update AccountShare for accounts(:depository) to grant writable access
for that user (so it appears in writable_by scope) and ensure
accounts(:credit_card) either has an AccountShare with read-only permissions or
no writable share; then run the same POST to bulk_update_domains_accounts_path
and assert the depository's domain changed while the credit_card's did not. Make
sure to reference AccountShare and the controller's writable_by scope when
creating/updating these shares so the test no longer depends on fixtures.
🪄 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: f7019961-cc90-4ad7-bec1-f600ffba82ee

📥 Commits

Reviewing files that changed from the base of the PR and between 4eaf193fd1dc59800285ba6e5073d90df38ea72a and ccc8fcb4800bb6e4e5499006c772bbf546e48894.

📒 Files selected for processing (24)
  • app/controllers/accounts_controller.rb
  • app/controllers/categories_controller.rb
  • app/controllers/family_merchants_controller.rb
  • app/models/account.rb
  • app/models/category/merger.rb
  • app/models/family_merchant.rb
  • app/models/merchant.rb
  • app/models/provider_merchant.rb
  • app/views/accounts/bulk_domains.html.erb
  • app/views/accounts/index.html.erb
  • app/views/categories/index.html.erb
  • app/views/categories/merge.html.erb
  • app/views/family_merchants/bulk_websites.html.erb
  • app/views/family_merchants/index.html.erb
  • app/views/family_merchants/merge.html.erb
  • config/locales/views/accounts/en.yml
  • config/locales/views/categories/en.yml
  • config/locales/views/merchants/en.yml
  • config/routes.rb
  • test/controllers/accounts_controller_test.rb
  • test/controllers/categories_controller_test.rb
  • test/controllers/family_merchants_controller_test.rb
  • test/models/account_test.rb
  • test/models/merchant_test.rb
✅ Files skipped from review due to trivial changes (9)
  • app/views/categories/merge.html.erb
  • app/views/categories/index.html.erb
  • app/views/family_merchants/index.html.erb
  • app/views/family_merchants/bulk_websites.html.erb
  • test/models/merchant_test.rb
  • config/locales/views/accounts/en.yml
  • config/locales/views/categories/en.yml
  • app/views/accounts/index.html.erb
  • app/models/family_merchant.rb
🚧 Files skipped from review as they are similar to previous changes (5)
  • config/routes.rb
  • app/views/accounts/bulk_domains.html.erb
  • config/locales/views/merchants/en.yml
  • test/controllers/categories_controller_test.rb
  • test/controllers/family_merchants_controller_test.rb

Comment thread app/models/category/merger.rb
Comment thread app/models/provider_merchant.rb Outdated
Copy link
Copy Markdown
Contributor

@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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/controllers/family_merchants_controller.rb`:
- Around line 203-209: merge_target_merchant sets a user-specified color but
FamilyMerchant#set_default_color currently overwrites it on every validation;
update set_default_color so it only assigns a default when color is nil/blank
(e.g., return if color.present?) instead of always replacing it, keeping the
callback and default logic but guarding the assignment so the color passed from
merge_target_merchant (new_target_color) is preserved.

In `@test/controllers/family_merchants_controller_test.rb`:
- Around line 181-193: The test seeds a stale logo but stubs
Setting.brand_fetch_client_id to nil before creation so the
FamilyMerchant.before_save already clears logo_url; instead create the merchant
with a valid Brandfetch state (i.e., do NOT stub Setting.brand_fetch_client_id)
or create the record first and then stub to nil, then set merchant.logo_url to
the stale URL (or create with the logo and then stub), call
merchant.update!(website_url: "new.example.com") and finally assert_nil
merchant.reload.logo_url; update references:
Setting.stubs(:brand_fetch_client_id), FamilyMerchant.create!, merchant.logo_url
assignment, merchant.update! and the final assert_nil to ensure the stale logo
exists before the update that should clear it.
🪄 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: 23981fc3-19d4-452d-8789-5473517a666c

📥 Commits

Reviewing files that changed from the base of the PR and between ccc8fcb and 8b3acfe.

📒 Files selected for processing (8)
  • app/controllers/categories_controller.rb
  • app/controllers/family_merchants_controller.rb
  • app/models/category/merger.rb
  • app/models/family_merchant.rb
  • app/models/provider_merchant.rb
  • config/locales/views/categories/en.yml
  • test/controllers/categories_controller_test.rb
  • test/controllers/family_merchants_controller_test.rb
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/models/category/merger.rb
  • app/controllers/categories_controller.rb
  • test/controllers/categories_controller_test.rb

Comment thread test/controllers/family_merchants_controller_test.rb
Copy link
Copy Markdown
Contributor

@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: 2

🧹 Nitpick comments (2)
test/controllers/family_merchants_controller_test.rb (1)

183-197: 💤 Low value

Consider moving this test to a model test file.

This test exercises ProviderMerchant model behavior directly rather than through the controller action. While it validates behavior needed by bulk_update_websites, it would be more appropriately placed in test/models/provider_merchant_test.rb for consistency with test organization conventions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/controllers/family_merchants_controller_test.rb` around lines 183 - 197,
The test belongs in the model suite because it directly exercises
ProviderMerchant behavior: move the test from
test/controllers/family_merchants_controller_test.rb into
test/models/provider_merchant_test.rb, keeping the same setup and assertions for
ProviderMerchant.create!, update_column(:logo_url, ...), stubbing
Setting.brand_fetch_client_id to nil, calling
provider_merchant.update!(website_url: ...) and
provider_merchant.generate_logo_url_from_website!, and asserting
provider_merchant.reload.logo_url is nil; ensure the test class and file use the
correct model test helper and naming conventions so it runs with the model
tests.
app/controllers/categories_controller.rb (1)

100-101: ⚡ Quick win

Don't surface raw merger exception text to users.

These alerts come straight from Category::Merger exception strings, so this path skips the locale files and will show hard-coded English copy instead of translated UI text. Please map these failures to translation keys before redirecting. As per coding guidelines, **/*.{erb,rb}: Organize i18n keys hierarchically by feature ... Always use t() helper for user-facing strings.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/categories_controller.rb` around lines 100 - 101, The rescue
in CategoriesController that handles Category::Merger::UnauthorizedCategoryError
currently surfaces e.message directly; replace the raw exception text with a
translation call using the t() helper (e.g. t('categories.merge.unauthorized')
or a namespaced key like t('controllers.categories.merge.unauthorized')) in the
redirect_to ... alert: argument, and add the corresponding key to locale files;
if the exception carries dynamic data you need to show, pass it as interpolation
to t(..., param: value) rather than rendering e.message.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/controllers/categories_controller.rb`:
- Around line 20-22: The merge action in CategoriesController is rendering with
the default layout instead of the settings shell; update the merge action (def
merge) to render using the settings layout (e.g., render layout: "settings") or
configure the controller to use layout "settings" for this action (or for both
index and merge) so the merge page appears inside the settings UI.
- Around line 102-103: The merge action currently only rescues
ActiveRecord::RecordInvalid; add handling for ActiveRecord::RecordNotDestroyed
so failures from Category::Merger#merge! (which uses destroy!) redirect back to
merge_categories_path with a useful alert. Update the controller to rescue
ActiveRecord::RecordNotDestroyed (either in a separate rescue or combined with
the existing rescue for ActiveRecord::RecordInvalid) and use
e.record.errors.full_messages.to_sentence (or fallback to e.message) in the
redirect alert so users see the destruction validation errors; reference the
merge action / rescue block in categories_controller.rb to locate where to add
this.

---

Nitpick comments:
In `@app/controllers/categories_controller.rb`:
- Around line 100-101: The rescue in CategoriesController that handles
Category::Merger::UnauthorizedCategoryError currently surfaces e.message
directly; replace the raw exception text with a translation call using the t()
helper (e.g. t('categories.merge.unauthorized') or a namespaced key like
t('controllers.categories.merge.unauthorized')) in the redirect_to ... alert:
argument, and add the corresponding key to locale files; if the exception
carries dynamic data you need to show, pass it as interpolation to t(..., param:
value) rather than rendering e.message.

In `@test/controllers/family_merchants_controller_test.rb`:
- Around line 183-197: The test belongs in the model suite because it directly
exercises ProviderMerchant behavior: move the test from
test/controllers/family_merchants_controller_test.rb into
test/models/provider_merchant_test.rb, keeping the same setup and assertions for
ProviderMerchant.create!, update_column(:logo_url, ...), stubbing
Setting.brand_fetch_client_id to nil, calling
provider_merchant.update!(website_url: ...) and
provider_merchant.generate_logo_url_from_website!, and asserting
provider_merchant.reload.logo_url is nil; ensure the test class and file use the
correct model test helper and naming conventions so it runs with the model
tests.
🪄 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: b8033254-bc52-4c6c-9187-93e863a2653e

📥 Commits

Reviewing files that changed from the base of the PR and between 8b3acfe and 9bff51d.

📒 Files selected for processing (5)
  • app/controllers/categories_controller.rb
  • app/models/family_merchant.rb
  • config/locales/views/categories/en.yml
  • test/controllers/categories_controller_test.rb
  • test/controllers/family_merchants_controller_test.rb
🚧 Files skipped from review as they are similar to previous changes (1)
  • config/locales/views/categories/en.yml

Comment thread app/controllers/categories_controller.rb
Comment thread app/controllers/categories_controller.rb Outdated
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.

Re-review — all previous issues addressed, no new concerns

All 7 issues from the prior review have been fixed, and the additional fixes prompted by CodeRabbit are solid. Summary:

Previously flagged — confirmed resolved:

  1. Missing rescue ActiveRecord::RecordInvalid in bulk_update_websites ✅ — rescue added with a t(".failure") redirect; locale key included.

  2. reparent_subcategories moved children to the target's parent instead of the target ✅ — now uses parent_id: target_category.id. A new validate_reparenting! guard also prevents merging a source-with-subcategories into a subcategory target (which would have created an illegal 3-level hierarchy via update_all). Two new controller tests confirm the corrected direction and the rejection case.

  3. border-gray-300 design system violation in three templates ✅ — replaced with border-secondary throughout.

  4. COLORS.sample non-deterministic fallback ✅ — Category::COLORS.first and FamilyMerchant::COLORS.first used consistently in both controllers and in FamilyMerchant#set_default_color.

  5. perform_merge controller complexity ✅ — refactored into a clean merge_categories! helper that raises typed sentinel exceptions (MergeTargetNotFound, EmptyCategoryMerge), giving explicit control flow without the variable-mutation-across-rollback pattern.

  6. Nested transaction in Category::Merger#merge! ✅ — the inner Category.transaction was removed from merge!; the outer transaction in merge_categories! owns it. The transaction boundary is now unambiguous.

  7. Target/source same-list UX ✅ — target_selected_as_source? guard added server-side with a redirect and alert; hint text updated to "Do not select the target as a source." Pragmatic fix without requiring JS.

Notable improvements in subsequent commits (CodeRabbit-prompted):

  • FamilyMerchant#set_default_color now only sets a default when color.blank? — user-specified colors are preserved on the new-target merge form.
  • ProviderMerchant#generate_logo_url_from_website! and FamilyMerchant#generate_logo_url_from_website now clear stale logos when Brandfetch is unavailable (removed the brand_fetch_client_id guard; brandfetch_logo_url_for handles the nil return internally).
  • Account.normalize_institution_domain now delegates to Merchant.extract_domain, eliminating the separate implementation and the raw-URL fallback. Extra test coverage for the nil case ("https://bad host") added.
  • ActiveRecord::RecordNotDestroyed is now caught alongside RecordInvalid in categories#perform_merge.

No new issues found. The code is well-structured. The one open thread from chatgpt-codex-connector (P1 — settings layout on bulk_domains) is outdated; bulk_domains correctly uses render layout: false in the current code.


Generated by Claude Code

Copy link
Copy Markdown
Contributor

@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.

🧹 Nitpick comments (1)
app/views/family_merchants/merge.html.erb (1)

16-16: 💤 Low value

Consider moving the default color value out of the template.

FamilyMerchant::COLORS.first couples the template to the model's internal array ordering. If the array is reordered or the default-color logic is centralised in the model (e.g., FamilyMerchant.default_color), this view will silently drift. Pass the default from the controller (e.g., @default_merchant_color) instead.

As per coding guidelines: "Keep domain logic out of views: compute values like button classes, conditional logic, and data transformations in the component file, not the template file."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/views/family_merchants/merge.html.erb` at line 16, The view currently
uses FamilyMerchant::COLORS.first directly in the color field (f.color_field
:new_target_color), coupling presentation to model internals; move the default
logic into the controller by computing and assigning an instance var (e.g.,
`@default_merchant_color` = FamilyMerchant.default_color ||
FamilyMerchant::COLORS.first) in the action that renders this view, and update
the template to use that instance var for the value (keep the form field name
:new_target_color unchanged).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@app/views/family_merchants/merge.html.erb`:
- Line 16: The view currently uses FamilyMerchant::COLORS.first directly in the
color field (f.color_field :new_target_color), coupling presentation to model
internals; move the default logic into the controller by computing and assigning
an instance var (e.g., `@default_merchant_color` = FamilyMerchant.default_color ||
FamilyMerchant::COLORS.first) in the action that renders this view, and update
the template to use that instance var for the value (keep the form field name
:new_target_color unchanged).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 63a86c65-fa2f-45ac-a446-8c842a7ef4fb

📥 Commits

Reviewing files that changed from the base of the PR and between 8b3acfe424f4027e99e23d97cc064c647010e015 and 6287683.

📒 Files selected for processing (24)
  • app/controllers/accounts_controller.rb
  • app/controllers/categories_controller.rb
  • app/controllers/family_merchants_controller.rb
  • app/models/account.rb
  • app/models/category/merger.rb
  • app/models/family_merchant.rb
  • app/models/merchant.rb
  • app/models/provider_merchant.rb
  • app/views/accounts/bulk_domains.html.erb
  • app/views/accounts/index.html.erb
  • app/views/categories/index.html.erb
  • app/views/categories/merge.html.erb
  • app/views/family_merchants/bulk_websites.html.erb
  • app/views/family_merchants/index.html.erb
  • app/views/family_merchants/merge.html.erb
  • config/locales/views/accounts/en.yml
  • config/locales/views/categories/en.yml
  • config/locales/views/merchants/en.yml
  • config/routes.rb
  • test/controllers/accounts_controller_test.rb
  • test/controllers/categories_controller_test.rb
  • test/controllers/family_merchants_controller_test.rb
  • test/models/account_test.rb
  • test/models/merchant_test.rb
✅ Files skipped from review due to trivial changes (9)
  • app/views/family_merchants/index.html.erb
  • app/views/categories/merge.html.erb
  • test/models/merchant_test.rb
  • config/locales/views/accounts/en.yml
  • config/locales/views/categories/en.yml
  • test/models/account_test.rb
  • app/views/accounts/index.html.erb
  • config/locales/views/merchants/en.yml
  • app/controllers/accounts_controller.rb
🚧 Files skipped from review as they are similar to previous changes (7)
  • app/views/accounts/bulk_domains.html.erb
  • app/views/family_merchants/bulk_websites.html.erb
  • app/models/provider_merchant.rb
  • app/models/family_merchant.rb
  • app/controllers/categories_controller.rb
  • app/models/category/merger.rb
  • app/controllers/family_merchants_controller.rb

Copy link
Copy Markdown
Contributor

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/controllers/family_merchants_controller.rb`:
- Around line 204-210: The merge_target_merchant flow currently calls
Merchant.extract_domain(permitted_params[:new_target_website_url]) and will
create the target even if extract_domain returns nil; change it to fail-fast:
when permitted_params[:new_target_website_url] is present, call
Merchant.extract_domain and if it returns nil do not call
Current.family.merchants.create! but instead redirect back with an alert (same
behavior as bulk_update_websites), preserving the user input and halting the
merge; keep the existing creation logic (Current.family.merchants.create!) only
when extract_domain yields a non-nil domain (use
permitted_params[:new_target_website_color] logic unchanged).
🪄 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: 7c98b2c8-3686-4c2f-a164-43cf3e290ec3

📥 Commits

Reviewing files that changed from the base of the PR and between 6287683 and 27a2783.

📒 Files selected for processing (8)
  • app/controllers/family_merchants_controller.rb
  • app/javascript/controllers/merchant_merge_target_controller.js
  • app/models/family_merchant.rb
  • app/models/merchant.rb
  • app/views/family_merchants/merge.html.erb
  • config/locales/views/merchants/en.yml
  • test/controllers/family_merchants_controller_test.rb
  • test/models/merchant_test.rb
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/models/merchant.rb
  • config/locales/views/merchants/en.yml

Comment thread app/controllers/family_merchants_controller.rb
@JSONbored JSONbored requested a review from jjmata May 4, 2026 09:04
@JSONbored
Copy link
Copy Markdown
Contributor Author

JSONbored commented May 4, 2026

Please include screenshots of new functionality and high-level description of feature objective, @JSONbored. 🙏

Thanks, added the high-level feature objective and screenshots to the PR description.

I also attached screenshots for the new visible flows: bulk institution-domain cleanup, bulk merchant website cleanup, merchant merge target selection/new-target creation, and category merge.

Latest validation is listed in the PR description as well.

@jjmata jjmata removed the contributor:flagged Contributor flagged for review by trust analysis. label May 4, 2026
@brin-security-scanner brin-security-scanner Bot added the contributor:flagged Contributor flagged for review by trust analysis. label May 5, 2026
Copy link
Copy Markdown

@sure-design sure-design left a comment

Choose a reason for hiding this comment

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

Re-review — independent read of current code

I read the code directly (not relying on prior review comments) and confirmed what was fixed vs. what remains.


Previously flagged open threads that are now addressed in the current code

Several jjmata threads are still marked unresolved but the code already has the fix:

  • Missing rescue in bulk_update_websites — The rescue ActiveRecord::RecordInvalid block is present in the current file. Thread can be closed.
  • border-gray-300 design system violation — All three views (bulk_domains, categories/merge, bulk_websites) now use border-secondary. Thread can be closed.
  • COLORS.sample non-deterministic fallback — Categories use Category::COLORS.first; merchants use FamilyMerchant.default_color (= COLORS.first). Thread can be closed.
  • Double-wrapping transactionsCategory::Merger#merge! has no transaction of its own. Only the controller's Category.transaction in merge_categories! exists. The concern is moot. Thread can be closed.
  • reparent_subcategories moves to target's parent — The code correctly uses update_all(parent_id: target_category.id) (children go under the target, not under the target's parent). jjmata's suggested fix is identical to what's already in the code. Thread can be closed.

Real bug — one new issue

FamilyMerchantsController#perform_merge can create an orphaned target merchant

This is the same atomicity issue that CodeRabbit flagged for categories_controller (commit 0a94adb) — and that was fixed by wrapping everything in Category.transaction via merge_categories!. That fix was never applied to the merchant equivalent.

Current flow in perform_merge:

target = merge_target_merchant(valid_merchants, permitted_params)  # calls create! — committed immediately
# ...
merger = Merchant::Merger.new(family: Current.family, target_merchant: target, source_merchants: sources)

if merger.merge!
  redirect_to family_merchants_path, ...
else
  redirect_to merge_family_merchants_path, ...
end
rescue Merchant::Merger::UnauthorizedMerchantError => e
  redirect_to merge_family_merchants_path, alert: e.message   # target merchant already in DB, orphaned
rescue ActiveRecord::RecordInvalid => e
  redirect_to merge_family_merchants_path, alert: ...          # same problem

If merger.merge! raises ActiveRecord::RecordInvalid (e.g., a validation fires while reassigning transactions) or UnauthorizedMerchantError, the rescue redirects back with an alert — but the newly created target FamilyMerchant is already committed and persists as an orphan with no transactions assigned to it.

Fix — same pattern as categories: wrap target creation + merge in a transaction:

def perform_merge
  permitted_params = merchant_merge_params
  return redirect_to merge_family_merchants_path, alert: t(".conflicting_target") if conflicting_merge_target?(permitted_params)

  valid_merchants = all_family_merchants
  sources = valid_merchants.where(id: permitted_params[:source_ids])
  return redirect_to merge_family_merchants_path, alert: t(".invalid_merchants") unless sources.any?

  merger = nil
  merge_succeeded = false

  Merchant.transaction do
    target = merge_target_merchant(valid_merchants, permitted_params) ||
             (return redirect_to merge_family_merchants_path, alert: t(".target_not_found"))
    merger = Merchant::Merger.new(family: Current.family, target_merchant: target, source_merchants: sources)
    merge_succeeded = merger.merge!
  end

  if merge_succeeded
    redirect_to family_merchants_path, notice: t(".success", count: merger.merged_count)
  else
    redirect_to merge_family_merchants_path, alert: t(".no_merchants_selected")
  end
rescue Merchant::Merger::UnauthorizedMerchantError => e
  redirect_to merge_family_merchants_path, alert: e.message
rescue InvalidMerchantWebsite
  redirect_to merge_family_merchants_path, alert: t(".invalid_website")
rescue ActiveRecord::RecordInvalid => e
  redirect_to merge_family_merchants_path, alert: e.record.errors.full_messages.to_sentence
end

Minor issues

Account#logo_url redundantly normalizes on every call

institution_domain is already normalized on save by before_validation :normalize_institution_domain_value. The extra normalize_institution_domain call in logo_url is a no-op (the value is already canonical or nil), but it runs URI.parse + a regex on every logo_url invocation. Replace with the raw attribute:

def logo_url
  domain = institution_domain  # already normalized by before_validation
  if domain.present? && Setting.brand_fetch_client_id.present?
    # ...
  end
end

brandfetch_logo_url_for test validates an impossible input path

Merchant.stubs(:extract_domain).returns("example.com/path")

The real extract_domain validates with /\A[a-z0-9.-]+\.[a-z0-9-]+\z/i, which rejects forward slashes — so "example.com/path" can never be returned in production. The test confirms the encoding helper works, but it only exercises a code path that doesn't exist without the stub. Consider either testing with a value that extract_domain can actually return, or noting that encoding is a purely defensive layer.

FamilyMerchantsController#merge renders in the default layout, not the settings layout

CategoriesController#merge explicitly does render layout: "settings". FamilyMerchantsController#merge has no render call, so it falls back to the application layout on direct navigation. Accessed via frame: :modal it works fine (Turbo extracts the frame), but the experience is inconsistent if a user lands directly on that URL.


Generated by Claude Code


Generated by Claude Code

@jjmata jjmata removed the contributor:flagged Contributor flagged for review by trust analysis. label May 6, 2026
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.

One final pass? See last Claude Code comment ...

@brin-security-scanner brin-security-scanner Bot added the contributor:verified Contributor passed trust analysis. label May 6, 2026
Copy link
Copy Markdown
Contributor

@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.

🧹 Nitpick comments (2)
app/models/account.rb (1)

305-311: ⚡ Quick win

logo_url still inlines the Brandfetch URL instead of using the new centralized helper.

The AI summary states logo_url is computed "via Merchant.brandfetch_logo_url_for", but this code path still constructs the URL by string interpolation and bypasses the encoding Merchant.brandfetch_logo_url_for applies (verified by the defensive-encoding test in test/models/merchant_test.rb). Routing both Account and Merchant logos through the same helper keeps the centralization the PR is introducing and avoids divergence if the Brandfetch URL shape changes again.

♻️ Proposed refactor
   def logo_url
     domain = institution_domain

-    if domain.present? && Setting.brand_fetch_client_id.present?
-      logo_size = Setting.brand_fetch_logo_size
-
-      "https://cdn.brandfetch.io/#{domain}/icon/fallback/lettermark/w/#{logo_size}/h/#{logo_size}?c=#{Setting.brand_fetch_client_id}"
+    brandfetch_url = Merchant.brandfetch_logo_url_for(domain) if domain.present?
+
+    if brandfetch_url
+      brandfetch_url
     elsif provider&.logo_url.present?
       provider.logo_url
     elsif logo.attached?
       Rails.application.routes.url_helpers.rails_blob_path(logo, only_path: true)
     end
   end

(Adjust to match the actual signature/return contract of Merchant.brandfetch_logo_url_for.)

🤖 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/models/account.rb` around lines 305 - 311, The logo_url method is still
building the Brandfetch URL inline and should delegate to the centralized
helper; instead of interpolating the URL in Account#logo_url (using
institution_domain and Settings), call Merchant.brandfetch_logo_url_for(...)
with the domain and relevant settings (logo_size and client_id) so the helper's
encoding/formatting is reused; update Account#logo_url to return the result of
Merchant.brandfetch_logo_url_for(institution_domain, logo_size:
Setting.brand_fetch_logo_size, client_id: Setting.brand_fetch_client_id) (adjust
argument names to match the helper's actual signature).
app/models/merchant/merger.rb (1)

37-51: ⚡ Quick win

Consider restoring the internal transaction for defensive atomicity.

Removing the internal Merchant.transaction shifts atomicity responsibility to callers. While FamilyMerchantsController#merge_merchants! and CategoriesController#merge_categories! both properly wrap their merger calls in Merchant.transaction and Category.transaction respectively, this creates a fragile contract—any future caller must remember to do the same. If a new call site forgets, update_all could succeed while destroy! fails midway through the loop, leaving some transactions reassigned and some source merchants still present. Restoring the internal transaction makes the method atomically safe regardless of context and is low-effort overhead.

🤖 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/models/merchant/merger.rb` around lines 37 - 51, The merge! method must
be made internally atomic: wrap the main logic (the early return check, the
source_merchants.each loop that calls family.transactions.where(merchant_id:
source.id).update_all(merchant_id: target_merchant.id), the conditional
source.destroy!, and the `@merged_count` increment) in a Merchant.transaction
block so all updates and destroys are committed or rolled back together; use
Merchant.transaction (optionally with requires_new: true if you want an isolated
savepoint when callers also wrap) around the body of merge! to ensure defensive
atomicity regardless of callers.
🤖 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.

Nitpick comments:
In `@app/models/account.rb`:
- Around line 305-311: The logo_url method is still building the Brandfetch URL
inline and should delegate to the centralized helper; instead of interpolating
the URL in Account#logo_url (using institution_domain and Settings), call
Merchant.brandfetch_logo_url_for(...) with the domain and relevant settings
(logo_size and client_id) so the helper's encoding/formatting is reused; update
Account#logo_url to return the result of
Merchant.brandfetch_logo_url_for(institution_domain, logo_size:
Setting.brand_fetch_logo_size, client_id: Setting.brand_fetch_client_id) (adjust
argument names to match the helper's actual signature).

In `@app/models/merchant/merger.rb`:
- Around line 37-51: The merge! method must be made internally atomic: wrap the
main logic (the early return check, the source_merchants.each loop that calls
family.transactions.where(merchant_id: source.id).update_all(merchant_id:
target_merchant.id), the conditional source.destroy!, and the `@merged_count`
increment) in a Merchant.transaction block so all updates and destroys are
committed or rolled back together; use Merchant.transaction (optionally with
requires_new: true if you want an isolated savepoint when callers also wrap)
around the body of merge! to ensure defensive atomicity regardless of callers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2d05bbee-5e92-4bcc-a0eb-e6b87325fb89

📥 Commits

Reviewing files that changed from the base of the PR and between 6db4812 and 4db76cc.

📒 Files selected for processing (6)
  • app/controllers/family_merchants_controller.rb
  • app/models/account.rb
  • app/models/merchant/merger.rb
  • config/routes.rb
  • test/controllers/family_merchants_controller_test.rb
  • test/models/merchant_test.rb

Copy link
Copy Markdown

The fix(settings): reuse merchant logo helpers commit (May 6) looks correct — consolidating the Brandfetch URL construction into the model is the right call.

A couple of things to verify before jjmata's code review:

  • Cross-family data leak in Merchant::Merger: the bulk merge flow touches provider merchant records. Confirm the family-scoping guard is tight enough that a provider merchant shared across families can't be merged/redirected by one family's user.
  • valid_domain? rejection path: the PR rejects invalid website input rather than silently dropping it. Make sure the error message is surfaced back to the user in the form response, not just dropped silently server-side.

Otherwise the description and screenshots are now in good shape — ready for a substantive code review from @jjmata.


Code review note from Claude Code


Generated by Claude Code

@JSONbored JSONbored requested review from jjmata and sure-design May 7, 2026 15:37
Copy link
Copy Markdown

New commits pushed 2026-05-07. The original 7-issue review round was fully resolved and confirmed by @jjmata on 2026-05-03. The remaining open item from the 2026-05-06 re-review is the atomicity bug in FamilyMerchantsController#perform_merge: merge_target_merchant calls create! (committed immediately) before Merchant::Merger#merge! runs, so a merge failure leaves an orphaned target merchant. The parallel fix for CategoriesController (wrapping in Category.transaction) was not applied to the merchant flow. Please confirm the new commits address this.

Generated by Claude Code


Generated by Claude Code

@JSONbored
Copy link
Copy Markdown
Contributor Author

New commits pushed 2026-05-07. The original 7-issue review round was fully resolved and confirmed by @jjmata on 2026-05-03. The remaining open item from the 2026-05-06 re-review is the atomicity bug in FamilyMerchantsController#perform_merge: merge_target_merchant calls create! (committed immediately) before Merchant::Merger#merge! runs, so a merge failure leaves an orphaned target merchant. The parallel fix for CategoriesController (wrapping in Category.transaction) was not applied to the merchant flow. Please confirm the new commits address this.

Generated by Claude Code

Generated by Claude Code

Thanks, I verified the current branch against those points.

The cross-family provider merchant path is covered now: Merchant::Merger validates target/source merchants against the current family’s family-owned or assigned merchant IDs, only reassigns family.transactions.where(merchant_id: source.id), and only destroys FamilyMerchant sources. There is also a regression test proving a shared ProviderMerchant merge only reassigns the current family’s transaction while the other family’s transaction still points to the shared provider merchant.

The invalid website path is also visible to the user: invalid reviewed new_target_website_url raises InvalidMerchantWebsite, redirects back to the merge form, and sets the localized invalid_website flash alert. The controller test asserts that flash path and that no target merchant is created.

I also confirmed the merchant merge atomicity issue is addressed: target creation and merge execution now run inside Merchant.transaction, Merchant::Merger#merge! is internally transaction-wrapped, and there is rollback coverage proving a failed merge does not leave an orphaned new target merchant.

Local targeted validation passed:

  • bin/rails test test/controllers/family_merchants_controller_test.rb test/models/merchant_test.rb test/controllers/accounts_controller_test.rb
  • npm run lint
  • scoped bin/rubocop
  • git diff --check

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.

Development

Successfully merging this pull request may close these issues.

3 participants