{"record":{"id":"46ca1424d2e7b05d","repo":"antiwork/gumroad","slug":"offer-code-must-apply-to-this-product","errorCode":null,"errorMessage":"Offer code must apply to this product","messagePattern":"Offer code must apply to this product","errorType":"exception","errorClass":"Link::LinkInvalid","httpStatus":null,"severity":"error","filePath":"app/controllers/bundles/product_controller.rb","lineNumber":156,"sourceCode":"      # Only touch the default offer code when the request includes the field.\n      # Requests from flows that don't render the toggle (like converting a\n      # product to a bundle) shouldn't silently clear an existing default code.\n      return unless params.key?(:default_offer_code_id)\n\n      default_offer_code_id = product_permitted_params[:default_offer_code_id]\n\n      return @bundle.default_offer_code = nil if default_offer_code_id.blank?\n\n      offer_code = @bundle.user.offer_codes.alive.find_by_external_id!(default_offer_code_id)\n\n      # The form re-sends the current id on every save, so a currency change\n      # would otherwise be rejected outright by applicable? below. Leave an\n      # unchanged id to Link#clear_detached_default_offer_code, which drops it\n      # when it stops applying.\n      return if offer_code == @bundle.default_offer_code\n\n      raise Link::LinkInvalid, \"Offer code cannot be expired\" if offer_code.inactive?\n      raise Link::LinkInvalid, \"Offer code must apply to this product\" unless offer_code.applicable?(@bundle)\n\n      @bundle.default_offer_code = offer_code\n    rescue ActiveRecord::RecordNotFound\n      raise Link::LinkInvalid, \"Invalid offer code\"\n    end\nend\n","sourceCodeStart":138,"sourceCodeEnd":163,"githubUrl":"https://github.com/antiwork/gumroad/blob/afeacbd394069a1cbf0c6c50ee8e900925050370/app/controllers/bundles/product_controller.rb#L138-L163","documentation":"Raised as `Link::LinkInvalid` when the chosen default offer code fails `OfferCode#applicable?` for the bundle (app/models/offer_code.rb:305). A non-universal code must include the product in its `products` list; a universal code must not exclude the product and, if the code declares a `currency_type`, the product's price currency must match. The controller renders the message on the form and aborts the save.","triggerScenarios":"Selecting an offer code that was created for other products (non-universal, bundle not attached), a universal code that explicitly excludes this bundle, or a code with `currency_type: \"usd\"` while the bundle's price is in EUR. Note this bundle controller deliberately early-returns when the id is unchanged, so a currency change alone does not trip it — only switching to a non-applicable code does.","commonSituations":"Seller has many per-product codes and picks the wrong one from the dropdown; a universal code was later scoped to a currency that no longer matches the product; product was removed from the code's product list while it stayed selected in a stale form.","solutions":["Choose an offer code that is attached to this bundle (or a universal code with no currency restriction and no exclusion for it) — reload the offer codes list and re-select.","Edit the offer code on the Offer Codes page to add this bundle to its products (or remove the currency restriction / exclusion), then retry.","As an API caller, run `offer_code.applicable?(@bundle)` before sending `default_offer_code_id`.","Clear the default offer code field if no applicable code is intended."],"exampleFix":"# before\n@bundle.default_offer_code = user.offer_codes.find_by(name: \"Summer sale\") # scoped to other products\n\n# after — only assign when the code applies\noffer_code = user.offer_codes.alive.find_by_external_id(id)\n@bundle.default_offer_code = offer_code if offer_code&.applicable?(@bundle)","handlingStrategy":"validation","validationCode":"return unless offer_code.applicable?(@bundle)\n@bundle.default_offer_code = offer_code","typeGuard":"def code_applies?(code, bundle)\n  return false if code.universal? && code.excluded_products.include?(bundle)\n  code.universal? ? (code.currency_type.blank? || bundle.price_currency_type == code.currency_type) : code.products.include?(bundle)\nend","tryCatchPattern":"begin\n  @bundle.update!(permitted)\nrescue Link::LinkInvalid => e\n  # e.message is form-ready: \"Offer code must apply to this product\"\n  render :edit, alert: e.message\nend","preventionTips":["Only offer codes attached to the bundle (or currency-free universal codes) in the default-code picker.","When changing a product's currency, clear/re-pick its default offer code in the same edit.","Run offer_code.applicable?(product) in client-side or service pre-validation before save.","Keep offer-code product lists and exclusions current when products are merged/renamed."],"tags":["offer-codes","bundles","link-invalid","currency-mismatch","validation"],"backgroundTag":"offer-code-not-applicable","analyzedSha":"afeacbd394069a1cbf0c6c50ee8e900925050370","analyzedAt":"2026-08-21T17:58:52.159Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}