← back to Discontinued Fix
fix.py
66 lines
#!/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()