Repository navigation
fix(payments): use the currency's minor unit for Stripe amounts - #859
Merged
evilguy4000 merged 1 commit intoNov 7, 2026
Merged
Conversation
Stripe takes amounts in the currency's smallest unit. Checkout and payment intents multiplied every amount by 100 and the webhook divided by 100, so a JPY invoice (the invoice currency comes from the free-text currency setting) was charged 100 times its balance, and three-decimal currencies such as KWD were charged a tenth. Convert with the currency's factor (1, 100 or 1000) in both directions.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What is wrong
Stripe takes amounts in the currency's smallest unit: cents for EUR or USD, but whole yen for JPY (and other zero-decimal currencies), and thousandths for three-decimal currencies such as KWD.
StripeIntegration.create_checkout_sessionandcreate_payment_intentmultiply every amount by 100, andStripeProvider.verify_webhookdivides every amount by 100.The invoice currency comes from the currency setting (
settings.currency, a free text field in the admin settings), so an instance set to JPY or KWD creates invoices in that currency. With such an invoice:unit_amountSo the invoice shows as paid while the customer was charged 100 times the balance (or a tenth of it for KWD).
What this changes
app/utils/stripe_integration.py:to_stripe_amount/from_stripe_amountconvert with the currency's factor (1 for zero-decimal, 1000 for three-decimal, 100 otherwise). Checkout sessions and payment intents useto_stripe_amount. Rounding is half-up instead of truncation, which makes no difference for two-decimal amounts. Invoice totals are stored with two decimals, so a JPY balance can still be fractional (e.g. hours × rate = 12,345.67); it is rounded half-up to whole yen (12,346), since Stripe cannot charge a fraction of a yen. The comment next to the currency sets also notes that ISK, UGX, HUF and TWD are left out on purpose (Stripe takes them in the two-decimal form).app/payments/stripe_provider.py: the webhook amount is converted withfrom_stripe_amountusing the event's currency.Unchanged: PayPal and Mollie, which take amounts in major units.
Tests
New tests in
tests/test_payment_gateways.py(Stripe calls mocked):developtest_stripe_checkout_amount_in_cents_for_two_decimal_currency(EUR 19.99 → 1999)test_stripe_checkout_amount_for_zero_decimal_currency(JPY 10000 → 10000)1000000 != 10000)test_stripe_checkout_rounds_fractional_zero_decimal_amount_half_up(JPY 12345.67 → 12346, 12345.49 → 12345)1234567 != 12346)test_stripe_checkout_amount_for_three_decimal_currency(KWD 12.50 → 12500)1250 != 12500)test_stripe_webhook_amount_for_two_decimal_currency(1999 EUR → 19.99)test_stripe_webhook_amount_for_zero_decimal_currency(10000 JPY → 10000)100 != 10000)test_stripe_webhook_amount_for_three_decimal_currency(12500 KWD → 12.5)125 != 12.5)test_stripe_payment_intent_amount_for_zero_decimal_currency(JPY 10000 → 10000)1000000 != 10000)test_payment_gateways.py,test_checkout_webhook.py,test_payment_routes.py,test_payment_model.py,test_payment_smoke.py,test_api_payments_v1.py): 65 passed with the change.developwithout it. The 5 failures are intest_webhook_service.py(4) andtest_time_format_display.py(1), outside this change.