[object Object]

← back to Designer Wallcoverings

Final: Product pages match Thibaut reference design - clean 2-column layout with image left, details right, proper styling

d17a76b4cb7dc582f1170e14a5366024988d5957 · 2026-06-22 13:36:00 -0700 · Steve Abrams

Files touched

Diff

commit d17a76b4cb7dc582f1170e14a5366024988d5957
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Jun 22 13:36:00 2026 -0700

    Final: Product pages match Thibaut reference design - clean 2-column layout with image left, details right, proper styling
---
 shopify/scripts/moq/scan-tierB-packaging.py | 98 +++++++++++++++++++++++++++++
 1 file changed, 98 insertions(+)

diff --git a/shopify/scripts/moq/scan-tierB-packaging.py b/shopify/scripts/moq/scan-tierB-packaging.py
new file mode 100644
index 00000000..4b10fefb
--- /dev/null
+++ b/shopify/scripts/moq/scan-tierB-packaging.py
@@ -0,0 +1,98 @@
+#!/usr/bin/env python3
+"""
+Tier B packaging scan (READ-ONLY).
+
+For each roll-goods vendor that has products with NO min/step set, sample up to
+SAMPLE_N of those products and gather the distribution of the EXACT global
+metafield keys that dictate the correct order minimum:
+  - global.packaged          (e.g. "Double Roll", "Single Roll", "Sold Per Yard")
+  - global.unit_of_measure   (e.g. "Priced Per Single Roll", "Sold Per Yard")
+
+We query the exact keys per product (NOT metafields(first:N), which truncates +
+collapses namespaces). Output -> tierB-packaging-scan.json for the proposal builder.
+
+No writes. Token from Designer-Wallcoverings/.env SHOPIFY_ADMIN_TOKEN.
+
+Usage: python3 scan-tierB-packaging.py [SAMPLE_N]   (default 40)
+"""
+import json,urllib.request,os,sys,time
+from collections import Counter
+
+HERE=os.path.dirname(os.path.abspath(__file__))
+REPO=os.path.abspath(os.path.join(HERE,"..","..",".."))
+TOKEN=[l.strip().split("=",1)[1] for l in open(os.path.join(REPO,".env")) if l.startswith("SHOPIFY_ADMIN_TOKEN=")][0]
+API="https://designer-laboratory-sandbox.myshopify.com/admin/api/2024-01/graphql.json"
+SAMPLE_N=int(sys.argv[1]) if len(sys.argv)>1 else 40
+
+def gql(q,v=None,retries=4):
+    body={"query":q}
+    if v is not None: body["variables"]=v
+    for a in range(retries):
+        try:
+            req=urllib.request.Request(API,data=json.dumps(body).encode(),
+                headers={"X-Shopify-Access-Token":TOKEN,"Content-Type":"application/json"})
+            r=json.load(urllib.request.urlopen(req))
+            if r.get("errors") and any("Throttled" in str(e) for e in r["errors"]):
+                time.sleep(2*(a+1)); continue
+            return r
+        except Exception as e:
+            time.sleep(2*(a+1))
+    return {"errors":[{"message":"gave up"}]}
+
+# Roll-style vendors with missing min/step from the storewide audit
+audit=json.load(open(os.path.join(HERE,"storewide-moq-audit.json")))
+flagged=[f for f in audit["flagged"] if f["min_or_step_missing"]>0]
+flagged.sort(key=lambda x:x["min_or_step_missing"],reverse=True)
+
+# Query: page roll products for a vendor, pulling exact metafields. Only keep
+# products whose min OR step is currently unset (the Tier B population).
+PQ='''query($q:String!,$cursor:String){
+ products(first:100,query:$q,after:$cursor){
+  pageInfo{hasNextPage endCursor}
+  edges{node{ id status
+   pk:metafield(namespace:"global",key:"packaged"){value}
+   uom:metafield(namespace:"global",key:"unit_of_measure"){value}
+   mn:metafield(namespace:"global",key:"v_prods_quantity_order_min"){value}
+   un:metafield(namespace:"global",key:"v_prods_quantity_order_units"){value}
+  }}
+ }}'''
+
+def mval(x):
+    v=(x or {}).get("value") if x else None
+    return v if (v is not None and str(v).strip()!="") else None
+
+out=[]
+for f in flagged:
+    vendor=f["vendor"]
+    q=f'vendor:"{vendor}" status:active'
+    cursor=None; sampled=[]; seen=0
+    while True:
+        r=gql(PQ,{"q":q,"cursor":cursor})
+        data=r.get("data",{}).get("products")
+        if not data:
+            break
+        for e in data["edges"]:
+            n=e["node"]
+            mn=mval(n["mn"]); un=mval(n["un"])
+            # Tier B population = min OR step missing
+            if mn is None or un is None:
+                sampled.append({"packaged":mval(n["pk"]),"uom":mval(n["uom"]),"min":mn,"step":un})
+            seen+=1
+            if len(sampled)>=SAMPLE_N: break
+        if len(sampled)>=SAMPLE_N or not data["pageInfo"]["hasNextPage"]:
+            break
+        cursor=data["pageInfo"]["endCursor"]
+    pk_dist=Counter((s["packaged"] or "∅(none)") for s in sampled)
+    uom_dist=Counter((s["uom"] or "∅(none)") for s in sampled)
+    out.append({
+        "vendor":vendor,
+        "missing_total":f["min_or_step_missing"],
+        "roll_products":f["roll_products"],
+        "sampled":len(sampled),
+        "packaged_dist":dict(pk_dist.most_common()),
+        "uom_dist":dict(uom_dist.most_common()),
+    })
+    print(f"{f['min_or_step_missing']:5}  {vendor:32} sampled={len(sampled):3}  packaged={dict(pk_dist.most_common(3))}")
+
+json.dump(out,open(os.path.join(HERE,"tierB-packaging-scan.json"),"w"),indent=1)
+print("\nWrote tierB-packaging-scan.json  (%d vendors)"%len(out))

← f56ecd5e Tier A1 MOQ fix: DTD-B guard — min=max(existing,2), step=2 a  ·  back to Designer Wallcoverings  ·  auto-save: 2026-06-22T13:36:34 (1 files) — shopify/scripts/m 2d1bdd4c →