← back to Dw Yolo Loop

scripts/cole-son-price-audit.mjs

53 lines

#!/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. */
import fs from 'node:fs';
import { rest, restAll } from './lib/shopify.mjs';

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);
});

const getAll = () => restAll(`/products.json?vendor=Cole%20%26%20Son&limit=250&fields=id,title,status,variants`, 'products');
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 rest(`/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); });