{"record":{"id":"6d72e214fe337630","repo":"we-promise/sure","slug":"label-does-not-belong-to-this-family-6d72e2","errorCode":null,"errorMessage":"#{label} does not belong to this family","messagePattern":"#(.+?) does not belong to this family","errorType":"exception","errorClass":"Merchant::Merger::UnauthorizedMerchantError","httpStatus":null,"severity":"error","filePath":"app/models/merchant/merger.rb","lineNumber":24,"sourceCode":"  def initialize(family:, target_merchant:, source_merchants:)\n    @family = family\n    @target_merchant = target_merchant\n    @merged_count = 0\n\n    validate_merchant_belongs_to_family!(target_merchant, \"Target merchant\")\n\n    sources = Array(source_merchants)\n    sources.each { |m| validate_merchant_belongs_to_family!(m, \"Source merchant '#{m.name}'\") }\n\n    @source_merchants = sources.reject { |m| m.id == target_merchant.id }\n  end\n\n  private\n\n    def validate_merchant_belongs_to_family!(merchant, label)\n      return if family_merchant_ids.include?(merchant.id)\n\n      raise UnauthorizedMerchantError, \"#{label} does not belong to this family\"\n    end\n\n    def family_merchant_ids\n      @family_merchant_ids ||= begin\n        family_ids = family.merchants.pluck(:id)\n        assigned_ids = family.assigned_merchants.pluck(:id)\n        (family_ids + assigned_ids).uniq\n      end\n    end\n\n  public\n\n  def merge!\n    return false if source_merchants.empty?\n\n    Merchant.transaction do\n      source_merchants.each do |source|\n        scope = family.transactions.where(merchant_id: source.id)","sourceCodeStart":6,"sourceCodeEnd":42,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/merchant/merger.rb#L6-L42","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Scope lookups to the family: family.merchants.find(params[:id]) / family.assigned_merchants, so foreign ids raise RecordNotFound instead.","Ensure the merchant is actually assigned to the family before merging (create the FamilyMerchant/assignment link).","If the merchant is legitimately shared, verify family.assigned_merchants includes it or reassign ownership first.","Rescue Merchant::Merger::UnauthorizedMerchantError in the controller and return 403/404 rather than 500."],"exampleFix":"# before\nmerger = Merchant::Merger.new(family: family, target_merchant: Merchant.find(params[:target_id]), source_merchants: Merchant.where(id: params[:source_ids]))\n\n# after\ntarget = family.merchants.find(params[:target_id])\nsources = family.merchants.where(id: params[:source_ids])\nmerger = Merchant::Merger.new(family: family, target_merchant: target, source_merchants: sources)","handlingStrategy":"validation","validationCode":"allowed = (family.merchants.pluck(:id) + family.assigned_merchants.pluck(:id)).uniq\nallowed.include?(merchant.id)","typeGuard":null,"tryCatchPattern":"begin\n  Merchant::Merger.new(family: family, target_merchant: target, source_merchants: sources).merge!\nrescue Merchant::Merger::UnauthorizedMerchantError\n  head :forbidden\nend","preventionTips":["Always scope merchant lookups through the current family (family.merchants.find), never Merchant.find on raw params.","Never trust merchant ids from the client; verify membership server-side.","Return 403/404 on this error so cross-tenant probing learns nothing."],"tags":["authorization","multi-tenant","merchants","rails"],"backgroundTag":"cross-tenant-access","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}