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