import { NextRequest, NextResponse } from 'next/server' import { prisma } from '@/lib/prisma' import { stripeService } from '@/lib/services/stripe-service' import { OrderStatus, AutomationMode, SubscriptionStatus, UserStatus, } from '@prisma/client' import { emailService } from '@/lib/services/email-service' import { randomBytes } from 'crypto' import bcrypt from 'bcryptjs' /** * Generate a customer ID slug from company/user name * Only lowercase letters allowed (env_setup.sh requires ^[a-z]+$) */ function slugifyCustomer(name: string): string { return name .toLowerCase() .replace(/[^a-z]/g, '') .substring(0, 32) } /** * Generate a unique license key for the order */ function generateLicenseKey(): string { const hex = randomBytes(16).toString('hex') return `lb_inst_${hex}` } /** * POST /api/v1/webhooks/stripe * * Handles Stripe webhook events. The primary event is checkout.session.completed, * which creates a Customer, Order, and Subscription in the Hub database. * * Important: The raw body must be passed to constructEvent for signature verification. * Next.js App Router provides the raw body via request.text(). */ export async function POST(request: NextRequest) { if (!stripeService.isConfigured()) { console.error('Stripe webhook received but Stripe is not configured') return NextResponse.json( { error: 'Stripe is not configured' }, { status: 500 } ) } // Get the raw body as text for signature verification const rawBody = await request.text() // Get the Stripe signature header const signature = request.headers.get('stripe-signature') if (!signature) { return NextResponse.json( { error: 'Missing stripe-signature header' }, { status: 400 } ) } // Verify webhook signature and construct event let event try { event = stripeService.constructWebhookEvent(rawBody, signature) } catch (err) { const message = err instanceof Error ? err.message : 'Unknown error' console.error(`Stripe webhook signature verification failed: ${message}`) return NextResponse.json( { error: `Webhook signature verification failed: ${message}` }, { status: 400 } ) } // Handle the event try { switch (event.type) { case 'checkout.session.completed': { await handleCheckoutSessionCompleted(event.data.object as unknown as Record) break } default: // Acknowledge other events without processing console.log(`Unhandled Stripe event type: ${event.type}`) } } catch (err) { const message = err instanceof Error ? err.message : 'Unknown error' console.error(`Error handling Stripe event ${event.type}: ${message}`) return NextResponse.json( { error: `Failed to handle event: ${message}` }, { status: 500 } ) } return NextResponse.json({ received: true }) } /** * Handle checkout.session.completed event * * 1. Find or create User by email * 2. Create Subscription with Stripe IDs * 3. Create Order with PAYMENT_CONFIRMED status */ async function handleCheckoutSessionCompleted( session: Record ) { const customerEmail = session.customer_email as string | undefined const customerName = (session.metadata as Record)?.customer_name const customerDomain = (session.metadata as Record)?.domain const stripeCustomerId = session.customer as string | undefined const stripeSubscriptionId = session.subscription as string | undefined if (!customerEmail) { throw new Error('checkout.session.completed missing customer_email') } // Extract plan info from session metadata const planMapping = stripeService.extractPlanFromSession( session as unknown as import('stripe').Stripe.Checkout.Session ) if (!planMapping) { throw new Error( 'Could not determine plan from checkout session. ' + 'Ensure metadata includes price_id or plan.' ) } // 1. Find or create User let user = await prisma.user.findUnique({ where: { email: customerEmail }, }) if (!user) { const randomPassword = randomBytes(16).toString('hex') const passwordHash = await bcrypt.hash(randomPassword, 10) user = await prisma.user.create({ data: { email: customerEmail, name: customerName || null, passwordHash, status: UserStatus.ACTIVE, }, }) } // 2. Create Subscription await prisma.subscription.create({ data: { userId: user.id, plan: planMapping.plan, tier: planMapping.tier, tokenLimit: planMapping.tokenLimit, status: SubscriptionStatus.ACTIVE, stripeCustomerId: stripeCustomerId || null, stripeSubscriptionId: stripeSubscriptionId || null, }, }) // 3. Create Order const displayName = customerName || user.company || user.name || 'customer' const customerSlug = slugifyCustomer(displayName) || 'customer' const domain = customerDomain || `${customerSlug}.letsbe.cloud` const licenseKey = generateLicenseKey() await prisma.order.create({ data: { userId: user.id, domain, tier: planMapping.tier, tools: planMapping.tools, status: OrderStatus.PAYMENT_CONFIRMED, automationMode: AutomationMode.AUTO, source: 'stripe', customer: customerSlug, companyName: displayName, licenseKey, configJson: { tools: planMapping.tools, tier: planMapping.tier, domain, stripeSessionId: session.id as string, stripeCustomerId, stripeSubscriptionId, }, }, }) console.log( `Stripe checkout completed: created order for ${customerEmail} ` + `(plan: ${planMapping.plan}, domain: ${domain})` ) // 4. Send welcome email (non-blocking - don't fail webhook on email error) try { const emailResult = await emailService.sendWelcomeEmail({ to: customerEmail, customerName: customerName || user.name || '', plan: planMapping.plan, domain, }) if (!emailResult.success) { console.warn(`Welcome email failed for ${customerEmail}: ${emailResult.error}`) } } catch (emailErr) { console.warn('Failed to send welcome email:', emailErr) } }