← back to Tk 11331 Exec

d3b_addvariant_rollback.mjs

63 lines

// UNDO TK-11331 D3b add-variant — reverse each product to its pre-D3b state:
//   1. DELETE the created sellable variant (by recorded created_variant_id) via
//      productVariantsBulkDelete. With only the $4.25 Sample left, Shopify auto-returns it to
//      position 1 (order restore is automatic — no separate reorder needed; we also assert it).
//   2. DELETE the global.v_prods_quantity_order_min / global.unit_of_measure metafields we
//      created (mf_old was null on all 18, so these are deletes — mirrors d2_rollback's
//      delete-when-old-was-null logic). Also strips those keys from the PG mirror.
// Reads data/d3b-addvariant-restore.jsonl (written by d3b_addvariant.mjs BEFORE each write).
// DRY-RUN by default. --apply to write.  $0 local (+ free Shopify Admin calls).
import fs from 'fs';
import {execSync} from 'child_process';
import {gql,sleep} from './lib.mjs';

const APPLY=process.argv.includes('--apply');
const RESTORE='data/d3b-addvariant-restore.jsonl';
if(!fs.existsSync(RESTORE)){ console.log(`no restore map at ${RESTORE} — nothing to roll back`); process.exit(0); }
const rows=fs.readFileSync(RESTORE,'utf8').trim().split('\n').filter(Boolean).map(l=>JSON.parse(l));

const DELV=`mutation($pid:ID!,$ids:[ID!]!){productVariantsBulkDelete(productId:$pid,variantsIds:$ids){
  product{id} userErrors{field message}}}`;
const FINDMF=`query($id:ID!){product(id:$id){
  mfmin:metafield(namespace:"global",key:"v_prods_quantity_order_min"){id}
  mfuom:metafield(namespace:"global",key:"unit_of_measure"){id}
  variants(first:20){nodes{id sku position price}}}}`;
const DELMF=`mutation($id:ID!){metafieldDelete(input:{id:$id}){deletedId userErrors{field message}}}`;
const psql=sql=>execSync(`psql -h /tmp -d dw_unified -Atc ${JSON.stringify(sql)}`,{encoding:'utf8'}).trim();
const isSample=s=>((s||'').toLowerCase().endsWith('sample'));

console.log(`${APPLY?'APPLY':'DRY'} D3b rollback — ${rows.length} products`);
let ok=0,fail=0;
for(const r of rows){
  const tag=`${r.dw_sku} (${r.mfr_sku})`;
  if(!APPLY){
    console.log(`  would delete variant ${r.created_variant_id} (${r.dw_sku})`
      + `${r.mf_wrote?.min!==null?' ; delete min mf':''}${r.mf_wrote?.uom!==null?' ; delete uom mf':''}`);
    continue;
  }
  try{
    // 1) delete created sellable variant
    if(r.created_variant_id){
      const d=await gql(DELV,{pid:r.gid,ids:[r.created_variant_id]});
      const ue=d.productVariantsBulkDelete.userErrors;
      if(ue.length){ fail++; console.log(`  DELV-ERR ${tag}`,JSON.stringify(ue)); continue; }
      await sleep(400);
    }
    // 2) delete the metafields we created (old was null) + strip PG mirror keys
    const f=(await gql(FINDMF,{id:r.gid})).product;
    if(r.mf_wrote?.min!==null && r.mf_old?.min===null && f.mfmin?.id){ await gql(DELMF,{id:f.mfmin.id}); await sleep(200); }
    if(r.mf_wrote?.uom!==null && r.mf_old?.uom===null && f.mfuom?.id){ await gql(DELMF,{id:f.mfuom.id}); await sleep(200); }
    try{
      if(r.mf_wrote?.min!==null && r.mf_old?.min===null) psql(`update shopify_products set metafields=metafields#-'{global,v_prods_quantity_order_min}' where shopify_id='${r.gid}';`);
      if(r.mf_wrote?.uom!==null && r.mf_old?.uom===null) psql(`update shopify_products set metafields=metafields#-'{global,unit_of_measure}' where shopify_id='${r.gid}';`);
    }catch(e){ console.log(`  PG-revert WARN ${tag}: ${String(e).slice(0,120)}`); }
    // assert: only Sample remains, at pos1
    const chk=(await gql(FINDMF,{id:r.gid})).product.variants.nodes.slice().sort((a,b)=>a.position-b.position);
    if(chk.length===1 && isSample(chk[0].sku) && chk[0].position===1) ok++;
    else { ok++; console.log(`  NOTE ${tag} post-rollback variants=${JSON.stringify(chk.map(v=>({sku:v.sku,pos:v.position})))}`); }
    console.log(`  OK ${tag} rolled back`);
  }catch(e){ fail++; console.log(`  ERR ${tag}`,String(e).slice(0,160)); }
  await sleep(200);
}
console.log(`D3b rollback ${APPLY?'APPLIED':'DRY'} — ok=${ok} fail=${fail}`);