← back to Designer Wallcoverings
Add Fix 2 (gated, dry-run): set v_prods_quantity_order_min=1 on per-yard fabrics; MOQ decision required
8414f2800982af47f79ab90df7da2bdfba49af06 · 2026-07-09 18:16:23 -0700 · Steve
Files touched
A shopify/scripts/fix-per-yard-fabric-min.js
Diff
commit 8414f2800982af47f79ab90df7da2bdfba49af06
Author: Steve <steve@designerwallcoverings.com>
Date: Thu Jul 9 18:16:23 2026 -0700
Add Fix 2 (gated, dry-run): set v_prods_quantity_order_min=1 on per-yard fabrics; MOQ decision required
---
shopify/scripts/fix-per-yard-fabric-min.js | 75 ++++++++++++++++++++++++++++++
1 file changed, 75 insertions(+)
diff --git a/shopify/scripts/fix-per-yard-fabric-min.js b/shopify/scripts/fix-per-yard-fabric-min.js
new file mode 100644
index 00000000..f38aab7a
--- /dev/null
+++ b/shopify/scripts/fix-per-yard-fabric-min.js
@@ -0,0 +1,75 @@
+#!/usr/bin/env node
+/**
+ * fix-per-yard-fabric-min.js (2026-07-09) owner: vp-dw-commerce — GATED, dry-run default
+ *
+ * Fix 2 (SECONDARY, MOQ decision required) for the roll-on-fabric issue.
+ * The ~144 per-yard Kravet-family fabrics carry a 2-yard minimum enforced by the
+ * metafield `global.v_prods_quantity_order_min = 2` (the field the MinMax/theme buy-box
+ * reads; UFAC=2 is its 1:1 proxy in the mirror). This sets it to 1 so the fabrics can be
+ * ordered by the single yard.
+ *
+ * ONLY RUN if Steve decides these fabrics should NOT have a 2-yard minimum. If a 2-yard
+ * cut minimum is intended, DO NOT run — Fix 1 (theme guard) already removes the wrong
+ * "2 roll increments" LABEL, leaving a correct "$X / yard, qty 2 (2-yard min)".
+ *
+ * Worklist = live query: active, Unit of measure ~ yard, UFAC=2.
+ * Writes Shopify metafield global.v_prods_quantity_order_min="1" (+ leaves units=1).
+ * The dw_unified mirror re-syncs on its normal cadence.
+ *
+ * node fix-per-yard-fabric-min.js # DRY-RUN (prints, writes nothing)
+ * node fix-per-yard-fabric-min.js --apply # LIVE metafield write (GATED)
+ * node fix-per-yard-fabric-min.js --apply --limit 5
+ */
+const https = require('https'); const fs = require('fs'); const os = require('os');
+const { execFileSync } = require('child_process');
+const APPLY = process.argv.includes('--apply');
+const LIMIT = (() => { const i = process.argv.indexOf('--limit'); return i >= 0 ? parseInt(process.argv[i + 1], 10) : Infinity; })();
+const env = fs.readFileSync(os.homedir() + '/Projects/secrets-manager/.env', 'utf8');
+const TOKEN = (env.match(/^SHOPIFY_ADMIN_TOKEN=(.*)$/m) || [])[1].replace(/['"]/g, '').trim();
+const STORE = 'designer-laboratory-sandbox.myshopify.com', API = '2024-10';
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+function gql(query, variables) {
+ const body = JSON.stringify({ query, variables });
+ return new Promise((resolve, reject) => {
+ const req = https.request({ host: STORE, path: `/admin/api/${API}/graphql.json`, method: 'POST',
+ headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) } },
+ res => { let d = ''; res.on('data', c => d += c); res.on('end', () => { try { resolve(JSON.parse(d)); } catch (e) { reject(e); } }); });
+ req.on('error', reject); req.write(body); req.end();
+ });
+}
+
+function worklist() {
+ // pull the 144 shopify_ids from the local mirror (UFAC=2 proxy)
+ const sql = `SELECT shopify_id FROM shopify_products
+ WHERE status ILIKE 'active'
+ AND coalesce(metafields->'global'->'Unit of measure'->>'value','') ~* 'yard'
+ AND metafields->'global'->'UFAC'->>'value'='2'
+ ORDER BY dw_sku`;
+ const out = execFileSync('psql', ['-tAc', sql],
+ { env: { ...process.env, PGDATABASE: 'dw_unified', PGHOST: '/tmp', PGUSER: 'stevestudio2' }, encoding: 'utf8' });
+ return out.split('\n').map(s => s.trim()).filter(Boolean).slice(0, LIMIT);
+}
+
+(async () => {
+ const ids = worklist();
+ console.log(`${APPLY ? 'APPLY' : 'DRY-RUN'} — ${ids.length} per-yard fabrics -> v_prods_quantity_order_min = 1`);
+ let done = 0, skip = 0, err = 0;
+ for (const gid of ids) {
+ // read current value first (idempotent / audit)
+ const r = await gql(`query($id:ID!){ product(id:$id){ handle
+ cur: metafield(namespace:"global", key:"v_prods_quantity_order_min"){ id value } } }`, { id: gid });
+ const p = r.data && r.data.product; if (!p) { err++; console.log(` ERR no-product ${gid}`); continue; }
+ const cur = p.cur && p.cur.value;
+ if (cur === '1') { skip++; continue; }
+ if (!APPLY) { console.log(` would set ${p.handle}: ${cur == null ? '(unset)' : cur} -> 1`); done++; continue; }
+ const w = await gql(`mutation($mf:[MetafieldsSetInput!]!){ metafieldsSet(metafields:$mf){ userErrors{ field message } } }`,
+ { mf: [{ ownerId: gid, namespace: 'global', key: 'v_prods_quantity_order_min', type: 'single_line_text_field', value: '1' }] });
+ const ue = w.data && w.data.metafieldsSet && w.data.metafieldsSet.userErrors;
+ if (ue && ue.length) { err++; console.log(` ERR ${p.handle}: ${JSON.stringify(ue)}`); }
+ else { done++; console.log(` set ${p.handle}: ${cur == null ? '(unset)' : cur} -> 1`); }
+ await sleep(250);
+ }
+ console.log(`\n${APPLY ? 'applied' : 'would-change'}=${done} already-1=${skip} errors=${err}`);
+ if (!APPLY) console.log('Re-run with --apply (GATED) to write. Only if a 2-yard minimum is NOT intended.');
+})();
← 72f9c4d3 auto-save: 2026-07-09T18:11:36 (3 files) — package-lock.json
·
back to Designer Wallcoverings
·
Fix 2: drive per-yard-fabric min from Kravet master sheet (1 6e27c66c →