[object Object]

← back to Eur Recrawl

REMEDIATION: restore_samples.py — the 5 default-Title stragglers lost their Sample when the Roll variant replaced the sole default variant; restore Sample @ original price; also fixed write_reonboard option-name logic

86d80b8e1ddbcc7cd59fd7b61312b2c485aed52f · 2026-07-30 20:17:23 -0700 · Steve

Files touched

Diff

commit 86d80b8e1ddbcc7cd59fd7b61312b2c485aed52f
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Jul 30 20:17:23 2026 -0700

    REMEDIATION: restore_samples.py — the 5 default-Title stragglers lost their Sample when the Roll variant replaced the sole default variant; restore Sample @ original price; also fixed write_reonboard option-name logic
---
 restore_samples.py | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++
 write-results.json | 76 +++---------------------------------------------------
 2 files changed, 76 insertions(+), 73 deletions(-)

diff --git a/restore_samples.py b/restore_samples.py
new file mode 100644
index 0000000..e578b05
--- /dev/null
+++ b/restore_samples.py
@@ -0,0 +1,73 @@
+#!/usr/bin/env python3
+"""
+REMEDIATION — restore the Sample variant on the 5 stragglers whose sample was
+dropped when a Roll variant was created on their "Title:[Default Title]" option
+(Shopify replaced the sole default variant instead of adding alongside it).
+
+Adds back "Title: Sample" @ the original memo price, tracked=false, so each
+product again has both a Sample and a Roll. GATED (DRY_RUN default).
+"""
+import os, json, time, urllib.request
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+DRY_RUN = os.environ.get("DRY_RUN", "1") != "0"
+
+# sku -> original sample price (from pre-fix diagnosis)
+SAMPLES = {
+    "EUR-70023": "3.50", "EUR-70022": "3.50", "EUR-90168": "3.50",
+    "EUR-70237": "4.25", "EUR-80735": "4.25",
+}
+
+def _tok():
+    for l in open(os.path.expanduser("~/Projects/secrets-manager/.env")):
+        if l.startswith("SHOPIFY_ADMIN_TOKEN="): return l.split("=", 1)[1].strip().strip('"')
+    raise SystemExit("token")
+TOKEN = os.environ.get("AT") or _tok()
+URL = "https://designer-laboratory-sandbox.myshopify.com/admin/api/2024-10/graphql.json"
+
+def gql(q, v=None):
+    b = json.dumps({"query": q, "variables": v or {}}).encode()
+    req = urllib.request.Request(URL, b, {"X-Shopify-Access-Token": TOKEN, "Content-Type": "application/json"})
+    for a in range(6):
+        try:
+            d = json.load(urllib.request.urlopen(req, timeout=60))
+            if "errors" in d and any("THROTTLED" in str(e) for e in d["errors"]):
+                time.sleep(2 * (a + 1)); continue
+            return d
+        except Exception:
+            time.sleep(2 * (a + 1))
+    raise RuntimeError("gql failed")
+
+FIND = """query($q:String!){products(first:1,query:$q){edges{node{id options{name values}
+  variants(first:10){edges{node{sku}}}}}}}"""
+VCREATE = """mutation($pid:ID!,$v:[ProductVariantsBulkInput!]!){
+  productVariantsBulkCreate(productId:$pid,variants:$v){userErrors{field message}}}"""
+
+def main():
+    ok = 0; errs = []
+    print(f"{'DRY-RUN' if DRY_RUN else 'LIVE'}: restoring {len(SAMPLES)} sample variants")
+    for sku, price in SAMPLES.items():
+        d = gql(FIND, {"q": "sku:" + sku})
+        e = (d.get("data") or {}).get("products", {}).get("edges", [])
+        if not e:
+            errs.append({"sku": sku, "err": "not found"}); continue
+        node = e[0]["node"]; pid = node["id"]
+        have = {v["node"]["sku"] for v in node["variants"]["edges"]}
+        opt_name = node["options"][0]["name"] if node["options"] else "Title"
+        if sku + "-Sample" in have:
+            print(f"  {sku}: sample already present — skip"); ok += 1; continue
+        if DRY_RUN:
+            print(f"  would restore {sku}-Sample @ ${price} on option '{opt_name}=Sample'"); ok += 1; continue
+        res = gql(VCREATE, {"pid": pid, "v": [{
+            "optionValues": [{"optionName": opt_name, "name": "Sample"}],
+            "price": price, "inventoryItem": {"tracked": False, "sku": sku + "-Sample"},
+            "inventoryPolicy": "CONTINUE"}]})
+        ue = ((res.get("data") or {}).get("productVariantsBulkCreate") or {}).get("userErrors") or []
+        if ue: errs.append({"sku": sku, "err": ue[:2]})
+        else: ok += 1; print(f"  restored {sku}-Sample @ ${price}")
+        time.sleep(0.3)
+    print(f"\nDONE {'(DRY-RUN)' if DRY_RUN else ''}: ok={ok} err={len(errs)}")
+    if errs: print("errs:", json.dumps(errs))
+
+if __name__ == "__main__":
+    main()
diff --git a/write-results.json b/write-results.json
index c0c628e..735d899 100644
--- a/write-results.json
+++ b/write-results.json
@@ -1,81 +1,11 @@
 {
-  "updated": 324,
-  "created": 1042,
-  "skipped": 38,
+  "updated": 0,
+  "created": 5,
+  "skipped": 1404,
   "errors": [
-    {
-      "sku": "EUR-70023",
-      "err": [
-        {
-          "field": [
-            "variants",
-            "0",
-            "optionValues",
-            "0"
-          ],
-          "message": "Option does not exist"
-        }
-      ]
-    },
-    {
-      "sku": "EUR-70022",
-      "err": [
-        {
-          "field": [
-            "variants",
-            "0",
-            "optionValues",
-            "0"
-          ],
-          "message": "Option does not exist"
-        }
-      ]
-    },
-    {
-      "sku": "EUR-90168",
-      "err": [
-        {
-          "field": [
-            "variants",
-            "0",
-            "optionValues",
-            "0"
-          ],
-          "message": "Option does not exist"
-        }
-      ]
-    },
-    {
-      "sku": "EUR-70237",
-      "err": [
-        {
-          "field": [
-            "variants",
-            "0",
-            "optionValues",
-            "0"
-          ],
-          "message": "Option does not exist"
-        }
-      ]
-    },
     {
       "sku": "EUR-90364",
       "err": "product not found"
-    },
-    {
-      "sku": "EUR-80735",
-      "err": [
-        {
-          "field": [
-            "variants",
-            "0",
-            "optionValues",
-            "0"
-          ],
-          "message": "Option does not exist"
-        }
-      ]
     }
   ]
 }
\ No newline at end of file

← 35f0725 Fix CREATE option-name (use product's actual option, not har  ·  back to Eur Recrawl  ·  chore: session-close — syntax-clean + version bump v0.1.0 -> 83fd205 →