[object Object]

← back to Dw Yolo Loop

Add Schumacher roll-price pusher + propagate price-sheet costs to Kamatera canonical (1,174 costed)

1193fa79f6893b83247a75f70d73f1744a390577 · 2026-06-15 10:41:27 -0700 · Steve Abrams

Files touched

Diff

commit 1193fa79f6893b83247a75f70d73f1744a390577
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Jun 15 10:41:27 2026 -0700

    Add Schumacher roll-price pusher + propagate price-sheet costs to Kamatera canonical (1,174 costed)
---
 scripts/price-sheets/push-schu-roll-prices.mjs | 70 ++++++++++++++++++++++++++
 1 file changed, 70 insertions(+)

diff --git a/scripts/price-sheets/push-schu-roll-prices.mjs b/scripts/price-sheets/push-schu-roll-prices.mjs
new file mode 100644
index 0000000..cc80124
--- /dev/null
+++ b/scripts/price-sheets/push-schu-roll-prices.mjs
@@ -0,0 +1,70 @@
+#!/usr/bin/env node
+/**
+ * push-schu-roll-prices.mjs — set the ROLL variant price = DW retail (cost/0.65/0.85)
+ * for the newly-costed Schumacher SINGLE-ROLL wallcoverings. These were dark ($0 or the
+ * $4.25 memo-sample trap); this gives them a real price for the first time.
+ *
+ * SAFETY (same rails as the Kravet MAP push):
+ *  - Only the ROLL (non -sample) variant. The $4.25 memo SAMPLE is never touched.
+ *  - cost is SINGLE-ROLL unit (verified) -> retail is per-roll. (YARD/PANEL/RL held out.)
+ *  - Only WRITES where current roll price is garbage (<= $10, i.e. $0 / $4.25). A product
+ *    that already has a real price (> $10) is NEVER clobbered -> flagged as anomaly + skipped.
+ *  - Anomaly guard: retail/cur > 2.5x or < 0.4x on a real price -> skip.
+ *  - Reversible: writes a plan json of {pid,vid,from,to} so a revert can restore.
+ *
+ * USAGE: node push-schu-roll-prices.mjs               # dry-run
+ *        node push-schu-roll-prices.mjs --apply --i-am-steve
+ */
+import fs from 'node:fs';
+const SHOP='designer-laboratory-sandbox.myshopify.com', VER='2024-10';
+const TOKEN=(fs.readFileSync(process.env.HOME+'/Projects/secrets-manager/.env','utf8').match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m)||[])[1]?.trim();
+if(!TOKEN){console.error('no token');process.exit(1);}
+const URL=`https://${SHOP}/admin/api/${VER}/graphql.json`;
+const args=Object.fromEntries(process.argv.slice(2).map(a=>{const[k,v]=a.replace(/^--/,'').split('=');return[k,v===undefined?true:v];}));
+const APPLY=args.apply===true&&args['i-am-steve']===true;
+const sleep=ms=>new Promise(r=>setTimeout(r,ms));
+async function gql(q,v){for(let a=0;a<8;a++){let j;try{const r=await fetch(URL,{method:'POST',headers:{'X-Shopify-Access-Token':TOKEN,'Content-Type':'application/json'},body:JSON.stringify({query:q,variables:v})});j=await r.json();}catch(e){await sleep(1500*(a+1));continue;}if(j.errors){if(JSON.stringify(j.errors).includes('THROTTLED')){await sleep(2000*(a+1));continue;}throw new Error(JSON.stringify(j.errors));}const t=j.extensions?.cost?.throttleStatus;if(t&&t.currentlyAvailable<400)await sleep(1200);return j.data;}throw new Error('retries');}
+const isSample=v=>/(sample|memo|swatch)/i.test([v.title,v.sku].join(' '))||/-sample$/i.test(v.sku||'');
+
+const rows=fs.readFileSync('/tmp/schu-roll-pricing.csv','utf8').trim().split('\n').map(l=>{const[gid,mfr,cost,retail]=l.split(',');return{gid,mfr,cost:+cost,retail:+retail};});
+console.log(`push-schu-roll-prices — mode: ${APPLY?'⚠️  LIVE APPLY':'DRY-RUN'} | SINGLE-ROLL targets: ${rows.length}`);
+
+const Q=`query($ids:[ID!]!){ nodes(ids:$ids){ ... on Product { id variants(first:30){ nodes { id title sku price } } } } }`;
+const M=`mutation($pid:ID!,$variants:[ProductVariantsBulkInput!]!){ productVariantsBulkUpdate(productId:$pid,variants:$variants){ userErrors{ field message } } }`;
+const b={fix:[],already_priced:[],anomaly:[],no_roll:[],missing:[]};
+for(let i=0;i<rows.length;i+=50){
+  const batch=rows.slice(i,i+50);
+  const data=await gql(Q,{ids:batch.map(x=>x.gid)});
+  const byId=new Map(batch.map(x=>[x.gid,x]));
+  const got=new Set();
+  for(const p of data.nodes){ if(!p)continue; got.add(p.id);
+    const t=byId.get(p.id); const rolls=p.variants.nodes.filter(v=>!isSample(v));
+    if(!rolls.length){b.no_roll.push(t);continue;}
+    const roll=rolls.reduce((a,c)=>+c.price>+a.price?c:a);
+    const cur=+roll.price; const rec={pid:p.id,vid:roll.id,mfr:t.mfr,cur,to:t.retail};
+    if(Math.abs(cur-t.retail)<0.01){b.already_priced.push(rec);continue;}
+    if(cur<=10){b.fix.push(rec);}                                  // $0/$4.25 garbage -> price it
+    else if(t.retail/cur>2.5||t.retail/cur<0.4){b.anomaly.push(rec);} // real price, suspicious -> skip
+    else{b.already_priced.push(rec);}                              // real price, close enough -> leave
+  }
+  for(const x of batch) if(!got.has(x.gid)) b.missing.push(x);
+  if(i%500===0&&i)process.stderr.write(`  scanned ${i}/${rows.length}\n`);
+}
+console.log(`  fix garbage ($0/$4.25) -> retail : ${b.fix.length}`);
+console.log(`  already real-priced (leave)      : ${b.already_priced.length}`);
+console.log(`  ⚠ anomaly (skip, real price)     : ${b.anomaly.length}`);
+console.log(`  no roll variant (skip)           : ${b.no_roll.length}`);
+console.log(`  not found on Shopify (skip)      : ${b.missing.length}`);
+console.log('  sample fixes:'); for(const r of b.fix.slice(0,6))console.log(`    ${r.mfr}: $${r.cur} -> $${r.to}`);
+fs.writeFileSync('data/price-sheets/schu-roll-price-plan.json',JSON.stringify({fix:b.fix,anomaly:b.anomaly},null,2));
+if(!APPLY){console.log('\nDRY-RUN only. To execute: node push-schu-roll-prices.mjs --apply --i-am-steve');process.exit(0);}
+
+console.log('\n⚠️  LIVE APPLY — pricing SINGLE-ROLL wallcoverings…');
+let ok=0,err=0;
+for(const r of b.fix){
+  try{const d=await gql(M,{pid:r.pid,variants:[{id:r.vid,price:r.to.toFixed(2)}]});
+    const ue=d.productVariantsBulkUpdate?.userErrors||[];if(ue.length){err++;if(err<=10)console.log('  err',r.mfr,JSON.stringify(ue));}else ok++;
+  }catch(e){err++;if(err<=10)console.log('  EX',r.mfr,e.message);}
+  if((ok+err)%200===0)console.log(`  progress ${ok} ok / ${err} err`);
+}
+console.log(`\nDONE — ${ok} roll prices set, ${err} errors.`);

← 8965036 Resolve staged price-sheet costs into shopify_products.cost  ·  back to Dw Yolo Loop  ·  Kravet dedup sweep: bulk full-catalog true-dup detector (ven e3ad9a1 →