[object Object]

← 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

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 →