Files
sure/app/models/merchant/merger.rb
dripsmvcp ea316b4277 fix(merchants): preserve manual merchant edits across provider sync (#1981)
* fix(merchants): preserve manual merchant edits across provider sync

Fixes #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.

* fix(merchants): pass transaction relation to bulk user_modified helper

Addresses PR #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.
2026-05-30 23:27:18 +02:00

62 lines
1.8 KiB
Ruby

class Merchant::Merger
class UnauthorizedMerchantError < StandardError; end
attr_reader :family, :target_merchant, :source_merchants, :merged_count
def initialize(family:, target_merchant:, source_merchants:)
@family = family
@target_merchant = target_merchant
@merged_count = 0
validate_merchant_belongs_to_family!(target_merchant, "Target merchant")
sources = Array(source_merchants)
sources.each { |m| validate_merchant_belongs_to_family!(m, "Source merchant '#{m.name}'") }
@source_merchants = sources.reject { |m| m.id == target_merchant.id }
end
private
def validate_merchant_belongs_to_family!(merchant, label)
return if family_merchant_ids.include?(merchant.id)
raise UnauthorizedMerchantError, "#{label} does not belong to this family"
end
def family_merchant_ids
@family_merchant_ids ||= begin
family_ids = family.merchants.pluck(:id)
assigned_ids = family.assigned_merchants.pluck(:id)
(family_ids + assigned_ids).uniq
end
end
public
def merge!
return false if source_merchants.empty?
Merchant.transaction do
source_merchants.each do |source|
scope = family.transactions.where(merchant_id: source.id)
# Protect the manual reassignment from being reverted on the next
# provider sync (issue #1977). Must run before the merchant_id update
# so the scope still matches the source merchant.
Entry.mark_user_modified_for_transactions!(scope)
# Reassign family's transactions to target
scope.update_all(merchant_id: target_merchant.id)
# Delete FamilyMerchant, keep ProviderMerchant (it may be used by other families)
source.destroy! if source.is_a?(FamilyMerchant)
@merged_count += 1
end
end
true
end
end