Skip to content

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. Uses toLocaleString('en-US', { timeZone }) to extract clinic-local HH:mm from Date objects. Also handles ISO string extraction via regex match on T(\d{2}:\d{2}).
  • normalizeDate(): Extracts YYYY-MM-DD from Date objects (via toISOString()) or ISO strings.
  • resolveScheduleCode(): Handles integer code point (JAXB standard) or char/string. Uses String.fromCharCode() for numeric values > 31 and < 128.
  • 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). New appointments are created with status: 't' (To Do).
  • Circuit breaker: Uses opossum library, 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-key auth, 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(). The soap module itself is lazy-loaded (await import('soap')) to avoid CJS/ESM compatibility issues with soap's get-stream dependency.
  • Clinic timezone: Configurable per-clinic (defaults to America/Vancouver). Used in normalizeTime() 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 to getProviders(). Used by OscarConfigService for provider roster sync.
  • getAvailability() vs BookingEngine: The adapter-level getAvailability() marks slots as booked using simple startTime matching (bookedTimes.has(slot.startTime)). This is NOT overlap-aware. The BookingEngine's getTrueAvailability() 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 public getScheduleTemplateCodes() 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):

  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. 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