fix(merchants): preserve manual merchant edits across provider sync#1981
fix(merchants): preserve manual merchant edits across provider sync#1981dripsmvcp wants to merge 3 commits into
Conversation
Fixes we-promise#1977. Merging merchants, converting a synced (provider) merchant to a family merchant, and unlinking a merchant all reassign transactions.merchant_id via update_all without flagging the entries as user_modified. The next provider sync sees the entries as unmodified and reverts the change. Add Entry.mark_user_modified_for_transactions! and call it (before the merchant_id update, so the scope still matches) in Merchant::Merger#merge!, ProviderMerchant#convert_to_family_merchant_for, and #unlink_from_family. The sync skip-guard already honours user_modified, so flagged entries are left untouched on subsequent syncs.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughAdds Entry.mark_user_modified_for_transactions! to bulk-flag entries for transaction IDs, and calls it from Merchant::Merger and ProviderMerchant before reassigning or nulling transaction merchant_id; tests added to assert merchant_id changes and that entries are marked user_modified. ChangesMerchant Modification Persistence
Sequence DiagramsequenceDiagram
participant Merge
participant Scope
participant EntryUtil
participant DB
Merge->>Scope: build scope for affected transactions
Scope-->>Merge: return scoped relation / ids
Merge->>EntryUtil: call with scope or ids
EntryUtil->>EntryUtil: normalize ids, dedupe, return if empty
EntryUtil->>DB: bulk update entries where entryable_type='Transaction' and entryable_id IN (ids) to set user_modified: true
DB-->>EntryUtil: updated entries
Merge->>DB: scope.update_all(merchant_id: target / nil)
DB-->>Merge: merchant_id reassigned / nulled
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
This PR looks solid! The fix is clean and well-thought-out. A few observations: Approach — The 3-step pattern (capture scope → flag entries → update merchant_id) is applied consistently across all three flows, and the ordering is correct: flagging must happen before the Reuses existing infrastructure — Rather than introducing new schema or a new protection mechanism, the fix piggybacks on the existing
One thing to consider going forward: are there other bulk operations that reassign Overall this is a focused, well-tested fix that addresses a real data-integrity issue. Nice work! 🎉 To reply, just mention @dosu. Docs are dead. Just use Dosu. |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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/models/entry.rb`:
- Around line 311-315: The current mark_user_modified_for_transactions! method
materializes transaction IDs into an Array causing memory/SQL-parameter issues;
change it to accept either an ActiveRecord::Relation or an enumerable and let
the DB perform selection: detect if the argument is a Relation (e.g.,
respond_to?(:arel) or is_a?(ActiveRecord::Relation)) and build the update as
where(entryable_type: "Transaction").where(entryable_id:
relation_or_subquery).update_all(user_modified: true), falling back to the
existing behavior for small enumerables only if necessary; update callers to
pass a relation/subquery (e.g.,
Entry.mark_user_modified_for_transactions!(scope) instead of scope.pluck(: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: 49bc17cb-8505-459a-8ddc-76ce399a3f1d
📒 Files selected for processing (5)
app/models/entry.rbapp/models/merchant/merger.rbapp/models/provider_merchant.rbtest/models/merchant/merger_test.rbtest/models/provider_merchant_test.rb
Addresses PR we-promise#1981 review (CodeRabbit): mark_user_modified_for_transactions! now accepts an ActiveRecord::Relation and selects ids via subquery, so large merges/unlinks don't materialize ids or hit SQL parameter limits. Array of ids still supported. Callers pass the scope relation directly.
|
The fix is well-designed and directly addresses the root cause. A few notes: Order dependency is critical: The comments noting that Relation vs. Array design: Accepting an
Test coverage: The "does not touch entries of unrelated merchants" test is valuable — it confirms the scope is tight and won't accidentally protect transactions that were already pointing at the target merchant before the merge. Red-verified before submit is noted in the PR description, which builds confidence. Generated by Claude Code |
|
Thanks for the thorough review! Quick clarification so this thread isn't left implying a pending change: the relation-accepting refactor you praised in note #2 is already in — commit The order-dependency comments (note #1) are still in place in all three call sites, and the |
Summary
Fixes #1977.
Merging merchants, converting a synced (provider) merchant to a family merchant, and unlinking a merchant all reassign
transactions.merchant_idin bulk — but never flag the affected entries asuser_modified. The next provider sync sees those entries as unmodified and reverts the change, forcing the user to re-merge or fix each transaction again.Root cause
Three bulk-
update_allsites changemerchant_idwithout protecting the entries:Merchant::Merger#merge!—family.transactions.where(merchant_id: source.id).update_all(merchant_id: target.id)ProviderMerchant#convert_to_family_merchant_for— same patternProviderMerchant#unlink_from_family— nullsmerchant_idThe provider sync skip-guard (
Account::ProviderImportAdapter#determine_skip_reason) only leaves an entry alone when it'sexcluded?,user_modified?, orimport_locked?. Since none of these flows setuser_modified, the merchant gets overwritten on the next sync.Fix
Add
Entry.mark_user_modified_for_transactions!(transaction_ids)and call it in all three flows before themerchant_idupdate (so the scope still matches the original merchant). Flagged entries are then skipped by the sync, so the manual change sticks.app/models/entry.rb— new bulk helperapp/models/merchant/merger.rb— flag before reassignapp/models/provider_merchant.rb— flag before convert and before unlinkNo schema change; reuses the existing protection mechanism.
Test plan
test/models/merchant/merger_test.rb,test/models/provider_merchant_test.rb): merge / convert / unlink flag the affected entries asuser_modified, and merge leaves unrelated transactions untouched.user_modifiedassertions fail onmain(no flag set) and pass on this branch.merger,provider_merchant,provider_merchant/enhancer,family_merchants_controller,api/v1/merchants_controller).bin/rubocop -aclean;bin/brakeman --no-pagerno warnings.Before / after
After merging two merchants, the affected transaction is now marked Protected from sync, so a re-sync won't revert it.
Before (

main) — merged transaction, no protection:Before
After (this PR) — the merge marks the entry protected:

After
Summary by CodeRabbit