[object Object]

← 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

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 →