← 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
A scripts/wallquest-refresh/mirror-upsert-carl-robinson.cjsA scripts/wallquest-refresh/set-increment-carl-robinson.cjs
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 →