102 lines
4.6 KiB
JavaScript
102 lines
4.6 KiB
JavaScript
/**
|
|
* 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, fallback } = req.body || {};
|
|
|
|
// The static map is authoritative for seed invoices (amount can't be
|
|
// tampered). DEMO ONLY: invoices the owner created live in the browser's
|
|
// localStorage, so the server doesn't know them — accept the client's
|
|
// amount/title as a fallback for those. In production every invoice would
|
|
// be resolved from the billing DB here and this fallback removed.
|
|
let invoice = INVOICES[invoiceId];
|
|
if (!invoice && fallback) {
|
|
const amount = Number(fallback.amount);
|
|
if (Number.isInteger(amount) && amount > 0) {
|
|
invoice = {
|
|
title: String(fallback.title || 'Payment').slice(0, 200),
|
|
amount,
|
|
currency: (fallback.currency || 'usd').toLowerCase(),
|
|
};
|
|
}
|
|
}
|
|
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' });
|
|
}
|
|
}
|