fix: guard against undefined order in stripe webhook

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-03-01 21:30:10 +00:00
parent 0c66bab042
commit 3eb38b75f9

View File

@@ -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 });
}