← back to Dw Checkout Guard
README.md
150 lines
# 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
```
### ⚠️ MANDATORY post-deploy step — ACTIVATE the validation
`shopify app deploy` uploads the function but **does NOT enforce it** until you
turn it on. A deployed-but-inactive validation lets every $0 line straight
through — it looks done and isn't. So this is step 1, not a footnote:
**Admin → Settings → Checkout → Validations (a.k.a. "Manage checkout rules")**
→ add **DW Checkout Guard** → **toggle it ON**.
Then confirm it's actually enforcing with the smoke test in "Verify after deploy"
below — do not consider this shipped until a $0 line is provably blocked at the
Checkout step in a real browser.
### ⚠️ MANDATORY pre-deploy step — confirm the cost field name
The one field this function depends on is the per-unit line price. Before you
deploy, confirm the Function input schema actually exposes it (schemas differ
from the Storefront API and can shift between `api_version`s):
```sh
shopify app function typegen
# open the generated types for CartLineCost and confirm `amountPerQuantity`
# exists. If the field is named differently in your api_version, update
# src/cart_validations_generate_run.graphql to match.
```
If the field name is wrong, `shopify app function build` FAILS LOUDLY with an
"unknown field" error before anything reaches the store — so a bad field can't
silently ship. And even at runtime the JS **fails open** (an unreadable price is
skipped, never treated as $0), so the worst case is "the $0 block doesn't fire,"
never "all checkout blocked."
## 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, verify all three cases:
1. **$0 line alone** → click Checkout → blocked with the guard message. ✅
2. **Mixed cart** ($0 line + a $4.25 sample) → blocked while the $0 line is
present; **remove the $0 line** → the sample alone checks out fine. ✅
(This proves the guard doesn't collateral-block the sample flow.)
3. **A normal-priced product alone** → checks out fine (no false positive). ✅
**Scope caveat:** Cart & Checkout Validation functions run on the standard
browser checkout AND draft orders — which covers this exposure (the `/cart/add.js`
vector). They do **not** necessarily run on a fully headless/custom Storefront-API
checkout or a Subscriptions-app flow. DW doesn't use those today; revisit this
guard if that ever changes.
## 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.