antiwork/gumroad · error · Link::LinkInvalid

Offer code cannot be expired

Error message

Offer code cannot be expired

What it means

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.

Source

Thrown at app/controllers/links_controller.rb:1932

        return unless @product.installment_plan.changed?
      end

      @product.installment_plan&.destroy_if_no_payment_options!
      @product.reset_installment_plan

      if product_permitted_params[:installment_plan].present?
        @product.create_installment_plan!(product_permitted_params[:installment_plan])
      end
    end

    def update_default_offer_code
      default_offer_code_id = product_permitted_params[:default_offer_code_id]

      return @product.default_offer_code = nil if default_offer_code_id.blank?

      offer_code = @product.user.offer_codes.alive.find_by_external_id!(default_offer_code_id)

      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?(@product)

      @product.default_offer_code = offer_code
    rescue ActiveRecord::RecordNotFound
      raise Link::LinkInvalid, "Invalid offer code"
    end

    def toggle_community_chat!(enabled)
      return if [Link::NATIVE_TYPE_COFFEE, Link::NATIVE_TYPE_BUNDLE].include?(@product.native_type)

      @product.toggle_community_chat!(enabled)
    end

    def generate_product_details_using_ai
      if Rails.env.test?
        generate_product_cover_and_thumbnail_using_ai
        generate_product_content_using_ai
      else

View on GitHub (pinned to afeacbd394)

Solutions

  1. 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.
  2. Extend the offer code's expiry on the Offer Codes page if it should remain the default.
  3. Clear the default offer code (submit blank `default_offer_code_id`) when the expired code is no longer wanted — blank unassigns it.
  4. 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.

Example fix

# before — saving an unrelated change with a stale default_offer_code_id hidden field
put product_path(@product), params: { product: { name: "New name", default_offer_code_id: expired_code.external_id } }

# after — drop or refresh the assignment before saving
put product_path(@product), params: { product: { name: "New name", default_offer_code_id: nil } }
# or: default_offer_code_id: active_code.external_id
Defensive patterns

Strategy: validation

Validate before calling

code = @product.user.offer_codes.alive.find_by(external_id: default_offer_code_id)
# omit the key entirely on unrelated saves; never re-send a possibly-expired id blindly
@product.default_offer_code = code if code && !code.inactive? && code.applicable?(@product)

Type guard

def active_default_offer?(code)
  !code.inactive? # valid_at blank/past AND expires_at blank/future
end

Try / catch

begin
  update_product_with_default_offer_code
rescue Link::LinkInvalid => e
  flash.now[:alert] = e.message
  render :edit # nothing persisted; user re-picks the code
end

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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