← back to Professional Directory

scripts/seed_specialties.js

85 lines

#!/usr/bin/env node
/**
 * Seed the `specialties` table from the official NUCC Health Care Provider
 * Taxonomy code set. Free public download — no key required.
 *
 *   https://www.nucc.org/  →  Code Sets  →  Provider Taxonomy
 *
 * The CSV ships with these columns (varies slightly across versions, but
 * `Code` and `Display Name`/`Classification` are stable):
 *   Code, Grouping, Classification, Specialization, Definition, Notes,
 *   Display Name, Section
 */
const fs = require('node:fs');
const path = require('node:path');
const { parse } = require('csv-parse/sync');
const { fetch } = require('undici');
const { pool, query } = require('../agents/shared/db');

const NUCC_URL = process.env.NUCC_URL
  || 'https://www.nucc.org/images/stories/CSV/nucc_taxonomy_250.csv';

const CACHE = path.resolve(__dirname, '..', 'data', 'nucc_taxonomy.csv');

async function downloadIfNeeded() {
  if (fs.existsSync(CACHE)) {
    const stat = fs.statSync(CACHE);
    const ageDays = (Date.now() - stat.mtimeMs) / 86400000;
    if (ageDays < 30) {
      console.log(`[nucc] using cache ${CACHE} (${ageDays.toFixed(1)}d old)`);
      return fs.readFileSync(CACHE, 'utf8');
    }
  }
  console.log(`[nucc] downloading ${NUCC_URL}`);
  const res = await fetch(NUCC_URL, { signal: AbortSignal.timeout(30000) });
  if (!res.ok) throw new Error(`NUCC download failed: HTTP ${res.status}`);
  const body = await res.text();
  fs.mkdirSync(path.dirname(CACHE), { recursive: true });
  fs.writeFileSync(CACHE, body);
  console.log(`[nucc] cached → ${CACHE} (${body.length} bytes)`);
  return body;
}

function pickName(row) {
  // Prefer "Display Name", then Classification + Specialization, else Code.
  const display = (row['Display Name'] || '').trim();
  if (display) return display;
  const cls = (row['Classification'] || '').trim();
  const spec = (row['Specialization'] || '').trim();
  if (cls && spec) return `${cls} — ${spec}`;
  return cls || (row['Code'] || '').trim();
}

async function main() {
  const csv = await downloadIfNeeded();
  const rows = parse(csv, { columns: true, skip_empty_lines: true, bom: true, relax_column_count: true });
  console.log(`[nucc] parsed ${rows.length} taxonomy rows`);

  let inserted = 0, updated = 0;
  for (const row of rows) {
    const code = (row['Code'] || '').trim();
    if (!code) continue;
    const name = pickName(row);
    const parent = (row['Grouping'] || row['Classification'] || null) || null;

    const r = await query(`
      INSERT INTO specialties (name, taxonomy_code, parent_specialty)
      VALUES ($1, $2, $3)
      ON CONFLICT (taxonomy_code) DO UPDATE
        SET name = EXCLUDED.name,
            parent_specialty = COALESCE(EXCLUDED.parent_specialty, specialties.parent_specialty)
      RETURNING (xmax = 0) AS is_insert
    `, [name, code, parent]);
    if (r.rows[0].is_insert) inserted++; else updated++;
  }

  console.log(`[nucc] done. inserted=${inserted} updated=${updated}`);
  await pool.end();
}

main().catch(async (err) => {
  console.error('[nucc] fatal:', err);
  try { await pool.end(); } catch (_) {}
  process.exit(1);
});