Fetch and cache active stripe subscription#50
Conversation
Co-authored-by: dogesman098 <dogesman098@gmail.com>
|
Cursor Agent can help with this pull request. Just |
|
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
WalkthroughA new API endpoint for handling Stripe webhooks has been added. This handler processes specific Stripe event types related to subscriptions and payments, verifies webhook signatures, extracts and synchronizes customer subscription data to a key-value store, and returns appropriate HTTP status codes. The default body parser is disabled for raw request access. Changes
Sequence Diagram(s)sequenceDiagram
participant Stripe
participant API Handler
participant KeyValueStore
Stripe->>API Handler: POST /api/stripe-webhook (with event payload)
API Handler->>API Handler: Verify HTTP method and required headers
API Handler->>API Handler: Verify Stripe signature using secret
API Handler->>API Handler: Parse and filter event type
API Handler->>API Handler: Extract customer ID
API Handler->>Stripe: Fetch all subscriptions for customer
API Handler->>API Handler: Select active/trialing subscription
API Handler->>KeyValueStore: Cache subscription data (or 'none')
API Handler-->>Stripe: Return HTTP 200 or error status
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~15 minutes Poem
Note 🔌 MCP (Model Context Protocol) integration is now available in Early Access!Pro users can now connect to remote MCP servers under the Integrations page to get reviews and chat conversations that understand additional development context. ✨ Finishing Touches
🧪 Generate unit tests
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (2)
api/stripe-webhook.ts (2)
100-110: Removeanyand correctly handle Buffer for Stripe signature verification (fixes ESLint and robustness).Accessing
rawBodyvia(req as any)fails lint and misses the case wherereq.bodyis a Buffer. Use a typed request and support both string and Buffer.Apply this diff within this block:
- const rawBody = (req as any).rawBody || (req.body && typeof req.body === 'string' ? req.body : undefined); + const rawBody = + (req as RawBodyRequest).rawBody ?? + (typeof req.body === 'string' || Buffer.isBuffer(req.body) ? req.body : undefined);Add this small type near the imports (outside this range):
type RawBodyRequest = VercelRequest & { rawBody?: string | Buffer };
114-120: Eliminateanyand robustly extractcustomerIdacross event payload shapes.
event.data.objectvaries by event; avoidanyand handle both string and expanded objects safely.Apply this diff within this block:
- if (allowedEvents.includes(event.type)) { - const obj: any = event.data?.object ?? {}; - const customerId = obj.customer as string | undefined; - if (customerId && typeof customerId === 'string') { - await syncStripeDataToKV(customerId); - } - } + if (allowedEvents.includes(event.type)) { + const customerId = extractCustomerId(event.data?.object); + if (customerId) { + await syncStripeDataToKV(customerId); + } + }Add this helper (outside this range, e.g., below the
StripeSubCachetype):function extractCustomerId(obj: unknown): string | undefined { if (!obj || typeof obj !== 'object') return undefined; const customer = (obj as { customer?: unknown }).customer; if (typeof customer === 'string') return customer; if (customer && typeof customer === 'object') { const id = (customer as { id?: unknown }).id; if (typeof id === 'string') return id; } return undefined; }
🧹 Nitpick comments (4)
api/stripe-webhook.ts (4)
6-26: Optional: strengthen type-safety for event names.Typing the allowlist makes typos impossible to compile.
Apply:
-const allowedEvents: string[] = [ +const allowedEvents = [ 'checkout.session.completed', 'customer.subscription.created', 'customer.subscription.updated', 'customer.subscription.deleted', 'customer.subscription.paused', 'customer.subscription.resumed', 'customer.subscription.pending_update_applied', 'customer.subscription.pending_update_expired', 'customer.subscription.trial_will_end', 'invoice.paid', 'invoice.payment_failed', 'invoice.payment_action_required', 'invoice.upcoming', 'invoice.marked_uncollectible', 'invoice.payment_succeeded', 'payment_intent.succeeded', 'payment_intent.payment_failed', 'payment_intent.canceled', -]; + ] as const;Optionally:
// type AllowedEvent = typeof allowedEvents[number]; // for narrowed checks if needed
69-75: Multi-item subscriptions: selecting only the first item may be misleading.If a subscription can have multiple items, using
items.data[0]could cache the wrong price. Consider deriving the “primary” item or caching all price IDs.Example (cache all price IDs without breaking shape):
- priceId: subscription.items.data[0]?.price?.id ?? null, + priceId: + subscription.items.data.find((it) => it.price?.active)?.price?.id ?? + subscription.items.data[0]?.price?.id ?? + null,
76-83: Payment method fallback: also check latest invoice’s payment intent.
default_payment_methodon the subscription is often null; card details are frequently onlatest_invoice.payment_intent.payment_method. Consider expanding and falling back to that for more complete caching.Possible approach (requires expanding nested fields):
- const subscriptions = await stripe.subscriptions.list({ + const subscriptions = await stripe.subscriptions.list({ customer: customerId, limit: 100, status: 'all', - expand: ['data.default_payment_method'], + expand: ['data.default_payment_method', 'data.latest_invoice.payment_intent.payment_method'], });Then, when building
paymentMethod, fallback ifdefault_payment_methodis missing:const pmFromSub = typeof subscription.default_payment_method !== 'string' ? subscription.default_payment_method : null; const pmFromPI = (typeof subscription.latest_invoice !== 'string' && subscription.latest_invoice?.payment_intent && typeof subscription.latest_invoice.payment_intent !== 'string' && subscription.latest_invoice.payment_intent.payment_method && typeof subscription.latest_invoice.payment_intent.payment_method !== 'string' ? subscription.latest_invoice.payment_intent.payment_method : null) || null; const pm = pmFromSub ?? pmFromPI; paymentMethod: pm ? { brand: pm.card?.brand ?? null, last4: pm.card?.last4 ?? null, } : null,
124-130: Good failure handling, minor nit: include error class in logs.Logging
messageis fine; optionally lognameto aid triage.- console.error('[STRIPE WEBHOOK] Error', errorMessage); + console.error('[STRIPE WEBHOOK] Error', { message: errorMessage, name: (err as any)?.name });
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (1)
api/stripe-webhook.ts(1 hunks)
🧰 Additional context used
🪛 ESLint
api/stripe-webhook.ts
[error] 103-103: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 115-115: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
🪛 GitHub Check: ESLint
api/stripe-webhook.ts
[failure] 103-103: Disallow the any type
Unexpected any. Specify a different type.
[failure] 115-115: Disallow the any type
Unexpected any. Specify a different type.
🔇 Additional comments (3)
api/stripe-webhook.ts (3)
56-63: Confirm business rule: excludepast_due(and possiblyunpaid) from candidates?Currently only
active/trialingqualify. If users withpast_dueshould retain access, consider including it (and maybeunpaid) to avoid cachingstatus: 'none'.If inclusion is intended, minimal change:
- .filter((s) => s.status === 'active' || s.status === 'trialing') + .filter((s) => s.status === 'active' || s.status === 'trialing' || s.status === 'past_due')
89-94: LGTM: Method restriction and Allow header are correct.POST-only with
Allow: POSTis handled properly.
133-137: LGTM: Body parser disabled for raw signature verification.This matches Stripe’s requirement for using the raw request body.
Update Stripe webhook to fetch and cache the most recent active subscription status.
Previously, the code fetched only one subscription, which could be stale or cancelled, leading to incorrect caching of the user's subscription status. This change ensures the correct, most recent active subscription is identified and cached.
Summary by CodeRabbit