OR Request & Approval Contract
Central flow contract: cross-unit request to OR-manager approval to pre/post-approval change handling.
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
OperatingRoomRequestentity 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_cachelinkage), 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:
ACCEPTEDalready exists — that is the OR-manager-approved state. The approval flow (point 5) does not need a new status; it needs the transitionREQUESTED → ACCEPTEDto be gated + audited. Only one new optional pair of fields is proposed (approvedBy,approvedAt), reusing the existing detail-levelacknowledgeBy/acknowledgeAtpattern 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 → acceptedis the approval gate (point 5). SetapprovedBy/approvedAt. EmitOR_REQUEST_APPROVED.requested → (reject)returns control to the originator (notify via ack). No status sink — staysrequestedflaggedrejectReason, or moves todraftper hospital policy.pendingis the pre-submit/triage state;waiting= approved + scheduled, awaiting room-in;operating/completed/recoveredare 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-managerdecide=approveapplies the change (and re-stampsapprovedAt).
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_logswith 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
- Origin is immutable —
clinicRef/specialtyRef/encounterRefare set at creation, never rewritten. - Approval is the
REQUESTED → ACCEPTEDtransition — gated, stamped (approvedBy/approvedAt), audited via event. No bypass. - Post-approval edits never mutate in place — they go through
or_request_change_logs+ OR-manager decision (point 7). - 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. - Cancellation cascades — a cancel after scheduling MUST free the room (
or_room_runtime_state), drop theprocedure_day_queuerow, clearsurgical_status, and (if a recovery request exists) close it. - Fail-open gates — missing/disabled policy gate ⇒ today’s behavior (no block).
- Recovery is a child lifecycle — a
RecoveryRoomRequestlinks to exactly oneOperatingRoomRequest(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.
12. Related contracts
- admission-to-ward-unified-contract.md — the lifecycle template this mirrors (REQUESTED→ACKNOWLEDGED→ACCEPTED).
- transfer-request-system.md — change-request + compensation pattern reused in §5.
- or-feature-suite-20260515.md —
or_state_cachecanonical linkage + reverse-link trigger. - configurable-movement-and-financial-gates.md — the policy-gate engine (§7).
- acknowledgement-system.md — the notify/ack used for point 7.
- recovery-room-request-flow-linkage.md — the companion handoff: how to wire RecoveryRoomRequest into this contract.
- TOR-coverage UAT page:
/operating-room/tor-coverage(web/src/containers/operating-room/tor-coverage/).