diff --git a/README.md b/README.md index 5c69eb9..bdfe693 100644 --- a/README.md +++ b/README.md @@ -9,3 +9,4 @@ The catalogued examples include: * [Data value set synchronisation between two DHIS2 instances using the DHIS2 Java SDK](data-value-set-sync-dhis2-java-sdk/README.md) * [DHIS2 tracked entities to FHIR questionnaire responses](fhir-esavi-paho/README.md) * [Automating DHIS2 integration testing](integration-test/README.md) +* [Generating a FHIR IPS Patient based on DHIS2 tracked entitites ](dhis2-to-fhir-patient-bundle-datasonnet/README.md) \ No newline at end of file diff --git a/dhis2-to-fhir-patient-bundle-datasonnet/.gitignore b/dhis2-to-fhir-patient-bundle-datasonnet/.gitignore new file mode 100644 index 0000000..ea43e37 --- /dev/null +++ b/dhis2-to-fhir-patient-bundle-datasonnet/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +node_modules +.pnp +.pnp.js + +# testing +**/coverage + +# production +**/build +**/dist + +# DHIS2 +.d2 +**/src/locales +cli/assets + +# Editors +.vscode + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/dhis2-to-fhir-patient-bundle-datasonnet/LICENSE b/dhis2-to-fhir-patient-bundle-datasonnet/LICENSE new file mode 100644 index 0000000..c957710 --- /dev/null +++ b/dhis2-to-fhir-patient-bundle-datasonnet/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2004-2025, University of Oslo +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors +may be used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/dhis2-to-fhir-patient-bundle-datasonnet/README.md b/dhis2-to-fhir-patient-bundle-datasonnet/README.md new file mode 100644 index 0000000..daaf1fc --- /dev/null +++ b/dhis2-to-fhir-patient-bundle-datasonnet/README.md @@ -0,0 +1,481 @@ +# DHIS2 to FHIR IPS Patient Mapping + +This example demonstrates how to transform DHIS2 tracked entities from the WHO RMNCAH (Reproductive, Maternal, Newborn, Child and Adolescent Health) program into FHIR resources conformant to the [International Patient Summary (IPS)](https://build.fhir.org/ig/HL7/fhir-ips/en/StructureDefinition-Patient-uv-ips.html) profile. It provides a practical reference implementation for integrating DHIS2 maternal health data with FHIR-based health information exchanges. + +## Core Example vs. Exercise + +This repository consists of two parts: a core example and three exercises that extend the core functionality. + +**Core Example (Already Implemented):** + +The primary transformation maps patient demographics from DHIS2 tracked entity attributes to an IPS-conformant FHIR Patient resource. This demonstrates the complete pipeline from DHIS2 enrollment to FHIR server persistence, including: + +- Mapping tracked entity attributes (first name, last name, date of birth, etc.) to FHIR Patient elements +- Using conditional updates (PUT with identifiers) to prevent duplicates +- Bundling resources in FHIR transaction bundles for atomic operations +- Real-time synchronization using Apache Camel middleware + +**Hands-On Exercise (Three-Part Implementation):** + +The exercise guides you through extending the core example by implementing three additional resource mappings: + +1. **Practitioner Resource** - Map the DHIS2 user who created the enrollment to a FHIR Practitioner +2. **Observation Resource** - Map civil status (an attribute that doesn't fit in the IPS Patient profile) to a FHIR Observation +3. **AllergyIntolerance Resources** - Parse comma-separated allergy text and create individual AllergyIntolerance resources + +These exercises demonstrate how to handle attributes that don't have direct mappings in the IPS Patient profile, ensuring a lossless transformation (at the attribute level) where all DHIS2 data is preserved in appropriate FHIR resources. + +## Quick Start + +**Prerequisites:** +- Java 11+ +- Maven 3.6+ +- Node.js 18+ +- Yarn +- Docker + +After installing the prerequisites, run the entire stack with the following commands: + +```bash +yarn install --frozen-lockfile +yarn build +yarn start +``` + +This will spin up a DHIS2 instance with the Sierra Leone demo database on `localhost:8080`, a HAPI FHIR server with the IPS profiles loaded on `localhost:8081/fhir`, and middleware that listens to tracked entity changes in the DHIS2 instance. + +To run the transformation tests: + +```bash +cd fhir-sync-agent +mvn clean test -Dtest=IpsPatientMappingTestCase +``` + +## Walkthrough: Testing the Integration + +Once the stack is running, you can test the end-to-end integration by creating a new patient enrollment in DHIS2 and observing the automatic synchronization to the FHIR server. + +**Step 1: Create a Patient Enrollment in DHIS2** + +1. Open your browser and navigate to `http://localhost:8080` +2. Log in with the default credentials (admin/district) +3. Open the **Capture** app from the main menu +4. Select the **WHO RMNCAH** program from the program dropdown +5. Select an organization unit of your choice from the organization tree +6. Click **New** to create a new enrollment +7. Fill out the enrollment form with patient demographics (first name, last name, date of birth, etc.) +8. Click **Save** to complete the enrollment + +![Capture](docs/capture.png) + +**Step 2: Observe the Synchronization** + +The `fhir-sync-agent` middleware automatically detects the new tracked entity enrollment. The agent performs the following operations: + +1. Fetches the complete tracked entity data from DHIS2 (including all attributes) +2. Passes the tracked entity JSON to the DataSonnet transformation script +3. Transforms the data into a FHIR transaction bundle with IPS-conformant resources +4. Pushes the bundle to the HAPI FHIR server using conditional updates + +**Step 3: Verify the FHIR Resources** + +1. Navigate to `http://localhost:8081/fhir/Patient` in your browser +2. The HAPI FHIR server returns a Bundle containing all Patient resources +3. Locate your newly created patient by searching for the name or identifier you entered +4. You can also query specific patients using FHIR search parameters, e.g., `http://localhost:8081/fhir/Patient?identifier=urn:dhis2:rmncah:patient-id|` + +The patient data from DHIS2 is now available as a FHIR resource, ready for integration with other health information systems. + +## Architecture + +When someone enrolls in the WHO RMNCAH program in DHIS2, the sync agent detects the tracked entity changes, transforms the data into FHIR resources conformant to the IPS Patient profile, and pushes them to the HAPI FHIR server. + +### Technology Stack + +This example uses **Apache Camel** as middleware to connect DHIS2 with the FHIR server. Camel is a mature open-source integration framework with extensive connectors for health systems, making it a popular choice for DHIS2 integrations (also used by OpenMRS ETL, OpenEHR FHIR bridge, and others). + +**Why Apache Camel?** It's flexible, used across multiple industries, and comes with built-in components for DHIS2, FHIR, HL7v2, and hundreds of other systems. This means you can adapt this example to connect DHIS2 with whatever systems your country needs. + +**Transformation approach:** This example uses **DataSonnet**, a declarative mapping language for transforming JSON structures. Mappings are written once and can be updated without modifying application code. The alternative is procedural Java with the HAPI FHIR library, but DataSonnet offers simpler syntax and easier maintenance. + +![Middleware Diagram](docs/image.png) + +Want to learn more? See the [FHIR Page](https://dhis2.org/integration/fhir/) on the DHIS2 website. + +**Key files:** + +- `fhirBundle.ds` - Main transformation entry point +- `helperFunctions.libsonnet` - Utilities for extracting DHIS2 attributes and building FHIR data types +- `patientResource.libsonnet` - Builds the IPS Patient resource +- EXERCISE: `practitionerResource.libsonnet` - Maps the DHIS2 user who created the record +- EXERCISE: `observationResources.libsonnet` - Handles attributes that don't fit in Patient (civil status, allergies) + +## FHIR Fundamentals + +Before diving into the mapping, let's cover some FHIR basics. If you're already familiar with FHIR, skip to [How It Works](#how-it-works). + +### What is FHIR? + +[FHIR](https://www.hl7.org/fhir/R4/) (Fast Healthcare Interoperability Resources) is a standard for exchanging healthcare information electronically. It provides a common language that different health systems use to communicate and defines a standardized data exchange format. The FHIR specification serves as an entry point for understanding key components of FHIR-compliant data exchange, including [FHIR Resources](https://www.hl7.org/fhir/R4/resourcelist.html), [Data Types](https://www.hl7.org/fhir/R4/datatypes.html), and [Terminology Systems](https://www.hl7.org/fhir/R4/terminologies-systems.html). + +### FHIR Resources + +Everything in FHIR is a **resource**. A resource is a structured piece of healthcare information. What follows is a list of common resources and their DHIS2 counterparts: + +- [Patient](https://www.hl7.org/fhir/R4/patient.html) - Demographics and administrative info about a person. In DHIS2, these demographics are often captured with tracked entity attributes. +- [Observation](https://www.hl7.org/fhir/R4/observation.html) - Measurements and facts (blood pressure, marital status, etc.). In DHIS2, such Observations are often captured as data elements within Tracker program stages. +- [Practitioner](https://www.hl7.org/fhir/R4/practitioner.html) - Healthcare provider information. In DHIS2, you can use the [DHIS2 user](https://docs.dhis2.org/en/develop/using-the-api/dhis-core-version-241/users.html) metadata as source when populating a Practitioner resource. +- [Location](https://www.hl7.org/fhir/R4/location.html) and [Organization](https://www.hl7.org/fhir/R4/organization.html) - Used to express organizations, their physical location and hierarchy. Can be used together to form a shared registry. In DHIS2, you can use [organisation units](https://docs.dhis2.org/en/develop/using-the-api/dhis-core-version-241/metadata.html#webapi_organisation_units) together with organisation unit groups and hierarchies as the source for Location and Organization resources. + +Each resource has a defined structure with fields (called `elements`). Here is a simple Patient resource: + +```json +{ + "resourceType": "Patient", + "id": "123", + "name": [{ + "given": ["Jane"], + "family": "Doe" + }], + "birthDate": "1997-04-18" +} +``` + +Each FHIR resource follows this structure, with a `resourceType`, `id`, and a list of `elements` to include. In the example above, we capture two elements: the `name` and `birthDate` of the patient. + +### Data Types in FHIR + +FHIR has two types of data: + +**Primitive types** - Simple values like strings, dates, booleans: +```json +{ + "birthDate": "1997-04-18", + "active": true +} +``` + +**Complex types** - Structured objects with multiple fields: +```json +{ + "name": [{ + "use": "official", + "given": ["Jane"], + "family": "Doe" + }] +} +``` + +Notice that `name` is an array - FHIR often uses arrays even when there's only one value, because a person might have multiple names. + +### What is a Bundle? + +A **Bundle** is a container that holds multiple resources, allowing them to be sent together in a single operation. There are different types of bundles: + +- **transaction** - All resources are created/updated together (all succeed or all fail) +- **batch** - Resources are processed independently +- **searchset** - Results from a search query + +Here's a simple transaction bundle: + +```json +{ + "resourceType": "Bundle", + "type": "transaction", + "entry": [ + { + "resource": { + "resourceType": "Patient", + "name": [{"given": ["Jane"], "family": "Doe"}] + }, + "request": { + "method": "POST", + "url": "Patient" + } + } + ] +} +``` + +**Why use bundles?** +- Send multiple resources in a single HTTP request +- Ensure atomic operations (all succeed or all fail) +- Reference resources internally without knowing their server-assigned IDs yet +- Simplify cross-resource references within the same bundle (e.g., a `Patient` referencing a `Practitioner`) + +### Conditional Updates (PUT with Identifiers) + +When synchronizing data repeatedly (like from DHIS2), you need to avoid creating duplicates. **Conditional updates** solve this problem: + +```json +{ + "request": { + "method": "PUT", + "url": "Patient?identifier=urn:dhis2:patient-id|8437107" + } +} +``` + +This says: "If a Patient with this identifier exists, update it. Otherwise, create it." + +The explicit identifier URL is used because the FHIR server assigns its own resource IDs, but you control the identifiers. This allows you to look up resources using your system's IDs. Multiple identifiers can be added, enabling different systems to reference the same resource using their own identifier schemes. + +### Implementation Guides and Profiles + +FHIR resources are generic by default. An [Implementation Guide (IG)](https://build.fhir.org/ig/FHIR/ig-guidance/index.html) customizes FHIR for specific use cases and contexts by: + +- Defining which elements are required +- Adding constraints and validation rules +- Specifying which code systems to use + +A **Profile** is a set of rules from an IG that constrains a specific resource. For example: + +- [Base FHIR Patient](https://www.hl7.org/fhir/R4/patient.html) has optional fields with no requirements on which to include or omit +- [IPS Patient profile](https://build.fhir.org/ig/HL7/fhir-ips/en/StructureDefinition-Patient-uv-ips.html) requires `name` and `birthDate` elements to be populated +- National profiles may require additional country-specific fields + +**IPS (International Patient Summary)** is a globally-recognized IG for minimal patient summaries designed for cross-border care. When we refer to "IPS Patient," we mean a Patient resource that conforms to the IPS profile's requirements. + +### Simple DataSonnet Example + +Before examining the complex IPS mapping, let's start with a simple example transforming a DHIS2 tracked entity into a FHIR Patient: + +**Input (DHIS2 Tracked Entity):** +```json +{ + "trackedEntity": "tracked-entity-id", + "trackedEntityType": "nEenWmSyUEp", + "orgUnit": "DiszpKrYNg8", + "enrollments": [ + { + "attributes": [ + { "attribute": "lZGmxYbs97q", "value": "ID001" }, + { "attribute": "w75KJ2mc4zz", "value": "Jane" }, + { "attribute": "zDhUuAYrxNC", "value": "Smith" }, + { "attribute": "gHGyrwKPzej", "value": "2000-01-01" } + ] + } + ] +} +``` + +**DataSonnet transformation:** +```jsonnet +{ + resourceType: 'Patient', + identifier: [{ + system: 'urn:dhis2:patient-id', + value: body.enrollments[0].attributes[0].value + }], + name: [{ + given: [body.enrollments[0].attributes[1].value], + family: body.enrollments[0].attributes[2].value + }], + birthDate: body.enrollments[0].attributes[3].value +} +``` + +**Output (FHIR Patient):** +```json +{ + "resourceType": "Patient", + "identifier": [{ + "system": "urn:dhis2:patient-id", + "value": "ID001" + }], + "name": [{ + "given": ["Jane"], + "family": "Smith" + }], + "birthDate": "2000-01-01" +} +``` +This example demonstrates basic DataSonnet syntax, but it has several limitations: + +1. **Hard-coded array indices**: The transformation uses `body.enrollments[0].attributes[0].value`, `attributes[1].value`, etc. This approach is fragile because: + - It assumes attributes always appear in a specific order + - If attributes are reordered in DHIS2, the mapping breaks + - There is no way to identify which attribute is which (first name vs. last name) + +2. **No attribute ID matching**: The transformation doesn't use the actual attribute IDs (`lZGmxYbs97q`, `w75KJ2mc4zz`, etc.) to identify which value goes where. + +3. **Assumes data exists**: There's no null checking or fallback logic if attributes are missing. + +**How the Core Example Solves This:** + +The core implementation uses helper functions to make the transformation more robust and maintainable: + +```jsonnet +local helpers = import 'helperFunctions.libsonnet'; + +{ + resourceType: 'Patient', + identifier: [{ + system: 'urn:dhis2:patient-id', + value: helpers.getAttrValue(body, 'lZGmxYbs97q') // Looks up by attribute ID + }], + name: [helpers.parseName( + helpers.getAttrValue(body, 'w75KJ2mc4zz'), // First name + helpers.getAttrValue(body, 'zDhUuAYrxNC') // Last name + )], + birthDate: helpers.getAttrValue(body, 'gHGyrwKPzej') +} +``` + +The `getAttrValue(tei, attributeId)` function searches through the enrollment-level attributes to find the correct value by ID, regardless of order. It also handles missing values gracefully. The `parseName()` function constructs a FHIR-compliant HumanName structure that satisfies IPS profile requirements. + +This approach means the transformation works reliably even when some attributes are missing or reordered. + + +**DataSonnet Quick Reference:** + +The [DataSonnet cookbook](https://datasonnet.github.io/datasonnet-mapper/datasonnet/latest/cookbook.html) provides practical examples for common transformation patterns. DataSonnet uses [Jsonnet syntax](https://jsonnet.org/learning/tutorial.html), so understanding Jsonnet will help you write transformations. Key concepts: + +- **Reference input data**: Use `body` to access the input JSON +- **Build objects**: Use `{}` with field definitions like `resourceType: 'Patient'` +- **Build arrays**: Use `[]` like `[body.attributes[1].value]` +- **Access nested data**: Use dot notation like `body.trackedEntity` or bracket notation like `body.attributes[0].value` +- **Variables**: Define local variables with `local varName = value;` +- **Conditionals**: Use `if condition then value else otherValue` +- **Array comprehension**: `[expression for item in array]` to transform arrays +- **String interpolation**: Use `'text %s' % value` for formatting + +Check out `docs/QUICK_REFERENCE.md` for other handy utility functions that are used in this example. + +## How It Works + +### Source: DHIS2 Tracked Entity + +DHIS2 stores patient data as tracked entities with attributes at both the entity level and enrollment level: + +```json +{ + "trackedEntity": "QfFVkOL8ixj", + "attributes": [ + {"attribute": "w75KJ2mc4zz", "value": "Jane"}, + {"attribute": "zDhUuAYrxNC", "value": "Doe"} + ], + "enrollments": [{ + "attributes": [ + {"attribute": "gHGyrwKPzej", "value": "1997-04-18"}, + {"attribute": "ciq2USN94oJ", "value": "Single or widow"}, + {"attribute": "gu1fqsmoU8r", "value": "NSAIDS, Penicillin"} + ] + }] +} +``` + +### Target: FHIR Bundle + +The transformation produces a transaction bundle with multiple resources: + +```json +{ + "resourceType": "Bundle", + "type": "transaction", + "entry": [ + { + "resource": { + "resourceType": "Patient", + "identifier": [{"value": "8437107", "system": "urn:dhis2:rmncah:patient-id"}], + "name": [{"given": ["Jane"], "family": "Doe"}], + "birthDate": "1997-04-18" + }, + "request": { + "method": "PUT", + "url": "Patient?identifier=urn:dhis2:rmncah:patient-id|8437107" + } + }, + { + "resource": { + "resourceType": "Observation", + "code": {"coding": [{"system": "http://loinc.org", "code": "45404-1"}]}, + "valueString": "Single or widow" + }, + "request": {"method": "PUT", "url": "Observation?identifier=..."} + }, + { + "resource": { + "resourceType": "AllergyIntolerance", + "reaction": [{"manifestation": [{"text": "NSAIDS"}]}] + }, + "request": {"method": "PUT", "url": "AllergyIntolerance?identifier=..."} + } + ] +} +``` + +## Core Concepts + +### Helper Functions (`helperFunctions.libsonnet`) + +These utilities make the transformation code cleaner: + +**`getAttrValue(tei, attributeId)`** - Extracts attribute values with smart fallback. Checks enrollment attributes first (program-specific), then falls back to TEI attributes (general demographic). + +**`parseName(firstName, lastName)`** - Combines name parts into a FHIR HumanName structure that satisfies IPS constraints (must have `given`, `family`, or `text`). + +**`parseAddress(...)`**, **`buildTelecom(...)`** - Transform DHIS2 address and contact data into FHIR structures. + +All helpers handle null values gracefully and use conditional field syntax to avoid empty objects. + +Not everything in DHIS2 fits into a FHIR Patient resource. The IPS Patient profile has specific fields for demographics, but what about: + +- Civil status? In the exercise, we use an `Observation` resource with LOINC code 45404-1 to capture this in FHIR. +- Allergies? In the exercise, we use `AllergyIntolerance` resources (one per allergy) +- Who created the record? In the exercise, we create a Practitioner and link via the `generalPractitioner` element. + +In this way, nothing that is captured during the DHIS2 enrollment is lost in the transformation. + +### Conditional Updates + +The bundle uses `PUT` with identifier queries: + +``` +PUT Patient?identifier=urn:dhis2:rmncah:patient-id|8437107 +``` + +This means: +- If a patient with that identifier exists it gets update +- If not - create it + +This means that if you create a new enrollment in DHIS2 and need to make changes to the attributes, the resulting FHIR patient is updated instead of duplicated. This is critical for keeping DHIS2 and FHIR in sync. + +## Hands-On Exercise + +Want to learn by doing? Check out `docs/HANDS_ON_GUIDE.md` for a step-by-step exercise where you will: + +1. Add a `Practitioner` resource (map DHIS2 user to FHIR Practitioner) +2. Add `Observation` for civil status +3. Handle multi-value attributes (splitting comma-separated allergies into separate `AllergyIntolerance` resources) + +The exercise uses guided TODO files with hints, and you can check your work against the solution files in `/fhir-sync-agent/src/main/resources/solutions`. + +## Key Files Reference + +- **`fhir-sync-agent/src/main/resources/fhirBundle.ds`** - Entry point that orchestrates the transformation +- **`fhir-sync-agent/src/main/resources/helperFunctions.libsonnet`** - Reusable utilities (documented inline) +- **`fhir-sync-agent/src/main/resources/patientResource.libsonnet`** - Patient resource builder (see comments for attribute IDs) +- **`fhir-sync-agent/src/main/resources/observationResources.libsonnet`** - Observation and AllergyIntolerance builders +- **`fhir-sync-agent/src/test/java/.../IpsPatientMappingTestCase.java`** - Tests with HAPI FHIR validation + +## Adapting This Example + +To use this with your own DHIS2 instance: + +1. **Identify your attribute IDs and/or data element IDs** - Look up the IDs in your DHIS2 metadata +2. **Update the constants** - Change `ATTR_FIRST_NAME`, `ATTR_DOB`, etc. in the resource libraries +3. **Adjust the identifier system** - Replace `urn:dhis2:rmncah:patient-id` with your system URI +4. **Add/remove resources** - Map additional attributes as needed +5. **Test thoroughly** - Run the validator against your FHIR server's profiles + +## Resources + +- [FHIR IPS Implementation Guide](http://hl7.org/fhir/uv/ips/) +- [DHIS2 Tracker API](https://docs.dhis2.org/en/develop/using-the-api/dhis-core-version-master/tracker.html) +- [DataSonnet Documentation](https://datasonnet.com/) +- [Jsonnet Tutorial](https://jsonnet.org/learning/tutorial.html) + +## Support + +Questions, issues or difficulties with the exercise? Open an issue on GitHub or reach out on the [DHIS2 Community of Practice](https://community.dhis2.org/). \ No newline at end of file diff --git a/dhis2-to-fhir-patient-bundle-datasonnet/config/authorisation-server/fhir-realm.json b/dhis2-to-fhir-patient-bundle-datasonnet/config/authorisation-server/fhir-realm.json new file mode 100644 index 0000000..5a8a4a3 --- /dev/null +++ b/dhis2-to-fhir-patient-bundle-datasonnet/config/authorisation-server/fhir-realm.json @@ -0,0 +1,2382 @@ +{ + "id": "5b7241af-b43c-4af1-a0b5-4957e338204b", + "realm": "fhir", + "notBefore": 0, + "defaultSignatureAlgorithm": "RS256", + "revokeRefreshToken": false, + "refreshTokenMaxReuse": 0, + "accessTokenLifespan": 300, + "accessTokenLifespanForImplicitFlow": 900, + "ssoSessionIdleTimeout": 1800, + "ssoSessionMaxLifespan": 36000, + "ssoSessionIdleTimeoutRememberMe": 0, + "ssoSessionMaxLifespanRememberMe": 0, + "offlineSessionIdleTimeout": 2592000, + "offlineSessionMaxLifespanEnabled": false, + "offlineSessionMaxLifespan": 5184000, + "clientSessionIdleTimeout": 0, + "clientSessionMaxLifespan": 0, + "clientOfflineSessionIdleTimeout": 0, + "clientOfflineSessionMaxLifespan": 0, + "accessCodeLifespan": 60, + "accessCodeLifespanUserAction": 300, + "accessCodeLifespanLogin": 1800, + "actionTokenGeneratedByAdminLifespan": 43200, + "actionTokenGeneratedByUserLifespan": 300, + "oauth2DeviceCodeLifespan": 600, + "oauth2DevicePollingInterval": 5, + "enabled": true, + "sslRequired": "external", + "registrationAllowed": false, + "registrationEmailAsUsername": false, + "rememberMe": false, + "verifyEmail": false, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": false, + "editUsernameAllowed": false, + "bruteForceProtected": false, + "permanentLockout": false, + "maxTemporaryLockouts": 0, + "maxFailureWaitSeconds": 900, + "minimumQuickLoginWaitSeconds": 60, + "waitIncrementSeconds": 60, + "quickLoginCheckMilliSeconds": 1000, + "maxDeltaTimeSeconds": 43200, + "failureFactor": 30, + "roles": { + "realm": [ + { + "id": "73a5dcfc-891c-44a5-bbdc-cf9fa42de4e4", + "name": "uma_authorization", + "description": "${role_uma_authorization}", + "composite": false, + "clientRole": false, + "containerId": "5b7241af-b43c-4af1-a0b5-4957e338204b", + "attributes": {} + }, + { + "id": "c981b14b-d83b-4202-96d1-5e85d94148ba", + "name": "default-roles-fhir", + "description": "${role_default-roles}", + "composite": true, + "composites": { + "realm": [ + "offline_access", + "uma_authorization" + ], + "client": { + "account": [ + "view-profile", + "manage-account" + ] + } + }, + "clientRole": false, + "containerId": "5b7241af-b43c-4af1-a0b5-4957e338204b", + "attributes": {} + }, + { + "id": "1eb5593a-3535-4dfa-9367-c50aebd326c2", + "name": "offline_access", + "description": "${role_offline-access}", + "composite": false, + "clientRole": false, + "containerId": "5b7241af-b43c-4af1-a0b5-4957e338204b", + "attributes": {} + } + ], + "client": { + "realm-management": [ + { + "id": "feaf0811-58f1-46e1-901c-a655bef40465", + "name": "query-clients", + "description": "${role_query-clients}", + "composite": false, + "clientRole": true, + "containerId": "053aaef6-efaf-4476-8e44-9ff636bca0ee", + "attributes": {} + }, + { + "id": "20cea7d0-4d74-446a-8ee1-e7c6a137e44e", + "name": "query-realms", + "description": "${role_query-realms}", + "composite": false, + "clientRole": true, + "containerId": "053aaef6-efaf-4476-8e44-9ff636bca0ee", + "attributes": {} + }, + { + "id": "59e06acd-bce7-4a5a-935d-ada807770636", + "name": "manage-users", + "description": "${role_manage-users}", + "composite": false, + "clientRole": true, + "containerId": "053aaef6-efaf-4476-8e44-9ff636bca0ee", + "attributes": {} + }, + { + "id": "530597e7-9f2b-41ab-8189-2c5342698699", + "name": "manage-authorization", + "description": "${role_manage-authorization}", + "composite": false, + "clientRole": true, + "containerId": "053aaef6-efaf-4476-8e44-9ff636bca0ee", + "attributes": {} + }, + { + "id": "68234117-5f14-441e-a28b-cc4533cef44e", + "name": "manage-events", + "description": "${role_manage-events}", + "composite": false, + "clientRole": true, + "containerId": "053aaef6-efaf-4476-8e44-9ff636bca0ee", + "attributes": {} + }, + { + "id": "baf0fcad-cf1d-4bbc-9194-086209259373", + "name": "manage-realm", + "description": "${role_manage-realm}", + "composite": false, + "clientRole": true, + "containerId": "053aaef6-efaf-4476-8e44-9ff636bca0ee", + "attributes": {} + }, + { + "id": "19b080ff-fd1a-4367-bc42-8b9ecf393f50", + "name": "realm-admin", + "description": "${role_realm-admin}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-clients", + "query-realms", + "manage-users", + "manage-authorization", + "manage-events", + "manage-realm", + "query-users", + "manage-clients", + "view-authorization", + "view-users", + "view-events", + "view-identity-providers", + "view-realm", + "query-groups", + "impersonation", + "view-clients", + "manage-identity-providers", + "create-client" + ] + } + }, + "clientRole": true, + "containerId": "053aaef6-efaf-4476-8e44-9ff636bca0ee", + "attributes": {} + }, + { + "id": "72101910-9c7d-47d9-90b0-a44771175c6f", + "name": "query-users", + "description": "${role_query-users}", + "composite": false, + "clientRole": true, + "containerId": "053aaef6-efaf-4476-8e44-9ff636bca0ee", + "attributes": {} + }, + { + "id": "48eb2bed-3d94-4057-bf1b-d98a5b319c6f", + "name": "manage-clients", + "description": "${role_manage-clients}", + "composite": false, + "clientRole": true, + "containerId": "053aaef6-efaf-4476-8e44-9ff636bca0ee", + "attributes": {} + }, + { + "id": "4f6cc18b-cce2-4c8a-9453-fb5239b1b26f", + "name": "view-authorization", + "description": "${role_view-authorization}", + "composite": false, + "clientRole": true, + "containerId": "053aaef6-efaf-4476-8e44-9ff636bca0ee", + "attributes": {} + }, + { + "id": "7be5e463-7a8e-414b-98b0-69771d65f8c7", + "name": "view-users", + "description": "${role_view-users}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-users", + "query-groups" + ] + } + }, + "clientRole": true, + "containerId": "053aaef6-efaf-4476-8e44-9ff636bca0ee", + "attributes": {} + }, + { + "id": "cb812bc2-0419-4413-8034-698f35626e1c", + "name": "view-events", + "description": "${role_view-events}", + "composite": false, + "clientRole": true, + "containerId": "053aaef6-efaf-4476-8e44-9ff636bca0ee", + "attributes": {} + }, + { + "id": "35a8e226-7826-43de-828f-0b32dc26c23e", + "name": "view-identity-providers", + "description": "${role_view-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "053aaef6-efaf-4476-8e44-9ff636bca0ee", + "attributes": {} + }, + { + "id": "0bea7967-ecea-491b-9d7d-b30f2a114d93", + "name": "view-realm", + "description": "${role_view-realm}", + "composite": false, + "clientRole": true, + "containerId": "053aaef6-efaf-4476-8e44-9ff636bca0ee", + "attributes": {} + }, + { + "id": "f1726ab1-6f8a-4fb7-bd6c-b47f3298b33f", + "name": "query-groups", + "description": "${role_query-groups}", + "composite": false, + "clientRole": true, + "containerId": "053aaef6-efaf-4476-8e44-9ff636bca0ee", + "attributes": {} + }, + { + "id": "91350e61-07ca-415d-81cf-5aee7feee424", + "name": "impersonation", + "description": "${role_impersonation}", + "composite": false, + "clientRole": true, + "containerId": "053aaef6-efaf-4476-8e44-9ff636bca0ee", + "attributes": {} + }, + { + "id": "7fe63cdd-ddb7-469c-a3a4-aad99ab0daee", + "name": "view-clients", + "description": "${role_view-clients}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-clients" + ] + } + }, + "clientRole": true, + "containerId": "053aaef6-efaf-4476-8e44-9ff636bca0ee", + "attributes": {} + }, + { + "id": "c89edab4-8493-42be-86f7-0c266255e0a5", + "name": "create-client", + "description": "${role_create-client}", + "composite": false, + "clientRole": true, + "containerId": "053aaef6-efaf-4476-8e44-9ff636bca0ee", + "attributes": {} + }, + { + "id": "95981782-1fd1-4d21-9c75-2bdc0e98b537", + "name": "manage-identity-providers", + "description": "${role_manage-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "053aaef6-efaf-4476-8e44-9ff636bca0ee", + "attributes": {} + } + ], + "fhir-client": [], + "security-admin-console": [], + "admin-cli": [], + "account-console": [], + "broker": [ + { + "id": "eb183ca5-4063-4b67-8ecf-c4b6c36eac28", + "name": "read-token", + "description": "${role_read-token}", + "composite": false, + "clientRole": true, + "containerId": "80204967-91e3-45bd-b185-84c446e579e6", + "attributes": {} + } + ], + "account": [ + { + "id": "b94ab092-a4fc-4949-96b3-64d1a3752593", + "name": "manage-account", + "description": "${role_manage-account}", + "composite": true, + "composites": { + "client": { + "account": [ + "manage-account-links" + ] + } + }, + "clientRole": true, + "containerId": "5dc2cca1-7475-4dbd-952a-c014f2155618", + "attributes": {} + }, + { + "id": "10369923-33de-482c-be22-17477984a871", + "name": "view-groups", + "description": "${role_view-groups}", + "composite": false, + "clientRole": true, + "containerId": "5dc2cca1-7475-4dbd-952a-c014f2155618", + "attributes": {} + }, + { + "id": "4fc6dae7-b949-4667-ab12-58a27ddff0b5", + "name": "view-profile", + "description": "${role_view-profile}", + "composite": false, + "clientRole": true, + "containerId": "5dc2cca1-7475-4dbd-952a-c014f2155618", + "attributes": {} + }, + { + "id": "b4697784-87fa-4223-9865-73fd032970f7", + "name": "manage-account-links", + "description": "${role_manage-account-links}", + "composite": false, + "clientRole": true, + "containerId": "5dc2cca1-7475-4dbd-952a-c014f2155618", + "attributes": {} + }, + { + "id": "a6148a40-bc1a-4e6b-b1bd-e42752a5db64", + "name": "view-applications", + "description": "${role_view-applications}", + "composite": false, + "clientRole": true, + "containerId": "5dc2cca1-7475-4dbd-952a-c014f2155618", + "attributes": {} + }, + { + "id": "1931fa23-be1e-4730-abf1-ebe98888c158", + "name": "view-consent", + "description": "${role_view-consent}", + "composite": false, + "clientRole": true, + "containerId": "5dc2cca1-7475-4dbd-952a-c014f2155618", + "attributes": {} + }, + { + "id": "04e6ae03-998d-4288-a8bf-0b3a5b49cc60", + "name": "manage-consent", + "description": "${role_manage-consent}", + "composite": true, + "composites": { + "client": { + "account": [ + "view-consent" + ] + } + }, + "clientRole": true, + "containerId": "5dc2cca1-7475-4dbd-952a-c014f2155618", + "attributes": {} + }, + { + "id": "c5e91760-e529-4638-b935-422513b47f0a", + "name": "delete-account", + "description": "${role_delete-account}", + "composite": false, + "clientRole": true, + "containerId": "5dc2cca1-7475-4dbd-952a-c014f2155618", + "attributes": {} + } + ] + } + }, + "groups": [], + "defaultRole": { + "id": "c981b14b-d83b-4202-96d1-5e85d94148ba", + "name": "default-roles-fhir", + "description": "${role_default-roles}", + "composite": true, + "clientRole": false, + "containerId": "5b7241af-b43c-4af1-a0b5-4957e338204b" + }, + "requiredCredentials": [ + "password" + ], + "otpPolicyType": "totp", + "otpPolicyAlgorithm": "HmacSHA1", + "otpPolicyInitialCounter": 0, + "otpPolicyDigits": 6, + "otpPolicyLookAheadWindow": 1, + "otpPolicyPeriod": 30, + "otpPolicyCodeReusable": false, + "otpSupportedApplications": [ + "totpAppFreeOTPName", + "totpAppGoogleName", + "totpAppMicrosoftAuthenticatorName" + ], + "localizationTexts": {}, + "webAuthnPolicyRpEntityName": "keycloak", + "webAuthnPolicySignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyRpId": "", + "webAuthnPolicyAttestationConveyancePreference": "not specified", + "webAuthnPolicyAuthenticatorAttachment": "not specified", + "webAuthnPolicyRequireResidentKey": "not specified", + "webAuthnPolicyUserVerificationRequirement": "not specified", + "webAuthnPolicyCreateTimeout": 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyAcceptableAaguids": [], + "webAuthnPolicyExtraOrigins": [], + "webAuthnPolicyPasswordlessRpEntityName": "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyPasswordlessRpId": "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified", + "webAuthnPolicyPasswordlessCreateTimeout": 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyPasswordlessAcceptableAaguids": [], + "webAuthnPolicyPasswordlessExtraOrigins": [], + "users": [ + { + "id": "cf86adcd-3226-41ff-8cca-1bb3ac7528c9", + "username": "service-account-fhir-client", + "emailVerified": false, + "createdTimestamp": 1727293066268, + "enabled": true, + "totp": false, + "serviceAccountClientId": "fhir-client", + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": [ + "default-roles-fhir" + ], + "notBefore": 0, + "groups": [] + } + ], + "scopeMappings": [ + { + "clientScope": "offline_access", + "roles": [ + "offline_access" + ] + } + ], + "clientScopeMappings": { + "account": [ + { + "client": "account-console", + "roles": [ + "manage-account", + "view-groups" + ] + } + ] + }, + "clients": [ + { + "id": "5dc2cca1-7475-4dbd-952a-c014f2155618", + "clientId": "account", + "name": "${client_account}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/fhir/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/fhir/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "1610c132-b257-4c68-b559-fd294af55439", + "clientId": "account-console", + "name": "${client_account-console}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/fhir/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/fhir/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+", + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "9c234624-1f00-4986-adcc-a35e2c702a34", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "53a4a677-3c12-43ed-90ba-4cc52ea4852f", + "clientId": "admin-cli", + "name": "${client_admin-cli}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "80204967-91e3-45bd-b185-84c446e579e6", + "clientId": "broker", + "name": "${client_broker}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "5dd8af46-357c-4465-bac2-61a1f8b58e85", + "clientId": "fhir-client", + "name": "", + "description": "", + "rootUrl": "", + "adminUrl": "", + "baseUrl": "", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "ibz2lEXdGe02CJS2N7tY13r6664eJpoX", + "redirectUris": [ + "/*" + ], + "webOrigins": [ + "/*" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": true, + "publicClient": false, + "frontchannelLogout": true, + "protocol": "openid-connect", + "attributes": { + "oidc.ciba.grant.enabled": "false", + "client.secret.creation.time": "1727294203", + "backchannel.logout.session.required": "true", + "post.logout.redirect.uris": "+", + "oauth2.device.authorization.grant.enabled": "false", + "backchannel.logout.revoke.offline.tokens": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "protocolMappers": [ + { + "id": "46ddc55e-8b39-4058-ac28-2be16bbb7af9", + "name": "Client IP Address", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientAddress", + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientAddress", + "jsonType.label": "String" + } + }, + { + "id": "7a42157b-b31e-4869-890b-db00db5a4946", + "name": "Client ID", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "client_id", + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "client_id", + "jsonType.label": "String" + } + }, + { + "id": "7724ecd5-476f-4587-b7a1-94c949f3e52a", + "name": "Civil Registry Client Audience", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-mapper", + "consentRequired": false, + "config": { + "included.client.audience": "account", + "id.token.claim": "false", + "lightweight.claim": "false", + "introspection.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "false" + } + }, + { + "id": "e65fc9b3-e195-4c3d-b789-52df5c391e14", + "name": "Client Host", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientHost", + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientHost", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "fhir-audience", + "profile", + "roles", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "053aaef6-efaf-4476-8e44-9ff636bca0ee", + "clientId": "realm-management", + "name": "${client_realm-management}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "b7fc9ace-5d82-45f4-99fa-6013b4827a2f", + "clientId": "security-admin-console", + "name": "${client_security-admin-console}", + "rootUrl": "${authAdminUrl}", + "baseUrl": "/admin/fhir/console/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/admin/fhir/console/*" + ], + "webOrigins": [ + "+" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+", + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "4602113d-8b8f-45c0-856e-51845e59079a", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + } + ], + "clientScopes": [ + { + "id": "fb5fc8f6-f6db-436a-ae24-9fd71e19c473", + "name": "microprofile-jwt", + "description": "Microprofile - JWT built-in scope", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "06909c65-839f-4c9b-9cfd-9f4b0fa1a24a", + "name": "groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "multivalued": "true", + "userinfo.token.claim": "true", + "user.attribute": "foo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "groups", + "jsonType.label": "String" + } + }, + { + "id": "8437a24b-5c11-4d90-838e-9bebd37619b3", + "name": "upn", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "upn", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "8bd05efb-9849-4e20-b3b2-0c30c7a6587b", + "name": "basic", + "description": "OpenID Connect scope for add all basic claims to the token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "f0b79939-8f2b-4444-9df5-1104ddd2106b", + "name": "sub", + "protocol": "openid-connect", + "protocolMapper": "oidc-sub-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "access.token.claim": "true" + } + }, + { + "id": "c25a0e1d-87db-48e0-8b28-0d55b07791d5", + "name": "auth_time", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "AUTH_TIME", + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "auth_time", + "jsonType.label": "long" + } + } + ] + }, + { + "id": "521f1e88-a0f9-431c-949e-33e5189b0b73", + "name": "phone", + "description": "OpenID Connect built-in scope: phone", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "consent.screen.text": "${phoneScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "b63382dd-dcc9-40e1-9ccf-5ea74521fb62", + "name": "phone number", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "phoneNumber", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number", + "jsonType.label": "String" + } + }, + { + "id": "ec1a8dbd-41d1-4c8c-83b6-0e45026f8160", + "name": "phone number verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "phoneNumberVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number_verified", + "jsonType.label": "boolean" + } + } + ] + }, + { + "id": "0a798206-469f-4fbd-8aab-138d387e8227", + "name": "offline_access", + "description": "OpenID Connect built-in scope: offline_access", + "protocol": "openid-connect", + "attributes": { + "consent.screen.text": "${offlineAccessScopeConsentText}", + "display.on.consent.screen": "true" + } + }, + { + "id": "91ab111b-6420-4d4e-90bd-1ce94a07ea0d", + "name": "role_list", + "description": "SAML role list", + "protocol": "saml", + "attributes": { + "consent.screen.text": "${samlRoleListScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "19a6907c-3e5a-498a-9d71-864086509adc", + "name": "role list", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "single": "false", + "attribute.nameformat": "Basic", + "attribute.name": "Role" + } + } + ] + }, + { + "id": "f2f5f946-e84f-460a-a6dd-53d558f40f4c", + "name": "address", + "description": "OpenID Connect built-in scope: address", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "consent.screen.text": "${addressScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "1da014d2-4564-45ca-9919-7864a100d68f", + "name": "address", + "protocol": "openid-connect", + "protocolMapper": "oidc-address-mapper", + "consentRequired": false, + "config": { + "user.attribute.formatted": "formatted", + "user.attribute.country": "country", + "introspection.token.claim": "true", + "user.attribute.postal_code": "postal_code", + "userinfo.token.claim": "true", + "user.attribute.street": "street", + "id.token.claim": "true", + "user.attribute.region": "region", + "access.token.claim": "true", + "user.attribute.locality": "locality" + } + } + ] + }, + { + "id": "c742ac75-dff0-4ef1-887e-1623e597848d", + "name": "email", + "description": "OpenID Connect built-in scope: email", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "consent.screen.text": "${emailScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "f7f08260-6f73-48cd-ac40-d86a9acfa4ae", + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "jsonType.label": "String" + } + }, + { + "id": "001d5196-fadd-40e0-b7ef-dc6449c1554d", + "name": "email verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "emailVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email_verified", + "jsonType.label": "boolean" + } + } + ] + }, + { + "id": "c8333467-3f25-4cb8-8428-9ad2a4f524e6", + "name": "web-origins", + "description": "OpenID Connect scope for add allowed web origins to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "consent.screen.text": "", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "0516d248-ff78-497f-9950-e2dd51e49f20", + "name": "allowed web origins", + "protocol": "openid-connect", + "protocolMapper": "oidc-allowed-origins-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "access.token.claim": "true" + } + } + ] + }, + { + "id": "63a2427b-cb2f-42ab-a5e1-95fff9d1b929", + "name": "profile", + "description": "OpenID Connect built-in scope: profile", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "consent.screen.text": "${profileScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "b489c837-8a00-4d98-9c0b-c67f88d716a8", + "name": "picture", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "picture", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "picture", + "jsonType.label": "String" + } + }, + { + "id": "06de74ae-c804-4293-adb7-f9c5651d7388", + "name": "full name", + "protocol": "openid-connect", + "protocolMapper": "oidc-full-name-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + }, + { + "id": "c6376a34-ff0b-419f-a767-4f1e0f54fc65", + "name": "family name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "lastName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "family_name", + "jsonType.label": "String" + } + }, + { + "id": "72278e67-d71c-40cb-b8d6-ce28ab8d4852", + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "preferred_username", + "jsonType.label": "String" + } + }, + { + "id": "d21feac9-1a94-4039-a034-94a6c937c2d1", + "name": "profile", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "profile", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "profile", + "jsonType.label": "String" + } + }, + { + "id": "d5640f9f-2048-487b-8122-f245d681fdd4", + "name": "gender", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "gender", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "gender", + "jsonType.label": "String" + } + }, + { + "id": "2d2098ef-ab2e-4878-9b4d-51500d3a9a03", + "name": "nickname", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "nickname", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "nickname", + "jsonType.label": "String" + } + }, + { + "id": "aa8ba9a3-9cd4-4466-9ccd-bd4d85085934", + "name": "zoneinfo", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "zoneinfo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "zoneinfo", + "jsonType.label": "String" + } + }, + { + "id": "ed481800-5ed8-45de-aa2d-94e4f6992e11", + "name": "middle name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "middleName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "middle_name", + "jsonType.label": "String" + } + }, + { + "id": "fd95b628-aa43-4435-990b-120b2f04d9ce", + "name": "updated at", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "updatedAt", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "updated_at", + "jsonType.label": "long" + } + }, + { + "id": "ae14f5ae-7e04-4420-a86d-6da0dfaa7a0a", + "name": "birthdate", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "birthdate", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "birthdate", + "jsonType.label": "String" + } + }, + { + "id": "abf9c579-2e25-4929-b3e9-b0904edc9425", + "name": "given name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "firstName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "given_name", + "jsonType.label": "String" + } + }, + { + "id": "a59f9b6c-b23e-4331-8e0e-87a76589cd3d", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + }, + { + "id": "a245418b-6167-4d40-9f14-e8e907f5a681", + "name": "website", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "website", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "website", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "907d2274-aee0-4a09-b3d7-821df60c100c", + "name": "roles", + "description": "OpenID Connect scope for add user roles to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "consent.screen.text": "${rolesScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "cf411b7e-6e14-41d4-b4ef-6ed3bb1507f6", + "name": "realm roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "introspection.token.claim": "true", + "access.token.claim": "true", + "claim.name": "realm_access.roles", + "jsonType.label": "String", + "multivalued": "true" + } + }, + { + "id": "4f3bc3dc-4796-4018-8a08-285f70a688b5", + "name": "client roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-client-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "introspection.token.claim": "true", + "access.token.claim": "true", + "claim.name": "resource_access.${client_id}.roles", + "jsonType.label": "String", + "multivalued": "true" + } + }, + { + "id": "42e550e2-03d6-4fb6-8d45-1a5fe12e6367", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "access.token.claim": "true" + } + } + ] + }, + { + "id": "ef2dd352-833d-4e4c-ae93-66d3a7148d04", + "name": "acr", + "description": "OpenID Connect scope for add acr (authentication context class reference) to the token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "5518101c-965a-4db2-89f3-7896904c517b", + "name": "acr loa level", + "protocol": "openid-connect", + "protocolMapper": "oidc-acr-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + } + ] + }, + { + "id": "ddf2f0d5-38d8-42d4-be2f-73ca35b7075a", + "name": "fhir-audience", + "description": "", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "true", + "gui.order": "", + "consent.screen.text": "" + }, + "protocolMappers": [ + { + "id": "7ee31ab6-b0b4-408c-86ae-83f254fde9c5", + "name": "fhir", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-mapper", + "consentRequired": false, + "config": { + "included.client.audience": "fhir-client", + "id.token.claim": "false", + "lightweight.claim": "false", + "access.token.claim": "true", + "introspection.token.claim": "true" + } + } + ] + } + ], + "defaultDefaultClientScopes": [ + "role_list", + "profile", + "email", + "roles", + "web-origins", + "acr", + "basic" + ], + "defaultOptionalClientScopes": [ + "offline_access", + "address", + "phone", + "microprofile-jwt" + ], + "browserSecurityHeaders": { + "contentSecurityPolicyReportOnly": "", + "xContentTypeOptions": "nosniff", + "referrerPolicy": "no-referrer", + "xRobotsTag": "none", + "xFrameOptions": "SAMEORIGIN", + "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "xXSSProtection": "1; mode=block", + "strictTransportSecurity": "max-age=31536000; includeSubDomains" + }, + "smtpServer": {}, + "eventsEnabled": false, + "eventsListeners": [ + "jboss-logging" + ], + "enabledEventTypes": [], + "adminEventsEnabled": false, + "adminEventsDetailsEnabled": false, + "identityProviders": [], + "identityProviderMappers": [], + "components": { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ + { + "id": "e58be7bc-daad-44c6-a71b-fb69334b935e", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "saml-user-attribute-mapper", + "oidc-full-name-mapper", + "saml-role-list-mapper", + "oidc-address-mapper", + "saml-user-property-mapper", + "oidc-usermodel-property-mapper", + "oidc-sha256-pairwise-sub-mapper", + "oidc-usermodel-attribute-mapper" + ] + } + }, + { + "id": "f4c77166-4735-4707-9f08-e6dd3ec619bc", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + }, + { + "id": "8d693388-c608-48b6-b0bd-70961b4c4d81", + "name": "Max Clients Limit", + "providerId": "max-clients", + "subType": "anonymous", + "subComponents": {}, + "config": { + "max-clients": [ + "200" + ] + } + }, + { + "id": "55355863-d849-4b0a-b086-5199262fc019", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "oidc-address-mapper", + "saml-role-list-mapper", + "oidc-full-name-mapper", + "oidc-sha256-pairwise-sub-mapper", + "saml-user-property-mapper", + "oidc-usermodel-attribute-mapper", + "saml-user-attribute-mapper", + "oidc-usermodel-property-mapper" + ] + } + }, + { + "id": "7a870337-eaf7-42d4-93ed-49c17941a2f8", + "name": "Trusted Hosts", + "providerId": "trusted-hosts", + "subType": "anonymous", + "subComponents": {}, + "config": { + "host-sending-registration-request-must-match": [ + "true" + ], + "client-uris-must-match": [ + "true" + ] + } + }, + { + "id": "7d1071c1-f58f-4836-b19a-ac97637547ab", + "name": "Consent Required", + "providerId": "consent-required", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "c13ef89e-3940-4ff6-afeb-c454a2df1165", + "name": "Full Scope Disabled", + "providerId": "scope", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "86bea299-c7e2-48b3-9506-0ec87ad4eee8", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + } + ], + "org.keycloak.keys.KeyProvider": [ + { + "id": "c6abbfce-6c1a-4f08-ab68-cb579aeb5937", + "name": "hmac-generated-hs512", + "providerId": "hmac-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ], + "algorithm": [ + "HS512" + ] + } + }, + { + "id": "9125cd82-0f06-44d8-9d4b-6bb8c2cd24ce", + "name": "aes-generated", + "providerId": "aes-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ] + } + }, + { + "id": "105e09bd-32a0-476b-b8d3-abd06ad03d7a", + "name": "rsa-enc-generated", + "providerId": "rsa-enc-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ], + "algorithm": [ + "RSA-OAEP" + ] + } + }, + { + "id": "9cbf6bc5-1c48-4c56-b165-66ba58b5285e", + "name": "rsa-generated", + "providerId": "rsa-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ] + } + } + ] + }, + "internationalizationEnabled": false, + "supportedLocales": [], + "authenticationFlows": [ + { + "id": "0402a30b-c89d-4f9c-abff-b7960df27651", + "alias": "Account verification options", + "description": "Method with which to verity the existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-email-verification", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Verify Existing Account by Re-authentication", + "userSetupAllowed": false + } + ] + }, + { + "id": "09eb8eab-e2bc-4ac3-870e-d1a711179a6e", + "alias": "Browser - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "52f8e039-5205-44dd-b129-bf27f029e8df", + "alias": "Direct Grant - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "fb754def-7063-4936-bac4-c9ff326ad549", + "alias": "First broker login - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "1c046ae8-7732-4f12-ab80-9cde1bcd1417", + "alias": "Handle Existing Account", + "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-confirm-link", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Account verification options", + "userSetupAllowed": false + } + ] + }, + { + "id": "f19fdda8-dcbc-47d1-9f5c-150e52eca530", + "alias": "Reset - Conditional OTP", + "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "3e1efae3-b231-4f9f-ac49-c1f4c89d5252", + "alias": "User creation or linking", + "description": "Flow for the existing/non-existing user alternatives", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "create unique user config", + "authenticator": "idp-create-user-if-unique", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Handle Existing Account", + "userSetupAllowed": false + } + ] + }, + { + "id": "c2515545-288a-46b6-b352-b784181281b6", + "alias": "Verify Existing Account by Re-authentication", + "description": "Reauthentication of existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "First broker login - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "96557b7e-7840-411c-ac24-91fa52c7e91d", + "alias": "browser", + "description": "browser based authentication", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-cookie", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "identity-provider-redirector", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 25, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "forms", + "userSetupAllowed": false + } + ] + }, + { + "id": "07b57d0b-383e-44b8-83df-4086b9cc1206", + "alias": "clients", + "description": "Base authentication for clients", + "providerId": "client-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "client-secret", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-secret-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-x509", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 40, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "49c49b0a-15d8-4391-a1dc-c86b0ec4eef6", + "alias": "direct grant", + "description": "OpenID Connect Resource Owner Grant", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "direct-grant-validate-username", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "Direct Grant - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "74d7a0a1-90d5-4152-9455-80230591ad52", + "alias": "docker auth", + "description": "Used by Docker clients to authenticate against the IDP", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "docker-http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "c96aae6c-29ec-4c80-bd2e-2d9ac0d93b10", + "alias": "first broker login", + "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "review profile config", + "authenticator": "idp-review-profile", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "User creation or linking", + "userSetupAllowed": false + } + ] + }, + { + "id": "85826cc9-7632-4423-8fcd-0a9ff199d969", + "alias": "forms", + "description": "Username, password, otp and other auth forms.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Browser - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "b7d15349-e9b8-4ded-9078-5b821b1acb8a", + "alias": "registration", + "description": "registration flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-page-form", + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": true, + "flowAlias": "registration form", + "userSetupAllowed": false + } + ] + }, + { + "id": "a66e746e-100f-4b87-967f-76d826d8ed51", + "alias": "registration form", + "description": "registration form", + "providerId": "form-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-user-creation", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-password-action", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 50, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-recaptcha-action", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 60, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-terms-and-conditions", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 70, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "8c189524-45d9-4d82-b40d-444aa4f6999d", + "alias": "reset credentials", + "description": "Reset credentials for a user if they forgot their password or something", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "reset-credentials-choose-user", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-credential-email", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 40, + "autheticatorFlow": true, + "flowAlias": "Reset - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "33bfe166-85e9-450c-bdf9-fff3cc2c45eb", + "alias": "saml ecp", + "description": "SAML ECP Profile Authentication Flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + } + ], + "authenticatorConfig": [ + { + "id": "34cc044b-509d-4094-b41c-edab122b997d", + "alias": "create unique user config", + "config": { + "require.password.update.after.registration": "false" + } + }, + { + "id": "5df6a107-29ee-4de0-90e7-74a9eb4175c7", + "alias": "review profile config", + "config": { + "update.profile.on.first.login": "missing" + } + } + ], + "requiredActions": [ + { + "alias": "CONFIGURE_TOTP", + "name": "Configure OTP", + "providerId": "CONFIGURE_TOTP", + "enabled": true, + "defaultAction": false, + "priority": 10, + "config": {} + }, + { + "alias": "TERMS_AND_CONDITIONS", + "name": "Terms and Conditions", + "providerId": "TERMS_AND_CONDITIONS", + "enabled": false, + "defaultAction": false, + "priority": 20, + "config": {} + }, + { + "alias": "UPDATE_PASSWORD", + "name": "Update Password", + "providerId": "UPDATE_PASSWORD", + "enabled": true, + "defaultAction": false, + "priority": 30, + "config": {} + }, + { + "alias": "UPDATE_PROFILE", + "name": "Update Profile", + "providerId": "UPDATE_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 40, + "config": {} + }, + { + "alias": "VERIFY_EMAIL", + "name": "Verify Email", + "providerId": "VERIFY_EMAIL", + "enabled": true, + "defaultAction": false, + "priority": 50, + "config": {} + }, + { + "alias": "delete_account", + "name": "Delete Account", + "providerId": "delete_account", + "enabled": false, + "defaultAction": false, + "priority": 60, + "config": {} + }, + { + "alias": "webauthn-register", + "name": "Webauthn Register", + "providerId": "webauthn-register", + "enabled": true, + "defaultAction": false, + "priority": 70, + "config": {} + }, + { + "alias": "webauthn-register-passwordless", + "name": "Webauthn Register Passwordless", + "providerId": "webauthn-register-passwordless", + "enabled": true, + "defaultAction": false, + "priority": 80, + "config": {} + }, + { + "alias": "VERIFY_PROFILE", + "name": "Verify Profile", + "providerId": "VERIFY_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 90, + "config": {} + }, + { + "alias": "delete_credential", + "name": "Delete Credential", + "providerId": "delete_credential", + "enabled": true, + "defaultAction": false, + "priority": 100, + "config": {} + }, + { + "alias": "update_user_locale", + "name": "Update User Locale", + "providerId": "update_user_locale", + "enabled": true, + "defaultAction": false, + "priority": 1000, + "config": {} + } + ], + "browserFlow": "browser", + "registrationFlow": "registration", + "directGrantFlow": "direct grant", + "resetCredentialsFlow": "reset credentials", + "clientAuthenticationFlow": "clients", + "dockerAuthenticationFlow": "docker auth", + "firstBrokerLoginFlow": "first broker login", + "attributes": { + "cibaBackchannelTokenDeliveryMode": "poll", + "cibaAuthRequestedUserHint": "login_hint", + "clientOfflineSessionMaxLifespan": "0", + "oauth2DevicePollingInterval": "5", + "clientSessionIdleTimeout": "0", + "clientOfflineSessionIdleTimeout": "0", + "cibaInterval": "5", + "realmReusableOtpCode": "false", + "cibaExpiresIn": "120", + "oauth2DeviceCodeLifespan": "600", + "parRequestUriLifespan": "60", + "clientSessionMaxLifespan": "0", + "organizationsEnabled": "false" + }, + "keycloakVersion": "25.0.6", + "userManagedAccessAllowed": false, + "organizationsEnabled": false, + "clientProfiles": { + "profiles": [] + }, + "clientPolicies": { + "policies": [] + } +} \ No newline at end of file diff --git a/dhis2-to-fhir-patient-bundle-datasonnet/config/dhis2/dhis.conf b/dhis2-to-fhir-patient-bundle-datasonnet/config/dhis2/dhis.conf new file mode 100644 index 0000000..3e49059 --- /dev/null +++ b/dhis2-to-fhir-patient-bundle-datasonnet/config/dhis2/dhis.conf @@ -0,0 +1,5 @@ +connection.dialect = org.hibernate.dialect.PostgreSQLDialect +connection.driver_class = org.postgresql.Driver +connection.url = jdbc:postgresql://dhis2-db:5432/dhis +connection.username = dhis +connection.password = dhis diff --git a/dhis2-to-fhir-patient-bundle-datasonnet/config/dhis2/nginx.conf b/dhis2-to-fhir-patient-bundle-datasonnet/config/dhis2/nginx.conf new file mode 100644 index 0000000..5e2f64b --- /dev/null +++ b/dhis2-to-fhir-patient-bundle-datasonnet/config/dhis2/nginx.conf @@ -0,0 +1,33 @@ +worker_processes 1; + +events { + worker_connections 1024; +} + +http { + upstream dhis2 { + server dhis2:8080 max_fails=3 fail_timeout=60s; + } + + gzip on; + gzip_types + "application/json;charset=utf-8" application/json + "application/javascript;charset=utf-8" application/javascript text/javascript + "application/xml;charset=utf-8" application/xml text/xml + "text/css;charset=utf-8" text/css + "text/plain;charset=utf-8" text/plain; + + server { + listen 80; + + client_max_body_size 10m; + + location / { + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_pass http://dhis2; + } + } +} \ No newline at end of file diff --git a/dhis2-to-fhir-patient-bundle-datasonnet/config/dhis2/postgresql.conf b/dhis2-to-fhir-patient-bundle-datasonnet/config/dhis2/postgresql.conf new file mode 100644 index 0000000..4ea7234 --- /dev/null +++ b/dhis2-to-fhir-patient-bundle-datasonnet/config/dhis2/postgresql.conf @@ -0,0 +1,751 @@ +# ----------------------------- +# PostgreSQL configuration file +# ----------------------------- +# +# This file consists of lines of the form: +# +# name = value +# +# (The "=" is optional.) Whitespace may be used. Comments are introduced with +# "#" anywhere on a line. The complete list of parameter names and allowed +# values can be found in the PostgreSQL documentation. +# +# The commented-out settings shown in this file represent the default values. +# Re-commenting a setting is NOT sufficient to revert it to the default value; +# you need to reload the server. +# +# This file is read on server startup and when the server receives a SIGHUP +# signal. If you edit the file on a running system, you have to SIGHUP the +# server for the changes to take effect, run "pg_ctl reload", or execute +# "SELECT pg_reload_conf()". Some parameters, which are marked below, +# require a server shutdown and restart to take effect. +# +# Any parameter can also be given as a command-line option to the server, e.g., +# "postgres -c log_connections=on". Some parameters can be changed at run time +# with the "SET" SQL command. +# +# Memory units: B = bytes Time units: us = microseconds +# kB = kilobytes ms = milliseconds +# MB = megabytes s = seconds +# GB = gigabytes min = minutes +# TB = terabytes h = hours +# d = days + + +#------------------------------------------------------------------------------ +# FILE LOCATIONS +#------------------------------------------------------------------------------ + +# The default values of these variables are driven from the -D command-line +# option or PGDATA environment variable, represented here as ConfigDir. + +#data_directory = 'ConfigDir' # use data in another directory + # (change requires restart) +#hba_file = 'ConfigDir/pg_hba.conf' # host-based authentication file + # (change requires restart) +#ident_file = 'ConfigDir/pg_ident.conf' # ident configuration file + # (change requires restart) + +# If external_pid_file is not explicitly set, no extra PID file is written. +#external_pid_file = '' # write an extra PID file + # (change requires restart) + + +#------------------------------------------------------------------------------ +# CONNECTIONS AND AUTHENTICATION +#------------------------------------------------------------------------------ + +# - Connection Settings - + +listen_addresses = '*' + # comma-separated list of addresses; + # defaults to 'localhost'; use '*' for all + # (change requires restart) +#port = 5432 # (change requires restart) +max_connections = 100 # (change requires restart) +#superuser_reserved_connections = 3 # (change requires restart) +#unix_socket_directories = '/var/run/postgresql' # comma-separated list of directories + # (change requires restart) +#unix_socket_group = '' # (change requires restart) +#unix_socket_permissions = 0777 # begin with 0 to use octal notation + # (change requires restart) +#bonjour = off # advertise server via Bonjour + # (change requires restart) +#bonjour_name = '' # defaults to the computer name + # (change requires restart) + +# - TCP settings - +# see "man 7 tcp" for details + +#tcp_keepalives_idle = 0 # TCP_KEEPIDLE, in seconds; + # 0 selects the system default +#tcp_keepalives_interval = 0 # TCP_KEEPINTVL, in seconds; + # 0 selects the system default +#tcp_keepalives_count = 0 # TCP_KEEPCNT; + # 0 selects the system default +#tcp_user_timeout = 0 # TCP_USER_TIMEOUT, in milliseconds; + # 0 selects the system default + +# - Authentication - + +#authentication_timeout = 1min # 1s-600s +#password_encryption = md5 # md5 or scram-sha-256 +#db_user_namespace = off + +# GSSAPI using Kerberos +#krb_server_keyfile = 'FILE:${sysconfdir}/krb5.keytab' +#krb_caseins_users = off + +# - SSL - + +#ssl = off +#ssl_ca_file = '' +#ssl_cert_file = 'server.crt' +#ssl_crl_file = '' +#ssl_key_file = 'server.key' +#ssl_ciphers = 'HIGH:MEDIUM:+3DES:!aNULL' # allowed SSL ciphers +#ssl_prefer_server_ciphers = on +#ssl_ecdh_curve = 'prime256v1' +#ssl_min_protocol_version = 'TLSv1' +#ssl_max_protocol_version = '' +#ssl_dh_params_file = '' +#ssl_passphrase_command = '' +#ssl_passphrase_command_supports_reload = off + + +#------------------------------------------------------------------------------ +# RESOURCE USAGE (except WAL) +#------------------------------------------------------------------------------ + +# - Memory - + +shared_buffers = 128MB # min 128kB + # (change requires restart) +#huge_pages = try # on, off, or try + # (change requires restart) +#temp_buffers = 8MB # min 800kB +#max_prepared_transactions = 0 # zero disables the feature + # (change requires restart) +# Caution: it is not advisable to set max_prepared_transactions nonzero unless +# you actively intend to use prepared transactions. +#work_mem = 4MB # min 64kB +#maintenance_work_mem = 64MB # min 1MB +#autovacuum_work_mem = -1 # min 1MB, or -1 to use maintenance_work_mem +#max_stack_depth = 2MB # min 100kB +#shared_memory_type = mmap # the default is the first option + # supported by the operating system: + # mmap + # sysv + # windows + # (change requires restart) +dynamic_shared_memory_type = posix # the default is the first option + # supported by the operating system: + # posix + # sysv + # windows + # mmap + # (change requires restart) + +# - Disk - + +#temp_file_limit = -1 # limits per-process temp file space + # in kB, or -1 for no limit + +# - Kernel Resources - + +#max_files_per_process = 1000 # min 25 + # (change requires restart) + +# - Cost-Based Vacuum Delay - + +#vacuum_cost_delay = 0 # 0-100 milliseconds (0 disables) +#vacuum_cost_page_hit = 1 # 0-10000 credits +#vacuum_cost_page_miss = 10 # 0-10000 credits +#vacuum_cost_page_dirty = 20 # 0-10000 credits +#vacuum_cost_limit = 200 # 1-10000 credits + +# - Background Writer - + +#bgwriter_delay = 200ms # 10-10000ms between rounds +#bgwriter_lru_maxpages = 100 # max buffers written/round, 0 disables +#bgwriter_lru_multiplier = 2.0 # 0-10.0 multiplier on buffers scanned/round +#bgwriter_flush_after = 512kB # measured in pages, 0 disables + +# - Asynchronous Behavior - + +#effective_io_concurrency = 1 # 1-1000; 0 disables prefetching +#max_worker_processes = 8 # (change requires restart) +#max_parallel_maintenance_workers = 2 # taken from max_parallel_workers +#max_parallel_workers_per_gather = 2 # taken from max_parallel_workers +#parallel_leader_participation = on +#max_parallel_workers = 8 # maximum number of max_worker_processes that + # can be used in parallel operations +#old_snapshot_threshold = -1 # 1min-60d; -1 disables; 0 is immediate + # (change requires restart) +#backend_flush_after = 0 # measured in pages, 0 disables + + +#------------------------------------------------------------------------------ +# WRITE-AHEAD LOG +#------------------------------------------------------------------------------ + +# - Settings - + +wal_level = logical # minimal, replica, or logical + # (change requires restart) +#fsync = on # flush data to disk for crash safety + # (turning this off can cause + # unrecoverable data corruption) +#synchronous_commit = on # synchronization level; + # off, local, remote_write, remote_apply, or on +#wal_sync_method = fsync # the default is the first option + # supported by the operating system: + # open_datasync + # fdatasync (default on Linux and FreeBSD) + # fsync + # fsync_writethrough + # open_sync +#full_page_writes = on # recover from partial page writes +#wal_compression = off # enable compression of full-page writes +#wal_log_hints = off # also do full page writes of non-critical updates + # (change requires restart) +#wal_init_zero = on # zero-fill new WAL files +#wal_recycle = on # recycle WAL files +#wal_buffers = -1 # min 32kB, -1 sets based on shared_buffers + # (change requires restart) +#wal_writer_delay = 200ms # 1-10000 milliseconds +#wal_writer_flush_after = 1MB # measured in pages, 0 disables + +#commit_delay = 0 # range 0-100000, in microseconds +#commit_siblings = 5 # range 1-1000 + +# - Checkpoints - + +#checkpoint_timeout = 5min # range 30s-1d +max_wal_size = 1GB +min_wal_size = 80MB +#checkpoint_completion_target = 0.5 # checkpoint target duration, 0.0 - 1.0 +#checkpoint_flush_after = 256kB # measured in pages, 0 disables +#checkpoint_warning = 30s # 0 disables + +# - Archiving - + +#archive_mode = off # enables archiving; off, on, or always + # (change requires restart) +#archive_command = '' # command to use to archive a logfile segment + # placeholders: %p = path of file to archive + # %f = file name only + # e.g. 'test ! -f /mnt/server/archivedir/%f && cp %p /mnt/server/archivedir/%f' +#archive_timeout = 0 # force a logfile segment switch after this + # number of seconds; 0 disables + +# - Archive Recovery - + +# These are only used in recovery mode. + +#restore_command = '' # command to use to restore an archived logfile segment + # placeholders: %p = path of file to restore + # %f = file name only + # e.g. 'cp /mnt/server/archivedir/%f %p' + # (change requires restart) +#archive_cleanup_command = '' # command to execute at every restartpoint +#recovery_end_command = '' # command to execute at completion of recovery + +# - Recovery Target - + +# Set these only when performing a targeted recovery. + +#recovery_target = '' # 'immediate' to end recovery as soon as a + # consistent state is reached + # (change requires restart) +#recovery_target_name = '' # the named restore point to which recovery will proceed + # (change requires restart) +#recovery_target_time = '' # the time stamp up to which recovery will proceed + # (change requires restart) +#recovery_target_xid = '' # the transaction ID up to which recovery will proceed + # (change requires restart) +#recovery_target_lsn = '' # the WAL LSN up to which recovery will proceed + # (change requires restart) +#recovery_target_inclusive = on # Specifies whether to stop: + # just after the specified recovery target (on) + # just before the recovery target (off) + # (change requires restart) +#recovery_target_timeline = 'latest' # 'current', 'latest', or timeline ID + # (change requires restart) +#recovery_target_action = 'pause' # 'pause', 'promote', 'shutdown' + # (change requires restart) + + +#------------------------------------------------------------------------------ +# REPLICATION +#------------------------------------------------------------------------------ + +# - Sending Servers - + +# Set these on the master and on any standby that will send replication data. + +#max_wal_senders = 10 # max number of walsender processes + # (change requires restart) +#wal_keep_segments = 0 # in logfile segments; 0 disables +#wal_sender_timeout = 60s # in milliseconds; 0 disables + +#max_replication_slots = 10 # max number of replication slots + # (change requires restart) +#track_commit_timestamp = off # collect timestamp of transaction commit + # (change requires restart) + +# - Master Server - + +# These settings are ignored on a standby server. + +#synchronous_standby_names = '' # standby servers that provide sync rep + # method to choose sync standbys, number of sync standbys, + # and comma-separated list of application_name + # from standby(s); '*' = all +#vacuum_defer_cleanup_age = 0 # number of xacts by which cleanup is delayed + +# - Standby Servers - + +# These settings are ignored on a master server. + +#primary_conninfo = '' # connection string to sending server + # (change requires restart) +#primary_slot_name = '' # replication slot on sending server + # (change requires restart) +#promote_trigger_file = '' # file name whose presence ends recovery +#hot_standby = on # "off" disallows queries during recovery + # (change requires restart) +#max_standby_archive_delay = 30s # max delay before canceling queries + # when reading WAL from archive; + # -1 allows indefinite delay +#max_standby_streaming_delay = 30s # max delay before canceling queries + # when reading streaming WAL; + # -1 allows indefinite delay +#wal_receiver_status_interval = 10s # send replies at least this often + # 0 disables +#hot_standby_feedback = off # send info from standby to prevent + # query conflicts +#wal_receiver_timeout = 60s # time that receiver waits for + # communication from master + # in milliseconds; 0 disables +#wal_retrieve_retry_interval = 5s # time to wait before retrying to + # retrieve WAL after a failed attempt +#recovery_min_apply_delay = 0 # minimum delay for applying changes during recovery + +# - Subscribers - + +# These settings are ignored on a publisher. + +#max_logical_replication_workers = 4 # taken from max_worker_processes + # (change requires restart) +#max_sync_workers_per_subscription = 2 # taken from max_logical_replication_workers + + +#------------------------------------------------------------------------------ +# QUERY TUNING +#------------------------------------------------------------------------------ + +# - Planner Method Configuration - + +#enable_bitmapscan = on +#enable_hashagg = on +#enable_hashjoin = on +#enable_indexscan = on +#enable_indexonlyscan = on +#enable_material = on +#enable_mergejoin = on +#enable_nestloop = on +#enable_parallel_append = on +#enable_seqscan = on +#enable_sort = on +#enable_tidscan = on +#enable_partitionwise_join = off +#enable_partitionwise_aggregate = off +#enable_parallel_hash = on +#enable_partition_pruning = on + +# - Planner Cost Constants - + +#seq_page_cost = 1.0 # measured on an arbitrary scale +#random_page_cost = 4.0 # same scale as above +#cpu_tuple_cost = 0.01 # same scale as above +#cpu_index_tuple_cost = 0.005 # same scale as above +#cpu_operator_cost = 0.0025 # same scale as above +#parallel_tuple_cost = 0.1 # same scale as above +#parallel_setup_cost = 1000.0 # same scale as above + +#jit_above_cost = 100000 # perform JIT compilation if available + # and query more expensive than this; + # -1 disables +#jit_inline_above_cost = 500000 # inline small functions if query is + # more expensive than this; -1 disables +#jit_optimize_above_cost = 500000 # use expensive JIT optimizations if + # query is more expensive than this; + # -1 disables + +#min_parallel_table_scan_size = 8MB +#min_parallel_index_scan_size = 512kB +#effective_cache_size = 4GB + +# - Genetic Query Optimizer - + +#geqo = on +#geqo_threshold = 12 +#geqo_effort = 5 # range 1-10 +#geqo_pool_size = 0 # selects default based on effort +#geqo_generations = 0 # selects default based on effort +#geqo_selection_bias = 2.0 # range 1.5-2.0 +#geqo_seed = 0.0 # range 0.0-1.0 + +# - Other Planner Options - + +#default_statistics_target = 100 # range 1-10000 +#constraint_exclusion = partition # on, off, or partition +#cursor_tuple_fraction = 0.1 # range 0.0-1.0 +#from_collapse_limit = 8 +#join_collapse_limit = 8 # 1 disables collapsing of explicit + # JOIN clauses +#force_parallel_mode = off +#jit = on # allow JIT compilation +#plan_cache_mode = auto # auto, force_generic_plan or + # force_custom_plan + + +#------------------------------------------------------------------------------ +# REPORTING AND LOGGING +#------------------------------------------------------------------------------ + +# - Where to Log - + +#log_destination = 'stderr' # Valid values are combinations of + # stderr, csvlog, syslog, and eventlog, + # depending on platform. csvlog + # requires logging_collector to be on. + +# This is used when logging to stderr: +#logging_collector = off # Enable capturing of stderr and csvlog + # into log files. Required to be on for + # csvlogs. + # (change requires restart) + +# These are only used if logging_collector is on: +#log_directory = 'log' # directory where log files are written, + # can be absolute or relative to PGDATA +#log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log' # log file name pattern, + # can include strftime() escapes +#log_file_mode = 0600 # creation mode for log files, + # begin with 0 to use octal notation +#log_truncate_on_rotation = off # If on, an existing log file with the + # same name as the new log file will be + # truncated rather than appended to. + # But such truncation only occurs on + # time-driven rotation, not on restarts + # or size-driven rotation. Default is + # off, meaning append to existing files + # in all cases. +#log_rotation_age = 1d # Automatic rotation of logfiles will + # happen after that time. 0 disables. +#log_rotation_size = 10MB # Automatic rotation of logfiles will + # happen after that much log output. + # 0 disables. + +# These are relevant when logging to syslog: +#syslog_facility = 'LOCAL0' +#syslog_ident = 'postgres' +#syslog_sequence_numbers = on +#syslog_split_messages = on + +# This is only relevant when logging to eventlog (win32): +# (change requires restart) +#event_source = 'PostgreSQL' + +# - When to Log - + +#log_min_messages = warning # values in order of decreasing detail: + # debug5 + # debug4 + # debug3 + # debug2 + # debug1 + # info + # notice + # warning + # error + # log + # fatal + # panic + +#log_min_error_statement = error # values in order of decreasing detail: + # debug5 + # debug4 + # debug3 + # debug2 + # debug1 + # info + # notice + # warning + # error + # log + # fatal + # panic (effectively off) + +#log_min_duration_statement = -1 # -1 is disabled, 0 logs all statements + # and their durations, > 0 logs only + # statements running at least this number + # of milliseconds + +#log_transaction_sample_rate = 0.0 # Fraction of transactions whose statements + # are logged regardless of their duration. 1.0 logs all + # statements from all transactions, 0.0 never logs. + +# - What to Log - + +#debug_print_parse = off +#debug_print_rewritten = off +#debug_print_plan = off +#debug_pretty_print = on +#log_checkpoints = off +#log_connections = off +#log_disconnections = off +#log_duration = off +#log_error_verbosity = default # terse, default, or verbose messages +#log_hostname = off +#log_line_prefix = '%m [%p] ' # special values: + # %a = application name + # %u = user name + # %d = database name + # %r = remote host and port + # %h = remote host + # %p = process ID + # %t = timestamp without milliseconds + # %m = timestamp with milliseconds + # %n = timestamp with milliseconds (as a Unix epoch) + # %i = command tag + # %e = SQL state + # %c = session ID + # %l = session line number + # %s = session start timestamp + # %v = virtual transaction ID + # %x = transaction ID (0 if none) + # %q = stop here in non-session + # processes + # %% = '%' + # e.g. '<%u%%%d> ' +#log_lock_waits = off # log lock waits >= deadlock_timeout +#log_statement = 'none' # none, ddl, mod, all +#log_replication_commands = off +#log_temp_files = -1 # log temporary files equal or larger + # than the specified size in kilobytes; + # -1 disables, 0 logs all temp files +log_timezone = 'UTC' + +#------------------------------------------------------------------------------ +# PROCESS TITLE +#------------------------------------------------------------------------------ + +#cluster_name = '' # added to process titles if nonempty + # (change requires restart) +#update_process_title = on + + +#------------------------------------------------------------------------------ +# STATISTICS +#------------------------------------------------------------------------------ + +# - Query and Index Statistics Collector - + +#track_activities = on +#track_counts = on +#track_io_timing = off +#track_functions = none # none, pl, all +#track_activity_query_size = 1024 # (change requires restart) +#stats_temp_directory = 'pg_stat_tmp' + + +# - Monitoring - + +#log_parser_stats = off +#log_planner_stats = off +#log_executor_stats = off +#log_statement_stats = off + + +#------------------------------------------------------------------------------ +# AUTOVACUUM +#------------------------------------------------------------------------------ + +#autovacuum = on # Enable autovacuum subprocess? 'on' + # requires track_counts to also be on. +#log_autovacuum_min_duration = -1 # -1 disables, 0 logs all actions and + # their durations, > 0 logs only + # actions running at least this number + # of milliseconds. +#autovacuum_max_workers = 3 # max number of autovacuum subprocesses + # (change requires restart) +#autovacuum_naptime = 1min # time between autovacuum runs +#autovacuum_vacuum_threshold = 50 # min number of row updates before + # vacuum +#autovacuum_analyze_threshold = 50 # min number of row updates before + # analyze +#autovacuum_vacuum_scale_factor = 0.2 # fraction of table size before vacuum +#autovacuum_analyze_scale_factor = 0.1 # fraction of table size before analyze +#autovacuum_freeze_max_age = 200000000 # maximum XID age before forced vacuum + # (change requires restart) +#autovacuum_multixact_freeze_max_age = 400000000 # maximum multixact age + # before forced vacuum + # (change requires restart) +#autovacuum_vacuum_cost_delay = 2ms # default vacuum cost delay for + # autovacuum, in milliseconds; + # -1 means use vacuum_cost_delay +#autovacuum_vacuum_cost_limit = -1 # default vacuum cost limit for + # autovacuum, -1 means use + # vacuum_cost_limit + + +#------------------------------------------------------------------------------ +# CLIENT CONNECTION DEFAULTS +#------------------------------------------------------------------------------ + +# - Statement Behavior - + +#client_min_messages = notice # values in order of decreasing detail: + # debug5 + # debug4 + # debug3 + # debug2 + # debug1 + # log + # notice + # warning + # error +#search_path = '"$user", public' # schema names +#row_security = on +#default_tablespace = '' # a tablespace name, '' uses the default +#temp_tablespaces = '' # a list of tablespace names, '' uses + # only default tablespace +#default_table_access_method = 'heap' +#check_function_bodies = on +#default_transaction_isolation = 'read committed' +#default_transaction_read_only = off +#default_transaction_deferrable = off +#session_replication_role = 'origin' +#statement_timeout = 0 # in milliseconds, 0 is disabled +#lock_timeout = 0 # in milliseconds, 0 is disabled +#idle_in_transaction_session_timeout = 0 # in milliseconds, 0 is disabled +#vacuum_freeze_min_age = 50000000 +#vacuum_freeze_table_age = 150000000 +#vacuum_multixact_freeze_min_age = 5000000 +#vacuum_multixact_freeze_table_age = 150000000 +#vacuum_cleanup_index_scale_factor = 0.1 # fraction of total number of tuples + # before index cleanup, 0 always performs + # index cleanup +#bytea_output = 'hex' # hex, escape +#xmlbinary = 'base64' +#xmloption = 'content' +#gin_fuzzy_search_limit = 0 +#gin_pending_list_limit = 4MB + +# - Locale and Formatting - + +datestyle = 'iso, mdy' +#intervalstyle = 'postgres' +timezone = 'UTC' +#timezone_abbreviations = 'Default' # Select the set of available time zone + # abbreviations. Currently, there are + # Default + # Australia (historical usage) + # India + # You can create your own file in + # share/timezonesets/. +#extra_float_digits = 1 # min -15, max 3; any value >0 actually + # selects precise output mode +#client_encoding = sql_ascii # actually, defaults to database + # encoding + +# These settings are initialized by initdb, but they can be changed. +lc_messages = 'en_US.utf8' # locale for system error message + # strings +lc_monetary = 'en_US.utf8' # locale for monetary formatting +lc_numeric = 'en_US.utf8' # locale for number formatting +lc_time = 'en_US.utf8' # locale for time formatting + +# default configuration for text search +default_text_search_config = 'pg_catalog.english' + +# - Shared Library Preloading - + +#shared_preload_libraries = '' # (change requires restart) +#local_preload_libraries = '' +#session_preload_libraries = '' +#jit_provider = 'llvmjit' # JIT library to use + +# - Other Defaults - + +#dynamic_library_path = '$libdir' + + +#------------------------------------------------------------------------------ +# LOCK MANAGEMENT +#------------------------------------------------------------------------------ + +#deadlock_timeout = 1s +#max_locks_per_transaction = 64 # min 10 + # (change requires restart) +#max_pred_locks_per_transaction = 64 # min 10 + # (change requires restart) +#max_pred_locks_per_relation = -2 # negative values mean + # (max_pred_locks_per_transaction + # / -max_pred_locks_per_relation) - 1 +#max_pred_locks_per_page = 2 # min 0 + + +#------------------------------------------------------------------------------ +# VERSION AND PLATFORM COMPATIBILITY +#------------------------------------------------------------------------------ + +# - Previous PostgreSQL Versions - + +#array_nulls = on +#backslash_quote = safe_encoding # on, off, or safe_encoding +#escape_string_warning = on +#lo_compat_privileges = off +#operator_precedence_warning = off +#quote_all_identifiers = off +#standard_conforming_strings = on +#synchronize_seqscans = on + +# - Other Platforms and Clients - + +#transform_null_equals = off + + +#------------------------------------------------------------------------------ +# ERROR HANDLING +#------------------------------------------------------------------------------ + +#exit_on_error = off # terminate session on any error? +#restart_after_crash = on # reinitialize after backend crash? +#data_sync_retry = off # retry or panic on failure to fsync + # data? + # (change requires restart) + + +#------------------------------------------------------------------------------ +# CONFIG FILE INCLUDES +#------------------------------------------------------------------------------ + +# These options allow settings to be loaded from files other than the +# default postgresql.conf. Note that these are directives, not variable +# assignments, so they can usefully be given more than once. + +#include_dir = '...' # include files ending in '.conf' from + # a directory, e.g., 'conf.d' +#include_if_exists = '...' # include file only if it exists +#include = '...' # include file + + +#------------------------------------------------------------------------------ +# CUSTOMIZED OPTIONS +#------------------------------------------------------------------------------ + +# Add settings for extensions here diff --git a/dhis2-to-fhir-patient-bundle-datasonnet/config/fhir-gateway/oauth2-proxy-keycloak.cfg b/dhis2-to-fhir-patient-bundle-datasonnet/config/fhir-gateway/oauth2-proxy-keycloak.cfg new file mode 100644 index 0000000..befd27f --- /dev/null +++ b/dhis2-to-fhir-patient-bundle-datasonnet/config/fhir-gateway/oauth2-proxy-keycloak.cfg @@ -0,0 +1,11 @@ +http_address="0.0.0.0:4180" +cookie_secret=" " +email_domains="*" +upstreams="http://hapi-fhir-ips:8080" +client_secret="dummy" +client_id="fhir-client" +oidc_issuer_url="http://authorisation-server:8080/realms/fhir" +provider="keycloak-oidc" +provider_display_name="Keycloak" +skip_jwt_bearer_tokens="true" +insecure_oidc_allow_unverified_email="true" \ No newline at end of file diff --git a/dhis2-to-fhir-patient-bundle-datasonnet/config/hapi-fhir-ips/fhir-ips-ig.tgz b/dhis2-to-fhir-patient-bundle-datasonnet/config/hapi-fhir-ips/fhir-ips-ig.tgz new file mode 100644 index 0000000..45043bd Binary files /dev/null and b/dhis2-to-fhir-patient-bundle-datasonnet/config/hapi-fhir-ips/fhir-ips-ig.tgz differ diff --git a/dhis2-to-fhir-patient-bundle-datasonnet/config/hapi-fhir-ips/hapi.application.yaml b/dhis2-to-fhir-patient-bundle-datasonnet/config/hapi-fhir-ips/hapi.application.yaml new file mode 100644 index 0000000..1d91086 --- /dev/null +++ b/dhis2-to-fhir-patient-bundle-datasonnet/config/hapi-fhir-ips/hapi.application.yaml @@ -0,0 +1,366 @@ +#Uncomment the "servlet" and "context-path" lines below to make the fhir endpoint available at /example/path/fhir instead of the default value of /fhir +server: + # servlet: + # context-path: /example/path + port: 8080 + tomcat: + # allow | as a separator in the URL + relaxed-query-chars: "|" +#Adds the option to go to eg. http://localhost:8080/actuator/health for seeing the running configuration +#see https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html#actuator.endpoints +management: + #The following configuration will enable the actuator endpoints at /actuator/health, /actuator/info, /actuator/prometheus, /actuator/metrics. For security purposes, only /actuator/health is enabled by default. + endpoints: + enabled-by-default: false + web: + exposure: + include: 'health' # or e.g. 'info,health,prometheus,metrics' or '*' for all' + endpoint: + info: + enabled: true + metrics: + enabled: true + health: + enabled: true + probes: + enabled: true + group: + liveness: + include: + - livenessState + - readinessState + prometheus: + enabled: true + prometheus: + metrics: + export: + enabled: true +spring: + main: + allow-circular-references: true + flyway: + enabled: false + baselineOnMigrate: true + fail-on-missing-locations: false + datasource: + #url: 'jdbc:h2:file:./target/database/h2' + url: jdbc:h2:mem:test_mem + username: sa + password: null + driverClassName: org.h2.Driver + max-active: 15 + + # database connection pool size + hikari: + maximum-pool-size: 10 + jpa: + properties: + hibernate.format_sql: false + hibernate.show_sql: false + + #Hibernate dialect is automatically detected except Postgres and H2. + #If using H2, then supply the value of ca.uhn.fhir.jpa.model.dialect.HapiFhirH2Dialect + #If using postgres, then supply the value of ca.uhn.fhir.jpa.model.dialect.HapiFhirPostgresDialect + hibernate.dialect: ca.uhn.fhir.jpa.model.dialect.HapiFhirH2Dialect + # hibernate.hbm2ddl.auto: update + # hibernate.jdbc.batch_size: 20 + # hibernate.cache.use_query_cache: false + # hibernate.cache.use_second_level_cache: false + # hibernate.cache.use_structured_entries: false + # hibernate.cache.use_minimal_puts: false + + ### These settings will enable fulltext search with lucene or elastic + hibernate.search.enabled: false + ### lucene parameters + # hibernate.search.backend.type: lucene + # hibernate.search.backend.analysis.configurer: ca.uhn.fhir.jpa.search.HapiHSearchAnalysisConfigurers$HapiLuceneAnalysisConfigurer + # hibernate.search.backend.directory.type: local-filesystem + # hibernate.search.backend.directory.root: target/lucenefiles + # hibernate.search.backend.lucene_version: lucene_current + ### elastic parameters ===> see also elasticsearch section below <=== +# hibernate.search.backend.type: elasticsearch +# hibernate.search.backend.analysis.configurer: ca.uhn.fhir.jpa.search.HapiHSearchAnalysisConfigurers$HapiElasticAnalysisConfigurer +hapi: + fhir: + ### This flag when enabled to true, will avail evaluate measure operations from CR Module. + ### Flag is false by default, can be passed as command line argument to override. + cr: + enabled: false + caregaps: + reporter: "default" + section_author: "default" + cql: + use_embedded_libraries: true + compiler: + ### These are low-level compiler options. + ### They are not typically needed by most users. + # validate_units: true + # verify_only: false + # compatibility_level: "1.5" + error_level: Info + signature_level: All + # analyze_data_requirements: false + # collapse_data_requirements: false + # translator_format: JSON + # enable_date_range_optimization: true + enable_annotations: true + enable_locators: true + enable_results_type: true + enable_detailed_errors: true + # disable_list_traversal: false + # disable_list_demotion: false + # enable_interval_demotion: false + # enable_interval_promotion: false + # disable_method_invocation: false + # require_from_keyword: false + # disable_default_model_info_load: false + runtime: + debug_logging_enabled: false + # enable_validation: false + # enable_expression_caching: true + terminology: + valueset_preexpansion_mode: REQUIRE # USE_IF_PRESENT, REQUIRE, IGNORE + valueset_expansion_mode: PERFORM_NAIVE_EXPANSION # AUTO, USE_EXPANSION_OPERATION, PERFORM_NAIVE_EXPANSION + valueset_membership_mode: USE_EXPANSION # AUTO, USE_VALIDATE_CODE_OPERATION, USE_EXPANSION + code_lookup_mode: USE_VALIDATE_CODE_OPERATION # AUTO, USE_VALIDATE_CODE_OPERATION, USE_CODESYSTEM_URL + data: + search_parameter_mode: USE_SEARCH_PARAMETERS # AUTO, USE_SEARCH_PARAMETERS, FILTER_IN_MEMORY + terminology_parameter_mode: FILTER_IN_MEMORY # AUTO, USE_VALUE_SET_URL, USE_INLINE_CODES, FILTER_IN_MEMORY + profile_mode: DECLARED # ENFORCED, DECLARED, OPTIONAL, TRUST, OFF + + cdshooks: + enabled: false + clientIdHeaderName: client_id + + ### This enables the swagger-ui at /fhir/swagger-ui/index.html as well as the /fhir/api-docs (see https://hapifhir.io/hapi-fhir/docs/server_plain/openapi.html) + openapi_enabled: true + ### This is the FHIR version. Choose between, DSTU2, DSTU3, R4 or R5 + fhir_version: R4 + ### Flag is false by default. This flag enables runtime installation of IG's. + ig_runtime_upload_enabled: true + ### This flag when enabled to true, will avail evaluate measure operations from CR Module. + + ### enable to use the ApacheProxyAddressStrategy which uses X-Forwarded-* headers + ### to determine the FHIR server address + # use_apache_address_strategy: false + ### forces the use of the https:// protocol for the returned server address. + ### alternatively, it may be set using the X-Forwarded-Proto header. + # use_apache_address_strategy_https: false + ### enables the server to overwrite defaults on HTML, css, etc. under the url pattern of eg. /content/custom ** + ### Folder with custom content MUST be named custom. If omitted then default content applies + #custom_content_path: ./custom + ### enables the server host custom content. If e.g. the value ./configs/app is supplied then the content + ### will be served under /web/app + #app_content_path: ./configs/app + ### enable to set the Server URL + # server_address: http://hapi.fhir.org/baseR4 + # defer_indexing_for_codesystems_of_size: 101 + ### Flag is true by default. This flag filters resources during package installation, allowing only those resources with a valid status (e.g. active) to be installed. + # validate_resource_status_for_package_upload: false + # install_transitive_ig_dependencies: true + implementationguides: + ips: + packageUrl: file:///package.tgz + name: hl7.fhir.uv.ips.r4 + version: 2.0.0 + installMode: STORE_AND_INSTALL + validate_resource_status_for_package_upload: false + ### example from registry (packages.fhir.org) + # swiss: + # name: swiss.mednet.fhir + # version: 0.8.0 + # reloadExisting: false + # installMode: STORE_AND_INSTALL + # example not from registry + # ips_1_0_0: + # packageUrl: https://build.fhir.org/ig/HL7/fhir-ips/package.tgz + # name: hl7.fhir.uv.ips + # version: 1.0.0 + # supported_resource_types: + # - Patient + # - Observation + ################################################## + # Allowed Bundle Types for persistence (defaults are: COLLECTION,DOCUMENT,MESSAGE) + ################################################## + # allowed_bundle_types: COLLECTION,DOCUMENT,MESSAGE,TRANSACTION,TRANSACTIONRESPONSE,BATCH,BATCHRESPONSE,HISTORY,SEARCHSET + # allow_cascading_deletes: true + # allow_contains_searches: true + # allow_external_references: true + # allow_multiple_delete: true + # allow_override_default_search_params: true + # auto_create_placeholder_reference_targets: false + # mass_ingestion_mode_enabled: false + ### tells the server to automatically append the current version of the target resource to references at these paths + # auto_version_reference_at_paths: Device.patient, Device.location, Device.parent, DeviceMetric.parent, DeviceMetric.source, Observation.device, Observation.subject + # ips_enabled: false + # default_encoding: JSON + # default_pretty_print: true + # default_page_size: 20 + # delete_expunge_enabled: true + # enable_repository_validating_interceptor: true + # enable_index_missing_fields: false + # enable_index_of_type: true + # enable_index_contained_resource: false + # upliftedRefchains_enabled: true + # resource_dbhistory_enabled: false + ### !!Extended Lucene/Elasticsearch Indexing is still a experimental feature, expect some features (e.g. _total=accurate) to not work as expected!! + ### more information here: https://hapifhir.io/hapi-fhir/docs/server_jpa/elastic.html + advanced_lucene_indexing: false + search_index_full_text_enabled: false + bulk_export_enabled: false + bulk_import_enabled: false + # language_search_parameter_enabled: true + # enforce_referential_integrity_on_delete: false + # This is an experimental feature, and does not fully support _total and other FHIR features. + # enforce_referential_integrity_on_delete: false + # enforce_referential_integrity_on_write: false + # etag_support_enabled: true + # expunge_enabled: true + # client_id_strategy: ALPHANUMERIC + # server_id_strategy: SEQUENTIAL_NUMERIC + # fhirpath_interceptor_enabled: false + # filter_search_enabled: true + # graphql_enabled: true + narrative_enabled: false + mdm_enabled: false + mdm_rules_json_location: "mdm-rules.json" + ## see: https://hapifhir.io/hapi-fhir/docs/interceptors/built_in_server_interceptors.html#jpa-server-retry-on-version-conflicts + # userRequestRetryVersionConflictsInterceptorEnabled : false + # local_base_urls: + # - https://hapi.fhir.org/baseR4 + # pre_expand_value_sets: true + # enable_task_pre_expand_value_sets: true + # pre_expand_value_sets_default_count: 1000 + # pre_expand_value_sets_max_count: 1000 + # maximum_expansion_size: 1000 + + logical_urls: + - http://terminology.hl7.org/* + - https://terminology.hl7.org/* + - http://snomed.info/* + - https://snomed.info/* + - http://unitsofmeasure.org/* + - https://unitsofmeasure.org/* + - http://loinc.org/* + - https://loinc.org/* + # partitioning: + # allow_references_across_partitions: false + # partitioning_include_in_search_hashes: false + # conditional_create_duplicate_identifiers_enabled: false + # request_tenant_partitioning_mode: true + cors: + allow_Credentials: true + # These are allowed_origin patterns, see: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/cors/CorsConfiguration.html#setAllowedOriginPatterns-java.util.List- + allowed_origin: + - '*' + + # Search coordinator thread pool sizes + search-coord-core-pool-size: 20 + search-coord-max-pool-size: 100 + search-coord-queue-capacity: 200 + + # Search Prefetch Thresholds. + + # This setting sets the number of search results to prefetch. For example, if this list + # is set to [100, 1000, -1] then the server will initially load 100 results and not + # attempt to load more. If the user requests subsequent page(s) of results and goes + # past 100 results, the system will load the next 900 (up to the following threshold of 1000). + # The system will progressively work through these thresholds. + # A threshold of -1 means to load all results. Note that if the final threshold is a + # number other than -1, the system will never prefetch more than the given number. + search_prefetch_thresholds: 13,503,2003,-1 + + # comma-separated package names, will be @ComponentScan'ed by Spring to allow for creating custom Spring beans + #custom-bean-packages: + + # comma-separated list of fully qualified interceptor classes. + # classes listed here will be fetched from the Spring context when combined with 'custom-bean-packages', + # or will be instantiated via reflection using an no-arg contructor; then registered with the server + #custom-interceptor-classes: + + # comma-separated list of fully qualified provider classes. + # classes listed here will be fetched from the Spring context when combined with 'custom-bean-packages', + # or will be instantiated via reflection using an no-arg contructor; then registered with the server + #custom-provider-classes: + # specify what should be stored in meta.source based on StoreMetaSourceInformationEnum defaults to NONE + # store_meta_source_information: NONE + # Threadpool size for BATCH'ed GETs in a bundle. + # bundle_batch_pool_size: 10 + # bundle_batch_pool_max_size: 50 + + # logger: + # error_format: 'ERROR - ${requestVerb} ${requestUrl}' + # format: >- + # Path[${servletPath}] Source[${requestHeader.x-forwarded-for}] + # Operation[${operationType} ${operationName} ${idOrResourceName}] + # UA[${requestHeader.user-agent}] Params[${requestParameters}] + # ResponseEncoding[${responseEncodingNoDefault}] + # log_exceptions: true + # name: fhirtest.access + # max_binary_size: 104857600 + # max_page_size: 200 + # retain_cached_searches_mins: 60 + # reuse_cached_search_results_millis: 60000 + # The remote_terminology_service block is commented out by default because it requires external terminology service endpoints. + # Uncomment and configure the block below if you need to enable remote terminology validation or mapping. + #remote_terminology_service: + # all: + # system: '*' + # url: 'https://tx.fhir.org/r4/' + # snomed: + # system: 'http://snomed.info/sct' + # url: 'https://tx.fhir.org/r4/' + # loinc: + # system: 'http://loinc.org' + # url: 'https://hapi.fhir.org/baseR4/' + tester: + home: + name: Local Tester + server_address: 'http://localhost:8080/fhir' + refuse_to_fetch_third_party_urls: false + fhir_version: R4 + global: + name: Global Tester + server_address: "http://hapi.fhir.org/baseR4" + refuse_to_fetch_third_party_urls: false + fhir_version: R4 + # validation: + # enabled: true + # requests_enabled: true + # responses_enabled: true + # binary_storage_enabled: true + inline_resource_storage_below_size: 4000 +# bulk_export_enabled: true +# subscription: +# resthook_enabled: true +# websocket_enabled: false +# polling_interval_ms: 5000 +# immediately_queued: false +# email: +# from: some@test.com +# host: google.com +# port: +# username: +# password: +# auth: +# startTlsEnable: +# startTlsRequired: +# quitWait: +# lastn_enabled: true +# store_resource_in_lucene_index_enabled: true +### This is configuration for normalized quantity search level default is 0 +### 0: NORMALIZED_QUANTITY_SEARCH_NOT_SUPPORTED - default +### 1: NORMALIZED_QUANTITY_STORAGE_SUPPORTED +### 2: NORMALIZED_QUANTITY_SEARCH_SUPPORTED +# normalized_quantity_search_level: 2 +#elasticsearch: +# debug: +# pretty_print_json_log: false +# refresh_after_write: false +# enabled: false +# password: SomePassword +# required_index_status: YELLOW +# rest_url: 'localhost:9200' +# protocol: 'http' +# schema_management_strategy: CREATE +# username: SomeUsername diff --git a/dhis2-to-fhir-patient-bundle-datasonnet/config/reverse-proxy/nginx.conf b/dhis2-to-fhir-patient-bundle-datasonnet/config/reverse-proxy/nginx.conf new file mode 100644 index 0000000..0f7485a --- /dev/null +++ b/dhis2-to-fhir-patient-bundle-datasonnet/config/reverse-proxy/nginx.conf @@ -0,0 +1,78 @@ +worker_processes 1; + +events { + worker_connections 1024; +} + +http { + gzip on; # Enables compression, incl Web API content-types + gzip_types + "application/json;charset=utf-8" application/json + "application/javascript;charset=utf-8" application/javascript text/javascript + "application/xml;charset=utf-8" application/xml text/xml + "text/css;charset=utf-8" text/css + "text/plain;charset=utf-8" text/plain; + + upstream dhis2 { + server dhis2:8080 max_fails=3 fail_timeout=60s; + } + + upstream hapi-fhir-ips { + server hapi-fhir-ips:8080 max_fails=3 fail_timeout=60s; + } + + upstream esignet-ui { + server esignet-ui:4000 max_fails=3 fail_timeout=60s; + } + + # HTTP server - rewrite to force use of SSL + server { + listen 80; + rewrite ^ https://mosip.integration.dhis2.org$request_uri? permanent; + } + + # HTTPS server + server { + listen 443 ssl; + client_max_body_size 10M; + + ssl_certificate /etc/letsencrypt/live/mosip.integration.dhis2.org/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/mosip.integration.dhis2.org/privkey.pem; + + include /etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + + # Proxy pass to servlet container + location / { + proxy_pass http://dhis2/; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + proxy_buffer_size 128k; + proxy_buffers 8 128k; + proxy_busy_buffers_size 256k; + proxy_cookie_path ~*^/(.*) "/$1; SameSite=Lax"; + } + + location /fhir/ { + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_pass http://fhir/; + + sub_filter_types text/html; + sub_filter_once off; + sub_filter 'href="/' 'href="/fhir/'; + sub_filter 'src="/' 'src="/fhir/'; + } + + location /esignet/ { + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_pass http://esignet-ui/; + } + } +} \ No newline at end of file diff --git a/dhis2-to-fhir-patient-bundle-datasonnet/db-dump/.gitkeep b/dhis2-to-fhir-patient-bundle-datasonnet/db-dump/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/dhis2-to-fhir-patient-bundle-datasonnet/db-dump/dump-sierra-leone.sql.gz b/dhis2-to-fhir-patient-bundle-datasonnet/db-dump/dump-sierra-leone.sql.gz new file mode 100644 index 0000000..b94c134 Binary files /dev/null and b/dhis2-to-fhir-patient-bundle-datasonnet/db-dump/dump-sierra-leone.sql.gz differ diff --git a/dhis2-to-fhir-patient-bundle-datasonnet/docker-compose.yml b/dhis2-to-fhir-patient-bundle-datasonnet/docker-compose.yml new file mode 100644 index 0000000..82aec0c --- /dev/null +++ b/dhis2-to-fhir-patient-bundle-datasonnet/docker-compose.yml @@ -0,0 +1,181 @@ +services: + dhis2: + image: "${DHIS2_IMAGE:-dhis2/core:42.1.0}" + restart: always + volumes: + - ./config/dhis2/dhis.conf:/opt/dhis2/dhis.conf:ro + environment: + JAVA_OPTS: "-Dcontext.path='${DHIS2_CORE_CONTEXT_PATH:-}' -Dlog4j2.configurationFile=/opt/dhis2/log4j2.xml" + depends_on: + dhis2-db: + condition: service_healthy + healthcheck: + test: [ "CMD-SHELL", "wget -q -S -O /dev/null http://127.0.0.1:8080/dhis-web-login/ 2>&1| grep -q 'HTTP.*200'" ] + start_period: 120s + interval: 10s + timeout: 5s + retries: 5 + security_opt: + - no-new-privileges:true + networks: + - dhis2 + - dhis2-db + + dhis2-db: + image: ghcr.io/baosystems/postgis:13-3.4 + restart: always + volumes: + - ./db-dump:/docker-entrypoint-initdb.d/:ro + - ./config/dhis2/postgresql.conf:/etc/postgresql.conf + command: postgres -c config_file=/etc/postgresql.conf + environment: + POSTGRES_USER: dhis + POSTGRES_DB: dhis + POSTGRES_PASSWORD: &postgres_password dhis + PGPASSWORD: *postgres_password # needed by psql in healthcheck + healthcheck: + test: [ "CMD-SHELL", "psql --no-password --quiet --username $$POSTGRES_USER postgres://127.0.0.1/$$POSTGRES_DB -p 5432 --command \"SELECT 'ok'\" > /dev/null" ] + start_period: 120s + interval: 1s + timeout: 3s + retries: 5 + security_opt: + - no-new-privileges:true + networks: + - dhis2-db + + reverse-proxy: + image: nginx:stable-alpine3.21-slim + restart: always + depends_on: + seed: + condition: service_completed_successfully + healthcheck: + test: [ "CMD-SHELL", "wget -q -S -O /dev/null http://127.0.0.1/api/system/info 2>&1 | grep -q 'HTTP.*200'" ] + start_period: 120s + interval: 10s + timeout: 5s + retries: 5 + ports: + - "${DHIS2_PORT:-8080}:80" + volumes: + - ./config/dhis2/nginx.conf:/etc/nginx/nginx.conf:ro + security_opt: + - no-new-privileges:true + networks: + - dhis2 + - default + + hapi-fhir-ips: + image: "hapiproject/hapi:v8.2.0-2-tomcat" + restart: always + volumes: + - ./config/hapi-fhir-ips/hapi.application.yaml:/data/hapi/application.yaml:ro + - ./config/hapi-fhir-ips/fhir-ips-ig.tgz:/package.tgz:ro + environment: + SPRING_CONFIG_LOCATION: file:///data/hapi/application.yaml + healthcheck: + test: [ "CMD", "bash", "-c", "echo -n '' > /dev/tcp/127.0.0.1/8080" ] + start_period: 120s + interval: 5s + timeout: 1s + retries: 5 + security_opt: + - no-new-privileges:true + networks: + - hapi-fhir-ips + + authorisation-server: + image: "quay.io/keycloak/keycloak:25.0.6" + restart: always + environment: + KEYCLOAK_ADMIN: "admin" + KEYCLOAK_ADMIN_PASSWORD: "admin" + KC_HEALTH_ENABLED: "true" + + # comment KC_HOSTNAME and KC_HOSTNAME_BACKCHANNEL_DYNAMIC to test Keycloak from Docker host + KC_HOSTNAME: "http://authorisation-server:8080" + KC_HOSTNAME_BACKCHANNEL_DYNAMIC: "true" + healthcheck: + test: + [ + "CMD-SHELL", + "exec 3<>/dev/tcp/localhost/9000 && echo -e 'GET /health/ready HTTP/1.1\\r\\nHost: localhost\\r\\nConnection: close\\r\\n\\r\\n' >&3 && cat <&3 | grep -q '200 OK'" + ] + start_period: 120s + interval: 5s + timeout: 5s + retries: 10 + volumes: + - ./config/authorisation-server/fhir-realm.json:/opt/keycloak/data/import/fhir-realm.json:ro + command: [ "start-dev", "--import-realm" ] + security_opt: + - no-new-privileges:true + networks: + - auth + + fhir-gateway: + image: "quay.io/oauth2-proxy/oauth2-proxy:v7.6.0" + restart: always + command: --config /oauth2-proxy.cfg + volumes: + - "./config/fhir-gateway/oauth2-proxy-keycloak.cfg:/oauth2-proxy.cfg" + depends_on: + authorisation-server: + condition: service_healthy + security_opt: + - no-new-privileges:true + networks: + - auth + - hapi-fhir-ips + + fhir-sync-agent: + build: ./fhir-sync-agent + restart: always + environment: + FHIR_URL: http://fhir-gateway:4180/fhir + OAUTH2_TOKENENDPOINT: http://authorisation-server:8080/realms/fhir/protocol/openid-connect/token + OAUTH2_CLIENTID: fhir-client + OAUTH2_CLIENTSECRET: ibz2lEXdGe02CJS2N7tY13r6664eJpoX + DHIS2DATABASEHOSTNAME: dhis2-db + DHIS2DATABASEPORT: 5432 + DHIS2DATABASEUSER: dhis + DHIS2DATABASEPASSWORD: dhis + DHIS2DATABASEDBNAME: dhis + DHIS2APIURL: http://reverse-proxy/api + DHIS2APIUSERNAME: admin + DHIS2APIPASSWORD: district + depends_on: + authorisation-server: + condition: service_healthy + reverse-proxy: + condition: service_healthy + security_opt: + - no-new-privileges:true + networks: + - auth + - dhis2 + - dhis2-db + + seed: + image: curlimages/curl + command: + - "/bin/sh" + - "-c" + - | + curl -X POST -s --max-time 0 -H "Authorization: Basic YWRtaW46ZGlzdHJpY3Q=" -H "Content-Type: application/json" -d '["http://localhost:*"]' http://dhis2:8080/api/configuration/corsWhitelist + depends_on: + dhis2: + condition: service_healthy + security_opt: + - no-new-privileges:true + networks: + - dhis2 + +networks: + dhis2: + hapi-fhir-ips: + auth: + internal: true + dhis2-db: + internal: true \ No newline at end of file diff --git a/dhis2-to-fhir-patient-bundle-datasonnet/docs/HANDS_ON_GUIDE.md b/dhis2-to-fhir-patient-bundle-datasonnet/docs/HANDS_ON_GUIDE.md new file mode 100644 index 0000000..68ff455 --- /dev/null +++ b/dhis2-to-fhir-patient-bundle-datasonnet/docs/HANDS_ON_GUIDE.md @@ -0,0 +1,163 @@ +# Hands-On: Extending the DHIS2 to FHIR Mapping + +The Patient resource is already implemented and working. Your job is to complete the mapping by adding the missing pieces. This gives you hands-on experience with the transformation pattern while achieving a lossless mapping. + +## What You'll Build + +Right now, the mapping only creates Patient resources. But we're losing data: +- Civil status ("Single or widow") doesn't fit in Patient +- Allergies ("NSAIDS, Penicillin") need proper clinical resources +- The DHIS2 user who created the record isn't captured + +You'll add: +1. **Practitioner resource** - Map the DHIS2 user +2. **Observation resource** - Capture civil status +3. **AllergyIntolerance resources** - One per allergy (split comma-separated values) + +## Setup + +```bash +cd fhir-sync-agent +mvn clean test -Dtest=IpsPatientMappingTestCase +``` + +**Current state:** 7 tests pass, 5 tests fail. Your job is to get all 12 passing. + +## The Pattern + +Look at `patientResource.libsonnet` to see how it's done: + +1. Import helpers +2. Extract values with `helpers.getAttrValue(tei, 'attributeId')` +3. Transform using helper functions +4. Build the resource with `std.prune()` to remove nulls +5. Return `{fullUrl, resource, request}` + +Every resource follows this pattern. + +## Task 1: Add Practitioner + +The IPS Patient profile expects `generalPractitioner` to reference who's managing the patient. We can map the DHIS2 user who created the tracked entity. + +**What you will do:** + +1. Use `practitionerResource.todo.libsonnet` (has guided TODOs) or create from scratch +2. Extract user info from `tei.createdBy` (uid, firstName, surname) +3. Build a Practitioner resource with identifier and name +4. Use conditional PUT so the same user doesn't get duplicated +5. Update Patient to reference the Practitioner +6. Wire it into the bundle + +The TODO file walks you through each step. Key things to remember: +- Use `helpers.parseName()` for the name +- The identifier system is `urn:dhis2:user:uid` +- The `fullUrl` is `urn:uuid:practitioner-{uid}` so Patient can reference it +- Conditional PUT: `Practitioner?identifier=urn:dhis2:user:uid|{uid}` + +**Test it:** +```bash +mvn test -Dtest=IpsPatientMappingTestCase#testPractitionerResourceExists +``` + +## Task 2: Civil Status Observation + +The tracked entity has a "Civil status" attribute (`ciq2USN94oJ`) with values like "Single or widow". There's no field for this in the Patient resource. + +**The solution:** Create an Observation resource. Observations are perfect for capturing facts that don't have a dedicated resource or field. + +**What you'll do:** + +Use `observationResources.todo.libsonnet` - it has TODOs 2-13 for civil status. + +Key concepts: +- Extract with `helpers.getAttrValue(tei, ATTR_CIVIL_STATUS)` +- Return null if no data (gets pruned from bundle) +- Create unique identifier: `attributeId + '-' + tei.trackedEntity` +- Use LOINC code `45404-1` (standard code for marital status) +- Category is `social-history` +- Subject references the Patient +- Use PUT with identifier query for conditional updates + +Why PUT instead of POST? Again, when the tracked entity updates in DHIS2 and we sync again, PUT will update the existing Observation instead of creating a duplicate. + +**Test it:** +```bash +mvn test -Dtest=IpsPatientMappingTestCase#testCivilStatusObservationExists +``` + +## Task 3: AllergyIntolerance Resources (Advanced) + +The allergies attribute (`gu1fqsmoU8r`) stores comma-separated values: `"NSAIDS, Penicillin, ..."`. We need to create separate AllergyIntolerance resources for each allergy. + +**What you will do:** + +Continue in `observationResources.todo.libsonnet` - TODOs 14-26 cover allergies. + +This is more advanced because it involves: + +1. **Splitting the multi-value field:** + ```jsonnet + local allergiesList = std.split(allergiesValue, ','); + ``` + +2. **List comprehension** to create multiple resources: + ```jsonnet + [ + { /* resource definition */ } + for allergy in allergiesList + if std.length(std.stripChars(allergy, ' ')) > 0 + ] + ``` + +3. **Unique ID per allergy:** + ```jsonnet + local allergyText = std.stripChars(allergy, ' '); + local allergyId = ATTR_ALLERGIES + '-' + tei.trackedEntity + '-' + allergyText; + ``` + +The TODO file guides you through building the AllergyIntolerance resource structure. Key points: +- `clinicalStatus`: 'active' (currently relevant) +- `verificationStatus`: 'unconfirmed' (self-reported, not clinically verified) +- `patient`: reference to Patient +- `reaction.manifestation`: the allergy text +- Conditional PUT for each allergy + +**Wiring it up:** + +In `fhirBundle.ds`, note the `+` operator to concatenate arrays: + +```jsonnet +entry: std.prune([ + patient.patient_entry(body), + practitioner.practitioner_entry(body), + observations.civil_status_observation(body), +] + observations.allergies_resources(body)) +``` + +The `allergies_resources()` function returns an array, so we concatenate it with the other single entries. + +**Test everything:** +```bash +mvn test -Dtest=IpsPatientMappingTestCase # All 12 should pass! +``` + +## What You Learned + +By completing this exercise, you will have seen: + +**Complete mapping** - When data doesn't fit the target profile, use additional resources rather than discarding it. Observations work for generic facts, but prefer specialized resources (AllergyIntolerance) when they exist. Following this approach ensures that we both capture all of the `MustSupport` elements for the IPS Profile while still including additional elements captured in DHIS2. + +**Conditional updates** - Using `PUT` with identifier queries (`Resource?identifier=system|value`) enables idempotent operations. Push the same data twice and you get updates to existing FHIR resources. + +**The transformation pattern** - Each resource that is included in the final FHIR bundle is created using the same pattern: Extract, Transform, Build, insert in Bundle. + +**List comprehension** - Jsonnet's `[{item} for x in array if condition]` lets you create multiple resources from a single multi-value attribute. + +## Next Steps + +- Check out the solution files in `src/main/resources/solutions/` to compare approaches +- Try adding more attributes to your DHIS2 metadata, and try to capture the changes in the Datasonnet transformations (age, next of kin, etc.) +- Add support for program stage events +- Experiment with other FHIR resources (Condition, Medication, etc.) + +It is important to remember that the concepts you learned here apply to any DHIS2-to-FHIR mapping, not just IPS Patient profiles. diff --git a/dhis2-to-fhir-patient-bundle-datasonnet/docs/QUICK_REFERENCE.md b/dhis2-to-fhir-patient-bundle-datasonnet/docs/QUICK_REFERENCE.md new file mode 100644 index 0000000..c42ca30 --- /dev/null +++ b/dhis2-to-fhir-patient-bundle-datasonnet/docs/QUICK_REFERENCE.md @@ -0,0 +1,173 @@ +# References for Exercise + +## Test Commands + +```bash +# Verify setup (should have 7 passing tests) +mvn clean test -Dtest=IpsPatientMappingTestCase + +# Task 1: Practitioner +mvn test -Dtest=IpsPatientMappingTestCase#testPatientReferencesPractitioner + +# Task 2: Civil Status +mvn test -Dtest=IpsPatientMappingTestCase#testCivilStatusObservation + +# Task 3: Allergies +mvn test -Dtest=IpsPatientMappingTestCase#testAllergiesResource + +# Final verification (should have 12 passing tests) +mvn test -Dtest=IpsPatientMappingTestCase +``` + +## File Locations + +**Participants Create/Edit:** +- `src/main/resources/practitionerResource.libsonnet` +- `src/main/resources/observationResources.libsonnet` +- `src/main/resources/fhirBundle.ds` (add imports) +- `src/main/resources/patientResource.libsonnet` (add generalPractitioner) + +**Solution Files (if stuck):** +- `src/main/resources/solutions/practitionerResource.solution.libsonnet` +- `src/main/resources/solutions/observationResources.solution.libsonnet` + +**TODO Templates (for guidance):** +- `src/main/resources/practitionerResource.todo.libsonnet` +- `src/main/resources/observationResources.todo.libsonnet` + +**Test Data:** +- `src/test/resources/trackedEntity.json` (complete data) +- `src/test/resources/minimalTrackedEntity.json` (minimal data) + +## Key Attribute IDs from trackedEntity.json + +```javascript +const ATTRIBUTES = { + UNIQUE_ID: 'lZGmxYbs97q', // "8437107" + FIRST_NAME: 'w75KJ2mc4zz', // "Jane" + LAST_NAME: 'zDhUuAYrxNC', // "Doe" + BIRTH_DATE: 'gHGyrwKPzej', // "1997-04-18" + ADDRESS: 'VqEFza8wbwA', // "Madison Avenue 12" + CITY: 'FO4sWYJ64LQ', // "New York" + POSTAL: 'ZcBPrXKahq2', // "10022" + MOBILE: 'Agywv2JGwuq', // "+13052065294" + EMAIL: 'KmEUg2hHEtx', // "jane@doe.com" + CIVIL_STATUS: 'ciq2USN94oJ', // "Single or widow" unmapped + ALLERGIES: 'gu1fqsmoU8r' // "NSAIDS" unmapped +}; +``` + +## Code Snippets for Jsonnet/Datasonnet + +### Import Pattern +```jsonnet +local helpers = import 'helperFunctions.libsonnet'; +``` + +### Extract Attribute +```jsonnet +local value = helpers.getAttrValue(tei, 'attribute-id'); +``` + +### Build "Name" FHIR DataType +```jsonnet +local name = helpers.parseName(firstName, lastName); +``` + +### Practitioner Pattern +```jsonnet +{ + fullUrl: 'urn:uuid:practitioner-' + userId, + resource: { + resourceType: 'Practitioner', + identifier: [{ system: 'urn:dhis2:user:uid', value: userId }], + name: [practitionerName] + }, + request: { + method: 'PUT', + url: 'Practitioner?identifier=urn:dhis2:user:uid|' + userId + } +} +``` + +### Observation Pattern +```jsonnet +{ + fullUrl: 'urn:uuid:obs-' + obsType + '-' + tei.trackedEntity, + resource: { + resourceType: 'Observation', + status: 'final', + code: { coding: [{ system: 'http://loinc.org', code: '45404-1' }] }, + subject: { reference: 'urn:uuid:' + tei.trackedEntity }, + valueString: value + }, + request: { method: 'POST', url: 'Observation' } +} +``` + +### AllergyIntolerance Pattern +```jsonnet +[{ + fullUrl: 'urn:uuid:allergy-' + tei.trackedEntity + '-' + std.md5(allergy), + resource: { + resourceType: 'AllergyIntolerance', + clinicalStatus: { coding: [{ code: 'active', ... }] }, + patient: { reference: 'urn:uuid:' + tei.trackedEntity }, + reaction: [{ manifestation: [{ text: allergy }] }] + }, + request: { method: 'POST', url: 'AllergyIntolerance' } +} for allergy in allergiesList if std.length(std.stripChars(allergy, ' ')) > 0] +``` + +### Bundle with Flatten +```jsonnet +{ + resourceType: 'Bundle', + type: 'transaction', + entry: std.prune(std.flattenArrays([ + [patient.patient_entry(body)], + [practitioner.practitioner_entry(body)], + [observations.civil_status_observation(body)], + observations.allergies_resources(body) // Returns array + ])) +} +``` + +## Common Issues & Fixes + +| Issue | Fix | +|-------|-----| +| "Cannot find symbol" | `mvn clean compile test-compile` | +| Transformation returns null | Check attribute ID, verify `helpers.getAttrValue()` | +| Nested arrays in bundle | Use `std.flattenArrays()` | +| Reference not found | Ensure `fullUrl` matches reference exactly | +| Validation fails | Add `meta.profile`, check required fields | + +## Code Systems + +For `Observation` FHIR resources, you often need to refer to certain terminologies and code systems. Here is a list of the most common ones, and the ones used in this exercise. Some UID definitions are also included: + +```javascript + LOINC: 'http://loinc.org', + OBS_CATEGORY: 'http://terminology.hl7.org/CodeSystem/observation-category', + ALLERGY_CLINICAL: 'http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical', + ALLERGY_VERIFICATION: 'http://terminology.hl7.org/CodeSystem/allergyintolerance-verification', + DHIS2_USER: 'urn:dhis2:user:uid', + DHIS2_PATIENT: 'urn:dhis2:rmncah:patient-id' +``` + +## LOINC Codes + +- `45404-1` - Marital status (for civil status) +- Category: `social-history` + +## Jsonnet Utilities + +```jsonnet +std.length(arr) // Array/string length +std.split(str, sep) // Split string +std.stripChars(str, chars) // Remove characters +std.prune(obj) // Remove nulls/empty +std.flattenArrays(arr) // Flatten nested arrays +[expr for x in arr if condition] // List comprehension +``` \ No newline at end of file diff --git a/dhis2-to-fhir-patient-bundle-datasonnet/docs/capture.png b/dhis2-to-fhir-patient-bundle-datasonnet/docs/capture.png new file mode 100644 index 0000000..e80f0de Binary files /dev/null and b/dhis2-to-fhir-patient-bundle-datasonnet/docs/capture.png differ diff --git a/dhis2-to-fhir-patient-bundle-datasonnet/docs/image.png b/dhis2-to-fhir-patient-bundle-datasonnet/docs/image.png new file mode 100644 index 0000000..c2ae62e Binary files /dev/null and b/dhis2-to-fhir-patient-bundle-datasonnet/docs/image.png differ diff --git a/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/.gitignore b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/.gitignore new file mode 100644 index 0000000..ee5b36b --- /dev/null +++ b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/.gitignore @@ -0,0 +1,41 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store + +.db-dump/ + +*.log + +data/offset.dat \ No newline at end of file diff --git a/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/Dockerfile b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/Dockerfile new file mode 100644 index 0000000..8871e8d --- /dev/null +++ b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/Dockerfile @@ -0,0 +1,34 @@ +# +# Copyright (c) 2004-2025, University of Oslo +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + +FROM eclipse-temurin:21-jre +COPY target/fhir-sync-agent-1.0.0-SNAPSHOT.jar fhir-sync-agent-1.0.0-SNAPSHOT.jar +EXPOSE 9070 +ENTRYPOINT ["java","-jar","/fhir-sync-agent-1.0.0-SNAPSHOT.jar"] \ No newline at end of file diff --git a/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/README.md b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/README.md new file mode 100644 index 0000000..a3fd7e6 --- /dev/null +++ b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/README.md @@ -0,0 +1,7 @@ +## fhir-sync-agent + +This is the documentation page of fhir-sync-agent. This Apache Camel 4 project uses [Spring Boot](https://spring.io/projects/spring-boot) and the [Camel DHIS2 component](https://camel.apache.org/components/4.4.x/dhis2-component.html). + +From your terminal, within the project root directory path, enter `mvn clean package` to build the project. Enter `mvn clean package -DskipTests` to build the project without running the test suite. The test suite starts Docker containers so you should skip the tests if you do not have [Docker Engine](https://docs.docker.com/engine/) installed locally. Project settings like the DHIS2 server address can be changed from `application.yaml` in `src/main/resources`. Alternatively, you can override the settings when launching the application as [documented](https://docs.spring.io/spring-boot/reference/features/external-config.html#features.external-config.files) in the Spring Boot website. + +Run `java -jar target/fhir-sync-agent-1.0.0-SNAPSHOT.jar` to launch the application from your terminal. If you included [Hawtio](https://hawt.io/) during archetype generation, the web console can be opened from `http://127.0.0.1:9070/management/hawtio`. Note that the default username is `user` and the default password will be printed in the terminal at the time when your application starts. diff --git a/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/license/bsd_3_clause/header.txt b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/license/bsd_3_clause/header.txt new file mode 100644 index 0000000..fc505c5 --- /dev/null +++ b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/license/bsd_3_clause/header.txt @@ -0,0 +1,27 @@ +Copyright (c) 2004-${year}, University of Oslo +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors +may be used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/package.json b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/package.json new file mode 100644 index 0000000..e69de29 diff --git a/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/pom.xml b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/pom.xml new file mode 100644 index 0000000..1081856 --- /dev/null +++ b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/pom.xml @@ -0,0 +1,293 @@ + + + + 4.0.0 + + org.hisp.dhis.integration.camel + fhir-sync-agent + 1.0.0-SNAPSHOT + jar + + + 17 + 17 + UTF-8 + 4.14.0 + 4.2.0 + 3.5.3 + 1.21.3 + 6.10.5 + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring.boot-version} + + + + repackage + + + + + + org.apache.maven.plugins + maven-war-plugin + 3.4.0 + + + + war + + package + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.6.0 + + + enforce + + + + true + No Snapshots Allowed! + + + 17 + + + + + enforce + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.3 + + false + + + + com.mycila + license-maven-plugin + 4.6 + + + 2025 + + + +
license/bsd_3_clause/header.txt
+ + **/README + +
+
+
+ + + format + + format + + process-sources + + +
+
+
+ + + + + org.apache.camel + camel-bom + ${camel.version} + pom + import + + + org.apache.camel.springboot + camel-spring-boot-bom + ${camel.version} + pom + import + + + org.springframework.boot + spring-boot-dependencies + ${spring.boot-version} + pom + import + + + org.testcontainers + testcontainers-bom + ${testcontainers.version} + pom + import + + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-security + + + org.apache.camel.springboot + camel-spring-boot-starter + + + org.apache.camel.springboot + camel-jetty-starter + + + org.apache.camel.springboot + camel-datasonnet-starter + + + org.apache.camel.springboot + camel-jackson-starter + + + org.apache.camel.springboot + camel-management-starter + + + org.apache.camel.springboot + camel-yaml-dsl-starter + + + org.apache.camel.springboot + camel-spring-boot-xml-starter + + + org.apache.camel.springboot + camel-http-starter + + + org.apache.camel.springboot + camel-debezium-postgres-starter + + + org.apache.camel.springboot + camel-dhis2-starter + + + io.hawt + hawtio-springboot + ${hawtio.version} + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.apache.camel.springboot + camel-fhir-starter + test + + + + ca.uhn.hapi.fhir + hapi-fhir-base + ${hapi.fhir.version} + test + + + ca.uhn.hapi.fhir + hapi-fhir-client + ${hapi.fhir.version} + test + + + ca.uhn.hapi.fhir + hapi-fhir-structures-r4 + ${hapi.fhir.version} + test + + + ca.uhn.hapi.fhir + hapi-fhir-validation + ${hapi.fhir.version} + test + + + ca.uhn.hapi.fhir + hapi-fhir-validation-resources-r4 + ${hapi.fhir.version} + test + + + org.apache.camel + camel-test-spring-junit5 + test + + + org.testcontainers + junit-jupiter + test + + + org.testcontainers + postgresql + test + + + io.rest-assured + rest-assured + 5.5.5 + test + + + +
diff --git a/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/main/java/org/hisp/dhis/integration/camel/Application.java b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/main/java/org/hisp/dhis/integration/camel/Application.java new file mode 100644 index 0000000..e0634dc --- /dev/null +++ b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/main/java/org/hisp/dhis/integration/camel/Application.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2004-2025, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.integration.camel; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; + +@SpringBootApplication +public class Application extends SpringBootServletInitializer { + public static void main(String[] args) { + SpringApplication springApplication = new SpringApplication(Application.class); + springApplication.run(args); + } +} diff --git a/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/main/java/org/hisp/dhis/integration/camel/security/DefaultSecurityConfig.java b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/main/java/org/hisp/dhis/integration/camel/security/DefaultSecurityConfig.java new file mode 100644 index 0000000..fcdfbcc --- /dev/null +++ b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/main/java/org/hisp/dhis/integration/camel/security/DefaultSecurityConfig.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2004-2025, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.integration.camel.security; + +import static org.springframework.security.config.Customizer.withDefaults; + +import jakarta.servlet.DispatcherType; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.function.Supplier; + +import org.springframework.boot.web.context.WebServerInitializedEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; +import org.springframework.security.web.csrf.CookieCsrfTokenRepository; +import org.springframework.security.web.csrf.CsrfToken; +import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler; +import org.springframework.security.web.csrf.CsrfTokenRequestHandler; +import org.springframework.security.web.csrf.XorCsrfTokenRequestAttributeHandler; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +@Configuration +public class DefaultSecurityConfig implements ApplicationListener { + + @Bean + protected SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + return http.authorizeHttpRequests( + a -> + a.dispatcherTypeMatchers(DispatcherType.ASYNC) + .permitAll() + .requestMatchers("/login", "/logout") + .permitAll() + .requestMatchers("/management/**") + .hasRole("ADMIN") + .anyRequest() + .denyAll()) + .formLogin(withDefaults()) + .httpBasic(withDefaults()) + .csrf( + csrf -> + csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) + .csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler())) + .addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class) + .build(); + } + + @Override + public void onApplicationEvent(WebServerInitializedEvent event) { + /* + * Logic to execute when the web server is initialized + */ + } + + static class SpaCsrfTokenRequestHandler extends CsrfTokenRequestAttributeHandler { + private final CsrfTokenRequestHandler delegate = new XorCsrfTokenRequestAttributeHandler(); + + @Override + public void handle( + HttpServletRequest request, HttpServletResponse response, Supplier csrfToken) { + /* + * Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection of + * the CsrfToken when it is rendered in the response body. + */ + this.delegate.handle(request, response, csrfToken); + } + + @Override + public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) { + if (StringUtils.hasText(request.getHeader(csrfToken.getHeaderName()))) { + return super.resolveCsrfTokenValue(request, csrfToken); + } + return this.delegate.resolveCsrfTokenValue(request, csrfToken); + } + } + + static class CsrfCookieFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal( + HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + CsrfToken csrfToken = (CsrfToken) request.getAttribute("_csrf"); + // Render the token value to a cookie by causing the deferred token to be loaded + csrfToken.getToken(); + + filterChain.doFilter(request, response); + } + } +} diff --git a/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/main/resources/application.yaml b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/main/resources/application.yaml new file mode 100644 index 0000000..5b6f812 --- /dev/null +++ b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/main/resources/application.yaml @@ -0,0 +1,69 @@ +# +# Copyright (c) 2004-2025, University of Oslo +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + +server: + port: 9070 + address: 0.0.0.0 + error: + whitelabel: + enabled: false + servlet: + session: + cookie: + same-site: strict + +camel: + springboot: + main-run-controller: true + dataformat: + json-jackson: + auto-discover-object-mapper: true + +management: + server: + port: 9071 + endpoints: + web: + base-path: /management + exposure: + include: '*' + health: + show-details: always + +spring: + jmx: + enabled: true + +hawtio: + authenticationEnabled: false + +logging: + level: + root: INFO \ No newline at end of file diff --git a/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/main/resources/camel/upsert-patient-route.camel.yaml b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/main/resources/camel/upsert-patient-route.camel.yaml new file mode 100644 index 0000000..5bdc33e --- /dev/null +++ b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/main/resources/camel/upsert-patient-route.camel.yaml @@ -0,0 +1,104 @@ +# +# Copyright (c) 2004-2025, University of Oslo +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + +- route: + id: captureChangeRoute + description: Listens for tracked entity changes in the source DHIS2 database. + from: + description: | + Captures all database row changes applied to the `trackedentity` table. + uri: debezium-postgres:dhis2 + parameters: + databaseHostname: "{{dhis2DatabaseHostname}}" + databasePort: "{{dhis2DatabasePort}}" + databaseUser: "{{dhis2DatabaseUser}}" + databasePassword: "{{dhis2DatabasePassword}}" + databaseDbname: "{{dhis2DatabaseDbName}}" + offsetStorageFileName: "{{offsetStorageFileName:offset.dat}}" + topicPrefix: dhis2 + pluginName: pgoutput + tableIncludeList: "{{tableIncludeList:public.trackedentity}}" + columnIncludeList: ".*" + schemaIncludeList: "{{schemaIncludeList:public}}" + slotName: "{{slotName:slot1}}" + snapshotMode: "{{snapshotMode:no_data}}" + publicationAutocreateMode: "{{publicationAutocreateMode:all_tables}}" + publicationName: "{{publicationName:dbz_publication}}" + steps: + - filter: + description: Filters out delete sync operations since a delete operation does not import data. + simple: ${headers.CamelDebeziumOperation} != 'd' + - log: "Capturing database change [${headers}]..." + - to: direct:fetchTrackedEntity + - transform: + datasonnet: + outputMediaType: application/json + resultType: String + bodyMediaType: application/json + expression: resource:classpath:fhirBundle.ds + - to: direct:upsertPatient + +- route: + id: fetchTrackedEntity + description: | + Fetches the changed tracked entity from the DHIS2 Web API. + from: + uri: direct:fetchTrackedEntity + steps: + - toD: + description: Fetches the tracked entity from the source DHIS2 server. + uri: dhis2:get/resource + parameters: + path: tracker/trackedEntities/${body.get('uid')} + fields: "*" + baseApiUrl: ${properties:dhis2ApiUrl} + username: ${properties:dhis2ApiUsername:} + password: ${properties:dhis2ApiPassword:} + personalAccessToken: ${properties:dhis2ApiPersonalAccessToken:} + - log: | + Fetched tracked entity => + + ${body} + +- route: + id: upsertPatient + from: + uri: direct:upsertPatient + steps: + - setHeader: + name: Content-Type + constant: application/json + - toD: + uri: "{{fhir-url}}" + parameters: + httpMethod: POST + oauth2ClientId: "{{oauth2.clientId}}" + oauth2ClientSecret: "{{oauth2.clientSecret}}" + oauth2TokenEndpoint: "{{oauth2.tokenEndpoint}}" \ No newline at end of file diff --git a/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/main/resources/fhirBundle.ds b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/main/resources/fhirBundle.ds new file mode 100644 index 0000000..7605aaf --- /dev/null +++ b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/main/resources/fhirBundle.ds @@ -0,0 +1,19 @@ +// This DataSonnet script transforms DHIS2 WHO RMNCAH Tracked Entities +// into FHIR IPS-compliant transaction bundles. +// +// Input: DHIS2 Tracked Entity JSON (body) +// Output: FHIR Bundle (transaction type) with IPS-conformant resources + +local patient = import 'patientResource.libsonnet'; + +{ + resourceType: "Bundle", + type: "transaction", + + // Build bundle entries + // Currently includes: Patient resource + // Can be extended with: Practitioner, Encounter, Observation, AllergyIntolerance, etc. + entry: std.prune([ + patient.patient_entry(body) + ]) +} \ No newline at end of file diff --git a/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/main/resources/helperFunctions.libsonnet b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/main/resources/helperFunctions.libsonnet new file mode 100644 index 0000000..189ea59 --- /dev/null +++ b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/main/resources/helperFunctions.libsonnet @@ -0,0 +1,320 @@ +// Helper Functions Library for DHIS2 to FHIR IPS Mapping +// This library provides reusable utilities for extracting and transforming +// DHIS2 Tracked Entity data into FHIR-compliant structures. + +{ + // ============================================================================ + // ATTRIBUTE LOOKUP + // ============================================================================ + + /** + * Get attribute value from TEI + * + * @param tei: The tracked entity instance object + * @param attrId: The DHIS2 attribute UID to look for + * @returns: The attribute value or null if not found + * + * Rationale: Enrollment attributes are program-specific and take precedence + * over TEI-level attributes. This ensures we get the most contextual data. + */ + getAttrValue(tei, attrId):: + local attributes = if std.length(tei.enrollments default []) > 0 + then tei.enrollments[0].attributes + else []; + + local attributeMatch = std.filter( + function(attr) attr.attribute == attrId, + attributes + ); + if std.length(attributeMatch) > 0 then + attributeMatch[0].value + else + null, + + /** + * Get multiple attribute values at once + * + * @param tei: The tracked entity instance object + * @param attrIds: Array of DHIS2 attribute UIDs + * @returns: Object with attrId as key and value as value + * + * Usage: getAttrValues(tei, ['attr1', 'attr2']) => { attr1: 'value1', attr2: null } + */ + getAttrValues(tei, attrIds):: + { + [attrId]: $.getAttrValue(tei, attrId) + for attrId in attrIds + }, + + // ============================================================================ + // NAME PARSING AND TRANSFORMATION + // ============================================================================ + + /** + * Parse first and last name into FHIR HumanName structure + * + * @param firstName: Given name (can be null) + * @param lastName: Family name (can be null) + * @returns: FHIR HumanName object or null if both are empty + * + * IPS Constraint: ips-pat-1 requires at least one of given, family, or text + * This function ensures compliance by including 'text' field. + */ + parseName(firstName, lastName):: + local hasFirst = firstName != null && std.length(std.toString(firstName)) > 0; + local hasLast = lastName != null && std.length(std.toString(lastName)) > 0; + + if !hasFirst && !hasLast then + // No name data available + null + else + { + // Always include text to satisfy ips-pat-1 constraint + [if hasFirst || hasLast then 'text']: + std.join(' ', std.prune([firstName, lastName])), + // Include structured parts when available + [if hasFirst then 'given']: [firstName], + [if hasLast then 'family']: lastName, + }, + + /** + * Parse full name string into structured components + * Useful when DHIS2 stores full name in a single field + * + * @param fullName: Complete name as single string "FirstName LastName" + * @returns: FHIR HumanName object with given and family parsed + * + * Limitation: Assumes Western name order (given names first, family name last) + */ + parseFullName(fullName):: + if fullName == null || std.length(std.toString(fullName)) == 0 then + null + else + local parts = std.split(fullName, ' '); + local names = [p for p in parts if std.length(p) > 0]; + local n = std.length(names); + + if n == 0 then + null + else if n == 1 then + { + text: fullName, + family: names[0], + } + else + { + text: fullName, + given: [names[i] for i in std.range(0, n - 2)], + family: names[n - 1], + }, + + // ============================================================================ + // ADDRESS PARSING AND TRANSFORMATION + // ============================================================================ + + /** + * Build FHIR Address from separate components + * + * @param line: Street address (single line or null) + * @param city: City name + * @param postalCode: Postal/ZIP code + * @param country: Country code (ISO 3166-1 alpha-2 recommended) + * @param state: State/province (optional) + * @param district: District/county (optional) + * @returns: FHIR Address object or null if all components are empty + * + * Note: Returns null instead of empty object to allow std.prune() to remove it + */ + parseAddress(line, city, postalCode, country, state=null, district=null):: + local hasLine = line != null && std.length(std.toString(line)) > 0; + local hasCity = city != null && std.length(std.toString(city)) > 0; + local hasPostal = postalCode != null && std.length(std.toString(postalCode)) > 0; + local hasCountry = country != null && std.length(std.toString(country)) > 0; + local hasState = state != null && std.length(std.toString(state)) > 0; + local hasDistrict = district != null && std.length(std.toString(district)) > 0; + + if !hasLine && !hasCity && !hasPostal && !hasCountry && !hasState && !hasDistrict then + null + else + { + [if hasLine then 'line']: [line], + [if hasCity then 'city']: city, + [if hasDistrict then 'district']: district, + [if hasState then 'state']: state, + [if hasPostal then 'postalCode']: postalCode, + [if hasCountry then 'country']: country, + }, + + /** + * Parse comma-separated address string into structured components + * Expected format: "Street, PostalCode City, District, State, Country" + * + * @param address: Full address as comma-separated string + * @returns: FHIR Address object + * + * Example: "123 Main St, 10001 New York, Manhattan, NY, USA" + */ + parseAddressString(address):: + if address == null || std.length(std.toString(address)) == 0 then + null + else + local parts = std.map(function(s) std.stripChars(s, ' \t'), std.split(address, ',')); + { + [if std.length(parts) > 0 then 'line']: [parts[0]], + [if std.length(parts) > 1 then 'postalCode']: std.split(parts[1], ' ')[0], + [if std.length(parts) > 1 && std.length(std.split(parts[1], ' ')) > 1 then 'city']: + std.join(' ', std.split(parts[1], ' ')[1:]), + [if std.length(parts) > 2 then 'district']: parts[2], + [if std.length(parts) > 3 then 'state']: parts[3], + [if std.length(parts) > 4 then 'country']: parts[4], + }, + + // ============================================================================ + // TELECOM (CONTACT POINT) FUNCTIONS + // ============================================================================ + + /** + * Build FHIR ContactPoint (telecom entry) + * + * @param system: phone | fax | email | pager | url | sms | other + * @param value: The actual phone number, email address, etc. + * @param use: home | work | temp | old | mobile (optional) + * @returns: FHIR ContactPoint object or null if value is empty + * + * Validation: Returns null for empty values to prevent invalid FHIR resources + */ + buildTelecom(system, value, use=null):: + if value == null || std.length(std.toString(value)) == 0 then + null + else + { + system: system, + value: value, + [if use != null then 'use']: use, + }, + + /** + * Build array of telecom entries from phone and email + * Convenience function for common case + * + * @param phone: Phone number + * @param email: Email address + * @returns: Array of ContactPoint objects (pruned of nulls) + */ + buildTelecomArray(phone=null, email=null):: + std.prune([ + $.buildTelecom('phone', phone, 'mobile'), + $.buildTelecom('email', email, null), + ]), + + // ============================================================================ + // IDENTIFIER FUNCTIONS + // ============================================================================ + + /** + * Build FHIR Identifier with proper structure + * + * @param system: URI identifying the identifier system + * @param value: The actual identifier value + * @param use: official | usual | temp | secondary | old (optional) + * @param type: CodeableConcept describing identifier type (optional) + * @returns: FHIR Identifier object or null if value is empty + * + * Best Practice: Always provide system URI for proper identifier matching + */ + buildIdentifier(system, value, use='official', type=null):: + if value == null || std.length(std.toString(value)) == 0 then + null + else + { + use: use, + [if type != null then 'type']: type, + system: system, + value: value, + }, + + // ============================================================================ + // DATA TYPE CONVERSION FUNCTIONS + // ============================================================================ + + /** + * Convert DHIS2 gender to FHIR administrative gender + * + * @param dhis2Gender: Gender value from DHIS2 (various formats) + * @returns: FHIR gender code: male | female | other | unknown + * + * Handles common variations and returns 'unknown' for unmappable values + */ + convertGender(dhis2Gender):: + if dhis2Gender == null then + null + else + local normalized = std.asciiLower(std.toString(dhis2Gender)); + if std.startsWith(normalized, 'male') then + 'male' + else if std.startsWith(normalized, 'female') then + 'female' + else if normalized == 'm' then + 'male' + else if normalized == 'f' then + 'female' + else if std.startsWith(normalized, 'other') then + 'other' + else + 'unknown', + + /** + * Validate and format date string to FHIR date format (YYYY-MM-DD) + * + * @param dateStr: Date string in various formats + * @returns: ISO 8601 date string or null if invalid + * + * Note: Currently passes through assuming DHIS2 provides ISO format + * Could be extended to handle format conversion + */ + formatDate(dateStr):: + if dateStr == null || std.length(std.toString(dateStr)) == 0 then + null + else + // DHIS2 typically provides ISO 8601 dates + // Extract just the date part (YYYY-MM-DD) + std.substr(dateStr, 0, 10), + + // ============================================================================ + // UTILITY FUNCTIONS + // ============================================================================ + + /** + * Check if a value is effectively empty (null, empty string, empty array, empty object) + * + * @param value: Any value to check + * @returns: true if value is empty, false otherwise + */ + isEmpty(value):: + value == null + || (std.isString(value) && std.length(value) == 0) + || (std.isArray(value) && std.length(value) == 0) + || (std.isObject(value) && std.length(std.objectFields(value)) == 0), + + /** + * Safe string conversion that handles null + * + * @param value: Value to convert to string + * @returns: String representation or empty string if null + */ + safeString(value):: + if value == null then '' else std.toString(value), + + /** + * Get first non-null value from array of options + * Useful for fallback chains + * + * @param options: Array of potential values + * @returns: First non-null value or null if all are null + * + * Usage: coalesce([null, '', 'value', 'other']) => '' + */ + coalesce(options):: + local nonNull = std.filter(function(v) v != null, options); + if std.length(nonNull) > 0 then nonNull[0] else null, +} diff --git a/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/main/resources/observationResources.todo.libsonnet b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/main/resources/observationResources.todo.libsonnet new file mode 100644 index 0000000..14a3862 --- /dev/null +++ b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/main/resources/observationResources.todo.libsonnet @@ -0,0 +1,167 @@ +// TODO: Observation and AllergyIntolerance Resources +// Your task: Map DHIS2 attributes (civil status, allergies) that don't fit IPS Patient profile + +// If you get stuck, check solutions/observationResources.solution.libsonnet + +// CIVIL STATUS CHECKLIST: +// [ ] Extract civil status with helpers.getAttrValue +// [ ] Return null if no data +// [ ] Set resourceType='Observation' +// [ ] Add category with social-history code +// [ ] Add LOINC code 45404-1 (Marital status) +// [ ] Add subject reference to Patient +// [ ] Add valueString with civil status value +// [ ] Add PUT request + +// ALLERGIES CHECKLIST: +// [ ] Extract allergies value +// [ ] Split by comma into array +// [ ] Use list comprehension to create multiple resources +// [ ] Set resourceType='AllergyIntolerance' +// [ ] Add clinicalStatus='active' +// [ ] Add verificationStatus='unconfirmed' +// [ ] Add patient reference +// [ ] Add reaction manifestation with allergy text +// [ ] Filter empty values +// [ ] Add PUT request for each + +// INTEGRATION CHECKLIST: +// [ ] Update fhirBundle.ds to import this file +// [ ] Add civil_status_observation to bundle entry array +// [ ] Add allergies_resources to bundle entry array +// [ ] Use std.flattenArrays since allergies_resources returns array +// [ ] Run: mvn test -Dtest=IpsPatientMappingTestCase#testCivilStatusObservation +// [ ] Run: mvn test -Dtest=IpsPatientMappingTestCase#testAllergiesResource + +local helpers = import 'helperFunctions.libsonnet'; + +{ + // TODO 1: Define attribute ID constants + // Hint: Find these IDs in trackedEntity.json + + // local ATTR_CIVIL_STATUS = ..., + // local ATTR_ALLERGIES = ..., + + /** + * Creates an Observation resource for civil status + * + * Why Observation? Civil status doesn't fit in IPS Patient profile, + * but we don't want to lose this data (lossless mapping). + * + * @param tei - DHIS2 Tracked Entity Instance + * @return Bundle entry with Observation or null if no data + */ + civil_status_observation(tei):: + // TODO 2: Extract civil status value using helpers.getAttrValue + // local civilStatus = helpers. .... + + // TODO 3: Return null if no data (will be removed by std.prune) + // if civilStatus == null then null else + + // TODO 4: Create unique identifier (observationId) using attributeId + teiId + // Hint: local observationId = ATTR_CIVIL_STATUS + '-' + tei.trackedEntity + // local observationId = ... + + { + // TODO 5: Create unique fullUrl using observationId + // fullUrl: ... + + resource: std.prune({ + // TODO 6: Set resourceType to 'Observation' + // resourceType: ... + + // TODO 7: Set status to 'final' + // status: ... + + // TODO 8: Add identifier for conditional update + // Hint: system='urn:dhis2:observation:attribute', value=observationId + // identifier: [...] + + // TODO 9: Add category array with social-history coding + // Hint: system='http://terminology.hl7.org/CodeSystem/observation-category' + // code='social-history', display='Social History' + // category: [...] + + // TODO 10: Add code with LOINC coding + // Hint: system='http://loinc.org', code='45404-1', display='Marital status' + // text='Civil Status' + // code: { ... } + + // TODO 11: Add subject reference to Patient + // Hint: reference='urn:uuid:' + tei.trackedEntity, type='Patient' + // subject: { ... } + + // TODO 12: Add valueString with the civilStatus value + // valueString: ... + }), + + // TODO 13: Add PUT request with conditional update using identifier + // Hint: method='PUT', url='Observation?identifier=urn:dhis2:observation:attribute|' + observationId + // request: { ... } + }, + + /** + * ADVANCED + * Creates AllergyIntolerance resources for allergies + * + * DHIS2 stores allergies as MULTI_TEXT (comma-separated values). + * We create one AllergyIntolerance resource per allergy. + * + * @param tei - DHIS2 Tracked Entity Instance + * @return Array of bundle entries (one per allergy) + */ + allergies_resources(tei):: + // TODO 14: Extract allergies value + // local allergiesValue = ... + + // TODO 15: Split multi-text value by comma + // Hint: Use std.split(allergiesValue, ',') or return empty array if null + // local allergiesList = ... + + // TODO 16: Create array of AllergyIntolerance resources using list comprehension + // Hint: [{ resource } for allergy in allergiesList if condition] + [ + // TODO 17: Strip whitespace and create unique identifier + // Hint: local allergyText = std.stripChars(allergy, ' '); + // local allergyId = ATTR_ALLERGIES + '-' + tei.trackedEntity + '-' + allergyText; + // local allergyText = ... + // local allergyId = ... + + { + // TODO 18: Create unique fullUrl using allergyId + // fullUrl: ... + + resource: std.prune({ + // TODO 19: Set resourceType to 'AllergyIntolerance' + // resourceType: ... + + // TODO 20: Add identifier for conditional update + // Hint: system='urn:dhis2:allergyintolerance:attribute', value=allergyId + // identifier: [...] + + // TODO 21: Add clinicalStatus with 'active' code + // Hint: system='http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical' + // clinicalStatus: { ... } + + // TODO 22: Add verificationStatus with 'unconfirmed' code + // Hint: system='http://terminology.hl7.org/CodeSystem/allergyintolerance-verification' + // verificationStatus: { ... } + + // TODO 23: Add patient reference + // Hint: Same pattern as Observation subject + // patient: { ... } + + // TODO 24: Add reaction array with manifestation + // Hint: Use allergyText variable (already stripped) + // reaction: [...] + }), + + // TODO 25: Add PUT request with conditional update using identifier + // Hint: method='PUT', url='AllergyIntolerance?identifier=urn:dhis2:allergyintolerance:attribute|' + allergyId + // request: { ... } + } + // TODO 26: Add list comprehension filter + // Hint: for allergy in allergiesList if std.length(std.stripChars(allergy, ' ')) > 0 + // for allergy in ... if ... + ], +} diff --git a/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/main/resources/patientResource.libsonnet b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/main/resources/patientResource.libsonnet new file mode 100644 index 0000000..f6ca27d --- /dev/null +++ b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/main/resources/patientResource.libsonnet @@ -0,0 +1,181 @@ +// Patient Resource Builder for DHIS2 to FHIR IPS Mapping +// Transforms DHIS2 WHO RMNCAH Tracked Entity to FHIR IPS Patient profile +// +// FHIR Profile: http://hl7.org/fhir/uv/ips/StructureDefinition/Patient-uv-ips +// IPS Constraint: ips-pat-1 - Patient.name.given, Patient.name.family or Patient.name.text SHALL be present + +local helpers = import 'helperFunctions.libsonnet'; + +{ + /** + * Create a FHIR Bundle entry for a Patient resource conforming to IPS profile + * + * @param tei: DHIS2 Tracked Entity Instance with enrollments and attributes + * @returns: FHIR Bundle entry with Patient resource and conditional update request + * + * Bundle Entry Structure: + * - fullUrl: Temporary UUID reference for use within the bundle + * - resource: The actual Patient resource + * - request: Conditional PUT to upsert based on identifier + */ + patient_entry(tei):: + // ======================================================================== + // STEP 1: Define DHIS2 Attribute IDs + // ======================================================================== + // These UIDs correspond to WHO RMNCAH metadata structure + // Mapping based on trackedEntity.json example + + local ATTR_UNIQUE_ID = 'lZGmxYbs97q'; // MMD_PER_ID - Unique ID + local ATTR_FIRST_NAME = 'w75KJ2mc4zz'; // MMD_PER_NAM - First name + local ATTR_LAST_NAME = 'zDhUuAYrxNC'; // Last name + local ATTR_DOB = 'gHGyrwKPzej'; // MMD_PER_DOB - Birth date + local ATTR_ADDRESS = 'VqEFza8wbwA'; // MMD_PER_ADR1 - Address + local ATTR_CITY = 'FO4sWYJ64LQ'; // City + local ATTR_POSTAL = 'ZcBPrXKahq2'; // Postal code + local ATTR_MOBILE = 'Agywv2JGwuq'; // MMD_PER_MOB - Mobile number + local ATTR_EMAIL = 'KmEUg2hHEtx'; // Email address + local ATTR_CIVIL_STATUS = 'ciq2USN94oJ'; // MMD_PER_STA - Civil status + + // ======================================================================== + // STEP 2: Extract Attribute Values + // ======================================================================== + + local uniqueId = helpers.getAttrValue(tei, ATTR_UNIQUE_ID); + local firstName = helpers.getAttrValue(tei, ATTR_FIRST_NAME); + local lastName = helpers.getAttrValue(tei, ATTR_LAST_NAME); + local birthDate = helpers.getAttrValue(tei, ATTR_DOB); + local addressLine = helpers.getAttrValue(tei, ATTR_ADDRESS); + local city = helpers.getAttrValue(tei, ATTR_CITY); + local postalCode = helpers.getAttrValue(tei, ATTR_POSTAL); + local mobile = helpers.getAttrValue(tei, ATTR_MOBILE); + local email = helpers.getAttrValue(tei, ATTR_EMAIL); + + // ======================================================================== + // STEP 3: Transform Complex Data Types + // ======================================================================== + + // 3.1 Parse name into FHIR HumanName structure + // IPS requires at least one of: given, family, or text + local patientName = helpers.parseName(firstName, lastName); + + // 3.2 Build address from separate components + // Returns null if all components are empty (will be pruned) + local patientAddress = helpers.parseAddress( + line=addressLine, + city=city, + postalCode=postalCode, + country=null // Not provided in this example + ); + + // 3.3 Build telecom (contact points) array + // Prunes null entries automatically + local telecomItems = std.prune([ + helpers.buildTelecom('phone', mobile, 'mobile'), + helpers.buildTelecom('email', email, null) + ]); + + // 3.4 Format birth date to FHIR format (YYYY-MM-DD) + local formattedBirthDate = helpers.formatDate(birthDate); + + // ======================================================================== + // STEP 4: Build Identifier Array + // ======================================================================== + // IPS recommends including patient identifiers for matching + // Using conditional array construction to include only when value exists + + local identifiers = std.prune([ + // Primary identifier - official use + if uniqueId != null then helpers.buildIdentifier( + system='urn:dhis2:rmncah:patient-id', + value=uniqueId, + use='official' + ) + ]); + + // ======================================================================== + // STEP 5: Construct FHIR Patient Resource + // ======================================================================== + // Using std.prune() to remove null/empty fields for clean FHIR output + + { + // Bundle entry metadata + fullUrl: 'urn:uuid:' + tei.trackedEntity, + + // The actual Patient resource + resource: std.prune({ + resourceType: 'Patient', + + // IPS Profile declaration - REQUIRED for IPS conformance + meta: { + profile: ['http://hl7.org/fhir/uv/ips/StructureDefinition/Patient-uv-ips'] + }, + + // Identifiers - recommended for patient matching + identifier: identifiers, + + // Name - REQUIRED by IPS (min cardinality 1) + // ips-pat-1 constraint: Must have given, family, or text + name: if patientName != null then [patientName] else [], + + // Telecom - contact information + // Include if any contact methods are available + [if std.length(telecomItems) > 0 then 'telecom']: telecomItems, + + // Birth date - important demographic information + [if formattedBirthDate != null then 'birthDate']: formattedBirthDate, + + // Address - physical address information + [if patientAddress != null then 'address']: [patientAddress], + }), + + // ======================================================================== + // STEP 6: Define Bundle Request (Conditional Update) + // ======================================================================== + // Using conditional PUT for idempotent upsert behavior: + // - If patient with this identifier exists: UPDATE + // - If patient doesn't exist: CREATE + // This prevents duplicate patient records + + request: { + method: 'PUT', + url: 'Patient?identifier=urn:dhis2:rmncah:patient-id|' + uniqueId, + }, + }, + + /** + * Create a minimal Patient entry with only required fields + * Useful for testing minimum IPS compliance + * + * @param tei: DHIS2 Tracked Entity Instance + * @returns: FHIR Bundle entry with minimal Patient resource + */ + minimal_patient_entry(tei):: + local ATTR_UNIQUE_ID = 'lZGmxYbs97q'; + local ATTR_FIRST_NAME = 'w75KJ2mc4zz'; + local ATTR_LAST_NAME = 'zDhUuAYrxNC'; + local ATTR_DOB = 'gHGyrwKPzej'; + + local uniqueId = helpers.getAttrValue(tei, ATTR_UNIQUE_ID); + local firstName = helpers.getAttrValue(tei, ATTR_FIRST_NAME); + local lastName = helpers.getAttrValue(tei, ATTR_LAST_NAME); + local dob = helpers.getAttrValue(tei, ATTR_DOB); + + local patientName = helpers.parseName(firstName, lastName); + + { + fullUrl: 'urn:uuid:' + tei.trackedEntity, + resource: { + resourceType: 'Patient', + meta: { + profile: ['http://hl7.org/fhir/uv/ips/StructureDefinition/Patient-uv-ips'] + }, + + name: if patientName != null then [patientName] else [{ text: 'Unknown' }], + dateOfBirth: if dob != null then helpers.formatDate(dob) else null, + }, + request: { + method: 'POST', + url: 'Patient', + }, + }, +} diff --git a/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/main/resources/practitionerResource.todo.libsonnet b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/main/resources/practitionerResource.todo.libsonnet new file mode 100644 index 0000000..b386b71 --- /dev/null +++ b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/main/resources/practitionerResource.todo.libsonnet @@ -0,0 +1,62 @@ +// TODO: Practitioner Resource Mapping +// Your task: Complete this implementation to map DHIS2 user (createdBy) to FHIR Practitioner +// +// If you get stuck, check solutions/practitionerResource.solution.libsonnet + +// CHECKLIST: +// [ ] Extract all fields from tei.createdBy +// [ ] Use helpers.parseName for name transformation +// [ ] Set correct resourceType +// [ ] Add identifier with urn:dhis2:user:uid system +// [ ] Add name array +// [ ] Create conditional PUT request +// [ ] Update patientResource.libsonnet to add generalPractitioner reference +// [ ] Update fhirBundle.ds to import and include this resource +// [ ] Run: mvn test -Dtest=IpsPatientMappingTestCase#testPatientReferencesPractitioner + +local helpers = import 'helperFunctions.libsonnet'; + +{ + /** + * Creates a FHIR Practitioner resource from DHIS2 TEI createdBy information + * + * @param tei - DHIS2 Tracked Entity Instance object + * @return Bundle entry with Practitioner resource and conditional PUT request + */ + practitioner_entry(tei):: + // TODO 1: Extract practitioner information from tei.createdBy + // Hint: You need uid, firstName, surname, and username + // local practitionerId = ... + // local firstName = ... + // local lastName = ... + // local username = ... + + // TODO 2: Use helpers.parseName to create practitionerName + // Hint: This is the same function used in patientResource.libsonnet + // local practitionerName = ... + + // TODO 3: Build the bundle entry + { + // TODO 3a: Create unique fullUrl + // Hint: Use pattern 'urn:uuid:practitioner-' + practitionerId + // fullUrl: ... + + resource: std.prune({ + // TODO 3b: Set resourceType + // resourceType: ... + + // TODO 3c: Add identifier array with system and value + // Hint: system should be 'urn:dhis2:user:uid', you can use helpers.buildIdentifier(...) + // identifier: [...] + + // TODO 3d: Add name array + // Hint: Wrap practitionerName in array brackets + // name: ... + + }), + + // TODO 3e: Add conditional PUT request + // Hint: method='PUT', url='Practitioner?identifier=urn:dhis2:user:uid|' + practitionerId + // request: { ... } + }, +} diff --git a/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/main/resources/solutions/observationResources.solution.libsonnet b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/main/resources/solutions/observationResources.solution.libsonnet new file mode 100644 index 0000000..88291e8 --- /dev/null +++ b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/main/resources/solutions/observationResources.solution.libsonnet @@ -0,0 +1,155 @@ +// SOLUTION: Observation and AllergyIntolerance Resources +// Maps DHIS2 attributes that don't fit IPS Patient profile + +local helpers = import '../helperFunctions.libsonnet'; + +{ + // DHIS2 Attribute ID Constants + local ATTR_CIVIL_STATUS = 'ciq2USN94oJ', // Civil status (e.g., "Single or widow") + local ATTR_ALLERGIES = 'gu1fqsmoU8r', // Allergies (MULTI_TEXT type) + + /** + * Creates an Observation resource for civil status + * + * Civil status doesn't map to IPS Patient profile, so we use Observation + * to map with proper LOINC coding. + * + * @param tei - DHIS2 Tracked Entity Instance + * @return Bundle entry with Observation or null if no data + */ + civil_status_observation(tei):: + local civilStatus = helpers.getAttrValue(tei, ATTR_CIVIL_STATUS); + + // Only create observation if data exists + if civilStatus == null then null else + + // Unique identifier: attributeId + teiId + local observationId = ATTR_CIVIL_STATUS + '-' + tei.trackedEntity; + + { + fullUrl: 'urn:uuid:' + observationId, + + resource: std.prune({ + resourceType: 'Observation', + status: 'final', + + // Identifier for conditional update (attributeId + teiId) + identifier: [{ + system: 'urn:dhis2:observation:attribute', + value: observationId + }], + + // Category: social-history (demographic/social information) + category: [{ + coding: [{ + system: 'http://terminology.hl7.org/CodeSystem/observation-category', + code: 'social-history', + display: 'Social History' + }] + }], + + // Code: LOINC 45404-1 = Marital status + code: { + coding: [{ + system: 'http://loinc.org', + code: '45404-1', + display: 'Marital status' + }], + text: 'Civil Status' + }, + + // Subject: reference to Patient + subject: { + reference: 'urn:uuid:' + tei.trackedEntity, + type: 'Patient' + }, + + // Value: the actual civil status text + valueString: civilStatus, + }), + + // PUT with identifier query: updates if exists, creates if not + request: { + method: 'PUT', + url: 'Observation?identifier=urn:dhis2:observation:attribute|' + observationId + } + }, + + /** + * Creates AllergyIntolerance resources for allergies + * + * DHIS2 stores allergies as MULTI_TEXT (comma-separated). We create + * a separate AllergyIntolerance resource for each allergy. + * + * @param tei - DHIS2 Tracked Entity Instance + * @return Array of bundle entries (one per allergy) + */ + allergies_resources(tei):: + local allergiesValue = helpers.getAttrValue(tei, ATTR_ALLERGIES); + + // Parse multi-text value (split by comma) + local allergiesList = if allergiesValue == null then [] + else std.split(allergiesValue, ','); + + // Create AllergyIntolerance for each allergy + [ + // Unique identifier: attributeId + teiId + stripped allergy text + local allergyText = std.stripChars(allergy, ' '); + local allergyId = ATTR_ALLERGIES + '-' + tei.trackedEntity + '-' + allergyText; + + { + fullUrl: 'urn:uuid:' + allergyId, + + resource: std.prune({ + resourceType: 'AllergyIntolerance', + + // Identifier for conditional update (attributeId + teiId + allergyHash) + identifier: [{ + system: 'urn:dhis2:allergyintolerance:attribute', + value: allergyId + }], + + // Clinical status: active (currently relevant) + clinicalStatus: { + coding: [{ + system: 'http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical', + code: 'active', + display: 'Active' + }] + }, + + // Verification status: unconfirmed (not clinically verified) + verificationStatus: { + coding: [{ + system: 'http://terminology.hl7.org/CodeSystem/allergyintolerance-verification', + code: 'unconfirmed', + display: 'Unconfirmed' + }] + }, + + // Patient reference + patient: { + reference: 'urn:uuid:' + tei.trackedEntity, + type: 'Patient' + }, + + // Reaction with manifestation (the allergy substance/effect) + reaction: [{ + manifestation: [{ + text: allergyText + }] + }], + }), + + // PUT with identifier query: updates if exists, creates if not + request: { + method: 'PUT', + url: 'AllergyIntolerance?identifier=urn:dhis2:allergyintolerance:attribute|' + allergyId + } + } + // List comprehension: create one resource per allergy + for allergy in allergiesList + // Filter: skip empty strings + if std.length(std.stripChars(allergy, ' ')) > 0 + ], +} diff --git a/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/main/resources/solutions/practitionerResource.solution.libsonnet b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/main/resources/solutions/practitionerResource.solution.libsonnet new file mode 100644 index 0000000..1b9f25d --- /dev/null +++ b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/main/resources/solutions/practitionerResource.solution.libsonnet @@ -0,0 +1,49 @@ +// SOLUTION: Practitioner Resource Mapping +// This file contains the complete implementation for mapping DHIS2 createdBy user to FHIR Practitioner + +local helpers = import '../helperFunctions.libsonnet'; + +{ + /** + * Creates a FHIR Practitioner resource from DHIS2 TEI createdBy information + * + * @param tei - DHIS2 Tracked Entity Instance object + * @return Bundle entry with Practitioner resource and conditional PUT request + */ + practitioner_entry(tei):: + // Extract practitioner information from createdBy + local practitionerId = tei.createdBy.uid; + local firstName = tei.createdBy.firstName; + local lastName = tei.createdBy.surname; + local username = tei.createdBy.username; + + // Transform name using helper function (same as Patient) + local practitionerName = helpers.parseName(firstName, lastName); + + // Build the Practitioner resource + { + // Use unique fullUrl for internal references + fullUrl: 'urn:uuid:practitioner-' + practitionerId, + + resource: std.prune({ + resourceType: 'Practitioner', + + // Official identifier from DHIS2 user UID + identifier: [{ + use: 'official', + system: 'urn:dhis2:user:uid', + value: practitionerId + }], + + // Name from user profile + name: [practitionerName], + + }), + + // Conditional PUT: update if exists, create if not (upsert pattern) + request: { + method: 'PUT', + url: 'Practitioner?identifier=urn:dhis2:user:uid|' + practitionerId + } + }, +} diff --git a/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/test/java/org/hisp/dhis/integration/camel/AbstractFunctionalTestBase.java b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/test/java/org/hisp/dhis/integration/camel/AbstractFunctionalTestBase.java new file mode 100644 index 0000000..b4ae49f --- /dev/null +++ b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/test/java/org/hisp/dhis/integration/camel/AbstractFunctionalTestBase.java @@ -0,0 +1,209 @@ +/* + * Copyright (c) 2004-2025, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.integration.camel; + +import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.rest.client.api.IGenericClient; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.Map; +import java.util.function.Function; + +import org.apache.camel.CamelContext; +import org.apache.camel.Exchange; +import org.apache.camel.test.spring.junit5.CamelSpringBootTest; +import org.hisp.dhis.integration.sdk.Dhis2ClientBuilder; +import org.hisp.dhis.integration.sdk.api.Dhis2Client; +import org.junit.jupiter.api.BeforeAll; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.util.TestSocketUtils; +import org.testcontainers.containers.BindMode; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.containers.wait.strategy.HttpWaitStrategy; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.utility.DockerImageName; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@CamelSpringBootTest +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +@ActiveProfiles("test") +public class AbstractFunctionalTestBase { + + @Container public static GenericContainer HAPI_FHIR_CONTAINER; + + @Container public static GenericContainer DHIS2_CONTAINER; + + @Container public static GenericContainer DHIS2_DB_CONTAINER; + + @Autowired protected CamelContext camelContext; + + protected static IGenericClient fhirClient; + + protected static String authorisationServerUrl; + + protected static Dhis2Client dhis2Client; + + protected CamelContext authorisationServerCamelContext; + + private static GenericContainer newHapiFhirContainer() { + return new GenericContainer<>(DockerImageName.parse("hapiproject/hapi:v8.2.0-2-tomcat")) + .withEnv("SPRING_CONFIG_LOCATION", "file:///data/hapi/application.yaml") + .withFileSystemBind( + "../config/hapi-fhir-ips/fhir-ips-ig.tgz", + "/package.tgz", + BindMode.READ_ONLY) + .withFileSystemBind( + "../config/hapi-fhir-ips/hapi.application.yaml", + "/data/hapi/application.yaml", + BindMode.READ_ONLY) + .withExposedPorts(8080) + .waitingFor( + new HttpWaitStrategy().forStatusCode(200).withStartupTimeout(Duration.ofSeconds(300))); + } + + private static GenericContainer newDhis2Container() { + Network.NetworkImpl dhis2Network = Network.builder().build(); + + DHIS2_DB_CONTAINER = newPostgreSQLContainer("dhis2", "dhis", "dhis", dhis2Network); + DHIS2_DB_CONTAINER.start(); + System.setProperty("dhis2DatabasePort", DHIS2_DB_CONTAINER.getFirstMappedPort().toString()); + + return new GenericContainer<>("dhis2/core:42.1.0") + .withClasspathResourceMapping("dhis.conf", "/opt/dhis2/dhis.conf", BindMode.READ_WRITE) + .withNetwork(dhis2Network) + .withExposedPorts(8080) + .dependsOn(DHIS2_DB_CONTAINER) + .waitingFor( + new HttpWaitStrategy().forStatusCode(200).withStartupTimeout(Duration.ofSeconds(120))) + .withEnv("WAIT_FOR_DB_CONTAINER", "db" + ":" + 5432 + " -t 0"); + } + + private static PostgreSQLContainer newPostgreSQLContainer( + String databaseName, String username, String password, Network network) { + return new PostgreSQLContainer<>( + DockerImageName.parse("postgis/postgis:12-3.2-alpine") + .asCompatibleSubstituteFor("postgres")) + .withFileSystemBind("../db-dump", "/docker-entrypoint-initdb.d/", BindMode.READ_ONLY) + .withFileSystemBind( + "../config/dhis2/postgresql.conf", "/etc/postgresql.conf", BindMode.READ_ONLY) + .withCommand("postgres -c config_file=/etc/postgresql.conf") + .withExposedPorts(5432) + .withDatabaseName(databaseName) + .withNetworkAliases("db") + .withUsername(username) + .withPassword(password) + .withNetwork(network); + } + + @BeforeAll + public static void beforeAll() throws IOException { + if (HAPI_FHIR_CONTAINER == null) { + Files.deleteIfExists(Path.of("target/offset.dat")); + + DHIS2_CONTAINER = newDhis2Container(); + DHIS2_CONTAINER.start(); + String dhis2ApiUrl = + String.format( + "http://%s:%s/api", DHIS2_CONTAINER.getHost(), DHIS2_CONTAINER.getFirstMappedPort()); + System.setProperty("dhis2ApiUrl", dhis2ApiUrl); + dhis2Client = Dhis2ClientBuilder.newClient(dhis2ApiUrl, "admin", "district").build(); + + HAPI_FHIR_CONTAINER = newHapiFhirContainer(); + HAPI_FHIR_CONTAINER.start(); + String fhirServerUrl = + String.format("http://localhost:%s/fhir", HAPI_FHIR_CONTAINER.getFirstMappedPort()); + System.setProperty("fhir-url", fhirServerUrl); + authorisationServerUrl = + String.format( + "http://localhost:%s/realms/ehr/protocol/openid-connect/token", + TestSocketUtils.findAvailableTcpPort()); + fhirClient = FhirVersionEnum.R4.newContext().newRestfulGenericClient(fhirServerUrl); + + System.setProperty("oauth2.tokenEndpoint", authorisationServerUrl); + } + } + + protected void startMockAuthorisationServer() throws Exception { + if (authorisationServerCamelContext == null) { + authorisationServerCamelContext = new org.apache.camel.impl.DefaultCamelContext(); + authorisationServerCamelContext.addRoutes(new org.apache.camel.builder.RouteBuilder() { + @Override + public void configure() { + from("jetty:" + authorisationServerUrl) + .process( + exchange -> { + assertEquals( + "Basic Zmhpci1jbGllbnQ6cGFzc3cwcmQ=", + exchange.getMessage().getHeader("Authorization")); + assertEquals( + "grant_type=client_credentials", + exchange.getMessage().getBody(String.class)); + }) + .setBody( + (Function) + exchange -> + Map.of( + "access_token", + "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIzOGZSMXNmQzlQb0IxTlcyTTRHUnN4d1UzdUZfYmNjUGNseWt2WVU5c2pRIn0.eyJleHAiOjE3MjczNDg0MTMsImlhdCI6MTcyNzM0ODExMywianRpIjoiN2E4OWQxYWQtOWY4OS00ZDVhLWI4MWItNDU1NjRkZDNjMTNjIiwiaXNzIjoiaHR0cDovL2tleWNsb2FrOjgwODAvcmVhbG1zL2NpdmlsLXJlZ2lzdHJ5IiwiYXVkIjpbImFjY291bnQiLCJjaXZpbC1yZWdpc3RyeS1jbGllbnQiXSwic3ViIjoiY2Y4NmFkY2QtMzIyNi00MWZmLThjY2EtMWJiM2FjNzUyOGM5IiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiY2l2aWwtcmVnaXN0cnktY2xpZW50IiwiYWNyIjoiMSIsImFsbG93ZWQtb3JpZ2lucyI6WyIvKiJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiZGVmYXVsdC1yb2xlcy1jaXZpbC1yZWdpc3RyeSIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6ImVtYWlsIHByb2ZpbGUiLCJjbGllbnRIb3N0IjoiMTkyLjE2OC45Ni4xIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJzZXJ2aWNlLWFjY291bnQtY2l2aWwtcmVnaXN0cnktY2xpZW50IiwiY2xpZW50QWRkcmVzcyI6IjE5Mi4xNjguOTYuMSIsImNsaWVudF9pZCI6ImNpdmlsLXJlZ2lzdHJ5LWNsaWVudCJ9.MYcQDPNz7Z4URYcLOH3v60bNxkqJlyWvYPWIBWp_VYKKZrmTXH2nvG3hPkF8aTHT2P-Kom5iQSwrZz519WB16X-qVYCdvqnCQY1poRITnAXOsjF3I1Ymli29vWdKJvkn7aXmEYn54c00VvfyjCfKbjOweKa-UdIXjfcO8hATP7neo-UiNQ6a7-Sj2TEwGDBFc989Sj40JjIVh6G6rH2h5zte8mxZy1RZUhXDp3DppHZB0ddfrk5rkECLITfsAg6pzyHmzaPYOq8kSRis59yzKgWCXurkq4WOw9-Rz7oNIc1CfPan_8YvYtsnYUG35Rh44UU6cWJnyv1sDIgyUHPIZw", + "expires_in", + 300, + "refresh_expires_in", + 0, + "token_type", + "Bearer", + "not-before-policy", + 0, + "scope", + "email profile")) + .marshal() + .json(); + } + }); + } + if (!authorisationServerCamelContext.isStarted()) { + authorisationServerCamelContext.start(); + } + } + protected void stopMockAuthorisationServer() throws Exception { + if (authorisationServerCamelContext != null && authorisationServerCamelContext.isStarted()) { + authorisationServerCamelContext.stop(); + } + } +} \ No newline at end of file diff --git a/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/test/java/org/hisp/dhis/integration/camel/FhirBundleDataSonnetTestCase.java b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/test/java/org/hisp/dhis/integration/camel/FhirBundleDataSonnetTestCase.java new file mode 100644 index 0000000..72b73fb --- /dev/null +++ b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/test/java/org/hisp/dhis/integration/camel/FhirBundleDataSonnetTestCase.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2004-2025, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.integration.camel; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.camel.CamelContext; +import org.apache.camel.builder.ValueBuilder; +import org.apache.camel.impl.DefaultCamelContext; +import org.apache.camel.model.language.DatasonnetExpression; +import org.apache.camel.support.DefaultExchange; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.util.StreamUtils; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class FhirBundleDataSonnetTestCase { + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + private DefaultExchange exchange; + private DatasonnetExpression dsExpression; + + @BeforeEach + public void beforeEach() { + dsExpression = new DatasonnetExpression("resource:classpath:fhirBundle.ds"); + dsExpression.setResultType(Map.class); + dsExpression.setBodyMediaType("application/x-java-object"); + dsExpression.setOutputMediaType("application/x-java-object"); + + CamelContext camelContext = new DefaultCamelContext(); + + exchange = new DefaultExchange(camelContext); + } + + @Test + public void testCompleteFhirPatientBundleDatasonnetMapping() throws IOException { + Map trackedEntity = OBJECT_MAPPER.readValue( + StreamUtils.copyToString( + Thread.currentThread() + .getContextClassLoader() + .getResourceAsStream("trackedEntity.json"), + Charset.defaultCharset()), + Map.class); + + exchange.getMessage().setBody(trackedEntity); + + Map fhirBundle = new ValueBuilder(dsExpression).evaluate(exchange, Map.class); + Map expectedFhirBundle = OBJECT_MAPPER.readValue( + StreamUtils.copyToString( + Thread.currentThread() + .getContextClassLoader() + .getResourceAsStream("expectedFhirBundle.json"), + Charset.defaultCharset()), + Map.class); + + assertEquals(expectedFhirBundle, fhirBundle); + } + + @Test +public void testMinimalFhirPatientBundleDatasonnetMapping() throws IOException { + Map minimalTrackedEntity = OBJECT_MAPPER.readValue( + StreamUtils.copyToString( + Thread.currentThread() + .getContextClassLoader() + .getResourceAsStream("minimalTrackedEntity.json"), + Charset.defaultCharset()), + Map.class); + + exchange.getMessage().setBody(minimalTrackedEntity); + + Map fhirBundle = new ValueBuilder(dsExpression).evaluate(exchange, Map.class); + Map expectedFhirBundle = OBJECT_MAPPER.readValue( + StreamUtils.copyToString( + Thread.currentThread() + .getContextClassLoader() + .getResourceAsStream("expectedMinimalFhirBundle.json"), + Charset.defaultCharset()), + Map.class); + + assertEquals(expectedFhirBundle, fhirBundle); +} +} diff --git a/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/test/java/org/hisp/dhis/integration/camel/HawtioWebConsoleFunctionalTestCase.java b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/test/java/org/hisp/dhis/integration/camel/HawtioWebConsoleFunctionalTestCase.java new file mode 100644 index 0000000..514201f --- /dev/null +++ b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/test/java/org/hisp/dhis/integration/camel/HawtioWebConsoleFunctionalTestCase.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2004-2025, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.integration.camel; + +import static io.restassured.RestAssured.given; + +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.specification.RequestSpecification; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.web.server.LocalManagementPort; + +public class HawtioWebConsoleFunctionalTestCase extends AbstractFunctionalTestBase { + @LocalManagementPort private int managementPort; + + private RequestSpecification hawtioRequestSpec; + + @BeforeEach + public void beforeEach() { + hawtioRequestSpec = + new RequestSpecBuilder() + .setBaseUri(String.format("http://localhost:%s/management/hawtio", managementPort)) + .setRelaxedHTTPSValidation() + .build(); + } + + @Test + public void testAnonymousHttpGet() { + given(hawtioRequestSpec) + .when() + .get() + .then() + .statusCode(401); + } + + @Test + public void testAuthorisedHttpGet() { + given(hawtioRequestSpec) + .auth() + .basic("test", "test") + .when() + .get() + .then() + .statusCode(200); + } +} diff --git a/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/test/java/org/hisp/dhis/integration/camel/IpsPatientMappingTestCase.java b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/test/java/org/hisp/dhis/integration/camel/IpsPatientMappingTestCase.java new file mode 100644 index 0000000..e527411 --- /dev/null +++ b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/test/java/org/hisp/dhis/integration/camel/IpsPatientMappingTestCase.java @@ -0,0 +1,519 @@ +/* + * Copyright (c) 2004-2025, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.integration.camel; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.validation.FhirValidator; +import ca.uhn.fhir.validation.ValidationResult; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.camel.CamelContext; +import org.apache.camel.builder.ValueBuilder; +import org.apache.camel.impl.DefaultCamelContext; +import org.apache.camel.model.language.DatasonnetExpression; +import org.apache.camel.support.DefaultExchange; +import org.hisp.dhis.integration.camel.util.FhirValidatorUtil; +import ca.uhn.fhir.context.support.DefaultProfileValidationSupport; +import org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService; +import org.hl7.fhir.common.hapi.validation.support.InMemoryTerminologyServerValidationSupport; +import org.hl7.fhir.common.hapi.validation.support.PrePopulatedValidationSupport; +import org.hl7.fhir.common.hapi.validation.support.SnapshotGeneratingValidationSupport; +import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain; +import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Patient; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.core.io.ClassPathResource; +import org.springframework.util.StreamUtils; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +public class IpsPatientMappingTestCase { + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final String IPS_PATIENT_PROFILE = "http://hl7.org/fhir/uv/ips/StructureDefinition/Patient-uv-ips"; + + private FhirContext fhirContext; + private FhirValidator validator; + private DefaultExchange exchange; + private DatasonnetExpression dsExpression; + + private Map loadTrackedEntity(String filename) throws IOException { + return OBJECT_MAPPER.readValue( + StreamUtils.copyToString( + Thread.currentThread() + .getContextClassLoader() + .getResourceAsStream(filename), + Charset.defaultCharset()), + Map.class); + } + + @BeforeEach + public void beforeEach() throws IOException { + fhirContext = FhirContext.forR4(); + + // Load IPS profile from classpath + PrePopulatedValidationSupport prePopulatedSupport = new PrePopulatedValidationSupport(fhirContext); + String ipsProfileJson = StreamUtils.copyToString( + new ClassPathResource("StructureDefinition-Patient-uv-ips.json").getInputStream(), + Charset.defaultCharset() + ); + org.hl7.fhir.r4.model.StructureDefinition ipsProfile = + fhirContext.newJsonParser().parseResource( + org.hl7.fhir.r4.model.StructureDefinition.class, + ipsProfileJson + ); + prePopulatedSupport.addStructureDefinition(ipsProfile); + + ValidationSupportChain validationSupportChain = new ValidationSupportChain( + new DefaultProfileValidationSupport(fhirContext), + prePopulatedSupport, + new InMemoryTerminologyServerValidationSupport(fhirContext), + new CommonCodeSystemsTerminologyService(fhirContext), + new SnapshotGeneratingValidationSupport(fhirContext) + ); + + // Create and configure validator + validator = fhirContext.newValidator(); + FhirInstanceValidator instanceValidator = new FhirInstanceValidator(validationSupportChain); + validator.registerValidatorModule(instanceValidator); + + // Set up DataSonnet expression for transformation + dsExpression = new DatasonnetExpression("resource:classpath:fhirBundle.ds"); + dsExpression.setResultType(Map.class); + dsExpression.setBodyMediaType("application/x-java-object"); + dsExpression.setOutputMediaType("application/x-java-object"); + + CamelContext camelContext = new DefaultCamelContext(); + exchange = new DefaultExchange(camelContext); + } + + @Test + public void testBundleStructure() throws IOException { + Map trackedEntity = loadTrackedEntity("trackedEntity.json"); + exchange.getMessage().setBody(trackedEntity); + + + // Perform datasonnet transformation + Map fhirBundle = new ValueBuilder(dsExpression).evaluate(exchange, Map.class); + + assertEquals("Bundle", fhirBundle.get("resourceType"), "Should produce a Bundle resource"); + assertEquals("transaction", fhirBundle.get("type"), "Bundle type should be transaction"); + + @SuppressWarnings("unchecked") + java.util.List> entries = (java.util.List>) fhirBundle.get("entry"); + assertNotNull(entries, "Bundle should have entries"); + assertFalse(entries.isEmpty(), "Bundle should have at least one entry"); + + Map firstEntry = entries.get(0); + @SuppressWarnings("unchecked") + Map resource = (Map) firstEntry.get("resource"); + assertEquals("Patient", resource.get("resourceType"), "First entry should be a Patient resource"); + } + + @Test + public void testPatientConformsToIpsProfile() throws Exception { + Map trackedEntity = loadTrackedEntity("trackedEntity.json"); + exchange.getMessage().setBody(trackedEntity); + + Map fhirBundle = new ValueBuilder(dsExpression).evaluate(exchange, Map.class); + + String bundleJson = OBJECT_MAPPER.writeValueAsString(fhirBundle); + + Bundle bundle = fhirContext.newJsonParser().parseResource(Bundle.class, bundleJson); + + assertFalse(bundle.getEntry().isEmpty(), "Bundle should have entries"); + Patient patient = (Patient) bundle.getEntryFirstRep().getResource(); + + assertTrue( + patient.getMeta().getProfile().stream() + .anyMatch(p -> p.getValue().equals(IPS_PATIENT_PROFILE)), + "Patient should declare IPS profile" + ); + + // Validate against IPS profile + ValidationResult result = validator.validateWithResult(patient); + + // Assert validation success + assertTrue( + result.isSuccessful(), + "Patient should conform to IPS profile. Validation errors:\n" + + FhirValidatorUtil.extractValidationErrors((org.hl7.fhir.r4.model.OperationOutcome) result.toOperationOutcome()) + ); + } + + @Test + public void testPatientNameConstraint() throws Exception { + Map trackedEntity = loadTrackedEntity("trackedEntity.json"); + exchange.getMessage().setBody(trackedEntity); + + Map fhirBundle = new ValueBuilder(dsExpression).evaluate(exchange, Map.class); + + Bundle bundle = fhirContext.newJsonParser().parseResource( + Bundle.class, + OBJECT_MAPPER.writeValueAsString(fhirBundle) + ); + Patient patient = (Patient) bundle.getEntryFirstRep().getResource(); + + assertFalse(patient.getName().isEmpty(), "Patient must have at least one name"); + + // Verify ips-pat-1: at least one of given, family, or text + org.hl7.fhir.r4.model.HumanName name = patient.getNameFirstRep(); + boolean hasGiven = !name.getGiven().isEmpty(); + boolean hasFamily = name.hasFamily(); + boolean hasText = name.hasText(); + + assertTrue( + hasGiven || hasFamily || hasText, + "Name must have at least one of: given, family, or text (ips-pat-1 constraint)" + ); + } + + @Test + public void testPatientIdentifier() throws Exception { + Map trackedEntity = loadTrackedEntity("trackedEntity.json"); + exchange.getMessage().setBody(trackedEntity); + + Map fhirBundle = new ValueBuilder(dsExpression).evaluate(exchange, Map.class); + + Bundle bundle = fhirContext.newJsonParser().parseResource( + Bundle.class, + OBJECT_MAPPER.writeValueAsString(fhirBundle) + ); + Patient patient = (Patient) bundle.getEntryFirstRep().getResource(); + + assertFalse(patient.getIdentifier().isEmpty(), "Patient should have at least one identifier"); + + org.hl7.fhir.r4.model.Identifier identifier = patient.getIdentifierFirstRep(); + assertTrue(identifier.hasSystem(), "Identifier must have a system"); + assertTrue(identifier.hasValue(), "Identifier must have a value"); + assertEquals("official", identifier.getUse().toCode(), "Primary identifier should be official use"); + + assertEquals("8437107", identifier.getValue(), "Identifier value should match DHIS2 Unique ID"); + } + + @Test + public void testDemographicDataMapping() throws Exception { + Map trackedEntity = loadTrackedEntity("trackedEntity.json"); + exchange.getMessage().setBody(trackedEntity); + + Map fhirBundle = new ValueBuilder(dsExpression).evaluate(exchange, Map.class); + + Bundle bundle = fhirContext.newJsonParser().parseResource( + Bundle.class, + OBJECT_MAPPER.writeValueAsString(fhirBundle) + ); + Patient patient = (Patient) bundle.getEntryFirstRep().getResource(); + + assertTrue(patient.hasBirthDate(), "Patient should have birth date"); + assertEquals("1997-04-18", patient.getBirthDateElement().getValueAsString()); + + assertFalse(patient.getAddress().isEmpty(), "Patient should have address"); + org.hl7.fhir.r4.model.Address address = patient.getAddressFirstRep(); + assertTrue(address.hasLine(), "Address should have line"); + assertTrue(address.hasCity(), "Address should have city"); + assertTrue(address.hasPostalCode(), "Address should have postal code"); + + assertFalse(patient.getTelecom().isEmpty(), "Patient should have telecom"); + + assertTrue( + patient.getTelecom().stream().anyMatch(t -> t.getSystem().toCode().equals("phone")) || + patient.getTelecom().stream().anyMatch(t -> t.getSystem().toCode().equals("email")), + "Patient should have phone or email contact" + ); + } + + @Test + public void testConditionalUpdateRequest() throws Exception { + Map trackedEntity = loadTrackedEntity("trackedEntity.json"); + exchange.getMessage().setBody(trackedEntity); + + Map fhirBundle = new ValueBuilder(dsExpression).evaluate(exchange, Map.class); + + @SuppressWarnings("unchecked") + java.util.List> entries = + (java.util.List>) fhirBundle.get("entry"); + Map firstEntry = entries.get(0); + + assertTrue(firstEntry.containsKey("request"), "Entry should have request element"); + + @SuppressWarnings("unchecked") + Map request = (Map) firstEntry.get("request"); + assertEquals("PUT", request.get("method"), "Request method should be PUT for conditional update"); + + String url = (String) request.get("url"); + assertTrue(url.startsWith("Patient?identifier="), "Request URL should include identifier query"); + assertTrue(url.contains("8437107"), "Request URL should include the patient's unique ID"); + } + + @Test + public void testMinimalDataHandling() throws Exception { + Map minimalTei = loadTrackedEntity("minimalTrackedEntity.json"); + exchange.getMessage().setBody(minimalTei); + + Map fhirBundle = new ValueBuilder(dsExpression).evaluate(exchange, Map.class); + + Bundle bundle = fhirContext.newJsonParser().parseResource( + Bundle.class, + OBJECT_MAPPER.writeValueAsString(fhirBundle) + ); + Patient patient = (Patient) bundle.getEntryFirstRep().getResource(); + + assertFalse(patient.getName().isEmpty(), "Even minimal patient should have a name"); + + ValidationResult result = validator.validateWithResult(patient); + assertTrue( + result.isSuccessful(), + "Minimal patient should still conform to IPS profile. Errors:\n" + + FhirValidatorUtil.extractValidationErrors((org.hl7.fhir.r4.model.OperationOutcome) result.toOperationOutcome()) + ); + } + + // EXERCISE TEST 1 + @Test + public void testPractitionerResourceExists() throws Exception { + Map trackedEntity = loadTrackedEntity("trackedEntity.json"); + exchange.getMessage().setBody(trackedEntity); + + Map fhirBundle = new ValueBuilder(dsExpression).evaluate(exchange, Map.class); + + Bundle bundle = fhirContext.newJsonParser().parseResource( + Bundle.class, + OBJECT_MAPPER.writeValueAsString(fhirBundle) + ); + + // Find Practitioner resource + org.hl7.fhir.r4.model.Practitioner practitioner = bundle.getEntry().stream() + .map(Bundle.BundleEntryComponent::getResource) + .filter(r -> r.getResourceType().name().equals("Practitioner")) + .map(r -> (org.hl7.fhir.r4.model.Practitioner) r) + .findFirst() + .orElse(null); + + assertNotNull(practitioner, "Bundle should contain a Practitioner resource"); + + assertFalse(practitioner.getIdentifier().isEmpty(), "Practitioner should have identifier"); + assertTrue( + practitioner.getIdentifier().stream() + .anyMatch(id -> id.getSystem().equals("urn:dhis2:user:uid") && id.getValue().equals("xE7jOejl9FI")), + "Practitioner should have DHIS2 user UID identifier" + ); + + assertFalse(practitioner.getName().isEmpty(), "Practitioner should have name"); + } + + // EXERCISE TEST 2 + @Test + public void testPatientReferencesPractitioner() throws Exception { + Map trackedEntity = loadTrackedEntity("trackedEntity.json"); + exchange.getMessage().setBody(trackedEntity); + + Map fhirBundle = new ValueBuilder(dsExpression).evaluate(exchange, Map.class); + + Bundle bundle = fhirContext.newJsonParser().parseResource( + Bundle.class, + OBJECT_MAPPER.writeValueAsString(fhirBundle) + ); + + Patient patient = (Patient) bundle.getEntry().stream() + .map(Bundle.BundleEntryComponent::getResource) + .filter(r -> r.getResourceType().name().equals("Patient")) + .findFirst() + .orElseThrow(() -> new AssertionError("Patient resource not found")); + + assertFalse( + patient.getGeneralPractitioner().isEmpty(), + "Patient should have generalPractitioner reference" + ); + + org.hl7.fhir.r4.model.Reference practitionerRef = patient.getGeneralPractitionerFirstRep(); + assertTrue( + practitionerRef.getReference().contains("practitioner-xE7jOejl9FI"), + "Patient should reference Practitioner with correct UID" + ); + assertTrue( + practitionerRef.hasDisplay(), + "Practitioner reference should have display name" + ); + } + + // EXERCISE TEST 3 + @Test + public void testCivilStatusObservation() throws Exception { + Map trackedEntity = loadTrackedEntity("trackedEntity.json"); + exchange.getMessage().setBody(trackedEntity); + + Map fhirBundle = new ValueBuilder(dsExpression).evaluate(exchange, Map.class); + + Bundle bundle = fhirContext.newJsonParser().parseResource( + Bundle.class, + OBJECT_MAPPER.writeValueAsString(fhirBundle) + ); + + org.hl7.fhir.r4.model.Observation observation = bundle.getEntry().stream() + .map(Bundle.BundleEntryComponent::getResource) + .filter(r -> r.getResourceType().name().equals("Observation")) + .map(r -> (org.hl7.fhir.r4.model.Observation) r) + .filter(obs -> obs.getCode().getCoding().stream() + .anyMatch(coding -> coding.getCode().equals("45404-1"))) + .findFirst() + .orElse(null); + + assertNotNull(observation, "Bundle should contain civil status Observation"); + + assertTrue( + observation.getCode().getCoding().stream() + .anyMatch(c -> c.getSystem().equals("http://loinc.org") && c.getCode().equals("45404-1")), + "Observation should have LOINC code 45404-1 (Marital status)" + ); + + assertTrue( + observation.getCategory().stream() + .flatMap(cat -> cat.getCoding().stream()) + .anyMatch(c -> c.getCode().equals("social-history")), + "Observation should have social-history category" + ); + + assertTrue( + observation.getSubject().getReference().contains("QfFVkOL8ixj"), + "Observation should reference Patient" + ); + + assertTrue( + observation.hasValueStringType(), + "Observation should have valueString" + ); + assertEquals( + "Single or widow", + observation.getValueStringType().getValue(), + "Observation value should match civil status from DHIS2" + ); + } + + // EXERCISE TEST 4 + @Test + public void testAllergiesResource() throws Exception { + Map trackedEntity = loadTrackedEntity("trackedEntity.json"); + exchange.getMessage().setBody(trackedEntity); + + Map fhirBundle = new ValueBuilder(dsExpression).evaluate(exchange, Map.class); + + Bundle bundle = fhirContext.newJsonParser().parseResource( + Bundle.class, + OBJECT_MAPPER.writeValueAsString(fhirBundle) + ); + + java.util.List allergies = bundle.getEntry().stream() + .map(Bundle.BundleEntryComponent::getResource) + .filter(r -> r.getResourceType().name().equals("AllergyIntolerance")) + .map(r -> (org.hl7.fhir.r4.model.AllergyIntolerance) r) + .collect(java.util.stream.Collectors.toList()); + + assertFalse(allergies.isEmpty(), "Bundle should contain at least one AllergyIntolerance resource"); + + org.hl7.fhir.r4.model.AllergyIntolerance allergy = allergies.get(0); + + assertTrue( + allergy.getClinicalStatus().getCoding().stream() + .anyMatch(c -> c.getCode().equals("active")), + "AllergyIntolerance should have active clinical status" + ); + + assertTrue( + allergy.getVerificationStatus().getCoding().stream() + .anyMatch(c -> c.getCode().equals("unconfirmed")), + "AllergyIntolerance should have unconfirmed verification status" + ); + + assertTrue( + allergy.getPatient().getReference().contains("QfFVkOL8ixj"), + "AllergyIntolerance should reference Patient" + ); + + assertFalse(allergy.getReaction().isEmpty(), "AllergyIntolerance should have reaction"); + assertFalse( + allergy.getReactionFirstRep().getManifestation().isEmpty(), + "AllergyIntolerance reaction should have manifestation" + ); + + String manifestationText = allergy.getReactionFirstRep().getManifestationFirstRep().getText(); + assertTrue( + manifestationText.contains("NSAID") || manifestationText.equals("NSAIDS"), + "AllergyIntolerance manifestation should contain allergy from DHIS2" + ); + } + + // EXERCISE TEST 5 + @Test + public void testBundleContainsAllResources() throws Exception { + Map trackedEntity = loadTrackedEntity("trackedEntity.json"); + exchange.getMessage().setBody(trackedEntity); + + Map fhirBundle = new ValueBuilder(dsExpression).evaluate(exchange, Map.class); + + Bundle bundle = fhirContext.newJsonParser().parseResource( + Bundle.class, + OBJECT_MAPPER.writeValueAsString(fhirBundle) + ); + + java.util.Map resourceCounts = bundle.getEntry().stream() + .map(Bundle.BundleEntryComponent::getResource) + .collect(java.util.stream.Collectors.groupingBy( + r -> r.getResourceType().name(), + java.util.stream.Collectors.counting() + )); + + // Verify all expected resource types exist + assertTrue(resourceCounts.containsKey("Patient"), "Bundle should contain Patient"); + assertTrue(resourceCounts.containsKey("Practitioner"), "Bundle should contain Practitioner"); + assertTrue(resourceCounts.containsKey("Observation"), "Bundle should contain Observation"); + assertTrue(resourceCounts.containsKey("AllergyIntolerance"), "Bundle should contain AllergyIntolerance"); + + assertEquals(1L, resourceCounts.get("Patient"), "Bundle should have exactly 1 Patient"); + assertEquals(1L, resourceCounts.get("Practitioner"), "Bundle should have exactly 1 Practitioner"); + assertTrue(resourceCounts.get("Observation") >= 1, "Bundle should have at least 1 Observation"); + assertTrue(resourceCounts.get("AllergyIntolerance") >= 1, "Bundle should have at least 1 AllergyIntolerance"); + + assertEquals("transaction", bundle.getType().toCode(), "Bundle type should be transaction"); + + bundle.getEntry().forEach(entry -> { + assertTrue(entry.hasRequest(), "All bundle entries should have request element"); + assertTrue( + entry.getRequest().getMethod().toCode().equals("PUT") || + entry.getRequest().getMethod().toCode().equals("POST"), + "Request method should be PUT or POST" + ); + }); + } +} diff --git a/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/test/java/org/hisp/dhis/integration/camel/route/UpsertPatientRouteFunctionalTestCase.java b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/test/java/org/hisp/dhis/integration/camel/route/UpsertPatientRouteFunctionalTestCase.java new file mode 100644 index 0000000..14c41e5 --- /dev/null +++ b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/test/java/org/hisp/dhis/integration/camel/route/UpsertPatientRouteFunctionalTestCase.java @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2004-2025, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.integration.camel.route; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; +import org.apache.camel.CamelContext; +import org.apache.camel.builder.AdviceWith; +import org.apache.camel.component.mock.MockEndpoint; +import org.apache.camel.test.spring.junit5.UseAdviceWith; +import org.hisp.dhis.api.model.v40_2_2.AttributeInfo; +import org.hisp.dhis.api.model.v40_2_2.EnrollmentInfo; +import org.hisp.dhis.api.model.v40_2_2.EventInfo; +import org.hisp.dhis.api.model.v40_2_2.TrackedEntityInfo; +import org.hisp.dhis.api.model.v40_2_2.TrackerImportReport; +import org.hisp.dhis.integration.camel.AbstractFunctionalTestBase; +import org.hisp.dhis.integration.camel.util.FhirValidatorUtil; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Patient; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +@UseAdviceWith +public class UpsertPatientRouteFunctionalTestCase extends AbstractFunctionalTestBase { + @Autowired private CamelContext camelContext; + + @BeforeEach + public void beforeEach() throws Exception { + startMockAuthorisationServer(); + } + + @AfterEach + public void afterEach() throws Exception { + stopMockAuthorisationServer(); + } + + @Test + public void testUpsertPatient() throws Exception { + AdviceWith.adviceWith(camelContext, "upsertPatient", r -> r.weaveAddLast().to("mock:spy")); + MockEndpoint spyEndpoint = camelContext.getEndpoint("mock:spy", MockEndpoint.class); + spyEndpoint.setExpectedCount(1); + + camelContext.start(); + String orgUnit = "DiszpKrYNg8"; + TrackedEntityInfo trackedEntity = new TrackedEntityInfo() + .withOrgUnit(orgUnit) + .withTrackedEntityType("nEenWmSyUEp") + .withAttributes(List.of( + new AttributeInfo().withAttribute("lZGmxYbs97q").withValue("8437107"), // Unique ID + new AttributeInfo().withAttribute("VqEFza8wbwA").withValue("Madison Avenue 12"), // Address + new AttributeInfo().withAttribute("w75KJ2mc4zz").withValue("Jane"), // First name + new AttributeInfo().withAttribute("zDhUuAYrxNC").withValue("Doe"), // Last name + new AttributeInfo().withAttribute("FO4sWYJ64LQ").withValue("New York") // City + )) + .withEnrollments( + addEnrollment( + orgUnit, + List.of("WZbXY0S00lP"))); + + dhis2Client + .post("tracker") + .withResource(Map.of("trackedEntities", List.of(trackedEntity))) + .withParameter("async", "false") + .transfer() + .returnAs(TrackerImportReport.class) + .getBundleReport() + .get() + .getTypeReportMap() + .get() + .getAdditionalProperties() + .get("ENROLLMENT") + .getObjectReports() + .get() + .get(0) + .getUid() + .get() + .toString(); + + spyEndpoint.assertIsSatisfied(30000); + Bundle patientBundle = (Bundle) fhirClient.search().forResource(Patient.class).execute(); + List entries = patientBundle.getEntry(); + assertEquals(1, entries.size()); + Patient patient = (Patient) entries.get(0).getResource(); + assertEquals("8437107", patient.getIdentifier().get(0).getValue()); + + org.hl7.fhir.r4.model.Parameters params = new org.hl7.fhir.r4.model.Parameters(); + params.addParameter().setName("resource").setResource(patient); + + org.hl7.fhir.r4.model.Parameters resultParams = fhirClient + .operation() + .onType(Patient.class) + .named("validate") + .withParameters(params) + .execute(); + + org.hl7.fhir.r4.model.OperationOutcome outcome = (org.hl7.fhir.r4.model.OperationOutcome) resultParams.getParameterFirstRep().getResource(); + String errorDetails = FhirValidatorUtil.extractValidationErrors(outcome); + boolean hasError = !errorDetails.isEmpty(); + assertEquals(false, hasError, errorDetails); + } + + public List addEnrollment(String orgUnitId, List programStageIds) { + List events = new ArrayList<>(); + + String today = new SimpleDateFormat("yyyy-MM-dd").format(new Date()); + for (String programStage : programStageIds) { + events.add( + new EventInfo() + .withProgramStage(programStage) + .withOrgUnit(orgUnitId) + .withScheduledAt(today) + .withProgram("WSGAb5XwJ3Y") + .withStatus(EventInfo.StatusRef.SCHEDULE)); + } + + return List.of( + new EnrollmentInfo() + .withOrgUnit(orgUnitId) + .withProgram("WSGAb5XwJ3Y") + .withEnrolledAt(today) + .withAttributes( + List.of( + new AttributeInfo().withAttribute("Agywv2JGwuq").withValue("+13052065294"), + new AttributeInfo().withAttribute("ZcBPrXKahq2").withValue("10022"), + new AttributeInfo().withAttribute("KmEUg2hHEtx").withValue("jane@doe.com"), + new AttributeInfo().withAttribute("ciq2USN94oJ").withValue("Single or widow"), + new AttributeInfo().withAttribute("w75KJ2mc4zz").withValue("Jane"), + new AttributeInfo().withAttribute("zDhUuAYrxNC").withValue("Doe"), + new AttributeInfo().withAttribute("FO4sWYJ64LQ").withValue("New York"), + new AttributeInfo().withAttribute("gHGyrwKPzej").withValue("1997-04-18"), + new AttributeInfo().withAttribute("VqEFza8wbwA").withValue("Madison Avenue 12"), + new AttributeInfo().withAttribute("lZGmxYbs97q").withValue("8437107"), + new AttributeInfo().withAttribute("gu1fqsmoU8r").withValue("NSAIDS") + )) + .withOccurredAt(today) + .withStatus(EnrollmentInfo.StatusRef.ACTIVE) + .withEvents(events)); + } +} diff --git a/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/test/java/org/hisp/dhis/integration/camel/util/FhirValidatorUtil.java b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/test/java/org/hisp/dhis/integration/camel/util/FhirValidatorUtil.java new file mode 100644 index 0000000..73c2e47 --- /dev/null +++ b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/test/java/org/hisp/dhis/integration/camel/util/FhirValidatorUtil.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2004-2025, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.integration.camel.util; + +import org.hl7.fhir.r4.model.OperationOutcome; + +public class FhirValidatorUtil { + + private FhirValidatorUtil() { + // Utility class + } + + public static String extractValidationErrors(OperationOutcome outcome) { + StringBuilder errorDetails = new StringBuilder(); + for (OperationOutcome.OperationOutcomeIssueComponent issue : outcome.getIssue()) { + if (issue.getSeverity() == OperationOutcome.IssueSeverity.ERROR || + issue.getSeverity() == OperationOutcome.IssueSeverity.FATAL) { + if (errorDetails.isEmpty()) { + errorDetails.append("Patient resource is not conformant to NEHR IPS profile:\nFailures:\n"); + } + errorDetails.append(issue.getSeverity().toCode().toUpperCase()) + .append(": ") + .append(issue.getCode().toCode()) + .append(" - ") + .append(issue.getDiagnostics() != null ? issue.getDiagnostics() : "") + .append("\n"); + } + } + return errorDetails.toString(); + } +} diff --git a/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/test/resources/StructureDefinition-Patient-uv-ips.json b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/test/resources/StructureDefinition-Patient-uv-ips.json new file mode 100644 index 0000000..907df65 --- /dev/null +++ b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/test/resources/StructureDefinition-Patient-uv-ips.json @@ -0,0 +1,3319 @@ +{ + "resourceType" : "StructureDefinition", + "id" : "Patient-uv-ips", + "language" : "en", + "text" : { + "status" : "extensions", + "div" : "

Generated Narrative: StructureDefinition Patient-uv-ips

\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n
NameFlagsCard.TypeDescription & Constraints\"doco\"
\".\"\".\" Patient Patient
\".\"\".\"\".\" Slices for extension 0..*ExtensionExtension
Slice: Unordered, Open by value:url
\".\"\".\"\".\"\".\" extension:genderIdentity 0..*(Complex)The individual's gender identity
URL: http://hl7.org/fhir/StructureDefinition/individual-genderIdentity
\".\"\".\"\".\"\".\" extension:personalPronouns 0..*(Complex)The pronouns to use when communicating about an individual.
URL: http://hl7.org/fhir/StructureDefinition/individual-pronouns
\".\"\".\"\".\" identifier SO0..*IdentifierAn identifier for this patient
ObligationsActor
SHALL:populate-if-knownCreator (IPS)
SHALL:handleConsumer (IPS)
SHOULD:displayConsumer (IPS)
\".\"\".\"\".\" name SOC1..*HumanNameA name associated with the patient
Constraints: ips-pat-1
ObligationsActor
SHALL:populateCreator (IPS)
SHALL:handleConsumer (IPS)
SHOULD:displayConsumer (IPS)
\".\"\".\"\".\"\".\" use SO0..1codeusual | official | temp | nickname | anonymous | old | maiden
ObligationsActor
SHALL:populate-if-knownCreator (IPS)
SHALL:handleConsumer (IPS)
SHOULD:displayConsumer (IPS)
\".\"\".\"\".\"\".\" text SO0..1stringText representation of the full name
ObligationsActor
SHALL:populate-if-knownCreator (IPS)
SHALL:handleConsumer (IPS)
SHOULD:displayConsumer (IPS)
\".\"\".\"\".\"\".\" family SO0..1stringFamily name (often called 'Surname')
ObligationsActor
SHALL:populate-if-knownCreator (IPS)
SHALL:handleConsumer (IPS)
SHOULD:displayConsumer (IPS)
\".\"\".\"\".\"\".\" given SO0..*stringGiven names (not always 'first'). Includes middle names
ObligationsActor
SHALL:populate-if-knownCreator (IPS)
SHALL:handleConsumer (IPS)
SHOULD:displayConsumer (IPS)
\".\"\".\"\".\" telecom SO0..*ContactPointA contact detail for the individual
ObligationsActor
SHALL:populate-if-knownCreator (IPS)
SHALL:handleConsumer (IPS)
SHOULD:displayConsumer (IPS)
\".\"\".\"\".\" gender SO0..1codemale | female | other | unknown
ObligationsActor
SHALL:populate-if-knownCreator (IPS)
SHALL:handleConsumer (IPS)
SHOULD:displayConsumer (IPS)
\".\"\".\"\".\" birthDate SO1..1dateThe date of birth for the individual
ObligationsActor
SHALL:populate-if-knownCreator (IPS)
SHALL:handleConsumer (IPS)
SHOULD:displayConsumer (IPS)
\".\"\".\"\".\" address SO0..*AddressAn address for the individual
ObligationsActor
SHALL:populate-if-knownCreator (IPS)
SHALL:handleConsumer (IPS)
SHOULD:displayConsumer (IPS)
\".\"\".\"\".\" generalPractitioner SO0..*Reference(Organization | Practitioner | PractitionerRole)Patient's nominated primary care provider
ObligationsActor
SHALL:populate-if-knownCreator (IPS)
SHALL:handleConsumer (IPS)
SHOULD:displayConsumer (IPS)

\"doco\" Documentation for this format
" + }, + "extension" : [{ + "url" : "http://hl7.org/fhir/StructureDefinition/structuredefinition-wg", + "valueCode" : "pc" + }, + { + "url" : "http://hl7.org/fhir/StructureDefinition/structuredefinition-fmm", + "valueInteger" : 3, + "_valueInteger" : { + "extension" : [{ + "url" : "http://hl7.org/fhir/StructureDefinition/structuredefinition-conformance-derivedFrom", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ImplementationGuide/hl7.fhir.uv.ips" + }] + } + }, + { + "url" : "http://hl7.org/fhir/StructureDefinition/structuredefinition-standards-status", + "valueCode" : "trial-use", + "_valueCode" : { + "extension" : [{ + "url" : "http://hl7.org/fhir/StructureDefinition/structuredefinition-conformance-derivedFrom", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ImplementationGuide/hl7.fhir.uv.ips" + }] + } + }], + "url" : "http://hl7.org/fhir/uv/ips/StructureDefinition/Patient-uv-ips", + "version" : "2.0.0", + "name" : "PatientUvIps", + "title" : "Patient (IPS)", + "status" : "active", + "date" : "2024-06-19T10:50:07-05:00", + "publisher" : "HL7 International / Patient Care", + "contact" : [{ + "name" : "HL7 International / Patient Care", + "telecom" : [{ + "system" : "url", + "value" : "http://www.hl7.org/Special/committees/patientcare" + }] + }], + "description" : "This profile represents the constraints applied to the Patient resource by the International Patient Summary (IPS) FHIR Implementation Guide and describes the minimum expectations for the Patient resource when used in the IPS composition or in one of the referred resources.", + "jurisdiction" : [{ + "coding" : [{ + "system" : "http://unstats.un.org/unsd/methods/m49/m49.htm", + "code" : "001", + "display" : "World" + }] + }], + "fhirVersion" : "4.0.1", + "mapping" : [{ + "identity" : "rim", + "uri" : "http://hl7.org/v3", + "name" : "RIM Mapping" + }, + { + "identity" : "cda", + "uri" : "http://hl7.org/v3/cda", + "name" : "CDA (R2)" + }, + { + "identity" : "w5", + "uri" : "http://hl7.org/fhir/fivews", + "name" : "FiveWs Pattern Mapping" + }, + { + "identity" : "v2", + "uri" : "http://hl7.org/v2", + "name" : "HL7 v2 Mapping" + }, + { + "identity" : "loinc", + "uri" : "http://loinc.org", + "name" : "LOINC code for the element" + }], + "kind" : "resource", + "abstract" : false, + "type" : "Patient", + "baseDefinition" : "http://hl7.org/fhir/StructureDefinition/Patient", + "derivation" : "constraint", + "snapshot" : { + "extension" : [{ + "url" : "http://hl7.org/fhir/tools/StructureDefinition/snapshot-base-version", + "valueString" : "4.0.1" + }], + "element" : [{ + "id" : "Patient", + "path" : "Patient", + "short" : "Information about an individual or animal receiving health care services", + "definition" : "Demographics and other administrative information about an individual or animal receiving care or other health-related services.", + "alias" : ["SubjectOfCare Client Resident"], + "min" : 0, + "max" : "*", + "base" : { + "path" : "Patient", + "min" : 0, + "max" : "*" + }, + "constraint" : [{ + "key" : "dom-2", + "severity" : "error", + "human" : "If the resource is contained in another resource, it SHALL NOT contain nested Resources", + "expression" : "contained.contained.empty()", + "xpath" : "not(parent::f:contained and f:contained)", + "source" : "http://hl7.org/fhir/StructureDefinition/DomainResource" + }, + { + "key" : "dom-3", + "severity" : "error", + "human" : "If the resource is contained in another resource, it SHALL be referred to from elsewhere in the resource or SHALL refer to the containing resource", + "expression" : "contained.where((('#'+id in (%resource.descendants().reference | %resource.descendants().as(canonical) | %resource.descendants().as(uri) | %resource.descendants().as(url))) or descendants().where(reference = '#').exists() or descendants().where(as(canonical) = '#').exists() or descendants().where(as(canonical) = '#').exists()).not()).trace('unmatched', id).empty()", + "xpath" : "not(exists(for $id in f:contained/*/f:id/@value return $contained[not(parent::*/descendant::f:reference/@value=concat('#', $contained/*/id/@value) or descendant::f:reference[@value='#'])]))", + "source" : "http://hl7.org/fhir/StructureDefinition/DomainResource" + }, + { + "key" : "dom-4", + "severity" : "error", + "human" : "If a resource is contained in another resource, it SHALL NOT have a meta.versionId or a meta.lastUpdated", + "expression" : "contained.meta.versionId.empty() and contained.meta.lastUpdated.empty()", + "xpath" : "not(exists(f:contained/*/f:meta/f:versionId)) and not(exists(f:contained/*/f:meta/f:lastUpdated))", + "source" : "http://hl7.org/fhir/StructureDefinition/DomainResource" + }, + { + "key" : "dom-5", + "severity" : "error", + "human" : "If a resource is contained in another resource, it SHALL NOT have a security label", + "expression" : "contained.meta.security.empty()", + "xpath" : "not(exists(f:contained/*/f:meta/f:security))", + "source" : "http://hl7.org/fhir/StructureDefinition/DomainResource" + }, + { + "extension" : [{ + "url" : "http://hl7.org/fhir/StructureDefinition/elementdefinition-bestpractice", + "valueBoolean" : true + }, + { + "url" : "http://hl7.org/fhir/StructureDefinition/elementdefinition-bestpractice-explanation", + "valueMarkdown" : "When a resource has no narrative, only systems that fully understand the data can display the resource to a human safely. Including a human readable representation in the resource makes for a much more robust eco-system and cheaper handling of resources by intermediary systems. Some ecosystems restrict distribution of resources to only those systems that do fully understand the resources, and as a consequence implementers may believe that the narrative is superfluous. However experience shows that such eco-systems often open up to new participants over time." + }], + "key" : "dom-6", + "severity" : "warning", + "human" : "A resource should have narrative for robust management", + "expression" : "text.`div`.exists()", + "xpath" : "exists(f:text/h:div)", + "source" : "http://hl7.org/fhir/StructureDefinition/DomainResource" + }], + "isModifier" : false, + "isSummary" : false, + "mapping" : [{ + "identity" : "rim", + "map" : "Entity. Role, or Act" + }, + { + "identity" : "rim", + "map" : "Patient[classCode=PAT]" + }, + { + "identity" : "cda", + "map" : "ClinicalDocument.recordTarget.patientRole" + }] + }, + { + "id" : "Patient.id", + "path" : "Patient.id", + "short" : "Logical id of this artifact", + "definition" : "The logical id of the resource, as used in the URL for the resource. Once assigned, this value never changes.", + "comment" : "The only time that a resource does not have an id is when it is being submitted to the server using a create operation.", + "min" : 0, + "max" : "1", + "base" : { + "path" : "Resource.id", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "extension" : [{ + "url" : "http://hl7.org/fhir/StructureDefinition/structuredefinition-fhir-type", + "valueUrl" : "id" + }], + "code" : "http://hl7.org/fhirpath/System.String" + }], + "isModifier" : false, + "isSummary" : true + }, + { + "id" : "Patient.meta", + "path" : "Patient.meta", + "short" : "Metadata about the resource", + "definition" : "The metadata about the resource. This is content that is maintained by the infrastructure. Changes to the content might not always be associated with version changes to the resource.", + "min" : 0, + "max" : "1", + "base" : { + "path" : "Resource.meta", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "code" : "Meta" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : true + }, + { + "id" : "Patient.implicitRules", + "path" : "Patient.implicitRules", + "short" : "A set of rules under which this content was created", + "definition" : "A reference to a set of rules that were followed when the resource was constructed, and which must be understood when processing the content. Often, this is a reference to an implementation guide that defines the special rules along with other profiles etc.", + "comment" : "Asserting this rule set restricts the content to be only understood by a limited set of trading partners. This inherently limits the usefulness of the data in the long term. However, the existing health eco-system is highly fractured, and not yet ready to define, collect, and exchange data in a generally computable sense. Wherever possible, implementers and/or specification writers should avoid using this element. Often, when used, the URL is a reference to an implementation guide that defines these special rules as part of it's narrative along with other profiles, value sets, etc.", + "min" : 0, + "max" : "1", + "base" : { + "path" : "Resource.implicitRules", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "code" : "uri" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : true, + "isModifierReason" : "This element is labeled as a modifier because the implicit rules may provide additional knowledge about the resource that modifies it's meaning or interpretation", + "isSummary" : true + }, + { + "id" : "Patient.language", + "path" : "Patient.language", + "short" : "Language of the resource content", + "definition" : "The base language in which the resource is written.", + "comment" : "Language is provided to support indexing and accessibility (typically, services such as text to speech use the language tag). The html language tag in the narrative applies to the narrative. The language tag on the resource may be used to specify the language of other presentations generated from the data in the resource. Not all the content has to be in the base language. The Resource.language should not be assumed to apply to the narrative automatically. If a language is specified, it should it also be specified on the div element in the html (see rules in HTML5 for information about the relationship between xml:lang and the html lang attribute).", + "min" : 0, + "max" : "1", + "base" : { + "path" : "Resource.language", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "code" : "code" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : false, + "binding" : { + "extension" : [{ + "url" : "http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet", + "valueCanonical" : "http://hl7.org/fhir/ValueSet/all-languages" + }, + { + "url" : "http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", + "valueString" : "Language" + }], + "strength" : "preferred", + "description" : "A human language.", + "valueSet" : "http://hl7.org/fhir/ValueSet/languages|4.0.1" + } + }, + { + "id" : "Patient.text", + "path" : "Patient.text", + "short" : "Text summary of the resource, for human interpretation", + "definition" : "A human-readable narrative that contains a summary of the resource and can be used to represent the content of the resource to a human. The narrative need not encode all the structured data, but is required to contain sufficient detail to make it \"clinically safe\" for a human to just read the narrative. Resource definitions may define what content should be represented in the narrative to ensure clinical safety.", + "comment" : "Contained resources do not have narrative. Resources that are not contained SHOULD have a narrative. In some cases, a resource may only have text with little or no additional discrete data (as long as all minOccurs=1 elements are satisfied). This may be necessary for data from legacy systems where information is captured as a \"text blob\" or where text is additionally entered raw or narrated and encoded information is added later.", + "alias" : ["narrative", + "html", + "xhtml", + "display"], + "min" : 0, + "max" : "1", + "base" : { + "path" : "DomainResource.text", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "code" : "Narrative" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : false, + "mapping" : [{ + "identity" : "rim", + "map" : "Act.text?" + }] + }, + { + "id" : "Patient.contained", + "path" : "Patient.contained", + "short" : "Contained, inline Resources", + "definition" : "These resources do not have an independent existence apart from the resource that contains them - they cannot be identified independently, and nor can they have their own independent transaction scope.", + "comment" : "This should never be done when the content can be identified properly, as once identification is lost, it is extremely difficult (and context dependent) to restore it again. Contained resources may have profiles and tags In their meta elements, but SHALL NOT have security labels.", + "alias" : ["inline resources", + "anonymous resources", + "contained resources"], + "min" : 0, + "max" : "*", + "base" : { + "path" : "DomainResource.contained", + "min" : 0, + "max" : "*" + }, + "type" : [{ + "code" : "Resource" + }], + "isModifier" : false, + "isSummary" : false, + "mapping" : [{ + "identity" : "rim", + "map" : "N/A" + }] + }, + { + "id" : "Patient.extension", + "path" : "Patient.extension", + "slicing" : { + "discriminator" : [{ + "type" : "value", + "path" : "url" + }], + "ordered" : false, + "rules" : "open" + }, + "short" : "Extension", + "definition" : "An Extension", + "min" : 0, + "max" : "*", + "base" : { + "path" : "DomainResource.extension", + "min" : 0, + "max" : "*" + }, + "type" : [{ + "code" : "Extension" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }, + { + "key" : "ext-1", + "severity" : "error", + "human" : "Must have either extensions or value[x], not both", + "expression" : "extension.exists() != value.exists()", + "xpath" : "exists(f:extension)!=exists(f:*[starts-with(local-name(.), \"value\")])", + "source" : "http://hl7.org/fhir/StructureDefinition/Extension" + }], + "isModifier" : false, + "isSummary" : false + }, + { + "id" : "Patient.extension:genderIdentity", + "path" : "Patient.extension", + "sliceName" : "genderIdentity", + "short" : "The individual's gender identity", + "definition" : "An individual's personal sense of being a man, woman, boy, girl, nonbinary, or something else.", + "comment" : "This represents an individual’s identity, ascertained by asking them what that identity is.", + "min" : 0, + "max" : "*", + "base" : { + "path" : "DomainResource.extension", + "min" : 0, + "max" : "*" + }, + "type" : [{ + "code" : "Extension", + "profile" : ["http://hl7.org/fhir/StructureDefinition/individual-genderIdentity|5.3.0-ballot-tc1"] + }], + "condition" : ["ele-1"], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }, + { + "key" : "ext-1", + "severity" : "error", + "human" : "Must have either extensions or value[x], not both", + "expression" : "extension.exists() != value.exists()", + "xpath" : "exists(f:extension)!=exists(f:*[starts-with(local-name(.), 'value')])", + "source" : "http://hl7.org/fhir/StructureDefinition/Extension" + }], + "isModifier" : false, + "isSummary" : false + }, + { + "id" : "Patient.extension:personalPronouns", + "path" : "Patient.extension", + "sliceName" : "personalPronouns", + "short" : "The pronouns to use when communicating about an individual.", + "definition" : "The pronouns to use when referring to an individual in verbal or written communication.", + "min" : 0, + "max" : "*", + "base" : { + "path" : "DomainResource.extension", + "min" : 0, + "max" : "*" + }, + "type" : [{ + "code" : "Extension", + "profile" : ["http://hl7.org/fhir/StructureDefinition/individual-pronouns|5.3.0-ballot-tc1"] + }], + "condition" : ["ele-1"], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }, + { + "key" : "ext-1", + "severity" : "error", + "human" : "Must have either extensions or value[x], not both", + "expression" : "extension.exists() != value.exists()", + "xpath" : "exists(f:extension)!=exists(f:*[starts-with(local-name(.), 'value')])", + "source" : "http://hl7.org/fhir/StructureDefinition/Extension" + }], + "isModifier" : false, + "isSummary" : false + }, + { + "id" : "Patient.modifierExtension", + "path" : "Patient.modifierExtension", + "short" : "Extensions that cannot be ignored", + "definition" : "May be used to represent additional information that is not part of the basic definition of the resource and that modifies the understanding of the element that contains it and/or the understanding of the containing element's descendants. Usually modifier elements provide negation or qualification. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer is allowed to define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension. Applications processing a resource are required to check for modifier extensions.\n\nModifier extensions SHALL NOT change the meaning of any elements on Resource or DomainResource (including cannot change the meaning of modifierExtension itself).", + "comment" : "There can be no stigma associated with the use of extensions by any application, project, or standard - regardless of the institution or jurisdiction that uses or defines the extensions. The use of extensions is what allows the FHIR specification to retain a core level of simplicity for everyone.", + "requirements" : "Modifier extensions allow for extensions that *cannot* be safely ignored to be clearly distinguished from the vast majority of extensions which can be safely ignored. This promotes interoperability by eliminating the need for implementers to prohibit the presence of extensions. For further information, see the [definition of modifier extensions](http://hl7.org/fhir/R4/extensibility.html#modifierExtension).", + "alias" : ["extensions", + "user content"], + "min" : 0, + "max" : "*", + "base" : { + "path" : "DomainResource.modifierExtension", + "min" : 0, + "max" : "*" + }, + "type" : [{ + "code" : "Extension" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }, + { + "key" : "ext-1", + "severity" : "error", + "human" : "Must have either extensions or value[x], not both", + "expression" : "extension.exists() != value.exists()", + "xpath" : "exists(f:extension)!=exists(f:*[starts-with(local-name(.), \"value\")])", + "source" : "http://hl7.org/fhir/StructureDefinition/Extension" + }], + "isModifier" : true, + "isModifierReason" : "Modifier extensions are expected to modify the meaning or interpretation of the resource that contains them", + "isSummary" : false, + "mapping" : [{ + "identity" : "rim", + "map" : "N/A" + }] + }, + { + "id" : "Patient.identifier", + "extension" : [{ + "extension" : [{ + "url" : "code", + "valueCode" : "SHALL:populate-if-known" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Creator" + }, + { + "url" : "http://hl7.org/fhir/tools/StructureDefinition/snapshot-source", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/StructureDefinition/Patient-uv-ips|2.0.0" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }, + { + "extension" : [{ + "url" : "code", + "valueCode" : "SHALL:handle" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Consumer" + }, + { + "url" : "http://hl7.org/fhir/tools/StructureDefinition/snapshot-source", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/StructureDefinition/Patient-uv-ips|2.0.0" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }, + { + "extension" : [{ + "url" : "code", + "valueCode" : "SHOULD:display" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Consumer" + }, + { + "url" : "http://hl7.org/fhir/tools/StructureDefinition/snapshot-source", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/StructureDefinition/Patient-uv-ips|2.0.0" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }], + "path" : "Patient.identifier", + "short" : "An identifier for this patient", + "definition" : "An identifier for this patient.", + "requirements" : "Patients are almost always assigned specific numerical identifiers.", + "min" : 0, + "max" : "*", + "base" : { + "path" : "Patient.identifier", + "min" : 0, + "max" : "*" + }, + "type" : [{ + "code" : "Identifier" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "mustSupport" : true, + "isModifier" : false, + "isSummary" : true, + "mapping" : [{ + "identity" : "w5", + "map" : "FiveWs.identifier" + }, + { + "identity" : "v2", + "map" : "PID-3" + }, + { + "identity" : "rim", + "map" : "id" + }, + { + "identity" : "cda", + "map" : ".id" + }] + }, + { + "id" : "Patient.active", + "path" : "Patient.active", + "short" : "Whether this patient's record is in active use", + "definition" : "Whether this patient record is in active use. \nMany systems use this property to mark as non-current patients, such as those that have not been seen for a period of time based on an organization's business rules.\n\nIt is often used to filter patient lists to exclude inactive patients\n\nDeceased patients may also be marked as inactive for the same reasons, but may be active for some time after death.", + "comment" : "If a record is inactive, and linked to an active record, then future patient/record updates should occur on the other patient.", + "requirements" : "Need to be able to mark a patient record as not to be used because it was created in error.", + "min" : 0, + "max" : "1", + "base" : { + "path" : "Patient.active", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "code" : "boolean" + }], + "meaningWhenMissing" : "This resource is generally assumed to be active if no value is provided for the active element", + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : true, + "isModifierReason" : "This element is labelled as a modifier because it is a status element that can indicate that a record should not be treated as valid", + "isSummary" : true, + "mapping" : [{ + "identity" : "w5", + "map" : "FiveWs.status" + }, + { + "identity" : "rim", + "map" : "statusCode" + }, + { + "identity" : "cda", + "map" : "n/a" + }] + }, + { + "id" : "Patient.name", + "extension" : [{ + "extension" : [{ + "url" : "code", + "valueCode" : "SHALL:populate" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Creator" + }, + { + "url" : "http://hl7.org/fhir/tools/StructureDefinition/snapshot-source", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/StructureDefinition/Patient-uv-ips|2.0.0" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }, + { + "extension" : [{ + "url" : "code", + "valueCode" : "SHALL:handle" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Consumer" + }, + { + "url" : "http://hl7.org/fhir/tools/StructureDefinition/snapshot-source", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/StructureDefinition/Patient-uv-ips|2.0.0" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }, + { + "extension" : [{ + "url" : "code", + "valueCode" : "SHOULD:display" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Consumer" + }, + { + "url" : "http://hl7.org/fhir/tools/StructureDefinition/snapshot-source", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/StructureDefinition/Patient-uv-ips|2.0.0" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }], + "path" : "Patient.name", + "short" : "A name associated with the patient", + "definition" : "A name associated with the individual.", + "comment" : "A patient may have multiple names with different uses or applicable periods. For animals, the name is a \"HumanName\" in the sense that is assigned and used by humans and has the same patterns.", + "requirements" : "Need to be able to track the patient by multiple names. Examples are your official name and a partner name.\r\nThe Alphabetic representation of the name SHALL be always provided", + "min" : 1, + "max" : "*", + "base" : { + "path" : "Patient.name", + "min" : 0, + "max" : "*" + }, + "type" : [{ + "code" : "HumanName" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }, + { + "key" : "ips-pat-1", + "severity" : "error", + "human" : "Patient.name.given, Patient.name.family or Patient.name.text SHALL be present", + "expression" : "family.exists() or given.exists() or text.exists()", + "xpath" : "f:given or f:family or f:text", + "source" : "http://hl7.org/fhir/uv/ips/StructureDefinition/Patient-uv-ips" + }], + "mustSupport" : true, + "isModifier" : false, + "isSummary" : true, + "mapping" : [{ + "identity" : "v2", + "map" : "PID-5, PID-9" + }, + { + "identity" : "rim", + "map" : "name" + }, + { + "identity" : "cda", + "map" : ".patient.name" + }] + }, + { + "id" : "Patient.name.id", + "path" : "Patient.name.id", + "representation" : ["xmlAttr"], + "short" : "Unique id for inter-element referencing", + "definition" : "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.", + "min" : 0, + "max" : "1", + "base" : { + "path" : "Element.id", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "extension" : [{ + "url" : "http://hl7.org/fhir/StructureDefinition/structuredefinition-fhir-type", + "valueUrl" : "string" + }], + "code" : "http://hl7.org/fhirpath/System.String" + }], + "isModifier" : false, + "isSummary" : false, + "mapping" : [{ + "identity" : "rim", + "map" : "n/a" + }] + }, + { + "id" : "Patient.name.extension", + "path" : "Patient.name.extension", + "slicing" : { + "discriminator" : [{ + "type" : "value", + "path" : "url" + }], + "description" : "Extensions are always sliced by (at least) url", + "rules" : "open" + }, + "short" : "Additional content defined by implementations", + "definition" : "May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension.", + "comment" : "There can be no stigma associated with the use of extensions by any application, project, or standard - regardless of the institution or jurisdiction that uses or defines the extensions. The use of extensions is what allows the FHIR specification to retain a core level of simplicity for everyone.", + "alias" : ["extensions", + "user content"], + "min" : 0, + "max" : "*", + "base" : { + "path" : "Element.extension", + "min" : 0, + "max" : "*" + }, + "type" : [{ + "code" : "Extension" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }, + { + "key" : "ext-1", + "severity" : "error", + "human" : "Must have either extensions or value[x], not both", + "expression" : "extension.exists() != value.exists()", + "xpath" : "exists(f:extension)!=exists(f:*[starts-with(local-name(.), \"value\")])", + "source" : "http://hl7.org/fhir/StructureDefinition/Extension" + }], + "isModifier" : false, + "isSummary" : false, + "mapping" : [{ + "identity" : "rim", + "map" : "n/a" + }] + }, + { + "id" : "Patient.name.use", + "extension" : [{ + "extension" : [{ + "url" : "code", + "valueCode" : "SHALL:populate-if-known" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Creator" + }, + { + "url" : "http://hl7.org/fhir/tools/StructureDefinition/snapshot-source", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/StructureDefinition/Patient-uv-ips|2.0.0" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }, + { + "extension" : [{ + "url" : "code", + "valueCode" : "SHALL:handle" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Consumer" + }, + { + "url" : "http://hl7.org/fhir/tools/StructureDefinition/snapshot-source", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/StructureDefinition/Patient-uv-ips|2.0.0" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }, + { + "extension" : [{ + "url" : "code", + "valueCode" : "SHOULD:display" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Consumer" + }, + { + "url" : "http://hl7.org/fhir/tools/StructureDefinition/snapshot-source", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/StructureDefinition/Patient-uv-ips|2.0.0" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }], + "path" : "Patient.name.use", + "short" : "usual | official | temp | nickname | anonymous | old | maiden", + "definition" : "Identifies the purpose for this name.", + "comment" : "Applications can assume that a name is current unless it explicitly says that it is temporary or old.", + "requirements" : "Allows the appropriate name for a particular context of use to be selected from among a set of names.", + "min" : 0, + "max" : "1", + "base" : { + "path" : "HumanName.use", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "code" : "code" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "mustSupport" : true, + "isModifier" : true, + "isModifierReason" : "This is labeled as \"Is Modifier\" because applications should not mistake a temporary or old name etc.for a current/permanent one", + "isSummary" : true, + "binding" : { + "extension" : [{ + "url" : "http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", + "valueString" : "NameUse" + }], + "strength" : "required", + "description" : "The use of a human name.", + "valueSet" : "http://hl7.org/fhir/ValueSet/name-use|4.0.1" + }, + "mapping" : [{ + "identity" : "v2", + "map" : "XPN.7, but often indicated by which field contains the name" + }, + { + "identity" : "rim", + "map" : "unique(./use)" + }, + { + "identity" : "servd", + "map" : "./NamePurpose" + }] + }, + { + "id" : "Patient.name.text", + "extension" : [{ + "extension" : [{ + "url" : "code", + "valueCode" : "SHALL:populate-if-known" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Creator" + }, + { + "url" : "http://hl7.org/fhir/tools/StructureDefinition/snapshot-source", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/StructureDefinition/Patient-uv-ips|2.0.0" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }, + { + "extension" : [{ + "url" : "code", + "valueCode" : "SHALL:handle" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Consumer" + }, + { + "url" : "http://hl7.org/fhir/tools/StructureDefinition/snapshot-source", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/StructureDefinition/Patient-uv-ips|2.0.0" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }, + { + "extension" : [{ + "url" : "code", + "valueCode" : "SHOULD:display" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Consumer" + }, + { + "url" : "http://hl7.org/fhir/tools/StructureDefinition/snapshot-source", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/StructureDefinition/Patient-uv-ips|2.0.0" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }], + "path" : "Patient.name.text", + "short" : "Text representation of the full name", + "definition" : "Text representation of the full name. Due to the cultural variance around the world a consuming system may not know how to present the name correctly; moreover, not all the parts of the name go in given or family. Creators are therefore strongly encouraged to provide through this element a presented version of the name. Future versions of this guide may require this element", + "comment" : "Can provide both a text representation and parts. Applications updating a name SHALL ensure that when both text and parts are present, no content is included in the text that isn't found in a part.", + "requirements" : "A renderable, unencoded form.", + "min" : 0, + "max" : "1", + "base" : { + "path" : "HumanName.text", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "code" : "string" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "mustSupport" : true, + "isModifier" : false, + "isSummary" : true, + "mapping" : [{ + "identity" : "v2", + "map" : "implied by XPN.11" + }, + { + "identity" : "rim", + "map" : "./formatted" + }] + }, + { + "id" : "Patient.name.family", + "extension" : [{ + "extension" : [{ + "url" : "code", + "valueCode" : "SHALL:populate-if-known" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Creator" + }, + { + "url" : "http://hl7.org/fhir/tools/StructureDefinition/snapshot-source", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/StructureDefinition/Patient-uv-ips|2.0.0" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }, + { + "extension" : [{ + "url" : "code", + "valueCode" : "SHALL:handle" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Consumer" + }, + { + "url" : "http://hl7.org/fhir/tools/StructureDefinition/snapshot-source", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/StructureDefinition/Patient-uv-ips|2.0.0" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }, + { + "extension" : [{ + "url" : "code", + "valueCode" : "SHOULD:display" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Consumer" + }, + { + "url" : "http://hl7.org/fhir/tools/StructureDefinition/snapshot-source", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/StructureDefinition/Patient-uv-ips|2.0.0" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }], + "path" : "Patient.name.family", + "short" : "Family name (often called 'Surname')", + "definition" : "The part of a name that links to the genealogy. In some cultures (e.g. Eritrea) the family name of a son is the first name of his father.", + "comment" : "Family Name may be decomposed into specific parts using extensions (de, nl, es related cultures).", + "alias" : ["surname"], + "min" : 0, + "max" : "1", + "base" : { + "path" : "HumanName.family", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "code" : "string" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "mustSupport" : true, + "isModifier" : false, + "isSummary" : true, + "mapping" : [{ + "identity" : "v2", + "map" : "XPN.1/FN.1" + }, + { + "identity" : "rim", + "map" : "./part[partType = FAM]" + }, + { + "identity" : "servd", + "map" : "./FamilyName" + }] + }, + { + "id" : "Patient.name.given", + "extension" : [{ + "extension" : [{ + "url" : "code", + "valueCode" : "SHALL:populate-if-known" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Creator" + }, + { + "url" : "http://hl7.org/fhir/tools/StructureDefinition/snapshot-source", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/StructureDefinition/Patient-uv-ips|2.0.0" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }, + { + "extension" : [{ + "url" : "code", + "valueCode" : "SHALL:handle" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Consumer" + }, + { + "url" : "http://hl7.org/fhir/tools/StructureDefinition/snapshot-source", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/StructureDefinition/Patient-uv-ips|2.0.0" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }, + { + "extension" : [{ + "url" : "code", + "valueCode" : "SHOULD:display" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Consumer" + }, + { + "url" : "http://hl7.org/fhir/tools/StructureDefinition/snapshot-source", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/StructureDefinition/Patient-uv-ips|2.0.0" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }], + "path" : "Patient.name.given", + "short" : "Given names (not always 'first'). Includes middle names", + "definition" : "Given name.", + "comment" : "If only initials are recorded, they may be used in place of the full name parts. Initials may be separated into multiple given names but often aren't due to paractical limitations. This element is not called \"first name\" since given names do not always come first.", + "alias" : ["first name", + "middle name"], + "min" : 0, + "max" : "*", + "base" : { + "path" : "HumanName.given", + "min" : 0, + "max" : "*" + }, + "type" : [{ + "code" : "string" + }], + "orderMeaning" : "Given Names appear in the correct order for presenting the name", + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "mustSupport" : true, + "isModifier" : false, + "isSummary" : true, + "mapping" : [{ + "identity" : "v2", + "map" : "XPN.2 + XPN.3" + }, + { + "identity" : "rim", + "map" : "./part[partType = GIV]" + }, + { + "identity" : "servd", + "map" : "./GivenNames" + }] + }, + { + "id" : "Patient.name.prefix", + "path" : "Patient.name.prefix", + "short" : "Parts that come before the name", + "definition" : "Part of the name that is acquired as a title due to academic, legal, employment or nobility status, etc. and that appears at the start of the name.", + "min" : 0, + "max" : "*", + "base" : { + "path" : "HumanName.prefix", + "min" : 0, + "max" : "*" + }, + "type" : [{ + "code" : "string" + }], + "orderMeaning" : "Prefixes appear in the correct order for presenting the name", + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : true, + "mapping" : [{ + "identity" : "v2", + "map" : "XPN.5" + }, + { + "identity" : "rim", + "map" : "./part[partType = PFX]" + }, + { + "identity" : "servd", + "map" : "./TitleCode" + }] + }, + { + "id" : "Patient.name.suffix", + "path" : "Patient.name.suffix", + "short" : "Parts that come after the name", + "definition" : "Part of the name that is acquired as a title due to academic, legal, employment or nobility status, etc. and that appears at the end of the name.", + "min" : 0, + "max" : "*", + "base" : { + "path" : "HumanName.suffix", + "min" : 0, + "max" : "*" + }, + "type" : [{ + "code" : "string" + }], + "orderMeaning" : "Suffixes appear in the correct order for presenting the name", + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : true, + "mapping" : [{ + "identity" : "v2", + "map" : "XPN/4" + }, + { + "identity" : "rim", + "map" : "./part[partType = SFX]" + }] + }, + { + "id" : "Patient.name.period", + "path" : "Patient.name.period", + "short" : "Time period when name was/is in use", + "definition" : "Indicates the period of time when this name was valid for the named person.", + "requirements" : "Allows names to be placed in historical context.", + "min" : 0, + "max" : "1", + "base" : { + "path" : "HumanName.period", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "code" : "Period" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : true, + "mapping" : [{ + "identity" : "v2", + "map" : "XPN.13 + XPN.14" + }, + { + "identity" : "rim", + "map" : "./usablePeriod[type=\"IVL\"]" + }, + { + "identity" : "servd", + "map" : "./StartDate and ./EndDate" + }] + }, + { + "id" : "Patient.telecom", + "extension" : [{ + "extension" : [{ + "url" : "code", + "valueCode" : "SHALL:populate-if-known" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Creator" + }, + { + "url" : "http://hl7.org/fhir/tools/StructureDefinition/snapshot-source", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/StructureDefinition/Patient-uv-ips|2.0.0" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }, + { + "extension" : [{ + "url" : "code", + "valueCode" : "SHALL:handle" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Consumer" + }, + { + "url" : "http://hl7.org/fhir/tools/StructureDefinition/snapshot-source", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/StructureDefinition/Patient-uv-ips|2.0.0" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }, + { + "extension" : [{ + "url" : "code", + "valueCode" : "SHOULD:display" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Consumer" + }, + { + "url" : "http://hl7.org/fhir/tools/StructureDefinition/snapshot-source", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/StructureDefinition/Patient-uv-ips|2.0.0" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }], + "path" : "Patient.telecom", + "short" : "A contact detail for the individual", + "definition" : "A contact detail (e.g. a telephone number or an email address) by which the individual may be contacted.", + "comment" : "A Patient may have multiple ways to be contacted with different uses or applicable periods. May need to have options for contacting the person urgently and also to help with identification. The address might not go directly to the individual, but may reach another party that is able to proxy for the patient (i.e. home phone, or pet owner's phone).", + "requirements" : "People have (primary) ways to contact them in some way such as phone, email.", + "min" : 0, + "max" : "*", + "base" : { + "path" : "Patient.telecom", + "min" : 0, + "max" : "*" + }, + "type" : [{ + "code" : "ContactPoint" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "mustSupport" : true, + "isModifier" : false, + "isSummary" : true, + "mapping" : [{ + "identity" : "v2", + "map" : "PID-13, PID-14, PID-40" + }, + { + "identity" : "rim", + "map" : "telecom" + }, + { + "identity" : "cda", + "map" : ".telecom" + }] + }, + { + "id" : "Patient.gender", + "extension" : [{ + "extension" : [{ + "url" : "code", + "valueCode" : "SHALL:populate-if-known" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Creator" + }, + { + "url" : "http://hl7.org/fhir/tools/StructureDefinition/snapshot-source", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/StructureDefinition/Patient-uv-ips|2.0.0" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }, + { + "extension" : [{ + "url" : "code", + "valueCode" : "SHALL:handle" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Consumer" + }, + { + "url" : "http://hl7.org/fhir/tools/StructureDefinition/snapshot-source", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/StructureDefinition/Patient-uv-ips|2.0.0" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }, + { + "extension" : [{ + "url" : "code", + "valueCode" : "SHOULD:display" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Consumer" + }, + { + "url" : "http://hl7.org/fhir/tools/StructureDefinition/snapshot-source", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/StructureDefinition/Patient-uv-ips|2.0.0" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }], + "path" : "Patient.gender", + "short" : "male | female | other | unknown", + "definition" : "Administrative Gender - the gender that the patient is considered to have for administration and record keeping purposes.", + "comment" : "The gender might not match the biological sex as determined by genetics or the individual's preferred identification. Note that for both humans and particularly animals, there are other legitimate possibilities than male and female, though the vast majority of systems and contexts only support male and female. Systems providing decision support or enforcing business rules should ideally do this on the basis of Observations dealing with the specific sex or gender aspect of interest (anatomical, chromosomal, social, etc.) However, because these observations are infrequently recorded, defaulting to the administrative gender is common practice. Where such defaulting occurs, rule enforcement should allow for the variation between administrative and biological, chromosomal and other gender aspects. For example, an alert about a hysterectomy on a male should be handled as a warning or overridable error, not a \"hard\" error. See the Patient Gender and Sex section for additional information about communicating patient gender and sex.", + "requirements" : "Needed for identification of the individual, in combination with (at least) name and birth date.", + "min" : 0, + "max" : "1", + "base" : { + "path" : "Patient.gender", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "code" : "code" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "mustSupport" : true, + "isModifier" : false, + "isSummary" : true, + "binding" : { + "extension" : [{ + "url" : "http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", + "valueString" : "AdministrativeGender" + }], + "strength" : "required", + "description" : "The gender of a person used for administrative purposes.", + "valueSet" : "http://hl7.org/fhir/ValueSet/administrative-gender|4.0.1" + }, + "mapping" : [{ + "identity" : "v2", + "map" : "PID-8" + }, + { + "identity" : "rim", + "map" : "player[classCode=PSN|ANM and determinerCode=INSTANCE]/administrativeGender" + }, + { + "identity" : "cda", + "map" : ".patient.administrativeGenderCode" + }] + }, + { + "id" : "Patient.birthDate", + "extension" : [{ + "extension" : [{ + "url" : "code", + "valueCode" : "SHALL:populate-if-known" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Creator" + }, + { + "url" : "http://hl7.org/fhir/tools/StructureDefinition/snapshot-source", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/StructureDefinition/Patient-uv-ips|2.0.0" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }, + { + "extension" : [{ + "url" : "code", + "valueCode" : "SHALL:handle" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Consumer" + }, + { + "url" : "http://hl7.org/fhir/tools/StructureDefinition/snapshot-source", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/StructureDefinition/Patient-uv-ips|2.0.0" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }, + { + "extension" : [{ + "url" : "code", + "valueCode" : "SHOULD:display" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Consumer" + }, + { + "url" : "http://hl7.org/fhir/tools/StructureDefinition/snapshot-source", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/StructureDefinition/Patient-uv-ips|2.0.0" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }], + "path" : "Patient.birthDate", + "short" : "The date of birth for the individual", + "definition" : "The date of birth for the individual.", + "comment" : "At least an estimated year should be provided as a guess if the real DOB is unknown There is a standard extension \"patient-birthTime\" available that should be used where Time is required (such as in maternity/infant care systems).", + "requirements" : "Age of the individual drives many clinical processes.", + "min" : 1, + "max" : "1", + "base" : { + "path" : "Patient.birthDate", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "code" : "date" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "mustSupport" : true, + "isModifier" : false, + "isSummary" : true, + "mapping" : [{ + "identity" : "v2", + "map" : "PID-7" + }, + { + "identity" : "rim", + "map" : "player[classCode=PSN|ANM and determinerCode=INSTANCE]/birthTime" + }, + { + "identity" : "cda", + "map" : ".patient.birthTime" + }, + { + "identity" : "loinc", + "map" : "21112-8" + }] + }, + { + "id" : "Patient.deceased[x]", + "path" : "Patient.deceased[x]", + "short" : "Indicates if the individual is deceased or not", + "definition" : "Indicates if the individual is deceased or not.", + "comment" : "If there's no value in the instance, it means there is no statement on whether or not the individual is deceased. Most systems will interpret the absence of a value as a sign of the person being alive.", + "requirements" : "The fact that a patient is deceased influences the clinical process. Also, in human communication and relation management it is necessary to know whether the person is alive.", + "min" : 0, + "max" : "1", + "base" : { + "path" : "Patient.deceased[x]", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "code" : "boolean" + }, + { + "code" : "dateTime" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : true, + "isModifierReason" : "This element is labeled as a modifier because once a patient is marked as deceased, the actions that are appropriate to perform on the patient may be significantly different.", + "isSummary" : true, + "mapping" : [{ + "identity" : "v2", + "map" : "PID-30 (bool) and PID-29 (datetime)" + }, + { + "identity" : "rim", + "map" : "player[classCode=PSN|ANM and determinerCode=INSTANCE]/deceasedInd, player[classCode=PSN|ANM and determinerCode=INSTANCE]/deceasedTime" + }, + { + "identity" : "cda", + "map" : "n/a" + }] + }, + { + "id" : "Patient.address", + "extension" : [{ + "extension" : [{ + "url" : "code", + "valueCode" : "SHALL:populate-if-known" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Creator" + }, + { + "url" : "http://hl7.org/fhir/tools/StructureDefinition/snapshot-source", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/StructureDefinition/Patient-uv-ips|2.0.0" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }, + { + "extension" : [{ + "url" : "code", + "valueCode" : "SHALL:handle" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Consumer" + }, + { + "url" : "http://hl7.org/fhir/tools/StructureDefinition/snapshot-source", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/StructureDefinition/Patient-uv-ips|2.0.0" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }, + { + "extension" : [{ + "url" : "code", + "valueCode" : "SHOULD:display" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Consumer" + }, + { + "url" : "http://hl7.org/fhir/tools/StructureDefinition/snapshot-source", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/StructureDefinition/Patient-uv-ips|2.0.0" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }], + "path" : "Patient.address", + "short" : "An address for the individual", + "definition" : "An address for the individual.", + "comment" : "Patient may have multiple addresses with different uses or applicable periods.", + "requirements" : "May need to keep track of patient addresses for contacting, billing or reporting requirements and also to help with identification.", + "min" : 0, + "max" : "*", + "base" : { + "path" : "Patient.address", + "min" : 0, + "max" : "*" + }, + "type" : [{ + "code" : "Address" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "mustSupport" : true, + "isModifier" : false, + "isSummary" : true, + "mapping" : [{ + "identity" : "v2", + "map" : "PID-11" + }, + { + "identity" : "rim", + "map" : "addr" + }, + { + "identity" : "cda", + "map" : ".addr" + }] + }, + { + "id" : "Patient.maritalStatus", + "path" : "Patient.maritalStatus", + "short" : "Marital (civil) status of a patient", + "definition" : "This field contains a patient's most recent marital (civil) status.", + "requirements" : "Most, if not all systems capture it.", + "min" : 0, + "max" : "1", + "base" : { + "path" : "Patient.maritalStatus", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "code" : "CodeableConcept" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : false, + "binding" : { + "extension" : [{ + "url" : "http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", + "valueString" : "MaritalStatus" + }], + "strength" : "extensible", + "description" : "The domestic partnership status of a person.", + "valueSet" : "http://hl7.org/fhir/ValueSet/marital-status|4.0.1" + }, + "mapping" : [{ + "identity" : "v2", + "map" : "PID-16" + }, + { + "identity" : "rim", + "map" : "player[classCode=PSN]/maritalStatusCode" + }, + { + "identity" : "cda", + "map" : ".patient.maritalStatusCode" + }] + }, + { + "id" : "Patient.multipleBirth[x]", + "path" : "Patient.multipleBirth[x]", + "short" : "Whether patient is part of a multiple birth", + "definition" : "Indicates whether the patient is part of a multiple (boolean) or indicates the actual birth order (integer).", + "comment" : "Where the valueInteger is provided, the number is the birth number in the sequence. E.g. The middle birth in triplets would be valueInteger=2 and the third born would have valueInteger=3 If a boolean value was provided for this triplets example, then all 3 patient records would have valueBoolean=true (the ordering is not indicated).", + "requirements" : "For disambiguation of multiple-birth children, especially relevant where the care provider doesn't meet the patient, such as labs.", + "min" : 0, + "max" : "1", + "base" : { + "path" : "Patient.multipleBirth[x]", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "code" : "boolean" + }, + { + "code" : "integer" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : false, + "mapping" : [{ + "identity" : "v2", + "map" : "PID-24 (bool), PID-25 (integer)" + }, + { + "identity" : "rim", + "map" : "player[classCode=PSN|ANM and determinerCode=INSTANCE]/multipleBirthInd, player[classCode=PSN|ANM and determinerCode=INSTANCE]/multipleBirthOrderNumber" + }, + { + "identity" : "cda", + "map" : "n/a" + }] + }, + { + "id" : "Patient.photo", + "path" : "Patient.photo", + "short" : "Image of the patient", + "definition" : "Image of the patient.", + "comment" : "Guidelines:\n* Use id photos, not clinical photos.\n* Limit dimensions to thumbnail.\n* Keep byte count low to ease resource updates.", + "requirements" : "Many EHR systems have the capability to capture an image of the patient. Fits with newer social media usage too.", + "min" : 0, + "max" : "*", + "base" : { + "path" : "Patient.photo", + "min" : 0, + "max" : "*" + }, + "type" : [{ + "code" : "Attachment" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : false, + "mapping" : [{ + "identity" : "v2", + "map" : "OBX-5 - needs a profile" + }, + { + "identity" : "rim", + "map" : "player[classCode=PSN|ANM and determinerCode=INSTANCE]/desc" + }, + { + "identity" : "cda", + "map" : "n/a" + }] + }, + { + "id" : "Patient.contact", + "path" : "Patient.contact", + "short" : "A contact party (e.g. guardian, partner, friend) for the patient", + "definition" : "A contact party (e.g. guardian, partner, friend) for the patient.", + "comment" : "Contact covers all kinds of contact parties: family members, business contacts, guardians, caregivers. Not applicable to register pedigree and family ties beyond use of having contact.", + "requirements" : "Need to track people you can contact about the patient.", + "min" : 0, + "max" : "*", + "base" : { + "path" : "Patient.contact", + "min" : 0, + "max" : "*" + }, + "type" : [{ + "code" : "BackboneElement" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }, + { + "key" : "pat-1", + "severity" : "error", + "human" : "SHALL at least contain a contact's details or a reference to an organization", + "expression" : "name.exists() or telecom.exists() or address.exists() or organization.exists()", + "xpath" : "exists(f:name) or exists(f:telecom) or exists(f:address) or exists(f:organization)", + "source" : "http://hl7.org/fhir/StructureDefinition/Patient" + }], + "isModifier" : false, + "isSummary" : false, + "mapping" : [{ + "identity" : "rim", + "map" : "player[classCode=PSN|ANM and determinerCode=INSTANCE]/scopedRole[classCode=CON]" + }, + { + "identity" : "cda", + "map" : "n/a" + }] + }, + { + "id" : "Patient.contact.id", + "path" : "Patient.contact.id", + "representation" : ["xmlAttr"], + "short" : "Unique id for inter-element referencing", + "definition" : "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.", + "min" : 0, + "max" : "1", + "base" : { + "path" : "Element.id", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "extension" : [{ + "url" : "http://hl7.org/fhir/StructureDefinition/structuredefinition-fhir-type", + "valueUrl" : "string" + }], + "code" : "http://hl7.org/fhirpath/System.String" + }], + "isModifier" : false, + "isSummary" : false, + "mapping" : [{ + "identity" : "rim", + "map" : "n/a" + }] + }, + { + "id" : "Patient.contact.extension", + "path" : "Patient.contact.extension", + "short" : "Additional content defined by implementations", + "definition" : "May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension.", + "comment" : "There can be no stigma associated with the use of extensions by any application, project, or standard - regardless of the institution or jurisdiction that uses or defines the extensions. The use of extensions is what allows the FHIR specification to retain a core level of simplicity for everyone.", + "alias" : ["extensions", + "user content"], + "min" : 0, + "max" : "*", + "base" : { + "path" : "Element.extension", + "min" : 0, + "max" : "*" + }, + "type" : [{ + "code" : "Extension" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }, + { + "key" : "ext-1", + "severity" : "error", + "human" : "Must have either extensions or value[x], not both", + "expression" : "extension.exists() != value.exists()", + "xpath" : "exists(f:extension)!=exists(f:*[starts-with(local-name(.), \"value\")])", + "source" : "http://hl7.org/fhir/StructureDefinition/Extension" + }], + "isModifier" : false, + "isSummary" : false, + "mapping" : [{ + "identity" : "rim", + "map" : "n/a" + }] + }, + { + "id" : "Patient.contact.modifierExtension", + "path" : "Patient.contact.modifierExtension", + "short" : "Extensions that cannot be ignored even if unrecognized", + "definition" : "May be used to represent additional information that is not part of the basic definition of the element and that modifies the understanding of the element in which it is contained and/or the understanding of the containing element's descendants. Usually modifier elements provide negation or qualification. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension. Applications processing a resource are required to check for modifier extensions.\n\nModifier extensions SHALL NOT change the meaning of any elements on Resource or DomainResource (including cannot change the meaning of modifierExtension itself).", + "comment" : "There can be no stigma associated with the use of extensions by any application, project, or standard - regardless of the institution or jurisdiction that uses or defines the extensions. The use of extensions is what allows the FHIR specification to retain a core level of simplicity for everyone.", + "requirements" : "Modifier extensions allow for extensions that *cannot* be safely ignored to be clearly distinguished from the vast majority of extensions which can be safely ignored. This promotes interoperability by eliminating the need for implementers to prohibit the presence of extensions. For further information, see the [definition of modifier extensions](http://hl7.org/fhir/R4/extensibility.html#modifierExtension).", + "alias" : ["extensions", + "user content", + "modifiers"], + "min" : 0, + "max" : "*", + "base" : { + "path" : "BackboneElement.modifierExtension", + "min" : 0, + "max" : "*" + }, + "type" : [{ + "code" : "Extension" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }, + { + "key" : "ext-1", + "severity" : "error", + "human" : "Must have either extensions or value[x], not both", + "expression" : "extension.exists() != value.exists()", + "xpath" : "exists(f:extension)!=exists(f:*[starts-with(local-name(.), \"value\")])", + "source" : "http://hl7.org/fhir/StructureDefinition/Extension" + }], + "isModifier" : true, + "isModifierReason" : "Modifier extensions are expected to modify the meaning or interpretation of the element that contains them", + "isSummary" : true, + "mapping" : [{ + "identity" : "rim", + "map" : "N/A" + }] + }, + { + "id" : "Patient.contact.relationship", + "path" : "Patient.contact.relationship", + "short" : "The kind of relationship", + "definition" : "The nature of the relationship between the patient and the contact person.", + "requirements" : "Used to determine which contact person is the most relevant to approach, depending on circumstances.", + "min" : 0, + "max" : "*", + "base" : { + "path" : "Patient.contact.relationship", + "min" : 0, + "max" : "*" + }, + "type" : [{ + "code" : "CodeableConcept" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : false, + "binding" : { + "extension" : [{ + "url" : "http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", + "valueString" : "ContactRelationship" + }], + "strength" : "extensible", + "description" : "The nature of the relationship between a patient and a contact person for that patient.", + "valueSet" : "http://hl7.org/fhir/ValueSet/patient-contactrelationship|4.0.1" + }, + "mapping" : [{ + "identity" : "v2", + "map" : "NK1-7, NK1-3" + }, + { + "identity" : "rim", + "map" : "code" + }, + { + "identity" : "cda", + "map" : "n/a" + }] + }, + { + "id" : "Patient.contact.name", + "path" : "Patient.contact.name", + "short" : "A name associated with the contact person", + "definition" : "A name associated with the contact person.", + "requirements" : "Contact persons need to be identified by name, but it is uncommon to need details about multiple other names for that contact person.", + "min" : 0, + "max" : "1", + "base" : { + "path" : "Patient.contact.name", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "code" : "HumanName" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : false, + "mapping" : [{ + "identity" : "v2", + "map" : "NK1-2" + }, + { + "identity" : "rim", + "map" : "name" + }, + { + "identity" : "cda", + "map" : "n/a" + }] + }, + { + "id" : "Patient.contact.telecom", + "path" : "Patient.contact.telecom", + "short" : "A contact detail for the person", + "definition" : "A contact detail for the person, e.g. a telephone number or an email address.", + "comment" : "Contact may have multiple ways to be contacted with different uses or applicable periods. May need to have options for contacting the person urgently, and also to help with identification.", + "requirements" : "People have (primary) ways to contact them in some way such as phone, email.", + "min" : 0, + "max" : "*", + "base" : { + "path" : "Patient.contact.telecom", + "min" : 0, + "max" : "*" + }, + "type" : [{ + "code" : "ContactPoint" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : false, + "mapping" : [{ + "identity" : "v2", + "map" : "NK1-5, NK1-6, NK1-40" + }, + { + "identity" : "rim", + "map" : "telecom" + }, + { + "identity" : "cda", + "map" : "n/a" + }] + }, + { + "id" : "Patient.contact.address", + "path" : "Patient.contact.address", + "short" : "Address for the contact person", + "definition" : "Address for the contact person.", + "requirements" : "Need to keep track where the contact person can be contacted per postal mail or visited.", + "min" : 0, + "max" : "1", + "base" : { + "path" : "Patient.contact.address", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "code" : "Address" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : false, + "mapping" : [{ + "identity" : "v2", + "map" : "NK1-4" + }, + { + "identity" : "rim", + "map" : "addr" + }, + { + "identity" : "cda", + "map" : "n/a" + }] + }, + { + "id" : "Patient.contact.gender", + "path" : "Patient.contact.gender", + "short" : "male | female | other | unknown", + "definition" : "Administrative Gender - the gender that the contact person is considered to have for administration and record keeping purposes.", + "requirements" : "Needed to address the person correctly.", + "min" : 0, + "max" : "1", + "base" : { + "path" : "Patient.contact.gender", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "code" : "code" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : false, + "binding" : { + "extension" : [{ + "url" : "http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", + "valueString" : "AdministrativeGender" + }], + "strength" : "required", + "description" : "The gender of a person used for administrative purposes.", + "valueSet" : "http://hl7.org/fhir/ValueSet/administrative-gender|4.0.1" + }, + "mapping" : [{ + "identity" : "v2", + "map" : "NK1-15" + }, + { + "identity" : "rim", + "map" : "player[classCode=PSN|ANM and determinerCode=INSTANCE]/administrativeGender" + }, + { + "identity" : "cda", + "map" : "n/a" + }] + }, + { + "id" : "Patient.contact.organization", + "path" : "Patient.contact.organization", + "short" : "Organization that is associated with the contact", + "definition" : "Organization on behalf of which the contact is acting or for which the contact is working.", + "requirements" : "For guardians or business related contacts, the organization is relevant.", + "min" : 0, + "max" : "1", + "base" : { + "path" : "Patient.contact.organization", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "code" : "Reference", + "targetProfile" : ["http://hl7.org/fhir/StructureDefinition/Organization|4.0.1"] + }], + "condition" : ["pat-1"], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : false, + "mapping" : [{ + "identity" : "v2", + "map" : "NK1-13, NK1-30, NK1-31, NK1-32, NK1-41" + }, + { + "identity" : "rim", + "map" : "scoper" + }, + { + "identity" : "cda", + "map" : "n/a" + }] + }, + { + "id" : "Patient.contact.period", + "path" : "Patient.contact.period", + "short" : "The period during which this contact person or organization is valid to be contacted relating to this patient", + "definition" : "The period during which this contact person or organization is valid to be contacted relating to this patient.", + "min" : 0, + "max" : "1", + "base" : { + "path" : "Patient.contact.period", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "code" : "Period" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : false, + "mapping" : [{ + "identity" : "rim", + "map" : "effectiveTime" + }, + { + "identity" : "cda", + "map" : "n/a" + }] + }, + { + "id" : "Patient.communication", + "path" : "Patient.communication", + "short" : "A language which may be used to communicate with the patient about his or her health", + "definition" : "A language which may be used to communicate with the patient about his or her health.", + "comment" : "If no language is specified, this *implies* that the default local language is spoken. If you need to convey proficiency for multiple modes, then you need multiple Patient.Communication associations. For animals, language is not a relevant field, and should be absent from the instance. If the Patient does not speak the default local language, then the Interpreter Required Standard can be used to explicitly declare that an interpreter is required.", + "requirements" : "If a patient does not speak the local language, interpreters may be required, so languages spoken and proficiency are important things to keep track of both for patient and other persons of interest.", + "min" : 0, + "max" : "*", + "base" : { + "path" : "Patient.communication", + "min" : 0, + "max" : "*" + }, + "type" : [{ + "code" : "BackboneElement" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : false, + "mapping" : [{ + "identity" : "rim", + "map" : "LanguageCommunication" + }, + { + "identity" : "cda", + "map" : "patient.languageCommunication" + }] + }, + { + "id" : "Patient.communication.id", + "path" : "Patient.communication.id", + "representation" : ["xmlAttr"], + "short" : "Unique id for inter-element referencing", + "definition" : "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.", + "min" : 0, + "max" : "1", + "base" : { + "path" : "Element.id", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "extension" : [{ + "url" : "http://hl7.org/fhir/StructureDefinition/structuredefinition-fhir-type", + "valueUrl" : "string" + }], + "code" : "http://hl7.org/fhirpath/System.String" + }], + "isModifier" : false, + "isSummary" : false, + "mapping" : [{ + "identity" : "rim", + "map" : "n/a" + }] + }, + { + "id" : "Patient.communication.extension", + "path" : "Patient.communication.extension", + "short" : "Additional content defined by implementations", + "definition" : "May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension.", + "comment" : "There can be no stigma associated with the use of extensions by any application, project, or standard - regardless of the institution or jurisdiction that uses or defines the extensions. The use of extensions is what allows the FHIR specification to retain a core level of simplicity for everyone.", + "alias" : ["extensions", + "user content"], + "min" : 0, + "max" : "*", + "base" : { + "path" : "Element.extension", + "min" : 0, + "max" : "*" + }, + "type" : [{ + "code" : "Extension" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }, + { + "key" : "ext-1", + "severity" : "error", + "human" : "Must have either extensions or value[x], not both", + "expression" : "extension.exists() != value.exists()", + "xpath" : "exists(f:extension)!=exists(f:*[starts-with(local-name(.), \"value\")])", + "source" : "http://hl7.org/fhir/StructureDefinition/Extension" + }], + "isModifier" : false, + "isSummary" : false, + "mapping" : [{ + "identity" : "rim", + "map" : "n/a" + }] + }, + { + "id" : "Patient.communication.modifierExtension", + "path" : "Patient.communication.modifierExtension", + "short" : "Extensions that cannot be ignored even if unrecognized", + "definition" : "May be used to represent additional information that is not part of the basic definition of the element and that modifies the understanding of the element in which it is contained and/or the understanding of the containing element's descendants. Usually modifier elements provide negation or qualification. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension. Applications processing a resource are required to check for modifier extensions.\n\nModifier extensions SHALL NOT change the meaning of any elements on Resource or DomainResource (including cannot change the meaning of modifierExtension itself).", + "comment" : "There can be no stigma associated with the use of extensions by any application, project, or standard - regardless of the institution or jurisdiction that uses or defines the extensions. The use of extensions is what allows the FHIR specification to retain a core level of simplicity for everyone.", + "requirements" : "Modifier extensions allow for extensions that *cannot* be safely ignored to be clearly distinguished from the vast majority of extensions which can be safely ignored. This promotes interoperability by eliminating the need for implementers to prohibit the presence of extensions. For further information, see the [definition of modifier extensions](http://hl7.org/fhir/R4/extensibility.html#modifierExtension).", + "alias" : ["extensions", + "user content", + "modifiers"], + "min" : 0, + "max" : "*", + "base" : { + "path" : "BackboneElement.modifierExtension", + "min" : 0, + "max" : "*" + }, + "type" : [{ + "code" : "Extension" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }, + { + "key" : "ext-1", + "severity" : "error", + "human" : "Must have either extensions or value[x], not both", + "expression" : "extension.exists() != value.exists()", + "xpath" : "exists(f:extension)!=exists(f:*[starts-with(local-name(.), \"value\")])", + "source" : "http://hl7.org/fhir/StructureDefinition/Extension" + }], + "isModifier" : true, + "isModifierReason" : "Modifier extensions are expected to modify the meaning or interpretation of the element that contains them", + "isSummary" : true, + "mapping" : [{ + "identity" : "rim", + "map" : "N/A" + }] + }, + { + "id" : "Patient.communication.language", + "path" : "Patient.communication.language", + "short" : "The language which can be used to communicate with the patient about his or her health", + "definition" : "The ISO-639-1 alpha 2 code in lower case for the language, optionally followed by a hyphen and the ISO-3166-1 alpha 2 code for the region in upper case; e.g. \"en\" for English, or \"en-US\" for American English versus \"en-EN\" for England English.", + "comment" : "The structure aa-BB with this exact casing is one the most widely used notations for locale. However not all systems actually code this but instead have it as free text. Hence CodeableConcept instead of code as the data type.", + "requirements" : "Most systems in multilingual countries will want to convey language. Not all systems actually need the regional dialect.", + "min" : 1, + "max" : "1", + "base" : { + "path" : "Patient.communication.language", + "min" : 1, + "max" : "1" + }, + "type" : [{ + "code" : "CodeableConcept" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : false, + "binding" : { + "extension" : [{ + "url" : "http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet", + "valueCanonical" : "http://hl7.org/fhir/ValueSet/all-languages" + }, + { + "url" : "http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", + "valueString" : "Language" + }], + "strength" : "preferred", + "description" : "A human language.", + "valueSet" : "http://hl7.org/fhir/ValueSet/languages|4.0.1" + }, + "mapping" : [{ + "identity" : "v2", + "map" : "PID-15, LAN-2" + }, + { + "identity" : "rim", + "map" : "player[classCode=PSN|ANM and determinerCode=INSTANCE]/languageCommunication/code" + }, + { + "identity" : "cda", + "map" : ".languageCode" + }] + }, + { + "id" : "Patient.communication.preferred", + "path" : "Patient.communication.preferred", + "short" : "Language preference indicator", + "definition" : "Indicates whether or not the patient prefers this language (over other languages he masters up a certain level).", + "comment" : "This language is specifically identified for communicating healthcare information.", + "requirements" : "People that master multiple languages up to certain level may prefer one or more, i.e. feel more confident in communicating in a particular language making other languages sort of a fall back method.", + "min" : 0, + "max" : "1", + "base" : { + "path" : "Patient.communication.preferred", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "code" : "boolean" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : false, + "mapping" : [{ + "identity" : "v2", + "map" : "PID-15" + }, + { + "identity" : "rim", + "map" : "preferenceInd" + }, + { + "identity" : "cda", + "map" : ".preferenceInd" + }] + }, + { + "id" : "Patient.generalPractitioner", + "extension" : [{ + "extension" : [{ + "url" : "code", + "valueCode" : "SHALL:populate-if-known" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Creator" + }, + { + "url" : "http://hl7.org/fhir/tools/StructureDefinition/snapshot-source", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/StructureDefinition/Patient-uv-ips|2.0.0" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }, + { + "extension" : [{ + "url" : "code", + "valueCode" : "SHALL:handle" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Consumer" + }, + { + "url" : "http://hl7.org/fhir/tools/StructureDefinition/snapshot-source", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/StructureDefinition/Patient-uv-ips|2.0.0" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }, + { + "extension" : [{ + "url" : "code", + "valueCode" : "SHOULD:display" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Consumer" + }, + { + "url" : "http://hl7.org/fhir/tools/StructureDefinition/snapshot-source", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/StructureDefinition/Patient-uv-ips|2.0.0" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }], + "path" : "Patient.generalPractitioner", + "short" : "Patient's nominated primary care provider", + "definition" : "Patient's nominated care provider.", + "comment" : "This may be the primary care provider (in a GP context), or it may be a patient nominated care manager in a community/disability setting, or even organization that will provide people to perform the care provider roles. It is not to be used to record Care Teams, these should be in a CareTeam resource that may be linked to the CarePlan or EpisodeOfCare resources.\nMultiple GPs may be recorded against the patient for various reasons, such as a student that has his home GP listed along with the GP at university during the school semesters, or a \"fly-in/fly-out\" worker that has the onsite GP also included with his home GP to remain aware of medical issues.\n\nJurisdictions may decide that they can profile this down to 1 if desired, or 1 per type.", + "alias" : ["careProvider"], + "min" : 0, + "max" : "*", + "base" : { + "path" : "Patient.generalPractitioner", + "min" : 0, + "max" : "*" + }, + "type" : [{ + "code" : "Reference", + "targetProfile" : ["http://hl7.org/fhir/StructureDefinition/Organization|4.0.1", + "http://hl7.org/fhir/StructureDefinition/Practitioner|4.0.1", + "http://hl7.org/fhir/StructureDefinition/PractitionerRole|4.0.1"] + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "mustSupport" : true, + "isModifier" : false, + "isSummary" : false, + "mapping" : [{ + "identity" : "v2", + "map" : "PD1-4" + }, + { + "identity" : "rim", + "map" : "subjectOf.CareEvent.performer.AssignedEntity" + }, + { + "identity" : "cda", + "map" : "n/a" + }] + }, + { + "id" : "Patient.managingOrganization", + "path" : "Patient.managingOrganization", + "short" : "Organization that is the custodian of the patient record", + "definition" : "Organization that is the custodian of the patient record.", + "comment" : "There is only one managing organization for a specific patient record. Other organizations will have their own Patient record, and may use the Link property to join the records together (or a Person resource which can include confidence ratings for the association).", + "requirements" : "Need to know who recognizes this patient record, manages and updates it.", + "min" : 0, + "max" : "1", + "base" : { + "path" : "Patient.managingOrganization", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "code" : "Reference", + "targetProfile" : ["http://hl7.org/fhir/StructureDefinition/Organization|4.0.1"] + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : true, + "mapping" : [{ + "identity" : "rim", + "map" : "scoper" + }, + { + "identity" : "cda", + "map" : ".providerOrganization" + }] + }, + { + "id" : "Patient.link", + "path" : "Patient.link", + "short" : "Link to another patient resource that concerns the same actual person", + "definition" : "Link to another patient resource that concerns the same actual patient.", + "comment" : "There is no assumption that linked patient records have mutual links.", + "requirements" : "There are multiple use cases: \n\n* Duplicate patient records due to the clerical errors associated with the difficulties of identifying humans consistently, and \n* Distribution of patient information across multiple servers.", + "min" : 0, + "max" : "*", + "base" : { + "path" : "Patient.link", + "min" : 0, + "max" : "*" + }, + "type" : [{ + "code" : "BackboneElement" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : true, + "isModifierReason" : "This element is labeled as a modifier because it might not be the main Patient resource, and the referenced patient should be used instead of this Patient record. This is when the link.type value is 'replaced-by'", + "isSummary" : true, + "mapping" : [{ + "identity" : "rim", + "map" : "outboundLink" + }, + { + "identity" : "cda", + "map" : "n/a" + }] + }, + { + "id" : "Patient.link.id", + "path" : "Patient.link.id", + "representation" : ["xmlAttr"], + "short" : "Unique id for inter-element referencing", + "definition" : "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.", + "min" : 0, + "max" : "1", + "base" : { + "path" : "Element.id", + "min" : 0, + "max" : "1" + }, + "type" : [{ + "extension" : [{ + "url" : "http://hl7.org/fhir/StructureDefinition/structuredefinition-fhir-type", + "valueUrl" : "string" + }], + "code" : "http://hl7.org/fhirpath/System.String" + }], + "isModifier" : false, + "isSummary" : false, + "mapping" : [{ + "identity" : "rim", + "map" : "n/a" + }] + }, + { + "id" : "Patient.link.extension", + "path" : "Patient.link.extension", + "short" : "Additional content defined by implementations", + "definition" : "May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension.", + "comment" : "There can be no stigma associated with the use of extensions by any application, project, or standard - regardless of the institution or jurisdiction that uses or defines the extensions. The use of extensions is what allows the FHIR specification to retain a core level of simplicity for everyone.", + "alias" : ["extensions", + "user content"], + "min" : 0, + "max" : "*", + "base" : { + "path" : "Element.extension", + "min" : 0, + "max" : "*" + }, + "type" : [{ + "code" : "Extension" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }, + { + "key" : "ext-1", + "severity" : "error", + "human" : "Must have either extensions or value[x], not both", + "expression" : "extension.exists() != value.exists()", + "xpath" : "exists(f:extension)!=exists(f:*[starts-with(local-name(.), \"value\")])", + "source" : "http://hl7.org/fhir/StructureDefinition/Extension" + }], + "isModifier" : false, + "isSummary" : false, + "mapping" : [{ + "identity" : "rim", + "map" : "n/a" + }] + }, + { + "id" : "Patient.link.modifierExtension", + "path" : "Patient.link.modifierExtension", + "short" : "Extensions that cannot be ignored even if unrecognized", + "definition" : "May be used to represent additional information that is not part of the basic definition of the element and that modifies the understanding of the element in which it is contained and/or the understanding of the containing element's descendants. Usually modifier elements provide negation or qualification. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension. Applications processing a resource are required to check for modifier extensions.\n\nModifier extensions SHALL NOT change the meaning of any elements on Resource or DomainResource (including cannot change the meaning of modifierExtension itself).", + "comment" : "There can be no stigma associated with the use of extensions by any application, project, or standard - regardless of the institution or jurisdiction that uses or defines the extensions. The use of extensions is what allows the FHIR specification to retain a core level of simplicity for everyone.", + "requirements" : "Modifier extensions allow for extensions that *cannot* be safely ignored to be clearly distinguished from the vast majority of extensions which can be safely ignored. This promotes interoperability by eliminating the need for implementers to prohibit the presence of extensions. For further information, see the [definition of modifier extensions](http://hl7.org/fhir/R4/extensibility.html#modifierExtension).", + "alias" : ["extensions", + "user content", + "modifiers"], + "min" : 0, + "max" : "*", + "base" : { + "path" : "BackboneElement.modifierExtension", + "min" : 0, + "max" : "*" + }, + "type" : [{ + "code" : "Extension" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }, + { + "key" : "ext-1", + "severity" : "error", + "human" : "Must have either extensions or value[x], not both", + "expression" : "extension.exists() != value.exists()", + "xpath" : "exists(f:extension)!=exists(f:*[starts-with(local-name(.), \"value\")])", + "source" : "http://hl7.org/fhir/StructureDefinition/Extension" + }], + "isModifier" : true, + "isModifierReason" : "Modifier extensions are expected to modify the meaning or interpretation of the element that contains them", + "isSummary" : true, + "mapping" : [{ + "identity" : "rim", + "map" : "N/A" + }] + }, + { + "id" : "Patient.link.other", + "path" : "Patient.link.other", + "short" : "The other patient or related person resource that the link refers to", + "definition" : "The other patient resource that the link refers to.", + "comment" : "Referencing a RelatedPerson here removes the need to use a Person record to associate a Patient and RelatedPerson as the same individual.", + "min" : 1, + "max" : "1", + "base" : { + "path" : "Patient.link.other", + "min" : 1, + "max" : "1" + }, + "type" : [{ + "extension" : [{ + "url" : "http://hl7.org/fhir/StructureDefinition/structuredefinition-hierarchy", + "valueBoolean" : false + }], + "code" : "Reference", + "targetProfile" : ["http://hl7.org/fhir/StructureDefinition/Patient|4.0.1", + "http://hl7.org/fhir/StructureDefinition/RelatedPerson|4.0.1"] + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : true, + "mapping" : [{ + "identity" : "v2", + "map" : "PID-3, MRG-1" + }, + { + "identity" : "rim", + "map" : "id" + }, + { + "identity" : "cda", + "map" : "n/a" + }] + }, + { + "id" : "Patient.link.type", + "path" : "Patient.link.type", + "short" : "replaced-by | replaces | refer | seealso", + "definition" : "The type of link between this patient resource and another patient resource.", + "min" : 1, + "max" : "1", + "base" : { + "path" : "Patient.link.type", + "min" : 1, + "max" : "1" + }, + "type" : [{ + "code" : "code" + }], + "constraint" : [{ + "key" : "ele-1", + "severity" : "error", + "human" : "All FHIR elements must have a @value or children", + "expression" : "hasValue() or (children().count() > id.count())", + "xpath" : "@value|f:*|h:div", + "source" : "http://hl7.org/fhir/StructureDefinition/Element" + }], + "isModifier" : false, + "isSummary" : true, + "binding" : { + "extension" : [{ + "url" : "http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", + "valueString" : "LinkType" + }], + "strength" : "required", + "description" : "The type of link between this patient resource and another patient resource.", + "valueSet" : "http://hl7.org/fhir/ValueSet/link-type|4.0.1" + }, + "mapping" : [{ + "identity" : "rim", + "map" : "typeCode" + }, + { + "identity" : "cda", + "map" : "n/a" + }] + }] + }, + "differential" : { + "element" : [{ + "id" : "Patient.extension", + "path" : "Patient.extension", + "slicing" : { + "discriminator" : [{ + "type" : "value", + "path" : "url" + }], + "ordered" : false, + "rules" : "open" + } + }, + { + "id" : "Patient.extension:genderIdentity", + "path" : "Patient.extension", + "sliceName" : "genderIdentity", + "min" : 0, + "max" : "*", + "type" : [{ + "code" : "Extension", + "profile" : ["http://hl7.org/fhir/StructureDefinition/individual-genderIdentity|5.3.0-ballot-tc1"] + }] + }, + { + "id" : "Patient.extension:personalPronouns", + "path" : "Patient.extension", + "sliceName" : "personalPronouns", + "min" : 0, + "max" : "*", + "type" : [{ + "code" : "Extension", + "profile" : ["http://hl7.org/fhir/StructureDefinition/individual-pronouns|5.3.0-ballot-tc1"] + }] + }, + { + "id" : "Patient.identifier", + "extension" : [{ + "extension" : [{ + "url" : "code", + "valueCode" : "SHALL:populate-if-known" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Creator" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }, + { + "extension" : [{ + "url" : "code", + "valueCode" : "SHALL:handle" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Consumer" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }, + { + "extension" : [{ + "url" : "code", + "valueCode" : "SHOULD:display" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Consumer" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }], + "path" : "Patient.identifier", + "mustSupport" : true + }, + { + "id" : "Patient.name", + "extension" : [{ + "extension" : [{ + "url" : "code", + "valueCode" : "SHALL:populate" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Creator" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }, + { + "extension" : [{ + "url" : "code", + "valueCode" : "SHALL:handle" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Consumer" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }, + { + "extension" : [{ + "url" : "code", + "valueCode" : "SHOULD:display" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Consumer" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }], + "path" : "Patient.name", + "requirements" : "Need to be able to track the patient by multiple names. Examples are your official name and a partner name.\r\nThe Alphabetic representation of the name SHALL be always provided", + "min" : 1, + "constraint" : [{ + "key" : "ips-pat-1", + "severity" : "error", + "human" : "Patient.name.given, Patient.name.family or Patient.name.text SHALL be present", + "expression" : "family.exists() or given.exists() or text.exists()", + "xpath" : "f:given or f:family or f:text", + "source" : "http://hl7.org/fhir/uv/ips/StructureDefinition/Patient-uv-ips" + }], + "mustSupport" : true + }, + { + "id" : "Patient.name.use", + "extension" : [{ + "extension" : [{ + "url" : "code", + "valueCode" : "SHALL:populate-if-known" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Creator" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }, + { + "extension" : [{ + "url" : "code", + "valueCode" : "SHALL:handle" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Consumer" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }, + { + "extension" : [{ + "url" : "code", + "valueCode" : "SHOULD:display" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Consumer" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }], + "path" : "Patient.name.use", + "mustSupport" : true + }, + { + "id" : "Patient.name.text", + "extension" : [{ + "extension" : [{ + "url" : "code", + "valueCode" : "SHALL:populate-if-known" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Creator" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }, + { + "extension" : [{ + "url" : "code", + "valueCode" : "SHALL:handle" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Consumer" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }, + { + "extension" : [{ + "url" : "code", + "valueCode" : "SHOULD:display" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Consumer" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }], + "path" : "Patient.name.text", + "definition" : "Text representation of the full name. Due to the cultural variance around the world a consuming system may not know how to present the name correctly; moreover, not all the parts of the name go in given or family. Creators are therefore strongly encouraged to provide through this element a presented version of the name. Future versions of this guide may require this element", + "mustSupport" : true + }, + { + "id" : "Patient.name.family", + "extension" : [{ + "extension" : [{ + "url" : "code", + "valueCode" : "SHALL:populate-if-known" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Creator" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }, + { + "extension" : [{ + "url" : "code", + "valueCode" : "SHALL:handle" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Consumer" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }, + { + "extension" : [{ + "url" : "code", + "valueCode" : "SHOULD:display" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Consumer" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }], + "path" : "Patient.name.family", + "mustSupport" : true + }, + { + "id" : "Patient.name.given", + "extension" : [{ + "extension" : [{ + "url" : "code", + "valueCode" : "SHALL:populate-if-known" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Creator" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }, + { + "extension" : [{ + "url" : "code", + "valueCode" : "SHALL:handle" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Consumer" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }, + { + "extension" : [{ + "url" : "code", + "valueCode" : "SHOULD:display" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Consumer" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }], + "path" : "Patient.name.given", + "mustSupport" : true + }, + { + "id" : "Patient.telecom", + "extension" : [{ + "extension" : [{ + "url" : "code", + "valueCode" : "SHALL:populate-if-known" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Creator" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }, + { + "extension" : [{ + "url" : "code", + "valueCode" : "SHALL:handle" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Consumer" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }, + { + "extension" : [{ + "url" : "code", + "valueCode" : "SHOULD:display" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Consumer" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }], + "path" : "Patient.telecom", + "mustSupport" : true + }, + { + "id" : "Patient.gender", + "extension" : [{ + "extension" : [{ + "url" : "code", + "valueCode" : "SHALL:populate-if-known" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Creator" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }, + { + "extension" : [{ + "url" : "code", + "valueCode" : "SHALL:handle" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Consumer" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }, + { + "extension" : [{ + "url" : "code", + "valueCode" : "SHOULD:display" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Consumer" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }], + "path" : "Patient.gender", + "mustSupport" : true + }, + { + "id" : "Patient.birthDate", + "extension" : [{ + "extension" : [{ + "url" : "code", + "valueCode" : "SHALL:populate-if-known" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Creator" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }, + { + "extension" : [{ + "url" : "code", + "valueCode" : "SHALL:handle" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Consumer" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }, + { + "extension" : [{ + "url" : "code", + "valueCode" : "SHOULD:display" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Consumer" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }], + "path" : "Patient.birthDate", + "min" : 1, + "mustSupport" : true + }, + { + "id" : "Patient.address", + "extension" : [{ + "extension" : [{ + "url" : "code", + "valueCode" : "SHALL:populate-if-known" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Creator" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }, + { + "extension" : [{ + "url" : "code", + "valueCode" : "SHALL:handle" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Consumer" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }, + { + "extension" : [{ + "url" : "code", + "valueCode" : "SHOULD:display" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Consumer" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }], + "path" : "Patient.address", + "mustSupport" : true + }, + { + "id" : "Patient.generalPractitioner", + "extension" : [{ + "extension" : [{ + "url" : "code", + "valueCode" : "SHALL:populate-if-known" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Creator" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }, + { + "extension" : [{ + "url" : "code", + "valueCode" : "SHALL:handle" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Consumer" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }, + { + "extension" : [{ + "url" : "code", + "valueCode" : "SHOULD:display" + }, + { + "url" : "actor", + "valueCanonical" : "http://hl7.org/fhir/uv/ips/ActorDefinition/Consumer" + }], + "url" : "http://hl7.org/fhir/StructureDefinition/obligation" + }], + "path" : "Patient.generalPractitioner", + "mustSupport" : true + }] + } +} \ No newline at end of file diff --git a/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/test/resources/application-test.yaml b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/test/resources/application-test.yaml new file mode 100644 index 0000000..c4b6651 --- /dev/null +++ b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/test/resources/application-test.yaml @@ -0,0 +1,52 @@ +# +# Copyright (c) 2004-2025, University of Oslo +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + +fhir-url: http://hapi-fhir-ips + +oauth2: + clientId: fhir-client + clientSecret: passw0rd + tokenEndpoint: http://authorisation-server/realms/fhir/protocol/openid-connect/token + +spring: + security: + user: + name: test + password: test + +dhis2DatabaseHostname: localhost +dhis2DatabaseUser: dhis +dhis2DatabasePassword: dhis +dhis2DatabaseDbName: dhis2 + +dhis2ApiUsername: admin +dhis2ApiPassword: district + +offsetStorageFileName: target/offset.dat \ No newline at end of file diff --git a/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/test/resources/dhis.conf b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/test/resources/dhis.conf new file mode 100644 index 0000000..ea8adf1 --- /dev/null +++ b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/test/resources/dhis.conf @@ -0,0 +1,5 @@ +connection.dialect = org.hibernate.dialect.PostgreSQLDialect +connection.driver_class = org.postgresql.Driver +connection.url = jdbc:postgresql://db:5432/dhis2 +connection.username = dhis +connection.password = dhis \ No newline at end of file diff --git a/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/test/resources/expectedFhirBundle.json b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/test/resources/expectedFhirBundle.json new file mode 100644 index 0000000..aab3bb5 --- /dev/null +++ b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/test/resources/expectedFhirBundle.json @@ -0,0 +1,777 @@ +{ + "resourceType": "Bundle", + "type": "transaction", + "entry": [ + { + "fullUrl": "urn:uuid:KGY0UKmRqqh", + "resource": { + "resourceType": "Patient", + "meta": [ + { + "profile": "http://fhir.health.gov.lk/ips/StructureDefinition/ips-patient" + } + ], + "extension": [ + { + "url": "http://fhir.health.gov.lk/ips/StructureDefinition/patient-registration-system", + "valueReference": { + "reference": "Device?identifier=http://fhir.health.gov.lk/ips/identifier/system-id|5b21b377-f424-48c1-8c24-1980b4d00059" + } + } + ], + "identifier": [ + { + "use": "official", + "type": { + "coding": [ + { + "system": "http://fhir.health.gov.lk/ips/CodeSystem/cs-identifier-types", + "code": "PHN", + "display": "Personal Health Number" + } + ], + "text": "Personal Health Number" + }, + "system": "http://fhir.health.gov.lk/ips/identifier/phn", + "value": "12345678" + }, + { + "use": "secondary", + "system": "urn:esignet:sub", + "value": "SUB0001" + }, + { + "use": "secondary", + "type": { + "coding": [ + { + "system": "http://fhir.health.gov.lk/ips/CodeSystem/cs-identifier-types", + "code": "NIC", + "display": "National Identity Card" + } + ], + "text": "National identity number" + }, + "system": "http://fhir.health.gov.lk/ips/identifier/nic", + "value": "200012345678" + }, + { + "use": "secondary", + "system": "urn:dhis2:anc:regno", + "value": "ANC00000001" + } + ], + "name": [ + { + "text": "John A. B. C. Doe", + "given": ["John","A.", "B.", "C."], + "family": "Doe" + } + ], + "telecom": [ + { + "system": "phone", + "value": "+94712345678" + } + ], + "gender": "female", + "birthDate": "1997-08-01", + "address": [ + { + "line": ["123 Main Street"], + "postalCode": "1234", + "city": "Akurana", + "district": "Kandy", + "state": "Central Province", + "country": "LK" + } + ] + }, + "request": { + "method": "PUT", + "url": "Patient?identifier=http://fhir.health.gov.lk/ips/identifier/phn|12345678" + } + }, + { + "fullUrl": "urn:uuid:dhis2:user:M5zQapPyTZI", + "resource": { + "resourceType": "Practitioner", + "identifier": [ + { + "system": "urn:dhis2:user:uid", + "value": "M5zQapPyTZI" + } + ], + "active": true, + "name": [ + { + "text": "admin admin", + "given": ["admin"], + "family": "admin" + } + ] + }, + "request": { + "method": "PUT", + "url": "Practitioner?identifier=urn:dhis2:user:uid|M5zQapPyTZI" + } + }, + { + "fullUrl": "urn:uuid:ANEt5ec0HOM", + "resource": { + "resourceType": "Encounter", + "identifier": [ + { + "system": "urn:dhis2:eventId", + "value": "ANEt5ec0HOM" + } + ], + "status": "finished", + "class": { + "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", + "code": "AMB", + "display": "ambulatory" + }, + "subject": { + "reference": "Patient?identifier=http://fhir.health.gov.lk/ips/identifier/phn|12345678" + }, + "period": { + "start": "2025-09-03T00:00:00.000", + "end": "2025-09-04T07:37:47.106" + }, + "type": [ + { + "coding": [ + { + "system": "urn:dhis2:programStage", + "code": "LWJcStrI6kM", + "display": "Registration" + } + ], + "text": "Registration" + } + ], + "reasonCode": [ + { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "77386006", + "display": "Pregnant" + }, + { + "system": "urn:dhis2:program", + "code": "eozjj9UivfS", + "display": "ANC Program" + } + ], + "text": "ANC Care" + } + ], + "participant": [ + { + "type": [ + { + "text": "DHIS2 user who performed latest update to patient." + } + ], + "individual": { + "reference": "Practitioner?identifier=urn:dhis2:user:uid|M5zQapPyTZI" + } + } + ] + }, + "request": { + "method": "PUT", + "url": "Encounter?identifier=urn:dhis2:eventId|ANEt5ec0HOM" + } + }, + { + "fullUrl": "urn:uuid:bEPWxMXbAdX", + "resource": { + "resourceType": "Encounter", + "identifier": [ + { + "system": "urn:dhis2:eventId", + "value": "bEPWxMXbAdX" + } + ], + "status": "finished", + "class": { + "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", + "code": "AMB", + "display": "ambulatory" + }, + "subject": { + "reference": "Patient?identifier=http://fhir.health.gov.lk/ips/identifier/phn|12345678" + }, + "period": { + "end": "2025-09-16T13:47:48.961" + }, + "type": [ + { + "coding": [ + { + "system": "urn:dhis2:programStage", + "code": "GX0z9IXFaso", + "display": "ANC Visits" + } + ], + "text": "ANC Visits" + } + ], + "reasonCode": [ + { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "77386006", + "display": "Pregnant" + }, + { + "system": "urn:dhis2:program", + "code": "eozjj9UivfS", + "display": "ANC Program" + } + ], + "text": "ANC Care" + } + ], + "participant": [ + { + "type": [ + { + "text": "DHIS2 user who performed latest update to patient." + } + ], + "individual": { + "reference": "Practitioner?identifier=urn:dhis2:user:uid|M5zQapPyTZI" + } + } + ] + }, + "request": { + "method": "PUT", + "url": "Encounter?identifier=urn:dhis2:eventId|bEPWxMXbAdX" + } + }, + { + "fullUrl": "urn:uuid:ANEt5ec0HOM-BqkEw3MQDNI", + "resource": { + "resourceType": "Observation", + "status": "final", + "identifier": [ + { "system": "urn:dhis2:dataelement:uid", "value": "ANEt5ec0HOM-BqkEw3MQDNI" } + ], + "category": [ + { "coding": [ { "system": "http://terminology.hl7.org/CodeSystem/observation-category", "code": "survey", "display": "Survey" } ] } + ], + "code": { + "coding": [ { "system": "urn:dhis2:dataElement", "code": "age-youngest-child", "display": "Age of Youngest Child (yrs)" } ], + "text": "Age of Youngest Child (yrs)" + }, + "subject": { "reference": "Patient?identifier=http://fhir.health.gov.lk/ips/identifier/phn|12345678" }, + "encounter": { "reference": "Encounter?identifier=urn:dhis2:eventId|ANEt5ec0HOM" }, + "effectiveDateTime": "2025-09-04T07:37:47.106", + "valueInteger": 4 + }, + "request": { "method": "PUT", "url": "Observation?identifier=urn:dhis2:dataelement:uid|ANEt5ec0HOM-BqkEw3MQDNI" } + }, + { + "fullUrl": "urn:uuid:ANEt5ec0HOM-R4EnBNPKblS", + "resource": { + "resourceType": "Condition", + "clinicalStatus": { "coding": [ { "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", "code": "active", "display": "Active" } ] }, + "verificationStatus": { "coding": [ { "system": "http://terminology.hl7.org/CodeSystem/condition-verification", "code": "confirmed", "display": "Confirmed" } ] }, + "identifier": [ + { "system": "urn:dhis2:dataelement:uid", "value": "ANEt5ec0HOM-R4EnBNPKblS" } + ], + "code": { + "coding": [ { "system": "http://snomed.info/sct", "code": "56265001", "display": "Heart disease (disorder)" } ], + "text": "Heart disease (disorder)" + }, + "subject": { "reference": "Patient?identifier=http://fhir.health.gov.lk/ips/identifier/phn|12345678" }, + "encounter": { "reference": "Encounter?identifier=urn:dhis2:eventId|ANEt5ec0HOM" } + }, + "request": { "method": "PUT", "url": "Condition?identifier=urn:dhis2:dataelement:uid|ANEt5ec0HOM-R4EnBNPKblS" } + }, + { + "fullUrl": "urn:uuid:ANEt5ec0HOM-YuZEbIjLLKZ", + "resource": { + "resourceType": "Observation", + "status": "final", + "identifier": [ + { "system": "urn:dhis2:dataelement:uid", "value": "ANEt5ec0HOM-YuZEbIjLLKZ" } + ], + "category": [ + { "coding": [ { "system": "http://terminology.hl7.org/CodeSystem/observation-category", "code": "survey", "display": "Survey" } ] } + ], + "code": { + "coding": [ { "system": "http://snomed.info/sct", "code": "842009", "display": "Consanguinity" } ], + "text": "Consanguinity" + }, + "subject": { "reference": "Patient?identifier=http://fhir.health.gov.lk/ips/identifier/phn|12345678" }, + "encounter": { "reference": "Encounter?identifier=urn:dhis2:eventId|ANEt5ec0HOM" }, + "effectiveDateTime": "2025-09-04T07:37:47.106", + "valueBoolean": false + }, + "request": { "method": "PUT", "url": "Observation?identifier=urn:dhis2:dataelement:uid|ANEt5ec0HOM-YuZEbIjLLKZ" } + }, + { + "fullUrl": "urn:uuid:ANEt5ec0HOM-r5TIiovGHdi", + "resource": { + "resourceType": "Observation", + "status": "final", + "identifier": [ + { "system": "urn:dhis2:dataelement:uid", "value": "ANEt5ec0HOM-r5TIiovGHdi" } + ], + "category": [ + { "coding": [ { "system": "http://terminology.hl7.org/CodeSystem/observation-category", "code": "survey", "display": "Survey" } ] } + ], + "code": { + "coding": [ { "system": "http://loinc.org", "code": "11779-6", "display": "Delivery date Estimated from last menstrual period" } ], + "text": "EDD by dates" + }, + "subject": { "reference": "Patient?identifier=http://fhir.health.gov.lk/ips/identifier/phn|12345678" }, + "encounter": { "reference": "Encounter?identifier=urn:dhis2:eventId|ANEt5ec0HOM" }, + "effectiveDateTime": "2025-09-04T07:37:47.106", + "valueDateTime": "2026-04-08" + }, + "request": { "method": "PUT", "url": "Observation?identifier=urn:dhis2:dataelement:uid|ANEt5ec0HOM-r5TIiovGHdi" } + }, + { + "fullUrl": "urn:uuid:ANEt5ec0HOM-TRkCMVFhrmr", + "resource": { + "resourceType": "Observation", + "status": "final", + "identifier": [ + { "system": "urn:dhis2:dataelement:uid", "value": "ANEt5ec0HOM-TRkCMVFhrmr" } + ], + "category": [ + { "coding": [ { "system": "http://terminology.hl7.org/CodeSystem/observation-category", "code": "survey", "display": "Survey" } ] } + ], + "code": { + "coding": [ { "system": "http://loinc.org", "code": "11996-6", "display": "[#] Pregnancies" } ], + "text": "Gravida (G)" + }, + "subject": { "reference": "Patient?identifier=http://fhir.health.gov.lk/ips/identifier/phn|12345678" }, + "encounter": { "reference": "Encounter?identifier=urn:dhis2:eventId|ANEt5ec0HOM" }, + "effectiveDateTime": "2025-09-04T07:37:47.106", + "valueInteger": 3 + }, + "request": { "method": "PUT", "url": "Observation?identifier=urn:dhis2:dataelement:uid|ANEt5ec0HOM-TRkCMVFhrmr" } + }, + { + "fullUrl": "urn:uuid:ANEt5ec0HOM-n2rSTrjRQ8O", + "resource": { + "resourceType": "Observation", + "status": "final", + "identifier": [ + { "system": "urn:dhis2:dataelement:uid", "value": "ANEt5ec0HOM-n2rSTrjRQ8O" } + ], + "category": [ + { "coding": [ { "system": "http://terminology.hl7.org/CodeSystem/observation-category", "code": "social-history", "display": "Social History" } ] } + ], + "code": { + "coding": [ { "system": "http://snomed.info/sct", "code": "17276009", "display": "Decreased fertility" } ], + "text": "History of subfertility" + }, + "subject": { "reference": "Patient?identifier=http://fhir.health.gov.lk/ips/identifier/phn|12345678" }, + "encounter": { "reference": "Encounter?identifier=urn:dhis2:eventId|ANEt5ec0HOM" }, + "effectiveDateTime": "2025-09-04T07:37:47.106", + "valueBoolean": false + }, + "request": { "method": "PUT", "url": "Observation?identifier=urn:dhis2:dataelement:uid|ANEt5ec0HOM-n2rSTrjRQ8O" } + }, + { + "fullUrl": "urn:uuid:ANEt5ec0HOM-plTkCmySMzT", + "resource": { + "resourceType": "Condition", + "clinicalStatus": { "coding": [ { "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", "code": "active", "display": "Active" } ] }, + "verificationStatus": { "coding": [ { "system": "http://terminology.hl7.org/CodeSystem/condition-verification", "code": "confirmed", "display": "Confirmed" } ] }, + "identifier": [ + { "system": "urn:dhis2:dataelement:uid", "value": "ANEt5ec0HOM-plTkCmySMzT" } + ], + "code": { + "coding": [ { "system": "http://snomed.info/sct", "code": "38341003", "display": "Hypertensive disorder (disorder)" } ], + "text": "Hypertensive disorder (disorder)" + }, + "subject": { "reference": "Patient?identifier=http://fhir.health.gov.lk/ips/identifier/phn|12345678" }, + "encounter": { "reference": "Encounter?identifier=urn:dhis2:eventId|ANEt5ec0HOM" } + }, + "request": { "method": "PUT", "url": "Condition?identifier=urn:dhis2:dataelement:uid|ANEt5ec0HOM-plTkCmySMzT" } + }, + { + "fullUrl": "urn:uuid:ANEt5ec0HOM-vuDExM32SZ6", + "resource": { + "resourceType": "Observation", + "status": "final", + "identifier": [ + { "system": "urn:dhis2:dataelement:uid", "value": "ANEt5ec0HOM-vuDExM32SZ6" } + ], + "category": [ + { "coding": [ { "system": "http://terminology.hl7.org/CodeSystem/observation-category", "code": "survey", "display": "Survey" } ] } + ], + "code": { + "coding": [ { "system": "http://loinc.org", "code": "8665-2", "display": "Last menstrual period start date" } ], + "text": "Last Menstrual Period (LMP)" + }, + "subject": { "reference": "Patient?identifier=http://fhir.health.gov.lk/ips/identifier/phn|12345678" }, + "encounter": { "reference": "Encounter?identifier=urn:dhis2:eventId|ANEt5ec0HOM" }, + "effectiveDateTime": "2025-09-04T07:37:47.106", + "valueDateTime": "2025-07-02" + }, + "request": { "method": "PUT", "url": "Observation?identifier=urn:dhis2:dataelement:uid|ANEt5ec0HOM-vuDExM32SZ6" } + }, + { + "fullUrl": "urn:uuid:ANEt5ec0HOM-OMe6LMsusv4", + "resource": { + "resourceType": "Observation", + "status": "final", + "identifier": [ + { "system": "urn:dhis2:dataelement:uid", "value": "ANEt5ec0HOM-OMe6LMsusv4" } + ], + "category": [ + { "coding": [ { "system": "http://terminology.hl7.org/CodeSystem/observation-category", "code": "survey", "display": "Survey" } ] } + ], + "code": { + "coding": [ { "system": "http://loinc.org", "code": "11638-4", "display": "[#] Births.still living" } ], + "text": "Number of Living Children" + }, + "subject": { "reference": "Patient?identifier=http://fhir.health.gov.lk/ips/identifier/phn|12345678" }, + "encounter": { "reference": "Encounter?identifier=urn:dhis2:eventId|ANEt5ec0HOM" }, + "effectiveDateTime": "2025-09-04T07:37:47.106", + "valueInteger": 2 + }, + "request": { "method": "PUT", "url": "Observation?identifier=urn:dhis2:dataelement:uid|ANEt5ec0HOM-OMe6LMsusv4" } + }, + { + "fullUrl": "urn:uuid:ANEt5ec0HOM-J3eLUAZkM6M", + "resource": { + "resourceType": "MedicationStatement", + "status": "active", + "identifier": [ + { "system": "urn:dhis2:dataelement:uid", "value": "ANEt5ec0HOM-J3eLUAZkM6M" } + ], + "medicationCodeableConcept": { + "coding": [ { "system": "http://snomed.info/sct", "code": "63718003", "display": "Folic acid (substance)" } ], + "text": "Folic acid (substance)" + }, + "context": { "reference": "Encounter?identifier=urn:dhis2:eventId|ANEt5ec0HOM" }, + "subject": { "reference": "Patient?identifier=http://fhir.health.gov.lk/ips/identifier/phn|12345678" }, + "effectiveDateTime": "2025-09-04T07:37:47.106" + }, + "request": { "method": "PUT", "url": "MedicationStatement?identifier=urn:dhis2:dataelement:uid|ANEt5ec0HOM-J3eLUAZkM6M" } + }, + { + "fullUrl": "urn:uuid:ANEt5ec0HOM-NPnDmeGBLsl", + "resource": { + "resourceType": "Observation", + "status": "final", + "identifier": [ + { "system": "urn:dhis2:dataelement:uid", "value": "ANEt5ec0HOM-NPnDmeGBLsl" } + ], + "category": [ + { "coding": [ { "system": "http://terminology.hl7.org/CodeSystem/observation-category", "code": "survey", "display": "Survey" } ] } + ], + "code": { + "coding": [ { "system": "http://loinc.org", "code": "11640-0", "display": "[#] Births total" } ], + "text": "Parity (P)" + }, + "subject": { "reference": "Patient?identifier=http://fhir.health.gov.lk/ips/identifier/phn|12345678" }, + "encounter": { "reference": "Encounter?identifier=urn:dhis2:eventId|ANEt5ec0HOM" }, + "effectiveDateTime": "2025-09-04T07:37:47.106", + "valueInteger": 2 + }, + "request": { "method": "PUT", "url": "Observation?identifier=urn:dhis2:dataelement:uid|ANEt5ec0HOM-NPnDmeGBLsl" } + }, + { + "fullUrl": "urn:uuid:ANEt5ec0HOM-No3jJWIR7bn", + "resource": { + "resourceType": "Observation", + "status": "final", + "identifier": [ + { "system": "urn:dhis2:dataelement:uid", "value": "ANEt5ec0HOM-No3jJWIR7bn" } + ], + "category": [ + { "coding": [ { "system": "http://terminology.hl7.org/CodeSystem/observation-category", "code": "exam", "display": "Exam" } ] } + ], + "code": { + "coding": [{ + "system": "http://loinc.org", + "code": "11885-1", + "display": "Gestational age Estimated from last menstrual period" + }], + "text": "POA at Registration (weeks)" + }, + "subject": { "reference": "Patient?identifier=http://fhir.health.gov.lk/ips/identifier/phn|12345678" }, + "encounter": { "reference": "Encounter?identifier=urn:dhis2:eventId|ANEt5ec0HOM" }, + "effectiveDateTime": "2025-09-04T07:37:47.106", + "valueQuantity": { + "value": 18, + "unit": "wk", + "system": "http://unitsofmeasure.org", + "code": "wk" + } + }, + "request": { "method": "PUT", "url": "Observation?identifier=urn:dhis2:dataelement:uid|ANEt5ec0HOM-No3jJWIR7bn" } + }, + { + "fullUrl": "urn:uuid:ANEt5ec0HOM-RpRCMceQhks", + "resource": { + "resourceType": "Immunization", + "status": "completed", + "identifier": [ + { "system": "urn:dhis2:dataelement:uid", "value": "ANEt5ec0HOM-RpRCMceQhks" } + ], + "vaccineCode": { + "coding": [ { "system": "http://snomed.info/sct", "code": "871732000", "display": "Rubella virus antigen only vaccine product" } ], + "text": "Rubella virus antigen only vaccine product" + }, + "patient": { "reference": "Patient?identifier=http://fhir.health.gov.lk/ips/identifier/phn|12345678" }, + "occurrenceDateTime": "2025-09-04T07:37:47.106" + }, + "request": { "method": "PUT", "url": "Immunization?identifier=urn:dhis2:dataelement:uid|ANEt5ec0HOM-RpRCMceQhks" } + }, + { + "fullUrl": "urn:uuid:bEPWxMXbAdX-gu4sr8eOZcT", + "resource": { + "resourceType": "Observation", + "status": "final", + "identifier": [ + { + "system": "urn:dhis2:dataelement:uid", + "value": "bEPWxMXbAdX-gu4sr8eOZcT" + } + ], + "category": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/observation-category", + "code": "exam", + "display": "Exam" + } + ] + } + ], + "code": { + "coding": [ + { + "system": "urn:dhis2:dataElement", + "code": "gu4sr8eOZcT", + "display": "POA (weeks)" + } + ], + "text": "POA (weeks)" + }, + "subject": { "reference": "Patient?identifier=http://fhir.health.gov.lk/ips/identifier/phn|12345678" }, + "encounter": { "reference": "Encounter?identifier=urn:dhis2:eventId|bEPWxMXbAdX" }, + "effectiveDateTime": "2025-09-16T13:47:48.961", + "valueInteger": 18 + }, + "request": { + "method": "PUT", + "url": "Observation?identifier=urn:dhis2:dataelement:uid|bEPWxMXbAdX-gu4sr8eOZcT" + } + }, + { + "fullUrl": "urn:uuid:bEPWxMXbAdX-k7K4aO0RXew", + "resource": { + "resourceType": "Observation", + "status": "final", + "identifier": [ + { + "system": "urn:dhis2:dataelement:uid", + "value": "bEPWxMXbAdX-k7K4aO0RXew" + } + ], + "category": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/observation-category", + "code": "exam", + "display": "Exam" + } + ] + } + ], + "code": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "26237000", + "display": "Ankle edema (finding)" + } + ], + "text": "Ankle Oedema" + }, + "subject": { "reference": "Patient?identifier=http://fhir.health.gov.lk/ips/identifier/phn|12345678" }, + "encounter": { "reference": "Encounter?identifier=urn:dhis2:eventId|bEPWxMXbAdX" }, + "effectiveDateTime": "2025-09-16T13:47:48.961", + "valueBoolean": true + }, + "request": { + "method": "PUT", + "url": "Observation?identifier=urn:dhis2:dataelement:uid|bEPWxMXbAdX-k7K4aO0RXew" + } + }, + { + "fullUrl": "urn:uuid:bEPWxMXbAdX-Y3xF4qecAVw", + "resource": { + "resourceType": "Observation", + "status": "final", + "identifier": [ + { + "system": "urn:dhis2:dataelement:uid", + "value": "bEPWxMXbAdX-Y3xF4qecAVw" + } + ], + "category": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/observation-category", + "code": "vital-signs", + "display": "Vital Signs" + } + ] + } + ], + "code": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "271649006", + "display": "Systolic blood pressure (observable entity)" + } + ], + "text": "Systolic Blood Pressure (mmHg)" + }, + "subject": { "reference": "Patient?identifier=http://fhir.health.gov.lk/ips/identifier/phn|12345678" }, + "encounter": { "reference": "Encounter?identifier=urn:dhis2:eventId|bEPWxMXbAdX" }, + "effectiveDateTime": "2025-09-16T13:47:48.961", + "valueInteger": 120 + }, + "request": { + "method": "PUT", + "url": "Observation?identifier=urn:dhis2:dataelement:uid|bEPWxMXbAdX-Y3xF4qecAVw" + } + }, + { + "fullUrl": "urn:uuid:bEPWxMXbAdX-Ei55u2kvdzm", + "resource": { + "resourceType": "Observation", + "status": "final", + "identifier": [ + { + "system": "urn:dhis2:dataelement:uid", + "value": "bEPWxMXbAdX-Ei55u2kvdzm" + } + ], + "category": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/observation-category", + "code": "exam", + "display": "Exam" + } + ] + } + ], + "code": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "268470003", + "display": "Fetal movements felt (finding)" + } + ], + "text": "Foetal Movements" + }, + "subject": { "reference": "Patient?identifier=http://fhir.health.gov.lk/ips/identifier/phn|12345678" }, + "encounter": { "reference": "Encounter?identifier=urn:dhis2:eventId|bEPWxMXbAdX" }, + "effectiveDateTime": "2025-09-16T13:47:48.961", + "valueBoolean": true + }, + "request": { + "method": "PUT", + "url": "Observation?identifier=urn:dhis2:dataelement:uid|bEPWxMXbAdX-Ei55u2kvdzm" + } + }, + { + "fullUrl": "urn:uuid:bEPWxMXbAdX-yQk0w6KiXHJ", + "resource": { + "resourceType": "Observation", + "status": "final", + "identifier": [ + { + "system": "urn:dhis2:dataelement:uid", + "value": "bEPWxMXbAdX-yQk0w6KiXHJ" + } + ], + "category": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/observation-category", + "code": "laboratory", + "display": "Laboratory" + } + ] + } + ], + "code": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "167262009", + "display": "Urine glucose test = trace (finding)" + } + ], + "text": "Urine Sugar" + }, + "subject": { "reference": "Patient?identifier=http://fhir.health.gov.lk/ips/identifier/phn|12345678" }, + "encounter": { "reference": "Encounter?identifier=urn:dhis2:eventId|bEPWxMXbAdX" }, + "effectiveDateTime": "2025-09-16T13:47:48.961", + "valueBoolean": true + }, + "request": { + "method": "PUT", + "url": "Observation?identifier=urn:dhis2:dataelement:uid|bEPWxMXbAdX-yQk0w6KiXHJ" + } + }, + { + "fullUrl": "urn:uuid:bEPWxMXbAdX-GCHralHLuAB", + "resource": { + "resourceType": "MedicationStatement", + "status": "active", + "identifier": [ + { + "system": "urn:dhis2:dataelement:uid", + "value": "bEPWxMXbAdX-GCHralHLuAB" + } + ], + "medicationCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "43706004", + "display": "Ascorbic acid (substance)" + } + ], + "text": "Vitamin C" + }, + "subject": { "reference": "Patient?identifier=http://fhir.health.gov.lk/ips/identifier/phn|12345678" }, + "context": { "reference": "Encounter?identifier=urn:dhis2:eventId|bEPWxMXbAdX" }, + "effectiveDateTime": "2025-09-16T13:47:48.961" + }, + "request": { + "method": "PUT", + "url": "MedicationStatement?identifier=urn:dhis2:dataelement:uid|bEPWxMXbAdX-GCHralHLuAB" + } + } + ] +} \ No newline at end of file diff --git a/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/test/resources/minimalTrackedEntity.json b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/test/resources/minimalTrackedEntity.json new file mode 100644 index 0000000..b032803 --- /dev/null +++ b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/test/resources/minimalTrackedEntity.json @@ -0,0 +1,21 @@ +{ + "trackedEntity": "MinimalTEI001", + "trackedEntityType": "nEenWmSyUEp", + "orgUnit": "DiszpKrYNg8", + "attributes": [ + { "attribute": "lZGmxYbs97q", "value": "MIN001" }, + { "attribute": "w75KJ2mc4zz", "value": "Jane" }, + { "attribute": "zDhUuAYrxNC", "value": "Smith" }, + { "attribute": "gHGyrwKPzej", "value": "2000-01-01" } + ], + "enrollments": [ + { + "attributes": [ + { "attribute": "lZGmxYbs97q", "value": "MIN001" }, + { "attribute": "w75KJ2mc4zz", "value": "Jane" }, + { "attribute": "zDhUuAYrxNC", "value": "Smith" }, + { "attribute": "gHGyrwKPzej", "value": "2000-01-01" } + ] + } + ] +} diff --git a/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/test/resources/trackedEntity.json b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/test/resources/trackedEntity.json new file mode 100644 index 0000000..28df7a9 --- /dev/null +++ b/dhis2-to-fhir-patient-bundle-datasonnet/fhir-sync-agent/src/test/resources/trackedEntity.json @@ -0,0 +1,236 @@ +{ + "trackedEntity": "QfFVkOL8ixj", + "trackedEntityType": "nEenWmSyUEp", + "createdAt": "2025-11-03T14:19:59.540", + "updatedAt": "2025-11-03T14:19:59.601", + "orgUnit": "DiszpKrYNg8", + "inactive": false, + "deleted": false, + "potentialDuplicate": false, + "createdBy": { + "uid": "xE7jOejl9FI", + "username": "admin", + "firstName": "John", + "surname": "Traore" + }, + "updatedBy": { + "uid": "xE7jOejl9FI", + "username": "admin", + "firstName": "John", + "surname": "Traore" + }, + "relationships": [], + "attributes": [ + { + "attribute": "lZGmxYbs97q", + "code": "MMD_PER_ID", + "displayName": "Unique ID", + "createdAt": "2025-11-03T14:19:59.541", + "updatedAt": "2025-11-03T14:19:59.541", + "valueType": "TEXT", + "value": "8437107" + }, + { + "attribute": "VqEFza8wbwA", + "code": "MMD_PER_ADR1", + "displayName": "Address", + "createdAt": "2025-11-03T14:19:59.556", + "updatedAt": "2025-11-03T14:19:59.556", + "valueType": "TEXT", + "value": "Madison Avenue 12" + }, + { + "attribute": "w75KJ2mc4zz", + "code": "MMD_PER_NAM", + "displayName": "First name", + "createdAt": "2025-11-03T14:19:59.556", + "updatedAt": "2025-11-03T14:19:59.556", + "valueType": "TEXT", + "value": "Jane" + }, + { + "attribute": "zDhUuAYrxNC", + "displayName": "Last name", + "createdAt": "2025-11-03T14:19:59.556", + "updatedAt": "2025-11-03T14:19:59.556", + "valueType": "TEXT", + "value": "Doe" + }, + { + "attribute": "FO4sWYJ64LQ", + "code": "City", + "displayName": "City", + "createdAt": "2025-11-03T14:19:59.556", + "updatedAt": "2025-11-03T14:19:59.556", + "valueType": "TEXT", + "value": "New York" + } + ], + "enrollments": [ + { + "enrollment": "uWNTUmQH2q5", + "createdAt": "2025-11-03T14:19:59.558", + "updatedAt": "2025-11-03T14:19:59.558", + "trackedEntity": "QfFVkOL8ixj", + "program": "WSGAb5XwJ3Y", + "status": "ACTIVE", + "orgUnit": "DiszpKrYNg8", + "enrolledAt": "2025-11-03T00:00:00.000", + "occurredAt": "2025-11-03T00:00:00.000", + "followUp": false, + "deleted": false, + "createdBy": { + "uid": "xE7jOejl9FI", + "username": "admin", + "firstName": "John", + "surname": "Traore" + }, + "updatedBy": { + "uid": "xE7jOejl9FI", + "username": "admin", + "firstName": "John", + "surname": "Traore" + }, + "events": [ + { + "event": "oTnheGKRV8Z", + "status": "SCHEDULE", + "program": "WSGAb5XwJ3Y", + "programStage": "WZbXY0S00lP", + "enrollment": "uWNTUmQH2q5", + "trackedEntity": "QfFVkOL8ixj", + "orgUnit": "DiszpKrYNg8", + "relationships": [], + "occurredAt": "2025-11-03T00:00:00.000", + "scheduledAt": "2025-11-03T00:00:00.000", + "followUp": false, + "deleted": false, + "createdAt": "2025-11-03T14:19:59.566", + "updatedAt": "2025-11-03T14:19:59.566", + "attributeOptionCombo": "HllvX50cXC0", + "attributeCategoryOptions": "xYerKDKCefk", + "createdBy": { + "uid": "xE7jOejl9FI", + "username": "admin", + "firstName": "John", + "surname": "Traore" + }, + "updatedBy": { + "uid": "xE7jOejl9FI", + "username": "admin", + "firstName": "John", + "surname": "Traore" + }, + "dataValues": [], + "notes": [] + } + ], + "relationships": [], + "attributes": [ + { + "attribute": "Agywv2JGwuq", + "code": "MMD_PER_MOB", + "displayName": "Mobile number", + "createdAt": "2025-11-03T14:19:59.557", + "updatedAt": "2025-11-03T14:19:59.557", + "valueType": "PHONE_NUMBER", + "value": "+13052065294" + }, + { + "attribute": "ZcBPrXKahq2", + "displayName": "Postal code", + "createdAt": "2025-11-03T14:19:59.556", + "updatedAt": "2025-11-03T14:19:59.556", + "valueType": "TEXT", + "value": "10022" + }, + { + "attribute": "KmEUg2hHEtx", + "displayName": "Email address", + "createdAt": "2025-11-03T14:19:59.557", + "updatedAt": "2025-11-03T14:19:59.557", + "valueType": "EMAIL", + "value": "jane@doe.com" + }, + { + "attribute": "ciq2USN94oJ", + "code": "MMD_PER_STA", + "displayName": "Civil status", + "createdAt": "2025-11-03T14:19:59.557", + "updatedAt": "2025-11-03T14:19:59.557", + "valueType": "TEXT", + "value": "Single or widow" + }, + { + "attribute": "w75KJ2mc4zz", + "code": "MMD_PER_NAM", + "displayName": "First name", + "createdAt": "2025-11-03T14:19:59.556", + "updatedAt": "2025-11-03T14:19:59.556", + "valueType": "TEXT", + "value": "Jane" + }, + { + "attribute": "zDhUuAYrxNC", + "displayName": "Last name", + "createdAt": "2025-11-03T14:19:59.556", + "updatedAt": "2025-11-03T14:19:59.556", + "valueType": "TEXT", + "value": "Doe" + }, + { + "attribute": "FO4sWYJ64LQ", + "code": "City", + "displayName": "City", + "createdAt": "2025-11-03T14:19:59.556", + "updatedAt": "2025-11-03T14:19:59.556", + "valueType": "TEXT", + "value": "New York" + }, + { + "attribute": "gHGyrwKPzej", + "code": "MMD_PER_DOB", + "displayName": "Birth date", + "createdAt": "2025-11-03T14:19:59.557", + "updatedAt": "2025-11-03T14:19:59.557", + "valueType": "DATE", + "value": "1997-04-18" + }, + { + "attribute": "VqEFza8wbwA", + "code": "MMD_PER_ADR1", + "displayName": "Address", + "createdAt": "2025-11-03T14:19:59.556", + "updatedAt": "2025-11-03T14:19:59.556", + "valueType": "TEXT", + "value": "Madison Avenue 12" + }, + { + "attribute": "lZGmxYbs97q", + "code": "MMD_PER_ID", + "displayName": "Unique ID", + "createdAt": "2025-11-03T14:19:59.541", + "updatedAt": "2025-11-03T14:19:59.541", + "valueType": "TEXT", + "value": "8437107" + }, + { + "attribute": "gu1fqsmoU8r", + "displayName": "Allergies (multi-select)", + "createdAt": "2025-11-03T14:19:59.557", + "updatedAt": "2025-11-03T14:19:59.557", + "valueType": "MULTI_TEXT", + "value": "NSAIDS" + } + ], + "notes": [] + } + ], + "programOwners": [ + { + "orgUnit": "DiszpKrYNg8", + "trackedEntity": "QfFVkOL8ixj", + "program": "WSGAb5XwJ3Y" + } + ] +} \ No newline at end of file diff --git a/dhis2-to-fhir-patient-bundle-datasonnet/package.json b/dhis2-to-fhir-patient-bundle-datasonnet/package.json new file mode 100644 index 0000000..9acff97 --- /dev/null +++ b/dhis2-to-fhir-patient-bundle-datasonnet/package.json @@ -0,0 +1,22 @@ +{ + "name": "reference-dhis2-mosip-integration", + "version": "1.0.0", + "description": "This is a reference implementation of a **DHIS2 MOSIP Integration** from within the DHIS2 Capture App, with a FHIR-compliant electronic health record backend protected by OAuth2 authorization. This is an example which should be used for reference, it **SHOULD NOT** be used directly in production.", + "main": "index.js", + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@playwright/test": "^1.47.2", + "@types/node": "^22.7.3", + "concurrently": "^9.0.1", + "wait-on": "^8.0.1" + }, + "scripts": { + "build-fhir-sync-agent": "mvn -B clean package -Dmaven.test.skip=true -f fhir-sync-agent/pom.xml", + "build": "concurrently --kill-others-on-fail \"yarn:build-fhir-sync-agent\"", + "start-services": "docker compose -f docker-compose.yml -f tests/docker-compose.test.yml up --build --renew-anon-volumes --force-recreate --remove-orphans", + "start": "concurrently --kill-others-on-fail \"yarn:start-services\"", + "test": "playwright test" + } +} diff --git a/dhis2-to-fhir-patient-bundle-datasonnet/playwright.config.js b/dhis2-to-fhir-patient-bundle-datasonnet/playwright.config.js new file mode 100644 index 0000000..411d411 --- /dev/null +++ b/dhis2-to-fhir-patient-bundle-datasonnet/playwright.config.js @@ -0,0 +1,37 @@ +const { + defineConfig, + devices +} = require('@playwright/test'); + +module.exports = defineConfig({ + testDir: './tests', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: 'html', + use: { + baseURL: 'http://localhost:8080/', + extraHTTPHeaders: { + 'Content-Type': 'application/json', + 'Authorization': 'Basic YWRtaW46ZGlzdHJpY3Q=', + }, + trace: 'on-first-retry', + }, + + projects: [{ + name: 'chromium', + use: { + ...devices['Desktop Chrome'] + }, + }, ], + + webServer: { + command: 'docker compose -f esignet-docker-compose.yml -f docker-compose.yml -f tests/docker-compose.test.yml up --build --renew-anon-volumes --force-recreate --remove-orphans', + url: 'http://localhost:8080', + stdout: 'pipe', + stderr: 'pipe', + reuseExistingServer: true, + timeout: 300000 + }, +}); \ No newline at end of file diff --git a/dhis2-to-fhir-patient-bundle-datasonnet/tests/docker-compose.test.yml b/dhis2-to-fhir-patient-bundle-datasonnet/tests/docker-compose.test.yml new file mode 100644 index 0000000..3fedf60 --- /dev/null +++ b/dhis2-to-fhir-patient-bundle-datasonnet/tests/docker-compose.test.yml @@ -0,0 +1,6 @@ +services: + hapi-fhir-ips: + ports: + - "8081:8080" + networks: + - default \ No newline at end of file diff --git a/dhis2-to-fhir-patient-bundle-datasonnet/yarn.lock b/dhis2-to-fhir-patient-bundle-datasonnet/yarn.lock new file mode 100644 index 0000000..29687f0 --- /dev/null +++ b/dhis2-to-fhir-patient-bundle-datasonnet/yarn.lock @@ -0,0 +1,327 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@hapi/hoek@^9.0.0", "@hapi/hoek@^9.3.0": + version "9.3.0" + resolved "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz" + integrity sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ== + +"@hapi/topo@^5.1.0": + version "5.1.0" + resolved "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz" + integrity sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg== + dependencies: + "@hapi/hoek" "^9.0.0" + +"@playwright/test@^1.47.2": + version "1.47.2" + resolved "https://registry.npmjs.org/@playwright/test/-/test-1.47.2.tgz" + integrity sha512-jTXRsoSPONAs8Za9QEQdyjFn+0ZQFjCiIztAIF6bi1HqhBzG9Ma7g1WotyiGqFSBRZjIEqMdT8RUlbk1QVhzCQ== + dependencies: + playwright "1.47.2" + +"@sideway/address@^4.1.5": + version "4.1.5" + resolved "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz" + integrity sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q== + dependencies: + "@hapi/hoek" "^9.0.0" + +"@sideway/formula@^3.0.1": + version "3.0.1" + resolved "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz" + integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg== + +"@sideway/pinpoint@^2.0.0": + version "2.0.0" + resolved "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz" + integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ== + +"@types/node@^22.7.3": + version "22.7.3" + resolved "https://registry.npmjs.org/@types/node/-/node-22.7.3.tgz" + integrity sha512-qXKfhXXqGTyBskvWEzJZPUxSslAiLaB6JGP1ic/XTH9ctGgzdgYguuLP1C601aRTSDNlLb0jbKqXjZ48GNraSA== + dependencies: + undici-types "~6.19.2" + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +axios@^1.7.7: + version "1.7.7" + resolved "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz" + integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + +chalk@^4.1.2: + version "4.1.2" + resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +concurrently@^9.0.1: + version "9.0.1" + resolved "https://registry.npmjs.org/concurrently/-/concurrently-9.0.1.tgz" + integrity sha512-wYKvCd/f54sTXJMSfV6Ln/B8UrfLBKOYa+lzc6CHay3Qek+LorVSBdMVfyewFhRbH0Rbabsk4D+3PL/VjQ5gzg== + dependencies: + chalk "^4.1.2" + lodash "^4.17.21" + rxjs "^7.8.1" + shell-quote "^1.8.1" + supports-color "^8.1.1" + tree-kill "^1.2.2" + yargs "^17.7.2" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +escalade@^3.1.1: + version "3.2.0" + resolved "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + +follow-redirects@^1.15.6: + version "1.15.9" + resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz" + integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== + +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +fsevents@2.3.2: + version "2.3.2" + resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +joi@^17.13.3: + version "17.13.3" + resolved "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz" + integrity sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA== + dependencies: + "@hapi/hoek" "^9.3.0" + "@hapi/topo" "^5.1.0" + "@sideway/address" "^4.1.5" + "@sideway/formula" "^3.0.1" + "@sideway/pinpoint" "^2.0.0" + +lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12: + version "2.1.35" + resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +minimist@^1.2.8: + version "1.2.8" + resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +playwright-core@1.47.2: + version "1.47.2" + resolved "https://registry.npmjs.org/playwright-core/-/playwright-core-1.47.2.tgz" + integrity sha512-3JvMfF+9LJfe16l7AbSmU555PaTl2tPyQsVInqm3id16pdDfvZ8TTZ/pyzmkbDrZTQefyzU7AIHlZqQnxpqHVQ== + +playwright@1.47.2: + version "1.47.2" + resolved "https://registry.npmjs.org/playwright/-/playwright-1.47.2.tgz" + integrity sha512-nx1cLMmQWqmA3UsnjaaokyoUpdVaaDhJhMoxX2qj3McpjnsqFHs516QAKYhqHAgOP+oCFTEOCOAaD1RgD/RQfA== + dependencies: + playwright-core "1.47.2" + optionalDependencies: + fsevents "2.3.2" + +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +rxjs@^7.8.1: + version "7.8.1" + resolved "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz" + integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== + dependencies: + tslib "^2.1.0" + +shell-quote@^1.8.1: + version "1.8.1" + resolved "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz" + integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-color@^8.1.1: + version "8.1.1" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +tree-kill@^1.2.2: + version "1.2.2" + resolved "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz" + integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== + +tslib@^2.1.0: + version "2.7.0" + resolved "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz" + integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA== + +undici-types@~6.19.2: + version "6.19.8" + resolved "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz" + integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== + +wait-on@^8.0.1: + version "8.0.1" + resolved "https://registry.npmjs.org/wait-on/-/wait-on-8.0.1.tgz" + integrity sha512-1wWQOyR2LVVtaqrcIL2+OM+x7bkpmzVROa0Nf6FryXkS+er5Sa1kzFGjzZRqLnHa3n1rACFLeTwUqE1ETL9Mig== + dependencies: + axios "^1.7.7" + joi "^17.13.3" + lodash "^4.17.21" + minimist "^1.2.8" + rxjs "^7.8.1" + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + +yargs@^17.7.2: + version "17.7.2" + resolved "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== + dependencies: + cliui "^8.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.1.1"