fix(billing): add stripe_subscription_id fallback to prevent silent cancellation drop on customer drift - #4172
Merged
bokelley merged 2 commits intoMay 7, 2026
Conversation
…ancellation drop on customer drift When a Stripe cancellation event arrives for an org whose stripe_customer_id doesn't match the event's customer (drift scenario), all metadata-based resolution paths can miss and the resolver returns null — causing the UPDATE to be silently skipped while the webhook returns 200. Fixes #4170. - Add getOrganizationByStripeSubscriptionId DB method and 5th fallback in resolveOrgForStripeCustomer (looks up by sub ID when all metadata paths miss) - Add logger.warn + notifySystemError when org resolution fails for .updated/.deleted events so ops is alerted immediately rather than discovering via manual sweep - Add regression test for the customer-drift cancellation scenario - Empty changeset (server-only, non-protocol) https://claude.ai/code/session_017UQZCzNYrKnhBJKWUpBCcT
…t, add dist artifacts - Update resolveOrgForStripeCustomer lookup-order JSDoc to list step 3.5 - Add happy-path test for step 3.5 when customer re-link succeeds without conflict - Add dist/schemas/member-agents-openapi build artifacts (generated by build:openapi) https://claude.ai/code/session_017UQZCzNYrKnhBJKWUpBCcT
3 tasks
bokelley
marked this pull request as ready for review
May 7, 2026 09:01
bokelley
deleted the
claude/issue-4170-stripe-drift-customer-cancellation-silent-drop
branch
May 7, 2026 09:01
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.
Closes #4170
Adds a
stripe_subscription_idDB fallback toresolveOrgForStripeCustomerto prevent silent cancellation drops in the customer-drift scenario. Also addslogger.warn+notifySystemErroralerting for unresolvable.updated/.deletedevents, and a regression test covering the drift scenario end-to-end.Root cause (not captured by the three hypotheses in the issue):
resolveOrgForStripeCustomerhad no lookup keyed onstripe_subscription_id. When the event's customer field didn't match any org'sstripe_customer_idAND the subscription had noworkos_organization_idmetadata (pre-dates metadata-stamping), the resolver returned null — the DB UPDATE was silently skipped while Stripe got a 200 and never retried. TheStripeCustomerConflictErrorpath (hypothesis 1) andisStaleNonLiveEventguard (hypothesis 2) were both red herrings.Changes:
server/src/db/organization-db.ts— addsgetOrganizationByStripeSubscriptionId(mirrorsgetOrganizationByStripeCustomerId; uses existing unique partial index from migration 013)server/src/billing/webhook-helpers.ts— adds step 3.5: direct sub-ID DB lookup when all metadata paths miss; updates JSDoc lookup-order listserver/src/http.ts— addslogger.warn+notifySystemErrorwhen org resolution fails for.updated/.deletedso ops is alerted immediately rather than discovering via manual sweepserver/tests/unit/webhook-org-resolution.test.ts— adds drift regression test (conflict and no-conflict variants)Non-breaking justification: new read-only DB method + new resolver fallback path; existing call sites and consumer behaviour are unaffected. No schema, protocol, or API reference changes; changeset is
--empty.Nit (noted, not fixed): The step 3.5 fallback is unreachable for
invoice.paiddrift scenarios because those call sites don't pass asubscriptionobject. Pre-existing gap, not a regression introduced here. Follow-up: thread subscription into the invoice resolver or extend step 3.5 to resolve frominvoice.subscription.Pre-PR review:
--emptychangeset confirmed correctSession: https://claude.ai/code/session_017UQZCzNYrKnhBJKWUpBCcT
Generated by Claude Code