← back to Shopify Sample Shipping
TK-11333: hard-delete the 3 test orders (final cleanup)
3cd0d114cd010f2f00177c9b1b0b334fd9664100 · 2026-09-09 16:29:16 -0700 · Steve Abrams
delete-orders.mjs removed #33047/#33048/#33049 from the store (all DELETED).
TK-11333 complete: samples-only free shipping live (96,321 variants), cart notice
live, test orders gone.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
A create-freeship-codes-undo.mjsA create-freeship-codes.mjsA create-trade-segment-undo.mjsA create-trade-segment.mjsA delete-orders.mjsA grandfather-apply.mjsA grandfather-undo.mjsA proto-autoapply.mjsA trade-grant-check.mjsA verification/trade-grant-report.json
Diff
commit 3cd0d114cd010f2f00177c9b1b0b334fd9664100
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Sep 9 16:29:16 2026 -0700
TK-11333: hard-delete the 3 test orders (final cleanup)
delete-orders.mjs removed #33047/#33048/#33049 from the store (all DELETED).
TK-11333 complete: samples-only free shipping live (96,321 variants), cart notice
live, test orders gone.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
create-freeship-codes-undo.mjs | 22 +++
create-freeship-codes.mjs | 76 ++++++++
create-trade-segment-undo.mjs | 39 ++++
create-trade-segment.mjs | 94 ++++++++++
delete-orders.mjs | 11 ++
grandfather-apply.mjs | 95 ++++++++++
grandfather-undo.mjs | 34 ++++
proto-autoapply.mjs | 122 +++++++++++++
trade-grant-check.mjs | 143 +++++++++++++++
verification/trade-grant-report.json | 343 +++++++++++++++++++++++++++++++++++
10 files changed, 979 insertions(+)
diff --git a/create-freeship-codes-undo.mjs b/create-freeship-codes-undo.mjs
new file mode 100644
index 0000000..9cf848d
--- /dev/null
+++ b/create-freeship-codes-undo.mjs
@@ -0,0 +1,22 @@
+#!/usr/bin/env node
+// TK-11333 — UNDO for create-freeship-codes.mjs. Deletes EXACTLY the codes we created
+// (verification/freeship-codes-created.json), skipping any marked preexisting:true.
+// DRY-RUN BY DEFAULT; --apply to write.
+import { query } from './query.mjs';
+import fs from 'node:fs';
+
+const APPLY = process.argv.includes('--apply');
+const REC = new URL('./verification/freeship-codes-created.json', import.meta.url);
+if (!fs.existsSync(REC)) { console.error('no verification/freeship-codes-created.json — nothing to undo'); process.exit(1); }
+const rec = JSON.parse(fs.readFileSync(REC, 'utf8'));
+const toDelete = (rec.created || []).filter(c => !c.preexisting && c.id);
+console.log('=== create-freeship-codes UNDO (' + (APPLY ? 'APPLY' : 'DRY-RUN') + ') ===');
+for (const c of toDelete) console.log(' would delete', c.code, c.id);
+if (!toDelete.length) console.log(' (nothing we created to delete)');
+if (!APPLY) { console.log('\nDry-run only. Re-run with --apply.'); process.exit(0); }
+
+for (const c of toDelete) {
+ const r = (await query(`mutation($id:ID!){discountCodeDelete(id:$id){deletedCodeDiscountId userErrors{field message}}}`, { id: c.id })).discountCodeDelete;
+ console.log(' deleted', c.code, '->', r.deletedCodeDiscountId || JSON.stringify(r.userErrors));
+}
+console.log('undo complete.');
diff --git a/create-freeship-codes.mjs b/create-freeship-codes.mjs
new file mode 100644
index 0000000..da7247c
--- /dev/null
+++ b/create-freeship-codes.mjs
@@ -0,0 +1,76 @@
+#!/usr/bin/env node
+// TK-11333 — create the two free-shipping CODE discounts (Option A):
+// • TRADESHIP — DiscountCodeFreeShipping scoped to the DW Trade / Designers SEGMENT
+// (server-enforced: retail literally cannot claim it even by pasting the code).
+// • SAMPLESHIP — DiscountCodeFreeShipping, ALL customers, maximumShippingPrice guarded.
+// Both carry maximumShippingPrice '30' so a freight roll in a mixed cart never rides free.
+// Customer selection is set via `context` (2024-10+ moved it off customerSelection to
+// context.customerSegments / context.all=ALL — verified by introspection 2026-09-09).
+//
+// GATED (customer-facing config write). DRY-RUN BY DEFAULT — prints the exact mutation+vars.
+// Idempotent: skips a code whose `code` already exists. Records created ids to
+// verification/freeship-codes-created.json for create-freeship-codes-undo.mjs.
+//
+// node create-freeship-codes.mjs # dry-run (prints both mutations)
+// node create-freeship-codes.mjs --apply # WRITE (Steve-gated) — segment must exist first
+import { query } from './query.mjs';
+import fs from 'node:fs';
+
+const APPLY = process.argv.includes('--apply');
+const MAX_SHIP = '30';
+const SEG_REC = new URL('./verification/trade-segment-created.json', import.meta.url);
+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); }
+
+const now = new Date().toISOString();
+const combines = { orderDiscounts: true, productDiscounts: true, shippingDiscounts: false };
+const CODES = [
+ { code: 'TRADESHIP', title: 'DW Trade Sample Free Shipping', context: { customerSegments: { add: [segId] } } },
+ { code: 'SAMPLESHIP', title: 'DW Retail Sample Free Shipping', context: { all: 'ALL' } },
+];
+const MUT = `mutation($fs:DiscountCodeFreeShippingInput!){discountCodeFreeShippingCreate(freeShippingCodeDiscount:$fs){codeDiscountNode{id codeDiscount{__typename ... on DiscountCodeFreeShipping{title status}}} userErrors{field message}}}`;
+
+function buildInput(c) {
+ return { title: c.title, code: c.code, startsAt: now, appliesOnOneTimePurchase: true, appliesOnSubscription: false,
+ destination: { all: true }, maximumShippingPrice: MAX_SHIP, combinesWith: combines, context: c.context };
+}
+
+async function codeExists(code) {
+ const r = await query(`{codeDiscountNodes(first:5,query:${JSON.stringify('code:' + code)}){nodes{id codeDiscount{__typename ... on DiscountCodeFreeShipping{title codes(first:5){nodes{code}}}}}}}`);
+ const hit = r.codeDiscountNodes.nodes.find(n => n.codeDiscount?.codes?.nodes?.some(x => x.code === code));
+ return hit?.id || null;
+}
+
+console.log('=== create-freeship-codes (' + (APPLY ? 'APPLY' : 'DRY-RUN') + ') ===');
+console.log('segment (designer code) :', segId);
+console.log('maximumShippingPrice : $' + MAX_SHIP);
+
+const result = { at: now, segId, maxShip: MAX_SHIP, created: [] };
+for (const c of CODES) {
+ const input = buildInput(c);
+ console.log(`\n• ${c.code} — ${c.title}`);
+ console.log(' vars:', JSON.stringify({ fs: input }));
+ const existing = await codeExists(c.code);
+ if (existing) { console.log(' ALREADY EXISTS ->', existing, '(skip)'); result.created.push({ code: c.code, id: existing, preexisting: true }); continue; }
+ if (!APPLY) { console.log(' WOULD create.'); continue; }
+ const r = (await query(MUT, { fs: input })).discountCodeFreeShippingCreate;
+ if (r.userErrors?.length) { console.error(' ERR', JSON.stringify(r.userErrors)); continue; }
+ console.log(' created ->', r.codeDiscountNode.id, r.codeDiscountNode.codeDiscount?.status);
+ result.created.push({ code: c.code, id: r.codeDiscountNode.id });
+}
+
+if (!APPLY) { console.log('\nDry-run only. Re-run with --apply.'); process.exit(0); }
+fs.writeFileSync(OUT, JSON.stringify(result, null, 2) + '\n');
+console.log('\nrecorded -> verification/freeship-codes-created.json');
+try {
+ const { execSync } = await import('node:child_process');
+ const ids = result.created.filter(c => !c.preexisting).map(c => c.code).join(',');
+ execSync(`node ${LOGX} --agent vp-dw-commerce --ticket TK-11333 ` +
+ `--action ${JSON.stringify('created free-ship codes ' + (ids || '(none new)'))} --blast ${result.created.length} ` +
+ `--undo ${JSON.stringify('cd ~/Projects/shopify-sample-shipping && node create-freeship-codes-undo.mjs --apply')} ` +
+ `--verify ${JSON.stringify('node disc-feasible.mjs')}`, { stdio: 'inherit' });
+} catch (e) { console.log('(ledger note skipped:', e.message, ')'); }
diff --git a/create-trade-segment-undo.mjs b/create-trade-segment-undo.mjs
new file mode 100644
index 0000000..28c5795
--- /dev/null
+++ b/create-trade-segment-undo.mjs
@@ -0,0 +1,39 @@
+#!/usr/bin/env node
+// TK-11333 — UNDO for create-trade-segment.mjs.
+// Deletes the `DW Trade / Designers` segment we created, and recreates the broken
+// `interior-designer-res` segment from the recorded snapshot (so state returns exactly).
+// DRY-RUN BY DEFAULT; --apply to write.
+//
+// WARNING: if any free-ship CODE still references the segment (create-freeship-codes.mjs),
+// delete the codes FIRST (create-freeship-codes-undo.mjs) — this script will warn and refuse
+// to delete a segment that is still in use.
+import { query } from './query.mjs';
+import fs from 'node:fs';
+
+const APPLY = process.argv.includes('--apply');
+const REC = new URL('./verification/trade-segment-created.json', import.meta.url);
+if (!fs.existsSync(REC)) { console.error('no verification/trade-segment-created.json — nothing to undo'); process.exit(1); }
+const rec = JSON.parse(fs.readFileSync(REC, 'utf8'));
+console.log('=== create-trade-segment UNDO (' + (APPLY ? 'APPLY' : 'DRY-RUN') + ') ===');
+console.log('would delete segment:', rec.segmentId);
+if (rec.brokenSnapshot) console.log('would recreate broken segment:', rec.brokenSnapshot.name);
+
+// safety: is the segment still referenced by a free-ship code?
+try {
+ const nodes = (await query(`{codeDiscountNodes(first:100){nodes{codeDiscount{__typename ... on DiscountCodeFreeShipping{title customerSelection{__typename ... on DiscountCustomerSegments{segments{id}}}}}}}}`)).codeDiscountNodes.nodes;
+ const inUse = nodes.some(n => n.codeDiscount?.customerSelection?.segments?.some(s => s.id === rec.segmentId));
+ if (inUse) { console.error('\nREFUSING: segment still referenced by a free-ship code. Run create-freeship-codes-undo.mjs --apply first.'); process.exit(2); }
+} catch (e) { console.log('(in-use check skipped:', e.message, ')'); }
+
+if (!APPLY) { console.log('\nDry-run only. Re-run with --apply.'); process.exit(0); }
+
+if (rec.segmentId) {
+ const del = (await query(`mutation($id:ID!){segmentDelete(id:$id){deletedSegmentId userErrors{field message}}}`, { id: rec.segmentId })).segmentDelete;
+ console.log('deleted segment:', del.deletedSegmentId || JSON.stringify(del.userErrors));
+}
+if (rec.brokenSnapshot) {
+ const r = (await query(`mutation($name:String!,$q:String!){segmentCreate(name:$name,query:$q){segment{id} userErrors{field message}}}`,
+ { name: rec.brokenSnapshot.name, q: rec.brokenSnapshot.query })).segmentCreate;
+ console.log('recreated broken segment:', r.segment?.id || JSON.stringify(r.userErrors));
+}
+console.log('undo complete.');
diff --git a/create-trade-segment.mjs b/create-trade-segment.mjs
new file mode 100644
index 0000000..c32c5c4
--- /dev/null
+++ b/create-trade-segment.mjs
@@ -0,0 +1,94 @@
+#!/usr/bin/env node
+// TK-11333 — create the `DW Trade / Designers` customer segment, and delete the broken
+// `interior-designer-res` segment (which queries a literal that matches 0 customers).
+//
+// GATED (customer-facing config write). DRY-RUN BY DEFAULT — prints the exact segmentCreate
+// query it WOULD run. Pass --apply to actually write. Idempotent: if a segment with the same
+// name already exists it re-uses it and does NOT create a duplicate. Records created ids to
+// verification/trade-segment-created.json for the paired undo (create-trade-segment-undo.mjs).
+//
+// VEHICLE DECISION (trade-grant-check.mjs verdict, 2026-09-09): the segment ORs the real trade
+// tags PLUS the dedicated `sample-freeship` grandfather tag — NOT mass-`trade` — because `trade`
+// on this store also gates PRICING (TRADECODE/TRADE15 15%-off codes + trade-only-benefits page +
+// trade_approved theme entitlement). Tagging 2,663 grandfathered customers `trade` would leak
+// those perks; `sample-freeship` is single-purpose.
+//
+// Usage:
+// node create-trade-segment.mjs # dry-run
+// node create-trade-segment.mjs --with-confirm # dry-run incl. the §4 "confirm-these" tags
+// node create-trade-segment.mjs --apply # WRITE (Steve-gated)
+import { query } from './query.mjs';
+import fs from 'node:fs';
+
+const APPLY = process.argv.includes('--apply');
+const WITH_CONFIRM = process.argv.includes('--with-confirm');
+const SEG_NAME = 'DW Trade / Designers';
+const BROKEN_NAME = 'interior-designer-res';
+const OUT = new URL('./verification/trade-segment-created.json', import.meta.url);
+const LOGX = process.env.HOME + '/.claude/yolo-queue/executed-reversible/log-exec.mjs';
+
+// Real trade tags (memo §4 core) + the dedicated grandfather vehicle + the theme entitlement tag.
+const CORE = ['trade', 'trade_approved', 'Interior Designer - Residential', 'Interior Designer - Commercial',
+ 'interior design', 'interior designer', 'interior', 'Contractor', 'Commercial Property Owner',
+ 'Architect', 'Wallcovering Installer', 'sample-freeship'];
+const CONFIRM = ['Photography Studio', 'Graphic Designer', 'Illustrator', 'Visual Merchandiser',
+ 'Production Company', 'Manufacturer', 'Developer'];
+const tags = WITH_CONFIRM ? [...CORE, ...CONFIRM] : CORE;
+const SEG_QUERY = tags.map(t => `customer_tags CONTAINS '${t.replace(/'/g, "\\'")}'`).join(' OR ');
+
+async function existingSegments() {
+ return (await query(`{segments(first:200){nodes{id name query}}}`)).segments.nodes;
+}
+
+console.log('=== create-trade-segment (' + (APPLY ? 'APPLY' : 'DRY-RUN') + ') ===');
+console.log('segment name :', SEG_NAME);
+console.log('tag set :', WITH_CONFIRM ? 'CORE + confirm-these' : 'CORE only', `(${tags.length} tags)`);
+console.log('segment query:\n ' + SEG_QUERY);
+
+const segs = await existingSegments();
+const dupe = segs.find(s => s.name === SEG_NAME);
+const broken = segs.find(s => s.name === BROKEN_NAME);
+console.log('\nexisting DW Trade / Designers?', dupe ? `YES ${dupe.id}` : 'no');
+console.log('broken interior-designer-res?', broken ? `YES ${broken.id} (query: ${broken.query})` : 'no');
+
+if (!APPLY) {
+ console.log('\n-- WOULD segmentCreate(name, query) above');
+ if (broken) console.log('-- WOULD segmentDelete(' + broken.id + ') [snapshot recorded first]');
+ console.log('\nDry-run only. Re-run with --apply to write.');
+ process.exit(0);
+}
+
+const result = { at: new Date().toISOString(), segName: SEG_NAME, segQuery: SEG_QUERY, tags };
+
+// 1) create (or reuse) the segment
+let segId = dupe?.id;
+if (!segId) {
+ const r = (await query(`mutation($name:String!,$q:String!){segmentCreate(name:$name,query:$q){segment{id name query} userErrors{field message}}}`,
+ { name: SEG_NAME, q: SEG_QUERY })).segmentCreate;
+ if (r.userErrors?.length) { console.error('segmentCreate ERR', JSON.stringify(r.userErrors)); process.exit(1); }
+ segId = r.segment.id;
+ console.log('created segment', segId);
+} else {
+ console.log('re-using existing segment', segId, '(no duplicate created)');
+}
+result.segmentId = segId;
+
+// 2) snapshot + delete the broken segment
+if (broken) {
+ result.brokenSnapshot = { id: broken.id, name: broken.name, query: broken.query };
+ const del = (await query(`mutation($id:ID!){segmentDelete(id:$id){deletedSegmentId userErrors{field message}}}`, { id: broken.id })).segmentDelete;
+ if (del.userErrors?.length) { console.error('segmentDelete ERR', JSON.stringify(del.userErrors)); }
+ else console.log('deleted broken segment', del.deletedSegmentId);
+}
+
+fs.writeFileSync(OUT, JSON.stringify(result, null, 2) + '\n');
+console.log('\nrecorded -> verification/trade-segment-created.json');
+
+// 3) ledger (reversible)
+try {
+ const { execSync } = await import('node:child_process');
+ execSync(`node ${LOGX} --agent vp-dw-commerce --ticket TK-11333 ` +
+ `--action ${JSON.stringify('created segment "' + SEG_NAME + '" + deleted broken interior-designer-res')} --blast 2 ` +
+ `--undo ${JSON.stringify('cd ~/Projects/shopify-sample-shipping && node create-trade-segment-undo.mjs --apply')} ` +
+ `--verify ${JSON.stringify('node list-designer-segments.mjs')}`, { stdio: 'inherit' });
+} catch (e) { console.log('(ledger note skipped:', e.message, ')'); }
diff --git a/delete-orders.mjs b/delete-orders.mjs
new file mode 100644
index 0000000..19fcda9
--- /dev/null
+++ b/delete-orders.mjs
@@ -0,0 +1,11 @@
+// Hard-delete the 3 TK-11333 test orders (already voided). Shopify may refuse delete
+// on draft-completed orders — reports per-order result honestly.
+import {query} from './query.mjs';
+for(const o of ['6141633069107','6141644046387','6141651812403']){
+ const id='gid://shopify/Order/'+o;
+ try{
+ const r=await query(`mutation($id:ID!){orderDelete(orderId:$id){deletedId userErrors{field message}}}`,{id});
+ const od=r.orderDelete;
+ console.log(o, od.deletedId?('DELETED '+od.deletedId):('NOT deleted: '+JSON.stringify(od.userErrors)));
+ }catch(e){ console.log(o,'error:',e.message.slice(0,160)); }
+}
diff --git a/grandfather-apply.mjs b/grandfather-apply.mjs
new file mode 100644
index 0000000..ed00371
--- /dev/null
+++ b/grandfather-apply.mjs
@@ -0,0 +1,95 @@
+#!/usr/bin/env node
+// TK-11333 — grandfather TIER-1 designers (2,663) so they KEEP free sample shipping when
+// retail-charging goes live ("lose no designers"). Adds the dedicated `sample-freeship` tag
+// (NOT `trade` — see trade-grant-check.mjs verdict) to each TIER-1 customer.
+//
+// GATED (identity/customer write, blast radius 2,663 > 500 → hard-gated regardless of
+// reversibility). DRY-RUN BY DEFAULT. Resumable + idempotent + fully reversible.
+//
+// node grandfather-apply.mjs # dry-run: plan + validate a few lookups
+// node grandfather-apply.mjs --limit 20 # dry-run of a 20-customer pilot slice
+// node grandfather-apply.mjs --apply # WRITE all pending (Steve-gated)
+// node grandfather-apply.mjs --apply --limit 20 # WRITE only the first 20 pending (pilot)
+// node grandfather-apply.mjs --apply --tier2 # also include TIER-2 (765) — default is TIER-1 only
+//
+// Resume: re-running --apply skips customers already tagged (progress in
+// verification/grandfather-progress.json). Undo: grandfather-undo.mjs reads the exact
+// applied list (verification/grandfather-applied.json) and tagsRemove.
+import { query } from './query.mjs';
+import fs from 'node:fs';
+
+const APPLY = process.argv.includes('--apply');
+const WITH_TIER2 = process.argv.includes('--tier2');
+const li = process.argv.indexOf('--limit');
+const LIMIT = li >= 0 ? Number(process.argv[li + 1]) : Infinity;
+const TAG = 'sample-freeship';
+
+const LIST = JSON.parse(fs.readFileSync(new URL('./verification/grandfather-list.json', import.meta.url), 'utf8'));
+const PROG = new URL('./verification/grandfather-progress.json', import.meta.url);
+const APPLIED = new URL('./verification/grandfather-applied.json', import.meta.url);
+const LOGX = process.env.HOME + '/.claude/yolo-queue/executed-reversible/log-exec.mjs';
+
+const targets = [...LIST.tier1, ...(WITH_TIER2 ? LIST.tier2 : [])];
+const prog = fs.existsSync(PROG) ? JSON.parse(fs.readFileSync(PROG, 'utf8')) : { doneEmails: [], missing: [], ambiguous: [] };
+const done = new Set(prog.doneEmails.map(e => e.toLowerCase()));
+const applied = fs.existsSync(APPLIED) ? JSON.parse(fs.readFileSync(APPLIED, 'utf8')) : [];
+const appliedSet = new Set(applied.map(a => a.email.toLowerCase()));
+
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+async function q(gql, vars) { for (let a = 0; a < 6; a++) { try { return await query(gql, vars); } catch (e) { if (a === 5) throw e; await sleep(500 * (a + 1)); } } }
+async function findCustomer(email) {
+ const r = await q(`query($q:String!){customers(first:3,query:$q){edges{node{id email tags}}}}`, { q: `email:${email}` });
+ const exact = r.customers.edges.filter(e => (e.node.email || '').toLowerCase() === email.toLowerCase());
+ if (exact.length === 1) return exact[0].node;
+ if (exact.length > 1) return { ambiguous: true };
+ return null;
+}
+
+const pending = targets.filter(t => !done.has(t.email.toLowerCase())).slice(0, LIMIT);
+console.log('=== grandfather-apply (' + (APPLY ? 'APPLY' : 'DRY-RUN') + ') ===');
+console.log('tag :', TAG);
+console.log('tier :', WITH_TIER2 ? 'TIER-1 + TIER-2' : 'TIER-1 only');
+console.log('total targets :', targets.length, '| already done:', done.size, '| pending this run:', pending.length, LIMIT !== Infinity ? `(limit ${LIMIT})` : '');
+
+if (!APPLY) {
+ console.log('\n-- validating first 5 email lookups (read-only) --');
+ for (const t of pending.slice(0, 5)) {
+ const c = await findCustomer(t.email);
+ console.log(` ${t.email} -> ${c?.ambiguous ? 'AMBIGUOUS' : c ? c.id + (c.tags?.includes(TAG) ? ' [already tagged]' : '') : 'NOT FOUND'}`);
+ }
+ console.log(`\nWOULD tagsAdd '${TAG}' to up to ${pending.length} customers. Re-run with --apply.`);
+ process.exit(0);
+}
+
+let tagged = 0, already = 0, missing = 0, ambiguous = 0, i = 0;
+for (const t of pending) {
+ i++;
+ const email = t.email;
+ try {
+ const c = await findCustomer(email);
+ if (!c) { prog.missing.push(email); missing++; done.add(email.toLowerCase()); prog.doneEmails.push(email); }
+ else if (c.ambiguous) { prog.ambiguous.push(email); ambiguous++; done.add(email.toLowerCase()); prog.doneEmails.push(email); }
+ else if (c.tags?.includes(TAG)) { already++; done.add(email.toLowerCase()); prog.doneEmails.push(email); if (!appliedSet.has(email.toLowerCase())) { applied.push({ email, customerId: c.id, preexisting: true }); appliedSet.add(email.toLowerCase()); } }
+ else {
+ const r = (await q(`mutation($id:ID!,$tags:[String!]!){tagsAdd(id:$id,tags:$tags){userErrors{field message}}}`, { id: c.id, tags: [TAG] })).tagsAdd;
+ if (r.userErrors?.length) { console.error(' tagsAdd ERR', email, JSON.stringify(r.userErrors)); }
+ else { tagged++; applied.push({ email, customerId: c.id }); appliedSet.add(email.toLowerCase()); done.add(email.toLowerCase()); prog.doneEmails.push(email); }
+ }
+ } catch (e) { console.error(' ERR', email, e.message.slice(0, 100)); }
+ if (i % 50 === 0) {
+ fs.writeFileSync(PROG, JSON.stringify(prog, null, 2)); fs.writeFileSync(APPLIED, JSON.stringify(applied, null, 2));
+ console.log(` ...${i}/${pending.length} tagged=${tagged} already=${already} missing=${missing} ambiguous=${ambiguous}`);
+ }
+ await sleep(120);
+}
+fs.writeFileSync(PROG, JSON.stringify(prog, null, 2)); fs.writeFileSync(APPLIED, JSON.stringify(applied, null, 2));
+console.log(`\nDONE this run: tagged=${tagged} already=${already} missing=${missing} ambiguous=${ambiguous}`);
+console.log('applied list -> verification/grandfather-applied.json (', applied.length, 'total tagged )');
+
+try {
+ const { execSync } = await import('node:child_process');
+ execSync(`node ${LOGX} --agent vp-dw-commerce --ticket TK-11333 ` +
+ `--action ${JSON.stringify(`grandfather tagged '${TAG}' on ${tagged} customers this run (${applied.length} total)`)} --blast ${applied.length} ` +
+ `--undo ${JSON.stringify('cd ~/Projects/shopify-sample-shipping && node grandfather-undo.mjs --apply')} ` +
+ `--verify ${JSON.stringify('node -e "const a=require(\\"./verification/grandfather-applied.json\\");console.log(a.length,\\"tagged\\")"')}`, { stdio: 'inherit' });
+} catch (e) { console.log('(ledger note skipped:', e.message, ')'); }
diff --git a/grandfather-undo.mjs b/grandfather-undo.mjs
new file mode 100644
index 0000000..e760da5
--- /dev/null
+++ b/grandfather-undo.mjs
@@ -0,0 +1,34 @@
+#!/usr/bin/env node
+// TK-11333 — UNDO for grandfather-apply.mjs. Removes the `sample-freeship` tag from EXACTLY
+// the customers we tagged (verification/grandfather-applied.json), never a broader set.
+// Skips entries marked preexisting:true (they had the tag before we ran — leave them).
+// DRY-RUN BY DEFAULT; --apply to write.
+import { query } from './query.mjs';
+import fs from 'node:fs';
+
+const APPLY = process.argv.includes('--apply');
+const TAG = 'sample-freeship';
+const APPLIED = new URL('./verification/grandfather-applied.json', import.meta.url);
+if (!fs.existsSync(APPLIED)) { console.error('no verification/grandfather-applied.json — nothing to undo'); process.exit(1); }
+const applied = JSON.parse(fs.readFileSync(APPLIED, 'utf8'));
+const toRemove = applied.filter(a => !a.preexisting);
+console.log('=== grandfather UNDO (' + (APPLY ? 'APPLY' : 'DRY-RUN') + ') ===');
+console.log('would remove', TAG, 'from', toRemove.length, 'customers (', applied.length - toRemove.length, 'preexisting kept )');
+if (!APPLY) { console.log('\nDry-run only. Re-run with --apply.'); process.exit(0); }
+
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+let removed = 0, i = 0;
+const remaining = [];
+for (const a of applied) {
+ i++;
+ if (a.preexisting) { remaining.push(a); continue; }
+ try {
+ const r = (await query(`mutation($id:ID!,$tags:[String!]!){tagsRemove(id:$id,tags:$tags){userErrors{field message}}}`, { id: a.customerId, tags: [TAG] })).tagsRemove;
+ if (r.userErrors?.length) { console.error(' ERR', a.email, JSON.stringify(r.userErrors)); remaining.push(a); }
+ else removed++;
+ } catch (e) { console.error(' ERR', a.email, e.message.slice(0, 80)); remaining.push(a); }
+ if (i % 50 === 0) { console.log(` ...${i}/${applied.length} removed=${removed}`); fs.writeFileSync(APPLIED, JSON.stringify(remaining.concat(applied.slice(i)), null, 2)); }
+ await sleep(120);
+}
+fs.writeFileSync(APPLIED, JSON.stringify(remaining, null, 2));
+console.log(`\nDONE: removed ${TAG} from ${removed} customers. Remaining recorded (preexisting/failed): ${remaining.length}`);
diff --git a/proto-autoapply.mjs b/proto-autoapply.mjs
new file mode 100644
index 0000000..1e58a7c
--- /dev/null
+++ b/proto-autoapply.mjs
@@ -0,0 +1,122 @@
+#!/usr/bin/env node
+// TK-11333 — PROTOTYPE HARNESS (go/no-go for flipping). Answers the one unproven mechanic
+// (DTD dissent hole #1): does the /discount/CODE?redirect=/cart permalink actually make FREE
+// SHIPPING persist to CHECKOUT for a logged-in ELIGIBLE customer — or does it degrade to a
+// page-reload / lost-on-reload / still-charged experience?
+//
+// It drives the REAL Chrome over CDP (openclaw, 127.0.0.1:18800 — same seam as admin-browser.mjs)
+// so it uses whatever customer session is logged in THERE. Steve: in that Chrome, log in as a
+// throwaway ELIGIBLE (in-segment) customer and have a saved US address before running with
+// --checkout. The harness only manipulates a customer CART/checkout session (no admin writes).
+//
+// PREREQ: the test code must already exist. Either create TRADESHIP first (create-freeship-codes)
+// or have Steve make a throwaway free-ship code in Admin — note it as a runbook step.
+//
+// node proto-autoapply.mjs --code TRADESHIP --variant 44090076954675 --checkout
+// --domain designerwallcoverings.com (default domain shown)
+//
+// VERDICT (printed + verification/proto-autoapply-report.json):
+// PASS — checkout shows $0 shipping for the eligible customer AND it survives a reload
+// DEGRADE — permalink reloaded but code didn't stick / checkout still charges / lost on reload
+// UNKNOWN — couldn't complete (CDP down / not logged in / no address / no checkout reached)
+import fs from 'node:fs';
+
+const arg = k => { const i = process.argv.indexOf('--' + k); return i >= 0 ? process.argv[i + 1] : undefined; };
+const CODE = arg('code');
+const DOMAIN = arg('domain') || 'designerwallcoverings.com';
+const VARIANT = arg('variant'); // a Sample variant numeric id to add to cart
+const DO_CHECKOUT = process.argv.includes('--checkout');
+const CDP = arg('cdp') || 'http://127.0.0.1:18800';
+if (!CODE) { console.error('need --code CODE'); process.exit(1); }
+
+const OUT = new URL('./verification/proto-autoapply-report.json', import.meta.url);
+const rep = { at: new Date().toISOString(), code: CODE, domain: DOMAIN, variant: VARIANT || null, steps: {}, verdict: 'UNKNOWN', notes: [] };
+const base = `https://${DOMAIN}`;
+
+let chromium;
+try { ({ chromium } = await import('../Designer-Wallcoverings/node_modules/playwright/index.mjs')); }
+catch (e) { rep.notes.push('playwright import failed: ' + e.message); finish(); }
+
+let b;
+try { b = await chromium.connectOverCDP(CDP); }
+catch (e) { rep.notes.push('CDP connect failed (' + CDP + '): ' + e.message + ' — start openclaw real Chrome, then retry.'); finish(); }
+
+try {
+ const ctx = b.contexts()[0] || await b.newContext();
+ const page = await ctx.newPage();
+
+ // 0) login state
+ await page.goto(`${base}/account`, { waitUntil: 'domcontentloaded', timeout: 45000 });
+ const onLogin = /\/account\/login/.test(page.url());
+ rep.steps.loggedIn = !onLogin;
+ if (onLogin) rep.notes.push('NOT logged in — log in as an eligible (in-segment) customer in the CDP Chrome, then re-run.');
+
+ // 1) add a sample to cart (if a variant was provided)
+ if (VARIANT) {
+ const added = await page.evaluate(async (v) => {
+ const r = await fetch('/cart/add.js', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ items: [{ id: Number(v), quantity: 1 }] }) });
+ return { ok: r.ok, status: r.status };
+ }, VARIANT);
+ rep.steps.addToCart = added;
+ }
+ const cartBefore = await page.evaluate(async () => (await (await fetch('/cart.js')).json()));
+ rep.steps.cartBefore = { item_count: cartBefore.item_count, total_price: cartBefore.total_price, total_discount: cartBefore.total_discount };
+
+ // 2) hit the discount permalink (this NAVIGATES = the "reload" the dissent worries about)
+ const t0 = Date.now();
+ await page.goto(`${base}/discount/${encodeURIComponent(CODE)}?redirect=/cart`, { waitUntil: 'domcontentloaded', timeout: 45000 });
+ rep.steps.permalink = { landedUrl: page.url(), navMs: Date.now() - t0, causedReload: true };
+ const cartAfter = await page.evaluate(async () => (await (await fetch('/cart.js')).json()));
+ rep.steps.cartAfter = { item_count: cartAfter.item_count, total_price: cartAfter.total_price, total_discount: cartAfter.total_discount, discount_apps: cartAfter.cart_level_discount_applications?.length ?? null };
+ const codeStuckInCart = (cartAfter.total_discount > 0) || (cartAfter.cart_level_discount_applications?.length > 0);
+ rep.steps.codeStuckInCart = codeStuckInCart;
+ // NOTE: cart.js does NOT reflect a SHIPPING discount (shipping is priced at checkout) — so
+ // a free-shipping code correctly shows total_discount:0 here. Presence of the code cookie is
+ // proven by reaching checkout below. We record cart state for diagnostics either way.
+
+ // 3) persistence across a reload of /cart
+ await page.reload({ waitUntil: 'domcontentloaded' });
+ const cartReload = await page.evaluate(async () => (await (await fetch('/cart.js')).json()));
+ rep.steps.cartAfterReload = { total_price: cartReload.total_price, total_discount: cartReload.total_discount };
+
+ // 4) checkout: the ONLY place a free-shipping code proves itself
+ if (DO_CHECKOUT) {
+ try {
+ await page.goto(`${base}/checkout`, { waitUntil: 'domcontentloaded', timeout: 60000 });
+ await page.waitForTimeout(3500);
+ const url = page.url();
+ const body = (await page.locator('body').innerText().catch(() => '')).slice(0, 20000);
+ // capture any shipping line + free markers
+ const shipLine = (body.match(/shipping[^\n]{0,60}/i) || [])[0] || '';
+ const freeMarker = /free/i.test(shipLine) || /\$?0\.00\s*(shipping|delivery)/i.test(body);
+ const shot = new URL('./verification/proto-checkout.png', import.meta.url).pathname;
+ await page.screenshot({ path: shot, fullPage: false }).catch(() => {});
+ rep.steps.checkout = { url, shippingLine: shipLine.replace(/\s+/g, ' ').trim(), freeMarker, screenshot: shot, onCheckoutDomain: /\/checkout|checkouts\//.test(url) };
+ // reload persistence at checkout
+ await page.reload({ waitUntil: 'domcontentloaded' }); await page.waitForTimeout(2500);
+ const body2 = (await page.locator('body').innerText().catch(() => '')).slice(0, 20000);
+ rep.steps.checkout.freeAfterReload = /free/i.test((body2.match(/shipping[^\n]{0,60}/i) || [''])[0]) || /\$?0\.00\s*(shipping|delivery)/i.test(body2);
+ } catch (e) { rep.notes.push('checkout step error: ' + e.message); }
+ }
+
+ // ---- VERDICT ----
+ if (!rep.steps.loggedIn) { rep.verdict = 'UNKNOWN'; rep.notes.push('verdict UNKNOWN — not logged in; the eligible-customer path is the whole test.'); }
+ else if (DO_CHECKOUT && rep.steps.checkout?.onCheckoutDomain) {
+ if (rep.steps.checkout.freeMarker && rep.steps.checkout.freeAfterReload) { rep.verdict = 'PASS'; rep.notes.push('Free shipping showed at checkout AND survived a reload — auto-apply is viable.'); }
+ else if (rep.steps.checkout.freeMarker) { rep.verdict = 'DEGRADE'; rep.notes.push('Free shipping showed at checkout but did NOT survive a reload — session-only, flaky.'); }
+ else { rep.verdict = 'DEGRADE'; rep.notes.push('Reached checkout but shipping was NOT free — code did not carry to checkout.'); }
+ } else { rep.verdict = 'UNKNOWN'; rep.notes.push('Ran without --checkout (or checkout not reached). Re-run with --checkout + a logged-in eligible customer + saved address for a real go/no-go.'); }
+
+ await b.close().catch(() => {});
+} catch (e) { rep.notes.push('harness error: ' + e.message); try { await b.close(); } catch {} }
+
+finish();
+function finish() {
+ fs.writeFileSync(OUT, JSON.stringify(rep, null, 2) + '\n');
+ console.log('=== proto-autoapply — VERDICT:', rep.verdict, '===');
+ for (const [k, v] of Object.entries(rep.steps)) console.log(' ' + k + ':', JSON.stringify(v));
+ if (rep.notes.length) { console.log('notes:'); rep.notes.forEach(n => console.log(' - ' + n)); }
+ console.log('\nfull report -> verification/proto-autoapply-report.json');
+ console.log('\nGO/NO-GO: PASS => safe to create codes + flip band. DEGRADE => do NOT remove the band; keep a fallback band or rethink auto-apply. UNKNOWN => complete the manual prereqs and re-run.');
+ process.exit(0);
+}
diff --git a/trade-grant-check.mjs b/trade-grant-check.mjs
new file mode 100644
index 0000000..8eb0443
--- /dev/null
+++ b/trade-grant-check.mjs
@@ -0,0 +1,143 @@
+#!/usr/bin/env node
+// TK-11333 — READ-ONLY: determine what the `trade` customer tag actually GRANTS on this
+// LIVE store, so we can pick the grandfather VEHICLE safely.
+//
+// The danger: the grandfather step tags 2,663 customers so they keep FREE SAMPLE SHIPPING.
+// If we tag them `trade` and `trade` ALSO unlocks trade PRICING / net cost / gated products
+// / a %-off discount, we would silently hand 2,663 people trade pricing — a money leak.
+//
+// This script inspects every place a customer tag can grant something and reports whether
+// `trade` (or a segment keyed on `trade`) is wired to anything BEYOND sample free-shipping:
+// 1. Code discounts — customerSelection = DiscountCustomerSegments; which segments;
+// and the discount TYPE (free-shipping vs %-off/amount-off = pricing)
+// 2. Automatic discounts- (no customerSelection field on this plan, but list them anyway)
+// 3. Segments — every segment whose query CONTAINS 'trade', and what consumes it
+// 4. Price rules (REST) — legacy price_rules with a customer prerequisite (saved search)
+// 5. Catalogs / B2B — publications/price lists gated to a customer segment
+// 6. Theme references — Liquid asset text that branches on customer.tags contains 'trade'
+//
+// VERDICT: prints whether `trade` is SAFE to reuse as the grandfather vehicle, or whether a
+// DEDICATED single-purpose tag (`sample-freeship`) must be used instead.
+//
+// Usage: node trade-grant-check.mjs # read-only, writes verification/trade-grant-report.json
+import { query } from './query.mjs';
+import { TOKEN, SHOP } from '../designerwallcoverings/scripts/lib/shopify.mjs';
+import fs from 'node:fs';
+
+const REST = `https://${SHOP}/admin/api/2024-10`;
+const H = { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' };
+const restGet = async p => { const r = await fetch(REST + p, { headers: H }); return { status: r.status, json: await r.json().catch(() => null) }; };
+const mentionsTrade = s => typeof s === 'string' && /(^|[^a-z])trade([^a-z]|$)/i.test(s);
+
+const report = { at: new Date().toISOString(), store: SHOP, findings: {}, tradeGrants: [], verdict: null };
+
+// ---- 1) CODE DISCOUNTS: type + customerSelection segments -------------------------------
+try {
+ const nodes = (await query(`{codeDiscountNodes(first:100){nodes{codeDiscount{__typename
+ ... on DiscountCodeBasic{title status customerSelection{__typename ... on DiscountCustomerSegments{segments{id name query}} ... on DiscountCustomerAll{allCustomers}} customerGets{value{__typename}}}
+ ... on DiscountCodeFreeShipping{title status customerSelection{__typename ... on DiscountCustomerSegments{segments{id name query}} ... on DiscountCustomerAll{allCustomers}}}
+ ... on DiscountCodeBxgy{title status customerSelection{__typename ... on DiscountCustomerSegments{segments{id name query}}}}
+ }}}}`)).codeDiscountNodes.nodes;
+ const rows = [];
+ for (const n of nodes) {
+ const d = n.codeDiscount; if (!d) continue;
+ const sel = d.customerSelection;
+ const segs = sel?.segments || [];
+ const isPricing = d.__typename === 'DiscountCodeBasic' || d.__typename === 'DiscountCodeBxgy';
+ const segTrade = segs.filter(s => mentionsTrade(s.name) || mentionsTrade(s.query));
+ rows.push({ title: d.title, type: d.__typename, status: d.status, selection: sel?.__typename, segments: segs.map(s => s.name), tradeSegments: segTrade.map(s => s.name), grantsPricing: isPricing });
+ if (segTrade.length) report.tradeGrants.push({ where: 'code-discount', title: d.title, type: d.__typename, grantsPricing: isPricing, via: segTrade.map(s => s.name) });
+ }
+ report.findings.codeDiscounts = rows;
+} catch (e) { report.findings.codeDiscounts = { error: e.message }; }
+
+// ---- 2) AUTOMATIC DISCOUNTS -------------------------------------------------------------
+try {
+ const nodes = (await query(`{automaticDiscountNodes(first:100){nodes{automaticDiscount{__typename
+ ... on DiscountAutomaticBasic{title status}
+ ... on DiscountAutomaticFreeShipping{title status}
+ ... on DiscountAutomaticBxgy{title status}}}}}`)).automaticDiscountNodes.nodes;
+ report.findings.automaticDiscounts = nodes.map(n => ({ title: n.automaticDiscount?.title, type: n.automaticDiscount?.__typename, status: n.automaticDiscount?.status }));
+} catch (e) { report.findings.automaticDiscounts = { error: e.message }; }
+
+// ---- 3) SEGMENTS keyed on trade + what consumes them ------------------------------------
+try {
+ const segs = (await query(`{segments(first:150){nodes{id name query}}}`)).segments.nodes;
+ const tradeSegs = segs.filter(s => mentionsTrade(s.name) || mentionsTrade(s.query));
+ report.findings.tradeSegments = tradeSegs.map(s => ({ name: s.name, query: s.query }));
+} catch (e) { report.findings.tradeSegments = { error: e.message }; }
+
+// ---- 4) PRICE RULES (REST) with a customer prerequisite --------------------------------
+try {
+ const r = await restGet('/price_rules.json?limit=250');
+ const prs = (r.json?.price_rules || []).map(p => ({
+ title: p.title, value_type: p.value_type, value: p.value, target_type: p.target_type,
+ prerequisite_customer_ids: (p.prerequisite_customer_ids || []).length,
+ customer_selection: p.customer_selection, saved_search: p.prerequisite_saved_search_ids || [],
+ }));
+ // We cannot always read a saved_search's tag filter via REST, but a price rule with
+ // customer_selection='prerequisite' + a saved_search is a candidate tag-gated PRICING grant.
+ report.findings.priceRules = { count: prs.length, tagGatedCandidates: prs.filter(p => p.customer_selection === 'prerequisite') };
+} catch (e) { report.findings.priceRules = { error: e.message }; }
+
+// ---- 5) CATALOGS / B2B price lists gated to a segment ----------------------------------
+try {
+ const cats = (await query(`{catalogs(first:50,type:COMPANY_LOCATION){nodes{id title status ... on CompanyLocationCatalog{companyLocationsCount{count}}}}}`).catch(() => null));
+ report.findings.catalogs = cats?.catalogs?.nodes || 'none-or-not-supported';
+} catch (e) { report.findings.catalogs = { error: e.message }; }
+
+// ---- 6) THEME LIQUID references to customer.tags 'trade' -------------------------------
+try {
+ const themesR = await restGet('/themes.json');
+ const main = (themesR.json?.themes || []).find(t => t.role === 'main');
+ const hits = [];
+ if (main) {
+ const assetsR = await restGet(`/themes/${main.id}/assets.json`);
+ const keys = (assetsR.json?.assets || []).map(a => a.key).filter(k => /\.(liquid|js)$/i.test(k));
+ // Scan a bounded set of likely-relevant assets (cart/product/pricing/customer) to stay fast.
+ const scan = keys.filter(k => /(cart|product|price|customer|trade|snippet|template|section|main|theme)/i.test(k)).slice(0, 120);
+ for (const key of scan) {
+ const a = await restGet(`/themes/${main.id}/assets.json?asset[key]=${encodeURIComponent(key)}`);
+ const v = a.json?.asset?.value || '';
+ if (/customer\.tags[\s\S]{0,80}trade/i.test(v) || /['"]trade['"][\s\S]{0,80}customer\.tags/i.test(v) || /contains\s+['"]trade['"]/i.test(v)) {
+ const idx = v.search(/trade/i);
+ hits.push({ key, snippet: v.slice(Math.max(0, idx - 90), idx + 60).replace(/\s+/g, ' ') });
+ }
+ }
+ report.findings.themeMain = main.name;
+ }
+ report.findings.themeTradeTagRefs = hits;
+} catch (e) { report.findings.themeTradeTagRefs = { error: e.message }; }
+
+// ---- VERDICT ---------------------------------------------------------------------------
+const pricingGrants = report.tradeGrants.filter(g => g.grantsPricing);
+const themeGrants = (report.findings.themeTradeTagRefs || []).filter(h => h.key);
+const anyGrant = pricingGrants.length > 0 || themeGrants.length > 0;
+
+// Regardless of what we find, the single-purpose vehicle is strictly safer for a 2,663-customer
+// grandfather whose ONLY intent is sample free-shipping. We only "green-light reuse of `trade`"
+// if trade demonstrably grants NOTHING beyond sample shipping AND Steve prefers one tag.
+report.verdict = {
+ tradeGrantsBeyondSampleShipping: anyGrant,
+ pricingGrantsViaTrade: pricingGrants,
+ themeBranchesOnTrade: themeGrants.map(h => h.key),
+ recommendedVehicle: 'sample-freeship',
+ rationale: anyGrant
+ ? 'trade is wired to pricing/content beyond sample-shipping — mass-tagging 2,663 grandfathered customers `trade` would leak those perks. MUST use a dedicated `sample-freeship` tag; the DW Trade/Designers segment ORs the real trade tags AND `sample-freeship`.'
+ : 'No pricing/content grant found wired to `trade` today, BUT a dedicated single-purpose `sample-freeship` tag is still the safe vehicle: it is additive, cannot accidentally confer any FUTURE trade perk that later keys on `trade`, and the segment ORs it alongside the real trade tags so grandfathered designers get free sample shipping and nothing else.',
+ vehicleForSegment: "segment query ORs all real trade tags (memo §4) PLUS customer_tags CONTAINS 'sample-freeship'",
+};
+
+fs.writeFileSync(new URL('./verification/trade-grant-report.json', import.meta.url), JSON.stringify(report, null, 2) + '\n');
+console.log('=== TRADE-GRANT CHECK (read-only) ===');
+console.log('code discounts:', Array.isArray(report.findings.codeDiscounts) ? report.findings.codeDiscounts.length : report.findings.codeDiscounts);
+for (const r of (Array.isArray(report.findings.codeDiscounts) ? report.findings.codeDiscounts : [])) {
+ console.log(` • ${r.title} [${r.type} ${r.status}] sel=${r.selection} pricing=${r.grantsPricing} tradeSeg=${r.tradeSegments.join('|') || '—'}`);
+}
+console.log('trade-keyed segments:', JSON.stringify(report.findings.tradeSegments));
+console.log('price rules tag-gated candidates:', report.findings.priceRules?.tagGatedCandidates?.length ?? report.findings.priceRules);
+console.log('theme branches on trade tag:', JSON.stringify(report.findings.themeTradeTagRefs));
+console.log('\n>>> trade grants beyond sample shipping?', report.verdict.tradeGrantsBeyondSampleShipping);
+console.log('>>> RECOMMENDED VEHICLE:', report.verdict.recommendedVehicle);
+console.log('>>> rationale:', report.verdict.rationale);
+console.log('\nfull report -> verification/trade-grant-report.json');
diff --git a/verification/trade-grant-report.json b/verification/trade-grant-report.json
new file mode 100644
index 0000000..bfd827e
--- /dev/null
+++ b/verification/trade-grant-report.json
@@ -0,0 +1,343 @@
+{
+ "at": "2026-09-09T23:23:50.689Z",
+ "store": "designer-laboratory-sandbox.myshopify.com",
+ "findings": {
+ "codeDiscounts": [
+ {
+ "title": "FREE SAMPLE",
+ "type": "DiscountCodeBasic",
+ "status": "ACTIVE",
+ "selection": "DiscountCustomerAll",
+ "segments": [],
+ "tradeSegments": [],
+ "grantsPricing": true
+ },
+ {
+ "title": "FREESHIPPING2021",
+ "type": "DiscountCodeFreeShipping",
+ "status": "EXPIRED",
+ "selection": "DiscountCustomerAll",
+ "segments": [],
+ "tradeSegments": [],
+ "grantsPricing": false
+ },
+ {
+ "title": "FREE TAPE MEASURE",
+ "type": "DiscountCodeBxgy",
+ "status": "EXPIRED",
+ "selection": "DiscountCustomerAll",
+ "segments": [],
+ "tradeSegments": [],
+ "grantsPricing": true
+ },
+ {
+ "title": "FreeShipping112020",
+ "type": "DiscountCodeFreeShipping",
+ "status": "EXPIRED",
+ "selection": "DiscountCustomers",
+ "segments": [],
+ "tradeSegments": [],
+ "grantsPricing": false
+ },
+ {
+ "title": "RedWallcovering",
+ "type": "DiscountCodeBasic",
+ "status": "ACTIVE",
+ "selection": "DiscountCustomerAll",
+ "segments": [],
+ "tradeSegments": [],
+ "grantsPricing": true
+ },
+ {
+ "title": "SWEET",
+ "type": "DiscountCodeBasic",
+ "status": "ACTIVE",
+ "selection": "DiscountCustomerAll",
+ "segments": [],
+ "tradeSegments": [],
+ "grantsPricing": true
+ },
+ {
+ "title": "WELCOME BACK",
+ "type": "DiscountCodeBasic",
+ "status": "ACTIVE",
+ "selection": "DiscountCustomerAll",
+ "segments": [],
+ "tradeSegments": [],
+ "grantsPricing": true
+ },
+ {
+ "title": "PRINCE",
+ "type": "DiscountCodeBasic",
+ "status": "ACTIVE",
+ "selection": "DiscountCustomerAll",
+ "segments": [],
+ "tradeSegments": [],
+ "grantsPricing": true
+ },
+ {
+ "title": "3FREE",
+ "type": "DiscountCodeBasic",
+ "status": "ACTIVE",
+ "selection": "DiscountCustomerAll",
+ "segments": [],
+ "tradeSegments": [],
+ "grantsPricing": true
+ },
+ {
+ "title": "bananaleafwallpapercom",
+ "type": "DiscountCodeFreeShipping",
+ "status": "ACTIVE",
+ "selection": "DiscountCustomerAll",
+ "segments": [],
+ "tradeSegments": [],
+ "grantsPricing": false
+ },
+ {
+ "title": "International",
+ "type": "DiscountCodeBasic",
+ "status": "ACTIVE",
+ "selection": "DiscountCustomerAll",
+ "segments": [],
+ "tradeSegments": [],
+ "grantsPricing": true
+ },
+ {
+ "title": "BH15",
+ "type": "DiscountCodeBasic",
+ "status": "ACTIVE",
+ "selection": "DiscountCustomerAll",
+ "segments": [],
+ "tradeSegments": [],
+ "grantsPricing": true
+ },
+ {
+ "title": "CYBER",
+ "type": "DiscountCodeBasic",
+ "status": "ACTIVE",
+ "selection": "DiscountCustomerAll",
+ "segments": [],
+ "tradeSegments": [],
+ "grantsPricing": true
+ },
+ {
+ "title": "5FREE",
+ "type": "DiscountCodeBasic",
+ "status": "EXPIRED",
+ "selection": "DiscountCustomerAll",
+ "segments": [],
+ "tradeSegments": [],
+ "grantsPricing": true
+ },
+ {
+ "title": "thanksgiving2020",
+ "type": "DiscountCodeFreeShipping",
+ "status": "EXPIRED",
+ "selection": "DiscountCustomerAll",
+ "segments": [],
+ "tradeSegments": [],
+ "grantsPricing": false
+ },
+ {
+ "title": "CyberMonday",
+ "type": "DiscountCodeBasic",
+ "status": "EXPIRED",
+ "selection": "DiscountCustomerAll",
+ "segments": [],
+ "tradeSegments": [],
+ "grantsPricing": true
+ },
+ {
+ "title": "Clubhouse",
+ "type": "DiscountCodeBasic",
+ "status": "ACTIVE",
+ "selection": "DiscountCustomerAll",
+ "segments": [],
+ "tradeSegments": [],
+ "grantsPricing": true
+ },
+ {
+ "title": "TRADECODE",
+ "type": "DiscountCodeBasic",
+ "status": "EXPIRED",
+ "selection": "DiscountCustomerSegments",
+ "segments": [
+ "trade"
+ ],
+ "tradeSegments": [
+ "trade"
+ ],
+ "grantsPricing": true
+ },
+ {
+ "title": "SAMPLECODE",
+ "type": "DiscountCodeBasic",
+ "status": "EXPIRED",
+ "selection": "DiscountCustomerSegments",
+ "segments": [
+ "trade"
+ ],
+ "tradeSegments": [
+ "trade"
+ ],
+ "grantsPricing": true
+ },
+ {
+ "title": "Trade Professional - 15% Off",
+ "type": "DiscountCodeBasic",
+ "status": "ACTIVE",
+ "selection": "DiscountCustomerAll",
+ "segments": [],
+ "tradeSegments": [],
+ "grantsPricing": true
+ }
+ ],
+ "automaticDiscounts": [
+ {
+ "title": "15 test discount",
+ "type": "DiscountAutomaticBasic",
+ "status": "EXPIRED"
+ }
+ ],
+ "tradeSegments": [
+ {
+ "name": "trade",
+ "query": "customer_tags CONTAINS 'trade'"
+ }
+ ],
+ "priceRules": {
+ "count": 20,
+ "tagGatedCandidates": [
+ {
+ "title": "SAMPLECODE",
+ "value_type": "percentage",
+ "value": "-100.0",
+ "target_type": "line_item",
+ "prerequisite_customer_ids": 0,
+ "customer_selection": "prerequisite",
+ "saved_search": []
+ },
+ {
+ "title": "TRADECODE",
+ "value_type": "percentage",
+ "value": "-15.0",
+ "target_type": "line_item",
+ "prerequisite_customer_ids": 0,
+ "customer_selection": "prerequisite",
+ "saved_search": []
+ },
+ {
+ "title": "FreeShipping112020",
+ "value_type": "percentage",
+ "value": "-100.0",
+ "target_type": "shipping_line",
+ "prerequisite_customer_ids": 2,
+ "customer_selection": "prerequisite",
+ "saved_search": []
+ }
+ ]
+ },
+ "catalogs": [
+ {
+ "id": "gid://shopify/CompanyLocationCatalog/14161444915",
+ "title": "Carmel Home Collectioon",
+ "status": "ARCHIVED",
+ "companyLocationsCount": {
+ "count": 0
+ }
+ },
+ {
+ "id": "gid://shopify/CompanyLocationCatalog/14444494899",
+ "title": "Et Cie Catalog",
+ "status": "ARCHIVED",
+ "companyLocationsCount": {
+ "count": 0
+ }
+ },
+ {
+ "id": "gid://shopify/CompanyLocationCatalog/18457198643",
+ "title": "Custom",
+ "status": "ARCHIVED",
+ "companyLocationsCount": {
+ "count": 0
+ }
+ }
+ ],
+ "themeMain": "carnegie-color-swatch",
+ "themeTradeTagRefs": [
+ {
+ "key": "layout/theme.liquid",
+ "snippet": "nit);};})(); </script> {% endif %} {%- if template.name == 'page' and page.handle == 'trade-only-benefits' -%} <script> (function(){"
+ },
+ {
+ "key": "sections/contact-for-price.liquid",
+ "snippet": " the roll/unit price, which is not shown online (no fabricated price; in-house & to-the-trade lines). Generalized from koroseal-quote-button.liqu"
+ },
+ {
+ "key": "snippets/dw-samples-banner.liquid",
+ "snippet": "{%- comment -%} DW-SAMPLES-BANNER v1 — lifetime retail allowance + approved-trade entitlement. {%- endcomment -%} <!-- DW-SAMPLES-BANNER"
+ },
+ {
+ "key": "snippets/dw-signin-modal.liquid",
+ "snippet": "routes.account_url }}\">View account & orders</a> {%- if customer.tags contains 'trade_approved' -%} <div class=\"dwsm-section\"><h3>Yo"
+ },
+ {
+ "key": "snippets/dw-trade-apply.liquid",
+ "snippet": "{%- comment -%} DW-TRADE-APPLY v1 — benefits-first trade application and existi"
+ }
+ ]
+ },
+ "tradeGrants": [
+ {
+ "where": "code-discount",
+ "title": "TRADECODE",
+ "type": "DiscountCodeBasic",
+ "grantsPricing": true,
+ "via": [
+ "trade"
+ ]
+ },
+ {
+ "where": "code-discount",
+ "title": "SAMPLECODE",
+ "type": "DiscountCodeBasic",
+ "grantsPricing": true,
+ "via": [
+ "trade"
+ ]
+ }
+ ],
+ "verdict": {
+ "tradeGrantsBeyondSampleShipping": true,
+ "pricingGrantsViaTrade": [
+ {
+ "where": "code-discount",
+ "title": "TRADECODE",
+ "type": "DiscountCodeBasic",
+ "grantsPricing": true,
+ "via": [
+ "trade"
+ ]
+ },
+ {
+ "where": "code-discount",
+ "title": "SAMPLECODE",
+ "type": "DiscountCodeBasic",
+ "grantsPricing": true,
+ "via": [
+ "trade"
+ ]
+ }
+ ],
+ "themeBranchesOnTrade": [
+ "layout/theme.liquid",
+ "sections/contact-for-price.liquid",
+ "snippets/dw-samples-banner.liquid",
+ "snippets/dw-signin-modal.liquid",
+ "snippets/dw-trade-apply.liquid"
+ ],
+ "recommendedVehicle": "sample-freeship",
+ "rationale": "trade is wired to pricing/content beyond sample-shipping — mass-tagging 2,663 grandfathered customers `trade` would leak those perks. MUST use a dedicated `sample-freeship` tag; the DW Trade/Designers segment ORs the real trade tags AND `sample-freeship`.",
+ "vehicleForSegment": "segment query ORs all real trade tags (memo §4) PLUS customer_tags CONTAINS 'sample-freeship'"
+ }
+}
← 03b276d TK-11333: read-only feasibility + full-customer grandfather
·
back to Shopify Sample Shipping
·
TK-11333: authored sample-shipping executors (trade-grant-ch 5cb2341 →