← back to Dw Yolo Loop

scripts/cole-son-reconcile-pattern.js

71 lines

#!/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); });