{"record":{"id":"9b8e6fad7f9cb90f","repo":"antiwork/gumroad","slug":"tip-is-too-large-for-this-purchase","errorCode":null,"errorMessage":"Tip is too large for this purchase","messagePattern":"Tip is too large for this purchase","errorType":"exception","errorClass":"Purchase::PurchaseInvalid","httpStatus":null,"severity":"error","filePath":"app/services/purchase/create_service.rb","lineNumber":146,"sourceCode":"            purchase.offer_code = upsell.offer_code\n          end\n        end\n        purchase.build_upsell_purchase(\n          upsell:,\n          selected_product: Link.find_by_external_id(params[:accepted_offer][:original_product_id]),\n          upsell_variant: params[:accepted_offer][:original_variant_id].present? ?\n            upsell.upsell_variants.alive.find_by(\n              selected_variant: BaseVariant.find_by_external_id(params[:accepted_offer][:original_variant_id])\n            ) :\n            nil\n        )\n        raise Purchase::PurchaseInvalid, purchase.upsell_purchase.errors.first.message unless purchase.upsell_purchase.valid?\n      end\n\n      if params[:tip_cents].present? && params[:tip_cents] > 0\n        raise Purchase::PurchaseInvalid, \"Tip is not allowed for this product\" unless purchase.seller.tipping_enabled? && product.not_is_tiered_membership?\n\n        raise Purchase::PurchaseInvalid, \"Tip is too large for this purchase\" if (purchase_params[:perceived_price_cents].ceil - params[:tip_cents].floor) < purchase.minimum_paid_price_cents\n\n        purchase.build_tip(value_cents: params[:tip_cents], value_usd_cents: get_usd_cents(product.price_currency_type, params[:tip_cents]))\n      end\n\n      validate_bundle_products\n\n      purchase.prepare_for_charge!(locked_rate: buyer_currency_quote_rate_hint(purchase))\n\n      purchase.build_purchase_wallet_type(wallet_type: params[:wallet_type]) if params[:wallet_type].present?\n\n      payment_flow_attributes = PurchasePaymentFlow.attributes_for_checkout_params(params)\n      # The purchase may still be unsaved here: `prepare_for_charge!` leaves it\n      # unpersisted when a validation fails (for example, an invalid email).\n      # Creating the dependent analytics row on an unsaved parent raises\n      # ActiveRecord::RecordNotSaved, and a purchase that never saved has no\n      # payment flow worth recording anyway.\n      if payment_flow_attributes && purchase.persisted? && !purchase.free_purchase?\n        begin","sourceCodeStart":128,"sourceCodeEnd":164,"githubUrl":"https://github.com/antiwork/gumroad/blob/afeacbd394069a1cbf0c6c50ee8e900925050370/app/services/purchase/create_service.rb#L128-L164","documentation":"Raised when the tip would push the payment below the product's floor: perceived_price_cents.ceil minus tip_cents.floor is less than purchase.minimum_paid_price_cents. On pay-what-you-want products minimum_paid_price_cents is the seller's minimum price, so the amount entered must cover both the product minimum and the tip.","triggerScenarios":"Any submission where (purchase_params[:perceived_price_cents].ceil - params[:tip_cents].floor) < purchase.minimum_paid_price_cents - e.g. a PWYW product with a $1 minimum, buyer enters $1.00 total and adds a $0.50 tip: 100 - 50 = 50 < 100.","commonSituations":"Pay-what-you-want checkouts with generous tips; clients computing perceived_price_cents in dollars instead of cents; rounding drift between client and server (the server rounds the price up and the tip down).","solutions":["Lower the tip so that price minus tip stays at or above the product minimum.","Increase the total amount entered so it absorbs both the minimum and the tip.","Integrators: enforce tip_cents <= perceived_price_cents - minimum_paid_price_cents before enabling submit."],"exampleFix":"# before: tip accepted without bound (total 500c, minimum 100c, tip 450c -> 50 < 100)\nparams[:tip_cents] = 450\n\n# after: clamp the tip to what the entered price can cover\nmax_tip = purchase_params[:perceived_price_cents].ceil - minimum_paid_price_cents\nparams[:tip_cents] = [params[:tip_cents].to_i, max_tip].min","handlingStrategy":"validation","validationCode":"tip = params[:tip_cents].to_i.floor\nprice = purchase_params[:perceived_price_cents].to_i.ceil\nparams[:tip_cents] = 0 if tip > 0 && (price - tip) < minimum_paid_price_cents","typeGuard":"def tip_within_bounds?(perceived_price_cents, tip_cents, minimum_paid_price_cents)\n  (perceived_price_cents.to_i.ceil - tip_cents.to_i.floor) >= minimum_paid_price_cents\nend","tryCatchPattern":"begin\n  Purchase::CreateService.new(product:, params:, buyer:).perform\nrescue Purchase::PurchaseInvalid => e\n  # ask the buyer to lower the tip or raise the entered amount; do not auto-retry\n  render_checkout_error(e.message) if e.message == 'Tip is too large for this purchase'\nend","preventionTips":["Cap the tip input at perceived price minus the product minimum in the UI.","Compute every amount in integer cents; never send dollars.","Remember the server rounds the price up (.ceil) and the tip down (.floor) - leave a small buffer for rounding."],"tags":["tipping","pay-what-you-want","price-validation","checkout","gumroad"],"backgroundTag":"tip-exceeds-minimum-price","analyzedSha":"afeacbd394069a1cbf0c6c50ee8e900925050370","analyzedAt":"2026-08-21T17:58:52.159Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}