Skip to content

ADR-003: EMR Abstraction Layer

IMPLEMENTED — Production in v4.0.0+

The EMR abstraction layer is fully implemented. IEmrAdapter interface, EmrAdapterFactory, and OscarSoapAdapter are in production. See Adapter Implementations.

Architecture Decision Record for Multi-EMR Support

Status: Accepted — Updated v4.0.0 Date: January 14, 2026 (Updated February 15, 2026) Epic: 1.2 Implementation: Complete (Phase 1 + Phase 2 + v4.0 Onboarding Integration)


Context

VitaraVox currently has tight coupling to OSCAR EMR through: - Hardcoded endpoint paths (/ws/rs/*, /demographics) - OSCAR-specific field names (demographicNo, providerNo) - OAuth 1.0a credential structure in database schema - Availability detection assumptions (empty demographicNo = available) - Single-char appointment type codes

To support multiple EMR systems (Oscar Universal, Oscar Pro, Telus Health, Juno), we need an abstraction layer.


Decision

Implement the EMR Adapter Pattern with a unified interface that all EMR integrations must implement.

Architecture Overview (Phase 2 Complete)

┌─────────────────────────────────────────────────────────────────────────────────┐
│                         VOICE AGENT (vapiEndpoints.js)                           │
│                                                                                  │
│   search_patient  find_appointment  book_appointment  get_providers             │
└─────────────────────────────────────┬────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────────┐
│                         EMR SERVICE LAYER (Phase 2)                              │
│                         services/EmrService.js                                   │
│                                                                                  │
│   ┌────────────────────────────────────────────────────────────────────────┐    │
│   │  FEATURE FLAGS                                                          │    │
│   │  ┌────────────────────┐  ┌────────────────────────────────────────┐    │    │
│   │  │ USE_EMR_ADAPTER    │  │ EMR_ADAPTER_CLINICS                    │    │    │
│   │  │ (global toggle)    │  │ (per-clinic rollout)                   │    │    │
│   │  └────────────────────┘  └────────────────────────────────────────┘    │    │
│   └────────────────────────────────────────────────────────────────────────┘    │
│                                                                                  │
│   ┌────────────────────┐          ┌────────────────────────────────────┐        │
│   │  Legacy Fallback   │◄────OR───│  EmrAdapterFactory                 │        │
│   │  oscarRestService  │          │  (Clinic Config → Adapter)         │        │
│   │  (if flag=false)   │          │                                    │        │
│   └────────────────────┘          └────────────────┬───────────────────┘        │
│                                                    │                             │
└────────────────────────────────────────────────────┼─────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────────┐
│                         EMR ADAPTER INTERFACE (Phase 1)                          │
│                         adapters/IEmrAdapter.js                                  │
│                                                                                  │
│   IEmrAdapter {                                                                  │
│     searchPatient(query)      getAvailability(providerId, date)                 │
│     getPatient(id)            getAppointments(filter)                           │
│     createPatient(data)       createAppointment(data)                           │
│     getProviders()            updateAppointment(id, data)                       │
│     getProvider(id)           cancelAppointment(id)                             │
│     healthCheck()             getCircuitStatus()                                │
│   }                                                                              │
└─────────────────────────────────────┬────────────────────────────────────────────┘
           ┌──────────────────────────┼──────────────────────────┐
           │                          │                          │
           ▼                          ▼                          ▼
┌─────────────────────┐  ┌─────────────────────┐  ┌─────────────────────┐  ┌─────────────────────┐
│ OscarSoapAdapter    │  │ OscarBridgeAdapter  │  │  AccuroRestAdapter  │  │  TelusHealthAdapter │
│ ✅ PRODUCTION       │  │ ✅ DEV/FALLBACK     │  │  📋 PLANNED         │  │  📋 PLANNED         │
│                     │  │                     │  │                     │  │                     │
│ - Direct SOAP       │  │  - REST Bridge      │  │  - REST/OAuth 2.0   │  │  - GraphQL/JWT      │
│ - WS-Security       │  │  - X-API-Key auth   │  │  - 150+ endpoints   │  │  - TELUS Cloud      │
│ - UsernameToken     │  │  - Circuit breaker   │  │  - Schedule template│  │    Connect          │
│ - Circuit breaker   │  │  - 5-min cache      │  │                     │  │                     │
│ - ~870 lines        │  │  - ~200 lines       │  │                     │  │                     │
└─────────────────────┘  └─────────────────────┘  └─────────────────────┘  └─────────────────────┘
           │                          │                          │                          │
           │                    TRANSFORMERS                     │                          │
           │          ┌────────────────────────────────┐        │                          │
           │          │  PatientTransformer            │        │                          │
           │          │  ProviderTransformer           │        │                          │
           │          │  AppointmentTransformer        │        │                          │
           │          │  (EMR ↔ Canonical format)      │        │                          │
           │          └────────────────────────────────┘        │                          │
           │                          │                          │                          │
           ▼                          ▼                          ▼                          ▼
┌─────────────────────┐  ┌─────────────────────┐  ┌─────────────────────┐  ┌─────────────────────┐
│  OSCAR EMR (Direct) │  │  OSCAR REST Bridge  │  │  Accuro (QHR)       │  │  TELUS PS Suite     │
│  SOAP/WS-Security   │  │  (15.222.50.48:3000)│  │  (REST API)         │  │  (Cloud Connect)    │
│  Port 8443          │  │  Legacy, dev-only   │  │                     │  │                     │
└─────────────────────┘  └─────────────────────┘  └─────────────────────┘  └─────────────────────┘

EMR Adapter Interface

TypeScript Interface Definition

// adapters/IEmrAdapter.ts

export interface PatientSearchQuery {
  phone?: string;
  lastName?: string;
  firstName?: string;
  healthCardNumber?: string;
  dateOfBirth?: string;
}

export interface Patient {
  id: string;                    // EMR-agnostic ID (was demographicNo)
  firstName: string;
  lastName: string;
  dateOfBirth: string;           // YYYY-MM-DD
  gender?: 'M' | 'F' | 'O';
  phone?: string;
  email?: string;
  address?: {
    street: string;
    city: string;
    province: string;
    postalCode: string;
    country: string;
  };
  healthCardNumber?: string;
  healthCardProvince?: string;
}

export interface Provider {
  id: string;                    // EMR-agnostic ID (was providerNo)
  firstName: string;
  lastName: string;
  displayName: string;           // "Dr. Sarah Anderson"
  specialty?: string;
  acceptingNewPatients: boolean;
}

export interface TimeSlot {
  startTime: string;             // ISO 8601: 2026-01-14T09:00:00
  endTime: string;               // ISO 8601: 2026-01-14T09:15:00
  duration: number;              // Minutes
  status: 'available' | 'booked' | 'blocked';
  appointmentType?: string;      // EMR-agnostic type code
  description?: string;          // Human-readable type name
}

export interface Availability {
  providerId: string;
  date: string;                  // YYYY-MM-DD
  holiday: boolean;
  timeSlots: TimeSlot[];
}

export interface Appointment {
  id: string;                    // EMR-agnostic ID (was appointmentNo)
  patientId: string;
  providerId: string;
  startTime: string;             // ISO 8601
  endTime: string;               // ISO 8601
  duration: number;
  status: 'scheduled' | 'confirmed' | 'cancelled' | 'completed' | 'no_show';
  reason?: string;
  notes?: string;
  appointmentType?: string;
}

export interface AppointmentCreateRequest {
  patientId: string;
  providerId: string;
  startTime: string;             // ISO 8601
  duration: number;
  reason?: string;
  appointmentType?: string;
  notes?: string;
}

export interface EmrHealthStatus {
  status: 'healthy' | 'degraded' | 'unhealthy';
  latencyMs?: number;
  services?: Record<string, 'connected' | 'disconnected'>;
  version?: string;
}

export interface IEmrAdapter {
  // Metadata
  readonly emrType: string;      // 'oscar-universal' | 'oscar-pro' | 'telus-health'
  readonly version: string;

  // Patient Operations
  searchPatient(query: PatientSearchQuery): Promise<Patient[]>;
  getPatient(id: string): Promise<Patient | null>;
  createPatient(data: Omit<Patient, 'id'>): Promise<Patient>;

  // Provider Operations
  getProviders(): Promise<Provider[]>;
  getProvider(id: string): Promise<Provider | null>;

  // Schedule Operations
  getAvailability(providerId: string, date: string): Promise<Availability>;
  getAppointmentTypes(): Promise<{ code: string; name: string; duration: number }[]>;

  // Appointment Operations
  getAppointments(filter: {
    patientId?: string;
    providerId?: string;
    startDate?: string;
    endDate?: string;
  }): Promise<Appointment[]>;
  createAppointment(data: AppointmentCreateRequest): Promise<Appointment>;
  updateAppointment(id: string, data: Partial<Appointment>): Promise<Appointment>;
  cancelAppointment(id: string): Promise<boolean>;

  // Health Check
  healthCheck(): Promise<EmrHealthStatus>;
}

Impact to Current Architecture

Files Requiring Modification

File Current State Required Changes Risk
vapiEndpoints.js Direct oscarRestService calls Use adapter factory, generic field names High - 14 endpoints
oscarRestService.js REST Bridge client Refactor into OscarUniversalAdapter Medium
oscarService.js Legacy OAuth client Refactor into OscarProAdapter Medium
clinicRouter.js Loads OSCAR credentials Load EMR type + credentials Medium
vitaraDb.js OSCAR-specific schema Add emr_type column Low
utils.js OSCAR-specific formatting EMR-agnostic formatters Low

Database Schema Changes

-- Add EMR type to clinics table
ALTER TABLE clinics ADD COLUMN emr_type VARCHAR(50) DEFAULT 'oscar-universal';
ALTER TABLE clinics ADD COLUMN emr_config JSONB DEFAULT '{}';

-- emr_config structure varies by type:
-- oscar-universal: { "bridgeUrl": "...", "apiKey": "..." }
-- oscar-pro: { "baseUrl": "...", "oauthConsumerKey": "...", ... }
-- telus-health: { "fhirEndpoint": "...", "clientId": "...", "clientSecret": "..." }

New Files to Create

/opt/vitara-platform/voice-agent/
├── adapters/
│   ├── IEmrAdapter.js           # Interface definition
│   ├── EmrAdapterFactory.js     # Factory to create adapters
│   ├── OscarUniversalAdapter.js # Current REST Bridge
│   ├── OscarProAdapter.js       # OAuth 1.0a direct
│   └── BaseEmrAdapter.js        # Common functionality
├── transformers/
│   ├── PatientTransformer.js    # EMR → Canonical patient
│   ├── AppointmentTransformer.js
│   └── ProviderTransformer.js
└── __tests__/
    ├── adapters/
    │   ├── OscarUniversalAdapter.test.js
    │   └── EmrAdapterFactory.test.js
    └── transformers/
        └── PatientTransformer.test.js

Implementation Phases

Phase 1: Interface & Factory (Week 1)

  1. Create IEmrAdapter interface
  2. Create EmrAdapterFactory
  3. Create BaseEmrAdapter with common functionality
  4. Wrap current oscarRestService.js as OscarUniversalAdapter

Phase 2: Refactor Voice Agent (Week 2)

  1. Update clinicRouter.js to load EMR type
  2. Refactor vapiEndpoints.js to use adapter factory
  3. Update database schema
  4. Update field names throughout

Phase 3: Testing & Documentation (Week 3)

  1. Unit tests for all adapters
  2. Integration tests with mock EMR
  3. E2E tests with real OSCAR instance
  4. Documentation updates

v4.0.0 Update (February 2026)

The EMR abstraction layer has been extended for the onboarding redesign:

Renamed adapters:

  • OscarUniversalAdapterOscarBridgeAdapter (clarifies purpose — talks to REST Bridge, dev/fallback only)
  • OscarSoapAdapter added as production path — direct SOAP/WS-Security to OSCAR CXF (~870 lines)

New adapter methods used by onboarding:

Method Used By Purpose
getProviders() Step 4 Validation Verify provider roster pulled from EMR
getScheduleSlots() Step 4 Validation Test slot retrieval with live data
searchPatient() Step 4 Validation Test patient search with live data
testConnection() Step 2 EMR Connection Verify EMR credentials work

EMR type selector (onboarding Step 2):

EMR Type Adapter Onboarding Status
oscar-soap OscarSoapAdapter Full self-service (credentials → test → auto-pull config)
oscar-universal OscarBridgeAdapter Full self-service (dev/fallback)
telus TelusHealthAdapter Coming soon — saves type, shows contact message
accuro AccuroRestAdapter Coming soon — saves type, shows contact message
other Coming soon — saves type, shows contact message

Factory caching: EmrAdapterFactory.getAdapter(clinicId) uses a 5-minute TTL cache per clinic.