Skip to content

feat(mapper): map SIU scheduling messages to FHIR Appointments - #19

Merged
ipasechnikov merged 3 commits into
mainfrom
feat/siu-support
Aug 6, 2026
Merged

feat(mapper): map SIU scheduling messages to FHIR Appointments#19
ipasechnikov merged 3 commits into
mainfrom
feat/siu-support

Conversation

@ipasechnikov

Copy link
Copy Markdown
Collaborator

The mapper half of HealthSamurai/interbox#78.

Draft on purpose: this needs the scheduling segment accessors from
HealthSamurai/interbox#112, so CI here fails typecheck until that is merged,
released, and the SDK dep pinned to it. See Merge order below.

Why

The v2-to-fhir sample handled ADT, ORU, ORM and VXU, so a scheduling feed had
nowhere to land: every SIU message failed with unsupported/message_type.

What

One converter serves every SIU trigger event — S12 through S26 all use the
SIU_S12 message structure — and reads the event only to infer a status when SCH-25
does not give one.

Emits, per message:

  • Appointment from SCH (+ TQ1, AIS, NTE)
  • Patient[] from PID[]
  • Practitioner[] from AIP[] across every resource group
  • Location[] from AIL[] across every resource group

Each referenced resource is emitted once even when several RGS groups name the
same room or clinician, and every Appointment.participant.actor resolves inside
the same bundle.

New files: messages/siu-s12.ts, segments/sch-appointment.ts,
segments/ail-location.ts; appointmentIdFromSch added to support/identity.ts;
routing added in index.ts.

Decisions worth reviewing

  • Appointment id from SCH-2 (filler), plus SCH-3 when present. The filler is
    the SIU sender and keeps that ID stable, so the booking (S12), its
    modifications (S13/S14) and its cancellation (S15) update one Appointment
    instead of piling up. SCH-3 is included because a recurring appointment repeats
    one ID pair per occurrence.
  • Status: SCH-25 first, trigger event second. SCH-25 (HL7 table 0278) states
    the appointment's status; the event only names the kind of notification, so an
    S14 carrying a "Cancelled" filler status is cancelled. Fallbacks: S15/S16/S23 →
    cancelled, S17 → entered-in-error, S24/S26 → noshow, S12/S13/S14/S18–S22 →
    booked. Neither usable raises field/unknown_appointment_status rather than
    guessing.
  • Timing precedence: TQ1-7/8 → SCH-11.4/.5 → SCH-11.1 → AIS-4. The third step
    is for v2.4 senders like MEDITECH, which put the appointment datetime in the TQ
    quantity component and never populate TQ.4 — reading only TQ.4 loses the
    appointment time for all of that traffic. Guarded to require at least a full
    date, so a genuine TQ occurrence count (or a month-precision value) is never
    read as a start. Duration comes from SCH-9/10, falling back to AIS-7/8, with
    unit conversion; the end is computed when not sent.
  • No Encounter. ADT owns those, same line ORM_O01 draws — and R4 links the
    two from Encounter.appointment, not the other way.
  • AIG skipped. Equipment and other general resources would need Device or
    HealthcareService participants, which this mapper does not produce. Same
    boundary the ave-parser reference draws.
  • S17 is a status, not a delete. The reference parser issues a FHIR DELETE
    for appointment deletion; interbox's sender only upserts, so S17 arrives as
    status: entered-in-error. Flagging in case you want deletion handled
    differently.

Tests

test/siu.test.ts, 12 cases: a real-shape MEDITECH v2.4 SIU^S12 end to end
(ids, timing, serviceType, participants, notes, reference integrity); S12→S15
landing on one Appointment; SCH-25 vs trigger-event status resolution; two
resource groups sharing a clinician and repeating AIL-3; a blocked-slot S23 with
no PID; non-minute duration units; AIS-only timing; a TQ count not mistaken for a
datetime; day-precision timing; the SIU shape utils/hl7v2-simulator emits; and
the four domain errors (missing_sch, missing_appointment_id,
missing_appointment_participant, unknown_appointment_status).

bun run typecheck   # clean, against the SDK built from interbox#112
bun test            # 23 pass, 0 fail

Merge order

  1. Merge HealthSamurai/interbox#112
  2. Release the engine (1.13.0)
  3. Wait for the workspace version mirror
  4. Pin the SDK dep here (interbox pin 1.13.0) — CI goes green
  5. Mark this ready and merge

🤖 Generated with Claude Code

@ipasechnikov

Copy link
Copy Markdown
Collaborator Author

Rebased onto the regenerated SDK: interbox#112 now adds the scheduling segments by regenerating the HL7v2 surface with SIU_S12 in the message-type list, instead of the hand-written module this branch was first written against. Field names follow the codegen — SCH-10 appointmentDurationUnit, SCH-25 fillerStatus, AIS-3 service, AIS-4 start, AIS-8 durationUnit, AIL-4 locationTypeAil (0c51efe).

Still green against a locally built SDK: tsc --noEmit clean, 23 tests pass. Merge order in the description is unchanged.

ipasechnikov and others added 3 commits August 6, 2026 18:13
The v2-to-fhir sample handled ADT, ORU, ORM and VXU, so a scheduling feed had
nowhere to land: every SIU message failed with unsupported/message_type.

Route them all through one converter — every SIU trigger event uses the SIU_S12
structure — producing an Appointment plus the Patients, Practitioners and
Locations it references, each emitted once even when several resource groups
name the same room or clinician.

Decisions worth knowing:

- The Appointment id comes from SCH-2 (filler), so the booking (S12), its
  modifications (S13/S14) and its cancellation (S15) update one resource
  instead of piling up. SCH-3 is included for recurring appointments.
- SCH-25 decides the status when it maps to table 0278; otherwise the trigger
  event does (S15 cancelled, S17 entered-in-error, S26 noshow, …). Neither
  usable is field/unknown_appointment_status rather than a guess.
- Timing prefers TQ1, then SCH-11.4, then SCH-11.1, then AIS-4. That third step
  is for v2.4 senders like MEDITECH, which put the datetime in the TQ quantity
  and never populate TQ.4; a value that isn't at least a full date is ignored so
  a genuine TQ occurrence count is never read as a start.
- No Encounter: ADT owns those, as in ORM_O01. No Device/HealthcareService for
  AIG either, so equipment resource groups are skipped.

Needs the scheduling segment accessors added in interbox#78, so the SDK
dependency has to be bumped before this lands.

Refs HealthSamurai/interbox#78

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The SDK now generates the scheduling segments (HealthSamurai/interbox#112)
instead of shipping the hand-written module this branch was first written
against, and the codegen names a few fields differently: SCH-10
appointmentDurationUnit, SCH-25 fillerStatus, AIS-3 service, AIS-4 start, AIS-8
durationUnit, AIL-4 locationTypeAil.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The SIU mapper reads SCH/RGS/AIS/AIL/AIP, which the SDK only exports from 1.12.1
(HealthSamurai/interbox#112). The dependency was still ^1.0.0, so a workspace on
an older SDK would fail to typecheck with no hint why.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ipasechnikov
ipasechnikov marked this pull request as ready for review August 6, 2026 09:15
@ipasechnikov
ipasechnikov merged commit d6413ff into main Aug 6, 2026
3 checks passed
@ipasechnikov
ipasechnikov deleted the feat/siu-support branch August 6, 2026 09:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant