# Mapping from OscarPro schedule template tables to Nexus EMR FHIR Schedule + Slot.
# Phase 1 source: OscarPro. See oscar-to-fhir.md for narrative + edge-case rules.
# Symbol legend:
#   FHIRPath              — literal mapping
#   **derived**           — computed from one or more source fields (formula in narrative)
#   **converter-only**    — produced by converter logic, not a direct source field
#   **constant**          — hardcoded by the converter
#   **UNMAPPED**          — intentionally not emitted in Phase 1

Schedule_Mapping:
  # === NexusEmrCoreSchedule ===

  # Identifier
  Schedule.id: "**derived**: <provider_no>"  # deterministic; re-emission stable
  Schedule.identifier[rawCode].system: "**constant**: https://fhir.apps.health/NamingSystem/<instance-id>-schedule-raw-code"
  Schedule.identifier[rawCode].value: "**derived**: <provider_no>-<template_name>"
  Schedule.identifier[rawCode].use: "**constant**: secondary"

  # Actor — Phase 1 emits PractitionerRole + optional Location only.
  # HealthcareService slice supported by profile but not produced by OscarPro converter.
  Schedule.actor[PractitionerRole].reference: "**derived**: PractitionerRole/<provider_no>"
  Schedule.actor[Location].reference: "**derived**: Location/<primary clinic location id, if available>"
  Schedule.actor[HealthcareService]: "**UNMAPPED** (Phase 1; reserved for service-line schedules in future EMRs)"

  # Status / horizon
  Schedule.active: "**constant**: true"   # all emitted schedules are active in Phase 1
  Schedule.planningHorizon.start: "**derived**: earliest scheduledate.sdate across the materialised batch"
  Schedule.planningHorizon.end: "**derived**: latest scheduledate.sdate across the materialised batch"

  # Service classification
  Schedule.serviceCategory.coding[standard]: "**UNMAPPED** (Phase 1; populate when standard mapping exists)"
  Schedule.serviceCategory.coding[rawCode].system: "**constant**: https://fhir.apps.health/NamingSystem/<instance-id>-schedule-servicecategory-raw-code"
  Schedule.serviceCategory.coding[rawCode].code: "**UNMAPPED** (Phase 1; consider deriving from provider's primary specialty)"
  Schedule.serviceType.coding[standard]: "**UNMAPPED** (Phase 1)"
  Schedule.serviceType.coding[rawCode]: "**UNMAPPED** (Phase 1; populate per-template if templates carry service-type metadata)"

  # Narrative
  Schedule.comment: scheduletemplate.summary

Slot_Mapping:
  # === NexusEmrCoreSlot ===
  # Slots are materialised — one Slot per non-'_' character in scheduletemplate.timecode,
  # per scheduledate row. Holiday days produce no Slots (see holiday rule below).

  # Identifier — deterministic so re-emission is idempotent
  Slot.id: "**derived**: <provider_no>-<yyyyMMdd>-<slot-index>"
  Slot.identifier[rawCode].system: "**constant**: https://fhir.apps.health/NamingSystem/<instance-id>-slot-raw-code"
  Slot.identifier[rawCode].value: "**derived**: <yyyyMMdd>-<HH:mm>-<timecode-char>"
  Slot.identifier[rawCode].use: "**constant**: secondary"

  # Schedule reference — required by base FHIR (1..1)
  Slot.schedule.reference: "**derived**: Schedule/<provider_no>  (the parent Schedule for this provider)"

  # Status — converter writes free/busy/busy-unavailable; never busy-tentative
  Slot.status: "**converter-only**: 'free' default; 'busy' if appointment row overlaps [Slot.start, Slot.end); 'busy-unavailable' if scheduledate.available != '1'"

  # Window — derived from timecode position; see oscar-to-fhir.md §"Timecode arithmetic"
  Slot.start: "**derived**: scheduledate.sdate (00:00 local) + (slot-index × slotDurationMinutes); convert to instant using clinic timezone"
  Slot.end: "**derived**: Slot.start + slotDurationMinutes; use LocalDateTime.plus(Duration) to handle midnight overflow correctly"

  # Coded values from scheduletemplatecode lookup
  # rawCode slice is 0..* (not 1..*) — lookup misses are tolerated
  Slot.serviceType.coding[standard]: "**UNMAPPED** (Phase 1)"
  Slot.serviceType.coding[rawCode].system: "**constant**: https://fhir.apps.health/NamingSystem/<instance-id>-slot-servicetype-raw-code"
  Slot.serviceType.coding[rawCode].code: scheduletemplatecode.code   # single char from timecode position
  Slot.serviceType.coding[rawCode].display: scheduletemplatecode.description

  Slot.appointmentType.coding[standard]: "**UNMAPPED** (Phase 1)"
  Slot.appointmentType.coding[rawCode]: "**derived**: same as Slot.serviceType.coding[rawCode] when scheduletemplatecode resolves; absent otherwise"

  Slot.serviceCategory: "**UNMAPPED** (Phase 1)"

  # Overbooking — converter does NOT set; booking system's responsibility
  Slot.overbooked: "**UNMAPPED** (converter never sets; booking system writes on intentional overbook)"

  # Free-text — populated only for busy-unavailable Slots from within-day blockouts
  Slot.comment: "**conditional**: scheduledate.reason when Slot.status == busy-unavailable; absent otherwise"

# === Edge cases / rules ===

Special_Rules:
  underscore_character:
    timecode_char: "'_'"
    rule: "Skip — no Slot emitted at this index. Convention for 'clinic is not bookable here.'"

  holiday:
    source: scheduleholiday table (sdate, holiday_name)
    rule: "If scheduledate.sdate appears in scheduleholiday, emit ZERO Slots for the entire date. The absence of Slots is the signal that the clinic isn't bookable that day."

  within_day_blockout:
    source: scheduledate.available
    rule: "When scheduledate.available != '1', the converter still emits Slots, but with Slot.status='busy-unavailable' and scheduledate.reason carried in Slot.comment."

  appointment_overlay:
    source: oscar appointment table (start_time, end_time, status)
    rule: "After deriving each Slot's [start, end) window, query the appointment table for rows where appointment.status NOT IN ('cancelled', 'noshow') AND (appointment.start_time, appointment.end_time) overlaps [Slot.start, Slot.end). If any match: set Slot.status='busy'."

  timezone_resolution:
    rule: "Derive timezone from the clinic's Location.address province → IANA TZ lookup (e.g. ON → America/Toronto). Do NOT use ZoneId.systemDefault(). See converter-contracts.md for the lookup table."

  slot_duration:
    rule: "slotDurationMinutes = 1440 / timecode.length(). The existing converter computes this; the FHIR profile does NOT assume a fixed length (e.g. 96 chars for 15-min grids)."

  appointment_link_back:
    note: "When the booking system creates an Appointment from a Slot, it SHOULD populate Appointment.slot[0].reference with the Slot/<id>. This is a write-side concern documented in fhir-to-oscar.yaml."
