Skip to content

Commit 7fbc4dc

Browse files
fix(payment): pin promo-code checkout presentment to USD so the $269 minimum passes (#23299) (#23557)
## Summary Disable Stripe Adaptive Pricing on checkout sessions created with a resolved promotion code, pinning presentment to the price's native USD currency. Stripe evaluates a promotion code's `minimum_amount` restriction against the checkout session's presentment currency. With Adaptive Pricing enabled, a EUR customer's converted total (e.g. €249 for the $269 Unlimited yearly price) falls below the USD-authored $269 minimum on the Unlimited yearly activation codes, and redemption fails with "code is valid but order total is below the minimum amount" even though the code is valid. With presentment pinned to USD, the minimum passes and the 100%-off first-year coupon applies for any customer. Scope note: minimal durable fix by design. It does not add a coupon fallback — the upgrade path is repo-contracted to apply promotion codes, never raw coupons, to preserve Stripe-side restrictions and redemption tracking — and it does not change trial handling (#23300 tracks that separately). Fixes #23299 ## Failure-Class Failure-Class: none ## Testing - New `backend/tests/unit/test_payment_promo_adaptive_pricing.py`: promo sessions set `adaptive_pricing={'enabled': False}`; promo-free sessions keep Stripe's default; static contract scopes the pin to the promo branch. - Existing `backend/tests/unit/test_payment_promotion_codes.py` stays green — its no-coupon contract on checkout/upgrade is untouched. - Both suites run locally green on this head via the worktree venv; backend CI re-runs them on this PR.
2 parents a6f9d31 + 306a426 commit 7fbc4dc

2 files changed

Lines changed: 78 additions & 0 deletions

File tree

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
"""Promotion-code checkouts pin presentment currency (issue #23299).
2+
3+
Stripe evaluates a promotion code's ``minimum_amount`` restriction against the
4+
checkout session's presentment currency. With Adaptive Pricing enabled, the
5+
converted total for a foreign-currency customer (e.g. EUR 249 for the $269
6+
Unlimited yearly price) falls below the USD-authored minimum and a valid
7+
activation code is rejected with "order total is below the minimum amount".
8+
9+
Sessions created with a resolved promotion code must disable Adaptive Pricing
10+
so the minimum is evaluated against the price's native USD amount. Sessions
11+
without a promotion code keep Stripe's Adaptive Pricing default.
12+
"""
13+
14+
import os
15+
import sys
16+
from pathlib import Path
17+
from types import SimpleNamespace
18+
from unittest.mock import patch
19+
20+
os.environ.setdefault("BASE_API_URL", "http://localhost:8080")
21+
22+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
23+
24+
import utils.stripe as stripe_utils # noqa: E402
25+
26+
STRIPE_UTILS_SOURCE = Path(__file__).resolve().parents[2] / "utils" / "stripe.py"
27+
28+
29+
def _captured_session_params(promotion_code_id):
30+
captured = {}
31+
32+
def fake_session_create(**params):
33+
captured.update(params)
34+
return SimpleNamespace(url='https://checkout.stripe.com/c/pay/cs_test', id='cs_test')
35+
36+
with patch('utils.stripe.stripe.checkout.Session.create', side_effect=fake_session_create):
37+
stripe_utils.create_subscription_checkout_session(
38+
uid='uid_1',
39+
price_id='price_unlimited_yearly',
40+
idempotency_key='idem_1',
41+
promotion_code_id=promotion_code_id,
42+
)
43+
return captured
44+
45+
46+
def test_promo_checkout_disables_adaptive_pricing():
47+
"""A promo-code session pins presentment to the price's USD currency."""
48+
params = _captured_session_params(promotion_code_id='promo_123')
49+
50+
assert params['discounts'] == [{'promotion_code': 'promo_123'}]
51+
assert params['adaptive_pricing'] == {'enabled': False}
52+
53+
54+
def test_promo_free_checkout_keeps_adaptive_pricing_default():
55+
"""Without a promo code the session must not set adaptive_pricing, so
56+
Stripe's default (Adaptive Pricing where enabled) still applies."""
57+
params = _captured_session_params(promotion_code_id=None)
58+
59+
assert params['allow_promotion_codes'] is True
60+
assert 'adaptive_pricing' not in params
61+
assert 'discounts' not in params
62+
63+
64+
def test_adaptive_pricing_pin_is_scoped_to_promo_sessions():
65+
"""Static contract: the pin lives inside the promotion_code_id branch, so a
66+
refactor can never silently widen it to promo-free checkouts."""
67+
source = STRIPE_UTILS_SOURCE.read_text(encoding="utf-8")
68+
69+
branch_start = source.index("if promotion_code_id:")
70+
branch_end = source.index("session_params['allow_promotion_codes'] = True", branch_start)
71+
promo_branch = source[branch_start:branch_end]
72+
73+
assert "session_params['adaptive_pricing'] = {'enabled': False}" in promo_branch

‎backend/utils/stripe.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@ def create_subscription_checkout_session(
8484

8585
if promotion_code_id:
8686
session_params['discounts'] = [{'promotion_code': promotion_code_id}]
87+
# Pin presentment to the price's currency. Stripe evaluates a promotion
88+
# code's minimum_amount restriction against the checkout's presentment
89+
# currency, so Adaptive Pricing's converted total (e.g. EUR) can fall
90+
# below a USD-authored minimum and reject a valid code (issue #23299).
91+
session_params['adaptive_pricing'] = {'enabled': False}
8792
else:
8893
session_params['allow_promotion_codes'] = True
8994

0 commit comments

Comments
 (0)