Skip to content

Oscar → FHIR Mapping (Schedule + Slot)

This page describes how OscarPro's scheduletemplate family of tables maps to the new NexusEmrCoreSchedule and NexusEmrCoreSlot profiles. The structured mapping is in oscar-to-fhir.yaml; this page is the narrative + edge-case companion.

Phase 1 status (2026-05-26): OscarPro already has partial Schedule/Slot converter code committed (ScheduleConverter.java, SlotConverter.java, ScheduleTemplateCodeConverter.java). The migration spec in converter-contracts.md enumerates the 10 deltas required to bring it into profile compliance. This mapping doc describes the target state once the migration is complete.

Source tables (OscarPro)

Table Role
scheduletemplate One row per (provider_no, template_name). Carries the timecode string.
scheduletemplatecode Lookup of single-character codedescription, duration, color, bookingLimit.
scheduledate Assigns a template to a (provider_no, sdate) tuple. Carries available (within-day blockout flag) and reason.
scheduleholiday Holiday calendar — keyed by sdate, with holiday_name.
appointment Booked appointments — used for the busy/free overlay on Slots.

Materialisation model

The converter produces:

  • One Schedule resource per (provider_no), covering the rolling planning horizon (typically 90 days). Multiple scheduletemplate rows for the same provider collapse into one Schedule — the template's summary may land in Schedule.comment.
  • One Slot resource per non-_ character of timecode, per scheduledate row. A 96-character timecode with 24 non-_ chars produces 24 Slots per day.

Timecode arithmetic

The timecode string is interpreted as a per-15-minute grid (or other granularity — the existing converter uses slotDuration = 1440 / timecode.length()). For position i (0-indexed) in a timecode of length L:

  • slot.start = scheduledate.sdate (at 00:00 local) + (i × (1440 / L)) minutes
  • slot.end = slot.start + (1440 / L) minutes
  • slot.serviceType.coding[rawCode] = lookup scheduletemplatecode by code = timecode[i]

If timecode[i] == '_', no Slot is emitted at that position — '_' is the "no slot here" convention.

Midnight overflow

LocalTime.plusMinutes() wraps at midnight, which corrupts the end timestamp of the last slot in a full 24-hour grid. The migrated converter must use LocalDateTime + Duration arithmetic to advance the date correctly. See converter-contracts.md §"SlotConverter.java" item 5 for the fix.

Timezone resolution

Slot.start and Slot.end are FHIR instant values (UTC with offset). The source data is clinic-local; the converter derives the timezone from the clinic's Location.address province via a lookup table:

Province IANA TZ
ON America/Toronto
AB America/Edmonton
BC America/Vancouver
(etc. — see converter-contracts.md)

ZoneId.systemDefault() MUST NOT be used.

Slot status — busy/free overlay

The converter computes Slot.status as follows:

  1. Default: free
  2. If scheduledate.available != '1' (within-day blockout): busy-unavailable, with scheduledate.reason carried in Slot.comment
  3. Otherwise, query the appointment table for rows where:

    • appointment.status NOT IN ('cancelled', 'noshow')
    • (appointment.start_time, appointment.end_time) overlaps [Slot.start, Slot.end)

    If any row matches: busy

The converter never writes busy-tentative — that status belongs to the booking system. See Booking Flow Design for the full status state machine.

Holiday handling

If the date appears in scheduleholiday, the converter emits zero Slots for the entire day. This is preferred over emitting a stream of busy-unavailable Slots (which would create dozens of phantom resources for every grid position). The absence of Slots is the signal that the clinic isn't bookable.

Slot identifier strategy — determinism

Slots are derived, not stored. To support cache-by-reference and idempotent re-emission, Slot.id and Slot.identifier.value use:

<provider_no>-<yyyyMMdd>-<slot-index>

Example: 42-20260601-36 is the 36th slot in Dr. Navarro's June 1, 2026 schedule. Re-running the converter for the same (provider, date) produces identical resources.

What's NOT mapped (intentionally)

Field Why
scheduletemplatecode.color UI hint; not clinically meaningful for booking
scheduletemplatecode.bookingLimit Not surfaced in Phase 1; reconsider if overbooking semantics need explicit constraint
scheduletemplate.summary Not mapped on Schedule.comment in Phase 1 (could be added if useful)
scheduledate.creator / scheduledate.priority Operational metadata, not booking-relevant
scheduletemplatecode.duration (as a per-Slot field) Slot duration is computed from timecode.length(), not the per-code duration. The per-code duration is OscarPro's intra-grid hint, not a Slot-level field.

Worked example

See NexusEmrSchedule-example.json and the three Slot examples (free, busy, busy-tentative) for the wire format the migrated converter targets.