{"record":{"id":"87a18069af7fb333","repo":"antiwork/gumroad","slug":"the-price-just-changed-refresh-the-page-for-the-u","errorCode":null,"errorMessage":"The price just changed! Refresh the page for the updated price.","messagePattern":"The price just changed! Refresh the page for the updated price\\.","errorType":"exception","errorClass":"Subscription::UpdateFailed","httpStatus":null,"severity":"error","filePath":"app/services/subscription/updater_service.rb","lineNumber":277,"sourceCode":"      UpdateIntegrationsOnTierChangeWorker.perform_async(subscription.id)\n    end\n\n    subscription.send_restart_notifications! if is_resubscribing && result[:success] && !result[:requires_card_action] && terminated_or_scheduled_for_termination\n\n    result\n  end\n\n  private\n    def validate_params\n      return if !tiered_membership? || (variants.present? && price.present?)\n\n      \"Please select a valid tier and payment option.\"\n    end\n\n    def validate_perceived_prices_match\n      unless new_price_cents == params[:perceived_price_cents] && amount_owed == params[:perceived_upgrade_price_cents]\n        logger.info(\"SubscriptionUpdater: Error updating subscription - perceived prices do not match: id: #{subscription.external_id} ; new_price_cents: #{new_price_cents} ; amount_owed: #{amount_owed}\")\n        raise Subscription::UpdateFailed, \"The price just changed! Refresh the page for the updated price.\"\n      end\n    end\n\n    def purchase_perceived_price_cents(original_discount)\n      return params[:price_range] unless pwyw? && params[:price_range].present?\n\n      fixed_once_per_cart = if params[:offer_code].present?\n        params[:offer_code].is_cents? && params[:offer_code].once_per_cart?\n      else\n        original_discount.present? && !original_discount.offer_code_is_percent && original_discount.once_per_cart?\n      end\n      return params[:price_range] unless fixed_once_per_cart\n\n      params[:perceived_price_cents]\n    end\n\n    def should_clear_original_discount?\n      params[:offer_code].blank? && original_purchase.offer_code&.deleted?","sourceCodeStart":259,"sourceCodeEnd":295,"githubUrl":"https://github.com/antiwork/gumroad/blob/afeacbd394069a1cbf0c6c50ee8e900925050370/app/services/subscription/updater_service.rb#L259-L295","documentation":"validate_perceived_prices_match compares the server-computed new recurring price (new_price_cents) and prorated amount owed (amount_owed) against the browser-submitted perceived_price_cents and perceived_upgrade_price_cents. Any mismatch aborts the update with \"The price just changed! Refresh the page for the updated price.\" — a guard ensuring the buyer is never charged a different amount than the page displayed. The full computed values are logged with the subscription id.","triggerScenarios":"Plan change or overdue-charge update submitted after the seller changed the tier price, recurrence, or offer code between page render and submit; a once-per-cart or percent discount edited/deleted concurrently; amount_owed recomputed differently because overdue_for_charge, trial state, or discount allocation changed since the form was rendered.","commonSituations":"Buyer leaves an upgrade tab open for hours or days before confirming; seller edits price mid-sale while buyers are in checkout; double-submit of a stale form after a price change; offer code amount modified by creator between add-to-form and submit.","solutions":["Refresh the page and resubmit — the form then carries the current perceived prices.","Sellers: avoid editing a product's price or discounts while buyers are mid-upgrade during launches.","API integrators: always send perceived_price_cents / perceived_upgrade_price_cents read from the same server render that displayed the price, never hardcoded values."],"exampleFix":"# before: one-shot call that loses the buyer's context on mismatch\nresult = Subscription::UpdaterService.new(subscription:, params:, logged_in_user:, gumroad_guid:, remote_ip:).perform\n\n# after: treat the mismatch as a signal to re-fetch and re-confirm\nresult = Subscription::UpdaterService.new(subscription:, params:, logged_in_user:, gumroad_guid:, remote_ip:).perform\nif !result[:success] && result[:error_message].include?(\"price just changed\")\n  re_render_form_with_current_prices # buyer confirms the new amount, then resubmits\nend","handlingStrategy":"validation","validationCode":"# Server-side: verify perceived prices match before opening the transaction\nexpected_price = new_price_cents\nexpected_owed = amount_owed\nparams[:perceived_price_cents] == expected_price && params[:perceived_upgrade_price_cents] == expected_owed","typeGuard":null,"tryCatchPattern":"result = Subscription::UpdaterService.new(...).perform\nif !result[:success] && result[:error_message].include?(\"price just changed\")\n  # re-fetch current price/tier data, re-render the confirmation, let the buyer approve the new amount\nend","preventionTips":["Send perceived_price_cents and perceived_upgrade_price_cents from the same server render that displayed the price — never from a cached client value.","Show the buyer the final amount on the confirm step sourced from the same payload they will submit.","Schedule seller price edits outside launch windows when buyers are mid-upgrade."],"tags":["subscription","pricing","race-condition","stale-form","optimistic-check"],"backgroundTag":"stale-price-mismatch","analyzedSha":"afeacbd394069a1cbf0c6c50ee8e900925050370","analyzedAt":"2026-08-21T17:58:52.159Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}