← back to Dw Yolo Loop
Schumacher title fix (517 doubled-Wallcoverings) + Cole&Son MAP audit/apply + build-line title-strip handles redundant Wallcoverings
c106b30ac14a91247097675e59a796929ef2e25a · 2026-06-12 13:56:30 -0700 · Steve Abrams
Files touched
M artmura-site/build-line.jsA scripts/cole-son-apply-map.jsA scripts/cole-son-price-audit.jsA scripts/fix-schumacher-double-wallcovering.js
Diff
commit c106b30ac14a91247097675e59a796929ef2e25a
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri Jun 12 13:56:30 2026 -0700
Schumacher title fix (517 doubled-Wallcoverings) + Cole&Son MAP audit/apply + build-line title-strip handles redundant Wallcoverings
---
artmura-site/build-line.js | 3 +-
scripts/cole-son-apply-map.js | 36 ++++++++++++++
scripts/cole-son-price-audit.js | 72 +++++++++++++++++++++++++++
scripts/fix-schumacher-double-wallcovering.js | 52 +++++++++++++++++++
4 files changed, 162 insertions(+), 1 deletion(-)
diff --git a/artmura-site/build-line.js b/artmura-site/build-line.js
index 97cc23f..2af85c0 100644
--- a/artmura-site/build-line.js
+++ b/artmura-site/build-line.js
@@ -73,7 +73,8 @@ async function metafields(pid) {
const yard = nonSample.sort((a, b) => parseFloat(b.price) - parseFloat(a.price))[0] || p.variants[0];
const sku = (yard.sku || '').trim().replace(/-sample$/i, ''); // sample-only products: drop the -SAMPLE suffix
const tags = (p.tags || '').split(',').map(s => s.trim()).filter(Boolean);
- const baseTitle = p.title.replace(/\s*\|\s*[^|]+$/, '').replace(/\s+Wallcovering$/i, '');
+ // strip the trailing " | Vendor" and any redundant trailing "Wallcovering(s)" (incl. doubled)
+ const baseTitle = p.title.replace(/\s*\|\s*[^|]+$/, '').replace(/(\s+Wallcoverings?){1,2}\s*$/i, '').trim();
// skip placeholder/junk products: title is just the vendor name (e.g. "Schumacher | Schumacher
// Wallcovering") or no image — these have no real pattern and pollute the lookbook.
const cleanBase = baseTitle.replace(/\s+/g, ' ').trim().toLowerCase();
diff --git a/scripts/cole-son-apply-map.js b/scripts/cole-son-apply-map.js
new file mode 100644
index 0000000..fcf225c
--- /dev/null
+++ b/scripts/cole-son-apply-map.js
@@ -0,0 +1,36 @@
+#!/usr/bin/env node
+/* Apply MAP pricing to Cole & Son from the audit (/tmp/cs_audit.json).
+ - fixable: $4.25 sample-only → set yard variant to MAP
+ - offMap: real price ≠ MAP → set yard variant to MAP (Kravet-family prices AT MAP)
+ Samples stay $4.25. Yard variant only. DRY RUN by default; --apply writes live. */
+const fs = require('fs');
+const STORE = 'designer-laboratory-sandbox.myshopify.com';
+const TOKEN = process.env.T;
+const APPLY = process.argv.includes('--apply');
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+const a = JSON.parse(fs.readFileSync('/tmp/cs_audit.json', 'utf8'));
+const todo = [...a.fixable, ...a.offMap].filter(x => x.map > 0 && x.vid);
+
+async function api(p, opts = {}, tries = 5) {
+ for (let i = 0; i < tries; i++) {
+ const r = await fetch(`https://${STORE}/admin/api/2024-10${p}`, { ...opts, headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json', ...(opts.headers || {}) } });
+ if (r.status === 429 || r.status >= 500) { await sleep(1500 * (i + 1)); continue; }
+ await sleep(100); return r;
+ }
+ throw new Error('api fail ' + p);
+}
+
+(async () => {
+ console.log(`Cole & Son MAP apply — ${todo.length} variants (${a.fixable.length} $4.25→MAP + ${a.offMap.length} off-MAP→MAP) | mode: ${APPLY ? 'APPLY' : 'DRY RUN'}\n`);
+ todo.slice(0, 8).forEach(x => console.log(` ${x.code} $${x.cur} → $${x.map} [${x.status}] ${x.title.slice(0,40)}`));
+ if (!APPLY) { console.log(`\nDRY RUN — re-run with --apply.`); return; }
+ let ok = 0, fail = 0;
+ for (const x of todo) {
+ try {
+ const r = await api(`/variants/${x.vid}.json`, { method: 'PUT', body: JSON.stringify({ variant: { id: x.vid, price: String(x.map) } }) });
+ if (r.ok) { ok++; if (ok % 50 === 0) console.log(` …${ok}/${todo.length}`); }
+ else { fail++; console.log(` FAIL ${x.code} ${r.status}`); }
+ } catch (e) { fail++; console.log(` ERR ${x.code} ${e.message}`); }
+ }
+ console.log(`\nDONE — ${ok} repriced, ${fail} failed.`);
+})().catch(e => { console.error(e); process.exit(1); });
diff --git a/scripts/cole-son-price-audit.js b/scripts/cole-son-price-audit.js
new file mode 100644
index 0000000..48022e3
--- /dev/null
+++ b/scripts/cole-son-price-audit.js
@@ -0,0 +1,72 @@
+#!/usr/bin/env node
+/* Read-only Cole & Son pricing audit: join every live product to MAP (kravet_master_price,
+ MAP = wholesale x 1.5) via the custom.manufacturer_sku metafield. Classifies:
+ - sample-only $4.25 with a MAP available → fixable (set yard variant to MAP)
+ - real price that differs from MAP → off-MAP (over/under)
+ - no MAP match → can't auto-fix
+ Writes /tmp/cs_audit.json with the full fix plan. NO writes to Shopify. */
+const fs = require('fs');
+const STORE = 'designer-laboratory-sandbox.myshopify.com';
+const TOKEN = process.env.T;
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+const MAP = {};
+fs.readFileSync('/tmp/cs_map.txt', 'utf8').trim().split('\n').forEach(l => {
+ const [sku, m] = l.split('|'); if (sku) MAP[sku.trim().toUpperCase()] = parseFloat(m);
+});
+
+async function api(p, tries = 5) {
+ for (let i = 0; i < tries; i++) {
+ const r = await fetch(`https://${STORE}/admin/api/2024-10${p}`, { headers: { 'X-Shopify-Access-Token': TOKEN } });
+ if (r.status === 429 || r.status >= 500) { await sleep(1500 * (i + 1)); continue; }
+ await sleep(90); return r;
+ }
+ throw new Error('api fail ' + p);
+}
+async function getAll() {
+ let url = `/products.json?vendor=Cole%20%26%20Son&limit=250&fields=id,title,status,variants`;
+ const out = [];
+ while (url) {
+ const r = await api(url); const link = r.headers.get('Link') || '';
+ out.push(...((await r.json()).products || []));
+ const m = link.split(',').find(s => s.includes('rel="next"'));
+ url = m ? m.slice(m.indexOf('<') + 1, m.indexOf('>')).replace(/^https:\/\/[^/]+\/admin\/api\/[^/]+/, '') : null;
+ }
+ return out;
+}
+const isSample = v => (v.option1 || '').toLowerCase() === 'sample' || /-sample$/i.test(v.sku || '');
+
+(async () => {
+ const prods = await getAll();
+ console.log(`auditing ${prods.length} Cole & Son products...`);
+ const r = { fixable: [], offMap: [], noMap: [], ok: [] };
+ let i = 0;
+ for (const p of prods) {
+ if (++i % 200 === 0) console.log(` …${i}/${prods.length}`);
+ const mf = await (await api(`/products/${p.id}/metafields.json`)).json();
+ const msku = (mf.metafields || []).find(m => m.namespace === 'custom' && m.key === 'manufacturer_sku');
+ const code = msku ? String(msku.value).trim().toUpperCase() : null;
+ const map = code ? MAP[code] : null;
+ const yard = p.variants.filter(v => !isSample(v)).sort((a, b) => parseFloat(b.price) - parseFloat(a.price))[0] || p.variants[0];
+ const yardPrice = parseFloat(yard.price);
+ const rec = { id: p.id, vid: yard.id, title: p.title, status: p.status, code, map, cur: yardPrice };
+ if (!map) { r.noMap.push(rec); continue; }
+ if (yardPrice <= 4.25) r.fixable.push(rec); // sample-only, MAP available
+ else if (Math.abs(yardPrice - map) > 0.5) r.offMap.push(rec); // real price ≠ MAP
+ else r.ok.push(rec); // at MAP
+ }
+ fs.writeFileSync('/tmp/cs_audit.json', JSON.stringify(r, null, 0));
+ const sum = k => r[k].length;
+ const active = a => a.filter(x => x.status === 'active').length;
+ console.log('\n=== COLE & SON PRICING AUDIT ===');
+ console.log(`at MAP (correct): ${sum('ok')} (active ${active(r.ok)})`);
+ console.log(`FIXABLE ($4.25 → MAP): ${sum('fixable')} (active+live ${active(r.fixable)})`);
+ console.log(`OFF-MAP (real ≠ MAP): ${sum('offMap')} (active ${active(r.offMap)})`);
+ console.log(`no MAP match: ${sum('noMap')} (active ${active(r.noMap)})`);
+ const overMap = r.offMap.filter(x => x.cur > x.map);
+ console.log(` of off-MAP, OVER map: ${overMap.length} (total $${overMap.reduce((s,x)=>s+(x.cur-x.map),0).toFixed(0)} above MAP)`);
+ console.log('\nsample FIXABLE (now $4.25 → should be MAP):');
+ r.fixable.slice(0, 6).forEach(x => console.log(` ${x.code} $4.25 → $${x.map} ${x.title.slice(0,45)}`));
+ console.log('\nsample OFF-MAP:');
+ r.offMap.slice(0, 6).forEach(x => console.log(` ${x.code} $${x.cur} → MAP $${x.map} (${x.cur>x.map?'+':''}${(x.cur-x.map).toFixed(0)}) ${x.title.slice(0,40)}`));
+})().catch(e => { console.error(e); process.exit(1); });
diff --git a/scripts/fix-schumacher-double-wallcovering.js b/scripts/fix-schumacher-double-wallcovering.js
new file mode 100644
index 0000000..f684584
--- /dev/null
+++ b/scripts/fix-schumacher-double-wallcovering.js
@@ -0,0 +1,52 @@
+#!/usr/bin/env node
+/* Fix Schumacher product titles with a redundant doubled "Wallcoverings Wallcovering(s)".
+ "{name} Wallcoverings Wallcovering | Schumacher" → "{name} Wallcovering | Schumacher".
+ DRY RUN by default; --apply writes live (PUT title only). Read-modify is title-only, idempotent. */
+const STORE = 'designer-laboratory-sandbox.myshopify.com';
+const TOKEN = process.env.T;
+const APPLY = process.argv.includes('--apply');
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+// collapse any run of Wallcovering(s) Wallcovering(s)[ ...] → single "Wallcovering"
+function fixTitle(t) {
+ let f = t.replace(/\bWallcoverings?\s+Wallcoverings?\b/gi, 'Wallcovering');
+ f = f.replace(/\s{2,}/g, ' ').replace(/\s+\|/g, ' |').trim();
+ return f;
+}
+
+async function api(p, opts = {}, tries = 5) {
+ for (let i = 0; i < tries; i++) {
+ const r = await fetch(`https://${STORE}/admin/api/2024-10${p}`, { ...opts, headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json', ...(opts.headers || {}) } });
+ if (r.status === 429 || r.status >= 500) { await sleep(1500 * (i + 1)); continue; }
+ await sleep(90); return r;
+ }
+ throw new Error('api fail ' + p);
+}
+async function getAll() {
+ let url = `/products.json?vendor=Schumacher&limit=250&fields=id,title,status`;
+ const out = [];
+ while (url) {
+ const r = await api(url); const link = r.headers.get('Link') || '';
+ out.push(...((await r.json()).products || []));
+ const m = link.split(',').find(s => s.includes('rel="next"'));
+ url = m ? m.slice(m.indexOf('<') + 1, m.indexOf('>')).replace(/^https:\/\/[^/]+\/admin\/api\/[^/]+/, '') : null;
+ }
+ return out;
+}
+
+(async () => {
+ const all = await getAll();
+ const todo = all.map(p => ({ ...p, fixed: fixTitle(p.title) })).filter(p => p.fixed !== p.title);
+ console.log(`Schumacher: ${all.length} | titles to fix: ${todo.length} | mode: ${APPLY ? 'APPLY' : 'DRY RUN'}\n`);
+ todo.slice(0, 10).forEach(p => console.log(` ${p.title}\n → ${p.fixed}\n`));
+ if (!APPLY) { console.log(`DRY RUN — re-run with --apply to write ${todo.length} titles.`); return; }
+ let ok = 0, fail = 0;
+ for (const p of todo) {
+ try {
+ const r = await api(`/products/${p.id}.json`, { method: 'PUT', body: JSON.stringify({ product: { id: p.id, title: p.fixed } }) });
+ if (r.ok) { ok++; if (ok % 50 === 0) console.log(` …${ok}/${todo.length}`); }
+ else { fail++; console.log(` FAIL ${p.id} ${r.status}`); }
+ } catch (e) { fail++; console.log(` ERR ${p.id} ${e.message}`); }
+ }
+ console.log(`\nDONE — ${ok} fixed, ${fail} failed.`);
+})().catch(e => { console.error(e); process.exit(1); });
← ee9be2e Brands page redesign: pill-tile logo grid for /pages/brands
·
back to Dw Yolo Loop
·
Brands page: 62 real logos (vendor-logos CDN set + 14 dig-an 783ea1b →