Specimen Transport Bounded Context
Separate from patient transport: temperature chain, time-to-lab, chain of custody, disjoint carrier pools.
Status: Implemented (2026-05-27) DDD rationale: Specimen transport is a separate bounded context from patient transport (migration 044). The invariants diverge — temperature chain, time-to-lab limits, chain of custody, biohazard class — and the carrier pools are disjoint (runner/pneumatic tube/AGV/robot vs stretcher/wheelchair/gurney/ambulance).
Why Not Extend transport_request?
The “just add a subject_type discriminator” approach produces:
- Nullable-column sprawl — specimens have no
escort_required; patients have notemperature_zone - Per-type validation branches everywhere
- Carrier dispatch logic that filters out wrong-domain carriers on every query
- HL7 OML/FHIR Task integration that ignores 80% of rows
Instead: sibling aggregates with shared patterns (status lifecycle, pickup/dropoff geometry, append-only event log, ack-on-handoff, queue placement).
Entity Contracts
specimen_transport_request (aggregate root)
Status lifecycle:
pending → assigned → in_transit → delivered → received_at_lab
↗ rejected
↗ failed
(any) → cancelled
| Column | Type | Specimen-specific? |
|---|---|---|
specimen_ids |
jsonb | Yes — batch transport |
temperature_zone |
text (ambient/refrigerated/frozen/body_temp) | Yes |
biohazard_class |
text (standard/bsl2/bsl3/radioactive) | Yes |
time_limit_minutes |
integer | Yes — SLA enforcement |
lab_request_id |
text | Yes — links to MongoDB LabRequest |
carrier_id |
uuid FK → specimen_carrier | Yes — disjoint carrier pool |
assigned_runner_id |
uuid | Yes — lab runner, not porter |
specimen_carrier (carrier registry)
Types: runner, pneumatic_tube, agv, robot, courier, manual
Completely disjoint from stretcher registry (patient transport).
specimen_transport_event_log (append-only audit)
Event types include specimen-specific entries: temperature_alert, custody_handoff.
Event Contract
Legacy → Normalized mapping:
| Legacy | Normalized |
|---|---|
SPECIMEN_TRANSPORT_REQUESTED |
manifest.specimen_transport.requested |
SPECIMEN_TRANSPORT_ASSIGNED |
manifest.specimen_transport.assigned |
SPECIMEN_TRANSPORT_IN_TRANSIT |
manifest.specimen_transport.in_transit |
SPECIMEN_TRANSPORT_DELIVERED |
manifest.specimen_transport.delivered |
SPECIMEN_TRANSPORT_RECEIVED_AT_LAB |
manifest.specimen_transport.received_at_lab |
SPECIMEN_TRANSPORT_REJECTED |
manifest.specimen_transport.rejected |
SPECIMEN_TRANSPORT_FAILED |
manifest.specimen_transport.failed |
SPECIMEN_TRANSPORT_CANCELLED |
manifest.specimen_transport.cancelled |
Orchestrator Handlers
handleSpecimenTransportRequested (separate file)
Side effects:
- Insert
department_queuesrow withdept_type='specimen_transport' - Create
acknowledgement_requestswith tighter STAT escalation (3 min vs porter’s 5 min)
handleSpecimenTransportPipeline (inline in orchestrator)
Side effects:
- Update
clinical_context.specimen_transport_tracking[transportId]with stage + timestamps - Delegate to
handleSpecimenTransportCompletedon terminal events - Patch queue metadata for in-progress status updates
handleSpecimenTransportCompleted (separate file)
Side effects:
- Mark
department_queuesrows COMPLETED - Close
acknowledgement_requests
Queue Integration
The migration includes trg_sync_specimen_transport_queue — a Postgres trigger that auto-syncs specimen_transport_request status changes to department_queues with dept_type='specimen_transport'. This mirrors the billing-queue auto-sync pattern.
Backend Service
Lives in diagnostic service (DDD: specimen vocabulary = diagnostic domain).
| File | Purpose |
|---|---|
services/diagnostic/.../specimenTransport/specimenTransport.module.ts |
NestJS module |
services/diagnostic/.../specimenTransport/specimenTransport.service.ts |
Business logic |
services/diagnostic/.../specimenTransport/specimenTransport.repository.ts |
MongoDB access |
services/diagnostic/.../specimenTransport/specimenTransport.controller.mixin.ts |
Moleculer REST actions |
REST Endpoints
| Method | Path | Action |
|---|---|---|
| GET | /specimen-transports/by-encounter/:encounterId |
List by encounter |
| GET | /specimen-transports/by-lab-request/:labRequestId |
List by lab request |
| GET | /specimen-transports/pending |
Active requests |
| POST | /specimen-transports |
Create request |
| PUT | /specimen-transports/:id/assign |
Assign runner + carrier |
| PUT | /specimen-transports/:id/advance |
Move to next status |
| PUT | /specimen-transports/:id/reject |
Reject with reason |
| PUT | /specimen-transports/:id/fail |
Mark failed |
| PUT | /specimen-transports/:id/cancel |
Cancel with reason |
Frontend Service
web/src/services/medbase/specimenTransport.medbase.ts — Supabase read model client with column whitelist pattern. Same API shape as transport.medbase.ts but with specimen-specific types.
Shared Kernel (conventions, not code)
Both patient transport and specimen transport follow these patterns documented in docs/module-loop/BACKEND-CONTRACTS.md:
- Status lifecycle enum with ASCII progression
- Append-only event log with actor tracking
department_queuesplacement for worklist integrationacknowledgement_requestsfor escalation- Optimistic-lock claim pattern (first claim wins)
File Inventory
| File | Layer |
|---|---|
infrastructure/medbase/migrations/20260527c_specimen_transport.sql |
Schema |
infrastructure/medbase/functions/_shared/event-contract.ts |
Events |
infrastructure/medbase/functions/encounter-orchestrator/index.ts |
Orchestrator routing + inline handler |
infrastructure/medbase/functions/inpatient-handlers/handleSpecimenTransportRequested.ts |
Queue + ack creation |
infrastructure/medbase/functions/inpatient-handlers/handleSpecimenTransportCompleted.ts |
Queue + ack closure |
packages/platform-api-schema/src/diagnostic/specimenTransport/ |
TypeGoose entity |
services/diagnostic/.../modules/specimenTransport/ |
Backend service module |
web/src/services/medbase/specimenTransport.medbase.ts |
Frontend service |