Skip to content

fix(payments): pending-payment-aware checkout dedup guard (#4438)#4456

Merged
koala73 merged 11 commits into
mainfrom
feat/pending-payment-checkout-dedup
Jun 27, 2026
Merged

fix(payments): pending-payment-aware checkout dedup guard (#4438)#4456
koala73 merged 11 commits into
mainfrom
feat/pending-payment-checkout-dedup

Conversation

@koala73

@koala73 koala73 commented Jun 27, 2026

Copy link
Copy Markdown
Owner

What & why

Closes #4438 (epic #4440). The original 3DS "stuck payments" incident let a customer stack 4–5 duplicate payments all in "Requires customer action": the failure-retry path creates a brand-new Dodo checkout each time, and the existing dedup guard (getCheckoutBlockingSubscription) only inspects the subscriptions table — which a pending 3DS payment never created. The redirect-mode fix (#4454) addressed the hang but left the stacking open.

This extends checkout dedup to also block a new checkout when the user has a recent pending payment in the same tier group, surfacing a confirm-to-proceed dialog (the same UX family as the duplicate-subscription block) with an explicit escape hatch. Pro and API are different tier groups, so a pending Pro payment never blocks an API checkout.

How it works

The pending signal already lives in paymentEvents (status processing/requires_customer_action, persisted by #4436/#4446). The one missing piece was the tier group of a pending payment — threaded through the existing checkout-metadata bridge:

wm_plan_key set at session create  →  echoed on the payment.processing webhook
  →  persisted on the paymentEvents row  →  read by the guard on the NEXT checkout

Implementation (4 units, test-first)

  • U1 — data plumbing. metadata.wm_plan_key at session create (resolveProductToPlan); optional planKey on paymentEvents; webhook persists it. Backward-compatible — legacy in-flight sessions simply carry no planKey.
  • U2 — guard + enforcement. getBlockingPendingPayment (15-min staleness window, tier-group scoped, fails open when planKey is unresolvable). Enforced in both checkout actions after the subscription guard (which still wins), skippable via bypassPendingGuard. Bypass + the pendingPayment block context are threaded through the relay route and edge gateway.
  • U3 — taxonomy. payment_in_progress error code (non-retryable — recovery is a dialog confirmation, not a network retry), mapping the backend PAYMENT_IN_PROGRESS 409.
  • U4 — dialog. "Payment in progress — start a new checkout?" on both the dashboard (services-layer dialog) and /pro (inline DOM dialog, separate build). Confirm re-invokes with bypassPendingGuard: true; cancel is inert; the attempt is preserved (recoverable).

Tier-group acceptance matrix (the reviewer's case is pinned)

Pending payment New checkout Result
Pro (within 15m) Pro blocked
Pro API NOT blocked (different tier group)
Pro (older than 15m) Pro NOT blocked
api_starter api_starter_annual blocked (monthly/annual parity)
pending and active sub (same tier) same tier subscription guard wins
any, with bypassPendingGuard:true same tier NOT blocked (override)
pending row with no planKey any NOT blocked (fails open)

Design decisions

  • Fail open, never false-block. A pending row whose tier group is unresolvable never blocks — a false block (locking out a paying user) is worse than a missed dedup (which the dialog-on-next-attempt and the future reconciliation cron mitigate).
  • Friction, not a lock. bypassPendingGuard keeps the block as confirmation friction. The provider-side backstop (Dodo "Allow Multiple Subscriptions" + the subscription guard) is unchanged.
  • No raw server text / payment IDs reach the client — only the whitelisted plan display name and occurredAt.

Tests

  • convex/__tests__/billing.test.ts — the full guard matrix (tier-group scoping, staleness, status filter, fail-open, most-recent, monthly/annual parity).
  • convex/__tests__/checkout.test.ts — action enforcement (PAYMENT_IN_PROGRESS block, subscription precedence, bypass, cross-tier).
  • convex/__tests__/webhook.test.tsplanKey persistence + backward-compat.
  • tests/checkout-error-classification.test.mts — the new taxonomy code.
  • tests/checkout-pending-dialog.test.mts — dashboard dialog wiring (show / confirm-with-bypass / cancel), end-to-end through the real taxonomy.

npm run test:convex, npm run test:data, tsc (src + api + convex + pro-test) all green.

Out of scope (follow-ups)

🤖 Generated with Claude Code

https://claude.ai/code/session_01C2WR72ZeDg1vBQQpcVzaYi

koala73 added 6 commits June 27, 2026 08:02
…t planKey on pending payment rows (#4438)

Adds metadata.wm_plan_key at checkout-session create (resolveProductToPlan)
and persists it on the paymentEvents row from data.metadata.wm_plan_key, so a
pending 3DS payment can be resolved to its PRODUCT_CATALOG tierGroup. Optional/
backward-compatible: legacy in-flight sessions simply carry no planKey.

Claude-Session: https://claude.ai/code/session_01C2WR72ZeDg1vBQQpcVzaYi
…g payment (#4438)

Adds getBlockingPendingPayment (15-min staleness window, tier-group scoped,
fails open when planKey unresolvable) and enforces it in both checkout actions
after the subscription guard, skippable via bypassPendingGuard. Threads the
bypass + forwards the pendingPayment block context through the relay route and
edge gateway. A pending Pro payment never blocks an API checkout (different
tier group); the subscription guard still wins when both apply.

Claude-Session: https://claude.ai/code/session_01C2WR72ZeDg1vBQQpcVzaYi
…xonomy (#4438)

Maps the backend PAYMENT_IN_PROGRESS 409 block to a typed, user-safe
payment_in_progress code (non-retryable — recovery is a dialog confirmation,
not a network retry), parallel to duplicate_subscription.

Claude-Session: https://claude.ai/code/session_01C2WR72ZeDg1vBQQpcVzaYi
…4438)

On a PAYMENT_IN_PROGRESS 409, both the dashboard and /pro surfaces now show a
'payment in progress — start a new checkout?' dialog instead of navigating.
Confirm re-invokes startCheckout with bypassPendingGuard:true (skips the guard,
proceeds to the hosted redirect); cancel is inert. Dashboard dialog mirrors
checkout-duplicate-dialog (services layer); /pro uses an inline DOM dialog
(separate build, no shared components). The attempt is preserved (recoverable).

Claude-Session: https://claude.ai/code/session_01C2WR72ZeDg1vBQQpcVzaYi
)

pro-test/ source changed (U4 /pro dialog); public/pro/ is the committed bundle
Vercel ships (it does not rebuild pro-test on deploy), so the rebuild must land
with the source change. Enforced by .husky/pre-push.

Claude-Session: https://claude.ai/code/session_01C2WR72ZeDg1vBQQpcVzaYi
@mintlify

mintlify Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
WorldMonitor 🟢 Ready View Preview Jun 27, 2026, 4:30 AM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@vercel

vercel Bot commented Jun 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
worldmonitor Ready Ready Preview, Comment Jun 27, 2026 8:21am

Request Review

@greptile-apps

greptile-apps Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR extends the checkout dedup guard to also block new checkouts when a recent same-tier pending 3DS/SCA payment exists, addressing a gap that allowed customers to stack duplicate "Requires customer action" payments. The block surfaces as a confirm-to-proceed dialog (soft friction with a bypass), and the plan key is threaded through checkout-session metadata so the webhook can persist tier-group information on the paymentEvents row.

  • U1 (data plumbing): wm_plan_key added to Dodo checkout-session metadata; planKey persisted on new paymentEvents rows; schema migration is additive and backward-compatible.
  • U2 (guard): getBlockingPendingPayment (15-min staleness window, tier-group scoped, fails open) enforced after the subscription guard in both createCheckout and internalCreateCheckout; bypassPendingGuard flag threads through client → edge → relay → Convex.
  • U3/U4 (taxonomy + dialog): payment_in_progress error code added to the client taxonomy; confirmation dialogs implemented for both the dashboard (service-layer, DOM API) and /pro (inline, separate build).

Confidence Score: 4/5

The core dedup logic is correct — tier-group scoping, fail-open, bypass, and test coverage are all solid. The main risk is edge-case messaging: stale processing rows can show a "Payment in progress" dialog after a payment has already succeeded, though in practice the subscription guard fires first and the bypass dialog always allows recovery.

The guard, metadata bridge, schema change, and dialog wiring are all correct and well-tested. The only substantive concern is that getBlockingPendingPayment never cross-checks whether a terminal-status row (succeeded/failed) already exists for the same dodoPaymentId, which can cause misleading dialog messaging in a narrow timing window. The accessibility gaps in the pro-test dialog and the missing pendingPayment field on CheckoutErrorBody are straightforward clean-ups.

convex/payments/billing.ts — the guard query and the stale-row edge case; pro-test/src/services/checkout.ts — accessibility attributes on the inline dialog.

Important Files Changed

Filename Overview
convex/payments/billing.ts Adds getBlockingPendingPayment internalQuery; logic is correct, but uses .collect() + in-memory JS filter over all user payment events rather than a range-bounded index query.
convex/payments/checkout.ts Guard correctly chained after subscription check in both public and internal actions; bypassPendingGuard is properly threaded; wm_plan_key metadata added at session create.
convex/payments/subscriptionHelpers.ts Persists planKey from data.metadata?.wm_plan_key on new paymentEvent rows; backward-compatible (undefined for legacy rows).
convex/schema.ts Adds optional planKey field to paymentEvents table; additive, backward-compatible change.
src/services/checkout-pending-dialog.ts New service-layer dialog with proper accessibility (aria-labelledby, backdrop-click dismiss, Escape with stopPropagation, focus management via rAF); idempotent; no raw server text exposed.
src/services/checkout-errors.ts Adds payment_in_progress to the error taxonomy with retryable: false and correct copy; CheckoutErrorBody not updated to include pendingPayment (callers cast instead).
src/services/checkout.ts Correctly handles payment_in_progress 409 in the fetch error path; confirm re-invokes startCheckout with bypassPendingGuard: true; attempt is not cleared (recoverable).
pro-test/src/services/checkout.ts Adds showProPendingPaymentDialog and the 409 PAYMENT_IN_PROGRESS handler; dialog is missing aria-labelledby (unlike the main-app counterpart); otherwise correctly mirrors the dashboard flow.
convex/http.ts HTTP relay updated to forward pendingPayment alongside subscription on 409 responses; both blocked shapes correctly discriminated by the error field.
api/create-checkout.ts Edge gateway threads bypassPendingGuard from request body and forwards pendingPayment from relay response on 409; change is minimal and correct.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant U as User
    participant C as Client (checkout.ts)
    participant E as Edge (/api/create-checkout)
    participant R as Relay (convex/http.ts)
    participant A as internalCreateCheckout
    participant SG as Subscription Guard
    participant PG as Pending Guard (NEW)
    participant D as Dodo Payments

    U->>C: startCheckout(productId)
    C->>E: POST /api/create-checkout
    E->>R: relay to Convex
    R->>A: internalCreateCheckout(userId, productId)
    A->>SG: getCheckoutBlockingSubscription
    alt Active subscription in same tier
        SG-->>A: blocking sub
        A-->>C: 409 ACTIVE_SUBSCRIPTION_EXISTS
        C->>U: showDuplicateSubscriptionDialog
    else No active subscription
        SG-->>A: null
        A->>PG: getBlockingPendingPayment
        alt Recent pending 3DS, same tier, no bypass
            PG-->>A: blocking pending payment
            A-->>C: 409 PAYMENT_IN_PROGRESS
            C->>U: showCheckoutPendingDialog
            U->>C: onConfirm
            C->>E: "POST bypassPendingGuard=true"
            Note over E,D: Guard skipped
        else No blocking pending payment
            PG-->>A: null
            A->>D: create session + wm_plan_key
            D-->>C: checkout_url
            C->>U: navigate to Dodo hosted checkout
        end
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant U as User
    participant C as Client (checkout.ts)
    participant E as Edge (/api/create-checkout)
    participant R as Relay (convex/http.ts)
    participant A as internalCreateCheckout
    participant SG as Subscription Guard
    participant PG as Pending Guard (NEW)
    participant D as Dodo Payments

    U->>C: startCheckout(productId)
    C->>E: POST /api/create-checkout
    E->>R: relay to Convex
    R->>A: internalCreateCheckout(userId, productId)
    A->>SG: getCheckoutBlockingSubscription
    alt Active subscription in same tier
        SG-->>A: blocking sub
        A-->>C: 409 ACTIVE_SUBSCRIPTION_EXISTS
        C->>U: showDuplicateSubscriptionDialog
    else No active subscription
        SG-->>A: null
        A->>PG: getBlockingPendingPayment
        alt Recent pending 3DS, same tier, no bypass
            PG-->>A: blocking pending payment
            A-->>C: 409 PAYMENT_IN_PROGRESS
            C->>U: showCheckoutPendingDialog
            U->>C: onConfirm
            C->>E: "POST bypassPendingGuard=true"
            Note over E,D: Guard skipped
        else No blocking pending payment
            PG-->>A: null
            A->>D: create session + wm_plan_key
            D-->>C: checkout_url
            C->>U: navigate to Dodo hosted checkout
        end
    end
Loading

Comments Outside Diff (3)

  1. convex/payments/billing.ts, line 553-569 (link)

    P2 All user payment events loaded before time-window filter

    The query collects every paymentEvents row for the user and then applies the occurredAt > windowStart cut-off in JS. For a user with an extended payment history, this reads every historical row on every checkout attempt. A compound index ["userId", "occurredAt"] would let Convex skip old rows at the storage layer.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

  2. convex/payments/billing.ts, line 553-871 (link)

    P2 processing rows persist as a blocking signal after the payment has transitioned to succeeded

    Each webhook event inserts a new paymentEvents row rather than patching the existing one (see subscriptionHelpers.ts:975). After a payment completes normally — payment.processingpayment.succeededsubscription.active — the original row with status = 'processing' remains in the database. Because getBlockingPendingPayment only checks a row's own status, that stale row keeps the guard live for up to 15 minutes.

    In the common subscription path the subscription guard fires first and wins, so the user never reaches the pending guard. But in the narrow window between payment.succeeded landing and subscription.active being processed, a customer who retries sees "Payment in progress" even though their payment succeeded. The bypass dialog still lets them proceed, but the message is misleading. A cross-check against a terminal-status row for the same dodoPaymentId would suppress the false trigger.

  3. src/services/checkout-errors.ts, line 61-66 (link)

    P2 CheckoutErrorBody doesn't declare the new pendingPayment field

    Both src/services/checkout.ts and pro-test/src/services/checkout.ts access body.pendingPayment via a type-intersection cast rather than the interface. Declaring the field directly makes the structural contract explicit and removes the need for callers to carry the cast.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Reviews (1): Last reviewed commit: "build(pro): rebuild public/pro for the p..." | Re-trigger Greptile

Comment thread pro-test/src/services/checkout.ts
Comment thread pro-test/src/services/checkout.ts Outdated
…honest dialog copy (#4438)

Addresses adversarial review of PR #4456:

- BLOCKING: paymentEvents is append-only, so a 3DS payment that went
  processing -> failed/succeeded left its pending row behind and the guard
  falsely blocked the retry path for the whole 15-min window. Now dedup by
  dodoPaymentId: a payment only blocks if it has NO terminal (non-pending)
  charge row. Tests for processing+failed / processing+succeeded coexistence.
- Bounded read: query the new by_userId_occurredAt index with a range on
  occurredAt instead of collecting the user's whole (unbounded, rawPayload-
  carrying) history — keeps the guard fail-open, not fail-closed.
- Dialog copy: drop the unenforceable 'will not charge you twice' guarantee;
  offer honest refund recourse instead (both dashboard + /pro).

Claude-Session: https://claude.ai/code/session_01C2WR72ZeDg1vBQQpcVzaYi
@koala73

koala73 commented Jun 27, 2026

Copy link
Copy Markdown
Owner Author

Thanks for the adversarial pass — the BLOCKING finding was a real bug. Addressed in 7d6ba08:

✅ Finding 1 (BLOCKING) — stale pending rows from terminal payments — fixed

You were exactly right: paymentEvents is append-only, so processing → failed/succeeded leaves the pending row behind, and the guard blocked the failure-retry path for the whole window. getBlockingPendingPayment now dedups by dodoPaymentId: it builds the set of payment ids that have any terminal (non-pending) charge row and excludes them, so a payment only blocks when it has no terminal row. New tests cover the append-only coexistence your review pointed at:

  • processing + failed (same dodoPaymentId) → not blocked
  • processing + succeeded (same dodoPaymentId) → not blocked
  • genuinely-pending alongside an unrelated terminal payment → still blocked

✅ Finding 2 (SUGGESTION) — unbounded .collect() / fail-closed — fixed

Added a by_userId_occurredAt index and the guard now reads a time-bounded slice (occurredAt > windowStart) instead of the user's whole history. A terminal row always post-dates its own pending row, so it's still in-window and the dedup logic stays correct — and the read can no longer blow the transaction limit and reject the checkout (fail-open preserved).

✅ Finding 3 (SUGGESTION) — dialog overpromises — fixed

Dropped the "will not charge you twice for the same plan" guarantee on both surfaces; replaced with honest recourse: "if it does and you're charged twice, contact support and we'll refund the duplicate."

Finding 4 (NITPICK) — edge 409 default || 'ACTIVE_SUBSCRIPTION_EXISTS' — acknowledged, left as-is

Confirmed unreachable: the relay always sets error: result.code (convex/http.ts:1298), and both blocked shapes always carry code. Changing the default to a neutral value would route the impossible no-error case to invalid_product instead — no real improvement, so I left the historical default rather than churn it. Happy to add a one-line // relay always sets code comment if you'd prefer.

Finding 5 (NITPICK) — dialog test reimplements the unit — acknowledged

Agreed it stubs the dialog and drives a harness rather than the real startCheckout end-to-end (matches the repo's existing checkout-overlay-lifecycle.test.mts pattern, which stubs the duplicate dialog the same way). The contract — re-invoke with bypassPendingGuard, navigate on confirm, inert on cancel — is locked; left as-is for consistency.

Convex suite green (479), tsc clean (src + convex + pro-test), public/pro/ rebuilt.

…11y for /pro pending dialog (#4438)

Greptile review cleanups (its #1/#2 were already fixed in 7d6ba08):
- Declare subscription + pendingPayment on CheckoutErrorBody (the two 409
  block shapes sharing the route) and drop the type-intersection casts at both
  call sites; remove the now-unused CheckoutErrorBody import.
- Add aria-labelledby + titled heading to the /pro pending dialog, matching the
  dashboard counterpart.

Claude-Session: https://claude.ai/code/session_01C2WR72ZeDg1vBQQpcVzaYi
@koala73

koala73 commented Jun 27, 2026

Copy link
Copy Markdown
Owner Author

Thanks @greptileai — your pass landed on 2c39f3ca8, the commit just before the review fixes, so two of these were already addressed; the third + the a11y note are now done in 518a2b3.

#1 — unbounded .collect() before the time-window filter — already fixed in 7d6ba08

The guard now reads a bounded slice via a new by_userId_occurredAt index with q.eq("userId", …).gt("occurredAt", windowStart) (convex/payments/billing.ts), instead of collecting the whole history and filtering in JS. This also makes the read fail-open rather than risking a transaction-limit throw.

#2 — stale processing row blocks after the payment succeeded/failed — already fixed in 7d6ba08

Exactly the append-only hazard. getBlockingPendingPayment now dedups by dodoPaymentId: it builds the set of payment ids that have any terminal (non-pending) charge row in the window and excludes them, so a payment only blocks when it has no terminal row. Covered by three new tests (processing+failed, processing+succeeded, and genuinely-pending-alongside-unrelated-terminal).

#3CheckoutErrorBody doesn't declare pendingPaymentfixed in 518a2b3

Declared both 409 block shapes on the interface — subscription? and pendingPayment? — and dropped the type-intersection casts at both call sites in src/services/checkout.ts (removed the now-unused named import too). The interface now describes the real 409 body, discriminated by error.

✅ a11y — /pro pending dialog missing aria-labelledbyfixed in 518a2b3

Added aria-labelledby + an id on the heading to the /pro showProPendingPaymentDialog, matching the dashboard checkout-pending-dialog.ts.

tsc clean (src + convex + pro-test), convex suite green (479), public/pro/ rebuilt.

@greptile-apps

greptile-apps Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

All four items resolved — thanks for the detailed rundown. The append-only dedup via dodoPaymentId terminal-row exclusion is exactly the right approach for #2, and the CheckoutErrorBody interface cleanup is cleaner than the intersection casts.

This looks good to merge from my end.

koala73 added 3 commits June 27, 2026 10:19
…t log, neutral 409 fallback (#4438)

Multi-agent code review (11 reviewers) findings, all independently validated:
- P1 fail-open: wrap getCheckoutBlockingPendingPayment's runQuery in try/catch
  -> null. A Convex infra throw was propagating to relay 500 -> edge 502,
  fail-CLOSING the checkout against the guard's documented fail-open contract.
- P2 perf: run the subscription + pending guards via Promise.all (no shared
  data); subscription block still wins, bypassPendingGuard still skips the
  pending query. Saves a round-trip on every checkout.
- Observability: console.info audit trail when the pending guard is bypassed
  (the original incident was undetected stacked payments).
- P3: edge 409 fallback || 'ACTIVE_SUBSCRIPTION_EXISTS' -> ?? 'CHECKOUT_BLOCKED'
  so a missing block code can't misroute a PAYMENT_IN_PROGRESS to the wrong dialog.
- Tests: anchor the staleness offset to PENDING_PAYMENT_BLOCK_WINDOW_MS; add
  api_business cross-tier non-block + catalog-miss fail-open coverage.

Claude-Session: https://claude.ai/code/session_01C2WR72ZeDg1vBQQpcVzaYi
… dialogs (#4438 P2 review)

Maintainability finding (conf 100): checkout-pending-dialog.ts and
checkout-duplicate-dialog.ts shared ~130 lines of identical backdrop/card/
lifecycle DOM in the same src/services/ build. Extracted into
checkout-dialog-factory.ts (showCheckoutConfirmDialog); both dialogs are now
thin delegates passing only their id + copy + button labels. Public APIs
(showDuplicateSubscriptionDialog / showCheckoutPendingDialog) and option
interfaces unchanged — call sites and the stubbed dialog tests are unaffected.
A future fix to keyboard/focus/listener-cleanup now lands in one place.

(The /pro inline dialog stays duplicated — separate build, cannot import src/.)

Claude-Session: https://claude.ai/code/session_01C2WR72ZeDg1vBQQpcVzaYi
The pre-push Sentry-coverage guard flagged the new fail-open catch (#4438
review): a catch that logs via console.error but neither calls
captureSilentError nor re-throws. Re-throwing would defeat the fail-open
contract (the whole point — a transient guard-query error must not block
checkout), and Convex auto-Sentry forwards the structured console.error, so
on-call still sees it. Added the sentry-coverage-ok marker with justification,
matching the convex precedent in subscriptionHelpers.ts.

Claude-Session: https://claude.ai/code/session_01C2WR72ZeDg1vBQQpcVzaYi
@koala73

koala73 commented Jun 27, 2026

Copy link
Copy Markdown
Owner Author

Code review pass complete. No actionable findings from the remote PR-head review.\n\nChecked the pending-payment guard wiring, wm_plan_key metadata bridge, PAYMENT_IN_PROGRESS 409 propagation through Convex relay / Vercel edge / client taxonomy, dashboard and /pro confirm-to-bypass dialogs, and the prior Greptile ARIA feedback. The ARIA comments appear addressed in the current PR head.\n\nValidation note: git diff --check passed. I did not check out the PR branch, so I could not run the PR-head test files from this local main worktree.

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.

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

1 participant