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

  1. Pick a default offer code that is currently active (started and not expired) — reload the offer-code dropdown before saving.
  2. Extend the offer code's expiry (or clear its future `valid_at`) on the Offer Codes settings page, then retry the save.
  3. 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.
  4. 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

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


AI-assisted analysis of antiwork/gumroad@afeacbd394 (2026-08-21). Data as JSON: /api/errors/f824bac30759e6eb. Report an issue: GitHub.