← back to Dw Yolo Loop
scripts/cole-son-reprice-2026.js
50 lines
#!/usr/bin/env node
/* Reprice Cole & Son to the CURRENT 2026 MAP (March-7 list, in kravet_authoritative_pricing).
Match by manufacturer_sku → auth new_map, else by title-pattern → pattern's uniform 2026 MAP.
Yard variant only; samples untouched. DRY RUN unless --apply. Writes /tmp/cs_2026_plan.json. */
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 SKU = {}; fs.readFileSync('/tmp/cs_sku_newmap.txt','utf8').trim().split('\n').forEach(l=>{const[s,m]=l.split('|');if(s)SKU[s.trim().toUpperCase()]=parseFloat(m);});
const PAT = {}; fs.readFileSync('/tmp/cs_pattern_newmap.tsv','utf8').trim().split('\n').forEach(l=>{const[p,m]=l.split('\t');if(p)PAT[p.trim().toUpperCase()]=parseFloat(m);});
const norm = s => (s||'').toUpperCase().replace(/\s+/g,' ').trim();
const patternOf = t => norm(t.split('|')[0].trim().split(/\s+-\s+/)[0]);
const isSample = v => (v.option1||'').toLowerCase()==='sample' || /-sample$/i.test(v.sku||'');
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(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 o=[];
while(url){const r=await api(url);const link=r.headers.get('Link')||'';o.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 o; }
(async()=>{
const prods=await getAll(); console.log(`pulled ${prods.length}`);
const plan=[], noMap=[]; 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 code=norm(((mf.metafields||[]).find(x=>x.namespace==='custom'&&x.key==='manufacturer_sku')||{}).value||'');
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 target = SKU[code] ?? PAT[patternOf(p.title)];
if(!target){ noMap.push({title:p.title}); continue; }
if(Math.abs(cur-target)>0.5) plan.push({id:p.id,vid:yard.id,title:p.title,status:p.status,cur,target,via:SKU[code]?'sku':'pattern'});
}
fs.writeFileSync('/tmp/cs_2026_plan.json',JSON.stringify(plan));
const up=plan.filter(x=>x.target>x.cur), dn=plan.filter(x=>x.target<x.cur);
const move=plan.reduce((s,x)=>s+(x.target-x.cur),0);
console.log(`\n=== REPRICE → 2026 MAP | mode: ${APPLY?'APPLY':'DRY RUN'} ===`);
console.log(`need reprice: ${plan.length} (up ${up.length} / down ${dn.length}) | no-MAP: ${noMap.length}`);
console.log(`net $ movement on yard price: ${move>=0?'+':''}$${move.toFixed(0)} (avg ${(move/(plan.length||1)).toFixed(0)}/SKU)`);
console.log('sample:'); plan.slice(0,8).forEach(x=>console.log(` $${x.cur} → $${x.target} (${x.via}) ${x.title.slice(0,40)}`));
if(!APPLY){console.log(`\nDRY RUN — re-run with --apply to write ${plan.length}.`);return;}
let ok=0,fail=0;
for(const x of plan){ try{ const r=await api(`/variants/${x.vid}.json`,{method:'PUT',body:JSON.stringify({variant:{id:x.vid,price:String(x.target)}})}); if(r.ok){ok++;if(ok%50===0)console.log(` …${ok}/${plan.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);});