Skip to content

fix: make Stripe subscription updates order-aware (#88)#89

Merged
pablopunk merged 1 commit into
mainfrom
advisor/006-order-stripe-subscription-events
Jul 11, 2026
Merged

fix: make Stripe subscription updates order-aware (#88)#89
pablopunk merged 1 commit into
mainfrom
advisor/006-order-stripe-subscription-events

Conversation

@pablopunk

@pablopunk pablopunk commented Jul 11, 2026

Copy link
Copy Markdown
Owner

Implements Plan 006. Persists the latest applied subscription status-event version and makes out-of-order Stripe customer.subscription.updated/.deleted deliveries idempotent.

  • subscriptions gains nullable last_event_created_at (tz-aware) + last_event_id; existing rows backfill to NULL so the first post-deploy event establishes the authoritative version.
  • handleSubscriptionStatus receives full event metadata and, inside the existing transaction, compares incoming vs stored event.created: newer applies, older no-ops + logs stripe_subscription_event_stale, exact tie never lets updated resurrect a terminal deleted. Active-subscription guard preserved.
  • Event insert + stale check + subscription update + user-plan update stay in one transaction; a stale event still counts as processed.
  • Added reversed-order/replay tests (update→delete, delete→older-update, exact tie, reactivation, duplicate id, rollback) asserting final subscription status AND user plan.

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

  • Bug Fixes
    • Improved subscription webhook processing to ignore outdated events and prevent incorrect plan or credit changes.
    • Ensured subscription deletions and reactivations are handled reliably.
    • Improved duplicate-event handling and transaction rollback behavior.
  • Data Integrity
    • Subscription records now retain event timestamps and identifiers to support consistent event ordering.

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>
@vercel

vercel Bot commented Jul 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
nvm Ready Ready Preview, Comment Jul 11, 2026 8:56pm

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The 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)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The code matches the order-aware Stripe event handling requirements, but the linked issue also required updating plans/README.md and that change is absent. Add the required plans/README.md update, or document why it was intentionally deferred, then rerun validation against #88.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: making Stripe subscription updates order-aware.
Description check ✅ Passed The description covers summary, verification results, and the migration, but it does not fully reflect the template's backend compatibility section.
Out of Scope Changes check ✅ Passed The changes stay within the billing/schema/test/migration scope described by the issue and do not introduce unrelated modifications.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
backend/src/lib/billing.test.ts (1)

134-152: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Transaction rollback doesn't restore the selects queue.

The catch block restores eventIds, ledgerRefs, subscriptionRow, and truncates inserts/updates, but selects (drained via .shift() in createSelectChain) 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

📥 Commits

Reviewing files that changed from the base of the PR and between 4848ee7 and b7b7a64.

📒 Files selected for processing (6)
  • backend/drizzle/0007_marvelous_may_parker.sql
  • backend/drizzle/meta/0007_snapshot.json
  • backend/drizzle/meta/_journal.json
  • backend/src/db/schema.ts
  • backend/src/lib/billing.test.ts
  • backend/src/lib/billing.ts

@pablopunk
pablopunk merged commit 35bdc8b into main Jul 11, 2026
8 checks passed
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.

Make Stripe subscription updates order-aware

1 participant