Admission Future-Binding & Reminders
Future-binding mechanism for admissions plus patient reminders.
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:
- 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.
- Phantom encounter. A
PLANNEDIPD 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 board — department_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
appointmentDatestays intent — it does not pre-create the IPD encounter from the current visit. - On the admission day, the patient checks in →
encounter.checkIn→arrived→ the (new) encounter is the one they arrived under; the open admission binds to it (setsencounterIpd). - 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) todepartment_queuesand hoistappointment_dateontoencounter_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 hasnotification.{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,statusscheduled→sent/failed/cancelled,externalRefs= ICS uid / message ids). No ack, no inbox, no escalation. - Firing: a
cron_jobs-registered sweep finds due reminders → dispatches viamessaging.notificationper 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
- A future request never binds to the current/last visit’s encounter.
- The encounter is materialized at arrival; the request binds to that encounter.
bindOpenAdmissionsToEncounteris idempotent (no double-bind, no duplicate encounter).- Same-day admission behavior is unchanged.
- Reminders are PHI-safe (no clinical detail in SMS/ICS body — appointment + facility only).
- 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.