← back to AbramsOS
routes/recalls.js
96 lines
// /recalls — read-only viewer for recall_match × recall_event × purchase.
const express = require('express');
const db = require('../lib/db');
const router = express.Router();
const DEV_USER_ID = 'user_steve';
router.get('/recalls', async (req, res) => {
const status = req.query.status || null;
const params = [DEV_USER_ID];
let where = `rm.user_id = $1`;
if (status) {
params.push(status);
where += ` AND rm.status = $${params.length}`;
}
const r = await db.query(
`SELECT rm.id AS match_id, rm.confidence, rm.status, rm.matched_at,
re.id AS recall_id, re.authority, re.external_id, re.title, re.hazard, re.remedy, re.url, re.published_at,
p.id AS purchase_id, p.merchant_name, p.purchase_date, p.total_amount, p.currency
FROM recall_match rm
JOIN recall_event re ON re.id = rm.recall_id
LEFT JOIN purchase p ON p.id = rm.asset_id AND rm.asset_table = 'purchase'
WHERE ${where}
ORDER BY rm.matched_at DESC
LIMIT 200`,
params
);
const counts = await db.query(
`SELECT status, count(*)::int AS n FROM recall_match WHERE user_id = $1 GROUP BY status ORDER BY status`,
[DEV_USER_ID]
);
// FDA medication recalls (openFDA drug-enforcement), cross-referenced to your med list and
// grouped per medication. 'Class I' sorts before 'Class II'/'Class III' so MIN = worst severity.
// Matching is by drug NAME (ingredient) → advisory ("this drug has been recalled"), not lot-specific.
const medRecalls = (await db.query(
`SELECT m.name, m.generic_name, coalesce(pp.full_name,'—') AS person,
count(*)::int AS n, min(mr.classification) AS worst,
max(mr.recall_initiation_date) AS latest,
(array_agg(mr.url ORDER BY mr.recall_initiation_date DESC NULLS LAST))[1] AS sample_url,
(array_agg(mr.reason ORDER BY mr.recall_initiation_date DESC NULLS LAST))[1] AS sample_reason
FROM medication_recall mr
JOIN medication m ON m.id = mr.medication_id
LEFT JOIN person pp ON pp.id = m.person_id
WHERE mr.user_id = $1
GROUP BY m.name, m.generic_name, pp.full_name
ORDER BY min(mr.classification) ASC, count(*) DESC`,
[DEV_USER_ID]
)).rows;
const medRecallTotal = medRecalls.reduce((s, x) => s + x.n, 0);
// 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.
// One row per (product, person). Status/assessment now vary per fill (a 2023 vs 2026 fill of the
// same drug can differ), so we aggregate to the WORST relevance and surface how many fills are
// affected. rk() = relevance rank (possible > confirm > cleared).
const rk = `(CASE pf.ndc_recall_status WHEN 'possible' THEN 3 WHEN 'confirm' THEN 2 ELSE 1 END)`;
const ndcFlags = (await db.query(
`SELECT pf.drug_name, pf.ndc, pf.ndc_recall_class, coalesce(pp.full_name,'—') AS person,
count(*)::int AS fills,
count(*) FILTER (WHERE pf.ndc_recall_status <> 'cleared')::int AS relevant_fills,
min(pf.fill_date) AS first_fill, max(pf.fill_date) AS last_fill,
(array_agg(pf.ndc_recall_links ORDER BY pf.fill_date DESC))[1] AS links,
(array_agg(pf.ndc_recall_status ORDER BY ${rk} DESC))[1] AS status,
(array_agg(pf.ndc_recall_assessment ORDER BY ${rk} DESC))[1] AS assessment,
(array_agg(pf.ndc_recall_number ORDER BY ${rk} DESC))[1] AS ndc_recall_number
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_class, pp.full_name
ORDER BY max(${rk}) DESC,
(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) => {
const r = await db.query(
`SELECT rm.id AS match_id, rm.confidence, rm.status, rm.matched_at,
re.id AS recall_id, re.title, re.url, p.merchant_name
FROM recall_match rm
JOIN recall_event re ON re.id = rm.recall_id
LEFT JOIN purchase p ON p.id = rm.asset_id AND rm.asset_table = 'purchase'
WHERE rm.user_id = $1
ORDER BY rm.matched_at DESC LIMIT 200`,
[DEV_USER_ID]
);
res.json(r.rows);
});
module.exports = router;