"use server"; import { pool } from "@/lib/db"; // Stripe API version type type StripeApiVersion = "2026-05-27.dahlia"; /** * Creates a Stripe PaymentIntent for the supplied cart so the browser * can confirm the payment with embedded Stripe Elements (Apple Pay / * Google Pay / card) without redirecting to a hosted page. * * Mirrors `createRetailStripeCheckoutSession` in `retail-checkout.ts`: * the PaymentIntent is the embedded equivalent of the Checkout Session. * Order creation itself still happens on `/checkout/success` so the * webhooks and order pipeline don't need to know which path the buyer * used. */ type LineItem = { id: string; name: string; price: number; quantity: number; }; type CustomerInfo = { name?: string; email?: string; }; type CreatePaymentIntentResult = | { success: true; clientSecret: string; paymentIntentId: string; amount: number } | { success: false; error: string }; export async function createRetailPaymentIntent( items: LineItem[], customer: CustomerInfo, brandId: string | null, stopId: string | null, shippingAddress?: { state?: string; postal_code?: string; city?: string } | null ): Promise { const stripeKey = process.env.STRIPE_SECRET_KEY; if (!stripeKey) { return { success: false, error: "Stripe not configured on this server." }; } if (!Array.isArray(items) || items.length === 0) { return { success: false, error: "Cart is empty." }; } const Stripe = (await import("stripe")).default; const stripe = new Stripe(stripeKey, { apiVersion: "2026-05-27.dahlia" as StripeApiVersion }); // Compute the subtotal in cents. We don't compute sales tax here — // Stripe's `automatic_tax` would be ideal but requires address collection // client-side; for this single-product corn box the price is tax-inclusive // (see Tuxedo Corn product card) so we treat the line total as final. const amount = items.reduce((sum, item) => { const qty = Math.max(1, Math.floor(item.quantity || 1)); const unit = Math.max(0, Math.round(Number(item.price) * 100)); return sum + unit * qty; }, 0); if (amount <= 0) { return { success: false, error: "Cart total must be greater than $0." }; } // Pull the brand name for Stripe receipts + metadata let brandName = "Route Commerce"; if (brandId) { try { const brandRes = await pool.query<{ name: string }>( "SELECT name FROM brands WHERE id = $1 LIMIT 1", [brandId] ); if (brandRes.rows[0]?.name) brandName = brandRes.rows[0].name; } catch { // ignore — use default } } // Build a short human-readable description for the Stripe dashboard const description = items .slice(0, 3) .map((i) => `${i.quantity}× ${i.name}`) .join(", "); try { const intent = await stripe.paymentIntents.create({ amount, currency: "usd", automatic_payment_methods: { enabled: true }, receipt_email: customer.email || undefined, description, metadata: { brand_id: brandId ?? "unknown", brand_name: brandName, stop_id: stopId ?? "", customer_name: customer.name ?? "", customer_email: customer.email ?? "", shipping_state: shippingAddress?.state ?? "", shipping_postal_code: shippingAddress?.postal_code ?? "", shipping_city: shippingAddress?.city ?? "", item_count: String(items.reduce((s, i) => s + i.quantity, 0)), source: "storefront_express", }, }); if (!intent.client_secret) { return { success: false, error: "Stripe did not return a client secret." }; } return { success: true, clientSecret: intent.client_secret, paymentIntentId: intent.id, amount: intent.amount, }; } catch (err) { const message = err instanceof Error ? err.message : "Failed to create payment intent."; return { success: false, error: message }; } }