← 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}`);