← 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" }] } }],
  };
}