← back to Shopify Sample Shipping
RUNBOOK-TK-11333.md
124 lines
# 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 ran only READ-ONLY reads — no `--apply`.**
---
## ⚠️ 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.
**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.
---
## 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.
---
## Preconditions (once)
```
! 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)
```
Pick a real **Sample variant id** for the prototype (any `{DW_SKU}-Sample`), e.g. from `verification/all-sample-variants.json`.
---
## 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 — 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 engine-logic-test.mjs # 9/9 policy scenarios 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.
**One-paste DEV install (Steve runs this — theme writes are classifier-blocked from our sessions):**
```
! ~/theme
```
That launcher runs `install-theme-engine.mjs --apply` (tee'd to `/tmp/tk-theme.log`). It: (1) **duplicates the published theme** `carnegie-color-swatch` (gid 145121607731) into a NEW **unpublished** copy "DW Sample-Shipping DEV" — the published theme is never touched and nothing is published; (2) uploads `snippets/sample-shipping-cart-engine.liquid`; (3) injects `{% render 'sample-shipping-cart-engine' %}` into the DEV copy's **`sections/cart.liquid`** immediately before `<div class="cart-buttons-container">` (verified-unique anchor, right above the `<button name="checkout">`). If the anchor isn't uniquely found it ABORTS and prints the one manual edit instead of guessing.
- Preview first (read-only, no writes): `! cd ~/Projects/shopify-sample-shipping && node install-theme-engine.mjs`
- Selector VERIFIED against carnegie markup: checkout button = `<button class="cart-checkout" name="checkout">` (snippet matches on `.cart-checkout` + `[name="checkout"]`); wallets = `.additional-checkout-buttons` (snippet hides them over the soft cap). Product types confirmed Wallcovering/Fabric.
**Undo:** `! cd ~/Projects/shopify-sample-shipping && node theme-engine-undo.mjs --apply` (deletes the DEV theme; refuses if the recorded theme is published or renamed). Record: `verification/theme-engine-install.json`.
---
## 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.
**PUBLISH (Steve-gated, LIVE — only after the manual proof PASSes):** in Admin → Online Store → Themes, on **"DW Sample-Shipping DEV"** click ⋯ → **Publish**. That swaps it in as the live theme (the old `carnegie-color-swatch` becomes an unpublished backup — instant rollback by re-publishing it). Do NOT publish before the Step-5 proof passes.
---
## 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 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)
```
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. 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
# 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, non-destructive. Ledger: `~/.claude/yolo-queue/executed-reversible/ledger.jsonl`.