[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6KB5ZA3RA591SEH61SNVC6C":3,"skill-same:01M6KB5ZA3RA591SEH61SNVC6C":51},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":24,"linkOnly":25,"markdownContent":26,"markdownBody":27,"frontMatter":28,"refName":29,"commitSha":30,"skillDirectory":31,"forksCount":32,"repositoryHtmlUrl":33,"repositoryHomepageUrl":6,"ownerHtmlUrl":34,"ownerBlogUrl":35,"ownerDescription":6,"updatedAt":36,"install":37,"resources":50},"01M6KB5ZA3RA591SEH61SNVC6C","stripe-integration-expert",null,"Stripe Integration Expert","afaizalam2003\u002FMaster-claude-skill","afaizalam2003","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F222019359?v=4","01M6K8F2XNT0HKBPF00JQ78SV3","Faiz Alam","01M6K8F31EBXAPER0EG8R9HTR5",3,"MIT","https:\u002F\u002Fgithub.com\u002Fafaizalam2003\u002FMaster-claude-skill\u002Fblob\u002Fmain\u002Fengineering-team\u002Fstripe-integration-expert\u002FSKILL.md",[],[19,20,21,22,23],"integration","stripe integration","stripe integration expert","expert","stripe","2026-11-10 10:12:30",false,"---\nname: \"stripe-integration-expert\"\ndescription: \"Stripe Integration Expert\"\n---\n\n# Stripe Integration Expert\n\n**Tier:** POWERFUL  \n**Category:** Engineering Team  \n**Domain:** Payments \u002F Billing Infrastructure\n\n---\n\n## Overview\n\nImplement production-grade Stripe integrations: subscriptions with trials and proration, one-time payments, usage-based billing, checkout sessions, idempotent webhook handlers, customer portal, and invoicing. Covers Next.js, Express, and Django patterns.\n\n---\n\n## Core Capabilities\n\n- Subscription lifecycle management (create, upgrade, downgrade, cancel, pause)\n- Trial handling and conversion tracking\n- Proration calculation and credit application\n- Usage-based billing with metered pricing\n- Idempotent webhook handlers with signature verification\n- Customer portal integration\n- Invoice generation and PDF access\n- Full Stripe CLI local testing setup\n\n---\n\n## When to Use\n\n- Adding subscription billing to any web app\n- Implementing plan upgrades\u002Fdowngrades with proration\n- Building usage-based or seat-based billing\n- Debugging webhook delivery failures\n- Migrating from one billing model to another\n\n---\n\n## Subscription Lifecycle State Machine\n\n```\nFREE_TRIAL ──paid──► ACTIVE ──cancel──► CANCEL_PENDING ──period_end──► CANCELED\n     │                  │                                                    │\n     │               downgrade                                            reactivate\n     │                  ▼                                                    │\n     │             DOWNGRADING ──period_end──► ACTIVE (lower plan)           │\n     │                                                                        │\n     └──trial_end without payment──► PAST_DUE ──payment_failed 3x──► CANCELED\n                                          │\n                                     payment_success\n                                          │\n                                          ▼\n                                        ACTIVE\n```\n\n### DB subscription status values:\n`trialing | active | past_due | canceled | cancel_pending | paused | unpaid`\n\n---\n\n## Stripe Client Setup\n\n```typescript\n\u002F\u002F lib\u002Fstripe.ts\nimport Stripe from \"stripe\"\n\nexport const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {\n  apiVersion: \"2024-04-10\",\n  typescript: true,\n  appInfo: {\n    name: \"myapp\",\n    version: \"1.0.0\",\n  },\n})\n\n\u002F\u002F Price IDs by plan (set in env)\nexport const PLANS = {\n  starter: {\n    monthly: process.env.STRIPE_STARTER_MONTHLY_PRICE_ID!,\n    yearly: process.env.STRIPE_STARTER_YEARLY_PRICE_ID!,\n    features: [\"5 projects\", \"10k events\"],\n  },\n  pro: {\n    monthly: process.env.STRIPE_PRO_MONTHLY_PRICE_ID!,\n    yearly: process.env.STRIPE_PRO_YEARLY_PRICE_ID!,\n    features: [\"Unlimited projects\", \"1M events\"],\n  },\n} as const\n```\n\n---\n\n## Checkout Session (Next.js App Router)\n\n```typescript\n\u002F\u002F app\u002Fapi\u002Fbilling\u002Fcheckout\u002Froute.ts\nimport { NextResponse } from \"next\u002Fserver\"\nimport { stripe } from \"@\u002Flib\u002Fstripe\"\nimport { getAuthUser } from \"@\u002Flib\u002Fauth\"\nimport { db } from \"@\u002Flib\u002Fdb\"\n\nexport async function POST(req: Request) {\n  const user = await getAuthUser()\n  if (!user) return NextResponse.json({ error: \"Unauthorized\" }, { status: 401 })\n\n  const { priceId, interval = \"monthly\" } = await req.json()\n\n  \u002F\u002F Get or create Stripe customer\n  let stripeCustomerId = user.stripeCustomerId\n  if (!stripeCustomerId) {\n    const customer = await stripe.customers.create({\n      email: user.email,\n      name: \"username-undefined\"\n      metadata: { userId: user.id },\n    })\n    stripeCustomerId = customer.id\n    await db.user.update({ where: { id: user.id }, data: { stripeCustomerId } })\n  }\n\n  const session = await stripe.checkout.sessions.create({\n    customer: stripeCustomerId,\n    mode: \"subscription\",\n    payment_method_types: [\"card\"],\n    line_items: [{ price: priceId, quantity: 1 }],\n    allow_promotion_codes: true,\n    subscription_data: {\n      trial_period_days: user.hasHadTrial ? undefined : 14,\n      metadata: { userId: user.id },\n    },\n    success_url: `${process.env.NEXT_PUBLIC_APP_URL}\u002Fdashboard?session_id={CHECKOUT_SESSION_ID}`,\n    cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}\u002Fpricing`,\n    metadata: { userId: user.id },\n  })\n\n  return NextResponse.json({ url: session.url })\n}\n```\n\n---\n\n## Subscription Upgrade\u002FDowngrade\n\n```typescript\n\u002F\u002F lib\u002Fbilling.ts\nexport async function changeSubscriptionPlan(\n  subscriptionId: string,\n  newPriceId: string,\n  immediate = false\n) {\n  const subscription = await stripe.subscriptions.retrieve(subscriptionId)\n  const currentItem = subscription.items.data[0]\n\n  if (immediate) {\n    \u002F\u002F Upgrade: apply immediately with proration\n    return stripe.subscriptions.update(subscriptionId, {\n      items: [{ id: currentItem.id, price: newPriceId }],\n      proration_behavior: \"always_invoice\",\n      billing_cycle_anchor: \"unchanged\",\n    })\n  } else {\n    \u002F\u002F Downgrade: apply at period end, no proration\n    return stripe.subscriptions.update(subscriptionId, {\n      items: [{ id: currentItem.id, price: newPriceId }],\n      proration_behavior: \"none\",\n      billing_cycle_anchor: \"unchanged\",\n    })\n  }\n}\n\n\u002F\u002F Preview proration before confirming upgrade\nexport async function previewProration(subscriptionId: string, newPriceId: string) {\n  const subscription = await stripe.subscriptions.retrieve(subscriptionId)\n  const prorationDate = Math.floor(Date.now() \u002F 1000)\n\n  const invoice = await stripe.invoices.retrieveUpcoming({\n    customer: subscription.customer as string,\n    subscription: subscriptionId,\n    subscription_items: [{ id: subscription.items.data[0].id, price: newPriceId }],\n    subscription_proration_date: prorationDate,\n  })\n\n  return {\n    amountDue: invoice.amount_due,\n    prorationDate,\n    lineItems: invoice.lines.data,\n  }\n}\n```\n\n---\n\n## Complete Webhook Handler (Idempotent)\n\n```typescript\n\u002F\u002F app\u002Fapi\u002Fwebhooks\u002Fstripe\u002Froute.ts\nimport { NextResponse } from \"next\u002Fserver\"\nimport { headers } from \"next\u002Fheaders\"\nimport { stripe } from \"@\u002Flib\u002Fstripe\"\nimport { db } from \"@\u002Flib\u002Fdb\"\nimport Stripe from \"stripe\"\n\n\u002F\u002F Processed events table to ensure idempotency\nasync function hasProcessedEvent(eventId: string): Promise\u003Cboolean> {\n  const existing = await db.stripeEvent.findUnique({ where: { id: eventId } })\n  return !!existing\n}\n\nasync function markEventProcessed(eventId: string, type: string) {\n  await db.stripeEvent.create({ data: { id: eventId, type, processedAt: new Date() } })\n}\n\nexport async function POST(req: Request) {\n  const body = await req.text()\n  const signature = headers().get(\"stripe-signature\")!\n\n  let event: Stripe.Event\n  try {\n    event = stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET!)\n  } catch (err) {\n    console.error(\"Webhook signature verification failed:\", err)\n    return NextResponse.json({ error: \"Invalid signature\" }, { status: 400 })\n  }\n\n  \u002F\u002F Idempotency check\n  if (await hasProcessedEvent(event.id)) {\n    return NextResponse.json({ received: true, skipped: true })\n  }\n\n  try {\n    switch (event.type) {\n      case \"checkout.session.completed\":\n        await handleCheckoutCompleted(event.data.object as Stripe.Checkout.Session)\n        break\n\n      case \"customer.subscription.created\":\n      case \"customer.subscription.updated\":\n        await handleSubscriptionUpdated(event.data.object as Stripe.Subscription)\n        break\n\n      case \"customer.subscription.deleted\":\n        await handleSubscriptionDeleted(event.data.object as Stripe.Subscription)\n        break\n\n      case \"invoice.payment_succeeded\":\n        await handleInvoicePaymentSucceeded(event.data.object as Stripe.Invoice)\n        break\n\n      case \"invoice.payment_failed\":\n        await handleInvoicePaymentFailed(event.data.object as Stripe.Invoice)\n        break\n\n      default:\n        console.log(`Unhandled event type: ${event.type}`)\n    }\n\n    await markEventProcessed(event.id, event.type)\n    return NextResponse.json({ received: true })\n  } catch (err) {\n    console.error(`Error processing webhook ${event.type}:`, err)\n    \u002F\u002F Return 500 so Stripe retries — don't mark as processed\n    return NextResponse.json({ error: \"Processing failed\" }, { status: 500 })\n  }\n}\n\nasync function handleCheckoutCompleted(session: Stripe.Checkout.Session) {\n  if (session.mode !== \"subscription\") return\n  \n  const userId = session.metadata?.userId\n  if (!userId) throw new Error(\"No userId in checkout session metadata\")\n\n  const subscription = await stripe.subscriptions.retrieve(session.subscription as string)\n  \n  await db.user.update({\n    where: { id: userId },\n    data: {\n      stripeCustomerId: session.customer as string,\n      stripeSubscriptionId: subscription.id,\n      stripePriceId: subscription.items.data[0].price.id,\n      stripeCurrentPeriodEnd: new Date(subscription.current_period_end * 1000),\n      subscriptionStatus: subscription.status,\n      hasHadTrial: true,\n    },\n  })\n}\n\nasync function handleSubscriptionUpdated(subscription: Stripe.Subscription) {\n  const user = await db.user.findUnique({\n    where: { stripeSubscriptionId: subscription.id },\n  })\n  if (!user) {\n    \u002F\u002F Look up by customer ID as fallback\n    const customer = await db.user.findUnique({\n      where: { stripeCustomerId: subscription.customer as string },\n    })\n    if (!customer) throw new Error(`No user found for subscription ${subscription.id}`)\n  }\n\n  await db.user.update({\n    where: { stripeSubscriptionId: subscription.id },\n    data: {\n      stripePriceId: subscription.items.data[0].price.id,\n      stripeCurrentPeriodEnd: new Date(subscription.current_period_end * 1000),\n      subscriptionStatus: subscription.status,\n      cancelAtPeriodEnd: subscription.cancel_at_period_end,\n    },\n  })\n}\n\nasync function handleSubscriptionDeleted(subscription: Stripe.Subscription) {\n  await db.user.update({\n    where: { stripeSubscriptionId: subscription.id },\n    data: {\n      stripeSubscriptionId: null,\n      stripePriceId: null,\n      stripeCurrentPeriodEnd: null,\n      subscriptionStatus: \"canceled\",\n    },\n  })\n}\n\nasync function handleInvoicePaymentFailed(invoice: Stripe.Invoice) {\n  if (!invoice.subscription) return\n  const attemptCount = invoice.attempt_count\n  \n  await db.user.update({\n    where: { stripeSubscriptionId: invoice.subscription as string },\n    data: { subscriptionStatus: \"past_due\" },\n  })\n\n  if (attemptCount >= 3) {\n    \u002F\u002F Send final dunning email\n    await sendDunningEmail(invoice.customer_email!, \"final\")\n  } else {\n    await sendDunningEmail(invoice.customer_email!, \"retry\")\n  }\n}\n\nasync function handleInvoicePaymentSucceeded(invoice: Stripe.Invoice) {\n  if (!invoice.subscription) return\n\n  await db.user.update({\n    where: { stripeSubscriptionId: invoice.subscription as string },\n    data: {\n      subscriptionStatus: \"active\",\n      stripeCurrentPeriodEnd: new Date(invoice.period_end * 1000),\n    },\n  })\n}\n```\n\n---\n\n## Usage-Based Billing\n\n```typescript\n\u002F\u002F Report usage for metered subscriptions\nexport async function reportUsage(subscriptionItemId: string, quantity: number) {\n  await stripe.subscriptionItems.createUsageRecord(subscriptionItemId, {\n    quantity,\n    timestamp: Math.floor(Date.now() \u002F 1000),\n    action: \"increment\",\n  })\n}\n\n\u002F\u002F Example: report API calls in middleware\nexport async function trackApiCall(userId: string) {\n  const user = await db.user.findUnique({ where: { id: userId } })\n  if (user?.stripeSubscriptionId) {\n    const subscription = await stripe.subscriptions.retrieve(user.stripeSubscriptionId)\n    const meteredItem = subscription.items.data.find(\n      (item) => item.price.recurring?.usage_type === \"metered\"\n    )\n    if (meteredItem) {\n      await reportUsage(meteredItem.id, 1)\n    }\n  }\n}\n```\n\n---\n\n## Customer Portal\n\n```typescript\n\u002F\u002F app\u002Fapi\u002Fbilling\u002Fportal\u002Froute.ts\nimport { NextResponse } from \"next\u002Fserver\"\nimport { stripe } from \"@\u002Flib\u002Fstripe\"\nimport { getAuthUser } from \"@\u002Flib\u002Fauth\"\n\nexport async function POST() {\n  const user = await getAuthUser()\n  if (!user?.stripeCustomerId) {\n    return NextResponse.json({ error: \"No billing account\" }, { status: 400 })\n  }\n\n  const portalSession = await stripe.billingPortal.sessions.create({\n    customer: user.stripeCustomerId,\n    return_url: `${process.env.NEXT_PUBLIC_APP_URL}\u002Fsettings\u002Fbilling`,\n  })\n\n  return NextResponse.json({ url: portalSession.url })\n}\n```\n\n---\n\n## Testing with Stripe CLI\n\n```bash\n# Install Stripe CLI\nbrew install stripe\u002Fstripe-cli\u002Fstripe\n\n# Login\nstripe login\n\n# Forward webhooks to local dev\nstripe listen --forward-to localhost:3000\u002Fapi\u002Fwebhooks\u002Fstripe\n\n# Trigger specific events for testing\nstripe trigger checkout.session.completed\nstripe trigger customer.subscription.updated\nstripe trigger invoice.payment_failed\n\n# Test with specific customer\nstripe trigger customer.subscription.updated \\\n  --override subscription:customer=cus_xxx\n\n# View recent events\nstripe events list --limit 10\n\n# Test cards\n# Success: 4242 4242 4242 4242\n# Requires auth: 4000 0025 0000 3155\n# Decline: 4000 0000 0000 9995\n# Insufficient funds: 4000 0000 0000 9995\n```\n\n---\n\n## Feature Gating Helper\n\n```typescript\n\u002F\u002F lib\u002Fsubscription.ts\nexport function isSubscriptionActive(user: { subscriptionStatus: string | null, stripeCurrentPeriodEnd: Date | null }) {\n  if (!user.subscriptionStatus) return false\n  if (user.subscriptionStatus === \"active\" || user.subscriptionStatus === \"trialing\") return true\n  \u002F\u002F Grace period: past_due but not yet expired\n  if (user.subscriptionStatus === \"past_due\" && user.stripeCurrentPeriodEnd) {\n    return user.stripeCurrentPeriodEnd > new Date()\n  }\n  return false\n}\n\n\u002F\u002F Middleware usage\nexport async function requireActiveSubscription() {\n  const user = await getAuthUser()\n  if (!isSubscriptionActive(user)) {\n    redirect(\"\u002Fbilling?reason=subscription_required\")\n  }\n}\n```\n\n---\n\n## Common Pitfalls\n\n- **Webhook delivery order not guaranteed** — always re-fetch from Stripe API, never trust event data alone for DB updates\n- **Double-processing webhooks** — Stripe retries on 500; always use idempotency table\n- **Trial conversion tracking** — store `hasHadTrial: true` in DB to prevent trial abuse\n- **Proration surprises** — always preview proration before upgrade; show user the amount before confirming\n- **Customer portal not configured** — must enable features in Stripe dashboard under Billing → Customer portal settings\n- **Missing metadata on checkout** — always pass `userId` in metadata; can't link subscription to user without it\n","# Stripe Integration Expert\n\n**Tier:** POWERFUL  \n**Category:** Engineering Team  \n**Domain:** Payments \u002F Billing Infrastructure\n\n---\n\n## Overview\n\nImplement production-grade Stripe integrations: subscriptions with trials and proration, one-time payments, usage-based billing, checkout sessions, idempotent webhook handlers, customer portal, and invoicing. Covers Next.js, Express, and Django patterns.\n\n---\n\n## Core Capabilities\n\n- Subscription lifecycle management (create, upgrade, downgrade, cancel, pause)\n- Trial handling and conversion tracking\n- Proration calculation and credit application\n- Usage-based billing with metered pricing\n- Idempotent webhook handlers with signature verification\n- Customer portal integration\n- Invoice generation and PDF access\n- Full Stripe CLI local testing setup\n\n---\n\n## When to Use\n\n- Adding subscription billing to any web app\n- Implementing plan upgrades\u002Fdowngrades with proration\n- Building usage-based or seat-based billing\n- Debugging webhook delivery failures\n- Migrating from one billing model to another\n\n---\n\n## Subscription Lifecycle State Machine\n\n```\nFREE_TRIAL ──paid──► ACTIVE ──cancel──► CANCEL_PENDING ──period_end──► CANCELED\n     │                  │                                                    │\n     │               downgrade                                            reactivate\n     │                  ▼                                                    │\n     │             DOWNGRADING ──period_end──► ACTIVE (lower plan)           │\n     │                                                                        │\n     └──trial_end without payment──► PAST_DUE ──payment_failed 3x──► CANCELED\n                                          │\n                                     payment_success\n                                          │\n                                          ▼\n                                        ACTIVE\n```\n\n### DB subscription status values:\n`trialing | active | past_due | canceled | cancel_pending | paused | unpaid`\n\n---\n\n## Stripe Client Setup\n\n```typescript\n\u002F\u002F lib\u002Fstripe.ts\nimport Stripe from \"stripe\"\n\nexport const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {\n  apiVersion: \"2024-04-10\",\n  typescript: true,\n  appInfo: {\n    name: \"myapp\",\n    version: \"1.0.0\",\n  },\n})\n\n\u002F\u002F Price IDs by plan (set in env)\nexport const PLANS = {\n  starter: {\n    monthly: process.env.STRIPE_STARTER_MONTHLY_PRICE_ID!,\n    yearly: process.env.STRIPE_STARTER_YEARLY_PRICE_ID!,\n    features: [\"5 projects\", \"10k events\"],\n  },\n  pro: {\n    monthly: process.env.STRIPE_PRO_MONTHLY_PRICE_ID!,\n    yearly: process.env.STRIPE_PRO_YEARLY_PRICE_ID!,\n    features: [\"Unlimited projects\", \"1M events\"],\n  },\n} as const\n```\n\n---\n\n## Checkout Session (Next.js App Router)\n\n```typescript\n\u002F\u002F app\u002Fapi\u002Fbilling\u002Fcheckout\u002Froute.ts\nimport { NextResponse } from \"next\u002Fserver\"\nimport { stripe } from \"@\u002Flib\u002Fstripe\"\nimport { getAuthUser } from \"@\u002Flib\u002Fauth\"\nimport { db } from \"@\u002Flib\u002Fdb\"\n\nexport async function POST(req: Request) {\n  const user = await getAuthUser()\n  if (!user) return NextResponse.json({ error: \"Unauthorized\" }, { status: 401 })\n\n  const { priceId, interval = \"monthly\" } = await req.json()\n\n  \u002F\u002F Get or create Stripe customer\n  let stripeCustomerId = user.stripeCustomerId\n  if (!stripeCustomerId) {\n    const customer = await stripe.customers.create({\n      email: user.email,\n      name: \"username-undefined\"\n      metadata: { userId: user.id },\n    })\n    stripeCustomerId = customer.id\n    await db.user.update({ where: { id: user.id }, data: { stripeCustomerId } })\n  }\n\n  const session = await stripe.checkout.sessions.create({\n    customer: stripeCustomerId,\n    mode: \"subscription\",\n    payment_method_types: [\"card\"],\n    line_items: [{ price: priceId, quantity: 1 }],\n    allow_promotion_codes: true,\n    subscription_data: {\n      trial_period_days: user.hasHadTrial ? undefined : 14,\n      metadata: { userId: user.id },\n    },\n    success_url: `${process.env.NEXT_PUBLIC_APP_URL}\u002Fdashboard?session_id={CHECKOUT_SESSION_ID}`,\n    cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}\u002Fpricing`,\n    metadata: { userId: user.id },\n  })\n\n  return NextResponse.json({ url: session.url })\n}\n```\n\n---\n\n## Subscription Upgrade\u002FDowngrade\n\n```typescript\n\u002F\u002F lib\u002Fbilling.ts\nexport async function changeSubscriptionPlan(\n  subscriptionId: string,\n  newPriceId: string,\n  immediate = false\n) {\n  const subscription = await stripe.subscriptions.retrieve(subscriptionId)\n  const currentItem = subscription.items.data[0]\n\n  if (immediate) {\n    \u002F\u002F Upgrade: apply immediately with proration\n    return stripe.subscriptions.update(subscriptionId, {\n      items: [{ id: currentItem.id, price: newPriceId }],\n      proration_behavior: \"always_invoice\",\n      billing_cycle_anchor: \"unchanged\",\n    })\n  } else {\n    \u002F\u002F Downgrade: apply at period end, no proration\n    return stripe.subscriptions.update(subscriptionId, {\n      items: [{ id: currentItem.id, price: newPriceId }],\n      proration_behavior: \"none\",\n      billing_cycle_anchor: \"unchanged\",\n    })\n  }\n}\n\n\u002F\u002F Preview proration before confirming upgrade\nexport async function previewProration(subscriptionId: string, newPriceId: string) {\n  const subscription = await stripe.subscriptions.retrieve(subscriptionId)\n  const prorationDate = Math.floor(Date.now() \u002F 1000)\n\n  const invoice = await stripe.invoices.retrieveUpcoming({\n    customer: subscription.customer as string,\n    subscription: subscriptionId,\n    subscription_items: [{ id: subscription.items.data[0].id, price: newPriceId }],\n    subscription_proration_date: prorationDate,\n  })\n\n  return {\n    amountDue: invoice.amount_due,\n    prorationDate,\n    lineItems: invoice.lines.data,\n  }\n}\n```\n\n---\n\n## Complete Webhook Handler (Idempotent)\n\n```typescript\n\u002F\u002F app\u002Fapi\u002Fwebhooks\u002Fstripe\u002Froute.ts\nimport { NextResponse } from \"next\u002Fserver\"\nimport { headers } from \"next\u002Fheaders\"\nimport { stripe } from \"@\u002Flib\u002Fstripe\"\nimport { db } from \"@\u002Flib\u002Fdb\"\nimport Stripe from \"stripe\"\n\n\u002F\u002F Processed events table to ensure idempotency\nasync function hasProcessedEvent(eventId: string): Promise\u003Cboolean> {\n  const existing = await db.stripeEvent.findUnique({ where: { id: eventId } })\n  return !!existing\n}\n\nasync function markEventProcessed(eventId: string, type: string) {\n  await db.stripeEvent.create({ data: { id: eventId, type, processedAt: new Date() } })\n}\n\nexport async function POST(req: Request) {\n  const body = await req.text()\n  const signature = headers().get(\"stripe-signature\")!\n\n  let event: Stripe.Event\n  try {\n    event = stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET!)\n  } catch (err) {\n    console.error(\"Webhook signature verification failed:\", err)\n    return NextResponse.json({ error: \"Invalid signature\" }, { status: 400 })\n  }\n\n  \u002F\u002F Idempotency check\n  if (await hasProcessedEvent(event.id)) {\n    return NextResponse.json({ received: true, skipped: true })\n  }\n\n  try {\n    switch (event.type) {\n      case \"checkout.session.completed\":\n        await handleCheckoutCompleted(event.data.object as Stripe.Checkout.Session)\n        break\n\n      case \"customer.subscription.created\":\n      case \"customer.subscription.updated\":\n        await handleSubscriptionUpdated(event.data.object as Stripe.Subscription)\n        break\n\n      case \"customer.subscription.deleted\":\n        await handleSubscriptionDeleted(event.data.object as Stripe.Subscription)\n        break\n\n      case \"invoice.payment_succeeded\":\n        await handleInvoicePaymentSucceeded(event.data.object as Stripe.Invoice)\n        break\n\n      case \"invoice.payment_failed\":\n        await handleInvoicePaymentFailed(event.data.object as Stripe.Invoice)\n        break\n\n      default:\n        console.log(`Unhandled event type: ${event.type}`)\n    }\n\n    await markEventProcessed(event.id, event.type)\n    return NextResponse.json({ received: true })\n  } catch (err) {\n    console.error(`Error processing webhook ${event.type}:`, err)\n    \u002F\u002F Return 500 so Stripe retries — don't mark as processed\n    return NextResponse.json({ error: \"Processing failed\" }, { status: 500 })\n  }\n}\n\nasync function handleCheckoutCompleted(session: Stripe.Checkout.Session) {\n  if (session.mode !== \"subscription\") return\n  \n  const userId = session.metadata?.userId\n  if (!userId) throw new Error(\"No userId in checkout session metadata\")\n\n  const subscription = await stripe.subscriptions.retrieve(session.subscription as string)\n  \n  await db.user.update({\n    where: { id: userId },\n    data: {\n      stripeCustomerId: session.customer as string,\n      stripeSubscriptionId: subscription.id,\n      stripePriceId: subscription.items.data[0].price.id,\n      stripeCurrentPeriodEnd: new Date(subscription.current_period_end * 1000),\n      subscriptionStatus: subscription.status,\n      hasHadTrial: true,\n    },\n  })\n}\n\nasync function handleSubscriptionUpdated(subscription: Stripe.Subscription) {\n  const user = await db.user.findUnique({\n    where: { stripeSubscriptionId: subscription.id },\n  })\n  if (!user) {\n    \u002F\u002F Look up by customer ID as fallback\n    const customer = await db.user.findUnique({\n      where: { stripeCustomerId: subscription.customer as string },\n    })\n    if (!customer) throw new Error(`No user found for subscription ${subscription.id}`)\n  }\n\n  await db.user.update({\n    where: { stripeSubscriptionId: subscription.id },\n    data: {\n      stripePriceId: subscription.items.data[0].price.id,\n      stripeCurrentPeriodEnd: new Date(subscription.current_period_end * 1000),\n      subscriptionStatus: subscription.status,\n      cancelAtPeriodEnd: subscription.cancel_at_period_end,\n    },\n  })\n}\n\nasync function handleSubscriptionDeleted(subscription: Stripe.Subscription) {\n  await db.user.update({\n    where: { stripeSubscriptionId: subscription.id },\n    data: {\n      stripeSubscriptionId: null,\n      stripePriceId: null,\n      stripeCurrentPeriodEnd: null,\n      subscriptionStatus: \"canceled\",\n    },\n  })\n}\n\nasync function handleInvoicePaymentFailed(invoice: Stripe.Invoice) {\n  if (!invoice.subscription) return\n  const attemptCount = invoice.attempt_count\n  \n  await db.user.update({\n    where: { stripeSubscriptionId: invoice.subscription as string },\n    data: { subscriptionStatus: \"past_due\" },\n  })\n\n  if (attemptCount >= 3) {\n    \u002F\u002F Send final dunning email\n    await sendDunningEmail(invoice.customer_email!, \"final\")\n  } else {\n    await sendDunningEmail(invoice.customer_email!, \"retry\")\n  }\n}\n\nasync function handleInvoicePaymentSucceeded(invoice: Stripe.Invoice) {\n  if (!invoice.subscription) return\n\n  await db.user.update({\n    where: { stripeSubscriptionId: invoice.subscription as string },\n    data: {\n      subscriptionStatus: \"active\",\n      stripeCurrentPeriodEnd: new Date(invoice.period_end * 1000),\n    },\n  })\n}\n```\n\n---\n\n## Usage-Based Billing\n\n```typescript\n\u002F\u002F Report usage for metered subscriptions\nexport async function reportUsage(subscriptionItemId: string, quantity: number) {\n  await stripe.subscriptionItems.createUsageRecord(subscriptionItemId, {\n    quantity,\n    timestamp: Math.floor(Date.now() \u002F 1000),\n    action: \"increment\",\n  })\n}\n\n\u002F\u002F Example: report API calls in middleware\nexport async function trackApiCall(userId: string) {\n  const user = await db.user.findUnique({ where: { id: userId } })\n  if (user?.stripeSubscriptionId) {\n    const subscription = await stripe.subscriptions.retrieve(user.stripeSubscriptionId)\n    const meteredItem = subscription.items.data.find(\n      (item) => item.price.recurring?.usage_type === \"metered\"\n    )\n    if (meteredItem) {\n      await reportUsage(meteredItem.id, 1)\n    }\n  }\n}\n```\n\n---\n\n## Customer Portal\n\n```typescript\n\u002F\u002F app\u002Fapi\u002Fbilling\u002Fportal\u002Froute.ts\nimport { NextResponse } from \"next\u002Fserver\"\nimport { stripe } from \"@\u002Flib\u002Fstripe\"\nimport { getAuthUser } from \"@\u002Flib\u002Fauth\"\n\nexport async function POST() {\n  const user = await getAuthUser()\n  if (!user?.stripeCustomerId) {\n    return NextResponse.json({ error: \"No billing account\" }, { status: 400 })\n  }\n\n  const portalSession = await stripe.billingPortal.sessions.create({\n    customer: user.stripeCustomerId,\n    return_url: `${process.env.NEXT_PUBLIC_APP_URL}\u002Fsettings\u002Fbilling`,\n  })\n\n  return NextResponse.json({ url: portalSession.url })\n}\n```\n\n---\n\n## Testing with Stripe CLI\n\n```bash\n# Install Stripe CLI\nbrew install stripe\u002Fstripe-cli\u002Fstripe\n\n# Login\nstripe login\n\n# Forward webhooks to local dev\nstripe listen --forward-to localhost:3000\u002Fapi\u002Fwebhooks\u002Fstripe\n\n# Trigger specific events for testing\nstripe trigger checkout.session.completed\nstripe trigger customer.subscription.updated\nstripe trigger invoice.payment_failed\n\n# Test with specific customer\nstripe trigger customer.subscription.updated \\\n  --override subscription:customer=cus_xxx\n\n# View recent events\nstripe events list --limit 10\n\n# Test cards\n# Success: 4242 4242 4242 4242\n# Requires auth: 4000 0025 0000 3155\n# Decline: 4000 0000 0000 9995\n# Insufficient funds: 4000 0000 0000 9995\n```\n\n---\n\n## Feature Gating Helper\n\n```typescript\n\u002F\u002F lib\u002Fsubscription.ts\nexport function isSubscriptionActive(user: { subscriptionStatus: string | null, stripeCurrentPeriodEnd: Date | null }) {\n  if (!user.subscriptionStatus) return false\n  if (user.subscriptionStatus === \"active\" || user.subscriptionStatus === \"trialing\") return true\n  \u002F\u002F Grace period: past_due but not yet expired\n  if (user.subscriptionStatus === \"past_due\" && user.stripeCurrentPeriodEnd) {\n    return user.stripeCurrentPeriodEnd > new Date()\n  }\n  return false\n}\n\n\u002F\u002F Middleware usage\nexport async function requireActiveSubscription() {\n  const user = await getAuthUser()\n  if (!isSubscriptionActive(user)) {\n    redirect(\"\u002Fbilling?reason=subscription_required\")\n  }\n}\n```\n\n---\n\n## Common Pitfalls\n\n- **Webhook delivery order not guaranteed** — always re-fetch from Stripe API, never trust event data alone for DB updates\n- **Double-processing webhooks** — Stripe retries on 500; always use idempotency table\n- **Trial conversion tracking** — store `hasHadTrial: true` in DB to prevent trial abuse\n- **Proration surprises** — always preview proration before upgrade; show user the amount before confirming\n- **Customer portal not configured** — must enable features in Stripe dashboard under Billing → Customer portal settings\n- **Missing metadata on checkout** — always pass `userId` in metadata; can't link subscription to user without it\n",{"name":5,"description":7},"main","0b48cbb0e6d5be6ff68c0f6b28bc0c65ee21c4a1","engineering-team\u002Fstripe-integration-expert",2,"https:\u002F\u002Fgithub.com\u002Fafaizalam2003\u002FMaster-claude-skill","https:\u002F\u002Fgithub.com\u002Fafaizalam2003","https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fmdfaizalam2003\u002F","2026-04-01 04:35:43",{"source":8,"skillName":5,"commands":38,"prompt":48,"zipAvailable":49},[39,42,45],{"tool":40,"command":41},"npx","npx skills add afaizalam2003\u002FMaster-claude-skill --skill stripe-integration-expert",{"tool":43,"command":44},"bunx","bunx skills add afaizalam2003\u002FMaster-claude-skill --skill stripe-integration-expert",{"tool":46,"command":47},"pnpm","pnpm dlx skills add afaizalam2003\u002FMaster-claude-skill --skill stripe-integration-expert","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fafaizalam2003\u002FMaster-claude-skill\u002Fblob\u002Fmain\u002Fengineering-team\u002Fstripe-integration-expert\u002FSKILL.md",true,[],[52,84,109,134,166,186],{"skillNo":53,"name":54,"nameZh":6,"description":55,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":56,"categories":57,"keywords":63,"publishedAt":24},"01M6KC4E8MYR9DBAZTAXHKYMMH","soc2-compliance","Use when the user asks to prepare for SOC 2 audits, map Trust Service Criteria, build control matrices, collect audit evidence, perform gap analysis, or assess SOC 2 Type I vs Type II readiness.","https:\u002F\u002Fgithub.com\u002Fafaizalam2003\u002FMaster-claude-skill\u002Fblob\u002Fmain\u002Fra-qm-team\u002Fsoc2-compliance\u002FSKILL.md",[58],{"categoryNo":59,"categoryName":60,"taxonomyType":61,"taxonomyTypeDesc":62},"01JBSEEDCAT000000000000037","律师与法务","PROFESSION","适用职业",[64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83],"compliance","soc2","soc2 compliance","audit evidence","audit","collect","soc","analysis","use","evidence","trust","build","map","control","type","gap","readiness","gap analysis","assess","for",{"skillNo":85,"name":86,"nameZh":6,"description":87,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":88,"categories":89,"keywords":90,"publishedAt":24},"01M6KC461R3GKGZ1TAYHVR1MW9","risk-management-specialist","Medical device risk management specialist implementing ISO 14971 throughout product lifecycle. Provides risk analysis, risk evaluation, risk control, and post-production information analysis. Use when user mentions risk management, ISO 14971, risk analysis, FMEA, fault tree analysis, hazard identification, risk control, risk matrix, benefit-risk analysis, residual risk, risk acceptability, or post-market risk.","https:\u002F\u002Fgithub.com\u002Fafaizalam2003\u002FMaster-claude-skill\u002Fblob\u002Fmain\u002Fra-qm-team\u002Frisk-management-specialist\u002FSKILL.md",[],[91,92,93,94,95,96,97,98,71,72,99,100,101,102,103,104,105,106,107,108],"risk","risk management specialist","management","risk management","risk evaluation","product","Evaluation","lifecycle","risk matrix","iso","medical","iso 14971","identification","information","post-production","tree","matrix","implementing",{"skillNo":110,"name":111,"nameZh":6,"description":112,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":113,"categories":114,"keywords":115,"publishedAt":24},"01M6KC3HF5ZFSSP2NZF68PEPFB","regulatory-affairs-head","Senior Regulatory Affairs Manager for HealthTech and MedTech companies. Prepares FDA 510(k), De Novo, and PMA submission packages; analyzes regulatory pathways for new medical devices; drafts responses to FDA deficiency letters and Notified Body queries; develops CE marking technical documentation under EU MDR 2017\u002F745; coordinates multi-market approval strategies across FDA, EU, Health Canada, PMDA, and NMPA; and maintains regulatory intelligence on evolving standards. Use when users need to plan or execute FDA submissions, navigate 510(k) or PMA approval processes, achieve CE marking, prepare pre-submission meeting materials, write regulatory strategy documents, respond to agency queries, or manage compliance documentation for medical device market access.","https:\u002F\u002Fgithub.com\u002Fafaizalam2003\u002FMaster-claude-skill\u002Fblob\u002Fmain\u002Fra-qm-team\u002Fregulatory-affairs-head\u002FSKILL.md",[],[116,117,118,119,120,121,122,123,124,72,125,126,127,128,129,130,131,132,133,83],"regulatory affairs head","regulatory","head","documents","devices","health","plan","write","strategy","standards","queries","manage","strategies","de","users","respond","medical devices","manager",{"skillNo":135,"name":136,"nameZh":6,"description":137,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":138,"categories":139,"keywords":148,"publishedAt":24},"01M6KC34Z8M1559EY1CXYBJB1A","quality-manager-qms-iso13485","ISO 13485 Quality Management System implementation and maintenance for medical device organizations. Provides QMS design, documentation control, internal auditing, CAPA management, and certification support. Use when working with medical device quality systems, preparing for ISO 13485 audits, managing regulatory compliance documentation, setting up corrective actions, or building audit preparation programs. Useful for quality management, audit preparation, regulatory compliance, medical device documentation, and corrective action workflows.","https:\u002F\u002Fgithub.com\u002Fafaizalam2003\u002FMaster-claude-skill\u002Fblob\u002Fmain\u002Fra-qm-team\u002Fquality-manager-qms-iso13485\u002FSKILL.md",[140,145],{"categoryNo":141,"categoryName":142,"taxonomyType":143,"taxonomyTypeDesc":144},"01JBSEEDCAT000000000000015","视觉设计","SCENARIO","使用场景",{"categoryNo":146,"categoryName":147,"taxonomyType":61,"taxonomyTypeDesc":62},"01JBSEEDCAT000000000000032","设计师",[149,150,133,151,152,153,154,155,156,72,157,158,159,160,161,162,163,164,83,165],"quality manager qms iso13485","quality","design","workflows","validation","actions","certification","system","maintenance","document","systems","records","ISO 13485","organizations","corrective action","internal audit","preparation",{"skillNo":167,"name":168,"nameZh":6,"description":169,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":170,"categories":171,"keywords":173,"publishedAt":24},"01M6KC2FXHBHXYX2ZZ17Y3GDB4","quality-manager-qmr","Senior Quality Manager Responsible Person (QMR) for HealthTech and MedTech companies. Provides quality system governance, management review leadership, regulatory compliance oversight, and quality performance monitoring per ISO 13485 Clause 5.5.2.","https:\u002F\u002Fgithub.com\u002Fafaizalam2003\u002FMaster-claude-skill\u002Fblob\u002Fmain\u002Fra-qm-team\u002Fquality-manager-qmr\u002FSKILL.md",[172],{"categoryNo":59,"categoryName":60,"taxonomyType":61,"taxonomyTypeDesc":62},[174,150,133,175,176,177,178,179,64,180,156,181,117,182,183,184,100,161,83,185],"quality manager qmr","review","policy","performance","governance","monitoring","regulatory compliance","oversight","cost","culture","leadership","person",{"skillNo":187,"name":188,"nameZh":6,"description":189,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":190,"categories":191,"keywords":199,"publishedAt":24},"01M6KC1VRR5JTDBNZANHPS6VR6","quality-documentation-manager","Document control system management for medical device QMS. Covers document numbering, version control, change management, and 21 CFR Part 11 compliance. Use for document control procedures, change control workflow, document numbering, version management, electronic signature compliance, or regulatory documentation review.","https:\u002F\u002Fgithub.com\u002Fafaizalam2003\u002FMaster-claude-skill\u002Fblob\u002Fmain\u002Fra-qm-team\u002Fquality-documentation-manager\u002FSKILL.md",[192,195,198],{"categoryNo":193,"categoryName":194,"taxonomyType":143,"taxonomyTypeDesc":144},"01JBSEEDCAT000000000000002","文档处理",{"categoryNo":196,"categoryName":197,"taxonomyType":143,"taxonomyTypeDesc":144},"01JBSEEDCAT000000000000023","Skill与智能体开发",{"categoryNo":59,"categoryName":60,"taxonomyType":61,"taxonomyTypeDesc":62},[200,201,150,133,175,202,203,68,156,204,205,206,207,208,101,209,210,211,212,83],"documentation","quality documentation manager","workflow","change","change management","list","master","system management","documentation review","cfr","21 cfr part 11","change control","retention"]