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