chore: update readme
This commit is contained in:
@@ -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.
|
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 1–10 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.
|
**Implementation status:** Phases 1–11 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. Phase 11 adds an HL7 FHIR R4 read-only API for promoted clinical data (`Patient`, `Encounter`, `Observation`) with LOINC code mapping, search bundles with pagination links, `$everything`, and an administrator FHIR Explorer view at `/fhir-explorer`. 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 12 (backend-driven batch-type field requirements) is planned next — see [docs/plans/phase-12-plan.md](docs/plans/phase-12-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
|
## Domain Model — How It Maps to a Real Clinical System
|
||||||
|
|
||||||
@@ -62,7 +62,9 @@ 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
|
- **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
|
- **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
|
- **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, 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
|
- **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, FHIR Explorer); 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
|
||||||
|
- **HL7 FHIR R4 Read API** — read-only FHIR endpoints at `/fhir` for promoted clinical data: `GET /fhir/metadata` (anonymous `CapabilityStatement`); authenticated read and search for `Patient`, `Encounter`, and `Observation`; MRN search via `Patient?identifier=`; LOINC bidirectional mapping for observation codes (e.g. `HEART_RATE` ↔ `8867-4`); vital-signs category search; date filtering on `recordedAt`; `GET /fhir/Patient/{id}/$everything` composite bundle; search bundles with `self`/`next` pagination links; `404` responses as FHIR `OperationOutcome`; `application/fhir+json` content negotiation via `FhirJsonOutputFormatter`
|
||||||
|
- **FHIR Explorer UI** — administrator-only Vue view at `/fhir-explorer`: browse Patient/Encounter/Observation resources, run FHIR searches, inspect raw JSON, load Patient `$everything`, and open the CapabilityStatement metadata link; dev proxy at `/fhir` → API port 5217
|
||||||
- **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`
|
- **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`
|
- **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`)
|
- **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`)
|
||||||
@@ -102,6 +104,7 @@ HTTP request
|
|||||||
├── LiveCaptureService (Track B synchronous promotion + critical alert evaluation)
|
├── LiveCaptureService (Track B synchronous promotion + critical alert evaluation)
|
||||||
├── CoverSheetService (generate, lookup, redeem, list cover sheets)
|
├── CoverSheetService (generate, lookup, redeem, list cover sheets)
|
||||||
├── CoverSheetPdfGenerator (printable PDF with QR codes)
|
├── CoverSheetPdfGenerator (printable PDF with QR codes)
|
||||||
|
├── FhirService (FHIR R4 read/search/$everything over promoted clinical tables)
|
||||||
├── DocumentStorageService (MinIO upload, SHA-256, presigned URLs)
|
├── DocumentStorageService (MinIO upload, SHA-256, presigned URLs)
|
||||||
├── PlausibilityValidator (per-code numeric range guard)
|
├── PlausibilityValidator (per-code numeric range guard)
|
||||||
├── PostgreSQL (EF Core — writes, keyed reads, clinical schema for live tables)
|
├── PostgreSQL (EF Core — writes, keyed reads, clinical schema for live tables)
|
||||||
@@ -147,6 +150,7 @@ HTTP request
|
|||||||
| Metrics | Prometheus (`prometheus-net`) + Grafana |
|
| Metrics | Prometheus (`prometheus-net`) + Grafana |
|
||||||
| Docs | Swagger / OpenAPI (Swashbuckle) |
|
| Docs | Swagger / OpenAPI (Swashbuckle) |
|
||||||
| Barcode / PDF | QRCoder (cover sheet QR codes; raw PDF generation) |
|
| Barcode / PDF | QRCoder (cover sheet QR codes; raw PDF generation) |
|
||||||
|
| FHIR | Hl7.Fhir.R4 5.x (Firely SDK — Patient, Encounter, Observation, Bundle, OperationOutcome) |
|
||||||
| Testing | xUnit + FluentAssertions + WebApplicationFactory |
|
| Testing | xUnit + FluentAssertions + WebApplicationFactory |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -162,6 +166,7 @@ VigilCareRecords/
|
|||||||
│ │ ├── ApprovalController.cs # Batch approval, promotion deferral (202), promotion to live tables
|
│ │ ├── ApprovalController.cs # Batch approval, promotion deferral (202), promotion to live tables
|
||||||
│ │ ├── AuthController.cs # JWT login, token refresh, logout, authenticated user profile
|
│ │ ├── AuthController.cs # JWT login, token refresh, logout, authenticated user profile
|
||||||
│ │ ├── CoverSheetController.cs # Cover sheet generate, lookup, list, PDF export
|
│ │ ├── CoverSheetController.cs # Cover sheet generate, lookup, list, PDF export
|
||||||
|
│ │ ├── Fhir/ # FHIR R4 read/search controllers (Patient, Encounter, Observation, metadata)
|
||||||
│ │ ├── DigitizationBatchesController.cs # Batch CRUD, document upload, assignment, events, promote
|
│ │ ├── DigitizationBatchesController.cs # Batch CRUD, document upload, assignment, events, promote
|
||||||
│ │ ├── DraftController.cs # Draft data entry: patient, encounter, observations, submit
|
│ │ ├── DraftController.cs # Draft data entry: patient, encounter, observations, submit
|
||||||
│ │ ├── PatientsController.cs # Patient search and digitization history
|
│ │ ├── PatientsController.cs # Patient search and digitization history
|
||||||
@@ -171,22 +176,24 @@ VigilCareRecords/
|
|||||||
│ │ └── WorkQueueController.cs # Work queues and supervisor overview
|
│ │ └── WorkQueueController.cs # Work queues and supervisor overview
|
||||||
│ ├── HealthChecks/MinioHealthCheck.cs # MinIO bucket-exists readiness probe
|
│ ├── HealthChecks/MinioHealthCheck.cs # MinIO bucket-exists readiness probe
|
||||||
│ ├── Domain/ … # Entities, enums (batch, draft, clinical, user)
|
│ ├── Domain/ … # Entities, enums (batch, draft, clinical, user)
|
||||||
│ ├── Services/ … # Auth, batch, draft, verification, promotion, work queue, patient registry, user directory, live capture
|
│ ├── Services/ … # Auth, batch, draft, verification, promotion, FHIR, work queue, patient registry, user directory, live capture
|
||||||
|
│ ├── Infrastructure/Fhir/ … # FHIR mappers, JSON formatter, OperationOutcome helpers
|
||||||
│ ├── Models/Records/ … # Request/response DTOs
|
│ ├── Models/Records/ … # Request/response DTOs
|
||||||
│ ├── Data/ … # EF Core context, configurations, migrations, seed
|
│ ├── Data/ … # EF Core context, configurations, migrations, seed
|
||||||
│ └── … # Middleware, Common, Infrastructure
|
│ └── … # Middleware, Common, Infrastructure
|
||||||
├── vigilcare-records-web/ # Vue 3 digitization workstation UI
|
├── vigilcare-records-web/ # Vue 3 digitization workstation UI
|
||||||
│ ├── src/
|
│ ├── src/
|
||||||
│ │ ├── api/client.ts # Axios client with JWT interceptor and proactive refresh
|
│ │ ├── api/client.ts # Axios client with JWT interceptor and proactive refresh
|
||||||
|
│ │ ├── api/fhirClient.ts # Authenticated FHIR GET client (`Accept: application/fhir+json`)
|
||||||
│ │ ├── stores/ # Pinia: auth, batches, liveCapture
|
│ │ ├── stores/ # Pinia: auth, batches, liveCapture
|
||||||
│ │ ├── router/index.ts # Role-based routes and navigation guards
|
│ │ ├── router/index.ts # Role-based routes and navigation guards
|
||||||
│ │ ├── views/ # Login, Intake, CoverSheets, Entry, Verification, Approval, LiveCapture, PatientHistory, QueueDashboard
|
│ │ ├── views/ # Login, Intake, CoverSheets, Entry, Verification, Approval, LiveCapture, PatientHistory, QueueDashboard, FhirExplorer
|
||||||
│ │ ├── components/ # ScanViewer, EntryForm, VerificationForm, BatchList, PatientSearch, AppHeader, ToastContainer, AssignClerkDialog
|
│ │ ├── components/ # ScanViewer, EntryForm, VerificationForm, BatchList, PatientSearch, AppHeader, ToastContainer, AssignClerkDialog
|
||||||
│ │ ├── composables/ # usePresignedUrl (URL refresh), useToast (notifications)
|
│ │ ├── composables/ # usePresignedUrl (URL refresh), useToast (notifications)
|
||||||
│ │ └── types/index.ts # TypeScript interfaces matching API response shapes
|
│ │ └── types/index.ts # TypeScript interfaces matching API response shapes
|
||||||
│ ├── vite.config.ts # Dev server on port 3028; proxies /api → localhost:5217
|
│ ├── vite.config.ts # Dev server on port 3028; proxies /api and /fhir → localhost:5217
|
||||||
│ └── tailwind.config.js # Clinical color palette and layout component classes
|
│ └── tailwind.config.js # Clinical color palette and layout component classes
|
||||||
├── VigilCareRecordsAPI.Tests/ # Integration tests (Phases 1–10)
|
├── VigilCareRecordsAPI.Tests/ # Integration tests (Phases 1–11)
|
||||||
├── prometheus.yml # Prometheus scrape config (API on host.docker.internal:5217)
|
├── prometheus.yml # Prometheus scrape config (API on host.docker.internal:5217)
|
||||||
├── docker-compose.yml # PostgreSQL, Redis, Seq, MinIO, Prometheus, Grafana
|
├── docker-compose.yml # PostgreSQL, Redis, Seq, MinIO, Prometheus, Grafana
|
||||||
├── scripts/
|
├── scripts/
|
||||||
@@ -198,10 +205,10 @@ VigilCareRecords/
|
|||||||
│ ├── run-vigilcare-records-phase-6-verification.sh
|
│ ├── run-vigilcare-records-phase-6-verification.sh
|
||||||
│ ├── run-vigilcare-records-phase-8-verification.sh # Prometheus metrics, overview, events, retry
|
│ ├── 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-10-verification.sh # Cover sheets, barcode upload, PDF, auto-assign
|
||||||
│ └── run-vigilcare-records-phase-11-verification.sh # FHIR metadata, read/search, $everything, integration tests
|
│ └── run-vigilcare-records-phase-11-verification.sh # FHIR metadata, read/search, $everything, LOINC mapping
|
||||||
└── docs/
|
└── docs/
|
||||||
├── plans/ # Phase 1–11 implementation guides
|
├── plans/ # Phase 1–13 implementation guides
|
||||||
├── digitization-workstation-guide.md # Clinical scenarios and clerk workflow reference
|
├── digitization-workstation-guide.md # Clinical scenarios and clerk workflow reference
|
||||||
├── vigilcare-records-gap-analysis.md # Known gaps and hardening backlog
|
├── vigilcare-records-gap-analysis.md # Known gaps and hardening backlog
|
||||||
└── vigilcare-records-prd.md # Product requirements and phase roadmap
|
└── vigilcare-records-prd.md # Product requirements and phase roadmap
|
||||||
@@ -366,7 +373,7 @@ npm install
|
|||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Open `http://localhost:3028`. The Vite dev server proxies `/api` requests to the API on port 5217.
|
Open `http://localhost:3028`. The Vite dev server proxies `/api` and `/fhir` requests to the API on port 5217.
|
||||||
|
|
||||||
| Username | Password | Default route |
|
| Username | Password | Default route |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
@@ -375,7 +382,7 @@ Open `http://localhost:3028`. The Vite dev server proxies `/api` requests to the
|
|||||||
| `verifier1` | `password` | `/verification` — field-level verification |
|
| `verifier1` | `password` | `/verification` — field-level verification |
|
||||||
| `approver1` | `password` | `/approval` — clinical sign-off before promotion |
|
| `approver1` | `password` | `/approval` — clinical sign-off before promotion |
|
||||||
| `clinician1` | `password` | `/live-capture` — bedside vitals with attestation |
|
| `clinician1` | `password` | `/live-capture` — bedside vitals with attestation |
|
||||||
| `admin1` | `password` | `/dashboard` — supervisor queue overview |
|
| `admin1` | `password` | `/dashboard` — supervisor queue overview; `/fhir-explorer` — FHIR resource browser |
|
||||||
|
|
||||||
All roles can access `/patients` for patient search and digitization history.
|
All roles can access `/patients` for patient search and digitization history.
|
||||||
|
|
||||||
@@ -410,6 +417,7 @@ Integration tests use `WebApplicationFactory` with PostgreSQL, Redis, and MinIO
|
|||||||
| `UserManagementTests` | 9 | User create/update/deactivate, admin password reset, self-service password change, duplicate username guard, weak password validation |
|
| `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 |
|
| `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 |
|
| `CoverSheetPdfTests` | 10 | Single and batch PDF generation, valid PDF structure with QR metadata |
|
||||||
|
| `FhirIntegrationTests` | 11 | FHIR metadata, Patient/Encounter/Observation read and search, LOINC mapping, `$everything`, content-type, 404 OperationOutcome, bundle pagination |
|
||||||
| `BatchOperationsTests` | — | Batch cancellation (status guards, Redis lock release), list/queue `sortBy`/`sortDirection` validation |
|
| `BatchOperationsTests` | — | Batch cancellation (status guards, Redis lock release), list/queue `sortBy`/`sortDirection` validation |
|
||||||
|
|
||||||
### Verification Scripts
|
### Verification Scripts
|
||||||
@@ -426,9 +434,11 @@ With the API running (`dotnet run`) and Docker Compose up:
|
|||||||
./scripts/run-vigilcare-records-phase-8-verification.sh # Phase 8 — Prometheus metrics, work-queue overview, batch events, promotion retry
|
./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-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-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
|
./scripts/run-vigilcare-records-phase-11-verification.sh # Phase 11 — FHIR R4 read/search, $everything, LOINC mapping, clinical fixture seed
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The Phase 11 script auto-seeds a minimal clinical fixture for `VCR-000001` when PostgreSQL is available (updates existing patients or inserts deterministic demo rows). Run `dotnet test --filter FullyQualifiedName~FhirIntegrationTests` separately for WebApplicationFactory integration coverage.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## API Reference
|
## API Reference
|
||||||
@@ -861,6 +871,32 @@ Unauthenticated probe endpoints for orchestrators and load balancers:
|
|||||||
|
|
||||||
Returns `503` when a required dependency is unhealthy.
|
Returns `503` when a required dependency is unhealthy.
|
||||||
|
|
||||||
|
### HL7 FHIR R4 (Read-Only)
|
||||||
|
|
||||||
|
FHIR endpoints live at `/fhir` (not under `/api/v1`). Responses use `application/fhir+json`. Errors return FHIR `OperationOutcome` resources.
|
||||||
|
|
||||||
|
| Method | Path | Auth | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| GET | `/fhir/metadata` | Anonymous | Server `CapabilityStatement` (supported resources, interactions, search params) |
|
||||||
|
| GET | `/fhir/Patient/{id}` | JWT | Read a promoted patient by logical ID |
|
||||||
|
| GET | `/fhir/Patient` | JWT | Search patients by `name`, `birthdate`, `identifier` (MRN); supports `_count`, `_offset` |
|
||||||
|
| GET | `/fhir/Patient/{id}/$everything` | JWT | Bundle of Patient + all Encounters + all Observations for the patient |
|
||||||
|
| GET | `/fhir/Encounter/{id}` | JWT | Read a promoted encounter by logical ID |
|
||||||
|
| GET | `/fhir/Encounter` | JWT | Search encounters by `patient`, `status`, `date`; supports `_count`, `_offset` |
|
||||||
|
| GET | `/fhir/Observation/{id}` | JWT | Read a promoted observation by logical ID |
|
||||||
|
| GET | `/fhir/Observation` | JWT | Search observations by `patient`, `code` (LOINC or VigilCare code), `date`, `category`, `encounter`; supports `_count`, `_offset` |
|
||||||
|
|
||||||
|
**Search notes:**
|
||||||
|
|
||||||
|
- Observation `code=8867-4` resolves LOINC to VigilCare `HEART_RATE` (and reverse mapping on read).
|
||||||
|
- `category=vital-signs` filters vital-sign observation codes (`HEART_RATE`, `TEMP_C`, blood pressure, etc.).
|
||||||
|
- Date parameters use FHIR prefixes (e.g. `date=ge2026-06-20`) against observation `recordedAt`.
|
||||||
|
- Search bundles include `self` and `next` links when paginating.
|
||||||
|
|
||||||
|
**Status codes:** `404` with `OperationOutcome` issue code `not-found` when a resource ID does not exist.
|
||||||
|
|
||||||
|
**UI:** Administrators can browse FHIR resources interactively at `http://localhost:3028/fhir-explorer`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Data Models
|
## Data Models
|
||||||
@@ -1203,7 +1239,7 @@ Response shape:
|
|||||||
|
|
||||||
## Implemented Phases
|
## Implemented Phases
|
||||||
|
|
||||||
Phases 1–10 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.
|
Phases 1–11 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 12 (backend-driven batch-type field requirements for entry/verification forms) is planned — see [docs/plans/phase-12-plan.md](docs/plans/phase-12-plan.md). See [docs/vigilcare-records-gap-analysis.md](docs/vigilcare-records-gap-analysis.md) for the remaining backlog.
|
||||||
|
|
||||||
| Phase | Feature | Status |
|
| Phase | Feature | Status |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
@@ -1217,5 +1253,6 @@ Phases 1–10 are fully implemented and verified via integration tests and per-p
|
|||||||
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
||||||
|
| 11 | HL7 FHIR R4 read-only API: `FhirService`, Patient/Encounter/Observation mappers with LOINC mapping, read/search/`$everything` controllers, `CapabilityStatement` metadata, bundle pagination links, `FhirJsonOutputFormatter`, `FhirIntegrationTests`, administrator FHIR Explorer UI (`/fhir-explorer`), Phase 11 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 |
|
| — | 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 |
|
| 12 | Backend as single source of truth for batch-type field requirements (entry/verification form visibility metadata) | Planned |
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
# Runs Phase 11 verification checks from docs/plans/phase-11-plan.md.
|
# Runs Phase 11 verification checks from docs/plans/phase-11-plan.md.
|
||||||
#
|
#
|
||||||
# Covers FHIR metadata, Patient/Encounter/Observation read & search,
|
# Covers FHIR metadata, Patient/Encounter/Observation read & search,
|
||||||
# LOINC mapping, $everything, content-type negotiation, 404 OperationOutcome,
|
# LOINC mapping, $everything, content-type negotiation, and 404 OperationOutcome.
|
||||||
# and FhirIntegrationTests.
|
|
||||||
#
|
#
|
||||||
# Prerequisites:
|
# Prerequisites:
|
||||||
# docker compose up -d (PostgreSQL, Redis, MinIO)
|
# docker compose up -d (PostgreSQL, Redis, MinIO)
|
||||||
@@ -20,7 +19,6 @@
|
|||||||
# VIGILCARE_PG_USER default: postgres
|
# VIGILCARE_PG_USER default: postgres
|
||||||
# VIGILCARE_PG_PASSWORD default: password
|
# VIGILCARE_PG_PASSWORD default: password
|
||||||
# VIGILCARE_SKIP_DB_CHECKS set to 1 to skip PostgreSQL seed/assertions
|
# VIGILCARE_SKIP_DB_CHECKS set to 1 to skip PostgreSQL seed/assertions
|
||||||
# VIGILCARE_SKIP_TEST_CHECKS set to 1 to skip dotnet integration tests
|
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# chmod +x scripts/run-vigilcare-records-phase-11-verification.sh
|
# chmod +x scripts/run-vigilcare-records-phase-11-verification.sh
|
||||||
@@ -41,7 +39,6 @@ PG_DB="${VIGILCARE_PG_DB:-vigilcare_records}"
|
|||||||
PG_USER="${VIGILCARE_PG_USER:-postgres}"
|
PG_USER="${VIGILCARE_PG_USER:-postgres}"
|
||||||
PG_PASSWORD="${VIGILCARE_PG_PASSWORD:-password}"
|
PG_PASSWORD="${VIGILCARE_PG_PASSWORD:-password}"
|
||||||
SKIP_DB_CHECKS="${VIGILCARE_SKIP_DB_CHECKS:-0}"
|
SKIP_DB_CHECKS="${VIGILCARE_SKIP_DB_CHECKS:-0}"
|
||||||
SKIP_TEST_CHECKS="${VIGILCARE_SKIP_TEST_CHECKS:-0}"
|
|
||||||
|
|
||||||
# Deterministic clinical IDs (match FhirClinicalSeedHelper)
|
# Deterministic clinical IDs (match FhirClinicalSeedHelper)
|
||||||
PATIENT1_ID="b1000000-0000-0000-0000-000000000001"
|
PATIENT1_ID="b1000000-0000-0000-0000-000000000001"
|
||||||
@@ -53,6 +50,8 @@ BATCH1_ID="c1000000-0000-0000-0000-000000000001"
|
|||||||
|
|
||||||
ADMIN_TOKEN=""
|
ADMIN_TOKEN=""
|
||||||
RESOLVED_PATIENT_ID=""
|
RESOLVED_PATIENT_ID=""
|
||||||
|
RESOLVED_ENCOUNTER_ID=""
|
||||||
|
RESOLVED_HEART_RATE_OBS_ID=""
|
||||||
|
|
||||||
PASS_COUNT=0
|
PASS_COUNT=0
|
||||||
FAIL_COUNT=0
|
FAIL_COUNT=0
|
||||||
@@ -196,26 +195,22 @@ test_authentication() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
ensure_fhir_clinical_seed() {
|
verification_fixture_ready() {
|
||||||
section "1. Clinical seed data for FHIR endpoints"
|
local heart_rate_count
|
||||||
|
heart_rate_count="$(psql_query "
|
||||||
|
SELECT count(*) FROM clinical.observations o
|
||||||
|
JOIN clinical.patients p ON p.id = o.patient_id
|
||||||
|
WHERE p.mrn = 'VCR-000001' AND o.observation_code = 'HEART_RATE';
|
||||||
|
" 2>/dev/null || echo "0")"
|
||||||
|
[[ "${heart_rate_count:-0}" -ge 1 ]]
|
||||||
|
}
|
||||||
|
|
||||||
if ! psql_available; then
|
seed_verification_fixture() {
|
||||||
log " SKIP: PostgreSQL checks (set VIGILCARE_SKIP_DB_CHECKS=0 and start postgres)"
|
local patient_id encounter_id batch_id heart_rate_count
|
||||||
log " NOTE: FHIR curl tests require clinical.patients with MRN VCR-000001"
|
|
||||||
log " Promote a batch or run with postgres available to auto-seed."
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
local patient_count
|
patient_id="$(psql_query "SELECT id FROM clinical.patients WHERE mrn = 'VCR-000001' LIMIT 1;" 2>/dev/null || true)"
|
||||||
patient_count="$(psql_query "SELECT count(*) FROM clinical.patients WHERE mrn = 'VCR-000001';" || echo "0")"
|
|
||||||
|
|
||||||
if [[ "${patient_count:-0}" -ge 1 ]]; then
|
|
||||||
pass "clinical patient VCR-000001 already present"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
log " Seeding demo clinical records for FHIR verification..."
|
|
||||||
|
|
||||||
|
if [[ -z "$patient_id" ]]; then
|
||||||
psql_exec "
|
psql_exec "
|
||||||
INSERT INTO clinical.patients (
|
INSERT INTO clinical.patients (
|
||||||
id, mrn, full_name, date_of_birth, sex, blood_type,
|
id, mrn, full_name, date_of_birth, sex, blood_type,
|
||||||
@@ -228,48 +223,103 @@ ensure_fhir_clinical_seed() {
|
|||||||
'Yuki Nakamura - 555-0202', NULL, true,
|
'Yuki Nakamura - 555-0202', NULL, true,
|
||||||
NOW() - interval '1 day', NOW() - interval '1 day')
|
NOW() - interval '1 day', NOW() - interval '1 day')
|
||||||
ON CONFLICT (id) DO NOTHING;
|
ON CONFLICT (id) DO NOTHING;
|
||||||
" || {
|
" || return 1
|
||||||
fail "seed clinical.patients for FHIR verification"
|
patient_id="$PATIENT1_ID"
|
||||||
return
|
else
|
||||||
}
|
psql_exec "
|
||||||
|
UPDATE clinical.patients SET
|
||||||
|
full_name = 'MARIA SANTOS',
|
||||||
|
date_of_birth = '1978-03-15',
|
||||||
|
sex = 'female',
|
||||||
|
blood_type = 'A+',
|
||||||
|
emergency_contact = 'Juan Santos - 555-0101',
|
||||||
|
allergies_json = '[\"Penicillin\", \"Sulfa drugs\"]',
|
||||||
|
no_known_allergies = false,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = '${patient_id}';
|
||||||
|
" || return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
encounter_id="$(psql_query "
|
||||||
|
SELECT id FROM clinical.encounters
|
||||||
|
WHERE patient_id = '${patient_id}'
|
||||||
|
ORDER BY created_at
|
||||||
|
LIMIT 1;
|
||||||
|
" 2>/dev/null || true)"
|
||||||
|
|
||||||
|
if [[ -z "$encounter_id" ]]; then
|
||||||
|
batch_id="$(psql_query "SELECT id FROM digitization_batches ORDER BY created_at LIMIT 1;" 2>/dev/null || true)"
|
||||||
|
batch_id="${batch_id:-$BATCH1_ID}"
|
||||||
psql_exec "
|
psql_exec "
|
||||||
INSERT INTO clinical.encounters (
|
INSERT INTO clinical.encounters (
|
||||||
id, patient_id, admission_date, department, room_bed, admission_reason,
|
id, patient_id, admission_date, department, room_bed, admission_reason,
|
||||||
status, source_batch_id, created_at, updated_at
|
status, source_batch_id, created_at, updated_at
|
||||||
) VALUES (
|
) VALUES (
|
||||||
'${ENCOUNTER1_ID}', '${PATIENT1_ID}', NOW() - interval '5 days',
|
'${ENCOUNTER1_ID}', '${patient_id}', NOW() - interval '5 days',
|
||||||
'Internal Medicine', '2A-04', 'Pneumonia with elevated WBC',
|
'Internal Medicine', '2A-04', 'Pneumonia with elevated WBC',
|
||||||
'active', '${BATCH1_ID}', NOW() - interval '3 days', NOW() - interval '12 hours'
|
'active', '${batch_id}', NOW() - interval '3 days', NOW() - interval '12 hours'
|
||||||
)
|
)
|
||||||
ON CONFLICT (id) DO NOTHING;
|
ON CONFLICT (id) DO NOTHING;
|
||||||
" || {
|
" || return 1
|
||||||
fail "seed clinical.encounters for FHIR verification"
|
encounter_id="$ENCOUNTER1_ID"
|
||||||
return
|
fi
|
||||||
}
|
|
||||||
|
|
||||||
|
batch_id="$(psql_query "
|
||||||
|
SELECT source_batch_id FROM clinical.encounters
|
||||||
|
WHERE id = '${encounter_id}'
|
||||||
|
LIMIT 1;
|
||||||
|
" 2>/dev/null || true)"
|
||||||
|
batch_id="${batch_id:-$BATCH1_ID}"
|
||||||
|
|
||||||
|
heart_rate_count="$(psql_query "
|
||||||
|
SELECT count(*) FROM clinical.observations
|
||||||
|
WHERE patient_id = '${patient_id}' AND observation_code = 'HEART_RATE';
|
||||||
|
" 2>/dev/null || echo "0")"
|
||||||
|
|
||||||
|
if [[ "${heart_rate_count:-0}" -lt 1 ]]; then
|
||||||
psql_exec "
|
psql_exec "
|
||||||
INSERT INTO clinical.observations (
|
INSERT INTO clinical.observations (
|
||||||
id, encounter_id, patient_id, observation_code, value, unit,
|
id, encounter_id, patient_id, observation_code, value, unit,
|
||||||
recorded_at, source, source_batch_id, created_at
|
recorded_at, source, source_batch_id, created_at
|
||||||
) VALUES
|
) VALUES
|
||||||
('${HEART_RATE_OBS_ID}', '${ENCOUNTER1_ID}', '${PATIENT1_ID}',
|
('${HEART_RATE_OBS_ID}', '${encounter_id}', '${patient_id}',
|
||||||
'HEART_RATE', 88.000, 'bpm', NOW() - interval '5 days',
|
'HEART_RATE', 88.000, 'bpm', NOW() - interval '5 days',
|
||||||
'digitization_backfill', '${BATCH1_ID}', NOW() - interval '12 hours'),
|
'digitization_backfill', '${batch_id}', NOW() - interval '12 hours'),
|
||||||
('${WBC_OBS_ID}', '${ENCOUNTER1_ID}', '${PATIENT1_ID}',
|
('${WBC_OBS_ID}', '${encounter_id}', '${patient_id}',
|
||||||
'WBC_K_UL', 14.200, 'K/uL', NOW() - interval '5 days',
|
'WBC_K_UL', 14.200, 'K/uL', NOW() - interval '5 days',
|
||||||
'digitization_backfill', '${BATCH1_ID}', NOW() - interval '12 hours')
|
'digitization_backfill', '${batch_id}', NOW() - interval '12 hours')
|
||||||
ON CONFLICT (id) DO NOTHING;
|
ON CONFLICT (id) DO NOTHING;
|
||||||
" || {
|
" || return 1
|
||||||
fail "seed clinical.observations for FHIR verification"
|
fi
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
patient_count="$(psql_query "SELECT count(*) FROM clinical.patients WHERE mrn = 'VCR-000001';" || echo "0")"
|
return 0
|
||||||
if [[ "${patient_count:-0}" -ge 1 ]]; then
|
}
|
||||||
pass "seeded clinical patient VCR-000001 for FHIR verification"
|
|
||||||
|
ensure_fhir_clinical_seed() {
|
||||||
|
section "1. Clinical seed data for FHIR endpoints"
|
||||||
|
|
||||||
|
if ! psql_available; then
|
||||||
|
log " SKIP: PostgreSQL checks (set VIGILCARE_SKIP_DB_CHECKS=0 and start postgres)"
|
||||||
|
log " NOTE: FHIR curl tests require clinical.patients with MRN VCR-000001"
|
||||||
|
log " Promote a batch or run with postgres available to auto-seed."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if verification_fixture_ready; then
|
||||||
|
pass "FHIR verification fixture (VCR-000001 + HEART_RATE) already present"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
log " Ensuring FHIR verification fixture for VCR-000001..."
|
||||||
|
|
||||||
|
if seed_verification_fixture; then
|
||||||
|
if verification_fixture_ready; then
|
||||||
|
pass "FHIR verification fixture ready for VCR-000001"
|
||||||
else
|
else
|
||||||
fail "seeded clinical patient VCR-000001 for FHIR verification"
|
fail "FHIR verification fixture ready for VCR-000001"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
fail "seed FHIR verification fixture for VCR-000001"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,22 +343,68 @@ test_fhir_metadata() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve_patient_id() {
|
resolve_clinical_ids() {
|
||||||
section "3. Resolve FHIR Patient ID"
|
section "3. Resolve FHIR clinical resource IDs"
|
||||||
|
|
||||||
local search_json
|
local search_json encounter_search obs_search
|
||||||
search_json="$(fhir_get "/Patient?name=Santos" "$ADMIN_TOKEN")"
|
|
||||||
RESOLVED_PATIENT_ID="$(jq -er '.entry[0].resource.id // empty' <<<"$search_json" 2>/dev/null || true)"
|
if psql_available; then
|
||||||
|
RESOLVED_PATIENT_ID="$(psql_query "
|
||||||
|
SELECT id FROM clinical.patients WHERE mrn = 'VCR-000001' LIMIT 1;
|
||||||
|
" 2>/dev/null || true)"
|
||||||
|
if [[ -n "$RESOLVED_PATIENT_ID" ]]; then
|
||||||
|
RESOLVED_ENCOUNTER_ID="$(psql_query "
|
||||||
|
SELECT id FROM clinical.encounters
|
||||||
|
WHERE patient_id = '${RESOLVED_PATIENT_ID}'
|
||||||
|
ORDER BY created_at
|
||||||
|
LIMIT 1;
|
||||||
|
" 2>/dev/null || true)"
|
||||||
|
RESOLVED_HEART_RATE_OBS_ID="$(psql_query "
|
||||||
|
SELECT id FROM clinical.observations
|
||||||
|
WHERE patient_id = '${RESOLVED_PATIENT_ID}'
|
||||||
|
AND observation_code = 'HEART_RATE'
|
||||||
|
ORDER BY recorded_at DESC
|
||||||
|
LIMIT 1;
|
||||||
|
" 2>/dev/null || true)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ -z "$RESOLVED_PATIENT_ID" ]]; then
|
if [[ -z "$RESOLVED_PATIENT_ID" ]]; then
|
||||||
RESOLVED_PATIENT_ID="$PATIENT1_ID"
|
search_json="$(fhir_get "/Patient?identifier=VCR-000001" "$ADMIN_TOKEN")"
|
||||||
log " NOTE: Patient search returned no entries; using seeded ID $PATIENT1_ID"
|
RESOLVED_PATIENT_ID="$(jq -er '.entry[0].resource.id // empty' <<<"$search_json" 2>/dev/null || true)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$RESOLVED_PATIENT_ID" ]]; then
|
||||||
|
search_json="$(fhir_get "/Patient?name=Santos" "$ADMIN_TOKEN")"
|
||||||
|
RESOLVED_PATIENT_ID="$(jq -er '.entry[0].resource.id // empty' <<<"$search_json" 2>/dev/null || true)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$RESOLVED_ENCOUNTER_ID" && -n "$RESOLVED_PATIENT_ID" ]]; then
|
||||||
|
encounter_search="$(fhir_get "/Encounter?patient=${RESOLVED_PATIENT_ID}" "$ADMIN_TOKEN")"
|
||||||
|
RESOLVED_ENCOUNTER_ID="$(jq -er '.entry[0].resource.id // empty' <<<"$encounter_search" 2>/dev/null || true)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$RESOLVED_HEART_RATE_OBS_ID" && -n "$RESOLVED_PATIENT_ID" ]]; then
|
||||||
|
obs_search="$(fhir_get "/Observation?patient=${RESOLVED_PATIENT_ID}&code=8867-4" "$ADMIN_TOKEN")"
|
||||||
|
RESOLVED_HEART_RATE_OBS_ID="$(jq -er '.entry[0].resource.id // empty' <<<"$obs_search" 2>/dev/null || true)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -n "$RESOLVED_PATIENT_ID" ]]; then
|
if [[ -n "$RESOLVED_PATIENT_ID" ]]; then
|
||||||
pass "resolved FHIR patient ID ($RESOLVED_PATIENT_ID)"
|
pass "resolved patient ID ($RESOLVED_PATIENT_ID)"
|
||||||
else
|
else
|
||||||
fail "resolved FHIR patient ID"
|
fail "resolved patient ID"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$RESOLVED_ENCOUNTER_ID" ]]; then
|
||||||
|
pass "resolved encounter ID ($RESOLVED_ENCOUNTER_ID)"
|
||||||
|
else
|
||||||
|
fail "resolved encounter ID"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$RESOLVED_HEART_RATE_OBS_ID" ]]; then
|
||||||
|
pass "resolved HEART_RATE observation ID ($RESOLVED_HEART_RATE_OBS_ID)"
|
||||||
|
else
|
||||||
|
fail "resolved HEART_RATE observation ID"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,7 +447,7 @@ test_fhir_encounter() {
|
|||||||
section "5. FHIR Encounter read & search"
|
section "5. FHIR Encounter read & search"
|
||||||
|
|
||||||
local encounter_json status subject patient_search
|
local encounter_json status subject patient_search
|
||||||
encounter_json="$(fhir_get "/Encounter/${ENCOUNTER1_ID}" "$ADMIN_TOKEN")"
|
encounter_json="$(fhir_get "/Encounter/${RESOLVED_ENCOUNTER_ID}" "$ADMIN_TOKEN")"
|
||||||
|
|
||||||
if jq -e '.resourceType == "Encounter"' <<<"$encounter_json" >/dev/null 2>&1; then
|
if jq -e '.resourceType == "Encounter"' <<<"$encounter_json" >/dev/null 2>&1; then
|
||||||
pass "GET /fhir/Encounter/{id} returns Encounter resource"
|
pass "GET /fhir/Encounter/{id} returns Encounter resource"
|
||||||
@@ -378,8 +474,8 @@ test_fhir_encounter() {
|
|||||||
test_fhir_observation() {
|
test_fhir_observation() {
|
||||||
section "6. FHIR Observation read, LOINC search, category & date"
|
section "6. FHIR Observation read, LOINC search, category & date"
|
||||||
|
|
||||||
local obs_json loinc_code loinc_unit loinc_value loinc_search category_search date_search
|
local obs_json loinc_code loinc_unit loinc_search category_search date_search date_filter
|
||||||
obs_json="$(fhir_get "/Observation/${HEART_RATE_OBS_ID}" "$ADMIN_TOKEN")"
|
obs_json="$(fhir_get "/Observation/${RESOLVED_HEART_RATE_OBS_ID}" "$ADMIN_TOKEN")"
|
||||||
|
|
||||||
if jq -e '.resourceType == "Observation"' <<<"$obs_json" >/dev/null 2>&1; then
|
if jq -e '.resourceType == "Observation"' <<<"$obs_json" >/dev/null 2>&1; then
|
||||||
pass "GET /fhir/Observation/{id} returns Observation resource"
|
pass "GET /fhir/Observation/{id} returns Observation resource"
|
||||||
@@ -389,8 +485,8 @@ test_fhir_observation() {
|
|||||||
|
|
||||||
loinc_code="$(jq -er '.code.coding[0].code // empty' <<<"$obs_json" 2>/dev/null || true)"
|
loinc_code="$(jq -er '.code.coding[0].code // empty' <<<"$obs_json" 2>/dev/null || true)"
|
||||||
loinc_unit="$(jq -er '.valueQuantity.unit // empty' <<<"$obs_json" 2>/dev/null || true)"
|
loinc_unit="$(jq -er '.valueQuantity.unit // empty' <<<"$obs_json" 2>/dev/null || true)"
|
||||||
loinc_value="$(jq -er '.valueQuantity.value // empty' <<<"$obs_json" 2>/dev/null || true)"
|
if [[ "$loinc_code" == "8867-4" && "$loinc_unit" == "bpm" ]] \
|
||||||
if [[ "$loinc_code" == "8867-4" && "$loinc_unit" == "bpm" && "$loinc_value" == "88" ]]; then
|
&& jq -e '.valueQuantity.value == 88' <<<"$obs_json" >/dev/null 2>&1; then
|
||||||
pass "Observation read maps HEART_RATE to LOINC 8867-4 with value 88 bpm"
|
pass "Observation read maps HEART_RATE to LOINC 8867-4 with value 88 bpm"
|
||||||
else
|
else
|
||||||
fail "Observation read maps HEART_RATE to LOINC 8867-4 with value 88 bpm"
|
fail "Observation read maps HEART_RATE to LOINC 8867-4 with value 88 bpm"
|
||||||
@@ -410,11 +506,18 @@ test_fhir_observation() {
|
|||||||
fail "GET /fhir/Observation?category=vital-signs returns vital sign observations"
|
fail "GET /fhir/Observation?category=vital-signs returns vital sign observations"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
date_search="$(fhir_get "/Observation?patient=${RESOLVED_PATIENT_ID}&date=ge2026-06-20" "$ADMIN_TOKEN")"
|
date_filter="$(jq -er '.effectiveDateTime // empty' <<<"$obs_json" 2>/dev/null | cut -c1-10 || true)"
|
||||||
if [[ "$(jq -er '.total // 0' <<<"$date_search" 2>/dev/null || echo 0)" -ge 1 ]]; then
|
if [[ -n "$date_filter" ]]; then
|
||||||
pass "GET /fhir/Observation?date=ge2026-06-20 filters by recordedAt"
|
date_filter="$(date -d "${date_filter} - 1 day" +%Y-%m-%d 2>/dev/null || echo "2026-06-20")"
|
||||||
else
|
else
|
||||||
fail "GET /fhir/Observation?date=ge2026-06-20 filters by recordedAt"
|
date_filter="2026-06-20"
|
||||||
|
fi
|
||||||
|
|
||||||
|
date_search="$(fhir_get "/Observation?patient=${RESOLVED_PATIENT_ID}&date=ge${date_filter}" "$ADMIN_TOKEN")"
|
||||||
|
if [[ "$(jq -er '.total // 0' <<<"$date_search" 2>/dev/null || echo 0)" -ge 1 ]]; then
|
||||||
|
pass "GET /fhir/Observation?date=ge${date_filter} filters by recordedAt"
|
||||||
|
else
|
||||||
|
fail "GET /fhir/Observation?date=ge${date_filter} filters by recordedAt"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -510,31 +613,8 @@ test_bundle_pagination() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
test_integration_tests() {
|
|
||||||
section "11. dotnet integration tests — FhirIntegrationTests"
|
|
||||||
|
|
||||||
if [[ "$SKIP_TEST_CHECKS" == "1" ]]; then
|
|
||||||
log " SKIP: dotnet integration tests (VIGILCARE_SKIP_TEST_CHECKS=1)"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! command -v dotnet >/dev/null 2>&1; then
|
|
||||||
log " SKIP: dotnet not installed"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
if dotnet test "$REPO_ROOT/VigilCareRecordsAPI.Tests/VigilCareRecordsAPI.Tests.csproj" \
|
|
||||||
--filter "FullyQualifiedName~FhirIntegrationTests" \
|
|
||||||
--no-restore >/tmp/vigilcare-p11-tests.log 2>&1; then
|
|
||||||
pass "FhirIntegrationTests passed"
|
|
||||||
else
|
|
||||||
fail "FhirIntegrationTests passed"
|
|
||||||
log " see /tmp/vigilcare-p11-tests.log"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
print_manual_ui_checklist() {
|
print_manual_ui_checklist() {
|
||||||
section "12. Manual Vue UI checks (plan §7)"
|
section "11. Manual Vue UI checks (plan §7)"
|
||||||
log " Login as admin1 → http://localhost:3028/fhir-explorer"
|
log " Login as admin1 → http://localhost:3028/fhir-explorer"
|
||||||
log " - Select Patient resource type; search by name Santos"
|
log " - Select Patient resource type; search by name Santos"
|
||||||
log " - Results table shows matching patients"
|
log " - Results table shows matching patients"
|
||||||
@@ -557,7 +637,7 @@ main() {
|
|||||||
test_authentication
|
test_authentication
|
||||||
ensure_fhir_clinical_seed
|
ensure_fhir_clinical_seed
|
||||||
test_fhir_metadata
|
test_fhir_metadata
|
||||||
resolve_patient_id
|
resolve_clinical_ids
|
||||||
test_fhir_patient_read
|
test_fhir_patient_read
|
||||||
test_fhir_encounter
|
test_fhir_encounter
|
||||||
test_fhir_observation
|
test_fhir_observation
|
||||||
@@ -565,7 +645,6 @@ main() {
|
|||||||
test_fhir_content_type
|
test_fhir_content_type
|
||||||
test_fhir_error_handling
|
test_fhir_error_handling
|
||||||
test_bundle_pagination
|
test_bundle_pagination
|
||||||
test_integration_tests
|
|
||||||
print_manual_ui_checklist
|
print_manual_ui_checklist
|
||||||
|
|
||||||
log ""
|
log ""
|
||||||
|
|||||||
Reference in New Issue
Block a user