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,dateOfBirthPatient:id,firstName,lastName,dateOfBirth,gender,phone,email,address,healthCardNumberProvider:id,firstName,lastName,displayName,specialty,acceptingNewPatientsScheduleSlot:startTime,endTime,duration,code,bookinglimit,descriptionAppointment:id,patientId,providerId,date,startTime,endTime,duration,status,reasonAdapterResult<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. UsestoLocaleString('en-US', { timeZone })to extract clinic-local HH:mm from Date objects.getDayWorkSchedule()returns{date, scheduleCode}wherescheduleCodeis an integer code point (JAXB standard). UseString.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
blockedCodesoption. - OSCAR SOAP uses positional args (
arg0,arg1, ...) -- always check WSDL complexType definitions. - DateTime params need ISO format:
${date}T00:00:00(not justYYYY-MM-DD). addAppointment/updateAppointmenttakearg0: appointmentTransfer(wrapped object, not flat fields).- Circuit breaker timeout: 4s -- must be under Vapi's 5s tool-call timeout. Uses
opossumwith 50% error threshold and 30s reset. - Phone search: OSCAR SOAP has no phone search capability. Falls back to the REST bridge (
x-api-keyauth) 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 innormalizeTime()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):
- OSCAR: Full self-service -- enter credentials, test connection, auto-pull config (schedule codes, appointment types)
- 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