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