fix: make Stripe subscription updates order-aware (#88)#89
Conversation
Persist the latest applied subscription status-event version (event.created + event id) on subscriptions and make out-of-order customer.subscription.updated/ deleted deliveries idempotent: newer applies, older no-ops and logs stripe_subscription_event_stale, exact ties never let updated resurrect a terminal deleted. All work stays in the existing transaction; a stale event still counts as processed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughThe subscription schema now stores the latest Stripe event timestamp and ID. Billing status handling receives the full event, skips stale updates, persists the applied version, and continues updating subscription and plan state for current events. Billing tests now retain subscription state across calls and cover event ordering, timestamp ties, duplicate IDs, reactivation, and transaction rollback. 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
backend/src/lib/billing.test.ts (1)
134-152: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winTransaction rollback doesn't restore the
selectsqueue.The catch block restores
eventIds,ledgerRefs,subscriptionRow, and truncatesinserts/updates, butselects(drained via.shift()increateSelectChain) isn't snapshotted/restored. Currently harmless because the rollback test fails before any select is consumed, but if a future test triggers a failure after a select is consumed inside a transaction, the pre-seeded value is permanently lost and the retry will get[]instead of the intended fixture — a subtle, hard-to-diagnose test flake.♻️ Proposed fix to also snapshot/restore `selects`
transaction: async (callback: (tx: FakeDb) => Promise<unknown>) => { const eventSnapshot = new Set(eventIds); const ledgerSnapshot = new Set(ledgerRefs); const subscriptionSnapshot = subscriptionRow ? { ...subscriptionRow } : null; + const selectsSnapshot = [...selects]; const insertsLength = inserts.length; const updatesLength = updates.length; try { return await callback(db); } catch (error) { eventIds.clear(); eventSnapshot.forEach((id) => eventIds.add(id)); ledgerRefs.clear(); ledgerSnapshot.forEach((ref) => ledgerRefs.add(ref)); subscriptionRow = subscriptionSnapshot; + selects.length = 0; + selects.push(...selectsSnapshot); inserts.length = insertsLength; updates.length = updatesLength; throw error; } },🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/src/lib/billing.test.ts` around lines 134 - 152, Update the transaction mock’s rollback handling to snapshot the current selects queue before invoking the callback and restore it in the catch block, alongside eventIds, ledgerRefs, subscriptionRow, inserts, and updates. Preserve the queue contents and order so a retry receives the same fixtures after a failed transaction; anchor the change in the transaction callback and createSelectChain interaction.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@backend/src/lib/billing.test.ts`:
- Around line 134-152: Update the transaction mock’s rollback handling to
snapshot the current selects queue before invoking the callback and restore it
in the catch block, alongside eventIds, ledgerRefs, subscriptionRow, inserts,
and updates. Preserve the queue contents and order so a retry receives the same
fixtures after a failed transaction; anchor the change in the transaction
callback and createSelectChain interaction.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 0f13d838-7697-46c9-a74a-145921a88601
📒 Files selected for processing (6)
backend/drizzle/0007_marvelous_may_parker.sqlbackend/drizzle/meta/0007_snapshot.jsonbackend/drizzle/meta/_journal.jsonbackend/src/db/schema.tsbackend/src/lib/billing.test.tsbackend/src/lib/billing.ts
Implements Plan 006. Persists the latest applied subscription status-event version and makes out-of-order Stripe
customer.subscription.updated/.deleteddeliveries idempotent.subscriptionsgains nullablelast_event_created_at(tz-aware) +last_event_id; existing rows backfill to NULL so the first post-deploy event establishes the authoritative version.handleSubscriptionStatusreceives full event metadata and, inside the existing transaction, compares incoming vs storedevent.created: newer applies, older no-ops + logsstripe_subscription_event_stale, exact tie never letsupdatedresurrect a terminaldeleted. Active-subscription guard preserved.Migration:
backend/drizzle/0007_marvelous_may_parker.sql.Verification: billing tests 16/16, full backend suite 74/74 pass. Remaining tsc errors are pre-existing in
proxy.test.ts(unrelated).Closes #88
🤖 Generated with Claude Code
Summary by CodeRabbit