← back to Dw Yolo Loop
Kravet under-MAP reprice: 73 variants lifted to MAP (live, verified); variant-centric resolver dodges dup-SKU products; sample/unit/drift guards
18fc2856df23a20d7d7513f7cd67b2d8a1c69589 · 2026-06-15 10:20:07 -0700 · Steve Abrams
Files touched
A scripts/kravet-reprice-apply.jsA scripts/kravet-reprice-dryrun.js
Diff
commit 18fc2856df23a20d7d7513f7cd67b2d8a1c69589
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Jun 15 10:20:07 2026 -0700
Kravet under-MAP reprice: 73 variants lifted to MAP (live, verified); variant-centric resolver dodges dup-SKU products; sample/unit/drift guards
---
scripts/kravet-reprice-apply.js | 51 ++++++++++++++++++++++++++++++++++++++++
scripts/kravet-reprice-dryrun.js | 47 ++++++++++++++++++++++++++++++++++++
2 files changed, 98 insertions(+)
diff --git a/scripts/kravet-reprice-apply.js b/scripts/kravet-reprice-apply.js
new file mode 100644
index 0000000..45c8e5e
--- /dev/null
+++ b/scripts/kravet-reprice-apply.js
@@ -0,0 +1,51 @@
+#!/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);});
diff --git a/scripts/kravet-reprice-dryrun.js b/scripts/kravet-reprice-dryrun.js
new file mode 100644
index 0000000..c94b94e
--- /dev/null
+++ b/scripts/kravet-reprice-dryrun.js
@@ -0,0 +1,47 @@
+#!/usr/bin/env node
+/* DRY-RUN ONLY. For the 73 under-MAP Kravet-family products (from /tmp/kravet_offmap_paginated.csv),
+ re-fetch variants, pinpoint the exact REAL variant to reprice (non-sample, price==measured cur),
+ and emit the proposed price change cur->MAP. Writes /tmp/kravet_reprice_dryrun.csv. NO WRITES.
+ Guards: sample variants ($4.25 / option Sample) are NEVER the target; unit was already matched
+ upstream; only UNDER-MAP rows (delta<0) are repriced UP to MAP (MAP is a floor). */
+const https=require('https'), fs=require('fs');
+const T=process.env.T, S='designer-laboratory-sandbox.myshopify.com';
+const sleep=ms=>new Promise(r=>setTimeout(r,ms));
+function gql(q){return new Promise((res,rej)=>{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,200))}})});rq.write(JSON.stringify({query:q}));rq.end();});}
+const isSample=opts=>(opts||[]).some(o=>String(o.value).toLowerCase()==='sample');
+
+const rows=fs.readFileSync('/tmp/kravet_offmap_paginated.csv','utf8').trim().split('\n').slice(1)
+ .map(l=>{const[vendor,sku,unit,cur,map,delta,gid]=l.split(',');return{vendor,sku,unit,cur:+cur,map:+map,delta:+delta,gid};})
+ .filter(r=>r.delta<0); // UNDER-MAP only
+
+(async()=>{
+ const out=['vendor,mfr_sku,unit,variant_id,current_price,new_price_MAP,uplift,product_title'];
+ let ok=0, ambiguous=0, totalUplift=0;
+ const warn=[];
+ for(const r of rows){
+ const q=`{ product(id:"${r.gid}"){ title status
+ variants(first:25){ edges{ node{ id title price selectedOptions{ value } } } } } }`;
+ let s; try{ s=await gql(q); }catch(e){ warn.push(`${r.sku} FETCH-ERR ${e}`); continue; }
+ const p=s.data?.product; if(!p){ warn.push(`${r.sku} no-product`); continue; }
+ if(p.status!=='ACTIVE'){ warn.push(`${r.sku} not-active(${p.status})-SKIP`); continue; }
+ const vs=p.variants.edges.map(e=>e.node).filter(v=>!isSample(v.selectedOptions) && parseFloat(v.price)>5);
+ // target = the real variant whose price matches the measured 'cur' (the under-MAP one)
+ const cand=vs.filter(v=>Math.abs(parseFloat(v.price)-r.cur)<=0.01);
+ if(cand.length!==1){ ambiguous++; warn.push(`${r.sku} AMBIGUOUS (${cand.length} variants @ $${r.cur}) — SKIP, needs eyes`); continue; }
+ const v=cand[0];
+ const uplift=+(r.map-parseFloat(v.price)).toFixed(2);
+ totalUplift+=uplift; ok++;
+ out.push(`${r.vendor},${r.sku},${r.unit},${v.id.split('/').pop()},${v.price},${r.map},${uplift.toFixed(2)},"${p.title.replace(/"/g,'')}"`);
+ await sleep(250);
+ }
+ fs.writeFileSync('/tmp/kravet_reprice_dryrun.csv',out.join('\n'));
+ console.log('=== KRAVET UNDER-MAP REPRICE — DRY RUN (no writes) ===');
+ console.log(`under-MAP candidates: ${rows.length}`);
+ console.log(`clean (1 unambiguous real variant): ${ok} | ambiguous/skipped: ${ambiguous}`);
+ console.log(`total uplift to bring to MAP: +$${totalUplift.toFixed(2)}`);
+ // per-vendor summary
+ const per={}; out.slice(1).forEach(l=>{const c=l.split(','); per[c[0]]=(per[c[0]]||0)+1;});
+ console.log('per-vendor:'); for(const [v,n] of Object.entries(per)) console.log(` ${v.padEnd(20)} ${n}`);
+ if(warn.length){ console.log('\nWARNINGS / skipped:'); warn.forEach(w=>console.log(' '+w)); }
+ console.log(`\nfull proposed diff → /tmp/kravet_reprice_dryrun.csv`);
+})().catch(e=>{console.error(e);process.exit(1);});
← f57943b Kravet off-MAP: read-only paginated measurer (no bulk-op, no
·
back to Dw Yolo Loop
·
Ingest GDrive vendor price sheets into dw_unified.vendor_pri 1856ee2 →