/** * Stripe — Create Checkout Session * -------------------------------- * Creates a Stripe-hosted Checkout Session and returns its URL so the frontend * can redirect the customer to Stripe's secure payment page. We never see or * store card details — that is the whole point of using hosted Checkout. * * Flow: Pay Now → POST here → Stripe Session → redirect to session.url → * Stripe hosted page → success_url / cancel_url back to the app. * * Env (set in Vercel project settings, and .env locally for `vercel dev`): * STRIPE_SECRET_KEY sk_live_… / sk_test_… (server only — never exposed) * * If STRIPE_SECRET_KEY is missing we return 503 with a clear message instead of * crashing, so the module degrades gracefully until credentials are provided. */ // Server-side source of truth for invoice amounts. Prices are resolved HERE, // never trusted from the client, so a tampered request can't change the amount. // In production this would be a DB/CRM lookup; mirrors src/modules/payments/data/invoices.js. const INVOICES = { 'INV-2041': { title: 'Roof Repair — Leak Detection & Shingle Replacement', amount: 184500, currency: 'usd' }, 'INV-2039': { title: 'Full Roof Inspection & Storm Damage Report', amount: 32500, currency: 'usd' }, 'INV-2012': { title: 'Gutter Cleaning & Maintenance Plan (Q1)', amount: 14900, currency: 'usd' }, }; export default async function handler(req, res) { if (req.method !== 'POST') { return res.status(405).json({ error: 'Method not allowed' }); } const secretKey = (process.env.STRIPE_SECRET_KEY || '').trim(); if (!secretKey) { return res.status(503).json({ error: 'Stripe is not configured. Set STRIPE_SECRET_KEY to enable payments.', code: 'stripe_not_configured', }); } try { const { invoiceId, customerEmail } = req.body || {}; const invoice = INVOICES[invoiceId]; if (!invoice) { return res.status(404).json({ error: `Unknown invoice: ${invoiceId}` }); } // Lazy import keeps the module out of the cold-start path when unconfigured. const Stripe = (await import('stripe')).default; const stripe = new Stripe(secretKey, { apiVersion: '2024-06-20' }); // Where Stripe sends the customer back. Derive the app origin from the // request so this works across local/preview/production without config. const origin = req.headers.origin || (req.headers.host ? `https://${req.headers.host}` : ''); const session = await stripe.checkout.sessions.create({ mode: 'payment', payment_method_types: ['card'], line_items: [ { quantity: 1, price_data: { currency: invoice.currency, unit_amount: invoice.amount, // integer, in cents product_data: { name: invoice.title, metadata: { invoiceId }, }, }, }, ], ...(customerEmail ? { customer_email: customerEmail } : {}), // Echoed back on the session + every webhook event for reconciliation. metadata: { invoiceId }, success_url: `${origin}/payments/success?session_id={CHECKOUT_SESSION_ID}`, cancel_url: `${origin}/payments/cancel?invoice=${encodeURIComponent(invoiceId)}`, }); return res.status(200).json({ id: session.id, url: session.url }); } catch (err) { console.error('[create-checkout-session] error:', err.message); return res.status(500).json({ error: err.message || 'Failed to create checkout session' }); } }