← 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
A scripts/apply-1104-map.jsA scripts/kravet-offmap-unitaware.js
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 →