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