Use Cases

Claude Codeで決済システム(Stripe)を統合する方法

Claude Codeを使ってStripe決済のサブスクリプション・一回払い・Webhook処理を効率的に実装する方法を解説します。

決済システムの実装にClaude Codeを使うメリット

決済システムはセキュリティと正確性が求められる重要な機能です。Claude CodeはStripeのベストプラクティスに沿った実装を生成し、Webhook処理やエラーハンドリングの見落としを防ぎます。

Stripe Checkoutによる決済

> Stripe Checkoutを使ったサブスクリプション決済を実装して。
> フリー・プロ・エンタープライズの3プランで。
> 成功・キャンセル時のリダイレクトも設定して。

チェックアウトセッションの作成

// src/services/payment-service.ts
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2024-12-18.acacia',
});

const PLANS = {
  pro: {
    priceId: process.env.STRIPE_PRO_PRICE_ID!,
    name: 'Pro',
  },
  enterprise: {
    priceId: process.env.STRIPE_ENTERPRISE_PRICE_ID!,
    name: 'Enterprise',
  },
} as const;

type PlanKey = keyof typeof PLANS;

export class PaymentService {
  async createCheckoutSession(userId: string, plan: PlanKey) {
    // 既存のStripe顧客を検索、なければ作成
    let customer = await this.findOrCreateCustomer(userId);

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

    return { url: session.url };
  }

  async createPortalSession(userId: string) {
    const customer = await this.getCustomerByUserId(userId);

    const session = await stripe.billingPortal.sessions.create({
      customer: customer.id,
      return_url: `${process.env.APP_URL}/settings/billing`,
    });

    return { url: session.url };
  }

  private async findOrCreateCustomer(userId: string) {
    const user = await prisma.user.findUnique({ where: { id: userId } });
    if (!user) throw new Error('User not found');

    if (user.stripeCustomerId) {
      return stripe.customers.retrieve(user.stripeCustomerId) as Promise<Stripe.Customer>;
    }

    const customer = await stripe.customers.create({
      email: user.email,
      metadata: { userId },
    });

    await prisma.user.update({
      where: { id: userId },
      data: { stripeCustomerId: customer.id },
    });

    return customer;
  }

  private async getCustomerByUserId(userId: string) {
    const user = await prisma.user.findUnique({ where: { id: userId } });
    if (!user?.stripeCustomerId) throw new Error('No Stripe customer found');
    return stripe.customers.retrieve(user.stripeCustomerId) as Promise<Stripe.Customer>;
  }
}

APIルートの設定

// src/app/api/checkout/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { PaymentService } from '@/services/payment-service';
import { getSession } from '@/lib/auth';

const paymentService = new PaymentService();

export async function POST(req: NextRequest) {
  const session = await getSession();
  if (!session) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const { plan } = await req.json();
  if (!['pro', 'enterprise'].includes(plan)) {
    return NextResponse.json({ error: 'Invalid plan' }, { status: 400 });
  }

  const checkout = await paymentService.createCheckoutSession(session.userId, plan);
  return NextResponse.json(checkout);
}

Webhook処理

> Stripeのwebhookハンドラーを実装して。
> サブスクリプションの作成・更新・キャンセルを処理して。
> 署名検証も実装して。
// src/app/api/webhooks/stripe/route.ts
import { NextRequest, NextResponse } from 'next/server';
import Stripe from 'stripe';
import { prisma } from '@/lib/db';

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

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

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

  switch (event.type) {
    case 'checkout.session.completed': {
      const session = event.data.object as Stripe.Checkout.Session;
      await handleCheckoutCompleted(session);
      break;
    }

    case 'customer.subscription.updated': {
      const subscription = event.data.object as Stripe.Subscription;
      await handleSubscriptionUpdated(subscription);
      break;
    }

    case 'customer.subscription.deleted': {
      const subscription = event.data.object as Stripe.Subscription;
      await handleSubscriptionCanceled(subscription);
      break;
    }

    case 'invoice.payment_failed': {
      const invoice = event.data.object as Stripe.Invoice;
      await handlePaymentFailed(invoice);
      break;
    }
  }

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

async function handleCheckoutCompleted(session: Stripe.Checkout.Session) {
  const userId = session.metadata?.userId;
  if (!userId) return;

  await prisma.user.update({
    where: { id: userId },
    data: {
      plan: session.metadata?.plan || 'pro',
      stripeSubscriptionId: session.subscription as string,
      subscriptionStatus: 'active',
    },
  });
}

async function handleSubscriptionUpdated(subscription: Stripe.Subscription) {
  const user = await prisma.user.findFirst({
    where: { stripeSubscriptionId: subscription.id },
  });
  if (!user) return;

  await prisma.user.update({
    where: { id: user.id },
    data: { subscriptionStatus: subscription.status },
  });
}

async function handleSubscriptionCanceled(subscription: Stripe.Subscription) {
  const user = await prisma.user.findFirst({
    where: { stripeSubscriptionId: subscription.id },
  });
  if (!user) return;

  await prisma.user.update({
    where: { id: user.id },
    data: {
      plan: 'free',
      subscriptionStatus: 'canceled',
      stripeSubscriptionId: null,
    },
  });
}

async function handlePaymentFailed(invoice: Stripe.Invoice) {
  const customerId = invoice.customer as string;
  const user = await prisma.user.findFirst({
    where: { stripeCustomerId: customerId },
  });
  if (!user) return;

  // 支払い失敗の通知メールを送信
  await sendPaymentFailedEmail(user.email);
}

料金表コンポーネント

function PricingTable() {
  const handleCheckout = async (plan: string) => {
    const res = await fetch('/api/checkout', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ plan }),
    });
    const { url } = await res.json();
    window.location.href = url;
  };

  return (
    <div className="grid gap-6 md:grid-cols-3">
      <PricingCard
        name="Free"
        price="¥0"
        features={['基本機能', '月5プロジェクト']}
        buttonText="現在のプラン"
        disabled
      />
      <PricingCard
        name="Pro"
        price="¥1,980/月"
        features={['全機能', '無制限プロジェクト', '優先サポート']}
        buttonText="Proにアップグレード"
        onSelect={() => handleCheckout('pro')}
        highlighted
      />
      <PricingCard
        name="Enterprise"
        price="¥9,800/月"
        features={['全機能', 'SSO', '専任サポート', 'SLA保証']}
        buttonText="お問い合わせ"
        onSelect={() => handleCheckout('enterprise')}
      />
    </div>
  );
}

まとめ

Claude Codeを使えば、Stripe決済の統合をチェックアウトからWebhook処理まで安全かつ効率的に実装できます。決済はセキュリティに直結するため、コードレビューは必ず行いましょう。プロジェクトの決済方針はCLAUDE.mdに記述しておくことをお勧めします。コード品質の維持にはリファクタリングの自動化も活用してください。

Claude Codeの詳細はAnthropic公式ドキュメントをご覧ください。Stripeの実装ガイドはStripe公式ドキュメントも参照してください。

#Claude Code #Stripe #決済 #サブスクリプション #TypeScript