Out-of-stock UI, variant picker on marketing pages, server-side stock check
- ProductGrid/AddToCartInlineButton/AddToCartButton now show "Ausverkauft" and disable add-to-cart per variant (or product-level with no variants), derived from trackInventory/stock/allowBackorder via isOutOfStock(). - AddToCartButton (todo-cards Hero+Pricing, homepage spotlight) gains the same variant <select> AddToCartInlineButton already had — all three call sites already fetch full product data server-side. - /api/checkout re-validates stock server-side (depth-in-defense, not just the disabled button), rejecting when trackInventory is on, allowBackorder is off, and requested qty exceeds stock. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018PL4zfTY1sXc8x5QS6FatM
This commit is contained in:
+26
-3
@@ -170,7 +170,13 @@ export type Product = {
|
||||
spotlightHeadline: string | null;
|
||||
spotlightText: string | null;
|
||||
spotlightImage: string | null;
|
||||
variants: { name: string; priceOverride: number | null }[];
|
||||
// Plain booleans, not the raw stock/threshold numbers — the public API
|
||||
// has no reason to leak exact stock counts, callers only ever need
|
||||
// "can this be bought right now". `outOfStock` on the product itself
|
||||
// only matters for a product with no variants; a varianted product's
|
||||
// buyability is entirely per-variant (see each variant's own flag).
|
||||
outOfStock: boolean;
|
||||
variants: { name: string; priceOverride: number | null; outOfStock: boolean }[];
|
||||
};
|
||||
|
||||
type PayloadProduct = {
|
||||
@@ -189,9 +195,21 @@ type PayloadProduct = {
|
||||
spotlightHeadline: string | null;
|
||||
spotlightText: string | null;
|
||||
spotlightImage: { url: string } | number | null;
|
||||
variants: { name: string; priceOverride: number | null }[] | null;
|
||||
trackInventory: boolean;
|
||||
stock: number | null;
|
||||
allowBackorder: boolean;
|
||||
variants: { name: string; priceOverride: number | null; trackInventory: boolean; stock: number | null; allowBackorder: boolean }[] | null;
|
||||
};
|
||||
|
||||
// A product/variant is only actually unbuyable when it opted into
|
||||
// inventory tracking AND has zero stock AND backorders aren't allowed —
|
||||
// the same three-condition check lib/inventory.ts's adjustStock() effectively
|
||||
// mirrors from the other direction (it only ever touches stock when
|
||||
// trackInventory is on in the first place).
|
||||
function isOutOfStock(trackInventory: boolean, stock: number | null, allowBackorder: boolean): boolean {
|
||||
return trackInventory && !allowBackorder && (stock ?? 0) <= 0;
|
||||
}
|
||||
|
||||
// Shared by getProducts() and getPostBySlug()'s relatedProduct — kept in
|
||||
// one place instead of duplicating the same field mapping, which is
|
||||
// exactly the kind of drift this session's Shipping Settings work was
|
||||
@@ -213,7 +231,12 @@ export function mapPayloadProduct(product: PayloadProduct): Product {
|
||||
spotlightText: product.spotlightText || null,
|
||||
spotlightImage:
|
||||
typeof product.spotlightImage === "object" && product.spotlightImage ? product.spotlightImage.url : null,
|
||||
variants: product.variants ?? [],
|
||||
outOfStock: isOutOfStock(product.trackInventory, product.stock, product.allowBackorder),
|
||||
variants: (product.variants ?? []).map((v) => ({
|
||||
name: v.name,
|
||||
priceOverride: v.priceOverride,
|
||||
outOfStock: isOutOfStock(v.trackInventory, v.stock, v.allowBackorder),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user