DHIS2 Integration Software Requirements Specification

This document acts as the software requirements specification for the OpenLMIS DHIS2 integration. It pulls from numerous sources including a strategy document written by Josh, previous experience from the eLMIS community outlined in the Gap Interface feature request and a design discussion in late August 2018. The goal of this document is to define the scope of the project and develop the architecture.

User Stories

(developed from eLMIS experience)

  • As a district level program administrator, I want to see LMIS logistics data triangulated with service statistics-related data separately collected in DHIS2 through custom dashboards created in DHIS2. This allows me to match the numbers reported from the supply chain against numbers reported by the health system so I can verify them across each domain and act on discrepancies.
  • As a facility manager, every month, I need to report standard aggregate reports to DHIS2. This is an automated process in eLMIS and is sometimes a manual process in other implementation areas.
    • I want to be able to view my facility's recorded activities each month in OpenLMIS in the standard DHIS2 report format.
    • I want to be able to compare this format against my physical tally books and RnR that was submitted to OpenLMIS.
    • I want to be able to adjust any numbers in the system and submit the report to DHIS2 for the particular period.
    • (External to OpenLMIS) Once submitted, I want to be able to login to DHIS2 and verify that the report was submitted.

Dev team user story ideas:

  • Allow person to review DHIS2 publish of metrics
    • But how does that person (is it the same person even?) change what they just reviewed.
    • Also due to how requisitions work in terms of timing, for the most part nothing happens for 28 days and then everyone is in a scramble.
  • (tech oriented) Currently sync nightly, however there's a spike and so we should perhaps revisit doing that more freq around reporting due dates.

User Experience

This section defines the anticipated user experience for this integration. OpenLMIS will give users a standard set of reports available as a FHIR MeasureReport for each of seven measures that need to be calculated. These reports will be available to a third party system that is responsible for performing the ingestion and mapping process.

Scope

The following is within scope for this integration:

  • OpenLMIS will make aggregate information available (counts over time) that target the DHIS2 DataSet report. Individual transactions targeted for the DHIS2 tracker capture are out of scope.
  • OpenLMIS will focus on providing requisition information as a priority including any additional columns that are added to the RnR template.
  • Facility Matching
    • OpenLMIS will make a facility list available through the FHIR location API endpoint that can be matched by a third party tool.
  • Indicator Calculations Definition (FHIR Measure)
    • Key Performance Indicators (KPI) will be developed in the OpenLMIS reporting system and posted as FHIR Measure. 
    • KPIs/FHIR Measures will need to align to the Categories defined in DHIS2.
  • Push MeasureReports to FHIR server
    • Reports will be sent to the FHIR server where they will be made available to external consumers through an API
  • Expose the FHIR server API
    • External users need to expose the FHIR API

System Architecture

Systems Involved

OpenLMIS Core - OpenLMIS Core will be the primary data source. We are focusing on generating reports based on requisition information.

Apache Nifi - Nifi is the data ingestion and standardization engine. Nifi will be used to extract requisitions from OpenLMIS, convert them into multiple measureReports and post them to the HAPI FHIR server.

HAPI FHIR server - OpenLMIS contains a HAPI FHIR server built in. This FHIR server will be responsible for storing Measure Reports and make them available through the API.

Architecture Diagram

Sequence Diagrams

 Click here to expand...

title Workflow 1: Extract Requisitions

participant Nifi
participant Requisition Microservice
participant ReferenceData Microservice
participant HAPI FHIR

Nifi->Nifi: Cron Trigger
Nifi->+Requisition Microservice: Query Requisitions API
Requisition Microservice->-Nifi: Return latest requisitions
loop for each requisition
Nifi->+ReferenceData Microservice: Query for Location id
ReferenceData Microservice->Nifi: Return results
Nifi->Nifi: Extract FHIR location ID
loop for each measure
Nifi->Nifi: Convert the requisition column/product combo to a measure
Nifi->+HAPI FHIR: Post the measureReport
HAPI FHIR->Nifi: Return Created
end
end


 Click here to expand...

title Workflow 2: External Consumer GET Measure Report

participant External Consumer
participant OpenLMIS Auth Microservice
participant OpenLMIS FHIR API
participant OpenLMIS ReferenceData Microservice
participant FHIR Server


External Consumer->OpenLMIS Auth Microservice: Get Auth Token
OpenLMIS Auth Microservice->OpenLMIS Auth Microservice: Verify Auth
OpenLMIS Auth Microservice->External Consumer: Return Auth Token
External Consumer->OpenLMIS FHIR API: Request Measure with Auth Token
OpenLMIS FHIR API->OpenLMIS ReferenceData Microservice: Get User Role
OpenLMIS ReferenceData Microservice->OpenLMIS FHIR API: Return Role
OpenLMIS FHIR API->OpenLMIS FHIR API: Verify Role
OpenLMIS FHIR API->FHIR Server: Get Measure Report
FHIR Server->OpenLMIS FHIR API: Return Measure Report
OpenLMIS FHIR API->External Consumer: Return Measure Report

 Click here to expand...

title Workflow 3: External Consumer Transform Measure Report and Post to DHIS2

participant External Consumer
participant OpenLMIS
participant DHIS2

External Consumer->OpenLMIS: Request Measure Report (Workflow 3)
OpenLMIS->External Consumer: Return Measure Report
External Consumer->External Consumer: Crosswalk location, program, period & measure
External Consumer->External Consumer: Transform FHIR Measure Report to ADX
External Consumer->DHIS2: POST to DHIS2
DHIS2->DHIS2: Evaluate values and store report

Field Mapping

This section maps the fields between the OpenLMIS Requisition and the FHIR data resources. We chose to use the requisition, because that is the primary unit of data collection in the majority of OpenLMIS implementations.

Below is a screenshot of a requisition:

 Click here to expand...
{
  "id": "bc6d01dd-d861-4136-a0a3-2ffbe3cd27c5",
  "createdDate": "2018-01-16T14:34:57.915007Z",
  "modifiedDate": "2018-01-16T18:34:57.915007Z",
  "requisitionLineItems": [
    {
      "id": "93b201ad-942c-4a0e-bd0a-8f002218a2e3",
      "orderable": {
        "programs": [
          {
            "programId": "418bdc1d-c303-4bd0-b2d3-d8901150a983",
            "orderableDisplayCategoryId": "16173fd0-f439-4222-931e-91c413a495c3",
            "orderableCategoryDisplayName": "Vaccines",
            "orderableCategoryDisplayOrder": 5,
            "fullSupply": true,
            "displayOrder": 5,
            "pricePerPack": 12
          }
        ],
        "dispensable": {
          "dispensingUnit": null,
          "displayUnit": "20 dose,injection"
        },
        "identifiers": {
          "commodityType": "99ccf663-3304-44ae-b2e0-a67fd5511e2a"
        },
        "id": "8ef9d4da-b6e5-401c-b433-765a5fd8a0cc",
        "productCode": "bcg20",
        "fullProductName": "BCG",
        "netContent": 20,
        "packRoundingThreshold": 1,
        "roundToZero": true,
        "commodityTypeIdentifier": "99ccf663-3304-44ae-b2e0-a67fd5511e2a"
      },
      "beginningBalance": 100,
      "totalReceivedQuantity": 50,
      "totalLossesAndAdjustments": 0,
      "stockOnHand": 50,
      "requestedQuantity": 100,
      "totalConsumedQuantity": 100,
      "requestedQuantityExplanation": "Need more",
      "approvedQuantity": 100,
      "totalStockoutDays": 0,
      "total": 150,
      "packsToShip": 100,
      "pricePerPack": 6,
      "totalCost": 60,
      "skipped": false,
      "adjustedConsumption": 100,
      "previousAdjustedConsumptions": [],
      "averageConsumption": 100,
      "maxPeriodsOfStock": 3,
      "calculatedOrderQuantity": 250,
      "stockAdjustments": []
    },
    {
      "id": "5ec13936-3cc9-4c88-afcc-b9b636955d5c",
      "orderable": {
        "programs": [
          {
            "programId": "418bdc1d-c303-4bd0-b2d3-d8901150a983",
            "orderableDisplayCategoryId": "16173fd0-f439-4222-931e-91c413a495c3",
            "orderableCategoryDisplayName": "Vaccines",
            "orderableCategoryDisplayOrder": 5,
            "fullSupply": true,
            "displayOrder": 5,
            "pricePerPack": 8.5
          }
        ],
        "dispensable": {
          "dispensingUnit": null,
          "displayUnit": "1 dose,injection"
        },
        "identifiers": {
          "commodityType": "08561b98-2f90-4c4a-a1e3-f180a83abd66"
        },
        "id": "b61c652d-2259-41d7-8bb6-fc5fcdd95626",
        "productCode": "rota1",
        "fullProductName": "Rotavirus",
        "netContent": 1,
        "packRoundingThreshold": 1,
        "roundToZero": true,
        "commodityTypeIdentifier": "08561b98-2f90-4c4a-a1e3-f180a83abd66"
      },
      "beginningBalance": 100,
      "totalReceivedQuantity": 50,
      "totalLossesAndAdjustments": 0,
      "stockOnHand": 50,
      "requestedQuantity": 100,
      "totalConsumedQuantity": 100,
      "requestedQuantityExplanation": "Need more",
      "approvedQuantity": 100,
      "totalStockoutDays": 0,
      "total": 150,
      "packsToShip": 10,
      "pricePerPack": 6,
      "totalCost": 60,
      "skipped": false,
      "adjustedConsumption": 100,
      "previousAdjustedConsumptions": [],
      "averageConsumption": 100,
      "maxPeriodsOfStock": 3,
      "calculatedOrderQuantity": 250,
      "stockAdjustments": []
    },
    {
      "id": "93ad4347-7fa3-43fd-bcdb-53cf0b1f496f",
      "orderable": {
        "programs": [
          {
            "programId": "418bdc1d-c303-4bd0-b2d3-d8901150a983",
            "orderableDisplayCategoryId": "16173fd0-f439-4222-931e-91c413a495c3",
            "orderableCategoryDisplayName": "Vaccines",
            "orderableCategoryDisplayOrder": 5,
            "fullSupply": true,
            "displayOrder": 5,
            "pricePerPack": 2
          }
        ],
        "dispensable": {
          "dispensingUnit": null,
          "displayUnit": "1 dose,injection"
        },
        "identifiers": {
          "commodityType": "43a1cd06-42f1-4e7c-b8ed-594416985381"
        },
        "id": "5f0dd194-aae1-490c-8b51-3cf0c87af983",
        "productCode": "penta1",
        "fullProductName": "Pentavalent (1 dose)",
        "netContent": 1,
        "packRoundingThreshold": 1,
        "roundToZero": true,
        "commodityTypeIdentifier": "43a1cd06-42f1-4e7c-b8ed-594416985381"
      },
      "beginningBalance": 100,
      "totalReceivedQuantity": 50,
      "totalLossesAndAdjustments": 0,
      "stockOnHand": 50,
      "requestedQuantity": 100,
      "totalConsumedQuantity": 100,
      "requestedQuantityExplanation": "Need more",
      "approvedQuantity": 100,
      "totalStockoutDays": 0,
      "total": 150,
      "packsToShip": 10,
      "pricePerPack": 6,
      "totalCost": 60,
      "skipped": false,
      "adjustedConsumption": 100,
      "previousAdjustedConsumptions": [],
      "averageConsumption": 100,
      "maxPeriodsOfStock": 3,
      "calculatedOrderQuantity": 250,
      "stockAdjustments": []
    },
    {
      "id": "ee142aae-2b89-4d90-ab40-12aaa8a3fb7d",
      "orderable": {
        "programs": [
          {
            "programId": "418bdc1d-c303-4bd0-b2d3-d8901150a983",
            "orderableDisplayCategoryId": "16173fd0-f439-4222-931e-91c413a495c3",
            "orderableCategoryDisplayName": "Vaccines",
            "orderableCategoryDisplayOrder": 5,
            "fullSupply": true,
            "displayOrder": 5,
            "pricePerPack": 6
          }
        ],
        "dispensable": {
          "dispensingUnit": null,
          "displayUnit": "10 dose,injection"
        },
        "identifiers": {
          "commodityType": "ac5e9d62-dc0a-4a0e-bead-e66a2eb81b07"
        },
        "id": "e217910c-3364-46b3-92cd-8dd8acf0c557",
        "productCode": "tetanus10",
        "fullProductName": "VAT",
        "netContent": 10,
        "packRoundingThreshold": 1,
        "roundToZero": true,
        "commodityTypeIdentifier": "ac5e9d62-dc0a-4a0e-bead-e66a2eb81b07"
      },
      "beginningBalance": 100,
      "totalReceivedQuantity": 50,
      "totalLossesAndAdjustments": 0,
      "stockOnHand": 50,
      "requestedQuantity": 100,
      "totalConsumedQuantity": 100,
      "requestedQuantityExplanation": "Need more",
      "approvedQuantity": 100,
      "totalStockoutDays": 0,
      "total": 150,
      "packsToShip": 50,
      "pricePerPack": 6,
      "totalCost": 60,
      "skipped": false,
      "adjustedConsumption": 100,
      "previousAdjustedConsumptions": [],
      "averageConsumption": 100,
      "maxPeriodsOfStock": 3,
      "calculatedOrderQuantity": 250,
      "stockAdjustments": []
    }
  ],
  "draftStatusMessage": null,
  "facility": {
    "id": "7fc9bda8-ad8a-468d-8244-38e1918527d5",
    "code": "N003",
    "name": "Cuamba, Cuamba",
    "active": true,
    "geographicZone": {
      "id": "9b8cfb5a-217a-4261-a64f-16ca06ae79fa",
      "code": "cuamba",
      "name": "Cuamba",
      "level": {
        "id": "93c05138-4550-4461-9e8a-79d5f050c223",
        "code": "District",
        "name": null,
        "levelNumber": 3
      },
      "parent": {
        "id": "0d4eb5ee-ae7f-42e7-89e1-d0f276090755",
        "code": "niassa",
        "name": "Niassa",
        "level": {
          "id": "9b497d87-cdd9-400e-bb04-fae0bf6a9491",
          "code": "Region",
          "name": null,
          "levelNumber": 2
        },
        "parent": {
          "id": "d22d86fb-9123-437a-9eae-da2b31b77e34",
          "code": "moz",
          "name": "Mozambique",
          "level": {
            "id": "6b78e6c6-292e-4733-bb9c-3d802ad61206",
            "code": "Country",
            "name": null,
            "levelNumber": 1
          },
          "parent": null
        }
      }
    },
    "type": {
      "id": "ac1d268b-ce10-455f-bf87-9c667da8f060",
      "code": "health_center",
      "name": "Health Center",
      "description": null,
      "displayOrder": 3,
      "active": true
    },
    "goLiveDate": "2010-09-01",
    "enabled": true,
    "openLmisAccessible": true,
    "operator": {
      "id": "9456c3e9-c4a6-4a28-9e08-47ceb16a4121",
      "code": "moh",
      "name": "Ministry of Health"
    }
  },
  "program": {
    "id": "418bdc1d-c303-4bd0-b2d3-d8901150a983",
    "code": "PRG004",
    "name": "EPI",
    "description": null,
    "active": true,
    "periodsSkippable": false,
    "showNonFullSupplyTab": false,
    "skipAuthorization": true,
    "enableDatePhysicalStockCountCompleted": false
  },
  "processingPeriod": {
    "id": "516ac930-0d28-49f5-a178-64764e22b236",
    "name": "Jan2017",
    "startDate": "2017-01-01",
    "endDate": "2017-01-31",
    "processingSchedule": {
      "id": "9c15bd6e-3f6b-4b91-b53a-36c199d35eac",
      "code": "SCH001",
      "description": null,
      "modifiedDate": null,
      "name": "Monthly"
    },
    "description": null,
    "durationInMonths": 1,
    "extraData": {}
  },
  "status": "APPROVED",
  "emergency": false,
  "reportOnly": null,
  "supplyingFacility": null,
  "supervisoryNode": "6f5eae49-bc62-4664-a679-1ef0bfd5b21d",
  "template": {
    "id": "6c4b004b-b7c9-46f0-bb1b-bd128d36729f",
    "createdDate": "2016-06-14T12:00:00Z",
    "numberOfPeriodsToAverage": 3,
    "populateStockOnHandFromStockCards": true,
    "name": "EPI",
    "columnsMap": {
      "numberOfNewPatientsAdded": {
        "name": "numberOfNewPatientsAdded",
        "label": "Number of new patients added",
        "indicator": "F",
        "displayOrder": 20,
        "isDisplayed": true,
        "source": "USER_INPUT",
        "option": {
          "id": "34b8e763-71a0-41f1-86b4-1829963f0704",
          "optionName": "newPatientCount",
          "optionLabel": "requisitionConstants.newPatientCount"
        },
        "definition": "New patients data.",
        "tag": null,
        "columnDefinition": {
          "canChangeOrder": true,
          "columnType": "NUMERIC"
        }
      },
      "adjustedConsumption": {
        "name": "adjustedConsumption",
        "label": "Adjusted consumption",
        "indicator": "N",
        "displayOrder": 22,
        "isDisplayed": true,
        "source": "CALCULATED",
        "option": null,
        "definition": "Total consumed quantity after adjusting for stockout days. Quantified in dispensing units.",
        "tag": null,
        "columnDefinition": {
          "canChangeOrder": true,
          "columnType": "NUMERIC"
        }
      },
      "totalLossesAndAdjustments": {
        "name": "totalLossesAndAdjustments",
        "label": "Total losses and adjustments",
        "indicator": "D",
        "displayOrder": 7,
        "isDisplayed": true,
        "source": "STOCK_CARDS",
        "option": null,
        "definition": "All kind of losses/adjustments made at the facility.",
        "tag": "adjustment",
        "columnDefinition": {
          "canChangeOrder": true,
          "columnType": "NUMERIC"
        }
      },
      "totalStockoutDays": {
        "name": "totalStockoutDays",
        "label": "Total stockout days",
        "indicator": "X",
        "displayOrder": 8,
        "isDisplayed": true,
        "source": "STOCK_CARDS",
        "option": null,
        "definition": "Total number of days facility was out of stock.",
        "tag": null,
        "columnDefinition": {
          "canChangeOrder": true,
          "columnType": "NUMERIC"
        }
      },
      "packsToShip": {
        "name": "packsToShip",
        "label": "Packs to ship",
        "indicator": "V",
        "displayOrder": 14,
        "isDisplayed": false,
        "source": "CALCULATED",
        "option": {
          "id": "dcf41f06-3000-4af6-acf5-5de4fffc966f",
          "optionName": "showPackToShipInAllPages",
          "optionLabel": "requisitionConstants.showPackToShipInAllPages"
        },
        "definition": "Total packs to be shipped based on pack size and applying rounding rules.",
        "tag": null,
        "columnDefinition": {
          "canChangeOrder": true,
          "columnType": "NUMERIC"
        }
      },
      "skipped": {
        "name": "skipped",
        "label": "Skip",
        "indicator": "S",
        "displayOrder": 1,
        "isDisplayed": true,
        "source": "USER_INPUT",
        "option": {
          "id": "17d6e860-a746-4500-a0fa-afc84d799dca",
          "optionName": "disableSkippedLineItems",
          "optionLabel": "requisitionConstants.disableSkippedLineItems"
        },
        "definition": "Select the check box below to skip a single product. Remove all data from the row prior to selection.",
        "tag": null,
        "columnDefinition": {
          "canChangeOrder": false,
          "columnType": "BOOLEAN"
        }
      },
      "orderable.productCode": {
        "name": "orderable.productCode",
        "label": "Product code",
        "indicator": "O",
        "displayOrder": 2,
        "isDisplayed": true,
        "source": "REFERENCE_DATA",
        "option": null,
        "definition": "Unique identifier for each commodity/product.",
        "tag": null,
        "columnDefinition": {
          "canChangeOrder": false,
          "columnType": "TEXT"
        }
      },
      "idealStockAmount": {
        "name": "idealStockAmount",
        "label": "Ideal Stock Amount",
        "indicator": "G",
        "displayOrder": 10,
        "isDisplayed": true,
        "source": "REFERENCE_DATA",
        "option": null,
        "definition": "The Ideal Stock Amount is the target quantity for a specific commodity type, facility, and period.",
        "tag": null,
        "columnDefinition": {
          "canChangeOrder": true,
          "columnType": "NUMERIC"
        }
      },
      "total": {
        "name": "total",
        "label": "Total",
        "indicator": "Y",
        "displayOrder": 19,
        "isDisplayed": true,
        "source": "CALCULATED",
        "option": null,
        "definition": "Total of beginning balance and quantity received.",
        "tag": null,
        "columnDefinition": {
          "canChangeOrder": true,
          "columnType": "NUMERIC"
        }
      },
      "totalConsumedQuantity": {
        "name": "totalConsumedQuantity",
        "label": "Total consumed quantity",
        "indicator": "C",
        "displayOrder": 6,
        "isDisplayed": true,
        "source": "STOCK_CARDS",
        "option": null,
        "definition": "Quantity dispensed/consumed in the reporting period. This is quantified in dispensing units.",
        "tag": "consumed",
        "columnDefinition": {
          "canChangeOrder": true,
          "columnType": "NUMERIC"
        }
      },
      "stockOnHand": {
        "name": "stockOnHand",
        "label": "Stock on hand",
        "indicator": "E",
        "displayOrder": 9,
        "isDisplayed": true,
        "source": "STOCK_CARDS",
        "option": null,
        "definition": "Current physical count of stock on hand. This is quantified in dispensing units.",
        "tag": null,
        "columnDefinition": {
          "canChangeOrder": true,
          "columnType": "NUMERIC"
        }
      },
      "requestedQuantity": {
        "name": "requestedQuantity",
        "label": "Requested quantity",
        "indicator": "J",
        "displayOrder": 15,
        "isDisplayed": true,
        "source": "USER_INPUT",
        "option": null,
        "definition": "Requested override of calculated quantity. This is quantified in dispensing units.",
        "tag": null,
        "columnDefinition": {
          "canChangeOrder": true,
          "columnType": "NUMERIC"
        }
      },
      "beginningBalance": {
        "name": "beginningBalance",
        "label": "Beginning balance",
        "indicator": "A",
        "displayOrder": 4,
        "isDisplayed": true,
        "source": "STOCK_CARDS",
        "option": null,
        "definition": "Based on the Stock On Hand from the previous period. This is quantified in dispensing units.",
        "tag": null,
        "columnDefinition": {
          "canChangeOrder": true,
          "columnType": "NUMERIC"
        }
      },
      "orderable.dispensable.displayUnit": {
        "name": "orderable.dispensable.displayUnit",
        "label": "Display Unit",
        "indicator": "U",
        "displayOrder": 24,
        "isDisplayed": true,
        "source": "REFERENCE_DATA",
        "option": null,
        "definition": "Display unit for this product.",
        "tag": null,
        "columnDefinition": {
          "canChangeOrder": true,
          "columnType": "TEXT"
        }
      },
      "totalReceivedQuantity": {
        "name": "totalReceivedQuantity",
        "label": "Total received quantity",
        "indicator": "B",
        "displayOrder": 5,
        "isDisplayed": true,
        "source": "STOCK_CARDS",
        "option": null,
        "definition": "Total quantity received in the reporting period. This is quantified in dispensing units.",
        "tag": "received",
        "columnDefinition": {
          "canChangeOrder": true,
          "columnType": "NUMERIC"
        }
      },
      "approvedQuantity": {
        "name": "approvedQuantity",
        "label": "Approved quantity",
        "indicator": "K",
        "displayOrder": 17,
        "isDisplayed": true,
        "source": "USER_INPUT",
        "option": null,
        "definition": "Final approved quantity. This is quantified in dispensing units.",
        "tag": null,
        "columnDefinition": {
          "canChangeOrder": true,
          "columnType": "NUMERIC"
        }
      },
      "orderable.fullProductName": {
        "name": "orderable.fullProductName",
        "label": "Product",
        "indicator": "R",
        "displayOrder": 3,
        "isDisplayed": true,
        "source": "REFERENCE_DATA",
        "option": null,
        "definition": "Primary name of the product.",
        "tag": null,
        "columnDefinition": {
          "canChangeOrder": false,
          "columnType": "TEXT"
        }
      },
      "pricePerPack": {
        "name": "pricePerPack",
        "label": "Price per pack",
        "indicator": "T",
        "displayOrder": 23,
        "isDisplayed": true,
        "source": "REFERENCE_DATA",
        "option": null,
        "definition": "Price per pack. Will be blank if price is not defined.",
        "tag": null,
        "columnDefinition": {
          "canChangeOrder": true,
          "columnType": "CURRENCY"
        }
      },
      "calculatedOrderQuantityIsa": {
        "name": "calculatedOrderQuantityIsa",
        "label": "Calc Order Qty ISA",
        "indicator": "S",
        "displayOrder": 11,
        "isDisplayed": true,
        "source": "CALCULATED",
        "option": null,
        "definition": "Calculated Order Quantity ISA is based on an ISA configured by commodity type, and several trade items may fill for one commodity type.",
        "tag": null,
        "columnDefinition": {
          "canChangeOrder": true,
          "columnType": "NUMERIC"
        }
      },
      "averageConsumption": {
        "name": "averageConsumption",
        "label": "Average consumption",
        "indicator": "P",
        "displayOrder": 12,
        "isDisplayed": true,
        "source": "STOCK_CARDS",
        "option": null,
        "definition": "Average consumption over a specified number of periods/months. Quantified in dispensing units.",
        "tag": null,
        "columnDefinition": {
          "canChangeOrder": true,
          "columnType": "NUMERIC"
        }
      },
      "requestedQuantityExplanation": {
        "name": "requestedQuantityExplanation",
        "label": "Requested quantity explanation",
        "indicator": "W",
        "displayOrder": 16,
        "isDisplayed": true,
        "source": "USER_INPUT",
        "option": null,
        "definition": "Explanation of request for a quantity other than calculated order quantity.",
        "tag": null,
        "columnDefinition": {
          "canChangeOrder": true,
          "columnType": "TEXT"
        }
      },
      "calculatedOrderQuantity": {
        "name": "calculatedOrderQuantity",
        "label": "Calculated order quantity",
        "indicator": "I",
        "displayOrder": 13,
        "isDisplayed": true,
        "source": "CALCULATED",
        "option": null,
        "definition": "Actual quantity needed after deducting stock in hand. This is quantified in dispensing units.",
        "tag": null,
        "columnDefinition": {
          "canChangeOrder": true,
          "columnType": "NUMERIC"
        }
      },
      "remarks": {
        "name": "remarks",
        "label": "Remarks",
        "indicator": "L",
        "displayOrder": 18,
        "isDisplayed": true,
        "source": "USER_INPUT",
        "option": null,
        "definition": "Any additional remarks.",
        "tag": null,
        "columnDefinition": {
          "canChangeOrder": true,
          "columnType": "TEXT"
        }
      },
      "totalCost": {
        "name": "totalCost",
        "label": "Total cost",
        "indicator": "Q",
        "displayOrder": 25,
        "isDisplayed": true,
        "source": "CALCULATED",
        "option": null,
        "definition": "Total cost of the product based on quantity requested. Will be blank if price is not defined.",
        "tag": null,
        "columnDefinition": {
          "canChangeOrder": true,
          "columnType": "CURRENCY"
        }
      },
      "maximumStockQuantity": {
        "name": "maximumStockQuantity",
        "label": "Maximum stock quantity",
        "indicator": "H",
        "displayOrder": 21,
        "isDisplayed": true,
        "source": "CALCULATED",
        "option": {
          "id": "ff2b350c-37f2-4801-b21e-27ca12c12b3c",
          "optionName": "default",
          "optionLabel": "requisitionConstants.default"
        },
        "definition": "Maximum stock calculated based on consumption and max stock amounts. Quantified in dispensing units.",
        "tag": null,
        "columnDefinition": {
          "canChangeOrder": true,
          "columnType": "NUMERIC"
        }
      },
      "additionalQuantityRequired": {
        "name": "additionalQuantityRequired",
        "label": "Additional quantity required",
        "indicator": "Z",
        "displayOrder": 24,
        "isDisplayed": false,
        "source": "USER_INPUT",
        "option": null,
        "definition": "Additional quantity required for new patients",
        "tag": null,
        "columnDefinition": {
          "canChangeOrder": true,
          "columnType": "NUMERIC"
        }
      }
    }
  },
  "availableFullSupplyProducts": [
    {
      "programs": [
        {
          "programId": "418bdc1d-c303-4bd0-b2d3-d8901150a983",
          "orderableDisplayCategoryId": "16173fd0-f439-4222-931e-91c413a495c3",
          "orderableCategoryDisplayName": "Vaccines",
          "orderableCategoryDisplayOrder": 5,
          "fullSupply": true,
          "displayOrder": 5,
          "pricePerPack": 8.5
        }
      ],
      "dispensable": {
        "dispensingUnit": null,
        "displayUnit": "1 dose,injection"
      },
      "identifiers": {
        "commodityType": "08561b98-2f90-4c4a-a1e3-f180a83abd66"
      },
      "id": "b61c652d-2259-41d7-8bb6-fc5fcdd95626",
      "productCode": "rota1",
      "fullProductName": "Rotavirus",
      "netContent": 1,
      "packRoundingThreshold": 1,
      "roundToZero": true,
      "commodityTypeIdentifier": "08561b98-2f90-4c4a-a1e3-f180a83abd66"
    },
    {
      "programs": [
        {
          "programId": "418bdc1d-c303-4bd0-b2d3-d8901150a983",
          "orderableDisplayCategoryId": "16173fd0-f439-4222-931e-91c413a495c3",
          "orderableCategoryDisplayName": "Vaccines",
          "orderableCategoryDisplayOrder": 5,
          "fullSupply": true,
          "displayOrder": 5,
          "pricePerPack": 12
        }
      ],
      "dispensable": {
        "dispensingUnit": null,
        "displayUnit": "20 dose,injection"
      },
      "identifiers": {
        "commodityType": "99ccf663-3304-44ae-b2e0-a67fd5511e2a"
      },
      "id": "8ef9d4da-b6e5-401c-b433-765a5fd8a0cc",
      "productCode": "bcg20",
      "fullProductName": "BCG",
      "netContent": 20,
      "packRoundingThreshold": 1,
      "roundToZero": true,
      "commodityTypeIdentifier": "99ccf663-3304-44ae-b2e0-a67fd5511e2a"
    },
    {
      "programs": [
        {
          "programId": "418bdc1d-c303-4bd0-b2d3-d8901150a983",
          "orderableDisplayCategoryId": "16173fd0-f439-4222-931e-91c413a495c3",
          "orderableCategoryDisplayName": "Vaccines",
          "orderableCategoryDisplayOrder": 5,
          "fullSupply": true,
          "displayOrder": 5,
          "pricePerPack": 6
        }
      ],
      "dispensable": {
        "dispensingUnit": null,
        "displayUnit": "10 dose,injection"
      },
      "identifiers": {
        "commodityType": "ac5e9d62-dc0a-4a0e-bead-e66a2eb81b07"
      },
      "id": "e217910c-3364-46b3-92cd-8dd8acf0c557",
      "productCode": "tetanus10",
      "fullProductName": "VAT",
      "netContent": 10,
      "packRoundingThreshold": 1,
      "roundToZero": true,
      "commodityTypeIdentifier": "ac5e9d62-dc0a-4a0e-bead-e66a2eb81b07"
    },
    {
      "programs": [
        {
          "programId": "418bdc1d-c303-4bd0-b2d3-d8901150a983",
          "orderableDisplayCategoryId": "16173fd0-f439-4222-931e-91c413a495c3",
          "orderableCategoryDisplayName": "Vaccines",
          "orderableCategoryDisplayOrder": 5,
          "fullSupply": true,
          "displayOrder": 5,
          "pricePerPack": 2
        }
      ],
      "dispensable": {
        "dispensingUnit": null,
        "displayUnit": "1 dose,injection"
      },
      "identifiers": {
        "commodityType": "43a1cd06-42f1-4e7c-b8ed-594416985381"
      },
      "id": "5f0dd194-aae1-490c-8b51-3cf0c87af983",
      "productCode": "penta1",
      "fullProductName": "Pentavalent (1 dose)",
      "netContent": 1,
      "packRoundingThreshold": 1,
      "roundToZero": true,
      "commodityTypeIdentifier": "43a1cd06-42f1-4e7c-b8ed-594416985381"
    }
  ],
  "availableNonFullSupplyProducts": [],
  "statusChanges": {
    "IN_APPROVAL": {
      "authorId": "560be32a-ea2e-4d12-ae00-1f69376ad535",
      "changeDate": "2018-01-16T17:34:57.915007Z"
    },
    "INITIATED": {
      "authorId": "211a6b4d-3c59-4fb2-8075-eedb79a18103",
      "changeDate": "2018-01-16T14:34:57.915007Z"
    },
    "SUBMITTED": {
      "authorId": "211a6b4d-3c59-4fb2-8075-eedb79a18103",
      "changeDate": "2018-01-16T15:34:57.915007Z"
    },
    "APPROVED": {
      "authorId": "1e3b03a5-1d48-4de1-bb4a-389beece2277",
      "changeDate": "2018-01-16T18:34:57.915007Z"
    },
    "AUTHORIZED": {
      "authorId": "211a6b4d-3c59-4fb2-8075-eedb79a18103",
      "changeDate": "2018-01-16T16:34:57.915007Z"
    }
  },
  "statusHistory": [
    {
      "status": "AUTHORIZED",
      "statusMessageDto": null,
      "authorId": "211a6b4d-3c59-4fb2-8075-eedb79a18103",
      "createdDate": "2018-01-16T16:34:57.915007Z"
    },
    {
      "status": "APPROVED",
      "statusMessageDto": null,
      "authorId": "1e3b03a5-1d48-4de1-bb4a-389beece2277",
      "createdDate": "2018-01-16T18:34:57.915007Z"
    },
    {
      "status": "INITIATED",
      "statusMessageDto": null,
      "authorId": "211a6b4d-3c59-4fb2-8075-eedb79a18103",
      "createdDate": "2018-01-16T14:34:57.915007Z"
    },
    {
      "status": "SUBMITTED",
      "statusMessageDto": null,
      "authorId": "211a6b4d-3c59-4fb2-8075-eedb79a18103",
      "createdDate": "2018-01-16T15:34:57.915007Z"
    },
    {
      "status": "IN_APPROVAL",
      "statusMessageDto": null,
      "authorId": "560be32a-ea2e-4d12-ae00-1f69376ad535",
      "createdDate": "2018-01-16T17:34:57.915007Z"
    }
  ],
  "datePhysicalStockCountCompleted": null,
  "stockAdjustmentReasons": [],
  "extraData": {}
}

Requisition Fields for Metrics

The view above shows the fields from the requisition that need to be mapped to the metrics. This work has already been done for the OpenLMIS reporting stack.

  • Program - (Displayed as "EPI" in title) is a grouping of orderables, configuration and facility network associated with a specific funding source
  • Facility - Defines the location the requisition was created from
  • Reporting Period - The period of the requisition (usually, monthly or quarterly)
  • Product - Each row in the requisition represents a product with a Product Code and Product name
  • Beginning balance - Based on the Stock On Hand from the previous period quantified in dispensing units
  • Total received quantity - Total quantity received in the reporting period quantified in dispensing units
  • Total consumed quantity - Quantity dispensed/consumed in the reporting period quantified in dispensing units
  • Total losses and adjustments - All kind of losses/adjustments made at the facility 
  • Total stockout days - Total number of days facility was out of stock
  • Stock on hand - Current physical count of stock on hand quantified in dispensing units

Metrics (from DHIS2 Metric Scope and Definition Page)

Below is the list of indicators that our DHIS2 integration will support. Each one is calculated per product, reporting period, facility, and program.

Stock Status

Evaluated based on the logic below:

  • Stockout: stockOnHand = 0, totalStockoutDays > 0, beginningBalance = 0, or if maxPeriodsOfStock = 0
  • Understocked: 0 < maxPeriodsOfStock < 3
  • Adequately Stocked: 3 <= maxPeriodsIOfStock <= 6
  • Overstocked: maxPeriodsOfStock > 6

Received Quantities

The amount from the totalReceivedQuantity field on the requisition line item for that product, reporting period, and facility.

Consumed Quantities

The amount from the totalConsumedQuantity field on the requisition line item for that product, reporting period, and facility.

Total Stockout Days

The total number of days in the totalStockoutDays field on the requisition line item for that product, reporting period, and facility.

FHIR Measure Resource Crosswalk

We will create a measure for each column in the requisition that is required in the metrics defined above. The Measure, in this instance, provides a dataset definition showing what needs to be posted and it maps to the R&R template in OpenLMIS. We will build a Nifi process to create one measure per requisition column. In total we will develop 7 measures.

The measure will define a single group for each product with the group identifier and name mapping to the product code and name in OpenLMIS. We will also map the program to the 

Below is a crosswalk:


OpenLMIS Requisition FieldMeasure Resource Field NameNotes
Programgroup.identifierThe program will be embedded in an independent group
Facility
The facility will not be captured within the measure. Instead, it will be captured as the "reporter" in the MeasureReport.

Product Code

group.identifierEach product in the requisition will be mapped to a group
Productgroup.nameThe name of the product

Sample FHIR Messages (Using the Latest FHIR R4)

This section includes hand formed JSON to develop a proof of concept and testing purposes. The goal here is to develop a minimum set of resources to ensure FHIR can be crosswalked to a requisition.

Ordering Messages:

  1. We need to post the locations to the FHIR server to make sure the location exists (this is already available in OpenLMIS)
  2. We need to post each measure (one time activity)
  3. We need to post a MeasureReport for each measure every time a requisition is received

Sample Measure

The following Measure is built for the beginning balance measure. Each column in the requisition needs to be mapped to measures following this template. We only need to change the name. This measure has been shortened to only show the 4 products identified in the OpenLMIS screenshot above.

{
  "resourceType": "Measure",
  "name": "beginning_balance",
  "description": "Based on the Stock On Hand from the previous period quantified in dispensing units.",
  "status": "draft",
  "experimental": true,
  "group": [
    {
      "code": {
        "text": "programName"
      },
      "description": "The program for this measure"
    },
    {
      "code": {
        "text": "bcg20"
      },
      "description": "BCG"
    },
    {
      "code": {
        "text": "penta1"
      },
      "description": "Pentavalent (1 dose)"
    },
    {
      "code": {
        "text": "rota1"
      },
      "description": "Rotavirus"
    },
    {
      "code": {
        "text": "tetanus10"
      },
      "description": "VAT"
    }
  ]
}


Sample MeasureReport

The following MeasureReport is built for the beginning balance measure. Each column in the requisition needs to be mapped to measures following this template. We only need to change the name. This measure has been shortened to only show the 4 products identified in the OpenLMIS screenshot above. Note that measureScore is meant to store a numerical Quantity and as such our use is a temporary workaround.  We will work with the FHIR community and the authors of the IHE mADX profile to find the right solution to this problem, which could be a future change to the FHIR MeasureReport definition, or the recommendation that we use an extension.

{
  "resourceType": "MeasureReport",
  "text": {
    "status": "generated",
    "div": "<div xmlns=\"http://www.w3.org/1999/xhtml\"><p><b>Generated Narrative with Details</b></p></div>"
  },
  "status": "complete",
  "type": "summary",
  "measure": "http://hapi.fhir.org/baseR4/Measure/10949",
  "reporter": {
    "reference": "Location/5772"
  },
  "period": {
    "start": "2019-01-01",
    "end": "2019-01-31"
  },
  "group": [
    {
      "code": {
        "text": "programName"
      },
      "measureScore": {
        "system": "openlmisProgramName",
        "code": "EPI"
      }
    },
    {
      "code": {
        "text": "bcg20"
      },
      "measureScore": {
        "value": "100"
      }
    },
    {
      "code": {
        "text": "penta1"
      },
      "measureScore": {
        "value": "100"
      }
    },
    {
      "code": {
        "text": "rota1"
      },
      "measureScore": {
        "value": "100"
      }
    },
    {
      "code": {
        "text": "tetanus10"
      },
      "measureScore": {
        "value": "100"
      }
    }
  ]
}

ReferenceData API, User Rights and Roles

The HAPI FHIR server is not currently accessible from the public. Information is exchanged from the OpenLMIS microservices to the HAPI FHIR server to populate the information in the FHIR server. All public transactions need to go through the OpenLMIS ReferenceData API which acts as a gateway for access to the HAPI FHIR server. The ReferenceData API provides the authentication service for external consumers, forwards any valid requests to the HAPI FHIR server and returns the results to the client. This section defines the ReferenceData APIs that need to be exposed in the MVP, the user rights and roles that are required to be built in the ReferenceData API to support consuming Measures and Measure Reports.

API Endpoints

We will only expose the ability to read measures and measure reports. The creation of measures and measure reports will be completed by the OpenLMIS system's microservices and Apache Nifi. External consumers will have to initiate a change with system administrators in order to update measures.

OpenLMIS ResourceActionDescriptionHAPI FHIR ResourceURL Parameters
/api/MeasureGETThis endpoint allows the user to search all measures and returns a paginated list of results. /Measure

access_token (required)

/api/Measure/{id}GETThis endpoint returns a specific measure/Measure/{id}

access_token (required)

/api/MeasureReportGETThis endpoint allows the user to search all measure reports and returns a paginated list of resultsMeasureReport

access_token (required)

measure (optional) - Filters the list of reports by measure id

period (optional) - The period of the measure report

reporter (optional) - This is the reporter of the measure Report. By convention, we use the location Id from the Location endpoint for all Measure Report reporters.

lastUpdated (optional) - This returns all measure reports that have lastUpdated greater than or equal to this UTC dateTime

/api/MeasureReport/{id}GETThis endpoint returns a specific measure report/MeasureReport/{id}

access_token (required)

Notes:

Period is a complex type with start and end dates. We need to figure out how to represent this is a search parameter.

"period": {
  "start": "2018-01-01",
  "end": "2018-01-31"
}

lastUpdated requires that we pass in a parameter to the HAPI FHIR server that states return everything that is greater than or equal to the lastUpdated date. We need to research exactly how to do this in the URL parameters (it may be something called a "mode", which defaults to mode=match, but we need something like "greater than or equal to")

Reporting Rights and Roles

Access needs to be restricted to measures and measure reports (Role Type: Reports). This section defines the logical rights and roles that should be created in the system.

Right NameRight Description
View Measures (MEASURE_VIEW)This role allows users to view measures that are returned from the /api/Measure endpoint.
View Measure Reports (MEASUREREPORT_VIEW)This role allows users to view measure reports that have been returned by the /api/MeasureReport endpoint.
Reporting Role NameRole DescriptionRights
Reporting Measure and Measure Report AccessThis is a new role that allows access to Measures and MeasureReportsView Measures, View Measure Reports
(Update) Reporting PersonnelWe need to update this to include the following rightsView Measures, View Measure Reports

Work Process

We need to develop the following items:

Developing and Exposing Reports as FHIR Measure Reports:

  • (Done) Develop reports in the data warehouse to return the four indicators identified
  • (Done) Make those reports available to Superset for viewing
  • (Done) Make those reports available through an API endpoint
  • (Done) Research Measure and Measure Report in FHIR
  • (Done) Research Measure and Measure Report implementation in the OpenLMIS HAPI FHIR server
  • Develop roles in the referenceData microservice and demo data to support accessing the FHIR API Measure Report
  • Develop a facade API in the referenceData microservice that receives external requests, verifies access controls from ReferenceData and forwards the request to the OpenLMIS HAPI FHIR server
  • Test end-to-end that it functions

Transportation to DHIS2:

  • DECISION: Identify if we want to build this transportation and mapping as part of OpenLMIS or use a third party tool
  • Develop a Crosswalk:
    • Programs
    • Locations
    • Orderables/Data Elements
    • Measure/Indicator Map
  • Setup DHIS2 System or Align OpenLMIS metadata with existing DHIS2 implementation
    • Choose a country to be used for reference
    • Align demo data with a DHIS2 instance
    • Setup a DHIS2 instance with these indicators
    • Develop the crosswalk as stated above and develop a mechanism to import that into OpenLMIS (either OpenLMIS core or the data warehouse)
  • Develop a translation mechanism from Measure Report to DHIS2 ADK message
  • Develop transportation mechanism as defined in Workflow 4 above
  • Test end-to-end

The diagram below outlines these items as tickets.



OpenLMIS: the global initiative for powerful LMIS software