[object Object]

← back to Designer Wallcoverings

Carl Robinson: enforce 8-yard increments on all 67 live SKUs + mirror sync

3d5ae387a5d6e7e2e938e59c6aa44b179e8a1f16 · 2026-07-06 14:09:26 -0700 · Steve

Files touched

Diff

commit 3d5ae387a5d6e7e2e938e59c6aa44b179e8a1f16
Author: Steve <steve@designerwallcoverings.com>
Date:   Mon Jul 6 14:09:26 2026 -0700

    Carl Robinson: enforce 8-yard increments on all 67 live SKUs + mirror sync
---
 .../mirror-upsert-carl-robinson.cjs                | 77 ++++++++++++++++++++++
 .../set-increment-carl-robinson.cjs                | 43 ++++++++++++
 2 files changed, 120 insertions(+)

diff --git a/scripts/wallquest-refresh/mirror-upsert-carl-robinson.cjs b/scripts/wallquest-refresh/mirror-upsert-carl-robinson.cjs
new file mode 100644
index 00000000..dffcb641
--- /dev/null
+++ b/scripts/wallquest-refresh/mirror-upsert-carl-robinson.cjs
@@ -0,0 +1,77 @@
+// "update unified" for Carl Robinson — upsert the 67 live CR products into the
+// dw_unified shopify_products mirror so it reflects the 8-yard-increment change
+// (new display_variant tag + per-yard price). Same column semantics + 5-field
+// rollups as shopify/scripts/sync-shopify-products.js, but as dw_admin (INSERT/
+// UPDATE only) — skips that script's ownership-gated no-op ALTER TABLE.
+// Idempotent (upsert on shopify_id). Env: SHOPIFY_ADMIN_TOKEN, DATABASE_URL.
+const https = require('https');
+const { Client } = require('pg');
+
+const STORE = 'designer-laboratory-sandbox.myshopify.com';
+const API = '2024-10';
+const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
+const CONN = process.env.DATABASE_URL || 'postgresql://dw_admin@127.0.0.1:5432/dw_unified';
+if (!TOKEN) { console.error('no SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+function shopify(path) {
+  return new Promise((resolve, reject) => {
+    const req = https.request({ hostname: STORE, path: `/admin/api/${API}${path}`, method: 'GET',
+      headers: { 'X-Shopify-Access-Token': TOKEN } },
+      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.end();
+  });
+}
+// same SAMPLE classifier as the canonical sync
+const isSample = (v, only) => /sample/i.test(v.title || '') || /-sample$/i.test(v.sku || '') || (only && parseFloat(v.price) === 4.25 && !/sample/i.test(v.title || ''));
+
+(async () => {
+  const db = new Client({ connectionString: CONN }); await db.connect();
+  const { rows } = await db.query(`SELECT dw_sku, mfr_sku, shopify_product_id FROM carl_robinson_catalog WHERE on_shopify AND shopify_product_id IS NOT NULL ORDER BY dw_sku`);
+  console.log(`Upserting ${rows.length} Carl Robinson products into shopify_products mirror\n`);
+  let ok = 0, fail = 0;
+  for (const r of rows) {
+    try {
+      const { product: p } = await shopify(`/products/${r.shopify_product_id}.json`);
+      if (!p) throw new Error('not found');
+      const variants = p.variants || [];
+      const only = variants.length === 1;
+      const flags = variants.map(v => isSample(v, only));
+      const hasSample = flags.some(Boolean);
+      const hasProduct = variants.some((v, i) => !flags[i]);
+      const prices = variants.map(v => v.price == null ? null : parseFloat(v.price)).filter(x => x != null);
+      const minPrice = prices.length ? Math.min(...prices) : null;
+      const bodyHtml = p.body_html || '';
+      const hasDesc = bodyHtml.replace(/<[^>]*>/g, '').trim().length >= 5;
+      const gidP = `gid://shopify/Product/${p.id}`;
+      for (const v of variants) {
+        if (!v.sku) continue;
+        await db.query(`
+          INSERT INTO shopify_products (
+            shopify_id, handle, title, vendor, product_type,
+            sku, variant_id, variant_sku, tags, status,
+            created_at_shopify, updated_at_shopify, synced_at,
+            body_html, variant_count, min_variant_price,
+            has_sample_variant, has_product_variant, has_description, dw_sku, mfr_sku
+          ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,NOW(),$13,$14,$15,$16,$17,$18,$19,$20)
+          ON CONFLICT (shopify_id) DO UPDATE SET
+            handle=EXCLUDED.handle, title=EXCLUDED.title, vendor=EXCLUDED.vendor,
+            product_type=EXCLUDED.product_type, sku=EXCLUDED.sku, variant_sku=EXCLUDED.variant_sku,
+            tags=EXCLUDED.tags, status=EXCLUDED.status, updated_at_shopify=EXCLUDED.updated_at_shopify,
+            body_html=EXCLUDED.body_html, variant_count=EXCLUDED.variant_count,
+            min_variant_price=EXCLUDED.min_variant_price, has_sample_variant=EXCLUDED.has_sample_variant,
+            has_product_variant=EXCLUDED.has_product_variant, has_description=EXCLUDED.has_description,
+            dw_sku=EXCLUDED.dw_sku, mfr_sku=EXCLUDED.mfr_sku, synced_at=NOW()
+        `, [gidP, p.handle, p.title, p.vendor, p.product_type,
+            v.sku, `gid://shopify/ProductVariant/${v.id}`, v.sku, p.tags, p.status,
+            p.created_at, p.updated_at, bodyHtml.slice(0, 200000), variants.length, minPrice,
+            hasSample, hasProduct, hasDesc, r.dw_sku, r.mfr_sku]);
+      }
+      ok++;
+      if (ok <= 3 || ok % 20 === 0) console.log(`  ✅ ${r.dw_sku} (${variants.length} variants)`);
+    } catch (e) { fail++; console.error(`  ❌ ${r.dw_sku}: ${e.message}`); }
+    await sleep(120);
+  }
+  await db.end();
+  console.log(`\nMIRROR UPSERT — ok=${ok} fail=${fail}`);
+})().catch(e => { console.error('FATAL', e.message); process.exit(1); });
diff --git a/scripts/wallquest-refresh/set-increment-carl-robinson.cjs b/scripts/wallquest-refresh/set-increment-carl-robinson.cjs
new file mode 100644
index 00000000..5fd81cf8
--- /dev/null
+++ b/scripts/wallquest-refresh/set-increment-carl-robinson.cjs
@@ -0,0 +1,43 @@
+// Enforce 8-YARD INCREMENTS on the natural-wallcovering products (Steve 2026-07-06 "8 yard
+// increments only"). Sets the DW canonical order-min/increment metafields + unit_of_measure +
+// adds the display_variant tag so the storefront theme enforces the 8-yard step on the qty control.
+const https = require('https');
+const { execSync } = require('child_process');
+const TOK = process.env.SHOPIFY_ADMIN_TOKEN;
+const DOMAIN = 'designer-laboratory-sandbox.myshopify.com';
+const INC = process.env.INC || '8';
+const EXTRA_IDS = (process.env.EXTRA_IDS || '').split(',').filter(Boolean); // extra product ids (e.g. Sag Harbor)
+const LIMIT = parseInt(process.env.LIMIT || '999', 10);
+
+const MF = [
+  { namespace: 'global', key: 'v_prods_quantity_order_min', value: INC, type: 'single_line_text_field' },
+  { namespace: 'global', key: 'v_prods_quantity_order_units', value: INC, type: 'single_line_text_field' },
+  { namespace: 'dwc', key: 'order_unit', value: INC, type: 'single_line_text_field' },
+  { namespace: 'global', key: 'unit_of_measure', value: 'Priced Per Yard', type: 'single_line_text_field' },
+];
+function shopify(method, path, body) {
+  return new Promise((res, rej) => { const data = body ? JSON.stringify(body) : null;
+    const req = https.request({ hostname: DOMAIN, path: `/admin/api/2024-10/${path}`, method,
+      headers: { 'X-Shopify-Access-Token': TOK, 'Content-Type': 'application/json', ...(data?{'Content-Length':Buffer.byteLength(data)}:{}) } },
+      r => { let d=''; r.on('data',c=>d+=c); r.on('end',()=>{ try{const j=d?JSON.parse(d):{}; r.statusCode<300?res(j):rej(new Error(r.statusCode+' '+d.slice(0,120)));}catch(e){rej(e);} }); });
+    req.on('error', rej); if (data) req.write(data); req.end(); });
+}
+async function apply(pid) {
+  for (const m of MF) { try { await shopify('POST', `products/${pid}/metafields.json`, { metafield: m }); } catch(e){} await new Promise(r=>setTimeout(r,90)); }
+  // ensure display_variant tag present (theme reads it to render the qty/increment control)
+  const cur = await shopify('GET', `products/${pid}.json?fields=tags`);
+  const tags = (cur.product.tags||'').split(',').map(s=>s.trim()).filter(Boolean);
+  if (!tags.includes('display_variant')) { tags.push('display_variant'); await shopify('PUT', `products/${pid}.json`, { product:{ id:Number(pid), tags: tags.join(', ') } }); }
+}
+(async () => {
+  const daisy = execSync(`PGPASSWORD=DW2024SecurePass psql -h 127.0.0.1 -U dw_admin -d dw_unified -tAc "SELECT shopify_product_id FROM carl_robinson_catalog WHERE on_shopify AND shopify_product_id IS NOT NULL ORDER BY dw_sku"`).toString().trim().split('\n').filter(Boolean);
+  const ids = [...daisy, ...EXTRA_IDS].slice(0, LIMIT);
+  console.log(`Setting ${INC}-yard increments on ${ids.length} products (${daisy.length} Carl Robinson + ${EXTRA_IDS.length} extra)\n`);
+  let ok=0, fail=0;
+  for (const pid of ids) {
+    try { await apply(pid); ok++; if (ok<=3 || ok%20===0) console.log(`  ✅ ${pid} → min ${INC} / step ${INC} yd + display_variant`); }
+    catch(e){ console.error(`  ❌ ${pid}: ${e.message}`); fail++; }
+    await new Promise(r=>setTimeout(r,150));
+  }
+  console.log(`\nINCREMENT SET — ok=${ok} fail=${fail}`);
+})().catch(e=>{console.error('FATAL',e.message);process.exit(1);});

← 921c8ce3 PR re-scope audit (prefix→line) + sellable/sample breakdown  ·  back to Designer Wallcoverings  ·  PR rescope: brand-leak detection (found 11 Kravet mis-vendor f5ba693d →