[object Object]

← back to Shopify Sample Shipping

TK-11333: authored sample-shipping executors (trade-grant-check, segment, grandfather, codes, prototype, theme engine, remove/restore band) + paste-ready runbook

5cb23415c5938d1eb5350e4dc6e88504283a07f4 · 2026-09-09 16:35:44 -0700 · Steve Abrams

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019GGVzyUM2B8rVz9TxouGgY

Files touched

Diff

commit 5cb23415c5938d1eb5350e4dc6e88504283a07f4
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Sep 9 16:35:44 2026 -0700

    TK-11333: authored sample-shipping executors (trade-grant-check, segment, grandfather, codes, prototype, theme engine, remove/restore band) + paste-ready runbook
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_019GGVzyUM2B8rVz9TxouGgY
---
 RUNBOOK-TK-11333.md                      | 101 +++++++++++++++++++
 create-freeship-codes.mjs                |  10 +-
 remove-band.mjs                          |  58 +++++++++++
 restore-band.mjs                         |  72 ++++++++++++--
 theme/sample-shipping-cart-engine.liquid | 165 +++++++++++++++++++++++++++++++
 verification/proto-autoapply-report.json |  13 +++
 6 files changed, 407 insertions(+), 12 deletions(-)

diff --git a/RUNBOOK-TK-11333.md b/RUNBOOK-TK-11333.md
new file mode 100644
index 0000000..c663334
--- /dev/null
+++ b/RUNBOOK-TK-11333.md
@@ -0,0 +1,101 @@
+# RUNBOOK — TK-11333 Designer Sample-Shipping Policy (Option A)
+
+**Owner:** vp-dw-commerce · **Store:** designer-laboratory-sandbox (LIVE) · **Approved:** Steve 2026-09-09 (Option A, soft-launch+monitor, 5+5 = 5-of-each-type)
+
+Every step below is a paste Steve runs in his OWN session. Each executor is **dry-run by default** — it prints what it WOULD do; add `--apply` to write. Every write step has its undo listed right under it. **This authoring session did NOT run any `--apply`.**
+
+> **Grandfather vehicle = `sample-freeship` (dedicated tag), NOT `trade`.** Verified by `trade-grant-check.mjs`: `trade` on this store also gates PRICING (TRADECODE/TRADE15 15%-off codes scoped to the `trade` segment + `trade-only-benefits` page + `trade_approved` theme entitlement). Mass-tagging 2,663 grandfathered customers `trade` would leak those perks. The `DW Trade / Designers` segment ORs the real trade tags **plus** `sample-freeship`, so grandfathered designers get free sample shipping and nothing else.
+
+> **Live-state note (verified 2026-09-09):** the blunt `$0–$45` "Free Shipping (No Tracking)" band is **already gone** from every delivery profile — the General profile's Domestic zone is `ups_shipping` + `fedex` only. GAP2 is already closed; carrier-only base shipping is already live. So **Step 6 (remove-band) is a verified no-op today**, and the STOP GATE below flips to "if the prototype DEGRADES, RESTORE a fallback free band so eligible customers aren't charged while auto-apply is sorted."
+
+---
+
+## Preconditions (once)
+```
+! cd ~/Projects/shopify-sample-shipping && node trade-grant-check.mjs        # re-confirm vehicle (read-only)
+```
+Pick a real **Sample variant id** to use in the prototype (any product's `{DW_SKU}-Sample`), e.g. from `verification/all-sample-variants.json`.
+
+---
+
+## STEP 1 — Grandfather TIER-1 designers (2,663) FIRST  [protective, additive, reversible]
+Pilot 20 first, eyeball, then the full run. Resumable + idempotent; ledgers to `executed-reversible/ledger.jsonl`.
+```
+! cd ~/Projects/shopify-sample-shipping && node grandfather-apply.mjs --limit 20          # DRY pilot
+! cd ~/Projects/shopify-sample-shipping && node grandfather-apply.mjs --apply --limit 20  # WRITE pilot (20)
+! cd ~/Projects/shopify-sample-shipping && node grandfather-apply.mjs --apply             # WRITE all pending (2,663)
+```
+**Undo:** `! cd ~/Projects/shopify-sample-shipping && node grandfather-undo.mjs --apply` (removes `sample-freeship` from exactly the tagged set; keeps any preexisting).
+
+---
+
+## STEP 2 — Create the segment + delete the broken one  [reversible]
+Creates `DW Trade / Designers` (core tag set + `sample-freeship`); deletes broken `interior-designer-res` (snapshotted). Add `--with-confirm` to include the §4 "confirm-these" tags (Photography Studio, Graphic Designer, Illustrator, Visual Merchandiser, Production Company, Manufacturer, Developer) — default is CORE only.
+```
+! cd ~/Projects/shopify-sample-shipping && node create-trade-segment.mjs           # DRY
+! cd ~/Projects/shopify-sample-shipping && node create-trade-segment.mjs --apply   # WRITE
+```
+**Undo:** `! cd ~/Projects/shopify-sample-shipping && node create-trade-segment-undo.mjs --apply` (delete codes first if they exist).
+
+---
+
+## STEP 3 — Create the two free-shipping codes  [reversible]
+`TRADESHIP` (segment-scoped, server-enforced) + `SAMPLESHIP` (all customers). Both `maximumShippingPrice $30`.
+```
+! cd ~/Projects/shopify-sample-shipping && node create-freeship-codes.mjs           # DRY (prints exact mutations)
+! cd ~/Projects/shopify-sample-shipping && node create-freeship-codes.mjs --apply   # WRITE (segment must exist)
+```
+**Undo:** `! cd ~/Projects/shopify-sample-shipping && node create-freeship-codes-undo.mjs --apply`
+
+---
+
+## STEP 4 — PROTOTYPE the auto-apply  ⛔ STOP GATE ⛔
+This is the go/no-go (DTD dissent hole #1). In the **openclaw real Chrome (CDP 127.0.0.1:18800)**, log in as a throwaway **eligible (in-segment)** customer with a saved US address, then:
+```
+! cd ~/Projects/shopify-sample-shipping && node proto-autoapply.mjs --code TRADESHIP --variant <SAMPLE_VARIANT_ID> --checkout
+```
+Reads `verification/proto-autoapply-report.json`. **VERDICT:**
+- **PASS** (free shipping shows at checkout AND survives a reload) → proceed to Step 5.
+- **DEGRADE / UNKNOWN** → **DO NOT proceed to a no-band state.** Restore a fallback free band so eligible customers aren't charged while auto-apply is fixed:
+  ```
+  ! cd ~/Projects/shopify-sample-shipping && node restore-band.mjs --apply   # recreate the $0 free band as fallback
+  ```
+  Then rethink auto-apply (permalink UX) before continuing. **Do not run Step 5/6 until PASS.**
+
+*(No real code yet? Steve can make a throwaway free-ship code in Admin and pass it as `--code` instead of `TRADESHIP`.)*
+
+---
+
+## STEP 5 — Theme cart policy engine  [dev theme; Steve installs + publishes]
+File: `theme/sample-shipping-cart-engine.liquid` (starter + spec header). Counts samples by `product.type` (WC vs Fabric), reads `customer.tags`, auto-applies the right code when thresholds pass (designer ≤12; retail ≤5 WC AND ≤5 fabric), shows the "why charged" message, soft-caps at 20.
+1. Install as `snippets/sample-shipping-cart-engine.liquid` on a **DEV/duplicate theme**.
+2. Render near the cart checkout button: `{%- render 'sample-shipping-cart-engine' -%}`.
+3. Tune `CONFIG` (codes, caps, fabric product-types, checkout selector) to the live theme.
+4. QA carts (designer ≤12/13+, retail 5+5/6WC, 21+ soft-cap), then **Steve publishes the theme.**
+**Undo:** theme is version-backed (`verification/cart.liquid.backup`); remove the snippet render + file.
+
+---
+
+## STEP 6 — Remove the blunt $0 band  [LAST — verified no-op today]
+```
+! cd ~/Projects/shopify-sample-shipping && node remove-band.mjs           # DRY (reports "already absent")
+! cd ~/Projects/shopify-sample-shipping && node remove-band.mjs --apply   # deactivate IF a band ever gets re-added
+```
+Today this prints "BAND ALREADY ABSENT" and exits — nothing to do. Only relevant if a free band gets re-added later.
+**Undo:** `! cd ~/Projects/shopify-sample-shipping && node restore-band.mjs --apply` (reactivates, or recreates from snapshot).
+
+---
+
+## STEP 7 — Soft-launch monitor (2 weeks)
+Watch retail-charged sample orders; auto-tag designer-looking ones `sample-freeship` (rerun `grandfather-apply` on new finds); add the self-serve "I'm trade" prompt at cart. TIER-2 (765) list in `verification/grandfather-list.json` is for Steve's manual review (not auto-tagged).
+
+---
+
+## Full rollback (reverse order)
+```
+! node create-freeship-codes-undo.mjs --apply
+! node create-trade-segment-undo.mjs --apply
+! node grandfather-undo.mjs --apply
+! node restore-band.mjs --apply     # only if you had removed/deactivated a band
+```
+Each is enumerable, recorded, and non-destructive. Ledger: `~/.claude/yolo-queue/executed-reversible/ledger.jsonl`.
diff --git a/create-freeship-codes.mjs b/create-freeship-codes.mjs
index da7247c..cee7a88 100644
--- a/create-freeship-codes.mjs
+++ b/create-freeship-codes.mjs
@@ -22,9 +22,13 @@ const SEG_REC = new URL('./verification/trade-segment-created.json', import.meta
 const OUT = new URL('./verification/freeship-codes-created.json', import.meta.url);
 const LOGX = process.env.HOME + '/.claude/yolo-queue/executed-reversible/log-exec.mjs';
 
-if (!fs.existsSync(SEG_REC)) { console.error('MISSING verification/trade-segment-created.json — run create-trade-segment.mjs --apply FIRST'); process.exit(1); }
-const segId = JSON.parse(fs.readFileSync(SEG_REC, 'utf8')).segmentId;
-if (!segId) { console.error('no segmentId in trade-segment-created.json'); process.exit(1); }
+let segId;
+if (fs.existsSync(SEG_REC)) segId = JSON.parse(fs.readFileSync(SEG_REC, 'utf8')).segmentId;
+if (!segId) {
+  if (APPLY) { console.error('MISSING segmentId — run create-trade-segment.mjs --apply FIRST'); process.exit(1); }
+  segId = 'gid://shopify/Segment/PENDING';  // dry-run placeholder so the mutation shape prints
+  console.log('(dry-run: no segment created yet — using placeholder segId; create the segment before --apply)');
+}
 
 const now = new Date().toISOString();
 const combines = { orderDiscounts: true, productDiscounts: true, shippingDiscounts: false };
diff --git a/remove-band.mjs b/remove-band.mjs
new file mode 100644
index 0000000..c5d1e1a
--- /dev/null
+++ b/remove-band.mjs
@@ -0,0 +1,58 @@
+#!/usr/bin/env node
+// TK-11333 — remove the blunt "Free Shipping (No Tracking)" $0–$45 band so base shipping =
+// honest ups/fedex carrier rates (fixes GAP2: free untracked shipping leaking to ANY product
+// <= $45). Free SAMPLE shipping now comes from the segment/all-customer CODES instead.
+//
+// MECHANISM: the delivery API has NO methodDefinitionsToDelete, but a method has an `active`
+// flag — so we DEACTIVATE the free method (exact-GID, trivially reversible) rather than delete.
+// LAST STEP — run only AFTER grandfather + proto-autoapply PASS + codes exist.
+//
+// GATED (customer-facing shipping change). DRY-RUN BY DEFAULT; --apply to write.
+// Snapshots to verification/remove-band-snapshot.json; undo = restore-band.mjs (reactivates
+// + restores the exact prior conditions from that snapshot).
+import { query } from './query.mjs';
+import fs from 'node:fs';
+
+const APPLY = process.argv.includes('--apply');
+const PROFILE = 'gid://shopify/DeliveryProfile/29033627699';
+const METHOD_NAME = 'Free Shipping (No Tracking)';
+const SNAP = new URL('./verification/remove-band-snapshot.json', import.meta.url);
+const LOGX = process.env.HOME + '/.claude/yolo-queue/executed-reversible/log-exec.mjs';
+
+const rq = `query($id:ID!){deliveryProfile(id:$id){profileLocationGroups{locationGroup{id} locationGroupZones(first:20){nodes{zone{id name} methodDefinitions(first:40){nodes{id name active rateProvider{__typename ... on DeliveryRateDefinition{price{amount currencyCode}}} methodConditions{id operator conditionCriteria{__typename ... on MoneyV2{amount currencyCode}}}}}}}}}}`;
+const d = await query(rq, { id: PROFILE });
+let m = null, lg = null, zone = null;
+for (const g of d.deliveryProfile.profileLocationGroups) for (const z of g.locationGroupZones.nodes) {
+  if (z.zone.name !== 'Domestic') continue;
+  for (const md of z.methodDefinitions.nodes) if (md.name === METHOD_NAME) { m = md; lg = g.locationGroup.id; zone = z.zone.id; }
+}
+if (!m) {
+  // The $0 free band is already absent from the General profile's Domestic zone (verified
+  // 2026-09-09: Domestic = ups_shipping + fedex only). Removal is already done — idempotent success.
+  console.log('\nBAND ALREADY ABSENT — no "' + METHOD_NAME + '" method in the Domestic zone.');
+  console.log('GAP2 is already closed on the General profile: base domestic shipping = carrier (ups/fedex).');
+  console.log('Nothing to remove. (Free sample shipping must come from the CODES.)');
+  process.exit(0);
+}
+
+const snapshot = { at: new Date().toISOString(), profile: PROFILE, lg, zone, method: m };
+console.log('=== remove-band (' + (APPLY ? 'APPLY' : 'DRY-RUN') + ') ===');
+console.log('method:', m.name, '| active:', m.active, '| price:', m.rateProvider?.price?.amount);
+console.log('conditions:', m.methodConditions.map(c => `${c.operator} $${c.conditionCriteria?.amount}`).join(' & ') || '(none)');
+
+if (!m.active) { console.log('\nAlready INACTIVE — nothing to do (idempotent).'); fs.writeFileSync(SNAP, JSON.stringify(snapshot, null, 2) + '\n'); process.exit(0); }
+if (!APPLY) { console.log('\n-- WOULD set method active:false (deactivate the $0-band). Re-run with --apply.'); fs.writeFileSync(SNAP, JSON.stringify(snapshot, null, 2) + '\n'); console.log('snapshot -> verification/remove-band-snapshot.json'); process.exit(0); }
+
+fs.writeFileSync(SNAP, JSON.stringify(snapshot, null, 2) + '\n');
+const mut = `mutation($id:ID!,$profile:DeliveryProfileInput!){deliveryProfileUpdate(id:$id,profile:$profile){userErrors{field message}}}`;
+const r = (await query(mut, { id: PROFILE, profile: { locationGroupsToUpdate: [{ id: lg, zonesToUpdate: [{ id: zone, methodDefinitionsToUpdate: [{ id: m.id, active: false }] }] }] } })).deliveryProfileUpdate;
+if (r.userErrors?.length) { console.error('ERR', JSON.stringify(r.userErrors)); process.exit(1); }
+console.log('\nband DEACTIVATED. Base shipping is now carrier rates; free sample shipping via the CODES only.');
+console.log('snapshot -> verification/remove-band-snapshot.json  (undo: node restore-band.mjs --apply)');
+try {
+  const { execSync } = await import('node:child_process');
+  execSync(`node ${LOGX} --agent vp-dw-commerce --ticket TK-11333 ` +
+    `--action ${JSON.stringify('deactivated "' + METHOD_NAME + '" $0-band (GAP2 fix)')} --blast 1 ` +
+    `--undo ${JSON.stringify('cd ~/Projects/shopify-sample-shipping && node restore-band.mjs --apply')} ` +
+    `--verify ${JSON.stringify('node live-rate-state.mjs')}`, { stdio: 'inherit' });
+} catch (e) { console.log('(ledger note skipped:', e.message, ')'); }
diff --git a/restore-band.mjs b/restore-band.mjs
index 2e71e6d..bafa252 100644
--- a/restore-band.mjs
+++ b/restore-band.mjs
@@ -1,9 +1,63 @@
-import {query} from './query.mjs';
-const PROFILE='gid://shopify/DeliveryProfile/29033627699';
-const rq=`query($id:ID!){deliveryProfile(id:$id){profileLocationGroups{locationGroup{id} locationGroupZones(first:20){nodes{zone{id name} methodDefinitions(first:30){nodes{id name methodConditions{id}}}}}}}}`;
-const d=await query(rq,{id:PROFILE});
-let m,lg,zone;for(const g of d.deliveryProfile.profileLocationGroups)for(const z of g.locationGroupZones.nodes){if(z.zone.name!=='Domestic')continue;for(const md of z.methodDefinitions.nodes)if(md.name==='Free Shipping (No Tracking)'){m=md;lg=g.locationGroup.id;zone=z.zone.id;}}
-const mut=`mutation($id:ID!,$profile:DeliveryProfileInput!){deliveryProfileUpdate(id:$id,profile:$profile){userErrors{field message}}}`;
-await query(mut,{id:PROFILE,profile:{conditionsToDelete:m.methodConditions.map(c=>c.id)}});
-const add=await query(mut,{id:PROFILE,profile:{locationGroupsToUpdate:[{id:lg,zonesToUpdate:[{id:zone,methodDefinitionsToUpdate:[{id:m.id,priceConditionsToCreate:[{operator:'GREATER_THAN_OR_EQUAL_TO',criteria:{amount:'0',currencyCode:'USD'}},{operator:'LESS_THAN_OR_EQUAL_TO',criteria:{amount:'30',currencyCode:'USD'}}]}]}]}]}});
-console.log('restored to $0-$30:',JSON.stringify(add.deliveryProfileUpdate.userErrors));
+#!/usr/bin/env node
+// TK-11333 — UNDO for remove-band.mjs (and general "put the free band back"). Snapshot-driven
+// TRUE inverse with two paths:
+//   • method PRESENT (was deactivated) -> reactivate + restore exact prior conditions.
+//   • method ABSENT (was deleted, or never present) -> RECREATE it from the snapshot
+//     (DeliveryRateDefinition $0 + the snapshot's price conditions) in the Domestic zone.
+// Snapshot source: verification/remove-band-snapshot.json, else raise-band-snapshot.json.
+// DRY-RUN BY DEFAULT; --apply to write.
+import { query } from './query.mjs';
+import fs from 'node:fs';
+
+const APPLY = process.argv.includes('--apply');
+const PROFILE = 'gid://shopify/DeliveryProfile/29033627699';
+const METHOD_NAME = 'Free Shipping (No Tracking)';
+const CANDIDATES = ['./verification/remove-band-snapshot.json', './verification/raise-band-snapshot.json'];
+let snap = null, snapPath = null;
+for (const p of CANDIDATES) { const u = new URL(p, import.meta.url); if (fs.existsSync(u)) { snap = JSON.parse(fs.readFileSync(u, 'utf8')); snapPath = p; break; } }
+if (!snap) { console.error('no band snapshot found — expected verification/remove-band-snapshot.json'); process.exit(1); }
+const snapConds = (snap.method?.methodConditions || []).filter(c => c.conditionCriteria?.amount !== undefined)
+  .map(c => ({ operator: c.operator, amount: String(parseFloat(c.conditionCriteria.amount)) }));
+const snapPrice = snap.method?.rateProvider?.price?.amount != null ? String(parseFloat(snap.method.rateProvider.price.amount)) : '0';
+
+console.log('=== restore-band (' + (APPLY ? 'APPLY' : 'DRY-RUN') + ') using ' + snapPath + ' ===');
+console.log('band to restore: price $' + snapPrice + '  conditions:', snapConds.map(c => `${c.operator} $${c.amount}`).join(' & ') || '(none)');
+
+// same working query as remove-band (includes rateProvider so brace count is correct)
+const rq = `query($id:ID!){deliveryProfile(id:$id){profileLocationGroups{locationGroup{id} locationGroupZones(first:20){nodes{zone{id name} methodDefinitions(first:40){nodes{id name active rateProvider{__typename ... on DeliveryRateDefinition{price{amount currencyCode}}} methodConditions{id operator conditionCriteria{__typename ... on MoneyV2{amount currencyCode}}}}}}}}}}`;
+const d = await query(rq, { id: PROFILE });
+let m = null, lg = null, zone = null;
+for (const g of d.deliveryProfile.profileLocationGroups) for (const z of g.locationGroupZones.nodes) {
+  if (z.zone.name !== 'Domestic') continue;
+  lg = g.locationGroup.id; zone = z.zone.id;
+  for (const md of z.methodDefinitions.nodes) if (md.name === METHOD_NAME) m = md;
+}
+if (!lg || !zone) { console.error('ABORT: Domestic zone not found'); process.exit(1); }
+console.log(m ? `live method present (active=${m.active})` : 'live method ABSENT — will RECREATE');
+
+if (!APPLY) { console.log('\nDry-run only. Re-run with --apply.'); process.exit(0); }
+const mut = `mutation($id:ID!,$profile:DeliveryProfileInput!){deliveryProfileUpdate(id:$id,profile:$profile){userErrors{field message}}}`;
+
+if (m) {
+  // reactivate
+  const act = (await query(mut, { id: PROFILE, profile: { locationGroupsToUpdate: [{ id: lg, zonesToUpdate: [{ id: zone, methodDefinitionsToUpdate: [{ id: m.id, active: true }] }] }] } })).deliveryProfileUpdate;
+  if (act.userErrors?.length) { console.error('reactivate ERR', JSON.stringify(act.userErrors)); process.exit(1); }
+  const live = m.methodConditions.map(c => `${c.operator}:${parseFloat(c.conditionCriteria?.amount)}`).sort().join('|');
+  const want = snapConds.map(c => `${c.operator}:${parseFloat(c.amount)}`).sort().join('|');
+  if (snapConds.length && live !== want) {
+    if (m.methodConditions.length) await query(mut, { id: PROFILE, profile: { conditionsToDelete: m.methodConditions.map(c => c.id) } });
+    const add = (await query(mut, { id: PROFILE, profile: { locationGroupsToUpdate: [{ id: lg, zonesToUpdate: [{ id: zone, methodDefinitionsToUpdate: [{ id: m.id, priceConditionsToCreate: snapConds.map(c => ({ operator: c.operator, criteria: { amount: c.amount, currencyCode: 'USD' } })) }] }] }] } })).deliveryProfileUpdate;
+    if (add.userErrors?.length) { console.error('conditions ERR', JSON.stringify(add.userErrors)); process.exit(1); }
+  }
+  console.log('reactivated + conditions restored.');
+} else {
+  // recreate the method from scratch
+  const create = (await query(mut, { id: PROFILE, profile: { locationGroupsToUpdate: [{ id: lg, zonesToUpdate: [{ id: zone, methodDefinitionsToCreate: [{
+    name: METHOD_NAME, active: true,
+    rateDefinition: { price: { amount: snapPrice, currencyCode: 'USD' } },
+    priceConditionsToCreate: snapConds.map(c => ({ operator: c.operator, criteria: { amount: c.amount, currencyCode: 'USD' } })),
+  }] }] }] } })).deliveryProfileUpdate;
+  if (create.userErrors?.length) { console.error('recreate ERR', JSON.stringify(create.userErrors)); process.exit(1); }
+  console.log('recreated method "' + METHOD_NAME + '" $' + snapPrice + ' with band ' + snapConds.map(c => `${c.operator} $${c.amount}`).join(' & '));
+}
+console.log('\nband RESTORED.');
diff --git a/theme/sample-shipping-cart-engine.liquid b/theme/sample-shipping-cart-engine.liquid
new file mode 100644
index 0000000..6a3d224
--- /dev/null
+++ b/theme/sample-shipping-cart-engine.liquid
@@ -0,0 +1,165 @@
+{%- comment -%}
+  ============================================================================
+  TK-11333 — SAMPLE-SHIPPING CART POLICY ENGINE  (starter, NEEDS DEV THEME)
+  ============================================================================
+  STATUS: STARTER IMPLEMENTATION. Do NOT paste into the LIVE theme. Install on a
+  DEV/duplicate theme, QA with real carts, then Steve publishes. Go-live is gated
+  on proto-autoapply.mjs returning PASS (does the /discount/CODE permalink make free
+  shipping persist to checkout for an eligible logged-in customer?).
+
+  WHY THIS EXISTS (platform reality): the store is Advanced, NO Shopify Functions,
+  so a shipping RATE cannot be gated by customer tag nor count samples by type. The
+  ONLY place counts/types/tag-logic can live is theme JS on the CART/product pages.
+  The SERVER-ENFORCED gate is the segment-scoped free-ship CODE (TRADESHIP); this
+  engine only AUTO-APPLIES the correct code when thresholds pass and MESSAGES when not.
+
+  SPEC (locked, memo §1/§3):
+    • Eligibility: DESIGNER if customer.tags includes any trade tag OR 'sample-freeship';
+      otherwise RETAIL.
+    • Count SAMPLE line items only, split by product.type: Wallcovering vs Fabric.
+      (A line is a "sample" if variant.title == 'Sample' or sku ends in '-Sample'.)
+    • DESIGNER free rule: total samples <= 12  -> apply TRADESHIP (free ship).
+      13+ -> do NOT apply; show the "why you're charged" message.
+    • RETAIL free rule: WC samples <= 5 AND Fabric samples <= 5 -> apply SAMPLESHIP.
+      Over EITHER type -> show the "why you're charged" message (>5 of that type).
+    • SOFT CAP 20 samples/order: at 21+ disable the checkout button + message
+      (theme-soft; a hard cap needs Plus/Functions).
+    • Removal note: on Advanced there is no clean in-cart "remove code" API. This engine
+      APPLIES when eligible and MESSAGES when not; a stale code is dropped at checkout
+      (maximumShippingPrice '30' on both codes stops a freight roll ever riding free).
+
+  INSTALL:
+    1. Save this file as snippets/sample-shipping-cart-engine.liquid on a DEV theme.
+    2. In the cart template/section, render it near the checkout button:
+         {%- render 'sample-shipping-cart-engine' -%}
+    3. Confirm the CODES exist (create-freeship-codes.mjs) before enabling apply.
+    4. Adjust CONFIG below (codes, caps, product-type labels, selectors) to the theme.
+  ============================================================================
+{%- endcomment -%}
+
+<div id="dw-ship-policy" data-dw-ship-policy hidden></div>
+<script type="application/json" id="dw-ship-cart-data">
+{
+  "isLoggedIn": {{ customer | default: false | json }},
+  "tags": {{ customer.tags | default: '' | json }},
+  "lines": [
+    {%- for item in cart.items -%}
+    {
+      "product_type": {{ item.product.type | default: '' | json }},
+      "variant_title": {{ item.variant.title | default: '' | json }},
+      "sku": {{ item.sku | default: '' | json }},
+      "quantity": {{ item.quantity }}
+    }{%- unless forloop.last -%},{%- endunless -%}
+    {%- endfor -%}
+  ]
+}
+</script>
+
+<script>
+(function () {
+  // ---------------- CONFIG (tune to the store) ----------------
+  var CONFIG = {
+    designerCode: 'TRADESHIP',
+    retailCode:   'SAMPLESHIP',
+    designerCap:  12,   // total samples free for designers
+    retailWC:     5,    // free wallcovering samples for retail
+    retailFabric: 5,    // free fabric samples for retail
+    softCap:      20,   // block checkout above this many samples
+    // customer tags that mean "gets designer free shipping" (mirror the DW Trade/Designers segment)
+    designerTags: ['trade','trade_approved','sample-freeship','interior designer',
+      'interior design','interior','architect','contractor','commercial property owner',
+      'wallcovering installer','interior designer - residential','interior designer - commercial'],
+    // product.type values that count as Fabric; everything else with a sample counts as Wallcovering
+    fabricTypes: ['fabric','fabrics','textile','textiles'],
+    checkoutBtnSelector: '[name="checkout"], button[name="checkout"], .cart__checkout, #checkout',
+    messageMountSelector: '#dw-ship-policy'
+  };
+  var APPLIED_KEY = 'dw_ship_code_applied'; // sessionStorage guard against redirect loops
+
+  function readData() {
+    try { return JSON.parse(document.getElementById('dw-ship-cart-data').textContent); }
+    catch (e) { return { isLoggedIn: false, tags: '', lines: [] }; }
+  }
+  function isSample(l) {
+    return (l.variant_title || '').toLowerCase() === 'sample' || /-sample$/i.test(l.sku || '');
+  }
+  function isFabric(l) {
+    var t = (l.product_type || '').toLowerCase();
+    return CONFIG.fabricTypes.indexOf(t) !== -1;
+  }
+  function isDesigner(tags) {
+    var set = String(tags || '').toLowerCase().split(',').map(function (s) { return s.trim(); });
+    return CONFIG.designerTags.some(function (t) { return set.indexOf(t) !== -1; });
+  }
+
+  function evaluate(d) {
+    var samples = d.lines.filter(isSample);
+    var total = samples.reduce(function (n, l) { return n + l.quantity; }, 0);
+    var wc = samples.filter(function (l) { return !isFabric(l); }).reduce(function (n, l) { return n + l.quantity; }, 0);
+    var fab = samples.filter(isFabric).reduce(function (n, l) { return n + l.quantity; }, 0);
+    var designer = d.isLoggedIn && isDesigner(d.tags);
+    var overSoftCap = total > CONFIG.softCap;
+    var eligibleFree, code, reason;
+    if (designer) {
+      eligibleFree = total <= CONFIG.designerCap;
+      code = CONFIG.designerCode;
+      reason = eligibleFree ? '' : ('Free sample shipping covers up to ' + CONFIG.designerCap +
+        ' samples for trade accounts. You have ' + total + ' — shipping on the rest is charged at cost.');
+    } else {
+      eligibleFree = wc <= CONFIG.retailWC && fab <= CONFIG.retailFabric;
+      code = CONFIG.retailCode;
+      if (!eligibleFree) {
+        var bits = [];
+        if (wc > CONFIG.retailWC) bits.push(wc + ' wallcovering (free up to ' + CONFIG.retailWC + ')');
+        if (fab > CONFIG.retailFabric) bits.push(fab + ' fabric (free up to ' + CONFIG.retailFabric + ')');
+        reason = 'Free sample shipping covers ' + CONFIG.retailWC + ' wallcovering and ' + CONFIG.retailFabric +
+          ' fabric samples. You have ' + bits.join(' and ') + ' — shipping on the extras is charged.';
+      } else reason = '';
+    }
+    return { total: total, wc: wc, fab: fab, designer: designer, eligibleFree: eligibleFree, code: code, reason: reason, overSoftCap: overSoftCap };
+  }
+
+  function applyCode(code) {
+    // /discount/CODE?redirect=/cart sets the session discount cookie. Guard against loops:
+    // only redirect once per cart state (keyed on code+item signature).
+    var sig = code + ':' + (document.getElementById('dw-ship-cart-data').textContent.length);
+    if (sessionStorage.getItem(APPLIED_KEY) === sig) return;
+    sessionStorage.setItem(APPLIED_KEY, sig);
+    window.location.assign('/discount/' + encodeURIComponent(code) + '?redirect=/cart');
+  }
+
+  function render(state) {
+    var mount = document.querySelector(CONFIG.messageMountSelector);
+    if (mount) {
+      mount.hidden = false;
+      if (state.overSoftCap) {
+        mount.innerHTML = '<div class="dw-ship-msg dw-ship-msg--block" role="alert" style="padding:.75rem 1rem;border:1px solid #b00;border-radius:8px;margin:.5rem 0;color:#b00;">' +
+          'Sample orders are limited to ' + CONFIG.softCap + ' per order. Please reduce to ' + CONFIG.softCap + ' or fewer to check out.</div>';
+      } else if (state.reason) {
+        mount.innerHTML = '<div class="dw-ship-msg" role="status" style="padding:.75rem 1rem;border:1px solid #d8c9a8;border-radius:8px;margin:.5rem 0;background:#faf6ee;">' +
+          state.reason + '</div>';
+      } else if (state.eligibleFree) {
+        mount.innerHTML = '<div class="dw-ship-msg dw-ship-msg--ok" role="status" style="padding:.5rem 1rem;color:#2e6b2e;">Free sample shipping applied.</div>';
+      } else { mount.innerHTML = ''; }
+    }
+    // soft cap: disable checkout
+    var btns = document.querySelectorAll(CONFIG.checkoutBtnSelector);
+    btns.forEach(function (b) {
+      if (state.overSoftCap) { b.setAttribute('disabled', 'disabled'); b.setAttribute('aria-disabled', 'true'); b.style.opacity = '0.5'; b.style.pointerEvents = 'none'; }
+      else { b.removeAttribute('disabled'); b.removeAttribute('aria-disabled'); b.style.opacity = ''; b.style.pointerEvents = ''; }
+    });
+  }
+
+  function run() {
+    var d = readData();
+    var state = evaluate(d);
+    render(state);
+    // auto-apply the correct code only when the cart is all-eligible-free and NOT over soft cap
+    if (state.total > 0 && state.eligibleFree && !state.overSoftCap) applyCode(state.code);
+    // (removal is checkout-side on Advanced; maximumShippingPrice '30' guards a stale code)
+  }
+
+  if (document.readyState !== 'loading') run();
+  else document.addEventListener('DOMContentLoaded', run);
+})();
+</script>
diff --git a/verification/proto-autoapply-report.json b/verification/proto-autoapply-report.json
new file mode 100644
index 0000000..ea5749f
--- /dev/null
+++ b/verification/proto-autoapply-report.json
@@ -0,0 +1,13 @@
+{
+  "at": "2026-09-09T23:31:53.343Z",
+  "code": "TESTONLY",
+  "domain": "designerwallcoverings.com",
+  "variant": null,
+  "steps": {
+    "loggedIn": true
+  },
+  "verdict": "UNKNOWN",
+  "notes": [
+    "harness error: page.evaluate: BreadcrumbsPluginFetchError: Failed to fetch\n    at https://cdn.shopify.com/shopifycloud/customer-authentication-web/production/assets/index.C3c9O3k-.js:33:8758"
+  ]
+}

← 3cd0d11 TK-11333: hard-delete the 3 test orders (final cleanup)  ·  back to Shopify Sample Shipping  ·  auto-data-snapshot: 2026-09-09T16:44:54 (2 data files) — ver eefb2df →