[object Object]

← 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 →