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