Use Cases

Stripe:Claude Code 实战指南

了解stripe:Claude Code 实战. 包含实用技巧和代码示例。

Stripeサブスクリプション实现を通过 Claude Code 提高效率

サブスクリプション課金はSaaSビジネスの根幹です。プラン更改、トライアル、請求管理、Webhook処理など考慮すべき点が多いですが、借助 Claude CodeStripeの最佳实践に沿った实现を高效地進められます。

プラン定義とCheckout会话

> Stripeサブスクリプション实现。
> Free / Pro / Enterprise の3プランで、
> トライアル期間14日間、プラン更改・取消支持、
> Webhookでの状態同步も含めて。
// src/lib/subscription.ts
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export const PLANS = {
  free: { name: 'Free', priceId: null, features: ['基本機能', '3プロジェクト'] },
  pro: { name: 'Pro', priceId: process.env.STRIPE_PRO_PRICE_ID!, features: ['全機能', '無制限プロジェクト', '優先サポート'] },
  enterprise: { name: 'Enterprise', priceId: process.env.STRIPE_ENT_PRICE_ID!, features: ['全機能', 'SSO', '専任担当', 'SLA'] },
} as const;

export type PlanKey = keyof typeof PLANS;

export async function createSubscription(userId: string, plan: PlanKey) {
  if (plan === 'free') throw new Error('Freeプランにサブスクリプションは不要です');

  const customer = await findOrCreateCustomer(userId);
  const priceId = PLANS[plan].priceId;

  const session = await stripe.checkout.sessions.create({
    customer: customer.id,
    mode: 'subscription',
    line_items: [{ price: priceId!, quantity: 1 }],
    subscription_data: { trial_period_days: 14 },
    success_url: `${process.env.APP_URL}/dashboard?upgraded=true`,
    cancel_url: `${process.env.APP_URL}/pricing`,
    metadata: { userId, plan },
  });

  return session.url;
}

Webhook処理

// src/app/api/webhooks/stripe/route.ts
import { NextRequest, NextResponse } from 'next/server';
import Stripe from 'stripe';
import { prisma } from '@/lib/prisma';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function POST(request: NextRequest) {
  const body = await request.text();
  const signature = request.headers.get('stripe-signature')!;

  let event: Stripe.Event;
  try {
    event = stripe.webhooks.constructEvent(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET!
    );
  } catch {
    return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });
  }

  switch (event.type) {
    case 'customer.subscription.created':
    case 'customer.subscription.updated': {
      const subscription = event.data.object as Stripe.Subscription;
      await prisma.subscription.upsert({
        where: { stripeSubscriptionId: subscription.id },
        create: {
          stripeSubscriptionId: subscription.id,
          stripeCustomerId: subscription.customer as string,
          status: subscription.status,
          plan: subscription.metadata.plan || 'pro',
          currentPeriodEnd: new Date(subscription.current_period_end * 1000),
        },
        update: {
          status: subscription.status,
          currentPeriodEnd: new Date(subscription.current_period_end * 1000),
        },
      });
      break;
    }

    case 'customer.subscription.deleted': {
      const subscription = event.data.object as Stripe.Subscription;
      await prisma.subscription.update({
        where: { stripeSubscriptionId: subscription.id },
        data: { status: 'canceled' },
      });
      break;
    }

    case 'invoice.payment_failed': {
      const invoice = event.data.object as Stripe.Invoice;
      // 支払い失败の通知メールを发送
      await sendPaymentFailedEmail(invoice.customer as string);
      break;
    }
  }

  return NextResponse.json({ received: true });
}

プラン更改処理

// src/lib/subscription.ts(続き)
export async function changePlan(userId: string, newPlan: PlanKey) {
  const sub = await prisma.subscription.findFirst({
    where: { userId, status: 'active' },
  });

  if (!sub) throw new Error('アクティブなサブスクリプションがありません');

  const stripeSubscription = await stripe.subscriptions.retrieve(
    sub.stripeSubscriptionId
  );

  await stripe.subscriptions.update(sub.stripeSubscriptionId, {
    items: [
      {
        id: stripeSubscription.items.data[0].id,
        price: PLANS[newPlan].priceId!,
      },
    ],
    proration_behavior: 'create_prorations', // 日割り計算
    metadata: { plan: newPlan },
  });
}

export async function cancelSubscription(userId: string) {
  const sub = await prisma.subscription.findFirst({
    where: { userId, status: 'active' },
  });

  if (!sub) throw new Error('アクティブなサブスクリプションがありません');

  // 期間終了時に取消(即時ではない)
  await stripe.subscriptions.update(sub.stripeSubscriptionId, {
    cancel_at_period_end: true,
  });
}

顧客ポータル

Stripeの顧客ポータルを使えば、プラン更改・支払い方法の更新・請求書の閲覧をStripe側のUIに任せられます。

export async function createPortalSession(userId: string) {
  const customer = await getCustomerByUserId(userId);
  const session = await stripe.billingPortal.sessions.create({
    customer: customer.id,
    return_url: `${process.env.APP_URL}/settings/billing`,
  });
  return session.url;
}

関連文章

決済全般相关内容请参阅Stripe決済の集成指南、认证周りは认证機能の实现

Stripeの公式指南(stripe.com/docs/billing)も必ず确认吧。

#Claude Code #Stripe #subscription #課金 #TypeScript