← back to Designerwallcoverings

scripts/activate-1838/activate-1838.mjs

44 lines

// Activate 1838 Wallcoverings drafts (Steve-authorized 2026-06-16).
// Gate: only real-priced (maxVariant > $5); sample-only ($4.25-only) are SKIPPED.
// Per product: status DRAFT->ACTIVE (channels auto-publish), inventory=2026 BOTH variants.
// --apply to write; default dry-run. --limit N to cap. --pilot = 5.
import fs from 'fs'; import os from 'os';
const env={}; for(const l of fs.readFileSync(os.homedir()+'/Projects/secrets-manager/.env','utf8').split('\n')){const m=l.match(/^([A-Z0-9_]+)=(.*)$/);if(m)env[m[1]]=m[2].replace(/^["']|["']$/g,'');}
const API=`https://${env.SHOPIFY_STORE}/admin/api/2024-10/graphql.json`;
const LOCATION='gid://shopify/Location/5795643504';   // canonical inventory loc (qty 2026 on live products)
const APPLY=process.argv.includes('--apply');
const PILOT=process.argv.includes('--pilot');
const limArg=process.argv.find(a=>a.startsWith('--limit='));
const LIMIT=PILOT?5:(limArg?parseInt(limArg.split('=')[1]):99999);
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
async function gql(q,v){for(let t=0;t<5;t++){const r=await fetch(API,{method:'POST',headers:{'X-Shopify-Access-Token':env.SHOPIFY_ADMIN_TOKEN,'Content-Type':'application/json'},body:JSON.stringify({query:q,variables:v||{}})});if(r.status===429){await sleep(2000);continue;}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');}

// 1. gather all draft 1838 with variant inventoryItem ids
let cursor=null, prods=[];
const Q=`query($c:String){products(first:100,after:$c,query:"vendor:'1838 Wallcoverings' status:draft"){pageInfo{hasNextPage endCursor} nodes{id title handle priceRangeV2{maxVariantPrice{amount}} variants(first:10){nodes{id inventoryItem{id tracked}}}}}}`;
do{const d=await gql(Q,{c:cursor});const pg=d.data.products;prods.push(...pg.nodes);cursor=pg.pageInfo.hasNextPage?pg.pageInfo.endCursor:null;}while(cursor);
const real=prods.filter(p=>parseFloat(p.priceRangeV2.maxVariantPrice.amount)>5);
const sampleOnly=prods.filter(p=>parseFloat(p.priceRangeV2.maxVariantPrice.amount)<=5);
console.log(`draft 1838: ${prods.length} | real-priced(eligible): ${real.length} | sample-only(SKIP): ${sampleOnly.length}`);
const targets=real.slice(0,LIMIT);
console.log(`${APPLY?'APPLYING':'DRY-RUN'} on ${targets.length} products${PILOT?' (PILOT)':''}\n`);
if(!APPLY){ targets.slice(0,8).forEach(p=>console.log(`  would activate: ${p.title.slice(0,40)} | $${p.priceRangeV2.maxVariantPrice.amount} | ${p.variants.nodes.length}var`)); console.log('\n(dry-run, no writes)'); process.exit(0); }

const M_STATUS=`mutation($id:ID!){productUpdate(input:{id:$id,status:ACTIVE}){product{id status} userErrors{field message}}}`;
const M_INV=`mutation($input:InventorySetQuantitiesInput!){inventorySetQuantities(input:$input){userErrors{field message}}}`;
let ok=0,fail=0;const errs=[];
for(const p of targets){
  try{
    const s=await gql(M_STATUS,{id:p.id});
    const se=s.data?.productUpdate?.userErrors||[]; if(se.length)throw new Error('status: '+JSON.stringify(se));
    const quantities=p.variants.nodes.map(v=>({inventoryItemId:v.inventoryItem.id,locationId:LOCATION,quantity:2026}));
    const iv=await gql(M_INV,{input:{reason:'correction',name:'available',ignoreCompareQuantity:true,quantities}});
    const ie=iv.data?.inventorySetQuantities?.userErrors||[]; if(ie.length)throw new Error('inv: '+JSON.stringify(ie));
    ok++; console.log(`  ✓ ${p.title.slice(0,42)} | ACTIVE + inv2026 x${quantities.length}`);
  }catch(e){fail++;errs.push(`${p.handle}: ${e.message}`);console.log(`  ✗ ${p.title.slice(0,42)} — ${e.message.slice(0,80)}`);}
  await sleep(350);
}
console.log(`\nDONE: ${ok} activated, ${fail} failed`);
if(errs.length)console.log('errors:\n'+errs.join('\n'));
fs.writeFileSync('scripts/activate-1838/last-run.json',JSON.stringify({ts:new Date().toISOString(),apply:APPLY,ok,fail,handles:targets.map(t=>t.handle),errs},null,2));