[object Object]

← back to Designer Wallcoverings

Daisy Bennett GO LIVE: 63 products active on Shopify (Phillipe Romano), settlement all-OK, images rehosted

04409a10354d04a85c84e648cbacadba23d2aafd · 2026-07-06 12:41:32 -0700 · Steve

Files touched

Diff

commit 04409a10354d04a85c84e648cbacadba23d2aafd
Author: Steve <steve@designerwallcoverings.com>
Date:   Mon Jul 6 12:41:32 2026 -0700

    Daisy Bennett GO LIVE: 63 products active on Shopify (Phillipe Romano), settlement all-OK, images rehosted
---
 .../wallquest-refresh/activate-daisy-bennett.cjs   | 42 ++++++++++
 scripts/wallquest-refresh/push-daisy-bennett.cjs   | 92 ++++++++++++++++++++++
 .../settlement-check-daisy-bennett.cjs             | 56 +++++++++++++
 3 files changed, 190 insertions(+)

diff --git a/scripts/wallquest-refresh/activate-daisy-bennett.cjs b/scripts/wallquest-refresh/activate-daisy-bennett.cjs
new file mode 100644
index 00000000..9b77cdc6
--- /dev/null
+++ b/scripts/wallquest-refresh/activate-daisy-bennett.cjs
@@ -0,0 +1,42 @@
+// GO LIVE — activate settlement-clear Daisy Bennett drafts (status=active + published to Online Store).
+// Only activates verdict==='OK' from the settlement gate. Updates registry status. Idempotent.
+const fs = require('fs');
+const https = require('https');
+const { Client } = require('pg');
+const DOMAIN = 'designer-laboratory-sandbox.myshopify.com';
+const API = '2024-10';
+const CONN = 'postgresql://dw_admin:DW2024SecurePass@127.0.0.1:5432/dw_unified';
+const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
+const SETTLE = '/tmp/daisy-bennett-settlement.json';
+const LIMIT = parseInt(process.env.LIMIT || '999', 10);
+if (!TOKEN) { console.error('no token'); process.exit(1); }
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+function shopify(method, path, body) {
+  return new Promise((resolve, reject) => {
+    const data = body ? JSON.stringify(body) : null;
+    const req = https.request({ hostname: DOMAIN, 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(e){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();
+  });
+}
+(async () => {
+  const settle = JSON.parse(fs.readFileSync(SETTLE, 'utf8'));
+  const okSkus = new Set(settle.filter(x => x.verdict === 'OK').map(x => x.dw_sku));
+  const db = new Client({ connectionString: CONN }); await db.connect();
+  const { rows } = await db.query('SELECT dw_sku, shopify_product_id, pattern_name, color_name FROM daisy_bennett_catalog WHERE on_shopify AND shopify_product_id IS NOT NULL ORDER BY dw_sku');
+  const todo = rows.filter(r => okSkus.has(r.dw_sku)).slice(0, LIMIT);
+  console.log(`Activating ${todo.length} settlement-clear products live on ${DOMAIN}\n`);
+  let live = 0, fail = 0;
+  for (const r of todo) {
+    try {
+      await shopify('PUT', `products/${r.shopify_product_id}.json`, { product: { id: Number(r.shopify_product_id), status: 'active', published: true } });
+      await db.query("UPDATE dw_sku_registry SET status='active', updated_at=now() WHERE dw_sku=$1", [r.dw_sku]);
+      console.log(`  🟢 LIVE  ${r.dw_sku}  ${r.pattern_name} ${r.color_name}  (${r.shopify_product_id})`);
+      live++; await sleep(250);
+    } catch (e) { console.error(`  ❌ ${r.dw_sku}: ${String(e).slice(0,150)}`); fail++; }
+  }
+  console.log(`\nGO-LIVE DONE — live=${live} failed=${fail} (of ${todo.length} settlement-OK)`);
+  await db.end();
+})().catch(e => { console.error('FATAL', e.message); process.exit(1); });
diff --git a/scripts/wallquest-refresh/push-daisy-bennett.cjs b/scripts/wallquest-refresh/push-daisy-bennett.cjs
new file mode 100644
index 00000000..72c4d128
--- /dev/null
+++ b/scripts/wallquest-refresh/push-daisy-bennett.cjs
@@ -0,0 +1,92 @@
+// Push Daisy Bennett draft payloads → Shopify as DRAFT products (status=draft, NOT published).
+// Honors Steve's material-based dw_sku scheme by registering the EXACT dw_sku (manual insert),
+// not registerSku()'s auto-generated number. Idempotent: skips any mfr_sku already registered.
+// LIMIT env caps how many to push (test-first). Nothing is set to 'active' here — activation
+// (settlement-gated) is a separate step.
+const fs = require('fs');
+const https = require('https');
+const { Client } = require('pg');
+
+const DRAFTS = process.env.DRAFTS || '/tmp/daisy-bennett-PR-drafts.json';
+const LIMIT = parseInt(process.env.LIMIT || '2', 10);
+const DOMAIN = 'designer-laboratory-sandbox.myshopify.com';
+const API = '2024-10';
+const CONN = 'postgresql://dw_admin:DW2024SecurePass@127.0.0.1:5432/dw_unified';
+const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
+if (!TOKEN) { console.error('no SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
+
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+function shopify(method, path, body) {
+  return new Promise((resolve, reject) => {
+    const data = body ? JSON.stringify(body) : null;
+    const req = https.request({
+      hostname: DOMAIN, 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 (e) { j = { raw: d }; }
+        if (res.statusCode >= 200 && res.statusCode < 300) resolve(j);
+        else reject(new Error(`HTTP ${res.statusCode} ${JSON.stringify(j).slice(0, 300)}`)); });
+    });
+    req.on('error', reject); req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
+    if (data) req.write(data); req.end();
+  });
+}
+
+(async () => {
+  const drafts = JSON.parse(fs.readFileSync(DRAFTS, 'utf8'));
+  const db = new Client({ connectionString: CONN });
+  await db.connect();
+  const todo = drafts.slice(0, LIMIT);
+  console.log(`Pushing ${todo.length}/${drafts.length} as DRAFT to ${DOMAIN}\n`);
+  let created = 0, skipped = 0, failed = 0;
+
+  for (const d of todo) {
+    const prefix = d.dw_sku.split('-')[0];           // material code, e.g. DWGRS
+    const mfr = d._mfr;
+    try {
+      // cross-prefix dedup: is this mfr already registered anywhere?
+      const dup = await db.query('SELECT dw_sku, shopify_product_id FROM dw_sku_registry WHERE mfr_sku=$1', [mfr]);
+      if (dup.rows.length) { console.log(`  SKIP ${d.dw_sku} — ${mfr} already registered as ${dup.rows[0].dw_sku}`); skipped++; continue; }
+
+      // register the EXACT material-based dw_sku (preserve Steve's scheme)
+      await db.query(
+        `INSERT INTO dw_sku_registry (dw_sku, vendor_prefix, vendor_name, mfr_sku, status) VALUES ($1,$2,$3,$4,'draft')
+         ON CONFLICT DO NOTHING`, [d.dw_sku, prefix, 'Phillipe Romano', mfr]);
+
+      // create DRAFT product
+      const product = {
+        title: d.title, body_html: d.body_html, vendor: d.vendor, product_type: d.product_type,
+        status: 'draft', tags: (d.tags || []).join(', '),
+        options: [{ name: 'Size' }],
+        variants: [
+          { option1: 'Sample', sku: d.variants[0].sku, price: d.variants[0].price, inventory_management: null, inventory_policy: 'deny' },
+          { option1: 'Standard', sku: d.variants[1].sku, price: d.variants[1].price, inventory_management: null, inventory_policy: 'continue' },
+        ],
+        images: (d.images || []).slice(0, 12).map(src => ({ src })),
+      };
+      const res = await shopify('POST', 'products.json', { product });
+      const p = res.product; const pid = String(p.id);
+
+      // attach metafields (key set)
+      for (const m of (d.metafields || [])) {
+        if (!m.value) continue;
+        try { await shopify('POST', `products/${pid}/metafields.json`, { metafield: { namespace: m.namespace, key: m.key, value: String(m.value), type: m.type } }); }
+        catch (e) { /* non-fatal per-metafield */ }
+        await sleep(120);
+      }
+
+      await db.query('UPDATE dw_sku_registry SET shopify_product_id=$1, shopify_handle=$2, updated_at=now() WHERE dw_sku=$3', [pid, p.handle, d.dw_sku]);
+      await db.query('UPDATE daisy_bennett_catalog SET on_shopify=true, shopify_product_id=$1 WHERE dw_sku=$2', [pid, d.dw_sku]);
+      console.log(`  ✅ ${d.dw_sku}  "${d.title}"  → product ${pid} (draft, ${product.images.length} imgs)`);
+      created++;
+      await sleep(300);
+    } catch (e) {
+      console.error(`  ❌ ${d.dw_sku} ${mfr}: ${String(e).slice(0, 200)}`);
+      failed++;
+    }
+  }
+  console.log(`\nPUSH DONE — created=${created} skipped=${skipped} failed=${failed}`);
+  await db.end();
+})().catch(e => { console.error('FATAL', e.message); process.exit(1); });
diff --git a/scripts/wallquest-refresh/settlement-check-daisy-bennett.cjs b/scripts/wallquest-refresh/settlement-check-daisy-bennett.cjs
new file mode 100644
index 00000000..c3135fa2
--- /dev/null
+++ b/scripts/wallquest-refresh/settlement-check-daisy-bennett.cjs
@@ -0,0 +1,56 @@
+// Settlement post-gen VISION gate for Daisy Bennett (mandatory before publishing WallQuest patterns).
+// Implements the settlement image-check: Part A (directional leaves/palm fronds + open space between
+// foliage + >1 ink color in foliage) combined with Part B (bird/butterfly/banana/grape). BLOCK only
+// when full Part A AND a Part B element co-occur (defendant-favorable reading). Naturals expect OK.
+// Per-image Gemini yes/no; writes verdicts JSON. NOTHING is published here.
+const fs = require('fs');
+const https = require('https');
+const KEY = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
+if (!KEY) { console.error('no GEMINI_API_KEY'); process.exit(1); }
+const MODEL = 'gemini-3.5-flash';
+const URL_ = `https://generativelanguage.googleapis.com/v1beta/models/${MODEL}:generateContent?key=${KEY}`;
+const IN = process.env.IN || '/tmp/wq-daisy-bennett-enriched.json';
+const OUT = process.env.OUT || '/tmp/daisy-bennett-settlement.json';
+
+const PROMPT = `You are a legal settlement image gate for wallcovering patterns. Look at the image and answer STRICT JSON only:
+{"directional_foliage": true/false (repeating leaves/palm fronds/similar foliage with directional variation),
+ "open_space_between_leaves": true/false,
+ "multi_color_foliage": true/false (foliage drawn in more than one ink color),
+ "prohibited_element": "none|bird|butterfly|banana|grape" (any depicted),
+ "content": "one short phrase describing what the pattern actually depicts"}
+This is a woven natural texture line; most will have NO foliage at all. Return ONLY the JSON.`;
+
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+function dl(url) { return new Promise((res, rej) => { const rq=(u,n=0)=>{ if(n>5)return rej(new Error('redirects')); https.get(u,{timeout:15000},r=>{ if(r.statusCode>=300&&r.statusCode<400&&r.headers.location)return rq(r.headers.location,n+1); if(r.statusCode!==200){r.resume();return rej(new Error('HTTP '+r.statusCode));} const c=[];r.on('data',d=>c.push(d));r.on('end',()=>{const b=Buffer.concat(c);res({base64:b.toString('base64'),mime:(r.headers['content-type']||'image/jpeg').split(';')[0]});});}).on('error',rej).on('timeout',function(){this.destroy();rej(new Error('timeout'));}); }; rq(url); }); }
+function gemini(base64, mime) { const payload={contents:[{parts:[{text:PROMPT},{inline_data:{mime_type:mime,data:base64}}]}],generationConfig:{temperature:0.1,maxOutputTokens:1024,responseMimeType:'application/json'}}; return new Promise((res,rej)=>{const u=new URL(URL_);const body=JSON.stringify(payload);const r=https.request({hostname:u.hostname,path:u.pathname+u.search,method:'POST',headers:{'Content-Type':'application/json','Content-Length':Buffer.byteLength(body)},timeout:40000},rs=>{let d='';rs.on('data',c=>d+=c);rs.on('end',()=>{try{const j=JSON.parse(d);if(j.error)return rej(new Error(j.error.message));let t=(j.candidates?.[0]?.content?.parts?.[0]?.text||'').trim();const m=t.match(/\{[\s\S]*\}/);res(JSON.parse(m?m[0]:t));}catch(e){rej(e);}});});r.on('error',rej);r.on('timeout',()=>{r.destroy();rej(new Error('gemini timeout'));});r.write(body);r.end();});}
+
+function verdict(v) {
+  const partA = v.directional_foliage && v.open_space_between_leaves && v.multi_color_foliage;
+  const partB = v.prohibited_element && v.prohibited_element !== 'none';
+  if (partA && partB) return 'BLOCK';
+  if (v.directional_foliage || partB) return 'REVIEW'; // any foliage or prohibited element → human look
+  return 'OK';
+}
+
+(async () => {
+  const recs = JSON.parse(fs.readFileSync(IN, 'utf8'));
+  const out = []; let ok=0, review=0, block=0, err=0;
+  for (const r of recs) {
+    const img = (r.images || [])[0];
+    if (!img) { out.push({ dw_sku:r.dw_sku, mfr_sku:r.mfr_sku, verdict:'REVIEW', reason:'no image' }); review++; continue; }
+    try {
+      const { base64, mime } = await dl(img);
+      const g = await gemini(base64, mime);
+      const vd = verdict(g);
+      out.push({ dw_sku:r.dw_sku, mfr_sku:r.mfr_sku, verdict:vd, ...g });
+      vd==='OK'?ok++:vd==='REVIEW'?review++:block++;
+      process.stderr.write(`\r[${out.length}/${recs.length}] OK=${ok} REVIEW=${review} BLOCK=${block} err=${err}   `);
+    } catch (e) { out.push({ dw_sku:r.dw_sku, mfr_sku:r.mfr_sku, verdict:'REVIEW', reason:String(e).slice(0,80) }); review++; err++; }
+    await sleep(700);
+  }
+  fs.writeFileSync(OUT, JSON.stringify(out, null, 2));
+  console.error(`\nSETTLEMENT DONE OK=${ok} REVIEW=${review} BLOCK=${block} err=${err} → ${OUT}`);
+  const bad = out.filter(x => x.verdict !== 'OK');
+  if (bad.length) { console.log('NON-OK (hold from publish):'); bad.forEach(x => console.log(`  ${x.verdict}  ${x.dw_sku}  ${x.content||x.reason||''}`)); }
+  else console.log('ALL OK — settlement-clear for publish ✓');
+})();

← 5d6aa242 Carl Robinson: push material-inclusive titles (City Material  ·  back to Designer Wallcoverings  ·  Daisy Bennett Vol 2 -> LIVE: 65 products pushed+activated (P 3e22b8c9 →