feature: HL7 FHIR R4 Integration

This commit is contained in:
voltsrage
2026-06-27 22:23:45 +08:00
parent 756cff332c
commit 5646dfddb4
27 changed files with 2658 additions and 16 deletions
+70 -14
View File
@@ -2,7 +2,7 @@
A clinical records digitization platform built with ASP.NET Core 8, PostgreSQL, MinIO, Redis, and a Vue 3 workstation UI. The domain models the scan-to-approved lifecycle at the center of any paper chart digitization system: scanned document upload, human data entry, dual-human verification with separation of duties, site-configurable clinical approval routing, atomic promotion to live clinical tables, and governed correction via supersession.
**Implementation status:** Phases 19 are complete. Phase 7 delivers the digitization workstation UI (`vigilcare-records-web`) with role-specific views for intake, entry, verification, clinical approval, live capture, patient history, and supervisor dashboard. Phase 8 adds Prometheus metrics, supervisor work-queue overview, cursor-paginated batch audit trails, promotion deferral with exponential backoff retry, and a Docker monitoring stack (Prometheus on **9095**, Grafana on **3013**). Phase 9 adds extended demo seed data (10 batches across all types, tracks, and statuses), the E2E verification script (`scripts/run-vigilcare-records-verification-p9.sh`), and clinical scenario documentation ([digitization-workstation-guide.md](docs/digitization-workstation-guide.md)). Post-phase hardening includes health check endpoints, user management APIs, auth rate limiting, document access audit events, batch cancellation, list/queue sorting (`sortBy`/`sortDirection`), unified promotion retry logic, normalized patient deduplication, and assignment-time `IN_ENTRY` transitions. See [Implemented Phases](#implemented-phases) and the [gap analysis](docs/vigilcare-records-gap-analysis.md) for remaining work.
**Implementation status:** Phases 110 are complete. Phase 7 delivers the digitization workstation UI (`vigilcare-records-web`) with role-specific views for intake, entry, verification, clinical approval, live capture, patient history, and supervisor dashboard. Phase 8 adds Prometheus metrics, supervisor work-queue overview, cursor-paginated batch audit trails, promotion deferral with exponential backoff retry, and a Docker monitoring stack (Prometheus on **9095**, Grafana on **3013**). Phase 9 adds extended demo seed data (10 batches across all types, tracks, and statuses), the E2E verification script (`scripts/run-vigilcare-records-verification-p9.sh`), and clinical scenario documentation ([digitization-workstation-guide.md](docs/digitization-workstation-guide.md)). Phase 10 adds barcode/QR cover sheets for high-volume backfill: generate printable cover pages with encoded batch type, track, optional patient, and optional entry-clerk pre-assignment; barcode-assisted upload auto-creates batches and skips manual classification. Post-phase hardening includes health check endpoints, user management APIs, auth rate limiting, document access audit events, batch cancellation, list/queue sorting (`sortBy`/`sortDirection`), unified promotion retry logic, normalized patient deduplication, and assignment-time `IN_ENTRY` transitions. Phase 11 (HL7 FHIR R4 read API) is planned next — see [docs/plans/phase-11-plan.md](docs/plans/phase-11-plan.md). See [Implemented Phases](#implemented-phases) and the [gap analysis](docs/vigilcare-records-gap-analysis.md) for remaining work.
## Domain Model — How It Maps to a Real Clinical System
@@ -45,7 +45,8 @@ Append-only audit log entry for every state transition, field-level correction,
## Features
- **Document Upload and Batch Creation** — multipart upload (PDF, JPEG, PNG; max 25 MB) with MinIO storage, SHA-256 integrity hash, presigned GET URLs (15-minute expiry); duplicate detection per patient within 24 hours by SHA-256 (`409 DUPLICATE_DOCUMENT`); cross-patient duplicate scans allowed; optional `supersedesBatchId` creates a correction batch linked to a promoted batch; batch created in `UPLOADED` status with `DigitizationEvent` audit trail
- **Document Upload and Batch Creation** — multipart upload (PDF, JPEG, PNG; max 25 MB) with MinIO storage, SHA-256 integrity hash, presigned GET URLs (15-minute expiry); duplicate detection per patient within 24 hours by SHA-256 (`409 DUPLICATE_DOCUMENT`); cross-patient duplicate scans allowed; optional `supersedesBatchId` creates a correction batch linked to a promoted batch; batch created in `UPLOADED` status with `DigitizationEvent` audit trail; optional `coverSheetCode` looks up a cover sheet barcode, applies encoded batch type/track/patient, redeems the cover sheet on success (`409 COVER_SHEET_ALREADY_USED` on reuse), and auto-assigns the batch when the cover sheet has `assignToUserId` set
- **Cover Sheet System** — `POST /cover-sheets/generate` creates 1100 cover sheets with unique `VCR-CS-{8-hex}` codes encoding batch type, track, optional patient, and optional entry-clerk pre-assignment; `GET /cover-sheets/lookup/{code}` resolves a barcode for intake auto-fill; `GET /cover-sheets` lists sheets with `isUsed`/`patientId` filters; `POST /cover-sheets/{id}/pdf` and `POST /cover-sheets/batch-pdf` produce printable PDFs with QR codes (QRCoder); cover sheets are single-use and linked to the batch they create via `batchId`
- **Batch Assignment** — `PATCH /digitization-batches/:id/assign` assigns an entry clerk with a Redis lock (`SET batch:assign:{id} NX EX 3600`) to prevent double-assignment; transitions `UPLOADED → IN_ENTRY` immediately and writes an `entry_started` audit event; only `UPLOADED` batches can be assigned; `409 BATCH_ALREADY_ASSIGNED` on conflict
- **Batch Cancellation** — `POST /digitization-batches/:id/cancel` (administrator only) permanently cancels batches in `UPLOADED`, `IN_ENTRY`, or `REJECTED` status with a mandatory reason (min 5 characters); releases the Redis assignment lock; `CANCELLED` is terminal
- **Draft Data Entry** — full CRUD on draft payload: upsert patient demographics, upsert encounter context, add/edit/delete observation rows; plausibility validation on observation values at draft save time (catches decimal errors before verification); `DraftService` retains a fallback `UPLOADED`/`REJECTED → `IN_ENTRY` transition when entry begins without prior assignment; draft save requires the acting user to match `enteredByUserId` or hold `Administrator` role (`409 BATCH_NOT_ASSIGNED`)
@@ -61,7 +62,7 @@ Append-only audit log entry for every state transition, field-level correction,
- **Batch Audit Trail API** — `GET /digitization-batches/:id/events` returns cursor-paginated digitization events with actor username and full name; accessible by administrator, verifier, and clinical approver roles
- **User Directory and Management** — `GET /users?role=` lists active users for batch assignment; administrators can `POST /users` (create), `PATCH /users/:id` (update name, role, active flag), `POST /users/:id/reset-password`, and any authenticated user can `POST /users/me/change-password` with current-password verification
- **Document Access Audit** — `GET /digitization-batches/:id` writes a `document_accessed` digitization event (deduplicated per user within 5 minutes) whenever a presigned scan URL is issued
- **Digitization Workstation UI** — Vue 3 SPA at `vigilcare-records-web`: role-based routing and nav (intake, entry, verification, clinical approval, live capture, patient history, supervisor dashboard); split-pane scan viewer with zoom/pan/rotate; draft entry with auto-save and batch-type-aware fields (allergies, medications, discharge diagnosis); field-level verification checkboxes; clinical approval queue with scan review, approve/reject, and retroactive alert toggle; live capture form for new or existing encounters with attestation and password confirm; patient history timeline with correction chain and audit trail; presigned URL refresh for long sessions; JWT refresh interceptor and toast notifications
- **Digitization Workstation UI** — Vue 3 SPA at `vigilcare-records-web`: role-based routing and nav (intake, cover sheets, entry, verification, clinical approval, live capture, patient history, supervisor dashboard); split-pane scan viewer with zoom/pan/rotate; cover sheet management view (generate, list, print PDF); barcode-assisted intake (scan/type cover sheet code to auto-fill batch type, track, patient, then upload); draft entry with auto-save and batch-type-aware fields (allergies, medications, discharge diagnosis); field-level verification checkboxes; clinical approval queue with scan review, approve/reject, and retroactive alert toggle; live capture form for new or existing encounters with attestation and password confirm; patient history timeline with correction chain and audit trail; presigned URL refresh for long sessions; JWT refresh interceptor and toast notifications
- **Live Capture (Track B)** — `POST /live-capture/encounters/{encounterId}/observations` and `POST /live-capture/encounters` for credentialed clinicians entering vitals at bedside; clinician attestation + password re-confirm replaces the dual-human verification gate; observations promote synchronously to live tables in a single transaction with `source = live_capture`; critical threshold evaluation runs before the response returns, with inline `criticalAlert` per observation, committed `ClinicalAlert` rows, and `observation.recorded` / `alert.generated` outbox events; `422 ATTESTATION_REQUIRED`, `422 PASSWORD_CONFIRM_INVALID`, `422 EMPTY_OBSERVATIONS`, `409 ENCOUNTER_NOT_ACTIVE`, `409 ACTIVE_ENCOUNTER_EXISTS`
- **Batch Status Machine** — explicit transition matrix enforced in the service layer; illegal transitions return `409`; `PROMOTED` and `CANCELLED` are terminal — corrections require a new batch with `supersedesBatchId`
- **JWT Authentication** — `POST /auth/login` returns access token (15 min) and refresh token (7 days); `POST /auth/refresh` rotates tokens; `POST /auth/logout` revokes server-side; `GET /auth/me` returns authenticated user profile; BCrypt password hashing; login and refresh rate-limited to 10 requests per 5 minutes per client (`429`)
@@ -99,6 +100,8 @@ HTTP request
├── UserDirectoryService (active user listing, create/update/deactivate, password reset/change)
├── AttestationService (clinician role + password re-confirm for live capture)
├── LiveCaptureService (Track B synchronous promotion + critical alert evaluation)
├── CoverSheetService (generate, lookup, redeem, list cover sheets)
├── CoverSheetPdfGenerator (printable PDF with QR codes)
├── DocumentStorageService (MinIO upload, SHA-256, presigned URLs)
├── PlausibilityValidator (per-code numeric range guard)
├── PostgreSQL (EF Core — writes, keyed reads, clinical schema for live tables)
@@ -143,6 +146,7 @@ HTTP request
| Logging | Serilog + Seq sink |
| Metrics | Prometheus (`prometheus-net`) + Grafana |
| Docs | Swagger / OpenAPI (Swashbuckle) |
| Barcode / PDF | QRCoder (cover sheet QR codes; raw PDF generation) |
| Testing | xUnit + FluentAssertions + WebApplicationFactory |
---
@@ -157,6 +161,7 @@ VigilCareRecords/
│ ├── Controllers/
│ │ ├── ApprovalController.cs # Batch approval, promotion deferral (202), promotion to live tables
│ │ ├── AuthController.cs # JWT login, token refresh, logout, authenticated user profile
│ │ ├── CoverSheetController.cs # Cover sheet generate, lookup, list, PDF export
│ │ ├── DigitizationBatchesController.cs # Batch CRUD, document upload, assignment, events, promote
│ │ ├── DraftController.cs # Draft data entry: patient, encounter, observations, submit
│ │ ├── PatientsController.cs # Patient search and digitization history
@@ -175,13 +180,13 @@ VigilCareRecords/
│ │ ├── api/client.ts # Axios client with JWT interceptor and proactive refresh
│ │ ├── stores/ # Pinia: auth, batches, liveCapture
│ │ ├── router/index.ts # Role-based routes and navigation guards
│ │ ├── views/ # Login, Intake, Entry, Verification, Approval, LiveCapture, PatientHistory, QueueDashboard
│ │ ├── views/ # Login, Intake, CoverSheets, Entry, Verification, Approval, LiveCapture, PatientHistory, QueueDashboard
│ │ ├── components/ # ScanViewer, EntryForm, VerificationForm, BatchList, PatientSearch, AppHeader, ToastContainer, AssignClerkDialog
│ │ ├── composables/ # usePresignedUrl (URL refresh), useToast (notifications)
│ │ └── types/index.ts # TypeScript interfaces matching API response shapes
│ ├── vite.config.ts # Dev server on port 3028; proxies /api → localhost:5217
│ └── tailwind.config.js # Clinical color palette and layout component classes
├── VigilCareRecordsAPI.Tests/ # Integration tests (Phases 19)
├── VigilCareRecordsAPI.Tests/ # Integration tests (Phases 110)
├── prometheus.yml # Prometheus scrape config (API on host.docker.internal:5217)
├── docker-compose.yml # PostgreSQL, Redis, Seq, MinIO, Prometheus, Grafana
├── scripts/
@@ -192,9 +197,11 @@ VigilCareRecords/
│ ├── run-vigilcare-records-phase-5-verification.sh
│ ├── run-vigilcare-records-phase-6-verification.sh
│ ├── run-vigilcare-records-phase-8-verification.sh # Prometheus metrics, overview, events, retry
── run-vigilcare-records-verification-p9.sh # E2E workflow + work queue overview smoke test
── run-vigilcare-records-verification-p9.sh # E2E workflow + work queue overview smoke test
│ └── run-vigilcare-records-phase-10-verification.sh # Cover sheets, barcode upload, PDF, auto-assign
│ └── run-vigilcare-records-phase-11-verification.sh # FHIR metadata, read/search, $everything, integration tests
└── docs/
├── plans/ # Phase 19 implementation guides
├── plans/ # Phase 111 implementation guides
├── digitization-workstation-guide.md # Clinical scenarios and clerk workflow reference
├── vigilcare-records-gap-analysis.md # Known gaps and hardening backlog
└── vigilcare-records-prd.md # Product requirements and phase roadmap
@@ -363,7 +370,7 @@ Open `http://localhost:3028`. The Vite dev server proxies `/api` requests to the
| Username | Password | Default route |
|---|---|---|
| `intake1` | `password` | `/intake` — upload scans, assign entry clerks |
| `intake1` | `password` | `/intake` — upload scans, barcode-assisted cover sheet upload; `/cover-sheets` — generate and print cover sheets |
| `entry1` | `password` | `/entry` — data entry queue and split-pane form |
| `verifier1` | `password` | `/verification` — field-level verification |
| `approver1` | `password` | `/approval` — clinical sign-off before promotion |
@@ -401,6 +408,8 @@ Integration tests use `WebApplicationFactory` with PostgreSQL, Redis, and MinIO
| `CorrectionSupersessionTests` | 5 | Correction batch supersession, validation guards (non-promoted, already superseded), patient digitization history, unknown patient 404 |
| `LiveCaptureIntegrationTests` | 6 | Attestation and password confirm, synchronous promotion, critical low/high potassium alerts, mixed-batch alerting, open encounter + vitals, role and validation guards, audit events |
| `UserManagementTests` | 9 | User create/update/deactivate, admin password reset, self-service password change, duplicate username guard, weak password validation |
| `CoverSheetBatchTests` | 10 | Cover sheet redeem on upload, reuse prevention, auto-assign from pre-assigned cover sheet |
| `CoverSheetPdfTests` | 10 | Single and batch PDF generation, valid PDF structure with QR metadata |
| `BatchOperationsTests` | — | Batch cancellation (status guards, Redis lock release), list/queue `sortBy`/`sortDirection` validation |
### Verification Scripts
@@ -416,6 +425,8 @@ With the API running (`dotnet run`) and Docker Compose up:
./scripts/run-vigilcare-records-phase-6-verification.sh # Phase 6 — live capture, attestation, critical alerts
./scripts/run-vigilcare-records-phase-8-verification.sh # Phase 8 — Prometheus metrics, work-queue overview, batch events, promotion retry
./scripts/run-vigilcare-records-verification-p9.sh # E2E workflow + work queue overview smoke test
./scripts/run-vigilcare-records-phase-10-verification.sh # Phase 10 — cover sheets, barcode upload, PDF, auto-assign
./scripts/run-vigilcare-records-phase-11-verification.sh # Phase 11 — FHIR R4 read/search, $everything, LOINC, integration tests
```
---
@@ -501,10 +512,11 @@ Error response:
| Field | Type | Required | Description |
|---|---|---|---|
| `file` | binary | yes | PDF, JPEG, or PNG (max 25 MB) |
| `batchType` | string | yes | `PATIENT_REGISTRATION`, `ENCOUNTER_SUMMARY`, `VITALS_SHEET`, `LAB_RESULTS`, `MEDICATION_LIST`, `ALLERGY_UPDATE`, `MIXED` |
| `track` | string | no | `BACKFILL` (default) or `LIVE_CAPTURE` |
| `patientId` | Guid | no | Link to existing patient (enables duplicate detection) |
| `batchType` | string | yes* | `PATIENT_REGISTRATION`, `ENCOUNTER_SUMMARY`, `VITALS_SHEET`, `LAB_RESULTS`, `MEDICATION_LIST`, `ALLERGY_UPDATE`, `MIXED` — required unless `coverSheetCode` is provided; cover sheet values override when both are sent |
| `track` | string | no | `BACKFILL` (default) or `LIVE_CAPTURE` — overridden by cover sheet when `coverSheetCode` is set |
| `patientId` | Guid | no | Link to existing patient (enables duplicate detection); inherited from cover sheet when set |
| `supersedesBatchId` | Guid | no | Links a correction batch to the promoted batch it will supersede on promotion |
| `coverSheetCode` | string | no | Cover sheet barcode (e.g. `VCR-CS-A3F7B2D1`); auto-applies batch type, track, and patient; redeems on success; auto-assigns when cover sheet has `assignToUserId` |
**Status codes:**
@@ -512,8 +524,8 @@ Error response:
|---|---|
| 201 | Batch created |
| 400 | Empty file or invalid MIME type |
| 404 | Superseded batch not found (`SUPERSEDED_BATCH_NOT_FOUND`) |
| 409 | Duplicate document (same SHA-256 for same patient within 24 hours); batch already superseded (`BATCH_ALREADY_SUPERSEDED`) |
| 404 | Superseded batch not found (`SUPERSEDED_BATCH_NOT_FOUND`); cover sheet not found (`COVER_SHEET_NOT_FOUND`) |
| 409 | Duplicate document (same SHA-256 for same patient within 24 hours); batch already superseded (`BATCH_ALREADY_SUPERSEDED`); cover sheet already used (`COVER_SHEET_ALREADY_USED`) |
| 422 | Superseded batch not in `PROMOTED` status (`SUPERSEDED_BATCH_NOT_PROMOTED`) |
**PATCH `/digitization-batches/{id}/assign` body:**
@@ -530,6 +542,30 @@ Error response:
**Status codes:** `409 ILLEGAL_STATUS_TRANSITION` when the batch is not in a cancellable status.
### Cover Sheets
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | `/cover-sheets/generate` | Intake Clerk, Administrator | Generate 1100 cover sheets with unique barcode codes |
| GET | `/cover-sheets/lookup/{code}` | Any authenticated | Look up a cover sheet by barcode for intake auto-fill |
| GET | `/cover-sheets` | Intake Clerk, Administrator | List cover sheets; optional `isUsed`, `patientId` filters; paginated |
| POST | `/cover-sheets/{id}/pdf` | Intake Clerk, Administrator | Download a printable PDF with QR code for one cover sheet |
| POST | `/cover-sheets/batch-pdf` | Intake Clerk, Administrator | Download a multi-page PDF for a list of cover sheet IDs |
**POST `/cover-sheets/generate` body:**
| Field | Type | Required | Description |
|---|---|---|---|
| `count` | int | yes | Number of cover sheets to generate (1100) |
| `batchType` | string | yes | Batch type encoded in the barcode |
| `track` | string | no | `BACKFILL` (default) or `LIVE_CAPTURE` |
| `patientId` | Guid | no | Pre-link to an existing patient |
| `assignToUserId` | Guid | no | Pre-assign batches created with this cover sheet to an entry clerk |
**Cover sheet response fields:** `id`, `code` (`VCR-CS-{8-hex}`), `batchType`, `track`, `patientId`, `patientName`, `patientMrn`, `assignToUserId`, `assignToUserName`, `isUsed`, `batchId`, `createdAt`, `usedAt`.
**Status codes:** `404 PATIENT_NOT_FOUND`, `404 USER_NOT_FOUND`, `404 COVER_SHEET_NOT_FOUND`, `409 COVER_SHEET_ALREADY_USED`.
### Draft Data Entry
| Method | Path | Description |
@@ -829,6 +865,24 @@ Returns `503` when a required dependency is unhealthy.
## Data Models
### CoverSheet
Single-use barcode label that encodes batch metadata for high-volume backfill intake. Redeemed atomically when a batch is created with `coverSheetCode`.
```
id Guid PK
code string required, unique — VCR-CS-{8-hex} encoded in QR barcode
patientId Guid? optional pre-link to patient
batchType string encoded batch type
track string BACKFILL | LIVE_CAPTURE
assignToUserId Guid? optional entry clerk pre-assignment
generatedByUserId Guid FK → User who generated the cover sheet
isUsed bool default false — set true on batch creation
batchId Guid? FK → DigitizationBatch created from this cover sheet
createdAt DateTimeOffset
usedAt DateTimeOffset? set when redeemed
```
### DigitizationBatch
```
@@ -1149,7 +1203,7 @@ Response shape:
## Implemented Phases
Phases 19 are fully implemented and verified via integration tests and per-phase scripts. Post-phase hardening (health checks, user management, promotion retry unification, patient dedup normalization, assignment-time status transitions, document access audit, batch cancellation) is also in place. See [docs/vigilcare-records-gap-analysis.md](docs/vigilcare-records-gap-analysis.md) for the remaining backlog.
Phases 110 are fully implemented and verified via integration tests and per-phase scripts. Post-phase hardening (health checks, user management, promotion retry unification, patient dedup normalization, assignment-time status transitions, document access audit, batch cancellation) is also in place. Phase 11 (HL7 FHIR R4 read API) is planned — see [docs/plans/phase-11-plan.md](docs/plans/phase-11-plan.md). See [docs/vigilcare-records-gap-analysis.md](docs/vigilcare-records-gap-analysis.md) for the remaining backlog.
| Phase | Feature | Status |
|---|---|---|
@@ -1162,4 +1216,6 @@ Phases 19 are fully implemented and verified via integration tests and per-ph
| 7 | Digitization workstation UI (`vigilcare-records-web`): Vue 3 + Pinia + Tailwind, role-based routing and nav, split-pane scan viewer, batch-type-aware draft entry (allergies, medications, discharge diagnosis), verification checkboxes, clinical approval view, live capture view (new/existing encounter), patient history timeline, patient search, assign-clerk dialog, presigned URL refresh, supervisor dashboard, toast notifications | Done |
| 8 | Prometheus metrics (`GET /metrics`), custom digitization gauges/histograms/counters, `MetricsCollectorService`, supervisor `GET /work-queue/overview`, cursor-paginated `GET /digitization-batches/:id/events`, promotion deferral (202 / `PROMOTION_DEFERRED`), `PromotionRetryService` with exponential backoff, Docker Prometheus (9095) + Grafana (3013), Phase 8 verification script | Done |
| 9 | Extended seed data (10 demo batches across all types/tracks/statuses), E2E verification script (`run-vigilcare-records-verification-p9.sh`), clinical scenario docs (`docs/digitization-workstation-guide.md`), `UserManagementTests` | Done |
| 10 | Barcode/QR cover sheet system: `CoverSheet` entity, generate/lookup/list/PDF APIs, `coverSheetCode` on batch upload with redeem and auto-assign, printable PDF with QRCoder, `/cover-sheets` and barcode-assisted `/intake` UI views, `CoverSheetBatchTests`, `CoverSheetPdfTests`, Phase 10 verification script | Done |
| — | Health probes (`/health/live`, `/health/ready`, `/health/startup`), user management CRUD + password endpoints, auth rate limiting, document access audit, batch cancellation, list/queue sorting, unified `ExecutePromotionCoreAsync` for approve + retry promote, assignment-time `IN_ENTRY` transition, CORS policy for production frontend origins | Done |
| 11 | HL7 FHIR R4 read-only API for promoted clinical data (Patient, Encounter, Observation) | Planned |