← 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()