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.
| Ticket | Delivers | Status |
|---|---|---|
| M35-001 | Module skeleton, multi-target config, status endpoint | done |
| M35-002 | Metadata mapping CRUD + metadata pull | done |
| M35-003 | Aggregate dataValueSets push + PHI-free push log | done |
| M35-004 | Unattended push schedules + re-push window | done |
| M35-005 / FE | Operator console | done (FE !278) |
| M35-006 | Milestone gate | done |
| M35-008 | Tracker programme export | done |
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.
| Table | Purpose |
|---|---|
dhis2_data_element_mapping | Tenant-scoped report column → DHIS2 data element UID (+ optional category disaggregation) |
dhis2_programme_mapping | Tenant-scoped UHP programme → DHIS2 program / tracked-entity type + attribute UIDs (M35-008) |
dhis2_push_run | PHI-free push attempt summary (target, period, import counts, status, push kind) |
dhis2_push_value | Per-value outcome (UIDs, numeric count or conflict message — never patient identifiers) |
dhis2_push_schedule | Tenant-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
coreimport —Dhis2ArchitectureBoundaryTestrefuses it; platform access is Feign-only. - No sibling domain-module import — figures come from
reportingover HTTP/Feign, never from a Java import ofreporting. - Multi-target configuration —
dhis2.targets.<code>.*(D-d). Default keys:internal(self-hosted analytics tier) andnational(future aggregate-only submission boundary). Unconfigured targets reportconfigured=falseon status; boot never fails for missing credentials. - Scheduling never on the module launcher —
Dhis2Applicationis excluded from the single-jar scan (SCHED-002). Scheduled push belongs on a scanned@Configurationgated@Profile("scheduler"). - Permissions declared by
Dhis2ModuleDescriptor— undeclared@RequiresAccesscodes 403 for everyone includingsuper_admin.
API
Base path: /api/v1/dhis2 (ApiV1Paths.DHIS2).
| Method | Path | Permission | Notes |
|---|---|---|---|
| GET | /api/v1/dhis2/status | dhis2.status.read | Module identity, per-target connection status, capabilities |
| GET | /api/v1/dhis2/mappings | dhis2.mapping.read | List mappings (?targetCode= optional) |
| GET | /api/v1/dhis2/mappings/{id} | dhis2.mapping.read | One mapping |
| POST | /api/v1/dhis2/mappings | dhis2.mapping.write | Create mapping (UIDs validated against target) |
| PUT | /api/v1/dhis2/mappings/{id} | dhis2.mapping.write | Update mapping |
| DELETE | /api/v1/dhis2/mappings/{id} | dhis2.mapping.write | Void mapping |
| GET | /api/v1/dhis2/metadata/{targetCode}/data-elements | dhis2.mapping.read | Pull data elements from DHIS2 |
| GET | /api/v1/dhis2/metadata/{targetCode}/organisation-units | dhis2.mapping.read | Pull org units from DHIS2 |
| GET | /api/v1/dhis2/metadata/{targetCode}/category-option-combos | dhis2.mapping.read | Pull category option combos |
Aggregate push (M35-003):
| Method | Path | Permission | Notes |
|---|---|---|---|
| POST | /api/v1/dhis2/push | dhis2.push.write | Manual push for one target/period |
| GET | /api/v1/dhis2/push/runs | dhis2.push.read | Push history (?targetCode= optional) |
| GET | /api/v1/dhis2/push/runs/{id} | dhis2.push.read | One push run |
| GET | /api/v1/dhis2/push/runs/{id}/values | dhis2.push.read | Per-value outcomes and conflicts |
Push schedules (M35-004):
| Method | Path | Permission | Notes |
|---|---|---|---|
| GET | /api/v1/dhis2/push/schedules | dhis2.push.read | List schedules for signed tenant |
| PUT | /api/v1/dhis2/push/schedules | dhis2.push.write | Create/update schedule (unique per target + period type) |
| DELETE | /api/v1/dhis2/push/schedules/{id} | dhis2.push.write | Retire schedule |
Tracker export (M35-008):
| Method | Path | Permission | Notes |
|---|---|---|---|
| GET | /api/v1/dhis2/programme-mappings | dhis2.mapping.read | List programme tracker mappings |
| POST | /api/v1/dhis2/programme-mappings | dhis2.mapping.write | Create programme mapping |
| PUT | /api/v1/dhis2/programme-mappings/{id} | dhis2.mapping.write | Update programme mapping |
| DELETE | /api/v1/dhis2/programme-mappings/{id} | dhis2.mapping.write | Void programme mapping |
| POST | /api/v1/dhis2/push/tracker | dhis2.push.write | Manual tracker push for one target/period |
Platform export source (core program engine, Feign-only):
| Method | Path | Permission | Notes |
|---|---|---|---|
| GET | /api/v1/program/dhis2-tracker-export/rows | dhis2.push.write | PO-7 attributes only; PUBLIC_HEALTH export path |
Push permissions:
| Code | Intended use |
|---|---|
dhis2.push.read | Push run history |
dhis2.push.write | Trigger 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).
Related features
- reporting — source of aggregate figures for M35-003 (
nationallyReportableflag, 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").