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:
@@ -5,7 +5,8 @@ import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import type { CartItem } from "../../lib/cart";
|
||||
import { useProducts } from "../../lib/products";
|
||||
import { formatPrice, formatDate, discountPercent } from "../../lib/format";
|
||||
import { computeCartTotals } from "../../lib/cartTotals";
|
||||
import { formatPrice, formatDate } from "../../lib/format";
|
||||
import { Reveal } from "../../components/Reveal";
|
||||
import { CheckoutSteps } from "../../components/CheckoutSteps";
|
||||
import { ORDER_KEY, type OrderSnapshot } from "../../lib/order";
|
||||
@@ -26,7 +27,9 @@ function parseOrderSnapshot(raw: string): OrderSnapshot | null {
|
||||
typeof data.orderNumber !== "string" ||
|
||||
typeof data.orderDateIso !== "string" ||
|
||||
typeof data.shippingCost !== "number" ||
|
||||
typeof data.paymentMethodTitle !== "string"
|
||||
typeof data.paymentMethodTitle !== "string" ||
|
||||
(data.discountCode !== null && typeof data.discountCode !== "string") ||
|
||||
typeof data.discountAmount !== "number"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
@@ -95,12 +98,13 @@ export function BestellbestaetigungContent() {
|
||||
.map((entry) => ({ entry, product: products.find((p) => p.id === entry.id) }))
|
||||
.filter((row): row is { entry: CartItem; product: NonNullable<(typeof row)["product"]> } => Boolean(row.product));
|
||||
|
||||
const subtotal = items.reduce((sum, { entry, product }) => sum + entry.qty * product.price, 0);
|
||||
const totalSavings = items.reduce((sum, { entry, product }) => {
|
||||
const discount = discountPercent(product.price, product.compareAtPrice);
|
||||
return discount !== null ? sum + entry.qty * (product.compareAtPrice! - product.price) : sum;
|
||||
}, 0);
|
||||
const total = subtotal + order.shippingCost;
|
||||
// Displays the *persisted* discount from the snapshot, not a fresh
|
||||
// re-derivation — the purchase already happened, this page is a
|
||||
// receipt, not a live cart, so it doesn't re-validate the code at all.
|
||||
const { subtotal, totalSavings, total } = computeCartTotals(items, order.shippingCost, {
|
||||
type: "fixed",
|
||||
value: order.discountAmount,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -205,6 +209,13 @@ export function BestellbestaetigungContent() {
|
||||
<span className="font-bold text-body-sm text-success">-{formatPrice(totalSavings)}</span>
|
||||
</div>
|
||||
)}
|
||||
{order.discountCode && (
|
||||
<div className="flex items-center w-full">
|
||||
<span className="text-body-sm text-success">Rabattcode ({order.discountCode})</span>
|
||||
<span className="flex-1" />
|
||||
<span className="font-bold text-body-sm text-success">-{formatPrice(order.discountAmount)}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center w-full">
|
||||
<span className="text-body-sm text-text-primary">Versand</span>
|
||||
<span className="flex-1" />
|
||||
|
||||
Reference in New Issue
Block a user