feature: Warning Alert Consumer, Orders API & Input Validation
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
A production-quality clinical backend built with ASP.NET Core 8, PostgreSQL, Apache Kafka, RabbitMQ, Elasticsearch, Redis, and MinIO. The domain models the observe-alert-acknowledge lifecycle at the center of any clinical monitoring system: patient encounters, continuous vital sign and lab result ingest, real-time sepsis detection, and clinician notification with automatic escalation.
|
||||
|
||||
**Implementation status:** All nine planned phases are complete — from schema and CRUD through Kafka, Elasticsearch CQRS, sepsis detection, RabbitMQ paging with DLQ escalation, reconciliation jobs, Prometheus/Grafana observability, and the MinIO Parquet data lake. See [Implemented Phases](#implemented-phases) for the full breakdown.
|
||||
**Implementation status:** All ten planned phases are complete — from schema and CRUD through Kafka, Elasticsearch CQRS, sepsis detection, RabbitMQ paging with DLQ escalation, reconciliation jobs, Prometheus/Grafana observability, the MinIO Parquet data lake, and clinical data model expansion (patient demographics, encounter enrichment, 12 observation codes). See [Implemented Phases](#implemented-phases) for the full breakdown.
|
||||
|
||||
## Domain Model — How It Maps to a Real Clinical System
|
||||
|
||||
@@ -19,11 +19,11 @@ Patient ────────────────────────
|
||||
|
||||
### Patient
|
||||
|
||||
A `Patient` is registered with demographic information and assigned a Medical Record Number (MRN) — a stable identifier that never changes across encounters. Patient search supports both MRN exact match and name partial match (`ILIKE`).
|
||||
A `Patient` is registered with demographic information and assigned a Medical Record Number (MRN) — a stable identifier that never changes across encounters. Optional clinical fields include blood type (`A+`, `O-`, etc.), known allergies, and emergency contact name/phone. Patient search supports both MRN exact match and name partial match (`ILIKE`).
|
||||
|
||||
### Encounter
|
||||
|
||||
An `Encounter` is a single clinical episode. Status follows a controlled machine: `scheduled → active → discharged` (or `cancelled` from any pre-discharged state). Observations, alerts, and orders belong to an encounter, not directly to a patient — this bounds queries naturally and mirrors real clinical data ownership. Discharge triggers a RabbitMQ job to generate a discharge summary.
|
||||
An `Encounter` is a single clinical episode. Status follows a controlled machine: `scheduled → active → discharged` (or `cancelled` from any pre-discharged state). Optional `roomBed` and `admissionReason` fields support ward assignment and clinical context; `dischargeDiagnosis` is set on discharge. Observations, alerts, and orders belong to an encounter, not directly to a patient — this bounds queries naturally and mirrors real clinical data ownership. Discharge triggers a RabbitMQ job to generate a discharge summary.
|
||||
|
||||
### AlertThreshold
|
||||
|
||||
@@ -47,9 +47,9 @@ An `OutboxEvent` is written in the same transaction as any observation or alert,
|
||||
|
||||
## Features
|
||||
|
||||
- **Patient Registration** — register patients with MRN generation; paginated list with name (`ILIKE`) and MRN (exact) search; patient detail with active encounter summary
|
||||
- **Encounter Management** — open encounters against a patient; encounter status state machine (`scheduled → active → discharged / cancelled`) with 409 on illegal transitions; encounter timeline as a merged chronological view across status changes, observation summaries, and alerts
|
||||
- **Alert Threshold Management** — configure per-observation-code numeric bounds (`criticalLow`, `warningLow`, `warningHigh`, `criticalHigh`); thresholds pre-loaded into Redis on startup; write-through cache invalidation on update
|
||||
- **Patient Registration** — register patients with MRN generation; optional blood type, allergies, and emergency contact; paginated list with name (`ILIKE`) and MRN (exact) search; patient detail with active encounter summary
|
||||
- **Encounter Management** — open encounters against a patient with optional room/bed and admission reason; encounter status state machine (`scheduled → active → discharged / cancelled`) with 409 on illegal transitions; optional discharge diagnosis on discharge; encounter timeline as a merged chronological view across status changes, observation summaries, and alerts
|
||||
- **Alert Threshold Management** — configure per-observation-code numeric bounds (`criticalLow`, `warningLow`, `warningHigh`, `criticalHigh`) for 12 observation codes; thresholds pre-loaded into Redis on startup; write-through cache invalidation on update
|
||||
- **Observation Ingest** — `POST /encounters/:id/observations` accepts single or small batch (up to 10); idempotency via `Idempotency-Key` header (partial unique index); plausibility validation per observation code; synchronous critical alert creation within the ingest transaction; warning breach deferred to Kafka consumer; outbox event written in the same commit; cursor-paginated history on `(encounter_id, observation_code, recorded_at DESC)`
|
||||
- **Clinical Alert Lifecycle** — paginated alert list per encounter and globally; acknowledge with clinician ID and optional note; resolve (must be acknowledged first); global list filterable by status, severity, and department
|
||||
- **Outbox Relay** — `IHostedService` polling every 500ms; reads unprocessed outbox rows, publishes to Kafka, marks processed; partitioned by `encounterId` for per-encounter ordering
|
||||
@@ -149,12 +149,14 @@ VigilCareClinicalAPI/
|
||||
│ ├── EncounterType.cs # Inpatient, Outpatient, Emergency
|
||||
│ ├── AlertSeverity.cs # Warning, Critical
|
||||
│ ├── AlertStatus.cs # Open, Acknowledged, Resolved, Escalated
|
||||
│ ├── AlertType.cs # ThresholdBreach, SepsisWarning, …
|
||||
│ ├── AlertType.cs # Threshold breach, sepsis, systolic BP, AVPU, glucose, …
|
||||
│ ├── BloodType.cs # A+, O-, AB-, … with ToDbString/FromDbString
|
||||
│ ├── ObservationSource.cs # Device, Manual, Lab
|
||||
│ └── OrderType.cs / ReconciliationCheckType.cs / Department.cs / OrderStatus.cs
|
||||
│ └── Json/
|
||||
│ ├── ObservationSourceJsonConverter.cs
|
||||
│ └── DepartmentJsonConverter.cs
|
||||
│ ├── DepartmentJsonConverter.cs
|
||||
│ └── BloodTypeJsonConverter.cs # Clinical notation (A+, AB-) in JSON API
|
||||
├── Services/
|
||||
│ ├── Interfaces/ # IPatientService, IEncounterService, …
|
||||
│ ├── PatientService.cs
|
||||
@@ -247,7 +249,8 @@ tests/
|
||||
├── NotificationPipelineTests.cs # RabbitMQ topology, DLQ routing
|
||||
├── ReconciliationTests.cs # Three reconciliation checks, deduplication, RabbitMQ publish
|
||||
├── ObservabilityPhase8Tests.cs # /metrics families and correlation header behavior
|
||||
└── DataLakePhase9Tests.cs # Kafka → MinIO Parquet flow and schema checks
|
||||
├── DataLakePhase9Tests.cs # Kafka → MinIO Parquet flow and schema checks
|
||||
└── ClinicalDemographicsAndObservationTests.cs # Patient/encounter enrichment, expanded observation alerts
|
||||
|
||||
scripts/
|
||||
├── run-api-redis-tests.sh # Phase 1 — patient/encounter/threshold + Redis cache
|
||||
@@ -257,10 +260,11 @@ scripts/
|
||||
├── run-notification-pipeline-tests.sh # Phase 6 — RabbitMQ paging, DLQ, discharge summary
|
||||
├── run-reconciliation-tests.sh # Phase 7 — reconciliation scheduler checks
|
||||
├── run-phase8-verification.sh # Phase 8 — Prometheus metrics, alerts_unacknowledged_gauge, correlation headers
|
||||
└── run-phase9-verification.sh # Phase 9 — data lake tests, Kafka offsets, MinIO Parquet, DuckDB schema
|
||||
├── run-phase9-verification.sh # Phase 9 — data lake tests, Kafka offsets, MinIO Parquet, DuckDB schema
|
||||
└── run-phase10-verification.sh # Phase 10 — 12 Redis thresholds, clinical enrichment, ES pipeline, integration tests
|
||||
|
||||
docs/
|
||||
├── plans/ # Phase 1–9 implementation and verification guides
|
||||
├── plans/ # Phase 1–10 implementation and verification guides
|
||||
├── decisions/
|
||||
│ ├── data-lake-design.md # Parquet vs JSON, partitioning, replay rationale
|
||||
│ └── sepsis-engine-design.md # SIRS sliding window and idempotent alert design
|
||||
@@ -374,7 +378,7 @@ dotnet run
|
||||
|
||||
On startup the application:
|
||||
1. Runs EF Core migrations
|
||||
2. Seeds two patients, one active inpatient encounter each, four alert thresholds, and sample observations
|
||||
2. Seeds two patients (with blood type, allergies, emergency contact), one active inpatient encounter each (with room/bed and admission reason), twelve alert thresholds, and sample observations
|
||||
3. Pre-loads all thresholds into Redis
|
||||
4. Provisions Kafka topics and Elasticsearch indices
|
||||
5. Declares the RabbitMQ exchange and queue topology
|
||||
@@ -400,6 +404,7 @@ Integration tests use `WebApplicationFactory` with a `Testing` environment and T
|
||||
| `ReconciliationTests` | 7 | Three reconciliation checks, deduplication, RabbitMQ publish |
|
||||
| `ObservabilityPhase8Tests` | 8 | All eight `/metrics` families, correlation headers, ingest counter increment |
|
||||
| `DataLakePhase9Tests` | 9 | Kafka → MinIO Parquet flow and schema checks |
|
||||
| `ClinicalDemographicsAndObservationTests` | 10 | Patient clinical fields, encounter enrichment, expanded observation codes, critical glucose alert |
|
||||
|
||||
### Verification Scripts
|
||||
|
||||
@@ -408,6 +413,7 @@ With the API running (`dotnet run`) and Docker Compose up:
|
||||
```bash
|
||||
./scripts/run-phase8-verification.sh # Prometheus target UP, eight metrics, alerts_unacknowledged_gauge live update
|
||||
./scripts/run-phase9-verification.sh # DataLakePhase9Tests, Kafka consumer group, MinIO Parquet, DuckDB schema
|
||||
./scripts/run-phase10-verification.sh # 12 Redis thresholds, clinical enrichment, ES pipeline, Phase 10 integration tests
|
||||
```
|
||||
|
||||
Per-phase test runners (subset of `dotnet test`):
|
||||
@@ -434,7 +440,7 @@ curl https://install.duckdb.org | sh
|
||||
export PATH="$HOME/.duckdb/cli/latest:$HOME/.local/bin:$PATH"
|
||||
```
|
||||
|
||||
See `docs/plans/phase-8-plan.md` and `docs/plans/phase-9-plan.md` for manual Grafana, Seq, Kafka replay, and DuckDB query examples.
|
||||
See `docs/plans/phase-8-plan.md`, `docs/plans/phase-9-plan.md`, and `docs/plans/phase-10-plan.md` for manual Grafana, Seq, Kafka replay, and DuckDB query examples.
|
||||
|
||||
---
|
||||
|
||||
@@ -495,6 +501,10 @@ Error response:
|
||||
| `lastName` | string | yes | |
|
||||
| `dateOfBirth` | date | yes | |
|
||||
| `gender` | string | yes | |
|
||||
| `bloodType` | string | no | Clinical notation: `A+`, `O-`, `AB-`, etc. |
|
||||
| `allergies` | string | no | Free-text allergy list |
|
||||
| `emergencyContactName` | string | no | |
|
||||
| `emergencyContactPhone` | string | no | |
|
||||
|
||||
### Encounters
|
||||
|
||||
@@ -521,6 +531,10 @@ scheduled → active → discharged
|
||||
| `encounterType` | string | yes | `INPATIENT`, `OUTPATIENT`, `EMERGENCY` |
|
||||
| `department` | string | yes | |
|
||||
| `attendingPhysician` | string | yes | |
|
||||
| `roomBed` | string | no | Ward and bed assignment (e.g. `ICU-1A`) |
|
||||
| `admissionReason` | string | no | Clinical reason for admission |
|
||||
|
||||
**PATCH `/encounters/{id}/status` body** — optional `dischargeDiagnosis` when transitioning to `DISCHARGED`.
|
||||
|
||||
### Alert Thresholds
|
||||
|
||||
@@ -535,7 +549,7 @@ scheduled → active → discharged
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|---|---|---|---|
|
||||
| `observationCode` | string | yes | e.g. `HEART_RATE`, `TEMP_C`, `POTASSIUM_MEQ_L` |
|
||||
| `observationCode` | string | yes | e.g. `HEART_RATE`, `SYSTOLIC_BP`, `AVPU`, `GLUCOSE_MG_DL` |
|
||||
| `displayName` | string | yes | Human-readable label |
|
||||
| `unit` | string | yes | e.g. `bpm`, `°C`, `mEq/L` |
|
||||
| `criticalLow` | decimal | no | |
|
||||
@@ -543,14 +557,22 @@ scheduled → active → discharged
|
||||
| `warningHigh` | decimal | no | |
|
||||
| `criticalHigh` | decimal | no | |
|
||||
|
||||
Seeded thresholds:
|
||||
Seeded thresholds (12 codes):
|
||||
|
||||
| Code | Display | Unit | Critical Low | Warning Low | Warning High | Critical High |
|
||||
|---|---|---|---|---|---|---|
|
||||
| `HEART_RATE` | Heart Rate | bpm | 30 | 50 | 100 | 150 |
|
||||
| `TEMP_C` | Temperature | °C | 35.0 | 36.0 | 38.3 | 40.0 |
|
||||
| `POTASSIUM_MEQ_L` | Serum Potassium | mEq/L | 2.5 | 3.5 | 5.0 | 6.5 |
|
||||
| `SPO2_PCT` | Oxygen Saturation | % | 85 | 90 | — | — |
|
||||
| `SPO2` | Oxygen Saturation | % | 88 | 92 | — | — |
|
||||
| `RESP_RATE` | Respiratory Rate | breaths/min | — | 12 | 20 | 30 |
|
||||
| `WBC_K_UL` | White Blood Cell Count | k/µL | 2.0 | 4.0 | 12.0 | 20.0 |
|
||||
| `SYSTOLIC_BP` | Systolic Blood Pressure | mmHg | 70 | 90 | 160 | 180 |
|
||||
| `DIASTOLIC_BP` | Diastolic Blood Pressure | mmHg | 40 | 60 | 90 | 110 |
|
||||
| `LACTATE_MMOL_L` | Serum Lactate | mmol/L | — | — | 2.0 | 4.0 |
|
||||
| `AVPU` | AVPU Consciousness | score | — | — | — | 2 |
|
||||
| `SUPPLEMENTAL_O2` | Supplemental Oxygen | flag | — | — | — | — |
|
||||
| `GLUCOSE_MG_DL` | Blood Glucose | mg/dL | 40 | 70 | 180 | 400 |
|
||||
|
||||
### Observations
|
||||
|
||||
@@ -660,14 +682,18 @@ The `population` query uses Elasticsearch's numeric range aggregation engine —
|
||||
### Patient
|
||||
|
||||
```
|
||||
id Guid PK
|
||||
mrn string required, unique — auto-generated on registration (e.g. MRN-000001)
|
||||
firstName string required (max 100)
|
||||
lastName string required (max 100)
|
||||
dateOfBirth Date required
|
||||
gender string required (max 10)
|
||||
status string active | inactive (default: active)
|
||||
createdAt DateTimeOffset
|
||||
id Guid PK
|
||||
mrn string required, unique — auto-generated on registration (e.g. MRN-000001)
|
||||
firstName string required (max 100)
|
||||
lastName string required (max 100)
|
||||
dateOfBirth Date required
|
||||
gender string required (max 10)
|
||||
bloodType string? A+ | A- | B+ | B- | AB+ | AB- | O+ | O-
|
||||
allergies string? free text
|
||||
emergencyContactName string? (max 200)
|
||||
emergencyContactPhone string? (max 30)
|
||||
status string active | inactive (default: active)
|
||||
createdAt DateTimeOffset
|
||||
```
|
||||
|
||||
### Encounter
|
||||
@@ -679,6 +705,9 @@ encounterType string INPATIENT | OUTPATIENT | EMERGENCY
|
||||
status string scheduled | active | discharged | cancelled (default: scheduled)
|
||||
department string required (max 100)
|
||||
attendingPhysician string required (max 200)
|
||||
roomBed string? ward/bed assignment (max 50)
|
||||
admissionReason string? clinical reason for admission
|
||||
dischargeDiagnosis string? set on discharge
|
||||
admittedAt DateTimeOffset
|
||||
dischargedAt DateTimeOffset?
|
||||
createdAt DateTimeOffset
|
||||
@@ -790,6 +819,8 @@ createdAt DateTimeOffset
|
||||
"department": "ICU",
|
||||
"status": "active",
|
||||
"attendingPhysician": "Dr. Osei",
|
||||
"roomBed": "ICU-4B",
|
||||
"admissionReason": "Chest pain, rule out MI",
|
||||
"admittedAt": "2025-01-01T08:00:00Z",
|
||||
"openAlertCount": 2,
|
||||
"lastObservationAt": "2025-01-01T09:45:00Z"
|
||||
@@ -948,7 +979,7 @@ Observation history uses cursor pagination on `(recorded_at DESC, id DESC)`. Off
|
||||
|
||||
## Implemented Phases
|
||||
|
||||
All nine phases from the project roadmap are implemented and covered by integration tests and/or verification scripts.
|
||||
All ten phases from the project roadmap are implemented and covered by integration tests and/or verification scripts.
|
||||
|
||||
| Phase | Feature | Status |
|
||||
|---|---|---|
|
||||
@@ -961,5 +992,6 @@ All nine phases from the project roadmap are implemented and covered by integrat
|
||||
| 7 | Reconciliation scheduler — unacknowledged critical alerts, stale pending orders, disconnected monitors; `reconciliation_alerts` table; RabbitMQ publish; integration tests | Done |
|
||||
| 8 | Prometheus metrics (`GET /metrics`); eight metric families and three collectors; Grafana clinical dashboard; `ObservabilityPhase8Tests`; `run-phase8-verification.sh` | Done |
|
||||
| 9 | Data lake writer — `data-lake-writer` consumer group; date-partitioned Parquet flush to MinIO; `DataLakePhase9Tests`; `run-phase9-verification.sh`; design doc in `docs/decisions/data-lake-design.md` | Done |
|
||||
| 10 | Clinical data model expansion — `BloodType`, patient allergies/emergency contact, encounter room/bed/admission/discharge fields; five new observation codes (`SYSTOLIC_BP`, `DIASTOLIC_BP`, `LACTATE_MMOL_L`, `AVPU`, `SUPPLEMENTAL_O2`); `GLUCOSE_MG_DL` threshold fix; 12 seeded thresholds; `ClinicalDemographicsAndObservationTests`; `run-phase10-verification.sh` | Done |
|
||||
|
||||
**Optional follow-up:** execute and document the Kafka replay demonstration for the data lake (reset `data-lake-writer` offsets, clear MinIO prefixes, restart API, confirm Parquet rebuild). See `docs/plans/phase-9-plan.md` § Replay demonstration.
|
||||
|
||||
Reference in New Issue
Block a user