Cap add-to-cart quantity at actual remaining stock
Stock was only checked at checkout; a shopper could add more of a product to the cart than was actually in stock and only find out at the last step. Product/variant now carry a real maxQty, and AddToCartButton/AddToCartInlineButton/the cart's quantity stepper all disable or cap once the cart already holds that many. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,7 @@ const product = (overrides: Partial<Product> = {}): Product => ({
|
||||
variants: [],
|
||||
outOfStock: false,
|
||||
lowStock: false,
|
||||
maxQty: null,
|
||||
taxRatePercent: null,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
+18
-1
@@ -179,13 +179,21 @@ export type Product = {
|
||||
// Derived, like outOfStock — no raw stock count/threshold leaked, callers
|
||||
// only ever need "should a low-stock hint show for this right now".
|
||||
lowStock: boolean;
|
||||
// Unlike outOfStock/lowStock, this DOES expose the real number — it's
|
||||
// the cap the add-to-cart controls (AddToCartButton/AddToCartInlineButton,
|
||||
// CartContent's quantity stepper) need client-side to stop a shopper from
|
||||
// putting more in the cart than checkout would actually accept, instead
|
||||
// of only finding out at the very last step (api/checkout/route.ts's own
|
||||
// stock check, which stays as the authoritative server-side guard). null
|
||||
// means "no cap" — backorder allowed or inventory not tracked.
|
||||
maxQty: number | null;
|
||||
// Per-product override — null means "use the tenant's default rate"
|
||||
// (CompanySettings.taxRatePercent, fetched separately since it's behind
|
||||
// an admin-only secret, see getCompanySettings()). Display-only on the
|
||||
// storefront; the actual rate used for order totals is resolved and
|
||||
// snapshotted server-side at checkout (api/checkout/route.ts).
|
||||
taxRatePercent: number | null;
|
||||
variants: { name: string; priceOverride: number | null; outOfStock: boolean; lowStock: boolean }[];
|
||||
variants: { name: string; priceOverride: number | null; outOfStock: boolean; lowStock: boolean; maxQty: number | null }[];
|
||||
};
|
||||
|
||||
type PayloadProduct = {
|
||||
@@ -237,6 +245,13 @@ function isLowStock(trackInventory: boolean, stock: number | null, threshold: nu
|
||||
return trackInventory && threshold != null && stock != null && stock > 0 && stock <= threshold;
|
||||
}
|
||||
|
||||
// null (no cap) whenever backorder is allowed or inventory isn't tracked —
|
||||
// only a hard-tracked, non-backorderable stock count actually limits what a
|
||||
// shopper can add to their cart.
|
||||
function maxPurchasableQty(trackInventory: boolean, stock: number | null, allowBackorder: boolean): number | null {
|
||||
return trackInventory && !allowBackorder ? (stock ?? 0) : null;
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -260,12 +275,14 @@ export function mapPayloadProduct(product: PayloadProduct): Product {
|
||||
typeof product.spotlightImage === "object" && product.spotlightImage ? product.spotlightImage.url : null,
|
||||
outOfStock: isOutOfStock(product.trackInventory, product.stock, product.allowBackorder),
|
||||
lowStock: isLowStock(product.trackInventory, product.stock, product.lowStockThreshold),
|
||||
maxQty: maxPurchasableQty(product.trackInventory, product.stock, product.allowBackorder),
|
||||
taxRatePercent: product.taxRatePercent ?? null,
|
||||
variants: (product.variants ?? []).map((v) => ({
|
||||
name: v.name,
|
||||
priceOverride: v.priceOverride,
|
||||
outOfStock: isOutOfStock(v.trackInventory, v.stock, v.allowBackorder),
|
||||
lowStock: isLowStock(v.trackInventory, v.stock, v.lowStockThreshold),
|
||||
maxQty: maxPurchasableQty(v.trackInventory, v.stock, v.allowBackorder),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user