OR Closeout (Mongo-Canonical)
Audit-defensible source-of-truth + realtime UX for the three Post-Op closeout records.
Audit-defensible source-of-truth + realtime UX for the three Post-Op closeout artefacts: PACU Handoff (SBAR), Operative Note, OR Case Costing.
TL;DR
The three closeout features each have a different “right home”:
| Artefact | Write source of truth | Supabase role | Why |
|---|---|---|---|
| Operative Note | MongoDB (already) — operativeNote collection via NestJS |
n/a today | Surgeon-signed clinical record. Already canonical. |
| PACU Handoff (SBAR) | MongoDB — pacu_handoff collection via new services/clinical/pacuHandoff module |
pacu_handoff_sbar table = realtime projection |
Signed clinical handover; legal/HCRC requires audit chain. |
| OR Case Costing | MongoDB — or_case_costing collection via new services/financial/orCaseCosting module |
or_case_costing table = realtime preview |
Drives invoice posting; financial-domain record. |
| Intra-op Counts (existing) | MongoDB (already) | or_intraop_counts table = realtime collab cache |
Already canonical. |
| Procedure Event Log (existing) | MongoDB (already) | procedure_event_log table = projection |
Already canonical. |
| OR State Cache (existing) | MongoDB (already) | or_state_cache table = read-mirror |
Already canonical. |
or_case_closeout_state (summary) |
Pure Supabase — derived | Summary view fed by triggers from the 3 sources | No independent truth; recomputable. |
086_or_case_closeout_demo_seed.sql |
Pure Supabase (demo only) | n/a | Test fixture. |
Flow
┌──────────────┐ REST (callApi) ┌─────────────────┐ ctx.emit ┌──────────────┐ postgres_changes
│ MedOS Clinic │ ───────────────▶ │ NestJS service │ ─────────▶ │ NATS bus │ ─────────────────┐
│ (React) │ │ (clinical / fin)│ │ │ │
└──────┬───────┘ └─────────┬───────┘ └──────┬───────┘ ▼
│ │ │ ┌──────────────┐
│ realtime subscribe ▼ ▼ │ Supabase │
│ (pacu_handoff_sbar, ┌──────────────┐ ┌──────────────────┐ │ edge fn: │
│ or_case_costing, │ MongoDB │ │ encounter- │ ◀────── │ encounter- │
│ or_case_closeout_state) │ (write truth)│ │ orchestrator │ │ orchestrator │
│ └──────────────┘ │ (Deno) │ └──────────────┘
│ └──────┬───────────┘
│ │ upsert
│ ▼
│ ┌──────────────────────┐
│ │ Supabase tables │
│ │ • pacu_handoff_sbar │
│ │ • or_case_costing │
│ │ • or_case_closeout_ │
│ │ state (via triggers)│
└─────────────────────────────────────────────────▶│ • v_or_closeout_status│
└──────────────────────┘
Read path
Frontend reads from Supabase exclusively (realtime channels on the 3 tables +
the v_or_closeout_status view). No GET round-trips through NestJS for the
list pages or the closeout shell hydration.
Write path (after hardening)
- Frontend calls REST endpoint via
callApi(/api/v2/pacu-handoff/upsert,/api/v2/pacu-handoff/sign,/api/v2/or-case-costing/upsert,.../submit,.../post). - NestJS controller (Moleculer mixin) validates → writes Mongo → emits
manifest.clinical.pacu_handoff_signed/manifest.financial.or_case_costing_posted. - encounter-orchestrator edge function receives the event, projects the document into the Supabase table, and the existing frontend realtime subscription picks it up automatically — same UI code path as today.
Fallback (dev / demo / no backend)
The frontend service exports a writeMode setting (env-driven). When
writeMode === 'rest-first' (prod), it tries REST first and only falls back
to direct Supabase upsert on network/5xx failure. When writeMode === 'supabase'
(demo, current), it writes Supabase directly — same as today. This keeps
the demo deployable today and unblocks prod hardening without a code rewrite.
Module layout (mirrors existing pacuDischargeDecision exactly)
Clinical — PACU Handoff
services/clinical/src/api/clinical/modules/pacuHandoff/
├── pacuHandoff.module.ts
├── pacuHandoff.service.ts
├── pacuHandoff.repository.ts
├── pacuHandoff.controller.mixin.ts
└── dto/
├── upsertPacuHandoff.dto.ts
├── signPacuHandoff.dto.ts
└── getPacuHandoff.dto.ts
packages/platform-api-schema/src/clinical/pacuHandoff/
├── index.ts
└── entity/
├── PacuHandoff.ts # typegoose schema
└── PacuHandoffModel.ts
Financial — OR Case Costing
services/financial/src/api/financial/modules/orCaseCosting/
├── orCaseCosting.module.ts
├── orCaseCosting.service.ts
├── orCaseCosting.repository.ts
├── orCaseCosting.controller.mixin.ts
└── dto/
├── upsertOrCaseCosting.dto.ts
├── submitOrCaseCosting.dto.ts
├── postOrCaseCosting.dto.ts
└── getOrCaseCosting.dto.ts
packages/platform-api-schema/src/financial/orCaseCosting/
├── index.ts
└── entity/
├── OrCaseCosting.ts # parent doc
├── OrCaseCostingLineItem.ts # embedded line items
└── OrCaseCostingModel.ts
Orchestrator handlers
infrastructure/medbase/functions/encounter-orchestrator/index.ts:
case 'manifest.clinical.pacu_handoff_signed':
await handlePacuHandoffSigned(evt);
break;
case 'manifest.financial.or_case_costing_posted':
await handleOrCaseCostingPosted(evt);
break;
Each handler:
ensureCacheRow(db, encounterId, patientId, …)- Upsert the projected row into
pacu_handoff_sbar/or_case_costing(the existing tables — no schema change). - The Postgres triggers on those tables auto-update
or_case_closeout_state(andor_state_cache.status='closed'when all three phases complete) — already wired by migrations 083–085.
NATS event names
Follow the existing manifest.<domain>.<action> convention used by
manifest.financial.updated / manifest.discharge.summary_signed:
| Action | Event |
|---|---|
| PACU handoff drafted (autosave) | manifest.clinical.pacu_handoff_drafted |
| PACU handoff signed | manifest.clinical.pacu_handoff_signed |
| PACU handoff amended (post-sign edit) | manifest.clinical.pacu_handoff_amended |
| Case costing saved (draft) | manifest.financial.or_case_costing_drafted |
| Case costing submitted to cashier | manifest.financial.or_case_costing_submitted |
| Case costing posted (invoice created) | manifest.financial.or_case_costing_posted |
What we do NOT change
- Existing legacy OR form (
OperatingRoomListPatient/ListOrPatientTable2) — only adds the “ปิดเคส” row action + closeout pip column, never edits pre-op / intra-op behavior. - Op Note miniapp — already Mongo-canonical via the existing operativeNote collection. The closeout shell embeds it unchanged.
- Intra-op counts, procedure event log, OR state cache, PACU discharge decision — all already correctly placed.
What changes for prod hardening
- New NestJS modules (PACU Handoff + OR Case Costing) — additive, no breakage.
- Two new orchestrator handlers — additive.
- Frontend service modules (
pacuHandoff.medbase.ts,orCaseCosting.medbase.ts) gain awriteModeswitch. Default stays Supabase for the in-repo demo; flips to REST-first whenVITE_OR_CLOSEOUT_WRITE_MODE=rest-first. - No schema change to Supabase tables — they remain the projection target.
- No frontend UI change — same
OrCloseoutshell, same row action.
Rollout
| Stage | Action | Risk |
|---|---|---|
| Today (demo) | Supabase-only writes; demo seed populates Supabase | None |
| UAT prep | Deploy NestJS modules + orchestrator handlers; keep frontend in supabase mode |
None — backend is dormant |
| UAT cut-over | Flip VITE_OR_CLOSEOUT_WRITE_MODE=rest-first per region |
Low — fallback path catches backend errors |
| Prod | Remove Supabase write policies for these two tables (read-only) | Cuts unauthorized writes |
Related files
- 083_pacu_handoff_sbar.sql — Supabase table
- 084_or_case_costing.sql — Supabase table
- 085_or_case_closeout_state.sql — derived summary
- pacuHandoff.medbase.ts — frontend service
- orCaseCosting.medbase.ts — frontend service
- OrCloseout.tsx — shell