Add discount code feature (server-validated) and RelatedProducts polish
Discount codes: - New shared lib/cartTotals.ts (computeSubtotal/computeCartTotals) factored out of the previously-triplicated subtotal/totalSavings/total math in CartContent/CheckoutContent/BestellbestaetigungContent, extended to also fold in a discount amount (percent or fixed, clamped so total can't go negative). - lib/discount.ts mirrors lib/cart.ts's exact localStorage pattern so an applied code survives the /cart -> /checkout transition without a second input field — Checkout only displays it. - New /api/discount/validate (read-only check) and /api/discount/redeem (re-validates + increments the redemption counter, called once from checkout's handlePurchase right before the OrderSnapshot is written). Both talk to Payload's new discount-codes collection through lib/discountServer.ts, a server-only module kept separate from lib/payload.ts on purpose (that file is also imported by "use client" components; the RSC-boundary break hit earlier this session was exactly this mistake with next/headers). - OrderSnapshot gains discountCode/discountAmount so /bestellbestaetigung displays what was actually applied instead of losing it on recompute. RelatedProducts: no longer falls back to re-suggesting a product already in the cart just to pad the grid out to 3 cards — shows only the genuinely available remainder (down to 1 card), centered in the 12-column grid instead of left-aligned.
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { validateDiscountCode, redeemDiscountCode } from "../../../lib/discountServer";
|
||||
|
||||
// Called once, from CheckoutContent.tsx's handlePurchase(), right before
|
||||
// the OrderSnapshot is written — re-validates (the window/limit may have
|
||||
// changed since the cart-side /validate check, however unlikely) and only
|
||||
// then increments the redemption counter. If this fails, the caller must
|
||||
// not complete the purchase with a dead code silently still showing as
|
||||
// applied.
|
||||
export async function POST(request: Request) {
|
||||
const body = await request.json().catch(() => null);
|
||||
const code = typeof body?.code === "string" ? body.code : "";
|
||||
const subtotal = typeof body?.subtotal === "number" ? body.subtotal : 0;
|
||||
|
||||
if (!code) {
|
||||
return NextResponse.json({ redeemed: false, reason: "Kein Code angegeben." }, { status: 400 });
|
||||
}
|
||||
|
||||
const result = await validateDiscountCode(code, subtotal);
|
||||
if (!result.valid) return NextResponse.json({ redeemed: false, reason: result.reason });
|
||||
|
||||
const ok = await redeemDiscountCode(result.doc);
|
||||
if (!ok) return NextResponse.json({ redeemed: false, reason: "Rabattcode konnte nicht eingelöst werden." });
|
||||
|
||||
return NextResponse.json({ redeemed: true });
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { validateDiscountCode } from "../../../lib/discountServer";
|
||||
|
||||
// Called from CartContent.tsx when a shopper clicks "Anwenden" — read-only
|
||||
// check (active/window/minOrderValue/remaining-redemptions), does NOT
|
||||
// increment the redemption counter. That only happens in /redeem, at
|
||||
// actual purchase time (see CheckoutContent.tsx's handlePurchase()).
|
||||
export async function POST(request: Request) {
|
||||
const body = await request.json().catch(() => null);
|
||||
const code = typeof body?.code === "string" ? body.code : "";
|
||||
const subtotal = typeof body?.subtotal === "number" ? body.subtotal : 0;
|
||||
|
||||
if (!code) {
|
||||
return NextResponse.json({ valid: false, reason: "Bitte einen Code eingeben." }, { status: 400 });
|
||||
}
|
||||
|
||||
const result = await validateDiscountCode(code, subtotal);
|
||||
if (!result.valid) return NextResponse.json(result);
|
||||
|
||||
return NextResponse.json({ valid: true, type: result.doc.type, value: result.doc.value });
|
||||
}
|
||||
Reference in New Issue
Block a user