payment-integration

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Resources

资源

scripts/
  validate-payments.sh
references/
  payment-patterns.md
scripts/
  validate-payments.sh
references/
  payment-patterns.md

Payment Integration Implementation

支付集成实现

This skill guides you through implementing payment processing in applications, from provider selection to webhook handling. It leverages GoodVibes precision tools for secure, production-ready payment integrations.
本技能将引导你在应用中实现支付处理功能,从支付服务商选择到Webhook处理的全流程。它借助GoodVibes精准工具,帮助你构建安全、可用于生产环境的支付集成方案。

When to Use This Skill

何时使用此技能

Use this skill when you need to:
  • Set up payment processing (one-time or recurring)
  • Implement checkout flows and payment forms
  • Handle subscription billing and management
  • Process payment webhooks securely
  • Test payment flows in development
  • Ensure PCI compliance and security
  • Migrate between payment providers
当你需要完成以下操作时,可使用本技能:
  • 设置一次性或定期(recurring)支付处理
  • 实现结账流程和支付表单
  • 处理订阅账单与管理
  • 安全处理支付Webhook
  • 在开发环境中测试支付流程
  • 确保PCI合规性与安全性
  • 在不同支付服务商之间迁移

Workflow

工作流程

Follow this sequence for payment integration:
按照以下步骤完成支付集成:

1. Discover Existing Payment Infrastructure

1. 调研现有支付基础设施

Before implementing payment features, understand the current state:
yaml
precision_grep:
  queries:
    - id: payment-libs
      pattern: "stripe|lemonsqueezy|paddle"
      glob: "package.json"
    - id: webhook-routes
      pattern: "(webhook|payment|checkout)"
      glob: "src/**/*.{ts,js,tsx,jsx}"
  output:
    format: files_only
Check for:
  • Existing payment libraries
  • Webhook endpoints
  • Environment variables for API keys
  • Payment-related database models
Check project memory for payment decisions:
yaml
precision_read:
  files:
    - path: ".goodvibes/memory/decisions.md"
      extract: content
  output:
    format: minimal
Search for payment provider choices, checkout flow patterns, and subscription models.
在实现支付功能前,先了解当前项目的状态:
yaml
precision_grep:
  queries:
    - id: payment-libs
      pattern: "stripe|lemonsqueezy|paddle"
      glob: "package.json"
    - id: webhook-routes
      pattern: "(webhook|payment|checkout)"
      glob: "src/**/*.{ts,js,tsx,jsx}"
  output:
    format: files_only
检查以下内容:
  • 已有的支付库
  • Webhook端点
  • 用于API密钥的环境变量
  • 支付相关的数据库模型
查看项目记忆中的支付决策:
yaml
precision_read:
  files:
    - path: ".goodvibes/memory/decisions.md"
      extract: content
  output:
    format: minimal
搜索支付服务商选择、结账流程模式以及订阅模型相关内容。

2. Select Payment Provider

2. 选择支付服务商

Choose based on your requirements:
Use Stripe when:
  • You need maximum flexibility and control
  • Supporting complex subscription models
  • Requiring advanced features (invoicing, tax, fraud detection)
  • Building custom payment flows
  • Need extensive API documentation
Use LemonSqueezy when:
  • You're selling digital products/SaaS
  • Want merchant of record (handles taxes, VAT, compliance)
  • Need simple subscription management
  • Prefer minimal compliance burden
  • Operating globally with automatic tax handling
Use Paddle when:
  • Selling software/SaaS products
  • Need merchant of record services
  • Want subscription and license management
  • Prefer revenue recovery features
  • Operating in B2B/enterprise markets
Document the decision:
yaml
precision_write:
  files:
    - path: ".goodvibes/memory/decisions.md"
      mode: overwrite
      content: |
        ## Payment Provider Selection
        
        **Decision**: Using [Provider] for payment processing
        **Rationale**: [Why this provider fits requirements]
        **Date**: YYYY-MM-DD
        
        Key factors:
        - [Factor 1]
        - [Factor 2]
        - [Factor 3]
根据你的需求选择合适的服务商:
当以下情况时选择Stripe:
  • 你需要最大的灵活性和控制权
  • 支持复杂的订阅模型
  • 需要高级功能(发票、税务、欺诈检测)
  • 构建自定义支付流程
  • 需要详尽的API文档
当以下情况时选择LemonSqueezy:
  • 你销售数字产品/SaaS服务
  • 希望使用商户代运营服务(处理税务、VAT、合规性)
  • 需要简单的订阅管理
  • 偏好最小化合规负担
  • 面向全球运营,需要自动税务处理
当以下情况时选择Paddle:
  • 销售软件/SaaS产品
  • 需要商户代运营服务
  • 希望进行订阅和许可证管理
  • 偏好营收恢复功能
  • 面向B2B/企业市场运营
记录决策:
yaml
precision_write:
  files:
    - path: ".goodvibes/memory/decisions.md"
      mode: overwrite
      content: |
        ## 支付服务商选择
        
        **决策**: 使用[服务商名称]进行支付处理
        **理由**: [该服务商符合需求的原因]
        **日期**: YYYY-MM-DD
        
        关键因素:
        - [因素1]
        - [因素2]
        - [因素3]

3. Setup and Installation

3. 安装与配置

Install the appropriate SDK:
Stripe:
yaml
precision_exec:
  commands:
    - cmd: "npm install stripe @stripe/stripe-js"
    # @types/stripe is not needed - Stripe ships with built-in types
  verbosity: minimal
LemonSqueezy:
yaml
precision_exec:
  commands:
    - cmd: "npm install @lemonsqueezy/lemonsqueezy.js"
  verbosity: minimal
Paddle:
yaml
precision_exec:
  commands:
    - cmd: "npm install @paddle/paddle-node-sdk"
  verbosity: minimal
Configure environment variables:
Create
.env.example
template:
yaml
precision_write:
  files:
    - path: ".env.example"
      mode: overwrite
      content: |
        # Payment Provider Configuration
        
        # Stripe (if using Stripe)
        STRIPE_SECRET_KEY=sk_test_...
        STRIPE_PUBLISHABLE_KEY=pk_test_...
        STRIPE_WEBHOOK_SECRET=whsec_...
        
        # LemonSqueezy (if using LemonSqueezy)
        LEMONSQUEEZY_API_KEY=...
        LEMONSQUEEZY_STORE_ID=...
        LEMONSQUEEZY_WEBHOOK_SECRET=...
        
        # Paddle (if using Paddle)
        PADDLE_API_KEY=...
        PADDLE_WEBHOOK_SECRET=...
        PADDLE_ENVIRONMENT=sandbox
Verify secrets are gitignored:
yaml
precision_grep:
  queries:
    - id: check-gitignore
      pattern: "^\\.env$"
      path: ".gitignore"
  output:
    format: files_only
If not found, add
.env
to
.gitignore
.
安装对应的SDK:
Stripe:
yaml
precision_exec:
  commands:
    - cmd: "npm install stripe @stripe/stripe-js"
    # @types/stripe 无需安装 - Stripe自带内置类型
  verbosity: minimal
LemonSqueezy:
yaml
precision_exec:
  commands:
    - cmd: "npm install @lemonsqueezy/lemonsqueezy.js"
  verbosity: minimal
Paddle:
yaml
precision_exec:
  commands:
    - cmd: "npm install @paddle/paddle-node-sdk"
  verbosity: minimal
配置环境变量:
创建
.env.example
模板:
yaml
precision_write:
  files:
    - path: ".env.example"
      mode: overwrite
      content: |
        # 支付服务商配置
        
        # Stripe(如果使用Stripe)
        STRIPE_SECRET_KEY=sk_test_...
        STRIPE_PUBLISHABLE_KEY=pk_test_...
        STRIPE_WEBHOOK_SECRET=whsec_...
        
        # LemonSqueezy(如果使用LemonSqueezy)
        LEMONSQUEEZY_API_KEY=...
        LEMONSQUEEZY_STORE_ID=...
        LEMONSQUEEZY_WEBHOOK_SECRET=...
        
        # Paddle(如果使用Paddle)
        PADDLE_API_KEY=...
        PADDLE_WEBHOOK_SECRET=...
        PADDLE_ENVIRONMENT=sandbox
验证密钥已被git忽略:
yaml
precision_grep:
  queries:
    - id: check-gitignore
      pattern: "^\\.env$"
      path: ".gitignore"
  output:
    format: files_only
如果未找到,将
.env
添加到
.gitignore
中。

4. Implement Checkout Flow

4. 实现结账流程

One-Time Payments (Stripe)

一次性支付(Stripe)

Create checkout session endpoint:
yaml
precision_write:
  files:
    - path: "src/app/api/checkout/route.ts"
      content: |
        import { NextRequest, NextResponse } from 'next/server';
        import Stripe from 'stripe';
        import { getServerSession } from 'next-auth';
        import { authOptions } from '@/lib/auth';
        import { z } from 'zod';
        
        const stripeSecretKey = process.env.STRIPE_SECRET_KEY;
        if (!stripeSecretKey) {
          throw new Error('STRIPE_SECRET_KEY is required');
        }
        
        const stripe = new Stripe(stripeSecretKey, {
          apiVersion: '2024-11-20.acacia',
        });
        
        export async function POST(request: NextRequest) {
          try {
            // Check authentication
            const authSession = await getServerSession();
            if (!authSession?.user) {
              return NextResponse.json(
                { error: 'Unauthorized' },
                { status: 401 }
              );
            }
            
            // Validate input with Zod
            const schema = z.object({
              priceId: z.string().min(1),
              successUrl: z.string().url(),
              cancelUrl: z.string().url(),
            });
            
            const body = await request.json();
            const result = schema.safeParse(body);
            
            if (!result.success) {
              return NextResponse.json(
                { error: 'Invalid input', details: result.error.flatten() },
                { status: 400 }
              );
            }
            
            const { priceId, successUrl, cancelUrl } = result.data;
            
            // Validate URLs against allowed origins
            const appUrl = process.env.NEXT_PUBLIC_APP_URL;
            if (!appUrl) throw new Error('NEXT_PUBLIC_APP_URL is required');
            const allowedOrigins = [appUrl];
            const successOrigin = new URL(successUrl).origin;
            const cancelOrigin = new URL(cancelUrl).origin;
            
            if (!allowedOrigins.includes(successOrigin) || !allowedOrigins.includes(cancelOrigin)) {
              return NextResponse.json(
                { error: 'Invalid redirect URLs' },
                { status: 400 }
              );
            }
            
            const stripeSession = await stripe.checkout.sessions.create({
              mode: 'payment',
              line_items: [
                {
                  price: priceId,
                  quantity: 1,
                },
              ],
              success_url: successUrl,
              cancel_url: cancelUrl,
              metadata: {
                userId: authSession.user.id,
              },
            });
            
            return NextResponse.json({ sessionId: stripeSession.id });
          } catch (error: unknown) {
            const message = error instanceof Error ? error.message : 'Unknown error';
            console.error('Checkout error:', message);
            return NextResponse.json(
              { error: 'Failed to create checkout session' },
              { status: 500 }
            );
          }
        }
创建结账会话端点:
yaml
precision_write:
  files:
    - path: "src/app/api/checkout/route.ts"
      content: |
        import { NextRequest, NextResponse } from 'next/server';
        import Stripe from 'stripe';
        import { getServerSession } from 'next-auth';
        import { authOptions } from '@/lib/auth';
        import { z } from 'zod';
        
        const stripeSecretKey = process.env.STRIPE_SECRET_KEY;
        if (!stripeSecretKey) {
          throw new Error('STRIPE_SECRET_KEY is required');
        }
        
        const stripe = new Stripe(stripeSecretKey, {
          apiVersion: '2024-11-20.acacia',
        });
        
        export async function POST(request: NextRequest) {
          try {
            // 检查身份验证
            const authSession = await getServerSession();
            if (!authSession?.user) {
              return NextResponse.json(
                { error: 'Unauthorized' },
                { status: 401 }
              );
            }
            
            // 使用Zod验证输入
            const schema = z.object({
              priceId: z.string().min(1),
              successUrl: z.string().url(),
              cancelUrl: z.string().url(),
            });
            
            const body = await request.json();
            const result = schema.safeParse(body);
            
            if (!result.success) {
              return NextResponse.json(
                { error: 'Invalid input', details: result.error.flatten() },
                { status: 400 }
              );
            }
            
            const { priceId, successUrl, cancelUrl } = result.data;
            
            // 验证URL是否符合允许的源
            const appUrl = process.env.NEXT_PUBLIC_APP_URL;
            if (!appUrl) throw new Error('NEXT_PUBLIC_APP_URL is required');
            const allowedOrigins = [appUrl];
            const successOrigin = new URL(successUrl).origin;
            const cancelOrigin = new URL(cancelUrl).origin;
            
            if (!allowedOrigins.includes(successOrigin) || !allowedOrigins.includes(cancelOrigin)) {
              return NextResponse.json(
                { error: 'Invalid redirect URLs' },
                { status: 400 }
              );
            }
            
            const stripeSession = await stripe.checkout.sessions.create({
              mode: 'payment',
              line_items: [
                {
                  price: priceId,
                  quantity: 1,
                },
              ],
              success_url: successUrl,
              cancel_url: cancelUrl,
              metadata: {
                userId: authSession.user.id,
              },
            });
            
            return NextResponse.json({ sessionId: stripeSession.id });
          } catch (error: unknown) {
            const message = error instanceof Error ? error.message : 'Unknown error';
            console.error('Checkout error:', message);
            return NextResponse.json(
              { error: 'Failed to create checkout session' },
              { status: 500 }
            );
          }
        }

One-Time Payments (LemonSqueezy)

一次性支付(LemonSqueezy)

yaml
precision_write:
  files:
    - path: "src/app/api/checkout/route.ts"
      content: |
        import { NextRequest, NextResponse } from 'next/server';
        import { lemonSqueezySetup, createCheckout } from '@lemonsqueezy/lemonsqueezy.js';
        import { getServerSession } from 'next-auth';
        import { authOptions } from '@/lib/auth';
        
        const lemonSqueezyApiKey = process.env.LEMONSQUEEZY_API_KEY;
        if (!lemonSqueezyApiKey) {
          throw new Error('LEMONSQUEEZY_API_KEY is required');
        }
        lemonSqueezySetup({ apiKey: lemonSqueezyApiKey });
        
        export async function POST(request: NextRequest) {
          try {
            const authSession = await getServerSession(authOptions);
            if (!authSession?.user) {
              return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
            }
            
            const { variantId, email } = await request.json();
            
            const lemonSqueezyStoreId = process.env.LEMONSQUEEZY_STORE_ID;
            if (!lemonSqueezyStoreId) {
              throw new Error('LEMONSQUEEZY_STORE_ID is required');
            }
            
            const { data, error } = await createCheckout(
              lemonSqueezyStoreId,
              variantId,
              {
                checkoutData: {
                  email,
                  custom: {
                    user_id: authSession.user.id,
                  },
                },
              }
            );
            
            if (error) {
              throw new Error(error.message);
            }
            
            return NextResponse.json({ checkoutUrl: data.attributes.url });
          } catch (error: unknown) {
            const message = error instanceof Error ? error.message : 'Unknown error';
            console.error('Checkout error:', message);
            return NextResponse.json(
              { error: 'Failed to create checkout' },
              { status: 500 }
            );
          }
        }
yaml
precision_write:
  files:
    - path: "src/app/api/checkout/route.ts"
      content: |
        import { NextRequest, NextResponse } from 'next/server';
        import { lemonSqueezySetup, createCheckout } from '@lemonsqueezy/lemonsqueezy.js';
        import { getServerSession } from 'next-auth';
        import { authOptions } from '@/lib/auth';
        
        const lemonSqueezyApiKey = process.env.LEMONSQUEEZY_API_KEY;
        if (!lemonSqueezyApiKey) {
          throw new Error('LEMONSQUEEZY_API_KEY is required');
        }
        lemonSqueezySetup({ apiKey: lemonSqueezyApiKey });
        
        export async function POST(request: NextRequest) {
          try {
            const authSession = await getServerSession(authOptions);
            if (!authSession?.user) {
              return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
            }
            
            const { variantId, email } = await request.json();
            
            const lemonSqueezyStoreId = process.env.LEMONSQUEEZY_STORE_ID;
            if (!lemonSqueezyStoreId) {
              throw new Error('LEMONSQUEEZY_STORE_ID is required');
            }
            
            const { data, error } = await createCheckout(
              lemonSqueezyStoreId,
              variantId,
              {
                checkoutData: {
                  email,
                  custom: {
                    user_id: authSession.user.id,
                  },
                },
              }
            );
            
            if (error) {
              throw new Error(error.message);
            }
            
            return NextResponse.json({ checkoutUrl: data.attributes.url });
          } catch (error: unknown) {
            const message = error instanceof Error ? error.message : 'Unknown error';
            console.error('Checkout error:', message);
            return NextResponse.json(
              { error: 'Failed to create checkout' },
              { status: 500 }
            );
          }
        }

5. Subscription Billing

5. 订阅账单管理

Create Subscription Plans (Stripe)

创建订阅计划(Stripe)

yaml
precision_write:
  files:
    - path: "src/lib/stripe/plans.ts"
      content: |
        export const SUBSCRIPTION_PLANS = {
          starter: {
            name: 'Starter',
            priceId: (() => {
              const priceId = process.env.STRIPE_PRICE_STARTER;
              if (!priceId) throw new Error('STRIPE_PRICE_STARTER is required');
              return priceId;
            })(),
            price: 9,
            interval: 'month' as const,
            features: ['Feature 1', 'Feature 2'],
          },
          pro: {
            name: 'Pro',
            priceId: (() => {
              const priceId = process.env.STRIPE_PRICE_PRO;
              if (!priceId) throw new Error('STRIPE_PRICE_PRO is required');
              return priceId;
            })(),
            price: 29,
            interval: 'month' as const,
            features: ['All Starter features', 'Feature 3', 'Feature 4'],
          },
        } as const;
        
        export type PlanId = keyof typeof SUBSCRIPTION_PLANS;
yaml
precision_write:
  files:
    - path: "src/lib/stripe/plans.ts"
      content: |
        export const SUBSCRIPTION_PLANS = {
          starter: {
            name: 'Starter',
            priceId: (() => {
              const priceId = process.env.STRIPE_PRICE_STARTER;
              if (!priceId) throw new Error('STRIPE_PRICE_STARTER is required');
              return priceId;
            })(),
            price: 9,
            interval: 'month' as const,
            features: ['Feature 1', 'Feature 2'],
          },
          pro: {
            name: 'Pro',
            priceId: (() => {
              const priceId = process.env.STRIPE_PRICE_PRO;
              if (!priceId) throw new Error('STRIPE_PRICE_PRO is required');
              return priceId;
            })(),
            price: 29,
            interval: 'month' as const,
            features: ['All Starter features', 'Feature 3', 'Feature 4'],
          },
        } as const;
        
        export type PlanId = keyof typeof SUBSCRIPTION_PLANS;

Subscription Checkout

订阅结账

yaml
precision_write:
  files:
    - path: "src/app/api/subscribe/route.ts"
      content: |
        import { NextRequest, NextResponse } from 'next/server';
        import Stripe from 'stripe';
        import { z } from 'zod';
        import { SUBSCRIPTION_PLANS } from '@/lib/stripe/plans';
        import { getServerSession } from 'next-auth';
        import { authOptions } from '@/lib/auth';
        
        // Validate required environment variables
        const secretKey = process.env.STRIPE_SECRET_KEY;
        if (!secretKey) throw new Error('STRIPE_SECRET_KEY is required');
        const stripe = new Stripe(secretKey, {
          apiVersion: '2024-11-20.acacia',
        });
        
        const subscribeSchema = z.object({
          planId: z.string().min(1),
          customerId: z.string().min(1),
          successUrl: z.string().url(),
          cancelUrl: z.string().url(),
        });
        
        export async function POST(request: NextRequest) {
          try {
            const authSession = await getServerSession(authOptions);
            if (!authSession?.user) {
              return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
            }
            
            const body = await request.json();
            const result = subscribeSchema.safeParse(body);
            
            if (!result.success) {
              return NextResponse.json(
                { error: result.error.flatten() },
                { status: 400 }
              );
            }
            
            const { planId, customerId, successUrl, cancelUrl } = result.data;
            
            const plan = SUBSCRIPTION_PLANS[planId as keyof typeof SUBSCRIPTION_PLANS];
            if (!plan) {
              return NextResponse.json({ error: 'Invalid plan' }, { status: 400 });
            }
            
            const stripeSession = await stripe.checkout.sessions.create({
              mode: 'subscription',
              customer: customerId,
              line_items: [
                {
                  price: plan.priceId,
                  quantity: 1,
                },
              ],
              success_url: successUrl,
              cancel_url: cancelUrl,
              subscription_data: {
                trial_period_days: 14,
              },
            });
            
            return NextResponse.json({ sessionId: stripeSession.id });
          } catch (error: unknown) {
            const message = error instanceof Error ? error.message : 'Unknown error';
            console.error('Subscription error:', message);
            return NextResponse.json(
              { error: 'Failed to create subscription' },
              { status: 500 }
            );
          }
        }
yaml
precision_write:
  files:
    - path: "src/app/api/subscribe/route.ts"
      content: |
        import { NextRequest, NextResponse } from 'next/server';
        import Stripe from 'stripe';
        import { z } from 'zod';
        import { SUBSCRIPTION_PLANS } from '@/lib/stripe/plans';
        import { getServerSession } from 'next-auth';
        import { authOptions } from '@/lib/auth';
        
        // 验证所需环境变量
        const secretKey = process.env.STRIPE_SECRET_KEY;
        if (!secretKey) throw new Error('STRIPE_SECRET_KEY is required');
        const stripe = new Stripe(secretKey, {
          apiVersion: '2024-11-20.acacia',
        });
        
        const subscribeSchema = z.object({
          planId: z.string().min(1),
          customerId: z.string().min(1),
          successUrl: z.string().url(),
          cancelUrl: z.string().url(),
        });
        
        export async function POST(request: NextRequest) {
          try {
            const authSession = await getServerSession(authOptions);
            if (!authSession?.user) {
              return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
            }
            
            const body = await request.json();
            const result = subscribeSchema.safeParse(body);
            
            if (!result.success) {
              return NextResponse.json(
                { error: result.error.flatten() },
                { status: 400 }
              );
            }
            
            const { planId, customerId, successUrl, cancelUrl } = result.data;
            
            const plan = SUBSCRIPTION_PLANS[planId as keyof typeof SUBSCRIPTION_PLANS];
            if (!plan) {
              return NextResponse.json({ error: 'Invalid plan' }, { status: 400 });
            }
            
            const stripeSession = await stripe.checkout.sessions.create({
              mode: 'subscription',
              customer: customerId,
              line_items: [
                {
                  price: plan.priceId,
                  quantity: 1,
                },
              ],
              success_url: successUrl,
              cancel_url: cancelUrl,
              subscription_data: {
                trial_period_days: 14,
              },
            });
            
            return NextResponse.json({ sessionId: stripeSession.id });
          } catch (error: unknown) {
            const message = error instanceof Error ? error.message : 'Unknown error';
            console.error('Subscription error:', message);
            return NextResponse.json(
              { error: 'Failed to create subscription' },
              { status: 500 }
            );
          }
        }

Manage Subscriptions

管理订阅

yaml
precision_write:
  files:
    - path: "src/app/api/subscription/manage/route.ts"
      content: |
        import { NextRequest, NextResponse } from 'next/server';
        import Stripe from 'stripe';
        import { getServerSession } from 'next-auth';
        import { authOptions } from '@/lib/auth';
        
        const secretKey = process.env.STRIPE_SECRET_KEY;
        if (!secretKey) throw new Error('STRIPE_SECRET_KEY is required');
        const stripe = new Stripe(secretKey, {
          apiVersion: '2024-11-20.acacia',
        });
        
        // Cancel subscription
        export async function DELETE(request: NextRequest) {
          try {
            const authSession = await getServerSession(authOptions);
            if (!authSession?.user) {
              return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
            }
            
            const { subscriptionId } = await request.json();
            
            const subscription = await stripe.subscriptions.update(subscriptionId, {
              cancel_at_period_end: true,
            });
            
            return NextResponse.json({ subscription });
          } catch (error: unknown) {
            const message = error instanceof Error ? error.message : 'Unknown error';
            console.error('Cancel error:', message);
            return NextResponse.json(
              { error: 'Failed to cancel subscription' },
              { status: 500 }
            );
          }
        }
        
        // Update subscription
        export async function PATCH(request: NextRequest) {
          try {
            const authSession = await getServerSession(authOptions);
            if (!authSession?.user) {
              return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
            }
            
            const { subscriptionId, newPriceId } = await request.json();
            
            const subscription = await stripe.subscriptions.retrieve(subscriptionId);
            
            const updated = await stripe.subscriptions.update(subscriptionId, {
              items: [
                {
                  id: subscription.items.data[0].id,
                  price: newPriceId,
                },
              ],
              proration_behavior: 'create_prorations',
            });
            
            return NextResponse.json({ subscription: updated });
          } catch (error: unknown) {
            const message = error instanceof Error ? error.message : 'Unknown error';
            console.error('Update error:', message);
            return NextResponse.json(
              { error: 'Failed to update subscription' },
              { status: 500 }
            );
          }
        }
yaml
precision_write:
  files:
    - path: "src/app/api/subscription/manage/route.ts"
      content: |
        import { NextRequest, NextResponse } from 'next/server';
        import Stripe from 'stripe';
        import { getServerSession } from 'next-auth';
        import { authOptions } from '@/lib/auth';
        
        const secretKey = process.env.STRIPE_SECRET_KEY;
        if (!secretKey) throw new Error('STRIPE_SECRET_KEY is required');
        const stripe = new Stripe(secretKey, {
          apiVersion: '2024-11-20.acacia',
        });
        
        // 取消订阅
        export async function DELETE(request: NextRequest) {
          try {
            const authSession = await getServerSession(authOptions);
            if (!authSession?.user) {
              return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
            }
            
            const { subscriptionId } = await request.json();
            
            const subscription = await stripe.subscriptions.update(subscriptionId, {
              cancel_at_period_end: true,
            });
            
            return NextResponse.json({ subscription });
          } catch (error: unknown) {
            const message = error instanceof Error ? error.message : 'Unknown error';
            console.error('Cancel error:', message);
            return NextResponse.json(
              { error: 'Failed to cancel subscription' },
              { status: 500 }
            );
          }
        }
        
        // 更新订阅
        export async function PATCH(request: NextRequest) {
          try {
            const authSession = await getServerSession(authOptions);
            if (!authSession?.user) {
              return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
            }
            
            const { subscriptionId, newPriceId } = await request.json();
            
            const subscription = await stripe.subscriptions.retrieve(subscriptionId);
            
            const updated = await stripe.subscriptions.update(subscriptionId, {
              items: [
                {
                  id: subscription.items.data[0].id,
                  price: newPriceId,
                },
              ],
              proration_behavior: 'create_prorations',
            });
            
            return NextResponse.json({ subscription: updated });
          } catch (error: unknown) {
            const message = error instanceof Error ? error.message : 'Unknown error';
            console.error('Update error:', message);
            return NextResponse.json(
              { error: 'Failed to update subscription' },
              { status: 500 }
            );
          }
        }

6. Webhook Handling

6. Webhook处理

Webhooks are critical for payment processing. They notify your application of payment events.
Webhook是支付处理的关键组件,用于通知应用支付事件的发生。

Stripe Webhook Handler

Stripe Webhook处理器

yaml
precision_write:
  files:
    - path: "src/app/api/webhooks/stripe/route.ts"
      content: |
        import { NextRequest, NextResponse } from 'next/server';
        import Stripe from 'stripe';
        import { headers } from 'next/headers';
        import { db } from '@/lib/db';
        
        const secretKey = process.env.STRIPE_SECRET_KEY;
        if (!secretKey) throw new Error('STRIPE_SECRET_KEY is required');
        const stripe = new Stripe(secretKey, {
          apiVersion: '2024-11-20.acacia',
        });
        
        const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
        if (!webhookSecret) {
          throw new Error('STRIPE_WEBHOOK_SECRET is required');
        }
        
        export async function POST(request: NextRequest) {
          const body = await request.text();
          const headersList = await headers();
          const signature = headersList.get('stripe-signature');
          
          if (!signature) {
            return NextResponse.json(
              { error: 'Missing stripe-signature header' },
              { status: 400 }
            );
          }
          
          let event: Stripe.Event;
          
          try {
            // Verify webhook signature
            event = stripe.webhooks.constructEvent(body, signature, webhookSecret);
          } catch (error: unknown) {
            const message = error instanceof Error ? error.message : 'Unknown error';
            console.error('Webhook signature verification failed:', message);
            return NextResponse.json(
              { error: 'Invalid signature' },
              { status: 400 }
            );
          }
          
          // Handle the event
          try {
            switch (event.type) {
              case 'checkout.session.completed': {
                const session = event.data.object as Stripe.Checkout.Session;
                await handleCheckoutComplete(session);
                break;
              }
                
              case 'customer.subscription.created': {
                const subscription = event.data.object as Stripe.Subscription;
                await handleSubscriptionCreated(subscription);
                break;
              }
                
              case 'customer.subscription.updated':
                await handleSubscriptionUpdated(event.data.object as Stripe.Subscription);
                break;
                
              case 'customer.subscription.deleted':
                await handleSubscriptionCanceled(event.data.object as Stripe.Subscription);
                break;
                
              case 'invoice.payment_succeeded':
                await handlePaymentSucceeded(event.data.object as Stripe.Invoice);
                break;
                
              case 'invoice.payment_failed':
                await handlePaymentFailed(event.data.object as Stripe.Invoice);
                break;
                
              default:
                // Replace with structured logger in production
                console.log(`Unhandled event type: ${event.type}`);
            }
            
            return NextResponse.json({ received: true });
          } catch (error: unknown) {
            const message = error instanceof Error ? error.message : 'Unknown error';
            console.error('Webhook handler error:', message);
            return NextResponse.json(
              { error: 'Webhook handler failed' },
              { status: 500 }
            );
          }
        }
        
        async function handleCheckoutComplete(session: Stripe.Checkout.Session) {
          // Update database with payment details
          await db.payment.create({
            data: {
              id: session.payment_intent as string,
              userId: session.metadata?.userId,
              amount: session.amount_total ?? 0,
              currency: session.currency ?? 'usd',
              status: 'succeeded',
            },
          });
          // Grant access to product/service based on metadata
        }
        
        async function handleSubscriptionCreated(subscription: Stripe.Subscription) {
          // Create subscription record in database
          await db.subscription.create({
            data: {
              id: subscription.id,
              userId: subscription.metadata?.userId,
              status: subscription.status,
              currentPeriodEnd: new Date(subscription.current_period_end * 1000),
            },
          });
        }
        
        async function handleSubscriptionUpdated(subscription: Stripe.Subscription) {
          // Update subscription status in database
          await db.subscription.update({
            where: { id: subscription.id },
            data: {
              status: subscription.status,
              currentPeriodEnd: new Date(subscription.current_period_end * 1000),
            },
          });
        }
        
        async function handleSubscriptionCanceled(subscription: Stripe.Subscription) {
          // Mark subscription as canceled in database
          await db.subscription.update({
            where: { id: subscription.id },
            data: {
              status: 'canceled',
              canceledAt: new Date(),
            },
          });
          // Revoke access at period end
        }
        
        async function handlePaymentSucceeded(invoice: Stripe.Invoice) {
          // Record successful payment
          await db.payment.create({
            data: {
              id: invoice.payment_intent as string,
              userId: invoice.subscription_details?.metadata?.userId,
              amount: invoice.amount_paid,
              currency: invoice.currency,
              status: 'succeeded',
            },
          });
        }
        
        async function handlePaymentFailed(invoice: Stripe.Invoice) {
          // Notify user of failed payment
          await db.payment.create({
            data: {
              id: invoice.payment_intent as string,
              userId: invoice.subscription_details?.metadata?.userId,
              amount: invoice.amount_due,
              currency: invoice.currency,
              status: 'failed',
            },
          });
          // Send notification email
        }
yaml
precision_write:
  files:
    - path: "src/app/api/webhooks/stripe/route.ts"
      content: |
        import { NextRequest, NextResponse } from 'next/server';
        import Stripe from 'stripe';
        import { headers } from 'next/headers';
        import { db } from '@/lib/db';
        
        const secretKey = process.env.STRIPE_SECRET_KEY;
        if (!secretKey) throw new Error('STRIPE_SECRET_KEY is required');
        const stripe = new Stripe(secretKey, {
          apiVersion: '2024-11-20.acacia',
        });
        
        const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
        if (!webhookSecret) {
          throw new Error('STRIPE_WEBHOOK_SECRET is required');
        }
        
        export async function POST(request: NextRequest) {
          const body = await request.text();
          const headersList = await headers();
          const signature = headersList.get('stripe-signature');
          
          if (!signature) {
            return NextResponse.json(
              { error: 'Missing stripe-signature header' },
              { status: 400 }
            );
          }
          
          let event: Stripe.Event;
          
          try {
            // 验证Webhook签名
            event = stripe.webhooks.constructEvent(body, signature, webhookSecret);
          } catch (error: unknown) {
            const message = error instanceof Error ? error.message : 'Unknown error';
            console.error('Webhook signature verification failed:', message);
            return NextResponse.json(
              { error: 'Invalid signature' },
              { status: 400 }
            );
          }
          
          // 处理事件
          try {
            switch (event.type) {
              case 'checkout.session.completed': {
                const session = event.data.object as Stripe.Checkout.Session;
                await handleCheckoutComplete(session);
                break;
              }
                
              case 'customer.subscription.created': {
                const subscription = event.data.object as Stripe.Subscription;
                await handleSubscriptionCreated(subscription);
                break;
              }
                
              case 'customer.subscription.updated':
                await handleSubscriptionUpdated(event.data.object as Stripe.Subscription);
                break;
                
              case 'customer.subscription.deleted':
                await handleSubscriptionCanceled(event.data.object as Stripe.Subscription);
                break;
                
              case 'invoice.payment_succeeded':
                await handlePaymentSucceeded(event.data.object as Stripe.Invoice);
                break;
                
              case 'invoice.payment_failed':
                await handlePaymentFailed(event.data.object as Stripe.Invoice);
                break;
                
              default:
                // 生产环境中替换为结构化日志
                console.log(`Unhandled event type: ${event.type}`);
            }
            
            return NextResponse.json({ received: true });
          } catch (error: unknown) {
            const message = error instanceof Error ? error.message : 'Unknown error';
            console.error('Webhook handler error:', message);
            return NextResponse.json(
              { error: 'Webhook handler failed' },
              { status: 500 }
            );
          }
        }
        
        async function handleCheckoutComplete(session: Stripe.Checkout.Session) {
          // 在数据库中更新支付详情
          await db.payment.create({
            data: {
              id: session.payment_intent as string,
              userId: session.metadata?.userId,
              amount: session.amount_total ?? 0,
              currency: session.currency ?? 'usd',
              status: 'succeeded',
            },
          });
          // 根据元数据授予产品/服务访问权限
        }
        
        async function handleSubscriptionCreated(subscription: Stripe.Subscription) {
          // 在数据库中创建订阅记录
          await db.subscription.create({
            data: {
              id: subscription.id,
              userId: subscription.metadata?.userId,
              status: subscription.status,
              currentPeriodEnd: new Date(subscription.current_period_end * 1000),
            },
          });
        }
        
        async function handleSubscriptionUpdated(subscription: Stripe.Subscription) {
          // 在数据库中更新订阅状态
          await db.subscription.update({
            where: { id: subscription.id },
            data: {
              status: subscription.status,
              currentPeriodEnd: new Date(subscription.current_period_end * 1000),
            },
          });
        }
        
        async function handleSubscriptionCanceled(subscription: Stripe.Subscription) {
          // 在数据库中将订阅标记为已取消
          await db.subscription.update({
            where: { id: subscription.id },
            data: {
              status: 'canceled',
              canceledAt: new Date(),
            },
          });
          // 在周期结束时撤销访问权限
        }
        
        async function handlePaymentSucceeded(invoice: Stripe.Invoice) {
          // 记录成功的支付
          await db.payment.create({
            data: {
              id: invoice.payment_intent as string,
              userId: invoice.subscription_details?.metadata?.userId,
              amount: invoice.amount_paid,
              currency: invoice.currency,
              status: 'succeeded',
            },
          });
        }
        
        async function handlePaymentFailed(invoice: Stripe.Invoice) {
          // 通知用户支付失败
          await db.payment.create({
            data: {
              id: invoice.payment_intent as string,
              userId: invoice.subscription_details?.metadata?.userId,
              amount: invoice.amount_due,
              currency: invoice.currency,
              status: 'failed',
            },
          });
          // 发送通知邮件
        }

LemonSqueezy Webhook Handler

LemonSqueezy Webhook处理器

yaml
precision_write:
  files:
    - path: "src/app/api/webhooks/lemonsqueezy/route.ts"
      content: |
        import { NextRequest, NextResponse } from 'next/server';
        import crypto from 'crypto';
        import { db } from '@/lib/db';
        
        const webhookSecret = process.env.LEMONSQUEEZY_WEBHOOK_SECRET;
        if (!webhookSecret) {
          throw new Error('LEMONSQUEEZY_WEBHOOK_SECRET is required');
        }
        
        export async function POST(request: NextRequest) {
          const body = await request.text();
          const signature = request.headers.get('x-signature');
          
          if (!signature) {
            return NextResponse.json({ error: 'Missing signature' }, { status: 400 });
          }
          
          // Verify webhook signature
          const hmac = crypto.createHmac('sha256', webhookSecret);
          const digest = hmac.update(body).digest('hex');
          
          const signatureBuffer = Buffer.from(signature, 'utf8');
          const digestBuffer = Buffer.from(digest, 'utf8');
          if (signatureBuffer.length !== digestBuffer.length || !crypto.timingSafeEqual(signatureBuffer, digestBuffer)) {
            console.error('Webhook signature verification failed');
            return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });
          }
          
          const event = JSON.parse(body);
          
          try {
            switch (event.meta.event_name) {
              case 'order_created':
                await handleOrderCreated(event.data);
                break;
                
              case 'subscription_created':
                await handleSubscriptionCreated(event.data);
                break;
                
              case 'subscription_updated':
                await handleSubscriptionUpdated(event.data);
                break;
                
              case 'subscription_cancelled':
                await handleSubscriptionCancelled(event.data);
                break;
                
              case 'subscription_payment_success':
                await handlePaymentSuccess(event.data);
                break;
                
              default:
                // Replace with structured logger in production
                console.log(`Unhandled event: ${event.meta.event_name}`);
            }
            
            return NextResponse.json({ received: true });
          } catch (error: unknown) {
            const message = error instanceof Error ? error.message : 'Unknown error';
            console.error('Webhook handler error:', message);
            return NextResponse.json(
              { error: 'Webhook handler failed' },
              { status: 500 }
            );
          }
        }
        
        interface LemonSqueezyWebhookData {
          id: string;
          attributes: Record<string, unknown>;
        }
        
        async function handleOrderCreated(data: LemonSqueezyWebhookData) {
          await db.order.create({
            data: {
              id: data.id,
              status: 'completed',
              attributes: data.attributes,
            },
          });
        }
        
        async function handleSubscriptionCreated(data: LemonSqueezyWebhookData) {
          await db.subscription.create({
            data: {
              id: data.id,
              status: 'active',
              userId: data.attributes.user_id as string,
              attributes: data.attributes,
            },
          });
        }
        
        async function handleSubscriptionUpdated(data: LemonSqueezyWebhookData) {
          await db.subscription.update({
            where: { id: data.id },
            data: {
              status: data.attributes.status as string,
              attributes: data.attributes,
            },
          });
        }
        
        async function handleSubscriptionCancelled(data: LemonSqueezyWebhookData) {
          await db.subscription.update({
            where: { id: data.id },
            data: {
              status: 'cancelled',
              cancelledAt: new Date(),
            },
          });
        }
        
        async function handlePaymentSuccess(data: LemonSqueezyWebhookData) {
          await db.payment.create({
            data: {
              id: data.id,
              status: 'succeeded',
              amount: data.attributes.total as number,
              attributes: data.attributes,
            },
          });
        }
yaml
precision_write:
  files:
    - path: "src/app/api/webhooks/lemonsqueezy/route.ts"
      content: |
        import { NextRequest, NextResponse } from 'next/server';
        import crypto from 'crypto';
        import { db } from '@/lib/db';
        
        const webhookSecret = process.env.LEMONSQUEEZY_WEBHOOK_SECRET;
        if (!webhookSecret) {
          throw new Error('LEMONSQUEEZY_WEBHOOK_SECRET is required');
        }
        
        export async function POST(request: NextRequest) {
          const body = await request.text();
          const signature = request.headers.get('x-signature');
          
          if (!signature) {
            return NextResponse.json({ error: 'Missing signature' }, { status: 400 });
          }
          
          // 验证Webhook签名
          const hmac = crypto.createHmac('sha256', webhookSecret);
          const digest = hmac.update(body).digest('hex');
          
          const signatureBuffer = Buffer.from(signature, 'utf8');
          const digestBuffer = Buffer.from(digest, 'utf8');
          if (signatureBuffer.length !== digestBuffer.length || !crypto.timingSafeEqual(signatureBuffer, digestBuffer)) {
            console.error('Webhook signature verification failed');
            return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });
          }
          
          const event = JSON.parse(body);
          
          try {
            switch (event.meta.event_name) {
              case 'order_created':
                await handleOrderCreated(event.data);
                break;
                
              case 'subscription_created':
                await handleSubscriptionCreated(event.data);
                break;
                
              case 'subscription_updated':
                await handleSubscriptionUpdated(event.data);
                break;
                
              case 'subscription_cancelled':
                await handleSubscriptionCancelled(event.data);
                break;
                
              case 'subscription_payment_success':
                await handlePaymentSuccess(event.data);
                break;
                
              default:
                // 生产环境中替换为结构化日志
                console.log(`Unhandled event: ${event.meta.event_name}`);
            }
            
            return NextResponse.json({ received: true });
          } catch (error: unknown) {
            const message = error instanceof Error ? error.message : 'Unknown error';
            console.error('Webhook handler error:', message);
            return NextResponse.json(
              { error: 'Webhook handler failed' },
              { status: 500 }
            );
          }
        }
        
        interface LemonSqueezyWebhookData {
          id: string;
          attributes: Record<string, unknown>;
        }
        
        async function handleOrderCreated(data: LemonSqueezyWebhookData) {
          await db.order.create({
            data: {
              id: data.id,
              status: 'completed',
              attributes: data.attributes,
            },
          });
        }
        
        async function handleSubscriptionCreated(data: LemonSqueezyWebhookData) {
          await db.subscription.create({
            data: {
              id: data.id,
              status: 'active',
              userId: data.attributes.user_id as string,
              attributes: data.attributes,
            },
          });
        }
        
        async function handleSubscriptionUpdated(data: LemonSqueezyWebhookData) {
          await db.subscription.update({
            where: { id: data.id },
            data: {
              status: data.attributes.status as string,
              attributes: data.attributes,
            },
          });
        }
        
        async function handleSubscriptionCancelled(data: LemonSqueezyWebhookData) {
          await db.subscription.update({
            where: { id: data.id },
            data: {
              status: 'cancelled',
              cancelledAt: new Date(),
            },
          });
        }
        
        async function handlePaymentSuccess(data: LemonSqueezyWebhookData) {
          await db.payment.create({
            data: {
              id: data.id,
              status: 'succeeded',
              amount: data.attributes.total as number,
              attributes: data.attributes,
            },
          });
        }

Webhook Idempotency

Webhook幂等性

Always implement idempotency to handle duplicate webhook deliveries:
yaml
precision_write:
  files:
    - path: "src/lib/webhooks/idempotency.ts"
      content: |
        import { db } from '@/lib/db';
        
        export async function isProcessed(eventId: string): Promise<boolean> {
          const existing = await db.webhookEvent.findUnique({
            where: { id: eventId },
          });
          
          return !!existing;
        }
        
        export async function markProcessed(eventId: string): Promise<void> {
          await db.webhookEvent.create({
            data: {
              id: eventId,
              processedAt: new Date(),
            },
          });
        }
        
        export async function withIdempotency<T>(
          eventId: string,
          handler: () => Promise<T>
        ): Promise<T | null> {
          // Use database transaction to prevent TOCTOU race condition
          return db.$transaction(async (tx) => {
            const existing = await tx.webhookEvent.findUnique({
              where: { id: eventId },
            });
            
            if (existing) {
              // Replace with structured logger in production
              console.log(`Event ${eventId} already processed, skipping`);
              return null;
            }
            
            const result = await handler();
            
            await tx.webhookEvent.create({
              data: {
                id: eventId,
                processedAt: new Date(),
              },
            });
            
            return result;
          });
        }
Update webhook handlers to use idempotency:
typescript
// In Stripe webhook
const eventId = event.id;
await withIdempotency(eventId, async () => {
  // Handle event
});
始终实现幂等性以处理重复的Webhook交付:
yaml
precision_write:
  files:
    - path: "src/lib/webhooks/idempotency.ts"
      content: |
        import { db } from '@/lib/db';
        
        export async function isProcessed(eventId: string): Promise<boolean> {
          const existing = await db.webhookEvent.findUnique({
            where: { id: eventId },
          });
          
          return !!existing;
        }
        
        export async function markProcessed(eventId: string): Promise<void> {
          await db.webhookEvent.create({
            data: {
              id: eventId,
              processedAt: new Date(),
            },
          });
        }
        
        export async function withIdempotency<T>(
          eventId: string,
          handler: () => Promise<T>
        ): Promise<T | null> {
          // 使用数据库事务防止TOCTOU竞争条件
          return db.$transaction(async (tx) => {
            const existing = await tx.webhookEvent.findUnique({
              where: { id: eventId },
            });
            
            if (existing) {
              // 生产环境中替换为结构化日志
              console.log(`Event ${eventId} already processed, skipping`);
              return null;
            }
            
            const result = await handler();
            
            await tx.webhookEvent.create({
              data: {
                id: eventId,
                processedAt: new Date(),
              },
            });
            
            return result;
          });
        }
更新Webhook处理器以使用幂等性:
typescript
// 在Stripe Webhook中
const eventId = event.id;
await withIdempotency(eventId, async () => {
  // 处理事件
});

7. Testing Payment Flows

7. 测试支付流程

Use Test Mode Keys

使用测试模式密钥

Always use test mode keys in development:
yaml
precision_grep:
  queries:
    - id: check-test-keys
      pattern: "(sk_test|pk_test)"
      path: ".env"
  output:
    format: files_only
在开发环境中始终使用测试模式密钥:
yaml
precision_grep:
  queries:
    - id: check-test-keys
      pattern: "(sk_test|pk_test)"
      path: ".env"
  output:
    format: files_only

Stripe CLI for Webhook Testing

使用Stripe CLI测试Webhook

Install and configure Stripe CLI:
yaml
precision_exec:
  commands:
    - cmd: "stripe listen --forward-to localhost:3000/api/webhooks/stripe"
  background: true
  verbosity: minimal
安装并配置Stripe CLI:
yaml
precision_exec:
  commands:
    - cmd: "stripe listen --forward-to localhost:3000/api/webhooks/stripe"
  background: true
  verbosity: minimal

Test Card Numbers

测试卡号

Document test card numbers in development docs:
yaml
precision_write:
  files:
    - path: "docs/testing-payments.md"
      content: |
        # Payment Testing Guide
        
        ## Test Cards (Stripe)
        
        - Success: 4242 4242 4242 4242
        - Decline: 4000 0000 0000 0002
        - Requires authentication: 4000 0025 0000 3155
        - Insufficient funds: 4000 0000 0000 9995
        
        Use any future expiry date and any 3-digit CVC.
        
        ## Test Webhooks
        
        1. Start webhook listener: `stripe listen --forward-to localhost:3000/api/webhooks/stripe`
        2. Trigger events: `stripe trigger payment_intent.succeeded`
        
        ## LemonSqueezy Testing
        
        Use sandbox mode in .env:
        LEMONSQUEEZY_API_KEY=... (sandbox key)
在开发文档中记录测试卡号:
yaml
precision_write:
  files:
    - path: "docs/testing-payments.md"
      content: |
        # 支付测试指南
        
        ## 测试卡号(Stripe)
        
        - 支付成功:4242 4242 4242 4242
        - 支付失败:4000 0000 0000 0002
        - 需要身份验证:4000 0025 0000 3155
        - 余额不足:4000 0000 0000 9995
        
        可使用任意未来到期日期和任意3位CVC码。
        
        ## 测试Webhook
        
        1. 启动Webhook监听器:`stripe listen --forward-to localhost:3000/api/webhooks/stripe`
        2. 触发事件:`stripe trigger payment_intent.succeeded`
        
        ## LemonSqueezy测试
        
        在.env中使用沙盒模式:
        LEMONSQUEEZY_API_KEY=...(沙盒密钥)

Automated Testing

自动化测试

Create tests for webhook handlers:
yaml
precision_write:
  files:
    - path: "src/__tests__/webhooks/stripe.test.ts"
      content: |
        import { POST } from '@/app/api/webhooks/stripe/route';
        import { NextRequest } from 'next/server';
        import Stripe from 'stripe';
        
        describe('Stripe Webhook Handler', () => {
          it('should verify webhook signature', async () => {
            const mockEvent = {
              id: 'evt_test_123',
              type: 'checkout.session.completed',
              data: { object: {} },
            };
            
            const request = new NextRequest('http://localhost:3000/api/webhooks/stripe', {
              method: 'POST',
              body: JSON.stringify(mockEvent),
              headers: {
                'stripe-signature': 'invalid_signature',
              },
            });
            
            const response = await POST(request);
            expect(response.status).toBe(400);
          });
          
          it('should handle checkout.session.completed', async () => {
            // Test event handling
          });
        });
为Webhook处理器创建测试:
yaml
precision_write:
  files:
    - path: "src/__tests__/webhooks/stripe.test.ts"
      content: |
        import { POST } from '@/app/api/webhooks/stripe/route';
        import { NextRequest } from 'next/server';
        import Stripe from 'stripe';
        
        describe('Stripe Webhook Handler', () => {
          it('should verify webhook signature', async () => {
            const mockEvent = {
              id: 'evt_test_123',
              type: 'checkout.session.completed',
              data: { object: {} },
            };
            
            const request = new NextRequest('http://localhost:3000/api/webhooks/stripe', {
              method: 'POST',
              body: JSON.stringify(mockEvent),
              headers: {
                'stripe-signature': 'invalid_signature',
              },
            });
            
            const response = await POST(request);
            expect(response.status).toBe(400);
          });
          
          it('should handle checkout.session.completed', async () => {
            // 测试事件处理
          });
        });

8. Security and PCI Compliance

8. 安全性与PCI合规

Never Handle Card Data Directly

绝不直接处理卡号数据

Use hosted checkout pages or Elements to keep card data out of your servers:
yaml
precision_grep:
  queries:
    - id: check-card-handling
      pattern: "(card_number|cvv|cvc|card.*exp)"
      glob: "src/**/*.{ts,js,tsx,jsx}"
  output:
    format: files_only
If any matches are found, refactor to use provider's hosted solutions.
使用托管结账页面或Elements组件,避免卡号数据进入你的服务器:
yaml
precision_grep:
  queries:
    - id: check-card-handling
      pattern: "(card_number|cvv|cvc|card.*exp)"
      glob: "src/**/*.{ts,js,tsx,jsx}"
  output:
    format: files_only
如果找到匹配项,重构为使用服务商的托管解决方案。

Enforce HTTPS

强制使用HTTPS

yaml
precision_write:
  files:
    - path: "src/middleware.ts"
      content: |
        import { NextRequest, NextResponse } from 'next/server';
        
        export function middleware(request: NextRequest) {
          // Enforce HTTPS in production
          if (
            process.env.NODE_ENV === 'production' &&
            request.headers.get('x-forwarded-proto') !== 'https'
          ) {
            return NextResponse.redirect(
              `https://${request.headers.get('host')}${request.nextUrl.pathname}`,
              301
            );
          }
          
          return NextResponse.next();
        }
        
        export const config = {
          matcher: '/api/checkout/:path*',
        };
yaml
precision_write:
  files:
    - path: "src/middleware.ts"
      content: |
        import { NextRequest, NextResponse } from 'next/server';
        
        export function middleware(request: NextRequest) {
          // 在生产环境中强制使用HTTPS
          if (
            process.env.NODE_ENV === 'production' &&
            request.headers.get('x-forwarded-proto') !== 'https'
          ) {
            return NextResponse.redirect(
              `https://${request.headers.get('host')}${request.nextUrl.pathname}`,
              301
            );
          }
          
          return NextResponse.next();
        }
        
        export const config = {
          matcher: '/api/checkout/:path*',
        };

Secure API Keys

安全存储API密钥

Verify API keys are not hardcoded:
yaml
precision_grep:
  queries:
    - id: hardcoded-keys
      pattern: "(sk_live|sk_test)_[a-zA-Z0-9]{24,}"
      glob: "src/**/*.{ts,js,tsx,jsx}"
  output:
    format: files_only
If found, move to environment variables immediately.
验证API密钥未被硬编码:
yaml
precision_grep:
  queries:
    - id: hardcoded-keys
      pattern: "(sk_live|sk_test)_[a-zA-Z0-9]{24,}"
      glob: "src/**/*.{ts,js,tsx,jsx}"
  output:
    format: files_only
如果找到,立即迁移到环境变量中。

Content Security Policy

内容安全策略

Add CSP headers for payment pages:
yaml
precision_write:
  files:
    - path: "next.config.js"
      mode: overwrite
      content: |
        module.exports = {
          async headers() {
            return [
              {
                source: '/(checkout|subscribe)/:path*',
                headers: [
                  {
                    key: 'Content-Security-Policy',
                    value: [
                      "default-src 'self'",
                      "script-src 'self' 'unsafe-inline' https://js.stripe.com",
                      "frame-src https://js.stripe.com",
                      "connect-src 'self' https://api.stripe.com",
                    ].join('; '),
                  },
                ],
              },
            ];
          },
        };
为支付页面添加CSP头:
yaml
precision_write:
  files:
    - path: "next.config.js"
      mode: overwrite
      content: |
        module.exports = {
          async headers() {
            return [
              {
                source: '/(checkout|subscribe)/:path*',
                headers: [
                  {
                    key: 'Content-Security-Policy',
                    value: [
                      "default-src 'self'",
                      "script-src 'self' 'unsafe-inline' https://js.stripe.com",
                      "frame-src https://js.stripe.com",
                      "connect-src 'self' https://api.stripe.com",
                    ].join('; '),
                  },
                ],
              },
            ];
          },
        };

9. Validation

9. 验证

Run the validation script to ensure best practices:
yaml
precision_exec:
  commands:
    - cmd: "bash plugins/goodvibes/skills/outcome/payment-integration/scripts/validate-payments.sh ."
      expect:
        exit_code: 0
  verbosity: standard
The script checks:
  • Payment library installation
  • API keys in .env.example (not .env)
  • Webhook endpoint exists
  • Webhook signature verification
  • No hardcoded secrets
  • HTTPS enforcement
  • Error handling
运行验证脚本以确保遵循最佳实践:
yaml
precision_exec:
  commands:
    - cmd: "bash plugins/goodvibes/skills/outcome/payment-integration/scripts/validate-payments.sh ."
      expect:
        exit_code: 0
  verbosity: standard
该脚本检查以下内容:
  • 支付库是否已安装
  • API密钥是否在.env.example中(而非.env)
  • Webhook端点是否存在
  • Webhook签名验证是否实现
  • 无硬编码密钥
  • 是否强制使用HTTPS
  • 错误处理是否完善

10. Database Schema for Payments

10. 支付相关数据库Schema

Add payment tracking to your database:
prisma
model Customer {
  id              String   @id @default(cuid())
  userId          String   @unique
  stripeCustomerId String?  @unique
  createdAt       DateTime @default(now())
  updatedAt       DateTime @updatedAt
  
  subscriptions   Subscription[]
  payments        Payment[]
  
  @@index([stripeCustomerId])
}

model Subscription {
  id                   String   @id @default(cuid())
  customerId           String
  customer             Customer @relation(fields: [customerId], references: [id])
  stripeSubscriptionId String   @unique
  stripePriceId        String
  status               String
  currentPeriodEnd     DateTime
  cancelAtPeriodEnd    Boolean  @default(false)
  createdAt            DateTime @default(now())
  updatedAt            DateTime @updatedAt
  
  @@index([customerId])
  @@index([status])
}

model Payment {
  id              String   @id @default(cuid())
  customerId      String
  customer        Customer @relation(fields: [customerId], references: [id])
  stripePaymentId String   @unique
  amount          Int
  currency        String
  status          String
  createdAt       DateTime @default(now())
  
  @@index([customerId])
  @@index([status])
}

model WebhookEvent {
  id          String   @id
  processedAt DateTime @default(now())
  
  @@index([processedAt])
}
在数据库中添加支付跟踪相关模型:
prisma
model Customer {
  id              String   @id @default(cuid())
  userId          String   @unique
  stripeCustomerId String?  @unique
  createdAt       DateTime @default(now())
  updatedAt       DateTime @updatedAt
  
  subscriptions   Subscription[]
  payments        Payment[]
  
  @@index([stripeCustomerId])
}

model Subscription {
  id                   String   @id @default(cuid())
  customerId           String
  customer             Customer @relation(fields: [customerId], references: [id])
  stripeSubscriptionId String   @unique
  stripePriceId        String
  status               String
  currentPeriodEnd     DateTime
  cancelAtPeriodEnd    Boolean  @default(false)
  createdAt            DateTime @default(now())
  updatedAt            DateTime @updatedAt
  
  @@index([customerId])
  @@index([status])
}

model Payment {
  id              String   @id @default(cuid())
  customerId      String
  customer        Customer @relation(fields: [customerId], references: [id])
  stripePaymentId String   @unique
  amount          Int
  currency        String
  status          String
  createdAt       DateTime @default(now())
  
  @@index([customerId])
  @@index([status])
}

model WebhookEvent {
  id          String   @id
  processedAt DateTime @default(now())
  
  @@index([processedAt])
}

Common Patterns

常见模式

Pattern: Proration on Plan Changes

模式:计划变更时的按比例计费

When upgrading/downgrading subscriptions:
typescript
const updated = await stripe.subscriptions.update(subscriptionId, {
  items: [{ id: itemId, price: newPriceId }],
  proration_behavior: 'create_prorations', // Credit/charge immediately
});
当升级/降级订阅时:
typescript
const updated = await stripe.subscriptions.update(subscriptionId, {
  items: [{ id: itemId, price: newPriceId }],
  proration_behavior: 'create_prorations', // 立即进行信用/收费
});

Pattern: Trial Periods

模式:试用期

Offer free trials:
typescript
const session = await stripe.checkout.sessions.create({
  mode: 'subscription',
  subscription_data: {
    trial_period_days: 14,
    trial_settings: {
      end_behavior: {
        missing_payment_method: 'cancel', // Cancel if no payment method
      },
    },
  },
});
提供免费试用期:
typescript
const session = await stripe.checkout.sessions.create({
  mode: 'subscription',
  subscription_data: {
    trial_period_days: 14,
    trial_settings: {
      end_behavior: {
        missing_payment_method: 'cancel', // 若无支付方式则取消
      },
    },
  },
});

Pattern: Webhook Retry Logic

模式:Webhook重试逻辑

Payment providers retry failed webhooks. Always:
  • Return 200 OK quickly (process async if needed)
  • Implement idempotency
  • Log all webhook events
支付服务商会重试失败的Webhook。始终:
  • 快速返回200 OK(如需处理可异步进行)
  • 实现幂等性
  • 记录所有Webhook事件

Pattern: Failed Payment Recovery

模式:失败支付恢复

Handle dunning (failed payment recovery):
typescript
case 'invoice.payment_failed':
  const invoice = event.data.object as Stripe.Invoice;
  if (invoice.attempt_count >= 3) {
    // Cancel subscription after 3 failures
    await stripe.subscriptions.cancel(invoice.subscription as string);
    await notifyCustomer(invoice.customer as string, 'payment_failed_final');
  } else {
    await notifyCustomer(invoice.customer as string, 'payment_failed_retry');
  }
  break;
处理催缴(失败支付恢复):
typescript
case 'invoice.payment_failed':
  const invoice = event.data.object as Stripe.Invoice;
  if (invoice.attempt_count >= 3) {
    // 3次失败后取消订阅
    await stripe.subscriptions.cancel(invoice.subscription as string);
    await notifyCustomer(invoice.customer as string, 'payment_failed_final');
  } else {
    await notifyCustomer(invoice.customer as string, 'payment_failed_retry');
  }
  break;

Anti-Patterns to Avoid

需避免的反模式

DON'T: Store Card Details

不要:存储卡号详情

Never store card numbers, CVV, or full card data. Use tokenization.
绝不存储卡号、CVV或完整的卡数据。使用令牌化。

DON'T: Skip Webhook Verification

不要:跳过Webhook签名验证

Always verify webhook signatures. Unverified webhooks are a security risk.
始终验证Webhook签名。未验证的Webhook存在安全风险。

DON'T: Use Client-Side Pricing

不要:在客户端处理定价

Never trust prices from the client. Always use server-side price lookup:
typescript
// BAD
const { amount } = await request.json();
await stripe.charges.create({ amount });

// GOOD
const { priceId } = await request.json();
const price = await stripe.prices.retrieve(priceId);
await stripe.charges.create({ amount: price.unit_amount });
绝不信任来自客户端的价格。始终在服务器端查询价格:
typescript
// 错误做法
const { amount } = await request.json();
await stripe.charges.create({ amount });

// 正确做法
const { priceId } = await request.json();
const price = await stripe.prices.retrieve(priceId);
await stripe.charges.create({ amount: price.unit_amount });

DON'T: Ignore Failed Webhooks

不要:忽略失败的Webhook

Monitor webhook delivery and investigate failures. Set up alerts.
监控Webhook交付情况并调查失败原因。设置告警。

DON'T: Hardcode Currency

不要:硬编码货币

Support multiple currencies if serving international customers:
typescript
const session = await stripe.checkout.sessions.create({
  currency: userCountry === 'US' ? 'usd' : 'eur',
});
如果服务国际客户,支持多种货币:
typescript
const session = await stripe.checkout.sessions.create({
  currency: userCountry === 'US' ? 'usd' : 'eur',
});

References

参考资料

See
references/payment-patterns.md
for:
  • Provider comparison table
  • Complete webhook event reference
  • Subscription lifecycle state machine
  • Testing strategies
  • Security checklist
查看
references/payment-patterns.md
获取以下内容:
  • 服务商对比表
  • 完整的Webhook事件参考
  • 订阅生命周期状态机
  • 测试策略
  • 安全检查清单

Next Steps

后续步骤

After implementing payment integration:
  1. Test thoroughly - Use test mode, simulate failures
  2. Monitor webhooks - Set up logging and alerting
  3. Document flows - Create internal docs for payment processes
  4. Plan for scale - Consider rate limits, concurrent webhooks
  5. Implement analytics - Track conversion rates, churn
  6. Review compliance - Ensure PCI-DSS compliance if applicable
  7. Set up monitoring - Track payment success rates, errors
完成支付集成后:
  1. 全面测试 - 使用测试模式,模拟各种失败场景
  2. 监控Webhook - 设置日志和告警
  3. 记录流程 - 创建支付流程的内部文档
  4. 规划扩展性 - 考虑速率限制、并发Webhook
  5. 实现分析 - 跟踪转化率、客户流失率
  6. 审核合规性 - 确保符合PCI-DSS合规要求(如适用)
  7. 设置监控 - 跟踪支付成功率、错误情况