Add real order persistence, customer accounts, and cart sync
Checkout now persists orders server-side (Payload orders collection, re-priced from live product data, discount codes redeemed exactly once) instead of writing a client-only sessionStorage snapshot. Buying requires an account (registration inline in checkout, no separate step) — accounts get order history with delivery status, profile/address editing, password change, and a cart that syncs across devices while logged in. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import type { CartItem } from "../../../lib/cart";
|
||||
import { getServerCart, getSessionCustomer, saveServerCart } from "../../../lib/customerAuth";
|
||||
import { fetchProductsBySlug } from "../../../lib/productsServer";
|
||||
|
||||
export async function GET() {
|
||||
const session = await getSessionCustomer();
|
||||
if (!session) return NextResponse.json({ cart: [] }, { status: 401 });
|
||||
const cart = await getServerCart(session.token);
|
||||
return NextResponse.json({ cart });
|
||||
}
|
||||
|
||||
// Called by CartSync.tsx (debounced) on every local cart change while a
|
||||
// session is active — keeps the server-side mirror current so the cart
|
||||
// follows the customer across devices. Silently no-ops when logged out;
|
||||
// the caller doesn't care either way.
|
||||
export async function POST(request: Request) {
|
||||
const session = await getSessionCustomer();
|
||||
if (!session) return NextResponse.json({ ok: false }, { status: 401 });
|
||||
|
||||
const body = await request.json().catch(() => null);
|
||||
const cart: CartItem[] = Array.isArray(body?.cart) ? body.cart : [];
|
||||
|
||||
const productsBySlug = await fetchProductsBySlug();
|
||||
const lines = cart
|
||||
.map((item) => {
|
||||
const product = productsBySlug.get(item.id);
|
||||
return product ? { productId: product.id, productSlug: product.slug, quantity: item.qty } : null;
|
||||
})
|
||||
.filter((line): line is { productId: number; productSlug: string; quantity: number } => line !== null);
|
||||
|
||||
const ok = await saveServerCart(session.token, session.customer.id, lines);
|
||||
return NextResponse.json({ ok });
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { loginCustomer, setSessionCookie } from "../../../lib/customerAuth";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const body = await request.json().catch(() => null);
|
||||
const email = typeof body?.email === "string" ? body.email : "";
|
||||
const password = typeof body?.password === "string" ? body.password : "";
|
||||
if (!email || !password) {
|
||||
return NextResponse.json({ ok: false, reason: "Bitte E-Mail und Passwort angeben." }, { status: 400 });
|
||||
}
|
||||
|
||||
const result = await loginCustomer({ email, password });
|
||||
if (!result.ok) return NextResponse.json(result, { status: 401 });
|
||||
|
||||
await setSessionCookie(result.token);
|
||||
return NextResponse.json({ ok: true, customer: result.customer });
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { clearSessionCookie } from "../../../lib/customerAuth";
|
||||
|
||||
export async function POST() {
|
||||
await clearSessionCookie();
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getSessionCustomer } from "../../../lib/customerAuth";
|
||||
|
||||
export async function GET() {
|
||||
const session = await getSessionCustomer();
|
||||
if (!session) return NextResponse.json({ customer: null }, { status: 401 });
|
||||
return NextResponse.json({ customer: session.customer });
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getCustomerOrders, getSessionCustomer } from "../../../lib/customerAuth";
|
||||
|
||||
export async function GET() {
|
||||
const session = await getSessionCustomer();
|
||||
if (!session) return NextResponse.json({ orders: [] }, { status: 401 });
|
||||
|
||||
const orders = await getCustomerOrders(session.token, session.customer.id);
|
||||
return NextResponse.json({ orders });
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { changeCustomerPassword, getSessionCustomer } from "../../../lib/customerAuth";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const session = await getSessionCustomer();
|
||||
if (!session) return NextResponse.json({ ok: false, reason: "Bitte zuerst einloggen." }, { status: 401 });
|
||||
|
||||
const body = await request.json().catch(() => null);
|
||||
const currentPassword = typeof body?.currentPassword === "string" ? body.currentPassword : "";
|
||||
const newPassword = typeof body?.newPassword === "string" ? body.newPassword : "";
|
||||
if (!currentPassword || !newPassword || newPassword.length < 8) {
|
||||
return NextResponse.json({ ok: false, reason: "Bitte aktuelles und ein neues Passwort (mind. 8 Zeichen) angeben." }, { status: 400 });
|
||||
}
|
||||
|
||||
const result = await changeCustomerPassword(session.customer.email, currentPassword, newPassword);
|
||||
return NextResponse.json(result, { status: result.ok ? 200 : 400 });
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getSessionCustomer, updateCustomerProfile } from "../../../lib/customerAuth";
|
||||
|
||||
export async function GET() {
|
||||
const session = await getSessionCustomer();
|
||||
if (!session) return NextResponse.json({ profile: null }, { status: 401 });
|
||||
return NextResponse.json({ profile: session.customer });
|
||||
}
|
||||
|
||||
export async function PATCH(request: Request) {
|
||||
const session = await getSessionCustomer();
|
||||
if (!session) return NextResponse.json({ ok: false, reason: "Bitte zuerst einloggen." }, { status: 401 });
|
||||
|
||||
const body = await request.json().catch(() => null);
|
||||
const { firstName, lastName, deliveryMethod, street, packstationNumber, postNumber, zip, city, country } = body ?? {};
|
||||
if (
|
||||
typeof firstName !== "string" ||
|
||||
!firstName ||
|
||||
typeof lastName !== "string" ||
|
||||
!lastName ||
|
||||
(deliveryMethod !== "address" && deliveryMethod !== "packstation") ||
|
||||
typeof zip !== "string" ||
|
||||
!zip ||
|
||||
typeof city !== "string" ||
|
||||
!city ||
|
||||
typeof country !== "string" ||
|
||||
!country
|
||||
) {
|
||||
return NextResponse.json({ ok: false, reason: "Bitte alle Pflichtfelder ausfüllen." }, { status: 400 });
|
||||
}
|
||||
if (deliveryMethod === "address" && !street) {
|
||||
return NextResponse.json({ ok: false, reason: "Bitte Straße und Hausnummer angeben." }, { status: 400 });
|
||||
}
|
||||
if (deliveryMethod === "packstation" && (!packstationNumber || !postNumber)) {
|
||||
return NextResponse.json({ ok: false, reason: "Bitte Packstation- und Postnummer angeben." }, { status: 400 });
|
||||
}
|
||||
|
||||
const result = await updateCustomerProfile(session.token, session.customer.id, {
|
||||
firstName,
|
||||
lastName,
|
||||
deliveryMethod,
|
||||
street,
|
||||
packstationNumber,
|
||||
postNumber,
|
||||
zip,
|
||||
city,
|
||||
country,
|
||||
});
|
||||
return NextResponse.json(result, { status: result.ok ? 200 : 400 });
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { registerCustomer, setSessionCookie } from "../../../lib/customerAuth";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const body = await request.json().catch(() => null);
|
||||
const { firstName, lastName, email, password } = body ?? {};
|
||||
if (
|
||||
typeof firstName !== "string" ||
|
||||
typeof lastName !== "string" ||
|
||||
typeof email !== "string" ||
|
||||
typeof password !== "string" ||
|
||||
!firstName ||
|
||||
!lastName ||
|
||||
!email ||
|
||||
!password
|
||||
) {
|
||||
return NextResponse.json({ ok: false, reason: "Bitte alle Felder ausfüllen." }, { status: 400 });
|
||||
}
|
||||
|
||||
const result = await registerCustomer({ firstName, lastName, email, password });
|
||||
if (!result.ok) return NextResponse.json(result, { status: 400 });
|
||||
|
||||
await setSessionCookie(result.token);
|
||||
return NextResponse.json({ ok: true, customer: result.customer });
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import type { CartItem } from "../../lib/cart";
|
||||
import { getShippingMethods, getPaymentMethods } from "../../lib/payload";
|
||||
import { validateDiscountCode, redeemDiscountCode } from "../../lib/discountServer";
|
||||
import { createOrder } from "../../lib/orderServer";
|
||||
import { getSessionCustomer, registerCustomer, setSessionCookie, type CustomerSummary } from "../../lib/customerAuth";
|
||||
import { fetchProductsBySlug } from "../../lib/productsServer";
|
||||
|
||||
type CheckoutBody = {
|
||||
cart: CartItem[];
|
||||
shippingMethodId: number;
|
||||
paymentMethodId: number;
|
||||
discountCode: string | null;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
password?: string;
|
||||
deliveryMethod: "address" | "packstation";
|
||||
street?: string;
|
||||
packstationNumber?: string;
|
||||
postNumber?: string;
|
||||
zip: string;
|
||||
city: string;
|
||||
country: string;
|
||||
newsletterOptIn: boolean;
|
||||
};
|
||||
|
||||
function isValidBody(body: unknown): body is CheckoutBody {
|
||||
const b = body as Partial<CheckoutBody> | null;
|
||||
return Boolean(
|
||||
b &&
|
||||
Array.isArray(b.cart) &&
|
||||
b.cart.length > 0 &&
|
||||
typeof b.shippingMethodId === "number" &&
|
||||
typeof b.paymentMethodId === "number" &&
|
||||
typeof b.firstName === "string" &&
|
||||
b.firstName &&
|
||||
typeof b.lastName === "string" &&
|
||||
b.lastName &&
|
||||
typeof b.email === "string" &&
|
||||
b.email &&
|
||||
(b.deliveryMethod === "address" || b.deliveryMethod === "packstation") &&
|
||||
typeof b.zip === "string" &&
|
||||
b.zip &&
|
||||
typeof b.city === "string" &&
|
||||
b.city &&
|
||||
typeof b.country === "string" &&
|
||||
b.country,
|
||||
);
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const body = await request.json().catch(() => null);
|
||||
if (!isValidBody(body)) {
|
||||
return NextResponse.json({ ok: false, reason: "Bitte alle Pflichtfelder ausfüllen." }, { status: 400 });
|
||||
}
|
||||
if (body.deliveryMethod === "address" && !body.street) {
|
||||
return NextResponse.json({ ok: false, reason: "Bitte Straße und Hausnummer angeben." }, { status: 400 });
|
||||
}
|
||||
if (body.deliveryMethod === "packstation" && (!body.packstationNumber || !body.postNumber)) {
|
||||
return NextResponse.json({ ok: false, reason: "Bitte Packstation- und Postnummer angeben." }, { status: 400 });
|
||||
}
|
||||
|
||||
// Auth: an existing session wins; otherwise this checkout submit doubles
|
||||
// as inline registration ("Konto Pflicht, Registrierung direkt im
|
||||
// Checkout") — logging in with an *existing* account happens separately
|
||||
// beforehand via /api/account/login from the checkout page's own toggle.
|
||||
let customer: CustomerSummary;
|
||||
const session = await getSessionCustomer();
|
||||
if (session) {
|
||||
customer = session.customer;
|
||||
} else {
|
||||
if (!body.password) {
|
||||
return NextResponse.json({ ok: false, reason: "Bitte ein Passwort für dein neues Konto vergeben." }, { status: 400 });
|
||||
}
|
||||
const result = await registerCustomer({
|
||||
firstName: body.firstName,
|
||||
lastName: body.lastName,
|
||||
email: body.email,
|
||||
password: body.password,
|
||||
});
|
||||
if (!result.ok) return NextResponse.json(result, { status: 400 });
|
||||
await setSessionCookie(result.token);
|
||||
customer = result.customer;
|
||||
}
|
||||
|
||||
// Re-price everything server-side — never trust client-submitted prices.
|
||||
const productsBySlug = await fetchProductsBySlug();
|
||||
const items: { productId: number; productName: string; quantity: number; unitPrice: number }[] = [];
|
||||
for (const line of body.cart) {
|
||||
const product = productsBySlug.get(line.id);
|
||||
if (!product) return NextResponse.json({ ok: false, reason: "Ein Artikel im Warenkorb ist nicht mehr verfügbar." }, { status: 400 });
|
||||
items.push({ productId: product.id, productName: product.name, quantity: line.qty, unitPrice: product.price });
|
||||
}
|
||||
const subtotal = items.reduce((sum, i) => sum + i.quantity * i.unitPrice, 0);
|
||||
|
||||
const shippingMethods = await getShippingMethods();
|
||||
const shippingMethod = shippingMethods.find((m) => m.id === body.shippingMethodId);
|
||||
if (!shippingMethod) return NextResponse.json({ ok: false, reason: "Versandart ist ungültig." }, { status: 400 });
|
||||
const freeShipping = shippingMethod.freeShippingThreshold != null && subtotal >= shippingMethod.freeShippingThreshold;
|
||||
const shippingCost = freeShipping ? 0 : shippingMethod.price;
|
||||
|
||||
const paymentMethods = await getPaymentMethods();
|
||||
const paymentMethod = paymentMethods.find((m) => m.id === body.paymentMethodId);
|
||||
if (!paymentMethod) return NextResponse.json({ ok: false, reason: "Zahlungsart ist ungültig." }, { status: 400 });
|
||||
|
||||
let discountAmount = 0;
|
||||
if (body.discountCode) {
|
||||
const validation = await validateDiscountCode(body.discountCode, subtotal);
|
||||
if (!validation.valid) return NextResponse.json({ ok: false, reason: validation.reason }, { status: 400 });
|
||||
const redeemed = await redeemDiscountCode(validation.doc);
|
||||
if (!redeemed) return NextResponse.json({ ok: false, reason: "Rabattcode konnte nicht eingelöst werden." }, { status: 400 });
|
||||
discountAmount =
|
||||
validation.doc.type === "percent" ? (subtotal * validation.doc.value) / 100 : Math.min(validation.doc.value, subtotal);
|
||||
}
|
||||
const total = Math.max(0, subtotal - discountAmount) + shippingCost;
|
||||
|
||||
const order = await createOrder({
|
||||
customerId: customer.id,
|
||||
customerFirstName: body.firstName,
|
||||
customerLastName: body.lastName,
|
||||
customerEmail: body.email,
|
||||
deliveryMethod: body.deliveryMethod,
|
||||
street: body.street,
|
||||
packstationNumber: body.packstationNumber,
|
||||
postNumber: body.postNumber,
|
||||
zip: body.zip,
|
||||
city: body.city,
|
||||
country: body.country,
|
||||
newsletterOptIn: Boolean(body.newsletterOptIn),
|
||||
items,
|
||||
subtotal,
|
||||
shippingCost,
|
||||
shippingMethodTitle: shippingMethod.title,
|
||||
paymentMethodTitle: paymentMethod.title,
|
||||
discountCode: body.discountCode || null,
|
||||
discountAmount,
|
||||
total,
|
||||
});
|
||||
if (!order) return NextResponse.json({ ok: false, reason: "Bestellung konnte nicht gespeichert werden." }, { status: 500 });
|
||||
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
orderNumber: order.orderNumber,
|
||||
orderDateIso: order.createdAt,
|
||||
shippingCost,
|
||||
paymentMethodTitle: paymentMethod.title,
|
||||
discountCode: body.discountCode || null,
|
||||
discountAmount,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user