← back to AbramsOS
NDC-precise FDA recall match: flag fills whose actual dispensed product is recalled
b02722e7e9fe2474dcb13d36e7722a29907b069e · 2026-07-08 15:30:08 -0700 · Steve
- scripts/ndc-recall-check.js: match each fill's 11-digit package NDC to openFDA product NDC
(leading-zero-normalized labeler|product, exact-confirmed); 4/46 NDCs on a recall list, 40 fills
- recalls tab: prominent 'Your actual products on an FDA recall list' NDC section (headline signal)
- Rx tab: red RECALL badge on flagged fills (CREON, Fluticasone, Nystatin, Atorvastatin)
Files touched
M routes/recalls.jsA scripts/ndc-recall-check.jsM views/prescriptions.ejsM views/recalls.ejs
Diff
commit b02722e7e9fe2474dcb13d36e7722a29907b069e
Author: Steve <steve@designerwallcoverings.com>
Date: Wed Jul 8 15:30:08 2026 -0700
NDC-precise FDA recall match: flag fills whose actual dispensed product is recalled
- scripts/ndc-recall-check.js: match each fill's 11-digit package NDC to openFDA product NDC
(leading-zero-normalized labeler|product, exact-confirmed); 4/46 NDCs on a recall list, 40 fills
- recalls tab: prominent 'Your actual products on an FDA recall list' NDC section (headline signal)
- Rx tab: red RECALL badge on flagged fills (CREON, Fluticasone, Nystatin, Atorvastatin)
---
routes/recalls.js | 15 +++++++-
scripts/ndc-recall-check.js | 86 +++++++++++++++++++++++++++++++++++++++++++++
views/prescriptions.ejs | 4 ++-
views/recalls.ejs | 32 +++++++++++++++++
4 files changed, 135 insertions(+), 2 deletions(-)
diff --git a/routes/recalls.js b/routes/recalls.js
index cfe267c..d805b3f 100644
--- a/routes/recalls.js
+++ b/routes/recalls.js
@@ -51,7 +51,20 @@ router.get('/recalls', async (req, res) => {
)).rows;
const medRecallTotal = medRecalls.reduce((s, x) => s + x.n, 0);
- res.render('recalls', { matches: r.rows, counts: counts.rows, activeStatus: status, medRecalls, medRecallTotal });
+ // NDC-PRECISE: fills whose ACTUAL dispensed product (NDC) is on an FDA recall list. This is the
+ // actionable signal (vs the by-name advisory above). Grouped per product, most-severe first.
+ const ndcFlags = (await db.query(
+ `SELECT pf.drug_name, pf.ndc, pf.ndc_recall_number, pf.ndc_recall_class, pf.ndc_recall_reason,
+ coalesce(pp.full_name,'—') AS person, count(*)::int AS fills,
+ min(pf.fill_date) AS first_fill, max(pf.fill_date) AS last_fill
+ FROM prescription_fill pf LEFT JOIN person pp ON pp.id = pf.person_id
+ WHERE pf.user_id = $1 AND pf.ndc_recalled
+ GROUP BY pf.drug_name, pf.ndc, pf.ndc_recall_number, pf.ndc_recall_class, pf.ndc_recall_reason, pp.full_name
+ ORDER BY (CASE pf.ndc_recall_class WHEN 'Class I' THEN 1 WHEN 'Class II' THEN 2 ELSE 3 END), max(pf.fill_date) DESC`,
+ [DEV_USER_ID]
+ )).rows;
+
+ res.render('recalls', { matches: r.rows, counts: counts.rows, activeStatus: status, medRecalls, medRecallTotal, ndcFlags });
});
router.get('/api/recalls', async (_req, res) => {
diff --git a/scripts/ndc-recall-check.js b/scripts/ndc-recall-check.js
new file mode 100644
index 0000000..f309bdc
--- /dev/null
+++ b/scripts/ndc-recall-check.js
@@ -0,0 +1,86 @@
+#!/usr/bin/env node
+// NDC-PRECISE FDA recall match. The by-name pass is advisory ("this drug has had recalls");
+// this pass matches each fill's ACTUAL dispensed NDC against openFDA enforcement records, so a
+// flag means "your specific product/manufacturer is on a recall list" — the actionable signal.
+//
+// Our fills store 11-digit package NDCs ("00032-3016-13"); openFDA stores 10-digit product NDCs
+// with leading zeros stripped ("0032-3016"). We query a couple of candidate formats, then CONFIRM
+// each hit by comparing leading-zero-stripped labeler|product pairs — exact, conservative.
+//
+// openFDA = free ($0). Idempotent. Usage: node scripts/ndc-recall-check.js
+
+const db = require('../lib/db');
+const audit = require('../lib/audit');
+const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
+const USER = 'user_steve', SOURCE = 'shangoo-pharmacy-tax-2023-2026';
+const BASE = 'https://api.fda.gov/drug/enforcement.json';
+const KEY = process.env.OPENFDA_API_KEY ? `&api_key=${process.env.OPENFDA_API_KEY}` : '';
+
+// leading-zero-stripped "labeler|product" from any NDC (ignores the package segment)
+function segPair(ndc) {
+ const p = String(ndc || '').split('-');
+ if (p.length < 2 || !/^\d+$/.test(p[0]) || !/^\d+$/.test(p[1])) return null;
+ return `${parseInt(p[0], 10)}|${parseInt(p[1], 10)}`;
+}
+// candidate product_ndc query strings for a stored 5-4-2 package NDC
+function candidates(ndc) {
+ const p = String(ndc).split('-'); if (p.length < 2) return [];
+ const c = new Set([`${p[0]}-${p[1]}`]); // as-stored labeler (e.g. 00032-3016)
+ if (p[0][0] === '0') c.add(`${p[0].replace(/^0/, '')}-${p[1]}`); // one leading zero off (0032-3016)
+ return [...c];
+}
+const sev = (c) => (c === 'Class I' ? 3 : c === 'Class II' ? 2 : c === 'Class III' ? 1 : 0);
+
+async function main() {
+ await db.query(`ALTER TABLE prescription_fill
+ ADD COLUMN IF NOT EXISTS ndc_recalled boolean NOT NULL DEFAULT false,
+ ADD COLUMN IF NOT EXISTS ndc_recall_number text,
+ ADD COLUMN IF NOT EXISTS ndc_recall_class text,
+ ADD COLUMN IF NOT EXISTS ndc_recall_reason text`);
+ await db.query(`UPDATE prescription_fill SET ndc_recalled=false, ndc_recall_number=NULL, ndc_recall_class=NULL, ndc_recall_reason=NULL
+ WHERE user_id=$1 AND source=$2`, [USER, SOURCE]);
+
+ const ndcs = (await db.query(
+ `SELECT DISTINCT ndc FROM prescription_fill WHERE user_id=$1 AND source=$2
+ AND ndc IS NOT NULL AND ndc<>'' AND ndc NOT LIKE '00000%'`, [USER, SOURCE])).rows.map((r) => r.ndc);
+ console.log(`checking ${ndcs.length} distinct NDCs against openFDA (NDC-precise)…`);
+
+ let flagged = 0, fillsFlagged = 0;
+ for (const ndc of ndcs) {
+ const want = segPair(ndc); if (!want) continue;
+ let best = null;
+ for (const cand of candidates(ndc)) {
+ let results = [];
+ try {
+ const r = await fetch(`${BASE}?search=openfda.product_ndc:%22${encodeURIComponent(cand)}%22&limit=20${KEY}`,
+ { headers: { 'User-Agent': 'AbramsOS/1.0 (personal household admin)' } });
+ if (r.status === 404) { await sleep(180); continue; }
+ if (r.ok) results = (await r.json()).results || [];
+ } catch { /* transient */ }
+ for (const rec of results) {
+ const pn = (rec.openfda && rec.openfda.product_ndc) || [];
+ if (!pn.some((x) => segPair(x) === want)) continue; // CONFIRM exact labeler|product
+ const cls = rec.classification || null;
+ if (!best || sev(cls) > sev(best.classification)) best = rec;
+ }
+ await sleep(180);
+ }
+ if (best) {
+ const up = await db.query(
+ `UPDATE prescription_fill SET ndc_recalled=true, ndc_recall_number=$3, ndc_recall_class=$4, ndc_recall_reason=$5
+ WHERE user_id=$1 AND source=$2 AND ndc=$6`,
+ [USER, SOURCE, best.recall_number || null, best.classification || null,
+ (best.reason_for_recall || '').slice(0, 300), ndc]);
+ flagged++; fillsFlagged += up.rowCount;
+ console.log(` ⚠ NDC ${ndc} → ${best.classification} ${best.recall_number} (${up.rowCount} fills)`);
+ }
+ }
+
+ await audit.log({ actorType: 'user', actorId: USER, objectType: 'prescription_fill', objectId: SOURCE,
+ eventType: 'fda_ndc_recall_crossref', metadata: { medical: true, source: 'openFDA', ndcs: ndcs.length, ndcs_recalled: flagged, fills_flagged: fillsFlagged } });
+
+ console.log(`\nNDC-precise DONE: ${flagged} of ${ndcs.length} NDCs on a recall list · ${fillsFlagged} fills flagged`);
+ if (!flagged) console.log(' ✓ None of your actual dispensed products appear in an FDA recall.');
+ process.exit(0);
+}
+main().catch((e) => { console.error('FAILED:', e.message); process.exit(1); });
diff --git a/views/prescriptions.ejs b/views/prescriptions.ejs
index 38a7633..d338c49 100644
--- a/views/prescriptions.ejs
+++ b/views/prescriptions.ejs
@@ -46,7 +46,7 @@
data-find="<%= ((f.drug_name||'')+' '+(f.doctor||'')+' '+(f.rx_number||'')).toLowerCase() %>">
<td class="mono"><%= f.fill_date ? String(f.fill_date).slice(0,10) : '' %></td>
<td><%= f.person_name || '' %></td>
- <td><%= f.drug_name %></td>
+ <td><%= f.drug_name %><% if (f.ndc_recalled) { %> <span class="recall-flag" title="This dispensed product (NDC <%= f.ndc %>) is on an FDA recall list: <%= f.ndc_recall_class %> <%= f.ndc_recall_number %>">⚠ RECALL</span><% } %></td>
<td class="mono"><%= f.rx_number || '' %><% if (f.refill) { %><span class="subtle"> · rf<%= f.refill %></span><% } %></td>
<td class="num"><%= f.quantity != null ? f.quantity : '' %></td>
<td class="num"><%= f.days_supply != null ? f.days_supply : '' %></td>
@@ -70,6 +70,8 @@
.rx-table td.num, .rx-table th.num { text-align:right; }
.rx-table td.mono { font-variant-numeric:tabular-nums; }
.rx-table tbody tr:hover { background:rgba(255,255,255,.03); }
+ .recall-flag { display:inline-block; margin-left:6px; padding:0 6px; border-radius:4px; font-size:10px; font-weight:700;
+ background:rgba(248,113,113,.18); color:#f87171; border:1px solid rgba(248,113,113,.5); white-space:nowrap; }
</style>
<script>
(function(){
diff --git a/views/recalls.ejs b/views/recalls.ejs
index 715d348..cb92ba8 100644
--- a/views/recalls.ejs
+++ b/views/recalls.ejs
@@ -5,6 +5,38 @@
<p class="subtle" style="margin:0;color:var(--text-dim);font-size:13px">CPSC recalls auto-matched to your purchases. Confidence ≥ 0.60 lands here for review.</p>
</section>
+<% const ndcF = (typeof ndcFlags !== 'undefined') ? ndcFlags : []; %>
+<% if (ndcF.length) { %>
+<section class="page-head" style="margin-top:18px">
+ <div>
+ <h1 style="font-size:16px">🚨 Your actual products on an FDA recall list <span class="subtle" style="font-weight:400">(NDC-matched)</span></h1>
+ <p class="subtle" style="margin:0;color:var(--text-dim);font-size:13px">
+ The exact product/manufacturer you were dispensed (matched by <strong>NDC</strong>) appears in an FDA recall.
+ This is the actionable list — unlike the by-name section below, these are <strong>your specific products</strong>.
+ </p>
+ </div>
+</section>
+<section class="glass" style="padding:8px 6px;overflow:auto;border:1px solid rgba(248,113,113,.35)">
+ <table class="mr-table">
+ <thead><tr><th>Severity</th><th>Medication</th><th>Person</th><th>NDC</th><th>Recall #</th><th class="num">Fills</th><th>Fill dates</th><th>Reason</th></tr></thead>
+ <tbody>
+ <% ndcF.forEach(function(x){ var badge = x.ndc_recall_class==='Class I' ? '#f87171' : x.ndc_recall_class==='Class II' ? '#fbbf24' : '#9aa0aa'; %>
+ <tr>
+ <td><span class="sev" style="background:<%= badge %>22;color:<%= badge %>;border:1px solid <%= badge %>55"><%= x.ndc_recall_class || '—' %></span></td>
+ <td><%= x.drug_name %></td>
+ <td><%= x.person %></td>
+ <td class="mono"><%= x.ndc %></td>
+ <td class="mono"><a href="https://www.accessdata.fda.gov/scripts/ires/index.cfm?Product=<%= encodeURIComponent(x.ndc_recall_number||'') %>" target="_blank" rel="noopener noreferrer"><%= x.ndc_recall_number || '' %></a></td>
+ <td class="num"><%= x.fills %></td>
+ <td class="mono"><%= x.first_fill ? String(x.first_fill).slice(0,10) : '' %> → <%= x.last_fill ? String(x.last_fill).slice(0,10) : '' %></td>
+ <td class="reason subtle"><%= (x.ndc_recall_reason||'').slice(0,110) %></td>
+ </tr>
+ <% }) %>
+ </tbody>
+ </table>
+</section>
+<% } %>
+
<% const medR = (typeof medRecalls !== 'undefined') ? medRecalls : []; %>
<% if (medR.length) { %>
<section class="page-head" style="margin-top:18px">
← 78b80e6 FDA recall cross-reference: clean drug names, derive generic
·
back to AbramsOS
·
Recalls: add curated article/FDA links per NDC-recalled prod 2caebba →