[object Object]

← 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

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 →