"""Audited questionnaire review for the synthetic prototype. Like the existing API, doctor_id is supplied by the caller; production must put these routes behind authenticated staff identity. No clinical data cross into identity. """ from typing import Literal from fastapi import APIRouter, HTTPException from pydantic import BaseModel, ConfigDict, Field from app.core import clock from app.db import repo from app.db.audit import audit from app.db.intake import QuestionnaireRecord from app.db.session import get_clinical_session from app.services import questionnaire as service router = APIRouter(tags=["questionnaire-review"]) class ReviewIn(BaseModel): model_config = ConfigDict(extra="forbid", str_strip_whitespace=True) doctor_id: str = Field(min_length=1) assessment_performed: Literal[True] decision: Literal["resume", "keep_hold"] reference: str = Field(min_length=1, max_length=200, pattern=r"^[A-Za-z0-9_./:#-]+$") async def require_doctor(session, token, doctor_id): profile = await repo.get_profile(session, token) if profile is None or profile.doctor_id != doctor_id: raise HTTPException(404, "patient not found") @router.get("/patients/{token}/questionnaires") async def history(token: str, doctor_id: str): async with get_clinical_session() as session: await require_doctor(session, token, doctor_id) rows = await service.records(session, token) await audit(session, actor=doctor_id, action="view_questionnaires", token=token, ts=clock.now()) return [ { "id": r.id, "kind": r.kind, "status": r.status, "version": r.version, "policy_version": r.policy_version, "started_at": r.started_at, "completed_at": r.completed_at, "answers": r.answers, "details": r.details, } for r in rows ] @router.post("/patients/{token}/questionnaires/{record_id}/review") async def review(token: str, record_id: str, body: ReviewIn): now = clock.now() async with get_clinical_session() as session: await service.lock_patient(session, token) await require_doctor(session, token, body.doctor_id) state = await repo.get_dialog_state(session, token) row = await session.get(QuestionnaireRecord, record_id) if row is None or row.token != token: raise HTTPException(404, "record not found") if not state or state.data.get("consent") is not True: raise HTTPException(409, "consent required") if row.status not in ("review_required", "interrupted"): raise HTTPException(409, "record does not require review") if body.decision == "resume" and (state.data.get("crisis") or {}).get("open"): raise HTTPException(409, "close the acute episode after assessment before resuming routine work") detail = { "doctor_id": body.doctor_id, "decision": body.decision, "reference": body.reference, "at": now.isoformat(), } row.details = {**row.details, "clinician_review": detail} if body.decision == "resume": row.status = "reviewed" remaining = [ r for r in await service.records(session, token) if r.id != row.id and r.status in ("review_required", "interrupted") ] if not remaining: state.data = {**state.data, "intake_review_required": False, "questionnaire_record": None} await audit( session, actor=body.doctor_id, action="review_questionnaire", token=token, details={"record_id": row.id, "decision": body.decision, "reference": body.reference}, ts=now, ) return {"id": row.id, "status": row.status, "review": detail}