326 lines
9.6 KiB
TypeScript
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 }
|
|
);
|
|
}
|
|
}
|