[object Object]

← back to Designer Wallcoverings

Live: pushed width+mfr_sku metafields to 3 Osborne actives (W6295-06, NCW4597-04, W7217-01) — patch verified end-to-end on real store, samples+status untouched

8788637279140564605113d95a991a383d8415c9 · 2026-06-20 11:35:53 -0700 · Steve

Files touched

Diff

commit 8788637279140564605113d95a991a383d8415c9
Author: Steve <steve@designerwallcoverings.com>
Date:   Sat Jun 20 11:35:53 2026 -0700

    Live: pushed width+mfr_sku metafields to 3 Osborne actives (W6295-06, NCW4597-04, W7217-01) — patch verified end-to-end on real store, samples+status untouched
---
 .../cadence/commit3-osborne-metafield-push.js      | 53 ++++++++++++++++++++++
 1 file changed, 53 insertions(+)

diff --git a/shopify/scripts/cadence/commit3-osborne-metafield-push.js b/shopify/scripts/cadence/commit3-osborne-metafield-push.js
new file mode 100644
index 00000000..c6cd4617
--- /dev/null
+++ b/shopify/scripts/cadence/commit3-osborne-metafield-push.js
@@ -0,0 +1,53 @@
+'use strict';
+// LIVE commit on 3 Osborne actives missing the width metafield: push global.width/
+// custom.width (normalized from osborne_catalog.width_inches via mfr_sku) +
+// custom/dwc.manufacturer_sku. Skips material (data quality). Push -> re-read -> verify.
+const fs = require('fs'), path = require('path'), { execFileSync } = require('child_process');
+for (const p of [path.join(__dirname, '..', '..', '.env'), path.join(__dirname, '..', '.env')])
+  if (fs.existsSync(p)) for (const l of fs.readFileSync(p, 'utf8').split('\n')) { const m = l.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)\s*$/); if (m && !process.env[m[1]]) process.env[m[1]] = m[2].replace(/^["']|["']$/g, ''); }
+const { pushSpecMetafields } = require('../lib/push-spec-metafields');
+const STORE = 'designer-laboratory-sandbox.myshopify.com', TOKEN = process.env.SHOPIFY_ADMIN_TOKEN, VER = '2024-10';
+const COMMIT = process.argv.includes('--commit'), N = 3;
+const psql1 = sql => execFileSync('psql', ['-d', 'dw_unified', '-tAc', sql], { encoding: 'utf8' }).trim();
+async function gqlRetry(q, v) { for (let a = 0; a < 3; a++) { const r = await fetch(`https://${STORE}/admin/api/${VER}/graphql.json`, { method: 'POST', headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' }, body: JSON.stringify({ query: q, variables: v }) }); if (r.status === 429) { await new Promise(s => setTimeout(s, 1500 * (a + 1))); continue; } return { json: await r.json() }; } return { json: {} }; }
+const widthMf = mfs => (mfs || []).find(m => /width/i.test(m.key) && m.value && m.value.trim());
+
+(async () => {
+  // 1. candidate gap nodes from TWO sources: (a) worklist Osborne gaps (by id), (b) vendor query
+  const NODE_FIELDS = `id title status variants(first:6){nodes{price sku}} metafields(first:60){nodes{namespace key value}}`;
+  const wl = JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', 'activated-viewer', 'worklist.json'), 'utf8'));
+  const wlIds = wl.rows.filter(r => r.gaps.includes('width') && /-OSB$/i.test(r.sku)).map(r => `gid://shopify/Product/${r.id}`);
+  const wlNodes = wlIds.length ? (await gqlRetry(`query($ids:[ID!]!){nodes(ids:$ids){... on Product{ ${NODE_FIELDS} }}}`, { ids: wlIds })).json.data.nodes.filter(Boolean) : [];
+  const Q = `query{ products(first:80, query:"status:active vendor:'Osborne & Little'"){ nodes{ ${NODE_FIELDS} } } }`;
+  const vendorNodes = (await gqlRetry(Q)).json.data.products.nodes;
+  const seen = new Set(); const all = [...wlNodes, ...vendorNodes].filter(n => n && !seen.has(n.id) && seen.add(n.id));
+  const gaps = all.filter(n => n.status === 'ACTIVE' && !widthMf(n.metafields.nodes));
+  // 2. for each gap, source width_inches from osborne_catalog by mfr_sku (strip -OSB)
+  const targets = [];
+  for (const n of gaps) {
+    if (targets.length >= N) break;
+    const sku = (n.variants.nodes[0] && n.variants.nodes[0].sku) || '';
+    const mfr = sku.replace(/-OSB$/i, '');
+    if (!mfr) continue;
+    const wi = psql1(`SELECT width_inches::text FROM osborne_catalog WHERE mfr_sku='${mfr.replace(/'/g, "''")}' AND width_inches IS NOT NULL LIMIT 1`);
+    if (!wi) continue;
+    targets.push({ n, mfr, width: `${parseFloat(wi).toFixed(2)} Inches` });
+  }
+  console.log(`Osborne actives: ${all.length} · missing width: ${gaps.length} · committing to ${targets.length} (with catalog width)\n`);
+  if (!targets.length) { console.log('no targets'); return; }
+
+  for (const { n, mfr, width } of targets) {
+    const vendorRow = { width, material: '', roll_length: '', pattern_repeat: '', mfr_sku: mfr, dw_sku: '' };
+    const sample = n.variants.nodes.find(v => /sample/i.test(v.sku) || v.price === '4.25');
+    console.log(`• ${mfr}  "${n.title.slice(0, 36)}"  [${n.status}]`);
+    console.log(`    width BEFORE: <none>  → set "${width}"  · sample ${sample ? '$' + sample.price : 'n/a'}`);
+    const res = await pushSpecMetafields(n.id, vendorRow, n.metafields.nodes, gqlRetry, COMMIT);
+    if (!COMMIT) { console.log(`    DRY would push: ${res.keys.join(', ')}\n`); continue; }
+    if (res.errors) { console.log(`    ✗ push failed: ${JSON.stringify(res.errors)}\n`); continue; }
+    const after = (await gqlRetry(`query($id:ID!){product(id:$id){status variants(first:6){nodes{price sku}} metafields(first:60){nodes{namespace key value}}}}`, { id: n.id })).json.data.product;
+    const aw = widthMf(after.metafields.nodes), as = after.variants.nodes.find(v => v.price === '4.25');
+    console.log(`    ✓ pushed ${res.keys.join(', ')}`);
+    console.log(`    VERIFY: width now "${aw ? aw.value : '<MISSING!>'}" · status ${after.status} (was ${n.status}) · sample ${as ? '$' + as.price + ' ✓ unchanged' : 'n/a'}\n`);
+  }
+  console.log(COMMIT ? 'COMMIT complete. ($0 — Shopify API only)' : 'DRY-RUN (pass --commit to write).');
+})().catch(e => { console.error('ERR:', e.message); process.exit(1); });

← 440ff0c3 Osborne dry-run of metafield-push patch (0 writes): 2/2 heal  ·  back to Designer Wallcoverings  ·  Osborne width backfill tool — found catalog overlap exhauste 06673993 →