[object Object]

← back to Discontinued Fix

Discontinued-22 fix prep — dry-run draft tooling + restore-map (no live writes)

fa16e59f0a15ce521e82e2fc2ebf31003e8c47d6 · 2026-08-01 22:06:26 -0700 · Steve Abrams

Files touched

Diff

commit fa16e59f0a15ce521e82e2fc2ebf31003e8c47d6
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sat Aug 1 22:06:26 2026 -0700

    Discontinued-22 fix prep — dry-run draft tooling + restore-map (no live writes)
---
 .gitignore         |  6 +++++
 README.md          | 23 +++++++++++++++++++
 data/worklist.json |  1 +
 fix.py             | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 reverse.py         | 24 ++++++++++++++++++++
 5 files changed, 119 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6ed96eb
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+node_modules/
+.env*
+tmp/
+*.log
+.DS_Store
+data/restore-map.json
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..a341d87
--- /dev/null
+++ b/README.md
@@ -0,0 +1,23 @@
+# Discontinued-but-sellable fix (PREP — nothing live)
+
+23 products are tagged `Discontinued-2026-04` yet still ACTIVE with a purchasable roll variant
+(`inventory_policy=continue` → a customer can order the unfulfillable). Built 2026-08-01.
+
+## Work-list (`data/worklist.json`) — all 23 live-verified ACTIVE (23/23)
+- Jeffrey Stevens 11, Ronald Redding 7, LA Walls 4, Thibaut 1 (a `-SAMPLE` edge, lower risk).
+
+## Fix
+- `fix.py` — sets each product `status -> draft` (default) or `archived` (`--archive`). DRAFT hides it
+  from the storefront (no purchase possible), keeps the product + sample variant intact, instantly
+  reversible. Writes `data/restore-map.json` (num → prev_status=active) BEFORE any write.
+- `reverse.py` — restores the batch to active from the restore-map (one-command undo).
+
+## Safety
+- DRY-RUN is default. LIVE requires BOTH `--apply` AND `DISC_FIX_CONFIRM=YES`.
+- `--canary N` limits to first N. ~0.4s paced.
+
+## Gated — go-live when Steve approves
+Recommended: `DISC_FIX_CONFIRM=YES python3 fix.py --apply --canary 5` → verify 5 are off the
+storefront → widen to all 23. Undo anytime: `DISC_FIX_CONFIRM=YES python3 reverse.py --apply`.
+Root-cause follow-up (separate): make the discontinue flow (discontinued-agent) draft-on-discontinue.
+Memo: `~/.claude/yolo-queue/pending-approval/2026-08-01-discontinued-but-still-sellable.md`.
diff --git a/data/worklist.json b/data/worklist.json
new file mode 100644
index 0000000..0ad19d5
--- /dev/null
+++ b/data/worklist.json
@@ -0,0 +1 @@
+[{"num":"1498526220400","vendor":"Jeffrey Stevens","dw_sku":"GGA-82653","mirror_status":"ACTIVE","has_sample_variant":true}, {"num":"1498528546928","vendor":"Jeffrey Stevens","dw_sku":"GGA-82664","mirror_status":"ACTIVE","has_sample_variant":true}, {"num":"1498529529968","vendor":"Jeffrey Stevens","dw_sku":"GGA-82668","mirror_status":"ACTIVE","has_sample_variant":true}, {"num":"1498529923184","vendor":"Jeffrey Stevens","dw_sku":"GGA-82670","mirror_status":"ACTIVE","has_sample_variant":true}, {"num":"1498530840688","vendor":"Jeffrey Stevens","dw_sku":"GGA-82674","mirror_status":"ACTIVE","has_sample_variant":true}, {"num":"1498041483376","vendor":"Jeffrey Stevens","dw_sku":"TRF-56847","mirror_status":"ACTIVE","has_sample_variant":true}, {"num":"1498044137584","vendor":"Jeffrey Stevens","dw_sku":"TRF-56861","mirror_status":"ACTIVE","has_sample_variant":true}, {"num":"1498044596336","vendor":"Jeffrey Stevens","dw_sku":"TRF-56863","mirror_status":"ACTIVE","has_sample_variant":true}, {"num":"1498045350000","vendor":"Jeffrey Stevens","dw_sku":"TRF-56867","mirror_status":"ACTIVE","has_sample_variant":true}, {"num":"1498046890096","vendor":"Jeffrey Stevens","dw_sku":"TRF-56875","mirror_status":"ACTIVE","has_sample_variant":true}, {"num":"1498047414384","vendor":"Jeffrey Stevens","dw_sku":"TRF-56878","mirror_status":"ACTIVE","has_sample_variant":true}, {"num":"1498563674224","vendor":"LA Walls","dw_sku":"CCA-82817","mirror_status":"ACTIVE","has_sample_variant":true}, {"num":"1498573242480","vendor":"LA Walls","dw_sku":"CCA-82877","mirror_status":"ACTIVE","has_sample_variant":true}, {"num":"1498617282672","vendor":"LA Walls","dw_sku":"CCA-83049","mirror_status":"ACTIVE","has_sample_variant":true}, {"num":"1498676330608","vendor":"LA Walls","dw_sku":"CCA-83282","mirror_status":"ACTIVE","has_sample_variant":true}, {"num":"6716551331891","vendor":"Ronald Redding","dw_sku":"DWRR-50007","mirror_status":"ACTIVE","has_sample_variant":true}, {"num":"6716551430195","vendor":"Ronald Redding","dw_sku":"DWRR-50010","mirror_status":"ACTIVE","has_sample_variant":true}, {"num":"6716552609843","vendor":"Ronald Redding","dw_sku":"DWRR-50037","mirror_status":"ACTIVE","has_sample_variant":true}, {"num":"6716552970291","vendor":"Ronald Redding","dw_sku":"DWRR-50044","mirror_status":"ACTIVE","has_sample_variant":true}, {"num":"6716553068595","vendor":"Ronald Redding","dw_sku":"DWRR-50047","mirror_status":"ACTIVE","has_sample_variant":true}, {"num":"6716553199667","vendor":"Ronald Redding","dw_sku":"DWRR-50049","mirror_status":"ACTIVE","has_sample_variant":true}, {"num":"1497026986096","vendor":"Ronald Redding","dw_sku":"RDM-66490","mirror_status":"ACTIVE","has_sample_variant":true}, {"num":"7552706543667","vendor":"Thibaut","dw_sku":"DWTT-80529-SAMPLE","mirror_status":"ACTIVE","has_sample_variant":true}]
diff --git a/fix.py b/fix.py
new file mode 100644
index 0000000..875bbb0
--- /dev/null
+++ b/fix.py
@@ -0,0 +1,65 @@
+#!/usr/bin/env python3
+"""
+Discontinued-but-still-sellable fix.  PREP / DRY-RUN BY DEFAULT.
+
+23 products are tagged Discontinued-2026-04 yet still ACTIVE with a purchasable roll variant
+(a customer can order the unfulfillable). This takes them OFF the storefront by setting
+status -> draft (default) or archived (--archive). Fully reversible via reverse.py.
+
+SAFE BY CONSTRUCTION:
+  * DRY-RUN default. Prints the plan, writes the restore-map, ZERO Shopify writes.
+  * Live requires BOTH:  --apply  AND  env DISC_FIX_CONFIRM=YES
+  * --canary N limits to first N.  ~0.4s paced.  Restore-map written before any write.
+  * DRAFT (default) is the safest: instantly reversible (draft->active), keeps the product
+    + its sample variant intact, just hidden from the storefront. --archive is also reversible.
+
+Usage:
+  python3 fix.py                                  # DRY-RUN, all 23 (no writes)
+  python3 fix.py --canary 5                       # DRY-RUN first 5
+  DISC_FIX_CONFIRM=YES python3 fix.py --apply --canary 5   # LIVE canary (Steve-gated)
+
+Owner: vp-dw-commerce. Memo: ~/.claude/yolo-queue/pending-approval/2026-08-01-discontinued-but-still-sellable.md
+"""
+import os, sys, json, time, urllib.request, urllib.error
+HERE = os.path.dirname(os.path.abspath(__file__))
+SHOP = "designer-laboratory-sandbox.myshopify.com"; API = "2024-10"
+
+def token():
+    for l in open(os.path.expanduser("~/Projects/secrets-manager/.env")):
+        if l.startswith("SHOPIFY_ADMIN_TOKEN="): return l.split("=", 1)[1].strip()
+    sys.exit("no SHOPIFY_ADMIN_TOKEN")
+
+def main():
+    target = "archived" if "--archive" in sys.argv else "draft"
+    apply = "--apply" in sys.argv
+    confirmed = os.environ.get("DISC_FIX_CONFIRM") == "YES"
+    live = apply and confirmed
+    if apply and not confirmed:
+        print("REFUSING --apply without DISC_FIX_CONFIRM=YES. DRY-RUN instead.\n")
+    canary = int(sys.argv[sys.argv.index("--canary") + 1]) if "--canary" in sys.argv else None
+
+    work = json.load(open(os.path.join(HERE, "data", "worklist.json")))
+    if canary: work = work[:canary]
+    json.dump([{"num": w["num"], "prev_status": "active"} for w in work],
+              open(os.path.join(HERE, "data", "restore-map.json"), "w"), indent=0)
+
+    print(f"=== discontinued fix [{'LIVE' if live else 'DRY-RUN'}] -> {target.upper()} "
+          f"— {len(work)} products (restore-map written) ===\n")
+    tok = token() if live else None
+    done = err = 0
+    for i, w in enumerate(work, 1):
+        line = f"[{i}/{len(work)}] {w['num']} {w['vendor']} {w['dw_sku']}"
+        if not live:
+            print(f"WOULD: set -> {target} |", line); continue
+        try:
+            req = urllib.request.Request(f"https://{SHOP}/admin/api/{API}/products/{w['num']}.json",
+                data=json.dumps({"product": {"id": int(w["num"]), "status": target}}).encode(),
+                method="PUT", headers={"X-Shopify-Access-Token": tok, "Content-Type": "application/json"})
+            urllib.request.urlopen(req, timeout=40); done += 1; print(f"OK   -> {target} |", line)
+        except urllib.error.HTTPError as e:
+            err += 1; print(f"FAIL {line} -> HTTP {e.code}")
+        time.sleep(0.4)
+    print(f"\n{'applied=%d failed=%d. Reverse with reverse.py' % (done, err) if live else 'DRY-RUN complete. Nothing written.'}")
+
+if __name__ == "__main__":
+    main()
diff --git a/reverse.py b/reverse.py
new file mode 100644
index 0000000..d798e17
--- /dev/null
+++ b/reverse.py
@@ -0,0 +1,24 @@
+#!/usr/bin/env python3
+"""Reverse the discontinued fix: restore each product in data/restore-map.json to active.
+DRY-RUN default; live requires --apply + DISC_FIX_CONFIRM=YES."""
+import os, sys, json, time, urllib.request, urllib.error
+HERE = os.path.dirname(os.path.abspath(__file__)); SHOP="designer-laboratory-sandbox.myshopify.com"; API="2024-10"
+def token():
+    for l in open(os.path.expanduser("~/Projects/secrets-manager/.env")):
+        if l.startswith("SHOPIFY_ADMIN_TOKEN="): return l.split("=",1)[1].strip()
+    sys.exit("no token")
+def main():
+    live = "--apply" in sys.argv and os.environ.get("DISC_FIX_CONFIRM")=="YES"
+    rows = json.load(open(os.path.join(HERE,"data","restore-map.json")))
+    print(f"=== reverse [{'LIVE' if live else 'DRY-RUN'}] — {len(rows)} -> active ===")
+    tok = token() if live else None
+    for r in rows:
+        if not live: print(f"WOULD: {r['num']} -> {r['prev_status']}"); continue
+        try:
+            req=urllib.request.Request(f"https://{SHOP}/admin/api/{API}/products/{r['num']}.json",
+                data=json.dumps({"product":{"id":int(r['num']),"status":r['prev_status']}}).encode(),
+                method="PUT",headers={"X-Shopify-Access-Token":tok,"Content-Type":"application/json"})
+            urllib.request.urlopen(req,timeout=40); print(f"OK: {r['num']} -> {r['prev_status']}")
+        except urllib.error.HTTPError as e: print(f"FAIL {r['num']} HTTP {e.code}")
+        time.sleep(0.4)
+if __name__=="__main__": main()

(oldest)  ·  back to Discontinued Fix  ·  (newest)