← back to Kravet Wrongjoin Reprice 2026 07

audit-current-state.js

148 lines

#!/usr/bin/env node
/**
 * audit-current-state.js — READ-ONLY (no writes)
 * TK-00065. Verifies the CURRENT live Shopify state for:
 *   (A) the 425 parent CSV rows (already-applied 2026-07-15) — confirm they landed / stayed at MAP
 *   (B) the 63 followup drift rows — resolve TRUE MAP from authoritative source (NOT the stale CSV correct_map)
 *
 * For each row: fetch live product (status, global.Item, global.MAP, main variant price),
 * resolve MAP via priority: (1) kravet_authoritative_pricing.new_map on global.Item (exact),
 * (2) global.MAP metafield, (3) WHLS Price metafield x 1.5. Classify each row.
 * Emits JSON to audit-current-state-<ts>.json.
 */
const https = require('https');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { execSync } = require('child_process');

const env = fs.readFileSync(os.homedir() + '/Projects/secrets-manager/.env', 'utf8');
const TOKEN = (env.match(/^SHOPIFY_ADMIN_TOKEN=(.*)$/m) || [])[1].replace(/['"]/g, '').trim();
const STORE = 'designer-laboratory-sandbox.myshopify.com', API = '2024-10';
const sleep = ms => new Promise(r => setTimeout(r, ms));

function gql(query, variables = {}) {
  return new Promise((resolve, reject) => {
    const body = JSON.stringify({ query, variables });
    const req = https.request({ host: STORE, path: `/admin/api/${API}/graphql.json`, method: 'POST',
      headers: { 'Content-Type': 'application/json', 'X-Shopify-Access-Token': TOKEN, 'Content-Length': Buffer.byteLength(body) } },
      r => { let d = ''; r.on('data', c => d += c); r.on('end', () => { try { resolve(JSON.parse(d)); } catch (e) { reject(new Error('bad json: ' + d.slice(0, 200))); } }); });
    req.on('error', reject); req.write(body); req.end();
  });
}
async function gqlR(q, v, tries = 5) {
  for (let i = 0; i < tries; i++) {
    let r; try { r = await gql(q, v); } catch (e) { r = { errors: [{ message: String(e).slice(0, 200) }] }; }
    if (r && !r.errors) return r;
    const throttled = JSON.stringify(r.errors || []).match(/throttl/i);
    if (!throttled && i >= 1) return r;
    await sleep(2000 * (i + 1));
  }
  return gql(q, v);
}
function splitCsvLine(line) {
  const out = []; let cur = ''; let inQ = false;
  for (let i = 0; i < line.length; i++) {
    const ch = line[i];
    if (inQ) { if (ch === '"') { if (line[i + 1] === '"') { cur += '"'; i++; } else inQ = false; } else cur += ch; }
    else if (ch === '"') inQ = true;
    else if (ch === ',') { out.push(cur); cur = ''; }
    else cur += ch;
  }
  out.push(cur); return out;
}

function pg(sql) {
  return execSync(`psql -h /tmp -d dw_unified -tAc ${JSON.stringify(sql)}`, { encoding: 'utf8' }).trim();
}

// authoritative MAP for an exact mfr_sku (global.Item); returns number or null
function authMap(item) {
  if (!item) return null;
  const norm = item.replace(/'/g, "''");
  const r = pg(`select new_map from kravet_authoritative_pricing where mfr_sku='${norm}' limit 1`);
  if (r) return parseFloat(r);
  return null;
}

const Q_PRODUCT = `query($id:ID!){product(id:$id){id status handle title
  variants(first:30){nodes{id title sku price}}
  mItem:metafield(namespace:"global",key:"Item"){value}
  mMap:metafield(namespace:"global",key:"MAP"){value}
  mWhls:metafield(namespace:"global",key:"WHLS Price"){value}}}`;

(async () => {
  const which = process.argv[2] || 'followup'; // 'followup' | 'parent'
  let rows;
  if (which === 'parent') {
    const csv = fs.readFileSync(path.join(__dirname, 'affected-dwkk-wrongjoin.csv'), 'utf8').trim().split('\n');
    rows = csv.slice(1).map(splitCsvLine).map(v => ({
      dw_sku: v[2], gid: v[1], handle: v[0], csv_true_sku: v[5],
      csv_correct: parseFloat(v[8]),
    }));
  } else {
    // followup: resolve gid + true_sku from mirror
    const csv = fs.readFileSync(path.join(__dirname, 'FOLLOWUP-drift-rows.csv'), 'utf8').trim().split('\n');
    rows = csv.slice(1).map(splitCsvLine).map(v => ({
      dw_sku: v[0], csv_live_now: parseFloat(v[1]), csv_correct_map: parseFloat(v[2] === undefined ? NaN : v[3]),
      csv_raw_correct: parseFloat(v[3]),
    }));
    for (const r of rows) {
      const line = pg(`select shopify_id||'|'||coalesce(metafields->'global'->'Item'->>'value','')||'|'||coalesce(metafields->'global'->'MAP'->>'value','')||'|'||status||'|'||coalesce(handle,'') from shopify_products where dw_sku='${r.dw_sku}' limit 1`);
      const [gid, item, mfMap, status, handle] = line.split('|');
      r.gid = gid; r.mirror_item = item; r.mirror_map = mfMap; r.mirror_status = status; r.handle = handle;
    }
  }

  const ts = new Date().toISOString().replace(/[:.]/g, '-');
  const out = [];
  const near = (a, b) => Math.abs(a - b) <= 0.011;
  let i = 0;
  for (const r of rows) {
    i++;
    if (!r.gid || !/^gid:/.test(r.gid)) { out.push({ ...r, verdict: 'NO_GID' }); continue; }
    const res = await gqlR(Q_PRODUCT, { id: r.gid });
    const p = res && res.data && res.data.product;
    if (!p) { out.push({ ...r, verdict: 'FETCH_FAIL' }); continue; }
    const item = p.mItem && p.mItem.value;
    const mfMap = p.mMap && p.mMap.value ? parseFloat(p.mMap.value) : null;
    const whls = p.mWhls && p.mWhls.value ? parseFloat(p.mWhls.value) : null;
    const main = p.variants.nodes.find(v => !/sample/i.test(v.title || '') && !/sample/i.test(v.sku || ''));
    const live = main ? parseFloat(main.price) : null;

    // resolve MAP by priority
    const aMap = authMap(item);
    const whlsMap = whls != null ? +(whls * 1.5).toFixed(2) : null;
    let resolvedMap = null, mapSource = null;
    if (aMap != null) { resolvedMap = aMap; mapSource = 'authoritative_new_map'; }
    else if (mfMap != null) { resolvedMap = mfMap; mapSource = 'global.MAP_metafield'; }
    else if (whlsMap != null) { resolvedMap = whlsMap; mapSource = 'WHLS_x1.5'; }

    let verdict;
    if (p.status !== 'ACTIVE') verdict = 'NOT_ACTIVE';
    else if (!main) verdict = 'NO_MAIN_VARIANT';
    else if (resolvedMap == null) verdict = 'NO_RESOLVABLE_MAP';
    else if (near(live, resolvedMap)) verdict = 'AT_MAP';
    else if (live > resolvedMap) verdict = 'ABOVE_MAP';
    else verdict = 'BELOW_MAP';

    out.push({
      dw_sku: r.dw_sku, handle: p.handle, gid: r.gid, status: p.status,
      global_Item: item, live_price: live, main_variant_id: main ? main.id : null,
      map_authoritative: aMap, map_metafield: mfMap, whls_metafield: whls, map_whls_x15: whlsMap,
      resolved_map: resolvedMap, map_source: mapSource, verdict,
      csv_correct: r.csv_correct != null ? r.csv_correct : r.csv_raw_correct,
      mirror_map: r.mirror_map,
    });
    await sleep(250);
    if (i % 25 === 0) process.stderr.write(`...${i}/${rows.length}\n`);
  }
  const outPath = path.join(__dirname, `audit-${which}-${ts}.json`);
  fs.writeFileSync(outPath, JSON.stringify(out, null, 2));
  // summary
  const tally = {};
  for (const o of out) tally[o.verdict] = (tally[o.verdict] || 0) + 1;
  console.log('AUDIT ' + which + ' n=' + out.length + ' -> ' + JSON.stringify(tally));
  console.log('written: ' + outPath);
})();