← back to Dw Yolo Loop
Kravet add-roll-variant writer: adds Sold-Per-Roll @ MAP to 305 sample-only wallcoverings (idempotent, custom.width label, qty2026/CONTINUE); canary verified on Gallier Diamond $747
5f0ef46029b02bb37ff390838ddd5e58c56cff4e · 2026-06-16 14:49:11 -0700 · Steve Abrams
Files touched
A scripts/kravet-master-2026/add_roll_variants.mjs
Diff
commit 5f0ef46029b02bb37ff390838ddd5e58c56cff4e
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue Jun 16 14:49:11 2026 -0700
Kravet add-roll-variant writer: adds Sold-Per-Roll @ MAP to 305 sample-only wallcoverings (idempotent, custom.width label, qty2026/CONTINUE); canary verified on Gallier Diamond $747
---
scripts/kravet-master-2026/add_roll_variants.mjs | 81 ++++++++++++++++++++++++
1 file changed, 81 insertions(+)
diff --git a/scripts/kravet-master-2026/add_roll_variants.mjs b/scripts/kravet-master-2026/add_roll_variants.mjs
new file mode 100644
index 0000000..0ee957d
--- /dev/null
+++ b/scripts/kravet-master-2026/add_roll_variants.mjs
@@ -0,0 +1,81 @@
+#!/usr/bin/env node
+/* Add the missing "Sold Per Roll" variant @ 2026 MAP to sample-only Kravet-family
+ WALLCOVERING products (DTD verdict A 3/3, Steve "go 305" 2026-06-16).
+ - Reads /tmp/kravet_roll_dryrun.csv (SAMPLE_ONLY rows) + Kamatera new_map.
+ - Per product: derive roll sku = sample sku minus -Sample; width from custom.width
+ metafield; option "Size" value = "Sold Per Roll - {W}In Wide".
+ - IDEMPOTENT: skips any product that already has a Sold-Per-Roll variant.
+ - Mirrors the store template: inventoryPolicy CONTINUE, qty 2026 @ the one location.
+ Flags: --apply (default dry-run, no writes) | --limit N (canary).
+ Token: SHOPIFY_ADMIN_TOKEN. */
+import fs from 'fs';
+import { execSync } from 'child_process';
+const STORE='designer-laboratory-sandbox.myshopify.com';
+const TOKEN=process.env.SHOPIFY_ADMIN_TOKEN;
+const LOCATION='gid://shopify/Location/5795643504';
+const APPLY=process.argv.includes('--apply');
+const li=process.argv.indexOf('--limit'); const LIMIT=li>=0?parseInt(process.argv[li+1]):Infinity;
+if(!TOKEN){ console.error('no SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
+const sleep=ms=>new Promise(r=>setTimeout(r,ms));
+const log=s=>{ console.log(s); fs.appendFileSync('/tmp/add_roll_variants.log',s+'\n'); };
+
+// new_map per gid from Kamatera (source file built earlier)
+const nm={};
+for(const line of fs.readFileSync('/tmp/kravet_305_source.txt','utf8').trim().split('\n')){
+ const c=line.split('|'); if(c.length>=4) nm[c[0]]=parseFloat(c[3]);
+}
+// the 305 sample-only gids
+const targets=fs.readFileSync('/tmp/kravet_roll_dryrun.csv','utf8').trim().split('\n')
+ .filter(l=>l.includes('SAMPLE_ONLY_NEEDS_PRICE')).map(l=>{const c=l.split(','); return {gid:c[0],vendor:c[1],sku:c[2],new_map:nm[c[0]]};})
+ .filter(t=>t.new_map>0);
+
+async function gql(query,variables){
+ for(let a=0;a<4;a++){
+ const r=await fetch(`https://${STORE}/admin/api/2024-10/graphql.json`,{method:'POST',
+ headers:{'X-Shopify-Access-Token':TOKEN,'Content-Type':'application/json'},body:JSON.stringify({query,variables})});
+ const j=await r.json();
+ if(j.errors && JSON.stringify(j.errors).includes('Throttled')){ await sleep(2000); continue; }
+ return j;
+ }
+ throw new Error('throttled out');
+}
+const READ=`query($id:ID!){ product(id:$id){ id title options{ name }
+ width:metafield(namespace:"custom",key:"width"){ value }
+ variants(first:30){ edges{ node{ title sku } } } } }`;
+const CREATE=`mutation($pid:ID!,$variants:[ProductVariantsBulkInput!]!){
+ productVariantsBulkCreate(productId:$pid, variants:$variants){
+ productVariants{ id title sku price selectedOptions{ name value } }
+ userErrors{ field message } } }`;
+
+let added=0,skipped=0,failed=0,n=0;
+log(`\n=== add_roll_variants ${APPLY?'APPLY':'DRY-RUN'} limit=${LIMIT} targets=${targets.length} @ ${new Date().toISOString?.()||'now'} ===`);
+for(const t of targets){
+ if(n>=LIMIT) break; n++;
+ let p;
+ try{ const d=await gql(READ,{id:t.gid}); p=d.data?.product; }catch(e){ failed++; log(`FAIL read ${t.gid} ${e.message}`); continue; }
+ if(!p){ failed++; log(`FAIL no-product ${t.gid}`); continue; }
+ const vs=p.variants.edges.map(e=>e.node);
+ if(vs.some(v=>/Sold Per Roll/i.test(v.title))){ skipped++; log(`SKIP has-roll ${p.title}`); continue; }
+ const sample=vs.find(v=>/-sample$/i.test(v.sku||'')) || vs[0];
+ const rollSku=(sample.sku||'').replace(/-Sample$/i,'');
+ let w=(p.width?.value||'').replace(/["\s]/g,'');
+ const wn=parseFloat(w);
+ const label=(wn>=12 && wn<=60) ? `Sold Per Roll - ${w}In Wide` : `Sold Per Roll`;
+ const price=t.new_map.toFixed(2);
+ const variant={ price,
+ optionValues:[{ optionName:(p.options[0]?.name||'Size'), name:label }],
+ inventoryItem:{ sku:rollSku, tracked:true },
+ inventoryPolicy:'CONTINUE',
+ inventoryQuantities:[{ locationId:LOCATION, availableQuantity:2026 }] };
+ if(!APPLY){ log(`DRY ${p.title} | +${label} @ $${price} sku=${rollSku}`); added++; continue; }
+ try{
+ const d=await gql(CREATE,{pid:t.gid,variants:[variant]});
+ const ue=d.data?.productVariantsBulkCreate?.userErrors||[];
+ if(ue.length){ failed++; log(`FAIL ${p.title} :: ${JSON.stringify(ue)}`); }
+ else { added++; const nv=d.data.productVariantsBulkCreate.productVariants[0];
+ log(`OK ${p.title} | ${nv.title} $${nv.price} sku=${nv.sku}`); }
+ }catch(e){ failed++; log(`FAIL create ${p.title} ${e.message}`); }
+ await sleep(700);
+ if(n%25===0) log(`-- progress ${n}/${Math.min(LIMIT,targets.length)} added=${added} skip=${skipped} fail=${failed}`);
+}
+log(`\n=== DONE ${APPLY?'APPLY':'DRY'} added=${added} skipped=${skipped} failed=${failed} of ${n} processed ===`);
← c97eb92 collection-leak sweep (c40): 3 confirmed collection-level le
·
back to Dw Yolo Loop
·
orphan-collection LIVE re-scan (c41): TRUE empty count = 43 d32d8a1 →