diff --git a/.gitignore b/.gitignore index 6b325ba..9a3236e 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,5 @@ out/ .vscode/ /dev/gpas* /deploy/.env +/dev/gICS* +/dev/gPAS* diff --git a/README.md b/README.md index dd465c1..1307af1 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,45 @@ # ETL-Processor for DNPM:DIP [![Run Tests](https://github.com/pcvolkmer/etl-processor/actions/workflows/test.yml/badge.svg)](https://github.com/pcvolkmer/etl-processor/actions/workflows/test.yml) -Diese Anwendung versendet ein bwHC-MTB-File im bwHC-Datenmodell 1.0 an DNPM:DIP und pseudonymisiert die Patienten-ID. +Diese Anwendung versendet ein bwHC-MTB-File im bwHC-Datenmodell 1.0 an DNPM:DIP und pseudonymisiert +die Patienten-ID. ## Einordnung innerhalb einer DNPM-ETL-Strecke -Diese Anwendung erlaubt das Entgegennehmen von HTTP/REST-Anfragen aus dem Onkostar-Plugin **[onkostar-plugin-dnpmexport](https://github.com/CCC-MF/onkostar-plugin-dnpmexport)**. +Diese Anwendung erlaubt das Entgegennehmen von HTTP/REST-Anfragen aus dem Onkostar-Plugin * +*[onkostar-plugin-dnpmexport](https://github.com/CCC-MF/onkostar-plugin-dnpmexport)**. Der Inhalt einer Anfrage, wenn ein bwHC-MTBFile, wird pseudonymisiert und auf Duplikate geprüft. Duplikate werden verworfen, Änderungen werden weitergeleitet. Löschanfragen werden immer als Löschanfrage an DNPM:DIP weitergeleitet. -Zudem ist eine minimalistische Weboberfläche integriert, die einen Einblick in den aktuellen Zustand der Anwendung gewährt. +Zudem ist eine minimalistische Weboberfläche integriert, die einen Einblick in den aktuellen Zustand +der Anwendung gewährt. ![Modell DNPM-ETL-Strecke](docs/etl.png) ### Duplikaterkennung -Die Erkennung von Duplikaten ist normalerweise immer aktiv, kann jedoch über den Konfigurationsparameter +Die Erkennung von Duplikaten ist normalerweise immer aktiv, kann jedoch über den +Konfigurationsparameter `APP_DUPLICATION_DETECTION=false` deaktiviert werden. +### Modelvorhaben genomDE §64e + +Um die voll Betriebsbereitschaft herzustellen, muss eine erfolgreiche Übertragung mit dem +Submission-Typ *Test* erfolgt sein. Über die Umgebungsvariable wird dieser Übertragungsmodus +aktiviert. Alle Datensätze mit erteilter Teilnahme am Modelvorhaben werden mit der Test-Kennung +übertragen. + +`APP_GENOM_DE_TEST_SUBMISSION` -> `true` | `false` (falls fehlt wird `true` angenommen) + ### Datenübermittlung über HTTP/REST -Anfragen werden, wenn nicht als Duplikat behandelt, nach der Pseudonymisierung direkt an DNPM:DIP gesendet. +Anfragen werden, wenn nicht als Duplikat behandelt, nach der Pseudonymisierung direkt an DNPM:DIP +gesendet. -Ein HTTP Request kann, angenommen die Installation erfolgte auf dem Host `dnpm.example.com` an nachfolgende URLs gesendet werden: +Ein HTTP Request kann, angenommen die Installation erfolgte auf dem Host `dnpm.example.com` an +nachfolgende URLs gesendet werden: | HTTP-Request | URL | Consent-Status im Datensatz | Bemerkung | |--------------|-----------------------------------------|-----------------------------|---------------------------------------------------------------------------------| @@ -32,12 +47,15 @@ Ein HTTP Request kann, angenommen die Installation erfolgte auf dem Host `dnpm.e | `POST` | `https://dnpm.example.com/mtb` | `REJECT` | Die Anwendung sendet einen Lösch-Request für die im Datensatz angegebene Pat-ID | | `DELETE` | `https://dnpm.example.com/mtb/12345678` | - | Die Anwendung sendet einen Lösch-Request für Pat-ID `12345678` | -Anstelle des Pfads `/mtb` kann auch, wie in Version 0.9 und älter üblich, `/mtbfile` verwendet werden. +Anstelle des Pfads `/mtb` kann auch, wie in Version 0.9 und älter üblich, `/mtbfile` verwendet +werden. ### Datenübermittlung mit Apache Kafka -Anfragen werden, wenn nicht als Duplikat behandelt, nach der Pseudonymisierung an Apache Kafka übergeben. -Eine Antwort wird dabei ebenfalls mithilfe von Apache Kafka übermittelt und nach der Entgegennahme verarbeitet. +Anfragen werden, wenn nicht als Duplikat behandelt, nach der Pseudonymisierung an Apache Kafka +übergeben. +Eine Antwort wird dabei ebenfalls mithilfe von Apache Kafka übermittelt und nach der Entgegennahme +verarbeitet. Siehe hierzu auch: https://github.com/CCC-MF/kafka-to-bwhc @@ -45,15 +63,19 @@ Siehe hierzu auch: https://github.com/CCC-MF/kafka-to-bwhc ### 🔥 Wichtige Änderungen in Version 0.10 -Ab Version 0.10 wird [DNPM:DIP](https://github.com/dnpm-dip) unterstützt und als Standardendpunkt verwendet. -Soll noch das alte bwHC-Backend verwendet werden, so ist die Umgebungsvariable `APP_REST_IS_BWHC` auf `true` zu setzen. +Ab Version 0.10 wird [DNPM:DIP](https://github.com/dnpm-dip) unterstützt und als Standardendpunkt +verwendet. +Soll noch das alte bwHC-Backend verwendet werden, so ist die Umgebungsvariable `APP_REST_IS_BWHC` +auf `true` zu setzen. ### 🔥 Breaking Changes nach Version 0.10 -In Versionen des ETL-Processors **nach Version 0.10** werden die folgenden Konfigurationsoptionen entfernt: +In Versionen des ETL-Processors **nach Version 0.10** werden die folgenden Konfigurationsoptionen +entfernt: * `APP_KAFKA_TOPIC`: Nutzen Sie nun die Konfigurationsoption `APP_KAFKA_OUTPUT_TOPIC` -* `APP_KAFKA_RESPONSE_TOPIC`: Nutzen Sie nun die Konfigurationsoption `APP_KAFKA_OUTPUT_RESPONSE_TOPIC` +* `APP_KAFKA_RESPONSE_TOPIC`: Nutzen Sie nun die Konfigurationsoption + `APP_KAFKA_OUTPUT_RESPONSE_TOPIC` Der Pfad zum Versenden von MTB-Daten ist nun offiziell `/mtb`. In Versionen **nach Version 0.10** wird die Unterstützung des Pfads `/mtbfile` entfernt. @@ -66,30 +88,92 @@ Ist diese nicht gesetzt. wird intern eine Anonymisierung der Patienten-ID vorgen * `APP_PSEUDONYMIZE_PREFIX`: Standortbezogenes Präfix - `UNKNOWN`, wenn nicht gesetzt * `APP_PSEUDONYMIZE_GENERATOR`: `BUILDIN` oder `GPAS` - `BUILDIN`, wenn nicht gesetzt -**Hinweis** +**Hinweis** Die Pseudonymisierung erfolgt im ETL-Prozessor nur für die Patienten-ID. -Andere IDs werden mithilfe des standortbezogenen Präfixes (erneut) anonymisiert, um für den aktuellen Kontext nicht +Andere IDs werden mithilfe des standortbezogenen Präfixes (erneut) anonymisiert, um für den +aktuellen Kontext nicht vergleichbare IDs bereitzustellen. #### Eingebaute Anonymisierung -Wurde keine oder die Verwendung der eingebauten Anonymisierung konfiguriert, so wird für die Patienten-ID der -entsprechende SHA-256-Hash gebildet und Base64-codiert - hier ohne endende "=" - zuzüglich des konfigurierten Präfixes +Wurde keine oder die Verwendung der eingebauten Anonymisierung konfiguriert, so wird für die +Patienten-ID der +entsprechende SHA-256-Hash gebildet und Base64-codiert - hier ohne endende "=" - zuzüglich des +konfigurierten Präfixes als Patienten-Pseudonym verwendet. #### Pseudonymisierung mit gPAS Wurde die Verwendung von gPAS konfiguriert, so sind weitere Angaben zu konfigurieren. -* `APP_PSEUDONYMIZE_GPAS_URI`: URI der gPAS-Instanz inklusive Endpoint (z.B. `http://localhost:8080/ttp-fhir/fhir/gpas/$$pseudonymizeAllowCreate`) +* `APP_PSEUDONYMIZE_GPAS_URI`: URI der gPAS-Instanz inklusive Endpoint (z.B. + `http://localhost:8080/ttp-fhir/fhir/gpas/$$pseudonymizeAllowCreate`) * `APP_PSEUDONYMIZE_GPAS_TARGET`: gPas Domänenname * `APP_PSEUDONYMIZE_GPAS_USERNAME`: gPas Basic-Auth Benutzername * `APP_PSEUDONYMIZE_GPAS_PASSWORD`: gPas Basic-Auth Passwort +### (Externe) Consent-Services + +Consent-Services können konfiguriert werden. + +* `APP_CONSENT_SERVICE`: Zu verwendender (externer) Consent-Service: + * `NONE`: Verwende Consent-Angaben im MTB-File v1 und ändere diese nicht. Für MTB-File v2 wird + die Prüfung übersprungen. + * `GICS`: Verwende gICS der Greiswalder Tools (siehe unten). + +#### Einwilligung gICS + +Ab gIcs Version 2.13.0 kann im ETL-Processor +per [REST-Schnittstelle](https://simplifier.net/guide/ttp-fhir-gateway-ig/ImplementationGuide-markdown-Einwilligungsmanagement-Operations-isConsented?version=current) +der Einwilligungsstatus abgefragt werden. +Vor der MTB-Übertragung kann der zum Sendezeitpunkt verfügbarer Einwilligungsstatus über Endpunkt +*isConsented* (MTB-File v1) und *currentPolicyStatesForPerson* (MTB-File v2) abgefragt werden. + +Falls Anbindung an gICS aktiviert wurde, wird der Einwilligungsstatus der MTB Datei ignoriert. +Stattdessen werden vorhandene Einwilligungen abgefragt und in die MTB Datei eingebettet. + +Es werden zwei Einwilligungsdomänen unterstützt, eine für Broad Consent und als zweites GenomDE +Modelvorhaben §64e. + +##### Hinweise + +1. Die aktuelle Impl. nimmt an, dass die hinterlegten Domänen der Einwilligungen ausschließlich für + die genannten Art von Einwilligungen genutzt werden. Es finde keine weitere Filterung statt. Wir + fragen pro Domäne die Schnittstelle `CurrentPolicyStatesForPerson` - siehe + auch [IG TTP-FHIR Gateway + ](https://www.ths-greifswald.de/wp-content/uploads/tools/fhirgw/ig/2024-3-0/ImplementationGuide-markdown-Einwilligungsmanagement-Operations-currentPolicyStatesForPerson.html) + ab. +2. Die Einwilligung wird für den Patienten-Identifier der MTB abgerufen und anschließend durch das + DNPM Pseudonym ersetzt. +3. Abfragen von Einwilligungen über gesonderte Pseudonyme anstatt des MTB-Identifiers fehlt in der + ersten Implementierung. +4. Bei Verarbeitung von MTB Version 1.x Inhalten ist eine positive Einwilligung für die + Weiterverarbeitung notwendig. Das Fehlen einer Einwilligung löst die Löschung des Patienten im + Brückenkopf aus. + +##### Konfiguration + +* `APP_CONSRENT_SERVICE`: Muss Wert `GICS` gesetzt sein um die Abfragen zu aktivieren. Der Wert + `NONE` deaktiviert die Abfrage in gICS. +* `APP_CONSENT_GICS_URI`: URI der gICS-Instanz (z.B. `http://localhost:8090/ttp-fhir/fhir/gics`) +* `APP_CONSENT_GICS_USERNAME`: gIcs Basic-Auth Benutzername +* `APP_CONSENT_GICS_PASSWORD`: gIcs Basic-Auth Passwort +* `APP_CONSENT_GICS_PERSONIDENTIFIERSYSTEM`: Derzeit wird nur die PID unterstützt. wenn leer wird + `https://ths-greifswald.de/fhir/gics/identifiers/Patienten-ID` angenommen +* `APP_CONSENT_GICS_BROADCONSENTDOMAINNAME`: Domäne in der gIcs Broad Consent Einwilligungen + verwaltet. Falls Wert leer, wird `MII` angenommen. +* `APP_CONSENT_GICS_GNOMDECONSENTDOMAINNAME`: Domäne in der gIcs GenomDE Modelvorhaben §64e + Einwilligungen verwaltet. Falls Wert leer, wird `GenomDE_MV` angenommen. +* `APP_CONSENT_GICS_POLICYCODE`: Die entscheidende Objekt-ID der zu prüfenden Einwilligung-Regel. + Falls leer wird `2.16.840.1.113883.3.1937.777.24.5.3.6` angenommen. +* `APP_CONSENT_GICS_POLICYSYSTEM`: Das System der Einwilligung-Regel der Objekt-IDs. Falls leer wird + `urn:oid:2.16.840.1.113883.3.1937.777.24.5.3` angenommen. + ### Anmeldung mit einem Passwort -Ein initialer Administrator-Account kann optional konfiguriert werden und sorgt dafür, dass bestimmte Bereiche nur nach +Ein initialer Administrator-Account kann optional konfiguriert werden und sorgt dafür, dass +bestimmte Bereiche nur nach einem erfolgreichen Login erreichbar sind. * `APP_SECURITY_ADMIN_USER`: Muss angegeben werden zur Aktivierung der Zugriffsbeschränkung. @@ -103,27 +187,34 @@ Hier Beispiele für das Beispielpasswort `very-secret`: * `{bcrypt}$2y$05$CCkfsMr/wbTleMyjVIK8g.Aa3RCvrvoLXVAsL.f6KeouS88vXD9b6` * `{sha256}9a34717f0646b5e9cfcba70055de62edb026ff4f68671ba3db96aa29297d2df5f1a037d58c745657` -Wird kein Administrator-Passwort angegeben, wird ein zufälliger Wert generiert und beim Start der Anwendung in den Logs +Wird kein Administrator-Passwort angegeben, wird ein zufälliger Wert generiert und beim Start der +Anwendung in den Logs angezeigt. #### Weitere (nicht administrative) Nutzer mit OpenID Connect -Die folgenden Konfigurationsparameter werden benötigt, um die Authentifizierung weiterer Benutzer an einen OIDC-Provider +Die folgenden Konfigurationsparameter werden benötigt, um die Authentifizierung weiterer Benutzer an +einen OIDC-Provider zu delegieren. Ein Admin-Benutzer muss dabei konfiguriert sein. -* `APP_SECURITY_ENABLE_OIDC`: Aktiviert die Nutzung von OpenID Connect. Damit sind weitere Parameter erforderlich -* `SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_CUSTOM_CLIENT_NAME`: Name. Wird beim zusätzlichen Loginbutton angezeigt. +* `APP_SECURITY_ENABLE_OIDC`: Aktiviert die Nutzung von OpenID Connect. Damit sind weitere Parameter + erforderlich +* `SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_CUSTOM_CLIENT_NAME`: Name. Wird beim zusätzlichen + Loginbutton angezeigt. * `SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_CUSTOM_CLIENT_ID`: Client-ID * `SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_CUSTOM_CLIENT_SECRET`: Client-Secret -* `SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_CUSTOM_CLIENT_SCOPE[0]`: Hier sollte immer `openid` angegeben werden. +* `SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_CUSTOM_CLIENT_SCOPE[0]`: Hier sollte immer `openid` + angegeben werden. * `SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_CUSTOM_ISSUER_URI`: Die URI des Providers, z.B. `https://auth.example.com/realm/example` -* `SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_CUSTOM_USER_NAME_ATTRIBUTE`: Name des Attributes, welches den Benutzernamen +* `SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_CUSTOM_USER_NAME_ATTRIBUTE`: Name des Attributes, welches + den Benutzernamen enthält. Oft verwendet: `preferred_username` -Ist die Nutzung von OpenID Connect konfiguriert, erscheint ein zusätzlicher Login-Button zur Nutzung mit OpenID Connect +Ist die Nutzung von OpenID Connect konfiguriert, erscheint ein zusätzlicher Login-Button zur Nutzung +mit OpenID Connect und dem konfigurierten `CLIENT_NAME`. ![Login mit OpenID Connect](docs/login.png) @@ -134,62 +225,76 @@ zu finden. #### Rollenbasierte Berechtigungen -Wird OpenID Connect verwendet, gibt es eine rollenbasierte Berechtigungszuweisung. +Wird OpenID Connect verwendet, gibt es eine rollenbasierte Berechtigungszuweisung. -Die Standardrolle für neue OIDC-Benutzer kann mit der Option `APP_SECURITY_DEFAULT_USER_ROLE` festgelegt werden. +Die Standardrolle für neue OIDC-Benutzer kann mit der Option `APP_SECURITY_DEFAULT_USER_ROLE` +festgelegt werden. Mögliche Werte sind `user` oder `guest`. Standardwert ist `user`. Benutzer mit der Rolle "Gast" sehen nur die Inhalte, die auch nicht angemeldete Benutzer sehen. -Hierdurch ist es möglich, einzelne Benutzer einzuschränken oder durch Änderung der Standardrolle auf `guest` nur +Hierdurch ist es möglich, einzelne Benutzer einzuschränken oder durch Änderung der Standardrolle auf +`guest` nur einzelne Benutzer als vollwertige Nutzer zuzulassen. ![Rollenverwaltung](docs/userroles.png) -Benutzer werden nach dem Entfernen oder der Änderung der vergebenen Rolle automatisch abgemeldet und müssen sich neu anmelden. +Benutzer werden nach dem Entfernen oder der Änderung der vergebenen Rolle automatisch abgemeldet und +müssen sich neu anmelden. Sie bekommen dabei wieder die Standardrolle zugewiesen. #### Auswirkungen auf den dargestellten Inhalt -Nur Administratoren haben Zugriff auf den Konfigurationsbereich, nur angemeldete Benutzer können die anonymisierte oder +Nur Administratoren haben Zugriff auf den Konfigurationsbereich, nur angemeldete Benutzer können die +anonymisierte oder pseudonymisierte Patienten-ID sowie den Qualitätsbericht von DNPM:DIP einsehen. Wurde kein Administrator-Account konfiguriert, sind diese Inhalte generell nicht verfügbar. ### Tokenbasierte Authentifizierung für MTBFile-Endpunkt -Die Anwendung unterstützt das Erstellen und Nutzen einer tokenbasierten Authentifizierung für den MTB-File-Endpunkt. +Die Anwendung unterstützt das Erstellen und Nutzen einer tokenbasierten Authentifizierung für den +MTB-File-Endpunkt. -Dies kann mit der Umgebungsvariable `APP_SECURITY_ENABLE_TOKENS` aktiviert (`true` oder `false`) werden +Dies kann mit der Umgebungsvariable `APP_SECURITY_ENABLE_TOKENS` aktiviert (`true` oder `false`) +werden und ist als Standardeinstellung nicht aktiv. -Ist diese Einstellung aktiviert worden, ist es Administratoren möglich, Zugriffstokens für Onkostar zu erstellen, die +Ist diese Einstellung aktiviert worden, ist es Administratoren möglich, Zugriffstokens für Onkostar +zu erstellen, die zur Nutzung des MTB-File-Endpunkts eine HTTP-Basic-Authentifizierung voraussetzen. ![Tokenverwaltung](docs/tokens.png) -In diesem Fall kann der Endpunkt für das Onkostar-Plugin **[onkostar-plugin-dnpmexport](https://github.com/CCC-MF/onkostar-plugin-dnpmexport)** wie folgt konfiguriert werden: +In diesem Fall kann der Endpunkt für das Onkostar-Plugin * +*[onkostar-plugin-dnpmexport](https://github.com/CCC-MF/onkostar-plugin-dnpmexport)** wie folgt +konfiguriert werden: ``` https://testonkostar:MTg1NTL...NGU4@etl.example.com/mtbfile ``` -Ist die Verwendung von Tokens aktiv, werden Anfragen ohne die Angabe der Token-Information abgelehnt. +Ist die Verwendung von Tokens aktiv, werden Anfragen ohne die Angabe der Token-Information +abgelehnt. Alternativ kann eine Authentifizierung über Benutzername/Passwort oder OIDC erfolgen. ### Transformation von Werten -In Onkostar kann es vorkommen, dass ein Wert eines Merkmalskatalogs an einem Standort angepasst wurde und dadurch nicht dem Wert entspricht, +In Onkostar kann es vorkommen, dass ein Wert eines Merkmalskatalogs an einem Standort angepasst +wurde und dadurch nicht dem Wert entspricht, der von DNPM:DIP akzeptiert wird. -Diese Anwendung bietet daher die Möglichkeit, eine Transformation vorzunehmen. Hierzu muss der "Pfad" innerhalb des JSON-MTB-Files angegeben werden und +Diese Anwendung bietet daher die Möglichkeit, eine Transformation vorzunehmen. Hierzu muss der " +Pfad" innerhalb des JSON-MTB-Files angegeben werden und welcher Wert wie ersetzt werden soll. Hier ein Beispiel für die erste (Index 0 - weitere dann mit 1,2, ...) Transformationsregel: -* `APP_TRANSFORMATIONS_0_PATH`: Pfad zum Wert in der JSON-MTB-Datei. Beispiel: `diagnoses[*].icd10.version` für **alle** Diagnosen -* `APP_TRANSFORMATIONS_0_FROM`: Angabe des Werts, der ersetzt werden soll. Andere Werte bleiben dabei unverändert. +* `APP_TRANSFORMATIONS_0_PATH`: Pfad zum Wert in der JSON-MTB-Datei. Beispiel: + `diagnoses[*].icd10.version` für **alle** Diagnosen +* `APP_TRANSFORMATIONS_0_FROM`: Angabe des Werts, der ersetzt werden soll. Andere Werte bleiben + dabei unverändert. * `APP_TRANSFORMATIONS_0_TO`: Angabe des neuen Werts. ### Mögliche Endpunkte zur Datenübermittlung @@ -204,51 +309,61 @@ Werden sowohl REST als auch Kafka-Endpunkt konfiguriert, wird nur der REST-Endpu Folgende Umgebungsvariablen müssen gesetzt sein, damit ein bwHC-MTB-File an DNPM:DIP gesendet wird: * `APP_REST_URI`: URI der zu benutzenden API der Backend-Instanz. Zum Beispiel: - * `http://localhost:9000/bwhc/etl/api` für **bwHC Backend** - * `http://localhost:9000/api` für **dnpm:dip** + * `http://localhost:9000/bwhc/etl/api` für **bwHC Backend** + * `http://localhost:9000/api` für **dnpm:dip** * `APP_REST_USERNAME`: Basic-Auth-Benutzername für den REST-Endpunkt * `APP_REST_PASSWORD`: Basic-Auth-Passwort für den REST-Endpunkt * `APP_REST_IS_BWHC`: `true` für **bwHC Backend**, weglassen oder `false` für **dnpm:dip** #### Kafka-Topics -Folgende Umgebungsvariablen müssen gesetzt sein, damit ein bwHC-MTB-File an ein Kafka-Topic übermittelt wird: +Folgende Umgebungsvariablen müssen gesetzt sein, damit ein bwHC-MTB-File an ein Kafka-Topic +übermittelt wird: -* `APP_KAFKA_OUTPUT_TOPIC`: Zu verwendendes Topic zum Versenden von Anfragen. -* `APP_KAFKA_OUTPUT_RESPONSE_TOPIC`: Topic mit Antworten über den Erfolg des Versendens. Standardwert: `APP_KAFKA_TOPIC` mit Anhang "_response". -* `APP_KAFKA_GROUP_ID`: Kafka GroupID des Consumers. Standardwert: `APP_KAFKA_TOPIC` mit Anhang "_group". +* `APP_KAFKA_OUTPUT_TOPIC`: Zu verwendendes Topic zum Versenden von Anfragen. +* `APP_KAFKA_OUTPUT_RESPONSE_TOPIC`: Topic mit Antworten über den Erfolg des Versendens. + Standardwert: `APP_KAFKA_TOPIC` mit Anhang "_response". +* `APP_KAFKA_GROUP_ID`: Kafka GroupID des Consumers. Standardwert: `APP_KAFKA_TOPIC` mit Anhang "_ + group". * `APP_KAFKA_SERVERS`: Zu verwendende Kafka-Bootstrap-Server als kommagetrennte Liste -Wird keine Rückantwort über Apache Kafka empfangen und es gibt keine weitere Möglichkeit den Status festzustellen, verbleibt der Status auf `UNKNOWN`. +Wird keine Rückantwort über Apache Kafka empfangen und es gibt keine weitere Möglichkeit den Status +festzustellen, verbleibt der Status auf `UNKNOWN`. Weitere Einstellungen können über die Parameter von Spring Kafka konfiguriert werden. -Lässt sich keine Verbindung zu dem Backend aufbauen, wird eine Rückantwort mit Status-Code `900` erwartet, welchen es +Lässt sich keine Verbindung zu dem Backend aufbauen, wird eine Rückantwort mit Status-Code `900` +erwartet, welchen es für HTTP nicht gibt. -Wird die Umgebungsvariable `APP_KAFKA_INPUT_TOPIC` gesetzt, kann eine Nachricht auch über dieses Kafka-Topic an den ETL-Prozessor übermittelt werden. +Wird die Umgebungsvariable `APP_KAFKA_INPUT_TOPIC` gesetzt, kann eine Nachricht auch über dieses +Kafka-Topic an den ETL-Prozessor übermittelt werden. ##### Retention Time Generell werden in Apache Kafka alle Records entsprechend der Konfiguration vorgehalten. So wird ohne spezielle Konfiguration ein Record für 7 Tage in Apache Kafka gespeichert. -Es sind innerhalb dieses Zeitraums auch alte Informationen weiterhin enthalten, wenn der Consent später abgelehnt wurde. +Es sind innerhalb dieses Zeitraums auch alte Informationen weiterhin enthalten, wenn der Consent +später abgelehnt wurde. Durch eine entsprechende Konfiguration des Topics kann dies verhindert werden. Beispiel - auszuführen innerhalb des Kafka-Containers: Löschen alter Records nach einem Tag + ``` kafka-configs.sh --bootstrap-server localhost:9092 --alter --topic test --add-config retention.ms=86400000 ``` ##### Key based Retention -Möchten Sie hingegen immer nur die letzte Meldung für einen Patienten und eine Erkrankung in Apache Kafka vorhalten, +Möchten Sie hingegen immer nur die letzte Meldung für einen Patienten und eine Erkrankung in Apache +Kafka vorhalten, so ist die nachfolgend genannte Konfiguration der Kafka-Topics hilfreich. - -* `retention.ms`: Möglichst kurze Zeit in der alte Records noch erhalten bleiben, z.B. 10 Sekunden 10000 -* `cleanup.policy`: Löschen alter Records und Beibehalten des letzten Records zu einem Key [delete,compact] +* `retention.ms`: Möglichst kurze Zeit in der alte Records noch erhalten bleiben, z.B. 10 Sekunden + 10000 +* `cleanup.policy`: Löschen alter Records und Beibehalten des letzten Records zu einem + Key [delete,compact] Beispiele für ein Topic `test`, hier bitte an die verwendeten Topics anpassen. @@ -257,17 +372,23 @@ kafka-configs.sh --bootstrap-server localhost:9092 --alter --topic test --add-co kafka-configs.sh --bootstrap-server localhost:9092 --alter --topic test --add-config cleanup.policy=[delete,compact] ``` -Da als Key eines Records die (pseudonymisierte) Patienten-ID verwendet wird, stehen mit obiger Konfiguration -der Kafka-Topics nach 10 Sekunden nur noch der jeweils letzte Eintrag für den entsprechenden Key zur Verfügung. +Da als Key eines Records die (pseudonymisierte) Patienten-ID verwendet wird, stehen mit obiger +Konfiguration +der Kafka-Topics nach 10 Sekunden nur noch der jeweils letzte Eintrag für den entsprechenden Key zur +Verfügung. -Da der Key sowohl für die Records in Richtung DNPM:DIP, als auch für die Rückantwort identisch aufgebaut ist, lassen sich so -auch im Falle eines Consent-Widerspruchs die enthaltenen Daten als auch die Offenlegung durch Verifikationsdaten in der +Da der Key sowohl für die Records in Richtung DNPM:DIP, als auch für die Rückantwort identisch +aufgebaut ist, lassen sich so +auch im Falle eines Consent-Widerspruchs die enthaltenen Daten als auch die Offenlegung durch +Verifikationsdaten in der Antwort effektiv verhindern, da diese nach 10 Sekunden gelöscht werden. -Es steht dann nur noch die jeweils letzten Information zur Verfügung, dass für einen Patienten/eine Erkrankung +Es steht dann nur noch die jeweils letzten Information zur Verfügung, dass für einen Patienten/eine +Erkrankung ein Consent-Widerspruch erfolgte. -Dieses Vorgehen empfiehlt sich, wenn Sie gespeicherte Records nachgelagert für andere Auswertungen verwenden möchten. +Dieses Vorgehen empfiehlt sich, wenn Sie gespeicherte Records nachgelagert für andere Auswertungen +verwenden möchten. ### Antworten und Statusauswertung @@ -279,10 +400,12 @@ Anfragen an das bwHC-Backend aus Versionen bis 0.9.x wurden wie folgt behandelt: | `HTTP 201` | `WARNING` | | `HTTP 400-...` | `ERROR` | -Dies konnte dazu führen, dass zwar mit einem `HTTP 201` geantwortet wurde, aber dennoch in der Issue-Liste die +Dies konnte dazu führen, dass zwar mit einem `HTTP 201` geantwortet wurde, aber dennoch in der +Issue-Liste die Severity `error` aufgetaucht ist. -Ab Version 0.10 wird die Issue-Liste der Antwort verwendet und die darion enthaltene höchste Severity-Stufe als Ergebnis verwendet. +Ab Version 0.10 wird die Issue-Liste der Antwort verwendet und die darion enthaltene höchste +Severity-Stufe als Ergebnis verwendet. | Höchste Severity | Status | |------------------|-----------| @@ -292,9 +415,10 @@ Ab Version 0.10 wird die Issue-Liste der Antwort verwendet und die darion enthal ## Docker-Images -Diese Anwendung ist auch als Docker-Image verfügbar: https://github.com/pcvolkmer/etl-processor/pkgs/container/etl-processor +Diese Anwendung ist auch als Docker-Image +verfügbar: https://github.com/pcvolkmer/etl-processor/pkgs/container/etl-processor -### Images lokal bauen +### Images lokal bauen ```bash ./gradlew bootBuildImage @@ -302,20 +426,25 @@ Diese Anwendung ist auch als Docker-Image verfügbar: https://github.com/pcvolkm ### Integration eines eigenen Root CA Zertifikats -Wird eine eigene Root CA verwendet, die nicht offiziell signiert ist, wird es zu Problemen beim SSL-Handshake kommen, wenn z.B. gPAS zur Generierung von Pseudonymen verwendet wird. +Wird eine eigene Root CA verwendet, die nicht offiziell signiert ist, wird es zu Problemen beim +SSL-Handshake kommen, wenn z.B. gPAS zur Generierung von Pseudonymen verwendet wird. Hier bietet es sich an, das Root CA Zertifikat in das Image zu integrieren. #### Integration beim Bauen des Images -Hier muss die Zeile `"BP_EMBED_CERTS" to "true"` in der Datei `build.gradle.kts` verwendet werden und darf nicht als Kommentar verwendet werden. +Hier muss die Zeile `"BP_EMBED_CERTS" to "true"` in der Datei `build.gradle.kts` verwendet werden +und darf nicht als Kommentar verwendet werden. -Die PEM-Datei mit dem/den Root CA Zertifikat(en) muss dabei im vorbereiteten Verzeichnis [`bindings/ca-certificates`](bindings/ca-certificates) enthalten sein. +Die PEM-Datei mit dem/den Root CA Zertifikat(en) muss dabei im vorbereiteten Verzeichnis [ +`bindings/ca-certificates`](bindings/ca-certificates) enthalten sein. #### Integration zur Laufzeit Hier muss die Umgebungsvariable `SERVICE_BINDING_ROOT` z.B. auf den Wert `/bindings` gesetzt sein. -Zudem muss ein Verzeichnis `bindings/ca-certificates` - analog zum Verzeichnis [`bindings/ca-certificates`](bindings/ca-certificates) mit einer PEM-Datei als Docker-Volume eingebunden werden. +Zudem muss ein Verzeichnis `bindings/ca-certificates` - analog zum Verzeichnis [ +`bindings/ca-certificates`](bindings/ca-certificates) mit einer PEM-Datei als Docker-Volume +eingebunden werden. Beispiel für Docker-Compose: @@ -330,12 +459,14 @@ Beispiel für Docker-Compose: ``` ## Deployment + *Ausführen als Docker Container:* ```bash cd ./deploy cp env-sample.env .env ``` + Wenn gewünscht, Änderungen in der `.env` vornehmen. ```bash @@ -344,15 +475,19 @@ docker compose up -d ### Einfaches Beispiel für ein eigenes Docker-Compose-File -Die Datei [`docs/docker-compose.yml`](docs/docker-compose.yml) zeigt eine einfache Konfiguration für REST-Requests basierend +Die Datei [`docs/docker-compose.yml`](docs/docker-compose.yml) zeigt eine einfache Konfiguration für +REST-Requests basierend auf Docker-Compose mit der gestartet werden kann. ### Betrieb hinter einem Reverse-Proxy -Die Anwendung verarbeitet `X-Forwarded`-HTTP-Header und kann daher auch hinter einem Reverse-Proxy betrieben werden. +Die Anwendung verarbeitet `X-Forwarded`-HTTP-Header und kann daher auch hinter einem Reverse-Proxy +betrieben werden. -Dabei werden, je nachdem welche Header durch den Reverse-Proxy gesendet werden auch Protokoll, Host oder auch Path-Präfix -automatisch erkannt und verwendet werden. Dadurch ist z.B. eine abweichende Angabe des Pfads problemlos möglich. +Dabei werden, je nachdem welche Header durch den Reverse-Proxy gesendet werden auch Protokoll, Host +oder auch Path-Präfix +automatisch erkannt und verwendet werden. Dadurch ist z.B. eine abweichende Angabe des Pfads +problemlos möglich. #### Beispiel *Traefik* (mit Docker-Labels): @@ -388,13 +523,17 @@ Das folgende Beispiel zeigt die Konfiguration einer _location_ in einer nginx-Ko ## Entwicklungssetup -Zum Starten einer lokalen Entwicklungs- und Testumgebung kann die beiliegende Datei `dev-compose.yml` verwendet werden. +Zum Starten einer lokalen Entwicklungs- und Testumgebung kann die beiliegende Datei +`dev-compose.yml` verwendet werden. Diese kann zur Nutzung der Datenbanken **MariaDB** als auch **PostgreSQL** angepasst werden. -Zur Nutzung von Apache Kafka muss dazu ein Eintrag im hosts-File vorgenommen werden und der Hostname `kafka` auf die lokale -IP-Adresse verweisen. Ohne diese Einstellung ist eine Nutzung von Apache Kafka außerhalb der Docker-Umgebung nicht möglich. +Zur Nutzung von Apache Kafka muss dazu ein Eintrag im hosts-File vorgenommen werden und der Hostname +`kafka` auf die lokale +IP-Adresse verweisen. Ohne diese Einstellung ist eine Nutzung von Apache Kafka außerhalb der +Docker-Umgebung nicht möglich. -Beim Start der Anwendung mit dem Profil `dev` wird die in `dev-compose.yml` definierte Umgebung beim Start der +Beim Start der Anwendung mit dem Profil `dev` wird die in `dev-compose.yml` definierte Umgebung beim +Start der Anwendung mit gestartet: ``` @@ -406,4 +545,5 @@ Die Datei `application-dev.yml` enthält hierzu die Konfiguration für das Profi Beim Ausführen der Integrationstests wird eine Testdatenbank in einem Docker-Container gestartet. Siehe hier auch die Klasse `AbstractTestcontainerTest` unter `src/integrationTest`. -Ein einfaches Entwickler-Setup inklusive DNPM:DIP ist mit Hilfe von https://github.com/pcvolkmer/dnpmdip-devenv realisierbar. \ No newline at end of file +Ein einfaches Entwickler-Setup inklusive DNPM:DIP ist mit Hilfe +von https://github.com/pcvolkmer/dnpmdip-devenv realisierbar. diff --git a/dev-compose.yml b/dev-compose.yml index e2dfdb6..faaedc5 100644 --- a/dev-compose.yml +++ b/dev-compose.yml @@ -16,6 +16,11 @@ services: KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE: true KAFKA_CFG_CONTROLLER_QUORUM_VOTERS: 0@kafka:9093 KAFKA_CFG_CONTROLLER_LISTENER_NAMES: CONTROLLER + healthcheck: + test: kafka-topics --bootstrap-server kafka:9092 --list + interval: 30s + timeout: 10s + retries: 3 ## Use AKHQ as Kafka web frontend akhq: @@ -53,4 +58,4 @@ services: # environment: # POSTGRES_DB: dev # POSTGRES_USER: dev -# POSTGRES_PASSWORD: dev \ No newline at end of file +# POSTGRES_PASSWORD: dev diff --git a/dev/docker-compose.dev.yml b/dev/docker-compose.dev.yml index d7a436b..f8f9183 100644 --- a/dev/docker-compose.dev.yml +++ b/dev/docker-compose.dev.yml @@ -2,31 +2,55 @@ version: '3.7' services: - zoo1: - image: zookeeper:3.8.0 - hostname: zoo1 + zoo: + image: zookeeper:3.9.2 + restart: unless-stopped ports: - "2181:2181" environment: ZOO_MY_ID: 1 ZOO_PORT: 2181 - ZOO_SERVERS: server.1=zoo1:2888:3888;2181 + ZOO_SERVERS: server.1=zoo:2888:3888;2181 - kafka1: - image: confluentinc/cp-kafka:7.2.1 - hostname: kafka1 + kafka: + image: confluentinc/cp-kafka:7.6.1 ports: - "9092:9092" environment: - KAFKA_ADVERTISED_LISTENERS: LISTENER_DOCKER_INTERNAL://kafka1:19092,LISTENER_DOCKER_EXTERNAL://${DOCKER_HOST_IP:-127.0.0.1}:9092 - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: LISTENER_DOCKER_INTERNAL:PLAINTEXT,LISTENER_DOCKER_EXTERNAL:PLAINTEXT + KAFKA_ADVERTISED_LISTENERS: LISTENER_DOCKER_INTERNAL://kafka:19092,LISTENER_DOCKER_EXTERNAL://172.17.0.1:9093,LISTENER_EXTERNAL://127.0.0.1:9092 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: LISTENER_DOCKER_INTERNAL:PLAINTEXT,LISTENER_DOCKER_EXTERNAL:PLAINTEXT,LISTENER_EXTERNAL:PLAINTEXT KAFKA_INTER_BROKER_LISTENER_NAME: LISTENER_DOCKER_INTERNAL - KAFKA_ZOOKEEPER_CONNECT: "zoo1:2181" + KAFKA_ZOOKEEPER_CONNECT: zoo:2181 KAFKA_BROKER_ID: 1 - KAFKA_LOG4J_LOGGERS: "kafka.controller=INFO,kafka.producer.async.DefaultEventHandler=INFO,state.change.logger=INFO" + KAFKA_LOG4J_LOGGERS: kafka.controller=INFO,kafka.producer.async.DefaultEventHandler=INFO,state.change.logger=INFO KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_MESSAGE_MAX_BYTES: 5242880 + KAFKA_REPLICA_FETCH_MAX_BYTES: 5242880 + KAFKA_COMPRESSION_TYPE: gzip depends_on: - - zoo1 + - zoo + healthcheck: + test: kafka-topics --bootstrap-server kafka:9092 --list + interval: 30s + timeout: 10s + retries: 3 + + akhq: + image: tchiotludo/akhq:0.25.0 + environment: + AKHQ_CONFIGURATION: | + akhq: + ui-options: + topic.show-all-consumer-groups: true + topic-data.sort: NEWEST + connections: + docker-kafka-server: + properties: + bootstrap.servers: "kafka:19092" + ports: + - "9000:8080" + depends_on: + - kafka kafka-rest-proxy: image: confluentinc/cp-kafka-rest:7.2.1 @@ -40,8 +64,8 @@ services: KAFKA_REST_HOST_NAME: kafka-rest-proxy KAFKA_REST_BOOTSTRAP_SERVERS: PLAINTEXT://kafka1:19092 depends_on: - - zoo1 - - kafka1 + - zoo + - kafka kafka-connect: image: confluentinc/cp-kafka-connect:7.2.1 @@ -67,24 +91,6 @@ services: #volumes: # - ./connectors:/etc/kafka-connect/jars/ depends_on: - - zoo1 - - kafka1 + - zoo + - kafka - kafka-rest-proxy - - akhq: - image: tchiotludo/akhq:0.21.0 - environment: - AKHQ_CONFIGURATION: | - akhq: - connections: - docker-kafka-server: - properties: - bootstrap.servers: "kafka1:19092" - connect: - - name: "kafka-connect" - url: "http://kafka-connect:8083" - ports: - - "8084:8080" - depends_on: - - kafka1 - - kafka-connect diff --git a/src/integrationTest/kotlin/dev/dnpm/etl/processor/EtlProcessorApplicationTests.kt b/src/integrationTest/kotlin/dev/dnpm/etl/processor/EtlProcessorApplicationTests.kt index 8984e60..7e48e62 100644 --- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/EtlProcessorApplicationTests.kt +++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/EtlProcessorApplicationTests.kt @@ -50,7 +50,8 @@ import org.testcontainers.junit.jupiter.Testcontainers @TestPropertySource( properties = [ "app.rest.uri=http://example.com", - "app.pseudonymize.generator=buildin" + "app.pseudonymize.generator=buildin", + "app.consent.service=none" ] ) class EtlProcessorApplicationTests : AbstractTestcontainerTest() { @@ -67,6 +68,7 @@ class EtlProcessorApplicationTests : AbstractTestcontainerTest() { @TestPropertySource( properties = [ "app.pseudonymize.generator=buildin", + "app.consent.service=none", "app.transformations[0].path=diagnoses[*].icd10.version", "app.transformations[0].from=2013", "app.transformations[0].to=2014", diff --git a/src/integrationTest/kotlin/dev/dnpm/etl/processor/config/AppConfigurationTest.kt b/src/integrationTest/kotlin/dev/dnpm/etl/processor/config/AppConfigurationTest.kt index 39a0997..9db509c 100644 --- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/config/AppConfigurationTest.kt +++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/config/AppConfigurationTest.kt @@ -20,6 +20,8 @@ package dev.dnpm.etl.processor.config import com.fasterxml.jackson.databind.ObjectMapper +import dev.dnpm.etl.processor.consent.ConsentByMtbFile +import dev.dnpm.etl.processor.consent.GicsConsentService import dev.dnpm.etl.processor.input.KafkaInputListener import dev.dnpm.etl.processor.monitoring.RequestRepository import dev.dnpm.etl.processor.output.KafkaMtbFileSender @@ -276,4 +278,44 @@ class AppConfigurationTest { } -} \ No newline at end of file + @Nested + @TestPropertySource( + properties = [ + "app.consent.service=GICS" + ] + ) + inner class AppConfigurationConsentGicsTest(private val context: ApplicationContext) { + + @Test + fun shouldUseConfiguredGenerator() { + assertThat(context.getBean(GicsConsentService::class.java)).isNotNull + } + + } + + @Nested + @TestPropertySource( + properties = [ + "app.consent.gics.enabled=true" + ] + ) + inner class AppConfigurationConsentGicsEnabledTest(private val context: ApplicationContext) { + + @Test + fun shouldUseConfiguredGenerator() { + assertThat(context.getBean(GicsConsentService::class.java)).isNotNull + } + + } + + @Nested + inner class AppConfigurationConsentBuildinTest(private val context: ApplicationContext) { + + @Test + fun shouldUseConfiguredGenerator() { + assertThat(context.getBean(ConsentByMtbFile::class.java)).isNotNull + } + + } + +} diff --git a/src/integrationTest/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt b/src/integrationTest/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt index f1b1476..8aa8ba0 100644 --- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt +++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt @@ -23,6 +23,9 @@ import com.fasterxml.jackson.databind.ObjectMapper import de.ukw.ccc.bwhc.dto.* import dev.dnpm.etl.processor.anyValueClass import dev.dnpm.etl.processor.config.AppSecurityConfiguration +import dev.dnpm.etl.processor.consent.ConsentByMtbFile +import dev.dnpm.etl.processor.consent.TtpConsentStatus +import dev.dnpm.etl.processor.consent.IGetConsent import dev.dnpm.etl.processor.security.TokenRepository import dev.dnpm.etl.processor.security.UserRoleRepository import dev.dnpm.etl.processor.services.RequestProcessor @@ -31,10 +34,7 @@ import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.kotlin.any -import org.mockito.kotlin.never -import org.mockito.kotlin.times -import org.mockito.kotlin.verify +import org.mockito.kotlin.* import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest import org.springframework.http.MediaType @@ -54,7 +54,8 @@ import org.springframework.test.web.servlet.post @ContextConfiguration( classes = [ MtbFileRestController::class, - AppSecurityConfiguration::class + AppSecurityConfiguration::class, + ConsentByMtbFile::class, IGetConsent::class ] ) @MockitoBean(types = [TokenRepository::class, RequestProcessor::class]) @@ -63,7 +64,8 @@ import org.springframework.test.web.servlet.post "app.pseudonymize.generator=BUILDIN", "app.security.admin-user=admin", "app.security.admin-password={noop}very-secret", - "app.security.enable-tokens=true" + "app.security.enable-tokens=true", + "app.consent.gics.enabled=false" ] ) class MtbFileRestControllerTest { @@ -141,7 +143,7 @@ class MtbFileRestControllerTest { status { isAccepted() } } - verify(requestProcessor, times(1)).processDeletion(anyValueClass()) + verify(requestProcessor, times(1)).processDeletion(anyValueClass(), eq(TtpConsentStatus.UNKNOWN_CHECK_FILE)) } @Test @@ -152,7 +154,7 @@ class MtbFileRestControllerTest { status { isUnauthorized() } } - verify(requestProcessor, never()).processDeletion(anyValueClass()) + verify(requestProcessor, never()).processDeletion(anyValueClass(), any()) } @Nested @@ -163,7 +165,8 @@ class MtbFileRestControllerTest { "app.security.admin-user=admin", "app.security.admin-password={noop}very-secret", "app.security.enable-tokens=true", - "app.security.enable-oidc=true" + "app.security.enable-oidc=true", + "app.consent.gics.enabled=false" ] ) inner class WithOidcEnabled { diff --git a/src/integrationTest/kotlin/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGeneratorTest.kt b/src/integrationTest/kotlin/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGeneratorTest.kt index 2e539e9..1275239 100644 --- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGeneratorTest.kt +++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGeneratorTest.kt @@ -19,6 +19,7 @@ package dev.dnpm.etl.processor.pseudonym +import dev.dnpm.etl.processor.config.AppFhirConfig import dev.dnpm.etl.processor.config.GPasConfigProperties import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach @@ -42,6 +43,7 @@ class GpasPseudonymGeneratorTest { private lateinit var mockRestServiceServer: MockRestServiceServer private lateinit var generator: GpasPseudonymGenerator private lateinit var restTemplate: RestTemplate + private var appFhirConfig: AppFhirConfig = AppFhirConfig() @BeforeEach fun setup() { @@ -55,7 +57,8 @@ class GpasPseudonymGeneratorTest { this.restTemplate = RestTemplate() this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate) - this.generator = GpasPseudonymGenerator(gPasConfigProperties, retryTemplate, restTemplate) + this.generator = + GpasPseudonymGenerator(gPasConfigProperties, retryTemplate, restTemplate, appFhirConfig) } @Test @@ -64,7 +67,13 @@ class GpasPseudonymGeneratorTest { method(HttpMethod.POST) requestTo("https://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate") }.andRespond { - withStatus(HttpStatus.OK).body(getDummyResponseBody("1234", "test", "test1234ABCDEF567890")) + withStatus(HttpStatus.OK).body( + getDummyResponseBody( + "1234", + "test", + "test1234ABCDEF567890" + ) + ) .createResponse(it) } @@ -90,7 +99,10 @@ class GpasPseudonymGeneratorTest { requestTo("https://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate") }.andRespond { withStatus(HttpStatus.FOUND) - .header(HttpHeaders.LOCATION, "https://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate") + .header( + HttpHeaders.LOCATION, + "https://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate" + ) .createResponse(it) } diff --git a/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/ConfigControllerTest.kt b/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/ConfigControllerTest.kt index 9f3ae62..8e5d38e 100644 --- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/ConfigControllerTest.kt +++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/ConfigControllerTest.kt @@ -22,6 +22,7 @@ package dev.dnpm.etl.processor.web import dev.dnpm.etl.processor.config.AppConfiguration import dev.dnpm.etl.processor.config.AppSecurityConfiguration import dev.dnpm.etl.processor.monitoring.ConnectionCheckResult +import dev.dnpm.etl.processor.monitoring.GIcsConnectionCheckService import dev.dnpm.etl.processor.monitoring.GPasConnectionCheckService import dev.dnpm.etl.processor.monitoring.RestConnectionCheckService import dev.dnpm.etl.processor.output.MtbFileSender @@ -89,7 +90,8 @@ abstract class MockSink : Sinks.Many RequestProcessor::class, TransformationService::class, GPasConnectionCheckService::class, - RestConnectionCheckService::class + RestConnectionCheckService::class, + GIcsConnectionCheckService::class ] ) class ConfigControllerTest { @@ -182,7 +184,13 @@ class ConfigControllerTest { @Test fun testShouldNotSaveTokenWithExstingName() { - whenever(tokenService.addToken(anyString())).thenReturn(Result.failure(RuntimeException("Testfailure"))) + whenever(tokenService.addToken(anyString())).thenReturn( + Result.failure( + RuntimeException( + "Testfailure" + ) + ) + ) mockMvc.post("/configs/tokens") { with(user("admin").roles("ADMIN")) @@ -303,7 +311,10 @@ class ConfigControllerTest { val idCaptor = argumentCaptor() val roleCaptor = argumentCaptor() - verify(userRoleService, times(1)).updateUserRole(idCaptor.capture(), roleCaptor.capture()) + verify(userRoleService, times(1)).updateUserRole( + idCaptor.capture(), + roleCaptor.capture() + ) assertThat(idCaptor.firstValue).isEqualTo(42) assertThat(roleCaptor.firstValue).isEqualTo(Role.ADMIN) @@ -341,23 +352,26 @@ class ConfigControllerTest { @BeforeEach fun setup( - applicationContext: WebApplicationContext, + applicationContext: WebApplicationContext ) { this.webClient = MockMvcWebTestClient .bindToApplicationContext(applicationContext).build() } @Test - fun testShouldRequestSSE() { - val expectedEvent = ConnectionCheckResult.GPasConnectionCheckResult(true, Instant.now(), Instant.now()) + fun testShouldRequestGPasSSE() { + val expectedEvent = + ConnectionCheckResult.GPasConnectionCheckResult(true, Instant.now(), Instant.now()) connectionCheckUpdateProducer.tryEmitNext(expectedEvent) connectionCheckUpdateProducer.emitComplete { _, _ -> true } - val result = webClient.get().uri("http://localhost/configs/events").accept(TEXT_EVENT_STREAM).exchange() - .expectStatus().isOk() - .expectHeader().contentType(TEXT_EVENT_STREAM) - .returnResult(ConnectionCheckResult.GPasConnectionCheckResult::class.java) + val result = + webClient.get().uri("http://localhost/configs/events").accept(TEXT_EVENT_STREAM) + .exchange() + .expectStatus().isOk() + .expectHeader().contentType(TEXT_EVENT_STREAM) + .returnResult(ConnectionCheckResult.GPasConnectionCheckResult::class.java) StepVerifier.create(result.responseBody) .expectNext(expectedEvent) diff --git a/src/main/java/dev/dnpm/etl/processor/consent/ConsentByMtbFile.java b/src/main/java/dev/dnpm/etl/processor/consent/ConsentByMtbFile.java new file mode 100644 index 0000000..f7ce39e --- /dev/null +++ b/src/main/java/dev/dnpm/etl/processor/consent/ConsentByMtbFile.java @@ -0,0 +1,31 @@ +package dev.dnpm.etl.processor.consent; + +import java.util.Date; +import org.hl7.fhir.r4.model.Bundle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ConsentByMtbFile implements IGetConsent { + + private static final Logger log = LoggerFactory.getLogger(ConsentByMtbFile.class); + + public ConsentByMtbFile() { + log.info("ConsentCheckFileBased initialized..."); + } + + @Override + public TtpConsentStatus getTtpBroadConsentStatus(String personIdentifierValue) { + return TtpConsentStatus.UNKNOWN_CHECK_FILE; + } + + /** + * EMPTY METHOD: NOT IMPLEMENTED + * + * @return empty bundle + */ + @Override + public Bundle getConsent(String personIdentifierValue, Date requestDate, + ConsentDomain consentDomain) { + return new Bundle(); + } +} diff --git a/src/main/java/dev/dnpm/etl/processor/consent/ConsentDomain.java b/src/main/java/dev/dnpm/etl/processor/consent/ConsentDomain.java new file mode 100644 index 0000000..6d0b160 --- /dev/null +++ b/src/main/java/dev/dnpm/etl/processor/consent/ConsentDomain.java @@ -0,0 +1,13 @@ +package dev.dnpm.etl.processor.consent; + +public enum ConsentDomain { + /** + * MII Broad consent + */ + BroadConsent, + + /** + * GenomDe Modelvohaben §64e + */ + Modelvorhaben64e +} diff --git a/src/main/java/dev/dnpm/etl/processor/consent/GicsConsentService.java b/src/main/java/dev/dnpm/etl/processor/consent/GicsConsentService.java new file mode 100644 index 0000000..6f3c987 --- /dev/null +++ b/src/main/java/dev/dnpm/etl/processor/consent/GicsConsentService.java @@ -0,0 +1,281 @@ +package dev.dnpm.etl.processor.consent; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.parser.DataFormatException; +import dev.dnpm.etl.processor.config.AppFhirConfig; +import dev.dnpm.etl.processor.config.GIcsConfigProperties; +import java.util.Date; +import org.apache.commons.lang3.StringUtils; +import org.hl7.fhir.r4.model.BooleanType; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.DateType; +import org.hl7.fhir.r4.model.Identifier; +import org.hl7.fhir.r4.model.OperationOutcome; +import org.hl7.fhir.r4.model.Parameters; +import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent; +import org.hl7.fhir.r4.model.StringType; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.retry.TerminatedRetryException; +import org.springframework.retry.support.RetryTemplate; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + + +public class GicsConsentService implements IGetConsent { + + private final Logger log = LoggerFactory.getLogger(GicsConsentService.class); + + public static final String IS_CONSENTED_ENDPOINT = "/$isConsented"; + public static final String IS_POLICY_STATES_FOR_PERSON_ENDPOINT = "/$currentPolicyStatesForPerson"; + private final RetryTemplate retryTemplate; + private final RestTemplate restTemplate; + private final FhirContext fhirContext; + private final HttpHeaders httpHeader; + private final GIcsConfigProperties gIcsConfigProperties; + private String url; + + public GicsConsentService(GIcsConfigProperties gIcsConfigProperties, + RetryTemplate retryTemplate, RestTemplate restTemplate, AppFhirConfig appFhirConfig) { + + this.retryTemplate = retryTemplate; + this.restTemplate = restTemplate; + this.fhirContext = appFhirConfig.fhirContext(); + httpHeader = buildHeader(gIcsConfigProperties.getUsername(), + gIcsConfigProperties.getPassword()); + this.gIcsConfigProperties = gIcsConfigProperties; + log.info("GicsConsentService initialized..."); + } + + public String getGicsUri(String endpoint) { + if (url == null) { + final String gIcsBaseUri = gIcsConfigProperties.getUri(); + if (StringUtils.isBlank(gIcsBaseUri)) { + throw new IllegalArgumentException( + "gICS base URL is empty - should call gICS with false configuration."); + } + url = UriComponentsBuilder.fromUriString(gIcsBaseUri).path(endpoint) + .toUriString(); + } + return url; + } + + @NotNull + private static HttpHeaders buildHeader(String gPasUserName, String gPasPassword) { + var headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_XML); + + if (StringUtils.isBlank(gPasUserName) || StringUtils.isBlank(gPasPassword)) { + return headers; + } + + headers.setBasicAuth(gPasUserName, gPasPassword); + return headers; + } + + protected static Parameters getIsConsentedRequestParam(GIcsConfigProperties configProperties, + String personIdentifierValue) { + var result = new Parameters(); + result.addParameter(new ParametersParameterComponent().setName("personIdentifier").setValue( + new Identifier().setValue(personIdentifierValue) + .setSystem(configProperties.getPersonIdentifierSystem()))); + result.addParameter(new ParametersParameterComponent().setName("domain") + .setValue(new StringType().setValue(configProperties.getBroadConsentDomainName()))); + result.addParameter(new ParametersParameterComponent().setName("policy").setValue( + new Coding().setCode(configProperties.getBroadConsentPolicyCode()) + .setSystem(configProperties.getBroadConsentPolicySystem()))); + + /* + * is mandatory parameter, but we ignore it via additional configuration parameter + * 'ignoreVersionNumber'. + */ + result.addParameter(new ParametersParameterComponent().setName("version") + .setValue(new StringType().setValue("1.1"))); + + /* add config parameter with: + * ignoreVersionNumber -> true ->> Reason is we cannot know which policy version each patient + * has possibly signed or not, therefore we are happy with any version found. + * unknownStateIsConsideredAsDecline -> true + */ + var config = new ParametersParameterComponent().setName("config").addPart( + new ParametersParameterComponent().setName("ignoreVersionNumber") + .setValue(new BooleanType().setValue(true))).addPart( + new ParametersParameterComponent().setName("unknownStateIsConsideredAsDecline") + .setValue(new BooleanType().setValue(false))); + result.addParameter(config); + + return result; + } + + protected String callGicsApi(Parameters parameter, String endpoint) { + var parameterAsXml = fhirContext.newXmlParser().encodeResourceToString(parameter); + + HttpEntity requestEntity = new HttpEntity<>(parameterAsXml, this.httpHeader); + ResponseEntity responseEntity; + try { + var url = getGicsUri(endpoint); + + responseEntity = retryTemplate.execute( + ctx -> restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class)); + } catch (RestClientException e) { + var msg = String.format("Get consents status request failed reason: '%s", + e.getMessage()); + log.error(msg); + return null; + + } catch (TerminatedRetryException terminatedRetryException) { + var msg = String.format( + "Get consents status process has been terminated. termination reason: '%s", + terminatedRetryException.getMessage()); + log.error(msg); + return null; + + } + if (responseEntity.getStatusCode().is2xxSuccessful()) { + return responseEntity.getBody(); + } else { + var msg = String.format( + "Trusted party system reached but request failed! code: '%s' response: '%s'", + responseEntity.getStatusCode(), responseEntity.getBody()); + log.error(msg); + return null; + } + } + + @Override + public TtpConsentStatus getTtpBroadConsentStatus(String personIdentifierValue) { + var parameter = GicsConsentService.getIsConsentedRequestParam(gIcsConfigProperties, + personIdentifierValue); + + var consentStatusResponse = callGicsApi(parameter, + GicsConsentService.IS_CONSENTED_ENDPOINT); + return evaluateConsentResponse(consentStatusResponse); + } + + protected Bundle currentConsentForPersonAndTemplate(String personIdentifierValue, + ConsentDomain targetConsentDomain, Date requestDate) { + + String consentDomain = getConsentDomain(targetConsentDomain); + + var requestParameter = GicsConsentService.buildRequestParameterCurrentPolicyStatesForPerson( + gIcsConfigProperties, personIdentifierValue, requestDate, consentDomain); + + var consentDataSerialized = callGicsApi(requestParameter, + GicsConsentService.IS_POLICY_STATES_FOR_PERSON_ENDPOINT); + + if (consentDataSerialized == null) { + // error occurred - should not process further! + throw new IllegalStateException( + "consent data request failed - stopping processing! - try again or fix other problems first."); + } + var iBaseResource = fhirContext.newJsonParser() + .parseResource(consentDataSerialized); + if (iBaseResource instanceof OperationOutcome) { + // log error - very likely a configuration error + String errorMessage = + "Consent request failed! Check outcome:\n " + consentDataSerialized; + log.error(errorMessage); + throw new IllegalStateException(errorMessage); + } else if (iBaseResource instanceof Bundle) { + return (Bundle) iBaseResource; + } else { + String errorMessage = "Consent request failed! Unexpected response received! -> " + + consentDataSerialized; + log.error(errorMessage); + throw new IllegalStateException(errorMessage); + } + } + + @NotNull + private String getConsentDomain(ConsentDomain targetConsentDomain) { + String consentDomain; + switch (targetConsentDomain) { + case BroadConsent -> consentDomain = gIcsConfigProperties.getBroadConsentDomainName(); + case Modelvorhaben64e -> + consentDomain = gIcsConfigProperties.getGenomDeConsentDomainName(); + default -> throw new IllegalArgumentException( + "target ConsentDomain is missing but must be provided!"); + } + return consentDomain; + } + + protected static Parameters buildRequestParameterCurrentPolicyStatesForPerson( + GIcsConfigProperties gIcsConfigProperties, String personIdentifierValue, Date requestDate, + String targetDomain) { + var requestParameter = new Parameters(); + requestParameter.addParameter(new ParametersParameterComponent().setName("personIdentifier") + .setValue(new Identifier().setValue(personIdentifierValue) + .setSystem(gIcsConfigProperties.getPersonIdentifierSystem()))); + + requestParameter.addParameter(new ParametersParameterComponent().setName("domain") + .setValue(new StringType().setValue(targetDomain))); + + Parameters nestedConfigParameters = new Parameters(); + nestedConfigParameters.addParameter( + new ParametersParameterComponent().setName("idMatchingType").setValue( + new Coding().setSystem( + "https://ths-greifswald.de/fhir/CodeSystem/gics/IdMatchingType") + .setCode("AT_LEAST_ONE"))).addParameter("ignoreVersionNumber", false) + .addParameter("unknownStateIsConsideredAsDecline", false) + .addParameter("requestDate", new DateType().setValue(requestDate)); + + requestParameter.addParameter(new ParametersParameterComponent().setName("config").addPart() + .setResource(nestedConfigParameters)); + + return requestParameter; + } + + private TtpConsentStatus evaluateConsentResponse(String consentStatusResponse) { + if (consentStatusResponse == null) { + return TtpConsentStatus.FAILED_TO_ASK; + } + try { + var response = fhirContext.newJsonParser().parseResource(consentStatusResponse); + + if (response instanceof Parameters responseParameters) { + + var responseValue = responseParameters.getParameter("consented").getValue(); + var isConsented = responseValue.castToBoolean(responseValue); + if (!isConsented.hasValue()) { + return TtpConsentStatus.FAILED_TO_ASK; + } + if (isConsented.booleanValue()) { + return TtpConsentStatus.BROAD_CONSENT_GIVEN; + } else { + return TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED; + } + } else if (response instanceof OperationOutcome outcome) { + log.error("failed to get consent status from ttp. probably configuration error. " + + "outcome: '{}'", fhirContext.newJsonParser().encodeToString(outcome)); + + } + } catch (DataFormatException dfe) { + log.error("failed to parse response to FHIR R4 resource.", dfe); + } + return TtpConsentStatus.FAILED_TO_ASK; + } + + @Override + public Bundle getConsent(String patientId, Date requestDate, ConsentDomain consentDomain) { + switch (consentDomain) { + case BroadConsent -> { + return currentConsentForPersonAndTemplate(patientId, ConsentDomain.BroadConsent, + requestDate); + } + case Modelvorhaben64e -> { + return currentConsentForPersonAndTemplate(patientId, + ConsentDomain.Modelvorhaben64e, requestDate); + } + } + + return new Bundle(); + } +} diff --git a/src/main/java/dev/dnpm/etl/processor/consent/IGetConsent.java b/src/main/java/dev/dnpm/etl/processor/consent/IGetConsent.java new file mode 100644 index 0000000..3482b9a --- /dev/null +++ b/src/main/java/dev/dnpm/etl/processor/consent/IGetConsent.java @@ -0,0 +1,27 @@ +package dev.dnpm.etl.processor.consent; + +import java.util.Date; +import org.hl7.fhir.r4.model.Bundle; + +public interface IGetConsent { + + /** + * Get broad consent status for a patient identifier + * + * @param personIdentifierValue patient identifier used for consent data + * @return status of broad consent + * @apiNote cannot not differ between not asked and rejected + * + */ + TtpConsentStatus getTtpBroadConsentStatus(String personIdentifierValue); + + /** + * Get broad consent policies with respect to a request date + * + * @param personIdentifierValue patient identifier used for consent data + * @param requestDate target date until consent data should be considered + * @return consent policies as bundle;

if empty patient has not been asked, yet.

+ */ + Bundle getConsent(String personIdentifierValue, Date requestDate, ConsentDomain consentDomain); + +} diff --git a/src/main/java/dev/dnpm/etl/processor/consent/TtpConsentStatus.java b/src/main/java/dev/dnpm/etl/processor/consent/TtpConsentStatus.java new file mode 100644 index 0000000..2af1683 --- /dev/null +++ b/src/main/java/dev/dnpm/etl/processor/consent/TtpConsentStatus.java @@ -0,0 +1,38 @@ +package dev.dnpm.etl.processor.consent; + +public enum TtpConsentStatus { + /** + * Valid consent found + */ + BROAD_CONSENT_GIVEN, + /** + * Missing or rejected...actually unknown + */ + BROAD_CONSENT_MISSING_OR_REJECTED, + /** + * No Broad consent policy found + */ + BROAD_CONSENT_MISSING, + /** + * Research policy has been rejected + */ + BROAD_CONSENT_REJECTED, + + GENOM_DE_CONSENT_SEQUENCING_PERMIT, + /** + * No GenomDE consent policy found + */ + GENOM_DE_CONSENT_MISSING, + /** + * GenomDE consent policy found, but has been rejected + */ + GENOM_DE_SEQUENCING_REJECTED, + /** + * Consent status is validate via file property 'consent.status' + */ + UNKNOWN_CHECK_FILE, + /** + * Due technical problems consent status is unknown + */ + FAILED_TO_ASK +} diff --git a/src/main/java/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGenerator.java b/src/main/java/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGenerator.java index 77caa77..a22100b 100644 --- a/src/main/java/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGenerator.java +++ b/src/main/java/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGenerator.java @@ -21,6 +21,7 @@ package dev.dnpm.etl.processor.pseudonym; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.parser.IParser; +import dev.dnpm.etl.processor.config.AppFhirConfig; import dev.dnpm.etl.processor.config.GPasConfigProperties; import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.r4.model.Identifier; @@ -32,11 +33,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.*; import org.springframework.retry.support.RetryTemplate; +import org.springframework.web.client.HttpClientErrorException.BadRequest; +import org.springframework.web.client.HttpClientErrorException.Unauthorized; +import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; public class GpasPseudonymGenerator implements Generator { - private final static FhirContext r4Context = FhirContext.forR4(); + private final FhirContext r4Context; private final String gPasUrl; private final String psnTargetDomain; private final HttpHeaders httpHeader; @@ -45,11 +49,13 @@ public class GpasPseudonymGenerator implements Generator { private final RestTemplate restTemplate; - public GpasPseudonymGenerator(GPasConfigProperties gpasCfg, RetryTemplate retryTemplate, RestTemplate restTemplate) { + public GpasPseudonymGenerator(GPasConfigProperties gpasCfg, RetryTemplate retryTemplate, + RestTemplate restTemplate, AppFhirConfig appFhirConfig) { this.retryTemplate = retryTemplate; this.restTemplate = restTemplate; this.gPasUrl = gpasCfg.getUri(); this.psnTargetDomain = gpasCfg.getTarget(); + this.r4Context = appFhirConfig.fhirContext(); httpHeader = getHttpHeaders(gpasCfg.getUsername(), gpasCfg.getPassword()); log.debug(String.format("%s has been initialized", this.getClass().getName())); @@ -61,7 +67,7 @@ public class GpasPseudonymGenerator implements Generator { var gPasRequestBody = getGpasRequestBody(id); var responseEntity = getGpasPseudonym(gPasRequestBody); var gPasPseudonymResult = (Parameters) r4Context.newJsonParser() - .parseResource(responseEntity.getBody()); + .parseResource(responseEntity.getBody()); return unwrapPseudonym(gPasPseudonymResult); } @@ -75,9 +81,9 @@ public class GpasPseudonymGenerator implements Generator { } final var identifier = (Identifier) parameters.get().getPart().stream() - .filter(a -> a.getName().equals("pseudonym")) - .findFirst() - .orElseGet(ParametersParameterComponent::new).getValue(); + .filter(a -> a.getName().equals("pseudonym")) + .findFirst() + .orElseGet(ParametersParameterComponent::new).getValue(); // pseudonym return sanitizeValue(identifier.getValue()); @@ -97,38 +103,48 @@ public class GpasPseudonymGenerator implements Generator { return psnValue.replaceAll(forbiddenCharsRegex, "_"); } - @NotNull protected ResponseEntity getGpasPseudonym(String gPasRequestBody) { HttpEntity requestEntity = new HttpEntity<>(gPasRequestBody, this.httpHeader); - ResponseEntity responseEntity; try { - responseEntity = retryTemplate.execute( - ctx -> restTemplate.exchange(gPasUrl, HttpMethod.POST, requestEntity, - String.class)); - + ResponseEntity responseEntity = retryTemplate.execute( + ctx -> restTemplate.exchange(gPasUrl, HttpMethod.POST, requestEntity, + String.class)); if (responseEntity.getStatusCode().is2xxSuccessful()) { log.debug("API request succeeded. Response: {}", responseEntity.getStatusCode()); - } else { - log.warn("API request unsuccessful. Response: {}", requestEntity.getBody()); - throw new PseudonymRequestFailed("API request unsuccessful gPas unsuccessful."); + return responseEntity; + } + } catch (RestClientException rce) { + if (rce instanceof BadRequest) { + String msg = "gPas or request configuration is incorrect. Please check both." + + rce.getMessage(); + log.debug( + msg); + throw new PseudonymRequestFailed(msg, rce); + } + if (rce instanceof Unauthorized) { + var msg = "gPas access credentials are invalid check your configuration. msg: '%s".formatted( + rce.getMessage()); + log.error(msg); + throw new PseudonymRequestFailed(msg, rce); } - - return responseEntity; } catch (Exception unexpected) { throw new PseudonymRequestFailed( - "API request due unexpected error unsuccessful gPas unsuccessful.", unexpected); + "API request due unexpected error unsuccessful gPas unsuccessful.", unexpected); } + throw new PseudonymRequestFailed( + "API request due unexpected error unsuccessful gPas unsuccessful."); + } protected String getGpasRequestBody(String id) { var requestParameters = new Parameters(); requestParameters.addParameter().setName("target") - .setValue(new StringType().setValue(psnTargetDomain)); + .setValue(new StringType().setValue(psnTargetDomain)); requestParameters.addParameter().setName("original") - .setValue(new StringType().setValue(id)); + .setValue(new StringType().setValue(id)); final IParser iParser = r4Context.newJsonParser(); return iParser.encodeResourceToString(requestParameters); } diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt index 331c8b5..a2ea032 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt @@ -27,7 +27,8 @@ data class AppConfigProperties( var bwhcUri: String?, var transformations: List = listOf(), var maxRetryAttempts: Int = 3, - var duplicationDetection: Boolean = true + var duplicationDetection: Boolean = true, + var genomDeTestSubmission: Boolean = true ) { companion object { const val NAME = "app" @@ -56,6 +57,72 @@ data class GPasConfigProperties( } } +@ConfigurationProperties(ConsentConfigProperties.NAME) +data class ConsentConfigProperties( + var service: ConsentService = ConsentService.NONE +) { + companion object { + const val NAME = "app.consent" + } +} + +@ConfigurationProperties(GIcsConfigProperties.NAME) +data class GIcsConfigProperties( + /** + * Base URL to gICS System + * + */ + val uri: String?, + val username: String?, + val password: String?, + + /** + * gICS specific system + * **/ + val personIdentifierSystem: String = + "https://ths-greifswald.de/fhir/gics/identifiers/Patienten-ID", + + /** + * Domain of broad consent resources + **/ + val broadConsentDomainName: String = "MII", + + /** + * Domain of Modelvorhaben 64e consent resources + **/ + val genomDeConsentDomainName: String = "GenomDE_MV", + + /** + * Value to expect in case of positiv consent + */ + val broadConsentPolicyCode: String = "2.16.840.1.113883.3.1937.777.24.5.3.6", + + /** + * Consent Policy which should be used for consent check + */ + val broadConsentPolicySystem: String = "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + + /** + * Value to expect in case of positiv consent + */ + val genomeDePolicyCode: String = "sequencing", + + /** + * Consent Policy which should be used for consent check + */ + val genomeDePolicySystem: String = "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV", + + /** + * Consent version (fixed version) + * + */ + val genomeDeConsentVersion: String = "2.0" +) { + companion object { + const val NAME = "app.consent.gics" + } +} + @ConfigurationProperties(RestTargetProperties.NAME) data class RestTargetProperties( val uri: String?, @@ -99,8 +166,13 @@ enum class PseudonymGenerator { GPAS } +enum class ConsentService { + NONE, + GICS +} + data class TransformationProperties( val path: String, val from: String, val to: String -) \ No newline at end of file +) diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt index c8f3fba..8f90947 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt @@ -20,24 +20,28 @@ package dev.dnpm.etl.processor.config import com.fasterxml.jackson.databind.ObjectMapper -import dev.dnpm.etl.processor.monitoring.ConnectionCheckResult -import dev.dnpm.etl.processor.monitoring.ConnectionCheckService -import dev.dnpm.etl.processor.monitoring.GPasConnectionCheckService -import dev.dnpm.etl.processor.monitoring.ReportService +import dev.dnpm.etl.processor.consent.ConsentByMtbFile +import dev.dnpm.etl.processor.consent.GicsConsentService +import dev.dnpm.etl.processor.consent.IGetConsent +import dev.dnpm.etl.processor.monitoring.* import dev.dnpm.etl.processor.pseudonym.AnonymizingGenerator import dev.dnpm.etl.processor.pseudonym.Generator import dev.dnpm.etl.processor.pseudonym.GpasPseudonymGenerator import dev.dnpm.etl.processor.pseudonym.PseudonymizeService import dev.dnpm.etl.processor.security.TokenRepository import dev.dnpm.etl.processor.security.TokenService +import dev.dnpm.etl.processor.services.ConsentProcessor import dev.dnpm.etl.processor.services.Transformation import dev.dnpm.etl.processor.services.TransformationService import org.slf4j.LoggerFactory +import org.springframework.boot.autoconfigure.condition.AnyNestedCondition import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Conditional import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.ConfigurationCondition import org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration import org.springframework.retry.RetryCallback import org.springframework.retry.RetryContext @@ -60,7 +64,9 @@ import kotlin.time.toJavaDuration value = [ AppConfigProperties::class, PseudonymizeConfigProperties::class, - GPasConfigProperties::class + GPasConfigProperties::class, + ConsentConfigProperties::class, + GIcsConfigProperties::class ] ) @EnableScheduling @@ -73,13 +79,27 @@ class AppConfiguration { return RestTemplate() } - @ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "GPAS") @Bean - fun gpasPseudonymGenerator(configProperties: GPasConfigProperties, retryTemplate: RetryTemplate, restTemplate: RestTemplate): Generator { - return GpasPseudonymGenerator(configProperties, retryTemplate, restTemplate) + fun appFhirConfig(): AppFhirConfig { + return AppFhirConfig() } - @ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "BUILDIN", matchIfMissing = true) + @ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "GPAS") + @Bean + fun gpasPseudonymGenerator( + configProperties: GPasConfigProperties, + retryTemplate: RetryTemplate, + restTemplate: RestTemplate, + appFhirConfig: AppFhirConfig + ): Generator { + return GpasPseudonymGenerator(configProperties, retryTemplate, restTemplate, appFhirConfig) + } + + @ConditionalOnProperty( + value = ["app.pseudonymize.generator"], + havingValue = "BUILDIN", + matchIfMissing = true + ) @Bean fun buildinPseudonymGenerator(): Generator { return AnonymizingGenerator() @@ -94,17 +114,21 @@ class AppConfiguration { } @Bean - fun reportService(objectMapper: ObjectMapper): ReportService { - return ReportService(objectMapper) + fun reportService(): ReportService { + return ReportService(getObjectMapper()) + } + + @Bean + fun getObjectMapper(): ObjectMapper { + return JacksonConfig().objectMapper() } @Bean fun transformationService( - objectMapper: ObjectMapper, configProperties: AppConfigProperties ): TransformationService { logger.info("Apply ${configProperties.transformations.size} transformation rules") - return TransformationService(objectMapper, configProperties.transformations.map { + return TransformationService(getObjectMapper(), configProperties.transformations.map { Transformation.of(it.path) from it.from to it.to }) } @@ -123,7 +147,11 @@ class AppConfiguration { callback: RetryCallback, throwable: Throwable ) { - logger.warn("Error occured: {}. Retrying {}", throwable.message, context.retryCount) + logger.warn( + "Error occured: {}. Retrying {}", + throwable.message, + context.retryCount + ) } }) .build() @@ -131,7 +159,11 @@ class AppConfiguration { @ConditionalOnProperty(value = ["app.security.enable-tokens"], havingValue = "true") @Bean - fun tokenService(userDetailsManager: InMemoryUserDetailsManager, passwordEncoder: PasswordEncoder, tokenRepository: TokenRepository): TokenService { + fun tokenService( + userDetailsManager: InMemoryUserDetailsManager, + passwordEncoder: PasswordEncoder, + tokenRepository: TokenRepository + ): TokenService { return TokenService(userDetailsManager, passwordEncoder, tokenRepository) } @@ -152,7 +184,11 @@ class AppConfiguration { gPasConfigProperties: GPasConfigProperties, connectionCheckUpdateProducer: Sinks.Many ): ConnectionCheckService { - return GPasConnectionCheckService(restTemplate, gPasConfigProperties, connectionCheckUpdateProducer) + return GPasConnectionCheckService( + restTemplate, + gPasConfigProperties, + connectionCheckUpdateProducer + ) } @ConditionalOnProperty(value = ["app.pseudonymizer"], havingValue = "GPAS") @@ -163,12 +199,85 @@ class AppConfiguration { gPasConfigProperties: GPasConfigProperties, connectionCheckUpdateProducer: Sinks.Many ): ConnectionCheckService { - return GPasConnectionCheckService(restTemplate, gPasConfigProperties, connectionCheckUpdateProducer) + return GPasConnectionCheckService( + restTemplate, + gPasConfigProperties, + connectionCheckUpdateProducer + ) } @Bean fun jdbcConfiguration(): AbstractJdbcConfiguration { return AppJdbcConfiguration() } + + @Conditional(GicsEnabledCondition::class) + @Bean + fun gicsConsentService( + gIcsConfigProperties: GIcsConfigProperties, + retryTemplate: RetryTemplate, + restTemplate: RestTemplate, + appFhirConfig: AppFhirConfig + ): IGetConsent { + return GicsConsentService( + gIcsConfigProperties, + retryTemplate, + restTemplate, + appFhirConfig + ) + } + + @Conditional(GicsEnabledCondition::class) + @Bean + fun consentProcessor( + configProperties: AppConfigProperties, + gIcsConfigProperties: GIcsConfigProperties, + getObjectMapper: ObjectMapper, + appFhirConfig: AppFhirConfig, + gicsConsentService: IGetConsent + ): ConsentProcessor { + return ConsentProcessor( + configProperties, + gIcsConfigProperties, + getObjectMapper, + appFhirConfig.fhirContext(), + gicsConsentService + ) + } + + @Conditional(GicsEnabledCondition::class) + @Bean + fun gIcsConnectionCheckService( + restTemplate: RestTemplate, + gIcsConfigProperties: GIcsConfigProperties, + connectionCheckUpdateProducer: Sinks.Many + ): ConnectionCheckService { + return GIcsConnectionCheckService( + restTemplate, + gIcsConfigProperties, + connectionCheckUpdateProducer + ) + } + + @Bean + @ConditionalOnMissingBean + fun iGetConsentService(): IGetConsent { + return ConsentByMtbFile() + } + } +class GicsEnabledCondition : + AnyNestedCondition(ConfigurationCondition.ConfigurationPhase.REGISTER_BEAN) { + + @ConditionalOnProperty(name = ["app.consent.service"], havingValue = "gics") + class OnGicsServiceSelected { + // Just for Condition + } + + @ConditionalOnProperty(name = ["app.consent.gics.enabled"], havingValue = "true") + class OnGicsEnabled { + // Just for Condition + } + +} diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/AppFhirConfig.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/AppFhirConfig.kt new file mode 100644 index 0000000..2b5ff8f --- /dev/null +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppFhirConfig.kt @@ -0,0 +1,16 @@ +package dev.dnpm.etl.processor.config + +import ca.uhn.fhir.context.FhirContext +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + + +@Configuration +class AppFhirConfig { + private val fhirCtx: FhirContext = FhirContext.forR4() + + @Bean + fun fhirContext(): FhirContext { + return fhirCtx + } +} \ No newline at end of file diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/ConsentResourceDeserializer.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/ConsentResourceDeserializer.kt new file mode 100644 index 0000000..5469b1b --- /dev/null +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/ConsentResourceDeserializer.kt @@ -0,0 +1,18 @@ +package dev.dnpm.etl.processor.config + +import com.fasterxml.jackson.core.JsonParser + +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JsonDeserializer +import com.fasterxml.jackson.databind.JsonNode +import org.hl7.fhir.r4.model.Consent + +class ConsentResourceDeserializer : JsonDeserializer() { + override fun deserialize(p: JsonParser?, ctxt: DeserializationContext?): Consent { + + val jsonNode = p?.readValueAsTree() + val json = jsonNode?.toString() + + return JacksonConfig.fhirContext().newJsonParser().parseResource(json) as Consent + } +} \ No newline at end of file diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/ConsentResourceSerializer.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/ConsentResourceSerializer.kt new file mode 100644 index 0000000..812ce44 --- /dev/null +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/ConsentResourceSerializer.kt @@ -0,0 +1,15 @@ +package dev.dnpm.etl.processor.config + +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.databind.JsonSerializer +import com.fasterxml.jackson.databind.SerializerProvider +import org.hl7.fhir.r4.model.Consent + +class ConsentResourceSerializer : JsonSerializer() { + override fun serialize( + value: Consent, gen: JsonGenerator, serializers: SerializerProvider + ) { + val json = JacksonConfig.fhirContext().newJsonParser().encodeResourceToString(value) + gen.writeRawValue(json) + } +} \ No newline at end of file diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/FhirResourceModule.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/FhirResourceModule.kt new file mode 100644 index 0000000..2ae0dd3 --- /dev/null +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/FhirResourceModule.kt @@ -0,0 +1,12 @@ +package dev.dnpm.etl.processor.config + + +import com.fasterxml.jackson.databind.module.SimpleModule +import org.hl7.fhir.r4.model.Consent + +class FhirResourceModule : SimpleModule() { + init { + addSerializer(Consent::class.java, ConsentResourceSerializer()) + addDeserializer(Consent::class.java, ConsentResourceDeserializer()) + } +} \ No newline at end of file diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/JacksonConfig.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/JacksonConfig.kt new file mode 100644 index 0000000..fb03d66 --- /dev/null +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/JacksonConfig.kt @@ -0,0 +1,27 @@ +package dev.dnpm.etl.processor.config + +import ca.uhn.fhir.context.FhirContext +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule + +@Configuration +class JacksonConfig { + + companion object { + var fhirContext: FhirContext = FhirContext.forR4() + + @JvmStatic + fun fhirContext(): FhirContext { + return fhirContext + } + } + + @Bean + fun objectMapper(): ObjectMapper = ObjectMapper().registerModule(FhirResourceModule()) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS).registerModule( + JavaTimeModule() + ) +} diff --git a/src/main/kotlin/dev/dnpm/etl/processor/input/KafkaInputListener.kt b/src/main/kotlin/dev/dnpm/etl/processor/input/KafkaInputListener.kt index e797390..415a68f 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/input/KafkaInputListener.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/input/KafkaInputListener.kt @@ -25,6 +25,7 @@ import de.ukw.ccc.bwhc.dto.MtbFile import dev.dnpm.etl.processor.CustomMediaType import dev.dnpm.etl.processor.PatientId import dev.dnpm.etl.processor.RequestId +import dev.dnpm.etl.processor.consent.TtpConsentStatus import dev.dnpm.etl.processor.services.RequestProcessor import org.apache.kafka.clients.consumer.ConsumerRecord import org.slf4j.LoggerFactory @@ -76,9 +77,13 @@ class KafkaInputListener( } else { logger.debug("Accepted MTB File and process deletion") if (requestId.isBlank()) { - requestProcessor.processDeletion(patientId) + requestProcessor.processDeletion(patientId, TtpConsentStatus.UNKNOWN_CHECK_FILE) } else { - requestProcessor.processDeletion(patientId, requestId) + requestProcessor.processDeletion( + patientId, + requestId, + TtpConsentStatus.UNKNOWN_CHECK_FILE + ) } } } diff --git a/src/main/kotlin/dev/dnpm/etl/processor/input/MtbFileRestController.kt b/src/main/kotlin/dev/dnpm/etl/processor/input/MtbFileRestController.kt index e67a380..44c74e3 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/input/MtbFileRestController.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/input/MtbFileRestController.kt @@ -23,6 +23,8 @@ import de.ukw.ccc.bwhc.dto.Consent import de.ukw.ccc.bwhc.dto.MtbFile import dev.dnpm.etl.processor.CustomMediaType import dev.dnpm.etl.processor.PatientId +import dev.dnpm.etl.processor.consent.IGetConsent +import dev.dnpm.etl.processor.consent.TtpConsentStatus import dev.dnpm.etl.processor.services.RequestProcessor import dev.pcvolkmer.mv64e.mtb.Mtb import org.slf4j.LoggerFactory @@ -33,7 +35,7 @@ import org.springframework.web.bind.annotation.* @RestController @RequestMapping(path = ["mtbfile", "mtb"]) class MtbFileRestController( - private val requestProcessor: RequestProcessor, + private val requestProcessor: RequestProcessor, private val iGetConsent: IGetConsent ) { private val logger = LoggerFactory.getLogger(MtbFileRestController::class.java) @@ -43,20 +45,39 @@ class MtbFileRestController( return ResponseEntity.ok("Test") } - @PostMapping( consumes = [ MediaType.APPLICATION_JSON_VALUE ] ) + @PostMapping(consumes = [MediaType.APPLICATION_JSON_VALUE]) fun mtbFile(@RequestBody mtbFile: MtbFile): ResponseEntity { - if (mtbFile.consent.status == Consent.Status.ACTIVE) { + val consentStatusBooleanPair = checkConsentStatus(mtbFile) + val ttpConsentStatus = consentStatusBooleanPair.first + val isConsentOK = consentStatusBooleanPair.second + if (isConsentOK) { logger.debug("Accepted MTB File (bwHC V1) for processing") requestProcessor.processMtbFile(mtbFile) } else { + logger.debug("Accepted MTB File (bwHC V1) and process deletion") val patientId = PatientId(mtbFile.patient.id) - requestProcessor.processDeletion(patientId) + requestProcessor.processDeletion(patientId, ttpConsentStatus) } return ResponseEntity.accepted().build() } - @PostMapping( consumes = [ CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE] ) + private fun checkConsentStatus(mtbFile: MtbFile): Pair { + var ttpConsentStatus = iGetConsent.getTtpBroadConsentStatus(mtbFile.patient.id) + + val isConsentOK = + (ttpConsentStatus.equals(TtpConsentStatus.UNKNOWN_CHECK_FILE) && mtbFile.consent.status == Consent.Status.ACTIVE) || + ttpConsentStatus.equals( + TtpConsentStatus.BROAD_CONSENT_GIVEN + ) + if (ttpConsentStatus.equals(TtpConsentStatus.UNKNOWN_CHECK_FILE) && mtbFile.consent.status == Consent.Status.REJECTED) { + // in case ttp check is disabled - we propagate rejected status anyway + ttpConsentStatus = TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED + } + return Pair(ttpConsentStatus, isConsentOK) + } + + @PostMapping(consumes = [CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE]) fun mtbFile(@RequestBody mtbFile: Mtb): ResponseEntity { logger.debug("Accepted MTB File (DNPM V2) for processing") requestProcessor.processMtbFile(mtbFile) @@ -66,7 +87,7 @@ class MtbFileRestController( @DeleteMapping(path = ["{patientId}"]) fun deleteData(@PathVariable patientId: String): ResponseEntity { logger.debug("Accepted patient ID to process deletion") - requestProcessor.processDeletion(PatientId(patientId)) + requestProcessor.processDeletion(PatientId(patientId), TtpConsentStatus.UNKNOWN_CHECK_FILE) return ResponseEntity.accepted().build() } diff --git a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/ConnectionCheckService.kt b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/ConnectionCheckService.kt index b845e21..fe02b69 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/ConnectionCheckService.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/ConnectionCheckService.kt @@ -20,6 +20,7 @@ package dev.dnpm.etl.processor.monitoring +import dev.dnpm.etl.processor.config.GIcsConfigProperties import dev.dnpm.etl.processor.config.GPasConfigProperties import dev.dnpm.etl.processor.config.RestTargetProperties import jakarta.annotation.PostConstruct @@ -68,6 +69,12 @@ sealed class ConnectionCheckResult { override val timestamp: Instant, override val lastChange: Instant ) : ConnectionCheckResult() + + data class GIcsConnectionCheckResult( + override val available: Boolean, + override val timestamp: Instant, + override val lastChange: Instant + ) : ConnectionCheckResult() } class KafkaConnectionCheckService( @@ -207,4 +214,57 @@ class GPasConnectionCheckService( override fun connectionAvailable(): ConnectionCheckResult.GPasConnectionCheckResult { return this.result } +} + +class GIcsConnectionCheckService( + private val restTemplate: RestTemplate, + private val gIcsConfigProperties: GIcsConfigProperties, + @Qualifier("connectionCheckUpdateProducer") + private val connectionCheckUpdateProducer: Sinks.Many +) : ConnectionCheckService { + + private var result = ConnectionCheckResult.GIcsConnectionCheckResult(false, Instant.now(), Instant.now()) + + @PostConstruct + @Scheduled(cron = "0 * * * * *") + fun check() { + result = try { + + val uri = UriComponentsBuilder.fromUriString( + gIcsConfigProperties.uri.toString()).path("/metadata").build().toUri() + + val headers = HttpHeaders() + headers.contentType = MediaType.APPLICATION_JSON + if (!gIcsConfigProperties.username.isNullOrBlank() && !gIcsConfigProperties.password.isNullOrBlank()) { + headers.setBasicAuth(gIcsConfigProperties.username, gIcsConfigProperties.password) + } + + val available = restTemplate.exchange( + uri, + HttpMethod.GET, + HttpEntity(headers), + Void::class.java + ).statusCode == HttpStatus.OK + + ConnectionCheckResult.GIcsConnectionCheckResult( + available, + Instant.now(), + if (result.available == available) { result.lastChange } else { Instant.now() } + ) + } catch (_: Exception) { + ConnectionCheckResult.GIcsConnectionCheckResult( + false, + Instant.now(), + if (!result.available) { result.lastChange } else { Instant.now() } + ) + } + connectionCheckUpdateProducer.emitNext( + result, + Sinks.EmitFailureHandler.FAIL_FAST + ) + } + + override fun connectionAvailable(): ConnectionCheckResult.GIcsConnectionCheckResult { + return this.result + } } \ No newline at end of file diff --git a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/RequestStatus.kt b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/RequestStatus.kt index 8c19e86..0c8adb1 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/RequestStatus.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/RequestStatus.kt @@ -24,5 +24,6 @@ enum class RequestStatus(val value: String) { WARNING("warning"), ERROR("error"), UNKNOWN("unknown"), - DUPLICATION("duplication") + DUPLICATION("duplication"), + NO_CONSENT("no-consent") } \ No newline at end of file diff --git a/src/main/kotlin/dev/dnpm/etl/processor/output/KafkaMtbFileSender.kt b/src/main/kotlin/dev/dnpm/etl/processor/output/KafkaMtbFileSender.kt index c00b2fd..1f2743e 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/output/KafkaMtbFileSender.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/output/KafkaMtbFileSender.kt @@ -44,13 +44,20 @@ class KafkaMtbFileSender( return try { return retryTemplate.execute { val record = - ProducerRecord(kafkaProperties.outputTopic, key(request), objectMapper.writeValueAsString(request)) + ProducerRecord( + kafkaProperties.outputTopic, + key(request), + objectMapper.writeValueAsString(request) + ) when (request) { is BwhcV1MtbFileRequest -> record.headers() .add("contentType", MediaType.APPLICATION_JSON_VALUE.toByteArray()) is DnpmV2MtbFileRequest -> record.headers() - .add("contentType", CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE.toByteArray()) + .add( + "contentType", + CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE.toByteArray() + ) } val result = kafkaTemplate.send(record) @@ -84,7 +91,12 @@ class KafkaMtbFileSender( kafkaProperties.outputTopic, key(request), // Always use old BwhcV1FileRequest with Consent REJECT - objectMapper.writeValueAsString(BwhcV1MtbFileRequest(request.requestId, dummyMtbFile)) + objectMapper.writeValueAsString( + BwhcV1MtbFileRequest( + request.requestId, + dummyMtbFile + ) + ) ) val result = kafkaTemplate.send(record) diff --git a/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/extensions.kt b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/extensions.kt index 8d5a2cc..77f3399 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/extensions.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/extensions.kt @@ -21,7 +21,9 @@ package dev.dnpm.etl.processor.pseudonym import de.ukw.ccc.bwhc.dto.MtbFile import dev.dnpm.etl.processor.PatientId +import dev.pcvolkmer.mv64e.mtb.ModelProjectConsent import dev.pcvolkmer.mv64e.mtb.Mtb +import dev.pcvolkmer.mv64e.mtb.MvhMetadata import org.apache.commons.codec.digest.DigestUtils /** Replaces patient ID with generated patient pseudonym @@ -289,6 +291,16 @@ infix fun Mtb.pseudonymizeWith(pseudonymizeService: PseudonymizeService) { this.followUps?.forEach { it.patient.id = patientPseudonym } + + this.metadata?.researchConsents?.forEach { it -> + val entry = it ?: return@forEach + if (entry.contains("patient")) { + // here we expect only a patient reference any other data like display + // need to be removed, since may contain unsecure data + entry.remove("patient") + entry["patient"] = mapOf("reference" to "Patient/$patientPseudonym") + } + } } /** @@ -317,3 +329,23 @@ infix fun Mtb.anonymizeContentWith(pseudonymizeService: PseudonymizeService) { // TODO all other properties } + +fun Mtb.ensureMetaDataIsInitialized() { + // init metadata if necessary + if (this.metadata == null) { + val mvhMetadata = MvhMetadata.builder().build() + this.metadata = mvhMetadata + } + if (this.metadata.researchConsents == null) { + this.metadata.researchConsents = mutableListOf() + } + if (this.metadata.modelProjectConsent == null) { + this.metadata.modelProjectConsent = ModelProjectConsent() + this.metadata.modelProjectConsent.provisions = mutableListOf() + } else + if (this.metadata.modelProjectConsent.provisions != null) { + // make sure list can be changed + this.metadata.modelProjectConsent.provisions = + this.metadata.modelProjectConsent.provisions.toMutableList() + } +} diff --git a/src/main/kotlin/dev/dnpm/etl/processor/services/ConsentProcessor.kt b/src/main/kotlin/dev/dnpm/etl/processor/services/ConsentProcessor.kt new file mode 100644 index 0000000..3841641 --- /dev/null +++ b/src/main/kotlin/dev/dnpm/etl/processor/services/ConsentProcessor.kt @@ -0,0 +1,282 @@ +package dev.dnpm.etl.processor.services + +import ca.uhn.fhir.context.FhirContext +import com.fasterxml.jackson.core.JsonProcessingException +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.databind.ObjectMapper +import dev.dnpm.etl.processor.config.AppConfigProperties +import dev.dnpm.etl.processor.config.GIcsConfigProperties +import dev.dnpm.etl.processor.consent.ConsentByMtbFile +import dev.dnpm.etl.processor.consent.ConsentDomain +import dev.dnpm.etl.processor.consent.IGetConsent +import dev.dnpm.etl.processor.pseudonym.ensureMetaDataIsInitialized +import dev.pcvolkmer.mv64e.mtb.* +import org.apache.commons.lang3.NotImplementedException +import org.hl7.fhir.r4.model.* +import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent +import org.hl7.fhir.r4.model.Coding +import org.hl7.fhir.r4.model.Consent.ConsentState +import org.hl7.fhir.r4.model.Consent.ProvisionComponent +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.io.IOException +import java.time.Clock +import java.time.Instant +import java.util.* + +@Service +class ConsentProcessor( + private val appConfigProperties: AppConfigProperties, + private val gIcsConfigProperties: GIcsConfigProperties, + private val objectMapper: ObjectMapper, + private val fhirContext: FhirContext, + private val consentService: IGetConsent +) { + private var logger: Logger = LoggerFactory.getLogger("ConsentProcessor") + + /** + * In case an instance of {@link ICheckConsent} is active, consent will be embedded and checked. + * + * Logik: + * * true IF consent check is disabled. + * * true IF broad consent (BC) has been given. + * * true BC has been asked AND declined but genomDe consent has been consented. + * * ELSE false is returned. + * + * @param mtbFile File v2 (will be enriched with consent data) + * @return true if consent is given + * + */ + fun consentGatedCheckAndTryEmbedding(mtbFile: Mtb): Boolean { + if (consentService is ConsentByMtbFile) { + // consent check is disabled + return true + } + + mtbFile.ensureMetaDataIsInitialized() + + val personIdentifierValue = mtbFile.patient.id + val requestDate = Date.from(Instant.now(Clock.systemUTC())) + + // 1. Broad consent Entry exists? + // 1.1. -> yes and research consent is given -> send mtb file + // 1.2. -> no -> return status error - consent has not been asked + // 2. -> Broad consent found but rejected -> is GenomDe consent provision 'sequencing' given? + // 2.1 -> yes -> send mtb file + // 2.2 -> no -> warn/info no consent given + + /* + * broad consent + */ + val broadConsent = consentService.getConsent( + personIdentifierValue, requestDate, ConsentDomain.BroadConsent + ) + val broadConsentHasBeenAsked = !broadConsent.entry.isEmpty() + + // fast exit - if patient has not been asked, we can skip and exit + if (!broadConsentHasBeenAsked) return false + + val genomeDeConsent = consentService.getConsent( + personIdentifierValue, requestDate, ConsentDomain.Modelvorhaben64e + ) + + addGenomeDbProvisions(mtbFile, genomeDeConsent) + + if (!genomeDeConsent.entry.isEmpty()) setGenomDeSubmissionType(mtbFile) + + embedBroadConsentResources(mtbFile, broadConsent) + + val broadConsentStatus = getProvisionTypeByPolicyCode( + broadConsent, requestDate, ConsentDomain.BroadConsent + ) + + val genomDeSequencingStatus = getProvisionTypeByPolicyCode( + genomeDeConsent, requestDate, ConsentDomain.Modelvorhaben64e + ) + + if (Consent.ConsentProvisionType.NULL == broadConsentStatus) { + // bc not asked + return false + } + if (Consent.ConsentProvisionType.PERMIT == broadConsentStatus || Consent.ConsentProvisionType.PERMIT == genomDeSequencingStatus) return true + + return false + } + + fun embedBroadConsentResources(mtbFile: Mtb, broadConsent: Bundle) { + for (entry in broadConsent.getEntry()) { + val resource = entry.getResource() + if (resource is Consent) { + // since jackson convertValue does not work here, + // we need another step to back to string, before we convert to object map + val asJsonString = fhirContext.newJsonParser().encodeResourceToString(resource) + try { + val mapOfJson: HashMap? = + objectMapper.readValue?>( + asJsonString, object : TypeReference?>() {}) + mtbFile.metadata.researchConsents.add(mapOfJson) + } catch (e: JsonProcessingException) { + throw RuntimeException(e) + } + } + } + } + + fun addGenomeDbProvisions(mtbFile: Mtb, consentGnomeDe: Bundle) { + for (entry in consentGnomeDe.getEntry()) { + val resource = entry.getResource() + if (resource !is Consent) { + continue + } + + // We expect only one provision in collection, therefore get first or none + val provisions = resource.getProvision().getProvision() + if (provisions.isEmpty()) { + continue + } + + val provisionComponent: ProvisionComponent = provisions.first() + + var provisionCode: String? = null + if (provisionComponent.getCode() != null && !provisionComponent.getCode().isEmpty()) { + val codableConcept: CodeableConcept = provisionComponent.getCode().first() + if (codableConcept.getCoding() != null && !codableConcept.getCoding().isEmpty()) { + provisionCode = codableConcept.getCoding().first().getCode() + } + } + + if (provisionCode != null) { + try { + val modelProjectConsentPurpose = + ModelProjectConsentPurpose.forValue(provisionCode) + + if (ModelProjectConsentPurpose.SEQUENCING == modelProjectConsentPurpose) { + // CONVENTION: wrapping date is date of SEQUENCING consent + mtbFile.metadata.modelProjectConsent.date = resource.getDateTime() + } + + val provision = Provision.builder() + .type(ConsentProvision.valueOf(provisionComponent.getType().name)) + .date(provisionComponent.getPeriod().getStart()) + .purpose(modelProjectConsentPurpose).build() + + mtbFile.metadata.modelProjectConsent.provisions.add(provision) + } catch (ioe: IOException) { + logger.error( + "Provision code '$provisionCode' is unknown and cannot be mapped.", + ioe.toString() + ) + } + } + + if (!mtbFile.metadata.modelProjectConsent.provisions.isEmpty()) { + mtbFile.metadata.modelProjectConsent.version = + gIcsConfigProperties.genomeDeConsentVersion + } + } + } + + /** + * fixme: currently we do not have information about submission type + */ + private fun setGenomDeSubmissionType(mtbFile: Mtb) { + if (appConfigProperties.genomDeTestSubmission) { + + // fixme: remove INITIAL and uncomment when data model is updated + mtbFile.metadata.type = MvhSubmissionType.INITIAL + // mtbFile.metadata.type = MvhSubmissionType.Test + + logger.info("genomeDe submission mit TEST") + + } else { + mtbFile.metadata.type = MvhSubmissionType.INITIAL + } + } + + /** + * @param consentBundle consent resource + * @param requestDate date which must be within validation period of provision + * @return type of provision, will be [org.hl7.fhir.r4.model.Consent.ConsentProvisionType.NULL] if none is found. + */ + fun getProvisionTypeByPolicyCode( + consentBundle: Bundle, requestDate: Date?, consentDomain: ConsentDomain + ): Consent.ConsentProvisionType { + val code: String? + val system: String? + if (ConsentDomain.BroadConsent == consentDomain) { + code = gIcsConfigProperties.broadConsentPolicyCode + system = gIcsConfigProperties.broadConsentPolicySystem + } else if (ConsentDomain.Modelvorhaben64e == consentDomain) { + code = gIcsConfigProperties.genomeDePolicyCode + system = gIcsConfigProperties.genomeDePolicySystem + } else { + throw NotImplementedException("unknown consent domain " + consentDomain.name) + } + + val provisionTypeByPolicyCode = getProvisionTypeByPolicyCode( + consentBundle, code, system, requestDate + ) + return provisionTypeByPolicyCode + } + + /** + * @param consentBundle consent resource + * @param policyAndProvisionCode policyRule and provision code value + * @param policyAndProvisionSystem policyRule and provision system value + * @param requestDate date which must be within validation period of provision + * @return type of provision, will be [org.hl7.fhir.r4.model.Consent.ConsentProvisionType.NULL] if none is found. + */ + fun getProvisionTypeByPolicyCode( + consentBundle: Bundle, + policyAndProvisionCode: String?, + policyAndProvisionSystem: String?, + requestDate: Date? + ): Consent.ConsentProvisionType { + val entriesOfInterest = consentBundle.entry.filter { entry -> + entry.resource.isResource && entry.resource.resourceType == ResourceType.Consent && (entry.resource as Consent).status == ConsentState.ACTIVE && checkCoding( + policyAndProvisionCode, + policyAndProvisionSystem, + (entry.resource as Consent).policyRule.codingFirstRep + ) && isIsRequestDateInRange( + requestDate, (entry.resource as Consent).provision.period + ) + }.map { consentWithTargetPolicy: BundleEntryComponent -> + val provision = (consentWithTargetPolicy.getResource() as Consent).getProvision() + val provisionComponentByCode = + provision.getProvision().stream().filter { prov: ProvisionComponent? -> + checkCoding( + policyAndProvisionCode, + policyAndProvisionSystem, + prov!!.getCodeFirstRep().getCodingFirstRep() + ) && isIsRequestDateInRange( + requestDate, prov.getPeriod() + ) + }.findFirst() + if (provisionComponentByCode.isPresent) { + // actual provision we search for + return@map provisionComponentByCode.get().getType() + } else { + if (provision.type != null) return provision.type + + } + return Consent.ConsentProvisionType.NULL + }.firstOrNull() + + if (entriesOfInterest == null) return Consent.ConsentProvisionType.NULL + return entriesOfInterest + } + + fun checkCoding( + researchAllowedPolicyOid: String?, researchAllowedPolicySystem: String?, coding: Coding + ): Boolean { + return coding.getSystem() == researchAllowedPolicySystem && (coding.getCode() == researchAllowedPolicyOid) + } + + fun isIsRequestDateInRange(requestdate: Date?, provPeriod: Period): Boolean { + val isRequestDateAfterOrEqualStart = provPeriod.getStart().compareTo(requestdate) + val isRequestDateBeforeOrEqualEnd = provPeriod.getEnd().compareTo(requestdate) + return isRequestDateAfterOrEqualStart <= 0 && isRequestDateBeforeOrEqualEnd >= 0 + } + +} \ No newline at end of file diff --git a/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt b/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt index f25452e..bb226c0 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt @@ -23,6 +23,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import de.ukw.ccc.bwhc.dto.MtbFile import dev.dnpm.etl.processor.* import dev.dnpm.etl.processor.config.AppConfigProperties +import dev.dnpm.etl.processor.consent.TtpConsentStatus import dev.dnpm.etl.processor.monitoring.Report import dev.dnpm.etl.processor.monitoring.Request import dev.dnpm.etl.processor.monitoring.RequestStatus @@ -34,8 +35,11 @@ import dev.dnpm.etl.processor.pseudonym.pseudonymizeWith import dev.pcvolkmer.mv64e.mtb.Mtb import org.apache.commons.codec.binary.Base32 import org.apache.commons.codec.digest.DigestUtils +import org.slf4j.Logger +import org.slf4j.LoggerFactory import org.springframework.context.ApplicationEventPublisher import org.springframework.stereotype.Service +import java.lang.RuntimeException import java.time.Instant import java.util.* @@ -47,9 +51,11 @@ class RequestProcessor( private val requestService: RequestService, private val objectMapper: ObjectMapper, private val applicationEventPublisher: ApplicationEventPublisher, - private val appConfigProperties: AppConfigProperties + private val appConfigProperties: AppConfigProperties, + private val consentProcessor: ConsentProcessor? ) { + private var logger: Logger = LoggerFactory.getLogger("RequestProcessor") fun processMtbFile(mtbFile: MtbFile) { processMtbFile(mtbFile, randomRequestId()) } @@ -66,12 +72,25 @@ class RequestProcessor( processMtbFile(mtbFile, randomRequestId()) } + fun processMtbFile(mtbFile: Mtb, requestId: RequestId) { - val pid = PatientId(mtbFile.patient.id) - mtbFile pseudonymizeWith pseudonymizeService - mtbFile anonymizeContentWith pseudonymizeService - val request = DnpmV2MtbFileRequest(requestId, transformationService.transform(mtbFile)) - saveAndSend(request, pid) + val pid = PatientId(extractPatientIdentifier(mtbFile)) + + val isConsentOk = consentProcessor != null && + consentProcessor.consentGatedCheckAndTryEmbedding(mtbFile) || consentProcessor == null + if (isConsentOk) { + mtbFile pseudonymizeWith pseudonymizeService + mtbFile anonymizeContentWith pseudonymizeService + val request = DnpmV2MtbFileRequest(requestId, transformationService.transform(mtbFile)) + saveAndSend(request, pid) + } else { + logger.warn("consent check failed file will not be processed further!") + applicationEventPublisher.publishEvent( + ResponseEvent( + requestId, Instant.now(), RequestStatus.NO_CONSENT + ) + ) + } } private fun saveAndSend(request: MtbFileRequest, pid: PatientId) { @@ -89,9 +108,7 @@ class RequestProcessor( if (appConfigProperties.duplicationDetection && isDuplication(request)) { applicationEventPublisher.publishEvent( ResponseEvent( - request.requestId, - Instant.now(), - RequestStatus.DUPLICATION + request.requestId, Instant.now(), RequestStatus.DUPLICATION ) ) return @@ -120,21 +137,31 @@ class RequestProcessor( val lastMtbFileRequestForPatient = requestService.lastMtbFileRequestForPatientPseudonym(patientPseudonym) - val isLastRequestDeletion = requestService.isLastRequestWithKnownStatusDeletion(patientPseudonym) + val isLastRequestDeletion = + requestService.isLastRequestWithKnownStatusDeletion(patientPseudonym) - return null != lastMtbFileRequestForPatient - && !isLastRequestDeletion - && lastMtbFileRequestForPatient.fingerprint == fingerprint(pseudonymizedMtbFileRequest) + return null != lastMtbFileRequestForPatient && !isLastRequestDeletion && lastMtbFileRequestForPatient.fingerprint == fingerprint( + pseudonymizedMtbFileRequest + ) } - fun processDeletion(patientId: PatientId) { - processDeletion(patientId, randomRequestId()) + fun processDeletion(patientId: PatientId, isConsented: TtpConsentStatus) { + processDeletion(patientId, randomRequestId(), isConsented) } - fun processDeletion(patientId: PatientId, requestId: RequestId) { + fun processDeletion(patientId: PatientId, requestId: RequestId, isConsented: TtpConsentStatus) { try { val patientPseudonym = pseudonymizeService.patientPseudonym(patientId) + val requestStatus: RequestStatus = when (isConsented) { + TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED, TtpConsentStatus.BROAD_CONSENT_MISSING, TtpConsentStatus.BROAD_CONSENT_REJECTED -> RequestStatus.NO_CONSENT + TtpConsentStatus.FAILED_TO_ASK -> RequestStatus.ERROR + TtpConsentStatus.BROAD_CONSENT_GIVEN, TtpConsentStatus.UNKNOWN_CHECK_FILE -> RequestStatus.UNKNOWN + TtpConsentStatus.GENOM_DE_CONSENT_SEQUENCING_PERMIT, TtpConsentStatus.GENOM_DE_CONSENT_MISSING, TtpConsentStatus.GENOM_DE_SEQUENCING_REJECTED -> { + throw RuntimeException("processDelete should never deal with '" + isConsented.name + "' consent status. This is a bug and need to be fixed!") + } + } + requestService.save( Request( requestId, @@ -142,7 +169,7 @@ class RequestProcessor( patientId, fingerprint(patientPseudonym.value), RequestType.DELETE, - RequestStatus.UNKNOWN + requestStatus ) ) @@ -150,17 +177,14 @@ class RequestProcessor( applicationEventPublisher.publishEvent( ResponseEvent( - requestId, - Instant.now(), - responseStatus.status, - when (responseStatus.status) { + requestId, Instant.now(), responseStatus.status, when (responseStatus.status) { RequestStatus.WARNING, RequestStatus.ERROR -> Optional.of(responseStatus.body) else -> Optional.empty() } ) ) - } catch (e: Exception) { + } catch (_: Exception) { requestService.save( Request( uuid = requestId, @@ -184,10 +208,10 @@ class RequestProcessor( private fun fingerprint(s: String): Fingerprint { return Fingerprint( - Base32().encodeAsString(DigestUtils.sha256(s)) - .replace("=", "") - .lowercase() + Base32().encodeAsString(DigestUtils.sha256(s)).replace("=", "").lowercase() ) } } + +private fun extractPatientIdentifier(mtbFile: Mtb): String = mtbFile.patient.id diff --git a/src/main/kotlin/dev/dnpm/etl/processor/services/ResponseProcessor.kt b/src/main/kotlin/dev/dnpm/etl/processor/services/ResponseProcessor.kt index ecb2ec7..fb82647 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/services/ResponseProcessor.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/services/ResponseProcessor.kt @@ -70,6 +70,12 @@ class ResponseProcessor( ) } + RequestStatus.NO_CONSENT -> { + it.report = Report( + "Einwilligung Status fehlt, widerrufen oder ungeklärt." + ) + } + else -> { logger.error("Cannot process response: Unknown response!") return@ifPresentOrElse diff --git a/src/main/kotlin/dev/dnpm/etl/processor/web/ConfigController.kt b/src/main/kotlin/dev/dnpm/etl/processor/web/ConfigController.kt index 25ec7cc..ea89e98 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/web/ConfigController.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/web/ConfigController.kt @@ -19,10 +19,7 @@ package dev.dnpm.etl.processor.web -import dev.dnpm.etl.processor.monitoring.ConnectionCheckResult -import dev.dnpm.etl.processor.monitoring.ConnectionCheckService -import dev.dnpm.etl.processor.monitoring.GPasConnectionCheckService -import dev.dnpm.etl.processor.monitoring.OutputConnectionCheckService +import dev.dnpm.etl.processor.monitoring.* import dev.dnpm.etl.processor.output.MtbFileSender import dev.dnpm.etl.processor.pseudonym.Generator import dev.dnpm.etl.processor.security.Role @@ -61,11 +58,15 @@ class ConfigController( val gPasConnectionAvailable = connectionCheckServices.filterIsInstance().firstOrNull()?.connectionAvailable() + val gIcsConnectionAvailable = + connectionCheckServices.filterIsInstance().firstOrNull()?.connectionAvailable() + model.addAttribute("pseudonymGenerator", pseudonymGenerator.javaClass.simpleName) model.addAttribute("mtbFileSender", mtbFileSender.javaClass.simpleName) model.addAttribute("mtbFileEndpoint", mtbFileSender.endpoint()) model.addAttribute("outputConnectionAvailable", outputConnectionAvailable) model.addAttribute("gPasConnectionAvailable", gPasConnectionAvailable) + model.addAttribute("gIcsConnectionAvailable", gIcsConnectionAvailable) model.addAttribute("tokensEnabled", tokenService != null) if (tokenService != null) { model.addAttribute("tokens", tokenService.findAll()) @@ -119,6 +120,24 @@ class ConfigController( return "configs/gPasConnectionAvailable" } + @GetMapping(params = ["gIcsConnectionAvailable"]) + fun gIcsConnectionAvailable(model: Model): String { + val gIcsConnectionAvailable = + connectionCheckServices.filterIsInstance().firstOrNull()?.connectionAvailable() + + model.addAttribute("mtbFileSender", mtbFileSender.javaClass.simpleName) + model.addAttribute("mtbFileEndpoint", mtbFileSender.endpoint()) + model.addAttribute("gIcsConnectionAvailable", gIcsConnectionAvailable) + if (tokenService != null) { + model.addAttribute("tokensEnabled", true) + model.addAttribute("tokens", tokenService.findAll()) + } else { + model.addAttribute("tokens", listOf()) + } + + return "configs/gIcsConnectionAvailable" + } + @PostMapping(path = ["tokens"]) fun addToken(@ModelAttribute("name") name: String, model: Model): String { if (tokenService == null) { @@ -190,6 +209,7 @@ class ConfigController( is ConnectionCheckResult.KafkaConnectionCheckResult -> "output-connection-check" is ConnectionCheckResult.RestConnectionCheckResult -> "output-connection-check" is ConnectionCheckResult.GPasConnectionCheckResult -> "gpas-connection-check" + is ConnectionCheckResult.GIcsConnectionCheckResult -> "gics-connection-check" } ServerSentEvent.builder() diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 0d219aa..9807b9b 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -16,6 +16,7 @@ spring: content: enabled: true paths: /**/*.js,/**/*.css,/**/*.svg,/**/*.jpeg - +app: + isGenomDeTestSubmission: true server: forward-headers-strategy: framework \ No newline at end of file diff --git a/src/main/resources/templates/configs.html b/src/main/resources/templates/configs.html index d94deb6..e0056ee 100644 --- a/src/main/resources/templates/configs.html +++ b/src/main/resources/templates/configs.html @@ -49,6 +49,11 @@ +
+
+
+
+
diff --git a/src/main/resources/templates/configs/gIcsConnectionAvailable.html b/src/main/resources/templates/configs/gIcsConnectionAvailable.html new file mode 100644 index 0000000..907a5a2 --- /dev/null +++ b/src/main/resources/templates/configs/gIcsConnectionAvailable.html @@ -0,0 +1,24 @@ + +

🟦 gICS nicht konfiguriert - Einwilligung wird über Dateiinhalt geprüft

+
+ +

Verbindung zu gICS

+
+ Stand: +  |  + Letzte Änderung: +
+
+ Die Verbindung ist aktuell + verfügbar. + nicht verfügbar. +
+
+ ETL-Processor + + gICS + ETL-Processor + + gICS +
+
\ No newline at end of file diff --git a/src/test/java/dev/dnpm/etl/processor/consent/GicsConsentServiceTest.java b/src/test/java/dev/dnpm/etl/processor/consent/GicsConsentServiceTest.java new file mode 100644 index 0000000..d26eca2 --- /dev/null +++ b/src/test/java/dev/dnpm/etl/processor/consent/GicsConsentServiceTest.java @@ -0,0 +1,123 @@ +package dev.dnpm.etl.processor.consent; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.dnpm.etl.processor.config.AppConfiguration; +import dev.dnpm.etl.processor.config.AppFhirConfig; +import dev.dnpm.etl.processor.config.GIcsConfigProperties; +import java.time.Instant; +import java.util.Date; +import org.hl7.fhir.r4.model.BooleanType; +import org.hl7.fhir.r4.model.Identifier; +import org.hl7.fhir.r4.model.OperationOutcome; +import org.hl7.fhir.r4.model.OperationOutcome.IssueSeverity; +import org.hl7.fhir.r4.model.OperationOutcome.IssueType; +import org.hl7.fhir.r4.model.OperationOutcome.OperationOutcomeIssueComponent; +import org.hl7.fhir.r4.model.Parameters; +import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent; +import org.hl7.fhir.r4.model.StringType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.client.RestClientTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.client.MockRestServiceServer; + +@ContextConfiguration(classes = {AppConfiguration.class, ObjectMapper.class}) +@TestPropertySource(properties = {"app.consent.gics.enabled=true", + "app.consent.gics.uri=http://localhost:8090/ttp-fhir/fhir/gics"}) +@RestClientTest +public class GicsConsentServiceTest { + + public static final String GICS_BASE_URI = "http://localhost:8090/ttp-fhir/fhir/gics"; + @Autowired + MockRestServiceServer mockRestServiceServer; + + @Autowired + GicsConsentService gicsConsentService; + + @Autowired + AppConfiguration appConfiguration; + + @Autowired + AppFhirConfig appFhirConfig; + + @Autowired + GIcsConfigProperties gIcsConfigProperties; + + @BeforeEach + public void setUp() { + mockRestServiceServer = MockRestServiceServer.createServer(appConfiguration.restTemplate()); + } + + @Test + void getTtpBroadConsentStatus() { + final Parameters responseConsented = new Parameters().addParameter( + new ParametersParameterComponent().setName("consented") + .setValue(new BooleanType().setValue(true))); + + mockRestServiceServer.expect(requestTo( + "http://localhost:8090/ttp-fhir/fhir/gics" + GicsConsentService.IS_CONSENTED_ENDPOINT)) + .andRespond(withSuccess(appFhirConfig.fhirContext().newJsonParser() + .encodeResourceToString(responseConsented), MediaType.APPLICATION_JSON)); + + var consentStatus = gicsConsentService.getTtpBroadConsentStatus("123456"); + assertThat(consentStatus).isEqualTo(TtpConsentStatus.BROAD_CONSENT_GIVEN); + } + + @Test + void consentRevoced() { + final Parameters responseRevoced = new Parameters().addParameter( + new ParametersParameterComponent().setName("consented") + .setValue(new BooleanType().setValue(false))); + + mockRestServiceServer.expect(requestTo( + "http://localhost:8090/ttp-fhir/fhir/gics" + GicsConsentService.IS_CONSENTED_ENDPOINT)) + .andRespond(withSuccess( + appFhirConfig.fhirContext().newJsonParser().encodeResourceToString(responseRevoced), + MediaType.APPLICATION_JSON)); + + var consentStatus = gicsConsentService.getTtpBroadConsentStatus("123456"); + assertThat(consentStatus).isEqualTo(TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED); + } + + + @Test + void gicsParameterInvalid() { + final OperationOutcome responseErrorOutcome = new OperationOutcome().addIssue( + new OperationOutcomeIssueComponent().setSeverity(IssueSeverity.ERROR) + .setCode(IssueType.PROCESSING).setDiagnostics("Invalid policy parameter...")); + + mockRestServiceServer.expect( + requestTo(GICS_BASE_URI + GicsConsentService.IS_CONSENTED_ENDPOINT)).andRespond( + withSuccess(appFhirConfig.fhirContext().newJsonParser() + .encodeResourceToString(responseErrorOutcome), MediaType.APPLICATION_JSON)); + + var consentStatus = gicsConsentService.getTtpBroadConsentStatus("123456"); + assertThat(consentStatus).isEqualTo(TtpConsentStatus.FAILED_TO_ASK); + } + + @Test + void buildRequestParameterCurrentPolicyStatesForPersonTest() { + + String pid = "12345678"; + var result = GicsConsentService.buildRequestParameterCurrentPolicyStatesForPerson( + gIcsConfigProperties, pid, Date.from(Instant.now()), + gIcsConfigProperties.getGenomDeConsentDomainName()); + + assertThat(result.getParameter().size()).as("should contain 3 parameter resources") + .isEqualTo(3); + + assertThat(((StringType) result.getParameter("domain").getValue()).getValue()).isEqualTo( + gIcsConfigProperties.getGenomDeConsentDomainName()); + assertThat( + ((Identifier) result.getParameter("personIdentifier").getValue()).getValue()).isEqualTo( + pid); + } + + +} diff --git a/src/test/kotlin/dev/dnpm/etl/processor/input/KafkaInputListenerTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/input/KafkaInputListenerTest.kt index f2abd27..fbcfb3f 100644 --- a/src/test/kotlin/dev/dnpm/etl/processor/input/KafkaInputListenerTest.kt +++ b/src/test/kotlin/dev/dnpm/etl/processor/input/KafkaInputListenerTest.kt @@ -23,6 +23,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import de.ukw.ccc.bwhc.dto.Consent import de.ukw.ccc.bwhc.dto.MtbFile import de.ukw.ccc.bwhc.dto.Patient +import dev.dnpm.etl.processor.consent.TtpConsentStatus import dev.dnpm.etl.processor.CustomMediaType import dev.dnpm.etl.processor.services.RequestProcessor import org.apache.kafka.clients.consumer.ConsumerRecord @@ -34,10 +35,7 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.mockito.Mock import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.kotlin.any -import org.mockito.kotlin.anyValueClass -import org.mockito.kotlin.times -import org.mockito.kotlin.verify +import org.mockito.kotlin.* import java.util.* @ExtendWith(MockitoExtension::class) @@ -49,7 +47,7 @@ class KafkaInputListenerTest { @BeforeEach fun setup( - @Mock requestProcessor: RequestProcessor + @Mock requestProcessor: RequestProcessor, ) { this.requestProcessor = requestProcessor this.objectMapper = ObjectMapper() @@ -94,7 +92,10 @@ class KafkaInputListenerTest { ) ) - verify(requestProcessor, times(1)).processDeletion(anyValueClass()) + verify(requestProcessor, times(1)).processDeletion( + anyValueClass(), + eq(TtpConsentStatus.UNKNOWN_CHECK_FILE) + ) } @Test @@ -147,7 +148,8 @@ class KafkaInputListenerTest { Optional.empty() ) ) - verify(requestProcessor, times(1)).processDeletion(anyValueClass(), anyValueClass()) + verify(requestProcessor, times(1)).processDeletion(anyValueClass(), anyValueClass(), eq( + TtpConsentStatus.UNKNOWN_CHECK_FILE)) } @Test @@ -178,7 +180,8 @@ class KafkaInputListenerTest { Optional.empty() ) ) - verify(requestProcessor, times(0)).processDeletion(anyValueClass(), anyValueClass()) + verify(requestProcessor, times(0)).processDeletion(anyValueClass(), anyValueClass(), eq( + TtpConsentStatus.UNKNOWN_CHECK_FILE)) } } diff --git a/src/test/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt index 4a33078..eb7e0b6 100644 --- a/src/test/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt +++ b/src/test/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt @@ -21,21 +21,29 @@ package dev.dnpm.etl.processor.input import com.fasterxml.jackson.databind.ObjectMapper import de.ukw.ccc.bwhc.dto.* +import de.ukw.ccc.bwhc.dto.Consent.Status import dev.dnpm.etl.processor.CustomMediaType +import dev.dnpm.etl.processor.consent.ConsentByMtbFile +import dev.dnpm.etl.processor.consent.GicsConsentService +import dev.dnpm.etl.processor.consent.TtpConsentStatus import dev.dnpm.etl.processor.services.RequestProcessor import dev.pcvolkmer.mv64e.mtb.Mtb import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource import org.mockito.Mock import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.junit.jupiter.MockitoExtension import org.mockito.kotlin.any import org.mockito.kotlin.anyValueClass +import org.mockito.kotlin.whenever import org.springframework.core.io.ClassPathResource import org.springframework.http.MediaType +import org.springframework.test.context.TestPropertySource import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.delete import org.springframework.test.web.servlet.post @@ -53,19 +61,22 @@ class MtbFileRestControllerTest { private lateinit var requestProcessor: RequestProcessor + @BeforeEach fun setup( @Mock requestProcessor: RequestProcessor ) { this.requestProcessor = requestProcessor - val controller = MtbFileRestController(requestProcessor) + val controller = MtbFileRestController(requestProcessor, + ConsentByMtbFile() + ) this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build() } @Test fun shouldProcessPostRequest() { mockMvc.post("/mtbfile") { - content = objectMapper.writeValueAsString(bwhcMtbFileContent(Consent.Status.ACTIVE)) + content = objectMapper.writeValueAsString(bwhcMtbFileContent(Status.ACTIVE)) contentType = MediaType.APPLICATION_JSON }.andExpect { status { @@ -79,7 +90,8 @@ class MtbFileRestControllerTest { @Test fun shouldProcessPostRequestWithRejectedConsent() { mockMvc.post("/mtbfile") { - content = objectMapper.writeValueAsString(bwhcMtbFileContent(Consent.Status.REJECTED)) + content = + objectMapper.writeValueAsString(bwhcMtbFileContent(Status.REJECTED)) contentType = MediaType.APPLICATION_JSON }.andExpect { status { @@ -87,7 +99,10 @@ class MtbFileRestControllerTest { } } - verify(requestProcessor, times(1)).processDeletion(anyValueClass()) + verify(requestProcessor, times(1)).processDeletion( + anyValueClass(), + org.mockito.kotlin.eq(TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED) + ) } @Test @@ -98,10 +113,100 @@ class MtbFileRestControllerTest { } } - verify(requestProcessor, times(1)).processDeletion(anyValueClass()) + verify(requestProcessor, times(1)).processDeletion( + anyValueClass(), + org.mockito.kotlin.eq(TtpConsentStatus.UNKNOWN_CHECK_FILE) + ) } } + @TestPropertySource( + properties = ["app.consent.gics.enabled=true", + "app.consent.gics.gIcsBaseUri=http://localhost:8090/ttp-fhir/fhir/gics"] + ) + @Nested + inner class BwhcRequestsCheckConsentViaTtp { + + private lateinit var mockMvc: MockMvc + + private lateinit var requestProcessor: RequestProcessor + + private lateinit var gicsConsentService: GicsConsentService + + @BeforeEach + fun setup( + @Mock requestProcessor: RequestProcessor, + @Mock gicsConsentService: GicsConsentService + ) { + this.requestProcessor = requestProcessor + val controller = MtbFileRestController(requestProcessor, gicsConsentService) + this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build() + this.gicsConsentService = gicsConsentService + + } + + @ParameterizedTest + @ValueSource(strings = ["ACTIVE", "REJECTED"]) + fun shouldProcessPostRequest(status: String) { + + whenever(gicsConsentService.getTtpBroadConsentStatus(any())).thenReturn(TtpConsentStatus.BROAD_CONSENT_GIVEN) + + mockMvc.post("/mtbfile") { + content = + objectMapper.writeValueAsString(bwhcMtbFileContent(Status.valueOf(status))) + contentType = MediaType.APPLICATION_JSON + }.andExpect { + status { + isAccepted() + } + } + + verify(requestProcessor, times(1)).processMtbFile(any()) + } + + + @ParameterizedTest + @ValueSource(strings = ["ACTIVE", "REJECTED"]) + fun shouldProcessPostRequestWithRejectedConsent(status: String) { + + whenever(gicsConsentService.getTtpBroadConsentStatus(any())).thenReturn(TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED) + + mockMvc.post("/mtbfile") { + content = + objectMapper.writeValueAsString(bwhcMtbFileContent(Status.valueOf(status))) + contentType = MediaType.APPLICATION_JSON + }.andExpect { + status { + isAccepted() + } + } + + // consent status from ttp should override file consent value + verify(requestProcessor, times(1)).processDeletion( + anyValueClass(), + org.mockito.kotlin.eq(TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED) + ) + } + + @Test + fun shouldProcessDeleteRequest() { + + mockMvc.delete("/mtbfile/TEST_12345678").andExpect { + status { + isAccepted() + } + } + + verify(requestProcessor, times(1)).processDeletion( + anyValueClass(), + org.mockito.kotlin.eq(TtpConsentStatus.UNKNOWN_CHECK_FILE) + ) + verify(gicsConsentService, times(0)).getTtpBroadConsentStatus(any()) + + } + } + + @Nested inner class BwhcRequestsWithAlias { @@ -114,14 +219,16 @@ class MtbFileRestControllerTest { @Mock requestProcessor: RequestProcessor ) { this.requestProcessor = requestProcessor - val controller = MtbFileRestController(requestProcessor) + val controller = MtbFileRestController(requestProcessor, + ConsentByMtbFile() + ) this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build() } @Test fun shouldProcessPostRequest() { mockMvc.post("/mtb") { - content = objectMapper.writeValueAsString(bwhcMtbFileContent(Consent.Status.ACTIVE)) + content = objectMapper.writeValueAsString(bwhcMtbFileContent(Status.ACTIVE)) contentType = MediaType.APPLICATION_JSON }.andExpect { status { @@ -135,7 +242,8 @@ class MtbFileRestControllerTest { @Test fun shouldProcessPostRequestWithRejectedConsent() { mockMvc.post("/mtb") { - content = objectMapper.writeValueAsString(bwhcMtbFileContent(Consent.Status.REJECTED)) + content = + objectMapper.writeValueAsString(bwhcMtbFileContent(Status.REJECTED)) contentType = MediaType.APPLICATION_JSON }.andExpect { status { @@ -143,7 +251,11 @@ class MtbFileRestControllerTest { } } - verify(requestProcessor, times(1)).processDeletion(anyValueClass()) + verify(requestProcessor, times(1)).processDeletion( + anyValueClass(), org.mockito.kotlin.eq( + TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED + ) + ) } @Test @@ -154,7 +266,11 @@ class MtbFileRestControllerTest { } } - verify(requestProcessor, times(1)).processDeletion(anyValueClass()) + verify(requestProcessor, times(1)).processDeletion( + anyValueClass(), org.mockito.kotlin.eq( + TtpConsentStatus.UNKNOWN_CHECK_FILE + ) + ) } } @@ -167,16 +283,21 @@ class MtbFileRestControllerTest { @BeforeEach fun setup( - @Mock requestProcessor: RequestProcessor + @Mock requestProcessor: RequestProcessor, + @Mock gicsConsentService: GicsConsentService ) { this.requestProcessor = requestProcessor - val controller = MtbFileRestController(requestProcessor) + val controller = MtbFileRestController(requestProcessor, + gicsConsentService + ) this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build() } @Test fun shouldRespondPostRequest() { - val mtbFileContent = ClassPathResource("mv64e-mtb-fake-patient.json").inputStream.readAllBytes().toString(Charsets.UTF_8) + val mtbFileContent = + ClassPathResource("mv64e-mtb-fake-patient.json").inputStream.readAllBytes() + .toString(Charsets.UTF_8) mockMvc.post("/mtb") { content = mtbFileContent @@ -193,7 +314,7 @@ class MtbFileRestControllerTest { } companion object { - fun bwhcMtbFileContent(consentStatus: Consent.Status) = MtbFile.builder() + fun bwhcMtbFileContent(consentStatus: Status) = MtbFile.builder() .withPatient( Patient.builder() .withId("TEST_12345678") diff --git a/src/test/kotlin/dev/dnpm/etl/processor/pseudonym/ExtensionsTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/pseudonym/ExtensionsTest.kt index 65986f1..b6baec9 100644 --- a/src/test/kotlin/dev/dnpm/etl/processor/pseudonym/ExtensionsTest.kt +++ b/src/test/kotlin/dev/dnpm/etl/processor/pseudonym/ExtensionsTest.kt @@ -19,11 +19,19 @@ package dev.dnpm.etl.processor.pseudonym +import ca.uhn.fhir.context.FhirContext import com.fasterxml.jackson.databind.ObjectMapper import de.ukw.ccc.bwhc.dto.* import de.ukw.ccc.bwhc.dto.Patient +import dev.dnpm.etl.processor.config.AppConfigProperties +import dev.dnpm.etl.processor.config.GIcsConfigProperties +import dev.dnpm.etl.processor.config.JacksonConfig +import dev.dnpm.etl.processor.consent.ConsentByMtbFile +import dev.dnpm.etl.processor.services.ConsentProcessor +import dev.dnpm.etl.processor.services.ConsentProcessorTest import dev.pcvolkmer.mv64e.mtb.* import org.assertj.core.api.Assertions.assertThat +import org.hl7.fhir.r4.model.Bundle import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows @@ -39,6 +47,9 @@ import java.util.* @ExtendWith(MockitoExtension::class) class ExtensionsTest { + fun getObjectMapper(): ObjectMapper { + return JacksonConfig().objectMapper() + } @Nested inner class UsingBwhcDatamodel { @@ -46,13 +57,14 @@ class ExtensionsTest { val FAKE_MTB_FILE_PATH = "fake_MTBFile.json" val CLEAN_PATIENT_ID = "5dad2f0b-49c6-47d8-a952-7b9e9e0f7549" + private fun fakeMtbFile(): MtbFile { val mtbFile = ClassPathResource(FAKE_MTB_FILE_PATH).inputStream - return ObjectMapper().readValue(mtbFile, MtbFile::class.java) + return getObjectMapper().readValue(mtbFile, MtbFile::class.java) } private fun MtbFile.serialized(): String { - return ObjectMapper().writeValueAsString(this) + return getObjectMapper().writeValueAsString(this) } @Test @@ -86,7 +98,9 @@ class ExtensionsTest { mtbFile.pseudonymizeWith(pseudonymizeService) mtbFile.anonymizeContentWith(pseudonymizeService) - val pattern = "\"[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\"".toRegex().toPattern() + val pattern = + "\"[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\"".toRegex() + .toPattern() val matcher = pattern.matcher(mtbFile.serialized()) assertThrows { @@ -207,15 +221,15 @@ class ExtensionsTest { inner class UsingDnpmV2Datamodel { val FAKE_MTB_FILE_PATH = "mv64e-mtb-fake-patient.json" - val CLEAN_PATIENT_ID = "aca5a971-28be-4089-8128-0036a4fe6f1a" + val CLEAN_PATIENT_ID = "644bae7a-56f6-4ee8-b02f-c532e65af5b1" private fun fakeMtbFile(): Mtb { val mtbFile = ClassPathResource(FAKE_MTB_FILE_PATH).inputStream - return ObjectMapper().readValue(mtbFile, Mtb::class.java) + return getObjectMapper().readValue(mtbFile, Mtb::class.java) } private fun Mtb.serialized(): String { - return ObjectMapper().writeValueAsString(this) + return getObjectMapper().writeValueAsString(this) } @Test @@ -226,6 +240,8 @@ class ExtensionsTest { }.whenever(pseudonymizeService).patientPseudonym(anyValueClass()) val mtbFile = fakeMtbFile() + mtbFile.ensureMetaDataIsInitialized() + addConsentData(mtbFile) mtbFile.pseudonymizeWith(pseudonymizeService) @@ -233,6 +249,25 @@ class ExtensionsTest { assertThat(mtbFile.serialized()).doesNotContain(CLEAN_PATIENT_ID) } + private fun addConsentData(mtbFile: Mtb) { + val gIcsConfigProperties = GIcsConfigProperties("", "", "") + val appConfigProperties = AppConfigProperties(null, emptyList()) + + val bundle = Bundle() + val dummyConsent = ConsentProcessorTest.getDummyGenomDeConsent() + dummyConsent.patient.reference = "Patient/$CLEAN_PATIENT_ID" + bundle.addEntry().resource = dummyConsent + + ConsentProcessor( + appConfigProperties, + gIcsConfigProperties, + JacksonConfig().objectMapper(), + FhirContext.forR4(), + ConsentByMtbFile() + ).embedBroadConsentResources(mtbFile, bundle) + + } + @Test fun shouldNotThrowExceptionOnNullValues(@Mock pseudonymizeService: PseudonymizeService) { doAnswer { diff --git a/src/test/kotlin/dev/dnpm/etl/processor/services/ConsentProcessorTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/services/ConsentProcessorTest.kt new file mode 100644 index 0000000..38ce0b3 --- /dev/null +++ b/src/test/kotlin/dev/dnpm/etl/processor/services/ConsentProcessorTest.kt @@ -0,0 +1,171 @@ +package dev.dnpm.etl.processor.services + +import ca.uhn.fhir.context.FhirContext +import com.fasterxml.jackson.databind.ObjectMapper +import dev.dnpm.etl.processor.config.AppConfigProperties +import dev.dnpm.etl.processor.config.GIcsConfigProperties +import dev.dnpm.etl.processor.config.JacksonConfig +import dev.dnpm.etl.processor.consent.ConsentDomain +import dev.dnpm.etl.processor.consent.GicsConsentService +import dev.pcvolkmer.mv64e.mtb.* +import org.assertj.core.api.Assertions.assertThat +import org.hl7.fhir.r4.model.Bundle +import org.hl7.fhir.r4.model.CodeableConcept +import org.hl7.fhir.r4.model.Coding +import org.hl7.fhir.r4.model.Consent +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.eq +import org.mockito.kotlin.whenever +import org.springframework.core.io.ClassPathResource +import java.io.IOException +import java.io.InputStream +import java.time.Instant +import java.time.OffsetDateTime +import java.util.* + +@ExtendWith(MockitoExtension::class) +class ConsentProcessorTest { + + private lateinit var appConfigProperties: AppConfigProperties + private lateinit var gicsConsentService: GicsConsentService + private lateinit var objectMapper: ObjectMapper + private lateinit var gIcsConfigProperties: GIcsConfigProperties + private lateinit var fhirContext: FhirContext + private lateinit var consentProcessor: ConsentProcessor + + @BeforeEach + fun setups( + @Mock gicsConsentService: GicsConsentService, + ) { + + this.gIcsConfigProperties = GIcsConfigProperties(null, null, null) + val jacksonConfig = JacksonConfig() + this.objectMapper = jacksonConfig.objectMapper() + this.fhirContext = JacksonConfig.fhirContext() + this.gicsConsentService = gicsConsentService + this.appConfigProperties = AppConfigProperties(null, emptyList()) + this.consentProcessor = + ConsentProcessor( + appConfigProperties, + gIcsConfigProperties, + objectMapper, + fhirContext, + gicsConsentService + ) + } + + @Test + fun consentOk() { + assertThat(consentProcessor.toString()).isNotNull + // prep gICS response + doAnswer { getDummyBroadConsentBundle() }.whenever(gicsConsentService) + .getConsent(any(), any(), eq(ConsentDomain.BroadConsent)) + + doAnswer { Bundle() }.whenever(gicsConsentService) + .getConsent(any(), any(), eq(ConsentDomain.Modelvorhaben64e)) + + val inputMtb = Mtb.builder() + .patient(Patient.builder().id("d611d429-5003-11f0-a144-661e92ac9503").build()).build() + val checkResult = consentProcessor.consentGatedCheckAndTryEmbedding(inputMtb) + + assertThat(checkResult).isTrue + assertThat(inputMtb.metadata.researchConsents).hasSize(13) + } + + companion object { + fun getDummyGenomDeConsent(): Consent { + val consent = Consent() + consent.id = "consent 1 id" + consent.patient.reference = "Patient/1234-pat1" + + consent.provision.setType( + Consent.ConsentProvisionType.fromCode( + "deny" + ) + ) + consent.provision.period.start = + Date.from(Instant.parse("2025-06-23T00:00:00.00Z")) + consent.provision.period.end = + Date.from(Instant.parse("3000-01-01T00:00:00.00Z")) + + val addProvision1 = consent.provision.addProvision() + addProvision1.setType(Consent.ConsentProvisionType.fromCode("permit")) + addProvision1.period.start = Date.from(Instant.parse("2025-06-23T00:00:00.00Z")) + addProvision1.period.end = Date.from(Instant.parse("3000-01-01T00:00:00.00Z")) + addProvision1.code.addLast( + CodeableConcept( + Coding( + "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV", + "Teilnahme", + "Teilnahme am Modellvorhaben und Einwilligung zur Genomsequenzierung" + ) + ) + ) + + val addProvision2 = consent.provision.addProvision() + addProvision2.setType(Consent.ConsentProvisionType.fromCode("deny")) + addProvision2.period.start = Date.from(Instant.parse("2025-06-23T00:00:00.00Z")) + addProvision2.period.end = Date.from(Instant.parse("3000-01-01T00:00:00.00Z")) + addProvision2.code.addLast( + CodeableConcept( + Coding( + "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV", + "Rekontaktierung", + "Re-Identifizierung meiner Daten über die Vertrauensstelle beim Robert Koch-Institut und in die erneute Kontaktaufnahme durch meine behandelnde Ärztin oder meinen behandelnden Arzt" + ) + ) + ) + return consent + } + } + + @ParameterizedTest + @CsvSource( + "2.16.840.1.113883.3.1937.777.24.5.3.8,urn:oid:2.16.840.1.113883.3.1937.777.24.5.3,2025-07-23T00:00:00+02:00,PERMIT,expect permit", + "2.16.840.1.113883.3.1937.777.24.5.3.8,urn:oid:2.16.840.1.113883.3.1937.777.24.5.3,2025-06-23T00:00:00+02:00,PERMIT,expect permit date is exactly on start", + "2.16.840.1.113883.3.1937.777.24.5.3.8,urn:oid:2.16.840.1.113883.3.1937.777.24.5.3,2055-06-23T00:00:00+02:00,PERMIT,expect permit date is exactly on end", + "2.16.840.1.113883.3.1937.777.24.5.3.8,urn:oid:2.16.840.1.113883.3.1937.777.24.5.3,2021-06-23T00:00:00+02:00,NULL,date is before start", + "2.16.840.1.113883.3.1937.777.24.5.3.8,urn:oid:2.16.840.1.113883.3.1937.777.24.5.3,2060-06-23T00:00:00+02:00,NULL,date is after end", + "2.16.840.1.113883.3.1937.777.24.5.3.8,XXXX,2025-07-23T00:00:00+02:00,NULL,system not found - therefore expect NULL", + "2.16.840.1.113883.3.1937.777.24.5.3.27,urn:oid:2.16.840.1.113883.3.1937.777.24.5.3,2025-07-23T00:00:00+02:00,DENY,provision is denied" + ) + fun getProvisionTypeByPolicyCode( + code: String?, system: String?, timeStamp: String, expected: String?, + desc: String? + ) { + val testData = getDummyBroadConsentBundle() + + val requestDate = Date.from(OffsetDateTime.parse(timeStamp).toInstant()) + + val result: Consent.ConsentProvisionType = + consentProcessor.getProvisionTypeByPolicyCode(testData, code, system, requestDate) + assertThat(result).isNotNull() + + + assertThat(result).`as`(desc) + .isEqualTo(Consent.ConsentProvisionType.valueOf(expected!!)) + } + + fun getDummyBroadConsentBundle(): Bundle { + val bundle: InputStream? + try { + bundle = ClassPathResource( + "fake_broadConsent_gics_response_permit.json" + ).getInputStream() + } catch (e: IOException) { + throw RuntimeException(e) + } + + return FhirContext.forR4().newJsonParser() + .parseResource(Bundle::class.java, bundle) + } + +} \ No newline at end of file diff --git a/src/test/kotlin/dev/dnpm/etl/processor/services/RequestProcessorTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/services/RequestProcessorTest.kt index fe61852..b36c696 100644 --- a/src/test/kotlin/dev/dnpm/etl/processor/services/RequestProcessorTest.kt +++ b/src/test/kotlin/dev/dnpm/etl/processor/services/RequestProcessorTest.kt @@ -25,6 +25,8 @@ import dev.dnpm.etl.processor.Fingerprint import dev.dnpm.etl.processor.PatientId import dev.dnpm.etl.processor.PatientPseudonym import dev.dnpm.etl.processor.config.AppConfigProperties +import dev.dnpm.etl.processor.consent.GicsConsentService +import dev.dnpm.etl.processor.consent.TtpConsentStatus import dev.dnpm.etl.processor.monitoring.Request import dev.dnpm.etl.processor.monitoring.RequestStatus import dev.dnpm.etl.processor.monitoring.RequestType @@ -58,7 +60,7 @@ class RequestProcessorTest { private lateinit var requestService: RequestService private lateinit var applicationEventPublisher: ApplicationEventPublisher private lateinit var appConfigProperties: AppConfigProperties - + private lateinit var consentProcessor: ConsentProcessor private lateinit var requestProcessor: RequestProcessor @BeforeEach @@ -67,7 +69,8 @@ class RequestProcessorTest { @Mock transformationService: TransformationService, @Mock sender: RestMtbFileSender, @Mock requestService: RequestService, - @Mock applicationEventPublisher: ApplicationEventPublisher + @Mock applicationEventPublisher: ApplicationEventPublisher, + @Mock consentProcessor: ConsentProcessor ) { this.pseudonymizeService = pseudonymizeService this.transformationService = transformationService @@ -75,6 +78,7 @@ class RequestProcessorTest { this.requestService = requestService this.applicationEventPublisher = applicationEventPublisher this.appConfigProperties = AppConfigProperties(null) + this.consentProcessor = consentProcessor val objectMapper = ObjectMapper() @@ -85,7 +89,8 @@ class RequestProcessorTest { requestService, objectMapper, applicationEventPublisher, - appConfigProperties + appConfigProperties, + consentProcessor ) } @@ -343,7 +348,10 @@ class RequestProcessorTest { MtbFileSender.Response(status = RequestStatus.UNKNOWN) }.whenever(sender).send(any()) - this.requestProcessor.processDeletion(TEST_PATIENT_ID) + this.requestProcessor.processDeletion( + TEST_PATIENT_ID, + isConsented = TtpConsentStatus.UNKNOWN_CHECK_FILE + ) val requestCaptor = argumentCaptor() verify(requestService, times(1)).save(requestCaptor.capture()) @@ -361,7 +369,10 @@ class RequestProcessorTest { MtbFileSender.Response(status = RequestStatus.SUCCESS) }.whenever(sender).send(any()) - this.requestProcessor.processDeletion(TEST_PATIENT_ID) + this.requestProcessor.processDeletion( + TEST_PATIENT_ID, + isConsented = TtpConsentStatus.UNKNOWN_CHECK_FILE + ) val eventCaptor = argumentCaptor() verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture()) @@ -379,7 +390,10 @@ class RequestProcessorTest { MtbFileSender.Response(status = RequestStatus.ERROR) }.whenever(sender).send(any()) - this.requestProcessor.processDeletion(TEST_PATIENT_ID) + this.requestProcessor.processDeletion( + TEST_PATIENT_ID, + isConsented = TtpConsentStatus.UNKNOWN_CHECK_FILE + ) val eventCaptor = argumentCaptor() verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture()) @@ -391,7 +405,10 @@ class RequestProcessorTest { fun testShouldSendDeleteRequestWithPseudonymErrorAndSaveErrorRequestStatus() { doThrow(RuntimeException()).whenever(pseudonymizeService).patientPseudonym(anyValueClass()) - this.requestProcessor.processDeletion(TEST_PATIENT_ID) + this.requestProcessor.processDeletion( + TEST_PATIENT_ID, + isConsented = TtpConsentStatus.UNKNOWN_CHECK_FILE + ) val requestCaptor = argumentCaptor() verify(requestService, times(1)).save(requestCaptor.capture()) diff --git a/src/test/kotlin/dev/dnpm/etl/processor/services/TransformationServiceTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/services/TransformationServiceTest.kt index 487b502..113245a 100644 --- a/src/test/kotlin/dev/dnpm/etl/processor/services/TransformationServiceTest.kt +++ b/src/test/kotlin/dev/dnpm/etl/processor/services/TransformationServiceTest.kt @@ -19,14 +19,23 @@ package dev.dnpm.etl.processor.services -import com.fasterxml.jackson.databind.ObjectMapper import de.ukw.ccc.bwhc.dto.Consent import de.ukw.ccc.bwhc.dto.Diagnosis import de.ukw.ccc.bwhc.dto.Icd10 import de.ukw.ccc.bwhc.dto.MtbFile +import dev.dnpm.etl.processor.config.JacksonConfig +import dev.pcvolkmer.mv64e.mtb.ConsentProvision +import dev.pcvolkmer.mv64e.mtb.ModelProjectConsent +import dev.pcvolkmer.mv64e.mtb.ModelProjectConsentPurpose import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import dev.pcvolkmer.mv64e.mtb.Mtb +import dev.pcvolkmer.mv64e.mtb.MvhMetadata +import dev.pcvolkmer.mv64e.mtb.Provision +import org.hl7.fhir.instance.model.api.IBaseResource +import java.time.Instant +import java.util.Date class TransformationServiceTest { @@ -35,7 +44,7 @@ class TransformationServiceTest { @BeforeEach fun setup() { this.service = TransformationService( - ObjectMapper(), listOf( + JacksonConfig().objectMapper(), listOf( Transformation.of("consent.status") from Consent.Status.ACTIVE to Consent.Status.REJECTED, Transformation.of("diagnoses[*].icd10.version") from "2013" to "2014", ) @@ -92,4 +101,59 @@ class TransformationServiceTest { assertThat(actual.consent.status).isEqualTo(Consent.Status.REJECTED) } + @Test + fun shouldTransformConsentValues() { + val mtbFile = MtbFile.builder().withDiagnoses( + listOf( + Diagnosis.builder().withId("1234").withIcd10(Icd10("F79.9").also { + it.version = "2013" + }).build(), + Diagnosis.builder().withId("5678").withIcd10(Icd10("F79.8").also { + it.version = "2019" + }).build() + ) + ).build() + + val actual = this.service.transform(mtbFile) + + assertThat(actual).isNotNull + assertThat(actual.diagnoses[0].icd10.code).isEqualTo("F79.9") + assertThat(actual.diagnoses[0].icd10.version).isEqualTo("2014") + assertThat(actual.diagnoses[1].icd10.code).isEqualTo("F79.8") + assertThat(actual.diagnoses[1].icd10.version).isEqualTo("2019") + } + + @Test + fun shouldTransformConsent() { + val mvhMetadata = MvhMetadata.builder().transferTan("transfertan12345").build() + + assertThat(mvhMetadata).isNotNull + mvhMetadata.modelProjectConsent = + ModelProjectConsent.builder().date(Date.from(Instant.parse("2025-06-23T00:00:00.00Z"))) + .version("1").provisions( + listOf( + Provision.builder().type(ConsentProvision.PERMIT) + .purpose(ModelProjectConsentPurpose.SEQUENCING) + .date(Date.from(Instant.parse("2025-06-23T00:00:00.00Z"))).build(), + Provision.builder().type(ConsentProvision.PERMIT) + .purpose(ModelProjectConsentPurpose.REIDENTIFICATION) + .date(Date.from(Instant.parse("2025-06-23T00:00:00.00Z"))).build(), + Provision.builder().type(ConsentProvision.DENY) + .purpose(ModelProjectConsentPurpose.CASE_IDENTIFICATION) + .date(Date.from(Instant.parse("2025-06-23T00:00:00.00Z"))).build() + ) + ).build() + val consent = ConsentProcessorTest.getDummyGenomDeConsent() + + mvhMetadata.researchConsents = mutableListOf() + mvhMetadata.researchConsents.add(mapOf(consent.id to consent as IBaseResource)) + + val mtbFile = Mtb.builder().metadata(mvhMetadata).build() + + val transformed = service.transform(mtbFile) + assertThat(transformed.metadata.modelProjectConsent.date).isNotNull + + } + + } \ No newline at end of file diff --git a/src/test/resources/fake_broadConsent_gics_response_deny.json b/src/test/resources/fake_broadConsent_gics_response_deny.json new file mode 100644 index 0000000..d0d312e --- /dev/null +++ b/src/test/resources/fake_broadConsent_gics_response_deny.json @@ -0,0 +1,1631 @@ +{ + "resourceType": "Bundle", + "type": "collection", + "entry": [ + { + "fullUrl": "http://127.0.0.1:8090/ttp-fhir/fhir/gics/Consent/0606d937-5004-11f0-a144-661e92ac9503", + "resource": { + "resourceType": "Consent", + "id": "0606d937-5004-11f0-a144-661e92ac9503", + "meta": { + "lastUpdated": "2025-06-23T09:31:05.965+02:00", + "profile": [ + "https://www.medizininformatik-initiative.de/fhir/modul-consent/StructureDefinition/mii-pr-consent-einwilligung" + ] + }, + "extension": [ + { + "url": "http://fhir.de/ConsentManagement/StructureDefinition/DomainReference", + "extension": [ + { + "url": "domain", + "valueReference": { + "reference": "ResearchStudy/90a1fcf9-5003-11f0-a144-661e92ac9503" + } + }, + { + "url": "status", + "valueCoding": { + "system": "http://hl7.org/fhir/publication-status", + "code": "active" + } + } + ] + } + ], + "status": "active", + "scope": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/consentscope", + "code": "research" + } + ] + }, + "category": [ + { + "coding": [ + { + "system": "http://loinc.org", + "code": "57016-8" + } + ] + }, + { + "coding": [ + { + "system": "http://fhir.de/ConsentManagement/CodeSystem/ResultType", + "code": "policy" + } + ] + }, + { + "coding": [ + { + "system": "https://www.medizininformatik-initiative.de/fhir/modul-consent/CodeSystem/mii-cs-consent-consent_category", + "code": "2.16.840.1.113883.3.1937.777.24.2.184" + } + ] + } + ], + "patient": { + "reference": "Patient/06052fed-5004-11f0-a144-661e92ac9503", + "display": "Patienten-ID 999999" + }, + "dateTime": "2025-06-23T00:00:00+02:00", + "organization": [ + { + "display": "MII" + } + ], + "sourceReference": { + "reference": "QuestionnaireResponse/06067759-5004-11f0-a144-661e92ac9503" + }, + "policy": [ + { + "uri": "2.16.840.1.113883.3.1937.777.24.2.184" + }, + { + "uri": "urn:oid:2.16.840.1.113883.3.1937.777.24.2.1790" + } + ], + "policyRule": { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.2", + "display": "IDAT_erheben" + } + ] + }, + "provision": { + "type": "deny", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2055-06-23T00:00:00+02:00" + }, + "provision": [ + { + "type": "deny", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2055-06-23T00:00:00+02:00" + }, + "code": [ + { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.2", + "display": "IDAT_erheben" + } + ] + } + ] + } + ] + } + } + }, + { + "fullUrl": "http://127.0.0.1:8090/ttp-fhir/fhir/gics/Consent/0607372e-5004-11f0-a144-661e92ac9503", + "resource": { + "resourceType": "Consent", + "id": "0607372e-5004-11f0-a144-661e92ac9503", + "meta": { + "lastUpdated": "2025-06-23T09:31:05.965+02:00", + "profile": [ + "https://www.medizininformatik-initiative.de/fhir/modul-consent/StructureDefinition/mii-pr-consent-einwilligung" + ] + }, + "extension": [ + { + "url": "http://fhir.de/ConsentManagement/StructureDefinition/DomainReference", + "extension": [ + { + "url": "domain", + "valueReference": { + "reference": "ResearchStudy/90a1fcf9-5003-11f0-a144-661e92ac9503" + } + }, + { + "url": "status", + "valueCoding": { + "system": "http://hl7.org/fhir/publication-status", + "code": "active" + } + } + ] + } + ], + "status": "active", + "scope": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/consentscope", + "code": "research" + } + ] + }, + "category": [ + { + "coding": [ + { + "system": "http://loinc.org", + "code": "57016-8" + } + ] + }, + { + "coding": [ + { + "system": "http://fhir.de/ConsentManagement/CodeSystem/ResultType", + "code": "policy" + } + ] + }, + { + "coding": [ + { + "system": "https://www.medizininformatik-initiative.de/fhir/modul-consent/CodeSystem/mii-cs-consent-consent_category", + "code": "2.16.840.1.113883.3.1937.777.24.2.184" + } + ] + } + ], + "patient": { + "reference": "Patient/06052fed-5004-11f0-a144-661e92ac9503", + "display": "Patienten-ID 999999" + }, + "dateTime": "2025-06-23T00:00:00+02:00", + "organization": [ + { + "display": "MII" + } + ], + "sourceReference": { + "reference": "QuestionnaireResponse/06067759-5004-11f0-a144-661e92ac9503" + }, + "policy": [ + { + "uri": "2.16.840.1.113883.3.1937.777.24.2.184" + }, + { + "uri": "urn:oid:2.16.840.1.113883.3.1937.777.24.2.1790" + } + ], + "policyRule": { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.31", + "display": "Rekontaktierung_Zusatzbefund" + } + ] + }, + "provision": { + "type": "deny", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2055-06-23T00:00:00+02:00" + }, + "provision": [ + { + "type": "deny", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2055-06-23T00:00:00+02:00" + }, + "code": [ + { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.31", + "display": "Rekontaktierung_Zusatzbefund" + } + ] + } + ] + } + ] + } + } + }, + { + "fullUrl": "http://127.0.0.1:8090/ttp-fhir/fhir/gics/Consent/0606ed1c-5004-11f0-a144-661e92ac9503", + "resource": { + "resourceType": "Consent", + "id": "0606ed1c-5004-11f0-a144-661e92ac9503", + "meta": { + "lastUpdated": "2025-06-23T09:31:05.965+02:00", + "profile": [ + "https://www.medizininformatik-initiative.de/fhir/modul-consent/StructureDefinition/mii-pr-consent-einwilligung" + ] + }, + "extension": [ + { + "url": "http://fhir.de/ConsentManagement/StructureDefinition/DomainReference", + "extension": [ + { + "url": "domain", + "valueReference": { + "reference": "ResearchStudy/90a1fcf9-5003-11f0-a144-661e92ac9503" + } + }, + { + "url": "status", + "valueCoding": { + "system": "http://hl7.org/fhir/publication-status", + "code": "active" + } + } + ] + } + ], + "status": "active", + "scope": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/consentscope", + "code": "research" + } + ] + }, + "category": [ + { + "coding": [ + { + "system": "http://loinc.org", + "code": "57016-8" + } + ] + }, + { + "coding": [ + { + "system": "http://fhir.de/ConsentManagement/CodeSystem/ResultType", + "code": "policy" + } + ] + }, + { + "coding": [ + { + "system": "https://www.medizininformatik-initiative.de/fhir/modul-consent/CodeSystem/mii-cs-consent-consent_category", + "code": "2.16.840.1.113883.3.1937.777.24.2.184" + } + ] + } + ], + "patient": { + "reference": "Patient/06052fed-5004-11f0-a144-661e92ac9503", + "display": "Patienten-ID 999999" + }, + "dateTime": "2025-06-23T00:00:00+02:00", + "organization": [ + { + "display": "MII" + } + ], + "sourceReference": { + "reference": "QuestionnaireResponse/06067759-5004-11f0-a144-661e92ac9503" + }, + "policy": [ + { + "uri": "2.16.840.1.113883.3.1937.777.24.2.184" + }, + { + "uri": "urn:oid:2.16.840.1.113883.3.1937.777.24.2.1790" + } + ], + "policyRule": { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.6", + "display": "MDAT_erheben" + } + ] + }, + "provision": { + "type": "deny", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2030-06-23T00:00:00+02:00" + }, + "provision": [ + { + "type": "deny", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2030-06-23T00:00:00+02:00" + }, + "code": [ + { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.6", + "display": "MDAT_erheben" + } + ] + } + ] + } + ] + } + } + }, + { + "fullUrl": "http://127.0.0.1:8090/ttp-fhir/fhir/gics/Consent/0606e44f-5004-11f0-a144-661e92ac9503", + "resource": { + "resourceType": "Consent", + "id": "0606e44f-5004-11f0-a144-661e92ac9503", + "meta": { + "lastUpdated": "2025-06-23T09:31:05.965+02:00", + "profile": [ + "https://www.medizininformatik-initiative.de/fhir/modul-consent/StructureDefinition/mii-pr-consent-einwilligung" + ] + }, + "extension": [ + { + "url": "http://fhir.de/ConsentManagement/StructureDefinition/DomainReference", + "extension": [ + { + "url": "domain", + "valueReference": { + "reference": "ResearchStudy/90a1fcf9-5003-11f0-a144-661e92ac9503" + } + }, + { + "url": "status", + "valueCoding": { + "system": "http://hl7.org/fhir/publication-status", + "code": "active" + } + } + ] + } + ], + "status": "active", + "scope": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/consentscope", + "code": "research" + } + ] + }, + "category": [ + { + "coding": [ + { + "system": "http://loinc.org", + "code": "57016-8" + } + ] + }, + { + "coding": [ + { + "system": "http://fhir.de/ConsentManagement/CodeSystem/ResultType", + "code": "policy" + } + ] + }, + { + "coding": [ + { + "system": "https://www.medizininformatik-initiative.de/fhir/modul-consent/CodeSystem/mii-cs-consent-consent_category", + "code": "2.16.840.1.113883.3.1937.777.24.2.184" + } + ] + } + ], + "patient": { + "reference": "Patient/06052fed-5004-11f0-a144-661e92ac9503", + "display": "Patienten-ID 999999" + }, + "dateTime": "2025-06-23T00:00:00+02:00", + "organization": [ + { + "display": "MII" + } + ], + "sourceReference": { + "reference": "QuestionnaireResponse/06067759-5004-11f0-a144-661e92ac9503" + }, + "policy": [ + { + "uri": "2.16.840.1.113883.3.1937.777.24.2.184" + }, + { + "uri": "urn:oid:2.16.840.1.113883.3.1937.777.24.2.1790" + } + ], + "policyRule": { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.8", + "display": "MDAT_wissenschaftlich_nutzen_EU_DSGVO_NIVEAU" + } + ] + }, + "provision": { + "type": "deny", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2055-06-23T00:00:00+02:00" + }, + "provision": [ + { + "type": "deny", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2055-06-23T00:00:00+02:00" + }, + "code": [ + { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.8", + "display": "MDAT_wissenschaftlich_nutzen_EU_DSGVO_NIVEAU" + } + ] + } + ] + } + ] + } + } + }, + { + "fullUrl": "http://127.0.0.1:8090/ttp-fhir/fhir/gics/Consent/0607292a-5004-11f0-a144-661e92ac9503", + "resource": { + "resourceType": "Consent", + "id": "0607292a-5004-11f0-a144-661e92ac9503", + "meta": { + "lastUpdated": "2025-06-23T09:31:05.965+02:00", + "profile": [ + "https://www.medizininformatik-initiative.de/fhir/modul-consent/StructureDefinition/mii-pr-consent-einwilligung" + ] + }, + "extension": [ + { + "url": "http://fhir.de/ConsentManagement/StructureDefinition/DomainReference", + "extension": [ + { + "url": "domain", + "valueReference": { + "reference": "ResearchStudy/90a1fcf9-5003-11f0-a144-661e92ac9503" + } + }, + { + "url": "status", + "valueCoding": { + "system": "http://hl7.org/fhir/publication-status", + "code": "active" + } + } + ] + } + ], + "status": "active", + "scope": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/consentscope", + "code": "research" + } + ] + }, + "category": [ + { + "coding": [ + { + "system": "http://loinc.org", + "code": "57016-8" + } + ] + }, + { + "coding": [ + { + "system": "http://fhir.de/ConsentManagement/CodeSystem/ResultType", + "code": "policy" + } + ] + }, + { + "coding": [ + { + "system": "https://www.medizininformatik-initiative.de/fhir/modul-consent/CodeSystem/mii-cs-consent-consent_category", + "code": "2.16.840.1.113883.3.1937.777.24.2.184" + } + ] + } + ], + "patient": { + "reference": "Patient/06052fed-5004-11f0-a144-661e92ac9503", + "display": "Patienten-ID 999999" + }, + "dateTime": "2025-06-23T00:00:00+02:00", + "organization": [ + { + "display": "MII" + } + ], + "sourceReference": { + "reference": "QuestionnaireResponse/06067759-5004-11f0-a144-661e92ac9503" + }, + "policy": [ + { + "uri": "2.16.840.1.113883.3.1937.777.24.2.184" + }, + { + "uri": "urn:oid:2.16.840.1.113883.3.1937.777.24.2.1790" + } + ], + "policyRule": { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.27", + "display": "Rekontaktierung_Verknuepfung_Datenbanken" + } + ] + }, + "provision": { + "type": "deny", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2055-06-23T00:00:00+02:00" + }, + "provision": [ + { + "type": "deny", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2055-06-23T00:00:00+02:00" + }, + "code": [ + { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.27", + "display": "Rekontaktierung_Verknuepfung_Datenbanken" + } + ] + } + ] + } + ] + } + } + }, + { + "fullUrl": "http://127.0.0.1:8090/ttp-fhir/fhir/gics/Consent/06073274-5004-11f0-a144-661e92ac9503", + "resource": { + "resourceType": "Consent", + "id": "06073274-5004-11f0-a144-661e92ac9503", + "meta": { + "lastUpdated": "2025-06-23T09:31:05.965+02:00", + "profile": [ + "https://www.medizininformatik-initiative.de/fhir/modul-consent/StructureDefinition/mii-pr-consent-einwilligung" + ] + }, + "extension": [ + { + "url": "http://fhir.de/ConsentManagement/StructureDefinition/DomainReference", + "extension": [ + { + "url": "domain", + "valueReference": { + "reference": "ResearchStudy/90a1fcf9-5003-11f0-a144-661e92ac9503" + } + }, + { + "url": "status", + "valueCoding": { + "system": "http://hl7.org/fhir/publication-status", + "code": "active" + } + } + ] + } + ], + "status": "active", + "scope": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/consentscope", + "code": "research" + } + ] + }, + "category": [ + { + "coding": [ + { + "system": "http://loinc.org", + "code": "57016-8" + } + ] + }, + { + "coding": [ + { + "system": "http://fhir.de/ConsentManagement/CodeSystem/ResultType", + "code": "policy" + } + ] + }, + { + "coding": [ + { + "system": "https://www.medizininformatik-initiative.de/fhir/modul-consent/CodeSystem/mii-cs-consent-consent_category", + "code": "2.16.840.1.113883.3.1937.777.24.2.184" + } + ] + } + ], + "patient": { + "reference": "Patient/06052fed-5004-11f0-a144-661e92ac9503", + "display": "Patienten-ID 999999" + }, + "dateTime": "2025-06-23T00:00:00+02:00", + "organization": [ + { + "display": "MII" + } + ], + "sourceReference": { + "reference": "QuestionnaireResponse/06067759-5004-11f0-a144-661e92ac9503" + }, + "policy": [ + { + "uri": "2.16.840.1.113883.3.1937.777.24.2.184" + }, + { + "uri": "urn:oid:2.16.840.1.113883.3.1937.777.24.2.1790" + } + ], + "policyRule": { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.29", + "display": "Rekontaktierung_weitere_Studien" + } + ] + }, + "provision": { + "type": "deny", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2055-06-23T00:00:00+02:00" + }, + "provision": [ + { + "type": "deny", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2055-06-23T00:00:00+02:00" + }, + "code": [ + { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.29", + "display": "Rekontaktierung_weitere_Studien" + } + ] + } + ] + } + ] + } + } + }, + { + "fullUrl": "http://127.0.0.1:8090/ttp-fhir/fhir/gics/Consent/06070996-5004-11f0-a144-661e92ac9503", + "resource": { + "resourceType": "Consent", + "id": "06070996-5004-11f0-a144-661e92ac9503", + "meta": { + "lastUpdated": "2025-06-23T09:31:05.965+02:00", + "profile": [ + "https://www.medizininformatik-initiative.de/fhir/modul-consent/StructureDefinition/mii-pr-consent-einwilligung" + ] + }, + "extension": [ + { + "url": "http://fhir.de/ConsentManagement/StructureDefinition/DomainReference", + "extension": [ + { + "url": "domain", + "valueReference": { + "reference": "ResearchStudy/90a1fcf9-5003-11f0-a144-661e92ac9503" + } + }, + { + "url": "status", + "valueCoding": { + "system": "http://hl7.org/fhir/publication-status", + "code": "active" + } + } + ] + } + ], + "status": "active", + "scope": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/consentscope", + "code": "research" + } + ] + }, + "category": [ + { + "coding": [ + { + "system": "http://loinc.org", + "code": "57016-8" + } + ] + }, + { + "coding": [ + { + "system": "http://fhir.de/ConsentManagement/CodeSystem/ResultType", + "code": "policy" + } + ] + }, + { + "coding": [ + { + "system": "https://www.medizininformatik-initiative.de/fhir/modul-consent/CodeSystem/mii-cs-consent-consent_category", + "code": "2.16.840.1.113883.3.1937.777.24.2.184" + } + ] + } + ], + "patient": { + "reference": "Patient/06052fed-5004-11f0-a144-661e92ac9503", + "display": "Patienten-ID 999999" + }, + "dateTime": "2025-06-23T00:00:00+02:00", + "organization": [ + { + "display": "MII" + } + ], + "sourceReference": { + "reference": "QuestionnaireResponse/06067759-5004-11f0-a144-661e92ac9503" + }, + "policy": [ + { + "uri": "2.16.840.1.113883.3.1937.777.24.2.184" + }, + { + "uri": "urn:oid:2.16.840.1.113883.3.1937.777.24.2.1790" + } + ], + "policyRule": { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.5", + "display": "IDAT_bereitstellen_EU_DSGVO_NIVEAU" + } + ] + }, + "provision": { + "type": "deny", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2055-06-23T00:00:00+02:00" + }, + "provision": [ + { + "type": "deny", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2055-06-23T00:00:00+02:00" + }, + "code": [ + { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.5", + "display": "IDAT_bereitstellen_EU_DSGVO_NIVEAU" + } + ] + } + ] + } + ] + } + } + }, + { + "fullUrl": "http://127.0.0.1:8090/ttp-fhir/fhir/gics/Consent/0606f6f0-5004-11f0-a144-661e92ac9503", + "resource": { + "resourceType": "Consent", + "id": "0606f6f0-5004-11f0-a144-661e92ac9503", + "meta": { + "lastUpdated": "2025-06-23T09:31:05.965+02:00", + "profile": [ + "https://www.medizininformatik-initiative.de/fhir/modul-consent/StructureDefinition/mii-pr-consent-einwilligung" + ] + }, + "extension": [ + { + "url": "http://fhir.de/ConsentManagement/StructureDefinition/DomainReference", + "extension": [ + { + "url": "domain", + "valueReference": { + "reference": "ResearchStudy/90a1fcf9-5003-11f0-a144-661e92ac9503" + } + }, + { + "url": "status", + "valueCoding": { + "system": "http://hl7.org/fhir/publication-status", + "code": "active" + } + } + ] + } + ], + "status": "active", + "scope": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/consentscope", + "code": "research" + } + ] + }, + "category": [ + { + "coding": [ + { + "system": "http://loinc.org", + "code": "57016-8" + } + ] + }, + { + "coding": [ + { + "system": "http://fhir.de/ConsentManagement/CodeSystem/ResultType", + "code": "policy" + } + ] + }, + { + "coding": [ + { + "system": "https://www.medizininformatik-initiative.de/fhir/modul-consent/CodeSystem/mii-cs-consent-consent_category", + "code": "2.16.840.1.113883.3.1937.777.24.2.184" + } + ] + } + ], + "patient": { + "reference": "Patient/06052fed-5004-11f0-a144-661e92ac9503", + "display": "Patienten-ID 999999" + }, + "dateTime": "2025-06-23T00:00:00+02:00", + "organization": [ + { + "display": "MII" + } + ], + "sourceReference": { + "reference": "QuestionnaireResponse/06067759-5004-11f0-a144-661e92ac9503" + }, + "policy": [ + { + "uri": "2.16.840.1.113883.3.1937.777.24.2.184" + }, + { + "uri": "urn:oid:2.16.840.1.113883.3.1937.777.24.2.1790" + } + ], + "policyRule": { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.7", + "display": "MDAT_speichern_verarbeiten" + } + ] + }, + "provision": { + "type": "deny", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2055-06-23T00:00:00+02:00" + }, + "provision": [ + { + "type": "deny", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2055-06-23T00:00:00+02:00" + }, + "code": [ + { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.7", + "display": "MDAT_speichern_verarbeiten" + } + ] + } + ] + } + ] + } + } + }, + { + "fullUrl": "http://127.0.0.1:8090/ttp-fhir/fhir/gics/Consent/060718e6-5004-11f0-a144-661e92ac9503", + "resource": { + "resourceType": "Consent", + "id": "060718e6-5004-11f0-a144-661e92ac9503", + "meta": { + "lastUpdated": "2025-06-23T09:31:05.965+02:00", + "profile": [ + "https://www.medizininformatik-initiative.de/fhir/modul-consent/StructureDefinition/mii-pr-consent-einwilligung" + ] + }, + "extension": [ + { + "url": "http://fhir.de/ConsentManagement/StructureDefinition/DomainReference", + "extension": [ + { + "url": "domain", + "valueReference": { + "reference": "ResearchStudy/90a1fcf9-5003-11f0-a144-661e92ac9503" + } + }, + { + "url": "status", + "valueCoding": { + "system": "http://hl7.org/fhir/publication-status", + "code": "active" + } + } + ] + } + ], + "status": "active", + "scope": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/consentscope", + "code": "research" + } + ] + }, + "category": [ + { + "coding": [ + { + "system": "http://loinc.org", + "code": "57016-8" + } + ] + }, + { + "coding": [ + { + "system": "http://fhir.de/ConsentManagement/CodeSystem/ResultType", + "code": "policy" + } + ] + }, + { + "coding": [ + { + "system": "https://www.medizininformatik-initiative.de/fhir/modul-consent/CodeSystem/mii-cs-consent-consent_category", + "code": "2.16.840.1.113883.3.1937.777.24.2.184" + } + ] + } + ], + "patient": { + "reference": "Patient/06052fed-5004-11f0-a144-661e92ac9503", + "display": "Patienten-ID 999999" + }, + "dateTime": "2025-06-23T00:00:00+02:00", + "organization": [ + { + "display": "MII" + } + ], + "sourceReference": { + "reference": "QuestionnaireResponse/06067759-5004-11f0-a144-661e92ac9503" + }, + "policy": [ + { + "uri": "2.16.840.1.113883.3.1937.777.24.2.184" + }, + { + "uri": "urn:oid:2.16.840.1.113883.3.1937.777.24.2.1790" + } + ], + "policyRule": { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.3", + "display": "IDAT_speichern_verarbeiten" + } + ] + }, + "provision": { + "type": "deny", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2055-06-23T00:00:00+02:00" + }, + "provision": [ + { + "type": "deny", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2055-06-23T00:00:00+02:00" + }, + "code": [ + { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.3", + "display": "IDAT_speichern_verarbeiten" + } + ] + } + ] + } + ] + } + } + }, + { + "fullUrl": "http://127.0.0.1:8090/ttp-fhir/fhir/gics/Consent/06072451-5004-11f0-a144-661e92ac9503", + "resource": { + "resourceType": "Consent", + "id": "06072451-5004-11f0-a144-661e92ac9503", + "meta": { + "lastUpdated": "2025-06-23T09:31:05.965+02:00", + "profile": [ + "https://www.medizininformatik-initiative.de/fhir/modul-consent/StructureDefinition/mii-pr-consent-einwilligung" + ] + }, + "extension": [ + { + "url": "http://fhir.de/ConsentManagement/StructureDefinition/DomainReference", + "extension": [ + { + "url": "domain", + "valueReference": { + "reference": "ResearchStudy/90a1fcf9-5003-11f0-a144-661e92ac9503" + } + }, + { + "url": "status", + "valueCoding": { + "system": "http://hl7.org/fhir/publication-status", + "code": "active" + } + } + ] + } + ], + "status": "active", + "scope": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/consentscope", + "code": "research" + } + ] + }, + "category": [ + { + "coding": [ + { + "system": "http://loinc.org", + "code": "57016-8" + } + ] + }, + { + "coding": [ + { + "system": "http://fhir.de/ConsentManagement/CodeSystem/ResultType", + "code": "policy" + } + ] + }, + { + "coding": [ + { + "system": "https://www.medizininformatik-initiative.de/fhir/modul-consent/CodeSystem/mii-cs-consent-consent_category", + "code": "2.16.840.1.113883.3.1937.777.24.2.184" + } + ] + } + ], + "patient": { + "reference": "Patient/06052fed-5004-11f0-a144-661e92ac9503", + "display": "Patienten-ID 999999" + }, + "dateTime": "2025-06-23T00:00:00+02:00", + "organization": [ + { + "display": "MII" + } + ], + "sourceReference": { + "reference": "QuestionnaireResponse/06067759-5004-11f0-a144-661e92ac9503" + }, + "policy": [ + { + "uri": "2.16.840.1.113883.3.1937.777.24.2.184" + }, + { + "uri": "urn:oid:2.16.840.1.113883.3.1937.777.24.2.1790" + } + ], + "policyRule": { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.37", + "display": "Rekontaktierung_Ergebnisse_erheblicher_Bedeutung" + } + ] + }, + "provision": { + "type": "deny", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2055-06-23T00:00:00+02:00" + }, + "provision": [ + { + "type": "deny", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2055-06-23T00:00:00+02:00" + }, + "code": [ + { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.37", + "display": "Rekontaktierung_Ergebnisse_erheblicher_Bedeutung" + } + ] + } + ] + } + ] + } + } + }, + { + "fullUrl": "http://127.0.0.1:8090/ttp-fhir/fhir/gics/Consent/06072dc8-5004-11f0-a144-661e92ac9503", + "resource": { + "resourceType": "Consent", + "id": "06072dc8-5004-11f0-a144-661e92ac9503", + "meta": { + "lastUpdated": "2025-06-23T09:31:05.965+02:00", + "profile": [ + "https://www.medizininformatik-initiative.de/fhir/modul-consent/StructureDefinition/mii-pr-consent-einwilligung" + ] + }, + "extension": [ + { + "url": "http://fhir.de/ConsentManagement/StructureDefinition/DomainReference", + "extension": [ + { + "url": "domain", + "valueReference": { + "reference": "ResearchStudy/90a1fcf9-5003-11f0-a144-661e92ac9503" + } + }, + { + "url": "status", + "valueCoding": { + "system": "http://hl7.org/fhir/publication-status", + "code": "active" + } + } + ] + } + ], + "status": "active", + "scope": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/consentscope", + "code": "research" + } + ] + }, + "category": [ + { + "coding": [ + { + "system": "http://loinc.org", + "code": "57016-8" + } + ] + }, + { + "coding": [ + { + "system": "http://fhir.de/ConsentManagement/CodeSystem/ResultType", + "code": "policy" + } + ] + }, + { + "coding": [ + { + "system": "https://www.medizininformatik-initiative.de/fhir/modul-consent/CodeSystem/mii-cs-consent-consent_category", + "code": "2.16.840.1.113883.3.1937.777.24.2.184" + } + ] + } + ], + "patient": { + "reference": "Patient/06052fed-5004-11f0-a144-661e92ac9503", + "display": "Patienten-ID 999999" + }, + "dateTime": "2025-06-23T00:00:00+02:00", + "organization": [ + { + "display": "MII" + } + ], + "sourceReference": { + "reference": "QuestionnaireResponse/06067759-5004-11f0-a144-661e92ac9503" + }, + "policy": [ + { + "uri": "2.16.840.1.113883.3.1937.777.24.2.184" + }, + { + "uri": "urn:oid:2.16.840.1.113883.3.1937.777.24.2.1790" + } + ], + "policyRule": { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.28", + "display": "Rekontaktierung_weitere_Erhebung" + } + ] + }, + "provision": { + "type": "deny", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2055-06-23T00:00:00+02:00" + }, + "provision": [ + { + "type": "deny", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2055-06-23T00:00:00+02:00" + }, + "code": [ + { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.28", + "display": "Rekontaktierung_weitere_Erhebung" + } + ] + } + ] + } + ] + } + } + }, + { + "fullUrl": "http://127.0.0.1:8090/ttp-fhir/fhir/gics/Consent/06070362-5004-11f0-a144-661e92ac9503", + "resource": { + "resourceType": "Consent", + "id": "06070362-5004-11f0-a144-661e92ac9503", + "meta": { + "lastUpdated": "2025-06-23T09:31:05.965+02:00", + "profile": [ + "https://www.medizininformatik-initiative.de/fhir/modul-consent/StructureDefinition/mii-pr-consent-einwilligung" + ] + }, + "extension": [ + { + "url": "http://fhir.de/ConsentManagement/StructureDefinition/DomainReference", + "extension": [ + { + "url": "domain", + "valueReference": { + "reference": "ResearchStudy/90a1fcf9-5003-11f0-a144-661e92ac9503" + } + }, + { + "url": "status", + "valueCoding": { + "system": "http://hl7.org/fhir/publication-status", + "code": "active" + } + } + ] + } + ], + "status": "active", + "scope": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/consentscope", + "code": "research" + } + ] + }, + "category": [ + { + "coding": [ + { + "system": "http://loinc.org", + "code": "57016-8" + } + ] + }, + { + "coding": [ + { + "system": "http://fhir.de/ConsentManagement/CodeSystem/ResultType", + "code": "policy" + } + ] + }, + { + "coding": [ + { + "system": "https://www.medizininformatik-initiative.de/fhir/modul-consent/CodeSystem/mii-cs-consent-consent_category", + "code": "2.16.840.1.113883.3.1937.777.24.2.184" + } + ] + } + ], + "patient": { + "reference": "Patient/06052fed-5004-11f0-a144-661e92ac9503", + "display": "Patienten-ID 999999" + }, + "dateTime": "2025-06-23T00:00:00+02:00", + "organization": [ + { + "display": "MII" + } + ], + "sourceReference": { + "reference": "QuestionnaireResponse/06067759-5004-11f0-a144-661e92ac9503" + }, + "policy": [ + { + "uri": "2.16.840.1.113883.3.1937.777.24.2.184" + }, + { + "uri": "urn:oid:2.16.840.1.113883.3.1937.777.24.2.1790" + } + ], + "policyRule": { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.9", + "display": "MDAT_zusammenfuehren_Dritte" + } + ] + }, + "provision": { + "type": "deny", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2055-06-23T00:00:00+02:00" + }, + "provision": [ + { + "type": "deny", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2055-06-23T00:00:00+02:00" + }, + "code": [ + { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.9", + "display": "MDAT_zusammenfuehren_Dritte" + } + ] + } + ] + } + ] + } + } + }, + { + "fullUrl": "http://127.0.0.1:8090/ttp-fhir/fhir/gics/Consent/06071f66-5004-11f0-a144-661e92ac9503", + "resource": { + "resourceType": "Consent", + "id": "06071f66-5004-11f0-a144-661e92ac9503", + "meta": { + "lastUpdated": "2025-06-23T09:31:05.965+02:00", + "profile": [ + "https://www.medizininformatik-initiative.de/fhir/modul-consent/StructureDefinition/mii-pr-consent-einwilligung" + ] + }, + "extension": [ + { + "url": "http://fhir.de/ConsentManagement/StructureDefinition/DomainReference", + "extension": [ + { + "url": "domain", + "valueReference": { + "reference": "ResearchStudy/90a1fcf9-5003-11f0-a144-661e92ac9503" + } + }, + { + "url": "status", + "valueCoding": { + "system": "http://hl7.org/fhir/publication-status", + "code": "active" + } + } + ] + } + ], + "status": "active", + "scope": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/consentscope", + "code": "research" + } + ] + }, + "category": [ + { + "coding": [ + { + "system": "http://loinc.org", + "code": "57016-8" + } + ] + }, + { + "coding": [ + { + "system": "http://fhir.de/ConsentManagement/CodeSystem/ResultType", + "code": "policy" + } + ] + }, + { + "coding": [ + { + "system": "https://www.medizininformatik-initiative.de/fhir/modul-consent/CodeSystem/mii-cs-consent-consent_category", + "code": "2.16.840.1.113883.3.1937.777.24.2.184" + } + ] + } + ], + "patient": { + "reference": "Patient/06052fed-5004-11f0-a144-661e92ac9503", + "display": "Patienten-ID 999999" + }, + "dateTime": "2025-06-23T00:00:00+02:00", + "organization": [ + { + "display": "MII" + } + ], + "sourceReference": { + "reference": "QuestionnaireResponse/06067759-5004-11f0-a144-661e92ac9503" + }, + "policy": [ + { + "uri": "2.16.840.1.113883.3.1937.777.24.2.184" + }, + { + "uri": "urn:oid:2.16.840.1.113883.3.1937.777.24.2.1790" + } + ], + "policyRule": { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.4", + "display": "IDAT_zusammenfuehren_Dritte" + } + ] + }, + "provision": { + "type": "deny", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2055-06-23T00:00:00+02:00" + }, + "provision": [ + { + "type": "deny", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2055-06-23T00:00:00+02:00" + }, + "code": [ + { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.4", + "display": "IDAT_zusammenfuehren_Dritte" + } + ] + } + ] + } + ] + } + } + } + ] +} \ No newline at end of file diff --git a/src/test/resources/fake_broadConsent_gics_response_permit.json b/src/test/resources/fake_broadConsent_gics_response_permit.json new file mode 100644 index 0000000..b38c459 --- /dev/null +++ b/src/test/resources/fake_broadConsent_gics_response_permit.json @@ -0,0 +1,1631 @@ +{ + "resourceType": "Bundle", + "type": "collection", + "entry": [ + { + "fullUrl": "http://127.0.0.1:8090/ttp-fhir/fhir/gics/Consent/d61782bc-5003-11f0-a144-661e92ac9503", + "resource": { + "resourceType": "Consent", + "id": "d61782bc-5003-11f0-a144-661e92ac9503", + "meta": { + "lastUpdated": "2025-06-23T09:29:45.532+02:00", + "profile": [ + "https://www.medizininformatik-initiative.de/fhir/modul-consent/StructureDefinition/mii-pr-consent-einwilligung" + ] + }, + "extension": [ + { + "url": "http://fhir.de/ConsentManagement/StructureDefinition/DomainReference", + "extension": [ + { + "url": "domain", + "valueReference": { + "reference": "ResearchStudy/90a1fcf9-5003-11f0-a144-661e92ac9503" + } + }, + { + "url": "status", + "valueCoding": { + "system": "http://hl7.org/fhir/publication-status", + "code": "active" + } + } + ] + } + ], + "status": "active", + "scope": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/consentscope", + "code": "research" + } + ] + }, + "category": [ + { + "coding": [ + { + "system": "http://loinc.org", + "code": "57016-8" + } + ] + }, + { + "coding": [ + { + "system": "http://fhir.de/ConsentManagement/CodeSystem/ResultType", + "code": "policy" + } + ] + }, + { + "coding": [ + { + "system": "https://www.medizininformatik-initiative.de/fhir/modul-consent/CodeSystem/mii-cs-consent-consent_category", + "code": "2.16.840.1.113883.3.1937.777.24.2.184" + } + ] + } + ], + "patient": { + "reference": "Patient/d611d429-5003-11f0-a144-661e92ac9503", + "display": "Patienten-ID 12345678" + }, + "dateTime": "2025-06-23T00:00:00+02:00", + "organization": [ + { + "display": "MII" + } + ], + "sourceReference": { + "reference": "QuestionnaireResponse/d615e32f-5003-11f0-a144-661e92ac9503" + }, + "policy": [ + { + "uri": "2.16.840.1.113883.3.1937.777.24.2.184" + }, + { + "uri": "urn:oid:2.16.840.1.113883.3.1937.777.24.2.1790" + } + ], + "policyRule": { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.2", + "display": "IDAT_erheben" + } + ] + }, + "provision": { + "type": "deny", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2055-06-23T00:00:00+02:00" + }, + "provision": [ + { + "type": "permit", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2055-06-23T00:00:00+02:00" + }, + "code": [ + { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.2", + "display": "IDAT_erheben" + } + ] + } + ] + } + ] + } + } + }, + { + "fullUrl": "http://127.0.0.1:8090/ttp-fhir/fhir/gics/Consent/d618b6e4-5003-11f0-a144-661e92ac9503", + "resource": { + "resourceType": "Consent", + "id": "d618b6e4-5003-11f0-a144-661e92ac9503", + "meta": { + "lastUpdated": "2025-06-23T09:29:45.532+02:00", + "profile": [ + "https://www.medizininformatik-initiative.de/fhir/modul-consent/StructureDefinition/mii-pr-consent-einwilligung" + ] + }, + "extension": [ + { + "url": "http://fhir.de/ConsentManagement/StructureDefinition/DomainReference", + "extension": [ + { + "url": "domain", + "valueReference": { + "reference": "ResearchStudy/90a1fcf9-5003-11f0-a144-661e92ac9503" + } + }, + { + "url": "status", + "valueCoding": { + "system": "http://hl7.org/fhir/publication-status", + "code": "active" + } + } + ] + } + ], + "status": "active", + "scope": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/consentscope", + "code": "research" + } + ] + }, + "category": [ + { + "coding": [ + { + "system": "http://loinc.org", + "code": "57016-8" + } + ] + }, + { + "coding": [ + { + "system": "http://fhir.de/ConsentManagement/CodeSystem/ResultType", + "code": "policy" + } + ] + }, + { + "coding": [ + { + "system": "https://www.medizininformatik-initiative.de/fhir/modul-consent/CodeSystem/mii-cs-consent-consent_category", + "code": "2.16.840.1.113883.3.1937.777.24.2.184" + } + ] + } + ], + "patient": { + "reference": "Patient/d611d429-5003-11f0-a144-661e92ac9503", + "display": "Patienten-ID 12345678" + }, + "dateTime": "2025-06-23T00:00:00+02:00", + "organization": [ + { + "display": "MII" + } + ], + "sourceReference": { + "reference": "QuestionnaireResponse/d615e32f-5003-11f0-a144-661e92ac9503" + }, + "policy": [ + { + "uri": "2.16.840.1.113883.3.1937.777.24.2.184" + }, + { + "uri": "urn:oid:2.16.840.1.113883.3.1937.777.24.2.1790" + } + ], + "policyRule": { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.31", + "display": "Rekontaktierung_Zusatzbefund" + } + ] + }, + "provision": { + "type": "deny", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2055-06-23T00:00:00+02:00" + }, + "provision": [ + { + "type": "permit", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2055-06-23T00:00:00+02:00" + }, + "code": [ + { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.31", + "display": "Rekontaktierung_Zusatzbefund" + } + ] + } + ] + } + ] + } + } + }, + { + "fullUrl": "http://127.0.0.1:8090/ttp-fhir/fhir/gics/Consent/d6180058-5003-11f0-a144-661e92ac9503", + "resource": { + "resourceType": "Consent", + "id": "d6180058-5003-11f0-a144-661e92ac9503", + "meta": { + "lastUpdated": "2025-06-23T09:29:45.532+02:00", + "profile": [ + "https://www.medizininformatik-initiative.de/fhir/modul-consent/StructureDefinition/mii-pr-consent-einwilligung" + ] + }, + "extension": [ + { + "url": "http://fhir.de/ConsentManagement/StructureDefinition/DomainReference", + "extension": [ + { + "url": "domain", + "valueReference": { + "reference": "ResearchStudy/90a1fcf9-5003-11f0-a144-661e92ac9503" + } + }, + { + "url": "status", + "valueCoding": { + "system": "http://hl7.org/fhir/publication-status", + "code": "active" + } + } + ] + } + ], + "status": "active", + "scope": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/consentscope", + "code": "research" + } + ] + }, + "category": [ + { + "coding": [ + { + "system": "http://loinc.org", + "code": "57016-8" + } + ] + }, + { + "coding": [ + { + "system": "http://fhir.de/ConsentManagement/CodeSystem/ResultType", + "code": "policy" + } + ] + }, + { + "coding": [ + { + "system": "https://www.medizininformatik-initiative.de/fhir/modul-consent/CodeSystem/mii-cs-consent-consent_category", + "code": "2.16.840.1.113883.3.1937.777.24.2.184" + } + ] + } + ], + "patient": { + "reference": "Patient/d611d429-5003-11f0-a144-661e92ac9503", + "display": "Patienten-ID 12345678" + }, + "dateTime": "2025-06-23T00:00:00+02:00", + "organization": [ + { + "display": "MII" + } + ], + "sourceReference": { + "reference": "QuestionnaireResponse/d615e32f-5003-11f0-a144-661e92ac9503" + }, + "policy": [ + { + "uri": "2.16.840.1.113883.3.1937.777.24.2.184" + }, + { + "uri": "urn:oid:2.16.840.1.113883.3.1937.777.24.2.1790" + } + ], + "policyRule": { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.6", + "display": "MDAT_erheben" + } + ] + }, + "provision": { + "type": "deny", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2030-06-23T00:00:00+02:00" + }, + "provision": [ + { + "type": "permit", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2030-06-23T00:00:00+02:00" + }, + "code": [ + { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.6", + "display": "MDAT_erheben" + } + ] + } + ] + } + ] + } + } + }, + { + "fullUrl": "http://127.0.0.1:8090/ttp-fhir/fhir/gics/Consent/d617e5c3-5003-11f0-a144-661e92ac9503", + "resource": { + "resourceType": "Consent", + "id": "d617e5c3-5003-11f0-a144-661e92ac9503", + "meta": { + "lastUpdated": "2025-06-23T09:29:45.532+02:00", + "profile": [ + "https://www.medizininformatik-initiative.de/fhir/modul-consent/StructureDefinition/mii-pr-consent-einwilligung" + ] + }, + "extension": [ + { + "url": "http://fhir.de/ConsentManagement/StructureDefinition/DomainReference", + "extension": [ + { + "url": "domain", + "valueReference": { + "reference": "ResearchStudy/90a1fcf9-5003-11f0-a144-661e92ac9503" + } + }, + { + "url": "status", + "valueCoding": { + "system": "http://hl7.org/fhir/publication-status", + "code": "active" + } + } + ] + } + ], + "status": "active", + "scope": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/consentscope", + "code": "research" + } + ] + }, + "category": [ + { + "coding": [ + { + "system": "http://loinc.org", + "code": "57016-8" + } + ] + }, + { + "coding": [ + { + "system": "http://fhir.de/ConsentManagement/CodeSystem/ResultType", + "code": "policy" + } + ] + }, + { + "coding": [ + { + "system": "https://www.medizininformatik-initiative.de/fhir/modul-consent/CodeSystem/mii-cs-consent-consent_category", + "code": "2.16.840.1.113883.3.1937.777.24.2.184" + } + ] + } + ], + "patient": { + "reference": "Patient/d611d429-5003-11f0-a144-661e92ac9503", + "display": "Patienten-ID 12345678" + }, + "dateTime": "2025-06-23T00:00:00+02:00", + "organization": [ + { + "display": "MII" + } + ], + "sourceReference": { + "reference": "QuestionnaireResponse/d615e32f-5003-11f0-a144-661e92ac9503" + }, + "policy": [ + { + "uri": "2.16.840.1.113883.3.1937.777.24.2.184" + }, + { + "uri": "urn:oid:2.16.840.1.113883.3.1937.777.24.2.1790" + } + ], + "policyRule": { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.8", + "display": "MDAT_wissenschaftlich_nutzen_EU_DSGVO_NIVEAU" + } + ] + }, + "provision": { + "type": "deny", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2055-06-23T00:00:00+02:00" + }, + "provision": [ + { + "type": "permit", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2055-06-23T00:00:00+02:00" + }, + "code": [ + { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.8", + "display": "MDAT_wissenschaftlich_nutzen_EU_DSGVO_NIVEAU" + } + ] + } + ] + } + ] + } + } + }, + { + "fullUrl": "http://127.0.0.1:8090/ttp-fhir/fhir/gics/Consent/d6186042-5003-11f0-a144-661e92ac9503", + "resource": { + "resourceType": "Consent", + "id": "d6186042-5003-11f0-a144-661e92ac9503", + "meta": { + "lastUpdated": "2025-06-23T09:29:45.532+02:00", + "profile": [ + "https://www.medizininformatik-initiative.de/fhir/modul-consent/StructureDefinition/mii-pr-consent-einwilligung" + ] + }, + "extension": [ + { + "url": "http://fhir.de/ConsentManagement/StructureDefinition/DomainReference", + "extension": [ + { + "url": "domain", + "valueReference": { + "reference": "ResearchStudy/90a1fcf9-5003-11f0-a144-661e92ac9503" + } + }, + { + "url": "status", + "valueCoding": { + "system": "http://hl7.org/fhir/publication-status", + "code": "active" + } + } + ] + } + ], + "status": "active", + "scope": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/consentscope", + "code": "research" + } + ] + }, + "category": [ + { + "coding": [ + { + "system": "http://loinc.org", + "code": "57016-8" + } + ] + }, + { + "coding": [ + { + "system": "http://fhir.de/ConsentManagement/CodeSystem/ResultType", + "code": "policy" + } + ] + }, + { + "coding": [ + { + "system": "https://www.medizininformatik-initiative.de/fhir/modul-consent/CodeSystem/mii-cs-consent-consent_category", + "code": "2.16.840.1.113883.3.1937.777.24.2.184" + } + ] + } + ], + "patient": { + "reference": "Patient/d611d429-5003-11f0-a144-661e92ac9503", + "display": "Patienten-ID 12345678" + }, + "dateTime": "2025-06-23T00:00:00+02:00", + "organization": [ + { + "display": "MII" + } + ], + "sourceReference": { + "reference": "QuestionnaireResponse/d615e32f-5003-11f0-a144-661e92ac9503" + }, + "policy": [ + { + "uri": "2.16.840.1.113883.3.1937.777.24.2.184" + }, + { + "uri": "urn:oid:2.16.840.1.113883.3.1937.777.24.2.1790" + } + ], + "policyRule": { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.27", + "display": "Rekontaktierung_Verknuepfung_Datenbanken" + } + ] + }, + "provision": { + "type": "deny", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2055-06-23T00:00:00+02:00" + }, + "provision": [ + { + "type": "deny", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2055-06-23T00:00:00+02:00" + }, + "code": [ + { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.27", + "display": "Rekontaktierung_Verknuepfung_Datenbanken" + } + ] + } + ] + } + ] + } + } + }, + { + "fullUrl": "http://127.0.0.1:8090/ttp-fhir/fhir/gics/Consent/d618abf6-5003-11f0-a144-661e92ac9503", + "resource": { + "resourceType": "Consent", + "id": "d618abf6-5003-11f0-a144-661e92ac9503", + "meta": { + "lastUpdated": "2025-06-23T09:29:45.532+02:00", + "profile": [ + "https://www.medizininformatik-initiative.de/fhir/modul-consent/StructureDefinition/mii-pr-consent-einwilligung" + ] + }, + "extension": [ + { + "url": "http://fhir.de/ConsentManagement/StructureDefinition/DomainReference", + "extension": [ + { + "url": "domain", + "valueReference": { + "reference": "ResearchStudy/90a1fcf9-5003-11f0-a144-661e92ac9503" + } + }, + { + "url": "status", + "valueCoding": { + "system": "http://hl7.org/fhir/publication-status", + "code": "active" + } + } + ] + } + ], + "status": "active", + "scope": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/consentscope", + "code": "research" + } + ] + }, + "category": [ + { + "coding": [ + { + "system": "http://loinc.org", + "code": "57016-8" + } + ] + }, + { + "coding": [ + { + "system": "http://fhir.de/ConsentManagement/CodeSystem/ResultType", + "code": "policy" + } + ] + }, + { + "coding": [ + { + "system": "https://www.medizininformatik-initiative.de/fhir/modul-consent/CodeSystem/mii-cs-consent-consent_category", + "code": "2.16.840.1.113883.3.1937.777.24.2.184" + } + ] + } + ], + "patient": { + "reference": "Patient/d611d429-5003-11f0-a144-661e92ac9503", + "display": "Patienten-ID 12345678" + }, + "dateTime": "2025-06-23T00:00:00+02:00", + "organization": [ + { + "display": "MII" + } + ], + "sourceReference": { + "reference": "QuestionnaireResponse/d615e32f-5003-11f0-a144-661e92ac9503" + }, + "policy": [ + { + "uri": "2.16.840.1.113883.3.1937.777.24.2.184" + }, + { + "uri": "urn:oid:2.16.840.1.113883.3.1937.777.24.2.1790" + } + ], + "policyRule": { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.29", + "display": "Rekontaktierung_weitere_Studien" + } + ] + }, + "provision": { + "type": "deny", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2055-06-23T00:00:00+02:00" + }, + "provision": [ + { + "type": "permit", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2055-06-23T00:00:00+02:00" + }, + "code": [ + { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.29", + "display": "Rekontaktierung_weitere_Studien" + } + ] + } + ] + } + ] + } + } + }, + { + "fullUrl": "http://127.0.0.1:8090/ttp-fhir/fhir/gics/Consent/d618341d-5003-11f0-a144-661e92ac9503", + "resource": { + "resourceType": "Consent", + "id": "d618341d-5003-11f0-a144-661e92ac9503", + "meta": { + "lastUpdated": "2025-06-23T09:29:45.532+02:00", + "profile": [ + "https://www.medizininformatik-initiative.de/fhir/modul-consent/StructureDefinition/mii-pr-consent-einwilligung" + ] + }, + "extension": [ + { + "url": "http://fhir.de/ConsentManagement/StructureDefinition/DomainReference", + "extension": [ + { + "url": "domain", + "valueReference": { + "reference": "ResearchStudy/90a1fcf9-5003-11f0-a144-661e92ac9503" + } + }, + { + "url": "status", + "valueCoding": { + "system": "http://hl7.org/fhir/publication-status", + "code": "active" + } + } + ] + } + ], + "status": "active", + "scope": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/consentscope", + "code": "research" + } + ] + }, + "category": [ + { + "coding": [ + { + "system": "http://loinc.org", + "code": "57016-8" + } + ] + }, + { + "coding": [ + { + "system": "http://fhir.de/ConsentManagement/CodeSystem/ResultType", + "code": "policy" + } + ] + }, + { + "coding": [ + { + "system": "https://www.medizininformatik-initiative.de/fhir/modul-consent/CodeSystem/mii-cs-consent-consent_category", + "code": "2.16.840.1.113883.3.1937.777.24.2.184" + } + ] + } + ], + "patient": { + "reference": "Patient/d611d429-5003-11f0-a144-661e92ac9503", + "display": "Patienten-ID 12345678" + }, + "dateTime": "2025-06-23T00:00:00+02:00", + "organization": [ + { + "display": "MII" + } + ], + "sourceReference": { + "reference": "QuestionnaireResponse/d615e32f-5003-11f0-a144-661e92ac9503" + }, + "policy": [ + { + "uri": "2.16.840.1.113883.3.1937.777.24.2.184" + }, + { + "uri": "urn:oid:2.16.840.1.113883.3.1937.777.24.2.1790" + } + ], + "policyRule": { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.5", + "display": "IDAT_bereitstellen_EU_DSGVO_NIVEAU" + } + ] + }, + "provision": { + "type": "deny", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2055-06-23T00:00:00+02:00" + }, + "provision": [ + { + "type": "permit", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2055-06-23T00:00:00+02:00" + }, + "code": [ + { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.5", + "display": "IDAT_bereitstellen_EU_DSGVO_NIVEAU" + } + ] + } + ] + } + ] + } + } + }, + { + "fullUrl": "http://127.0.0.1:8090/ttp-fhir/fhir/gics/Consent/d61817be-5003-11f0-a144-661e92ac9503", + "resource": { + "resourceType": "Consent", + "id": "d61817be-5003-11f0-a144-661e92ac9503", + "meta": { + "lastUpdated": "2025-06-23T09:29:45.532+02:00", + "profile": [ + "https://www.medizininformatik-initiative.de/fhir/modul-consent/StructureDefinition/mii-pr-consent-einwilligung" + ] + }, + "extension": [ + { + "url": "http://fhir.de/ConsentManagement/StructureDefinition/DomainReference", + "extension": [ + { + "url": "domain", + "valueReference": { + "reference": "ResearchStudy/90a1fcf9-5003-11f0-a144-661e92ac9503" + } + }, + { + "url": "status", + "valueCoding": { + "system": "http://hl7.org/fhir/publication-status", + "code": "active" + } + } + ] + } + ], + "status": "active", + "scope": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/consentscope", + "code": "research" + } + ] + }, + "category": [ + { + "coding": [ + { + "system": "http://loinc.org", + "code": "57016-8" + } + ] + }, + { + "coding": [ + { + "system": "http://fhir.de/ConsentManagement/CodeSystem/ResultType", + "code": "policy" + } + ] + }, + { + "coding": [ + { + "system": "https://www.medizininformatik-initiative.de/fhir/modul-consent/CodeSystem/mii-cs-consent-consent_category", + "code": "2.16.840.1.113883.3.1937.777.24.2.184" + } + ] + } + ], + "patient": { + "reference": "Patient/d611d429-5003-11f0-a144-661e92ac9503", + "display": "Patienten-ID 12345678" + }, + "dateTime": "2025-06-23T00:00:00+02:00", + "organization": [ + { + "display": "MII" + } + ], + "sourceReference": { + "reference": "QuestionnaireResponse/d615e32f-5003-11f0-a144-661e92ac9503" + }, + "policy": [ + { + "uri": "2.16.840.1.113883.3.1937.777.24.2.184" + }, + { + "uri": "urn:oid:2.16.840.1.113883.3.1937.777.24.2.1790" + } + ], + "policyRule": { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.7", + "display": "MDAT_speichern_verarbeiten" + } + ] + }, + "provision": { + "type": "deny", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2055-06-23T00:00:00+02:00" + }, + "provision": [ + { + "type": "permit", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2055-06-23T00:00:00+02:00" + }, + "code": [ + { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.7", + "display": "MDAT_speichern_verarbeiten" + } + ] + } + ] + } + ] + } + } + }, + { + "fullUrl": "http://127.0.0.1:8090/ttp-fhir/fhir/gics/Consent/d6183b46-5003-11f0-a144-661e92ac9503", + "resource": { + "resourceType": "Consent", + "id": "d6183b46-5003-11f0-a144-661e92ac9503", + "meta": { + "lastUpdated": "2025-06-23T09:29:45.532+02:00", + "profile": [ + "https://www.medizininformatik-initiative.de/fhir/modul-consent/StructureDefinition/mii-pr-consent-einwilligung" + ] + }, + "extension": [ + { + "url": "http://fhir.de/ConsentManagement/StructureDefinition/DomainReference", + "extension": [ + { + "url": "domain", + "valueReference": { + "reference": "ResearchStudy/90a1fcf9-5003-11f0-a144-661e92ac9503" + } + }, + { + "url": "status", + "valueCoding": { + "system": "http://hl7.org/fhir/publication-status", + "code": "active" + } + } + ] + } + ], + "status": "active", + "scope": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/consentscope", + "code": "research" + } + ] + }, + "category": [ + { + "coding": [ + { + "system": "http://loinc.org", + "code": "57016-8" + } + ] + }, + { + "coding": [ + { + "system": "http://fhir.de/ConsentManagement/CodeSystem/ResultType", + "code": "policy" + } + ] + }, + { + "coding": [ + { + "system": "https://www.medizininformatik-initiative.de/fhir/modul-consent/CodeSystem/mii-cs-consent-consent_category", + "code": "2.16.840.1.113883.3.1937.777.24.2.184" + } + ] + } + ], + "patient": { + "reference": "Patient/d611d429-5003-11f0-a144-661e92ac9503", + "display": "Patienten-ID 12345678" + }, + "dateTime": "2025-06-23T00:00:00+02:00", + "organization": [ + { + "display": "MII" + } + ], + "sourceReference": { + "reference": "QuestionnaireResponse/d615e32f-5003-11f0-a144-661e92ac9503" + }, + "policy": [ + { + "uri": "2.16.840.1.113883.3.1937.777.24.2.184" + }, + { + "uri": "urn:oid:2.16.840.1.113883.3.1937.777.24.2.1790" + } + ], + "policyRule": { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.3", + "display": "IDAT_speichern_verarbeiten" + } + ] + }, + "provision": { + "type": "deny", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2055-06-23T00:00:00+02:00" + }, + "provision": [ + { + "type": "permit", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2055-06-23T00:00:00+02:00" + }, + "code": [ + { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.3", + "display": "IDAT_speichern_verarbeiten" + } + ] + } + ] + } + ] + } + } + }, + { + "fullUrl": "http://127.0.0.1:8090/ttp-fhir/fhir/gics/Consent/d61848d9-5003-11f0-a144-661e92ac9503", + "resource": { + "resourceType": "Consent", + "id": "d61848d9-5003-11f0-a144-661e92ac9503", + "meta": { + "lastUpdated": "2025-06-23T09:29:45.532+02:00", + "profile": [ + "https://www.medizininformatik-initiative.de/fhir/modul-consent/StructureDefinition/mii-pr-consent-einwilligung" + ] + }, + "extension": [ + { + "url": "http://fhir.de/ConsentManagement/StructureDefinition/DomainReference", + "extension": [ + { + "url": "domain", + "valueReference": { + "reference": "ResearchStudy/90a1fcf9-5003-11f0-a144-661e92ac9503" + } + }, + { + "url": "status", + "valueCoding": { + "system": "http://hl7.org/fhir/publication-status", + "code": "active" + } + } + ] + } + ], + "status": "active", + "scope": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/consentscope", + "code": "research" + } + ] + }, + "category": [ + { + "coding": [ + { + "system": "http://loinc.org", + "code": "57016-8" + } + ] + }, + { + "coding": [ + { + "system": "http://fhir.de/ConsentManagement/CodeSystem/ResultType", + "code": "policy" + } + ] + }, + { + "coding": [ + { + "system": "https://www.medizininformatik-initiative.de/fhir/modul-consent/CodeSystem/mii-cs-consent-consent_category", + "code": "2.16.840.1.113883.3.1937.777.24.2.184" + } + ] + } + ], + "patient": { + "reference": "Patient/d611d429-5003-11f0-a144-661e92ac9503", + "display": "Patienten-ID 12345678" + }, + "dateTime": "2025-06-23T00:00:00+02:00", + "organization": [ + { + "display": "MII" + } + ], + "sourceReference": { + "reference": "QuestionnaireResponse/d615e32f-5003-11f0-a144-661e92ac9503" + }, + "policy": [ + { + "uri": "2.16.840.1.113883.3.1937.777.24.2.184" + }, + { + "uri": "urn:oid:2.16.840.1.113883.3.1937.777.24.2.1790" + } + ], + "policyRule": { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.37", + "display": "Rekontaktierung_Ergebnisse_erheblicher_Bedeutung" + } + ] + }, + "provision": { + "type": "deny", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2055-06-23T00:00:00+02:00" + }, + "provision": [ + { + "type": "permit", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2055-06-23T00:00:00+02:00" + }, + "code": [ + { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.37", + "display": "Rekontaktierung_Ergebnisse_erheblicher_Bedeutung" + } + ] + } + ] + } + ] + } + } + }, + { + "fullUrl": "http://127.0.0.1:8090/ttp-fhir/fhir/gics/Consent/d61886e7-5003-11f0-a144-661e92ac9503", + "resource": { + "resourceType": "Consent", + "id": "d61886e7-5003-11f0-a144-661e92ac9503", + "meta": { + "lastUpdated": "2025-06-23T09:29:45.532+02:00", + "profile": [ + "https://www.medizininformatik-initiative.de/fhir/modul-consent/StructureDefinition/mii-pr-consent-einwilligung" + ] + }, + "extension": [ + { + "url": "http://fhir.de/ConsentManagement/StructureDefinition/DomainReference", + "extension": [ + { + "url": "domain", + "valueReference": { + "reference": "ResearchStudy/90a1fcf9-5003-11f0-a144-661e92ac9503" + } + }, + { + "url": "status", + "valueCoding": { + "system": "http://hl7.org/fhir/publication-status", + "code": "active" + } + } + ] + } + ], + "status": "active", + "scope": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/consentscope", + "code": "research" + } + ] + }, + "category": [ + { + "coding": [ + { + "system": "http://loinc.org", + "code": "57016-8" + } + ] + }, + { + "coding": [ + { + "system": "http://fhir.de/ConsentManagement/CodeSystem/ResultType", + "code": "policy" + } + ] + }, + { + "coding": [ + { + "system": "https://www.medizininformatik-initiative.de/fhir/modul-consent/CodeSystem/mii-cs-consent-consent_category", + "code": "2.16.840.1.113883.3.1937.777.24.2.184" + } + ] + } + ], + "patient": { + "reference": "Patient/d611d429-5003-11f0-a144-661e92ac9503", + "display": "Patienten-ID 12345678" + }, + "dateTime": "2025-06-23T00:00:00+02:00", + "organization": [ + { + "display": "MII" + } + ], + "sourceReference": { + "reference": "QuestionnaireResponse/d615e32f-5003-11f0-a144-661e92ac9503" + }, + "policy": [ + { + "uri": "2.16.840.1.113883.3.1937.777.24.2.184" + }, + { + "uri": "urn:oid:2.16.840.1.113883.3.1937.777.24.2.1790" + } + ], + "policyRule": { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.28", + "display": "Rekontaktierung_weitere_Erhebung" + } + ] + }, + "provision": { + "type": "deny", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2055-06-23T00:00:00+02:00" + }, + "provision": [ + { + "type": "permit", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2055-06-23T00:00:00+02:00" + }, + "code": [ + { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.28", + "display": "Rekontaktierung_weitere_Erhebung" + } + ] + } + ] + } + ] + } + } + }, + { + "fullUrl": "http://127.0.0.1:8090/ttp-fhir/fhir/gics/Consent/d6182af5-5003-11f0-a144-661e92ac9503", + "resource": { + "resourceType": "Consent", + "id": "d6182af5-5003-11f0-a144-661e92ac9503", + "meta": { + "lastUpdated": "2025-06-23T09:29:45.532+02:00", + "profile": [ + "https://www.medizininformatik-initiative.de/fhir/modul-consent/StructureDefinition/mii-pr-consent-einwilligung" + ] + }, + "extension": [ + { + "url": "http://fhir.de/ConsentManagement/StructureDefinition/DomainReference", + "extension": [ + { + "url": "domain", + "valueReference": { + "reference": "ResearchStudy/90a1fcf9-5003-11f0-a144-661e92ac9503" + } + }, + { + "url": "status", + "valueCoding": { + "system": "http://hl7.org/fhir/publication-status", + "code": "active" + } + } + ] + } + ], + "status": "active", + "scope": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/consentscope", + "code": "research" + } + ] + }, + "category": [ + { + "coding": [ + { + "system": "http://loinc.org", + "code": "57016-8" + } + ] + }, + { + "coding": [ + { + "system": "http://fhir.de/ConsentManagement/CodeSystem/ResultType", + "code": "policy" + } + ] + }, + { + "coding": [ + { + "system": "https://www.medizininformatik-initiative.de/fhir/modul-consent/CodeSystem/mii-cs-consent-consent_category", + "code": "2.16.840.1.113883.3.1937.777.24.2.184" + } + ] + } + ], + "patient": { + "reference": "Patient/d611d429-5003-11f0-a144-661e92ac9503", + "display": "Patienten-ID 12345678" + }, + "dateTime": "2025-06-23T00:00:00+02:00", + "organization": [ + { + "display": "MII" + } + ], + "sourceReference": { + "reference": "QuestionnaireResponse/d615e32f-5003-11f0-a144-661e92ac9503" + }, + "policy": [ + { + "uri": "2.16.840.1.113883.3.1937.777.24.2.184" + }, + { + "uri": "urn:oid:2.16.840.1.113883.3.1937.777.24.2.1790" + } + ], + "policyRule": { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.9", + "display": "MDAT_zusammenfuehren_Dritte" + } + ] + }, + "provision": { + "type": "deny", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2055-06-23T00:00:00+02:00" + }, + "provision": [ + { + "type": "permit", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2055-06-23T00:00:00+02:00" + }, + "code": [ + { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.9", + "display": "MDAT_zusammenfuehren_Dritte" + } + ] + } + ] + } + ] + } + } + }, + { + "fullUrl": "http://127.0.0.1:8090/ttp-fhir/fhir/gics/Consent/d618422d-5003-11f0-a144-661e92ac9503", + "resource": { + "resourceType": "Consent", + "id": "d618422d-5003-11f0-a144-661e92ac9503", + "meta": { + "lastUpdated": "2025-06-23T09:29:45.532+02:00", + "profile": [ + "https://www.medizininformatik-initiative.de/fhir/modul-consent/StructureDefinition/mii-pr-consent-einwilligung" + ] + }, + "extension": [ + { + "url": "http://fhir.de/ConsentManagement/StructureDefinition/DomainReference", + "extension": [ + { + "url": "domain", + "valueReference": { + "reference": "ResearchStudy/90a1fcf9-5003-11f0-a144-661e92ac9503" + } + }, + { + "url": "status", + "valueCoding": { + "system": "http://hl7.org/fhir/publication-status", + "code": "active" + } + } + ] + } + ], + "status": "active", + "scope": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/consentscope", + "code": "research" + } + ] + }, + "category": [ + { + "coding": [ + { + "system": "http://loinc.org", + "code": "57016-8" + } + ] + }, + { + "coding": [ + { + "system": "http://fhir.de/ConsentManagement/CodeSystem/ResultType", + "code": "policy" + } + ] + }, + { + "coding": [ + { + "system": "https://www.medizininformatik-initiative.de/fhir/modul-consent/CodeSystem/mii-cs-consent-consent_category", + "code": "2.16.840.1.113883.3.1937.777.24.2.184" + } + ] + } + ], + "patient": { + "reference": "Patient/d611d429-5003-11f0-a144-661e92ac9503", + "display": "Patienten-ID 12345678" + }, + "dateTime": "2025-06-23T00:00:00+02:00", + "organization": [ + { + "display": "MII" + } + ], + "sourceReference": { + "reference": "QuestionnaireResponse/d615e32f-5003-11f0-a144-661e92ac9503" + }, + "policy": [ + { + "uri": "2.16.840.1.113883.3.1937.777.24.2.184" + }, + { + "uri": "urn:oid:2.16.840.1.113883.3.1937.777.24.2.1790" + } + ], + "policyRule": { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.4", + "display": "IDAT_zusammenfuehren_Dritte" + } + ] + }, + "provision": { + "type": "deny", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2055-06-23T00:00:00+02:00" + }, + "provision": [ + { + "type": "permit", + "period": { + "start": "2025-06-23T00:00:00+02:00", + "end": "2055-06-23T00:00:00+02:00" + }, + "code": [ + { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", + "code": "2.16.840.1.113883.3.1937.777.24.5.3.4", + "display": "IDAT_zusammenfuehren_Dritte" + } + ] + } + ] + } + ] + } + } + } + ] +} \ No newline at end of file diff --git a/src/test/resources/fake_mv64e-gics-response_deny.json b/src/test/resources/fake_mv64e-gics-response_deny.json new file mode 100644 index 0000000..1d39607 --- /dev/null +++ b/src/test/resources/fake_mv64e-gics-response_deny.json @@ -0,0 +1,333 @@ +{ + "resourceType": "Bundle", + "type": "collection", + "entry": [ + { + "fullUrl": "http://127.0.0.1:8090/ttp-fhir/fhir/gics/Consent/24673204-50e1-11f0-a144-661e92ac9503", + "resource": { + "resourceType": "Consent", + "id": "24673204-50e1-11f0-a144-661e92ac9503", + "meta": { + "lastUpdated": "2025-06-24T11:58:27.178+02:00", + "profile": [ + "http://fhir.de/ConsentManagement/StructureDefinition/Consent" + ] + }, + "extension": [ + { + "url": "http://fhir.de/ConsentManagement/StructureDefinition/DomainReference", + "extension": [ + { + "url": "domain", + "valueReference": { + "reference": "ResearchStudy/ef86d80e-50e0-11f0-a144-661e92ac9503" + } + }, + { + "url": "status", + "valueCoding": { + "system": "http://hl7.org/fhir/publication-status", + "code": "active" + } + } + ] + } + ], + "status": "active", + "scope": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/consentscope", + "code": "research" + } + ] + }, + "category": [ + { + "coding": [ + { + "system": "http://loinc.org", + "code": "59284-0" + } + ] + }, + { + "coding": [ + { + "system": "http://fhir.de/ConsentManagement/CodeSystem/ResultType", + "code": "policy" + } + ] + } + ], + "patient": { + "reference": "Patient/2466d49b-50e1-11f0-a144-661e92ac9503", + "display": "Patienten-ID 999999" + }, + "dateTime": "2025-06-24T00:00:00+02:00", + "organization": [ + { + "display": "GenomDE_MV" + } + ], + "sourceReference": { + "reference": "QuestionnaireResponse/24670c77-50e1-11f0-a144-661e92ac9503" + }, + "policyRule": { + "coding": [ + { + "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV", + "code": "Teilnahme", + "display": "Teilnahme am Modellvorhaben und Einwilligung zur Genomsequenzierung" + } + ] + }, + "provision": { + "type": "deny", + "period": { + "start": "2025-06-24T00:00:00+02:00", + "end": "3000-01-01T00:00:00+01:00" + }, + "provision": [ + { + "type": "deny", + "period": { + "start": "2025-06-24T00:00:00+02:00", + "end": "3000-01-01T00:00:00+01:00" + }, + "code": [ + { + "coding": [ + { + "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV", + "code": "Teilnahme", + "display": "Teilnahme am Modellvorhaben und Einwilligung zur Genomsequenzierung" + } + ] + } + ] + } + ] + } + } + }, + { + "fullUrl": "http://127.0.0.1:8090/ttp-fhir/fhir/gics/Consent/24673913-50e1-11f0-a144-661e92ac9503", + "resource": { + "resourceType": "Consent", + "id": "24673913-50e1-11f0-a144-661e92ac9503", + "meta": { + "lastUpdated": "2025-06-24T11:58:27.194+02:00", + "profile": [ + "http://fhir.de/ConsentManagement/StructureDefinition/Consent" + ] + }, + "extension": [ + { + "url": "http://fhir.de/ConsentManagement/StructureDefinition/DomainReference", + "extension": [ + { + "url": "domain", + "valueReference": { + "reference": "ResearchStudy/ef86d80e-50e0-11f0-a144-661e92ac9503" + } + }, + { + "url": "status", + "valueCoding": { + "system": "http://hl7.org/fhir/publication-status", + "code": "active" + } + } + ] + } + ], + "status": "active", + "scope": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/consentscope", + "code": "research" + } + ] + }, + "category": [ + { + "coding": [ + { + "system": "http://loinc.org", + "code": "59284-0" + } + ] + }, + { + "coding": [ + { + "system": "http://fhir.de/ConsentManagement/CodeSystem/ResultType", + "code": "policy" + } + ] + } + ], + "patient": { + "reference": "Patient/2466d49b-50e1-11f0-a144-661e92ac9503", + "display": "Patienten-ID 999999" + }, + "dateTime": "2025-06-24T00:00:00+02:00", + "organization": [ + { + "display": "GenomDE_MV" + } + ], + "sourceReference": { + "reference": "QuestionnaireResponse/24670c77-50e1-11f0-a144-661e92ac9503" + }, + "policyRule": { + "coding": [ + { + "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV", + "code": "Fallidentifizierung", + "display": "Fallidentifizierung zum fachlichen Austausch unter Behandelnden" + } + ] + }, + "provision": { + "type": "deny", + "period": { + "start": "2025-06-24T00:00:00+02:00", + "end": "3000-01-01T00:00:00+01:00" + }, + "provision": [ + { + "type": "deny", + "period": { + "start": "2025-06-24T00:00:00+02:00", + "end": "3000-01-01T00:00:00+01:00" + }, + "code": [ + { + "coding": [ + { + "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV", + "code": "Fallidentifizierung", + "display": "Fallidentifizierung zum fachlichen Austausch unter Behandelnden" + } + ] + } + ] + } + ] + } + } + }, + { + "fullUrl": "http://127.0.0.1:8090/ttp-fhir/fhir/gics/Consent/24673da9-50e1-11f0-a144-661e92ac9503", + "resource": { + "resourceType": "Consent", + "id": "24673da9-50e1-11f0-a144-661e92ac9503", + "meta": { + "lastUpdated": "2025-06-24T11:58:27.211+02:00", + "profile": [ + "http://fhir.de/ConsentManagement/StructureDefinition/Consent" + ] + }, + "extension": [ + { + "url": "http://fhir.de/ConsentManagement/StructureDefinition/DomainReference", + "extension": [ + { + "url": "domain", + "valueReference": { + "reference": "ResearchStudy/ef86d80e-50e0-11f0-a144-661e92ac9503" + } + }, + { + "url": "status", + "valueCoding": { + "system": "http://hl7.org/fhir/publication-status", + "code": "active" + } + } + ] + } + ], + "status": "active", + "scope": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/consentscope", + "code": "research" + } + ] + }, + "category": [ + { + "coding": [ + { + "system": "http://loinc.org", + "code": "59284-0" + } + ] + }, + { + "coding": [ + { + "system": "http://fhir.de/ConsentManagement/CodeSystem/ResultType", + "code": "policy" + } + ] + } + ], + "patient": { + "reference": "Patient/2466d49b-50e1-11f0-a144-661e92ac9503", + "display": "Patienten-ID 999999" + }, + "dateTime": "2025-06-24T00:00:00+02:00", + "organization": [ + { + "display": "GenomDE_MV" + } + ], + "sourceReference": { + "reference": "QuestionnaireResponse/24670c77-50e1-11f0-a144-661e92ac9503" + }, + "policyRule": { + "coding": [ + { + "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV", + "code": "Rekontaktierung", + "display": "Re-Identifizierung meiner Daten über die Vertrauensstelle beim Robert Koch-Institut und in die erneute Kontaktaufnahme durch meine behandelnde Ärztin oder meinen behandelnden Arzt" + } + ] + }, + "provision": { + "type": "deny", + "period": { + "start": "2025-06-24T00:00:00+02:00", + "end": "3000-01-01T00:00:00+01:00" + }, + "provision": [ + { + "type": "deny", + "period": { + "start": "2025-06-24T00:00:00+02:00", + "end": "3000-01-01T00:00:00+01:00" + }, + "code": [ + { + "coding": [ + { + "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV", + "code": "Rekontaktierung", + "display": "Re-Identifizierung meiner Daten über die Vertrauensstelle beim Robert Koch-Institut und in die erneute Kontaktaufnahme durch meine behandelnde Ärztin oder meinen behandelnden Arzt" + } + ] + } + ] + } + ] + } + } + } + ] +} \ No newline at end of file diff --git a/src/test/resources/fake_mv64e-gics-response_permit.json b/src/test/resources/fake_mv64e-gics-response_permit.json new file mode 100644 index 0000000..1dcaed0 --- /dev/null +++ b/src/test/resources/fake_mv64e-gics-response_permit.json @@ -0,0 +1,333 @@ +{ + "resourceType": "Bundle", + "type": "collection", + "entry": [ + { + "fullUrl": "http://127.0.0.1:8090/ttp-fhir/fhir/gics/Consent/121a8368-50e1-11f0-a144-661e92ac9503", + "resource": { + "resourceType": "Consent", + "id": "121a8368-50e1-11f0-a144-661e92ac9503", + "meta": { + "lastUpdated": "2025-06-24T11:55:42.079+02:00", + "profile": [ + "http://fhir.de/ConsentManagement/StructureDefinition/Consent" + ] + }, + "extension": [ + { + "url": "http://fhir.de/ConsentManagement/StructureDefinition/DomainReference", + "extension": [ + { + "url": "domain", + "valueReference": { + "reference": "ResearchStudy/ef86d80e-50e0-11f0-a144-661e92ac9503" + } + }, + { + "url": "status", + "valueCoding": { + "system": "http://hl7.org/fhir/publication-status", + "code": "active" + } + } + ] + } + ], + "status": "active", + "scope": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/consentscope", + "code": "research" + } + ] + }, + "category": [ + { + "coding": [ + { + "system": "http://loinc.org", + "code": "59284-0" + } + ] + }, + { + "coding": [ + { + "system": "http://fhir.de/ConsentManagement/CodeSystem/ResultType", + "code": "policy" + } + ] + } + ], + "patient": { + "reference": "Patient/12194791-50e1-11f0-a144-661e92ac9503", + "display": "Patienten-ID 12345678" + }, + "dateTime": "2025-06-24T00:00:00+02:00", + "organization": [ + { + "display": "GenomDE_MV" + } + ], + "sourceReference": { + "reference": "QuestionnaireResponse/1219ca42-50e1-11f0-a144-661e92ac9503" + }, + "policyRule": { + "coding": [ + { + "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV", + "code": "Teilnahme", + "display": "Teilnahme am Modellvorhaben und Einwilligung zur Genomsequenzierung" + } + ] + }, + "provision": { + "type": "deny", + "period": { + "start": "2025-06-24T00:00:00+02:00", + "end": "3000-01-01T00:00:00+01:00" + }, + "provision": [ + { + "type": "permit", + "period": { + "start": "2025-06-24T00:00:00+02:00", + "end": "3000-01-01T00:00:00+01:00" + }, + "code": [ + { + "coding": [ + { + "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV", + "code": "Teilnahme", + "display": "Teilnahme am Modellvorhaben und Einwilligung zur Genomsequenzierung" + } + ] + } + ] + } + ] + } + } + }, + { + "fullUrl": "http://127.0.0.1:8090/ttp-fhir/fhir/gics/Consent/121aad40-50e1-11f0-a144-661e92ac9503", + "resource": { + "resourceType": "Consent", + "id": "121aad40-50e1-11f0-a144-661e92ac9503", + "meta": { + "lastUpdated": "2025-06-24T11:55:42.096+02:00", + "profile": [ + "http://fhir.de/ConsentManagement/StructureDefinition/Consent" + ] + }, + "extension": [ + { + "url": "http://fhir.de/ConsentManagement/StructureDefinition/DomainReference", + "extension": [ + { + "url": "domain", + "valueReference": { + "reference": "ResearchStudy/ef86d80e-50e0-11f0-a144-661e92ac9503" + } + }, + { + "url": "status", + "valueCoding": { + "system": "http://hl7.org/fhir/publication-status", + "code": "active" + } + } + ] + } + ], + "status": "active", + "scope": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/consentscope", + "code": "research" + } + ] + }, + "category": [ + { + "coding": [ + { + "system": "http://loinc.org", + "code": "59284-0" + } + ] + }, + { + "coding": [ + { + "system": "http://fhir.de/ConsentManagement/CodeSystem/ResultType", + "code": "policy" + } + ] + } + ], + "patient": { + "reference": "Patient/12194791-50e1-11f0-a144-661e92ac9503", + "display": "Patienten-ID 12345678" + }, + "dateTime": "2025-06-24T00:00:00+02:00", + "organization": [ + { + "display": "GenomDE_MV" + } + ], + "sourceReference": { + "reference": "QuestionnaireResponse/1219ca42-50e1-11f0-a144-661e92ac9503" + }, + "policyRule": { + "coding": [ + { + "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV", + "code": "Fallidentifizierung", + "display": "Fallidentifizierung zum fachlichen Austausch unter Behandelnden" + } + ] + }, + "provision": { + "type": "deny", + "period": { + "start": "2025-06-24T00:00:00+02:00", + "end": "3000-01-01T00:00:00+01:00" + }, + "provision": [ + { + "type": "permit", + "period": { + "start": "2025-06-24T00:00:00+02:00", + "end": "3000-01-01T00:00:00+01:00" + }, + "code": [ + { + "coding": [ + { + "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV", + "code": "Fallidentifizierung", + "display": "Fallidentifizierung zum fachlichen Austausch unter Behandelnden" + } + ] + } + ] + } + ] + } + } + }, + { + "fullUrl": "http://127.0.0.1:8090/ttp-fhir/fhir/gics/Consent/121ac5f8-50e1-11f0-a144-661e92ac9503", + "resource": { + "resourceType": "Consent", + "id": "121ac5f8-50e1-11f0-a144-661e92ac9503", + "meta": { + "lastUpdated": "2025-06-24T11:55:42.110+02:00", + "profile": [ + "http://fhir.de/ConsentManagement/StructureDefinition/Consent" + ] + }, + "extension": [ + { + "url": "http://fhir.de/ConsentManagement/StructureDefinition/DomainReference", + "extension": [ + { + "url": "domain", + "valueReference": { + "reference": "ResearchStudy/ef86d80e-50e0-11f0-a144-661e92ac9503" + } + }, + { + "url": "status", + "valueCoding": { + "system": "http://hl7.org/fhir/publication-status", + "code": "active" + } + } + ] + } + ], + "status": "active", + "scope": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/consentscope", + "code": "research" + } + ] + }, + "category": [ + { + "coding": [ + { + "system": "http://loinc.org", + "code": "59284-0" + } + ] + }, + { + "coding": [ + { + "system": "http://fhir.de/ConsentManagement/CodeSystem/ResultType", + "code": "policy" + } + ] + } + ], + "patient": { + "reference": "Patient/12194791-50e1-11f0-a144-661e92ac9503", + "display": "Patienten-ID 12345678" + }, + "dateTime": "2025-06-24T00:00:00+02:00", + "organization": [ + { + "display": "GenomDE_MV" + } + ], + "sourceReference": { + "reference": "QuestionnaireResponse/1219ca42-50e1-11f0-a144-661e92ac9503" + }, + "policyRule": { + "coding": [ + { + "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV", + "code": "Rekontaktierung", + "display": "Re-Identifizierung meiner Daten über die Vertrauensstelle beim Robert Koch-Institut und in die erneute Kontaktaufnahme durch meine behandelnde Ärztin oder meinen behandelnden Arzt" + } + ] + }, + "provision": { + "type": "deny", + "period": { + "start": "2025-06-24T00:00:00+02:00", + "end": "3000-01-01T00:00:00+01:00" + }, + "provision": [ + { + "type": "permit", + "period": { + "start": "2025-06-24T00:00:00+02:00", + "end": "3000-01-01T00:00:00+01:00" + }, + "code": [ + { + "coding": [ + { + "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV", + "code": "Rekontaktierung", + "display": "Re-Identifizierung meiner Daten über die Vertrauensstelle beim Robert Koch-Institut und in die erneute Kontaktaufnahme durch meine behandelnde Ärztin oder meinen behandelnden Arzt" + } + ] + } + ] + } + ] + } + } + } + ] +} \ No newline at end of file