{"record":{"id":"f824bac30759e6eb","repo":"antiwork/gumroad","slug":"offer-code-cannot-be-expired","errorCode":null,"errorMessage":"Offer code cannot be expired","messagePattern":"Offer code cannot be expired","errorType":"exception","errorClass":"Link::LinkInvalid","httpStatus":null,"severity":"error","filePath":"app/controllers/bundles/product_controller.rb","lineNumber":155,"sourceCode":"    def update_default_offer_code\n      # 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":137,"sourceCodeEnd":163,"githubUrl":"https://github.com/antiwork/gumroad/blob/afeacbd394069a1cbf0c6c50ee8e900925050370/app/controllers/bundles/product_controller.rb#L137-L163","documentation":"Raised as `Link::LinkInvalid` when saving a bundle's product settings with a `default_offer_code_id` that resolves to an offer code that is inactive. `OfferCode#inactive?` (app/models/offer_code.rb:314) is true when `valid_at` is in the future (not yet valid) or `expires_at` is in the past (expired). The bundle controller rescues `LinkInvalid` at line 88 and re-renders the form with this message, so nothing is saved.","triggerScenarios":"POST/PATCH to the bundle product settings form selecting a NEW default offer code (different from the current one — the unchanged-id case early-returns) whose offer code has already expired or is scheduled for the future. Typical flow: seller A creates a code with a 30-day expiry, it lapses, then someone picks it from a stale dropdown months later.","commonSituations":"Offer code expired between the time the settings page loaded (dropdown populated) and the form was submitted; offer codes created with a future `valid_at` being picked prematurely; multiple admins editing codes so one expires a code another is mid-form on.","solutions":["Pick a default offer code that is currently active (started and not expired) — reload the offer-code dropdown before saving.","Extend the offer code's expiry (or clear its future `valid_at`) on the Offer Codes settings page, then retry the save.","If the code should no longer be used, clear the default offer code field (submit a blank id) instead of keeping the expired one selected.","As an API caller, verify `!offer_code.inactive?` before sending `default_offer_code_id`; note the form re-sends the current id on every save, so an unchanged id is tolerated (early return) but switching to an expired code is not."],"exampleFix":"# before\npatch bundle_product_path(@bundle), params: { product: { default_offer_code_id: expired_code.external_id } }\n\n# after — pre-check the code is active before assigning\noffer_code = @bundle.user.offer_codes.alive.find_by_external_id(default_offer_code_id)\nif offer_code && !offer_code.inactive?\n  @bundle.default_offer_code = offer_code\nend","handlingStrategy":"validation","validationCode":"offer_code = @bundle.user.offer_codes.alive.find_by(external_id: default_offer_code_id)\nassign = offer_code.present? && !offer_code.inactive? && offer_code != @bundle.default_offer_code\n@bundle.default_offer_code = offer_code if assign","typeGuard":"def usable_default_offer_code?(code, bundle)\n  code.is_a?(OfferCode) && !code.inactive? && code.applicable?(bundle)\nend","tryCatchPattern":"begin\n  update_default_offer_code\nrescue Link::LinkInvalid => e\n  flash.now[:alert] = e.message # renders form, nothing persisted\nend","preventionTips":["Refresh the offer-code list when the settings form loads so only live codes appear.","Pre-check !offer_code.inactive? before assigning it as a default.","Re-sending the unchanged current id is safe in the bundles controller (early return) — rely on that instead of clearing/re-setting.","Give long-lived default codes far-future expiries or no expiry."],"tags":["offer-codes","bundles","link-invalid","product-settings","validation"],"backgroundTag":"offer-code-expired","analyzedSha":"afeacbd394069a1cbf0c6c50ee8e900925050370","analyzedAt":"2026-08-21T17:58:52.159Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}