← back to Dw Checkout Guard
scaffold DW Checkout Guard validation function (rejects $0 or discontinued line items)
1ba0a3e537f9487bca8ec76cb556d9357e6d6c1a · 2026-07-30 08:42:26 -0700 · steve-office
Files touched
A .gitignoreA README.mdA extensions/dw-checkout-guard/shopify.extension.tomlA extensions/dw-checkout-guard/src/cart_validations_generate_run.graphqlA extensions/dw-checkout-guard/src/cart_validations_generate_run.jsA package.jsonA shopify.app.toml
Diff
commit 1ba0a3e537f9487bca8ec76cb556d9357e6d6c1a
Author: steve-office <steve@designerwallcoverings.com>
Date: Thu Jul 30 08:42:26 2026 -0700
scaffold DW Checkout Guard validation function (rejects $0 or discontinued line items)
---
.gitignore | 9 ++
README.md | 113 +++++++++++++++++++++
.../dw-checkout-guard/shopify.extension.toml | 26 +++++
.../src/cart_validations_generate_run.graphql | 29 ++++++
.../src/cart_validations_generate_run.js | 77 ++++++++++++++
package.json | 21 ++++
shopify.app.toml | 21 ++++
7 files changed, 296 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..78113b1
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+node_modules/
+.env*
+tmp/
+*.log
+.DS_Store
+dist/
+build/
+.next/
+.shopify/
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..5fd4feb
--- /dev/null
+++ b/README.md
@@ -0,0 +1,113 @@
+# DW Checkout Guard
+
+A Shopify **Cart & Checkout Validation Function** for the LIVE DW store
+(`designer-laboratory-sandbox.myshopify.com`). It blocks the "Checkout" button
+whenever a cart contains a line item that is either:
+
+1. **$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 (`Discontinued`,
+ `Discontinued-Review`, `YB-Discontinued-2026-04`). Belt-and-suspenders: the 42
+ known sellable-discontinued items were already drafted 2026-07-30.
+
+## Why a Validation Function (and not per-variant `available:false`)
+
+Canary-proven 2026-07-30: this store does **not** gate `/cart/add.js` on
+inventory. Setting the $0 variant to `available:false` (deny + qty 0) still lets
+the $0 line be added (tested at qty 1 and qty 5). Only a **checkout-layer
+validation** reliably blocks the $0 *order* — and, unlike drafting, it does so
+**without delisting the product**, so the intended `$4.25` sample flow keeps
+working. (Discontinued items get drafted instead, since they should be fully off
+the store.) See memory `dw-shopify-addjs-inventory-not-gated`.
+
+**Cost:** $0 — Shopify Functions have no per-invocation charge; the custom app is
+free.
+
+---
+
+## The two files that matter
+
+Everything else here is boilerplate. The business logic lives in exactly two
+files, and they are stable across CLI versions:
+
+- `extensions/dw-checkout-guard/src/cart_validations_generate_run.graphql` — the
+ input query (reads each line's per-unit `cost` + the product's discontinued tag).
+- `extensions/dw-checkout-guard/src/cart_validations_generate_run.js` — the rule
+ (`amount <= 0 || isDiscontinued` → add a blocking error scoped to that line).
+
+---
+
+## Deploy — RECOMMENDED path (version-proof)
+
+Because the exact `api_version` / target string can drift between `shopify` CLI
+versions, the safest path is to let the CLI scaffold the boilerplate, then paste
+in the two logic files above:
+
+```sh
+cd ~/Projects/dw-checkout-guard
+
+# 1. Log in to the DW Partner org (opens a browser)
+shopify auth login
+
+# 2. Link (or create) the app under the Partner org
+shopify app config link # fills client_id in shopify.app.toml
+
+# 3. Generate a Cart & Checkout Validation function extension
+shopify app generate extension
+# → pick: Function → "Cart and checkout validation"
+# → language: JavaScript
+# → name it e.g. dw-checkout-guard
+
+# 4. Overwrite the generated src/ logic with the two files from THIS repo:
+# extensions/dw-checkout-guard/src/cart_validations_generate_run.graphql
+# extensions/dw-checkout-guard/src/cart_validations_generate_run.js
+# (match the generated filenames/export name if the CLI used different ones —
+# the rule inside the run() function is what you're copying.)
+
+# 5. Build + deploy
+shopify app function typegen # optional, regenerates types
+shopify app deploy # pushes the function version live
+```
+
+After `deploy`, activate it in Admin:
+**Settings → Checkout → (Validations / "Manage checkout rules")** → add
+**DW Checkout Guard** → turn it on. (Some stores auto-enable a single validation
+on deploy; confirm it's toggled on.)
+
+## Deploy — as-is path
+
+If your `shopify` CLI matches `api_version = "2025-01"` and the
+`cart.validations.generate.run` target, this repo is already complete — just:
+
+```sh
+cd ~/Projects/dw-checkout-guard
+shopify auth login
+shopify app config link
+shopify app deploy
+```
+
+---
+
+## Verify after deploy (no real order needed)
+
+The exposure reproduces only through the cart endpoint, so test there:
+
+```sh
+# $0 quote-only variant should now be BLOCKED at checkout.
+# Add it, then try to advance to checkout — the guard message should appear.
+curl -s -X POST https://www.designerwallcoverings.com/cart/add.js \
+ -H 'Content-Type: application/json' \
+ --data '{"id":44494274199603,"quantity":1}' # $0 "Standard" variant
+# (add.js still succeeds — that's expected; the block is at the CHECKOUT step)
+```
+
+Then, in a browser cart with that line, click **Checkout** — you should see the
+guard message and be unable to proceed. A normal-priced line + the `$4.25`
+sample must still check out fine (regression check).
+
+## Rollback
+
+Turn the validation off in **Settings → Checkout**, or `shopify app deploy` a
+version with the function removed. No catalog data is touched by this function.
diff --git a/extensions/dw-checkout-guard/shopify.extension.toml b/extensions/dw-checkout-guard/shopify.extension.toml
new file mode 100644
index 0000000..f31e1c1
--- /dev/null
+++ b/extensions/dw-checkout-guard/shopify.extension.toml
@@ -0,0 +1,26 @@
+# DW Checkout Guard — Cart & Checkout Validation Function extension
+# API version pinned to a recent stable release. If your `shopify` CLI scaffolds
+# a different api_version / target string, prefer the CLI's values (see README)
+# and only paste in src/cart_validations_generate_run.{js,graphql}.
+api_version = "2025-01"
+
+[[extensions]]
+name = "DW Checkout Guard"
+handle = "dw-checkout-guard"
+type = "function"
+
+ [[extensions.targeting]]
+ # Cart & Checkout Validation target (2025-01). The run() export name must
+ # match the target: cart.validations.generate.run -> cartValidationsGenerateRun
+ target = "cart.validations.generate.run"
+ input_query = "src/cart_validations_generate_run.graphql"
+ export = "cartValidationsGenerateRun"
+
+ [extensions.build]
+ # Built by `shopify app function build` (javy) -> dist/function.wasm
+ path = "dist/function.wasm"
+ watch = ["src/**/*.js", "src/**/*.graphql"]
+
+ [extensions.ui.paths]
+ create = "/"
+ details = "/"
diff --git a/extensions/dw-checkout-guard/src/cart_validations_generate_run.graphql b/extensions/dw-checkout-guard/src/cart_validations_generate_run.graphql
new file mode 100644
index 0000000..124e882
--- /dev/null
+++ b/extensions/dw-checkout-guard/src/cart_validations_generate_run.graphql
@@ -0,0 +1,29 @@
+query CartValidationsGenerateRun {
+ cart {
+ lines {
+ quantity
+ # per-unit price of the line; $0.00 is the exposure we block
+ cost {
+ amountPerQuantity {
+ amount
+ }
+ }
+ merchandise {
+ __typename
+ ... on ProductVariant {
+ id
+ title
+ product {
+ id
+ title
+ # true if the product carries any discontinued tag (belt-and-suspenders;
+ # the 42 known sellable-discontinued were already drafted 2026-07-30)
+ isDiscontinued: hasAnyTag(
+ tags: ["Discontinued", "Discontinued-Review", "YB-Discontinued-2026-04"]
+ )
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/extensions/dw-checkout-guard/src/cart_validations_generate_run.js b/extensions/dw-checkout-guard/src/cart_validations_generate_run.js
new file mode 100644
index 0000000..c33bf0b
--- /dev/null
+++ b/extensions/dw-checkout-guard/src/cart_validations_generate_run.js
@@ -0,0 +1,77 @@
+// @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
+// to 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.
+//
+// This function runs at cart + checkout. When it returns any error, Shopify
+// blocks the "Checkout"/"Continue to checkout" button until the offending line
+// is removed. It 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_PRICE_MESSAGE =
+ "This item can't be purchased online — please request a quote or remove it to continue to checkout.";
+const DISCONTINUED_MESSAGE =
+ "This item has been discontinued and can no longer be ordered — please remove it to continue to checkout.";
+
+/**
+ * @param {RunInput} input
+ * @returns {{ operations: Array<{ validationAdd: { errors: Array<{ message: string, target: string }> } }> }}
+ */
+export function cartValidationsGenerateRun(input) {
+ /** @type {Array<{ message: string, target: string }>} */
+ const errors = [];
+
+ const lines = input?.cart?.lines ?? [];
+ lines.forEach((line, index) => {
+ const merchandise = line.merchandise;
+ const isVariant = merchandise && merchandise.__typename === "ProductVariant";
+
+ // (2) discontinued — only meaningful for a real product variant
+ const isDiscontinued = Boolean(isVariant && merchandise.product?.isDiscontinued);
+
+ // (1) $0.00 line — parse the per-unit cost; treat missing/NaN as 0 (block, don't leak)
+ const rawAmount = line?.cost?.amountPerQuantity?.amount;
+ const amount = rawAmount == null ? 0 : Number.parseFloat(rawAmount);
+ const isZeroPriced = !Number.isFinite(amount) || amount <= 0;
+
+ if (isDiscontinued || isZeroPriced) {
+ errors.push({
+ message: isDiscontinued ? DISCONTINUED_MESSAGE : ZERO_PRICE_MESSAGE,
+ // JSON-path target: scope the error to the specific offending line
+ target: `$.cart.lines[${index}]`,
+ });
+ }
+ });
+
+ return {
+ operations: errors.length > 0 ? [{ validationAdd: { errors } }] : [],
+ };
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..b27d48e
--- /dev/null
+++ b/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "dw-checkout-guard",
+ "version": "1.0.0",
+ "private": true,
+ "description": "Shopify Cart & Checkout Validation Function that rejects $0 or discontinued line items on the DW live store.",
+ "license": "UNLICENSED",
+ "type": "module",
+ "scripts": {
+ "dev": "shopify app dev",
+ "build": "shopify app build",
+ "deploy": "shopify app deploy",
+ "function:build": "shopify app function build",
+ "function:typegen": "shopify app function typegen"
+ },
+ "dependencies": {
+ "@shopify/shopify_function": "^1.0.0"
+ },
+ "devDependencies": {
+ "@shopify/cli": "^3.66.0"
+ }
+}
diff --git a/shopify.app.toml b/shopify.app.toml
new file mode 100644
index 0000000..0ed3705
--- /dev/null
+++ b/shopify.app.toml
@@ -0,0 +1,21 @@
+# DW Checkout Guard — Shopify app config
+# ---------------------------------------
+# `client_id` is filled automatically the first time you run
+# `shopify app config link` (or `shopify app deploy`, which will prompt you to
+# create/select an app under the Designer Wallcoverings Partner org).
+# Leave client_id blank until then.
+client_id = ""
+
+name = "DW Checkout Guard"
+application_url = "https://designer-laboratory-sandbox.myshopify.com"
+embedded = true
+
+[access_scopes]
+# A validation Function needs no admin scopes — it reads only the cart at runtime.
+scopes = ""
+
+[webhooks]
+api_version = "2025-01"
+
+[build]
+include_config_on_deploy = true
(oldest)
·
back to Dw Checkout Guard
·
harden checkout-guard per contrarian: $.cart target, fail-op 7b5d844 →