fix: guard against undefined order in stripe webhook
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
125
apps/web/src/app/api/webhooks/stripe-blueprint/route.ts
Normal file
125
apps/web/src/app/api/webhooks/stripe-blueprint/route.ts
Normal 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 });
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user