# Backend Changes — feature/patient-intake-v2

**Branch:** `feature/patient-intake-v2`
**Docs reference:** `db-architecture.md v3.1`, `technical-architecture.md v3.2`, `trd-milestone-1.md v3.2`

---

## Change Flow

```mermaid
flowchart TD

    subgraph PHASE1["Phase 1 — Schema & Model Changes ✓"]
        A1[Rename models to singular + mstr_ prefix\nstaff → user\ndepartments → mstr_department\nprotocols → removed\nintake_sessions → intake_session\nquestion_responses → question_response\npatient_summaries → patient_summary\nsession_contributors → session_contributor]
        A2[Replace protocols JSONB\nCreate mstr_question model\nCreate question_dept_map model\nEach question = one DB row\nOrdered by sequence_number per department]
        A3[Create 5 new junction tables\npatient_department_map\nsession_patient_map\nuser_role_map\ndepartment_user_map\nquestion_dept_map]
        A4[Create active_session model\nstaff_id → user UNIQUE FK ON DELETE CASCADE\nsession_id → intake_session FK ON DELETE SET NULL\nexpires_at TIMESTAMPTZ]
        A5[Create patient_lock model\npatient_id → patient UNIQUE FK ON DELETE CASCADE\nstaff_id → user FK\nexpires_at TIMESTAMPTZ]
    end

    subgraph PHASE2["Phase 2 — Remove Redis ✓"]
        B1[Delete app/db/redis.py]
        B2[Remove redis asyncio from requirements.txt]
        B3[Remove REDIS_URL from config.py and .env]
        B4[Remove Redis service from docker-compose.yml\n4 services → 3 services]
        B5[Rename SESSION_LOCK_TTL_SECONDS\n→ SESSION_LOCK_TTL_MINUTES default 30]
    end

    subgraph PHASE3["Phase 3 — Seed ✓"]
        C2[Update seed.py\nUse Base.metadata.create_all for local testing\nInsert into mstr_department\nInsert mstr_question rows\nInsert question_dept_map with sequence_number\nInsert into user table]
    end

    subgraph PHASE4["Phase 4 — Concurrency & Lock Logic"]
        D1[active_session logic\nLOGIN: COUNT rows where expires_at > NOW\nif count >= 5 → 503\nUPSERT ON CONFLICT staff_id DO UPDATE\nRESPOND: UPDATE expires_at +30min\nCOMPLETE / LOGOUT: DELETE row\nASK-MORE: COUNT check + UPDATE\nDEACTIVATE STAFF: DELETE row]
        D2[patient_lock logic\nNEW SESSION: INSERT lock\nRESUME SESSION: SELECT lock\nif held by other staff → 409\nif free or expired → UPSERT\nRESPOND: verify lock owner → 423 if not\nUPDATE expires_at +30min\nCOMPLETE / LOGOUT: DELETE lock\nDEACTIVATE STAFF: DELETE lock]
        D3[Update session lookup\nQuery PostgreSQL directly\njoin patient + patient_department_map\nfilter by daily_id + department_id + intake_date]
    end

    subgraph PHASE5["Phase 5 — Endpoint Implementations"]
        E1[POST /auth/logout\nDELETE from active_session\nDELETE from patient_lock\nreturn success: true]
        E2[POST /sessions id /complete\nMark session COMPLETED\nCall generate_summary\nINSERT or UPDATE patient_summary\nDELETE patient_lock + active_session\nReturn summaryText + latencyMs]
        E3[GET /sessions id /summary\nSELECT from patient_summary\nReturn 404 SUMMARY_NOT_FOUND if missing]
        E4[POST /sessions id /ask-more\nCheck active_session count\nRe-open session to ACTIVE\nUPSERT patient_lock + active_session\nAdd session_contributor if new staff\nReturn next question from Claude]
    end

    subgraph PHASE6["Phase 6 — Business Logic"]
        F1[is_flagged scan at INTAKE_COMPLETE\nScan all question_response rows\nSet is_flagged = true where answer is\nno NA I dont know refused\ndoesnt recall empty or equivalent\nReturn flagged questions to frontend]
        F2[Move prompts from hardcoded gemini.py\n→ mstr_department.llm_system_prompt\n→ mstr_department.llm_summary_prompt\nFetch from DB using department_id from JWT]
        F3[Update Claude question context\nQuery mstr_question via question_dept_map\nORDER BY sequence_number\nReplace hardcoded CT_SURGERY_QUESTIONS list]
        F4[Update APScheduler — 3 deletes hourly\nDELETE active_session WHERE expires_at < NOW\nDELETE patient_lock WHERE expires_at < NOW\nDELETE patient WHERE expires_at < NOW\nCASCADE handles all child rows]
    end

    subgraph PHASE7["Phase 7 — Validation"]
        G1[Input validation on all endpoints\nPIN: exactly 6 digits numeric\nEmail: valid RFC 5322 format\nAge: integer 1–120\nDaily patient ID: max 50 chars trimmed\nResponse text: max 2000 chars\ninputType: VOICE or TEXT\ninputLanguage: ENGLISH HINDI TELUGU\nRole: MEDICAL_STAFF CARDIOLOGIST ADMIN]
    end

    PHASE1 --> PHASE2
    PHASE2 --> PHASE3
    PHASE3 --> PHASE4
    PHASE4 --> PHASE5
    PHASE5 --> PHASE6
    PHASE6 --> PHASE7
```

---

## Notes

### Phase 7 — Validation
- All session route path params (`session_id`) changed from `str` to `uuid.UUID` — FastAPI returns 422 automatically on malformed UUIDs; removed all manual `uuid.UUID(session_id)` conversions
- `update_staff` path param (`staff_id`) changed from `str` to `uuid.UUID` — same treatment
- Added `departmentId` UUID format validator to `CreateStaffRequest` — 422 if not a valid UUID
- All other Phase 7 rules were already in place from earlier phases:
  - PIN 6-digit numeric: `auth.py` + `staff.py`
  - Email RFC 5322: `EmailStr` in both files
  - Age 1–120: `sessions.py` `StartSessionRequest`
  - dailyPatientId max 50 trimmed: `sessions.py`
  - Response text max 2000: `sessions.py` `RespondRequest`
  - inputType VOICE/TEXT: `sessions.py`
  - inputLanguage ENGLISH/HINDI/TELUGU: `sessions.py`
  - Role MEDICAL_STAFF/CARDIOLOGIST/ADMIN: `staff.py`

### Phase 6 — Business Logic
- F1 `is_flagged` scan: per-response check already in `respond()` via `FLAGGED_PHRASES`; flagged questions returned in `intakeComplete` response — no additional changes needed
- F2 Prompts from DB: `gemini.py` rewrote `get_first_question`, `get_next_question`, `generate_summary` to accept `system_prompt`/`summary_prompt`/`questions` as parameters; removed hardcoded `SYSTEM_PROMPT`, `SUMMARY_PROMPT`, `CT_SURGERY_QUESTIONS`
- F3 Questions from DB: added `_get_dept_context(db, dept_id)` helper in `sessions.py` — queries `MstrQuestion JOIN QuestionDeptMap WHERE department_id ORDER BY sequence_number`; all LLM call sites in `create_session`, `respond`, `complete_session`, `ask_more` now fetch prompts + questions from DB before calling the service
- F4 APScheduler: rewrote `data_wipe.py` — `purge_expired()` deletes expired `active_session`, `patient_lock`, `patient` rows (in that order) using SQLAlchemy `delete()`; wired into `main.py` lifespan with `AsyncIOScheduler` on 1-hour interval

### Phase 5 — Endpoint Implementations
- `POST /auth/logout` — done in Phase 4 (auth.py)
- `POST /sessions/{id}/complete` — done in Phase 4 (sessions.py)
- `POST /sessions/{id}/ask-more` — done in Phase 4 (sessions.py)
- `GET /sessions/{id}/summary` — added to sessions.py; returns `summaryText`, `generationLatencyMs`, `generatedAt`; 404 `SESSION_NOT_FOUND` if session missing, 404 `SUMMARY_NOT_FOUND` if no summary yet
- Added `SUMMARY_NOT_FOUND` constant to `error_codes.py`

### Phase 4 — Concurrency & Lock Logic
- `auth.py`: Login checks `COUNT(active_session WHERE expires_at > NOW)` — 503 if >= `MAX_CONCURRENT_SESSIONS`; UPSERT active_session on successful login; logout DELETEs active_session + patient_lock
- `sessions.py`: All imports updated (`Staff` → `User`, `Department` → `MstrDepartment`, old model paths → new paths); added `ActiveSession`, `PatientLock`, `PatientDepartmentMap` imports
- `sessions.py` create_session: removed `Patient.department_id` filter (column gone); added `PatientDepartmentMap` insert on new patient; patient_lock 409 check on RESUME; UPSERT patient_lock; active_session.session_id + expires_at updated on create and resume
- `sessions.py` respond: lock ownership verified (423 `SESSION_LOCK_LOST` if not held); patient_lock + active_session expires_at refreshed on every respond
- `sessions.py` complete_session: patient_lock + active_session DELETEd after summary saved (staff must re-login after completing)
- `sessions.py` ask_more: active_session COUNT check before re-opening; UPSERT patient_lock; active_session TTL + session_id updated
- `staff.py`: `Staff` → `User`, `Department` → `MstrDepartment`; deactivation DELETEs active_session + patient_lock for the deactivated user
- `departments.py`: `Department` → `MstrDepartment`

### Phase 3 — Seed
- Alembic migrations skipped — using `Base.metadata.drop_all + create_all` in `seed.py` for local testing
- Old imports (`Department`, `Staff`, `Protocol`) replaced with `MstrDepartment`, `User`, `MstrQuestion`, `QuestionDeptMap`
- 28 CT Surgery questions inserted individually into `mstr_question` with fixed stable UUIDs
- Each question linked to CT Surgery department via `question_dept_map` with `sequence_number`
- `Protocol` JSONB seed removed — questions are now normalized rows

### Phase 2 — Remove Redis
- Deleted `app/db/redis.py`
- Removed `redis[asyncio]==5.2.0` from `requirements.txt`
- Removed `REDIS_URL` from `config.py`
- Renamed `SESSION_LOCK_TTL_SECONDS` → `SESSION_LOCK_TTL_MINUTES`, default changed from `1800` to `30`
- No `docker-compose.yml` existed yet so no service removal needed


### Phase 1 — Schema & Model Changes
- Old files deleted: `staff.py`, `department.py`, `protocol.py`, `session.py`, `response.py`, `summary.py`
- New files created: `user.py`, `mstr_department.py`, `mstr_role.py`, `mstr_question.py`, `question_dept_map.py`, `intake_session.py`, `session_contributor` moved into `intake_session.py`, `question_response.py`, `patient_summary.py`, `active_session.py`, `patient_lock.py`
- New junction tables: `patient_department_map.py`, `session_patient_map.py`, `user_role_map.py`, `department_user_map.py`
- `patient.py` updated: table renamed `patients` → `patient`, `department_id` column removed (now via `patient_department_map`)
- `protocols` JSONB replaced with normalized `mstr_question` rows + `question_dept_map` with `sequence_number`
- All FK references updated to point to new table names

---

## New Table Mappings

```mermaid
erDiagram
    mstr_department ||--o{ department_user_map : "has"
    mstr_department ||--o{ question_dept_map : "has"
    mstr_department ||--o{ patient_department_map : "has"

    mstr_question ||--o{ question_dept_map : "mapped via"

    user ||--o{ user_role_map : "has"
    user ||--o{ session_contributor : "contributes"
    user ||--o{ question_response : "records"
    user ||--o| active_session : "has one slot"
    user ||--o{ patient_lock : "holds"
    user ||--o{ department_user_map : "belongs to"

    patient ||--o{ patient_department_map : "belongs to"
    patient ||--|| intake_session : "has one"
    patient ||--o| patient_lock : "locked by"

    intake_session ||--o{ session_contributor : "has"
    intake_session ||--o{ question_response : "has"
    intake_session ||--o| patient_summary : "produces"
    intake_session ||--o{ session_patient_map : "has"

    active_session }o--|| user : "tracks"
    active_session }o--o| intake_session : "linked to"
```
