we-promise/sure · error · Merchant::Merger::UnauthorizedMerchantError

#{label} does not belong to this family

Error message

#{label} does not belong to this family

What it means

Merchant::Merger#validate_merchant_belongs_to_family! raises UnauthorizedMerchantError when a target or source merchant's id is not in the union of family.merchants and family.assigned_merchants ids. The check runs in the constructor, so instantiating Merchant::Merger with a foreign merchant fails before any merge work. This is a tenant-isolation guard: a family can only merge its own (or assigned) merchants.

Source

Thrown at app/models/merchant/merger.rb:24

  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)

View on GitHub (pinned to e69894adb9)

Solutions

  1. Scope lookups to the family: family.merchants.find(params[:id]) / family.assigned_merchants, so foreign ids raise RecordNotFound instead.
  2. Ensure the merchant is actually assigned to the family before merging (create the FamilyMerchant/assignment link).
  3. If the merchant is legitimately shared, verify family.assigned_merchants includes it or reassign ownership first.
  4. Rescue Merchant::Merger::UnauthorizedMerchantError in the controller and return 403/404 rather than 500.

Example fix

# before
merger = Merchant::Merger.new(family: family, target_merchant: Merchant.find(params[:target_id]), source_merchants: Merchant.where(id: params[:source_ids]))

# after
target = family.merchants.find(params[:target_id])
sources = family.merchants.where(id: params[:source_ids])
merger = Merchant::Merger.new(family: family, target_merchant: target, source_merchants: sources)
Defensive patterns

Strategy: validation

Validate before calling

allowed = (family.merchants.pluck(:id) + family.assigned_merchants.pluck(:id)).uniq
allowed.include?(merchant.id)

Try / catch

begin
  Merchant::Merger.new(family: family, target_merchant: target, source_merchants: sources).merge!
rescue Merchant::Merger::UnauthorizedMerchantError
  head :forbidden
end

Prevention

When it happens

Trigger: Passing a merchant from another family (e.g., fetched by id from params without scoping); stale object from a different tenant in a multi-family request; a ProviderMerchant that is used by the family but never assigned via family.assigned_merchants.

Common situations: IDOR-style param tampering (merchant_id from another account); copied seed data where merchant-family links were not recreated; frontend passing a merchant from a cached list after the family switched.

Related errors


AI-assisted analysis of we-promise/sure@e69894adb9 (2026-08-21). Data as JSON: /api/errors/6d72e214fe337630. Report an issue: GitHub.