Adapter Implementations¶
Last Updated: 2026-03-09
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 (SOAP or REST via preferRest) |
oscar-universal |
OscarBridgeAdapter |
emr_type: 'oscar-universal' |
Dev/Fallback |
oscar |
OscarBridgeAdapter |
emr_type: 'oscar' |
Dev/Fallback (alias) |
telus |
TelusPsSuiteAdapter |
emr_type: 'telus' |
Coming Soon |
accuro |
AccuroRestAdapter |
emr_type: 'accuro' |
Coming Soon |
other |
-- | emr_type: 'other' |
Contact Support |
| (unknown) | OscarBridgeAdapter |
(any unrecognized value) | Fallback default |
Factory Pattern¶
EmrAdapterFactory.getEmrAdapter(clinicId)
-> Reads clinic_config.emr_type from database
-> Reads clinic.timezone (default: 'America/Vancouver')
-> Decrypts credentials (AES-256-GCM, format: "iv:authTag:ciphertext")
-> Resolves OAuth creds (DB with env var fallback)
-> Creates appropriate adapter with clinic timezone + blocked codes
-> Passes bridge URL/key for phone search fallback (SOAP adapter)
-> 5-min TTL cache (per-clinic, with periodic cleanup every 10 min)
SOAP Credential Field Mapping
The oscar-soap adapter's WS-Security password is sourced from clinicConfig.oscarConsumerKey || config.oscarSoapPassword. The DB field oscarConsumerKey is repurposed from its original OAuth meaning to store the SOAP password. The WS-Security username always comes from the OSCAR_SOAP_USERNAME environment variable. This field name confusion is a known tech debt item -- do not assume oscarConsumerKey means an OAuth consumer key for the SOAP adapter path.
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: string, date: string) => Promise<AdapterResult<Availability>> |
Schedule + appointment data (see warning below) |
getScheduleSlots |
(providerId: string, date: string) => Promise<AdapterResult<{providerId: string, date: string, holiday: boolean, slots: ScheduleSlot[]}>> |
Raw schedule template slots (what the schedule ALLOWS, not true availability) |
getAvailability vs BookingEngine.getTrueAvailability
The adapter-level getAvailability() in the SOAP adapter marks slots as booked using simple startTime matching (set membership check). It does NOT perform overlap detection, buffer time calculation, or ScheduleSettings filtering. For production booking decisions, always use the BookingEngine's getTrueAvailability(), which does proper interval overlap detection (apptStart - buffer < slotEnd && apptEnd + buffer > slotStart), bookinglimit enforcement, and full ScheduleSettings filtering. The adapter-level getAvailability() is suitable only for display/informational purposes.
Appointment Operations¶
| Method | Signature | Description |
|---|---|---|
getAppointments |
(filter: {patientId?, providerId?, startDate?, endDate?}) => Promise<AdapterResult<Appointment[]>> |
Fetch appointments. Both OSCAR adapters require providerId, startDate, endDate. |
getAppointment |
(id: string) => Promise<AdapterResult<Appointment \| null>> |
Single appointment by ID. NOT_SUPPORTED by both OSCAR adapters. |
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. Only OscarSoapAdapter implements this. |
healthCheck |
() => Promise<EmrHealthStatus> |
Connection health check |
Key Canonical Types¶
PatientSearchQuery:
| Field | Type | Notes |
|---|---|---|
term |
string? |
Free-text multi-field search |
phone |
string? |
Phone number search |
lastName |
string? |
|
firstName |
string? |
|
healthCardNumber |
string? |
|
dateOfBirth |
string? |
YYYY-MM-DD |
Patient:
| Field | Type | Notes |
|---|---|---|
id |
string |
|
firstName |
string |
|
lastName |
string |
|
dateOfBirth |
string |
YYYY-MM-DD |
gender |
'M' \| 'F' \| 'O' |
Optional |
phone |
string? |
|
email |
string? |
|
address |
{street, city, province, postalCode, country}? |
Nested object |
healthCardNumber |
string? |
|
healthCardProvince |
string? |
Province code for health card |
Provider: id, firstName, lastName, displayName, specialty?, acceptingNewPatients
TimeSlot (used in Availability.timeSlots, returned by getAvailability):
| Field | Type | Notes |
|---|---|---|
startTime |
string |
HH:mm or ISO 8601 |
endTime |
string |
|
duration |
number |
Minutes |
status |
'available' \| 'booked' \| 'blocked' |
|
appointmentType |
string? |
|
description |
string? |
ScheduleSlot (used in getScheduleSlots -- raw template data):
| Field | Type | Notes |
|---|---|---|
startTime |
string |
HH:mm |
endTime |
string |
|
duration |
number |
Minutes |
code |
string |
Schedule template code (e.g. '1', 'C', 'A') |
bookinglimit |
number |
Max appointments per slot. 0 = unlimited. |
description |
string? |
Human-readable code description |
Appointment:
| Field | Type | Notes |
|---|---|---|
id |
string |
|
patientId |
string |
|
providerId |
string |
|
date |
string |
YYYY-MM-DD |
startTime |
string |
HH:mm |
endTime |
string |
|
duration |
number |
|
status |
'scheduled' \| 'confirmed' \| 'cancelled' \| 'completed' \| 'no_show' |
|
reason |
string? |
|
notes |
string? |
|
appointmentType |
string? |
OSCAR status code mapping to canonical status:
| OSCAR Status | OscarSoapAdapter | OscarBridgeAdapter |
|---|---|---|
'C' |
'cancelled' |
'cancelled' |
'N' |
'no_show' |
'no_show' |
'P' |
'completed' |
'scheduled' (not mapped) |
| All others | 'scheduled' |
'scheduled' |
Status Mapping Divergence
The SOAP adapter maps OSCAR status 'P' (Picked Up / Completed) to 'completed', while the bridge adapter maps it to 'scheduled'. This difference affects patient appointment history if you switch adapters.
AppointmentCreateRequest: patientId, providerId, date (YYYY-MM-DD), startTime (HH:mm), duration (minutes), reason?, appointmentType?
AdapterResult<T>: { success: boolean, data?: T, error?: { code: string, message: string } }
EmrHealthStatus: { status: 'healthy' | 'degraded' | 'unhealthy', latencyMs?: number, version?: string }
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, settings?)
Algorithm:
1. Holiday check (from ScheduleSettings.holidays)
2. Operating hours check (day-of-week open/closed)
3. Weekend booking check (if allowWeekendBooking=false)
4. Same-day booking check (if allowSameDayBooking=false)
5. maxAdvanceBookingDays check
6. emr.getScheduleSlots(providerId, date) -> raw template slots
7. emr.getAppointments({providerId, startDate: date, endDate: date}) -> existing appts
8. For each slot:
a. Operating hours filter (slotStart >= openTime, slotEnd <= closeTime)
b. Min advance booking filter (today only: slotStart >= now + minAdvanceHours)
c. Overlap detection: count appts where
(apptStart - bufferTime) < slotEnd AND (apptEnd + bufferTime) > slotStart
d. Bookinglimit check: if limit > 0 and overlapping >= limit, skip slot
9. Return available slots as AvailableSlot[]
getTrueAvailability Pipeline¶
getTrueAvailability(providerId, date, settings?)
┌───────────────────────────────────────────────────────────────┐
│ Step 1: Holiday check (ScheduleSettings.holidays) │
│ → if date is a holiday, return empty [] │
├───────────────────────────────────────────────────────────────┤
│ Step 2: Operating hours check (day-of-week isOpen) │
│ → if clinic closed this weekday, return empty [] │
├───────────────────────────────────────────────────────────────┤
│ Step 3: Weekend booking check │
│ → if allowWeekendBooking=false && isWeekend, empty │
├───────────────────────────────────────────────────────────────┤
│ Step 4: Same-day booking check │
│ → if allowSameDayBooking=false && isToday, empty │
├───────────────────────────────────────────────────────────────┤
│ Step 5: maxAdvanceBookingDays check │
│ → if date > today + maxDays, empty │
├───────────────────────────────────────────────────────────────┤
│ Step 6: emr.getScheduleSlots(providerId, date) │
│ → raw template slots from OSCAR │
├───────────────────────────────────────────────────────────────┤
│ Step 7: emr.getAppointments(providerId, date) │
│ → existing booked appointments for that day │
├───────────────────────────────────────────────────────────────┤
│ Step 8: Per-slot filtering loop: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ a. Operating hours: slotStart >= open, slotEnd <= close │
│ │ b. Min advance: today only, slotStart >= now+minHrs│ │
│ │ c. Overlap detection (interval math): │ │
│ │ (apptStart - buffer) < slotEnd AND │ │
│ │ (apptEnd + buffer) > slotStart → overlapping │ │
│ │ d. Bookinglimit: if limit>0 && overlaps >= limit │ │
│ │ → skip slot │ │
│ └─────────────────────────────────────────────────────┘ │
├───────────────────────────────────────────────────────────────┤
│ Step 9: Return AvailableSlot[] │
│ → only truly bookable slots │
└───────────────────────────────────────────────────────────────┘
findEarliestSlot(opts: FindSlotOptions)
Supports: clinicId, providerId, providerName (fuzzy match), startDate, endDate,
excludeDates, timeOfDay ('morning'=<12:00 | 'afternoon'=>=12:00), maxResults (default 1)
Provider resolution:
- Specific ID -> search that provider only
- Provider name -> fuzzy match against displayName (strips "Dr." prefix)
- Neither -> top-3 providers from EMR (filtered ID range 1-998)
Past dates clamped to today.
Searches day-by-day, calls getTrueAvailability per provider per day.
Returns first match (or null).
bookAppointment(params)
1. Acquire PostgreSQL advisory lock: pg_try_advisory_lock(hashtext(lockKey))
lockKey = "provider:{id}:date:{date}:slot:{startTime}"
2. Fresh getTrueAvailability check (slot may have been taken since search)
3. maxApptsPerPatientPerDay enforcement
4. emr.createAppointment(...)
5. Release lock (always, via finally block)
6. Degrades gracefully if lock acquisition fails (proceeds without lock)
rescheduleAppointment(params)
Book-then-cancel strategy:
1. bookAppointment for NEW slot (with locking)
2. Only if successful: emr.cancelAppointment(oldAppointmentId)
3. If cancel fails: return success with warning (patient has new appointment)
getPatientAppointments(params)
Cross-provider search:
- Takes patientId, optional startDate/endDate/providerIds
- If no providerIds: fetches all providers (capped at 5, ID range 1-998)
- Queries each provider's appointments, filters by patientId
- Sorts results by date then startTime
ScheduleSettings (loaded from ClinicConfig + ClinicHours + ClinicHoliday tables):
| Setting | Type | Description |
|---|---|---|
operatingHours |
{[day]: {isOpen, openTime, closeTime}} |
Per-day-of-week hours |
appointmentDuration |
number |
Default duration (minutes) |
bufferTime |
number |
Buffer between appointments (minutes) |
maxAdvanceBookingDays |
number |
How far ahead patients can book |
minAdvanceBookingHours |
number |
Minimum lead time for bookings |
holidays |
Array<{date, name}> |
Dates the clinic is closed |
allowSameDayBooking |
boolean |
|
allowWeekendBooking |
boolean |
|
maxApptsPerPatientPerDay |
number |
|
cancellationMinNoticeHours |
number |
|
newPatientRequiresLongerSlot |
boolean |
|
newPatientMinDuration |
number |
Minutes |
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 (growing to ~1,550 with REST extension)
Connection: Direct SOAP/WS-Security to OSCAR CXF endpoints — OR — OAuth 1.0a REST when preferRest=true
REST Protocol Extension (Production)
For Kai-hosted OSCAR instances behind Cloudflare WAF, the SOAP path is blocked. The preferRest: boolean flag routes all 11 methods to OAuth 1.0a REST (/ws/services/*) instead of SOAP. This is the Cortico integration model. 98 unit tests + 8 integration tests. See OSCAR API Protocol Analysis for the full technical analysis including CF WAF proof matrix, per-method routing table, and implementation plan.
When preferRest=true: All methods use oauthRestCall() helper → OAuth 1.0a signed JSON to /ws/services/* → separate restBreaker circuit breaker.
When preferRest=false (default): Zero changes to existing SOAP behavior. All existing code paths are 100% preserved.
SOAP Operations Actually Called (verified against source code):
| WSDL | Service | Operations Called |
|---|---|---|
/ws/ScheduleService?wsdl |
ScheduleService | getDayWorkSchedule, getAppointmentsForDateRangeAndProvider2, addAppointment, updateAppointment, getScheduleTemplateCodes |
/ws/DemographicService?wsdl |
DemographicService | getDemographic, searchDemographicByName |
/ws/ProviderService?wsdl |
ProviderService | getProviders2(true) |
Operations NOT Used
Despite being available in OSCAR SOAP, the adapter does not call: searchDemographicsByAttributes, getAppointmentsForProvider2, getAppointmentTypes, LoginService.login2, or SystemInfoService.isAlive. Authentication is handled via direct WS-Security credentials passed to soap.WSSecurity() -- there is no two-step LoginService flow.
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
}
No LoginService Flow
The adapter does NOT use the two-step LoginService.login2() authentication flow. It passes OSCAR WS-Security credentials (Security ID as username, password/token as password) directly via soap.WSSecurity() on every SOAP client. The healthCheck() method only verifies WSDL reachability by creating the schedule SOAP client -- it does not call SystemInfoService.isAlive.
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. Also handles ISO string extraction via regex match onT(\d{2}:\d{2}).normalizeDate(): Extracts YYYY-MM-DD from Date objects (viatoISOString()) or ISO strings.resolveScheduleCode(): Handles integer code point (JAXB standard) or char/string. UsesString.fromCharCode()for numeric values > 31 and < 128.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). New appointments are created withstatus: 't'(To Do).- Circuit breaker: Uses
opossumlibrary, per-service (schedule, demographic, provider). Timeout: 4000ms, errorThresholdPercentage: 50, resetTimeout: 30000ms. Must be under Vapi's 5s tool-call timeout. - Phone search: OSCAR SOAP has no phone search capability. Falls back to the REST bridge (
x-api-keyauth, 4s timeout via axios) for phone-based patient lookup when bridge credentials are configured. Returns empty array (not error) if bridge is not configured or fails. - Lazy SOAP client init: Clients are created on first use with
soap.createClientAsync(). Thesoapmodule itself is lazy-loaded (await import('soap')) to avoid CJS/ESM compatibility issues with soap'sget-streamdependency. - Clinic timezone: Configurable per-clinic (defaults to
America/Vancouver). Used innormalizeTime()for correct local time extraction. - Provider filtering:
getProviders()filters provider IDs to range 1-998 (excludes system providers like 999, 0). getProviderRoster(): Public convenience method that delegates togetProviders(). Used byOscarConfigServicefor provider roster sync.getAvailability()vs BookingEngine: The adapter-levelgetAvailability()marks slots as booked using simple startTime matching (bookedTimes.has(slot.startTime)). This is NOT overlap-aware. The BookingEngine'sgetTrueAvailability()performs proper interval overlap detection with buffer time. For booking decisions, always use the BookingEngine.- Template code cache: Loaded once on first schedule request via private
loadTemplateCodes(). Also populated as a side effect of the publicgetScheduleTemplateCodes()method. - HTTPS support: Accepts self-signed certificates via
{ rejectUnauthorized: false }WSDL options for HTTPS OSCAR instances.
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 all operations to an internal OscarService instance 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 |
|---|---|---|
getScheduleTemplateCodes() |
Full (code, duration, bookinglimit, confirm) | NOT IMPLEMENTED (optional method omitted) |
| Phone-based patient search | Via bridge fallback | Native (via OscarService) |
| Patient registration | Via OAuth 1.0a REST fallback | Via bridge |
getAppointment(id) |
NOT_SUPPORTED | NOT_SUPPORTED |
getAvailability() booked detection |
Simple startTime match | Delegates to bridge (template-only, no booked detection) |
getScheduleSlots() bookinglimit |
From OSCAR template codes (real values) | Hardcoded to 1 (bridge does not expose bookinglimit) |
| Appointment cancel | updateAppointment with status='C' |
Delegates to oscar.cancelAppointment() bridge endpoint |
Status 'P' mapping |
'completed' |
'scheduled' (not mapped) |
| Circuit breaker | Per-service via opossum (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. For self-hosted OSCAR, the SOAP API is the universal connector. For Kai-hosted OSCAR (Cloudflare-fronted), OAuth 1.0a REST is the only viable path — this is the Cortico model. See Protocol Analysis.
REST Protocol Extension (preferRest)¶
Added: 2026-03-03 | Status: Production (98 unit tests, 8 integration tests)
For Kai-hosted OSCAR instances behind Cloudflare WAF, the OscarSoapAdapter operates in REST mode when clinic_config.preferRest = true.
Adapter Factory Sequence¶
Source: EmrAdapterFactory.ts:43-130
getEmrAdapter(clinicId)
│
▼
┌──────────────────────────────────┐
│ Check adapterCache (5min TTL) │ line 44-48
│ Hit? → return cached adapter │
└──────────┬───────────────────────┘
│ miss
▼
┌──────────────────────────────────┐
│ Load ClinicConfig from DB │ lines 51-69
│ Load Clinic.timezone │ lines 72-75
│ Decrypt creds (AES-256-GCM) │ lines 78-86
└──────────┬───────────────────────┘
▼
┌──────────────────────────────────┐
│ resolveOAuthCreds() │ lines 142-186
│ │
│ Has clinic consumerKey in DB? │
│ YES → ALL 4 fields from DB │ lines 153-172
│ (NEVER mix with env) │
│ NO → ALL 4 fields from env │ lines 175-185
└──────────┬───────────────────────┘
▼
┌──────────────────────────────────┐
│ createAdapter(emrType, ...) │ lines 191-243
│ │
│ 'oscar-soap' → OscarSoapAdapter │
│ 'oscar-universal'/'oscar' │
│ → OscarBridgeAdapter │
│ unknown → OscarBridgeAdapter │
└──────────┬───────────────────────┘
▼
┌──────────────────────────────────┐
│ warmUp() │ lines 116-122
│ │
│ preferRest=true? │
│ YES → AWAIT warmUp (cold TLS) │ line 118
│ NO → fire-and-forget │ line 120
└──────────┬───────────────────────┘
▼
┌──────────────────────────────────┐
│ Cache adapter (5min TTL) │ lines 124-127
│ Return adapter │
└──────────────────────────────────┘
REST vs SOAP Decision¶
Source: OscarSoapAdapter.ts
Any adapter method called
│
▼
┌───────────────────────────────────┐
│ preferRest && oauthClient && │
│ oauthCreds available? │
└─────┬──────────────┬──────────────┘
│ YES │ NO
▼ ▼
┌──────────┐ ┌──────────────────┐
│ REST path │ │ SOAP path │
│ OAuth 1.0a│ │ WS-Security │
│ JSON/XML │ │ SOAP XML │
│ /services/│ │ /ws/{Service} │
│ restBreaker│ │ per-svc breaker │
└──────────┘ └──────────────────┘
REST Endpoint Map¶
When preferRest=true, all methods route to OAuth 1.0a REST endpoints:
| Method | REST Endpoint | HTTP | Notes |
|---|---|---|---|
searchPatient(name) |
/ws/services/demographics/quickSearch?query={term} |
GET | ?query= NOT ?searchString= |
searchPatient(phone) |
/ws/services/demographics/quickSearch?query={phone} |
GET | Falls back to /ws/rs/demographic/search |
getPatient(id) |
/ws/services/demographics/{id} |
GET | |
createPatient() |
/ws/services/demographics |
POST | Same path in both modes |
getProviders() |
/ws/services/providerService/providers |
GET | 3-tier fallback: JSON → XML → DB |
getScheduleSlots() |
/ws/services/schedule/{id}/day/{date} |
GET | |
getScheduleTemplateCodes() |
/ws/services/schedule/codes |
GET | Fallback: /scheduleTemplate |
getAppointments() |
/ws/services/schedule/{id}/day/{date} |
GET | Iterated per day in range |
createAppointment() |
/ws/services/schedule/add |
POST | NewAppointmentTo1 format |
cancelAppointment() |
/ws/services/schedule/appointment/{id}/updateStatus |
POST | Sets status='C' |
Provider 3-Tier Fallback¶
GET /ws/services/providerService/providers (JSON)
│
├─ 200 OK → parse JSON → return providers
│
├─ 406 (JAXB bug) → GET same URL with Accept: application/xml
│ └─ 200 → parseProvidersXml() → return providers
│
└─ 500/other → Direct DB query (if clinicId available)
└─ Query ClinicProvider table → return providers
schedule/add Format (NewAppointmentTo1)¶
The REST endpoint expects NewAppointmentTo1 (NOT AppointmentTo1):
| Field | Type | Required | Notes |
|---|---|---|---|
providerNo |
string | YES | Provider number |
demographicNo |
int | YES | Patient ID |
startTime |
string | YES | "HH:mm" (24h, no seconds) |
date |
string | YES | "YYYY-MM-DD" |
duration |
int | YES | Minutes (endTime calculated server-side) |
reason |
string | no | Visit reason |
type |
string | no | Appointment type code |
status |
string | YES | Must not be null (causes NPE); use "t" |
notes |
string | no |
warmUp Requirement¶
EmrAdapterFactory must await warmUp() for preferRest adapters. Cold TLS handshake to Kai's Cloudflare takes ~6 seconds, which exceeds the 4-second circuit breaker timeout. The factory now awaits warmUp on adapter creation (not fire-and-forget). Source: EmrAdapterFactory.ts:117-118.
PM2 startup also triggers warmUp via IIFE in index.ts.
Split Circuit Breakers¶
REST mode uses separate read/write circuit breakers so that POST 500 errors (e.g., schedule/add failures) don't trip the GET read breaker. Both use the same opossum config (4s timeout, 50% threshold, 30s reset).
Key Fixes¶
| Fix | Before | After |
|---|---|---|
| startTime normalization | .replace(/:\d{2}$/, '') → "14:00" became "14" |
.slice(0, 5) → "14:00" stays "14:00" |
| quickSearch parameter | ?searchString= (wrong) |
?query= (correct CXF @QueryParam annotation) |
| Provider JSON 406 | Request failed | 3-tier fallback: JSON → XML → DB |
| warmUp lifecycle | Fire-and-forget | Awaited for preferRest — prevents cold TLS exceeding breaker |
| OAuth scope mixing | Clinic consumer + env token = invalid signature | resolveOAuthCreds: ALL from clinic OR ALL from env, never mixed |
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