Skip to content

fix(payments): use the currency's minor unit for Stripe amounts - #859

Merged
evilguy4000 merged 1 commit into
DRYTRIX:developfrom
5eBeKs:fix/stripe-zero-decimal-amounts
Nov 7, 2026
Merged

evilguy4000 merged 1 commit into
DRYTRIX:developfrom
5eBeKs:fix/stripe-zero-decimal-amounts

Conversation

@5eBeKs

@5eBeKs 5eBeKs commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

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_session and create_payment_intent multiply every amount by 100, and StripeProvider.verify_webhook divides 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:

invoice balance sent to Stripe as unit_amount customer is charged recorded by the webhook
10,000 JPY 1,000,000 1,000,000 JPY 10,000.00 (looks right, because the division undoes the multiplication)
12.50 KWD 1,250 1.250 KWD 12.50

So 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_amount convert with the currency's factor (1 for zero-decimal, 1000 for three-decimal, 100 otherwise). Checkout sessions and payment intents use to_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 with from_stripe_amount using 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):

test develop this change
test_stripe_checkout_amount_in_cents_for_two_decimal_currency (EUR 19.99 → 1999) passes passes
test_stripe_checkout_amount_for_zero_decimal_currency (JPY 10000 → 10000) fails (1000000 != 10000) passes
test_stripe_checkout_rounds_fractional_zero_decimal_amount_half_up (JPY 12345.67 → 12346, 12345.49 → 12345) fails (1234567 != 12346) passes
test_stripe_checkout_amount_for_three_decimal_currency (KWD 12.50 → 12500) fails (1250 != 12500) passes
test_stripe_webhook_amount_for_two_decimal_currency (1999 EUR → 19.99) passes passes
test_stripe_webhook_amount_for_zero_decimal_currency (10000 JPY → 10000) fails (100 != 10000) passes
test_stripe_webhook_amount_for_three_decimal_currency (12500 KWD → 12.5) fails (125 != 12.5) passes
test_stripe_payment_intent_amount_for_zero_decimal_currency (JPY 10000 → 10000) fails (1000000 != 10000) passes
  • Payment and checkout test files (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.
  • Full suite (Python 3.11, SQLite, no network): 2676 passed, 5 failed, 83 skipped, 1 xfailed with the change; 2668 passed, the same 5 failed, 83 skipped, 1 xfailed on develop without it. The 5 failures are in test_webhook_service.py (4) and test_time_format_display.py (1), outside this change.

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.
@evilguy4000
evilguy4000 merged commit 8d26983 into DRYTRIX:develop Nov 7, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants