← back to Tk 11331 Exec
d1_reorder.mjs
60 lines
// DECISION 1 — move sellable variant to position 1, Sample to position 2.
// Records order-before restore map. Verifies each via Admin re-read. --apply to write.
import fs from 'fs';
import {gql,sleep} from './lib.mjs';
const APPLY=process.argv.includes('--apply');
const LIMIT=parseInt((process.argv.find(a=>a.startsWith('--limit='))||'').split('=')[1]||'99999');
const arg=n=>(process.argv.find(a=>a.startsWith('--'+n+'='))||'').split('=')[1];
// TK-11392: reuse this proven executor for the 26 DW Bespoke Studio products whose
// position 1 is a $12 sample (not the $4.25 one this script was written for).
const SET=arg('set')||'data/d1_set.json';
const ANY_SAMPLE_PRICE=process.argv.includes('--any-sample-price');
const d1=JSON.parse(fs.readFileSync(SET,'utf8')).slice(0,LIMIT);
const RESTORE=arg('restore')||'data/d1-restore-map.jsonl';
const rstream=fs.createWriteStream(RESTORE,{flags:'a'});
const M=`mutation($pid:ID!,$moves:[ProductVariantPositionInput!]!){
productVariantsBulkReorder(productId:$pid,positions:$moves){
userErrors{field message} }}`;
const VQ=`query($id:ID!){product(id:$id){variants(first:100){nodes{id sku title position price}}}}`;
// Sample detection: SKU suffix is the original signal; TITLE match covers the DW Bespoke
// Studio set, whose sample variants are marked only in the title ("... 2ft x 1ft Sample").
const isSample=v=>{
const sku=(v.sku||'').toLowerCase();
if(sku.endsWith('sample')) return true;
return /\bsample\b/i.test(v.title||'');
};
let ok=0,fail=0,skipped=0; const fails=[];
console.log(`${APPLY?'APPLY':'DRY-RUN'} D1 reorder — ${d1.length} products`);
for(const p of d1){
// re-read live to be authoritative right before write
const live=(await gql(VQ,{id:p.gid})).product.variants.nodes.sort((a,b)=>a.position-b.position);
const p1=live[0];
const sellable=live.filter(v=>!isSample(v));
const priceOk=ANY_SAMPLE_PRICE||Math.round(parseFloat(p1.price)*100)===425;
if(!(isSample(p1) && priceOk && sellable.length)){
skipped++; console.log(` SKIP ${p.pid} (live no longer sample@pos1)`); continue;
}
// target order: sellable(s) first (keep their relative order), then samples
const nonSample=sellable;
const samples=live.filter(v=>isSample(v));
const target=[...nonSample,...samples];
const moves=target.map((v,i)=>({id:v.id,position:i+1}));
if(!APPLY){ console.log(` would reorder ${p.pid}: sellable ${nonSample[0].sku} -> pos1`); ok++; continue; }
rstream.write(JSON.stringify({ts:new Date().toISOString(),pid:p.pid,gid:p.gid,
order_before:live.map(v=>({id:v.id,sku:v.sku,position:v.position})),
undo:`productVariantsBulkReorder ${p.gid} -> ${JSON.stringify(live.map((v,i)=>({id:v.id,position:v.position})))}`})+'\n');
const res=await gql(M,{pid:p.gid,moves});
const ue=res.productVariantsBulkReorder.userErrors;
if(ue.length){ fail++; fails.push({pid:p.pid,ue}); console.log(` ERR ${p.pid}`,JSON.stringify(ue)); continue; }
await sleep(400);
// verify
const after=(await gql(VQ,{id:p.gid})).product.variants.nodes.sort((a,b)=>a.position-b.position);
if(!isSample(after[0])){ ok++; if(ok%50===0)console.log(` ok ${ok}`); }
else { fail++; fails.push({pid:p.pid,reason:'verify-still-sample-pos1'}); console.log(` VERIFY-FAIL ${p.pid}`);}
await sleep(150);
}
rstream.end();
console.log(`D1 ${APPLY?'APPLIED':'DRY'} — ok=${ok} fail=${fail} skipped=${skipped}`);
if(fails.length) fs.writeFileSync('data/d1-fails.json',JSON.stringify(fails,null,1));