← back to Roll On Fabric Fix

fix.py

60 lines

#!/usr/bin/env python3
"""
Roll-on-fabric fix.  PREP / DRY-RUN BY DEFAULT.

45 Mulberry fabrics carry a stray legacy metafield  global.unit_of_measure = "Full Roll"
alongside the correct  global.Unit of measure = "YARD".  This DELETES the stray legacy key
(the canonical YARD stays), which also de-noises the roll-on-fabric canary.

Each target was pre-verified: legacy key present AND canonical YARD present (data/worklist.json).

SAFE BY CONSTRUCTION:
  * DRY-RUN default. Prints the plan, ZERO Shopify writes.
  * Live requires BOTH:  --apply  AND  env ROLLFIX_CONFIRM=YES
  * --canary N limits to first N.  ~0.4s paced.
  * Fully reversible: reverse.py re-creates the exact legacy metafield from the worklist.
  * The REAL root-cause fix (mirror sync replace-not-merge to prune deleted keys) is separate
    and code-side; this only removes the live stray keys.

Usage:
  python3 fix.py                                    # DRY-RUN all 45 (no writes)
  python3 fix.py --canary 5                         # DRY-RUN first 5
  ROLLFIX_CONFIRM=YES python3 fix.py --apply --canary 5   # LIVE canary (Steve-gated)
"""
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():
    apply = "--apply" in sys.argv
    live = apply and os.environ.get("ROLLFIX_CONFIRM") == "YES"
    if apply and not live:
        print("REFUSING --apply without ROLLFIX_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]
    print(f"=== roll-on-fabric fix [{'LIVE' if live else 'DRY-RUN'}] — delete legacy "
          f"global.unit_of_measure on {len(work)} Mulberry fabrics (reversible) ===\n")
    tok = token() if live else None
    done = err = 0
    for i, w in enumerate(work, 1):
        line = f"[{i}/{len(work)}] product {w['num']} mf_id={w['mf_id']} (was '{w['legacy_value']}', canon={w['canon']})"
        if not live:
            print("WOULD DELETE:", line); continue
        try:
            req = urllib.request.Request(f"https://{SHOP}/admin/api/{API}/metafields/{w['mf_id']}.json",
                method="DELETE", headers={"X-Shopify-Access-Token": tok})
            urllib.request.urlopen(req, timeout=40); done += 1; print("DELETED:", line)
        except urllib.error.HTTPError as e:
            err += 1; print(f"FAIL {line} -> HTTP {e.code}")
        time.sleep(0.4)
    print(f"\n{'deleted=%d failed=%d. Reverse with reverse.py' % (done, err) if live else 'DRY-RUN complete. Nothing written.'}")

if __name__ == "__main__":
    main()