← back to Dw Five Field Step0
scripts/set-lec-min5.py
85 lines
#!/usr/bin/env python3
"""Set 5-yard minimum on all ACTIVE LEC- (Le Embossed Croc) products.
Steve 2026-07-02: "update all lec- patterns to 5 yard minimum".
Root cause found on LEC-6000: min=5 was set on the LEGACY no-s product key
(global.v_prod_quantity_order_min) which the live theme IGNORES, and variant
metafields were absent entirely — so the storefront never enforced the minimum.
Per the update-product-min skill (verified against theme 2026-06-30), BOTH
levels must be set:
product : global.v_prods_quantity_order_min = 5 (with-s — the theme key)
global.v_prods_quantity_order_units = 1 (only if missing)
variant : non-Sample -> custom.v_prod_quantity_order_min = 5
custom.v_prods_quantity_order_units = 1
Sample -> 1 / 1 (skill hard rule)
Idempotent (upsert: PUT existing metafield id, else POST). REST rate-limited
at ~2/s via 0.55s sleeps. Audit JSON written next to this script's out/.
"""
import json, subprocess, time, urllib.request
TOKEN = os.environ.get('SHOPIFY_ADMIN_TOKEN','')
API = "https://designer-laboratory-sandbox.myshopify.com/admin/api/2024-01"
OUT = "/Users/macstudio3/Projects/dw-five-field-step0/out/lec-min5-result.json"
MIN_YD, STEP = "5", "1"
def req(method, path, body=None):
r = urllib.request.Request(API + path, method=method,
headers={"X-Shopify-Access-Token": TOKEN, "Content-Type": "application/json"},
data=json.dumps(body).encode() if body else None)
time.sleep(0.55)
with urllib.request.urlopen(r, timeout=30) as resp:
return json.loads(resp.read())
def upsert(owner_path, namespace, key, value, existing):
cur = next((m for m in existing if m["namespace"] == namespace and m["key"] == key), None)
if cur:
if str(cur["value"]) == value:
return "ok"
req("PUT", f"/metafields/{cur['id']}.json",
{"metafield": {"id": cur["id"], "value": value, "type": "single_line_text_field"}})
return f"updated {cur['value']}->{value}"
req("POST", owner_path + "/metafields.json",
{"metafield": {"namespace": namespace, "key": key, "value": value,
"type": "single_line_text_field"}})
return f"created ={value}"
ids = subprocess.run(["psql", "-d", "dw_unified", "-tAc",
"select replace(shopify_id,'gid://shopify/Product/','') from shopify_products "
"where (sku ilike 'LEC%' or handle ilike 'lec-%') and status='ACTIVE' order by sku"],
capture_output=True, text=True).stdout.split()
print(f"{len(ids)} active LEC products")
audit = []
for i, pid in enumerate(ids, 1):
try:
p = req("GET", f"/products/{pid}.json")["product"]
pmf = req("GET", f"/products/{pid}/metafields.json?limit=250")["metafields"]
entry = {"product_id": pid, "title": p["title"], "actions": {}}
entry["actions"]["product.min"] = upsert(f"/products/{pid}", "global",
"v_prods_quantity_order_min", MIN_YD, pmf)
# units: only fill if missing — don't clobber an existing step
if not any(m["namespace"] == "global" and m["key"] == "v_prods_quantity_order_units" for m in pmf):
entry["actions"]["product.units"] = upsert(f"/products/{pid}", "global",
"v_prods_quantity_order_units", STEP, pmf)
for v in p["variants"]:
vmf = req("GET", f"/variants/{v['id']}/metafields.json?limit=250")["metafields"]
is_sample = "sample" in (v["title"] or "").lower() or (v["sku"] or "").lower().endswith("-sample")
mn, st = ("1", "1") if is_sample else (MIN_YD, STEP)
tag = "sample" if is_sample else "yard"
entry["actions"][f"variant.{v['sku']}.min({tag})"] = upsert(
f"/variants/{v['id']}", "custom", "v_prod_quantity_order_min", mn, vmf)
entry["actions"][f"variant.{v['sku']}.units({tag})"] = upsert(
f"/variants/{v['id']}", "custom", "v_prods_quantity_order_units", st, vmf)
entry["status"] = "done"
except Exception as e:
entry = {"product_id": pid, "status": "error", "error": str(e)[:200]}
audit.append(entry)
print(f"[{i}/{len(ids)}] {pid} {entry.get('title','')[:50]} -> {entry['status']}")
with open(OUT, "w") as f:
json.dump(audit, f, indent=1)
done = sum(1 for a in audit if a["status"] == "done")
print(f"\nDONE {done}/{len(audit)} — audit: {OUT}")