Acknowledgement System
Universal AcknowledgementRequest (FHIR Task) wrapping any order/note/encounter to request explicit acknowledgement, multi-channel.
Shipped: 2026-04-29 (commit a77ada04)
Migration: infrastructure/medbase/migrations/035_acknowledgement_requests.sql
Edge function: infrastructure/medbase/functions/ack-escalator/
FHIR profile: http://medos.health/fhir/StructureDefinition/AcknowledgementTask
Problem
Acknowledgement was fragmented across the codebase:
CoSignRequest(foundation) — only referencesPatientNote; not extensible to lab/imaging/medication/etc.- medical-editor
wait-acknowledgementworkflow state — local-only, no backend persistence. Consultation— only multi-user “ack-like” entity; uses recipientType USER/ROLE/DEPT.- No multi-channel delivery (app/web/email/SMS/push), no escalation, no reminders.
The clinic needs any order — lab, imaging, medication, encounter sign-off, discharge approval, billing — to be able to ask any user / role / department / group for an acknowledgement, with a deadline + escalation chain + RRULE-driven reminders + multi-channel delivery.
Shape
Polymorphic AcknowledgementRequest entity that wraps an existing order. Maps cleanly onto FHIR R4 Task with Task.focus pointing at the underlying resource (Task is the FHIR “asked-to-do-something” resource; ServiceRequest is the order).
AcknowledgementRequest
├── subject { orderType, orderId, display }
├── requester (User ref)
├── recipient { recipientType: user|role|department|group, userId|roleId|...}
├── reason, priority routine|urgent|stat
├── status PENDING → ACKNOWLEDGED | DECLINED | EXPIRED | ESCALATED | CANCELLED
├── response { acknowledgedBy, acknowledgedAt, comment, signature }
├── deadlineAt drives escalation
├── reminderRule RRULE (rrule lib)
├── nextReminderAt set by escalator after each reminder
├── escalationChain [{ afterMinutes, escalateTo: recipient }]
├── escalationStep cursor into the chain
├── channels [APP, WEB, EMAIL, SMS, PUSH]
├── deliveryLog [{ channel, status, attemptedAt, providerMessageId }]
└── encounterRef, patientRef, workflowId, workflowStep
Data flow
Frontend / workflow node
│
│ POST /api/foundation/acknowledgementRequests
▼
foundation service (NestJS + Moleculer + MongoDB)
│
│ broadcasts Moleculer events:
│ acknowledgement.requested | responded | escalated | reminder
▼
messaging service (acknowledgementDispatcher.mixin)
│
├── Socket.IO (APP/WEB) ──→ recipient's room (userId / role:X / dept:Y / group:Z)
├── MailTransporterService (EMAIL) ──→ Nodemailer ──→ email
├── SMS (feature-flagged, provider TBD)
├── Push (feature-flagged, provider TBD)
└── For each attempt: foundation.acknowledgementRequest.appendDelivery → deliveryLog row
↓
hospital_events → encounter-orchestrator (Deno)
↓
acknowledgement_requests (Supabase read model)
↓
Supabase realtime ──→ AcknowledgementInbox (FAB + drawer)
Backend modules
| Module | Path | Purpose |
|---|---|---|
| Schema | packages/platform-api-schema/src/foundation/acknowledgementRequest/ |
Entity + enums (OrderType, RecipientType, Status, Priority, Channel) |
| Service | services/foundation/.../modules/acknowledgementRequest/ |
CRUD + respond + escalate + appendDelivery actions |
| Dispatcher | services/messaging/.../modules/acknowledgementDispatcher/ |
NestJS service + Moleculer event mixin; channel fan-out |
| Email template | services/messaging/views/emails/ack-request.hbs |
Handlebars body for the EMAIL channel |
| FHIR Task | services/public-api/.../modules/fhir/transformers/ack-to-fhir-task.transformer.ts |
Bidirectional medOS ↔ FHIR Task mapper |
| FHIR controller | services/public-api/.../modules/fhir/resources/task.controller.ts |
/fhir/Task (read/search/create/update); merges clinical-task and acknowledgement sources, profile-discriminated for writes |
Read model (Supabase)
| Object | Purpose |
|---|---|
acknowledgement_requests |
Projected from MongoDB via hospital_events → encounter-orchestrator |
acknowledgement_inbox (view) |
Per-user pending list with inbox_key (priority-ordered) |
acknowledgement_delivery_log |
One row per channel attempt (queued/sent/delivered/failed/skipped) |
| RLS | auth.uid() IS NOT NULL AND (recipient_user_id = auth.uid() OR requester_user_id = auth.uid() OR role/dept claim match) via ack_jwt_array_claim() helper |
| Realtime | Published — frontend subscribes via Supabase channels |
Escalator (edge function)
infrastructure/medbase/functions/ack-escalator/index.ts — Deno edge function invoked once per minute by a managed pg_cron job (registered in cron_jobs registry — see cron-jobs-registry.md).
State transitions:
PENDING + deadlineAt ≤ now()
├── escalation_step < length(escalationChain)
│ → status='pending' (or 'escalated' if no afterMinutes)
│ → recipient = chain[step].escalateTo
│ → deadlineAt = now + chain[step].afterMinutes
│ → emit hospital_events 'acknowledgement.escalated'
│
└── else
→ status='expired'
→ emit hospital_events 'acknowledgement.expired'
PENDING + next_reminder_at ≤ now()
└── recompute next_reminder_at via reminderRule (RRULE.after(now))
emit hospital_events 'acknowledgement.reminder' (no status change)
Frontend
| Component | Path | Mount |
|---|---|---|
AcknowledgementInbox |
web/src/common/components/medical/builder/acknowledgements/AcknowledgementInbox.tsx |
Drop-in FAB + side drawer; mounted in App.tsx for every authenticated user |
AcknowledgementAdminList |
same dir | Ops table (admin-only filterable list) |
useAcknowledgementInbox |
same dir | Supabase realtime subscription hook |
acknowledgementRequest.service.ts |
web/src/services/ever-foundation/acknowledgement-request/ |
Write helpers (create / respond / cancel / listByOrder) |
| Workflow action | web/packages/medical-kit/.../workflow-config/actions.ts |
ack-create-request payload builder for medical-editor wait-acknowledgement nodes |
Channel rollout
| Channel | v1 default | Feature flag |
|---|---|---|
| APP / WEB (Socket.IO + Supabase realtime) | ✅ on | — |
EMAIL (Nodemailer + ack-request.hbs) |
✅ on (when SMTP configured) | ACK_CHANNEL_EMAIL_ENABLED |
| SMS | scaffolded, off | ACK_CHANNEL_SMS_ENABLED |
| PUSH | scaffolded, off | ACK_CHANNEL_PUSH_ENABLED |
FHIR mapping
ackToFhirTask(record) produces a Task with:
Task.id← AcknowledgementRequest._idTask.status← mapped (pending→requested, acknowledged→completed, declined→rejected, expired→failed, escalated→in-progress)Task.intent='order',priority,descriptionTask.focus.reference←{ServiceRequest|MedicationRequest|Encounter|...}/{orderId}Task.for← Patient refTask.encounter← Encounter refTask.owner.reference←Practitioner|PractitionerRole|Organization|Group/{recipientId}Task.restriction.period.end← deadlineAtTask.meta.profileincludes the AcknowledgementTask profile URL
fhirTaskToAck(task) is the inverse; the task.controller.ts POST/PUT routes detect the AcknowledgementTask profile and route writes to foundation.acknowledgementRequest.create/update instead of the legacy clinical.task.*.
Verification (smoke tests)
- 26/26 pure-function transformer tests pass (status mappings, recipient → FHIR resource, focus, profile, round trip)
- 16/16 SQL structural checks pass on the migration
- Edge function smoke:
POST /functions/v1/ack-escalatorreturns 200 +{ok:true, summary:{escalated:0, expired:0, reminded:0, errors:0}} - REST
/rest/v1/{acknowledgement_requests,_inbox,_delivery_log}return[]under anon (RLS enforced)
Deploy state
| Surface | State | Notes |
|---|---|---|
| Frontend (Vercel) | ✅ auto on git push origin main |
AcknowledgementInbox is mounted in App.tsx |
| Supabase migration 035 | ✅ applied to hynsmfrevlsegbmjnoiy (PH demo, shared) |
Manual paste in Studio |
| Supabase migration 036 (cron_jobs registry) | ⚠️ apply via Studio after this PR lands | Required to control the escalator schedule |
Edge function ack-escalator |
✅ deployed | Symlinked at infrastructure/medbase/supabase/functions/ack-escalator |
pg_cron ack-escalator-tick |
⚠️ enable from /super-admin/cron-jobs after setting GUCs |
See cron-jobs-registry.md |
| Backend services (foundation/messaging/public-api) | ⚠️ manual EC2 deploy required | Until then, POST /api/foundation/acknowledgementRequests 404s |
Out of scope (v1)
- Replacing
CoSignRequest— leave as-is; can migrate later assubject.orderType='note'. - SMS / push provider selection — scaffolded behind feature flags, providers chosen per market.
- Multi-recipient response aggregation — single-responder MVP.
- Offline-queue ack on mobile — relies on existing Socket.IO reconnect.