From 3eb38b75f996a85845697d4d84a9f7381e01e5f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Guti=C3=A9rrez?= <35082514+alezmad@users.noreply.github.com> Date: Sun, 1 Mar 2026 21:30:10 +0000 Subject: [PATCH] fix: guard against undefined order in stripe webhook Co-Authored-By: Claude Opus 4.6 --- .../api/webhooks/stripe-blueprint/route.ts | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 apps/web/src/app/api/webhooks/stripe-blueprint/route.ts diff --git a/apps/web/src/app/api/webhooks/stripe-blueprint/route.ts b/apps/web/src/app/api/webhooks/stripe-blueprint/route.ts new file mode 100644 index 0000000..6b31efe --- /dev/null +++ b/apps/web/src/app/api/webhooks/stripe-blueprint/route.ts @@ -0,0 +1,125 @@ +import { eq } from "drizzle-orm"; +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; +import Stripe from "stripe"; + +import { db } from "@turbostarter/db/server"; +import { blueprintOrder } from "@turbostarter/db/schema"; + +function getStripe(): Stripe { + const key = process.env.STRIPE_SECRET_KEY; + if (!key) throw new Error("STRIPE_SECRET_KEY is required"); + return new Stripe(key); +} + +export async function POST(request: NextRequest) { + const stripe = getStripe(); + const body = await request.text(); + const signature = request.headers.get("stripe-signature"); + + if (!signature) { + return NextResponse.json({ error: "No signature" }, { status: 400 }); + } + + const webhookSecret = process.env.STRIPE_BLUEPRINT_WEBHOOK_SECRET || process.env.STRIPE_WEBHOOK_SECRET; + if (!webhookSecret) { + return NextResponse.json({ error: "Webhook secret not configured" }, { status: 500 }); + } + + let event: Stripe.Event; + try { + event = stripe.webhooks.constructEvent(body, signature, webhookSecret); + } catch (err) { + console.error("Webhook signature verification failed:", err); + return NextResponse.json({ error: "Invalid signature" }, { status: 400 }); + } + + if (event.type === "checkout.session.completed") { + const session = event.data.object as Stripe.Checkout.Session; + + // Only handle blueprint purchases + if (session.metadata?.product !== "blueprint") { + return NextResponse.json({ received: true }); + } + + const { business_name, place_id, email, maps_url, language } = session.metadata; + + try { + // Create blueprint order + const [order] = await db + .insert(blueprintOrder) + .values({ + email: email || session.customer_email || "", + businessName: business_name || "Unknown", + placeId: place_id || null, + mapsUrl: maps_url || null, + language: language || "en", + stripeSessionId: session.id, + stripePaymentIntentId: + typeof session.payment_intent === "string" + ? session.payment_intent + : session.payment_intent?.id || null, + status: "pending", + }) + .returning(); + + if (!order) { + console.error("Failed to create blueprint order"); + return NextResponse.json({ received: true }); + } + + // Trigger engine fulfillment + const engineUrl = process.env.ENGINE_API_URL; + const callbackSecret = process.env.ENGINE_CALLBACK_SECRET; + const publicUrl = process.env.NEXT_PUBLIC_URL || "https://whyrating.com"; + + if (!engineUrl || !callbackSecret) { + console.error("ENGINE_API_URL or ENGINE_CALLBACK_SECRET not configured"); + await db + .update(blueprintOrder) + .set({ status: "failed", errorMessage: "Engine not configured" }) + .where(eq(blueprintOrder.id, order.id)); + return NextResponse.json({ received: true }); + } + + const fulfillUrl = maps_url || `https://www.google.com/maps/place/?q=place_id:${place_id}`; + + const fulfillResponse = await fetch(`${engineUrl}/api/blueprint/fulfill`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + url: fulfillUrl, + business_name, + place_id, + email: email || session.customer_email, + language: language || "en", + callback_url: `${publicUrl}/api/webhooks/engine`, + callback_secret: callbackSecret, + order_id: order.id, + }), + }); + + if (!fulfillResponse.ok) { + const errText = await fulfillResponse.text(); + console.error("Engine fulfill failed:", errText); + await db + .update(blueprintOrder) + .set({ status: "failed", errorMessage: `Engine error: ${errText}` }) + .where(eq(blueprintOrder.id, order.id)); + } else { + const fulfillData = (await fulfillResponse.json()) as { job_id: string }; + await db + .update(blueprintOrder) + .set({ + status: "processing", + engineJobId: fulfillData.job_id, + }) + .where(eq(blueprintOrder.id, order.id)); + } + } catch (error) { + console.error("Blueprint order creation failed:", error); + } + } + + return NextResponse.json({ received: true }); +}