[object Object]

← 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

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 →