← back to Tk 11331 Exec
d3a_reprice.mjs
55 lines
// TK-11331 DECISION-3a — reprice sellable variant price = hw_price for diff rows only.
// Diff = manifest rows where current_price != new_price. Records restore map BEFORE writing.
// Does NOT touch the $4.25 Sample variant, its position, or any metafield. --apply to write.
import fs from 'fs';
import {gql,sleep} from './lib.mjs';
const APPLY=process.argv.includes('--apply');
const MAN='data/d3a-manifest.jsonl';
const RESTORE='data/d3a-reprice-restore.jsonl';
const rows=fs.readFileSync(MAN,'utf8').trim().split('\n').map(l=>JSON.parse(l));
// derive diff set: current_price != new_price (skip no-ops)
const diff=rows.filter(r=>Number(r.current_price)!==Number(r.new_price));
console.log(`${APPLY?'APPLY':'DRY-RUN'} D3a reprice — diff=${diff.length} of ${rows.length} (skipping ${rows.length-diff.length} no-ops)`);
const VQ=`query($id:ID!){productVariant(id:$id){id sku price product{id title}}}`;
const M=`mutation($pid:ID!,$variants:[ProductVariantsBulkInput!]!){
productVariantsBulkUpdate(productId:$pid,variants:$variants){
productVariants{id sku price} userErrors{field message} }}`;
const rstream=APPLY?fs.createWriteStream(RESTORE,{flags:'a'}):null;
let ok=0,fail=0,skip=0; const fails=[]; const results=[];
for(const r of diff){
const vid=r.variant_id, pid=`gid://shopify/Product/${r.shopify_product_id}`;
const np=Number(r.new_price);
// authoritative live read right before write
const live=(await gql(VQ,{id:vid})).productVariant;
if(!live){ fail++; fails.push({sku:r.sku,reason:'variant-not-found'}); console.log(` ERR ${r.sku} variant not found`); continue; }
// safety: must be the sellable variant (not the sample), and its sku must match manifest
if((live.sku||'').toLowerCase().endsWith('sample')){ skip++; console.log(` SKIP ${r.sku} — live variant is a Sample (${live.sku}); refusing to touch`); continue; }
if(live.sku!==r.sku){ skip++; console.log(` SKIP ${r.sku} — live sku mismatch (${live.sku})`); continue; }
const before=Number(live.price);
if(Math.abs(before-Number(r.current_price))>0.005){
console.log(` WARN ${r.sku} live price ${before} != manifest current ${r.current_price} (recording live as restore value)`);
}
if(Math.abs(before-np)<0.005){ skip++; console.log(` SKIP ${r.sku} — already ${np}`); continue; }
if(!APPLY){ console.log(` would reprice ${r.sku}: ${before} -> ${np}`); ok++; continue; }
// restore map BEFORE the write
rstream.write(JSON.stringify({ts:new Date().toISOString(),sku:live.sku,pid,variant_id:vid,
old_price:before,new_price:np,
undo:`productVariantsBulkUpdate ${pid} variants:[{id:${vid},price:"${before.toFixed(2)}"}]`})+'\n');
const res=await gql(M,{pid,variants:[{id:vid,price:np.toFixed(2)}]});
const ue=res.productVariantsBulkUpdate.userErrors;
if(ue.length){ fail++; fails.push({sku:r.sku,ue}); console.log(` ERR ${r.sku}`,JSON.stringify(ue)); continue; }
await sleep(400);
// verify via authoritative re-read
const after=(await gql(VQ,{id:vid})).productVariant;
if(Math.abs(Number(after.price)-np)<0.005){ ok++; results.push({sku:after.sku,before,after:Number(after.price)}); console.log(` OK ${after.sku}: ${before} -> ${after.price} (verified)`); }
else { fail++; fails.push({sku:r.sku,reason:'verify-mismatch',got:after.price,want:np}); console.log(` VERIFY-FAIL ${r.sku} got ${after.price} want ${np}`); }
await sleep(150);
}
if(rstream) rstream.end();
console.log(`D3a ${APPLY?'APPLIED':'DRY'} — ok=${ok} fail=${fail} skip=${skip}`);
if(fails.length) fs.writeFileSync('data/d3a-reprice-fails.json',JSON.stringify(fails,null,1));
if(results.length) fs.writeFileSync('data/d3a-reprice-results.json',JSON.stringify(results,null,1));