← back to AbramsOS

scripts/check-fda-recalls.js

107 lines

#!/usr/bin/env node
// Clean the imported pharmacy drug names, rebuild a deduped medication list with proper
// GENERIC names, then cross-reference every med against the FDA openFDA drug-enforcement
// (recall) database. Matches land in medication_recall → the Recalls tab.
//
// openFDA is a FREE public API ($0). A drug NAME is not PHI. Idempotent.
// Usage: node scripts/check-fda-recalls.js

const db = require('../lib/db');
const audit = require('../lib/audit');
const fda = require('../lib/fda-fetcher');
const { id } = require('../lib/ids');

const USER = 'user_steve';
const SOURCE = 'shangoo-pharmacy-tax-2023-2026';
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

// ── name cleaning ────────────────────────────────────────────────────────────────
// Strip the trailing Rx-number / qty / doctor / OCR-junk that leaked into some drug
// strings, and normalize internal OCR noise ("750.-MG" → "750 MG").
function cleanDrug(raw) {
  let s = String(raw || '').toUpperCase();
  s = s.split(/\s+\d{5,6}\b/)[0];              // drop from the Rx number onward
  s = s.split(/\s+[A-Z][A-Z'.]+,\s/)[0];       // drop from a "LASTNAME, " doctor onward
  s = s.replace(/[|~©"“”‘’]/g, ' ');           // OCR junk chars
  s = s.replace(/(\d)\.\s*-?\s*(MG|MCG|UNIT)/g, '$1 $2'); // "750.-MG"/"200. MG" → "750 MG"
  s = s.replace(/\s+/g, ' ').replace(/^[.\-\s]+|[.\-\s]+$/g, '').trim();
  return s;
}
// core active-ingredient name for the FDA search (brand/generic both work in openFDA)
const OVERRIDE = { 'HYDROCODON-APAP': 'HYDROCODONE', 'HYDROCODONE-APAP': 'HYDROCODONE',
  'FOLIC': 'FOLIC ACID', 'ACCU-CHEK': null, 'NA': null, 'GLUTOSE': null };
function genericOf(name) {
  const w = String(name).trim().split(/\s+/)[0].replace(/[^A-Z\-]/gi, '').toUpperCase();
  if (Object.prototype.hasOwnProperty.call(OVERRIDE, w)) return OVERRIDE[w];
  return w || null;
}
const strengthOf = (name) => {
  const m = String(name).match(/(\d[\d.\/-]*\s?(?:MG|MCG|UNIT|%))/i);
  return m ? m[1].toUpperCase().replace(/\s/g, '') : '';
};

async function main() {
  // 1. clean the fill drug names for this source (raw_text keeps the original)
  const fills = (await db.query(
    `SELECT id, drug_name FROM prescription_fill WHERE user_id=$1 AND source=$2`, [USER, SOURCE])).rows;
  let cleaned = 0;
  for (const f of fills) {
    const c = cleanDrug(f.drug_name);
    if (c && c !== f.drug_name) { await db.query(`UPDATE prescription_fill SET drug_name=$1 WHERE id=$2`, [c, f.id]); cleaned++; }
  }
  console.log(`cleaned ${cleaned} of ${fills.length} fill drug names`);

  // 2. rebuild the deduped medication list from cleaned fills (dedup by generic+strength)
  await db.query(`DELETE FROM medication WHERE user_id=$1 AND notes LIKE '%fill history%'`, [USER]);
  const rows = (await db.query(
    `SELECT pf.person_id, pf.drug_name, pf.doctor, pf.rx_number, pf.fill_date
       FROM prescription_fill pf WHERE pf.user_id=$1 AND pf.source=$2
        AND pf.drug_name NOT ILIKE '%DELIVERY FEE%'
      ORDER BY pf.fill_date`, [USER, SOURCE])).rows;
  const meds = new Map();
  for (const r of rows) {
    const g = genericOf(r.drug_name);
    const key = `${r.person_id}|${g || r.drug_name}|${strengthOf(r.drug_name)}`;
    const m = meds.get(key) || { person_id: r.person_id, name: r.drug_name, generic: g, dosage: strengthOf(r.drug_name), doctor: r.doctor, rx: r.rx_number, start: r.fill_date };
    if (r.fill_date >= m.start) { m.doctor = r.doctor || m.doctor; m.rx = r.rx_number || m.rx; } // latest wins for prescriber
    if (r.fill_date < m.start) m.start = r.fill_date;
    meds.set(key, m);
  }
  for (const m of meds.values()) {
    await db.query(
      `INSERT INTO medication (id,user_id,person_id,name,generic_name,dosage,prescriber,pharmacy,rx_number,start_date,is_active,notes)
       VALUES ($1,$2,$3,$4,$5,$6,$7,'Shangoo Pharmacy',$8,$9,true,'Auto-derived from Shangoo Pharmacy fill history.')`,
      [id('medication'), USER, m.person_id, m.name, m.generic, m.dosage || null, m.doctor || null, m.rx || null, m.start || null]);
  }
  console.log(`rebuilt ${meds.size} deduped medications`);

  // 3. FDA recall cross-reference for every med (openFDA, free)
  const list = (await db.query(`SELECT * FROM medication WHERE user_id=$1 AND is_active`, [USER])).rows;
  let checked = 0, withHits = 0, totalRecalls = 0; const hitList = [];
  for (const med of list) {
    const term = med.generic_name || med.name;
    if (!term) continue;
    let hits = [];
    try { hits = await fda.fetchDrugRecalls(term); } catch (e) { console.error(`  ${term}: ${e.message}`); }
    checked++;
    for (const h of hits) {
      await db.query(
        `INSERT INTO medication_recall (id,user_id,medication_id,source,recall_number,classification,status,reason,product_description,recall_initiation_date,url,raw_jsonb)
         VALUES ($1,$2,$3,'openFDA',$4,$5,$6,$7,$8,$9,$10,$11)
         ON CONFLICT (medication_id, recall_number) DO UPDATE SET status=EXCLUDED.status, reason=EXCLUDED.reason, matched_at=now()`,
        [id('medrecall'), USER, med.id, h.recall_number, h.classification, h.status, h.reason, h.product_description, h.recall_initiation_date, h.url, JSON.stringify(h.raw)]);
    }
    if (hits.length) { withHits++; totalRecalls += hits.length; hitList.push(`${med.name} (${med.person_id ? '' : ''}) → ${hits.length} recall(s) [${[...new Set(hits.map((h) => h.classification))].join(', ')}]`); }
    await sleep(250); // be gentle on the free API
  }
  await audit.log({ actorType: 'user', actorId: USER, objectType: 'medication_recall', objectId: SOURCE,
    eventType: 'fda_recall_crossref', metadata: { medical: true, source: 'openFDA', checked, medications_with_recalls: withHits, recalls: totalRecalls } });

  console.log(`\nFDA cross-reference DONE — checked ${checked} meds`);
  console.log(`  ${withHits} medications matched an FDA recall · ${totalRecalls} recall records stored`);
  if (hitList.length) { console.log('\nMatches:'); hitList.forEach((h) => console.log('  ⚠ ' + h)); }
  else console.log('  ✓ No FDA recalls matched any of your medications.');
  process.exit(0);
}
main().catch((e) => { console.error('FAILED:', e.message); process.exit(1); });