Population Health Service — User Stories
Service: population-health-service Story prefix: POPHEALTH-US Last updated: 2026-04-18
Stories
POPHEALTH-US-001 — View population dashboard aggregate metrics
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Display aggregate population health metrics scoped by facility/department/provider |
| Epic link | POPHEALTH-EPIC-01 |
| Status | To Do |
| Priority | Must |
| Story points | 5 |
| Labels | service:population-health, type:api, type:backend, slice:S1 |
| Components | dashboard |
| FR references | FR-POPHEALTH-001 – FR-POPHEALTH-007 |
| Legacy FR refs | FR-POP-001 – FR-POP-007 |
| Dependencies | patient-chart-service (encounter projections), registration-service |
User story: As a facility administrator, when I open the population health dashboard, I want to see aggregate counts, age/gender distributions, high-risk patient counts, and screening compliance rates for my authorized scope so that I can quickly assess the health status of my population.
Acceptance criteria (Gherkin):
- Given I am authenticated with
facility_adminrole andpopulation_health:dashboard:readscope, when I callGET /api/v1/population-health/dashboard?facilityId=<id>, then I receive aggregate metrics includingactivePatients,ageDistribution,highRiskCount,screeningCompliance, anddataFreshness. - Given I do not have
phi:readpermission, when I call the dashboard endpoint, then the response contains nopatientIdfields. - Given the clinical data is more than 2 hours stale, when I call the dashboard endpoint, then the
dataFreshnessfield reflects the last successful sync time and astaleDataWarningflag is set.
Technical notes:
- Dashboard aggregates served from materialized snapshots; read from analytics replica.
dataFreshnesscomputed from maxlast_synced_atacross upstream event watermarks.- Node-scope ABAC enforced at application layer before query.
Definition of Done:
- Unit + integration tests added; coverage ≥ thresholds.
- OpenAPI contract updated; Pact consumer tests green.
- Event schema registered; schema conformance test green.
- Telemetry spans/metrics added.
- Documentation updated in relevant 17 docs.
POPHEALTH-US-002 — View disease registry for a specific condition
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | View paginated disease registry members with control status and risk tier filters |
| Epic link | POPHEALTH-EPIC-01 |
| Status | To Do |
| Priority | Must |
| Story points | 3 |
| Labels | service:population-health, type:api, slice:S1 |
| Components | registries |
| FR references | FR-POPHEALTH-020 – FR-POPHEALTH-022 |
| Legacy FR refs | FR-POP-020 – FR-POP-022 |
| Dependencies | patient-chart-service |
User story: As a nurse care coordinator, when I open the diabetes registry, I want to see patients filtered by risk tier and control status with their last visit and lab dates so that I can prioritize outreach for uncontrolled high-risk patients.
Acceptance criteria (Gherkin):
- Given I have
population_health:registry:readandphi:readpermissions, when I callGET /api/v1/population-health/registries/diabetes?riskTier=high&controlStatus=uncontrolled, then I receive a paginated list ofDiseaseRegistryEntryrows includingpatientId. - Given I have
population_health:registry:readbut NOTphi:read, when I call the registry endpoint, thenpatientIdis null in all rows but aggregate counts are still returned. - Given I request a registry type outside my authorized node scope, when I call the endpoint, then I receive 403
CROSS_TENANT_SCOPE_VIOLATION.
Technical notes:
- Registry rows derived from clinical service projections; refreshed on
patient_chart.encounter.completed.v1and related events. activeGapFlagspopulated by care-gap engine run.
Definition of Done:
- Unit + integration tests added; coverage ≥ thresholds.
- OpenAPI contract updated; Pact consumer tests green.
- Telemetry spans/metrics added.
- Documentation updated.
POPHEALTH-US-003 — View screening compliance and immunization coverage
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Screening compliance aggregates and immunization coverage rates by type and age band |
| Epic link | POPHEALTH-EPIC-01 |
| Status | To Do |
| Priority | Must |
| Story points | 3 |
| Labels | service:population-health, type:api, slice:S1 |
| Components | screenings, immunizations |
| FR references | FR-POPHEALTH-030 – FR-POPHEALTH-042 |
| Legacy FR refs | FR-POP-030–042 |
| Dependencies | immunizations-service |
User story: As an MoPH analyst, when I review the immunization coverage dashboard, I want to see coverage rates broken down by vaccine type and age band including overdue counts so that I can identify vaccination gaps for targeted campaigns.
Acceptance criteria (Gherkin):
- Given I have
population_health:immunization:read, when I callGET /api/v1/population-health/immunizations/coverage?ageBand=0-4, then I receive coverage rates, overdue counts, and refusal counts for all vaccine types in the 0–4 age band. - Given I call screening compliance for
colorectal-cancer, when the response is returned, thencomplianceRate = completed / (due + overdue + completed)and the rate is between 0 and 1.
Technical notes:
- Immunization data read from
immunizations-serviceprojection; updated onimmunizations.immunization.administered.v1.
Definition of Done: Standard DoD applies.
POPHEALTH-US-004 — Drill down from aggregate metric to patient cohort
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Drill down from dashboard metric tile to role-filtered patient list |
| Epic link | POPHEALTH-EPIC-01 |
| Status | To Do |
| Priority | Must |
| Story points | 3 |
| Labels | service:population-health, type:api, slice:S1 |
| Components | dashboard, cohort-engine |
| FR references | FR-POPHEALTH-006 |
| Legacy FR refs | FR-POP-006 |
| Dependencies | — |
User story: As a clinician, when I click on an aggregate metric tile (e.g., "high-risk diabetic patients: 240"), I want to see the underlying patient cohort with PHI visible only if I have the correct permission so that I can act on the data.
Acceptance criteria (Gherkin):
- Given a dashboard metric tile returns
cohortDrilldownKey, when I callGET /api/v1/population-health/cohorts/<drilldownKey>, then I receive the membership list scoped to my authorized node. - Given my role does not have
phi:read, when I retrieve the drilldown cohort, thenpatientIdis suppressed but count is accurate.
Technical notes: Drill-down keys are dynamically generated cohort IDs from dashboard computation. No pre-stored cohort definition required.
Definition of Done: Standard DoD applies.
POPHEALTH-US-005 — Create and manage cohort definitions
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Create versioned cohort with boolean DSL, list, get, and archive cohorts |
| Epic link | POPHEALTH-EPIC-02 |
| Status | To Do |
| Priority | Must |
| Story points | 8 |
| Labels | service:population-health, type:backend, type:api, slice:S1 |
| Components | cohort-engine, cohort-dsl |
| FR references | FR-POPHEALTH-010 – FR-POPHEALTH-013 |
| Legacy FR refs | FR-POP-010 – FR-POP-013 |
| Dependencies | — |
User story: As an analyst, when I create a cohort definition using structured boolean predicates, I want the system to validate the expression and persist a versioned immutable definition so that cohort refreshes are deterministic and reproducible.
Acceptance criteria (Gherkin):
- Given I submit a valid
CohortExpressionNodetree, when IPOST /api/v1/population-health/cohorts, then the cohort is created withversion=1andstatus=Draft. - Given I submit an expression with an unknown
operatorvalue, when I POST the cohort, then I receive 400INVALID_COHORT_DSLwith the path of the offending node. - Given a cohort with
isShared=true, when another user in the same tenant callsGET /api/v1/population-health/cohorts, then the shared cohort appears in their list.
Technical notes:
- DSL parser implemented in
src/domain/cohort-expression.ts. - Version increment on each new definition save (not on metadata changes).
- Archived cohorts excluded from default list but accessible by ID.
Definition of Done: Standard DoD applies.
POPHEALTH-US-006 — Trigger and monitor cohort refresh
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Trigger async cohort refresh, coalesce duplicates, monitor job status |
| Epic link | POPHEALTH-EPIC-02 |
| Status | To Do |
| Priority | Must |
| Story points | 5 |
| Labels | service:population-health, type:backend, slice:S1 |
| Components | cohort-engine, refresh-worker |
| FR references | FR-POPHEALTH-014 |
| Legacy FR refs | FR-POP-014 |
| Dependencies | — |
User story: As an analyst, when I trigger a cohort refresh, I want the system to run membership computation in the background without blocking my session so that I can continue working while large cohorts are evaluated.
Acceptance criteria (Gherkin):
- Given a cohort in
Activestatus, when IPOST /api/v1/population-health/cohorts/:id/refresh, then I receive 202 with ajobIdandstatus: queued. - Given a refresh job is already running for a cohort, when I trigger another refresh for the same cohort, then I receive 202 with the existing
jobId(no duplicate job created). - Given a refresh job completes, when the completion event is emitted, then
population_health.cohort.refreshed.v1is published with the newmembershipCount.
Technical notes:
- Job coalescing:
SELECT ... FOR UPDATEon refresh job table before insert. - Worker reads cohort definition and evaluates against clinical projections in batches.
sourceWatermarkrecorded in event to enable staleness detection.
Definition of Done: Standard DoD applies.
POPHEALTH-US-007 — Run risk scoring for a cohort
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Trigger clinical risk scoring and view tier distribution for a cohort |
| Epic link | POPHEALTH-EPIC-03 |
| Status | To Do |
| Priority | Must |
| Story points | 5 |
| Labels | service:population-health, type:backend, slice:S1 |
| Components | risk-engine |
| FR references | FR-POPHEALTH-050 – FR-POPHEALTH-052 |
| Legacy FR refs | FR-POP-050 – FR-POP-052 |
| Dependencies | patient-chart-service |
User story: As a care coordinator, when I run risk scoring for a diabetic cohort, I want patients assigned to risk tiers (low/medium/high/critical) based on configurable clinical criteria so that I can prioritize interventions for the highest-risk patients.
Acceptance criteria (Gherkin):
- Given a valid cohort ID and
modelKey, when IPOST /api/v1/population-health/risk/score, then I receive 202 with a scoringjobId. - Given the scoring job completes, when I query the risk scores, then every patient in the cohort has a
computedTierassigned. - Given a senior clinician applies a manual override, when
PATCH /api/v1/population-health/risk/scores/:id/overrideis called with a reason, thenoverrideTierandoverrideReasonare persisted andpopulation_health.risk_score.overridden.v1is emitted.
Technical notes:
- Risk model config loaded from
config-service; key =clinical-risk-v1default. driversJsonstores per-factor contributions for display in UI.
Definition of Done: Standard DoD applies.
POPHEALTH-US-008 — Generate and work outreach lists
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Generate outreach list from cohort, assign to team, update contact status |
| Epic link | POPHEALTH-EPIC-03 |
| Status | To Do |
| Priority | Must |
| Story points | 5 |
| Labels | service:population-health, type:backend, type:api, slice:S1 |
| Components | outreach |
| FR references | FR-POPHEALTH-060 – FR-POPHEALTH-062 |
| Legacy FR refs | FR-POP-060 – FR-POP-062 |
| Dependencies | communication-service |
User story: As a care coordinator, when I generate an outreach list for overdue hypertension patients, I want to assign it to my team and track each contact attempt through to completion so that I can ensure all high-risk patients are followed up.
Acceptance criteria (Gherkin):
- Given a valid
cohortIdandassignedTeamId, when IPOST /api/v1/population-health/outreach/lists, then an outreach list is created andpopulation_health.outreach_list.generated.v1is emitted. - Given an outreach item in
Pendingstatus, when IPATCH /api/v1/population-health/outreach/items/:idwithstatus: attempted, then the status transitions andattemptCountincrements. - Given an outreach item in
Completedstatus, when I try to transition it toAttempted, then I receive 422INVALID_OUTREACH_TRANSITION.
Technical notes:
- Communication-service subscribes to
outreach_list.generatedfor notification delivery. - FSM transitions validated by
OutreachItemdomain object.
Definition of Done: Standard DoD applies.
POPHEALTH-US-009 — Compute quality metric snapshots
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Compute HEDIS/QOF/MoPH quality metric snapshots with numerator/denominator/exclusions |
| Epic link | POPHEALTH-EPIC-04 |
| Status | To Do |
| Priority | Must |
| Story points | 8 |
| Labels | service:population-health, type:backend, slice:S2 |
| Components | quality-metrics |
| FR references | FR-POPHEALTH-070 – FR-POPHEALTH-072 |
| Legacy FR refs | FR-POP-070 – FR-POP-072 |
| Dependencies | patient-chart-service, interop-service |
User story: As an MoPH quality officer, when I request quality metrics for Q1 2026, I want to see the HEDIS blood-pressure control rate including numerator, denominator, exclusions, and historical trend so that I can report program performance to donors.
Acceptance criteria (Gherkin):
- Given a configured HEDIS metric and a patient population, when the quality compute job runs, then
rate = numerator / (denominator - exclusions)is stored inquality_metric_snapshots. - Given
denominator = exclusions, when the snapshot is stored, thenrate = nullandemptyDenominatorReasonis populated. - Given a snapshot job completes, when
population_health.quality_metric.calculated.v1is emitted, then interop-service publishes a FHIRMeasureReportto the FHIR store.
Technical notes:
- Exclusion logic applied before numerator computation (BR-POP-003).
- Snapshots are immutable; new computation creates a new row.
Definition of Done: Standard DoD applies.
POPHEALTH-US-010 — View quality metric trends and donor indicator reports
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | View quality metric trend series and export donor indicator reports |
| Epic link | POPHEALTH-EPIC-04 |
| Status | To Do |
| Priority | Should |
| Story points | 3 |
| Labels | service:population-health, type:api, slice:S2 |
| Components | quality-metrics, exports |
| FR references | FR-POPHEALTH-071 |
| Legacy FR refs | FR-POP-071 |
| Dependencies | — |
User story:
As a donor program manager, when I query quality metrics for the donor program, I want to see only the indicators in my approved donor pack with quarterly trend data so that I can prepare program reports.
Acceptance criteria (Gherkin):
- Given my role is
donor_viewer, when I callGET /api/v1/population-health/quality-metrics?program=donor, then only metrics in thedonorprogram pack are returned. - Given multiple quarterly snapshots exist, when the endpoint returns a metric, then
trendcontains one entry per completed quarter up to 8 quarters.
Technical notes: donor_viewer role cannot access hedis, qof, or moph_custom programs.
Definition of Done: Standard DoD applies.
POPHEALTH-US-011 — Scheduled DHIS2 HMIS export
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Automated scheduled push of aggregate indicators to MoPH DHIS2 |
| Epic link | POPHEALTH-EPIC-05 |
| Status | To Do |
| Priority | Must |
| Story points | 8 |
| Labels | service:population-health, type:backend, slice:S2 |
| Components | hmis-exporter, dhis2-adapter |
| FR references | FR-POPHEALTH-080 |
| Legacy FR refs | FR-POP-080 |
| Dependencies | MoPH DHIS2 API |
User story: As an MoPH integration administrator, when the daily HMIS export schedule fires, I want the platform to automatically push service-utilization indicators to DHIS2 so that national reporting is always current without manual extraction.
Acceptance criteria (Gherkin):
- Given the export schedule is configured for daily at 02:00 AFT, when 02:00 arrives, then a HMIS export job is created and the DHIS2 POST is executed within 5 minutes.
- Given DHIS2 returns HTTP 502, when the adapter receives the failure, then the job is retried with exponential backoff up to 3 times before emitting
hmis_export.failed.v1. - Given DHIS2 accepts the push, when the import summary is parsed, then any
ignored > 0triggers an alert to the ops team.
Technical notes:
- DHIS2 credentials stored in KMS; never in DB.
- Indicator family → DHIS2 data element mapping in configurable YAML.
- Cron expression stored in
config-service; not hardcoded.
Definition of Done: Standard DoD applies.
POPHEALTH-US-012 — On-demand HMIS export trigger
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | API trigger for on-demand DHIS2 push for a specific period and indicator family |
| Epic link | POPHEALTH-EPIC-05 |
| Status | To Do |
| Priority | Should |
| Story points | 3 |
| Labels | service:population-health, type:api, slice:S2 |
| Components | hmis-exporter |
| FR references | FR-POPHEALTH-081 |
| Legacy FR refs | FR-POP-081 |
| Dependencies | MoPH DHIS2 API |
User story: As a platform administrator, when DHIS2 was unavailable during a scheduled run, I want to trigger a backfill push for the missed period via API so that MoPH receives the data without waiting for the next scheduled run.
Acceptance criteria (Gherkin):
- Given
platform_adminrole and a validindicatorFamily+period, when IPOST /api/v1/population-health/hmis/exports, then a new export job is created and queued. - Given an export job is already running for the same
(indicatorFamily, period), when I trigger again, then I receive 409EXPORT_JOB_ACTIVEwith the existingjobId.
Definition of Done: Standard DoD applies.
POPHEALTH-US-013 — Request de-identified research export
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Create de-identified cohort export with k-anonymity and differential privacy |
| Epic link | POPHEALTH-EPIC-06 |
| Status | To Do |
| Priority | Must |
| Story points | 13 |
| Labels | service:population-health, type:backend, slice:S3 |
| Components | deident-pipeline, research-export |
| FR references | FR-POPHEALTH-082 – FR-POPHEALTH-084 |
| Legacy FR refs | FR-POP-082 |
| Dependencies | audit-service, access-policy (consent) |
User story: As an academic researcher, when I request a de-identified export of the MCH cohort for my IRB-approved study, I want the system to apply k-anonymity and differential privacy before releasing the dataset so that patients cannot be re-identified from my analysis.
Acceptance criteria (Gherkin):
- Given
secondary_use:approvedscope and a validirbReference, when IPOST /api/v1/population-health/exportswithexportType: cohort-deidentified, then the de-identification pipeline is triggered. - Given the cohort has fewer than 5 patients in any quasi-identifier group, when k-anonymity check runs, then the export is blocked with 422
DEIDENT_K_THRESHOLD_VIOLATIONand the suppressed group count is returned. - Given the pipeline succeeds, when I poll the export status endpoint, then
status: completedand a presigneddownloadUrl(24h TTL) is returned. - Given the dataset is released, when
population_health.deident_export.released.v1is emitted, then the event includeskValue=5,dpEpsilon,irbReference, and no patient-identifiable fields.
Technical notes:
- k-anonymity: generalize quasi-identifiers (age → band, location → district) until k≥5.
- Differential privacy: add Laplace noise to count-based columns (ε≤1.0 per export).
- ε budget tracked per cohort per quarter; refuses export if budget exhausted.
Definition of Done: Standard DoD applies.
POPHEALTH-US-014 — Block unauthorized secondary-use exports
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Enforce consent and IRB requirement before releasing any identifiable export |
| Epic link | POPHEALTH-EPIC-06 |
| Status | To Do |
| Priority | Must |
| Story points | 5 |
| Labels | service:population-health, type:backend, slice:S3 |
| Components | consent-enforcement, research-export |
| FR references | FR-POPHEALTH-085 |
| Legacy FR refs | — (synthesized) |
| Dependencies | audit-service, access-policy |
User story: As a data governance officer, when a researcher attempts to export identifiable patient data without an IRB reference or with a purpose not on the approved list, I want the system to block the export and create an audit record so that I can review and investigate the attempt.
Acceptance criteria (Gherkin):
- Given a request without
irbReference, whenexportType: cohort-deidentifiedis submitted, then 403CONSENT_REQUIREDis returned. - Given the consent-check service is unavailable, when an export is requested, then the export is blocked (fail-closed) with 503
CONSENT_SERVICE_UNAVAILABLE. - Given the export is blocked, when the audit service receives the event, then
EXPORT_BLOCKEDis logged withrequesterId,cohortId,blockReason, and timestamp.
Technical notes: Consent-check always calls access-policy service; never trusts local cache for export gates.
Definition of Done: Standard DoD applies.
POPHEALTH-US-015 — Offline facility aggregate reports
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Generate offline facility aggregates and sync to national platform on reconnect |
| Epic link | POPHEALTH-EPIC-07 |
| Status | To Do |
| Priority | Should |
| Story points | 8 |
| Labels | service:population-health, type:backend, slice:S4 |
| Components | offline-reports, sync-protocol |
| FR references | FR-POPHEALTH-090 – FR-POPHEALTH-092 |
| Legacy FR refs | — (synthesized) |
| Dependencies | sync-protocol (@ghasi/sync-protocol), cross-service: POPHEALTH-US-001 |
User story: As a district health officer in a remote Afghan district, when I am offline, I want to generate daily aggregate population reports from local data so that I can continue monitoring facility health status, and have them automatically sync when connectivity is restored.
Acceptance criteria (Gherkin):
- Given the device is offline, when the daily report generation runs, then a JSON aggregate snapshot is created and signed with the device keypair.
- Given connectivity is restored, when the sync client calls
POST /api/v1/population-health/sync/facility-reports, then the server verifies the signature, deduplicates by(facilityId, periodKey, deviceId), and merges the report. - Given the same report is submitted twice, when the second request arrives, then the server returns 200 with the existing merged record (idempotent).
Technical notes:
- Offline client uses local SQLite for data; platform sync-protocol library for queue management.
dataFreshnessin dashboard response reflects offline sync lag.
Definition of Done: Standard DoD applies.
POPHEALTH-US-016 — RBAC, tenant isolation, and PHI audit
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Enforce RBAC/ABAC, PostgreSQL RLS, and audit every PHI access and export |
| Epic link | POPHEALTH-EPIC-08 |
| Status | To Do |
| Priority | Must |
| Story points | 5 |
| Labels | service:population-health, type:backend, slice:S0 |
| Components | security, rbac, audit, rls |
| FR references | FR-POPHEALTH-100 – FR-POPHEALTH-106 |
| Legacy FR refs | FR-POP-080 – FR-POP-083 |
| Dependencies | audit-service, identity-service |
User story: As a security auditor, when I review the population health service access logs, I want to see every PHI access, export operation, and cross-tenant violation recorded in the audit service with actor, timestamp, and scope so that I can demonstrate compliance with data protection requirements.
Acceptance criteria (Gherkin):
- Given a user with
population_health:registry:readbut NOTphi:readcalls a registry endpoint, when the response is returned, thenpatientIdis null and no PHI audit event is emitted. - Given a user with
phi:readcalls a registry endpoint, when the response is returned, then aPHI_ACCESSaudit event is emitted to audit-service withactorId,tenantId,endpoint, androwCount. - Given a request with a JWT from a different tenant, when any endpoint is called, then PostgreSQL RLS returns zero rows AND the application layer returns 403
CROSS_TENANT_SCOPE_VIOLATIONwith a security audit event.
Technical notes:
- RLS enforced via
app.tenant_idsession variable set in DB connection pool interceptor. - Audit events published via outbox to NATS → audit-service.
- PHI redaction enforced in
@ghasi/telemetrylog formatter.
Definition of Done: Standard DoD applies.