Skip to content

Adapter Implementations

Last Updated: 2026-02-16

Status: OscarSoapAdapter PRODUCTION | OscarBridgeAdapter DEV/FALLBACK | Others PLANNED


Overview

The EMR adapter layer provides a canonical interface (IEmrAdapter, defined in ADR-003) for all EMR operations. Each clinic selects their EMR type during onboarding (Step 2), and the EmrAdapterFactory creates the appropriate adapter instance with a 5-minute TTL cache per clinic.

The adapters serve the BookingEngine (booking.service.ts, ADR-004), which consumes the IEmrAdapter interface to compute true availability, enforce schedule settings, and manage appointments with advisory locking for race prevention.

Adapter Selection

EMR Type Adapter Config Key Status
oscar-soap OscarSoapAdapter emr_type: 'oscar-soap' Production
oscar-universal OscarBridgeAdapter emr_type: 'oscar-universal' Dev/Fallback
telus TelusPsSuiteAdapter emr_type: 'telus' Coming Soon
accuro AccuroRestAdapter emr_type: 'accuro' Coming Soon
other -- emr_type: 'other' Contact Support

Factory Pattern

EmrAdapterFactory.getAdapter(clinicId)
  -> Reads clinic_config.emr_type from database
  -> Decrypts credentials (AES-256-GCM)
  -> Resolves OAuth creds (DB with env var fallback)
  -> Creates appropriate adapter with clinic timezone + blocked codes
  -> 5-min TTL cache (per-clinic, with periodic cleanup)

IEmrAdapter Interface

All adapters implement the following canonical interface defined in IEmrAdapter.ts:

Properties

Property Type Description
emrType string Adapter identifier (e.g., 'oscar-soap', 'oscar-universal')
version string Adapter version

Patient Operations

Method Signature Description
searchPatient (query: PatientSearchQuery) => Promise<AdapterResult<Patient[]>> Multi-field patient search (name, phone, health card)
getPatient (id: string) => Promise<AdapterResult<Patient \| null>> Fetch single patient by ID
createPatient (data: Omit<Patient, 'id'>) => Promise<AdapterResult<Patient>> Register new patient

Provider Operations

Method Signature Description
getProviders () => Promise<AdapterResult<Provider[]>> List all active providers
getProvider (id: string) => Promise<AdapterResult<Provider \| null>> Fetch single provider by ID

Schedule Operations

Method Signature Description
getAvailability (providerId, date) => Promise<AdapterResult<Availability>> Combined schedule + appointment data
getScheduleSlots (providerId, date) => Promise<AdapterResult<{slots: ScheduleSlot[]}>> Raw schedule template slots (what the schedule ALLOWS, not true availability)

Appointment Operations

Method Signature Description
getAppointments (filter) => Promise<AdapterResult<Appointment[]>> Fetch appointments by provider + date range
getAppointment (id: string) => Promise<AdapterResult<Appointment \| null>> Fetch single appointment by ID
createAppointment (data: AppointmentCreateRequest) => Promise<AdapterResult<Appointment>> Book a new appointment
cancelAppointment (id: string) => Promise<AdapterResult<boolean>> Cancel an existing appointment

Optional / Health

Method Signature Description
getScheduleTemplateCodes? () => Promise<AdapterResult<any[]>> OSCAR-specific template code pull
healthCheck () => Promise<EmrHealthStatus> Connection health check

Key Canonical Types

  • PatientSearchQuery: term, phone, lastName, firstName, healthCardNumber, dateOfBirth
  • Patient: id, firstName, lastName, dateOfBirth, gender, phone, email, address, healthCardNumber
  • Provider: id, firstName, lastName, displayName, specialty, acceptingNewPatients
  • ScheduleSlot: startTime, endTime, duration, code, bookinglimit, description
  • Appointment: id, patientId, providerId, date, startTime, endTime, duration, status, reason
  • AdapterResult<T>: { success, data?, error?: { code, message } }

BookingEngine Integration

The BookingEngine (ADR-004, booking.service.ts) consumes adapters via the IEmrAdapter interface and provides higher-level booking intelligence:

BookingEngine(emr: IEmrAdapter, clinicId: string)
  getTrueAvailability(providerId, date)
    = getScheduleSlots() - getAppointments() - ScheduleSettings filters
  findEarliestSlot(opts)
    = day-by-day search across providers with settings enforcement
  bookAppointment(params)
    = advisory lock -> fresh availability check -> createAppointment
  rescheduleAppointment(params)
    = book-then-cancel strategy (new slot first, then cancel old)
  getPatientAppointments(params)
    = cross-provider appointment search

The BookingEngine enforces: operating hours, holiday exclusions, same-day/weekend restrictions, advance booking limits, buffer time, slot overlap detection with bookinglimit enforcement, max appointments per patient per day, and PostgreSQL advisory locking for race prevention.


Active Adapters

OscarSoapAdapter (Production)

File: admin-dashboard/server/src/adapters/OscarSoapAdapter.ts Lines: ~1211 Connection: Direct SOAP/WS-Security to OSCAR CXF endpoints

SOAP Endpoints Used:

WSDL Service Key Methods
/ws/ScheduleService?wsdl ScheduleService getDayWorkSchedule, getAppointmentsForDateRangeAndProvider2, addAppointment, updateAppointment, getScheduleTemplateCodes
/ws/DemographicService?wsdl DemographicService getDemographic, searchDemographicByName, searchDemographicsByAttributes
/ws/ProviderService?wsdl ProviderService getProviders2(true)

No deleteAppointment

OSCAR ScheduleService has no deleteAppointment operation. Cancellation is performed via updateAppointment with status='C'. Rescheduling uses the BookingEngine's book-then-cancel strategy: book the new slot first, then cancel the old appointment via updateAppointment.

No addDemographic

OSCAR DemographicService has no addDemographic method. The createPatient() adapter method returns NOT_CONFIGURED unless OAuth 1.0a credentials are provided, in which case it falls back to the OSCAR REST API (/ws/services/demographics) for patient registration. This is a known limitation: register_new_patient is NOT_SUPPORTED via SOAP alone.

No getProvider(id)

OSCAR ProviderService has no single-provider lookup. The adapter implements getProvider(id) by calling getProviders2(true) and filtering by ID.

WS-Security Configuration:

{
  passwordType: 'PasswordText',
  mustUnderstand: true,
  hasTimeStamp: false,  // CRITICAL: CXF has no Timestamp action configured
  hasNonce: false        // <wsu:Timestamp> causes SecurityError
}

Key Implementation Details:

  • normalizeTime(): Handles JAXB Calendar to JS Date conversion for OSCAR datetime fields. Uses toLocaleString('en-US', { timeZone }) to extract clinic-local HH:mm from Date objects.
  • getDayWorkSchedule() returns {date, scheduleCode} where scheduleCode is an integer code point (JAXB standard). Use String.fromCharCode() to convert.
  • Non-bookable codes: L(76), P(80), V(86) filtered by default. Full OSCAR set: L, P, V, A, a, B, H, R, E, G, M, m, d, t. Configurable via blockedCodes option.
  • OSCAR SOAP uses positional args (arg0, arg1, ...) -- always check WSDL complexType definitions.
  • DateTime params need ISO format: ${date}T00:00:00 (not just YYYY-MM-DD).
  • addAppointment / updateAppointment take arg0: appointmentTransfer (wrapped object, not flat fields).
  • Circuit breaker timeout: 4s -- must be under Vapi's 5s tool-call timeout. Uses opossum with 50% error threshold and 30s reset.
  • Phone search: OSCAR SOAP has no phone search capability. Falls back to the REST bridge (x-api-key auth) for phone-based patient lookup when bridge credentials are configured.
  • Lazy SOAP client init: Clients are created on first use with soap.createClientAsync(). Cold-start WSDL fetch may need warmup on PM2 startup.
  • Clinic timezone: Configurable per-clinic (defaults to America/Vancouver). Used in normalizeTime() for correct local time extraction.

OscarBridgeAdapter (Dev/Fallback)

File: admin-dashboard/server/src/adapters/OscarBridgeAdapter.ts Lines: ~296 Connection: X-API-Key authenticated REST to OSCAR REST-SOAP Bridge Note: Renamed from OscarUniversalAdapter in v4.0.0 to clarify purpose.

Authentication

The bridge adapter uses X-API-Key header authentication (not OAuth). The constructor takes (bridgeUrl: string, apiKey: string) and delegates to OscarService which sends the API key via x-api-key header on all requests. OAuth 1.0a is only used for direct OSCAR REST API access (e.g., patient registration in the SOAP adapter).

The bridge adapter talks to the REST-SOAP Bridge (/api/v1/*). This path is dev/test only -- production uses the SOAP adapter for direct CXF connectivity without the bridge as an intermediary.

Capabilities vs SOAP Adapter:

Capability OscarSoapAdapter OscarBridgeAdapter
Schedule template codes Full (code, duration, bookinglimit, confirm) Limited (no bookinglimit)
Phone-based patient search Via bridge fallback Native
Patient registration Via OAuth 1.0a REST fallback Via bridge
getAppointment(id) NOT_SUPPORTED NOT_SUPPORTED
Appointment cancel updateAppointment with status='C' cancelAppointment bridge endpoint
Circuit breaker Per-service (schedule, demographic, provider) Inherits from OscarService

Planned Adapters

TelusPsSuiteAdapter (Coming Soon)

  • OAuth 2.0 authentication flow
  • TELUS Cloud Connect integration
  • REST endpoint mapping
  • Schedule template to availability mapping
  • Compliance considerations (PHIPA/PIPA)

AccuroRestAdapter (Coming Soon)

  • OAuth 2.0 authentication flow
  • REST endpoint mapping (150+ endpoints)
  • Schedule template to availability mapping
  • Rate limiting and pagination
  • Integration testing with Accuro sandbox

Onboarding Integration

When a clinic selects their EMR type during onboarding (Step 2: EMR Connection):

  1. OSCAR: Full self-service -- enter credentials, test connection, auto-pull config (schedule codes, appointment types)
  2. TELUS/Accuro/Other: EMR type saved, clinic shown "Coming Soon" message with support contact

Pre-Launch Validation

The validatePreLaunch() method (Step 4) runs 10 checks against the active adapter. Of these, 7 are required for go-live and 3 are informational (non-blocking):

# Check ID Label Adapter Method Required?
1 clinic_info Clinic information complete -- Yes
2 business_hours Business hours configured -- Yes
3 providers Active provider with EMR mapping -- Yes
4 emr_connection EMR connection verified healthCheck() (implicit) Yes
5 vapi_phone Vapi phone number assigned -- Yes
6 privacy_officer Privacy officer designated -- Yes
7 credentials_encrypted Credentials encrypted -- Yes
8 test_call Test call completed -- No (informational)
9 schedule_data_flow Schedule data flow getScheduleSlots() No (informational)
10 oscar_config_synced OSCAR config synced -- No (informational)

The schedule_data_flow check (check 9) verifies the adapter returns well-shaped slot data by calling getScheduleSlots() for the first mapped provider on the next weekday. It validates that returned slots have startTime, endTime, and code fields. Zero slots is a warning but still passes (the clinic may not have configured their OSCAR schedule template yet).


Compliance Notes

No Sidecar Deployments on Customer OSCAR

Canadian healthcare clinics provide OAuth credentials, not SSH access. PHIPA/PIPA/HIA compliance prohibits custom DB-access components on customer EMR instances. The OSCAR SOAP API is the universal connector -- it ships with every version since OSCAR 12. Build on it, not on the bridge.


Current Status: OscarSoapAdapter is production-ready (~1211 lines, SOAP integration complete, live in v4.0.0 onboarding). OscarBridgeAdapter (~296 lines) available for dev/fallback. TelusPsSuite and Accuro adapters are planned for future milestones.

Next: Integration Roadmap