Communication Service — User Stories
Service: communication-service Story prefix: COMMS-US Last updated: 2026-04-17
Stories
COMMS-US-001 — Create a care-team thread
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Create a provider-to-provider thread with participants |
| Epic link | COMMS-EPIC-01 |
| Status | To Do |
| Priority | Must |
| Story points | 3 |
| Labels | service:communication, type:backend, slice:S1 |
| Components | messaging |
| FR references | FR-COMMS-MSG-001 |
| Legacy FR refs | FR-MSG-001, FR-DCOM-MSG-001 |
| Dependencies | IDENT-US-001 |
User story: As a physician, when I need to coordinate on a patient, I want to open a secure thread with selected colleagues so that we can discuss without insecure channels.
Acceptance criteria (Gherkin):
- Given I have the DOCTOR role, when I POST
/v1/communication/threadswith valid participantIds in my tenant, then the server returns 201 and emitscommunication.thread.created. - Given a participantId is in a different tenant, when I POST, then the server returns 403
FORBIDDEN. - Given I pass a
patientIdI cannot read (no chart permission), when I POST, then the server returns 403FORBIDDEN.
Technical notes: Controller ThreadsController; use case CreateThreadUseCase; RLS on message_threads.
Definition of Done: Unit + integration tests; OpenAPI updated; event schema registered; trace + metric added; docs updated.
COMMS-US-002 — Send message in a thread
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Send a message with urgency into an existing thread |
| Epic link | COMMS-EPIC-01 |
| Status | To Do |
| Priority | Must |
| Story points | 3 |
| Labels | service:communication, type:backend, slice:S1 |
| Components | messaging, interop |
| FR references | FR-COMMS-MSG-003 |
| Legacy FR refs | FR-MSG-001, FR-DCOM-MSG-003 |
| Dependencies | COMMS-US-001, INTEROP-US-005 |
User story: As a thread participant, when I compose a message, I want it persisted and projected to FHIR Communication so downstream systems and chart timelines see it.
Acceptance criteria:
- Given I am a participant, when I POST a non-empty body with urgency, then the server returns 201 with
messageIdand emitscommunication.message.sent. - Given the thread is archived, when I POST, then the server returns 409
INVALID_STATE_TRANSITION. - Given I repeat with the same
Idempotency-Key, when I POST, then the server returns the prior response without creating a duplicate.
Technical notes: Outbox-published event; FHIR write via interop-service in same saga; Idempotency-Key honored.
COMMS-US-003 — Mark messages read (LWW across devices)
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Idempotent read receipts with LWW across devices |
| Epic link | COMMS-EPIC-01 |
| Status | To Do |
| Priority | Must |
| Story points | 2 |
| Labels | service:communication, type:backend, slice:S1 |
| Components | messaging |
| FR references | FR-COMMS-MSG-004 |
| Legacy FR refs | FR-DCOM-MSG-004 |
| Dependencies | COMMS-US-002 |
User story: As a user reading messages on multiple devices, when I scroll through a thread, I want the latest read time preserved so unread counts are consistent.
Acceptance criteria:
- Given I POST
/readwithmessageIds, when the request succeeds, thenmessage_read_receiptsupserts withread_at. - Given another device POSTs older
read_at, when the second request arrives, then the newerread_atis retained (LWW).
COMMS-US-004 — Escalate thread
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Escalate a thread to on-call routing |
| Epic link | COMMS-EPIC-01 |
| Status | To Do |
| Priority | Should |
| Story points | 3 |
| Labels | service:communication, type:backend, slice:S1 |
| Components | messaging, notifications |
| FR references | FR-COMMS-MSG-006 |
| Legacy FR refs | FR-MSG-005, FR-DCOM-MSG-006 |
| Dependencies | COMMS-US-007 |
User story: As a clinician, when a conversation becomes urgent, I want to escalate it so on-call staff get paged immediately.
Acceptance criteria:
- Given I escalate a thread, when I POST
/escalate, then the server emitscommunication.thread.escalatedand queues a high-priority notification intent to the on-call role. - Given a role cannot escalate per policy, when they POST, then 403.
COMMS-US-005 — Archive thread
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Archive a thread |
| Epic link | COMMS-EPIC-01 |
| Status | To Do |
| Priority | Could |
| Story points | 1 |
| Labels | service:communication, type:backend |
| FR references | FR-COMMS-MSG-005 |
| Legacy FR refs | FR-DCOM-MSG-005 |
User story: As a thread owner, when a topic resolves, I want to archive the thread so it no longer appears in my inbox.
Acceptance criteria:
- Given an active thread, when I PATCH
/archive, then status = archived and new messages rejected.
COMMS-US-006 — Attachment upload with AV scan
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Upload attachment with malware scan before attach |
| Epic link | COMMS-EPIC-01 |
| Status | To Do |
| Priority | Should |
| Story points | 5 |
| Labels | service:communication, type:backend, slice:S2 |
| Components | messaging, av-scan |
| FR references | FR-COMMS-MSG-008 |
| Legacy FR refs | FR-MSG-004, FR-DCOM-ENH-003 |
User story: As a clinician, when I attach a file, I want it scanned before peers can see it so malware is blocked.
Acceptance criteria:
- Given a file uploaded, when scan = clean, then attachment becomes attachable to messages.
- Given scan = infected, when scan completes, then quarantine event emitted and attachment unusable.
COMMS-US-007 — Submit notification intent (internal API)
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Internal API for services to request notifications |
| Epic link | COMMS-EPIC-02 |
| Status | To Do |
| Priority | Must |
| Story points | 3 |
| Labels | service:communication, type:api, slice:S1 |
| FR references | FR-COMMS-NOTIF-001 |
| Legacy FR refs | FR-MSG-012, FR-DCOM-NOTIF-001 |
User story: As a domain service, when I need to notify a user, I want to post a single intent with category + recipient + template so I don't encode PHI or channel choice.
Acceptance criteria:
- Given a valid intent with opaque variables, when POST to
/notifications/intents, then 202 returned withintentIdand server emits.queued. - Given duplicate
(tenantId, correlationId, channel, recipientRef), when POST repeats, then idempotent — prior intent returned.
COMMS-US-008 — Send SMS via Ghasi-SMS-Gateway
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | SMS dispatch through Ghasi-SMS-Gateway adapter with DLR |
| Epic link | COMMS-EPIC-02 |
| Status | To Do |
| Priority | Must |
| Story points | 5 |
| Labels | service:communication, type:backend, slice:S1 |
| Components | adapters |
| FR references | FR-COMMS-NOTIF-002, FR-COMMS-NOTIF-004 |
| Legacy FR refs | FR-MSG-010 |
User story: As a platform operator, when intents target Afghan tenants, I want SMS routed through Ghasi-SMS-Gateway with DLR callbacks so delivery is visible.
Acceptance criteria:
- Given a queued SMS intent for Afghan tenant, when the worker dispatches, then adapter calls Ghasi-SMS-Gateway and records
providerMessageId. - Given a DLR callback arrives with valid HMAC, when received, then
DispatchRecord.outcomeupserts todelivered/failed. - Given Ghasi-SMS-Gateway is unhealthy, when configured secondary exists, then failover to secondary provider.
COMMS-US-009 — Push notifications (FCM / APNs / WebPush)
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Push dispatch without PHI in payload |
| Epic link | COMMS-EPIC-02 |
| Status | To Do |
| Priority | Must |
| Story points | 5 |
| Labels | service:communication, type:backend, slice:S2 |
| FR references | FR-COMMS-NOTIF-002 |
| Legacy FR refs | FR-MSG-009 |
User story: As a mobile user, when I receive a notification, I want it to be useful but free of PHI so a glance doesn't expose clinical data.
Acceptance criteria:
- Given a push intent with opaque variables, when dispatched, then payload contains only category, templateKey, deepLink; zero PHI.
- Given a device token is invalid, when FCM returns feedback, then token marked inactive and
DispatchRecord.outcome=undeliverable.
COMMS-US-010 — Email dispatch with bounce handling
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Email via SES/SendGrid with bounce feedback |
| Epic link | COMMS-EPIC-02 |
| Status | To Do |
| Priority | Must |
| Story points | 3 |
| Labels | service:communication, type:backend, slice:S1 |
| FR references | FR-COMMS-NOTIF-002 |
| Legacy FR refs | FR-MSG-009 |
User story: As an operator, when bounce rates spike, I want the system to auto-throttle the affected category so reputation is protected.
Acceptance criteria:
- Given bounce ratio > 5% over 15 min, when threshold breached, then alert fires and category paused for tenant.
COMMS-US-011 — Category channel policy configuration
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Tenant-configurable per-category channel allowlist |
| Epic link | COMMS-EPIC-02 |
| Status | To Do |
| Priority | Should |
| Story points | 3 |
| Labels | service:communication, type:api, slice:S2 |
| FR references | FR-COMMS-NOTIF-005 |
User story: As a tenant admin, when I want to disable SMS for marketing, I want to configure allowed channels per category without code changes.
Acceptance criteria:
- Given a PUT to
/config/notifications/channelswith valid policy, when accepted, then policy persists and takes effect on next dispatch.
COMMS-US-012 — Create virtual session from scheduling event
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Consume SCHEDULING appointment.created (class=VR) to create session idempotently |
| Epic link | COMMS-EPIC-03 |
| Status | To Do |
| Priority | Must |
| Story points | 5 |
| Labels | service:communication, type:backend, slice:S2 |
| FR references | FR-COMMS-VC-016 |
| Legacy FR refs | FR-DCOM-VC-016 |
User story: As a scheduler, when I create a virtual visit appointment, I want the session auto-created so the patient and provider can join when the time comes.
Acceptance criteria:
- Given
SCHEDULING.appointment.createdwith class=VR, when consumed, then at most oneVirtualSessionexists for that appointmentId in the tenant. - Given the same event redelivered, when consumed, then no duplicate session created.
COMMS-US-013 — Issue short-lived join token
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | One-time KMS-signed join token with ≤ 5 min TTL |
| Epic link | COMMS-EPIC-03 |
| Status | To Do |
| Priority | Must |
| Story points | 3 |
| Labels | service:communication, type:backend, slice:S2 |
| FR references | FR-COMMS-VC-002 |
| Legacy FR refs | FR-DCOM-VC-002 |
User story: As an enrolled participant, when I tap the join link, I want a short-lived token issued so unauthorized parties cannot join.
Acceptance criteria:
- Given I have a valid JWT and am a session participant, when GET
/join-token, then I receive a token with ≤ 5 min TTL. - Given the token has already been used, when validated, then the server rejects.
COMMS-US-014 — Waiting-room admission by provider
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Provider admits participants from waiting room |
| Epic link | COMMS-EPIC-03 |
| Status | To Do |
| Priority | Must |
| Story points | 2 |
| Labels | service:communication, type:backend, slice:S2 |
| FR references | FR-COMMS-VC-003 |
| Legacy FR refs | FR-DCOM-VC-003 |
User story: As a provider, when patients queue in waiting room, I want to admit them deliberately so I control the visit start.
Acceptance criteria:
- Given a participant is in WAITING state, when provider POSTs
/admit/{participantId}, then state transitions to ACTIVE and event emitted.
COMMS-US-015 — End virtual session & emit billing signal
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Session end emits duration + billing chargeable event |
| Epic link | COMMS-EPIC-03 |
| Status | To Do |
| Priority | Should |
| Story points | 3 |
| Labels | service:communication, type:backend, slice:S2 |
| FR references | FR-COMMS-VC-004 |
| Legacy FR refs | FR-DCOM-VC-004 |
User story: As a billing service, when a VC session ends, I want a chargeable event so billing can post charges automatically.
Acceptance criteria:
- Given session ended with duration > threshold, when ended, then
virtual_session.billing.chargeableemitted.
COMMS-US-016 — Fallback-to-messaging on VC failure
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Auto-spawn secure thread when virtual session fails |
| Epic link | COMMS-EPIC-04 |
| Status | To Do |
| Priority | Should |
| Story points | 5 |
| Labels | service:communication, type:backend, slice:S3 |
| FR references | FR-COMMS-VC-023 |
| Legacy FR refs | FR-DCOM-VC-023 |
User story: As a patient whose virtual session drops, I want the system to spin up a secure message thread with my provider so we don't lose continuity.
Acceptance criteria:
- Given session enters FAILED state, when failure detected, then new thread created linking all participants and session id.
- Given participants share no thread yet, when fallback spawns, then they are all added and a seed message prompts continuation.
COMMS-US-017 — Critical-result fan-out
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Critical-result-flagged events dispatch push + SMS + in-app |
| Epic link | COMMS-EPIC-04 |
| Status | To Do |
| Priority | Must |
| Story points | 3 |
| Labels | service:communication, slice:S3 |
| FR references | FR-COMMS-NOTIF-003 |
| Legacy FR refs | FR-MSG-011 |
User story: As an on-call physician, when a critical result is released, I want immediate push + SMS + in-app so I don't miss it.
Acceptance criteria:
- Given
laboratory.result.critical.flagged, when consumed, then three intents dispatch in parallel on distinct channels.
COMMS-US-018 — Identity-bound PATIENT access
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | PATIENT role queries only their own threads/sessions |
| Epic link | COMMS-EPIC-05 |
| Status | To Do |
| Priority | Must |
| Story points | 2 |
| Labels | service:communication, type:api, slice:S1 |
| FR references | FR-COMMS-018 |
| Legacy FR refs | FR-DCOM-018 |
User story: As a patient, when I query my communications, I want only my data so nobody else is exposed.
Acceptance criteria:
- Given PATIENT role JWT, when GET
/threadsor/virtual-sessions, then results are scoped to the caller's patientId only.
COMMS-US-019 — Module entitlement enforcement
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | 403 MODULE_NOT_LICENSED on unlicensed tenants |
| Epic link | COMMS-EPIC-05 |
| Status | To Do |
| Priority | Must |
| Story points | 2 |
| Labels | service:communication, slice:S1 |
| FR references | FR-COMMS-LIC-001 |
| Legacy FR refs | FR-MSG-008, FR-DCOM-LIC-001 |
User story: As a tenant without the messaging license, when my users try to call the API, I want a clear 403 so I know to upgrade.
Acceptance criteria:
- Given a tenant without
engage.messagingentitlement, when any/v1/communication/threads*called, then 403MODULE_NOT_LICENSED.
COMMS-US-020 — GDPR erasure participation
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Anonymize messages and drop dispatch PII on erasure |
| Epic link | COMMS-EPIC-05 |
| Status | To Do |
| Priority | Must |
| Story points | 5 |
| Labels | service:communication, slice:S3 |
| FR references | FR-COMMS-019 |
| Legacy FR refs | FR-DCOM-019 |
User story: As a data subject, when I request erasure, I want my content removed while legal audit preserved.
Acceptance criteria:
- Given
identity.user.gdpr_erasure_requested, when consumed, then message bodies redacted and dispatch_records recipient_ref removed within SLA; legal-hold overrides.
COMMS-US-021 — Per-channel dispatch status query
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Query outcome per channel by correlation id |
| Epic link | COMMS-EPIC-06 |
| Status | To Do |
| Priority | Should |
| Story points | 3 |
| Labels | service:communication, type:api, slice:S4 |
| FR references | FR-COMMS-ENH-001 |
| Legacy FR refs | FR-DCOM-ENH-001 |
User story: As support, when a user says "I never got the SMS", I want to look up the correlation id and see which channel succeeded.
Acceptance criteria:
- Given a correlation id, when GET
/dispatch-status, then per-channel outcomes are returned with timestamps.
COMMS-US-022 — Notification p95 latency SLO dashboards
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Dashboards + alerts for dispatch p95 by channel |
| Epic link | COMMS-EPIC-06 |
| Status | To Do |
| Priority | Should |
| Story points | 3 |
| Labels | service:communication, type:observability, slice:S4 |
| FR references | NFR-COMMS-ENH-001 |
| Legacy FR refs | NFR-DCOM-ENH-001 |
User story: As SRE, when p95 drifts past SLO, I want an alert so I act before customers escalate.
Acceptance criteria:
- Given p95 dispatch latency > SLO for 10 min, when the condition holds, then PagerDuty fires with runbook link.