[object Object]

← 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

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 →