fix(stripe): pro-tier sync failure + reconciliation backstop (FAULT 16)#48
Draft
SoapyRED wants to merge 3 commits into
Draft
fix(stripe): pro-tier sync failure + reconciliation backstop (FAULT 16)#48SoapyRED wants to merge 3 commits into
SoapyRED wants to merge 3 commits into
Conversation
Phase 1 of hotfix sprint customer-pro-tier-sync-2026-05-21. Stripe shows a paid active Pro subscription for the first paying customer; site reads FREE because the webhook handler's safeUpdateUserPlan wrapper calls itself recursively (app/api/stripe/webhook/route.ts:32) instead of the imported updateUserPlan. RangeError swallowed by try/catch, console.warn only, no Sentry capture → 100% silent failure. Webhook returned 200 so Stripe never retried. Phase 2 (immediate fix) + Phase 3 (recursion fix + reconciliation backstop + Sentry alert) follow in this PR.
The safeUpdateUserPlan wrapper in the Stripe webhook handler was self-recursive instead of delegating to the imported updateUserPlan. Every Pro upgrade since the wrapper was added stack-overflowed → RangeError swallowed by try/catch with console.warn only (no Sentry capture) → webhook still returned 200 so Stripe never retried. The first paying Pro customer (2026-05-20) hit the bug. - app/api/stripe/webhook/route.ts: call updateUserPlan; add Sentry.captureException in the catch so future swallows are visible. - lib/auth/reconcile.ts: new tier reconciliation function. When a user record reads plan='free' AND has a stripeCustomerId, asks Stripe for active subs and self-heals if mismatched. Cooldown-gated 1h per email; emits tier_mismatch_reconciled Sentry warning on heal (the future Sentry alert rule listens for this). - app/api/auth/me/route.ts: call reconcile after getUser so the /account page self-heals on next visit. - app/api/auth/whoami/route.ts: gated reconcile on free-tier reads only — Pro keys keep the fast path with one KV read. Stripe is the source of truth. Middleware deliberately does NOT reconcile (would hammer Stripe on every API request); the identity endpoints are the right cooldown-friendly hook.
- scripts/admin/upgrade-user.mjs: one-shot manual tier upgrade. Verifies the customer has an active Pro subscription on Stripe BEFORE writing KV — refuses to upgrade an unpaid account. Reads back the user:<email> and key:<apiKey> records post-write and prints before/after for the audit trail. Distinct exit codes for Stripe-verify-fail / no-record / already-Pro. - docs/runbooks/customer-tier-sync.md: end-to-end triage runbook. Stripe-side verification, KV inspection, manual upgrade, Stripe goodwill coupon (dashboard-applied per the Soap-manual rule), customer-side verification curl, Sentry alert wire-up. - docs/FAULT-HISTORY-AND-PREVENTION.md: FAULT CATEGORY 16 added with root cause, prevention model, and 2026-05-21 log row. - STATE.md: Customer incidents section logging 2026-05-21 with customer initials only per the no-PII-beyond-first-paying-customer rule. lastUpdated bumped to 21 May 2026. - CHANGELOG.md + lib/changelog-data.ts: 2026-05-21 entry mirrored per FAULT 5 dual-source-until-collapsed rule.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This was referenced May 21, 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.
Summary
Hotfix for the first paying Pro customer (2026-05-20 ~19:22 BST signup) who paid £19 on Stripe but the site kept reading FREE / 100 req/day. Branch is
claude/fix-pro-tier-sync-AvjCX(system-assigned override; the original task titlehotfix/customer-pro-tier-sync-2026-05-21is preserved in the audit doc).Root cause:
safeUpdateUserPlaninapp/api/stripe/webhook/route.ts:32called itself recursively instead of delegating to the importedupdateUserPlan. Everycheckout.session.completedevent since the wrapper was added stack-overflowed →RangeErrorcaught by try/catch withconsole.warnonly (no Sentry capture) → webhook still returned 200 so Stripe never retried. 100% silent.docs/audit/customer-pro-tier-sync-2026-05-21.md./api/auth/me+/api/auth/whoami.scripts/admin/upgrade-user.mjsis the one-shot for Phase 2 (KV write requires prod creds Soap holds).docs/runbooks/customer-tier-sync.md.What ships
app/api/stripe/webhook/route.tsSentry.captureExceptionon catchlib/auth/reconcile.tsreconcileTierFromStripe, cooldown-gated 1h/email, emitstier_mismatch_reconciledwarning on healapp/api/auth/me/route.tsgetUserapp/api/auth/whoami/route.tsplan: free(Pro fast path preserved)scripts/admin/upgrade-user.mjsdocs/audit/customer-pro-tier-sync-2026-05-21.mddocs/FAULT-HISTORY-AND-PREVENTION.mddocs/runbooks/customer-tier-sync.mdSTATE.mdCHANGELOG.md+lib/changelog-data.tsSoap's checklist before merge
1. Stripe goodwill coupon — apply via dashboard
Coupon already created via Stripe MCP:
2z8mCBes("Goodwill 1 month - tier sync 2026-05-21", 100% off, durationrepeating× 1 month).sub_*→ "Update subscription" → "Add coupon" → select2z8mCBes→ save.(Programmatic apply isn't exposed via the Stripe MCP tool surface we use; STATE.md already pins "Chrome BLOCKED on Stripe — Soap-manual only".)
2. KV write — run the admin script
The container this PR was prepared in has no prod creds, so the KV write is yours:
The script:
cus_idhas an active sub (refuses if not).user:<email>from KV.plan: "pro"+stripeCustomerIdto bothuser:<email>and mirrorkey:<apiKey>.plan: "pro".Exit codes:
0upgrade done,1Stripe verify fail / KV error,2no record,3already Pro.3. Customer-side verification
After step 2, confirm with the customer's existing API key (do NOT rotate):
Must return
tier: "pro".4. Sentry alert rule (one-time, manual)
Sentry → Alerts → Create Alert Rule → Issue Alert:
message:"tier_mismatch_reconciled"ANDlevel:warningThe alert fires the moment the reconciliation backstop heals a paying customer — which by definition means the primary webhook path failed silently. One alert = one paying customer almost-churned. P1.
5. Smoke + Sentry sweep
npm run smoke-testagainst preview / prod./api/stripe/*,/api/account,/api/auth/whoami,/api/auth/me.Customer follow-up email (draft — copy/paste)
FAULT 5 checklist
siteStats.ts— n/a (no displayed numbers changed)public/openapi.json— n/a (no new endpoint)/api-docspage — n/aCHANGELOG.mdentry added — 2026-05-21/changelogpage (lib/changelog-data.ts) — entry addedSoapyRED/freightutils-mcpREADME — n/a (no MCP-tool changes)freightutils-mcpversion bump — n/awithAuditRest— the affected route isapp/api/stripe/webhookwhich is inEXCLUDED_PATHSofscripts/lint-audit.mjs(webhook by design).lint:auditpasses.generateMetadata()— n/a (no new public page)Test plan
npx tsc --noEmitclean.npm run lint:auditpasses (20 routes wrapped, 18 excluded).npm run lint:seo-titlespasses.npm run buildsucceeds.node scripts/sentry-redact-smoke.mjspasses.node --check scripts/admin/upgrade-user.mjspasses.plan: pro./api/auth/whoamireturnstier: prowith customer's existing key.tier_mismatch_reconciledcreated./api/stripe/*,/api/auth/*.https://claude.ai/code/session_019A4f9SxA6vyzdoC67JLmTZ
Generated by Claude Code