antiwork/gumroad · error · Purchase::PurchaseInvalid

#{component.name} is no longer available in the quantity thi

Error message

#{component.name} is no longer available in the quantity this bundle includes. Please refresh the page!

What it means

Under the bundle's product_inventory lock, validate_bundle_component_inventory compares the requested quantity (bundle_product.quantity * purchase.quantity) with remaining stock: the minimum of variant.quantity_left and component.remaining_for_sale_count when the component is variant-scoped, otherwise just remaining_for_sale_count; nil means uncapped and passes. If remaining < requested, the bundle cannot be fulfilled complete and checkout aborts before any charge, naming the component.

Source

Thrown at app/services/purchase/create_service.rb:416

    # costs the buyer nothing. Only closes the non-concurrent hole (verdict computed then discarded);
    # the multi-lock direct-vs-bundle/bundle-vs-bundle race is gumroad-private#1786 items 2-4.
    def validate_bundle_component_inventory(bundle_product)
      requested_quantity = bundle_product.quantity * purchase.quantity
      component = bundle_product.product.reload

      # A variant can have stock left while its product-wide cap is exhausted (or vice versa),
      # so both must be checked — checking only one lets the other's cap be silently oversold.
      remaining = if bundle_product.variant.present?
        [bundle_product.variant.reload.quantity_left, component.remaining_for_sale_count].compact.min
      else
        component.remaining_for_sale_count
      end

      # nil means uncapped, so there is nothing to enforce.
      return if remaining.nil?
      return if remaining >= requested_quantity

      raise Purchase::PurchaseInvalid,
            "#{component.name} is no longer available in the quantity this bundle includes. Please refresh the page!"
    end

    def build_purchase(params_for_purchase)
      params_for_purchase[:country] = ISO3166::Country[params_for_purchase[:country]]&.common_name

      purchase = product.sales.build(params_for_purchase)
      purchase.authenticated_offer_code_buyer = buyer
      purchase.affiliate = product.collaborator if product.collaborator.present?
      should_ship = product.is_physical || product.require_shipping
      purchase.country = nil unless should_ship
      purchase.country ||= ISO3166::Country[params_for_purchase[:sales_tax_country_code_election]]&.common_name
      set_purchaser_for(purchase, params_for_purchase[:email])
      purchase.is_installment_payment = params[:pay_in_installments] && product.allow_installment_plan?
      purchase.installment_plan = product.installment_plan if purchase.is_installment_payment
      purchase.save_card = !!params_for_purchase[:save_card] || (product.is_recurring_billing && !is_gift?) || purchase.is_preorder_authorization || purchase.is_installment_payment
      purchase.seller = product.user
      purchase.is_gift_sender_purchase = is_gift? unless params_for_purchase.has_key?(:is_gift_receiver_purchase)

View on GitHub (pinned to afeacbd394)

Solutions

  1. Refresh the page and retry with a lower purchase quantity if the product allows quantity selection.
  2. Seller: restock the component or reduce the per-bundle quantity so bundles stay fulfillable.
  3. Integrators: before enabling submit, check that each component's min(variant.quantity_left, remaining_for_sale_count) >= bundle quantity x cart quantity, treating nil as uncapped.

Example fix

# before: submit proceeds without a stock check
params[:quantity] = 2 # bundle needs 2x component per unit, only 3 left

# after: gate submission on fulfillable stock
short = product.bundle_products.alive.any? do |bp|
  remaining = bp.variant.present? ? [bp.variant.quantity_left, bp.product.remaining_for_sale_count].compact.min : bp.product.remaining_for_sale_count
  remaining.present? && remaining < bp.quantity * params[:quantity].to_i
end
params[:quantity] = 1 if short
Defensive patterns

Strategy: validation

Validate before calling

if product.is_bundle?
  understocked = product.bundle_products.alive.any? do |bp|
    remaining = bp.variant.present? ? [bp.variant.quantity_left, bp.product.remaining_for_sale_count].compact.min : bp.product.remaining_for_sale_count
    remaining.present? && remaining < bp.quantity * params[:quantity].to_i
  end
  params[:quantity] = 1 if understocked
end

Type guard

def bundle_fulfillable?(bundle_product, purchase_quantity)
  remaining = bundle_product.variant.present? ?
    [bundle_product.variant.quantity_left, bundle_product.product.remaining_for_sale_count].compact.min :
    bundle_product.product.remaining_for_sale_count
  remaining.nil? || remaining >= bundle_product.quantity * purchase_quantity
end

Try / catch

begin
  Purchase::CreateService.new(product:, params:, buyer:).perform
rescue Purchase::PurchaseInvalid => e
  # stock moved under us: reload, recheck, offer lower quantity - never blind-retry
  render_checkout_error(e.message) if e.message.include?('no longer available in the quantity')
end

Prevention

When it happens

Trigger: A capped component (or its variant) has remaining stock below bundle_product.quantity * purchase.quantity - e.g. a bundle includes 2 of an item, purchase quantity is 2 (4 needed), and only 3 remain; note both caps are checked because a variant can have stock while the product-wide cap is exhausted.

Common situations: Stock drops between page load and submit; bundles that include more copies of an item than its cap allows; concurrent purchases consuming the last units.

Related errors


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