← back to Shopify Sample Shipping
TK-11333: correct band-truth (band is LIVE on dedicated Samples profile, not gone); commit DTD Option A verdict + honest count-enforcement limit; add correct-profile retune-samples-band.mjs (retire wrong-profile remove/raise-band); rewrite RUNBOOK with dev-theme build+proof+go/no-go
ffc2043f9f39ba52f39e4e815750320e363f0dac · 2026-09-09 22:55:45 -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
M RUNBOOK-TK-11333.mdD all-profiles.mjsA retune-samples-band.mjsA verify-codes.mjs
Diff
commit ffc2043f9f39ba52f39e4e815750320e363f0dac
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Sep 9 22:55:45 2026 -0700
TK-11333: correct band-truth (band is LIVE on dedicated Samples profile, not gone); commit DTD Option A verdict + honest count-enforcement limit; add correct-profile retune-samples-band.mjs (retire wrong-profile remove/raise-band); rewrite RUNBOOK with dev-theme build+proof+go/no-go
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019GGVzyUM2B8rVz9TxouGgY
---
RUNBOOK-TK-11333.md | 129 +++++++++++++++++++++++++++---------------------
all-profiles.mjs | 19 -------
retune-samples-band.mjs | 64 ++++++++++++++++++++++++
verify-codes.mjs | 16 ++++++
4 files changed, 154 insertions(+), 74 deletions(-)
diff --git a/RUNBOOK-TK-11333.md b/RUNBOOK-TK-11333.md
index c663334..64e5fed 100644
--- a/RUNBOOK-TK-11333.md
+++ b/RUNBOOK-TK-11333.md
@@ -2,100 +2,119 @@
**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`.**
+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 ran only READ-ONLY reads — no `--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."
+## ⚠️ BAND-TRUTH CORRECTION (supersedes the prior "band already gone" claim)
----
+The earlier RUNBOOK claim — *"the $0–$45 band is already gone / remove-band is a verified no-op"* — was **WRONG**. It was produced by `remove-band.mjs` / `raise-band.mjs`, which both **hardcode the General profile** (`29033627699`) — and the General profile genuinely has no band. But the free-sample band never lived there.
-## 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`.
+**Live-verified 2026-09-09** (read-only Admin GraphQL + `draftOrderAvailableDeliveryOptions`; full record in `verification/band-truth-2026-09-09.json`):
+
+- The **live free-sample band** is on a **dedicated profile** — `DeliveryProfile/96764067891 "Samples — Free Shipping (No Tracking)"`, method `DeliveryMethodDefinition/730269646899`, Domestic(US) zone `367888826419`. It is a **$0 rate gated by cart `TOTAL_PRICE <= $45`**. The ~500 sample variants sit on this profile.
+- **Rolls/bolts** sit on the **General profile** (`29033627699`) → carrier-only (UPS/FedEx).
+- Confirmed live cart behavior: **3–10 samples ($12.75–$42.50) = FREE**; **11 samples ($46.75) = charged** (~$20 UPS); **15/20 = charged** ~$21–24; **5 samples + 1 roll = charged** (~$38 — the $189 total blows the $45 cap; the band evaluates **whole-cart** subtotal, so any roll switches it off).
+
+**DO NOT run `remove-band.mjs`.** The band is the live free-sample floor; removing/deactivating it strands every customer. `remove-band.mjs` + `raise-band.mjs` target the wrong profile and are **retired** — use `retune-samples-band.mjs` (below) for any band edit.
---
-## 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).
+## DTD VERDICT — band + engine composition = **Option A** (committed)
+
+*Panel note: the external LLM wing was unavailable this run (Codex credit-blocked to Sep 14; Grok/Kimi/exo/muse down; local Ollama unresponsive). The decision was committed on the Claude analysis + a full Cody contrarian red-team — the adversarial node ran.*
+
+**Option A: keep the live band as the always-on free floor; the theme JS engine ONLY (a) messages honestly, (b) auto-applies TRADESHIP for logged-in designers who exceed the band cap (11–12 samples), (c) soft-caps 20.** No band cap change required.
+
+Why not the others: **Option B (remove band, enforce via codes)** violates the hard rail "never remove the band before auto-apply is proven" and would strand everyone if the (unproven) permalink auto-apply fails. **Option C (theme hard-blocks a 6th same-type sample)** is the only way to *truly* enforce per-type 5+5 without Functions, but it's heavier, blocks a customer who'd willingly pay, and contradicts the policy's own "over → pay" wording. Option A degrades gracefully: if auto-apply fails, only the 11–12-sample designer edge loses free (small, non-stranding) — everyone ≤10 stays free via the band. **Keep Option C's per-type hard-block documented as a future toggle** if Steve later wants real enforcement.
+
+### The honest count-enforcement limitation (state plainly to Steve)
+A `TOTAL_PRICE` band **cannot distinguish 6 wallcovering samples ($25.50) from 3 WC + 3 fabric ($25.50)** — identical price. On Advanced Shopify (no Functions) a shipping RATE cannot read count or product type. Therefore **the retail per-type "5 and 5" rule cannot be a shipping charge below the $45 cap** — below the cap the band gives everyone free regardless of type mix. In Option A, "5 and 5" is **guidance only** (surfaced as copy), NOT a charge. A true per-type charge requires **Shopify Plus + Functions** (or Option C's cart-level block). The designer→12 extension IS truly enforced, because TRADESHIP is **server-scoped to the designer segments** — a non-designer cannot redeem it even if the client JS is tampered with.
---
-## 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.
+## Preconditions (once)
```
-! cd ~/Projects/shopify-sample-shipping && node create-trade-segment.mjs # DRY
-! cd ~/Projects/shopify-sample-shipping && node create-trade-segment.mjs --apply # WRITE
+! cd ~/Projects/shopify-sample-shipping && node verify-codes.mjs # confirm TRADESHIP/SAMPLESHIP active + scoped (read-only)
+! cd ~/Projects/shopify-sample-shipping && node band-source.mjs # re-confirm the live band + which profile (read-only)
```
-**Undo:** `! cd ~/Projects/shopify-sample-shipping && node create-trade-segment-undo.mjs --apply` (delete codes first if they exist).
+Pick a real **Sample variant id** for the prototype (any `{DW_SKU}-Sample`), e.g. from `verification/all-sample-variants.json`.
---
-## 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 1–3 — Foundation (ALREADY LIVE, all reversible — done 2026-09-09)
+- 2,663 TIER-1 designers tagged `sample-freeship` (undo: `grandfather-undo.mjs --apply`).
+- Segments `DW Trade / Designers` + `DW Trade / Designers (2)` created; broken `interior-designer-res` deleted (undo: `create-trade-segment-undo.mjs --apply`).
+- Codes **TRADESHIP** (segment-scoped, maxShip $30, ACTIVE) + **SAMPLESHIP** (all-customers, maxShip $30, ACTIVE) created (undo: `create-freeship-codes-undo.mjs --apply`). *Note: `SAMPLESHIP` is all-customers/maxShip $30 — it is deliberately **NOT auto-applied** by the engine, because auto-applying it would give any retail cart free ship up to $30 and defeat the retail cap. It stays a manual/marketing code only.*
---
-## 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:
+## STEP 4 — Theme cart policy engine [DEV theme; Steve installs + publishes]
+
+**File:** `theme/sample-shipping-cart-engine.liquid` — **BUILT + logic-proven this session** (rewritten per Cody's FIX-FIRST red-team). It reads whole-cart subtotal + `customer.tags`, splits samples by `product.type` (Wallcovering vs Fabric — **verified** those are the only two live types; classifier + sample detection confirmed against the live catalog), and:
+- shows a "why charged" message **only when a charge is actually incurred** (subtotal over the $45 band AND no covering code) — it never claims a charge the band won't impose (this fixed the count-based "lie" in the starter);
+- auto-applies **TRADESHIP only when it changes the outcome** — logged-in designer, over the band cap, ≤12 samples (no needless `/discount` redirect for the ≤10 common case; SAMPLESHIP never auto-applied);
+- soft-caps 20: disables the standard checkout button + hides accelerated wallet buttons + shows a reduce message (**honestly soft** — the direct `/checkout` URL / some wallet flows can still slip past; a hard cap needs Plus/Functions).
+
+**Logic proof (headless-safe, deterministic):**
```
-! cd ~/Projects/shopify-sample-shipping && node proto-autoapply.mjs --code TRADESHIP --variant <SAMPLE_VARIANT_ID> --checkout
+! cd ~/Projects/shopify-sample-shipping && node engine-logic-test.mjs # 9/9 policy scenarios PASS
```
-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.**
+Covers: retail 6 WC (must NOT claim charged), retail 5+5 free, retail 11 charged, designer 10 (no redirect), designer 12 (free + applies code), designer 13 charged, sample+roll charged, 21 soft-cap, Default-Title `-Sample` edge.
-*(No real code yet? Steve can make a throwaway free-ship code in Admin and pass it as `--code` instead of `TRADESHIP`.)*
+**Install (Steve, on a DUPLICATE/DEV theme — NOT live):**
+1. Duplicate the live theme in Admin → Online Store → Themes → ⋯ → Duplicate.
+2. Save the file as `snippets/sample-shipping-cart-engine.liquid` on the DEV theme.
+3. Render near the cart checkout button: `{%- render 'sample-shipping-cart-engine' -%}`.
+4. Tune `CONFIG` (checkout/wallet selectors) to the theme; `bandCapCents:4500` matches the live band.
+5. QA carts on the DEV **preview** (see Step 5), then **Steve publishes**.
+**Undo:** theme is version-backed (`verification/cart.liquid.backup`); remove the snippet render + file, or revert the theme version.
---
-## 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 5 — PROVE the auto-apply ⛔ STOP GATE ⛔ (the one unproven mechanic)
+
+The theme LOGIC is proven (Step 4, 9/9). The unproven piece is whether the `/discount/TRADESHIP?redirect=/cart` permalink makes **free shipping persist to CHECKOUT** for a logged-in eligible customer. The headless harness (`proto-autoapply.mjs`) currently returns **UNKNOWN** — Shopify's new customer-accounts JS throws `BreadcrumbsPluginFetchError: Failed to fetch` inside `page.evaluate(fetch(...))`, so the fetch-in-page path is flaky. **Do not trust a headless PASS/FAIL for this; use the manual proof.**
+
+**Dependable manual proof (3 steps, on the DEV-theme preview):**
+1. In a fresh browser, log in as a **throwaway trade customer that is IN the `DW Trade / Designers` segment** (tag them `sample-freeship`), with a **saved US address**. Open the DEV theme **preview** URL.
+2. Add **12 sample products** to cart (subtotal $51, over the band). Let the engine fire (or hit `/discount/TRADESHIP?redirect=/cart`).
+3. Go to checkout → **shipping must read $0** → **reload the checkout** → it must **still read $0**.
+ - **PASS** = $0 at checkout AND survives reload → auto-apply is viable → go-live.
+ - **DEGRADE** = shows $0 but lost on reload, or checkout still charges → do NOT rely on the extension; designers 11–12 fall back to carrier. Keep the band (everyone ≤10 still free) and rethink the permalink UX (e.g. cart-attribute + Shopify Scripts alternative) before publishing the extension.
+
+Server-side entitlement is already proven: `verify-codes.mjs` shows TRADESHIP ACTIVE + scoped to both designer segments — so a non-designer is rejected at checkout regardless of the client JS.
---
-## STEP 6 — Remove the blunt $0 band [LAST — verified no-op today]
+## STEP 6 — Band retune [OPTIONAL — DTD says NO CHANGE]
+
+The band stays at `$45` (already ~10 samples free, covers retail 5+5). **No change is required and none is recommended.** If Steve wants the total to be exactly 10 samples ($42.50):
```
-! 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
+! cd ~/Projects/shopify-sample-shipping && node retune-samples-band.mjs # READ-ONLY (prints live band + recommendation)
+! cd ~/Projects/shopify-sample-shipping && node retune-samples-band.mjs --cap 42.50 # DRY (shows the change)
+! cd ~/Projects/shopify-sample-shipping && node retune-samples-band.mjs --cap 42.50 --apply # GATED WRITE (customer-facing — Steve only)
```
-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).
+Targets the **correct** profile (`96764067891`, method `730269646899`). **Undo:** `node retune-samples-band.mjs --cap 45 --apply` (snapshot in `verification/samples-band-retune-snapshot.json`). **NEVER run the retired `remove-band.mjs` / `raise-band.mjs` (wrong profile).**
---
## 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).
+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. Surface the "5 and 5" as guidance copy on the cart/product page (not a charge). TIER-2 (765) list in `verification/grandfather-list.json` is for Steve's manual review.
---
+## GO / NO-GO
+- **GREEN to publish** the theme engine to LIVE **only after** Step 5 manual proof returns **PASS** on the dev preview. The engine's counting/messaging logic is already proven (9/9); publishing is gated on the auto-apply permalink persisting to checkout.
+- **Band:** leave live and untouched (no-op is correct here — but because the band is LIVE, not because it's absent).
+- Every LIVE change (theme publish, any band edit) stays **Steve-gated**.
+
## 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
+# theme: revert the theme version / remove the snippet (never published this session)
+# band: node retune-samples-band.mjs --cap 45 --apply # only if a retune was applied
```
-Each is enumerable, recorded, and non-destructive. Ledger: `~/.claude/yolo-queue/executed-reversible/ledger.jsonl`.
+Each is enumerable, recorded, non-destructive. Ledger: `~/.claude/yolo-queue/executed-reversible/ledger.jsonl`.
diff --git a/all-profiles.mjs b/all-profiles.mjs
deleted file mode 100644
index 8b822d5..0000000
--- a/all-profiles.mjs
+++ /dev/null
@@ -1,19 +0,0 @@
-import {query} from './query.mjs';
-const q=`{deliveryProfiles(first:20){nodes{id name default productVariantsCount{count}
- profileLocationGroups{locationGroupZones(first:30){nodes{zone{id name}
- methodDefinitions(first:40){nodes{id name active rateProvider{__typename
- ... on DeliveryRateDefinition{price{amount currencyCode}}
- ... on DeliveryParticipant{carrierService{name}}}
- methodConditions{operator field conditionCriteria{__typename ... on MoneyV2{amount currencyCode} ... on Weight{value unit}}}}}}}}}}}`;
-const d=await query(q);
-for(const p of d.deliveryProfiles.nodes){
- console.log(`\n#### PROFILE: ${p.name} ${p.default?'(DEFAULT)':''} — variants:${p.productVariantsCount.count} — ${p.id}`);
- for(const g of p.profileLocationGroups)for(const z of g.locationGroupZones.nodes){
- for(const md of z.methodDefinitions.nodes){
- const rp=md.rateProvider;
- const price=rp?.price?`$${rp.price.amount}`:(rp?.carrierService?`carrier:${rp.carrierService.name}`:rp?.__typename);
- const conds=md.methodConditions.map(c=>{const cr=c.conditionCriteria;const v=cr?.amount!==undefined?`$${cr.amount}`:(cr?.value!==undefined?`${cr.value}${cr.unit}`:'?');return `${c.field} ${c.operator} ${v}`;}).join(' AND ')||'(none)';
- console.log(` [${z.zone.name}] ${md.name} [${md.active?'on':'OFF'}] ${price} {${conds}}`);
- }
- }
-}
diff --git a/retune-samples-band.mjs b/retune-samples-band.mjs
new file mode 100644
index 0000000..87e61c5
--- /dev/null
+++ b/retune-samples-band.mjs
@@ -0,0 +1,64 @@
+#!/usr/bin/env node
+// TK-11333 — CORRECT band-retune executor. Targets the LIVE free-sample band on the
+// DEDICATED "Samples — Free Shipping (No Tracking)" profile (96764067891), method
+// 730269646899, Domestic(US) zone 367888826419 — NOT the General profile.
+//
+// WHY THIS EXISTS: remove-band.mjs and raise-band.mjs both hardcode the GENERAL profile
+// (29033627699), which no longer carries the band. That mis-targeting is what produced the
+// false "band already gone / remove is a no-op" claim. This script reads + (gated) edits the
+// REAL band. Per the DTD Option-A verdict the recommended action is NO CHANGE ($45 already
+// gives ~10 samples free, comfortably covering retail 5+5); --cap is only for an optional
+// tighten (e.g. 42.50 = exactly 10) IF Steve wants it.
+//
+// node retune-samples-band.mjs # READ-ONLY: print the live band + recommend
+// node retune-samples-band.mjs --cap 42.50 --apply # GATED write: set the <= cap to 42.50
+//
+// Undo: verification/samples-band-retune-snapshot.json records the pre-change conditions;
+// re-run with --cap <old-value> --apply to restore.
+import fs from 'node:fs';
+import {query} from './query.mjs';
+
+const PROFILE = 'gid://shopify/DeliveryProfile/96764067891';
+const arg = k => { const i = process.argv.indexOf('--' + k); return i >= 0 ? process.argv[i + 1] : undefined; };
+const NEW_CAP = arg('cap'); // string dollars, e.g. "42.50"
+const APPLY = process.argv.includes('--apply');
+
+const rq = `query($id:ID!){deliveryProfile(id:$id){name profileLocationGroups{locationGroup{id}
+ locationGroupZones(first:20){nodes{zone{id name} methodDefinitions(first:40){nodes{id name active
+ rateProvider{__typename ... on DeliveryRateDefinition{price{amount}}}
+ methodConditions{id operator field conditionCriteria{__typename ... on MoneyV2{amount currencyCode}}}}}}}}}}`;
+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)' && md.rateProvider?.price?.amount === '0.0') { m = md; lg = g.locationGroup.id; zone = z.zone.id; }
+}
+if (!m) { console.error('Could not find the live $0 band on the Samples profile — aborting.'); process.exit(1); }
+
+const conds = m.methodConditions.filter(c => c.field === 'TOTAL_PRICE');
+const cap = conds.find(c => c.operator === 'LESS_THAN_OR_EQUAL_TO');
+console.log(`Profile: ${d.deliveryProfile.name}`);
+console.log(`Band method: ${m.id} [${m.active ? 'active' : 'INACTIVE'}] $${m.rateProvider.price.amount}`);
+console.log(`Current condition: ${conds.map(c => `${c.operator} $${c.conditionCriteria.amount}`).join(' AND ')}`);
+
+if (!NEW_CAP) {
+ console.log('\nRECOMMENDED (DTD Option A): leave the cap at $45 — no change. It already gives ~10 samples free (covers retail 5+5).');
+ console.log('To tighten to exactly 10 samples: node retune-samples-band.mjs --cap 42.50 --apply');
+ process.exit(0);
+}
+if (!APPLY) {
+ console.log(`\nDRY RUN — would replace the <= cap ($${cap?.conditionCriteria.amount}) with $${NEW_CAP}. Add --apply to write.`);
+ process.exit(0);
+}
+
+// GATED WRITE: snapshot, delete old TOTAL_PRICE conditions, recreate 0..NEW_CAP.
+fs.writeFileSync(new URL('./verification/samples-band-retune-snapshot.json', import.meta.url),
+ JSON.stringify({ at: new Date().toISOString(), profile: PROFILE, lg, zone, method: m.id, previousConditions: conds }, null, 2) + '\n');
+const mut = `mutation($id:ID!,$profile:DeliveryProfileInput!){deliveryProfileUpdate(id:$id,profile:$profile){profile{id} userErrors{field message}}}`;
+const del = await query(mut, { id: PROFILE, profile: { conditionsToDelete: conds.map(c => c.id) } });
+if (del.deliveryProfileUpdate.userErrors.length) { console.error('delete err', del.deliveryProfileUpdate.userErrors); process.exit(1); }
+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: NEW_CAP, currencyCode: 'USD' } }] }] }] }] } });
+if (add.deliveryProfileUpdate.userErrors.length) { console.error('add err', add.deliveryProfileUpdate.userErrors); process.exit(1); }
+console.log(`\nAPPLIED — band cap set to $${NEW_CAP}. Undo: node retune-samples-band.mjs --cap ${cap?.conditionCriteria.amount || '45'} --apply`);
diff --git a/verify-codes.mjs b/verify-codes.mjs
new file mode 100644
index 0000000..f6f3b78
--- /dev/null
+++ b/verify-codes.mjs
@@ -0,0 +1,16 @@
+import {query} from './query.mjs';
+const ids=['gid://shopify/DiscountCodeNode/1466042351667','gid://shopify/DiscountCodeNode/1466042384435'];
+for(const id of ids){
+ const q=`{codeDiscountNode(id:"${id}"){codeDiscount{__typename
+ ... on DiscountCodeFreeShipping{title status appliesOncePerCustomer
+ maximumShippingPrice{amount} destinationSelection{__typename ... on DiscountCountryAll{allCountries}}
+ customerSelection{__typename ... on DiscountCustomerSegments{segments{id name}} ... on DiscountCustomerAll{allCustomers}}
+ codes(first:3){nodes{code}}}}}}`;
+ const d=await query(q);const c=d.codeDiscountNode.codeDiscount;
+ console.log(`\n${c.title} [${c.status}] type:${c.__typename}`);
+ console.log(` code(s): ${c.codes.nodes.map(x=>x.code).join(', ')}`);
+ console.log(` maxShip: $${c.maximumShippingPrice?.amount}`);
+ console.log(` destination: ${c.destinationSelection?.__typename}${c.destinationSelection?.allCountries?' (all)':''}`);
+ const cs=c.customerSelection;
+ console.log(` customers: ${cs.__typename}${cs.allCustomers?' (ALL)':''}${cs.segments?' -> '+cs.segments.map(s=>s.name).join(' | '):''}`);
+}
← 3963701 TK-11333: fix theme engine per Cody red-team — honest price-
·
back to Shopify Sample Shipping
·
TK-11333: author one-paste DEV-theme installer (themeDuplica 4125a61 →