← back to Wolfgordon Crawl

assign-skus.js

93 lines

#!/usr/bin/env node
// ============================================================================
// Assign sequential DW SKUs to wolf_gordon_catalog rows that have none.
//
// HARD RULE (Steve 2026-06-19): every DW SKU is DW<XX>-<sequential number> for
// that vendor series. WG series = DWWG-, range start 530000. NEVER embed the
// manufacturer code. REUSE an existing dw_sku wherever one is set (never
// renumber). Assign the next sequential number ONLY to rows with dw_sku IS NULL.
//
// This is a SAFE, LOCAL, REVERSIBLE write to wolf_gordon_catalog.dw_sku only.
// Nothing is written to shopify_products or to live Shopify.
//
// Usage:
//   node assign-skus.js            # dry run (prints plan, writes nothing)
//   node assign-skus.js --apply    # performs the UPDATE inside a transaction
// ============================================================================

const { pool } = require('./scraper-utils');
const APPLY = process.argv.includes('--apply');

async function main() {
  // Next sequential number = max existing DWWG-<n> + 1.
  const { rows: mx } = await pool.query(`
    SELECT COALESCE(MAX((regexp_replace(dw_sku,'^DWWG-',''))::int), 530000) AS maxseq
    FROM wolf_gordon_catalog WHERE dw_sku ~ '^DWWG-[0-9]+$'
  `);
  let next = mx[0].maxseq + 1;

  // Rows needing a SKU, deterministic order (id ASC) so re-runs are stable.
  const { rows } = await pool.query(`
    SELECT id, mfr_sku, pattern_name, color_name
    FROM wolf_gordon_catalog
    WHERE dw_sku IS NULL
    ORDER BY id ASC
  `);

  console.log('='.repeat(70));
  console.log(`  WG SKU ASSIGNMENT  (${APPLY ? 'APPLY' : 'DRY RUN'})`);
  console.log('='.repeat(70));
  console.log(`  Rows needing a dw_sku: ${rows.length}`);
  console.log(`  Next sequential start: DWWG-${next}`);
  if (rows.length === 0) { console.log('  Nothing to do.'); await pool.end(); return; }

  const plan = rows.map(r => ({ id: r.id, dw_sku: `DWWG-${next++}`, r }));
  console.log(`  Range to assign:       ${plan[0].dw_sku} .. ${plan[plan.length-1].dw_sku}`);
  console.log('-'.repeat(70));
  for (const p of plan.slice(0, 8)) {
    console.log(`    id=${String(p.id).padEnd(6)} ${p.dw_sku}  <- ${p.r.mfr_sku} | ${p.r.pattern_name||'(no pat)'} | ${p.r.color_name||'(no color)'}`);
  }
  if (plan.length > 8) console.log(`    ... and ${plan.length - 8} more`);
  console.log('-'.repeat(70));

  if (!APPLY) {
    console.log('  DRY RUN — no writes. Re-run with --apply to commit.');
    await pool.end();
    return;
  }

  // Apply inside a transaction. Guard: each UPDATE only fires if dw_sku is
  // still NULL (idempotent against a concurrent crawl insert).
  const client = await pool.connect();
  let updated = 0;
  try {
    await client.query('BEGIN');
    for (const p of plan) {
      const res = await client.query(
        `UPDATE wolf_gordon_catalog SET dw_sku = $1, updated_at = NOW()
         WHERE id = $2 AND dw_sku IS NULL`,
        [p.dw_sku, p.id]
      );
      updated += res.rowCount;
    }
    // Safety: no duplicate DWWG sequential numbers exist after the write.
    const dup = await client.query(`
      SELECT dw_sku, count(*) c FROM wolf_gordon_catalog
      WHERE dw_sku ~ '^DWWG-[0-9]+$' GROUP BY dw_sku HAVING count(*) > 1`);
    if (dup.rows.length) {
      throw new Error(`ABORT: duplicate dw_sku detected: ${dup.rows.map(d=>d.dw_sku).join(', ')}`);
    }
    await client.query('COMMIT');
    console.log(`  COMMITTED. Rows updated: ${updated}`);
  } catch (e) {
    await client.query('ROLLBACK');
    console.error('  ROLLED BACK:', e.message);
    process.exitCode = 1;
  } finally {
    client.release();
    await pool.end();
  }
}

main().catch(e => { console.error(e); process.exit(1); });