Skip to content

fix(billing): drop over-deep Stripe expand at three sister sites of /sync#3872

Merged
bokelley merged 2 commits into
mainfrom
bokelley/stripe-expand-depth
May 2, 2026
Merged

fix(billing): drop over-deep Stripe expand at three sister sites of /sync#3872
bokelley merged 2 commits into
mainfrom
bokelley/stripe-expand-depth

Conversation

@bokelley

@bokelley bokelley commented May 2, 2026

Copy link
Copy Markdown
Contributor

Summary

The /sync hotfix in #3853 replaced its over-deep data.items.data.price.product expand with a separate subscriptions.list + per-product fetch. Three sister sites still had the broken pattern and were silently failing on Stripe's 4-level expand limit:

  • dedup-on-subscription-created.ts:117 — 5-level expand on subscriptions.list. The try/catch swallowed the Stripe error and fell through to no_duplicate, silently disabling the cross-path duplicate-subscription guard from Webhook dedup: revisit cancel-newer policy and add customer-facing apology #3245. Highest impact — dedup hasn't been running since the expand limit shipped.
  • stripe-sub-reflected-in-org-row invariant — 5-level expand on subscriptions.list. Threw on every run, so the Lina/Adzymic-class detection wasn't firing.
  • /api/admin/backfill-revenue (http.ts:5223) — 6-level subscriptions.data.items.data.price.product on customers.retrieve. Whole loop crashed per customer.

The pattern at all three is: drop the over-deep expand, classify by lookup_key with a per-product products.retrieve fallback (cached) for founding-era prices that lack the aao_membership_* convention. Same shape as #3853.

Adds shared helper isMembershipSubWithProductFetch in billing/membership-prices (per-sub filter case, sibling to existing pickMembershipSubWithProductFetch).

The Slack alert that prompted this (admin-accounts-billing source, data.items.data.price.product in the message) was probably from before #3853 deployed — the /sync route in current main can no longer trigger that error. But the three sister sites would have started surfacing it once exercised.

Test plan

  • npx vitest run tests/unit/billing tests/unit/integrity — 111 passed
  • tsc --noEmit clean
  • Manual: hit POST /api/admin/accounts/:orgId/sync against an org with a real Stripe customer in dev (no regression vs. fix(billing): drop over-deep Stripe expand in /sync, fetch product per-sub #3853)
  • Manual: trigger an integrity audit run, confirm stripe-sub-reflected-in-org-row returns results instead of throwing
  • Manual: run /api/admin/backfill-revenue against a small customer set in dev

🤖 Generated with Claude Code

bokelley and others added 2 commits May 2, 2026 14:41
…sync

The /sync hotfix (#3853) replaced its over-deep `data.items.data.price.product`
expand with a separate `subscriptions.list` + per-product fetch. Three sister
sites kept the broken pattern:

- `dedup-on-subscription-created.ts`: 5-level expand on `subscriptions.list`.
  Catch swallowed the Stripe error and fell through to `no_duplicate`,
  silently disabling the cross-path duplicate-subscription guard from #3245.
- `stripe-sub-reflected-in-org-row` invariant: 5-level expand on
  `subscriptions.list`. Threw on every run, so Lina/Adzymic-class detection
  never fired.
- `/api/admin/backfill-revenue`: 6-level
  `subscriptions.data.items.data.price.product` on `customers.retrieve`.
  Whole loop crashed per customer.

Drop expand at all three; classify by lookup_key with a per-product
`products.retrieve` fallback (cached) for founding-era prices. Tests stay
green: dedup tier-label fallback to `lookup_key` was already present, and
the invariant tests pass `product` as an inline object that
`isMembershipProductByMetadata` already accepts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…g-era test fidelity

Code/security review on PR #3872 surfaced three real issues:

- /backfill-revenue used limit:10 on subscriptions.list — could silently
  truncate the membership sub on customers with many lifetime subs.
  Bumped to 100, matching the dedup helper.
- The invariant's orphan-customer violation reads sub.customer.email,
  but the new code path doesn't expand customer, so the field always
  serialized null and admins lost the triage hint. Added
  expand: ['data.customer'] (2 levels, well under the limit).
- Founding-era invariant tests (Bidcliq, Equativ) passed `product` as
  an inline object, masking the real Stripe response shape. Rewrote to
  pass product as a string id, mock products.retrieve, and assert it
  was called — actually exercises the new fetch fallback path. Added
  a cache-coalescing test and a fetch-failure swallow test.

Also adds direct unit tests for the new isMembershipSubWithProductFetch
helper covering lookup_key fast path, metadata fallback, cache reuse,
and the silent-false-on-fetch-error path that's easy to misuse.

Test count: 145 passing (+34 from prior commit).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley merged commit d41a3e5 into main May 2, 2026
12 checks passed
@bokelley bokelley deleted the bokelley/stripe-expand-depth branch May 2, 2026 19:08
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