Skip to content

Commit 6c45bf3

Browse files
iHiDclaude
andauthored
Validate donation amount to prevent Stripe Elements overflow error (#8490)
Large custom amounts (e.g. 10 quintillion) caused currency.js intValue to produce 1e+21 in scientific notation, which Stripe rejects. Add max validation in CustomAmountInput (client-side) and PaymentIntentsController (server-side) capped at Stripe's $999,999.99 limit. Closes #8484 Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 6538137 commit 6c45bf3

3 files changed

Lines changed: 48 additions & 1 deletion

File tree

app/controllers/api/payments/payment_intents_controller.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
class API::Payments::PaymentIntentsController < API::BaseController
22
before_action :authenticate_user!
33

4+
MAX_AMOUNT_IN_CENTS = 99_999_999
5+
46
def create
7+
amount_in_cents = params[:amount_in_cents].to_i
8+
unless amount_in_cents.between?(1, MAX_AMOUNT_IN_CENTS)
9+
return render json: { error: "Amount must be between 1 and #{MAX_AMOUNT_IN_CENTS} cents" }, status: :ok
10+
end
11+
512
payment_intent = ::Payments::Stripe::PaymentIntent::Create.(
613
current_user || params[:email],
714
params[:type],

app/javascript/components/donations/donation-form/CustomAmountInput.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import React, { useCallback } from 'react'
22
import currency from 'currency.js'
33

4+
const MAX_AMOUNT = '999999.99'
5+
46
export const CustomAmountInput = ({
57
onChange,
68
selected,
79
placeholder,
810
defaultValue,
911
value,
1012
min = '0',
13+
max = MAX_AMOUNT,
1114
className = '',
1215
onBlur,
1316
}: {
@@ -18,6 +21,7 @@ export const CustomAmountInput = ({
1821
defaultValue?: currency
1922
value?: currency | string
2023
min?: string
24+
max?: string
2125
className?: string
2226
}): JSX.Element => {
2327
const handleCustomAmountChange = useCallback(
@@ -34,9 +38,14 @@ export const CustomAmountInput = ({
3438
return
3539
}
3640

41+
if (parseFloat(e.target.value) > parseFloat(max)) {
42+
onChange(currency(NaN))
43+
return
44+
}
45+
3746
onChange(currency(e.target.value))
3847
},
39-
[onChange]
48+
[onChange, max]
4049
)
4150

4251
const classNames = [
@@ -51,6 +60,7 @@ export const CustomAmountInput = ({
5160
<input
5261
type="number"
5362
min={min}
63+
max={max}
5464
step="0.01"
5565
onBlur={onBlur}
5666
placeholder={placeholder}

test/controllers/api/payments/payment_intents_controller_test.rb

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,36 @@ class API::Payments::PaymentIntentsControllerTest < API::BaseTestCase
3535
)
3636
end
3737

38+
test "rejects amount over maximum" do
39+
setup_user
40+
post api_payments_payment_intents_path(
41+
type: 'payment', amount_in_cents: 100_000_000
42+
), headers: @headers, as: :json
43+
44+
assert_response :ok
45+
assert_includes JSON.parse(response.body)["error"], "Amount must be between"
46+
end
47+
48+
test "rejects zero amount" do
49+
setup_user
50+
post api_payments_payment_intents_path(
51+
type: 'payment', amount_in_cents: 0
52+
), headers: @headers, as: :json
53+
54+
assert_response :ok
55+
assert_includes JSON.parse(response.body)["error"], "Amount must be between"
56+
end
57+
58+
test "rejects negative amount" do
59+
setup_user
60+
post api_payments_payment_intents_path(
61+
type: 'payment', amount_in_cents: -100
62+
), headers: @headers, as: :json
63+
64+
assert_response :ok
65+
assert_includes JSON.parse(response.body)["error"], "Amount must be between"
66+
end
67+
3868
test "returns an error if raised" do
3969
error = "oh dear!!"
4070
::Payments::Stripe::PaymentIntent::Create.expects(:call).raises(Stripe::InvalidRequestError.new(error, nil))

0 commit comments

Comments
 (0)