add patient lifecycle doc
This commit is contained in:
@@ -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)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user