import { NextRequest, NextResponse } from 'next/server' import { stripe } from '@/lib/stripe-server' import { prisma } from '@/lib/db' import Stripe from 'stripe' import { getTierFromPriceId, getIntervalFromPriceId, getLimitForTier } from '@/lib/subscription-utils' export async function POST(req: NextRequest) { const body = await req.text() const signature = req.headers.get('stripe-signature') if (!signature) { console.error('No stripe signature found') return NextResponse.json({ error: 'No signature' }, { status: 400 }) } let event: Stripe.Event try { // Verify webhook signature event = stripe.webhooks.constructEvent( body, signature, process.env.STRIPE_WEBHOOK_SECRET! ) } catch (err) { console.error('Webhook signature verification failed:', err) return NextResponse.json( { error: 'Webhook signature verification failed' }, { status: 400 } ) } // Handle the event try { switch (event.type) { case 'checkout.session.completed': { const session = event.data.object as Stripe.Checkout.Session // Update donation status to COMPLETED await prisma.donation.update({ where: { stripeSessionId: session.id }, data: { status: 'COMPLETED', stripePaymentId: session.payment_intent as string, metadata: { paymentStatus: session.payment_status, customerEmail: session.customer_email, }, }, }) console.log(`Donation completed for session: ${session.id}`) break } case 'checkout.session.expired': { const session = event.data.object as Stripe.Checkout.Session // Update donation status to CANCELLED await prisma.donation.update({ where: { stripeSessionId: session.id }, data: { status: 'CANCELLED', }, }) console.log(`Donation cancelled for session: ${session.id}`) break } case 'payment_intent.payment_failed': { const paymentIntent = event.data.object as Stripe.PaymentIntent // Update donation status to FAILED const donation = await prisma.donation.findFirst({ where: { stripePaymentId: paymentIntent.id }, }) if (donation) { await prisma.donation.update({ where: { id: donation.id }, data: { status: 'FAILED', metadata: { error: paymentIntent.last_payment_error?.message, }, }, }) } console.log(`Payment failed for intent: ${paymentIntent.id}`) break } case 'charge.refunded': { const charge = event.data.object as Stripe.Charge // Update donation status to REFUNDED const donation = await prisma.donation.findFirst({ where: { stripePaymentId: charge.payment_intent as string }, }) if (donation) { await prisma.donation.update({ where: { id: donation.id }, data: { status: 'REFUNDED', metadata: { refundReason: charge.refunds?.data[0]?.reason, }, }, }) } console.log(`Donation refunded for charge: ${charge.id}`) break } // Subscription events case 'customer.subscription.created': case 'customer.subscription.updated': { const stripeSubscription = event.data.object as Stripe.Subscription const userId = stripeSubscription.metadata.userId if (!userId) { console.warn('⚠️ No userId in subscription metadata:', stripeSubscription.id) break } const priceId = stripeSubscription.items.data[0]?.price.id if (!priceId) { console.warn('⚠️ No price ID in subscription:', stripeSubscription.id) break } const tier = getTierFromPriceId(priceId) const interval = getIntervalFromPriceId(priceId) const limit = getLimitForTier(tier) // Upsert subscription record await prisma.subscription.upsert({ where: { stripeSubscriptionId: stripeSubscription.id }, create: { userId, stripeSubscriptionId: stripeSubscription.id, stripePriceId: priceId, stripeCustomerId: stripeSubscription.customer as string, status: stripeSubscription.status.toUpperCase() as any, currentPeriodStart: new Date((stripeSubscription as any).current_period_start * 1000), currentPeriodEnd: new Date((stripeSubscription as any).current_period_end * 1000), cancelAtPeriodEnd: (stripeSubscription as any).cancel_at_period_end, tier, interval }, update: { status: stripeSubscription.status.toUpperCase() as any, currentPeriodStart: new Date((stripeSubscription as any).current_period_start * 1000), currentPeriodEnd: new Date((stripeSubscription as any).current_period_end * 1000), cancelAtPeriodEnd: (stripeSubscription as any).cancel_at_period_end, stripePriceId: priceId } }) // Update user subscription tier and limit await prisma.user.update({ where: { id: userId }, data: { subscriptionTier: tier, conversationLimit: limit, subscriptionStatus: stripeSubscription.status, stripeSubscriptionId: stripeSubscription.id, stripeCustomerId: stripeSubscription.customer as string } }) console.log(`✅ Subscription ${stripeSubscription.status} for user ${userId} (tier: ${tier})`) break } case 'customer.subscription.deleted': { const subscription = event.data.object as Stripe.Subscription const sub = await prisma.subscription.findUnique({ where: { stripeSubscriptionId: subscription.id }, select: { userId: true } }) if (sub) { // Downgrade to free tier await prisma.user.update({ where: { id: sub.userId }, data: { subscriptionTier: 'free', conversationLimit: 10, subscriptionStatus: 'cancelled' } }) await prisma.subscription.update({ where: { stripeSubscriptionId: subscription.id }, data: { status: 'CANCELLED' } }) console.log(`✅ Subscription cancelled for user ${sub.userId}`) } break } case 'invoice.payment_succeeded': { const invoice = event.data.object as any if (invoice.subscription) { console.log(`✅ Payment succeeded for subscription ${invoice.subscription}`) // Ensure subscription is still active const subscription = await prisma.subscription.findUnique({ where: { stripeSubscriptionId: invoice.subscription as string } }) if (subscription) { await prisma.user.update({ where: { id: subscription.userId }, data: { subscriptionStatus: 'active' } }) } } break } case 'invoice.payment_failed': { const invoice = event.data.object as any if (invoice.subscription) { const subscription = await prisma.subscription.findUnique({ where: { stripeSubscriptionId: invoice.subscription as string } }) if (subscription) { await prisma.user.update({ where: { id: subscription.userId }, data: { subscriptionStatus: 'past_due' } }) console.warn(`⚠️ Payment failed for subscription ${invoice.subscription}`) } } break } default: console.log(`Unhandled event type: ${event.type}`) } return NextResponse.json({ received: true }) } catch (error) { console.error('Error processing webhook:', error) return NextResponse.json( { error: 'Webhook processing failed' }, { status: 500 } ) } }