Skip to main content

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

FieldValue
Issue typeStory
SummaryDisplay aggregate population health metrics scoped by facility/department/provider
Epic linkPOPHEALTH-EPIC-01
StatusTo Do
PriorityMust
Story points5
Labelsservice:population-health, type:api, type:backend, slice:S1
Componentsdashboard
FR referencesFR-POPHEALTH-001 – FR-POPHEALTH-007
Legacy FR refsFR-POP-001 – FR-POP-007
Dependenciespatient-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_admin role and population_health:dashboard:read scope, when I call GET /api/v1/population-health/dashboard?facilityId=<id>, then I receive aggregate metrics including activePatients, ageDistribution, highRiskCount, screeningCompliance, and dataFreshness.
  • Given I do not have phi:read permission, when I call the dashboard endpoint, then the response contains no patientId fields.
  • Given the clinical data is more than 2 hours stale, when I call the dashboard endpoint, then the dataFreshness field reflects the last successful sync time and a staleDataWarning flag is set.

Technical notes:

  • Dashboard aggregates served from materialized snapshots; read from analytics replica.
  • dataFreshness computed from max last_synced_at across 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

FieldValue
Issue typeStory
SummaryView paginated disease registry members with control status and risk tier filters
Epic linkPOPHEALTH-EPIC-01
StatusTo Do
PriorityMust
Story points3
Labelsservice:population-health, type:api, slice:S1
Componentsregistries
FR referencesFR-POPHEALTH-020 – FR-POPHEALTH-022
Legacy FR refsFR-POP-020 – FR-POP-022
Dependenciespatient-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:read and phi:read permissions, when I call GET /api/v1/population-health/registries/diabetes?riskTier=high&controlStatus=uncontrolled, then I receive a paginated list of DiseaseRegistryEntry rows including patientId.
  • Given I have population_health:registry:read but NOT phi:read, when I call the registry endpoint, then patientId is 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.v1 and related events.
  • activeGapFlags populated 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

FieldValue
Issue typeStory
SummaryScreening compliance aggregates and immunization coverage rates by type and age band
Epic linkPOPHEALTH-EPIC-01
StatusTo Do
PriorityMust
Story points3
Labelsservice:population-health, type:api, slice:S1
Componentsscreenings, immunizations
FR referencesFR-POPHEALTH-030 – FR-POPHEALTH-042
Legacy FR refsFR-POP-030–042
Dependenciesimmunizations-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 call GET /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, then complianceRate = completed / (due + overdue + completed) and the rate is between 0 and 1.

Technical notes:

  • Immunization data read from immunizations-service projection; updated on immunizations.immunization.administered.v1.

Definition of Done: Standard DoD applies.


POPHEALTH-US-004 — Drill down from aggregate metric to patient cohort

FieldValue
Issue typeStory
SummaryDrill down from dashboard metric tile to role-filtered patient list
Epic linkPOPHEALTH-EPIC-01
StatusTo Do
PriorityMust
Story points3
Labelsservice:population-health, type:api, slice:S1
Componentsdashboard, cohort-engine
FR referencesFR-POPHEALTH-006
Legacy FR refsFR-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 call GET /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, then patientId is 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

FieldValue
Issue typeStory
SummaryCreate versioned cohort with boolean DSL, list, get, and archive cohorts
Epic linkPOPHEALTH-EPIC-02
StatusTo Do
PriorityMust
Story points8
Labelsservice:population-health, type:backend, type:api, slice:S1
Componentscohort-engine, cohort-dsl
FR referencesFR-POPHEALTH-010 – FR-POPHEALTH-013
Legacy FR refsFR-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 CohortExpressionNode tree, when I POST /api/v1/population-health/cohorts, then the cohort is created with version=1 and status=Draft.
  • Given I submit an expression with an unknown operator value, when I POST the cohort, then I receive 400 INVALID_COHORT_DSL with the path of the offending node.
  • Given a cohort with isShared=true, when another user in the same tenant calls GET /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

FieldValue
Issue typeStory
SummaryTrigger async cohort refresh, coalesce duplicates, monitor job status
Epic linkPOPHEALTH-EPIC-02
StatusTo Do
PriorityMust
Story points5
Labelsservice:population-health, type:backend, slice:S1
Componentscohort-engine, refresh-worker
FR referencesFR-POPHEALTH-014
Legacy FR refsFR-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 Active status, when I POST /api/v1/population-health/cohorts/:id/refresh, then I receive 202 with a jobId and status: 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.v1 is published with the new membershipCount.

Technical notes:

  • Job coalescing: SELECT ... FOR UPDATE on refresh job table before insert.
  • Worker reads cohort definition and evaluates against clinical projections in batches.
  • sourceWatermark recorded in event to enable staleness detection.

Definition of Done: Standard DoD applies.


POPHEALTH-US-007 — Run risk scoring for a cohort

FieldValue
Issue typeStory
SummaryTrigger clinical risk scoring and view tier distribution for a cohort
Epic linkPOPHEALTH-EPIC-03
StatusTo Do
PriorityMust
Story points5
Labelsservice:population-health, type:backend, slice:S1
Componentsrisk-engine
FR referencesFR-POPHEALTH-050 – FR-POPHEALTH-052
Legacy FR refsFR-POP-050 – FR-POP-052
Dependenciespatient-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 I POST /api/v1/population-health/risk/score, then I receive 202 with a scoring jobId.
  • Given the scoring job completes, when I query the risk scores, then every patient in the cohort has a computedTier assigned.
  • Given a senior clinician applies a manual override, when PATCH /api/v1/population-health/risk/scores/:id/override is called with a reason, then overrideTier and overrideReason are persisted and population_health.risk_score.overridden.v1 is emitted.

Technical notes:

  • Risk model config loaded from config-service; key = clinical-risk-v1 default.
  • driversJson stores per-factor contributions for display in UI.

Definition of Done: Standard DoD applies.


POPHEALTH-US-008 — Generate and work outreach lists

FieldValue
Issue typeStory
SummaryGenerate outreach list from cohort, assign to team, update contact status
Epic linkPOPHEALTH-EPIC-03
StatusTo Do
PriorityMust
Story points5
Labelsservice:population-health, type:backend, type:api, slice:S1
Componentsoutreach
FR referencesFR-POPHEALTH-060 – FR-POPHEALTH-062
Legacy FR refsFR-POP-060 – FR-POP-062
Dependenciescommunication-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 cohortId and assignedTeamId, when I POST /api/v1/population-health/outreach/lists, then an outreach list is created and population_health.outreach_list.generated.v1 is emitted.
  • Given an outreach item in Pending status, when I PATCH /api/v1/population-health/outreach/items/:id with status: attempted, then the status transitions and attemptCount increments.
  • Given an outreach item in Completed status, when I try to transition it to Attempted, then I receive 422 INVALID_OUTREACH_TRANSITION.

Technical notes:

  • Communication-service subscribes to outreach_list.generated for notification delivery.
  • FSM transitions validated by OutreachItem domain object.

Definition of Done: Standard DoD applies.


POPHEALTH-US-009 — Compute quality metric snapshots

FieldValue
Issue typeStory
SummaryCompute HEDIS/QOF/MoPH quality metric snapshots with numerator/denominator/exclusions
Epic linkPOPHEALTH-EPIC-04
StatusTo Do
PriorityMust
Story points8
Labelsservice:population-health, type:backend, slice:S2
Componentsquality-metrics
FR referencesFR-POPHEALTH-070 – FR-POPHEALTH-072
Legacy FR refsFR-POP-070 – FR-POP-072
Dependenciespatient-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 in quality_metric_snapshots.
  • Given denominator = exclusions, when the snapshot is stored, then rate = null and emptyDenominatorReason is populated.
  • Given a snapshot job completes, when population_health.quality_metric.calculated.v1 is emitted, then interop-service publishes a FHIR MeasureReport to 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.


FieldValue
Issue typeStory
SummaryView quality metric trend series and export donor indicator reports
Epic linkPOPHEALTH-EPIC-04
StatusTo Do
PriorityShould
Story points3
Labelsservice:population-health, type:api, slice:S2
Componentsquality-metrics, exports
FR referencesFR-POPHEALTH-071
Legacy FR refsFR-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 call GET /api/v1/population-health/quality-metrics?program=donor, then only metrics in the donor program pack are returned.
  • Given multiple quarterly snapshots exist, when the endpoint returns a metric, then trend contains 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

FieldValue
Issue typeStory
SummaryAutomated scheduled push of aggregate indicators to MoPH DHIS2
Epic linkPOPHEALTH-EPIC-05
StatusTo Do
PriorityMust
Story points8
Labelsservice:population-health, type:backend, slice:S2
Componentshmis-exporter, dhis2-adapter
FR referencesFR-POPHEALTH-080
Legacy FR refsFR-POP-080
DependenciesMoPH 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 > 0 triggers 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

FieldValue
Issue typeStory
SummaryAPI trigger for on-demand DHIS2 push for a specific period and indicator family
Epic linkPOPHEALTH-EPIC-05
StatusTo Do
PriorityShould
Story points3
Labelsservice:population-health, type:api, slice:S2
Componentshmis-exporter
FR referencesFR-POPHEALTH-081
Legacy FR refsFR-POP-081
DependenciesMoPH 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_admin role and a valid indicatorFamily + period, when I POST /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 409 EXPORT_JOB_ACTIVE with the existing jobId.

Definition of Done: Standard DoD applies.


POPHEALTH-US-013 — Request de-identified research export

FieldValue
Issue typeStory
SummaryCreate de-identified cohort export with k-anonymity and differential privacy
Epic linkPOPHEALTH-EPIC-06
StatusTo Do
PriorityMust
Story points13
Labelsservice:population-health, type:backend, slice:S3
Componentsdeident-pipeline, research-export
FR referencesFR-POPHEALTH-082 – FR-POPHEALTH-084
Legacy FR refsFR-POP-082
Dependenciesaudit-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:approved scope and a valid irbReference, when I POST /api/v1/population-health/exports with exportType: 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_VIOLATION and the suppressed group count is returned.
  • Given the pipeline succeeds, when I poll the export status endpoint, then status: completed and a presigned downloadUrl (24h TTL) is returned.
  • Given the dataset is released, when population_health.deident_export.released.v1 is emitted, then the event includes kValue=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

FieldValue
Issue typeStory
SummaryEnforce consent and IRB requirement before releasing any identifiable export
Epic linkPOPHEALTH-EPIC-06
StatusTo Do
PriorityMust
Story points5
Labelsservice:population-health, type:backend, slice:S3
Componentsconsent-enforcement, research-export
FR referencesFR-POPHEALTH-085
Legacy FR refs— (synthesized)
Dependenciesaudit-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, when exportType: cohort-deidentified is submitted, then 403 CONSENT_REQUIRED is 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_BLOCKED is logged with requesterId, 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

FieldValue
Issue typeStory
SummaryGenerate offline facility aggregates and sync to national platform on reconnect
Epic linkPOPHEALTH-EPIC-07
StatusTo Do
PriorityShould
Story points8
Labelsservice:population-health, type:backend, slice:S4
Componentsoffline-reports, sync-protocol
FR referencesFR-POPHEALTH-090 – FR-POPHEALTH-092
Legacy FR refs— (synthesized)
Dependenciessync-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.
  • dataFreshness in dashboard response reflects offline sync lag.

Definition of Done: Standard DoD applies.


POPHEALTH-US-016 — RBAC, tenant isolation, and PHI audit

FieldValue
Issue typeStory
SummaryEnforce RBAC/ABAC, PostgreSQL RLS, and audit every PHI access and export
Epic linkPOPHEALTH-EPIC-08
StatusTo Do
PriorityMust
Story points5
Labelsservice:population-health, type:backend, slice:S0
Componentssecurity, rbac, audit, rls
FR referencesFR-POPHEALTH-100 – FR-POPHEALTH-106
Legacy FR refsFR-POP-080 – FR-POP-083
Dependenciesaudit-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:read but NOT phi:read calls a registry endpoint, when the response is returned, then patientId is null and no PHI audit event is emitted.
  • Given a user with phi:read calls a registry endpoint, when the response is returned, then a PHI_ACCESS audit event is emitted to audit-service with actorId, tenantId, endpoint, and rowCount.
  • 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_VIOLATION with a security audit event.

Technical notes:

  • RLS enforced via app.tenant_id session variable set in DB connection pool interceptor.
  • Audit events published via outbox to NATS → audit-service.
  • PHI redaction enforced in @ghasi/telemetry log formatter.

Definition of Done: Standard DoD applies.