Files
Shifted/app/api/cart/checkout/route.ts
2026-02-10 01:14:19 +00:00

326 lines
9.6 KiB
TypeScript

import { NextResponse } from "next/server";
import { prisma } from "../../../../lib/prisma";
import { stripeClient } from "../../../../lib/stripeClient";
import { cookies } from "next/headers";
import { getServerSession } from "next-auth";
import { authOptions } from "../../../../lib/auth";
export async function POST(req: Request) {
try {
const body = await req.json();
const slug = body.slug as string;
const shipping = body.shipping as
| {
rateId?: string;
amount?: number;
currency?: string;
provider?: string;
servicelevel?: string;
}
| undefined;
const shippingAddress = body.shippingAddress as
| {
name?: string;
street1?: string;
city?: string;
state?: string;
zip?: string;
country?: string;
phone?: string;
}
| undefined;
if (!slug) {
return NextResponse.json({ error: "slug is required." }, { status: 400 });
}
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL;
if (!baseUrl) {
return NextResponse.json(
{ error: "Missing NEXT_PUBLIC_BASE_URL." },
{ status: 500 }
);
}
const store = await prisma.store.findUnique({ where: { slug } });
if (!store) {
return NextResponse.json({ error: "Store not found." }, { status: 404 });
}
const session = await getServerSession(authOptions);
const cookieStore = cookies();
const sessionId = cookieStore.get("sf_session")?.value;
const createdNew = !sessionId;
if (!sessionId && !session?.user?.email) {
return NextResponse.json({ error: "Missing cart session." }, { status: 400 });
}
if (!store.stripeAccountId) {
return NextResponse.json(
{ error: "Store is not connected to Stripe yet." },
{ status: 400 }
);
}
const sessionCart = sessionId
? await prisma.cart.findUnique({
where: { storeId_sessionId: { storeId: store.id, sessionId } },
include: { items: true },
})
: null;
let cart = null;
if (session?.user?.email) {
const user = await prisma.user.findUnique({
where: { email: session.user.email },
});
if (!user) {
return NextResponse.json({ error: "User not found." }, { status: 404 });
}
const userCart = await prisma.cart.findUnique({
where: { storeId_userId: { storeId: store.id, userId: user.id } },
include: { items: true },
});
if (!userCart) {
if (sessionCart) {
cart = await prisma.cart.update({
where: { id: sessionCart.id },
data: { userId: user.id },
include: { items: true },
});
} else {
cart = await prisma.cart.create({
data: { storeId: store.id, userId: user.id, sessionId: sessionId || "" },
include: { items: true },
});
}
} else if (sessionCart && sessionCart.id !== userCart.id) {
for (const item of sessionCart.items) {
const existing = await prisma.cartItem.findFirst({
where: { cartId: userCart.id, priceId: item.priceId },
});
if (existing) {
await prisma.cartItem.update({
where: { id: existing.id },
data: { quantity: existing.quantity + item.quantity },
});
} else {
await prisma.cartItem.create({
data: {
cartId: userCart.id,
productId: item.productId,
priceId: item.priceId,
name: item.name,
unitAmount: item.unitAmount,
currency: item.currency,
quantity: item.quantity,
},
});
}
}
await prisma.cart.delete({ where: { id: sessionCart.id } });
cart = await prisma.cart.update({
where: { id: userCart.id },
data: { sessionId: sessionId || "" },
include: { items: true },
});
} else if (userCart.sessionId !== sessionId) {
cart = await prisma.cart.update({
where: { id: userCart.id },
data: { sessionId: sessionId || "" },
include: { items: true },
});
} else {
cart = userCart;
}
} else if (sessionId) {
cart =
sessionCart ??
(await prisma.cart.create({
data: { storeId: store.id, sessionId },
include: { items: true },
}));
}
if (!cart || cart.items.length === 0) {
return NextResponse.json(
{ error: "Cart is empty." },
{ status: 400 }
);
}
const itemsTotal = cart.items.reduce(
(sum, item) => sum + item.unitAmount * item.quantity,
0
);
const shippingCents =
shipping?.amount && shipping.amount > 0
? Math.round(shipping.amount * 100)
: 0;
const totalAmount = itemsTotal + shippingCents;
const adjustedTotal = Math.max(0, totalAmount);
// Build line items from cart items.
const lineItems = cart.items.map((item) => ({
price_data: {
currency: item.currency,
product_data: { name: item.name },
unit_amount: item.unitAmount,
},
quantity: item.quantity,
}));
if (shippingCents > 0) {
lineItems.push({
price_data: {
currency: (shipping?.currency || cart.items[0]?.currency || "usd").toLowerCase(),
product_data: {
name: "Shipping",
description: shipping?.servicelevel
? `${shipping.provider || "Carrier"} - ${shipping.servicelevel}`
: "Shipping",
},
unit_amount: shippingCents,
},
quantity: 1,
});
}
const sessionResult = await stripeClient.checkout.sessions.create(
{
line_items: lineItems,
payment_intent_data: {
application_fee_amount: Math.max(50, Math.floor(adjustedTotal * 0.1)),
},
mode: "payment",
success_url: `${baseUrl}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${baseUrl}/cancel`,
metadata: {
storeSlug: slug,
},
},
{
stripeAccount: store.stripeAccountId,
}
);
// Persist order draft for webhook processing (shipping label).
const orderItems = cart.items.map((i) => ({
name: i.name,
productId: i.productId,
priceId: i.priceId,
unitAmount: i.unitAmount,
currency: i.currency,
quantity: i.quantity,
}));
const userId = session?.user?.email
? (await prisma.user.findUnique({ where: { email: session.user.email } }))
?.id
: undefined;
if (userId && shippingAddress) {
await prisma.userAddress.upsert({
where: { userId },
update: {
name: shippingAddress.name || "",
street1: shippingAddress.street1 || "",
city: shippingAddress.city || "",
state: shippingAddress.state || "",
zip: shippingAddress.zip || "",
country: shippingAddress.country || "",
phone: shippingAddress.phone || "",
},
create: {
userId,
name: shippingAddress.name || "",
street1: shippingAddress.street1 || "",
city: shippingAddress.city || "",
state: shippingAddress.state || "",
zip: shippingAddress.zip || "",
country: shippingAddress.country || "",
phone: shippingAddress.phone || "",
},
});
}
await prisma.order.create({
data: {
storeId: store.id,
userId: userId || null,
sessionId: sessionId || null,
stripeCheckoutSessionId: sessionResult.id,
shippingRateId: shipping?.rateId || null,
shippingRateSnapshot: shipping || null,
shippingAddress: shippingAddress || null,
items: orderItems as any,
status: "pending",
},
});
// Track checkout started in Klaviyo if configured.
try {
const apiKey = process.env.KLAVIYO_PRIVATE_API_KEY;
const email = session?.user?.email;
if (apiKey && email) {
await fetch("https://a.klaviyo.com/api/events/", {
method: "POST",
headers: {
Authorization: `Klaviyo-API-Key ${apiKey}`,
"Content-Type": "application/json",
Accept: "application/json",
Revision: "2024-02-15",
},
body: JSON.stringify({
data: {
type: "event",
attributes: {
profile: {
data: {
type: "profile",
attributes: { email },
},
},
metric: {
data: {
type: "metric",
attributes: { name: "Checkout Started" },
},
},
properties: {
cart_total: adjustedTotal / 100,
items: cart.items.map((i) => ({
name: i.name,
quantity: i.quantity,
unit_amount: i.unitAmount / 100,
})),
},
},
},
}),
});
}
} catch {
// Ignore marketing errors.
}
const res = NextResponse.json({ url: sessionResult.url });
if (createdNew && sessionId) {
res.cookies.set("sf_session", sessionId, {
httpOnly: true,
sameSite: "lax",
path: "/",
});
}
return res;
} catch (err: any) {
return NextResponse.json(
{ error: err?.message || "Failed to create checkout session." },
{ status: 500 }
);
}
}