← back to Dw Yolo Loop
scripts/kravet-reprice-apply.js
52 lines
#!/usr/bin/env node
/* APPLY the 73 under-MAP Kravet reprices -> MAP via Shopify Admin productVariantsBulkUpdate.
Reads /tmp/kravet_reprice_dryrun.csv (variant_id,current,new) + /tmp/kravet_offmap_paginated.csv
(sku->product gid). Grouped by product. PER-VARIANT GUARD: re-fetch live price first; only write
when live==expected-current (within 1c). Skip if already at MAP (idempotent) or drifted (logged).
Requires APPLY=1 to write; otherwise prints what it WOULD do. Logs every row + reads back. */
const https=require('https'), fs=require('fs');
const T=process.env.T, S='designer-laboratory-sandbox.myshopify.com', APPLY=process.env.APPLY==='1';
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
function gql(q,vars){return new Promise((res,rej)=>{const body=JSON.stringify({query:q,variables:vars||{}});const rq=https.request(`https://${S}/admin/api/2024-10/graphql.json`,{method:'POST',headers:{'X-Shopify-Access-Token':T,'Content-Type':'application/json'}},x=>{let b='';x.on('data',d=>b+=d);x.on('end',()=>{try{res(JSON.parse(b))}catch(e){rej(b.slice(0,300))}})});rq.write(body);rq.end();});}
// dryrun targets (variant-centric — resolve the parent product PER VARIANT to dodge dup-SKU collisions)
const targets=fs.readFileSync('/tmp/kravet_reprice_dryrun.csv','utf8').trim().split('\n').slice(1).map(l=>{
const c=l.split(',');return{vendor:c[0],sku:c[1],unit:c[2],vid:c[3],cur:+c[4],map:+c[5]};});
const MUT=`mutation($pid:ID!,$variants:[ProductVariantsBulkInput!]!){ productVariantsBulkUpdate(productId:$pid,variants:$variants){ productVariants{ id price } userErrors{ field message } } }`;
(async()=>{
let wrote=0, already=0, drift=0, err=0; const log=['vendor,sku,vid,product_id,from,to,result'];
console.log(`MODE: ${APPLY?'APPLY (writing live)':'DRY (no writes — set APPLY=1)'} | variants: ${targets.length}\n`);
// Step 1: resolve each variant's parent product + live price directly (collision-proof)
const byProd={};
for(const t of targets){
const vgid=`gid://shopify/ProductVariant/${t.vid}`;
let s; try{ s=await gql(`{ productVariant(id:"${vgid}"){ id price product{ id status } } }`); }
catch(e){ console.log(` ${t.sku} FETCH-ERR ${e}`); err++; continue; }
const v=s.data?.productVariant;
if(!v){ console.log(` ${t.sku} v${t.vid} MISSING`); log.push(`${t.vendor},${t.sku},${t.vid},,,,MISSING`); err++; continue; }
if(v.product.status!=='ACTIVE'){ console.log(` ${t.sku} not-active skip`); log.push(`${t.vendor},${t.sku},${t.vid},${v.product.id.split('/').pop()},,,SKIP-not-active`); continue; }
const lp=parseFloat(v.price);
if(Math.abs(lp-t.map)<=0.01){ already++; log.push(`${t.vendor},${t.sku},${t.vid},${v.product.id.split('/').pop()},${lp},${t.map},ALREADY-AT-MAP`); continue; }
if(Math.abs(lp-t.cur)>0.01){ drift++; console.log(` ${t.sku} DRIFT live=$${lp} expected=$${t.cur} — SKIP`); log.push(`${t.vendor},${t.sku},${t.vid},${v.product.id.split('/').pop()},${lp},${t.map},DRIFT-SKIP`); continue; }
(byProd[v.product.id]=byProd[v.product.id]||[]).push({...t,vgid,lp});
await sleep(120);
}
// Step 2: bulk-update per resolved product
for(const [pid,ups] of Object.entries(byProd)){
if(!APPLY){ ups.forEach(u=>{console.log(` WOULD set ${u.sku} $${u.cur}->$${u.map} (prod ${pid.split('/').pop()})`);log.push(`${u.vendor},${u.sku},${u.vid},${pid.split('/').pop()},${u.cur},${u.map},WOULD-WRITE`);}); continue; }
const r=await gql(MUT,{pid,variants:ups.map(u=>({id:u.vgid,price:String(u.map)}))});
const ue=r.data?.productVariantsBulkUpdate?.userErrors||[];
if(ue.length){ console.log(` ${ups[0].sku} ERR ${JSON.stringify(ue)}`); ups.forEach(u=>log.push(`${u.vendor},${u.sku},${u.vid},${pid.split('/').pop()},${u.cur},${u.map},ERR`)); err+=ups.length; continue; }
const back={}; (r.data.productVariantsBulkUpdate.productVariants||[]).forEach(v=>back[v.id]=parseFloat(v.price));
for(const u of ups){ const nv=back[u.vgid]; const okv=Math.abs(nv-u.map)<=0.01;
console.log(` ${okv?'✓':'✗'} ${u.sku} $${u.cur}->$${nv}`); wrote+=okv?1:0; if(!okv)err++;
log.push(`${u.vendor},${u.sku},${u.vid},${pid.split('/').pop()},${u.cur},${nv},${okv?'WROTE':'VERIFY-FAIL'}`); }
await sleep(300);
}
fs.writeFileSync('/tmp/kravet_reprice_applied.csv',log.join('\n'));
console.log(`\n=== ${APPLY?'APPLIED':'DRY'} === wrote:${wrote} already-at-MAP:${already} drift-skip:${drift} err:${err}`);
console.log(`log -> /tmp/kravet_reprice_applied.csv`);
})().catch(e=>{console.error(e);process.exit(1);});