[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill-d8e4b7e7-7a40-479d-8512-4f6939eea72f":3,"$fMh-Fewh2H2W7WKNY5bYBewbxs0lkYcD9m89VfHVEPmA":42},{"id":4,"title":5,"description":6,"categoryId":7,"moduleId":8,"tags":9,"prompt":10,"icon":11,"source":12,"sourceUrl":13,"authorId":14,"authorName":15,"isPublic":16,"stars":17,"runs":18,"createdAt":19,"updatedAt":19,"module":20,"category":27,"packages":33},"d8e4b7e7-7a40-479d-8512-4f6939eea72f","stripe-integration-expert","生产级Stripe集成：带试用和折算的订阅、一次性付款、按使用量计费、结账会话、幂等webhook处理器、客户门户和发票。涵盖Next.js、Express和Django模式。首次集成Stripe、调试webhook可靠性问题、从其他支付提供商迁移或向现有订阅产品添加按使用量计费时使用。","cat_coding_backend","mod_coding","alirezarezvani,coding","---\nname: \"stripe-integration-expert\"\ndescription: \"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. Use when integrating Stripe for the first time, debugging webhook reliability issues, migrating from a different payment provider, or adding usage-based billing to an existing subscription product.\"\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","","imported","https:\u002F\u002Fgithub.com\u002Falirezarezvani\u002Fclaude-skills","user_system_seed","SkillOPIC",true,136,453,"2026-05-16 13:58:15",{"id":8,"name":21,"slug":22,"icon":23,"description":24,"sort":25,"createdAt":26},"编程开发","coding","mdi-code-braces","代码生成、调试、审查，提升开发效率",2,"2026-05-16 12:53:40",{"id":7,"name":28,"slug":29,"icon":30,"description":31,"moduleId":8,"sort":25,"skillCount":32,"createdAt":26},"后端开发","backend","mdi-server","API、数据库、服务端架构",296,[34],{"id":35,"skillId":4,"version":36,"fileName":37,"fileSize":38,"filePath":39,"fileHash":40,"manifest":41,"createdAt":19},"e1656589-d5ab-40ea-bea6-b7c2950c3951","1.0.0","stripe-integration-expert.zip",4459,"uploads\u002Fskills\u002Fd8e4b7e7-7a40-479d-8512-4f6939eea72f\u002Fstripe-integration-expert.zip","4c05d90eace869a74645b1ce582ce03ec01baae4664088cef6ed58e9c7dd87f7","[{\"path\":\"SKILL.md\",\"isDirectory\":false,\"size\":14948}]",{"code":43,"message":44,"data":45},200,"success",{"items":46,"stats":47,"page":50},[],{"averageRating":48,"totalRatings":48,"ratingCounts":49},0,[48,48,48,48,48],{"limit":51,"offset":48,"hasMore":52,"nextOffset":51,"ratedOnly":16},15,false]