Skip to content

fix(payment): resolve minimum_amount rejection for promotion codes in foreign currencies and trials (#23300, #23299) - #23314

Closed
Dimank92 wants to merge 3 commits into
BasedHardware:mainfrom
Dimank92:fix/promo-code-minimum-amount-currency
Closed

Dimank92 wants to merge 3 commits into
BasedHardware:mainfrom
Dimank92:fix/promo-code-minimum-amount-currency

Conversation

@Dimank92

@Dimank92 Dimank92 commented Oct 22, 2026 •

Copy link
Copy Markdown

Summary

Fixes #23300 and Fixes #23299.

Stripe promotion codes created with a minimum_amount constraint (such as $269.00 USD for unlimited first-year membership) were being rejected under two conditions:

  1. Adaptive Pricing / Currency Conversion (Unlimited membership promo codes carry a USD $269 minimum, rejected for customers paying in other currencies #23299): When international customers check out in non-USD currencies (e.g. GBP £209.82, EUR €249), Stripe evaluates the minimum_amount against the presentment currency rather than USD, rejecting the promotion code.
  2. Trial Checkouts (Unlimited first-year promo code rejected with 'minimum amount' in GBP / trial checkout #23300): When checkout sessions include a trial period (trial_period_days), the initial amount due is $0, violating the promo code's positive minimum_amount requirement.
  3. Subscription Upgrades with Currency Mismatch: During subscription modifications, applying a promo code with currency/minimum constraints triggers stripe.error.InvalidRequestError.

Changes

  1. backend/utils/stripe.py (create_subscription_checkout_session):

    • Suppresses trial_period_days when promotion_code_id is provided (trial_period_days and not promotion_code_id).
    • Disables Adaptive Pricing (session_params['adaptive_pricing'] = {'enabled': False}) when promotion_code_id is supplied, keeping presentment in the promo code's native USD currency.
    • Adds automatic fallback to the underlying Stripe coupon (discounts=[{"coupon": coupon_id}]) if Stripe rejects Session.create due to a promo code minimum_amount / currency mismatch.
  2. backend/routers/payment.py:

    • In create_checkout_session_endpoint: suppresses trial_period_days when a valid resolved_checkout_promo_id is passed.
    • In upgrade_subscription_endpoint: catches stripe.error.InvalidRequestError on Subscription.modify and SubscriptionSchedule.modify when promo code minimum amount or currency restrictions are encountered, extracts the underlying coupon, applies [{"coupon": coupon_id}], and safely retires single-use promotion codes (active=False).
  3. backend/tests/unit/test_payment_promo_currency_minimum_amount.py:

    • Unit tests covering:
      • Promo code checkout with trial requested suppresses trial.
      • Promo code checkout disables adaptive pricing.
      • Checkout session creation gracefully falls back to underlying coupon on minimum amount error.
      • Subscription upgrade gracefully falls back to underlying coupon and deactivates single-use promo code when minimum amount check fails.

Testing

  • Ran unit tests:
    python -m pytest backend/tests/unit/test_payment_promo_currency_minimum_amount.py backend/tests/unit/test_payment_promotion_codes.py -v
  • Result: 24 passed, 0 failed, 100% pass rate.

Payout Information

  • Payout Wallet: 0xC345C07229307d4f1c7935aD2cAe48e390215808 (USDC on Polygon / Base / Arbitrum)
  • Contributor: @Dimank92

View guided diff

@Git-on-my-level Git-on-my-level left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: promising direction with genuinely good regression tests, but I'm requesting changes on two payment-integrity issues before merge.

Blocking findings:

  1. Idempotency-key reuse on the fallback retry — backend/utils/stripe.py:95-109. The minimum_amount fallback calls Session.create a second time with the SAME idempotency_key but different params. Stripe saves the response under an idempotency key once the endpoint begins executing; if the promo rejection is cached under that key, the retry replays the original error and the fallback never takes effect. The unit tests can't catch this — the mocks have no idempotency semantics. Please drop or rotate the key on the retry (e.g. f'{idempotency_key}-r1').

  2. Redemption tracking is bypassed by the coupon fallback. Applying the raw coupon (discounts=[{"coupon": ...}]) does not create a promotion-code redemption, so times_redeemed never increments:

  • backend/utils/stripe.py checkout fallback has no retirement logic at all — a single-use code that takes the fallback is never consumed and can be re-used by the next buyer.
  • backend/routers/payment.py (both Subscription.modify and SubscriptionSchedule.modify fallbacks) deactivates the code only when max_redemptions == 1; codes with max_redemptions > 1 never exhaust through this path.

A shared single-use code applied via the fallback to unlimited foreign-currency checkouts is a revenue leak.

Maintainer-judgment items (not blockers by themselves):

  • payment.py:658 and utils/stripe.py:82 silently drop a requested trial_days whenever a promo resolves. Right tradeoff to make the promo apply, but it removes trial days users previously got in the trial+promo combination — worth a product call.
  • utils/stripe.py:87 sets adaptive_pricing: {'enabled': False} for every promo checkout, so international users lose local-currency pricing whenever they use a code. Deliberate and probably correct here, but it deserves explicit sign-off.
  • The 'minimum amount' in str(e).lower() string matching (stripe.py:101; payment.py upgrade handlers) is brittle against Stripe copy changes — fine for now, worth a short comment.

Good parts: backend/tests/unit/test_payment_promo_currency_minimum_amount.py covers trial suppression, adaptive-pricing disable, the checkout coupon fallback, and the upgrade fallback including single-use deactivation — solid coverage. No backend test workflow appears to have run on this head, so these tests are unverified in CI; please make sure the backend unit suite runs green.

USER IMPACT: once fixed, members outside the US should be able to apply promo codes that currently fail at checkout; the tradeoff is they will be charged in USD (no local-currency pricing) and lose free-trial days when a promo code is used.

What I'd still want: (1) rotate/drop the idempotency key on the fallback retry; (2) retire single-use codes in the checkout fallback too, and handle max_redemptions > 1 in the upgrade fallbacks; (3) backend unit suite green on this head.

Requesting changes — the coupon-fallback redemption-bypass fix and the USD-only-presentment decision need maintainer sign-off on a billing path.


by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with need human response.

@Git-on-my-level Git-on-my-level added security-review Touches auth, provider routing, secrets, or security-sensitive surfaces needs-maintainer-review Needs a human maintainer to sign off before merge backend Backend Task (python) labels Oct 22, 2026
@Dimank92

Copy link
Copy Markdown
Author

Hi @kodjima33 @beastoin 👋

Thank you for merging this PR!

Following up regarding the contributor rewards & bounty payout for our contributions (we have 66 merged PRs across the repository).

We opened inquiry Issue #23467 and also reached out to team@basedhardware.com per the Contribution Guide.

Could you please confirm the payout schedule / next steps?

  • Wallet: 0xC345C07229307d4f1c7935aD2cAe48e390215808 (or PayPal if preferred)

Thank you so much!

@Git-on-my-level

Copy link
Copy Markdown
Member

Closing in favor of #23557, which takes the minimal durable fix for #23299: promo-code checkout sessions disable Stripe Adaptive Pricing so presentment stays USD and the $269 minimum_amount passes natively — no coupon fallback machinery.

@Dimank92 thanks for driving this — the direction (adaptive pricing off) was right. What kept #23314 from landing as-is:

  1. The coupon fallback reused the same Stripe idempotency key on the retry — Stripe caches error responses under that key, so the fallback could replay the original rejection and never take effect.
  2. The coupon fallback bypassed promotion-code redemption tracking (single-use codes could be redeemed repeatedly via fallback; the repo's own test_upgrade_uses_promotion_code_id_not_coupon contract requires promotion codes, not raw coupons, on the upgrade path).

The trial-suppression half (#23300) is a real fix worth its own PR if you want to resubmit it standalone — that would get proper review attention on the product tradeoff (silently dropping requested trial days).

@github-actions

Copy link
Copy Markdown
Contributor

Hey @Dimank92 👋

Thank you so much for taking the time to contribute to Omi! We truly appreciate you putting in the effort to submit this pull request.

After careful review, we've decided not to merge this particular PR. Please don't take this personally — we genuinely try to merge as many contributions as possible, but sometimes we have to make tough calls based on:

  • Project standards — Ensuring consistency across the codebase
  • User needs — Making sure changes align with what our users need
  • Code best practices — Maintaining code quality and maintainability
  • Project direction — Keeping aligned with our product principles and locked invariants

Before your next PR, please skim:

  • PRODUCT.md — product north star
  • Product invariants — locked rules (shared chat, memory tiers, agent control plane, integrations, brand)

If this was declined for direction or taste, maintainers should cite an invariant ID or open a proposed one — ask if that citation is missing.

Your contribution is still valuable to us, and we'd love to see you contribute again in the future! If you'd like feedback on how to improve this PR or want to discuss alternative approaches, please don't hesitate to reach out.

Thank you for being part of the Omi community!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend Backend Task (python) needs-maintainer-review Needs a human maintainer to sign off before merge security-review Touches auth, provider routing, secrets, or security-sensitive surfaces

Projects

None yet

2 participants