Skip to content

fix(payments): P1 — duplicate stuck payments (no idempotency key + dedup blind to pending 3DS) #4438

Description

@koala73

Summary

Nothing stops a customer from spawning a fresh payment on every retry. The Dodo dashboard shows the same customer with 4-5 payments all stuck in "Requires customer action" (e.g. one card with 4 attempts). This is the expected output of our current design, not bad luck.

Evidence

  • No idempotency key on checkoutSessions.create — grep across convex/, api/, src/services/ returns zero hits in the payments path. Every create = a new session = a new payment attempt.
  • The only dedup guard, getCheckoutBlockingSubscription (convex/payments/billing.ts:765-810), blocks only when a subscriptions row already exists (active/on_hold/cancelled-with-time). A payment stuck in 3DS has not fired subscription.active, so no subscription row exists → the guard returns null → checkout proceeds.
  • Frontend _checkoutInFlight (src/services/checkout.ts:726,740) only de-dupes double-clicks within one page session; it is released in finally immediately after open() returns.

Fix direction (medium)

Block (or warn before) re-checkout when a recent payment for the user is pending 3DS. Requires the pending-payment signal from the webhook fix (see the requires_customer_action webhook issue). Cheap interim mitigations: add an idempotency key per checkout intent, and/or confirm Dodo's provider-side "Allow Multiple Subscriptions" setting is off.

Test first

Seed a pending 3DS payment for a user and assert a second createCheckout is blocked (or surfaces the resume path) instead of creating a new session.


Part of the DodoPayments 3DS stuck-payments incident. Depends on the webhook pending-state fix.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1High priority, fix soonbugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions