← back to Dw Checkout Guard
extensions/dw-checkout-guard/src/cart_validations_generate_run.js
103 lines
// @ts-check
//
// DW Checkout Guard — Cart & Checkout Validation Function
// -------------------------------------------------------
// Rejects any cart line that is either:
// (1) priced $0.00 — the quote-only "$0 Standard variant" exposure
// (~1,744 Phillipe Romano / Fentucci Naturals products whose sellable
// variant is $0 + available:true and is addable via /cart/add.js even
// though the PDP button is hidden), OR
// (2) discontinued — a product carrying a discontinued tag (future-proofing;
// the 42 known sellable-discontinued items were drafted 2026-07-30).
//
// Why a Validation Function and not per-variant availability:
// The live store does NOT gate /cart/add.js on inventory — setting a variant
// available:false (deny + qty 0) still lets the $0 line be added (proven
// 2026-07-30). Only a checkout-layer validation reliably blocks the $0 order
// without delisting the product (these lines must stay live for the $4.25
// sample flow). See memory: dw-shopify-addjs-inventory-not-gated.
//
// DESIGN NOTES (after /contrarian review, TK-10050):
// * Error target is "$.cart" — the validation function does NOT support
// line-level targets like "$.cart.lines[N]"; those are silently dropped.
// We emit ONE consolidated blocking error on the whole cart.
// * FAIL OPEN on an unreadable price. A missing/NaN cost field is an
// infra/schema fault, NOT evidence of $0 — blocking on it would break
// checkout for EVERY line. So we only block when the price parses to a
// real number <= 0. (Worst case if the cost field is wrong: the $0 block
// silently doesn't fire — a known-gap false negative, never a store-wide
// false positive. The build step + README typegen check catch a bad field.)
// * We read cost.amountPerQuantity (per-UNIT merchandise price), which is
// discount-INDEPENDENT — so a 100%-off code / free-gift promo does NOT read
// as $0 here and is never falsely blocked. (totalAmount would be wrong.)
// * This function is FREE to run (Shopify Functions have no per-invocation cost).
/**
* @typedef {{ amount: string }} Money
* @typedef {{
* quantity: number,
* cost?: { amountPerQuantity?: Money | null } | null,
* merchandise: {
* __typename: string,
* id?: string,
* title?: string | null,
* product?: { id?: string, title?: string | null, isDiscontinued?: boolean } | null,
* }
* }} CartLine
* @typedef {{ cart: { lines: CartLine[] } }} RunInput
*/
const ZERO_ONLY_MESSAGE =
"Your cart contains an item that can't be purchased online — please request a quote or remove it to continue to checkout.";
const DISCONTINUED_ONLY_MESSAGE =
"Your cart contains a discontinued item that can no longer be ordered — please remove it to continue to checkout.";
const MIXED_MESSAGE =
"Your cart contains items that can't be ordered online (discontinued or quote-only) — please remove them to continue to checkout.";
/**
* @param {RunInput} input
* @returns {{ operations: Array<{ validationAdd: { errors: Array<{ message: string, target: string }> } }> }}
*/
export function cartValidationsGenerateRun(input) {
let anyDiscontinued = false;
let anyZeroPriced = false;
const lines = input?.cart?.lines ?? [];
for (const line of lines) {
const merchandise = line.merchandise;
const isVariant = merchandise && merchandise.__typename === "ProductVariant";
// (2) discontinued — only meaningful for a real product variant
if (isVariant && merchandise.product?.isDiscontinued) {
anyDiscontinued = true;
continue; // already offending; no need to also price-check this line
}
// (1) $0.00 line — parse the per-unit cost. FAIL OPEN: only block when the
// price parses to a finite number <= 0; a missing/NaN field is skipped.
const rawAmount = line?.cost?.amountPerQuantity?.amount;
if (rawAmount != null) {
const amount = Number.parseFloat(rawAmount);
if (Number.isFinite(amount) && amount <= 0) {
anyZeroPriced = true;
}
}
}
if (!anyDiscontinued && !anyZeroPriced) {
return { operations: [] };
}
const message =
anyDiscontinued && anyZeroPriced
? MIXED_MESSAGE
: anyDiscontinued
? DISCONTINUED_ONLY_MESSAGE
: ZERO_ONLY_MESSAGE;
// One consolidated blocking error on the supported "$.cart" target.
return {
operations: [{ validationAdd: { errors: [{ message, target: "$.cart" }] } }],
};
}