fix(billing): drop over-deep Stripe expand in /sync, fetch product per-sub#3853
Merged
Conversation
…r-sub Hotfix on top of #3850. The previous attempt expanded 'data.items.data.price.product' on subscriptions.list, which is 5 levels deep and still over Stripe's 4-level expansion limit. /sync kept failing. New approach: list subs without product expansion. The lookup_key fast path covers the modern cohort with zero extra round-trips. When no sub has a recognized lookup_key, fall back to retrieving the product per candidate via stripe.products.retrieve to check metadata.category= membership — Adzymic/Advertible/Bidcliq/Equativ-shape recovers through this path. New pickMembershipSubWithProductFetch async helper. Sync pickMembershipSub is unchanged for invariant callers that walk every sub on the account and can't afford per-sub round-trips. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 tasks
bokelley
added a commit
that referenced
this pull request
May 2, 2026
…sync (#3872) * fix(billing): drop over-deep Stripe expand at three sister sites of /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> * fix(billing): address review — backfill cap, customer expand, founding-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> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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 on top of #3850. The previous attempt expanded
data.items.data.price.productonsubscriptions.list, which is 5 levels deep and still over Stripe's 4-level expansion limit. /sync continued to fail with "Failed to sync from Stripe" for every org.What changed
New approach in
pickMembershipSubWithProductFetch(async,server/src/billing/membership-prices.ts):lookup_keyfast path first via the existingpickMembershipSub— covers the modern cohort with zero extra Stripe round-trips.aao_membership_*lookup_key, fall back to retrieving the product per candidate viastripe.products.retrieveand checkingmetadata.category === "membership". Founding-era subs (Adzymic, Advertible, Bidcliq, Equativ — May 2026) recover through this path./sync(server/src/routes/admin/accounts-billing.ts) lists subs without any deep expand and uses the new helper.Synchronous
pickMembershipSubis unchanged — invariant callers that walk every sub on the AAO Stripe account can't afford per-sub round-trips.Test plan
pickMembershipSubWithProductFetch: fast-path, metadata-fallback, neither-matches, transient-failure resilience, already-expanded product)/syncrevenue backfill updated to mockstripe.subscriptions.listandstripe.products.retrievestripe_subscription_id,subscription_price_lookup_key,membership_tierGET /api/admin/integrity/check/every-entitled-org-has-resolvable-tierto confirm Adzymic clears🤖 Generated with Claude Code