medOS ultra

Central Order System

How all clinical order types flow through orderRequest as the canonical order.

11 min read diagramsUpdated 2026-05-15docs/architecture/central-order-system.md

How all clinical order types flow through orderRequest as the canonical order wrapper for billing, webhooks, FHIR interoperability, and external system integration.


Problem

Seven order types exist across two backend services. Three are properly integrated with the central orderRequest entity; four are disconnected — no billing linkage, no unified event pattern, and in the case of ER Request, not even routable through the API gateway.

Order Type Service Collection orderRequest Billing Events Gateway
Pathology/Lab medication order_request IS the entity saleOrderStatus ORDER_CREATED
Blood Bank medication blood_request ✅ Auto-creates SalesOrder clinical.blood.* + hospital.financial.updated
Imaging/NM medication imaging_request ✅ Required field SalesOrder.updatePrice clinical.imaging.* ✅ (stale deploy)
OR Request medication operating_room_request ⚠️ Deferred ⚠️ Deferred emitOrderCreated + clinical.surgery.* + order_bus
Labour Room medication labour_room_request ⚠️ Deferred ⚠️ Deferred emitOrderCreated + clinical.labour.* + order_bus
ER Request administration emergency_medical_service ⚠️ Deferred ⚠️ Deferred hospital_events + clinical.emergency.* + order_bus
Admission administration admission ⚠️ Optional ⚠️ financialClass only hospital_events + clinical.admission.* + order_bus

Target Architecture

Hub-and-Spoke Model

orderRequest is the meta-order (header/container). Each specialized request becomes a child-order linked back to it. This already works for pathology, blood bank, and imaging:

                    ┌─────────────────────┐
                    │    orderRequest      │  ← canonical order header
                    │  (order_request)     │     patient, encounter, category,
                    │                     │     status, saleOrderStatus, code
                    └─────────┬───────────┘
                              │ 1:N
          ┌───────────┬───────┼───────┬───────────┬──────────┐
          ▼           ▼       ▼       ▼           ▼          ▼
   orderRequestItem  ...    ...     ...         ...        ...
   (product, qty,
    unitPrice, type)
          │
          │ type discriminator (ProductType enum)
          ▼
   ┌──────┬──────┬─────────┬──────────┬──────────┬──────────┐
   │ DRUG │ LAB  │ IMAGING │PROCEDURE │ BLOOD    │NUTRITION │
   │      │      │         │          │ PRODUCT  │          │
   └──┬───┴──┬───┴────┬────┴────┬─────┴────┬─────┴──────────┘
      ▼      ▼        ▼        ▼          ▼
  MedReq  LabReq  ImagingReq ProcReq  BloodReq
  (child)  (child)  (child)  (child)   (child)

What Needs to Change

The disconnected order types (OR, Labour, ER) must adopt the same pattern that Blood Bank already uses:

  1. On create, auto-create a parent orderRequest with the appropriate category and ProductType
  2. Store orderRequestRef on the specialized entity
  3. Emit hospital.financial.updated so billing picks it up
  4. Emit encounterJourney.orderCreated for queue/read-model sync

Admission is different — it’s not a clinical order in the CPOE sense. Its billing linkage is through financialClass + bed charges, not through orderRequest. It should emit encounterJourney.orderCreated for queue sync but does NOT need an orderRequestRef.


Implementation Plan

Phase 0 — Gateway Fixes (immediate, no schema changes)

Fix File Change
Add ER Request to whitelist services/gateway/src/ApiGatewayService.ts Add 'administration.emergencyMedicalService.*' to administration section
Redeploy gateway GH Actions workflow Trigger backend deploy to pick up existing medication.imagingRequest.* route

Phase 1 — orderRequest Linkage (backend, per-module)

For each disconnected order type, add to the create action controller:

// 1. Create parent orderRequest
const orderRequestPayload = {
  encounter: dto.encounterRef,
  patient: dto.patientRef,
  status: OrderRequestStatusType.PENDING,
  category: OrderRequestCategoryType.INPATIENT, // or derive from encounter
  saleorderstatus: DocumentStatus.NONE,
  newSaleOrder: true,
  requester: ctx.meta.user._id,
  remark: `${MODULE_NAME} auto-linked`,
  listitems: buildOrderItems(dto), // map procedure items to orderRequestItems
};
const orderRequest = await ctx.call('medication.orderRequest.create', orderRequestPayload);

// 2. Store ref on specialized entity
specializedEntity.orderRequestRef = orderRequest._id;

Per-Module Item Mapping

Module ProductType Item source Price source
OR Request PROCEDURE detail[] (procedures, ICD codes) Procedure catalog
Labour Room PROCEDURE detail[] (same shape as OR) Procedure catalog
ER Request PROCEDURE Service type + severity ER fee schedule

Schema Changes Required

OR Request (operating_room_request):

@prop({ ref: 'OrderRequest' })
orderRequestRef?: Ref<OrderRequest, Uuid>;

Labour Room (labour_room_request):

@prop({ ref: 'OrderRequest' })
orderRequestRef?: Ref<OrderRequest, Uuid>;

ER Request (emergency_medical_service):

@prop({ ref: 'OrderRequest' })
orderRequestRef?: Ref<OrderRequest, Uuid>;

Phase 2 — Event Standardization

Every order type must emit three event categories:

Event Purpose Current coverage
clinical.{type}.created Webhook for external systems All except ER
encounterJourney.orderCreated Read-model sync (department_queues) Missing from OR, Labour, ER
hospital.financial.updated Billing dashboard hydration Missing from OR, Labour, ER

New Events to Add

OR Request (in operatingRoomRequest.controller.mixin.ts create action):

// After orderRequest creation:
ctx.emit('hospital.financial.updated', {
  encounterId: dto.encounterRef,
  patientId: dto.patientRef,
  salesOrderId: orderRequest.saleOrderId,
  triggerSource: 'operatingRoomRequest',
});

Labour Room (same pattern in labourRoomRequest.controller.mixin.ts).

ER Request (in emergencyMedicalService.controller.mixin.ts):

// Add ALL three events:
ctx.emit('clinical.emergency.created', {
  emergencyMedicalServiceId: entity._id,
  encounter: entity.encounterRef,
  patient: entity.patientRef,
  status: entity.status,
  severityLevel: entity.severityLevel,
});

encounterJourneyService.emitOrderCreated(ctx, {
  emergency: [{ emergencyMedicalServiceId: entity._id }],
});

ctx.emit('hospital.financial.updated', { ... });

Phase 3 — Webhook & FHIR Surface

With all orders flowing through orderRequest, external systems get a single integration surface:

Inbound (external → medOS)

POST /fhir/ServiceRequest
  → FHIR transformer (already exists in public-api)
  → maps to orderRequest.create with appropriate category
  → spawns child-order based on ServiceRequest.code

Outbound (medOS → external)

orderRequest.create
  → clinical.{type}.created event
  → webhookDispatcher picks up
  → transforms to FHIR ServiceRequest Bundle
  → POST to subscriber endpoint (HMAC signed)

The existing fhir-subscription-matcher edge function already handles outbound dispatch. The gap is that OR/Labour/ER don’t emit the events it listens for.

Phase 4 — Payment Integration

With orderRequestRef on every order type:

Any Order → orderRequest → orderRequestItems → SalesOrder → Invoice

The financial service already knows how to:

  1. Create SalesOrder from orderRequest (when newSaleOrder: true)
  2. Calculate pricing from orderRequestItem.unitPrice × qty
  3. Apply discounts per payor scheme
  4. Sync status back via orderRequest.updateBySalesorder

The disconnected modules just need to create the orderRequest parent so this pipeline activates.


Order Type Detail Sheets

orderRequest (Central Entity)

  • Collection: order_request
  • Category enum: community | discharge | inpatient | outpatient | forensic
  • Status enum: DRAFT | PRE_PENDING | PENDING | ACCEPTED | COMPLETED | CANCELLED
  • Billing status: DocumentStatus (24 values: None, Draft, Pending, Ready, Paid, etc.)
  • Items: Separate OrderRequestItem documents (1:N via foreign key)
  • Item types: DRUG | LAB | IMAGING | PROCEDURE | NUTRITION | MEDICAL_SUPPLY | BLOOD_PRODUCT
  • Financial flow: orderRequest → SalesOrder → Invoice
  • Events: ORDER_CREATED, ORDERS_ACKNOWLEDGED, allergy/interaction alerts
  • FHIR mapping: ServiceRequest (inbound), Bundle (outbound)

OR Request

  • Collection: operating_room_request
  • Status: 13 states (DRAFT → PENDING → REQUESTED → ACCEPTED → WAITING → OPERATING → COMPLETED/CANCELLED/POSTPONE/RECOVERED/SENT_BACK_TO_WARD/SENT_TO_ICU/DEAD_ON_TABLE)
  • Key fields: patientRef, encounterRef, surgicalType, detail[], building, room, appointment, anesthesia, medicalTeam
  • Children: Creates recoveryRoomRequest on RECOVERED, anesthesiaSystem on create
  • Events: clinical.surgery.created, clinical.surgery.updated
  • Missing: orderRequestRef, hospital.financial.updated, encounterJourney.orderCreated
  • ProductType for items: PROCEDURE

Labour Room Request

  • Collection: labour_room_request
  • Status: 7 states (DRAFT → PENDING → OPERATING → COMPLETED/RECOVERED/POSTPONE/CANCELLED)
  • Key fields: Same shape as OR Request (shared DTO pattern)
  • Children: Creates recoveryRoomRequest on RECOVERED, anesthesiaSystem on create
  • Events: clinical.labour.created, clinical.labour.updated
  • Missing: orderRequestRef, hospital.financial.updated, encounterJourney.orderCreated
  • ProductType for items: PROCEDURE

ER Request (Emergency Medical Service)

  • Collection: emergency_medical_service
  • Status: 3 states (PENDING → IN_PROGRESS → COMPLETED)
  • Key fields: patientRef, encounterRef, emergencyMedicalServiceType, severityLevel, chiefComplaint, notes
  • Type enum: diagnosis | treatment | transfer | other
  • Severity enum: normal | urgent | emergency
  • Events: None
  • Missing: orderRequestRef, ALL events, gateway whitelist entry
  • ProductType for items: PROCEDURE
  • Note: Distinct from emsRequest (operational EMS dispatch). ER Request is clinical classification.

Blood Bank Request (already integrated)

  • Collection: blood_request
  • orderRequest: Auto-creates for non-emergency requests
  • Events: clinical.blood.created, clinical.blood.status_updated, hospital.financial.updated
  • Financial: Full SalesOrder integration via orderRequest
  • SLA tracking: Emergency tier deadlines (dueAt, acknowledgedAt, dispatchedAt, deliveredAt, slaMissed)

Imaging Request (already integrated)

  • Collection: imaging_request
  • orderRequest: Required field (orderRequest ref)
  • Events: clinical.imaging.created, clinical.imaging.status_updated, clinical.imaging.reported
  • Financial: saleOrderStatus field + financial.salesOrder.updatePrice on cancel
  • System discriminator: RADIOLOGY or NUCLEAR_MEDICINE

Admission (special case)

  • Collection: admission
  • orderRequest: Optional (via testFields.formDataOrderToday)
  • Financial: financialClass enum (self-funded, insurance, workers comp, etc.)
  • Billing model: Bed charges + per-diem, not item-based like other orders
  • Action: Should emit encounterJourney.orderCreated for queue sync, but does NOT need mandatory orderRequestRef — billing flows through bed/ward charges, not the order system

emsRequest vs emergencyMedicalService (Disambiguation)

These are different services for different domains:

Aspect emsRequest (medication) emergencyMedicalService (administration)
Purpose EMS dispatch operations — missions, vehicles, staff Clinical ER classification — severity, treatment
Collection ems_request emergency_medical_service
Status model 17 operational states 3 simple states
Gateway ✅ Whitelisted (medication.emsRequest.*) ❌ Missing
Frontend room-appointments, healthops-kit er-request miniapp

Both should eventually have orderRequest linkage, but emsRequest is operational/logistics while emergencyMedicalService is clinical/administrative.


Migration Safety

  • All changes are additive — new fields (orderRequestRef) are optional
  • Existing orders without orderRequestRef continue to work
  • The orderRequest.create call in each module is wrapped in try/catch with compensation (saga pattern already used by OR and Labour Room)
  • Billing activation is gated on newSaleOrder: true — existing orders that predate the change won’t suddenly generate invoices
  • Event emissions are fire-and-forget (ctx.emit, not ctx.call) so they don’t block the main create flow

Implementation Priority

  1. Phase 0 — Gateway fixes (5 min, zero risk)
  2. Phase 1a — OR Request + Labour Room orderRequest linkage (same DTO shape, do together)
  3. Phase 1b — ER Request orderRequest linkage + event emissions
  4. Phase 2 — Event standardization across all order types
  5. Phase 3 — FHIR ServiceRequest inbound mapping for all order types
  6. Phase 4 — Payment pipeline verification (create order → SalesOrder → Invoice end-to-end)

Order Bus — Real-Time Department Streaming

Architecture

Backend create/update → hospital_events INSERT
  → Postgres trigger → encounter-orchestrator Edge Function
    → department_queues (operational queue)
    → order_bus (real-time event log)    ← NEW
      → Supabase Realtime (postgres_changes)
        → Frontend useOrderBusSubscription hook
          → Department UI (alerts, dashboards, billing)

order_bus Table

Append-only event log. Each row captures one order status change.

Column Type Purpose
order_type text pathology, blood_bank, imaging, or_request, labour_room, admission, er_request
order_id text MongoDB _id of the specialized order
order_request_id text Parent orderRequest _id (billing hub)
event_kind text created, status_changed, cancelled, completed, acknowledged, financial_updated
dept_target text[] Routing array — which departments see this event
financial jsonb { salesOrderId, total, netPay, currency, paymentStatus }

Default Department Routing

Order Type Dept Targets
pathology lab, cashier, pharmacy
blood_bank blood_bank, cashier, lab
imaging imaging, cashier, radiology
or_request or, cashier, anesthesia, nursing
labour_room labour, cashier, anesthesia, nursing
admission ipd, cashier, nursing, bed_management
er_request er, cashier, triage

Frontend Subscription

import { useOrderBusSubscription } from '@/hooks/useOrderBusSubscription';

// Cashier sees ALL financial events across all order types
const { events } = useOrderBusSubscription({
  deptFilter: 'cashier',
  queryKeysToInvalidate: [['billing-queue']],
});

// Lab sees only pathology + blood bank orders
const { events } = useOrderBusSubscription({
  orderTypeFilter: ['pathology', 'blood_bank'],
  onEvent: (row) => showToast(`New ${row.order_type} order: ${row.patient_name}`),
});

// Single encounter view
const { events } = useOrderBusSubscription({
  encounterFilter: encounterId,
});

Financial Update Flow

When a SalesOrder is created or updated, the financial service emits hospital.financial.updated via Moleculer. The encounterJourney handler in the medication service catches this and:

  1. Hydrates encounter_journey_cache.financial_summary (existing behavior)
  2. Writes a FINANCIAL_UPDATE event to hospital_events (new)
  3. The orchestrator picks up the event and writes to order_bus with event_kind: 'financial_updated' and dept_target: ['cashier', 'billing']

Verification Status (2026-05-15)

Order Type Create → order_bus Update → order_bus Verified
ER Request ✅ Direct Supabase write ✅ Direct Supabase write ✅ End-to-end
OR Request ✅ Via emitOrderCreated ✅ Via emitTicketUpdate ✅ End-to-end
Labour Room ✅ Via emitOrderCreated ✅ Via emitTicketUpdate ✅ End-to-end
Admission ✅ Direct Supabase write ✅ Direct Supabase write ✅ End-to-end
Financial ✅ Via emitFinancialUpdate ✅ End-to-end
Pathology/Lab ✅ Existing ✅ Existing ✅ Pre-existing
Blood Bank ✅ Existing ✅ Existing ✅ Pre-existing
Imaging ✅ Existing ✅ Existing ✅ Pre-existing

Key Files

File Purpose
infrastructure/medbase/migrations/20260515h_order_bus.sql Table + routing + indexes + RLS
infrastructure/medbase/functions/encounter-orchestrator/index.ts Bus write from all order handlers
web/src/hooks/useOrderBusSubscription.ts Frontend subscription hook
infrastructure/medbase/functions/_shared/event-contract.ts Department key registry
services/medication/.../encounterJourney/encounterJourney.service.ts emitFinancialUpdate method
services/medication/.../encounterJourney/encounterJourney.controller.mixin.ts hospital.financial.updated handler

File Purpose
services/gateway/src/ApiGatewayService.ts Gateway whitelist (Phase 0)
services/medication/src/api/medication/modules/orderRequest/ Central order entity
services/medication/src/api/medication/modules/operatingRoomRequest/ OR Request module
services/medication/src/api/medication/modules/labourRoomRequest/ Labour Room module
services/administration/src/api/administration/modules/emergencyMedicalService/ ER Request module
services/medication/src/api/medication/modules/bloodRequest/ Blood Bank (reference impl)
services/medication/src/api/medication/modules/imagingRequest/ Imaging (reference impl)
packages/platform-api-schema/src/medication/orderRequest/entity/OrderRequest.ts OrderRequest schema
packages/platform-api-schema/src/medication/orderRequestItem/entity/OrderRequestItem.ts OrderRequestItem schema
docs/architecture/encounter-orchestrator-triggers.md Event flow reference
services/public-api/.../modules/fhir/ FHIR integration layer
Ask Anything