antiwork/gumroad · error · Link::LinkInvalid
Offer code cannot be expired
Error message
Offer code cannot be expired
What it means
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.
Source
Thrown at app/controllers/bundles/product_controller.rb:155
def update_default_offer_code
# Only touch the default offer code when the request includes the field.
# Requests from flows that don't render the toggle (like converting a
# product to a bundle) shouldn't silently clear an existing default code.
return unless params.key?(:default_offer_code_id)
default_offer_code_id = product_permitted_params[:default_offer_code_id]
return @bundle.default_offer_code = nil if default_offer_code_id.blank?
offer_code = @bundle.user.offer_codes.alive.find_by_external_id!(default_offer_code_id)
# The form re-sends the current id on every save, so a currency change
# would otherwise be rejected outright by applicable? below. Leave an
# unchanged id to Link#clear_detached_default_offer_code, which drops it
# when it stops applying.
return if offer_code == @bundle.default_offer_code
raise Link::LinkInvalid, "Offer code cannot be expired" if offer_code.inactive?
raise Link::LinkInvalid, "Offer code must apply to this product" unless offer_code.applicable?(@bundle)
@bundle.default_offer_code = offer_code
rescue ActiveRecord::RecordNotFound
raise Link::LinkInvalid, "Invalid offer code"
end
end
View on GitHub (pinned to afeacbd394)
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.
Example fix
# before
patch bundle_product_path(@bundle), params: { product: { default_offer_code_id: expired_code.external_id } }
# after — pre-check the code is active before assigning
offer_code = @bundle.user.offer_codes.alive.find_by_external_id(default_offer_code_id)
if offer_code && !offer_code.inactive?
@bundle.default_offer_code = offer_code
end Defensive patterns
Strategy: validation
Validate before calling
offer_code = @bundle.user.offer_codes.alive.find_by(external_id: default_offer_code_id) assign = offer_code.present? && !offer_code.inactive? && offer_code != @bundle.default_offer_code @bundle.default_offer_code = offer_code if assign
Type guard
def usable_default_offer_code?(code, bundle) code.is_a?(OfferCode) && !code.inactive? && code.applicable?(bundle) end
Try / catch
begin update_default_offer_code rescue Link::LinkInvalid => e flash.now[:alert] = e.message # renders form, nothing persisted end
Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Offer code must apply to this product
- Invalid offer code
- Offer code cannot be expired
- Offer code must apply to this product
- Invalid offer code
AI-assisted analysis of antiwork/gumroad@afeacbd394 (2026-08-21).
Data as JSON: /api/errors/f824bac30759e6eb.
Report an issue: GitHub.