← back to Designer Wallcoverings
Add gated daily cadence importer (dry-run first)
6bec4bde7df2da43de22d9d48c640d11527db89b · 2026-06-11 08:58:04 -0700 · SteveStudio2
cadence-import.js + vendors.js: imports up to N vendors × M net-new costed SKUs
per slot (07:30 am / 12:00 pm). HARD GATE per Steve's rule — vendor must be in
vendors.js with a verified costExpr, discount_confirmed=true, and have net-new
costed SKUs, else skipped. Per SKU: cost→retail=cost/0.65/0.85, log our_price+
price_updated_at to dw_unified FIRST, then productSet create (DRAFT) with main
variant=retail, Sample variant=$4.25, custom.cost + custom.price_updated_at,
title-cased + mfr-code/(Tcode) stripped from titles. Rotation cursor.
Dry-run default; --commit creates live. READY today: Romo(262), Thibaut(1512).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Files touched
A shopify/scripts/cadence/cadence-import.jsA shopify/scripts/cadence/data/cadence-cursor.jsonA shopify/scripts/cadence/data/cadence-plan-am-2026-06-11.jsonA shopify/scripts/cadence/vendors.js
Diff
commit 6bec4bde7df2da43de22d9d48c640d11527db89b
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Thu Jun 11 08:58:04 2026 -0700
Add gated daily cadence importer (dry-run first)
cadence-import.js + vendors.js: imports up to N vendors × M net-new costed SKUs
per slot (07:30 am / 12:00 pm). HARD GATE per Steve's rule — vendor must be in
vendors.js with a verified costExpr, discount_confirmed=true, and have net-new
costed SKUs, else skipped. Per SKU: cost→retail=cost/0.65/0.85, log our_price+
price_updated_at to dw_unified FIRST, then productSet create (DRAFT) with main
variant=retail, Sample variant=$4.25, custom.cost + custom.price_updated_at,
title-cased + mfr-code/(Tcode) stripped from titles. Rotation cursor.
Dry-run default; --commit creates live. READY today: Romo(262), Thibaut(1512).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
shopify/scripts/cadence/cadence-import.js | 253 ++++++++++++++++
shopify/scripts/cadence/data/cadence-cursor.json | 5 +
.../cadence/data/cadence-plan-am-2026-06-11.json | 322 +++++++++++++++++++++
shopify/scripts/cadence/vendors.js | 24 ++
4 files changed, 604 insertions(+)
diff --git a/shopify/scripts/cadence/cadence-import.js b/shopify/scripts/cadence/cadence-import.js
new file mode 100644
index 00000000..15c2cf0f
--- /dev/null
+++ b/shopify/scripts/cadence/cadence-import.js
@@ -0,0 +1,253 @@
+#!/usr/bin/env node
+/**
+ * Daily controlled-import cadence for DW Shopify.
+ *
+ * Replaces single-vendor BULK dumps (e.g. the disabled rebel-walls-daily-push)
+ * with a controlled cadence: each slot imports up to N vendors × M net-new SKUs.
+ * 07:30 slot ("am") and 12:00 slot ("pm"), daily.
+ *
+ * HARD GATE (Steve's standing rule — see memory scraper-must-confirm-cost-and-discount):
+ * A vendor is imported ONLY if it is in vendors.js with a verified costExpr,
+ * vendor_registry.discount_confirmed=true, AND has net-new costed SKUs.
+ * No cost -> vendor skipped (logged). NEVER import a placeholder price.
+ *
+ * Per SKU: cost (from catalog costExpr) -> retail = round(cost/0.65/0.85, 2)
+ * - log computed retail to dw_unified catalog FIRST (our_price col), then
+ * - create Shopify product (productSet): main variant price=retail,
+ * Sample variant price=$4.25 (correct sample price), custom.cost + custom.price_updated_at,
+ * title-cased "Pattern, Color Wallcoverings | Vendor".
+ *
+ * Usage:
+ * node cadence-import.js --slot am # DRY-RUN (default), 10 vendors × 20 skus
+ * node cadence-import.js --slot pm --vendors 10 --skus 20
+ * node cadence-import.js --slot am --commit # LIVE create on Shopify
+ * node cadence-import.js --gate # just print vendor readiness
+ */
+const https = require('https');
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+const { execFileSync } = require('child_process');
+const VENDORS = require('./vendors.js');
+
+// ---- args ----
+const args = process.argv.slice(2);
+const flag = n => args.includes(n);
+const val = (n, d) => { const i = args.indexOf(n); return i >= 0 ? args[i + 1] : d; };
+const COMMIT = flag('--commit');
+const GATE_ONLY = flag('--gate');
+const SLOT = val('--slot', 'am');
+const VENDORS_PER_SLOT = parseInt(val('--vendors', '10'), 10);
+const SKUS_PER_VENDOR = parseInt(val('--skus', '20'), 10);
+const TODAY = new Date().toISOString().slice(0, 10);
+const RETAIL = c => Math.round((c / 0.65 / 0.85) * 100) / 100;
+const SAMPLE_PRICE = '4.25'; // correct price for the Sample variant (Steve 2026-06-11)
+
+// ---- creds ----
+const env = fs.readFileSync(os.homedir() + '/Projects/secrets-manager/.env', 'utf8');
+const TOKEN = (env.match(/^SHOPIFY_ADMIN_TOKEN=(.*)$/m) || [])[1].replace(/['"]/g, '').trim();
+const STORE = 'designer-laboratory-sandbox.myshopify.com', API = '2024-10';
+const DATADIR = path.join(__dirname, 'data');
+const CURSOR = path.join(DATADIR, 'cadence-cursor.json');
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+// ---- title-case (same corrected logic as fix-24h-new-skus.js) ----
+const SMALL = new Set(['a','an','and','as','at','but','by','de','del','for','if','in','la','le','nor','of','on','or','per','the','to','und','via','vs','with']);
+const KEEP_UPPER = new Set(['usa','uk','ny','nyc','sf','clj','led','pvc','uv','3d','dna']);
+const decode = s => String(s||'').replace(/&/gi,'&').replace(/"/gi,'"').replace(/'|'/gi,"'").replace(/ /gi,' ');
+const capRuns = w => w.replace(/[A-Za-z]+/g, (m, off, str) => {
+ const prev = str[off - 1];
+ if ((prev === "'" || prev === '’') && m.length <= 2) return m.toLowerCase(); // possessive 's / contractions
+ return m[0].toUpperCase() + m.slice(1).toLowerCase();
+});
+// strip junk color_name (vendor echoes the pattern + "(Tcode)" into the color field)
+function cleanColor(color, pattern, mfrSku) {
+ if (!color) return '';
+ if (/unknown/i.test(color)) return '';
+ if (/\([A-Za-z]?\d{3,}\)/.test(color)) return ''; // contains an (Tnnnn) sku code
+ if (mfrSku && color.toLowerCase().includes(String(mfrSku).toLowerCase())) return '';
+ if (pattern && color.toLowerCase().includes(pattern.toLowerCase()) && color.length > pattern.length + 4) return '';
+ return color;
+}
+function tcWord(w, first, last) {
+ if (!w) return w;
+ if (/\d/.test(w)) return w;
+ const low = w.replace(/[^A-Za-z]/g,'').toLowerCase();
+ if (KEEP_UPPER.has(low)) return w.toUpperCase();
+ if (SMALL.has(low) && !first) return w.toLowerCase();
+ if (last && low.length === 1) return w.toLowerCase();
+ return capRuns(w);
+}
+function titleCase(s) {
+ const ws = decode(s).trim().split(/\s+/);
+ return ws.map((w,i) => tcWord(w, i===0, i===ws.length-1)).join(' ');
+}
+const esc = s => String(s==null?'':s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
+
+// ---- pg ----
+function psql(sql) {
+ return execFileSync('psql', ['-At', '-F', '\t', '-d', 'dw_unified', '-c', sql], { encoding: 'utf8', maxBuffer: 1 << 28 }).trim();
+}
+function confirmedDiscountVendors() {
+ const out = psql(`SELECT vendor_name FROM vendor_registry WHERE discount_confirmed IS TRUE;`);
+ return new Set(out ? out.split('\n') : []);
+}
+
+// ---- shopify ----
+function gql(query, variables) {
+ return new Promise(res => {
+ const data = JSON.stringify({ query, variables });
+ const req = https.request({ host: STORE, path: `/admin/api/${API}/graphql.json`, method: 'POST',
+ headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) } },
+ r => { let d=''; r.on('data',c=>d+=c); r.on('end',()=>{ try{res({status:r.statusCode,json:JSON.parse(d)})}catch{res({status:r.statusCode,raw:d.slice(0,400)})} }); });
+ req.on('error', e => res({ status: 0, err: e.message }));
+ req.write(data); req.end();
+ });
+}
+async function gqlRetry(q, v) {
+ for (let a=0;a<5;a++){ const r=await gql(q,v); const t=r.json&&r.json.errors&&JSON.stringify(r.json.errors).includes('THROTTLED');
+ if (r.status===429||t){await sleep(2000*(a+1));continue;} await sleep(550); return r; }
+ return { status: 429, raw: 'throttled' };
+}
+
+// ---- ensure dw_unified logs our computed price FIRST (Steve's hard rule) ----
+function ensureOurPriceCol(table) {
+ // additive, idempotent — our computed retail + when it was set (30-day audit)
+ psql(`ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS our_price numeric(10,2);`);
+ psql(`ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS price_updated_at date;`);
+}
+function logPriceToDb(table, dwSku, retail) {
+ psql(`UPDATE ${table} SET our_price=${retail}, price_updated_at='${TODAY}' WHERE dw_sku='${dwSku.replace(/'/g,"''")}';`);
+}
+
+// ---- gate: which configured vendors are READY with net-new costed fuel ----
+function gate() {
+ const confirmed = confirmedDiscountVendors();
+ const rows = [];
+ for (const [vendor, cfg] of Object.entries(VENDORS)) {
+ if (!confirmed.has(vendor)) { rows.push({ vendor, cfg, ready: false, why: 'discount-not-confirmed', netnew: 0 }); continue; }
+ let netnew = 0, why = 'READY';
+ try {
+ const c = psql(`SELECT count(*) FROM ${cfg.table} WHERE coalesce(shopify_product_id::text,'')='' AND (${cfg.costExpr})::numeric > 0;`);
+ netnew = parseInt(c || '0', 10);
+ if (!netnew) why = 'no-net-new-costed-skus';
+ } catch (e) { why = 'gate-query-error: ' + String(e.message).slice(0,60); }
+ rows.push({ vendor, cfg, ready: netnew > 0, why, netnew });
+ }
+ return rows;
+}
+
+// ---- rotation cursor so successive slots/days pick different vendors ----
+function loadCursor() { try { return JSON.parse(fs.readFileSync(CURSOR,'utf8')); } catch { return { idx: 0 }; } }
+function saveCursor(c) { fs.mkdirSync(DATADIR,{recursive:true}); fs.writeFileSync(CURSOR, JSON.stringify(c,null,1)); }
+
+// ---- select next M net-new costed SKUs for a vendor ----
+function selectSkus(cfg, limit) {
+ const cols = 'dw_sku, mfr_sku, pattern_name, color_name, collection, width, image_url, product_type, material, roll_length, pattern_repeat';
+ const sql = `SELECT ${cols}, (${cfg.costExpr})::numeric AS cost
+ FROM ${cfg.table}
+ WHERE coalesce(shopify_product_id::text,'')='' AND (${cfg.costExpr})::numeric > 0
+ ORDER BY dw_sku LIMIT ${limit};`;
+ const out = psql(sql);
+ if (!out) return [];
+ const keys = [...cols.split(', '), 'cost'];
+ return out.split('\n').map(line => { const v=line.split('\t'); const o={}; keys.forEach((k,i)=>o[k]=v[i]); o.cost=parseFloat(o.cost); return o; });
+}
+
+// ---- build the Shopify productSet input (generic across standard-schema vendors) ----
+function buildInput(vendor, cfg, row, retail) {
+ const stripCodes = s => String(s||'').replace(/\(\s*[A-Za-z]?\d{3,}[^)]*\)/g, '').replace(/\s{2,}/g,' ').trim(); // kill (T457) etc
+ const pattern = titleCase(stripCodes(row.pattern_name || ''));
+ const rawColor = cleanColor(row.color_name, row.pattern_name, row.mfr_sku);
+ const color = rawColor ? titleCase(stripCodes(rawColor)) : '';
+ let core = color ? `${pattern}, ${color}` : pattern;
+ if (!core) core = titleCase(row.pattern_name || '') || row.dw_sku; // never fall back to mfr_sku in title
+ const title = `${core} Wallcoverings | ${vendor}`.replace(/wallpaper/gi,'Wallcovering');
+ const handle = (`${core}-${row.dw_sku}`).toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-+|-+$/g,'').slice(0,100);
+ const specs = [];
+ if (row.material) specs.push(`<li><strong>Material:</strong> ${esc(row.material)}</li>`);
+ if (row.width) specs.push(`<li><strong>Width:</strong> ${esc(row.width)}</li>`);
+ if (row.roll_length) specs.push(`<li><strong>Roll Length:</strong> ${esc(row.roll_length)}</li>`);
+ if (row.pattern_repeat) specs.push(`<li><strong>Repeat:</strong> ${esc(row.pattern_repeat)}</li>`);
+ specs.push(`<li><strong>Sold By:</strong> ${esc(cfg.soldBy)}</li>`);
+ const intro = `${pattern}${color?' in '+color:''} is a designer wallcovering${row.collection?' from the '+esc(row.collection)+' collection':''} by ${vendor}. Priced per ${cfg.soldBy.toLowerCase()}.`;
+ const SL='single_line_text_field', ML='multi_line_text_field', ND='number_decimal';
+ const mf=[]; const m=(ns,k,v,t)=>{ if(v!=null&&v!=='') mf.push({namespace:ns,key:k,type:t,value:String(v)}); };
+ m('custom','manufacturer_sku',row.mfr_sku,SL); m('global','manufacturer_sku',row.mfr_sku,SL);
+ m('global','Brand',vendor,SL); m('global','dw_sku',row.dw_sku,SL);
+ if (row.width){ m('global','width',row.width,SL); m('custom','width',row.width,SL); }
+ if (pattern){ m('global','pattern_name',pattern,SL); m('custom','pattern_name',pattern,SL); }
+ if (color){ m('custom','color',color,SL); m('global','color',color,SL); m('dwc','color',color,SL); }
+ if (row.material){ m('global','material',row.material,SL); m('custom','material',row.material,ML); }
+ if (row.collection) m('custom','collection_name',row.collection,SL);
+ m('custom','cost',row.cost.toFixed(2),ND); // REAL cost
+ m('custom','price_updated_at',TODAY,'date'); // 30-day refresh audit
+ m('global','unit_of_measure',`Priced Per ${cfg.soldBy}`,SL);
+ const input = {
+ title, handle, vendor, productType: cfg.productType, status: 'DRAFT', // never ACTIVE on create (Steve rule)
+ tags: [vendor, cfg.productType, `Priced Per ${cfg.soldBy}`, ...(row.collection?[`Collection: ${row.collection}`]:[]), ...(color?[color]:[])],
+ descriptionHtml: `<p>${esc(intro)}</p><ul>${specs.join('')}</ul>`,
+ metafields: mf,
+ productOptions: [{ name:'Title', position:1, values:[{name:cfg.soldBy},{name:'Sample'}] }],
+ variants: [
+ { optionValues:[{optionName:'Title',name:cfg.soldBy}], price:String(retail), sku:row.dw_sku, inventoryItem:{sku:row.dw_sku,tracked:true,cost:row.cost.toFixed(2)}, inventoryPolicy:'CONTINUE', taxable:true },
+ { optionValues:[{optionName:'Title',name:'Sample'}], price:SAMPLE_PRICE, sku:`${row.dw_sku}-Sample`, inventoryItem:{sku:`${row.dw_sku}-Sample`,tracked:true}, inventoryPolicy:'CONTINUE', taxable:true },
+ ],
+ };
+ if (row.image_url) input.files = [{ originalSource: row.image_url, contentType:'IMAGE', alt: core }];
+ return { input, title, retail };
+}
+
+const PRODUCTSET = `mutation push($input: ProductSetInput!){productSet(synchronous:true,input:$input){product{id handle status} userErrors{field message}}}`;
+
+async function createProduct(table, row, payload) {
+ // dw_unified FIRST
+ logPriceToDb(table, row.dw_sku, payload.retail);
+ const r = await gqlRetry(PRODUCTSET, { input: payload.input });
+ const ue = r.json?.data?.productSet?.userErrors;
+ if (ue && ue.length) return { ok:false, err: ue };
+ const pid = r.json?.data?.productSet?.product?.id;
+ if (pid) psql(`UPDATE ${table} SET shopify_product_id='${String(pid).replace(/.*\//,'')}' WHERE dw_sku='${row.dw_sku.replace(/'/g,"''")}';`);
+ return { ok:true, pid };
+}
+
+// ---- main ----
+(async () => {
+ if (!TOKEN) { console.error('no SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
+ const all = gate();
+ console.log(`\n=== VENDOR GATE (slot=${SLOT}) ===`);
+ for (const r of all) console.log(` ${r.ready?'✅':'⛔'} ${r.vendor.padEnd(18)} net-new costed=${String(r.netnew).padStart(5)} ${r.why}`);
+ if (GATE_ONLY) return;
+
+ const ready = all.filter(r => r.ready);
+ if (!ready.length) { console.log('\nNo READY vendors with net-new costed SKUs. Nothing to import.'); return; }
+
+ // rotate
+ const cur = loadCursor();
+ const start = cur.idx % ready.length;
+ const picked = [];
+ for (let i=0;i<Math.min(VENDORS_PER_SLOT, ready.length);i++) picked.push(ready[(start+i)%ready.length]);
+ console.log(`\n=== ${COMMIT?'LIVE COMMIT':'DRY-RUN'} — slot ${SLOT}: ${picked.length} vendor(s) × up to ${SKUS_PER_VENDOR} SKUs ===`);
+
+ const plan = []; let created=0, failed=0;
+ for (const r of picked) {
+ if (COMMIT) ensureOurPriceCol(r.cfg.table);
+ const skus = selectSkus(r.cfg, SKUS_PER_VENDOR);
+ console.log(`\n${r.vendor} — ${skus.length} SKU(s) (cost ${r.cfg.costExpr}):`);
+ for (const row of skus) {
+ const retail = RETAIL(row.cost);
+ const payload = buildInput(r.vendor, r.cfg, row, retail);
+ plan.push({ vendor:r.vendor, dw_sku:row.dw_sku, cost:row.cost, retail, sample:SAMPLE_PRICE, title:payload.title });
+ if (!COMMIT) { console.log(` · ${row.dw_sku} cost $${row.cost.toFixed(2)} → roll $${retail} sample $${SAMPLE_PRICE} | ${payload.title.slice(0,52)}`); continue; }
+ const res = await createProduct(r.cfg.table, row, payload);
+ if (res.ok) { created++; if(created%10===0) process.stdout.write(`\r created ${created}…`); }
+ else { failed++; console.log(` ✗ ${row.dw_sku} ${JSON.stringify(res.err).slice(0,140)}`); }
+ }
+ }
+ // advance rotation
+ saveCursor({ idx: (start + picked.length) % ready.length, lastSlot: SLOT, lastRun: TODAY });
+ const planFile = path.join(DATADIR, `cadence-plan-${SLOT}-${TODAY}.json`);
+ fs.mkdirSync(DATADIR,{recursive:true}); fs.writeFileSync(planFile, JSON.stringify(plan,null,1));
+ console.log(`\n\n${COMMIT?`CREATED ${created}, failed ${failed}`:`DRY-RUN planned ${plan.length} products`} → ${planFile}`);
+ if (!COMMIT) console.log('Re-run with --commit to create these on Shopify (DRAFT status, dw_unified logged first).');
+})();
diff --git a/shopify/scripts/cadence/data/cadence-cursor.json b/shopify/scripts/cadence/data/cadence-cursor.json
new file mode 100644
index 00000000..cce0f09a
--- /dev/null
+++ b/shopify/scripts/cadence/data/cadence-cursor.json
@@ -0,0 +1,5 @@
+{
+ "idx": 0,
+ "lastSlot": "am",
+ "lastRun": "2026-06-11"
+}
\ No newline at end of file
diff --git a/shopify/scripts/cadence/data/cadence-plan-am-2026-06-11.json b/shopify/scripts/cadence/data/cadence-plan-am-2026-06-11.json
new file mode 100644
index 00000000..caaf607b
--- /dev/null
+++ b/shopify/scripts/cadence/data/cadence-plan-am-2026-06-11.json
@@ -0,0 +1,322 @@
+[
+ {
+ "vendor": "Romo",
+ "dw_sku": "DWRM-240007",
+ "cost": 425,
+ "retail": 769.23,
+ "sample": "4.25",
+ "title": "Abaca, Pewter Wallcoverings | Romo"
+ },
+ {
+ "vendor": "Romo",
+ "dw_sku": "DWRM-240013",
+ "cost": 493,
+ "retail": 892.31,
+ "sample": "4.25",
+ "title": "Tipi, Indigo Wallcoverings | Romo"
+ },
+ {
+ "vendor": "Romo",
+ "dw_sku": "DWRM-240016",
+ "cost": 555,
+ "retail": 1004.52,
+ "sample": "4.25",
+ "title": "Savanna, Bark Wallcoverings | Romo"
+ },
+ {
+ "vendor": "Romo",
+ "dw_sku": "DWRM-240020",
+ "cost": 432,
+ "retail": 781.9,
+ "sample": "4.25",
+ "title": "Raffia, Gunmetal Wallcoverings | Romo"
+ },
+ {
+ "vendor": "Romo",
+ "dw_sku": "DWRM-240022",
+ "cost": 493,
+ "retail": 892.31,
+ "sample": "4.25",
+ "title": "Akata, Plaster Wallcoverings | Romo"
+ },
+ {
+ "vendor": "Romo",
+ "dw_sku": "DWRM-240033",
+ "cost": 345,
+ "retail": 624.43,
+ "sample": "4.25",
+ "title": "Sisal, Chestnut Wallcoverings | Romo"
+ },
+ {
+ "vendor": "Romo",
+ "dw_sku": "DWRM-240037",
+ "cost": 390,
+ "retail": 705.88,
+ "sample": "4.25",
+ "title": "Seagrass, Metal Wallcoverings | Romo"
+ },
+ {
+ "vendor": "Romo",
+ "dw_sku": "DWRM-240046",
+ "cost": 458,
+ "retail": 828.96,
+ "sample": "4.25",
+ "title": "Duo Sisal, Anthracite Wallcoverings | Romo"
+ },
+ {
+ "vendor": "Romo",
+ "dw_sku": "DWRM-240048",
+ "cost": 514,
+ "retail": 930.32,
+ "sample": "4.25",
+ "title": "Kuima, Canvas Wallcoverings | Romo"
+ },
+ {
+ "vendor": "Romo",
+ "dw_sku": "DWRM-240050",
+ "cost": 514,
+ "retail": 930.32,
+ "sample": "4.25",
+ "title": "Kuima, Salt Marsh Wallcoverings | Romo"
+ },
+ {
+ "vendor": "Romo",
+ "dw_sku": "DWRM-240058",
+ "cost": 371,
+ "retail": 671.49,
+ "sample": "4.25",
+ "title": "Kami, Anthracite Wallcoverings | Romo"
+ },
+ {
+ "vendor": "Romo",
+ "dw_sku": "DWRM-240064",
+ "cost": 358,
+ "retail": 647.96,
+ "sample": "4.25",
+ "title": "Papier, Metal Wallcoverings | Romo"
+ },
+ {
+ "vendor": "Romo",
+ "dw_sku": "DWRM-240081",
+ "cost": 343,
+ "retail": 620.81,
+ "sample": "4.25",
+ "title": "Shifu, Metal Wallcoverings | Romo"
+ },
+ {
+ "vendor": "Romo",
+ "dw_sku": "DWRM-240085",
+ "cost": 1066,
+ "retail": 1929.41,
+ "sample": "4.25",
+ "title": "Square Cut, Silver Wallcoverings | Romo"
+ },
+ {
+ "vendor": "Romo",
+ "dw_sku": "DWRM-240093",
+ "cost": 1282,
+ "retail": 2320.36,
+ "sample": "4.25",
+ "title": "Anagram, Chestnut Wallcoverings | Romo"
+ },
+ {
+ "vendor": "Romo",
+ "dw_sku": "DWRM-240097",
+ "cost": 1110,
+ "retail": 2009.05,
+ "sample": "4.25",
+ "title": "Panorama, Atlantic Wallcoverings | Romo"
+ },
+ {
+ "vendor": "Romo",
+ "dw_sku": "DWRM-240100",
+ "cost": 1250,
+ "retail": 2262.44,
+ "sample": "4.25",
+ "title": "Grid, Chestnut Wallcoverings | Romo"
+ },
+ {
+ "vendor": "Romo",
+ "dw_sku": "DWRM-240103",
+ "cost": 904,
+ "retail": 1636.2,
+ "sample": "4.25",
+ "title": "Suna, Stoneware Wallcoverings | Romo"
+ },
+ {
+ "vendor": "Romo",
+ "dw_sku": "DWRM-240108",
+ "cost": 904,
+ "retail": 1636.2,
+ "sample": "4.25",
+ "title": "Sidestep, Indigo Wallcoverings | Romo"
+ },
+ {
+ "vendor": "Romo",
+ "dw_sku": "DWRM-240113",
+ "cost": 904,
+ "retail": 1636.2,
+ "sample": "4.25",
+ "title": "Washi, Indigo Wallcoverings | Romo"
+ },
+ {
+ "vendor": "Thibaut",
+ "dw_sku": "DWTT-70420",
+ "cost": 59.4,
+ "retail": 107.51,
+ "sample": "4.25",
+ "title": "Belgium Linen Wallcoverings | Thibaut"
+ },
+ {
+ "vendor": "Thibaut",
+ "dw_sku": "DWTT-70680",
+ "cost": 80.1,
+ "retail": 144.98,
+ "sample": "4.25",
+ "title": "Cornwall, Blush Wallcoverings | Thibaut"
+ },
+ {
+ "vendor": "Thibaut",
+ "dw_sku": "DWTT-70800",
+ "cost": 129.6,
+ "retail": 234.57,
+ "sample": "4.25",
+ "title": "Sachon Basket, Navy Wallcoverings | Thibaut"
+ },
+ {
+ "vendor": "Thibaut",
+ "dw_sku": "DWTT-70810",
+ "cost": 96.3,
+ "retail": 174.3,
+ "sample": "4.25",
+ "title": "Portage, Light Blue Wallcoverings | Thibaut"
+ },
+ {
+ "vendor": "Thibaut",
+ "dw_sku": "DWTT-70820",
+ "cost": 118.8,
+ "retail": 215.02,
+ "sample": "4.25",
+ "title": "Sutton, Metallic Gold Wallcoverings | Thibaut"
+ },
+ {
+ "vendor": "Thibaut",
+ "dw_sku": "DWTT-70880",
+ "cost": 35.1,
+ "retail": 63.53,
+ "sample": "4.25",
+ "title": "Cabo Raffia Wallcoverings | Thibaut"
+ },
+ {
+ "vendor": "Thibaut",
+ "dw_sku": "DWTT-71120",
+ "cost": 69.3,
+ "retail": 125.43,
+ "sample": "4.25",
+ "title": "Taluk Sisal Wallcoverings | Thibaut"
+ },
+ {
+ "vendor": "Thibaut",
+ "dw_sku": "DWTT-71320",
+ "cost": 69.3,
+ "retail": 125.43,
+ "sample": "4.25",
+ "title": "Woolston, Flax Wallcoverings | Thibaut"
+ },
+ {
+ "vendor": "Thibaut",
+ "dw_sku": "DWTT-71340",
+ "cost": 170.1,
+ "retail": 307.87,
+ "sample": "4.25",
+ "title": "Tabacon Abaca, Cream Wallcoverings | Thibaut"
+ },
+ {
+ "vendor": "Thibaut",
+ "dw_sku": "DWTT-71370",
+ "cost": 69.3,
+ "retail": 125.43,
+ "sample": "4.25",
+ "title": "Kendari Grass, Light Grey Wallcoverings | Thibaut"
+ },
+ {
+ "vendor": "Thibaut",
+ "dw_sku": "DWTT-71380",
+ "cost": 69.3,
+ "retail": 125.43,
+ "sample": "4.25",
+ "title": "Taluk Sisal Wallcoverings | Thibaut"
+ },
+ {
+ "vendor": "Thibaut",
+ "dw_sku": "DWTT-71410",
+ "cost": 69.3,
+ "retail": 125.43,
+ "sample": "4.25",
+ "title": "Luta Sisal, Ice Wallcoverings | Thibaut"
+ },
+ {
+ "vendor": "Thibaut",
+ "dw_sku": "DWTT-71490",
+ "cost": 170.1,
+ "retail": 307.87,
+ "sample": "4.25",
+ "title": "Tabacon Abaca, Robin's Egg Wallcoverings | Thibaut"
+ },
+ {
+ "vendor": "Thibaut",
+ "dw_sku": "DWTT-71600",
+ "cost": 69.3,
+ "retail": 125.43,
+ "sample": "4.25",
+ "title": "Painted Desert Wallcoverings | Thibaut"
+ },
+ {
+ "vendor": "Thibaut",
+ "dw_sku": "DWTT-71670",
+ "cost": 129.6,
+ "retail": 234.57,
+ "sample": "4.25",
+ "title": "Morada Bay, Turquoise Wallcoverings | Thibaut"
+ },
+ {
+ "vendor": "Thibaut",
+ "dw_sku": "DWTT-71810",
+ "cost": 49.5,
+ "retail": 89.59,
+ "sample": "4.25",
+ "title": "Baldwin Herringbone Wallcoverings | Thibaut"
+ },
+ {
+ "vendor": "Thibaut",
+ "dw_sku": "DWTT-71880",
+ "cost": 69.3,
+ "retail": 125.43,
+ "sample": "4.25",
+ "title": "Woolston, Ivory Wallcoverings | Thibaut"
+ },
+ {
+ "vendor": "Thibaut",
+ "dw_sku": "DWTT-72310",
+ "cost": 44.1,
+ "retail": 79.82,
+ "sample": "4.25",
+ "title": "Colored Blocks Wallcoverings | Thibaut"
+ },
+ {
+ "vendor": "Thibaut",
+ "dw_sku": "DWTT-72340",
+ "cost": 41.4,
+ "retail": 74.93,
+ "sample": "4.25",
+ "title": "Sunburst, Grey Wallcoverings | Thibaut"
+ },
+ {
+ "vendor": "Thibaut",
+ "dw_sku": "DWTT-72350",
+ "cost": 46.8,
+ "retail": 84.71,
+ "sample": "4.25",
+ "title": "Colored Blocks Wallcoverings | Thibaut"
+ }
+]
\ No newline at end of file
diff --git a/shopify/scripts/cadence/vendors.js b/shopify/scripts/cadence/vendors.js
new file mode 100644
index 00000000..8635ab5a
--- /dev/null
+++ b/shopify/scripts/cadence/vendors.js
@@ -0,0 +1,24 @@
+// Verified READY-vendor config for the daily cadence importer.
+// A vendor is import-eligible ONLY if: it appears here with a verified costExpr,
+// vendor_registry.discount_confirmed=true, AND it has net-new costed SKUs.
+//
+// costExpr = a SQL expression (against the catalog table) that yields the REAL COST.
+// Our per-roll retail is then computed as cost / 0.65 / 0.85 (fixed, all vendors).
+// Standard field columns assumed unless overridden: dw_sku, mfr_sku, pattern_name,
+// color_name, collection, width, image_url, product_type, material, roll_length, pattern_repeat.
+//
+// To add a vendor: verify its cost column by inspection, confirm its discount in
+// vendor_registry, add an entry here. NEVER guess a cost column (money).
+
+module.exports = {
+ // direct cost column
+ 'Romo': { table: 'romo_catalog', costExpr: 'cost', soldBy: 'Single Roll', productType: 'Wallcovering' },
+ 'Thibaut': { table: 'thibaut_catalog', costExpr: 'your_cost', soldBy: 'Single Roll', productType: 'Wallcovering' },
+ 'Osborne & Little': { table: 'osborne_catalog', costExpr: 'price_trade', soldBy: 'Single Roll', productType: 'Wallcovering' },
+ 'Designtex': { table: 'designtex_catalog', costExpr: 'price_trade', soldBy: 'Single Roll', productType: 'Wallcovering' },
+ 'Ralph Lauren': { table: 'rl_catalog', costExpr: 'cost', soldBy: 'Single Roll', productType: 'Wallcovering' },
+ // cost derived from vendor retail × (1 - confirmed trade discount)
+ 'York Contract': { table: 'york_contract_catalog', costExpr: 'retail_price * 0.50', soldBy: 'Single Roll', productType: 'Wallcovering', discountNote: '50% off retail (confirmed)' },
+ // WallQuest: price_retail column actually holds the COST (price_dw = our retail, verified 2026-06-11)
+ 'WallQuest': { table: 'wallquest_catalog', costExpr: 'price_retail::numeric', soldBy: 'Single Roll', productType: 'Wallcovering' },
+};
← 0f0ded51 Add fix-24h-new-skus.js: title-case + cost/price + price_upd
·
back to Designer Wallcoverings
·
cadence: Romo cost basis = total_price_per_roll (DTD-B, incl 950348b5 →