{"record":{"id":"d277170a6e308e28","repo":"antiwork/gumroad","slug":"the-product-s-variants-have-changed-please-refres","errorCode":null,"errorMessage":"The product's variants have changed, please refresh the page!","messagePattern":"The product's variants have changed, please refresh the page!","errorType":"exception","errorClass":"Purchase::PurchaseInvalid","httpStatus":null,"severity":"error","filePath":"app/services/purchase/create_service.rb","lineNumber":453,"sourceCode":"      purchase.offer_code = product.find_offer_code(code: purchase.discount_code.downcase.strip) if purchase.discount_code.present?\n      if purchase.offer_code.present? && product.default_offer_code.present? && purchase.offer_code.id == product.default_offer_code.id\n        purchase.default_offer_code_id = purchase.offer_code.id\n      end\n      purchase.business_vat_id = (params_for_purchase[:business_vat_id] && params_for_purchase[:business_vat_id].size > 0 ? params_for_purchase[:business_vat_id] : nil)\n      purchase.is_original_subscription_purchase = (product.is_recurring_billing && !params_for_purchase[:is_gift_receiver_purchase]) || purchase.is_installment_payment\n      purchase.is_free_trial_purchase = product.free_trial_enabled? && !is_gift?\n      purchase.should_exclude_product_review = product.free_trial_enabled? && !is_gift?\n\n      Shipment.create(purchase:) if should_ship\n\n      if params[:variants].present?\n        params[:variants].each do |external_id|\n          variant = product.current_base_variants.find_by_external_id(external_id)\n          if variant.present?\n            purchase.variant_attributes << variant\n          else\n            purchase.errors.add(:base, \"The product's variants have changed, please refresh the page!\")\n            raise Purchase::PurchaseInvalid, \"The product's variants have changed, please refresh the page!\"\n          end\n        end\n      elsif product.is_tiered_membership\n        purchase.variant_attributes << product.tiers.first\n      elsif product.is_physical && product.skus.is_default_sku.present?\n        purchase.variant_attributes << product.skus.is_default_sku.first\n      end\n\n      if product.native_type == Link::NATIVE_TYPE_CALL\n        start_time = Time.zone.parse(params[:call_start_time] || \"\")\n        duration_in_minutes = purchase.variant_attributes.first&.duration_in_minutes\n\n        if start_time.blank? || duration_in_minutes.blank?\n          raise Purchase::PurchaseInvalid, \"Please select a start time.\"\n        end\n\n        end_time = start_time + duration_in_minutes.minutes\n        purchase.build_call(start_time:, end_time:)","sourceCodeStart":435,"sourceCodeEnd":471,"githubUrl":"https://github.com/antiwork/gumroad/blob/afeacbd394069a1cbf0c6c50ee8e900925050370/app/services/purchase/create_service.rb#L435-L471","documentation":"Each external_id in params[:variants] is looked up in product.current_base_variants during build_purchase; an id that no longer resolves means the variant was deleted, archived, or replaced since checkout loaded. The message is added to purchase.errors and raised so bundle-checkout error rendering has a purchase object to attach it to.","triggerScenarios":"params[:variants] contains at least one external_id for which product.current_base_variants.find_by_external_id returns nil.","commonSituations":"Seller edits or removes variants mid-checkout; client persisting a stale variant selection; integrations caching variant ids across product edits.","solutions":["Refresh the checkout page and re-select a current variant.","Integrators: re-fetch the product's current_base_variants at submit time and drop ids that no longer resolve.","On this raise, reload the variant list and restart checkout rather than retrying the same params."],"exampleFix":"# before: stale variant id persisted from an earlier page state\nparams[:variants] = ['var_stale123']\n\n# after: keep only ids that still resolve against the product\nparams[:variants] = params[:variants] & product.current_base_variants.pluck(:external_id)","handlingStrategy":"validation","validationCode":"current_ids = product.current_base_variants.pluck(:external_id)\nparams[:variants] = params[:variants]&.select { current_ids.include?(_1) }","typeGuard":"def variants_current?(variant_ids, product)\n  (variant_ids.to_a - product.current_base_variants.pluck(:external_id)).empty?\nend","tryCatchPattern":"begin\n  Purchase::CreateService.new(product:, params:, buyer:).perform\nrescue Purchase::PurchaseInvalid => e\n  # variant set changed: reload variants, force re-selection, restart checkout\n  redirect_to_checkout if e.message == \"The product's variants have changed, please refresh the page!\"\nend","preventionTips":["Re-fetch current_base_variants at submit time and drop ids that no longer resolve.","Never cache variant ids across product edits in integrations."],"tags":["variants","stale-state","checkout","gumroad"],"backgroundTag":"variant-stale-checkout","analyzedSha":"afeacbd394069a1cbf0c6c50ee8e900925050370","analyzedAt":"2026-08-21T17:58:52.159Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}