[object Object]

← 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

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 →