← back to Jeffrey Stevens Margin Fix
Add sellable variant to 2 sample-only JS roll goods (TL1925 $67.87, CL2572 $54.30)
dfbeef090ff4e930fafff291d00cbf0bf1ccb726 · 2026-09-09 20:15:02 -0700 · Steve
Files touched
Diff
commit dfbeef090ff4e930fafff291d00cbf0bf1ccb726
Author: Steve <steve@designerwallcoverings.com>
Date: Wed Sep 9 20:15:02 2026 -0700
Add sellable variant to 2 sample-only JS roll goods (TL1925 $67.87, CL2572 $54.30)
---
addvariant-2.mjs | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 61 insertions(+)
diff --git a/addvariant-2.mjs b/addvariant-2.mjs
new file mode 100644
index 0000000..ebec8d4
--- /dev/null
+++ b/addvariant-2.mjs
@@ -0,0 +1,61 @@
+#!/usr/bin/env node
+// Add a sellable "Sold Per Single Roll" variant to the 2 ACTIVE sample-only JS roll goods
+// (TL1925 Keansburg Coral, CL2572 Cohasset Charcoal). Adapted from TK-11331 d3b_addvariant.
+// Roll goods: variant "Sold Per Single Roll" @ retail (master cost/0.65/0.85), min 2, sellable pos1, sample pos2.
+// DRY-RUN default; --apply writes. Reversible: data/addvariant2-restore.jsonl (created variant id -> delete to undo).
+import { readFileSync, appendFileSync } from 'node:fs';
+const APPLY=process.argv.includes('--apply');
+const TOKEN=readFileSync(process.env.HOME+'/Projects/secrets-manager/.env','utf8').split('\n').find(l=>l.startsWith('SHOPIFY_ADMIN_TOKEN=')).split('=')[1].trim();
+const SHOP='https://designer-laboratory-sandbox.myshopify.com/admin/api/2024-10/graphql.json';
+const T='single_line_text_field';
+const REST=new URL('.',import.meta.url).pathname+'data/addvariant2-restore.jsonl';
+const sleep=ms=>new Promise(r=>setTimeout(r,ms));
+const gql=(q,v)=>fetch(SHOP,{method:'POST',headers:{'X-Shopify-Access-Token':TOKEN,'Content-Type':'application/json'},body:JSON.stringify({query:q,variables:v})}).then(r=>r.json());
+const isSample=s=>((s||'').toLowerCase().endsWith('sample'));
+// the 2 targets: product_id, retail, bare sellable sku
+const TARGETS=[
+ {pid:'6702968111155',sku:'DWJS-14580',retail:67.87,name:'TL1925 Keansburg Coral'},
+ {pid:'6702988656691',sku:'DWJS-14674',retail:54.30,name:'CL2572 Cohasset Charcoal'},
+];
+const READ=`query($id:ID!){product(id:$id){id handle status options{name} variants(first:20){nodes{id sku title position price}}}}`;
+const CREATE=`mutation($pid:ID!,$v:[ProductVariantsBulkInput!]!){productVariantsBulkCreate(productId:$pid,variants:$v,strategy:DEFAULT){productVariants{id sku title position} userErrors{field message}}}`;
+const REORDER=`mutation($pid:ID!,$m:[ProductVariantPositionInput!]!){productVariantsBulkReorder(productId:$pid,positions:$m){userErrors{field message}}}`;
+const SETMF=`mutation($mf:[MetafieldsSetInput!]!){metafieldsSet(metafields:$mf){userErrors{field message}}}`;
+let ok=0,fail=0,skip=0;
+for(const t of TARGETS){
+ const gid=`gid://shopify/Product/${t.pid}`;
+ const p=(await gql(READ,{id:gid})).data?.product;
+ if(!p){console.log(' ERR',t.name,'not found');fail++;continue;}
+ const vs=p.variants.nodes.slice().sort((a,b)=>a.position-b.position);
+ const sample=vs.find(v=>isSample(v.sku));
+ const sellable=vs.filter(v=>!isSample(v.sku));
+ if(sellable.length){console.log(' SKIP',t.name,'already has sellable',sellable[0].sku);skip++;continue;}
+ if(!sample){console.log(' ERR',t.name,'no sample variant');fail++;continue;}
+ const optName=p.options?.[0]?.name||'Title';
+ if(!APPLY){console.log(` would ADD ${t.sku} "Sold Per Single Roll" @ $${t.retail} pos1 (opt="${optName}"), demote ${sample.sku} pos2, min=2`);ok++;continue;}
+ const restore={ts:new Date().toISOString(),pid:t.pid,handle:p.handle,sampleVid:sample.id,createdVid:null,
+ undo:'delete createdVid via productVariantsBulkDelete; order auto-restores to sample@pos1'};
+ const c=await gql(CREATE,{pid:gid,v:[{price:t.retail.toFixed(2),taxable:true,inventoryPolicy:'CONTINUE',inventoryItem:{sku:t.sku,tracked:true},optionValues:[{optionName:optName,name:'Sold Per Single Roll'}]}]});
+ const cue=c.data.productVariantsBulkCreate.userErrors;
+ if(cue.length){console.log(' CREATE-ERR',t.name,JSON.stringify(cue));fail++;continue;}
+ const nv=c.data.productVariantsBulkCreate.productVariants.find(v=>!isSample(v.sku));
+ restore.createdVid=nv.id; appendFileSync(REST,JSON.stringify(restore)+'\n');
+ await sleep(400);
+ const ro=await gql(REORDER,{pid:gid,m:[{id:nv.id,position:1},{id:sample.id,position:2}]});
+ if(ro.data.productVariantsBulkReorder.userErrors.length){console.log(' REORDER-ERR',t.name,JSON.stringify(ro.data.productVariantsBulkReorder.userErrors));fail++;continue;}
+ await sleep(400);
+ const mf=await gql(SETMF,{mf:[
+ {ownerId:gid,namespace:'global',key:'v_prods_quantity_order_min',type:T,value:'2'},
+ {ownerId:gid,namespace:'global',key:'v_prods_quantity_order_units',type:T,value:'2'},
+ {ownerId:nv.id,namespace:'custom',key:'v_prod_quantity_order_min',type:T,value:'2'},
+ ]});
+ if(mf.data.metafieldsSet.userErrors.length){console.log(' MF-ERR',t.name,JSON.stringify(mf.data.metafieldsSet.userErrors));fail++;continue;}
+ // verify
+ const after=(await gql(READ,{id:gid})).data.product;
+ const av=after.variants.nodes.slice().sort((a,b)=>a.position-b.position);
+ const okPos=av[0]&&!isSample(av[0].sku)&&Math.abs(Number(av[0].price)-t.retail)<0.01;
+ console.log(` ${okPos?'OK':'VERIFY-FAIL'} ${t.name} — sellable ${t.sku}@$${t.retail} pos1 (now pos1=${av[0]?.sku}@$${av[0]?.price})`);
+ okPos?ok++:fail++;
+ await sleep(300);
+}
+console.log(`\n${APPLY?'APPLIED':'DRY-RUN'} — ok=${ok} fail=${fail} skip=${skip}`);
← 934973c Argent (2) reprice to $40.72 — last below-cost remainder clo
·
back to Jeffrey Stevens Margin Fix
·
2 sample-only JS products fixed: sellable variant added pos1 343aae0 →