← back to Vahallan Onboard
vahallan quote-only fix: add global.width (30in) + quotes tag to drafts; real-validator gate-check (verified PASS)
2c6f72ba0f565c11b36844cbe69db5a5105215b6 · 2026-07-23 12:37:30 -0700 · Steve Abrams
Files touched
A fix-drafts-quoteonly.pyA gate-check.js
Diff
commit 2c6f72ba0f565c11b36844cbe69db5a5105215b6
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Jul 23 12:37:30 2026 -0700
vahallan quote-only fix: add global.width (30in) + quotes tag to drafts; real-validator gate-check (verified PASS)
---
fix-drafts-quoteonly.py | 48 ++++++++++++++++++++++++++++++++++++++++++++++++
gate-check.js | 38 ++++++++++++++++++++++++++++++++++++++
2 files changed, 86 insertions(+)
diff --git a/fix-drafts-quoteonly.py b/fix-drafts-quoteonly.py
new file mode 100644
index 0000000..2fffbb2
--- /dev/null
+++ b/fix-drafts-quoteonly.py
@@ -0,0 +1,48 @@
+#!/usr/bin/env python3
+"""Make Vahallan quote-only drafts gate-passable: add global.width metafield (real
+Vahallan standard 30" panel) + the `quotes` tag. Operates on DRAFT products only
+(not customer-facing). Resumable. --limit N to test. Steve-approved 2026-07-23
+('extend gate exemption + tag quotes')."""
+import json, subprocess, sys, time, urllib.request
+
+TOKEN = next(l.split('=',1)[1].strip() for l in open('/Users/macstudio3/Projects/secrets-manager/.env')
+ if l.startswith('SHOPIFY_ADMIN_TOKEN='))
+SHOP = 'designer-laboratory-sandbox.myshopify.com'
+API = f'https://{SHOP}/admin/api/2024-10'
+WIDTH = '30 Inches' # Vahallan documented panel width (30" untrimmed, 28" usable)
+LIMIT = int(sys.argv[sys.argv.index('--limit')+1]) if '--limit' in sys.argv else 10**9
+
+def req(method, path, body=None):
+ r = urllib.request.Request(API+path, method=method,
+ data=json.dumps(body).encode() if body else None,
+ headers={'X-Shopify-Access-Token':TOKEN,'Content-Type':'application/json'})
+ for attempt in range(4):
+ try: return json.loads(urllib.request.urlopen(r, timeout=40).read())
+ except urllib.error.HTTPError as e:
+ if e.code==429: time.sleep(float(e.headers.get('Retry-After','3'))+1); continue
+ raise
+ raise RuntimeError('retries exhausted')
+
+raw = subprocess.run(['psql','-h','/tmp','-d','dw_unified','-tA','-c',
+ "SELECT coalesce(json_agg(json_build_array(id,shopify_product_id,handle) ORDER BY dw_sku),'[]') "
+ "FROM vahallan_catalog WHERE NOT already_on_shopify AND shopify_product_id IS NOT NULL"],
+ capture_output=True,text=True)
+rows = json.loads(raw.stdout)[:LIMIT]
+print(f'fixing {len(rows)} drafts (width + quotes tag)', flush=True)
+done=0
+for rid, pid, handle in rows:
+ p = req('GET', f'/products/{pid}.json?fields=id,tags,status')['product']
+ if p['status'] != 'draft':
+ print(f' SKIP {handle}: status={p["status"]} (not draft)'); continue
+ tags = [t.strip() for t in (p['tags'] or '').split(',') if t.strip()]
+ if 'quotes' not in tags: tags.append('quotes')
+ req('PUT', f'/products/{pid}.json', {'product':{'id':pid,'tags':', '.join(tags)}})
+ # width metafield (global.width) — idempotent: skip if present
+ mfs = req('GET', f'/products/{pid}/metafields.json?namespace=global')['metafields']
+ if not any(m['key']=='width' and (m['value'] or '').strip() for m in mfs):
+ req('POST', f'/products/{pid}/metafields.json', {'metafield':{
+ 'namespace':'global','key':'width','value':WIDTH,'type':'single_line_text_field'}})
+ done+=1
+ if done%25==0: print(f' {done}/{len(rows)}', flush=True)
+ time.sleep(0.4)
+print(f'DONE fixed={done}', flush=True)
diff --git a/gate-check.js b/gate-check.js
new file mode 100644
index 0000000..455ba7f
--- /dev/null
+++ b/gate-check.js
@@ -0,0 +1,38 @@
+// Direct reproduction of the rotation gate on ONE product — runs the REAL
+// validateBeforeActivate + a copy of fiveFieldExtra against live Shopify data.
+const os=require('os'), path=require('path'), https=require('https');
+const { validateBeforeActivate } = require(path.join(os.homedir(),
+ 'Projects/Designer-Wallcoverings/shopify/scripts/lib/validate-before-activate.js'));
+const TOKEN=require('fs').readFileSync(path.join(os.homedir(),'Projects/secrets-manager/.env'),'utf8')
+ .split('\n').find(l=>l.startsWith('SHOPIFY_ADMIN_TOKEN=')).split('=')[1].trim();
+const PID=process.argv[2];
+function g(p){return new Promise((res,rej)=>{https.get(`https://designer-laboratory-sandbox.myshopify.com/admin/api/2024-10/${p}`,
+ {headers:{'X-Shopify-Access-Token':TOKEN}},r=>{let b='';r.on('data',d=>b+=d);r.on('end',()=>res(JSON.parse(b)));}).on('error',rej);});}
+// mirror of fiveFieldExtra (rotate-activate.js)
+function fiveFieldExtra(n){
+ const v=n.variants||[]; const hasSample=v.some(x=>/-sample$/i.test(x.sku||''));
+ const sellable=v.filter(x=>!/-sample$/i.test(x.sku||'')); const hasSellablePriced=sellable.some(x=>parseFloat(x.price)>0);
+ const isQuoteOnly=(n.tags||[]).includes('quotes'); const tagCount=(n.tags||[]).length; const r=[];
+ if(!hasSample)r.push('no-sample-variant');
+ if(!sellable.length&&!isQuoteOnly)r.push('no-sellable-variant');
+ if(!hasSellablePriced&&!isQuoteOnly)r.push('sellable-price-not-gt-0');
+ if(tagCount<2)r.push('fewer-than-2-tags'); return {ok:r.length===0,reasons:r};
+}
+(async()=>{
+ const p=(await g(`products/${PID}.json`)).product;
+ const mfs=(await g(`products/${PID}/metafields.json`)).metafields||[];
+ const mv=(ns,k)=>{const m=mfs.find(x=>x.namespace===ns&&x.key===k);return m&&m.value;};
+ const tags=(p.tags||'').split(',').map(t=>t.trim()).filter(Boolean);
+ const specs={width:mv('global','width')||mv('custom','width'),length:mv('global','length'),
+ repeat:mv('global','repeat'),material:mv('global','material'),unitOfMeasure:mv('global','unit_of_measure')||''};
+ const canonical=validateBeforeActivate({title:p.title,dwSku:'',vendor:p.vendor,tags,
+ descriptionHtml:p.body_html||'',specs,vendorSpecs:specs,
+ images:(p.images||[]).map(i=>i.src),vendorImages:(p.images||[]).map(i=>i.src),
+ variants:(p.variants||[]).map(v=>({sku:v.sku}))});
+ const extra=fiveFieldExtra({variants:p.variants,tags});
+ console.log('title:',p.title);
+ console.log('quotes tag:',tags.includes('quotes'),'| width:',specs.width,'| status:',p.status);
+ console.log('canonical gate:',canonical.ok?'PASS':'FAIL',canonical.reasons||'');
+ console.log('fiveFieldExtra:',extra.ok?'PASS':'FAIL',extra.reasons||'');
+ console.log('=> WOULD ACTIVATE:',canonical.ok&&extra.ok);
+})();
← 69f510b auto-save: 2026-07-23T10:50:25 (1 files) — import-drafts.py
·
back to Vahallan Onboard
·
(newest)