← back to AbramsOS
routes/medications.js
143 lines
// Medications — self-entered meds per household member, with on-demand FDA recall check.
// Health data: AGENTS.md gates medical features. These rows are user-entered (allowed),
// and every write is audit-logged with a medical-consent marker.
const express = require('express');
const db = require('../lib/db');
const audit = require('../lib/audit');
const { id } = require('../lib/ids');
const fda = require('../lib/fda-fetcher');
const router = express.Router();
const DEV_USER_ID = 'user_steve';
async function listMeds(userId) {
const r = await db.query(
`SELECT m.*, p.full_name AS person_name, p.relation AS person_relation,
(SELECT count(*)::int FROM medication_recall mr WHERE mr.medication_id = m.id) AS recall_count,
(SELECT max(matched_at) FROM medication_recall mr WHERE mr.medication_id = m.id) AS last_checked
FROM medication m LEFT JOIN person p ON p.id = m.person_id
WHERE m.user_id = $1
ORDER BY m.is_active DESC, m.name ASC`,
[userId]
);
return r.rows;
}
async function recallsFor(userId, medId) {
const r = await db.query(
`SELECT * FROM medication_recall WHERE user_id = $1 AND medication_id = $2 ORDER BY recall_initiation_date DESC NULLS LAST`,
[userId, medId]
);
return r.rows;
}
router.get('/medications', async (_req, res) => {
const [meds, people] = await Promise.all([
listMeds(DEV_USER_ID),
db.query(`SELECT id, full_name, relation FROM person WHERE user_id = $1 ORDER BY (relation<>'self'), full_name`, [DEV_USER_ID]),
]);
// attach stored recalls to each med for render
for (const m of meds) m.recalls = m.recall_count ? await recallsFor(DEV_USER_ID, m.id) : [];
res.render('medications', { meds, people: people.rows });
});
router.get('/api/medications', async (_req, res) => {
res.json(await listMeds(DEV_USER_ID));
});
router.post('/api/medications', async (req, res) => {
try {
const b = req.body || {};
if (!b.name) return res.status(400).json({ error: 'name required' });
const mId = id('medication');
await db.query(
`INSERT INTO medication (id, user_id, person_id, name, generic_name, dosage, frequency, form, prescriber, pharmacy, rx_number, start_date, is_active, notes)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,
[mId, DEV_USER_ID, b.person_id || null, b.name, b.generic_name || null, b.dosage || null,
b.frequency || null, b.form || null, b.prescriber || null, b.pharmacy || null, b.rx_number || null,
b.start_date || null, b.is_active === false || b.is_active === 'false' ? false : true, b.notes || null]
);
await audit.log({ actorType: 'user', actorId: DEV_USER_ID, objectType: 'medication', objectId: mId, eventType: 'medication_created', metadata: { medical: true, consent: 'user-entered', name: b.name } });
const r = await db.query(`SELECT * FROM medication WHERE id = $1`, [mId]);
res.status(201).json(r.rows[0]);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
router.post('/api/medications/:id', async (req, res) => {
try {
const b = req.body || {};
const fields = ['person_id', 'name', 'generic_name', 'dosage', 'frequency', 'form', 'prescriber', 'pharmacy', 'rx_number', 'start_date', 'is_active', 'notes'];
const sets = [], vals = [];
let i = 1;
for (const f of fields) {
if (b[f] === undefined) continue;
sets.push(`${f} = $${i++}`);
vals.push(f === 'is_active' ? !(b[f] === false || b[f] === 'false') : (b[f] === '' ? null : b[f]));
}
if (!sets.length) return res.status(400).json({ error: 'no fields to update' });
sets.push(`updated_at = now()`);
vals.push(req.params.id, DEV_USER_ID);
const r = await db.query(`UPDATE medication SET ${sets.join(', ')} WHERE id = $${i++} AND user_id = $${i} RETURNING *`, vals);
if (!r.rows.length) return res.status(404).json({ error: 'not found' });
await audit.log({ actorType: 'user', actorId: DEV_USER_ID, objectType: 'medication', objectId: req.params.id, eventType: 'medication_updated', metadata: { medical: true } });
res.json(r.rows[0]);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
router.post('/api/medications/:id/delete', async (req, res) => {
await db.query(`DELETE FROM medication WHERE id = $1 AND user_id = $2`, [req.params.id, DEV_USER_ID]);
await audit.log({ actorType: 'user', actorId: DEV_USER_ID, objectType: 'medication', objectId: req.params.id, eventType: 'medication_deleted', metadata: { medical: true } });
res.json({ ok: true });
});
// Check FDA recalls for one medication → upsert matches into medication_recall.
async function checkOne(userId, med) {
const hits = await fda.fetchDrugRecalls(med.generic_name || med.name);
// also try brand name if generic differs
if (med.generic_name && med.name && med.generic_name !== med.name) {
const more = await fda.fetchDrugRecalls(med.name);
for (const h of more) if (!hits.find((x) => x.recall_number === h.recall_number)) hits.push(h);
}
let stored = 0;
for (const h of hits) {
const r = 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()
RETURNING (xmax = 0) AS inserted`,
[id('medrecall'), userId, 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 (r.rows[0].inserted) stored += 1;
}
await audit.log({ actorType: 'user', actorId: userId, objectType: 'medication', objectId: med.id, eventType: 'medication_recall_checked', metadata: { medical: true, source: 'openFDA', hits: hits.length } });
return { medication_id: med.id, name: med.name, hits: hits.length, new: stored };
}
router.post('/api/medications/:id/check-recalls', async (req, res) => {
try {
const m = await db.query(`SELECT * FROM medication WHERE id = $1 AND user_id = $2`, [req.params.id, DEV_USER_ID]);
if (!m.rows.length) return res.status(404).json({ error: 'not found' });
const summary = await checkOne(DEV_USER_ID, m.rows[0]);
res.json({ ...summary, recalls: await recallsFor(DEV_USER_ID, req.params.id) });
} catch (err) {
res.status(500).json({ error: `recall check failed: ${err.message}` });
}
});
router.post('/api/medications/check-all-recalls', async (_req, res) => {
try {
const meds = await db.query(`SELECT * FROM medication WHERE user_id = $1 AND is_active`, [DEV_USER_ID]);
const results = [];
for (const med of meds.rows) results.push(await checkOne(DEV_USER_ID, med));
res.json({ checked: results.length, results });
} catch (err) {
res.status(500).json({ error: `recall check failed: ${err.message}` });
}
});
module.exports = router;