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