Skip to main content

DHIS2 interoperability — How it works

Overview

The dhis2 module is where platform aggregates and programme tracker data reach a DHIS2 server: metadata mapping, period computation, dataValueSets push, import-summary handling, scheduling, retry, and a PHI-free push log. It is a domain module (com.zhenus.uhp.api.dhis2), depends only on common and exchange, and reaches platform engines over Feign — the same shape as hl7 and reporting. Design: MILESTONE35_PLAN.md, decision D33 in DECISIONS_2026-08-16.md.

M35-001 delivers the skeleton; M35-002 adds metadata mapping; M35-003 adds aggregate push; M35-004 adds scheduling.

TicketDeliversStatus
M35-001Module skeleton, multi-target config, status endpointdone
M35-002Metadata mapping CRUD + metadata pulldone
M35-003Aggregate dataValueSets push + PHI-free push logdone
M35-004Unattended push schedules + re-push windowdone
M35-005 / FEOperator consoledone (FE !278)
M35-006Milestone gatedone
M35-008Tracker programme exportdone

Data model & ownership

The module owns the dhis2 schema, created by dhis2.db.changelog-master.yaml and registered in app's platform.db.changelog-master.yaml.

TablePurpose
dhis2_data_element_mappingTenant-scoped report column → DHIS2 data element UID (+ optional category disaggregation)
dhis2_programme_mappingTenant-scoped UHP programme → DHIS2 program / tracked-entity type + attribute UIDs (M35-008)
dhis2_push_runPHI-free push attempt summary (target, period, import counts, status, push kind)
dhis2_push_valuePer-value outcome (UIDs, numeric count or conflict message — never patient identifiers)
dhis2_push_scheduleTenant-scoped cron schedule for unattended aggregate push (claimed via FOR UPDATE SKIP LOCKED)

Facility → org unit mapping uses the platform facility_identifier table with global identifier type DHIS2_ORG_UNIT — not a second mapping table (MFL precedent).

Key rules & invariants

  • No core importDhis2ArchitectureBoundaryTest refuses it; platform access is Feign-only.
  • No sibling domain-module import — figures come from reporting over HTTP/Feign, never from a Java import of reporting.
  • Multi-target configurationdhis2.targets.<code>.* (D-d). Default keys: internal (self-hosted analytics tier) and national (future aggregate-only submission boundary). Unconfigured targets report configured=false on status; boot never fails for missing credentials.
  • Scheduling never on the module launcherDhis2Application is excluded from the single-jar scan (SCHED-002). Scheduled push belongs on a scanned @Configuration gated @Profile("scheduler").
  • Permissions declared by Dhis2ModuleDescriptor — undeclared @RequiresAccess codes 403 for everyone including super_admin.

API

Base path: /api/v1/dhis2 (ApiV1Paths.DHIS2).

MethodPathPermissionNotes
GET/api/v1/dhis2/statusdhis2.status.readModule identity, per-target connection status, capabilities
GET/api/v1/dhis2/mappingsdhis2.mapping.readList mappings (?targetCode= optional)
GET/api/v1/dhis2/mappings/{id}dhis2.mapping.readOne mapping
POST/api/v1/dhis2/mappingsdhis2.mapping.writeCreate mapping (UIDs validated against target)
PUT/api/v1/dhis2/mappings/{id}dhis2.mapping.writeUpdate mapping
DELETE/api/v1/dhis2/mappings/{id}dhis2.mapping.writeVoid mapping
GET/api/v1/dhis2/metadata/{targetCode}/data-elementsdhis2.mapping.readPull data elements from DHIS2
GET/api/v1/dhis2/metadata/{targetCode}/organisation-unitsdhis2.mapping.readPull org units from DHIS2
GET/api/v1/dhis2/metadata/{targetCode}/category-option-combosdhis2.mapping.readPull category option combos

Aggregate push (M35-003):

MethodPathPermissionNotes
POST/api/v1/dhis2/pushdhis2.push.writeManual push for one target/period
GET/api/v1/dhis2/push/runsdhis2.push.readPush history (?targetCode= optional)
GET/api/v1/dhis2/push/runs/{id}dhis2.push.readOne push run
GET/api/v1/dhis2/push/runs/{id}/valuesdhis2.push.readPer-value outcomes and conflicts

Push schedules (M35-004):

MethodPathPermissionNotes
GET/api/v1/dhis2/push/schedulesdhis2.push.readList schedules for signed tenant
PUT/api/v1/dhis2/push/schedulesdhis2.push.writeCreate/update schedule (unique per target + period type)
DELETE/api/v1/dhis2/push/schedules/{id}dhis2.push.writeRetire schedule

Tracker export (M35-008):

MethodPathPermissionNotes
GET/api/v1/dhis2/programme-mappingsdhis2.mapping.readList programme tracker mappings
POST/api/v1/dhis2/programme-mappingsdhis2.mapping.writeCreate programme mapping
PUT/api/v1/dhis2/programme-mappings/{id}dhis2.mapping.writeUpdate programme mapping
DELETE/api/v1/dhis2/programme-mappings/{id}dhis2.mapping.writeVoid programme mapping
POST/api/v1/dhis2/push/trackerdhis2.push.writeManual tracker push for one target/period

Platform export source (core program engine, Feign-only):

MethodPathPermissionNotes
GET/api/v1/program/dhis2-tracker-export/rowsdhis2.push.writePO-7 attributes only; PUBLIC_HEALTH export path

Push permissions:

CodeIntended use
dhis2.push.readPush run history
dhis2.push.writeTrigger or retry push

Configuration & feature flags

Module-owned keys live in dhis2/src/main/resources/uhp-module-defaults.yml (CONF-002 — loaded in both standalone and single-jar deployments):

dhis2:
targets:
internal:
base-url: ${DHIS2_INTERNAL_BASE_URL:}
api-token: ${DHIS2_INTERNAL_API_TOKEN:}
# … username/password fallback, org-unit-root, timeouts
national:
base-url: ${DHIS2_NATIONAL_BASE_URL:}
# …

Standalone-only infrastructure (datasource, Feign service URLs) is in dhis2/src/main/resources/application.yml. See .env.example for documented environment variables.

A target is configured when it has a non-blank base-url and either an API token or both username and password.

PHI

The skeleton reads none. GET /api/v1/dhis2/status describes the module and target readiness — never patient data, tokens, or passwords. Push logs (dhis2_push_run, dhis2_push_value) are PHI-free by construction — UIDs, periods, counts, and conflict messages only (Dhis2PushRunPhiFreeTest).

  • reporting — source of aggregate figures for M35-003 (nationallyReportable flag, small-cell policy — D33 / PO-3).
  • hl7 — precedent for integration transport (recorded-before-attempted, re-emit from source).

Source layout

dhis2/
Dhis2Application.java
config/
Dhis2ModuleDescriptor.java
Dhis2Properties.java
Dhis2TargetConnectionProperties.java
controller/Dhis2StatusController.java
service/impl/Dhis2StatusServiceImpl.java
db/changelog/dhis2.db.changelog-master.yaml

Why it is this way

Figures never leave reporting through a Java import. The dhis2 module is a transport shell: it asks reporting for already-aggregated numbers over Feign so PHI never crosses a compile-time boundary into another domain module's tables.

Push logs are PHI-free by construction. Runs store UIDs, periods, counts, and conflict messages — never patient identifiers — because a failed push log must be safe for operators to paste into tickets.

Multi-target config is optional at boot. Unconfigured internal / national targets report configured=false on status rather than refusing to start; deployments wire credentials when the integration is actually live.

Traps

Do not import core or reporting. ArchUnit enforces the Feign-only boundary; a "just this once" repository call becomes the path every future shortcut takes.

Tracker export attributes are PO-gated (PO-7). Only attributes approved for PUBLIC_HEALTH export reach DHIS2; adding a column without that review is a confidentiality defect, not a mapping bug.

Scheduling belongs on the scheduler profile, not Dhis2Application. The module launcher is excluded from single-jar scan (SCHED-002); cron jobs that run in-process must live on a scanned @Configuration gated @Profile("scheduler").