← 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
A scripts/wallquest-refresh/activate-daisy-bennett.cjsA scripts/wallquest-refresh/push-daisy-bennett.cjsA scripts/wallquest-refresh/settlement-check-daisy-bennett.cjs
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 →