import { NextRequest, NextResponse } from 'next/server'; import { auth } from '@/auth'; import { stripe } from '@/lib/stripe'; import { isBillingEnabled, resolvePriceId } from '@/lib/billing/stripe-prices'; import { prisma } from '@/lib/prisma'; import { z } from 'zod'; const bodySchema = z.object({ tier: z.enum(['PRO', 'BUSINESS']), interval: z.enum(['month', 'year']), /** Prefer hosted redirect when embedded checkout is unavailable */ mode: z.enum(['hosted', 'embedded']).optional(), }); export async function POST(req: NextRequest) { const session = await auth(); if (!session?.user?.id || !session.user.email) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } if (!(await isBillingEnabled())) { return NextResponse.json({ error: 'Billing is not enabled' }, { status: 403 }); } const secret = process.env.STRIPE_SECRET_KEY; if (!secret || secret === 'sk_test_placeholder') { return NextResponse.json( { error: 'Stripe is not configured (STRIPE_SECRET_KEY missing)' }, { status: 503 }, ); } const parsed = bodySchema.safeParse(await req.json()); if (!parsed.success) { return NextResponse.json({ error: 'Invalid request body' }, { status: 400 }); } const { tier, interval } = parsed.data; const preferredMode = parsed.data.mode ?? 'hosted'; const userId = session.user.id; const userEmail = session.user.email; try { let priceId: string; try { priceId = await resolvePriceId(tier, interval); } catch (e) { console.error('[billing/create-checkout] price resolve failed:', e); return NextResponse.json( { error: 'Stripe price IDs not configured. Set them in Admin > Billing.' }, { status: 503 }, ); } if (priceId.startsWith('price_mock_')) { return NextResponse.json( { error: 'Stripe price IDs not configured. Set them in Admin > Billing.' }, { status: 503 }, ); } const subscription = await prisma.subscription.findUnique({ where: { userId } }); let customerId = subscription?.stripeCustomerId ?? undefined; if (customerId && customerId.startsWith('cus_mock')) { customerId = undefined; } if (!customerId) { const customer = await stripe.customers.create({ email: userEmail, metadata: { userId }, }); customerId = customer.id; await prisma.subscription.upsert({ where: { userId }, update: { stripeCustomerId: customerId }, create: { userId, stripeCustomerId: customerId, tier: 'BASIC', status: 'ACTIVE', currentPeriodStart: new Date(), currentPeriodEnd: new Date(Date.now() + 30 * 24 * 3600 * 1000), }, }); } const host = req.headers.get('x-forwarded-host') ?? req.headers.get('host') ?? 'localhost:3000'; const proto = req.headers.get('x-forwarded-proto') ?? 'http'; const origin = `${proto}://${host}`; // Hosted Checkout is the most reliable path (redirect). Embedded is optional. if (preferredMode === 'embedded') { try { const embedded = await stripe.checkout.sessions.create({ customer: customerId, mode: 'subscription', line_items: [{ price: priceId, quantity: 1 }], ui_mode: 'embedded' as any, return_url: `${origin}/settings/billing?session_id={CHECKOUT_SESSION_ID}`, metadata: { userId, tier }, subscription_data: { metadata: { userId, tier } }, customer_update: { address: 'auto' }, allow_promotion_codes: true, } as any); if (embedded.client_secret) { return NextResponse.json({ clientSecret: embedded.client_secret, sessionId: embedded.id, }); } } catch (embeddedErr) { console.warn('[billing/create-checkout] embedded failed, falling back to hosted:', embeddedErr); } } const checkoutSession = await stripe.checkout.sessions.create({ customer: customerId, mode: 'subscription', line_items: [{ price: priceId, quantity: 1 }], success_url: `${origin}/settings/billing?session_id={CHECKOUT_SESSION_ID}`, cancel_url: `${origin}/settings/billing?canceled=1`, metadata: { userId, tier }, subscription_data: { metadata: { userId, tier } }, customer_update: { address: 'auto' }, allow_promotion_codes: true, }); if (!checkoutSession.url) { return NextResponse.json({ error: 'Checkout session has no URL' }, { status: 500 }); } return NextResponse.json({ url: checkoutSession.url, sessionId: checkoutSession.id }); } catch (error) { console.error('[billing/create-checkout]', error); const msg = error instanceof Error ? error.message : 'Failed to create checkout session'; return NextResponse.json({ error: msg }, { status: 500 }); } }