Skip to content

fix(billing): add stripe_subscription_id fallback to prevent silent cancellation drop on customer drift - #4172

Merged
bokelley merged 2 commits into
mainfrom
claude/issue-4170-stripe-drift-customer-cancellation-silent-drop
May 7, 2026
Merged

fix(billing): add stripe_subscription_id fallback to prevent silent cancellation drop on customer drift#4172
bokelley merged 2 commits into
mainfrom
claude/issue-4170-stripe-drift-customer-cancellation-silent-drop

Conversation

@bokelley

@bokelley bokelley commented May 6, 2026

Copy link
Copy Markdown
Contributor

Closes #4170

Adds a stripe_subscription_id DB fallback to resolveOrgForStripeCustomer to prevent silent cancellation drops in the customer-drift scenario. Also adds logger.warn + notifySystemError alerting for unresolvable .updated/.deleted events, and a regression test covering the drift scenario end-to-end.

Root cause (not captured by the three hypotheses in the issue): resolveOrgForStripeCustomer had no lookup keyed on stripe_subscription_id. When the event's customer field didn't match any org's stripe_customer_id AND the subscription had no workos_organization_id metadata (pre-dates metadata-stamping), the resolver returned null — the DB UPDATE was silently skipped while Stripe got a 200 and never retried. The StripeCustomerConflictError path (hypothesis 1) and isStaleNonLiveEvent guard (hypothesis 2) were both red herrings.

Changes:

  • server/src/db/organization-db.ts — adds getOrganizationByStripeSubscriptionId (mirrors getOrganizationByStripeCustomerId; 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 list
  • server/src/http.ts — adds logger.warn + notifySystemError when org resolution fails for .updated/.deleted so ops is alerted immediately rather than discovering via manual sweep
  • server/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.paid drift scenarios because those call sites don't pass a subscription object. Pre-existing gap, not a regression introduced here. Follow-up: thread subscription into the invoice resolver or extend step 3.5 to resolve from invoice.subscription.

Pre-PR review:

  • code-reviewer: approved — no blockers; JSDoc updated, happy-path test added; invoice.paid gap noted as latent follow-up
  • internal-tools-strategist: approved — fix proportionate, closes access-control gap; uniqueness constraint confirmed in migration 013; --empty changeset confirmed correct

Triage-managed PR. This bot does not currently iterate on
review comments or PR conversation threads (only on the source
issue). To unblock:

  • Push fixup commits directly: gh pr checkout <num>
    fix → push.
  • Or re-trigger: comment /triage execute on the source
    issue.

See #3121
for context.

Session: https://claude.ai/code/session_017UQZCzNYrKnhBJKWUpBCcT


Generated by Claude Code

claude added 2 commits May 6, 2026 19:45
…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
@bokelley bokelley added the claude-triaged Issue has been triaged by the Claude Code triage routine. Remove to re-triage. label May 6, 2026
@bokelley
bokelley marked this pull request as ready for review May 7, 2026 09:01
@bokelley
bokelley merged commit e8bfc2b into main May 7, 2026
14 checks passed
@bokelley
bokelley deleted the claude/issue-4170-stripe-drift-customer-cancellation-silent-drop branch May 7, 2026 09:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

claude-triaged Issue has been triaged by the Claude Code triage routine. Remove to re-triage.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Stripe webhook: cancellation silently dropped when customer link has drifted

2 participants