{"record":{"id":"3ebbbab5bf872277","repo":"antiwork/gumroad","slug":"offer-code-cannot-be-expired-3ebbba","errorCode":null,"errorMessage":"Offer code cannot be expired","messagePattern":"Offer code cannot be expired","errorType":"exception","errorClass":"Link::LinkInvalid","httpStatus":422,"severity":"error","filePath":"app/controllers/links_controller.rb","lineNumber":1932,"sourceCode":"        return unless @product.installment_plan.changed?\n      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","sourceCodeStart":1914,"sourceCodeEnd":1950,"githubUrl":"https://github.com/antiwork/gumroad/blob/afeacbd394069a1cbf0c6c50ee8e900925050370/app/controllers/links_controller.rb#L1914-L1950","documentation":"Same rule as the bundle variant, on the main product editor: links_controller.rb:1932 raises `Link::LinkInvalid` when the submitted `default_offer_code_id` resolves to an offer code where `OfferCode#inactive?` is true (`valid_at` future or `expires_at` past). Unlike the bundles controller, this path has NO unchanged-id early return, so the product form re-sending the current default offer code id after that code expired will trip this on an otherwise unrelated save. The action rescues LinkInvalid (links_controller.rb:663) and re-renders the form with the message.","triggerScenarios":"Saving any product change (price, name, files) while the form still carries the id of a default offer code that has since expired; explicitly selecting a not-yet-valid or expired code as the new default; offer code expired while the product editor tab sat open for days.","commonSituations":"Long-lived product-edit tabs with stale hidden-field values; codes with short expiry windows used as defaults; admin expires a code while a seller is mid-edit.","solutions":["Reload the product edit page so the form carries fresh state, then either re-select an active code or clear the default offer code field, and save again.","Extend the offer code's expiry on the Offer Codes page if it should remain the default.","Clear the default offer code (submit blank `default_offer_code_id`) when the expired code is no longer wanted — blank unassigns it.","As an API caller, pre-check `!offer_code.inactive?` before including `default_offer_code_id`, and omit… note that omitting/blank clears the assignment, so send the id only when it is currently valid."],"exampleFix":"# before — saving an unrelated change with a stale default_offer_code_id hidden field\nput product_path(@product), params: { product: { name: \"New name\", default_offer_code_id: expired_code.external_id } }\n\n# after — drop or refresh the assignment before saving\nput product_path(@product), params: { product: { name: \"New name\", default_offer_code_id: nil } }\n# or: default_offer_code_id: active_code.external_id","handlingStrategy":"validation","validationCode":"code = @product.user.offer_codes.alive.find_by(external_id: default_offer_code_id)\n# omit the key entirely on unrelated saves; never re-send a possibly-expired id blindly\n@product.default_offer_code = code if code && !code.inactive? && code.applicable?(@product)","typeGuard":"def active_default_offer?(code)\n  !code.inactive? # valid_at blank/past AND expires_at blank/future\nend","tryCatchPattern":"begin\n  update_product_with_default_offer_code\nrescue Link::LinkInvalid => e\n  flash.now[:alert] = e.message\n  render :edit # nothing persisted; user re-picks the code\nend","preventionTips":["Unlike bundles, the product editor raises even for the unchanged id — after an offer code expires, always clear or re-pick the default before the next save.","Avoid short expiries on codes used as product defaults.","Reload the edit page before saving a tab that has been open for days.","API clients: send default_offer_code_id only with a code verified active right now; remember blank clears the assignment."],"tags":["offer-codes","product-settings","link-invalid","stale-form","validation"],"backgroundTag":"offer-code-expired","analyzedSha":"afeacbd394069a1cbf0c6c50ee8e900925050370","analyzedAt":"2026-08-21T17:58:52.159Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}