← back to York Yardgood Exec

york32.mjs

48 lines

#!/usr/bin/env node
// York tail-32: relabel ALL (Sold Per Yard + min5/step1); reprice ONLY rows with a retail (Trousseau -> $70.59).
// DRY-RUN default; --apply writes. Reversible: data/restore-map-32.jsonl. Idempotent/resumable.
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', ND='number_decimal';
const DIR=new URL('.',import.meta.url).pathname;
const targets=JSON.parse(readFileSync(DIR+'data/york32-target.json'));
const REST=DIR+'data/restore-map-32.jsonl';
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
async function gql(q,v){for(let a=0;a<4;a++){try{const r=await fetch(SHOP,{method:'POST',headers:{'X-Shopify-Access-Token':TOKEN,'Content-Type':'application/json'},body:JSON.stringify({query:q,variables:v})});const j=await r.json();if(j.errors&&/throttl/i.test(JSON.stringify(j.errors))){await sleep(2000);continue;}return j;}catch(e){await sleep(1500);}}throw new Error('gql');}
const Q=`query($id:ID!){product(id:$id){id title uomMf:metafield(namespace:"global",key:"unit_of_measure"){value} variants(first:15){edges{node{id title price position sku}}}}}`;
let ok=0,skip=0,fail=0;
for(const t of targets){
  const pid=`gid://shopify/Product/${t.product_id}`;
  const r=await gql(Q,{id:pid}); const p=r.data&&r.data.product;
  if(!p){console.log('  MISS',t.sell_sku);fail++;continue;}
  const vs=p.variants.edges.map(e=>e.node);
  const sample=vs.find(v=>/sample/i.test(v.title)||v.price==='4.25'||/-sample$/i.test(v.sku||''));
  const sell=vs.find(v=>v!==sample)||vs[0];
  const curUom=p.uomMf?.value||'';
  const priceTarget=t.retail?String(t.retail):sell.price; // relabel-only keeps current price
  const already=curUom==='Sold Per Yard' && sell.position===1 && (!t.retail || sell.price===String(t.retail));
  if(already){console.log('  = already',t.sell_sku,t.pattern);skip++;continue;}
  const act=t.retail?`$${sell.price}->$${t.retail}`:`relabel-only (keep $${sell.price})`;
  if(!APPLY){console.log(`  DRY ${t.sell_sku} ${t.group.padEnd(9)} ${act} | ${curUom||'?'}->Sold Per Yard | sellPos ${sell.position}`);ok++;continue;}
  appendFileSync(REST,JSON.stringify({ts:new Date().toISOString(),pid:t.product_id,sellVid:sell.id,sampleVid:sample?.id,old:{price:sell.price,uom:curUom,sellPos:sell.position,title:sell.title}})+'\n');
  const mfs=[
    {ownerId:pid,namespace:'global',key:'unit_of_measure',type:T,value:'Sold Per Yard'},
    {ownerId:pid,namespace:'global',key:'v_prods_quantity_order_min',type:T,value:'5'},
    {ownerId:pid,namespace:'global',key:'v_prods_quantity_order_units',type:T,value:'1'},
    {ownerId:sell.id,namespace:'custom',key:'v_prod_quantity_order_min',type:T,value:'5'},
    {ownerId:sell.id,namespace:'custom',key:'v_prods_quantity_order_units',type:T,value:'1'},
  ];
  if(t.msrp) mfs.push({ownerId:pid,namespace:'custom',key:'us_msrp',type:ND,value:t.msrp.toFixed(2)});
  if(sample) mfs.push({ownerId:sample.id,namespace:'custom',key:'v_prod_quantity_order_min',type:T,value:'1'});
  const m=await gql(`mutation($m:[MetafieldsSetInput!]!){metafieldsSet(metafields:$m){userErrors{message}}}`,{m:mfs});
  if(m.data.metafieldsSet.userErrors.length){console.log('  MF-ERR',t.sell_sku,JSON.stringify(m.data.metafieldsSet.userErrors));fail++;continue;}
  const vu=await gql(`mutation($pid:ID!,$v:[ProductVariantsBulkInput!]!){productVariantsBulkUpdate(productId:$pid,variants:$v){userErrors{message}}}`,
    {pid,v:[{id:sell.id,price:priceTarget,optionValues:[{optionName:'Size',name:'Sold Per Yard'}]}]});
  if(vu.data.productVariantsBulkUpdate.userErrors.length){console.log('  VAR-ERR',t.sell_sku,JSON.stringify(vu.data.productVariantsBulkUpdate.userErrors));fail++;continue;}
  if(sample && sell.position!==1) await gql(`mutation($pid:ID!,$m:[ProductVariantPositionInput!]!){productVariantsBulkReorder(productId:$pid,positions:$m){userErrors{message}}}`,{pid,m:[{id:sell.id,position:1},{id:sample.id,position:2}]});
  console.log(`  OK ${t.sell_sku} ${t.group} ${act}`);ok++;await sleep(400);
}
console.log(`\n${APPLY?'APPLIED':'DRY-RUN'} — ok=${ok} skip=${skip} fail=${fail} of ${targets.length}`);