[object Object]

← back to Dw Yolo Loop

Kravet MAP integrity tools: coverage audit (GraphQL, REST-metafields-broken), unit-aware off-MAP delta, sample-trap exclusion, 1104 unit-verified reprice to 2026 MAP

394aa03c0fb3c23ce9ddea1b1e8e8b0aedf7d8f0 · 2026-06-14 07:48:10 -0700 · Steve Abrams

Files touched

Diff

commit 394aa03c0fb3c23ce9ddea1b1e8e8b0aedf7d8f0
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sun Jun 14 07:48:10 2026 -0700

    Kravet MAP integrity tools: coverage audit (GraphQL, REST-metafields-broken), unit-aware off-MAP delta, sample-trap exclusion, 1104 unit-verified reprice to 2026 MAP
---
 scripts/apply-1104-map.js          | 34 +++++++++++++++++++++
 scripts/kravet-offmap-unitaware.js | 62 ++++++++++++++++++++++++++++++++++++++
 2 files changed, 96 insertions(+)

diff --git a/scripts/apply-1104-map.js b/scripts/apply-1104-map.js
new file mode 100644
index 0000000..894211f
--- /dev/null
+++ b/scripts/apply-1104-map.js
@@ -0,0 +1,34 @@
+#!/usr/bin/env node
+/* Apply 2026 MAP to the 1,104 unit-verified under-MAP products (/tmp/cs_1104_plan.json).
+   Per product: GraphQL-read variants, pick the NON-SAMPLE (Price Per Unit) variant, set its price
+   to MAP via productVariantsBulkUpdate. Sample variant untouched. DRY RUN unless --apply. */
+const https=require('https'); const fs=require('fs');
+const TOKEN=process.env.T, STORE='designer-laboratory-sandbox.myshopify.com';
+const APPLY=process.argv.includes('--apply');
+const sleep=ms=>new Promise(r=>setTimeout(r,ms));
+function gql(q,vars){return new Promise(r=>{const rq=https.request(`https://${STORE}/admin/api/2024-10/graphql.json`,{method:'POST',headers:{'X-Shopify-Access-Token':TOKEN,'Content-Type':'application/json'}},x=>{let b='';x.on('data',d=>b+=d);x.on('end',()=>{try{r(JSON.parse(b))}catch(e){r({errors:b})}})});rq.write(JSON.stringify({query:q,variables:vars||{}}));rq.end();});}
+const plan=JSON.parse(fs.readFileSync('/tmp/cs_1104_plan.json','utf8'));
+const isSample=o=>/sample/i.test(o||'');
+
+(async()=>{
+  console.log(`${plan.length} products | mode: ${APPLY?'APPLY':'DRY RUN'}`);
+  let ok=0,fail=0,skip=0;
+  for(const it of plan){
+    try{
+      const d=await gql(`query($id:ID!){ product(id:$id){ variants(first:10){edges{node{id price selectedOptions{value}}}} } }`,{id:it.gid});
+      const vs=(d.data?.product?.variants?.edges||[]).map(e=>e.node);
+      const real=vs.filter(v=>!v.selectedOptions.some(o=>isSample(o.value)));
+      const v=real.sort((a,b)=>parseFloat(b.price)-parseFloat(a.price))[0];
+      if(!v){ skip++; continue; }
+      if(Math.abs(parseFloat(v.price)-it.map)<=0.5){ skip++; continue; } // already at MAP
+      if(!APPLY){ ok++; if(ok<=8)console.log(`  $${v.price} -> $${it.map}  ${it.vendor} ${it.sku}`); continue; }
+      const m=await gql(`mutation($pid:ID!,$vars:[ProductVariantsBulkInput!]!){ productVariantsBulkUpdate(productId:$pid, variants:$vars){ userErrors{field message} } }`,
+        {pid:it.gid, vars:[{id:v.id, price:String(it.map)}]});
+      const errs=m.data?.productVariantsBulkUpdate?.userErrors;
+      if(errs&&errs.length){ fail++; if(fail<=5)console.log('  ERR',it.sku,JSON.stringify(errs)); }
+      else { ok++; if(ok%100===0)console.log(`  …${ok}/${plan.length}`); }
+    }catch(e){ fail++; }
+    await sleep(60);
+  }
+  console.log(`\nDONE — ${ok} ${APPLY?'repriced':'would reprice'}, ${fail} failed, ${skip} skipped(already-at-MAP/no-variant).`);
+})().catch(e=>{console.error(e);process.exit(1);});
diff --git a/scripts/kravet-offmap-unitaware.js b/scripts/kravet-offmap-unitaware.js
new file mode 100644
index 0000000..b8aac1e
--- /dev/null
+++ b/scripts/kravet-offmap-unitaware.js
@@ -0,0 +1,62 @@
+#!/usr/bin/env node
+/* UNIT-AWARE off-MAP: poll bulk op → for each Kravet ACTIVE product, take the REAL (non-sample)
+   variant, derive its unit from the option name (Yard/Roll/Bolt/SqFt/Each/Panel), and compare its
+   price to the 2026 MAP ONLY when the variant's unit == the MAP's unit (from the Jan sheet).
+   Buckets: unit-matched off-MAP (TRUSTWORTHY) | unit-mismatch (suspect) | unit-unknown | sample-only.
+   READ-ONLY. Writes /tmp/kravet_offmap_trusted.csv (vendor,sku,unit,cur,map,delta,gid). */
+const https=require('https'); const fs=require('fs');
+const TOKEN=process.env.T, STORE='designer-laboratory-sandbox.myshopify.com';
+const sleep=ms=>new Promise(r=>setTimeout(r,ms));
+function gql(q){return new Promise(r=>{const rq=https.request(`https://${STORE}/admin/api/2024-10/graphql.json`,{method:'POST',headers:{'X-Shopify-Access-Token':TOKEN,'Content-Type':'application/json'}},x=>{let b='';x.on('data',d=>b+=d);x.on('end',()=>r(JSON.parse(b)))});rq.write(JSON.stringify({query:q}));rq.end();});}
+function download(url){return new Promise((res,rej)=>{https.get(url,r=>{let b='';r.on('data',d=>b+=d);r.on('end',()=>res(b));}).on('error',rej);});}
+
+const MAP={}; fs.readFileSync('/tmp/all_sku_newmap.txt','utf8').trim().split('\n').forEach(l=>{const[s,m]=l.split('|');if(s)MAP[s.trim().toUpperCase()]=parseFloat(m);});
+const UNIT=JSON.parse(fs.readFileSync('/tmp/sku_unit.json','utf8'));   // sku → YARD/ROLL/SQUARE FOOT/EACH
+const KFAM=new Set(['Kravet','Kravet Couture','Kravet Design','Lee Jofa','Lee Jofa Modern','Brunschwig & Fils','Cole & Son','GP & J Baker','Clarke And Clarke','Mulberry','Threads','Baker Lifestyle','Gaston y Daniela','Andrew Martin']);
+const norm=s=>(s||'').toUpperCase().replace(/\s+/g,' ').trim();
+function unitOf(opt){const o=(opt||'').toLowerCase();
+  if(/sample/.test(o))return 'SAMPLE';
+  if(/yard/.test(o))return 'YARD';
+  if(/roll|bolt/.test(o))return 'ROLL';
+  if(/sq|square|foot|sf\b/.test(o))return 'SQUARE FOOT';
+  if(/panel|mural/.test(o))return 'PANEL';
+  if(/each|tile|set/.test(o))return 'EACH';
+  return 'OTHER';}
+
+(async()=>{
+  let url=null;
+  for(let i=0;i<180;i++){const s=await gql(`{ currentBulkOperation{ status objectCount url errorCode } }`);const op=s.data?.currentBulkOperation;
+    if(i%4===0)console.log(`  poll ${i}: ${op?.status} obj=${op?.objectCount||0}`);
+    if(op?.status==='COMPLETED'){url=op.url;break;} if(op?.status==='FAILED'){console.log('FAILED',op.errorCode);process.exit(1);} await sleep(5000);}
+  if(!url){console.log('no url');process.exit(1);}
+  console.log('downloading...');const data=await download(url);
+  const lines=data.trim().split('\n').filter(Boolean).map(l=>JSON.parse(l));
+  const prod={}, vmap={};
+  for(const n of lines){ if(n.id&&n.id.includes('/Product/'))prod[n.id]={vendor:n.vendor,status:n.status,sku:norm(n.metafield?.value)};
+    else if(n.__parentId&&n.price!==undefined){(vmap[n.__parentId]=vmap[n.__parentId]||[]).push({p:parseFloat(n.price),u:unitOf((n.selectedOptions||[]).map(o=>o.value).join(' '))});} }
+  const out=['vendor,mfr_sku,unit,cur,map,delta,product_gid'];
+  let sampleOnly=0, noMap=0, unitUnknown=0, unitMismatch=0, atMap=0, under=0, over=0; const per={};
+  let underAmt=0, overAmt=0;
+  for(const [pid,p] of Object.entries(prod)){
+    if(!KFAM.has(p.vendor)||p.status!=='ACTIVE')continue;
+    const vs=vmap[pid]||[]; const real=vs.filter(v=>v.u!=='SAMPLE');
+    if(!real.length){sampleOnly++;continue;}
+    const map=MAP[p.sku]; if(!map){noMap++;continue;}
+    const mapUnit=UNIT[p.sku]; // may be undefined
+    // pick the real variant whose unit matches the MAP unit (if we know it); else the highest real
+    let v = mapUnit ? real.find(x=>x.u===mapUnit) : null;
+    if(!v){ if(mapUnit){ // map unit known but no matching variant unit → mismatch
+        const top=real.sort((a,b)=>b.p-a.p)[0]; if(top.u!=='OTHER'){unitMismatch++;continue;} v=top; }
+      else { v=real.sort((a,b)=>b.p-a.p)[0]; unitUnknown++; /* still compare, but flag */ } }
+    const d=v.p-map; per[p.vendor]=per[p.vendor]||{at:0,under:0,over:0};
+    if(Math.abs(d)<=0.5){atMap++;per[p.vendor].at++;}
+    else { if(d<0){under++;per[p.vendor].under++;underAmt+=(map-v.p);}else{over++;per[p.vendor].over++;overAmt+=(v.p-map);}
+      out.push(`${p.vendor},${p.sku},${v.u}${mapUnit?'':'(unit?)'} ,${v.p},${map},${d.toFixed(2)},${pid}`); }
+  }
+  fs.writeFileSync('/tmp/kravet_offmap_trusted.csv',out.join('\n'));
+  console.log('\n=== UNIT-AWARE off-MAP (real variant, unit-matched where possible) ===');
+  console.log(`sample-only (skip): ${sampleOnly} | no MAP: ${noMap} | unit-mismatch (skip, suspect): ${unitMismatch} | unit-unknown (compared, flagged): ${unitUnknown}`);
+  console.log(`compared: AT MAP ${atMap} | UNDER ${under} (+$${underAmt.toFixed(0)}) | OVER ${over} (-$${overAmt.toFixed(0)})`);
+  for(const [v,s] of Object.entries(per).sort((a,b)=>(b[1].under+b[1].over)-(a[1].under+a[1].over))) console.log(`  ${v.padEnd(20)} at:${s.at} under:${s.under} over:${s.over}`);
+  console.log(`\ntrusted off-MAP list → /tmp/kravet_offmap_trusted.csv`);
+})().catch(e=>{console.error(e);process.exit(1);});

← ad1f1a2 Brands page: fix broken DW Exclusive link (/pages/dw-exclusi  ·  back to Dw Yolo Loop  ·  Brands page officer refinements: type-to-filter search, edit 074ac33 →