antiwork/gumroad · error · Link::LinkInvalid

Invalid offer code

Error message

Invalid offer code

What it means

links_controller.rb:1937 converts `ActiveRecord::RecordNotFound` from `@product.user.offer_codes.alive.find_by_external_id!` into `Link::LinkInvalid("Invalid offer code")`. The submitted external id matches no alive offer code owned by the product's seller — deleted code, wrong account, wrong environment, or malformed id. The form save aborts with this message.

Source

Thrown at app/controllers/links_controller.rb:1937

      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
        cover_thread = Thread.new { generate_product_cover_and_thumbnail_using_ai }
        content_thread = Thread.new { generate_product_content_using_ai }

        cover_thread.join
        content_thread.join

View on GitHub (pinned to afeacbd394)

Solutions

  1. Reload the product editor so the offer-code list reflects current codes, then re-select.
  2. Verify the id belongs to the same seller account and environment.
  3. Recreate the deleted code if it was removed by mistake and use its new external id.
  4. Submit a blank `default_offer_code_id` to clear the assignment instead of sending a dead id.

Example fix

# before
put product_path(@product), params: { product: { default_offer_code_id: deleted_code.external_id } }

# after — guard on resolution before the request
code = @product.user.offer_codes.alive.exists?(external_id: submitted_id) ? submitted_id : nil
put product_path(@product), params: { product: { default_offer_code_id: code } }
Defensive patterns

Strategy: validation

Validate before calling

code = @product.user.offer_codes.alive.find_by(external_id: submitted_id)
raise Link::LinkInvalid, "Invalid offer code" unless code

Type guard

def resolvable_offer_code?(user, external_id)
  user.offer_codes.alive.exists?(external_id: external_id)
end

Try / catch

begin
  offer_code = @product.user.offer_codes.alive.find_by_external_id!(id)
rescue ActiveRecord::RecordNotFound
  # refresh picker; never blind-retry the same external id
end

Prevention

When it happens

Trigger: Product form submits `default_offer_code_id` for a code deleted in another tab/session; id from a different seller account; id truncated or altered in transit; code exists but is outside the `alive` scope.

Common situations: Stale dropdown/hidden field referencing a deleted code; scripts moving ids between environments; concurrent admin sessions deleting codes mid-edit.

Related errors


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