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