""" Medications API - medication scheduling, logging, and adherence tracking """ import os import uuid import flask import jwt import core.auth as auth import core.postgres as postgres def _get_user_uuid(token): try: payload = jwt.decode(token, os.getenv("JWT_SECRET"), algorithms=["HS256"]) return payload.get("sub") except (jwt.ExpiredSignatureError, jwt.InvalidTokenError): return None def _auth(request): """Extract and verify token. Returns user_uuid or None.""" header = request.headers.get("Authorization", "") if not header.startswith("Bearer "): return None token = header[7:] user_uuid = _get_user_uuid(token) if not user_uuid or not auth.verifyLoginToken(token, userUUID=user_uuid): return None return user_uuid def register(app): # ── Medications CRUD ────────────────────────────────────────── @app.route("/api/medications", methods=["GET"]) def api_listMedications(): """List all medications for the logged-in user.""" user_uuid = _auth(flask.request) if not user_uuid: return flask.jsonify({"error": "unauthorized"}), 401 meds = postgres.select("medications", where={"user_uuid": user_uuid}, order_by="name") return flask.jsonify(meds), 200 @app.route("/api/medications", methods=["POST"]) def api_addMedication(): """Add a medication. Body: {name, dosage, unit, frequency, times: ["08:00","20:00"], notes?}""" user_uuid = _auth(flask.request) if not user_uuid: return flask.jsonify({"error": "unauthorized"}), 401 data = flask.request.get_json() if not data: return flask.jsonify({"error": "missing body"}), 400 required = ["name", "dosage", "unit", "frequency"] missing = [f for f in required if not data.get(f)] if missing: return flask.jsonify({"error": f"missing required fields: {', '.join(missing)}"}), 400 data["id"] = str(uuid.uuid4()) data["user_uuid"] = user_uuid data["times"] = data.get("times", []) data["active"] = True med = postgres.insert("medications", data) return flask.jsonify(med), 201 @app.route("/api/medications/", methods=["GET"]) def api_getMedication(med_id): """Get a single medication with its schedule.""" user_uuid = _auth(flask.request) if not user_uuid: return flask.jsonify({"error": "unauthorized"}), 401 med = postgres.select_one("medications", {"id": med_id, "user_uuid": user_uuid}) if not med: return flask.jsonify({"error": "not found"}), 404 return flask.jsonify(med), 200 @app.route("/api/medications/", methods=["PUT"]) def api_updateMedication(med_id): """Update medication details. Body: {name?, dosage?, unit?, frequency?, times?, notes?, active?}""" user_uuid = _auth(flask.request) if not user_uuid: return flask.jsonify({"error": "unauthorized"}), 401 data = flask.request.get_json() if not data: return flask.jsonify({"error": "missing body"}), 400 existing = postgres.select_one("medications", {"id": med_id, "user_uuid": user_uuid}) if not existing: return flask.jsonify({"error": "not found"}), 404 allowed = ["name", "dosage", "unit", "frequency", "times", "notes", "active"] updates = {k: v for k, v in data.items() if k in allowed} if not updates: return flask.jsonify({"error": "no valid fields to update"}), 400 result = postgres.update("medications", updates, {"id": med_id, "user_uuid": user_uuid}) return flask.jsonify(result[0] if result else {}), 200 @app.route("/api/medications/", methods=["DELETE"]) def api_deleteMedication(med_id): """Delete a medication and its logs.""" user_uuid = _auth(flask.request) if not user_uuid: return flask.jsonify({"error": "unauthorized"}), 401 existing = postgres.select_one("medications", {"id": med_id, "user_uuid": user_uuid}) if not existing: return flask.jsonify({"error": "not found"}), 404 postgres.delete("med_logs", {"medication_id": med_id}) postgres.delete("medications", {"id": med_id, "user_uuid": user_uuid}) return flask.jsonify({"deleted": True}), 200 # ── Medication Logging (take / skip / snooze) ───────────────── @app.route("/api/medications//take", methods=["POST"]) def api_takeMedication(med_id): """Log that a dose was taken. Body: {scheduled_time?, notes?}""" user_uuid = _auth(flask.request) if not user_uuid: return flask.jsonify({"error": "unauthorized"}), 401 med = postgres.select_one("medications", {"id": med_id, "user_uuid": user_uuid}) if not med: return flask.jsonify({"error": "not found"}), 404 data = flask.request.get_json() or {} log_entry = { "id": str(uuid.uuid4()), "medication_id": med_id, "user_uuid": user_uuid, "action": "taken", "scheduled_time": data.get("scheduled_time"), "notes": data.get("notes"), } log = postgres.insert("med_logs", log_entry) return flask.jsonify(log), 201 @app.route("/api/medications//skip", methods=["POST"]) def api_skipMedication(med_id): """Log a skipped dose. Body: {scheduled_time?, reason?}""" user_uuid = _auth(flask.request) if not user_uuid: return flask.jsonify({"error": "unauthorized"}), 401 med = postgres.select_one("medications", {"id": med_id, "user_uuid": user_uuid}) if not med: return flask.jsonify({"error": "not found"}), 404 data = flask.request.get_json() or {} log_entry = { "id": str(uuid.uuid4()), "medication_id": med_id, "user_uuid": user_uuid, "action": "skipped", "scheduled_time": data.get("scheduled_time"), "notes": data.get("reason"), } log = postgres.insert("med_logs", log_entry) return flask.jsonify(log), 201 @app.route("/api/medications//snooze", methods=["POST"]) def api_snoozeMedication(med_id): """Snooze a reminder. Body: {minutes: 15}""" user_uuid = _auth(flask.request) if not user_uuid: return flask.jsonify({"error": "unauthorized"}), 401 med = postgres.select_one("medications", {"id": med_id, "user_uuid": user_uuid}) if not med: return flask.jsonify({"error": "not found"}), 404 data = flask.request.get_json() or {} minutes = data.get("minutes", 15) return flask.jsonify({"snoozed_until_minutes": minutes}), 200 # ── Medication Log / History ────────────────────────────────── @app.route("/api/medications//log", methods=["GET"]) def api_getMedLog(med_id): """Get dose log for a medication. Query: ?days=30""" user_uuid = _auth(flask.request) if not user_uuid: return flask.jsonify({"error": "unauthorized"}), 401 med = postgres.select_one("medications", {"id": med_id, "user_uuid": user_uuid}) if not med: return flask.jsonify({"error": "not found"}), 404 days = flask.request.args.get("days", 30, type=int) logs = postgres.select( "med_logs", where={"medication_id": med_id}, order_by="created_at DESC", limit=days * 10, ) return flask.jsonify(logs), 200 @app.route("/api/medications/today", methods=["GET"]) def api_todaysMeds(): """Get today's medication schedule with taken/pending status.""" user_uuid = _auth(flask.request) if not user_uuid: return flask.jsonify({"error": "unauthorized"}), 401 meds = postgres.select("medications", where={"user_uuid": user_uuid, "active": True}) from datetime import datetime today = datetime.now().strftime("%Y-%m-%d") result = [] for med in meds: times = med.get("times", []) taken_times = [ log["scheduled_time"] for log in postgres.select( "med_logs", where={"medication_id": med["id"], "action": "taken"}, ) if log.get("scheduled_time", "").startswith(today) ] result.append({ "medication": med, "scheduled_times": times, "taken_times": taken_times, }) return flask.jsonify(result), 200 # ── Adherence Stats ─────────────────────────────────────────── @app.route("/api/medications/adherence", methods=["GET"]) def api_adherenceStats(): """Get adherence stats across all meds. Query: ?days=30""" user_uuid = _auth(flask.request) if not user_uuid: return flask.jsonify({"error": "unauthorized"}), 401 days = flask.request.args.get("days", 30, type=int) meds = postgres.select("medications", where={"user_uuid": user_uuid, "active": True}) result = [] for med in meds: logs = postgres.select( "med_logs", where={"medication_id": med["id"]}, limit=days * 10, ) taken = sum(1 for log in logs if log.get("action") == "taken") skipped = sum(1 for log in logs if log.get("action") == "skipped") total = taken + skipped adherence = (taken / total * 100) if total > 0 else 0 result.append({ "medication_id": med["id"], "name": med["name"], "taken": taken, "skipped": skipped, "adherence_percent": round(adherence, 1), }) return flask.jsonify(result), 200 @app.route("/api/medications//adherence", methods=["GET"]) def api_medAdherence(med_id): """Get adherence stats for a single medication. Query: ?days=30""" user_uuid = _auth(flask.request) if not user_uuid: return flask.jsonify({"error": "unauthorized"}), 401 med = postgres.select_one("medications", {"id": med_id, "user_uuid": user_uuid}) if not med: return flask.jsonify({"error": "not found"}), 404 days = flask.request.args.get("days", 30, type=int) logs = postgres.select( "med_logs", where={"medication_id": med_id}, limit=days * 10, ) taken = sum(1 for log in logs if log.get("action") == "taken") skipped = sum(1 for log in logs if log.get("action") == "skipped") total = taken + skipped adherence = (taken / total * 100) if total > 0 else 0 return flask.jsonify({ "medication_id": med_id, "name": med["name"], "taken": taken, "skipped": skipped, "adherence_percent": round(adherence, 1), }), 200 # ── Refills ─────────────────────────────────────────────────── @app.route("/api/medications//refill", methods=["PUT"]) def api_setRefill(med_id): """Set refill info. Body: {quantity_remaining, refill_date?, pharmacy_notes?}""" user_uuid = _auth(flask.request) if not user_uuid: return flask.jsonify({"error": "unauthorized"}), 401 med = postgres.select_one("medications", {"id": med_id, "user_uuid": user_uuid}) if not med: return flask.jsonify({"error": "not found"}), 404 data = flask.request.get_json() if not data: return flask.jsonify({"error": "missing body"}), 400 allowed = ["quantity_remaining", "refill_date", "pharmacy_notes"] updates = {k: v for k, v in data.items() if k in allowed} if not updates: return flask.jsonify({"error": "no valid fields to update"}), 400 result = postgres.update("medications", updates, {"id": med_id, "user_uuid": user_uuid}) return flask.jsonify(result[0] if result else {}), 200 @app.route("/api/medications/refills-due", methods=["GET"]) def api_refillsDue(): """Get medications that need refills soon. Query: ?days_ahead=7""" user_uuid = _auth(flask.request) if not user_uuid: return flask.jsonify({"error": "unauthorized"}), 401 days_ahead = flask.request.args.get("days_ahead", 7, type=int) from datetime import datetime, timedelta cutoff = (datetime.now() + timedelta(days=days_ahead)).strftime("%Y-%m-%d") meds = postgres.select( "medications", where={"user_uuid": user_uuid}, ) due = [] for med in meds: qty = med.get("quantity_remaining") refill_date = med.get("refill_date") if qty is not None and qty <= 7: due.append(med) elif refill_date and refill_date <= cutoff: due.append(med) return flask.jsonify(due), 200