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