add patient lifecycle doc

This commit is contained in:
voltsrage
2026-06-20 17:11:08 +08:00
parent e1c78b49f4
commit e7d419cba8
3 changed files with 701 additions and 29 deletions
+176 -29
View File
@@ -23,6 +23,77 @@ fires (the BP is genuinely low and may need monitoring), but the details say:
That single line changes the clinical interpretation from "something is wrong" to "the
medication is working — keep monitoring."
The authoritative endpoint list lives in the [API Reference](../../README.md#api-reference)
(`/api/v1` prefix, standard `{ success, statusCode, data, error }` envelope). For the
full patient journey from registration through discharge — not only medication correlation —
see [Patient Encounter API Lifecycle](../patient-encounter-api-lifecycle.md). This
document explains how medication correlation fits into that surface — which endpoints
write administrations, which paths produce annotated alerts, and which endpoints are
deliberately outside the correlation scope.
---
## Relationship to the full API
Medication correlation is not a standalone micro-API. It is a cross-cutting concern that
connects medication write endpoints, observation ingest, asynchronous alert consumers, and
alert read endpoints. The table below maps every `/api/v1` resource group to its role.
| Resource group | Endpoints | Role in medication correlation |
|---|---|---|
| **Patients** | `POST/GET /patients`, `GET /patients/{id}` | Prerequisite only — patients must exist before encounters are opened. No correlation logic. |
| **Encounters** | `GET/POST /encounters`, `PATCH /encounters/{id}/status`, `GET /encounters/{id}/timeline`, `GET /encounters/{id}/qsofa/current` | Medications and observations are scoped to an **active** encounter. `GET /encounters/{id}` and `GET /encounters/{id}/timeline` return open alerts whose `details` may already include server-side annotations; the timeline does **not** include medication administrations. |
| **Alert thresholds** | `POST/GET/PUT /alert-thresholds` | Defines the warning/critical bands that `WarningEvaluator` compares against. Threshold changes invalidate the Redis cache but do not affect drug-vital mappings (`MedicationCorrelation` config is separate). |
| **Observations** | `POST/GET /encounters/{id}/observations` | **Write path** that eventually triggers correlation. `POST` returns `alertGenerated` only for **critical** breaches created synchronously (never annotated). Warning and NEWS2 alerts are created asynchronously by Kafka consumers and include annotations when applicable. |
| **Clinical alerts** | `GET /encounters/{id}/alerts`, `GET /alerts`, `GET /alerts/{id}`, `POST /alerts/{id}/acknowledge`, `POST /alerts/{id}/resolve` | **Read path** where annotations surface. The `details` field on `ClinicalAlert` carries the `— note: …` suffix. Acknowledge/resolve lifecycle is unchanged. |
| **Orders** | `POST/GET /encounters/{id}/orders`, `GET/PATCH /orders/{id}` | Unrelated to correlation. Sepsis-bundle orders are auto-created by the sepsis engine, not by medication recording. |
| **Analytics** | `GET /analytics/patients`, `/observations/trend`, `/alerts/summary`, `/population` | Elasticsearch-backed reporting. Alert summaries index the annotated `details` text but do not run correlation themselves. |
| **NEWS2** | `GET /encounters/{id}/news2/current`, `GET /encounters/{id}/news2/history` | Scores are computed asynchronously (`news2-scoring` consumer). When a NEWS2 warning/emergency alert is created, its `details` may be annotated the same way as threshold warnings. Score history endpoints return numeric components only — annotations live on the alert, not the score row. |
| **Sepsis bundles** | `GET /encounters/{id}/sepsis-bundle/current`, `GET /sepsis-bundles/{id}` | Deliberately excluded from annotation (see [What is NOT annotated](#what-is-not-annotated-and-why)). |
| **Medications** | `POST/GET /encounters/{id}/medications`, `GET /medications/{id}` | **Write and read path** for administrations. Correlation reads via `MedicationService.GetRecentForEncounterAsync`; HTTP clients (dashboard, simulator) use the list endpoint to show recent drugs alongside alert reasoning. |
There is no dedicated "correlate" or "annotate" endpoint. Correlation runs inside background
consumers at alert-creation time and is visible only through alert `details` and
medication list responses.
### End-to-end API choreography (metoprolol example)
A typical integration or simulator run exercises these endpoints in order:
```
1. POST /api/v1/patients → register patient (MRN assigned)
2. POST /api/v1/patients/{id}/encounters → open encounter (status: active)
3. POST /api/v1/encounters/{id}/medications → record metoprolol 25mg PO
4. POST /api/v1/encounters/{id}/observations → ingest SYSTOLIC_BP = 95
├─ HTTP 201: observation row created; alertGenerated = false
│ (warning path is async — no annotation in this response)
└─ Kafka observation.recorded
→ WarningAlertService (consumer group: warning-evaluator)
→ MedicationCorrelationHelper annotates details
→ clinical_alerts row + alert.generated outbox event
5. GET /api/v1/encounters/{id}/alerts → poll until WARNING_SYSTOLIC_BP appears
details: "SYSTOLIC_BP value 95 is below warning low of 90. — note: metoprolol 25mg (PO) administered 45 min ago"
6. GET /api/v1/encounters/{id}/medications → optional; dashboard uses this for the
"Recent medications" panel in alert reasoning (client-side 90-min window)
```
Allow a few seconds between steps 4 and 5 for the Kafka consumer to process the
`observation.recorded` event. NEWS2 follows the same pattern via `news2-scoring` after
all seven parameters are present.
### Where annotations appear (and where they do not)
| Response field / endpoint | Contains annotation? | Notes |
|---|---|---|
| `POST …/observations``alertGenerated` / inline alert | No (warnings) | Only critical alerts return synchronously; critical path never calls `MedicationCorrelationHelper`. |
| `GET …/alerts`, `GET /alerts/{id}`, `GET /encounters/{id}` (embedded alerts) | Yes (warning + NEWS2) | Primary consumer surface. `details` is plain text with appended `— note: …`. |
| `GET …/encounters/{id}/timeline` | Yes (on alert events) | Timeline merges observations and alerts; alert events include annotated `details`. |
| `alert.generated` Kafka / outbox payload | Yes | `details` in the event matches the persisted alert row. |
| `GET …/news2/current`, `GET …/news2/history` | No | Scores only; read the corresponding NEWS2 alert for annotated context. |
| `GET …/medications` | No | Returns raw administration rows; dashboard correlates client-side for the reasoning panel. |
---
## How the pieces fit together
@@ -36,8 +107,9 @@ Nurse records medication
┌─────────────────────┐
│ MedicationsController│ ◄── thin HTTP layer, no business logic
│ POST /encounters/
{id}/medications
│ POST /api/v1/
encounters/{id}/
│ medications │
└────────┬────────────┘
@@ -54,11 +126,17 @@ Nurse records medication
... time passes, vital signs arrive ...
New observation arrives (e.g. SYSTOLIC_BP = 95)
POST /api/v1/encounters/{id}/observations (e.g. SYSTOLIC_BP = 95)
┌─────────────────────┐
WarningEvaluator │ ◄── or News2Detector for composite scores
ObservationService │ ◄── sync path: critical alerts only (never annotated)
│ COMMIT + outbox │ always emits observation.recorded → Kafka
└────────┬────────────┘
▼ (async, consumer group: warning-evaluator)
┌─────────────────────┐
│ WarningEvaluator │ ◄── or News2Detector (consumer group: news2-scoring)
│ │
│ 1. Load threshold │ (from Redis cache)
│ 2. Check breach │ (is 95 < warningLow of 90?)
@@ -87,6 +165,10 @@ New observation arrives (e.g. SYSTOLIC_BP = 95)
│ which drugs are "relevant"│
│ to which vital signs │
└────────────────────────────┘
... client reads result ...
GET /api/v1/encounters/{id}/alerts → annotated details in response
```
---
@@ -208,11 +290,51 @@ do what it's good at.
### 4. MedicationsController
**What it is:** Three HTTP endpoints:
- `POST /api/v1/encounters/{encounterId}/medications` — record a new administration
- `GET /api/v1/encounters/{encounterId}/medications` — list with pagination and optional
`since` filter
- `GET /api/v1/medications/{id}` — look up a single record
**What it is:** Three HTTP endpoints under `/api/v1` (see also
[Medications in the API Reference](../../README.md#medications)):
| Method | Path | Purpose |
|---|---|---|
| `POST` | `/encounters/{encounterId}/medications` | Record a new administration |
| `GET` | `/encounters/{encounterId}/medications` | List with pagination and optional `since` filter |
| `GET` | `/medications/{id}` | Look up a single record (includes nested `encounter`) |
**POST body** (`CreateMedicationAdministrationRequest`):
| Field | Type | Required | Notes |
|---|---|---|---|
| `drugName` | string | yes | Trimmed on persist; case-insensitive for `DrugVitalMappings` lookup |
| `dose` | decimal | yes | Must be > 0 |
| `doseUnit` | string | yes | e.g. `mg`, `g`, `mcg`, `units` |
| `route` | string | yes | e.g. `PO`, `IV`, `SubQ` |
| `administeredAt` | DateTimeOffset | no | Defaults to server UTC time if omitted |
| `administeredBy` | string | yes | Clinician or nurse identifier |
**POST response:** `201 Created` with `ApiResponse<MedicationAdministration>` in the
standard envelope. The `data` object includes `id`, `encounterId`, `drugName`, `dose`,
`doseUnit`, `route`, `administeredAt`, `administeredBy`.
**POST status codes:**
| Code | When |
|---|---|
| `201` | Administration recorded |
| `400` | FluentValidation failure (empty drug name, zero dose, `administeredAt` > 5 min in future, field length exceeded) |
| `404` | Encounter not found (`ENCOUNTER_NOT_FOUND`) |
| `409` | Encounter not active (`ENCOUNTER_NOT_ACTIVE`) |
**GET list response:** `200 OK` with paginated `{ items, page, pageSize, totalCount, totalPages }`.
Query params: `since` (ISO 8601 — administrations at or after this time), `page` (default 1),
`pageSize` (default 20). The dashboard's `fetchMedications` helper pages through this
endpoint (default `pageSize` 50) to populate the alert-reasoning panel.
**GET by id response:** `200 OK` with full `MedicationAdministration` (404 if not found).
**Why medication POST does not trigger correlation:**
Recording a drug does not create or modify alerts. Correlation is evaluated only when a
warning or NEWS2 alert is **created** — triggered by observation ingest (async) or NEWS2
scoring (async). This keeps the medication write path fast and idempotent with no
side effects beyond the `medication_administrations` row.
**Why the controller is almost empty:**
The controller's only job is to translate HTTP concepts (route parameters, query strings,
@@ -329,6 +451,12 @@ The annotated details string is also included in the outbox event payload, so do
consumers (notifications, dashboards) receive the medication context without needing
their own correlation logic.
Clients that poll `GET /api/v1/encounters/{id}/alerts` or `GET /api/v1/alerts/{id}` read
the same annotated `details` string. The ward dashboard (`vigilcare-dashboard`) adds a
second layer: `AlertReasoning.vue` fetches `GET …/medications` and shows administrations
within 90 minutes before `alert.triggeredAt`, independent of whether the server appended
the `— note:` suffix (e.g. when the drug name is not in `DrugVitalMappings`).
### News2Detector annotation strategy
NEWS2 is different from single-vital warnings because it's a composite score of 7
@@ -349,7 +477,7 @@ pharmaceutical review.
## Data flow summary
```
HTTP POST
HTTP POST /api/v1/encounters/{id}/medications
┌────────────────┐
@@ -364,26 +492,45 @@ pharmaceutical review.
└────────────────┘ │ administrations │
└────────┬────────┘
... later, observation arrives ... │
HTTP POST /api/v1/encounters/{id}/observations
(observation row + observation.recorded outbox)
│ │
▼ │
┌────────────────┐ │
Warning │ │
Evaluator │ │
│ │
│ builds details│
│ ▼ │
│ Correlation │◄───────────────┘
│ Helper │ queries recent meds
│ │ │ for this encounter +
observation code
│ annotated
│ details
│ ▼
INSERT alert │──────► PostgreSQL clinical_alerts
│ + outbox event│──────► outbox_events → Kafka
└────────────────┘
Kafka │ │
observation. │ │
recorded │ │
└───────┬────────┘
┌────────────┴────────────┐
▼ ▼ │
┌──────────────┐ ┌──────────────┐ │
│ WarningAlert │ │ News2Scoring │ │
│ Service │ Service
└──────┬───────┘ └──────┬───────┘
┌────────────────┐ ┌────────────────┐
Warning News2 │ │
│ Evaluator │ │ Detector │ │
│ │ │ │ │ │ │
│ ▼ │ │ ▼ │ │
│ Correlation │◄──────┴───────┘ │
│ Helper │◄────────────────────────┘
│ │ │ queries recent meds
│ ▼ │ for encounter + code
│ annotated │
│ details │
│ │ │
│ ▼ │
│ INSERT alert │──────► PostgreSQL clinical_alerts
│ + outbox event│──────► outbox_events → Kafka alert.generated
└────────────────┘
GET /api/v1/encounters/{id}/alerts
GET /api/v1/alerts/{id}
(annotated details in JSON response)
```
---