VigilCare Records API

A clinical records intake backend built with ASP.NET Core 8, PostgreSQL, MinIO, and Redis. 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: Five planned phases are complete through Phase 5 — from schema, authentication, and batch CRUD through draft data entry, verification/rejection with separation of duties, approval with atomic promotion to VigilCareClinical live tables, and correction batches that supersede erroneous promoted observations without silent edits. See Implemented Phases for the full breakdown.

Domain Model — How It Maps to a Real Clinical System

In a paper-based hospital, patient records exist as handwritten charts, ward books, and index cards. VigilCare Records converts these into structured digital data through a governed workflow: an intake clerk scans a paper chart and uploads it to MinIO. A data entry clerk reads the scan and transcribes patient demographics, encounter context, and observation values into structured draft fields. A verifier (who cannot be the entry clerk) compares the draft against the original scan and approves or rejects field-by-field. For high-stakes batch types (vitals, labs, encounter summaries, medications), a clinical approver provides a final sign-off before promotion. On approval, draft records are promoted atomically to VigilCareClinical's live tables — Patient, Encounter, Observation — where they enter the real-time alerting and scoring pipeline. Unapproved drafts never trigger alerts, scoring, or surveillance.

DigitizationBatch ────────────── one unit of work: one scan, one chart section
 ├── ScannedDocument              MinIO object — PDF/JPEG/PNG, SHA-256, never deleted
 ├── DraftPatient                 demographics, allergies, blood type, medications (structured from paper)
 ├── DraftEncounter               admission date, department, room/bed, admission reason
 ├── DraftObservation[]           one measurement per row: observation code, value, unit, recordedAt
 └── DigitizationEvent[]          append-only audit log: every status transition, every actor, every timestamp

DigitizationBatch

The unit of work for one digitization effort — typically one scanned document or one logical chart section (vitals sheet, lab report, admission face sheet). Tracks the full lifecycle from upload through promotion with actor attribution at every gate. Seven batch types (PATIENT_REGISTRATION, ENCOUNTER_SUMMARY, VITALS_SHEET, LAB_RESULTS, MEDICATION_LIST, ALLERGY_UPDATE, MIXED) drive completeness validation rules on submit. Two tracks: BACKFILL (full dual-human gate for historical charts) and LIVE_CAPTURE (clinician attestation at bedside, lighter gate).

DraftPatient

Structured patient fields extracted from paper — full name, date of birth, sex, blood type, allergies (JSON), emergency contact, medications (JSON for MEDICATION_LIST batches). On approval of a PATIENT_REGISTRATION or ALLERGY_UPDATE batch, merges into VigilCareClinical's live Patient record.

DraftEncounter

A clinical episode extracted from the chart: admission date, department, room/bed, admission reason, discharge diagnosis. Promotes to VigilCareClinical's live Encounter.

DraftObservation

A single measurable value: observation code, numeric value, unit, recordedAt (from the chart, required), optional note. Subject to the same plausibility ranges as VigilCareClinical ingest. Never written to live observations until batch approval.

ScannedDocument

Stored in MinIO. Original paper is the legal source; the scan is the working reference for entry and verification. Scanned documents are never deleted when a batch is rejected — only the draft is returned for correction.

DigitizationEvent

Append-only audit log entry for every state transition, field-level correction, and workflow action. Each event records the batch, event type, actor user ID, timestamp, and optional JSON metadata (field checks, rejection reasons, assigned-to user).


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
  • Batch AssignmentPATCH /digitization-batches/:id/assign assigns an entry clerk with a Redis lock (SET batch:assign:{id} NX EX 3600) to prevent double-assignment; 409 BATCH_ALREADY_ASSIGNED on conflict
  • 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); automatic UPLOADED → IN_ENTRY transition on first save; draft save requires the acting user to match enteredByUserId or hold Administrator role (409 BATCH_NOT_ASSIGNED)
  • Submit for Verification — validates completeness per batch type (e.g. vitals sheet requires linked patient, encounter context, and at least one observation with recordedAt); transitions IN_ENTRY → PENDING_VERIFICATION; returns 422 with missing fields if incomplete
  • Verification and Rejection — verifier reviews entry against the scan with field-level checks (fieldName, status: ok|warning|error, optional note); verify pass transitions to VERIFIED or AWAITING_CLINICAL_APPROVAL based on site configuration for the batch type; verify fail transitions to REJECTED with mandatory reason; separation of duties enforced: entry clerk cannot verify their own batch (409 SEPARATION_OF_DUTIES_VIOLATION)
  • Clinical Approval Routing — site-configurable per batch type (SiteConfig.ClinicalApprovalRequired); high-stakes batch types (encounter summary, vitals sheet, lab results, medication list, mixed) route to AWAITING_CLINICAL_APPROVAL after verification for physician sign-off; low-stakes types (patient registration, allergy update) go directly to VERIFIED
  • Approval and PromotionPOST /digitization-batches/:id/approve atomically promotes draft data to live VigilCareClinical tables (patients, encounters, observations) in a single PostgreSQL transaction; generates MRN via PostgreSQL sequence (VCR-000001); patient deduplication by name + DOB; encounter matching by patient + department + active status; writes observation.created outbox events for downstream consumers; separation of duties enforced (approver cannot be entry clerk or verifier); Idempotency-Key header required for safe retries with 24-hour TTL; retroactive alert policy (enableRetroactiveAlerts) controls whether backfill observations emit outbox events
  • Promotion Result QueryGET /digitization-batches/:id/promotion-result returns live entity IDs (patient, MRN, encounter, observations) created during promotion
  • Corrections and Supersession — approved live observations are never silently edited; a correction uploads a new batch with supersedesBatchId, goes through the full entry → verify → approve cycle, and on promotion marks the original batch's live_observations rows as is_superseded (append-only — never deleted); 422 SUPERSEDED_BATCH_NOT_PROMOTED, 409 BATCH_ALREADY_SUPERSEDED, and 404 SUPERSEDED_BATCH_NOT_FOUND guard invalid supersession targets; correction batches inherit patient context and reuse the original promotion encounter
  • Patient Digitization HistoryGET /patients/:id/digitization-history returns all batches for a patient with correction chain metadata (isCorrection, hasBeenSuperseded, supersededByBatchId), live vs superseded observation counts, summary totals, and per-batch audit trails; 404 PATIENT_HISTORY_NOT_FOUND when no batches exist for the patient
  • Work QueuesGET /work-queue/verification (batches in PENDING_VERIFICATION, sorted by submission time ASC); GET /work-queue/entry (batches in UPLOADED, IN_ENTRY, or REJECTED); GET /work-queue/clinical-approval (batches in AWAITING_CLINICAL_APPROVAL); role-restricted access
  • Batch Status Machine — explicit transition matrix enforced in the service layer; illegal transitions return 409; PROMOTED is terminal — corrections require a new batch with supersedesBatchId
  • JWT AuthenticationPOST /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
  • Role-Based Access — six roles (INTAKE_CLERK, DATA_ENTRY_CLERK, VERIFIER, CLINICAL_APPROVER, CLINICIAN, ADMINISTRATOR) with role-based endpoint authorization; twelve seeded demo users (two per role)
  • Auth Audit Events — append-only auth_audit_events table records login, logout, token refresh, and failed login attempts with user ID, IP address, and timestamp
  • Standard Envelope — all responses use { success, statusCode, data, error } wrapper; validation errors use the same shape with stable error codes
  • Observability — Serilog structured logging with Seq sink; correlation IDs via CorrelationIdMiddleware; ExceptionHandlerMiddleware for consistent error responses
  • Swagger UI — OpenAPI spec via Swashbuckle (Development only) at http://localhost:5217/swagger

Architecture

HTTP request
  → CorrelationIdMiddleware
  → ExceptionHandlerMiddleware
  → JWT Authentication (Bearer)
  → Role-based Authorization ([Authorize(Roles = "...")])
  → Controllers (REST API)
  → Services
      ├── AuthService (login, refresh token rotation, logout, BCrypt verify)
      ├── BatchService (batch CRUD, status machine, Redis assignment lock, duplicate detection)
      ├── DraftService (draft patient/encounter/observation CRUD, plausibility validation, submit-for-verification)
      ├── VerificationService (verify/reject with separation of duties, site-config approval routing)
      ├── PromotionService (approve + atomic promote to live tables, patient dedup, encounter matching, outbox events, supersession on correction promotion)
      ├── DigitizationHistoryService (patient digitization history with correction chain and audit trails)
      ├── IdempotencyService (Idempotency-Key storage + replay for safe promotion retries)
      ├── MrnGenerator (PostgreSQL sequence-backed MRN generation: VCR-000001)
      ├── WorkQueueService (verification, entry, clinical approval queues)
      ├── DocumentStorageService (MinIO upload, SHA-256, presigned URLs)
      ├── PlausibilityValidator (per-code numeric range guard)
      ├── PostgreSQL (EF Core — writes, keyed reads, clinical schema for live tables)
      ├── Redis (batch assignment locks)
      └── MinIO (scanned document storage)

Relationship to VigilCareClinical: VigilCare Records is the precursor that creates the fuel — patient identity, encounters, and observations — in a governed, auditable way. Draft observations are invisible to VigilCareClinical's alert engine, Elasticsearch ward projection, and scoring consumers. Only batches that pass approval are promoted to live tables. Promotion writes directly to VigilCareClinical's patients, encounters, observations, and outbox_events tables in a single atomic transaction with idempotency protection.

┌─────────────────────────────────────────────────────────────────────┐
│  VigilCare Records (this project)                                  │
│                                                                     │
│  Scan → Entry → Verify → Approve → Promote                          │
│       ↓                                                             │
│  Draft tables (never alert)                                         │
│       ↓ on approval                                                 │
│  PromotionService (atomic txn + idempotency) ────────────────────┐  │
└──────────────────────────────────────────────────────────────────│──┘
                                                                   │
┌──────────────────────────────────────────────────────────────────▼──┐
│  VigilCareClinicalAPI                                              │
│                                                                     │
│  Patient → Encounter → Observation → Outbox → Kafka → Alerts       │
└─────────────────────────────────────────────────────────────────────┘

Tech Stack

Layer Technology
Server ASP.NET Core 8 (.NET 8.0)
Database PostgreSQL 16 with EF Core 8 (code-first migrations)
Cache / locking Redis 7 (batch assignment locks)
Object storage MinIO (scanned documents — PDF, JPEG, PNG)
Authentication JWT Bearer (Microsoft.AspNetCore.Authentication.JwtBearer)
Password hashing BCrypt.Net-Next
Logging Serilog + Seq sink
Docs Swagger / OpenAPI (Swashbuckle)
Testing xUnit + FluentAssertions + WebApplicationFactory

Project Structure

VigilCareRecordsAPI/
├── Program.cs                                  # Service registration, middleware, seed on startup
├── appsettings.json                            # Connection strings, Redis, MinIO, Seq, JWT, site config
├── Controllers/
│   ├── ApprovalController.cs                   # Batch approval and promotion to live clinical tables
│   ├── AuthController.cs                       # JWT login, token refresh, logout, authenticated user profile
│   ├── DigitizationBatchesController.cs        # Batch CRUD, document upload, assignment
│   ├── DraftController.cs                      # Draft data entry: patient, encounter, observations, submit
│   ├── VerificationController.cs               # Batch verification and rejection with separation of duties
│   └── WorkQueueController.cs                  # Work queues: verification, entry, clinical approval
├── Domain/
│   ├── Entities/
│   │   ├── Clinical/
│   │   │   ├── Patient.cs                      # Live patient record with MRN (promoted from draft)
│   │   │   ├── Encounter.cs                    # Live encounter (promoted from draft)
│   │   │   ├── Observation.cs                  # Live observation with source traceability (batchId, draftObsId)
│   │   │   ├── OutboxEvent.cs                  # Transactional outbox for downstream consumers (Kafka)
│   │   │   └── IdempotencyRecord.cs            # Idempotency-Key storage for safe promotion retries
│   │   ├── Draft/
│   │   │   ├── DraftPatient.cs                 # Structured patient demographics from paper chart
│   │   │   ├── DraftEncounter.cs               # Encounter context: admission, department, room/bed
│   │   │   └── DraftObservation.cs             # Single measurement: code, value, unit, recordedAt
│   │   ├── DigitizationBatch.cs                # Central workflow entity with status machine
│   │   ├── ScannedDocument.cs                  # MinIO object reference with SHA-256 integrity hash
│   │   ├── DigitizationEvent.cs                # Append-only audit trail for every status transition
│   │   ├── User.cs                             # Username, BCrypt hash, full name, role, active flag
│   │   ├── RefreshToken.cs                     # Opaque refresh token with user FK, expiry, revocation
│   │   └── AuthAuditEvent.cs                   # Auth event audit: login, logout, refresh, failed attempts
│   └── Enums/
│       ├── BatchStatus.cs                      # Uploaded → InEntry → PendingVerification → Verified/AwaitingClinicalApproval → Approved → Promoted
│       ├── BatchType.cs                        # PatientRegistration, VitalsSheet, LabResults, EncounterSummary, MedicationList, AllergyUpdate, Mixed
│       ├── BatchTrack.cs                       # Backfill (Track A) or LiveCapture (Track B)
│       ├── UserRole.cs                         # IntakeClerk, DataEntryClerk, Verifier, ClinicalApprover, Clinician, Administrator
│       ├── DigitizationEventType.cs            # 18 event types covering full lifecycle + corrections + promotion retry
│       ├── BloodType.cs                        # A+, A-, B+, B-, AB+, AB-, O+, O-
│       └── Department.cs                       # Clinical departments
├── Services/
│   ├── Interfaces/                             # IAuthService, IBatchService, IDraftService, IVerificationService, IWorkQueueService, IDocumentStorageService, IPromotionService, IIdempotencyService, IMrnGenerator
│   ├── AuthService.cs                          # Login (BCrypt verify), JWT generation, refresh rotation, logout, audit logging
│   ├── BatchService.cs                         # Batch CRUD, status machine transitions, Redis assignment lock, duplicate detection
│   ├── DraftService.cs                         # Draft CRUD with plausibility validation, batch-type completeness on submit
│   ├── VerificationService.cs                  # Verify/reject with separation of duties, site-config clinical approval routing
│   ├── PromotionService.cs                     # Atomic approve + promote: draft → live tables in single transaction
│   ├── IdempotencyService.cs                   # Idempotency-Key record storage + replay (24h TTL)
│   ├── MrnGenerator.cs                         # PostgreSQL sequence-backed MRN generation (VCR-000001)
│   ├── WorkQueueService.cs                     # Verification, entry, and clinical approval work queues
│   ├── DocumentStorageService.cs               # MinIO upload with SHA-256, presigned URL generation
│   └── PlausibilityValidator.cs                # Per-code numeric range guard (reused from VigilCareClinical)
├── Configurations/
│   ├── JwtOptions.cs                           # Issuer, audience, signing key, access/refresh token expiration
│   ├── MinioOptions.cs                         # Endpoint, credentials, bucket, presigned URL expiry
│   └── SiteConfigOptions.cs                    # ClinicalApprovalRequired map per batch type
├── Models/Records/
│   ├── Auth/                                   # LoginRequest, LoginResponse, RefreshRequest, LogoutRequest, TokenResponse, UserProfileResponse
│   ├── Batch/                                  # CreateBatchForm, CreateBatchRequest, AssignBatchRequest, BatchDetailResponse, BatchListResponse, DraftPayloadResponse, VerifyBatchRequest, RejectBatchRequest, FieldCheck
│   ├── Promotion/                              # ApproveRequest, PromotionResultResponse
│   ├── DraftPatient/                           # DraftPatientDto, UpsertDraftPatientRequest
│   ├── DraftEncounter/                         # DraftEncounterDto, UpsertDraftEncounterRequest
│   ├── DraftObservation/                       # DraftObservationDto, CreateDraftObservationRequest, UpdateDraftObservationRequest
│   ├── WorkQueue/                              # WorkQueueResponse, WorkQueueItemResponse
│   └── Common/                                 # PagedResult
├── Data/
│   ├── AppDbContext.cs                         # EF Core context — entity configs, indexes, constraints
│   ├── Configurations/
│   │   ├── Clinical/                           # PatientConfiguration, EncounterConfiguration, ObservationConfiguration, OutboxEventConfiguration, IdempotencyRecordConfiguration
│   │   ├── Draft/                              # DraftPatientConfiguration, DraftEncounterConfiguration, DraftObservationConfiguration
│   │   └── ...                                 # DigitizationBatch, DigitizationEvent, ScannedDocument, User, RefreshToken, AuthAuditEvent configs
│   ├── Seed/
│   │   └── DataSeeder.cs                       # Seeds 12 demo users (two per role) on startup
│   └── Migrations/                             # InitialCreate, InitialSchema, AddRefreshTokensAndAuthAudit, AddIdempotencyRecords, AddClinicalSchemaAndPromotion, AddMrnSequence
├── Common/
│   ├── ApiResponse.cs                          # { success, statusCode, data, error } envelope
│   └── Exceptions/
│       ├── NotFoundException.cs
│       ├── ConflictException.cs                # Status machine, separation of duties, assignment conflicts
│       ├── BadRequestException.cs
│       ├── DomainException.cs
│       └── ValidationException.cs
├── Middleware/
│   ├── CorrelationIdMiddleware.cs              # Per-request correlation IDs
│   └── ExceptionHandlerMiddleware.cs           # Consistent error responses
└── Infrastructure/
    └── OpenApi/
        └── SwaggerServiceCollectionExtensions.cs

tests/
└── VigilCareRecordsAPI.Tests/
    ├── DraftEntryTests.cs                      # Draft CRUD, plausibility validation, submit-for-verification completeness
    ├── VerificationTests.cs                    # Verification, rejection, separation of duties, clinical approval routing
    ├── PromotionTests.cs                       # Approval, atomic promotion, idempotency, separation of duties, retroactive alerts, patient dedup
    ├── Fixtures/
    │   ├── ApiFixture.cs                       # WebApplicationFactory with PostgreSQL, Redis, MinIO containers
    │   └── DatabaseCollection.cs               # Shared test collection
    └── Helpers/
        ├── AuthHelper.cs                       # JWT token generation for test users
        ├── BatchSeedHelper.cs                  # Creates seeded batches at various lifecycle stages
        ├── BatchPipelineHelper.cs              # End-to-end batch pipeline: upload → entry → verify → ready for approval
        └── DbResetHelper.cs                    # Database cleanup between tests

scripts/
├── run-vigilcare-records-verification.sh       # Phase 1 — schema, auth, roles, batch CRUD, MinIO, status machine
├── run-vigilcare-records-phase-2-verification.sh  # Phase 2 — draft entry, plausibility, submit-for-verification
├── run-vigilcare-records-phase-3-verification.sh  # Phase 3 — verification, rejection, separation of duties, work queues
└── run-vigilcare-records-phase-4-verification.sh  # Phase 4 — approval, atomic promotion, idempotency, patient dedup, outbox events

docs/
├── plans/                                      # Phase 19 implementation and verification guides
└── vigilcare-records-prd.md                    # Product requirements and phase roadmap

Batch Status State Machine

                    ┌──────────────┐
                    │   UPLOADED   │
                    └──────┬───────┘
                           │ assign / first save
                           ▼
                    ┌──────────────┐
         ┌──────────│   IN_ENTRY   │◄─────────┐
         │          └──────┬───────┘          │
         │                 │ submit           │ reject
         │                 ▼                  │
         │          ┌──────────────┐         │
         │          │   PENDING    │─────────┘
         │          │ VERIFICATION │
         │          └──────┬───────┘
         │                 │
         │    verify fail  │ verify pass
         │        ─────────┤
         │                 │
         │   site config   │ site config
         │   = false       │ = true
         │        ┌────────┴────────┐
         │        ▼                 ▼
         │  ┌──────────────┐  ┌──────────────────────┐
         │  │   VERIFIED   │  │ AWAITING_CLINICAL    │
         │  └──────┬───────┘  │     _APPROVAL          │
         │         │          └──────────┬─────────────┘
         │         └──────────┬──────────┘
         │                    │ approve
         │                    ▼
         │             ┌──────────────┐
         └────────────►│   APPROVED   │
                       └──────┬───────┘
                              │ promotion (Phase 4)
                              ▼
                       ┌──────────────┐
                       │  PROMOTED    │  (terminal — live records exist)
                       └──────────────┘

Allowed transitions:

From To
UPLOADED IN_ENTRY
IN_ENTRY PENDING_VERIFICATION
PENDING_VERIFICATION VERIFIED, AWAITING_CLINICAL_APPROVAL, REJECTED
REJECTED IN_ENTRY
VERIFIED APPROVED
AWAITING_CLINICAL_APPROVAL APPROVED, REJECTED
APPROVED PROMOTED
PROMOTED (none — terminal)

Illegal transitions return 409 with a stable error code. A PROMOTED batch cannot return to any earlier state. Corrections require a new batch referencing supersedesBatchId.

Site-configurable clinical approval routing:

batchType Clinical sign-off after verify?
PATIENT_REGISTRATION No
ALLERGY_UPDATE No
ENCOUNTER_SUMMARY Yes
VITALS_SHEET Yes
LAB_RESULTS Yes
MEDICATION_LIST Yes
MIXED Yes

Architecture Decisions

Draft Isolation — The Core Safety Invariant

Draft observations are invisible to VigilCareClinical's alert engine, Elasticsearch ward projection, and scoring consumers. Only batches that have passed approval are promoted to live tables. This is the architectural decision that separates a digitization system from a data entry form. If an entry clerk misreads a potassium of 5.2 mEq/L as 52 mEq/L, the plausibility validator catches it at draft save. If the plausibility range allows it but the value is wrong, the verifier catches it against the scan. If neither catches it, the clinical approver is the last gate for high-stakes batch types. At no point does the incorrect value enter the alerting pipeline until all human gates have cleared.

Separation of Duties — Who Cannot Do What

The person who enters data cannot verify their own entry. This is enforced in the service layer, not only in the UI. enteredByUserId === currentUserId blocks verify and approve actions with 409 SEPARATION_OF_DUTIES_VIOLATION. In a small island clinic with limited staff, the same person may hold intake and entry roles — but never entry and verifier on the same batch.

Two-Track Workflow (Backfill vs Live Capture)

Track A (backfill) is the full pipeline for historical charts: scan → entry → verification → clinical approval → promotion. Track B (live capture) is for credentialed clinicians entering vitals at bedside — attestation replaces the dual-human gate. Both tracks create DigitizationBatch records with full audit trails, keeping metrics and coverage stats consistent.

Redis for Batch Assignment Locking

Redis serves one purpose in this project: preventing double-assignment of batches to entry clerks. SET batch:assign:{id} NX EX 3600 acquires an exclusive lock with a 1-hour TTL. Work-queue counters are derived from PostgreSQL queries, not Redis counters.

Integrated Database Deployment

VigilCare Records draft tables and VigilCareClinical live tables share one PostgreSQL instance (separate logical concerns). Promotion runs in a single local transaction — no distributed saga required. Split deployment with HTTP + saga retry is documented as a future deployment option.


Getting Started

Prerequisites

  • .NET 8 SDK
  • Docker and Docker Compose

Start Infrastructure

docker compose up -d
Service Host Port Notes
PostgreSQL 16 5437 Database: vigilcare_records, user: postgres, password: password
Redis 7 6383 No auth
Seq 5346 UI at http://localhost:5346
MinIO 9012 (S3 API), 9013 (console) login: minioadmin / minioadmin

Install and Run

cd VigilCareRecordsAPI
dotnet restore
dotnet run

On startup the application:

  1. Runs EF Core migrations
  2. Seeds twelve demo users (two per role: intake clerk, data entry clerk, verifier, clinical approver, clinician, administrator)

Swagger UI is available at http://localhost:5217/swagger in Development.

Run Tests

dotnet test

Integration tests use WebApplicationFactory with PostgreSQL, Redis, and MinIO containers. No manual infrastructure setup is required for dotnet test.

Test class Phase Coverage
DraftEntryTests 2 Draft CRUD, plausibility validation, submit-for-verification completeness checks
VerificationTests 3 Verification, rejection, separation of duties enforcement, clinical approval routing
PromotionTests 4 Approval, atomic promotion to live tables, idempotency replay, separation of duties, retroactive alert policy, patient deduplication, MRN generation

Verification Scripts

With the API running (dotnet run) and Docker Compose up:

./scripts/run-vigilcare-records-verification.sh        # Phase 1 — schema, auth, batch CRUD, MinIO, status machine
./scripts/run-vigilcare-records-phase-2-verification.sh # Phase 2 — draft entry, plausibility, submit
./scripts/run-vigilcare-records-phase-3-verification.sh # Phase 3 — verification, rejection, separation of duties
./scripts/run-vigilcare-records-phase-4-verification.sh # Phase 4 — approval, promotion, idempotency, patient dedup

API Reference

All endpoints are prefixed /api/v1. Responses follow the standard envelope:

{ "success": true, "statusCode": 200, "data": {}, "error": null }

Error response:

{
  "success": false,
  "statusCode": 409,
  "data": null,
  "error": {
    "message": "Verifier cannot approve a batch they entered.",
    "code": "SEPARATION_OF_DUTIES_VIOLATION"
  }
}

Authentication

Method Path Auth Description
POST /auth/login Anonymous Authenticate with username/password; returns access + refresh tokens
POST /auth/refresh Anonymous Exchange a valid refresh token for new access + refresh token pair
POST /auth/logout Anonymous Revoke the refresh token server-side
GET /auth/me JWT Returns the authenticated user's profile

POST /auth/login body:

Field Type Required Description
username string yes Username
password string yes Password

Login response:

Field Type Description
accessToken string JWT bearer token (15 min)
refreshToken string Opaque refresh token (7 days)
expiresAt DateTimeOffset Access token expiration
userId Guid User ID
username string Username
displayName string Display name
role string INTAKE_CLERK, DATA_ENTRY_CLERK, VERIFIER, CLINICAL_APPROVER, CLINICIAN, ADMINISTRATOR

Seeded demo users:

Username Password Role
intake1 password Intake Clerk
intake2 password Intake Clerk
entry1 password Data Entry Clerk
entry2 password Data Entry Clerk
verifier1 password Verifier
verifier2 password Verifier
approver1 password Clinical Approver
approver2 password Clinical Approver
clinician1 password Clinician
clinician2 password Clinician
admin1 password Administrator
admin2 password Administrator

Digitization Batches

Method Path Description
POST /digitization-batches Upload a scanned document and create a batch (multipart/form-data)
GET /digitization-batches List batches; optional status, batchType, assignedTo, track filters; paginated
GET /digitization-batches/{id} Batch detail with presigned document URL (15-minute expiry)
PATCH /digitization-batches/{id}/assign Assign batch to an entry clerk (Redis lock)

POST body (multipart/form-data):

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)

Status codes:

Code Meaning
201 Batch created
400 Empty file or invalid MIME type
409 Duplicate document (same SHA-256 for same patient within 24 hours)

PATCH /digitization-batches/{id}/assign body:

Field Type Required Description
entryClerkUserId Guid yes User ID of the entry clerk to assign

Draft Data Entry

Method Path Description
GET /digitization-batches/{id}/draft Full draft payload: patient, encounter, observations
PUT /digitization-batches/{id}/draft/patient Upsert draft patient demographics
PUT /digitization-batches/{id}/draft/encounter Upsert draft encounter fields
POST /digitization-batches/{id}/draft/observations Add an observation row
PUT /digitization-batches/{id}/draft/observations/{obsId} Edit an observation row
DELETE /digitization-batches/{id}/draft/observations/{obsId} Remove an observation from draft
POST /digitization-batches/{id}/submit-for-verification Validate completeness and transition to PENDING_VERIFICATION

Observation request body:

Field Type Required Description
observationCode string yes e.g. HEART_RATE, SYSTOLIC_BP, TEMP_C, SPO2
value decimal yes Numeric measurement
unit string yes Unit of measure
recordedAt DateTimeOffset yes When the measurement was taken (from the chart)
note string no Optional note about the reading

Required fields before submit (by batch type):

batchType Required draft content
PATIENT_REGISTRATION Full name, date of birth, sex
ENCOUNTER_SUMMARY Linked patient; encounter with admission date, department, admission reason
VITALS_SHEET Linked patient, encounter context, at least one observation with recordedAt
LAB_RESULTS Linked patient, encounter, at least one lab observation code, recordedAt
MEDICATION_LIST Linked patient; medicationsJson with at least one entry or explicit noActiveMedications: true
ALLERGY_UPDATE Linked patient, allergies list (may be empty with explicit noKnownAllergies: true)
MIXED Linked patient, encounter context, and at least one of: observation with recordedAt, or complete encounter summary

Verification and Rejection

Method Path Auth Description
POST /digitization-batches/{id}/verify Verifier, Clinical Approver, Administrator Verify a batch with field-level checks
POST /digitization-batches/{id}/reject Verifier, Clinical Approver, Administrator Reject a batch with mandatory reason

POST /verify body:

Field Type Required Description
fieldChecks array yes Field-level review results
passed bool yes Overall verification pass/fail

Field check object:

Field Type Required Description
fieldName string yes e.g. observations[0].value, patient.fullName
status string yes ok, warning, or error
note string no Optional reviewer note

POST /reject body:

Field Type Required Description
reason string yes Rejection reason (minimum 10 characters)

Approval and Promotion

Method Path Auth Description
POST /digitization-batches/{id}/approve Clinical Approver, Administrator Approve and atomically promote draft data to live clinical tables
GET /digitization-batches/{id}/promotion-result Any authenticated Retrieve live entity IDs created during promotion

POST /approve headers:

Header Required Description
Idempotency-Key yes Unique key (max 100 chars) for safe retries; replays return the original response within 24 hours

POST /approve body:

Field Type Required Description
enableRetroactiveAlerts bool no Default false. When true, backfill observations emit outbox events for downstream alerting. Live capture batches always emit outbox events regardless of this flag.

Promotion response (PromotionResultResponse):

Field Type Description
batchId Guid The promoted batch
status string promoted
patientId Guid Live patient ID (created or matched)
mrn string Medical Record Number (e.g. VCR-000001)
encounterId Guid Live encounter ID (created or matched)
observationIds Guid[] Live observation IDs created
promotedAt DateTimeOffset Promotion timestamp
outboxEventsWritten int Number of outbox events emitted for downstream consumers

Status codes:

Code Meaning
200 Batch approved and promoted (or idempotent replay)
400 Missing or invalid Idempotency-Key header
404 Batch not found
409 Illegal status transition or separation of duties violation
422 Missing draft patient data

Separation of duties: The approver cannot be the entry clerk (enteredByUserId) or the verifier (verifiedByUserId) of the same batch. Both checks return 409 SEPARATION_OF_DUTIES_VIOLATION.

Patient deduplication: On promotion, the service matches existing patients by fullName + dateOfBirth. If a match is found, the existing patient record is updated with any new fields from the draft. Otherwise, a new patient is created with a sequence-generated MRN (VCR-NNNNNN).

Encounter matching: Active encounters for the same patient and department are reused. Otherwise, a new encounter is created. Encounters with a discharge diagnosis are created with discharged status.

Work Queues

Method Path Auth Description
GET /work-queue/verification Verifier, Clinical Approver, Administrator Batches in PENDING_VERIFICATION, sorted by submission time
GET /work-queue/entry Data Entry Clerk, Administrator Batches awaiting or in entry (UPLOADED, IN_ENTRY, REJECTED)
GET /work-queue/clinical-approval Clinical Approver, Administrator Batches in AWAITING_CLINICAL_APPROVAL

All work queue endpoints support pagination via ?page=1&pageSize=20.


Data Models

DigitizationBatch

id                      Guid    PK
status                  string  UPLOADED | IN_ENTRY | PENDING_VERIFICATION | REJECTED | VERIFIED | AWAITING_CLINICAL_APPROVAL | APPROVED | PROMOTED
batchType               string  PATIENT_REGISTRATION | ENCOUNTER_SUMMARY | VITALS_SHEET | LAB_RESULTS | MEDICATION_LIST | ALLERGY_UPDATE | MIXED
track                   string  BACKFILL | LIVE_CAPTURE
patientId               Guid?   nullable until linked
encounterDraftId        Guid?   encounter context for vitals/labs
documentRef             string  MinIO object key for the scanned document
documentSha256          string  content hash for integrity verification
enableRetroactiveAlerts bool    default false
enteredByUserId         Guid?   set on first draft save
verifiedByUserId        Guid?   set on verification pass
approvedByUserId        Guid?   set on final approval
rejectionReason         string? required when status → REJECTED
promotedAt              DateTimeOffset? timestamp when live records created
promotionEncounterId    Guid?   VigilCareClinical encounter ID after promotion
supersedesBatchId       Guid?   links a correction batch to the batch it replaces
clinicianAttestation    bool    true for Track B batches attested at bedside
createdAt               DateTimeOffset
updatedAt               DateTimeOffset

DraftPatient

id                Guid    PK
batchId           Guid    FK → DigitizationBatch
fullName          string? required for patient_registration on submit
dateOfBirth       DateOnly? required for patient_registration on submit
sex               string? required for patient_registration on submit
bloodType         string? A+ | A- | B+ | B- | AB+ | AB- | O+ | O-
emergencyContact  string?
allergiesJson     string? JSON array
noKnownAllergies  bool
medicationsJson   string? JSON array (for medication_list batches)
noActiveMedications bool
createdAt         DateTimeOffset
updatedAt         DateTimeOffset

DraftEncounter

id                 Guid    PK
batchId            Guid    FK → DigitizationBatch
admissionDate      DateTimeOffset?
department         string? ICU, ED, MedSurg, etc.
roomBed            string? ward/bed assignment
admissionReason    string?
dischargeDiagnosis string?
status             string? encounter status from chart
createdAt          DateTimeOffset
updatedAt          DateTimeOffset

DraftObservation

id              Guid    PK
batchId         Guid    FK → DigitizationBatch
observationCode string  required (e.g. HEART_RATE, TEMP_C)
value           decimal required — validated against plausibility ranges
unit            string  required (e.g. bpm, °C, mmHg)
recordedAt      DateTimeOffset required — from the paper chart
note            string? optional transcription note
createdAt       DateTimeOffset

ScannedDocument

id            Guid    PK
batchId       Guid    FK → DigitizationBatch
objectKey     string  MinIO object key
sha256        string  content hash
contentType   string  application/pdf | image/jpeg | image/png
fileSizeBytes long
uploadedAt    DateTimeOffset

DigitizationEvent

id            Guid    PK
batchId       Guid    FK → DigitizationBatch
eventType     string  uploaded | entry_started | submitted_for_verification | rejected | verified | awaiting_clinical_approval | approved | promoted | ...
actorUserId   Guid    FK → User
occurredAt    DateTimeOffset
metadataJson  string? optional JSON (field checks, rejection reason, assigned-to)

Patient (Clinical — Live)

id                Guid    PK
mrn               string  required, unique — generated from PostgreSQL sequence (VCR-000001)
fullName          string  required
dateOfBirth       DateOnly?
sex               string?
bloodType         BloodType?
emergencyContact  string?
allergiesJson     string?
noKnownAllergies  bool
createdAt         DateTimeOffset
updatedAt         DateTimeOffset

Encounter (Clinical — Live)

id                 Guid    PK
patientId          Guid    FK → Patient
admissionDate      DateTimeOffset?
department         Department?
roomBed            string?
admissionReason    string?
dischargeDiagnosis string?
status             string  active | discharged
sourceBatchId      Guid?   FK → DigitizationBatch (traceability)
createdAt          DateTimeOffset
updatedAt          DateTimeOffset

Observation (Clinical — Live)

id                       Guid    PK
encounterId              Guid    FK → Encounter
patientId                Guid    FK → Patient
observationCode          string  required (e.g. HEART_RATE, TEMP_C)
value                    decimal required
unit                     string  required
recordedAt               DateTimeOffset required
note                     string?
source                   string  digitization_backfill | live_capture
sourceDraftObservationId Guid?   FK → DraftObservation (traceability)
sourceBatchId            Guid?   FK → DigitizationBatch (traceability)
createdAt                DateTimeOffset

OutboxEvent

id            Guid    PK
eventType     string  e.g. observation.created
aggregateType string  e.g. Observation
aggregateId   Guid    FK → the created entity
payloadJson   string  full event payload for downstream consumers
createdAt     DateTimeOffset
processedAt   DateTimeOffset? set when consumed
retryCount    int     default 0

IdempotencyRecord

id                Guid    PK
idempotencyKey    string  required, unique (from Idempotency-Key header)
operationName     string  e.g. batch_promote
resourceId        Guid    the batch ID
httpStatusCode    int     original response code
responseBodyJson  string  serialized original response
createdAt         DateTimeOffset
expiresAt         DateTimeOffset  24-hour TTL

User

id            Guid    PK
username      string  required, unique
passwordHash  string  required (BCrypt)
fullName      string  required
role          string  INTAKE_CLERK | DATA_ENTRY_CLERK | VERIFIER | CLINICAL_APPROVER | CLINICIAN | ADMINISTRATOR
isActive      bool    default true
createdAt     DateTimeOffset
lastLoginAt   DateTimeOffset?

RefreshToken

id          Guid    PK
token       string  required, unique — opaque base64 token
userId      Guid    FK → User
expiresAt   DateTimeOffset required
createdAt   DateTimeOffset
revokedAt   DateTimeOffset? — set on refresh rotation or explicit logout

AuthAuditEvent

id          Guid    PK
eventType   string  LOGIN | LOGOUT | TOKEN_REFRESHED | LOGIN_FAILED
userId      Guid?
username    string?
ipAddress   string?
occurredAt  DateTimeOffset

Pagination

List endpoints use offset pagination:

Param Default Description
page 1 Page number (1-based)
pageSize 20 Items per page

Response shape:

{
  "items": [],
  "page": 1,
  "pageSize": 20,
  "totalCount": 42,
  "totalPages": 3
}

Implemented Phases

Four phases from the project roadmap are implemented and verified. Integration tests (dotnet test) and per-phase verification scripts cover Phases 14.

Phase Feature Status
1 Schema, EF Core migrations, JWT authentication with refresh tokens, six user roles, batch CRUD, MinIO upload with SHA-256 and presigned URLs, batch status machine with transition matrix, duplicate document detection, Redis batch assignment locks, twelve seeded demo users, auth audit events Done
2 Draft data entry API (patient demographics, encounter context, observations), plausibility validation on observation values, batch-type completeness validation on submit-for-verification, automatic UPLOADED → IN_ENTRY transition on first save, assignment guard (BATCH_NOT_ASSIGNED), DraftEntryTests integration tests Done
3 Verification with field-level checks (ok, warning, error per field), rejection with mandatory reason (min 10 chars), separation of duties enforcement (SEPARATION_OF_DUTIES_VIOLATION), site-configurable clinical approval routing per batch type, work queue endpoints (verification, entry, clinical approval) with role-based access, VerificationTests integration tests Done
4 Approval with three-person separation of duties (entry ≠ verifier ≠ approver), atomic promotion to live patients/encounters/observations tables in a single PostgreSQL transaction, PostgreSQL sequence-backed MRN generation (VCR-NNNNNN), patient dedup by name + DOB, encounter matching by patient + department + active status, transactional outbox (observation.created events), retroactive alert policy per batch, Idempotency-Key header with 24h TTL for safe retries, PromotionTests integration tests, Phase 4 verification script Done
5 Corrections and supersession — new batch replaces old, superseded observations soft-flagged Planned
6 Track B live capture with clinician attestation, synchronous alert evaluation Planned
7 Digitization workstation UI (Vue 3 side-by-side scan viewer + entry form) Planned
8 Prometheus metrics, supervisor dashboard, promotion retry job Planned
9 Seed data, E2E verification script, clinical scenario documentation Planned
S
Description
No description provided
Readme
15 MiB
Languages
C# 50.5%
Vue 16.9%
Shell 16.7%
TypeScript 15.1%
CSS 0.5%
Other 0.3%