Skip to content

fix(billing): a customer id from another Stripe mode no longer bricks checkout - #877

Merged
lilyshen0722 merged 1 commit into
mainfrom
fix/stripe-stale-customer
Aug 6, 2026
Merged

fix(billing): a customer id from another Stripe mode no longer bricks checkout#877
lilyshen0722 merged 1 commit into
mainfrom
fix/stripe-stale-customer

Conversation

@lilyshen0722

Copy link
Copy Markdown
Contributor

Found while verifying today's test-mode checkout end to end.

The test purchase stamped a test-mode billing.customerId onto a real user
row. routes/billing.ts only creates a Stripe customer when that field is
empty — and a stale value is not empty — so the first checkout after the
test → live cutover would have named a customer Stripe has never heard of and
returned checkout_failed. Permanently. The only recovery was a database edit.

This is not specific to the cutover. Deleting a customer in the Stripe
dashboard, or migrating between Stripe accounts, produces the same state.

What changed

Checkout re-attaches and retries once. A second failure means the cause was
never staleness, so it surfaces rather than looping.

The portal deliberately does not re-create. A fresh customer has no
subscription, so the portal would open empty and imply the subscription
vanished. It returns no_subscription — true, and actionable by the UI that
already handles that code.

The detection is narrow on purpose. It gates on param === 'customer', not
code === 'resource_missing' alone. That same code fires for a missing
price, which is exactly what a half-finished cutover produces — a test price
id against live keys. Treating that as a stale customer would mint a new Stripe
customer on every attempt and still fail. There is a test pinning that
distinction.

Verification

Four new tests. Reverted the fix and confirmed three of them fail; the fourth
(missing-price) passes either way by construction, since it guards against a
future over-broad fix rather than against this bug. 39 billing tests pass,
typecheck clean.

Related: #875, #876.

… checkout

`billing.customerId` is a cache of a Stripe-side object, not identity, and the
two diverge. A customer created under test keys does not exist under live ones,
so the first checkout after a test -> live cutover names a ghost and returns
checkout_failed — permanently, because the code only creates a customer when
the field is EMPTY, and a stale value is not empty. Recovery required a
database edit.

Found on a live account: the test-mode checkout run today stamped
cus_… on the operator's user, which would have failed every subsequent
checkout the moment live keys landed. Deleting a customer in the dashboard or
migrating Stripe accounts produces the same state.

Checkout now re-attaches and retries ONCE. Twice would mean the failure was
never staleness, so the second error surfaces instead of looping.

The portal deliberately does NOT re-create: a fresh customer has no
subscription, so the portal would open empty and imply the subscription
vanished. It returns no_subscription, which is both true and actionable.

Gated on `param === 'customer'`, not `code === 'resource_missing'` alone. That
code also fires for a missing PRICE — exactly what a half-finished cutover
produces — and minting a customer for that would churn customers on every
attempt while still failing. Pinned by a test.

Four tests; verified by reverting the fix and watching three of them fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XeUH4HVDsDHYPsHJthXjB8
@lilyshen0722
lilyshen0722 merged commit af3f131 into main Aug 6, 2026
11 checks passed
@lilyshen0722
lilyshen0722 deleted the fix/stripe-stale-customer branch August 6, 2026 10:17
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.

1 participant