fix(billing): a customer id from another Stripe mode no longer bricks checkout - #877
Merged
Conversation
… 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
This was referenced Aug 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Found while verifying today's test-mode checkout end to end.
The test purchase stamped a test-mode
billing.customerIdonto a real userrow.
routes/billing.tsonly creates a Stripe customer when that field isempty — 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 thatalready handles that code.
The detection is narrow on purpose. It gates on
param === 'customer', notcode === 'resource_missing'alone. That same code fires for a missingprice, 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.