From 4c28bffcc959b1bd725613f2976742105676a883 Mon Sep 17 00:00:00 2001 From: voltsrage Date: Sat, 27 Jun 2026 22:35:47 +0800 Subject: [PATCH] chore: update readme --- README.md | 65 +++- ...vigilcare-records-phase-11-verification.sh | 293 +++++++++++------- 2 files changed, 237 insertions(+), 121 deletions(-) diff --git a/README.md b/README.md index 39dfeea..5f3e702 100644 --- a/README.md +++ b/README.md @@ -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 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 @@ -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 - **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, 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` - **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`) @@ -102,6 +104,7 @@ HTTP request ├── LiveCaptureService (Track B synchronous promotion + critical alert evaluation) ├── CoverSheetService (generate, lookup, redeem, list cover sheets) ├── CoverSheetPdfGenerator (printable PDF with QR codes) + ├── FhirService (FHIR R4 read/search/$everything over promoted clinical tables) ├── DocumentStorageService (MinIO upload, SHA-256, presigned URLs) ├── PlausibilityValidator (per-code numeric range guard) ├── PostgreSQL (EF Core — writes, keyed reads, clinical schema for live tables) @@ -147,6 +150,7 @@ HTTP request | Metrics | Prometheus (`prometheus-net`) + Grafana | | Docs | Swagger / OpenAPI (Swashbuckle) | | 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 | --- @@ -162,6 +166,7 @@ VigilCareRecords/ │ │ ├── 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 +│ │ ├── Fhir/ # FHIR R4 read/search controllers (Patient, Encounter, Observation, metadata) │ │ ├── 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 @@ -171,22 +176,24 @@ VigilCareRecords/ │ │ └── WorkQueueController.cs # Work queues and supervisor overview │ ├── HealthChecks/MinioHealthCheck.cs # MinIO bucket-exists readiness probe │ ├── 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 │ ├── Data/ … # EF Core context, configurations, migrations, seed │ └── … # Middleware, Common, Infrastructure ├── vigilcare-records-web/ # Vue 3 digitization workstation UI │ ├── src/ │ │ ├── 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 │ │ ├── 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 │ │ ├── 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 +│ ├── vite.config.ts # Dev server on port 3028; proxies /api and /fhir → localhost:5217 │ └── 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) ├── docker-compose.yml # PostgreSQL, Redis, Seq, MinIO, Prometheus, Grafana ├── scripts/ @@ -198,10 +205,10 @@ VigilCareRecords/ │ ├── 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-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-10-verification.sh # Cover sheets, barcode upload, PDF, auto-assign +│ └── run-vigilcare-records-phase-11-verification.sh # FHIR metadata, read/search, $everything, LOINC mapping └── docs/ - ├── plans/ # Phase 1–11 implementation guides + ├── plans/ # Phase 1–13 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 @@ -366,7 +373,7 @@ npm install 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 | |---|---|---| @@ -375,7 +382,7 @@ Open `http://localhost:3028`. The Vite dev server proxies `/api` requests to the | `verifier1` | `password` | `/verification` — field-level verification | | `approver1` | `password` | `/approval` — clinical sign-off before promotion | | `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. @@ -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 | | `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 | +| `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 | ### 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-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 +./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 @@ -861,6 +871,32 @@ Unauthenticated probe endpoints for orchestrators and load balancers: 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 @@ -1203,7 +1239,7 @@ Response shape: ## 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 | |---|---|---| @@ -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 | | 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 | +| 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 | -| 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 | diff --git a/scripts/run-vigilcare-records-phase-11-verification.sh b/scripts/run-vigilcare-records-phase-11-verification.sh index 5672bb5..1d546e4 100755 --- a/scripts/run-vigilcare-records-phase-11-verification.sh +++ b/scripts/run-vigilcare-records-phase-11-verification.sh @@ -2,8 +2,7 @@ # Runs Phase 11 verification checks from docs/plans/phase-11-plan.md. # # Covers FHIR metadata, Patient/Encounter/Observation read & search, -# LOINC mapping, $everything, content-type negotiation, 404 OperationOutcome, -# and FhirIntegrationTests. +# LOINC mapping, $everything, content-type negotiation, and 404 OperationOutcome. # # Prerequisites: # docker compose up -d (PostgreSQL, Redis, MinIO) @@ -20,7 +19,6 @@ # VIGILCARE_PG_USER default: postgres # VIGILCARE_PG_PASSWORD default: password # 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: # 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_PASSWORD="${VIGILCARE_PG_PASSWORD:-password}" SKIP_DB_CHECKS="${VIGILCARE_SKIP_DB_CHECKS:-0}" -SKIP_TEST_CHECKS="${VIGILCARE_SKIP_TEST_CHECKS:-0}" # Deterministic clinical IDs (match FhirClinicalSeedHelper) PATIENT1_ID="b1000000-0000-0000-0000-000000000001" @@ -53,6 +50,8 @@ BATCH1_ID="c1000000-0000-0000-0000-000000000001" ADMIN_TOKEN="" RESOLVED_PATIENT_ID="" +RESOLVED_ENCOUNTER_ID="" +RESOLVED_HEART_RATE_OBS_ID="" PASS_COUNT=0 FAIL_COUNT=0 @@ -196,6 +195,106 @@ test_authentication() { fi } +verification_fixture_ready() { + 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 ]] +} + +seed_verification_fixture() { + local patient_id encounter_id batch_id heart_rate_count + + patient_id="$(psql_query "SELECT id FROM clinical.patients WHERE mrn = 'VCR-000001' LIMIT 1;" 2>/dev/null || true)" + + if [[ -z "$patient_id" ]]; then + psql_exec " + INSERT INTO clinical.patients ( + id, mrn, full_name, date_of_birth, sex, blood_type, + emergency_contact, allergies_json, no_known_allergies, created_at, updated_at + ) VALUES + ('${PATIENT1_ID}', 'VCR-000001', 'MARIA SANTOS', '1978-03-15', 'female', 'A+', + 'Juan Santos - 555-0101', '[\"Penicillin\", \"Sulfa drugs\"]', false, + NOW() - interval '3 days', NOW() - interval '12 hours'), + ('${PATIENT2_ID}', 'VCR-000002', 'KENJI NAKAMURA', '1952-11-08', 'male', 'O-', + 'Yuki Nakamura - 555-0202', NULL, true, + NOW() - interval '1 day', NOW() - interval '1 day') + ON CONFLICT (id) DO NOTHING; + " || return 1 + patient_id="$PATIENT1_ID" + 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 " + INSERT INTO clinical.encounters ( + id, patient_id, admission_date, department, room_bed, admission_reason, + status, source_batch_id, created_at, updated_at + ) VALUES ( + '${ENCOUNTER1_ID}', '${patient_id}', NOW() - interval '5 days', + 'Internal Medicine', '2A-04', 'Pneumonia with elevated WBC', + 'active', '${batch_id}', NOW() - interval '3 days', NOW() - interval '12 hours' + ) + ON CONFLICT (id) DO NOTHING; + " || return 1 + encounter_id="$ENCOUNTER1_ID" + 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 " + INSERT INTO clinical.observations ( + id, encounter_id, patient_id, observation_code, value, unit, + recorded_at, source, source_batch_id, created_at + ) VALUES + ('${HEART_RATE_OBS_ID}', '${encounter_id}', '${patient_id}', + 'HEART_RATE', 88.000, 'bpm', NOW() - interval '5 days', + 'digitization_backfill', '${batch_id}', NOW() - interval '12 hours'), + ('${WBC_OBS_ID}', '${encounter_id}', '${patient_id}', + 'WBC_K_UL', 14.200, 'K/uL', NOW() - interval '5 days', + 'digitization_backfill', '${batch_id}', NOW() - interval '12 hours') + ON CONFLICT (id) DO NOTHING; + " || return 1 + fi + + return 0 +} + ensure_fhir_clinical_seed() { section "1. Clinical seed data for FHIR endpoints" @@ -206,70 +305,21 @@ ensure_fhir_clinical_seed() { return fi - local patient_count - 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" + if verification_fixture_ready; then + pass "FHIR verification fixture (VCR-000001 + HEART_RATE) already present" return fi - log " Seeding demo clinical records for FHIR verification..." + log " Ensuring FHIR verification fixture for VCR-000001..." - psql_exec " - INSERT INTO clinical.patients ( - id, mrn, full_name, date_of_birth, sex, blood_type, - emergency_contact, allergies_json, no_known_allergies, created_at, updated_at - ) VALUES - ('${PATIENT1_ID}', 'VCR-000001', 'MARIA SANTOS', '1978-03-15', 'female', 'A+', - 'Juan Santos - 555-0101', '[\"Penicillin\", \"Sulfa drugs\"]', false, - NOW() - interval '3 days', NOW() - interval '12 hours'), - ('${PATIENT2_ID}', 'VCR-000002', 'KENJI NAKAMURA', '1952-11-08', 'male', 'O-', - 'Yuki Nakamura - 555-0202', NULL, true, - NOW() - interval '1 day', NOW() - interval '1 day') - ON CONFLICT (id) DO NOTHING; - " || { - fail "seed clinical.patients for FHIR verification" - return - } - - psql_exec " - INSERT INTO clinical.encounters ( - id, patient_id, admission_date, department, room_bed, admission_reason, - status, source_batch_id, created_at, updated_at - ) VALUES ( - '${ENCOUNTER1_ID}', '${PATIENT1_ID}', NOW() - interval '5 days', - 'Internal Medicine', '2A-04', 'Pneumonia with elevated WBC', - 'active', '${BATCH1_ID}', NOW() - interval '3 days', NOW() - interval '12 hours' - ) - ON CONFLICT (id) DO NOTHING; - " || { - fail "seed clinical.encounters for FHIR verification" - return - } - - psql_exec " - INSERT INTO clinical.observations ( - id, encounter_id, patient_id, observation_code, value, unit, - recorded_at, source, source_batch_id, created_at - ) VALUES - ('${HEART_RATE_OBS_ID}', '${ENCOUNTER1_ID}', '${PATIENT1_ID}', - 'HEART_RATE', 88.000, 'bpm', NOW() - interval '5 days', - 'digitization_backfill', '${BATCH1_ID}', NOW() - interval '12 hours'), - ('${WBC_OBS_ID}', '${ENCOUNTER1_ID}', '${PATIENT1_ID}', - 'WBC_K_UL', 14.200, 'K/uL', NOW() - interval '5 days', - 'digitization_backfill', '${BATCH1_ID}', NOW() - interval '12 hours') - ON CONFLICT (id) DO NOTHING; - " || { - fail "seed clinical.observations for FHIR verification" - return - } - - patient_count="$(psql_query "SELECT count(*) FROM clinical.patients WHERE mrn = 'VCR-000001';" || echo "0")" - if [[ "${patient_count:-0}" -ge 1 ]]; then - pass "seeded clinical patient VCR-000001 for FHIR verification" + if seed_verification_fixture; then + if verification_fixture_ready; then + pass "FHIR verification fixture ready for VCR-000001" + else + fail "FHIR verification fixture ready for VCR-000001" + fi else - fail "seeded clinical patient VCR-000001 for FHIR verification" + fail "seed FHIR verification fixture for VCR-000001" fi } @@ -293,22 +343,68 @@ test_fhir_metadata() { fi } -resolve_patient_id() { - section "3. Resolve FHIR Patient ID" +resolve_clinical_ids() { + section "3. Resolve FHIR clinical resource IDs" - local search_json - 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)" + local search_json encounter_search obs_search + + 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 - RESOLVED_PATIENT_ID="$PATIENT1_ID" - log " NOTE: Patient search returned no entries; using seeded ID $PATIENT1_ID" + search_json="$(fhir_get "/Patient?identifier=VCR-000001" "$ADMIN_TOKEN")" + 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 if [[ -n "$RESOLVED_PATIENT_ID" ]]; then - pass "resolved FHIR patient ID ($RESOLVED_PATIENT_ID)" + pass "resolved patient ID ($RESOLVED_PATIENT_ID)" 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 } @@ -351,7 +447,7 @@ test_fhir_encounter() { section "5. FHIR Encounter read & 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 pass "GET /fhir/Encounter/{id} returns Encounter resource" @@ -378,8 +474,8 @@ test_fhir_encounter() { test_fhir_observation() { section "6. FHIR Observation read, LOINC search, category & date" - local obs_json loinc_code loinc_unit loinc_value loinc_search category_search date_search - obs_json="$(fhir_get "/Observation/${HEART_RATE_OBS_ID}" "$ADMIN_TOKEN")" + local obs_json loinc_code loinc_unit loinc_search category_search date_search date_filter + obs_json="$(fhir_get "/Observation/${RESOLVED_HEART_RATE_OBS_ID}" "$ADMIN_TOKEN")" if jq -e '.resourceType == "Observation"' <<<"$obs_json" >/dev/null 2>&1; then 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_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" && "$loinc_value" == "88" ]]; then + if [[ "$loinc_code" == "8867-4" && "$loinc_unit" == "bpm" ]] \ + && 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" else 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" fi - date_search="$(fhir_get "/Observation?patient=${RESOLVED_PATIENT_ID}&date=ge2026-06-20" "$ADMIN_TOKEN")" - if [[ "$(jq -er '.total // 0' <<<"$date_search" 2>/dev/null || echo 0)" -ge 1 ]]; then - pass "GET /fhir/Observation?date=ge2026-06-20 filters by recordedAt" + date_filter="$(jq -er '.effectiveDateTime // empty' <<<"$obs_json" 2>/dev/null | cut -c1-10 || true)" + if [[ -n "$date_filter" ]]; then + date_filter="$(date -d "${date_filter} - 1 day" +%Y-%m-%d 2>/dev/null || echo "2026-06-20")" 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 } @@ -510,31 +613,8 @@ test_bundle_pagination() { 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() { - 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 " - Select Patient resource type; search by name Santos" log " - Results table shows matching patients" @@ -557,7 +637,7 @@ main() { test_authentication ensure_fhir_clinical_seed test_fhir_metadata - resolve_patient_id + resolve_clinical_ids test_fhir_patient_read test_fhir_encounter test_fhir_observation @@ -565,7 +645,6 @@ main() { test_fhir_content_type test_fhir_error_handling test_bundle_pagination - test_integration_tests print_manual_ui_checklist log ""