medOS ultra

OR Closeout (Mongo-Canonical)

Audit-defensible source-of-truth + realtime UX for the three Post-Op closeout records.

5 min read diagramsUpdated 2026-05-15docs/architecture/or-closeout-mongo-canonical.md

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) MongoDBpacu_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 MongoDBor_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)

  1. Frontend calls REST endpoint via callApi (/api/v2/pacu-handoff/upsert, /api/v2/pacu-handoff/sign, /api/v2/or-case-costing/upsert, .../submit, .../post).
  2. NestJS controller (Moleculer mixin) validates → writes Mongo → emits manifest.clinical.pacu_handoff_signed / manifest.financial.or_case_costing_posted.
  3. 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:

  1. ensureCacheRow(db, encounterId, patientId, …)
  2. Upsert the projected row into pacu_handoff_sbar / or_case_costing (the existing tables — no schema change).
  3. The Postgres triggers on those tables auto-update or_case_closeout_state (and or_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

  1. New NestJS modules (PACU Handoff + OR Case Costing) — additive, no breakage.
  2. Two new orchestrator handlers — additive.
  3. Frontend service modules (pacuHandoff.medbase.ts, orCaseCosting.medbase.ts) gain a writeMode switch. Default stays Supabase for the in-repo demo; flips to REST-first when VITE_OR_CLOSEOUT_WRITE_MODE=rest-first.
  4. No schema change to Supabase tables — they remain the projection target.
  5. No frontend UI change — same OrCloseout shell, 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
Ask Anything