{"record":{"id":"3fbd7819e8605153","repo":"antiwork/gumroad","slug":"offer-code-must-apply-to-this-product-3fbd78","errorCode":null,"errorMessage":"Offer code must apply to this product","messagePattern":"Offer code must apply to this product","errorType":"exception","errorClass":"Link::LinkInvalid","httpStatus":422,"severity":"error","filePath":"app/controllers/links_controller.rb","lineNumber":1933,"sourceCode":"      end\n\n      @product.installment_plan&.destroy_if_no_payment_options!\n      @product.reset_installment_plan\n\n      if product_permitted_params[:installment_plan].present?\n        @product.create_installment_plan!(product_permitted_params[:installment_plan])\n      end\n    end\n\n    def update_default_offer_code\n      default_offer_code_id = product_permitted_params[:default_offer_code_id]\n\n      return @product.default_offer_code = nil if default_offer_code_id.blank?\n\n      offer_code = @product.user.offer_codes.alive.find_by_external_id!(default_offer_code_id)\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?(@product)\n\n      @product.default_offer_code = offer_code\n    rescue ActiveRecord::RecordNotFound\n      raise Link::LinkInvalid, \"Invalid offer code\"\n    end\n\n    def toggle_community_chat!(enabled)\n      return if [Link::NATIVE_TYPE_COFFEE, Link::NATIVE_TYPE_BUNDLE].include?(@product.native_type)\n\n      @product.toggle_community_chat!(enabled)\n    end\n\n    def generate_product_details_using_ai\n      if Rails.env.test?\n        generate_product_cover_and_thumbnail_using_ai\n        generate_product_content_using_ai\n      else\n        cover_thread = Thread.new { generate_product_cover_and_thumbnail_using_ai }","sourceCodeStart":1915,"sourceCodeEnd":1951,"githubUrl":"https://github.com/antiwork/gumroad/blob/afeacbd394069a1cbf0c6c50ee8e900925050370/app/controllers/links_controller.rb#L1915-L1951","documentation":"links_controller.rb:1933 raises `Link::LinkInvalid` when the default offer code fails `OfferCode#applicable?(@product)` (app/models/offer_code.rb:305): a non-universal code must list the product, a universal code must not exclude it, and a currency-scoped universal code must match the product's price currency. Because this controller lacks the unchanged-id early return, even re-sending the CURRENT id fails once applicability drifted — e.g. the product's currency changed so it no longer matches the code.","triggerScenarios":"Saving a product whose currency was changed while a currency-scoped universal code stays set as default; a code whose product list no longer includes the product; a universal code that now excludes the product; selecting a code belonging to other products.","commonSituations":"Currency change on a product with a currency-restricted default offer code (the exact case the bundles controller comments about, still live here); product removed from a multi-product code's scope; stale editor tabs.","solutions":["If you just changed the product's currency, also clear or re-pick the default offer code in the same save — a currency-scoped code will keep failing otherwise.","Edit the offer code to include the product / drop the currency restriction / remove the exclusion, then save the product again.","Select a universal code without a currency_type, or one explicitly attached to this product.","Pre-check `offer_code.applicable?(@product)` before sending `default_offer_code_id` from API clients."],"exampleFix":"# before — currency flip + stale code id in one save\nput product_path(@product), params: { product: { price_currency_type: \"eur\", default_offer_code_id: usd_only_code.external_id } }\n\n# after — clear the detached default in the same request\nput product_path(@product), params: { product: { price_currency_type: \"eur\", default_offer_code_id: nil } }","handlingStrategy":"validation","validationCode":"code = @product.user.offer_codes.alive.find_by(external_id: default_offer_code_id)\n@product.default_offer_code = code if code&.applicable?(@product)","typeGuard":"def code_applies_after_change?(code, product, new_currency)\n  return false unless code.applicable?(product)\n  code.currency_type.blank? || code.currency_type == new_currency\nend","tryCatchPattern":"begin\n  @product.update!(permitted_params)\nrescue Link::LinkInvalid => e\n  # message is user-ready; prompt to re-pick or clear the default offer code\n  render :edit, alert: e.message\nend","preventionTips":["When changing a product's currency, always clear/re-pick its default offer code in the same request.","This path has no unchanged-id exemption — keep default codes currency-free or currency-synced with the product.","Re-check code applicability whenever you edit a code's product scope or exclusions.","Mirror the bundles controller's unchanged-id early return if you extend this controller."],"tags":["offer-codes","product-settings","currency-mismatch","link-invalid","validation"],"backgroundTag":"offer-code-not-applicable","analyzedSha":"afeacbd394069a1cbf0c6c50ee8e900925050370","analyzedAt":"2026-08-21T17:58:52.159Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}