← back to Dw Yolo Loop
Cole & Son MAP reconcile by title-pattern (248/249 patterns uniform MAP); reprice 789 over-MAP products to MAP
b51969d763324770a1f8b29dd8fc26319dbb3437 · 2026-06-12 15:19:45 -0700 · Steve Abrams
Files touched
A scripts/cole-son-apply-pattern-map.jsA scripts/cole-son-reconcile-pattern.js
Diff
commit b51969d763324770a1f8b29dd8fc26319dbb3437
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri Jun 12 15:19:45 2026 -0700
Cole & Son MAP reconcile by title-pattern (248/249 patterns uniform MAP); reprice 789 over-MAP products to MAP
---
scripts/cole-son-apply-pattern-map.js | 53 ++++++++++++++++++++++++++
scripts/cole-son-reconcile-pattern.js | 70 +++++++++++++++++++++++++++++++++++
2 files changed, 123 insertions(+)
diff --git a/scripts/cole-son-apply-pattern-map.js b/scripts/cole-son-apply-pattern-map.js
new file mode 100644
index 0000000..4c08965
--- /dev/null
+++ b/scripts/cole-son-apply-pattern-map.js
@@ -0,0 +1,53 @@
+#!/usr/bin/env node
+/* Price the remaining Cole & Son products (no SKU/pattern metafields) by parsing the PATTERN
+ from the title and looking up its uniform MAP (248/249 Cole & Son patterns have one MAP).
+ Reads /tmp/cs_recon.json (noMatch list) + /tmp/cs_pattern_map.tsv. DRY RUN unless --apply. */
+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 PMAP = {};
+fs.readFileSync('/tmp/cs_pattern_map.tsv', 'utf8').trim().split('\n').forEach(l => { const [p, m] = l.split('\t'); if (p) PMAP[p.trim().toUpperCase()] = parseFloat(m); });
+
+// "Acacia - Blue & Green Multi | Cole & Son" → pattern "ACACIA"
+function patternOf(title) {
+ let t = title.split('|')[0].trim(); // drop "| Cole & Son ..."
+ t = t.split(/\s+-\s+/)[0].trim(); // pattern is before " - colorway"
+ return t.toUpperCase().replace(/\s+/g, ' ').trim();
+}
+
+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(95); return r;
+ }
+ throw new Error('fail ' + p);
+}
+
+(async () => {
+ const recon = JSON.parse(fs.readFileSync('/tmp/cs_recon.json', 'utf8'));
+ const cand = recon.noMatch;
+ const matched = [], unmatched = [];
+ for (const x of cand) {
+ const pat = patternOf(x.title);
+ const map = PMAP[pat];
+ if (map && Math.abs(x.cur - map) > 0.5) matched.push({ ...x, pat, map });
+ else if (map) { /* already at MAP */ }
+ else unmatched.push({ ...x, pat });
+ }
+ console.log(`no-match pool: ${cand.length} | pattern-matched & need reprice: ${matched.length} | still unmatched: ${unmatched.length} | mode: ${APPLY ? 'APPLY' : 'DRY RUN'}\n`);
+ matched.slice(0, 10).forEach(x => console.log(` [${x.status}] $${x.cur} → $${x.map} (${x.pat}) ${x.title.slice(0,40)}`));
+ console.log('\nstill-unmatched patterns (sample):', [...new Set(unmatched.map(u => u.pat))].slice(0, 15).join(' | '));
+ if (!APPLY) { console.log(`\nDRY RUN — re-run with --apply to reprice ${matched.length}.`); return; }
+ let ok = 0, fail = 0;
+ for (const x of matched) {
+ 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}/${matched.length}`); } else { fail++; console.log(` FAIL ${x.id} ${r.status}`); }
+ } catch (e) { fail++; }
+ }
+ console.log(`\nDONE — ${ok} repriced, ${fail} failed.`);
+})().catch(e => { console.error(e); process.exit(1); });
diff --git a/scripts/cole-son-reconcile-pattern.js b/scripts/cole-son-reconcile-pattern.js
new file mode 100644
index 0000000..df5f5b1
--- /dev/null
+++ b/scripts/cole-son-reconcile-pattern.js
@@ -0,0 +1,70 @@
+#!/usr/bin/env node
+/* Reconcile Cole & Son to MAP by manufacturer_sku FIRST, then by pattern+colorway
+ (covers the ~897 products missing a proper Cole & Son SKU). Writes /tmp/cs_recon.json.
+ MAP source: kravet_master_price (COLE & SON). Read-only audit. */
+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));
+
+// sku → map
+const SKU = {};
+fs.readFileSync('/tmp/cs_map.txt', 'utf8').trim().split('\n').forEach(l => { const [s, m] = l.split('|'); if (s) SKU[s.trim().toUpperCase()] = parseFloat(m); });
+// pattern|color → map
+const PC = {};
+fs.readFileSync('/tmp/cs_pc_map.tsv', 'utf8').trim().split('\n').forEach(l => {
+ const [pat, col, m] = l.split('\t'); if (pat) PC[`${pat}||${col}`] = parseFloat(m);
+});
+const norm = s => (s || '').toUpperCase().replace(/\s+/g, ' ').trim();
+
+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(85); return r;
+ }
+ throw new Error('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(`reconciling ${prods.length} Cole & Son...`);
+ const r = { bySku: [], byPattern: [], noMatch: [] };
+ 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 get = k => { const m = (mf.metafields || []).find(x => x.namespace === 'custom' && x.key === k); return m ? String(m.value) : ''; };
+ const code = norm(get('manufacturer_sku'));
+ const pat = norm(get('pattern_name') || get('name_of_pattern'));
+ const col = norm(get('color') || get('colorway_name'));
+ const yard = p.variants.filter(v => !isSample(v)).sort((a, b) => parseFloat(b.price) - parseFloat(a.price))[0] || p.variants[0];
+ const cur = parseFloat(yard.price);
+ const rec = { id: p.id, vid: yard.id, title: p.title, status: p.status, cur };
+ let map = SKU[code];
+ if (map) { rec.map = map; rec.via = 'sku'; r.bySku.push(rec); continue; }
+ map = PC[`${pat}||${col}`];
+ if (map) { rec.map = map; rec.via = 'pattern+color'; r.byPattern.push(rec); continue; }
+ rec.pat = pat; rec.col = col; r.noMatch.push(rec);
+ }
+ fs.writeFileSync('/tmp/cs_recon.json', JSON.stringify(r, null, 0));
+ const live = a => a.filter(x => x.status === 'active').length;
+ const needFix = a => a.filter(x => Math.abs(x.cur - x.map) > 0.5);
+ console.log('\n=== COLE & SON RECONCILE ===');
+ console.log(`matched by SKU: ${r.bySku.length} (live ${live(r.bySku)}) — need reprice: ${needFix(r.bySku).length}`);
+ console.log(`matched by pattern+color: ${r.byPattern.length} (live ${live(r.byPattern)}) — need reprice: ${needFix(r.byPattern).length}`);
+ console.log(`NO match: ${r.noMatch.length} (live ${live(r.noMatch)})`);
+ const allFix = needFix([...r.bySku, ...r.byPattern]);
+ console.log(`\nTOTAL needing reprice → MAP: ${allFix.length} (of which $4.25 now: ${allFix.filter(x=>x.cur<=4.25).length})`);
+ console.log('\nsample pattern-matched fixes:');
+ needFix(r.byPattern).slice(0, 8).forEach(x => console.log(` $${x.cur} → $${x.map} ${x.title.slice(0,45)}`));
+ console.log('\nsample NO-match (need manual):');
+ r.noMatch.slice(0, 6).forEach(x => console.log(` "${x.pat}" / "${x.col}" ${x.title.slice(0,40)}`));
+})().catch(e => { console.error(e); process.exit(1); });
← 783ea1b Brands page: 62 real logos (vendor-logos CDN set + 14 dig-an
·
back to Dw Yolo Loop
·
Brands page: delete+recreate canonical (still cache-frozen); fca0d80 →