Central Order System
How all clinical order types flow through orderRequest as the canonical order.
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:
- On
create, auto-create a parentorderRequestwith the appropriatecategoryandProductType - Store
orderRequestRefon the specialized entity - Emit
hospital.financial.updatedso billing picks it up - Emit
encounterJourney.orderCreatedfor 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:
- Create
SalesOrderfromorderRequest(whennewSaleOrder: true) - Calculate pricing from
orderRequestItem.unitPrice × qty - Apply discounts per payor scheme
- 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
OrderRequestItemdocuments (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
recoveryRoomRequeston RECOVERED,anesthesiaSystemon 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
recoveryRoomRequeston RECOVERED,anesthesiaSystemon 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 (
orderRequestref) - Events:
clinical.imaging.created,clinical.imaging.status_updated,clinical.imaging.reported - Financial:
saleOrderStatusfield +financial.salesOrder.updatePriceon cancel - System discriminator:
RADIOLOGYorNUCLEAR_MEDICINE
Admission (special case)
- Collection:
admission - orderRequest: Optional (via
testFields.formDataOrderToday) - Financial:
financialClassenum (self-funded, insurance, workers comp, etc.) - Billing model: Bed charges + per-diem, not item-based like other orders
- Action: Should emit
encounterJourney.orderCreatedfor queue sync, but does NOT need mandatoryorderRequestRef— 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
orderRequestRefcontinue to work - The
orderRequest.createcall 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, notctx.call) so they don’t block the main create flow
Implementation Priority
- Phase 0 — Gateway fixes (5 min, zero risk)
- Phase 1a — OR Request + Labour Room orderRequest linkage (same DTO shape, do together)
- Phase 1b — ER Request orderRequest linkage + event emissions
- Phase 2 — Event standardization across all order types
- Phase 3 — FHIR ServiceRequest inbound mapping for all order types
- 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:
- Hydrates
encounter_journey_cache.financial_summary(existing behavior) - Writes a
FINANCIAL_UPDATEevent tohospital_events(new) - The orchestrator picks up the event and writes to
order_buswithevent_kind: 'financial_updated'anddept_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 |
Related Files
| 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 |