← back to Hollywood Optc

build-hwc-map.mjs

178 lines

#!/usr/bin/env node
// TK-10633 Option C — REVERSIBLE PREP. Build the HWC-#### restore-map for the ~646
// ACTIVE Momentum-real Hollywood/DWHD-* products, enumerating BOTH variants from LIVE
// Shopify (the mirror only holds the -Sample row and understates). READ-ONLY: reads the
// mirror to enumerate the target product-ids, then GETs each product LIVE for its actual
// variants + metafields, and writes ONLY a local restore file. Fires ZERO writes at
// Shopify or dw_unified.
//
// Steve's decision: identity = the real HWC-#### code WHERE IT EXISTS (in a dw_sku
// metafield). Products that HAVE one -> remap customer-facing SKU (both variants) to it.
// Products that LACK one -> HELD code-recovery bucket, stay DWHD for now (never fabricate,
// never hash).
import { createRequire } from 'module';
import { readFileSync, writeFileSync } from 'node:fs';
const require = createRequire(import.meta.url);
const { Pool } = require('pg');

const SHOP = 'designer-laboratory-sandbox.myshopify.com';
const VER = '2024-10';
const env = readFileSync(process.env.HOME + '/Projects/secrets-manager/.env', 'utf8');
const TOKEN = (env.split('\n').find(l => l.startsWith('SHOPIFY_ADMIN_TOKEN=')) || '')
  .replace('SHOPIFY_ADMIN_TOKEN=', '').replace(/["'\r]/g, '').trim();
if (!TOKEN) { console.error('no SHOPIFY_ADMIN_TOKEN'); process.exit(1); }

const pool = new Pool({ connectionString: (process.env.PGCONNSTRING || 'postgresql://dw_admin@127.0.0.1:5432/dw_unified?host=/tmp') });
const BASE = `status='ACTIVE' AND variant_sku ~ '^DWHD' AND (metafields->'dwc'->'real_vendor'->>'value')='Momentum'`;

async function getJSON(url, tries = 0) {
  const res = await fetch(url, { headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' } });
  if (res.status === 429) { await new Promise(r => setTimeout(r, 2000)); return getJSON(url, tries); }
  if ((res.status === 502 || res.status === 503) && tries < 3) { await new Promise(r => setTimeout(r, 1500)); return getJSON(url, tries + 1); }
  if (!res.ok) throw new Error(`${res.status} ${url} ${await res.text()}`);
  return res.json();
}

function extractHWC(metafields) {
  // real HWC-#### lives in a dw_sku metafield (custom / dwc / global). Return the first
  // HWC-\d+ value found; null if none.
  for (const m of metafields) {
    if (m.key === 'dw_sku' && /^HWC-\d+/i.test(String(m.value || ''))) return String(m.value).trim().toUpperCase();
  }
  return null;
}
function dwSkuNamespaces(metafields) {
  // which dw_sku metafields currently hold a DWH* fabricated code (must be rewritten to HWC)
  const out = [];
  for (const m of metafields) {
    if (m.key === 'dw_sku' && /^DWH/i.test(String(m.value || ''))) out.push({ namespace: m.namespace, value: m.value, metafield_id: m.id });
  }
  return out;
}

async function main() {
  // Enumerate target product-ids from the mirror (product-level metafields are reliable there).
  const { rows: prods } = await pool.query(
    `SELECT DISTINCT shopify_id, handle FROM shopify_products WHERE ${BASE} ORDER BY shopify_id`);
  console.error(`target products from mirror: ${prods.length}`);

  const have = [];   // remappable now (real HWC-####)
  const held = [];   // no HWC -> held recovery bucket
  const problems = [];

  let i = 0;
  for (const p of prods) {
    i++;
    const gid = p.shopify_id;                          // gid://shopify/Product/#####
    const numId = gid.replace(/^.*\/Product\//, '');
    let product, metafields;
    try {
      const pr = await getJSON(`https://${SHOP}/admin/api/${VER}/products/${numId}.json?fields=id,handle,title,variants`);
      product = pr.product;
      const mr = await getJSON(`https://${SHOP}/admin/api/${VER}/products/${numId}/metafields.json`);
      metafields = mr.metafields || [];
    } catch (e) {
      problems.push({ product_id: numId, handle: p.handle, why: 'fetch_failed', err: String(e).slice(0, 200) });
      continue;
    }
    if (!product) { problems.push({ product_id: numId, handle: p.handle, why: 'no_product' }); continue; }

    const variants = (product.variants || []).map(v => ({
      variant_id: v.id, sku: v.sku, title: v.title, price: v.price,
    }));
    // classify variants — base "Sold Per Yard" (sellable) + sample
    const sampleV = variants.find(v => /-sample$/i.test(v.sku || '') || /sample/i.test(v.title || ''));
    const baseV = variants.find(v => v !== sampleV && /^DWHD/i.test(v.sku || ''));

    const hwc = extractHWC(metafields);
    const mfr = (metafields.find(m => m.key === 'manufacturer_sku') || {}).value || null;

    const rec = {
      product_id: numId, gid, handle: product.handle, title: product.title,
      variants,
      base_variant: baseV ? { variant_id: baseV.variant_id, old_sku: baseV.sku, title: baseV.title, price: baseV.price } : null,
      sample_variant: sampleV ? { variant_id: sampleV.variant_id, old_sku: sampleV.sku, title: sampleV.title, price: sampleV.price } : null,
      manufacturer_sku: mfr,                            // INTERNAL — never customer-facing, untouched
      hwc,                                              // real HWC-#### or null
      polluted_dw_sku_metafields: dwSkuNamespaces(metafields),
    };

    // structural sanity: both variants must be present + base sku is DWHD
    if (!rec.base_variant || !rec.sample_variant) {
      problems.push({ product_id: numId, handle: product.handle, why: 'missing_variant', variants });
    }

    if (hwc) {
      // build the new SKUs from HWC — both variants
      rec.new_base_sku = hwc;                           // HWC-####
      rec.new_sample_sku = `${hwc}-Sample`;             // HWC-####-Sample
      // does the observed base sku equal (sample sku minus -Sample)? (verify assumption, don't rely on it)
      rec.base_matches_sample_stem = !!(rec.base_variant && rec.sample_variant &&
        rec.base_variant.old_sku.toUpperCase() === rec.sample_variant.old_sku.replace(/-sample$/i, '').toUpperCase());
      have.push(rec);
    } else {
      held.push(rec);
    }
    if (i % 50 === 0) console.error(`  ...${i}/${prods.length} (have=${have.length} held=${held.length})`);
  }

  // ---- VERIFY the HAVE bucket ----
  // (1) zero within-set HWC collisions (two live products -> same HWC = must NOT collapse)
  const codeCount = {};
  for (const r of have) codeCount[r.new_base_sku] = (codeCount[r.new_base_sku] || 0) + 1;
  const dupCodes = Object.entries(codeCount).filter(([, n]) => n > 1).map(([c]) => c);
  const collidingWithin = have.filter(r => dupCodes.includes(r.new_base_sku));
  const applySet = have.filter(r => !dupCodes.includes(r.new_base_sku));

  // (2) zero collision vs EXISTING live SKUs (any product NOT in our target set already using HWC-####)
  const targetVids = new Set();
  for (const r of have) for (const v of r.variants) targetVids.add(String(v.variant_id));
  // pull all live HWC-* skus from the mirror (fast) — any HWC not belonging to our targets is a collision
  const { rows: liveHwc } = await pool.query(
    `SELECT variant_id, variant_sku FROM shopify_products WHERE variant_sku ~* '^HWC-'`);
  const existingHW = new Set(
    liveHwc.filter(e => !targetVids.has(String(e.variant_id)))
      .map(e => e.variant_sku.replace(/-sample$/i, '').toUpperCase()));
  const collisionsVsExisting = [...new Set(applySet.map(r => r.new_base_sku.toUpperCase()).filter(c => existingHW.has(c)))];

  // (3) zero bare-number / Momentum-number leakage in the result (new sku must never be all-digits or == mfr#)
  const bareLeak = applySet.filter(r => /^\d+$/.test(r.new_base_sku.replace(/^HWC-/i, '')) === false ? false : false);
  const equalsMfr = applySet.filter(r => r.manufacturer_sku && r.new_base_sku === String(r.manufacturer_sku));
  const dwhResidue = applySet.filter(r => /^DWH/i.test(r.new_base_sku) || /DWH/i.test(r.new_sample_sku));
  // any new sku that is a pure momentum bare number (all digits)
  const anyBareDigits = applySet.filter(r => /^\d+$/.test(r.new_base_sku));

  const summary = {
    generated_at: new Date().toISOString(),
    ticket: 'TK-10633',
    source: 'LIVE Shopify (both variants) + mirror metafields for HWC extraction',
    identity_scheme: 'real HWC-#### where it exists (dw_sku metafield); else HELD (stay DWHD)',
    total_products: prods.length,
    bucket_HAVE_hwc: have.length,
    bucket_HELD_no_hwc: held.length,
    apply_set_count: applySet.length,
    held_within_collision_rows: collidingWithin.length,
    problems: problems.length,
    base_stem_mismatches: have.filter(r => r.base_matches_sample_stem === false).length,
    verify: {
      within_set_hwc_collisions: dupCodes.length,       // held out of apply-set
      collisions_vs_existing_live_HW: collisionsVsExisting.length, // MUST be 0
      dwh_residue_in_new_skus: dwhResidue.length,        // MUST be 0
      new_sku_all_digits_bareleak: anyBareDigits.length, // MUST be 0
      new_sku_equals_mfr_number: equalsMfr.length,       // MUST be 0
      colliding_codes: dupCodes,
      collisions_vs_existing: collisionsVsExisting,
    },
  };

  writeFileSync('data/hwc-restore-map.json', JSON.stringify({
    summary, problems,
    apply_set: applySet,             // HAVE-bucket, clean, both variants -> HWC-####
    held_collision_rows: collidingWithin,
    held_recovery_bucket: held,      // LACK-HWC, stay DWHD (LBI Boyd recovery)
  }, null, 2));
  console.log(JSON.stringify(summary, null, 2));
  await pool.end();
}
main().catch(e => { console.error(e); process.exit(1); });