← back to Dw Unbuyable Recovery Pilot
tk11252-hollywood/add-roll-variants.mjs
48 lines
#!/usr/bin/env node
// TK-11252 — add a sellable "Single Roll" variant to 18 unbuyable ACTIVE Hollywood products.
// Price sourced from the product's own pattern siblings (8-36 identically-priced live siblings each).
// DRY RUN unless --apply. Records every created variant id to created.json for undo.
import fs from 'node:fs';
const TOK = fs.readFileSync(process.env.HOME+'/Projects/secrets-manager/.env','utf8')
.split('\n').find(l=>l.startsWith('SHOPIFY_ADMIN_TOKEN=')).split('=').slice(1).join('=').trim();
const SHOP='designer-laboratory-sandbox.myshopify.com', API='2024-10';
const APPLY = process.argv.includes('--apply');
const gql = async (query, variables={}) => {
for (let a=0;a<4;a++){
const r = await fetch(`https://${SHOP}/admin/api/${API}/graphql.json`,{method:'POST',
headers:{'X-Shopify-Access-Token':TOK,'Content-Type':'application/json'},
body:JSON.stringify({query,variables})});
const t = await r.text();
try { const j=JSON.parse(t); if(j.errors) throw new Error(JSON.stringify(j.errors).slice(0,300)); return j; }
catch(e){ if(a===3) throw e; await new Promise(s=>setTimeout(s,1500*(a+1))); }
}
};
const targets = fs.readFileSync(process.argv[2]||'targets.tsv','utf8').trim().split('\n')
.filter(Boolean).map(l=>{const [pid,title,sku,price,optName,value,policy,tracked]=l.split('\t');return {pid,title,sku,price,optName,value:value||'Single Roll',policy:policy||'CONTINUE',tracked:(tracked||'False').toLowerCase()==='true'};});
const M = `mutation($productId: ID!, $variants: [ProductVariantsBulkInput!]!) {
productVariantsBulkCreate(productId: $productId, variants: $variants) {
productVariants { id sku price inventoryPolicy selectedOptions { name value } }
userErrors { field message }
}
}`;
const created=[]; let ok=0, fail=0;
for (const t of targets) {
const vars={ productId:`gid://shopify/Product/${t.pid}`,
variants:[{ price:String(t.price), inventoryPolicy:t.policy,
inventoryItem:{ sku:t.sku, tracked:t.tracked },
optionValues:[{ name:t.value, optionName:t.optName }] }] };
if(!APPLY){ console.log(`DRY ${t.pid} | ${t.sku.padEnd(16)} | $${String(t.price).padStart(6)} | ${t.optName}='${t.value}' | ${t.title.slice(0,40)}`); ok++; continue; }
try {
const j = await gql(M,vars);
const res = j.data.productVariantsBulkCreate;
if(res.userErrors?.length){ console.log(`FAIL ${t.pid} | ${JSON.stringify(res.userErrors).slice(0,140)}`); fail++; }
else { const v=res.productVariants[0];
created.push({pid:t.pid,sku:t.sku,price:t.price,variant_id:v.id,title:t.title});
console.log(`OK ${t.pid} | ${v.sku.padEnd(16)} | $${v.price} | ${v.inventoryPolicy} | ${v.selectedOptions.map(o=>o.name+'='+o.value).join(',')}`); ok++; }
} catch(e){ console.log(`FAIL ${t.pid} | ${String(e.message).slice(0,140)}`); fail++; }
await new Promise(s=>setTimeout(s,350));
}
if(APPLY) fs.writeFileSync(new URL('./created.json',import.meta.url), JSON.stringify(created,null,2));
console.log(`\n${APPLY?'APPLIED':'DRY RUN'}: ok=${ok} fail=${fail} of ${targets.length}`);