medOS ultra

OR Request & Approval Contract

Central flow contract: cross-unit request to OR-manager approval to pre/post-approval change handling.

10 min read diagramsUpdated 2026-06-10docs/architecture/or-request-approval-contract.md

Status: Design contract (2026-06-10). Foundational. Build TOR points 4 / 5 / 6 / 7 (cross-unit request → OR-manager approval → pre-approval cancel/change → post-approval change-notify) on top of this, and link the Recovery Room request to it via recovery-room-request-flow-linkage.md.

Why this doc exists: the OR module today has a rich OperatingRoomRequest entity and read-model (or_state_cache, procedure_day_queue) but no formal request→approval lifecycle: a registration goes straight to the schedule with no OR-manager sign-off, no reject path, and no post-approval change notification. There is also no central contract tying the OR request, the cross-unit origin, the approval gate, the orchestrator projection, and the recovery handoff into one shape. This is that contract. It deliberately mirrors the proven admission-to-ward-unified-contract.md and transfer-request-system.md lifecycles so the OR flow is a sibling, not a special case.

Read first: or-feature-suite-20260515.md (the OR read-model + canonical or_state_cache linkage), hospital-movement-architecture.md (movement conventions + override-with-reason), configurable-movement-and-financial-gates.md (the policy-gate engine), acknowledgement-system.md (the notify/ack primitive used for point 7).


0. Scope — which TOR points this contract owns

TOR Requirement Owned here
1 Register surgery (room/type/date/anesthesia) entity contract §2
2 Schedule fixed-time or on-call entity contract §2 (appointment)
3 Record team + per-person role entity contract §2 (medicalTeam[])
4 Link cross-unit (OPD/ER/IPD) registration → OR manager §3 origin contract + §6 worklist
5 OR manager approves cross-unit registration §4 approval state machine + §7 policy gate
6 Cancel/change before approval §5 pre-approval mutation rules
7 Post-approval change → notify OR manager §5 + §8 change-log + ack
“Patient is in OR now” indicator (reviewer ask) §9 read-model projection

Points 8–28 (worklist/search/print/intra-op/orders/registries) are catalogued in the TOR-coverage page (/operating-room/tor-coverage) and are not re-specified here; this contract is only the request lifecycle backbone they all hang off.


1. The one-paragraph model

An OperatingRoomRequest is created at a service point (OPD nursing / ER / IPD ward / OR itself), carrying its origin (clinicRef + specialtyRef + encounterRef). It enters a request lifecycle that the OR manager controls: requested → (approve) → accepted → … → completed → recovered. Before approval the originating unit may freely cancel or change it. After approval, a change/cancel from the originating unit is not silently applied — it raises a change request that notifies the OR manager for re-approval (an AcknowledgementRequest). Every state change emits a hospital_events row that the encounter-orchestrator projects into the read model (or_state_cache, procedure_day_queue, and a new surgical_status on encounter_journey_cache that powers a patient-level “in OR” indicator). When the case reaches the table and finishes, it hands off to a RecoveryRoomRequest (see the linkage doc), which is the same shape of lifecycle for PACU.


2. Entity contract — OperatingRoomRequest

Backend (write truth): packages/platform-api-schema/src/medication/operatingRoomRequest/ · Mongo collection operating_room_request Frontend service: web/src/services/ever-medication/operatingRoomRequest.service.ts

2.1 Status enum (EXISTS today — do not invent new values unless noted)

packages/platform-api-schema/src/medication/operatingRoomRequest/entity/OperatingRoomRequestStatus.ts

DRAFT='draft'  PENDING='pending'  REQUESTED='requested'  ACCEPTED='accepted'
WAITING='waiting'  OPERATING='operating'  COMPLETED='completed'  POSTPONE='postpone'
CANCELLED='cancelled'  RECOVERED='recovered'  SENT_BACK_TO_WARD='sentBackToWard'
SENT_TO_ICU='sentToICU'  DEAD_ON_TABLE='deadOnTable'

Key insight: ACCEPTED already exists — that is the OR-manager-approved state. The approval flow (point 5) does not need a new status; it needs the transition REQUESTED → ACCEPTED to be gated + audited. Only one new optional pair of fields is proposed (approvedBy, approvedAt), reusing the existing detail-level acknowledgeBy/acknowledgeAt pattern at request level.

2.2 Fields (real, from the entity)

Group Fields
Facility building {_id,display}, room {_id,display}
Schedule appointment { date, startTime, endTime, beginSurgeryDate?, finishTime? }on-call = null startTime/endTime (point 2). Recommend an explicit appointment.onCall: boolean to disambiguate.
Personnel surgeonRef, assistantDoctorRef, anes?, medicalTeam: OperatingRoomRequestMedicalTeam[] (order, detail[], startTime, endTime — per-person role, point 3)
Clinical surgicalType, surgicalLevel?, reOperation?, detail[] { appointment, woundClass, clinical, site, infection, icd10, icd9, procedure, time }
Origin (point 4) clinicRef? (originating clinic), specialtyRef? (sub-clinic), encounterRef?
Lifecycle acknowledgeBy?, acknowledgeAt? (detail-level ack today), typeCancle?, reasonCancle?
Approval (point 5, NEW) approvedBy?: Ref, approvedAt?: Date, rejectedBy?, rejectedAt?, rejectReason?
Peri-op signIn?, timeOut?, signOut?, operativeNotes?

2.3 REST endpoints (exist) + proposed additions

POST   /v2/medication/operatingRoomRequests                         create
GET    /v2/medication/operatingRoomRequests                         list (filters: status, buildingRef, room, woundClass, surgicalType, date range, clinic, specialty)
GET    /v2/medication/operatingRoomRequests/{id}                    get
PUT    /v2/medication/operatingRoomRequests/{id}                    update
GET    /v2/medication/operatingRoomRequests/checkAppointment        room/time conflict check
PUT    /v2/medication/operatingRoomRequests/updateAcknowledge/{id}  detail-level ack
GET    /v2/medication/operatingRoomRequests/findDataAcknowledgedByEncounterId/{encounterId}
GET    /v2/medication/operatingRoomRequests/findByEpisodeOfCareRef/{ref}
─────────────────────────────────────────────────────────────────  PROPOSED (points 5/7)
PUT    /v2/medication/operatingRoomRequests/{id}/approve            { approvedBy } → REQUESTED → ACCEPTED  (policy-gated)
PUT    /v2/medication/operatingRoomRequests/{id}/reject             { rejectedBy, rejectReason } → REQUESTED → (back to originator)
POST   /v2/medication/operatingRoomRequests/{id}/change-requests    { changes, requestedBy, reason } → raises change-request when status >= ACCEPTED
GET    /v2/medication/operatingRoomRequests/{id}/change-requests    list change requests
PUT    /v2/medication/operatingRoomRequests/{id}/change-requests/{logId}/decide  { decision: approve|reject, by }

3. Origin contract (point 4) — cross-unit request → OR manager

The data already exists: clinicRef + specialtyRef + encounterRef. What is missing is surfacing it and routing the request to an OR-manager inbox.

OPD nursing / ER / IPD ward                         OR manager
  PerformSurgery / order → POST operatingRoomRequests   list-or-wait-requested
        status = REQUESTED                                  ▲ must show:
        clinicRef = <origin clinic>            ───────────► │  • origin unit chip (clinicRef.display)
        specialtyRef = <origin sub-clinic>                  │  • "pending external approval" tab
        encounterRef = <encounter>                          │  • approve / reject row actions

Contract: the OR wait-list (web/packages/periops-kit/src/operating-room/components/list-or-wait-requested/MainTab.tsx) MUST render an origin column (clinicRef.display) and a “รออนุมัติ / pending approval” tab filtered to status === REQUESTED && approvedBy == null. (Today it filters only PENDING/REQUESTED with no approval concept.)


4. Approval state machine (point 5)

                         ┌─────────────── cancel (origin, §5) ──────────────┐
                         ▼                                                   │
 [draft] ──submit──▶ [requested] ──approve (OR-mgr, GATED)──▶ [accepted] ──▶ [waiting]
   ▲                    │  │                                       │            │
   └── save ────────────┘  └──reject (OR-mgr)──▶ back to origin    │         roomIn
                              (notify originator)                  │            ▼
                                                                   │       [operating]
   [postpone] ◀── reschedule ── (accepted/waiting)                 │            │ finishSurgery
                                                                   │            ▼
   [cancelled] ◀── cancel-with-reason (gated after accepted) ──────┘       [completed]
                                                                                │ roomOut → PACU
                                                                                ▼
                                                              [recovered] ──▶ RecoveryRoomRequest
                                                                                │
                                                       ┌────────────────────────┼───────────────┐
                                                       ▼                        ▼               ▼
                                              [sentBackToWard]            [sentToICU]      [deadOnTable]
  • requested → accepted is the approval gate (point 5). Set approvedBy/approvedAt. Emit OR_REQUEST_APPROVED.
  • requested → (reject) returns control to the originator (notify via ack). No status sink — stays requested flagged rejectReason, or moves to draft per hospital policy.
  • pending is the pre-submit/triage state; waiting = approved + scheduled, awaiting room-in; operating/completed/recovered are circulator/peri-op transitions already driven by sign-in/time-out/sign-out + roomIn/roomOut timestamps.

5. Mutation rules — cancel/change before vs after approval (points 6 & 7)

State Originating unit may… Mechanism Notifies OR manager?
draft, pending, requested (pre-approval) freely cancel or edit PUT {id} (status→cancelled or field edits) — ungated No (point 6)
accepted+ (post-approval) request a change/cancel POST {id}/change-requests → writes or_request_change_logs row + AcknowledgementRequest to OR-manager Yes (point 7)

Invariant (point 7): once approvedAt != null, a field edit or cancel from the originating unit MUST NOT mutate the request in place. It creates a change request that the OR manager approves/rejects. Only an OR-manager decide=approve applies the change (and re-stamps approvedAt).


6. The OR-manager worklist contract

web/src/containers/operating-room/list-or-wait-requested@periops-kit/operating-room/components/list-or-wait-requested/*

Required tabs/columns (today only PENDING/REQUESTED filter exists, no approval UI):

  • Tab “รออนุมัติ” (Pending approval): status==REQUESTED && !approvedBy — row actions Approve (gated) / Reject (reason).
  • Tab “มีการเปลี่ยนแปลง” (Change requests): open or_request_change_logs with unacknowledged entries — Approve change / Reject change.
  • Origin column: clinicRef.display + specialtyRef.display.
  • Existing scheduled/operating tabs unchanged.

7. Policy gates (point 5 + post-approval change)

Reuse the policy_gates engine (web/supabase/migrations/20260425_policy_gates.sql, web/src/services/policy-gate.service.ts, usePolicyGate). Add trigger actions:

trigger_action scope predicate (example) action
approve_or_request department = OR or_request.status == 'requested' require_override (role = OR-manager) or block if checklist incomplete
change_or_request_after_approval department = OR or_request.status in (accepted,waiting) warn + route to change-request flow (never silent edit)

Seed pattern mirrors 20260523c_surgical_count_policy_gates_seed.sql. Gates are data rows, per-hospital, fail-open (consistent with the engine’s invariants).


8. Events + orchestrator side-effects

Event contract: infrastructure/medbase/functions/_shared/event-contract.ts (department key 'surgery'; today only OR_CASE_STARTED → manifest.surgery.case_started). Add:

Emitted event_type normalizeEventType Payload (key fields)
OR_REQUEST_CREATED manifest.surgery.request_created { orRequestId, encounterId, patientId, clinicRef, specialtyRef, status }
OR_REQUEST_UPDATED manifest.surgery.request_updated { orRequestId, status, room, appointment }
OR_REQUEST_APPROVED manifest.surgery.request_approved { orRequestId, approvedBy, approvedAt, roomId, scheduledAt }
OR_REQUEST_REJECTED manifest.surgery.request_rejected { orRequestId, rejectedBy, rejectReason }
OR_REQUEST_CANCELLED manifest.surgery.request_cancelled { orRequestId, typeCancle, reasonCancle }
OR_REQUEST_CHANGE_REQUESTED manifest.surgery.change_requested { orRequestId, changes, requestedBy, reason }
OR_CASE_STARTED (exists) manifest.surgery.case_started room-in/phase

New orchestrator handler handleOperatingRoomRequest (infrastructure/medbase/functions/encounter-orchestrator/) side-effects:

On Must also update Wired today?
CREATED/UPDATED or_state_cache upsert (mongo_ref,encounter_id,patient_id,or_room_id,status,scheduled_at) partial — projector/reverse-link exists (20260515f_or_state_cache_reverse_link.sql); no event-driven handler
APPROVED procedure_day_queue insert (status scheduled) + or_state_cache.status='accepted'
APPROVED/CASE_STARTED encounter_journey_cache.surgical_status (§9)
CANCELLED remove from procedure_day_queue, or_state_cache.status='cancelled', free or_room_runtime_state
CHANGE_REQUESTED AcknowledgementRequest to OR-manager (messaging dispatcher)

9. Read-model projection — the “patient is in OR now” indicator

Finding (investigation, 2026-06-10): there is no patient-level “in OR” indicator anywhere outside the OR module. OR phase lives only in or_state_cache / or_room_runtime_state / procedure_day_queue (OR-module-only). The universal manifest encounter_journey_cache — read by the patient banner web/src/containers/ipd-command-center/components/cards/PatientHeaderCard.tsx and web/src/containers/patient-profile/DoctorActionBar.tsx — has no surgical field.

Contract: the handleOperatingRoomRequest / case-phase handler projects a derived surgical_status onto encounter_journey_cache (or clinical_context.surgical):

surgical_status: null | 'scheduled' | 'in_or' | 'in_pacu' | 'completed'
or_room_code:    string | null
or_phase:        'setup'|'induction'|'incision'|'closing'|'turnover'|'cleaning' | null   (from or_room_runtime_state)

Then add a reusable chip (matches the existing PatientHeaderCard chip pattern):

  • Banner: PatientHeaderCard.tsx{row.surgical_status==='in_or' && } label="OR" .../>}
  • Action bar: DoctorActionBar.tsx → optional warning banner “Patient is currently in OR · {or_phase}”.
  • Hover: worklist rows tooltip “In OR · {or_room_code}”.

This is pure additive once the projection writes the field (≈30 min frontend). It is the cross-cutting payoff of giving the OR request a real orchestrator handler.


10. Invariants

  1. Origin is immutableclinicRef/specialtyRef/encounterRef are set at creation, never rewritten.
  2. Approval is the REQUESTED → ACCEPTED transition — gated, stamped (approvedBy/approvedAt), audited via event. No bypass.
  3. Post-approval edits never mutate in place — they go through or_request_change_logs + OR-manager decision (point 7).
  4. One read model truth — frontend reads or_state_cache/procedure_day_queue/encounter_journey_cache; it never writes them. Mongo via NestJS is the write truth.
  5. Cancellation cascades — a cancel after scheduling MUST free the room (or_room_runtime_state), drop the procedure_day_queue row, clear surgical_status, and (if a recovery request exists) close it.
  6. Fail-open gates — missing/disabled policy gate ⇒ today’s behavior (no block).
  7. Recovery is a child lifecycle — a RecoveryRoomRequest links to exactly one OperatingRoomRequest (operatingRoomRequestRef) and inherits patient/encounter/origin (see linkage doc).

11. Phased rollout

Phase Deliverable Surfaces
P0 Origin column + “pending approval” tab in wait-list (read-only, no gate) frontend only
P1 approve/reject endpoints + approvedBy/approvedAt + status transition + OR_REQUEST_APPROVED/REJECTED events backend + frontend actions
P2 approve_or_request policy gate + seed migration + usePolicyGate
P3 or_request_change_logs + change-request endpoints + OR-manager ack (point 7) backend + ack-system
P4 handleOperatingRoomRequest orchestrator handler → or_state_cache/procedure_day_queue event-driven projection edge function
P5 surgical_status projection + “in OR” banner chip (§9) orchestrator + PatientHeaderCard/DoctorActionBar
P6 Recovery linkage — see recovery-room-request-flow-linkage.md per that doc

P0–P2 make the approval workflow (points 4/5/6) demoable; P3 closes point 7; P4–P5 deliver the “in OR” indicator; P6 completes recovery.


Ask Anything