[object Object]

← back to Dw Add Sellable Variant Tk10902

TK-10825: verify + reversible remediation for 1281 $0 quote-only checkout exposure

d326afc306deb9cab8fce543661de93e39f0a016 · 2026-08-27 10:53:46 -0700 · Steve Abrams

Enumerate + live-verify the 1,281 active/published Phillipe Romano quote-only
products with a $0 sellable variant (free-checkout exposure). Adds dry-run-safe
unpublish remediation (Option A) + rollback, verify-before-write guards, ledgered.
Gated memo drafted to pending-approval; no live writes performed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit d326afc306deb9cab8fce543661de93e39f0a016
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Aug 27 10:53:46 2026 -0700

    TK-10825: verify + reversible remediation for 1281 $0 quote-only checkout exposure
    
    Enumerate + live-verify the 1,281 active/published Phillipe Romano quote-only
    products with a $0 sellable variant (free-checkout exposure). Adds dry-run-safe
    unpublish remediation (Option A) + rollback, verify-before-write guards, ledgered.
    Gated memo drafted to pending-approval; no live writes performed.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 tk10825-rollback.mjs  |  34 +++++++++++++++
 tk10825-run-log.jsonl |  40 ++++++++++++++++++
 tk10825-unpublish.mjs | 113 ++++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 187 insertions(+)

diff --git a/tk10825-rollback.mjs b/tk10825-rollback.mjs
new file mode 100644
index 0000000..750d899
--- /dev/null
+++ b/tk10825-rollback.mjs
@@ -0,0 +1,34 @@
+#!/usr/bin/env node
+// TK-10825 ROLLBACK — re-publish every product this ticket unpublished, from the ledger record.
+// Reads tk10825-run-log.jsonl for action=='unpublished' rows and PUTs published=true back.
+// Usage: node tk10825-rollback.mjs [--live]   (default DRY-RUN)
+
+import fs from 'fs';
+import { execSync } from 'child_process';
+
+const DOMAIN = 'designer-laboratory-sandbox.myshopify.com';
+const API = '2024-10';
+const TOKEN = execSync(`grep -E '^SHOPIFY_ADMIN_TOKEN=' ${process.env.HOME}/Projects/secrets-manager/.env | cut -d= -f2-`).toString().trim();
+const LIVE = process.argv.includes('--live');
+const RUNLOG = `${process.cwd()}/tk10825-run-log.jsonl`;
+const base = `https://${DOMAIN}/admin/api/${API}`;
+const hdr = { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' };
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+const rows = fs.readFileSync(RUNLOG, 'utf8').split('\n').filter(Boolean)
+  .map(JSON.parse).filter(r => r.action === 'unpublished');
+console.log(`\n=== ${LIVE ? 'LIVE RE-PUBLISH' : 'DRY-RUN'} — ${rows.length} products to restore ===\n`);
+
+let done = 0;
+for (const r of rows) {
+  if (!LIVE) { console.log(`WOULD RE-PUBLISH ${r.pid} "${r.title}"`); done++; continue; }
+  for (let a = 0; a < 5; a++) {
+    const res = await fetch(`${base}/products/${r.pid}.json`, { method: 'PUT', headers: hdr, body: JSON.stringify({ product: { id: Number(r.pid), published: true } }) });
+    if (res.status === 429) { await sleep(2500); continue; }
+    console.log(`${res.status === 200 ? 'RE-PUBLISHED' : 'FAILED_' + res.status} ${r.pid} "${r.title}"`);
+    if (res.status === 200) done++;
+    break;
+  }
+  await sleep(600);
+}
+console.log(`\n=== DONE: restored=${done}/${rows.length} ===`);
diff --git a/tk10825-run-log.jsonl b/tk10825-run-log.jsonl
new file mode 100644
index 0000000..34a90f8
--- /dev/null
+++ b/tk10825-run-log.jsonl
@@ -0,0 +1,40 @@
+{"ts":"2026-08-27T17:52:34.644Z","mode":"dry","action":"would_unpublish","pid":"7896520196147","title":"Berlin Calvin Aloe Wallcovering | Phillipe Romano","prior_published_at":"2026-07-22T11:27:24-07:00"}
+{"ts":"2026-08-27T17:52:34.875Z","mode":"dry","action":"would_unpublish","pid":"7896520032307","title":"Berlin Calvin Amazon Wallcovering | Phillipe Romano","prior_published_at":"2026-07-22T11:27:12-07:00"}
+{"ts":"2026-08-27T17:52:35.108Z","mode":"dry","action":"would_unpublish","pid":"7896520163379","title":"Berlin Calvin Cattail Wallcovering | Phillipe Romano","prior_published_at":"2026-07-22T11:27:20-07:00"}
+{"ts":"2026-08-27T17:52:35.340Z","mode":"dry","action":"would_unpublish","pid":"7896519934003","title":"Berlin Calvin Celtic Gray Wallcovering | Phillipe Romano","prior_published_at":"2026-07-22T11:27:04-07:00"}
+{"ts":"2026-08-27T17:52:35.553Z","mode":"dry","action":"would_unpublish","pid":"7896520294451","title":"Berlin Calvin Cottage Wallcovering | Phillipe Romano","prior_published_at":"2026-07-22T11:27:33-07:00"}
+{"ts":"2026-08-27T17:52:35.774Z","mode":"dry","action":"would_unpublish","pid":"7896519901235","title":"Berlin Calvin Pebble Wallcovering | Phillipe Romano","prior_published_at":"2026-07-22T11:26:59-07:00"}
+{"ts":"2026-08-27T17:52:35.997Z","mode":"dry","action":"would_unpublish","pid":"7896519966771","title":"Berlin Calvin Tulle Wallcovering | Phillipe Romano","prior_published_at":"2026-07-22T11:27:08-07:00"}
+{"ts":"2026-08-27T17:52:36.201Z","mode":"dry","action":"would_unpublish","pid":"7896519868467","title":"Berlin Calvin Urban Wallcovering | Phillipe Romano","prior_published_at":"2026-07-22T11:26:55-07:00"}
+{"ts":"2026-08-27T17:52:36.484Z","mode":"dry","action":"would_unpublish","pid":"7896520228915","title":"Berlin Calvin Wild Flower Wallcovering | Phillipe Romano","prior_published_at":"2026-07-22T11:27:28-07:00"}
+{"ts":"2026-08-27T17:52:36.715Z","mode":"dry","action":"would_unpublish","pid":"7896520097843","title":"Berlin Calvin Yukon Wallcovering | Phillipe Romano","prior_published_at":"2026-07-22T11:27:16-07:00"}
+{"ts":"2026-08-27T17:52:36.956Z","mode":"dry","action":"would_unpublish","pid":"7896520785971","title":"Berlin Colt Ashen Wallcovering | Phillipe Romano","prior_published_at":"2026-07-22T11:28:10-07:00"}
+{"ts":"2026-08-27T17:52:37.164Z","mode":"dry","action":"would_unpublish","pid":"7896520687667","title":"Berlin Colt Camel Wallcovering | Phillipe Romano","prior_published_at":"2026-07-22T11:27:58-07:00"}
+{"ts":"2026-08-27T17:52:37.401Z","mode":"dry","action":"would_unpublish","pid":"7896520753203","title":"Berlin Colt Copper Wallcovering | Phillipe Romano","prior_published_at":"2026-07-22T11:28:06-07:00"}
+{"ts":"2026-08-27T17:52:37.656Z","mode":"dry","action":"would_unpublish","pid":"7896520523827","title":"Berlin Colt Ecru Wallcovering | Phillipe Romano","prior_published_at":"2026-07-22T11:27:45-07:00"}
+{"ts":"2026-08-27T17:52:37.873Z","mode":"dry","action":"would_unpublish","pid":"7896520654899","title":"Berlin Colt Emerald Wallcovering | Phillipe Romano","prior_published_at":"2026-07-22T11:27:53-07:00"}
+{"ts":"2026-08-27T17:52:38.102Z","mode":"dry","action":"would_unpublish","pid":"7896520327219","title":"Berlin Colt Gilded Saddle Wallcovering | Phillipe Romano","prior_published_at":"2026-07-22T11:27:37-07:00"}
+{"ts":"2026-08-27T17:52:38.336Z","mode":"dry","action":"would_unpublish","pid":"7896520556595","title":"Berlin Colt Icy Blue Wallcovering | Phillipe Romano","prior_published_at":"2026-07-22T11:27:49-07:00"}
+{"ts":"2026-08-27T17:52:38.577Z","mode":"dry","action":"would_unpublish","pid":"7896520851507","title":"Berlin Colt Silver Lilac Wallcovering | Phillipe Romano","prior_published_at":"2026-07-22T11:28:14-07:00"}
+{"ts":"2026-08-27T17:52:38.799Z","mode":"dry","action":"would_unpublish","pid":"7896520720435","title":"Berlin Colt Stable Wallcovering | Phillipe Romano","prior_published_at":"2026-07-22T11:28:02-07:00"}
+{"ts":"2026-08-27T17:52:39.017Z","mode":"dry","action":"would_unpublish","pid":"7896520491059","title":"Berlin Colt Whitewash Wallcovering | Phillipe Romano","prior_published_at":"2026-07-22T11:27:41-07:00"}
+{"ts":"2026-08-27T17:52:39.240Z","mode":"dry","action":"would_unpublish","pid":"7896521277491","title":"Berlin Fossey Canopy Wallcovering | Phillipe Romano","prior_published_at":"2026-07-22T11:28:55-07:00"}
+{"ts":"2026-08-27T17:52:39.481Z","mode":"dry","action":"would_unpublish","pid":"7896520884275","title":"Berlin Fossey Deep Woods Wallcovering | Phillipe Romano","prior_published_at":"2026-07-22T11:28:18-07:00"}
+{"ts":"2026-08-27T17:52:39.728Z","mode":"dry","action":"would_unpublish","pid":"7896521244723","title":"Berlin Fossey Glacier Wallcovering | Phillipe Romano","prior_published_at":"2026-07-22T11:28:51-07:00"}
+{"ts":"2026-08-27T17:52:39.940Z","mode":"dry","action":"would_unpublish","pid":"7896521080883","title":"Berlin Fossey Jade Wallcovering | Phillipe Romano","prior_published_at":"2026-07-22T11:28:38-07:00"}
+{"ts":"2026-08-27T17:52:40.131Z","mode":"dry","action":"would_unpublish","pid":"7896521015347","title":"Berlin Fossey Lagoon Wallcovering | Phillipe Romano","prior_published_at":"2026-07-22T11:28:30-07:00"}
+{"ts":"2026-08-27T17:52:40.366Z","mode":"dry","action":"would_unpublish","pid":"7896521179187","title":"Berlin Fossey Marble Wallcovering | Phillipe Romano","prior_published_at":"2026-07-22T11:28:42-07:00"}
+{"ts":"2026-08-27T17:52:40.582Z","mode":"dry","action":"would_unpublish","pid":"7896521048115","title":"Berlin Fossey Orchid Slate Wallcovering | Phillipe Romano","prior_published_at":"2026-07-22T11:28:34-07:00"}
+{"ts":"2026-08-27T17:52:40.789Z","mode":"dry","action":"would_unpublish","pid":"7896521211955","title":"Berlin Fossey Paper Maché Wallcovering | Phillipe Romano","prior_published_at":"2026-07-22T11:28:47-07:00"}
+{"ts":"2026-08-27T17:52:40.991Z","mode":"dry","action":"would_unpublish","pid":"7896520982579","title":"Berlin Fossey Spearmint Wallcovering | Phillipe Romano","prior_published_at":"2026-07-22T11:28:26-07:00"}
+{"ts":"2026-08-27T17:52:41.197Z","mode":"dry","action":"would_unpublish","pid":"7896520949811","title":"Berlin Fossey Watercolor Wallcovering | Phillipe Romano","prior_published_at":"2026-07-22T11:28:22-07:00"}
+{"ts":"2026-08-27T17:52:41.402Z","mode":"dry","action":"would_unpublish","pid":"7896521375795","title":"Berlin Galapagos Cashmere Wallcovering | Phillipe Romano","prior_published_at":"2026-07-22T11:29:08-07:00"}
+{"ts":"2026-08-27T17:52:41.604Z","mode":"dry","action":"would_unpublish","pid":"7896521441331","title":"Berlin Galapagos Cinderblock Wallcovering | Phillipe Romano","prior_published_at":"2026-07-22T11:29:16-07:00"}
+{"ts":"2026-08-27T17:52:41.861Z","mode":"dry","action":"would_unpublish","pid":"7896521539635","title":"Berlin Galapagos Ecru Wallcovering | Phillipe Romano","prior_published_at":"2026-07-22T11:29:29-07:00"}
+{"ts":"2026-08-27T17:52:42.064Z","mode":"dry","action":"would_unpublish","pid":"7896521572403","title":"Berlin Galapagos Mink Wallcovering | Phillipe Romano","prior_published_at":"2026-07-22T11:29:33-07:00"}
+{"ts":"2026-08-27T17:52:42.274Z","mode":"dry","action":"would_unpublish","pid":"7896521506867","title":"Berlin Galapagos Pearl Wallcovering | Phillipe Romano","prior_published_at":"2026-07-22T11:29:25-07:00"}
+{"ts":"2026-08-27T17:52:42.506Z","mode":"dry","action":"would_unpublish","pid":"7896521474099","title":"Berlin Galapagos Raven Wallcovering | Phillipe Romano","prior_published_at":"2026-07-22T11:29:20-07:00"}
+{"ts":"2026-08-27T17:52:42.709Z","mode":"dry","action":"would_unpublish","pid":"7896521605171","title":"Berlin Galapagos Saffron Wallcovering | Phillipe Romano","prior_published_at":"2026-07-22T11:29:38-07:00"}
+{"ts":"2026-08-27T17:52:42.939Z","mode":"dry","action":"would_unpublish","pid":"7896521408563","title":"Berlin Galapagos Smoke Wallcovering | Phillipe Romano","prior_published_at":"2026-07-22T11:29:12-07:00"}
+{"ts":"2026-08-27T17:52:43.170Z","mode":"dry","action":"would_unpublish","pid":"7896521343027","title":"Berlin Galapagos Snow Cap Wallcovering | Phillipe Romano","prior_published_at":"2026-07-22T11:29:03-07:00"}
+{"ts":"2026-08-27T17:52:43.397Z","mode":"dry","action":"would_unpublish","pid":"7896521310259","title":"Berlin Galapagos Wineberry Wallcovering | Phillipe Romano","prior_published_at":"2026-07-22T11:28:59-07:00"}
diff --git a/tk10825-unpublish.mjs b/tk10825-unpublish.mjs
new file mode 100644
index 0000000..ca8fd60
--- /dev/null
+++ b/tk10825-unpublish.mjs
@@ -0,0 +1,113 @@
+#!/usr/bin/env node
+// TK-10825 — Close the $0-free-checkout exposure on 1,281 quote-only Phillipe Romano products.
+// PRIMARY REMEDIATION = OPTION A: unpublish from Online Store sales channel (published_at=null).
+//   Product stays ACTIVE in admin; no longer orderable on storefront; fully reversible (re-publish).
+// GATED — customer-facing Shopify write. DEFAULT = DRY-RUN. --live performs writes. Steve-gated.
+// Reversible + ledgered. Verify-before-write. NO EMAIL. $0 (Shopify Admin API).
+//
+// For each product:
+//   1. Re-read LIVE. HARD-STOP (skip+log) unless it STILL matches the verified defect signature:
+//        - status == 'active'
+//        - published_at set (currently published)
+//        - has a variant priced exactly 0.00
+//        - carries the 'quote-only' tag
+//        - vendor == 'Phillipe Romano'
+//      (belt-and-suspenders so a since-fixed product is never touched)
+//   2. Record published_at (the exact restore value) BEFORE the write.
+//   3. PUT product published=false (unpublish from Online Store).
+//   4. Ledger product_id + prior published_at + exact re-publish undo command.
+//
+// Usage: node tk10825-unpublish.mjs <ids.csv> [--limit N] [--live]
+//   ids.csv = one product_id per line (first CSV field), as produced by the ticket export.
+
+import fs from 'fs';
+import { execSync } from 'child_process';
+
+const DOMAIN = 'designer-laboratory-sandbox.myshopify.com';
+const API = '2024-10';
+const TOKEN = execSync(`grep -E '^SHOPIFY_ADMIN_TOKEN=' ${process.env.HOME}/Projects/secrets-manager/.env | cut -d= -f2-`).toString().trim();
+if (!TOKEN) { console.error('NO SHOPIFY TOKEN'); process.exit(1); }
+
+const LEDGER = `${process.env.HOME}/.claude/yolo-queue/executed-reversible/ledger.jsonl`;
+const RUNLOG = `${process.cwd()}/tk10825-run-log.jsonl`;
+
+const args = process.argv.slice(2);
+const file = args[0];
+const LIVE = args.includes('--live');
+const limIdx = args.indexOf('--limit');
+const LIMIT = limIdx >= 0 ? parseInt(args[limIdx + 1], 10) : Infinity;
+
+const base = `https://${DOMAIN}/admin/api/${API}`;
+const hdr = { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' };
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+async function shopify(path, opts = {}) {
+  for (let attempt = 0; attempt < 5; attempt++) {
+    const res = await fetch(`${base}${path}`, { headers: hdr, ...opts });
+    if (res.status === 429) { await sleep(2500); continue; }
+    const body = await res.text();
+    let json; try { json = JSON.parse(body); } catch { json = { _raw: body }; }
+    return { status: res.status, json };
+  }
+  return { status: 429, json: { error: 'rate-limited' } };
+}
+
+function appendJsonl(path, obj) {
+  fs.mkdirSync(path.substring(0, path.lastIndexOf('/')), { recursive: true });
+  fs.appendFileSync(path, JSON.stringify(obj) + '\n');
+}
+
+const ids = fs.readFileSync(file, 'utf8').split('\n')
+  .map(l => l.trim().split(',')[0]).filter(Boolean);
+const batch = ids.slice(0, LIMIT);
+
+let done = 0, skipped = 0;
+const skips = [];
+console.log(`\n=== ${LIVE ? 'LIVE UNPUBLISH' : 'DRY-RUN'} — ${batch.length} products (of ${ids.length}) ===\n`);
+
+for (const pid of batch) {
+  const { status, json } = await shopify(`/products/${pid}.json?fields=id,title,status,published_at,vendor,tags,variants`);
+  if (status !== 200 || !json.product) {
+    skipped++; skips.push({ pid, reason: `fetch_failed_${status}` });
+    console.log(`SKIP ${pid} fetch_failed_${status}`); continue;
+  }
+  const p = json.product;
+  const tags = (p.tags || '').toLowerCase();
+  const hasZero = (p.variants || []).some(v => parseFloat(v.price) === 0);
+  // verify-before-write: STILL the defect?
+  if (p.status !== 'active') { skipped++; skips.push({ pid, reason: `not_active(${p.status})`, title: p.title }); console.log(`SKIP ${pid} not_active`); continue; }
+  if (!p.published_at)       { skipped++; skips.push({ pid, reason: 'already_unpublished', title: p.title }); console.log(`SKIP ${pid} already_unpublished`); continue; }
+  if (!hasZero)              { skipped++; skips.push({ pid, reason: 'no_zero_variant', title: p.title }); console.log(`SKIP ${pid} no_zero_variant`); continue; }
+  if (!tags.includes('quote-only')) { skipped++; skips.push({ pid, reason: 'not_quote_only', title: p.title }); console.log(`SKIP ${pid} not_quote_only`); continue; }
+  if ((p.vendor || '') !== 'Phillipe Romano') { skipped++; skips.push({ pid, reason: `vendor=${p.vendor}`, title: p.title }); console.log(`SKIP ${pid} vendor=${p.vendor}`); continue; }
+
+  const priorPublishedAt = p.published_at;
+
+  if (!LIVE) {
+    console.log(`WOULD UNPUBLISH ${pid} "${p.title}" (was published_at=${priorPublishedAt})`);
+    done++;
+    appendJsonl(RUNLOG, { ts: new Date().toISOString(), mode: 'dry', action: 'would_unpublish', pid, title: p.title, prior_published_at: priorPublishedAt });
+    continue;
+  }
+
+  const upd = await shopify(`/products/${pid}.json`, { method: 'PUT', body: JSON.stringify({ product: { id: Number(pid), published: false } }) });
+  if (upd.status !== 200) {
+    skipped++; skips.push({ pid, reason: `update_failed_${upd.status} ${JSON.stringify(upd.json).slice(0,160)}`, title: p.title });
+    console.log(`SKIP ${pid} update_failed_${upd.status}`); continue;
+  }
+  done++;
+  const undo = `curl -s -X PUT "https://${DOMAIN}/admin/api/${API}/products/${pid}.json" -H "X-Shopify-Access-Token: $SHOPIFY_ADMIN_TOKEN" -H "Content-Type: application/json" -d '{"product":{"id":${pid},"published":true}}'`;
+  appendJsonl(LEDGER, {
+    ts: new Date().toISOString(), agent: 'yolofx-TK-10825', ticket: 'TK-10825',
+    action: `unpublished $0 quote-only product ${pid} "${p.title}" (Phillipe Romano) from Online Store`,
+    product_id: pid, prior_published_at: priorPublishedAt,
+    blast_radius: 1, undo_cmd: undo,
+    verify: `curl -s "https://${DOMAIN}/admin/api/${API}/products/${pid}.json?fields=id,status,published_at" -H "X-Shopify-Access-Token: $SHOPIFY_ADMIN_TOKEN"`
+  });
+  appendJsonl(RUNLOG, { ts: new Date().toISOString(), mode: 'live', action: 'unpublished', pid, title: p.title, prior_published_at: priorPublishedAt });
+  console.log(`UNPUBLISHED ${pid} "${p.title}"`);
+  await sleep(600);
+}
+
+fs.writeFileSync(`${process.cwd()}/tk10825-skips.json`, JSON.stringify(skips, null, 1));
+console.log(`\n=== DONE: done=${done} skipped=${skipped} (skips -> tk10825-skips.json) ===`);

← 357ec73 auto-data-snapshot: 2026-08-27T10:47:06 (3 data files) — car  ·  back to Dw Add Sellable Variant Tk10902  ·  auto-data-snapshot: 2026-08-27T11:24:00 (1 data files) — tk1 009f09c →