OSCAR Integration Analysis¶
VitaraVox Enterprise Readiness Analysis¶
Date: February 17, 2026¶
Agent: EMR Integration & Interoperability Analyst¶
OSCAR EMR Integration - Comprehensive Architecture & Quality Analysis¶
Executive Summary¶
The VitaraPlatform implements a production-grade OSCAR EMR integration spanning 3,120 lines of core adapter, service, and booking logic. The architecture demonstrates strong software engineering practices with clear separation of concerns, proper error handling, and comprehensive test coverage. However, there are several enterprise readiness gaps and operational concerns that require attention before multi-clinic, high-volume deployments.
1. SOAP Adapter Implementation Quality & Completeness¶
Overall Assessment: 8.5/10 - Production-Ready with Caveats¶
Strengths¶
1.1 Proper WS-Security Implementation
- Lines 275-276, 287-288, 299-300 (OscarSoapAdapter.ts): Correct OSCAR CXF WSSecurity configuration
- Uses hasTimeStamp: false — critical fix for OSCAR's unsigned timestamp handling
- passwordType: 'PasswordText', mustUnderstand: true follow OSCAR SOAP spec exactly
- No unnecessary hasNonce — streamlined for CXF compatibility
1.2 JAXB Deserialization Handling
- Lines 160-170 (resolveScheduleCode): Elegant handling of integer code points vs. character codes
- OSCAR sends schedule codes as JAXB Character serialized to integer (e.g., 76='L', 80='P')
- Test coverage (oscar-soap-schedule.test.ts line 81-92, 178-196) validates both integer and string representations
- toLocaleString('en-US', { timeZone }) properly reconstructs clinic-local times from JS Date objects
1.3 Timezone-Aware Datetime Handling
- Lines 88-117 (normalizeTime/normalizeDate): Robust parsing of ISO timestamps with timezone offsets
- Handles OSCAR's "2026-02-17T09:00:00-08:00" format + node-soap's Date deserialization
- Clinic timezone (line 238: 'America/Vancouver' default) is configurable per clinic
- Critical caveat: Timezone is currently hardcoded in OscarSoapAdapter, NOT pulled from clinic config
1.4 Comprehensive Error Handling - Lines 329-350 (createBreaker): Circuit breaker per SOAP service (schedule, demographic, provider) - 4-second timeout (line 333) stays under Vapi's 5s tool-call timeout - Graceful degradation: connection failures logged but don't crash; advisory lock failures proceed without lock (line 654-655 in booking.service.ts) - Lines 468-550 (getScheduleSlots): Field-mapping drift detection — warns if slots received but none mapped
1.5 Template Code Caching - Lines 354-385 (loadTemplateCodes): One-time cache load for schedule template codes (duration, bookinglimit, confirm constraint) - Lazy-loaded on first schedule request — eliminates cold-start WSDL fetch penalty after PM2 restart - Lines 445-453: Fallback defaults (15 min duration, 1 booking limit) if cache unavailable
Gaps & Weaknesses¶
1.6 Missing SOAP Client Pooling - Adapter creates 3 SOAP clients (schedule, demographic, provider) but does NOT pool them - Each adapter instance holds one client indefinitely - Risk: Under high concurrency (100+ simultaneous voice calls), each clinic gets separate client instances - No connection pooling in node-soap library itself - Potential TCP connection exhaustion if OSCAR connection limits low - Recommendation: Implement per-clinic SOAP client singleton cache with TTL refresh
1.7 Single-Appointment-by-ID NOT Supported
- Line 679-691 (getAppointment): Returns NOT_SUPPORTED error
- Reason: OSCAR SOAP has no getAppointmentById(id) endpoint
- Workaround: Must fetch appointments by provider+date range and filter
- Enterprise impact: Post-booking verification requires expensive multi-provider scan
- Vapi's post-booking check (if added) would require searching all providers for 7 days
- Slot collision detection uses this pattern (booking.service.ts line 207-215) — acceptable because limited to current date
1.8 Patient Registration via REST Fallback, NOT SOAP
- Lines 942-1053 (createPatient): Uses OSCAR REST API with OAuth 1.0a, not SOAP
- Reason: OSCAR SOAP has no addDemographic method
- Risk: Requires separate OAuth credentials (consumerKey/Secret, tokenKey/Secret)
- If OAuth creds not configured, registration fails (lines 946-955)
- Falls back to bridge adapter if available
- Enterprise concern: Patient registration requires SEPARATE credential setup from SOAP auth
1.9 Phone Search Delegates to Bridge
- Lines 856-902 (searchPatientByPhone): Fallback to REST bridge for phone search
- Reason: OSCAR SOAP searchDemographicByName does NOT support phone searches
- Workaround: Uses bridge's GET /demographics/search?phone=X (last-7-digits match)
- Risk: Creates hard dependency on bridge for phone-based lookups
- If bridge is down, phone search fails silently (line 900 returns empty array)
- Degrades to voice interaction requiring patient name instead
Code Quality Observations¶
- WSDL Lazy Loading (lines 270-303): Defers soap.createClientAsync until first request — good resource efficiency
- Self-Signed HTTPS Support (lines 263-268): Accepts self-signed certs for dev/testing via
rejectUnauthorized: false - ⚠️ Security note: Must be disabled in production; clinic certs should be trusted
2. Bridge Adapter as Fallback - Isolation & Risks¶
Assessment: 7/10 - Properly Layered but Overly Permissive Fallback¶
Architecture¶
2.1 Clean Abstraction Layer
- OscarBridgeAdapter implements same IEmrAdapter interface as OscarSoapAdapter (lines 22-296)
- Transforms bridge-specific types to canonical types (Patient, Provider, Appointment)
- EmrAdapterFactory (lines 133-161) correctly selects adapter based on clinic's emrType config
- Adapter cache with 5-minute TTL (lines 17-33 in EmrAdapterFactory.ts) prevents repeated instantiation
2.2 REST Bridge Client
- OscarService (lines 106-585 in oscar.service.ts) wraps REST bridge with circuit breaker
- 4-second timeout, 50% error threshold, 30-second reset (lines 116-124)
- All endpoints follow consistent pattern: GET|POST ${baseUrl}/endpoint with X-API-Key header
- Debug mode logs request/response payloads for troubleshooting (lines 138-180)
2.3 Supported Operations - Patient: searchPatients, findByPhone, findByName, getPatient, registerPatient, listPatients - Appointments: getAvailability, getAppointmentTypes, getAppointments, bookAppointment, updateAppointment, cancelAppointment - Providers: getProviders, getProvider - Clinical: getAllergies, getMeasurements, getPrescriptions (lines 488-550)
Isolation Concerns¶
2.4 Default Fallback Path
- EmrAdapterFactory line 158-159: Unknown EMR type falls back to OscarBridgeAdapter
- Environment default (env.ts line 65): DEFAULT_EMR_TYPE: 'oscar-universal' (bridge)
- Risk: If clinic config corrupts or clinic.timezone lookup fails, adapter silently falls back to bridge
- No explicit warning logged for fallback condition
2.5 Bookinglimit Loss
- OscarBridgeAdapter.getScheduleSlots (line 173): Hard-codes bookinglimit: 1
- Bridge doesn't expose per-slot booking limits (multi-book slots)
- BookingEngine's slot collision detection (booking.service.ts lines 250-253) can't enforce limits
- Correctly handles bookinglimit: 0 (unlimited) but bridge always returns 1
- Enterprise consequence: Clinics using bridge cannot allow double-booking of slots
2.6 No Template Code Metadata - Bridge adapter doesn't implement getScheduleTemplateCodes() method - SOAP adapter has full template code support (lines 391-436) with duration/bookinglimit metadata - Clinical implication: Bridge slots lack semantic meaning (Lunch vs. Regular vs. Urgent codes)
Error Handling¶
2.7 Silent Fallbacks - Phone search failure (oscar.service.ts line 235-243): Returns empty array, not error - Provider lookup failure: Returns empty provider list - Availability check failure: Returns empty time slots - Enterprise concern: Clinic staff may not detect bridge downtime — they see "no availability" instead of "system error"
2.8 No Graceful Degradation Path - If bridge is down AND no SOAP adapter configured, entire booking flow fails - No fallback to "contact clinic for availability" mode - Voice call drops with no patient notification
3. Appointment Booking Flow - Race Conditions & Slot Collision Handling¶
Assessment: 8/10 - Well-Designed with Edge Cases¶
Race Condition Prevention¶
3.1 PostgreSQL Advisory Locks
- BookingEngine.bookAppointment (lines 354-437 in booking.service.ts)
- Uses pg_try_advisory_lock(hashtext(key)) per provider+date+slot combination (line 363)
- Lock key: "provider:100:date:2026-02-20:slot:09:00" — uniquely identifies slot
- Non-blocking: Returns immediately if locked (line 369-373), graceful degradation
- Always released: Finally block (lines 434-436) ensures cleanup even on error
3.2 Two-Phase Booking
1. Acquire lock (attempt, timeout 1-2s depending on DB latency)
2. Fresh availability check — slot may have been taken since search
3. Create appointment in OSCAR
4. Release lock
3.3 Overlap Detection with Buffer Time - Lines 243-247: Overlapping appointment detection with buffer time
const apptStart = timeToMinutes(appt.startTime) - bufferTime;
const apptEnd = timeToMinutes(appt.endTime) + bufferTime;
return apptStart < slotEnd && apptEnd > slotStart;
Slot Collision Handling — Detailed Analysis¶
3.4 Per-Slot Bookinglimit Enforcement
- Lines 250-253 (booking.service.ts): if (limit > 0 && overlapping.length >= limit) continue;
- bookinglimit: 0 = unlimited (often used for walk-in/urgent slots)
- bookinglimit: 1 = single booking (standard)
- bookinglimit: N = up to N concurrent appointments in same slot
- SOAP adapter correctly retrieves this from OSCAR template codes (OscarSoapAdapter lines 517-526)
- Bridge adapter returns fixed 1 (OscarBridgeAdapter line 173) — limitation
3.5 Max Appointments Per Patient Per Day - Lines 391-404: Enforced at booking time (not at slot level)
if (settings && settings.maxApptsPerPatientPerDay > 0) {
const activeAppts = await emr.getAppointments({...});
if (activeAppts.length >= settings.maxApptsPerPatientPerDay) return error;
}
3.6 Holiday & Operating Hours Filters
- Lines 144-162: Holiday check before slots returned
- Lines 150-156: Operating hours enforcement per day-of-week
- Lines 159-162: Weekend booking disable check
- Lines 165-168: Same-day booking disable check
- All filters return empty array [] if policy violated
Weaknesses & Edge Cases¶
3.7 Clock Skew Tolerance - Tests assume system clock is accurate (±seconds) - Risk: If server clock drifts, minAdvanceBookingHours enforcement breaks - E.g., if server thinks it's 09:00 but it's actually 08:55, "no same-day" slots appear available - Real OSCAR has same issue; Vapi calls use Vapi-server time
3.8 Concurrent Reschedule Collisions - If two concurrent voice calls reschedule SAME appointment simultaneously: - Thread A: book new slot, cancel old - Thread B: book new slot, cancel old (same old ID) - Result: One patient has TWO new appointments; old one successfully cancelled by first thread - Mitigation: Weak — reschedule doesn't lock old appointment; only new slot locked - Enterprise concern: Rare edge case but possible with repeated voice call retries
3.9 SOAP Appointment Fetch Limitations
- getAppointments (line 621-676): Requires providerId, startDate, endDate (all mandatory)
- getAppointment (by ID alone): NOT_SUPPORTED (line 679-691)
- Consequence: Cannot verify single appointment post-booking without provider+date context
- Vapi webhook handles this by caching appointmentId in callMetadataCache (line 227)
3.10 Past Date Clamping — Server-Side Critical - vapi-webhook.ts line 863-868: Clamps past dates to today - Why needed: GPT-4o doesn't know current date reliably; may send "February 5th" when it's Feb 17 - SOAP adapter timezone issue: clinic timezone hardcoded → clamping always uses UTC - If clinic is Vancouver (PST) and server is UTC, clamping uses UTC "today" - Patient in Vancouver says "tomorrow" (PST), gets clipped to UTC "today" (different date if it's midnight boundary)
4. Patient Lookup - Privacy, Data Minimization & PHI Handling¶
Assessment: 7.5/10 - GDPR/PIPEDA-Conscious but Incomplete¶
Privacy Architecture¶
4.1 PHI Redaction in Logs
- vapi-webhook.ts lines 194-219: PHI_KEYS set defines sensitive fields
- Recursive redaction before logging: [REDACTED] replaces name, DOB, phone, email, address, etc.
- Debug mode (debugManager.isActive()) preserves PHI for troubleshooting
- Compliance note: Logs sent to PM2 via stdout — recommend external log aggregation with encryption
4.2 Server-Side Phone Number Resolution
- vapi-webhook.ts lines 602-614: Caller's real phone comes from Telnyx metadata, NOT user speech
- Voice assistant doesn't know the caller's phone — LLM can't send it
- Server extracts call.customer.number automatically, overrides any LLM-sent phone
- Privacy benefit: Patient doesn't need to speak their phone number; extracted from call metadata
4.3 Patient Search Queries — Multi-Field Support - SOAP: searchDemographicByName (term-based: name, HIN, chart number) — NO phone search - Bridge: quickSearch + findByPhone (both supported) - Data minimization: Don't search by phone if possible; prefer name+DOB for privacy
Search Query Handling¶
4.4 Last-7-Digits Phone Matching - Bridge GET /demographics/search?phone=X uses last-7-digits database match - SOAP delegates to bridge for phone search (line 856-902) - Privacy consideration: Last-7 is low-entropy; could match multiple patients in small clinics - Mitigated by DOB filtering (line 834-836): if provided, filters to exact DOB match
4.5 Name-Based Search Limitations - Both adapters use free-text search (full name or last name) - SOAP getSearchDemographicByName (line 821): limit 20 results - Bridge quickSearch (line 210): limit 20 results (configurable) - Risk: Returns multiple candidates; LLM asked to disambiguate with DOB
4.6 No PII Pre-Check - Patient search does NOT validate query legitimacy before sending to OSCAR - E.g., anyone who calls voice line can search for "John Smith" - OSCAR should enforce clinic access controls; not validated client-side
Data Retention & Cleanup¶
4.7 Call Metadata Cache - callMetadataCache (vapi-webhook.ts lines 227, 229-247) - Stores: language, outcome, demographicId, appointmentId - Auto-expires after 30 minutes (line 230) - Swept on 1% of calls for cleanup (lines 240-245) - Risk: If call never ends (hung call), entry stays for 30 min - Could accumulate demographicId entries (low-volume but visible in memory)
4.8 Post-Call Log Storage - saveCallLog (end-of-call-report handler) stores full transcript, call summary - ⚠️ Healthcare-regulated: Transcripts may contain patient health info - Recommend: Crypto storage with audit logging
Missing Privacy Features¶
4.9 No Consent Verification - Caller phone is auto-detected but no verification call is authentic patient - E.g., attacker calls with spoofed Telnyx number → gets access to patient data - Mitigation: Vapi should enforce Telnyx caller ID verification; not implemented in adapter
4.10 No Rate Limiting on Patient Search - Patient search endpoint not rate-limited - Attacker could enumerate all patient names via repeated searches - Recommendation: Implement per-clinic-per-hour search quota
5. Provider & Schedule Management¶
Assessment: 8/10 - Complete but Inflexible¶
Provider Operations¶
5.1 Provider Roster Fetching - SOAP: getProviders2(activeOnly=true) — filters to active providers only (line 1062) - Bridge: getProviders() endpoint - Filter logic (lines 1074-1079): Only include providers with ID 1-998 - Excludes system providers (0, 999, 1000+) - Hard-coded but reasonable for OSCAR's numbering scheme
5.2 Single-Provider Lookup - SOAP: getProvider(id) not directly supported - Workaround (lines 1094-1102): Fetch all, filter by ID - Inefficient: Full roster fetch just to get one provider - Enterprise impact: 500-provider clinic pays 500x penalty for single lookup - Bridge: getProvider(id) direct endpoint
5.3 Provider Display Names
- Transformer (line 1206): Dr. ${firstName} ${lastName}
- Issue: Hardcoded "Dr." prefix
- Does not handle nurses (NP), physician assistants (PA), etc.
- Non-physician providers display as "Dr. Jane Smith" (incorrect)
- Recommendation: OSCAR should provide providerTitle field; fallback to "Dr." only if missing
Schedule Template Management¶
5.4 Schedule Codes - OSCAR uses 1-character codes for appointment types (1, C, P, L, V, etc.) - Lines 61-69 (OscarSoapAdapter.ts): Default non-bookable codes: L (Lunch), P (Phone), V (Vacation) - Extended list noted: A, a, B, H, R, E, G, M, m, d, t (per memory.md) - BlockedScheduleCodes configurable per clinic (EmrAdapterFactory line 82-84)
5.5 Schedule Template Code Pull - getScheduleTemplateCodes() (lines 391-436): SOAP-only method - Returns code, description, duration, bookinglimit, confirm (booking constraint) - Used by onboarding.service to populate clinicConfig.blockedScheduleCodes - Limitation: Bridge adapter cannot fetch template codes — clinic must manually configure
5.6 Schedule Availability Calculation - getScheduleSlots (lines 457-550): Returns template slots (what's ALLOWED) - getAvailability (lines 553-616): Returns actual availability (ALLOWED - BOOKED) - Difference: getTrueAvailability (BookingEngine) applies additional filters (buffer, operating hours, advance limits)
6. Error Handling for OSCAR Downtime¶
Assessment: 7/10 - Adequate Degradation, Missing Observability¶
Circuit Breaker Strategy¶
6.1 Per-Service Breakers - 3 circuit breakers (schedule, demographic, provider) — lines 308-327 - Each with 4-second timeout (under Vapi's 5s limit) - 50% error threshold before opening (line 334) - 30-second reset timeout (line 335) - Benefit: Partial degradation — if provider service is slow, schedule service remains fast
6.2 Graceful Timeouts - getAllergies, getMeasurements, getPrescriptions (oscar.service.ts) — wrapped in breaker - 4-second timeout means Vapi call times out at ~5-6s total (Vapi's own timeout) - Patient hears: "I'm having trouble accessing the system. Please try again later."
Failure Modes¶
6.3 OSCAR Totally Down - All service calls fail - Breaker opens (line 339: 'open' event) - Subsequent calls fast-fail (within milliseconds) - User gets "system unavailable" message - Duration: 30 seconds until half-open retry
6.4 OSCAR Slow But Responsive - Calls take 3-4 seconds (near timeout) - Accumulate 50% error rate threshold - Breaker opens after 5-10 slow requests - Risk: If OSCAR is flaky (50% fast, 50% slow), breaker toggles on/off repeatedly - Could cascade to patient frustration ("try again" loops)
6.5 Bridge Down, SOAP Available - If bridge is default adapter AND down, clinic falls back to ??? - Currently: silently fails (no automatic fallback in factory) - Recommendation: Implement per-clinic fallback adapter chain
Observability Gaps¶
6.6 Missing Metrics - No latency histogram (Prometheus/StatsD) - No circuit breaker state transitions exposed - Logs only on state change ('open', 'half-open', 'close') — line 339-347 - Enterprise concern: Cannot alert on degradation before it affects patients
6.7 Missing Health Check Aggregation - Individual adapter healthCheck() methods exist (lines 1117-1136) - Admin GET /api/oscar/health returns raw health (api.ts line 35-46) - No centralized health dashboard showing all 3 services (schedule, demographic, provider) separately - Recommendation: Implement structured health check with component-level status
7. Data Transformation - OSCAR to Canonical Formats¶
Assessment: 8.5/10 - Robust with Field-Mapping Drift Risk¶
Patient Transformation¶
7.1 Field Mapping
OscarSoapAdapter.transformPatient (lines 1140-1166):
demographicNo → id (OSCAR key)
firstName → firstName
dateOfBirth (or dob) → dateOfBirth (normalized)
sex/gender → gender (M/F/O)
phone (or phone2) → phone
address → address.street
city, province, postal → address.*
hin → healthCardNumber
hcType → healthCardProvince
demographicNo vs. id)
- Flexible: multiple source fields per target (sex OR gender)
- Issue: dateOfBirth normalize uses normalizeDate() — assumes ISO or Date object
- If OSCAR returns different format (e.g., "1990-05-15"), will be lost
7.2 Appointment Transformation - Lines 1168-1196: Handles appointmentNo, startTime/appointmentStartDateTime variants - Status mapping: C→cancelled, N→no_show, P→completed, else→scheduled - Duration computed from startTime+endTime if missing (line 1184) - Risk: Assumes endTime is always present or duration is calculable - If OSCAR returns partial data, duration defaults to 15 min
7.3 Provider Transformation
- Lines 1199-1210: Minimal transformation
- Display name: Dr. ${firstName} ${lastName} (hardcoded "Dr.")
- acceptingNewPatients: hardcoded true — never false
- Enterprise concern: Cannot distinguish accepting vs. closed providers
Timezone Transformation¶
7.4 Clinic Timezone Context
- Adapter initialized with timezone (line 238): 'America/Vancouver' default
- normalizeTime uses toLocaleString with timeZone (line 98-109)
- Issue: Timezone passed to adapter, but stored in memory — NOT persisted
- If adapter instance dies and recreates, uses ONLY env-var default (not clinic config)
- EMR factory loads clinic timezone from DB (line 65) but passes to adapter correctly
8. Timezone Handling - Critical Gap¶
Assessment: 6/10 - Partially Implemented, Multi-Layer Risk¶
Current Implementation¶
8.1 Clinic Timezone Storage
- Database: clinic.timezone field (EMR factory line 65: clinic?.timezone || 'America/Vancouver')
- Default: 'America/Vancouver' hardcoded (assumes all clinics are Pacific)
- Risk: Clinics in Atlantic, Mountain, Eastern timezones get wrong schedule times
8.2 SOAP Adapter Timezone Usage - Lines 88-117: normalizeTime correctly uses clinic timezone for JS Date → HH:mm conversion - Lines 238: Constructor accepts timezone option - Correct usage: For schedule slots and appointments, times normalized to clinic timezone
8.3 Booking Engine Timezone Issues
- BookingEngine (lines 72-111): All times in HH:mm format, dates in YYYY-MM-DD
- Day-of-week calculation (line 109-111): Uses UTC date parsing (line 87)
- new Date(\${d}T00:00:00Z`)` — always UTC midnight
- If clinic is PST (UTC-8), "today" in PST is "tomorrow" in UTC
- Day-of-week filter could return wrong day
- minAdvanceBookingHours (lines 173-179): Uses server's local timezone for "now"
- Server running in UTC → clinic in PST → "now" is 8 hours behind
- "2 hours advance" booking at PST 09:00 may be rejected if server clock is UTC
Missing Timezone Logic¶
8.4 Vapi Webhook Timezone Aware - vapi-webhook.ts line 749-750: Date parsing
const appointmentDate = new Date(rawTime);
dateStr = appointmentDate.toISOString().split('T')[0]; // UTC date!
8.5 Operating Hours Filter Timezone - BookingEngine.getTrueAvailability line 151-156: Checks dayOfWeek(date) - dayOfWeek (line 109-111) uses UTC midnight - Consequence: If clinic is PST and it's 11 PM PST Wed (3 AM UTC Thu), "Wednesday hours" won't apply - Wednesday night 11 PM bookings would be filtered by Thursday hours
Recommendations¶
- Standardize all time handling to UTC internally
- Convert to clinic timezone ONLY for display to Vapi voice
- Store booking requests with explicit clinic timezone in DB
- Test suite should include multi-timezone test cases
9. Connection Pooling & WSDL Caching¶
Assessment: 6/10 - Lazy Initialization but No True Pooling¶
Current Approach¶
9.1 Per-Adapter SOAP Clients - OscarSoapAdapter stores 3 singleton client objects (lines 222-224) - Created lazily on first use (getScheduleClient, getDemographicClient, getProviderClient) - Cached in adapter instance memory - EMR adapter cache (EmrAdapterFactory) keeps adapter per clinic for 5 minutes (lines 25, 90-92)
9.2 WSDL Fetch Optimization
- Lines 273-274: soap.createClientAsync(wsdlUrl)
- Fetches WSDL once per adapter instance creation
- Subsequent calls reuse cached client
- node-soap library caches WSDL internally via HTTP headers (ETag/Last-Modified)
Pooling Limitations¶
9.3 No Connection Pooling - node-soap uses Node's http module internally (no custom connection pooling) - Each client maintains ONE implicit TCP connection - If 100 concurrent voice calls → 100 clinic instances → 300 concurrent TCP connections (3 services × 100) - Risk: OSCAR connection limits (typically 50-200 connections) - Once limit hit, new calls queue or timeout - No backpressure mechanism to shed load gracefully
9.4 Recommended Pooling Architecture
Option A: Shared SOAP Client Pool per Clinic
- Global cache: Map<clinicId, SoapClientPool>
- Pool maintains 5-10 reusable client instances
- Each client has acquire/release lifecycle
- TTL refresh: recreate clients every 24 hours
Option B: Connection Pool at Node.js HTTP Level
- Configure Agent with maxSockets per clinic
- Limits concurrent connections by circuit breaker timeout
9.5 Current Degrade Mode - If all connections busy, node-soap queues requests (Node's http default) - Circuit breaker timeout (4s) eventually fails the request - Vapi retrys or gives up → patient hears error
10. Missing Enterprise Operations¶
Assessment: 5/10 - Core Features Present, Enterprise Gaps Significant¶
Implemented Features¶
✓ Patient search (name, phone, health card)
✓ Provider roster
✓ Schedule template codes (SOAP only)
✓ Appointment CRUD (create, read, cancel, update/reschedule)
✓ Availability calculation with buffering & bookinglimit
✓ Advisory locking for race prevention
✓ Multi-clinic support (per-clinic adapter, config)
Missing Enterprise Operations¶
10.1 Bulk Operations - ❌ Bulk appointment cancellation (e.g., "cancel all 9 AM slots on Friday") - ❌ Bulk provider availability update (e.g., "provider X on vacation Feb 20-28") - ❌ Bulk patient registration import - Impact: Staff must process one-at-a-time via voice or manual OSCAR access
10.2 Inventory Management - ❌ Resource booking (e.g., exam rooms, equipment) - ❌ Room unavailability (e.g., "Room 3 under maintenance Feb 15-20") - ❌ Equipment constraints (e.g., "Only ultrasound slots in Room 1") - Impact: Can book patients but cannot enforce resource constraints
10.3 Multi-Provider Coordination - ❌ Group appointments (e.g., Dr. A + Dr. B required for procedure) - ❌ Double-booking rules (e.g., "Dr. A must have Dr. B within 30 min") - Impact: Complex care workflows cannot be modeled
10.4 Reporting & Analytics - ❌ Appointment utilization reports (no-shows, cancellations by provider/day/time) - ❌ Overbooking patterns - ❌ Revenue impact analysis (slots booked × appointment value) - Impact: Clinic management blind to scheduling efficiency
10.5 Custom Rules Engine - ❌ Conditional availability (e.g., "new patients only on Tue/Thu 2-3 PM") - ❌ Provider-specific rules (e.g., "Dr. Chen 2-slot for new patients") - ❌ Patient class rules (e.g., "seniors get morning slots") - Impact: Clinics cannot implement nuanced policies
10.6 Integration & Webhooks - ❌ Outbound webhooks on appointment state changes - ❌ Integration with SMS/email reminders - ❌ Bi-directional sync (e.g., if clinic staff books in OSCAR, reflected in voice system) - Impact: Voice system is write-only; cannot detect manual OSCAR changes
10.7 Capacity Planning - ❌ Forecasting demand vs. supply - ❌ Alerts for under-booked slots - ❌ Recommendations for additional providers/hours - Impact: Clinic management cannot optimize scheduling
10.8 Multi-Language Support (Partial) - ✓ v3.0 architecture supports EN/ZH prompts - ✓ Language detection in Router assistant - ❌ Multilingual OSCAR data (e.g., appointment type names in French) - ❌ Timezone/regional calendar awareness (e.g., holidays differ by province)
10.9 Compliance & Audit - ✓ Call logging with transcript/summary - ✓ PHI redaction in logs - ❌ Detailed audit trail (who changed what appointment, when) - ❌ Access control enforcement (role-based booking restrictions) - ❌ Consent logging (voice confirmation recorded for booking)
10.10 Disaster Recovery - ❌ Appointment backup/restore - ❌ OSCAR failover (e.g., clinic has 2 OSCAR instances) - ❌ Data loss detection (e.g., alert if X appointments disappeared) - ❌ Sync recovery (voice system ↔ OSCAR reconciliation)
11. Test Coverage & Known Issues¶
Assessment: 7/10 - Unit Tests Present, Integration/E2E Minimal¶
Test Files¶
11.1 Unit Tests
- oscar-soap-schedule.test.ts: 11 tests covering schedule slot mapping, code resolution, timezone handling
- booking.service.test.ts: Partial test suite for getTrueAvailability (conflict detection, overlap logic)
- oscar.service.test.ts: Mock tests for bridge service
- clinic.service.test.ts: Clinic CRUD operations
11.2 Coverage Gaps - ❌ Integration tests (adapter + booking engine + OSCAR) - ❌ E2E tests (voice call → booking → OSCAR verification) - ❌ Multi-clinic isolation tests - ❌ Race condition reproduction tests (concurrent bookings) - ❌ Timezone boundary tests (DST transitions, international clinics) - ❌ OSCAR downtime/recovery scenarios - ❌ Malformed SOAP response handling
Known Issues from Code Review¶
11.3 Field Mapping Canary - OscarSoapAdapter line 529-535: Field drift detection - If OSCAR WSDL changes field names, adapter logs warning: "All schedule slots skipped — possible field mapping issue" - This is reactive (only caught when zero slots map); proactive test would help
11.4 JAXB Deserialization Edge Case
- normalizeTime line 95-111: Handles both Date objects and ISO strings
- Test (oscar-soap-schedule.test.ts line 178-196) validates Date deserialization
- But toLocaleString locale-specific behavior not tested (may differ by Node.js version)
11.5 Timezone Edge Case — Dawn Boundary
- If patient calls at 11:59 PM PST Wed, books for "tomorrow," what date is stored?
- vapi-webhook.ts line 749: new Date(rawTime).toISOString().split('T')[0]
- If LLM sends "2026-02-19T00:00:00-08:00" (midnight PST), toISOString → "2026-02-19T08:00:00Z" → "2026-02-19" ✓
- If LLM sends "2026-02-19T23:59:00-08:00" (11:59 PM PST), toISOString → "2026-02-20T07:59:00Z" → "2026-02-20" ✗ (one day ahead!)
- Observed risk: Not tested
12. Security Considerations¶
Assessment: 7.5/10 - Encryption Present, Access Control Weak¶
Credential Management¶
12.1 Encrypted Secrets in DB
- SOAP credentials encrypted in clinicConfig (lines 73-76 in EmrAdapterFactory.ts)
- decrypt() function used for oscarConsumerKey/Secret, oscarToken
- Encryption key from config.encryptionKey (env.ts line 33-35)
- Strength*: AES-256 if ENCRYPTION_KEY is 64-char hex; runtime validation fails if not
12.2 Development Plaintext Risk
- env.ts line 35: z.string().default('') if not production
- If not set: encryption disabled; secrets stored plaintext
- ⚠️ Not for clinics; dev-only fallback
WSDL & SOAP Security¶
12.3 Self-Signed Certificate Acceptance
- OscarSoapAdapter line 265: rejectUnauthorized: false
- Accepts ANY certificate (including man-in-the-middle attacks)
- ⚠️ Must be disabled in production via config
12.4 OAuth 1.0a Implementation - Uses HMAC-SHA1 signature (line 252) - Signatures validated by OSCAR server - Risk: Tokens stored in database plaintext (future: encrypt like SOAP passwords)
Access Control¶
12.5 No Role-Based Checks - Vapi webhook routes all tool calls directly to EMR (no permission check) - Patient calling voice line can book ANY provider, ANY date, ANY time (subject only to availability) - Recommendation: Implement patient-provider relationship checking (is this patient assigned to Dr. X?)
12.6 No Clinic Isolation Verification
- EMR adapter factory loads clinic from clinicId in call metadata
- Assumes Vapi phone number → clinic mapping is authoritative
- Risk: If attacker knows clinic's Vapi phone number ID, can book for any patient
- Mitigation: Vapi should enforce caller ID verification (not implemented in adapter)
12.7 No Audit Logging of Access - Call logging captures appointmentId, demographicId - Does NOT log "user X searched for patient Y at time Z" - Cannot trace data access patterns for compliance
Summary: Enterprise Readiness Scorecard¶
| Dimension | Score | Status |
|---|---|---|
| SOAP Adapter Quality | 8.5/10 | Production-ready; minor pooling gaps |
| Bridge Adapter Fallback | 7/10 | Well-layered; limited feature set |
| Race Condition Handling | 8/10 | Advisory locks + 2-phase booking; edge cases exist |
| Patient Privacy | 7.5/10 | PHI redaction good; phone verification missing |
| Provider Management | 8/10 | Roster + single lookup working; single-lookupinefficient |
| Timezone Handling | 6/10 | Partially implemented; DST/boundary bugs likely |
| Error Handling | 7/10 | Circuit breaker per service; missing observability |
| Data Transformation | 8.5/10 | Robust; field-mapping drift risk |
| Connection Pooling | 6/10 | No true pooling; may exhaust OSCAR connection limits |
| Enterprise Operations | 5/10 | Core CRUD working; missing bulk, capacity, compliance features |
| Testing | 7/10 | Unit tests good; missing integration/E2E |
| Security | 7.5/10 | Encryption present; access control weak |
| OVERALL SCORE | 7.3/10 | Ready for single-clinic pilot; NOT production-ready for enterprise scale |
Critical Recommendations Before Enterprise Deployment¶
P0 (Blocking)¶
- Implement true SOAP client connection pooling — prevent connection exhaustion
- Fix timezone handling — standardize to UTC, convert for display only; test DST boundaries
- Add clinic access control — verify patient-provider relationship before booking
- Implement multi-clinic connection limits — prevent one clinic from starving others
P1 (High Priority)¶
- Implement single-appointment post-booking verification — add getAppointmentById endpoint to OSCAR bridge or cache appointmentNo in database
- Add per-clinic rate limiting — prevent patient enumeration attacks
- Implement observability — Prometheus metrics for latency, circuit breaker state, error rates
- Improve provider lookup efficiency — cache provider roster with 5-minute TTL
- Document timezone assumptions — clearly state all times are clinic-local unless marked UTC
- Add overbooking detection alerts — notify clinic if slots consistently exceed bookinglimit
P2 (Medium Priority)¶
- Implement bulk operations — bulk cancel, bulk availability update
- Add resource/room inventory — extend booking to include room constraints
- Create audit trail dashboard — searchable log of who booked/modified/cancelled what
- Implement bi-directional sync — detect manual OSCAR changes; warn voice system
- Add capacity planning tools — forecast demand vs. available slots
Code Quality Highlights¶
Strengths: - Clean adapter pattern with canonical types (IEmrAdapter) - Comprehensive error handling with circuit breakers - Thoughtful timezone handling (where implemented) - Good test coverage for SOAP transformation logic - PHI-aware logging with redaction
Areas for Improvement: - Document API contracts (what does getAppointments return if date range has no appointments?) - Add integration tests (adapter + booking engine + OSCAR flow) - Refactor BookingEngine to be timezone-aware throughout - Centralize timezone logic; avoid duplication - Add observability (structured logging, metrics)
Conclusion¶
The OSCAR EMR integration demonstrates strong software engineering with clean architecture, proper error handling, and security-conscious design. The core booking flow is production-quality for single-clinic deployments with experienced staff managing edge cases.
However, enterprise readiness is limited:
- Connection pooling gaps could cause outages under load
- Timezone bugs will cause booking errors across international clinics
- Missing compliance features (audit trails, access control) prevent healthcare deployment
- No multi-clinic isolation enforcement risks data leaks
- Limited business intelligence leaves clinic managers blind
Recommendation: Deploy to 1-2 pilot clinics in North America (single timezone), add the P0/P1 items above, then scale to enterprise deployment.