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
- Refresh the page and retry with a lower purchase quantity if the product allows quantity selection.
- Seller: restock the component or reduce the per-bundle quantity so bundles stay fulfillable.
- 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
- Check both caps before submit: variant quantity_left AND product remaining_for_sale_count (nil means uncapped).
- Remember the requirement multiplies: bundle quantity x purchase quantity.
- Stock is checked under an inventory lock at submit; treat a pass at page-load time as advisory only.
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
- The bundle's contents have changed. Please refresh the page!
- The cart does not have any products to which the upsell appl
- Tip is not allowed for this product
- Tip is too large for this purchase
- You can't gift your own product. To give it away for free, c
AI-assisted analysis of antiwork/gumroad@afeacbd394 (2026-08-21).
Data as JSON: /api/errors/6b08d2183c019d69.
Report an issue: GitHub.