diff --git a/scripts/create-test-subscription.ts b/scripts/create-test-subscription.ts new file mode 100644 index 0000000..6e1524e --- /dev/null +++ b/scripts/create-test-subscription.ts @@ -0,0 +1,103 @@ +#!/usr/bin/env tsx + +import { PrismaClient } from '@prisma/client' + +const prisma = new PrismaClient() + +async function createTestSubscription() { + try { + console.log('🧪 Creating test subscription...') + + // 使用真实的数据 + const subscriptionData = { + userId: '9975b2c5-7955-48cf-9594-74b8d9beab25', + subscriptionPlanId: 'pro', + stripeSubscriptionId: 'sub_1Rt8nRLW0cChKPJ0Osn5UBcV', + stripeCustomerId: 'cus_SojwymqWZ4EXlZ', + isActive: true, + status: 'active', + startDate: new Date(1754492307 * 1000), // 2025-08-06T14:58:27.000Z + endDate: new Date(1757170707 * 1000), // 2025-09-06T14:58:27.000Z + metadata: { + test: true, + created_by: 'manual_script' + } + } + + console.log('📊 Creating subscription with data:', subscriptionData) + + // 检查是否已存在 + const existing = await prisma.subscription.findFirst({ + where: { + stripeSubscriptionId: subscriptionData.stripeSubscriptionId + } + }) + + if (existing) { + console.log('⚠️ Subscription already exists:', existing.id) + console.log('🗑️ Deleting existing subscription...') + await prisma.subscription.delete({ + where: { id: existing.id } + }) + } + + // 创建新订阅 + const newSubscription = await prisma.subscription.create({ + data: subscriptionData + }) + + console.log('✅ Created subscription:', newSubscription.id) + + // 更新用户的订阅计划 + await prisma.user.update({ + where: { id: subscriptionData.userId }, + data: { + subscriptionPlanId: 'pro', + subscribePlan: 'pro' + } + }) + + console.log('✅ Updated user subscription plan') + + // 验证创建结果 + const createdSubscription = await prisma.subscription.findUnique({ + where: { id: newSubscription.id }, + include: { + user: { + select: { + email: true, + subscriptionPlanId: true, + subscribePlan: true + } + }, + subscriptionPlan: { + select: { + name: true, + price: true + } + } + } + }) + + console.log('📋 Subscription details:') + console.log(' ID:', createdSubscription?.id) + console.log(' User:', createdSubscription?.user.email) + console.log(' Plan:', createdSubscription?.subscriptionPlan.name) + console.log(' Price:', `$${createdSubscription?.subscriptionPlan.price}`) + console.log(' Status:', createdSubscription?.status) + console.log(' Active:', createdSubscription?.isActive) + console.log(' Start:', createdSubscription?.startDate) + console.log(' End:', createdSubscription?.endDate) + console.log(' User Plan ID:', createdSubscription?.user.subscriptionPlanId) + console.log(' User Plan:', createdSubscription?.user.subscribePlan) + + console.log('🎉 Test subscription created successfully!') + + } catch (error) { + console.error('❌ Error creating test subscription:', error) + } finally { + await prisma.$disconnect() + } +} + +createTestSubscription() diff --git a/scripts/test-subscription-table.ts b/scripts/test-subscription-table.ts new file mode 100644 index 0000000..40c4915 --- /dev/null +++ b/scripts/test-subscription-table.ts @@ -0,0 +1,88 @@ +#!/usr/bin/env tsx + +import { PrismaClient } from '@prisma/client' + +const prisma = new PrismaClient() + +async function testSubscriptionTable() { + try { + console.log('🔍 Testing subscription table...') + + // 测试查询所有订阅 + const subscriptions = await prisma.subscription.findMany({ + include: { + user: { + select: { + id: true, + email: true, + stripeCustomerId: true + } + }, + subscriptionPlan: { + select: { + id: true, + name: true, + price: true + } + } + } + }) + + console.log(`📊 Found ${subscriptions.length} subscriptions:`) + subscriptions.forEach((sub, index) => { + console.log(`${index + 1}. Subscription ${sub.id}:`) + console.log(` - User: ${sub.user.email} (${sub.user.id})`) + console.log(` - Plan: ${sub.subscriptionPlan.name} ($${sub.subscriptionPlan.price})`) + console.log(` - Status: ${sub.status}`) + console.log(` - Active: ${sub.isActive}`) + console.log(` - Stripe ID: ${sub.stripeSubscriptionId}`) + console.log(` - Start: ${sub.startDate}`) + console.log(` - End: ${sub.endDate}`) + console.log('') + }) + + // 测试查询用户 + const users = await prisma.user.findMany({ + where: { + stripeCustomerId: { + not: null + } + }, + select: { + id: true, + email: true, + stripeCustomerId: true, + subscriptionPlanId: true, + subscribePlan: true + } + }) + + console.log(`👥 Found ${users.length} users with Stripe customer IDs:`) + users.forEach((user, index) => { + console.log(`${index + 1}. ${user.email}:`) + console.log(` - User ID: ${user.id}`) + console.log(` - Stripe Customer ID: ${user.stripeCustomerId}`) + console.log(` - Subscription Plan ID: ${user.subscriptionPlanId}`) + console.log(` - Subscribe Plan: ${user.subscribePlan}`) + console.log('') + }) + + // 测试查询订阅套餐 + const plans = await prisma.subscriptionPlan.findMany() + console.log(`📦 Found ${plans.length} subscription plans:`) + plans.forEach((plan, index) => { + console.log(`${index + 1}. ${plan.name} (${plan.id}):`) + console.log(` - Price: $${plan.price}`) + console.log(` - Stripe Price ID: ${plan.stripePriceId}`) + console.log(` - Active: ${plan.isActive}`) + console.log('') + }) + + } catch (error) { + console.error('❌ Error testing subscription table:', error) + } finally { + await prisma.$disconnect() + } +} + +testSubscriptionTable() diff --git a/scripts/test-webhook-data.ts b/scripts/test-webhook-data.ts new file mode 100644 index 0000000..a596a36 --- /dev/null +++ b/scripts/test-webhook-data.ts @@ -0,0 +1,232 @@ +#!/usr/bin/env tsx + +// 模拟从你的日志中提取的 Stripe 订阅数据 +const mockSubscriptionData = { + "id": "sub_1Rt8nRLW0cChKPJ0Osn5UBcV", + "object": "subscription", + "application": null, + "application_fee_percent": null, + "automatic_tax": { + "disabled_reason": null, + "enabled": false, + "liability": null + }, + "billing_cycle_anchor": 1754492307, + "billing_cycle_anchor_config": null, + "billing_mode": { + "type": "classic" + }, + "billing_thresholds": null, + "cancel_at": null, + "cancel_at_period_end": false, + "canceled_at": null, + "cancellation_details": { + "comment": null, + "feedback": null, + "reason": null + }, + "collection_method": "charge_automatically", + "created": 1754492307, + "currency": "usd", + "customer": "cus_SojwymqWZ4EXlZ", + "days_until_due": null, + "default_payment_method": "pm_1Rt8nPLW0cChKPJ0EF0QrEyS", + "default_source": null, + "default_tax_rates": [], + "description": null, + "discounts": [], + "ended_at": null, + "invoice_settings": { + "account_tax_ids": null, + "issuer": { + "type": "self" + } + }, + "items": { + "object": "list", + "data": [ + { + "id": "si_SomMVRu4Bpje2r", + "object": "subscription_item", + "billing_thresholds": null, + "created": 1754492308, + "current_period_end": 1757170707, + "current_period_start": 1754492307, + "discounts": [], + "metadata": {}, + "plan": { + "id": "price_1RslfmLW0cChKPJ0VurJSg9I", + "object": "plan", + "active": true, + "amount": 1999, + "amount_decimal": "1999", + "billing_scheme": "per_unit", + "created": 1754403422, + "currency": "usd", + "interval": "month", + "interval_count": 1, + "livemode": false, + "metadata": {}, + "meter": null, + "nickname": null, + "product": "prod_SoOSFPRNsYcTF8", + "tiers_mode": null, + "transform_usage": null, + "trial_period_days": null, + "usage_type": "licensed" + }, + "price": { + "id": "price_1RslfmLW0cChKPJ0VurJSg9I", + "object": "price", + "active": true, + "billing_scheme": "per_unit", + "created": 1754403422, + "currency": "usd", + "custom_unit_amount": null, + "livemode": false, + "lookup_key": null, + "metadata": {}, + "nickname": null, + "product": "prod_SoOSFPRNsYcTF8", + "recurring": { + "interval": "month", + "interval_count": 1, + "meter": null, + "trial_period_days": null, + "usage_type": "licensed" + }, + "tax_behavior": "inclusive", + "tiers_mode": null, + "transform_quantity": null, + "type": "recurring", + "unit_amount": 1999, + "unit_amount_decimal": "1999" + }, + "quantity": 1, + "subscription": "sub_1Rt8nRLW0cChKPJ0Osn5UBcV", + "tax_rates": [] + } + ], + "has_more": false, + "total_count": 1, + "url": "/v1/subscription_items?subscription=sub_1Rt8nRLW0cChKPJ0Osn5UBcV" + }, + "latest_invoice": "in_1Rt8nPLW0cChKPJ0YOG0OCof", + "livemode": false, + "metadata": {}, + "next_pending_invoice_item_invoice": null, + "on_behalf_of": null, + "pause_collection": null, + "payment_settings": { + "payment_method_options": { + "acss_debit": null, + "bancontact": null, + "card": { + "network": null, + "request_three_d_secure": "automatic" + }, + "customer_balance": null, + "konbini": null, + "sepa_debit": null, + "us_bank_account": null + }, + "payment_method_types": [ + "card" + ], + "save_default_payment_method": "off" + }, + "pending_invoice_item_interval": null, + "pending_setup_intent": null, + "pending_update": null, + "plan": { + "id": "price_1RslfmLW0cChKPJ0VurJSg9I", + "object": "plan", + "active": true, + "amount": 1999, + "amount_decimal": "1999", + "billing_scheme": "per_unit", + "created": 1754403422, + "currency": "usd", + "interval": "month", + "interval_count": 1, + "livemode": false, + "metadata": {}, + "meter": null, + "nickname": null, + "product": "prod_SoOSFPRNsYcTF8", + "tiers_mode": null, + "transform_usage": null, + "trial_period_days": null, + "usage_type": "licensed" + }, + "quantity": 1, + "schedule": null, + "start_date": 1754492307, + "status": "active", + "test_clock": null, + "transfer_data": null, + "trial_end": null, + "trial_settings": { + "end_behavior": { + "missing_payment_method": "create_invoice" + } + }, + "trial_start": null +} + +function testWebhookData() { + console.log('🔍 Testing webhook data extraction...') + + const subscription = mockSubscriptionData as any + + console.log('📊 Subscription object keys:', Object.keys(subscription)) + + // 测试当前的提取逻辑 + const customerId = subscription.customer as string + const status = subscription.status as string + const stripeSubscriptionId = subscription.id as string + const items = subscription.items as { data: Array<{ price: { id: string } }> } + const priceId = items?.data[0]?.price?.id + const currentPeriodStart = subscription.current_period_start as number + const currentPeriodEnd = subscription.current_period_end as number + + console.log('🎯 Extracted data (current logic):') + console.log(' customerId:', customerId) + console.log(' status:', status) + console.log(' stripeSubscriptionId:', stripeSubscriptionId) + console.log(' priceId:', priceId) + console.log(' currentPeriodStart:', currentPeriodStart) + console.log(' currentPeriodEnd:', currentPeriodEnd) + + // 检查日期是否有效 + if (currentPeriodStart) { + const startDate = new Date(currentPeriodStart * 1000) + console.log(' startDate:', startDate.toISOString()) + } else { + console.log(' ❌ currentPeriodStart is undefined/null') + } + + if (currentPeriodEnd) { + const endDate = new Date(currentPeriodEnd * 1000) + console.log(' endDate:', endDate.toISOString()) + } else { + console.log(' ❌ currentPeriodEnd is undefined/null') + } + + // 检查替代的日期字段 + console.log('\n🔍 Looking for alternative date fields:') + console.log(' start_date:', subscription.start_date) + console.log(' created:', subscription.created) + console.log(' billing_cycle_anchor:', subscription.billing_cycle_anchor) + + // 检查 items 中的日期 + if (subscription.items?.data?.[0]) { + const item = subscription.items.data[0] + console.log(' item.current_period_start:', item.current_period_start) + console.log(' item.current_period_end:', item.current_period_end) + } + + console.log('\n✅ Analysis complete!') +} + +testWebhookData() diff --git a/scripts/test-webhook-fix.ts b/scripts/test-webhook-fix.ts new file mode 100644 index 0000000..df36bf2 --- /dev/null +++ b/scripts/test-webhook-fix.ts @@ -0,0 +1,111 @@ +#!/usr/bin/env tsx + +import { PrismaClient } from '@prisma/client' + +const prisma = new PrismaClient() + +// 模拟修复后的 webhook 处理逻辑 +async function testWebhookFix() { + try { + console.log('🧪 Testing webhook fix...') + + // 模拟 Stripe 订阅数据 + const mockSubscription = { + id: "sub_1Rt8nRLW0cChKPJ0Osn5UBcV", + customer: "cus_SojwymqWZ4EXlZ", + status: "active", + start_date: 1754492307, + items: { + data: [{ + price: { id: "price_1RslfmLW0cChKPJ0VurJSg9I" }, + current_period_start: 1754492307, + current_period_end: 1757170707 + }] + } + } + + // 提取数据(使用修复后的逻辑) + const customerId = mockSubscription.customer + const status = mockSubscription.status + const stripeSubscriptionId = mockSubscription.id + const items = mockSubscription.items + const priceId = items?.data[0]?.price?.id + const currentPeriodStart = items?.data[0]?.current_period_start || mockSubscription.start_date + const currentPeriodEnd = items?.data[0]?.current_period_end || (mockSubscription.start_date + 30 * 24 * 60 * 60) + + console.log('📊 Extracted data:') + console.log(' customerId:', customerId) + console.log(' status:', status) + console.log(' stripeSubscriptionId:', stripeSubscriptionId) + console.log(' priceId:', priceId) + console.log(' currentPeriodStart:', currentPeriodStart) + console.log(' currentPeriodEnd:', currentPeriodEnd) + + // 验证日期 + if (!currentPeriodStart || !currentPeriodEnd) { + console.error('❌ Missing period dates') + return + } + + const startDate = new Date(currentPeriodStart * 1000) + const endDate = new Date(currentPeriodEnd * 1000) + + console.log('📅 Converted dates:') + console.log(' startDate:', startDate.toISOString()) + console.log(' endDate:', endDate.toISOString()) + + // 检查日期是否有效 + if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) { + console.error('❌ Invalid dates') + return + } + + console.log('✅ Dates are valid!') + + // 查找用户 + const user = await prisma.user.findFirst({ + where: { stripeCustomerId: customerId } + }) + + if (!user) { + console.error('❌ User not found for customer:', customerId) + return + } + + console.log('👤 Found user:', user.email) + + // 查找套餐 + const plan = await prisma.subscriptionPlan.findFirst({ + where: { stripePriceId: priceId } + }) + + if (!plan) { + console.error('❌ Plan not found for price:', priceId) + return + } + + console.log('📦 Found plan:', plan.name) + + // 模拟创建订阅记录 + console.log('🔄 Would create subscription with data:') + console.log({ + userId: user.id, + subscriptionPlanId: plan.id, + stripeSubscriptionId, + stripeCustomerId: customerId, + isActive: true, + status: 'active', + startDate, + endDate, + }) + + console.log('✅ Webhook fix test completed successfully!') + + } catch (error) { + console.error('❌ Error testing webhook fix:', error) + } finally { + await prisma.$disconnect() + } +} + +testWebhookFix() diff --git a/src/app/api/webhooks/stripe/route.ts b/src/app/api/webhooks/stripe/route.ts index d17e667..76b9830 100644 --- a/src/app/api/webhooks/stripe/route.ts +++ b/src/app/api/webhooks/stripe/route.ts @@ -98,20 +98,47 @@ async function handleSubscriptionCreated(subscription: Record) const customerId = subscription.customer as string const status = subscription.status as string const stripeSubscriptionId = subscription.id as string - const items = subscription.items as { data: Array<{ price: { id: string } }> } + const items = subscription.items as { data: Array<{ + price: { id: string } + current_period_start: number + current_period_end: number + }> } const priceId = items?.data[0]?.price?.id - const currentPeriodStart = subscription.current_period_start as number - const currentPeriodEnd = subscription.current_period_end as number + + // 从 subscription items 中获取周期日期,如果不存在则使用 subscription 的 start_date + const currentPeriodStart = items?.data[0]?.current_period_start || subscription.start_date as number + const currentPeriodEnd = items?.data[0]?.current_period_end || (subscription.start_date as number + 30 * 24 * 60 * 60) // 默认30天后 console.log(`📊 New subscription details:`, { customerId, status, stripeSubscriptionId, - priceId + priceId, + currentPeriodStart, + currentPeriodEnd }) + // 验证必要的字段 + if (!customerId || !stripeSubscriptionId || !priceId) { + console.error('❌ Missing required subscription data:', { + customerId: !!customerId, + stripeSubscriptionId: !!stripeSubscriptionId, + priceId: !!priceId + }) + return + } + + // 验证日期字段 + if (!currentPeriodStart || !currentPeriodEnd) { + console.error('❌ Missing or invalid period dates:', { + currentPeriodStart, + currentPeriodEnd + }) + return + } + // 查找用户 - const user = await prisma.user.findFirst({ + const user = await (prisma as any).user.findFirst({ where: { stripeCustomerId: customerId } }) @@ -126,13 +153,24 @@ async function handleSubscriptionCreated(subscription: Record) // 根据 Stripe 价格 ID 获取套餐 ID const planId = await SubscriptionService.getPlanIdByStripePriceId(priceId) + // 创建日期对象,确保有效性 + const startDate = new Date(currentPeriodStart * 1000) + const endDate = new Date(currentPeriodEnd * 1000) + + console.log(`📅 Subscription dates:`, { + currentPeriodStart, + currentPeriodEnd, + startDate: startDate.toISOString(), + endDate: endDate.toISOString() + }) + // 创建新的订阅记录 const newSubscription = await SubscriptionService.createRenewalSubscription( user.id, planId, stripeSubscriptionId, - new Date(currentPeriodStart * 1000), - new Date(currentPeriodEnd * 1000), + startDate, + endDate, customerId, subscription ) @@ -154,10 +192,16 @@ async function handleSubscriptionUpdate(subscription: Record) { const customerId = subscription.customer as string const status = subscription.status as string const stripeSubscriptionId = subscription.id as string - const items = subscription.items as { data: Array<{ price: { id: string } }> } + const items = subscription.items as { data: Array<{ + price: { id: string } + current_period_start: number + current_period_end: number + }> } const priceId = items?.data[0]?.price?.id - const currentPeriodStart = subscription.current_period_start as number - const currentPeriodEnd = subscription.current_period_end as number + + // 从 subscription items 中获取周期日期 + const currentPeriodStart = items?.data[0]?.current_period_start || subscription.start_date as number + const currentPeriodEnd = items?.data[0]?.current_period_end || (subscription.start_date as number + 30 * 24 * 60 * 60) console.log(`📊 Subscription details:`, { customerId, @@ -167,7 +211,7 @@ async function handleSubscriptionUpdate(subscription: Record) { }) // 查找用户 - const user = await prisma.user.findFirst({ + const user = await (prisma as any).user.findFirst({ where: { stripeCustomerId: customerId } }) @@ -183,7 +227,7 @@ async function handleSubscriptionUpdate(subscription: Record) { const planId = await SubscriptionService.getPlanIdByStripePriceId(priceId) // 查找现有的订阅记录(通过 Stripe 订阅 ID) - const existingSubscription = await prisma.subscription.findFirst({ + const existingSubscription = await (prisma as any).subscription.findFirst({ where: { stripeSubscriptionId, } @@ -191,7 +235,7 @@ async function handleSubscriptionUpdate(subscription: Record) { if (existingSubscription) { // 更新现有的订阅记录 - await prisma.subscription.update({ + await (prisma as any).subscription.update({ where: { id: existingSubscription.id }, data: { isActive: true, @@ -227,7 +271,7 @@ async function handleSubscriptionDeleted(subscription: Record) const customerId = subscription.customer as string // 查找用户 - const user = await prisma.user.findFirst({ + const user = await (prisma as any).user.findFirst({ where: { stripeCustomerId: customerId } })