← back to Dw Yolo Loop

scripts/kravet-cost/push-anomalies.mjs

46 lines

#!/usr/bin/env node
/**
 * push-anomalies.mjs — push the 59 HELD ROLL anomalies (under-priced rolls) up to MAP.
 * Steve-authorized 2026-06-15 ("push the 59"). These are ROLL-unit wallcoverings currently
 * BELOW MAP (a Kravet compliance violation); raising to MAP fixes it. Reversible.
 *
 * Safety: only touches the roll variant id already recorded; verifies the live price still
 * equals the expected `cur` (skips if it drifted, so we never double-write or clobber a
 * concurrently-changed price). DRY-RUN default; LIVE requires --apply --i-am-steve.
 */
import fs from 'node:fs';
const SHOP='designer-laboratory-sandbox.myshopify.com',VER='2024-10';
const TOKEN=(fs.readFileSync(process.env.HOME+'/Projects/secrets-manager/.env','utf8').match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m)||[])[1]?.trim();
const URL=`https://${SHOP}/admin/api/${VER}/graphql.json`;
const args=Object.fromEntries(process.argv.slice(2).map(a=>{const[k,v]=a.replace(/^--/,'').split('=');return[k,v===undefined?true:v];}));
const APPLY=args.apply===true&&args['i-am-steve']===true;
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
async function gql(q,v){for(let a=0;a<8;a++){let j;try{const r=await fetch(URL,{method:'POST',headers:{'X-Shopify-Access-Token':TOKEN,'Content-Type':'application/json'},body:JSON.stringify({query:q,variables:v})});j=await r.json();}catch(e){await sleep(1500*(a+1));continue;}if(j.errors){if(JSON.stringify(j.errors).includes('THROTTLED')){await sleep(2000*(a+1));continue;}throw new Error(JSON.stringify(j.errors));}return j.data;}throw new Error('retries');}

const anomalies=JSON.parse(fs.readFileSync('data/kravet-cost/price-push-plan.json','utf8')).anomaly||[];
console.log(`push-anomalies — mode: ${APPLY?'⚠️  LIVE APPLY':'DRY-RUN'} | held anomalies: ${anomalies.length}`);

// verify live price still == cur (not stale)
const Q=`query($ids:[ID!]!){nodes(ids:$ids){... on ProductVariant{id price}}}`;
const fresh=[],stale=[];
for(let i=0;i<anomalies.length;i+=100){
  const b=anomalies.slice(i,i+100);
  const d=await gql(Q,{ids:b.map(x=>x.vid)});
  const live=new Map(d.nodes.filter(Boolean).map(n=>[n.id,parseFloat(n.price)]));
  for(const x of b){const lp=live.get(x.vid);
    if(lp!=null&&Math.abs(lp-x.cur)<0.01)fresh.push(x); else stale.push({...x,live:lp});}
}
console.log(`fresh (live == expected current): ${fresh.length} | drifted/skip: ${stale.length}`);
for(const x of fresh.slice(0,12))console.log(`  ${x.mfr}: $${x.cur} -> $${x.map} (${(x.map/x.cur).toFixed(1)}x)`);
if(stale.length)console.log('drifted (will NOT touch):',stale.map(s=>s.mfr).join(', '));
if(!APPLY){console.log('\nDRY-RUN. To execute: node push-anomalies.mjs --apply --i-am-steve');process.exit(0);}

const M=`mutation($pid:ID!,$variants:[ProductVariantsBulkInput!]!){productVariantsBulkUpdate(productId:$pid,variants:$variants){userErrors{field message}}}`;
let ok=0,err=0;
for(const x of fresh){
  try{const d=await gql(M,{pid:x.pid,variants:[{id:x.vid,price:x.map.toFixed(2)}]});
    const ue=d.productVariantsBulkUpdate?.userErrors||[];if(ue.length){err++;console.log('  err',x.mfr,JSON.stringify(ue));}else ok++;
  }catch(e){err++;console.log('  EX',x.mfr,e.message);}
}
console.log(`\nDONE — ${ok} rolls raised to MAP, ${err} errors, ${stale.length} skipped (drifted).`);