medOS ultra

Admission Future-Binding & Reminders

Future-binding mechanism for admissions plus patient reminders.

4 min read diagramsUpdated 2026-06-01docs/architecture/admission-future-binding-and-reminders.md

Status: design + phase-1 build (binding mechanism). Backend; auto-deploys to PH demo on push. Extends: consult-visit-encounter-chain.md (the consult refactor, commit 3f3605e2c) — applies the same request-vs-event model to admission, and adds a reminder layer for future-dated requests.


1. The problem (same inversion the consult refactor fixed)

A future-dated admission today pre-creates an IPD PLANNED encounter at request time, derived from the patient’s current OPD visit (admission.service.ts ~L665–700: getEncounter(oldAdmission.encounterID)newEncounter{ level: IMP, status: PLANNED }createEncounter).

Two consequences the user flagged:

  1. Bound to the wrong visit. The IPD encounter inherits from this OPD visit, not the encounter the patient will actually arrive under on the admission day.
  2. Phantom encounter. A PLANNED IPD encounter exists before the patient arrives — pollutes census/queues (identical to the consult phantom).

And: a future admission does not surface on its scheduled day’s boarddepartment_queues / encounter_journey_cache have no appointment_date/scheduled_at column (read-model gap).

2. The model (mirror the consult chain)

A request is not an event. The encounter is materialized at arrival and the request binds back to it.

  • An admission request created for a future appointmentDate stays intent — it does not pre-create the IPD encounter from the current visit.
  • On the admission day, the patient checks in → encounter.checkInarrived → the (new) encounter is the one they arrived under; the open admission binds to it (sets encounterIpd).
  • Same-day admissions are unchanged (the existing reserve/admit flow still runs).

This is the admission analogue of medication.consultRequest.bindOpenConsultsToEncounter.

3. Build — file-by-file (mirrors 3f3605e2c)

File Change Risk
administration/.../admission/admission.service.ts (a) findOpenAdmissionsForBinding(patientRef) — open admissions (status request/pending/appointment, encounterIpd unset, active) for a patient. (b) bindOpenAdmissionsToEncounter({encounterId, patientRef}) — idempotent: binds each open admission’s encounterIpd to the arrival encounter, sets status → admitted/reserved. Skips any that already have encounterIpd (so it’s safe even if the legacy pre-create ran). low (additive)
administration/.../admission/admission.controller.mixin.ts Expose administration.admission.bindOpenAdmissionsToEncounter Moleculer action. low
administration/.../encounter/encounter.controller.mixin.ts In the checkIn arrived block (next to the existing consult bind call), add ctx.call('administration.admission.bindOpenAdmissionsToEncounter', {...}). low
administration/.../admission/admission.service.ts (reserve/admit block ~L673) Gate the pre-create: when appointmentDate is in the future (> today), skip creating the IPD PLANNED encounter — leave the admission as intent; binding happens on arrival. Same-day/past keeps current behavior. medium — dense flow; review + test before push.

Idempotency rule (the key safety net): bindOpenAdmissionsToEncounter only touches admissions with no encounterIpd. So phase-1 (mechanism) can ship before the gate; the gate then flips future admissions onto the bind path.

4. Read-model — surface on the scheduled day

  • Add scheduled_at (= appointmentDate) to department_queues and hoist appointment_date onto encounter_journey_cache (orchestrator handler).
  • New view / filter: “Expected admissions for [date]” (appointmentDate = day AND status ∈ {request, reserved, appointment}) on the bed board / IPD dashboard.
  • Migration: department_queues.scheduled_at TIMESTAMPTZ NULL + index; orchestrator projects it from the admission event.

5. Reminders (SMS / Ever app / email / Google Calendar)

A reminder is a one-way patient notification — NOT an acknowledgement. Do not use AcknowledgementRequest (that entity means “a clinician/role/dept must explicitly acknowledge + escalate”, with an inbox — wrong semantics + pollutes the ack inbox). Reminders are their own domain, built directly on the lower-level dispatch primitive.

  • Dispatch primitive = messaging.notification.* — the messaging service already has notification.{email,sms,web}.gateway.ts + NotificationService. Reminders call these directly. (AcknowledgementRequest itself only sits on top of this same primitive.)
  • Storage = dedicated appointment_reminders (patient, sourceRequest = order/consult/admission, scheduledAt, channels[], offsets[] e.g. 1 day / 2 h before, status scheduled→sent/failed/cancelled, externalRefs = ICS uid / message ids). No ack, no inbox, no escalation.
  • Firing: a cron_jobs-registered sweep finds due reminders → dispatches via messaging.notification per channel.
  • Calendar = ICS-first (no OAuth): generate an .ics (VEVENT, DTSTART=appointmentDate, summary, location, organizer) — emailed as attachment + download link, adds to Google/Apple/Outlook with zero OAuth. Full Google Calendar OAuth sync (event id, update/cancel) is a later phase.
  • Channels resolved from patient: phone (SMS), email, Ever-app device token (push) — all on the patient record.
  • Optional, separate: appointment confirmation (patient replies confirm/reschedule) is a two-way feature added later — still a confirmation, not a clinical acknowledgement.

6. Invariants

  1. A future request never binds to the current/last visit’s encounter.
  2. The encounter is materialized at arrival; the request binds to that encounter.
  3. bindOpenAdmissionsToEncounter is idempotent (no double-bind, no duplicate encounter).
  4. Same-day admission behavior is unchanged.
  5. Reminders are PHI-safe (no clinical detail in SMS/ICS body — appointment + facility only).
  6. ICS/calendar carries no diagnosis; just “appointment at [facility] on [date/time]”.

7. Rollout

  • P1 (this commit): binding mechanism — findOpenAdmissionsForBinding + bindOpenAdmissionsToEncounter + checkIn hook. Additive + idempotent; no behavior change until P2 gate flips future admissions onto it.
  • P2: gate the pre-create for future-dated admissions (review + test, then push).
  • P3: read-model scheduled_at + “expected admissions for [date]” view.
  • P4: reminder AcknowledgementRequests + cron sweep + ICS generation.
  • P5: Google Calendar OAuth sync.
Ask Anything