← back to Designer Wallcoverings
Daisy Bennett Vol 2 -> LIVE: 65 products pushed+activated (Phillipe Romano), sold per-yard 8-yd increments; settlement override on 4 bird SKUs
3e22b8c941e4d60abcf7b82153633d3ed8ef0968 · 2026-07-06 12:42:39 -0700 · Steve
Files touched
A scripts/wallquest-refresh/activate-publish-v2.cjsA scripts/wallquest-refresh/build-v2-drafts.cjsA scripts/wallquest-refresh/update-yard-v2.cjs
Diff
commit 3e22b8c941e4d60abcf7b82153633d3ed8ef0968
Author: Steve <steve@designerwallcoverings.com>
Date: Mon Jul 6 12:42:39 2026 -0700
Daisy Bennett Vol 2 -> LIVE: 65 products pushed+activated (Phillipe Romano), sold per-yard 8-yd increments; settlement override on 4 bird SKUs
---
scripts/wallquest-refresh/activate-publish-v2.cjs | 54 ++++++++++++
scripts/wallquest-refresh/build-v2-drafts.cjs | 101 ++++++++++++++++++++++
scripts/wallquest-refresh/update-yard-v2.cjs | 63 ++++++++++++++
3 files changed, 218 insertions(+)
diff --git a/scripts/wallquest-refresh/activate-publish-v2.cjs b/scripts/wallquest-refresh/activate-publish-v2.cjs
new file mode 100644
index 00000000..5b47cc06
--- /dev/null
+++ b/scripts/wallquest-refresh/activate-publish-v2.cjs
@@ -0,0 +1,54 @@
+// Activate + publish the Daisy Bennett Vol 2 → Phillipe Romano products to ALL sales channels.
+// Selects V2 by mfr_sku LIKE 'DB%' (V1 is BX%) so we never touch the sibling's V1 rows.
+// Pre-req: pushed as draft with width + description + images (DW activation gate). Idempotent.
+// Settlement: Blossom Citrus Floral (Limonaia) BLOCK was overridden by Steve — published too.
+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;
+if (!TOKEN) { console.error('no SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
+const CONN = process.env.DATABASE_URL || 'postgresql://dw_admin@127.0.0.1:5432/dw_unified';
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+function gql(query, variables) {
+ return new Promise((resolve, reject) => {
+ const body = JSON.stringify({ query, variables });
+ const req = https.request({ hostname: STORE, path: `/admin/api/${API}/graphql.json`, method: 'POST',
+ headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }, timeout: 40000 },
+ res => { let d = ''; res.on('data', c => d += c); res.on('end', () => { try { resolve(JSON.parse(d)); } catch (e) { reject(new Error('bad json: ' + d.slice(0, 120))); } }); });
+ req.on('error', reject); req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
+ req.write(body); req.end();
+ });
+}
+
+(async () => {
+ const pubRes = await gql('{ publications(first:25){edges{node{id name}}} }');
+ if (pubRes.errors) { console.error('publications error:', JSON.stringify(pubRes.errors).slice(0, 200)); process.exit(1); }
+ const pubs = pubRes.data.publications.edges.map(e => e.node);
+ const pubInput = pubs.map(p => ({ publicationId: p.id }));
+ console.log('sales channels:', pubs.map(p => p.name).join(', '));
+
+ const db = new Client({ connectionString: CONN }); await db.connect();
+ // V2 = mfr DB##### (V1 is BX#####). Only rows this session registered + pushed.
+ const { rows } = await db.query("SELECT dw_sku, mfr_sku, shopify_product_id FROM dw_sku_registry WHERE mfr_sku LIKE 'DB%' AND shopify_product_id IS NOT NULL ORDER BY dw_sku");
+ console.log(`activating + publishing ${rows.length} V2 products to ${pubs.length} channels…`);
+
+ let act = 0, pub = 0, err = 0;
+ for (const r of rows) {
+ const gid = `gid://shopify/Product/${r.shopify_product_id}`;
+ try {
+ const a = await gql('mutation($id:ID!){ productUpdate(input:{id:$id, status:ACTIVE}){ product{id status} userErrors{message} } }', { id: gid });
+ const aErr = a.errors || a.data?.productUpdate?.userErrors?.length;
+ if (!aErr) act++;
+ const p = await gql('mutation($id:ID!,$in:[PublicationInput!]!){ publishablePublish(id:$id, input:$in){ userErrors{message} } }', { id: gid, in: pubInput });
+ const pErr = p.errors || p.data?.publishablePublish?.userErrors?.length;
+ if (!pErr) { pub++; await db.query("UPDATE dw_sku_registry SET status='active', updated_at=now() WHERE dw_sku=$1", [r.dw_sku]); }
+ else { err++; console.log(`✗ ${r.dw_sku} publish: ${JSON.stringify(p.errors || p.data?.publishablePublish?.userErrors).slice(0,140)}`); }
+ if (!aErr && !pErr) console.log(`✓ ${r.dw_sku} (${r.mfr_sku}) active + on ${pubs.length} channels`);
+ } catch (e) { err++; console.log(`✗ ${r.dw_sku} ${String(e).slice(0, 100)}`); }
+ await sleep(600);
+ }
+ console.log(`\nLIVE: activated=${act} published=${pub} errors=${err} across: ${pubs.map(p=>p.name).join(', ')}`);
+ await db.end();
+})().catch(e => { console.error('FATAL', e.message); process.exit(1); });
diff --git a/scripts/wallquest-refresh/build-v2-drafts.cjs b/scripts/wallquest-refresh/build-v2-drafts.cjs
new file mode 100644
index 00000000..fbfb0754
--- /dev/null
+++ b/scripts/wallquest-refresh/build-v2-drafts.cjs
@@ -0,0 +1,101 @@
+// Build Daisy Bennett Vol 2 → Phillipe Romano DRAFT payloads for push-daisy-bennett.cjs.
+// Applies Steve's material-based dw_sku prefixes + invented (V1-style) pattern names + material
+// descriptions. Reads live rows from daisy_bennett_catalog (source_book='...Volume 2').
+// Settlement: Blossom Citrus Floral is BLOCK but Steve issued an explicit OVERRIDE to publish —
+// recorded in a metafield for the audit trail. Emits /tmp/daisy-bennett-v2-PR-drafts.json.
+const fs = require('fs');
+const { Client } = require('pg');
+const CONN = process.env.DATABASE_URL || 'postgresql://dw_admin@127.0.0.1:5432/dw_unified';
+
+// family → material prefix (Steve's directive; grasscloth-ground prints route to DWGRS)
+const PREFIX = {
+ 'Meilin Grasscloth': 'DWGRS', 'Huanzhu Leaf Stripe': 'DWGRS', 'Pei Ling Lattice': 'DWGRS',
+ 'Blossom Citrus Floral': 'DWGRS', 'Enya Sisal': 'DWSIS', 'Meilian Raffia': 'DWRAF',
+ 'Anhe Abaca': 'DWABA', 'Yujin Cork': 'DWCORK', 'Zinzhu Jute': 'DWJUT',
+};
+// family → invented single-word PR pattern name (V1 style: Azra, Abatha…), source hidden
+const NAME = {
+ 'Meilin Grasscloth': 'Livia', 'Enya Sisal': 'Savona', 'Meilian Raffia': 'Rafaela',
+ 'Anhe Abaca': 'Bianca', 'Pei Ling Lattice': 'Portico', 'Yujin Cork': 'Cortina',
+ 'Zinzhu Jute': 'Giona', 'Huanzhu Leaf Stripe': 'Felcia', 'Blossom Citrus Floral': 'Limonaia',
+};
+// family → luxury retail description (no brand/source leak, no banned word "Wallpaper")
+const DESC = {
+ 'Meilin Grasscloth': 'Hand-woven natural grasscloth with fine tonal striations that bring organic texture and quiet warmth to sophisticated interiors.',
+ 'Enya Sisal': 'Finely woven sisal with a delicate horizontal weave, adding understated natural texture and a subtle sheen.',
+ 'Meilian Raffia': 'Natural raffia in a relaxed basketweave, offering rich tactile depth and organic character.',
+ 'Anhe Abaca': 'Softly woven abaca with a pale, airy hand that lends light natural texture to refined spaces.',
+ 'Pei Ling Lattice': 'An architectural fretwork trellis set over a natural woven ground, blending graphic structure with organic texture.',
+ 'Yujin Cork': 'Natural cork with mineral flecks, delivering organic warmth and a subtle shimmer.',
+ 'Zinzhu Jute': 'Natural jute in a substantial open weave, bringing earthy texture and casual sophistication.',
+ 'Huanzhu Leaf Stripe': 'A tonal frond stripe hand-printed on natural grasscloth, pairing botanical rhythm with an artisanal woven ground.',
+ 'Blossom Citrus Floral': 'A romantic garden chinoiserie of blossoming branches and citrus hand-printed over a natural grasscloth ground.',
+};
+const cleanWP = s => !s ? '' : s.replace(/\bWalls Wallpaper\b/gi, 'Wallcoverings').replace(/\bWallpapers\b/gi, 'Wallcoverings').replace(/\bWallpaper\b/gi, 'Wallcovering');
+const SMALL = new Set(['a','an','the','and','but','or','for','in','on','at','of','to','with']);
+const tc = s => !s ? '' : s.split(/\s+/).map((w,i) => (i>0&&SMALL.has(w.toLowerCase()))?w.toLowerCase():w.charAt(0).toUpperCase()+w.slice(1)).join(' ');
+
+// Per-family settlement (from my visual triage; sibling wiped the DB rows so we read /tmp).
+const SETTLE = {
+ 'Blossom Citrus Floral': 'BLOCK', 'Huanzhu Leaf Stripe': 'REVIEW',
+ 'Anhe Abaca': 'OK', 'Enya Sisal': 'OK', 'Meilian Raffia': 'OK', 'Meilin Grasscloth': 'OK',
+ 'Pei Ling Lattice': 'OK', 'Yujin Cork': 'OK', 'Zinzhu Jute': 'OK',
+};
+(async () => {
+ // Read from /tmp (decoupled from the sibling-churned daisy_bennett_catalog table).
+ const norm = JSON.parse(fs.readFileSync('/tmp/wq-daisy-bennett-v2-normalized.json', 'utf8'));
+ const rows = norm.map(r => ({
+ mfr_sku: r.mfr_sku, color_name: r.color, material: r.material, price_retail: r.price_retail,
+ our_price: r.our_price, width: r.width, length: r.length, repeat_v: r.repeat, match_type: r.match,
+ specs: r.specs, all_images: JSON.stringify(r.images || []), src_pattern: r.pattern,
+ settlement_status: SETTLE[r.pattern] || 'REVIEW', product_url: r.product_url,
+ }));
+ const drafts = []; const leaks = [];
+ const FORBIDDEN = /wallquest|daisy bennett|chesapeake|brewster|nextwall|seabrook|voyage/i;
+ for (const r of rows) {
+ const fam = r.src_pattern;
+ const prefix = PREFIX[fam] || 'DWGRS';
+ const num = (r.mfr_sku.match(/\d+/) || ['0'])[0];
+ const dw_sku = `${prefix}-${num}`;
+ const name = NAME[fam] || fam;
+ const color = tc(r.color_name || '');
+ const title = cleanWP(tc(`${name}${color ? ' ' + color : ''} | Phillipe Romano`));
+ const images = (() => { try { return JSON.parse(r.all_images || '[]'); } catch { return []; } })();
+ let width = r.width || '';
+ if (!width && r.specs) { try { const s = typeof r.specs === 'string' ? JSON.parse(r.specs) : r.specs; width = s['Roll Width'] || s['Width'] || ''; } catch {} }
+ const tags = [name, r.material || 'Natural Texture', 'Grasscloth & Naturals', 'Natural Texture',
+ 'Phillipe Romano', color].filter(Boolean).filter((v,i,a)=>a.indexOf(v)===i);
+ const meta = [
+ { namespace:'custom', key:'manufacturer_sku', value:r.mfr_sku, type:'single_line_text_field' },
+ { namespace:'dwc', key:'manufacturer_sku', value:r.mfr_sku, type:'single_line_text_field' },
+ { namespace:'private_label', key:'real_vendor_name', value:'WallQuest', type:'single_line_text_field' },
+ { namespace:'private_label', key:'source_book', value:'Daisy Bennett Naturals Volume 2', type:'single_line_text_field' },
+ { namespace:'custom', key:'color', value:color, type:'single_line_text_field' },
+ { namespace:'global', key:'width', value:String(width||''), type:'single_line_text_field' },
+ { namespace:'specs', key:'material', value:r.material||'', type:'single_line_text_field' },
+ { namespace:'specs', key:'repeat_v', value:r.repeat_v||'', type:'single_line_text_field' },
+ ];
+ if (r.settlement_status === 'BLOCK') meta.push({ namespace:'private_label', key:'settlement_override', value:'Steve-authorized override 2026-07-06 (bird chinoiserie)', type:'single_line_text_field' });
+ const draft = {
+ dw_sku, title, body_html: `<p>${DESC[fam] || ''}</p>`, vendor: 'Phillipe Romano', product_type: 'Wallcovering',
+ tags, images,
+ variants: [
+ { sku: `${dw_sku}-Sample`, price: '4.25' },
+ { sku: dw_sku, price: String(r.our_price ?? '') },
+ ],
+ metafields: meta, _mfr: r.mfr_sku, _cost: r.price_retail, _settlement: r.settlement_status, _fam: fam,
+ };
+ const cust = [title, draft.body_html, ...tags].join(' | ');
+ if (FORBIDDEN.test(cust)) leaks.push({ dw_sku, hit: cust.match(FORBIDDEN)[0] });
+ drafts.push(draft);
+ }
+ fs.writeFileSync('/tmp/daisy-bennett-v2-PR-drafts.json', JSON.stringify(drafts, null, 2));
+ // summary
+ const byFam = {}; drafts.forEach(d => (byFam[d._fam] = byFam[d._fam] || { n:0, sku:d.dw_sku.split('-')[0], set:d._settlement }).n++);
+ console.log(`built ${drafts.length} drafts → /tmp/daisy-bennett-v2-PR-drafts.json`);
+ console.log('leak-check:', leaks.length ? 'FAIL ' + JSON.stringify(leaks) : 'PASS (0 forbidden names customer-facing)');
+ console.log('\nfamily → prefix → name → count → settlement:');
+ for (const [f, o] of Object.entries(byFam).sort()) console.log(` ${o.sku.padEnd(7)} ${NAME[f].padEnd(9)} ${String(o.n).padStart(2)} ${o.set} (${f})`);
+ console.log('\nSAMPLE TITLES:');
+ drafts.slice(0, 3).concat(drafts.filter(d=>d._settlement==='BLOCK').slice(0,1)).forEach(d => console.log(` ${d.dw_sku} "${d.title}" std $${d.variants[1].price} ${d.images.length} imgs`));
+})().catch(e => { console.error('ERR', e.message); process.exit(1); });
diff --git a/scripts/wallquest-refresh/update-yard-v2.cjs b/scripts/wallquest-refresh/update-yard-v2.cjs
new file mode 100644
index 00000000..4d1375dd
--- /dev/null
+++ b/scripts/wallquest-refresh/update-yard-v2.cjs
@@ -0,0 +1,63 @@
+// Convert Daisy Bennett Vol 2 (Phillipe Romano) to SOLD-BY-THE-YARD, 8-YARD INCREMENTS ONLY.
+// Each bolt = 8 yd (Roll Length spec). Per-yard price = roll our_price / 8. Sets DW quantity
+// metafields so the theme enforces min 8 + step 8. Sample variant ($4.25) untouched.
+// Scope: V2 only (mfr DB%). Idempotent. LIMIT env caps for a test-first run.
+const fs = require('fs');
+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;
+if (!TOKEN) { console.error('no SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
+const CONN = process.env.DATABASE_URL || 'postgresql://dw_admin@127.0.0.1:5432/dw_unified';
+const LIMIT = parseInt(process.env.LIMIT || '999', 10);
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+// mfr → roll our_price (from /tmp normalized), to derive per-yard = /8
+const norm = JSON.parse(fs.readFileSync('/tmp/wq-daisy-bennett-v2-normalized.json', 'utf8'));
+const rollPrice = {}; norm.forEach(r => { rollPrice[r.mfr_sku] = r.our_price; });
+
+function rest(method, path, body) {
+ return new Promise((resolve, reject) => {
+ const data = body ? JSON.stringify(body) : null;
+ const req = https.request({ hostname: STORE, path: `/admin/api/${API}/${path}`, method,
+ headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json', ...(data ? { 'Content-Length': Buffer.byteLength(data) } : {}) }, timeout: 30000 },
+ res => { let d = ''; res.on('data', c => d += c); res.on('end', () => { let j; try { j = d ? JSON.parse(d) : {}; } catch { j = { raw: d }; }
+ (res.statusCode >= 200 && res.statusCode < 300) ? resolve(j) : reject(new Error(`HTTP ${res.statusCode} ${JSON.stringify(j).slice(0,200)}`)); }); });
+ req.on('error', reject); req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
+ if (data) req.write(data); req.end();
+ });
+}
+const META = [
+ { namespace: 'global', key: 'unit_of_measure', value: 'Priced Per Yard', type: 'single_line_text_field' },
+ { namespace: 'global', key: 'Sold-Per', value: 'Yard', type: 'single_line_text_field' },
+ { namespace: 'global', key: 'v_prods_quantity_order_min', value: '8', type: 'single_line_text_field' },
+ { namespace: 'global', key: 'v_prods_quantity_order_units', value: '8', type: 'single_line_text_field' },
+ { namespace: 'global', key: 'Minimum', value: '8', type: 'single_line_text_field' },
+];
+
+(async () => {
+ const db = new Client({ connectionString: CONN }); await db.connect();
+ const { rows } = await db.query("SELECT dw_sku, mfr_sku, shopify_product_id FROM dw_sku_registry WHERE mfr_sku LIKE 'DB%' AND shopify_product_id IS NOT NULL ORDER BY dw_sku");
+ const todo = rows.slice(0, LIMIT);
+ console.log(`Converting ${todo.length} V2 products to per-yard / 8-yd increments…\n`);
+ let ok = 0, err = 0;
+ for (const r of todo) {
+ try {
+ const roll = rollPrice[r.mfr_sku];
+ if (!roll) { console.log(` ? ${r.dw_sku} no roll price, skip`); continue; }
+ const perYard = (Math.round((roll / 8) * 100) / 100).toFixed(2);
+ // find the Standard variant (sku === dw_sku, not the -Sample)
+ const pj = await rest('GET', `products/${r.shopify_product_id}.json?fields=id,variants`);
+ const std = pj.product.variants.find(v => v.sku === r.dw_sku);
+ if (!std) { console.log(` ? ${r.dw_sku} no Standard variant, skip`); err++; continue; }
+ await rest('PUT', `variants/${std.id}.json`, { variant: { id: std.id, price: perYard } });
+ for (const m of META) { try { await rest('POST', `products/${r.shopify_product_id}/metafields.json`, { metafield: m }); } catch {} await sleep(90); }
+ await db.query('UPDATE dw_sku_registry SET updated_at=now() WHERE dw_sku=$1', [r.dw_sku]);
+ console.log(` ✅ ${r.dw_sku} roll $${roll} → $${perYard}/yd (min 8 yd, ×8)`);
+ ok++; await sleep(250);
+ } catch (e) { console.log(` ❌ ${r.dw_sku}: ${String(e).slice(0,140)}`); err++; }
+ }
+ console.log(`\nDONE per-yard conversion: ok=${ok} err=${err}`);
+ await db.end();
+})().catch(e => { console.error('FATAL', e.message); process.exit(1); });
← 04409a10 Daisy Bennett GO LIVE: 63 products active on Shopify (Philli
·
back to Designer Wallcoverings
·
Daisy Bennett viewer: local images (bypass WallQuest hotlink a48458ac →