summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md13
-rw-r--r--build.gradle.kts28
-rw-r--r--src/integrationTest/kotlin/dev/dnpm/etl/processor/config/AppConfigurationTest.kt20
-rw-r--r--src/integrationTest/kotlin/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGeneratorTest.kt70
-rw-r--r--src/main/java/dev/dnpm/etl/processor/consent/ConsentDomain.java12
-rw-r--r--src/main/java/dev/dnpm/etl/processor/consent/GicsConsentService.java564
-rw-r--r--src/main/java/dev/dnpm/etl/processor/consent/IConsentService.java35
-rw-r--r--src/main/java/dev/dnpm/etl/processor/consent/MtbFileConsentService.java36
-rw-r--r--src/main/java/dev/dnpm/etl/processor/consent/TtpConsentStatus.java50
-rw-r--r--src/main/java/dev/dnpm/etl/processor/pseudonym/Generator.java5
-rw-r--r--src/main/java/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGenerator.java341
-rw-r--r--src/main/java/dev/dnpm/etl/processor/pseudonym/PseudonymRequestFailed.java12
-rw-r--r--src/main/java/dev/dnpm/etl/processor/pseudonym/PsnDomainType.java12
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt1
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt39
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/monitoring/Request.kt29
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/pseudonym/GpasSoapPseudonymGenerator.kt26
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/pseudonym/GpasSoapService.kt31
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/services/ConsentProcessor.kt48
-rw-r--r--src/main/resources/templates/index.html3
-rw-r--r--src/test/java/dev/dnpm/etl/processor/consent/GicsConsentServiceTest.java328
-rw-r--r--src/test/kotlin/dev/dnpm/etl/processor/consent/ConsentProcessorTest.kt58
-rw-r--r--src/test/kotlin/dev/dnpm/etl/processor/monitoring/ConnectionCheckServiceTest.kt162
-rw-r--r--src/test/kotlin/dev/dnpm/etl/processor/services/ConsentProcessorTest.kt2
-rw-r--r--src/test/resources/deniedConsentBundle.json692
-rw-r--r--src/test/resources/fake_broadConsent_mii_response_permit.json513
-rw-r--r--src/test/resources/permittedConsentBundle.json692
27 files changed, 3086 insertions, 736 deletions
diff --git a/README.md b/README.md
index ae3c9d8..9b3cba9 100644
--- a/README.md
+++ b/README.md
@@ -112,9 +112,14 @@ Ab Version 2025.1 (Multi-Pseudonym Support)
* `APP_PSEUDONYMIZE_GPAS_USERNAME`: gPas Basic-Auth Benutzername
* `APP_PSEUDONYMIZE_GPAS_PASSWORD`: gPas Basic-Auth Passwort
* `APP_PSEUDONYMIZE_GPAS_PID_DOMAIN`: gPas Domänenname für Patienten ID
-* `APP_PSEUDONYMIZE_GPAS_GENOM_DE_TAN_DOMAIN`: gPas Multi-Pseudonym-Domäne für genomDE Vorgangsnummern (
+* `APP_PSEUDONYMIZE_GPAS_GENOM_DE_TAN_DOMAIN`: gPAS Multi-Pseudonym-Domäne für genomDE Vorgangsnummern (
Clinical data node)
+Soll anstelle der REST-Schnittstelle von gPAS die SOAP-Schnittstelle verwendet werden,
+so ist nicht die URI der gPAS-Instanz anzugeben, sondern der SOAP-Endpoint:
+
+* `APP_PSEUDONYMIZE_GPAS_SOAP_ENDPOINT`: SOAP-Endpoint der gPAS-Instanz (e.g. http://127.0.0.1:9990/gpas/gpasService)
+
### (Externe) Consent-Services
Consent-Services können konfiguriert werden.
@@ -336,6 +341,12 @@ 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.
+Soll eine SSL-gesicherte Verbindung zu Kafka verwendet werden, so sind die SSL-Zertifikate in
+der Spring-Konfiguration anzugeben.
+Ein Beispiel findet sich in [`application-dev.yml`](src/main/resources/application-dev.yml).
+Dies kann auch mit Umgebungsvariablen wie `SPRING_KAFKA_SECURITY_...` und `SPRING_KAFKA_SSL_...`
+umgesetzt werden.
+
##### Retention Time
Generell werden in Apache Kafka alle Records entsprechend der Konfiguration vorgehalten.
diff --git a/build.gradle.kts b/build.gradle.kts
index 5053602..738ed54 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -7,22 +7,24 @@ plugins {
war
id("org.springframework.boot") version "3.5.6"
id("io.spring.dependency-management") version "1.1.7"
- kotlin("jvm") version "1.9.25"
- kotlin("plugin.spring") version "1.9.25"
+ id("com.diffplug.spotless") version "8.0.0"
+ kotlin("jvm") version "2.2.10"
+ kotlin("plugin.spring") version "2.2.10"
jacoco
}
group = "dev.dnpm"
-version = "0.11.3"
+version = "0.12.0-SNAPSHOT"
var versions = mapOf(
"mtb-dto" to "0.1.0-SNAPSHOT",
- "hapi-fhir" to "7.6.1",
- "mockito-kotlin" to "5.4.0",
- "archunit" to "1.3.0",
+ "hapi-fhir" to "8.4.0",
+ "apache-cxf" to "4.1.3",
+ "mockito-kotlin" to "6.0.0",
+ "archunit" to "1.4.1",
// Webjars
"webjars-locator" to "0.52",
- "echarts" to "5.4.3",
+ "echarts" to "6.0.0",
"htmx.org" to "1.9.12"
)
@@ -90,6 +92,9 @@ dependencies {
implementation("org.webjars.npm:htmx.org:${versions["htmx.org"]}")
// Fix for CVE-2025-48924
implementation("org.apache.commons:commons-lang3:3.18.0")
+ // gPAS via Soap
+ implementation("org.apache.cxf:cxf-rt-frontend-jaxws:${versions["apache-cxf"]}")
+ implementation("org.apache.cxf:cxf-rt-transports-http:${versions["apache-cxf"]}")
runtimeOnly("org.mariadb.jdbc:mariadb-java-client")
runtimeOnly("org.postgresql:postgresql")
@@ -127,6 +132,7 @@ tasks.withType<Test> {
testLogging {
events(TestLogEvent.FAILED, TestLogEvent.PASSED, TestLogEvent.SKIPPED)
}
+ dependsOn(tasks.spotlessCheck)
}
tasks.register<Test>("integrationTest") {
@@ -171,3 +177,11 @@ tasks.named<BootBuildImage>("bootBuildImage") {
"BP_OCI_DESCRIPTION" to "ETL Processor for MV § 64e and DNPM:DIP"
))
}
+
+spotless {
+ java {
+ importOrder()
+ removeUnusedImports()
+ googleJavaFormat()
+ }
+} \ No newline at end of file
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 66b62c8..5e25428 100644
--- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/config/AppConfigurationTest.kt
+++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/config/AppConfigurationTest.kt
@@ -29,6 +29,7 @@ import dev.dnpm.etl.processor.output.KafkaMtbFileSender
import dev.dnpm.etl.processor.output.RestMtbFileSender
import dev.dnpm.etl.processor.pseudonym.AnonymizingGenerator
import dev.dnpm.etl.processor.pseudonym.GpasPseudonymGenerator
+import dev.dnpm.etl.processor.pseudonym.GpasSoapPseudonymGenerator
import dev.dnpm.etl.processor.security.TokenRepository
import dev.dnpm.etl.processor.security.TokenService
import dev.dnpm.etl.processor.services.RequestProcessor
@@ -201,7 +202,8 @@ class AppConfigurationTest {
@Nested
@TestPropertySource(
properties = [
- "app.pseudonymize.generator=gpas"
+ "app.pseudonymize.generator=gpas",
+ "app.pseudonymize.gpas.uri=http://localhost/"
]
)
inner class AppConfigurationPseudonymizeGeneratorGpasTest(private val context: ApplicationContext) {
@@ -216,6 +218,22 @@ class AppConfigurationTest {
@Nested
@TestPropertySource(
properties = [
+ "app.pseudonymize.generator=gpas",
+ "app.pseudonymize.gpas.soap-endpoint=http://localhost/"
+ ]
+ )
+ inner class AppConfigurationPseudonymizeGeneratorGpasSoapTest(private val context: ApplicationContext) {
+
+ @Test
+ fun shouldUseConfiguredGenerator() {
+ assertThat(context.getBean(GpasSoapPseudonymGenerator::class.java)).isNotNull
+ }
+
+ }
+
+ @Nested
+ @TestPropertySource(
+ properties = [
"app.security.enable-tokens=true"
]
)
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 c2a8ba6..1bf443e 100644
--- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGeneratorTest.kt
+++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGeneratorTest.kt
@@ -21,6 +21,7 @@ package dev.dnpm.etl.processor.pseudonym
import dev.dnpm.etl.processor.config.AppFhirConfig
import dev.dnpm.etl.processor.config.GPasConfigProperties
+import org.apache.hc.core5.net.URIBuilder
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
@@ -37,6 +38,7 @@ import org.springframework.test.web.client.response.MockRestResponseCreators.wit
import org.springframework.test.web.client.response.MockRestResponseCreators.withStatus
import org.springframework.web.client.RestTemplate
import java.io.IOException
+import java.net.URI
class GpasPseudonymGeneratorTest {
@@ -49,7 +51,8 @@ class GpasPseudonymGeneratorTest {
fun setup() {
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(1)).build()
val gPasConfigProperties = GPasConfigProperties(
- "https://localhost:9990/ttp-fhir/fhir/gpas",
+ CONFIGURED_URI,
+ null,
"test", "test2",
null,
null
@@ -63,54 +66,59 @@ class GpasPseudonymGeneratorTest {
@Test
fun shouldReturnExpectedPseudonym() {
- this.mockRestServiceServer.expect {
- method(HttpMethod.POST)
- requestTo("https://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate")
- }.andRespond {
- withStatus(HttpStatus.OK).body(
- getDummyResponseBody(
- "1234",
- "test",
- "test1234ABCDEF567890"
+ this.mockRestServiceServer
+ .expect(method(HttpMethod.POST))
+ .andExpect(requestTo(EXPECTED_URI))
+ .andRespond {
+ withStatus(HttpStatus.OK).body(
+ getDummyResponseBody(
+ "1234",
+ "test",
+ "test1234ABCDEF567890"
+ )
)
- )
- .createResponse(it)
- }
+ .createResponse(it)
+ }
assertThat(this.generator.generate("ID1234")).isEqualTo("test1234ABCDEF567890")
}
@Test
fun shouldThrowExceptionIfGpasNotAvailable() {
- this.mockRestServiceServer.expect {
- method(HttpMethod.POST)
- requestTo("https://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate")
- }.andRespond {
- withException(IOException("Simulated IO error")).createResponse(it)
- }
+ this.mockRestServiceServer
+ .expect(method(HttpMethod.POST))
+ .andExpect(requestTo(EXPECTED_URI))
+ .andRespond {
+ withException(IOException("Simulated IO error")).createResponse(it)
+ }
assertThrows<PseudonymRequestFailed> { this.generator.generate("ID1234") }
}
@Test
fun shouldThrowExceptionIfGpasDoesNotReturn2xxResponse() {
- this.mockRestServiceServer.expect {
- method(HttpMethod.POST)
- requestTo("https://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate")
- }.andRespond {
- withStatus(HttpStatus.FOUND)
- .header(
- HttpHeaders.LOCATION,
- "https://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate"
- )
- .createResponse(it)
- }
+ this.mockRestServiceServer
+ .expect(method(HttpMethod.POST))
+ .andExpect(requestTo(EXPECTED_URI))
+ .andRespond {
+ withStatus(HttpStatus.FOUND)
+ .header(
+ HttpHeaders.LOCATION,
+ "https://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate"
+ )
+ .createResponse(it)
+ }
assertThrows<PseudonymRequestFailed> { this.generator.generate("ID1234") }
}
companion object {
+ const val CONFIGURED_URI = "https://localhost/ttp-fhir/fhir/gpas"
+
+ val EXPECTED_URI = URIBuilder(URI.create(CONFIGURED_URI)).appendPath($$"$pseudonymizeAllowCreate")
+ .build()!!
+
fun getDummyResponseBody(original: String, target: String, pseudonym: String) = """{
"resourceType": "Parameters",
"parameter": [
@@ -144,4 +152,4 @@ class GpasPseudonymGeneratorTest {
}""".trimIndent()
}
-} \ No newline at end of file
+}
diff --git a/src/main/java/dev/dnpm/etl/processor/consent/ConsentDomain.java b/src/main/java/dev/dnpm/etl/processor/consent/ConsentDomain.java
index 51bfd50..e339ba4 100644
--- a/src/main/java/dev/dnpm/etl/processor/consent/ConsentDomain.java
+++ b/src/main/java/dev/dnpm/etl/processor/consent/ConsentDomain.java
@@ -1,13 +1,9 @@
package dev.dnpm.etl.processor.consent;
public enum ConsentDomain {
- /**
- * MII Broad consent
- */
- BROAD_CONSENT,
+ /** MII Broad consent */
+ BROAD_CONSENT,
- /**
- * GenomDe Modellvorhaben §64e
- */
- MODELLVORHABEN_64E
+ /** GenomDe Modellvorhaben §64e */
+ MODELLVORHABEN_64E
}
diff --git a/src/main/java/dev/dnpm/etl/processor/consent/GicsConsentService.java b/src/main/java/dev/dnpm/etl/processor/consent/GicsConsentService.java
index 95e8e8f..03348ff 100644
--- a/src/main/java/dev/dnpm/etl/processor/consent/GicsConsentService.java
+++ b/src/main/java/dev/dnpm/etl/processor/consent/GicsConsentService.java
@@ -4,6 +4,10 @@ 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.net.URI;
+import java.util.Date;
+import kotlin.random.Random;
+import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.r4.model.*;
import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent;
@@ -20,9 +24,6 @@ import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
-import java.net.URI;
-import java.util.Date;
-
/**
* Service to request Consent from remote gICS installation
*
@@ -30,278 +31,355 @@ import java.util.Date;
*/
public class GicsConsentService implements IConsentService {
- 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 GIcsConfigProperties gIcsConfigProperties;
-
- public GicsConsentService(
- GIcsConfigProperties gIcsConfigProperties,
- RetryTemplate retryTemplate,
- RestTemplate restTemplate,
- AppFhirConfig appFhirConfig
- ) {
- this.retryTemplate = retryTemplate;
- this.restTemplate = restTemplate;
- this.fhirContext = appFhirConfig.fhirContext();
- this.gIcsConfigProperties = gIcsConfigProperties;
- log.info("GicsConsentService initialized...");
- }
-
- protected Parameters getFhirRequestParameters(
- String personIdentifierValue
- ) {
- var result = new Parameters();
- result.addParameter(
- new ParametersParameterComponent()
- .setName("personIdentifier")
- .setValue(
- new Identifier()
- .setValue(personIdentifierValue)
- .setSystem(this.gIcsConfigProperties.getPersonIdentifierSystem())
- )
- );
- result.addParameter(
- new ParametersParameterComponent()
- .setName("domain")
- .setValue(
- new StringType()
- .setValue(this.gIcsConfigProperties.getBroadConsentDomainName())
- )
- );
- result.addParameter(
- new ParametersParameterComponent()
- .setName("policy")
- .setValue(
- new Coding()
- .setCode(this.gIcsConfigProperties.getBroadConsentPolicyCode())
- .setSystem(this.gIcsConfigProperties.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()
+ 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 static final String BROAD_CONSENT_PROFILE_URI =
+ "https://www.medizininformatik-initiative.de/fhir/modul-consent/StructureDefinition/mii-pr-consent-einwilligung";
+ private static final String BROAD_CONSENT_POLICY =
+ "urn:oid:2.16.840.1.113883.3.1937.777.24.2.1791";
+
+ private final RetryTemplate retryTemplate;
+ private final RestTemplate restTemplate;
+ private final FhirContext fhirContext;
+ private final GIcsConfigProperties gIcsConfigProperties;
+
+ public GicsConsentService(
+ GIcsConfigProperties gIcsConfigProperties,
+ RetryTemplate retryTemplate,
+ RestTemplate restTemplate,
+ AppFhirConfig appFhirConfig) {
+ this.retryTemplate = retryTemplate;
+ this.restTemplate = restTemplate;
+ this.fhirContext = appFhirConfig.fhirContext();
+ this.gIcsConfigProperties = gIcsConfigProperties;
+ log.info("GicsConsentService initialized...");
+ }
+
+ protected Parameters getFhirRequestParameters(String personIdentifierValue) {
+ var result = new Parameters();
+ result.addParameter(
+ new ParametersParameterComponent()
+ .setName("personIdentifier")
+ .setValue(
+ new Identifier()
+ .setValue(personIdentifierValue)
+ .setSystem(this.gIcsConfigProperties.getPersonIdentifierSystem())));
+ result.addParameter(
+ new ParametersParameterComponent()
+ .setName("domain")
+ .setValue(
+ new StringType().setValue(this.gIcsConfigProperties.getBroadConsentDomainName())));
+ result.addParameter(
+ new ParametersParameterComponent()
+ .setName("policy")
+ .setValue(
+ new Coding()
+ .setCode(this.gIcsConfigProperties.getBroadConsentPolicyCode())
+ .setSystem(this.gIcsConfigProperties.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))
- )
+ .setValue(new BooleanType().setValue(true)))
.addPart(
new ParametersParameterComponent()
.setName("unknownStateIsConsideredAsDecline")
- .setValue(new BooleanType().setValue(false))
- );
+ .setValue(new BooleanType().setValue(false)));
+
+ result.addParameter(config);
- result.addParameter(config);
+ return result;
+ }
- return result;
+ private URI endpointUri(String endpoint) {
+ assert this.gIcsConfigProperties.getUri() != null;
+ return UriComponentsBuilder.fromUriString(this.gIcsConfigProperties.getUri())
+ .path(endpoint)
+ .build()
+ .toUri();
+ }
+
+ private HttpHeaders headersWithHttpBasicAuth() {
+ assert this.gIcsConfigProperties.getUri() != null;
+
+ var headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_XML);
+
+ if (StringUtils.isBlank(this.gIcsConfigProperties.getUsername())
+ || StringUtils.isBlank(this.gIcsConfigProperties.getPassword())) {
+ return headers;
}
- private URI endpointUri(String endpoint) {
- assert this.gIcsConfigProperties.getUri() != null;
- return UriComponentsBuilder.fromUriString(this.gIcsConfigProperties.getUri()).path(endpoint).build().toUri();
+ headers.setBasicAuth(
+ this.gIcsConfigProperties.getUsername(), this.gIcsConfigProperties.getPassword());
+ return headers;
+ }
+
+ protected String callGicsApi(Parameters parameter, String endpoint) {
+ var parameterAsXml = fhirContext.newXmlParser().encodeResourceToString(parameter);
+ HttpEntity<String> requestEntity =
+ new HttpEntity<>(parameterAsXml, this.headersWithHttpBasicAuth());
+ try {
+ var responseEntity =
+ retryTemplate.execute(
+ ctx ->
+ restTemplate.exchange(
+ endpointUri(endpoint), HttpMethod.POST, requestEntity, String.class));
+
+ 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;
+ }
+ } 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;
}
+ }
- private HttpHeaders headersWithHttpBasicAuth() {
- assert this.gIcsConfigProperties.getUri() != null;
+ @Override
+ public TtpConsentStatus getTtpBroadConsentStatus(String personIdentifierValue) {
+ var consentStatusResponse =
+ callGicsApi(
+ getFhirRequestParameters(personIdentifierValue),
+ GicsConsentService.IS_CONSENTED_ENDPOINT);
+ return evaluateConsentResponse(consentStatusResponse);
+ }
- var headers = new HttpHeaders();
- headers.setContentType(MediaType.APPLICATION_XML);
+ protected Bundle currentConsentForPersonAndTemplate(
+ String personIdentifierValue, ConsentDomain consentDomain, Date requestDate) {
- if (
- StringUtils.isBlank(this.gIcsConfigProperties.getUsername())
- || StringUtils.isBlank(this.gIcsConfigProperties.getPassword())
- ) {
- return headers;
- }
+ var requestParameter =
+ buildRequestParameterCurrentPolicyStatesForPerson(
+ personIdentifierValue, requestDate, consentDomain);
- headers.setBasicAuth(this.gIcsConfigProperties.getUsername(), this.gIcsConfigProperties.getPassword());
- return headers;
- }
+ var consentDataSerialized =
+ callGicsApi(requestParameter, GicsConsentService.IS_POLICY_STATES_FOR_PERSON_ENDPOINT);
- protected String callGicsApi(Parameters parameter, String endpoint) {
- var parameterAsXml = fhirContext.newXmlParser().encodeResourceToString(parameter);
- HttpEntity<String> requestEntity = new HttpEntity<>(parameterAsXml, this.headersWithHttpBasicAuth());
- try {
- var responseEntity = retryTemplate.execute(
- ctx -> restTemplate.exchange(endpointUri(endpoint), HttpMethod.POST, requestEntity, String.class)
- );
-
- 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;
- }
- } 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 (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 bundle) {
+ return bundle;
+ } else {
+ String errorMessage =
+ "Consent request failed! Unexpected response received! -> " + consentDataSerialized;
+ log.error(errorMessage);
+ throw new IllegalStateException(errorMessage);
}
+ }
+
+ @NotNull
+ private String getConsentDomainName(ConsentDomain targetConsentDomain) {
+ return switch (targetConsentDomain) {
+ case BROAD_CONSENT -> gIcsConfigProperties.getBroadConsentDomainName();
+ case MODELLVORHABEN_64E -> gIcsConfigProperties.getGenomDeConsentDomainName();
+ };
+ }
+
+ protected Parameters buildRequestParameterCurrentPolicyStatesForPerson(
+ String personIdentifierValue, Date requestDate, ConsentDomain consentDomain) {
+ var requestParameter = new Parameters();
+ requestParameter.addParameter(
+ new ParametersParameterComponent()
+ .setName("personIdentifier")
+ .setValue(
+ new Identifier()
+ .setValue(personIdentifierValue)
+ .setSystem(this.gIcsConfigProperties.getPersonIdentifierSystem())));
+
+ requestParameter.addParameter(
+ new ParametersParameterComponent()
+ .setName("domain")
+ .setValue(new StringType().setValue(getConsentDomainName(consentDomain))));
+
+ 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", true)
+ .addParameter("unknownStateIsConsideredAsDecline", false)
+ .addParameter("requestDate", new DateType().setValue(requestDate));
+
+ requestParameter.addParameter(
+ new ParametersParameterComponent()
+ .setName("config")
+ .addPart()
+ .setResource(nestedConfigParameters));
- @Override
- public TtpConsentStatus getTtpBroadConsentStatus(String personIdentifierValue) {
- var consentStatusResponse = callGicsApi(
- getFhirRequestParameters(personIdentifierValue),
- GicsConsentService.IS_CONSENTED_ENDPOINT
- );
- return evaluateConsentResponse(consentStatusResponse);
+ return requestParameter;
+ }
+
+ private TtpConsentStatus evaluateConsentResponse(String consentStatusResponse) {
+ if (consentStatusResponse == null) {
+ return TtpConsentStatus.FAILED_TO_ASK;
}
+ try {
+ var response = fhirContext.newJsonParser().parseResource(consentStatusResponse);
- protected Bundle currentConsentForPersonAndTemplate(
- String personIdentifierValue,
- ConsentDomain consentDomain,
- Date requestDate
- ) {
-
- var requestParameter = buildRequestParameterCurrentPolicyStatesForPerson(
- 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.");
+ if (response instanceof Parameters responseParameters) {
+
+ var responseValue = responseParameters.getParameter("consented").getValue();
+ var isConsented = responseValue.castToBoolean(responseValue);
+ if (!isConsented.hasValue()) {
+ return TtpConsentStatus.FAILED_TO_ASK;
}
- 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 bundle) {
- return bundle;
+ if (isConsented.booleanValue()) {
+ return TtpConsentStatus.BROAD_CONSENT_GIVEN;
} else {
- String errorMessage = "Consent request failed! Unexpected response received! -> "
- + consentDataSerialized;
- log.error(errorMessage);
- throw new IllegalStateException(errorMessage);
+ 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);
}
-
- @NotNull
- private String getConsentDomainName(ConsentDomain targetConsentDomain) {
- return switch (targetConsentDomain) {
- case BROAD_CONSENT -> gIcsConfigProperties.getBroadConsentDomainName();
- case MODELLVORHABEN_64E -> gIcsConfigProperties.getGenomDeConsentDomainName();
- };
+ return TtpConsentStatus.FAILED_TO_ASK;
+ }
+
+ @Override
+ public Bundle getConsent(String patientId, Date requestDate, ConsentDomain consentDomain) {
+ Bundle gIcsResultBundle =
+ currentConsentForPersonAndTemplate(patientId, consentDomain, requestDate);
+ if (ConsentDomain.BROAD_CONSENT == consentDomain) {
+ return anonymizeBroadConsent(convertGicsResultToMiiBroadConsent(gIcsResultBundle));
}
+ return gIcsResultBundle;
+ }
- protected Parameters buildRequestParameterCurrentPolicyStatesForPerson(
- String personIdentifierValue,
- Date requestDate,
- ConsentDomain consentDomain
- ) {
- var requestParameter = new Parameters();
- requestParameter.addParameter(
- new ParametersParameterComponent()
- .setName("personIdentifier")
- .setValue(
- new Identifier()
- .setValue(personIdentifierValue)
- .setSystem(this.gIcsConfigProperties.getPersonIdentifierSystem())
- )
- );
+ protected Bundle convertGicsResultToMiiBroadConsent(Bundle gIcsResultBundle) {
+ if (gIcsResultBundle == null
+ || gIcsResultBundle.getEntry().isEmpty()
+ || !(gIcsResultBundle.getEntry().getFirst().getResource() instanceof Consent))
+ return gIcsResultBundle;
- requestParameter.addParameter(
- new ParametersParameterComponent()
- .setName("domain")
- .setValue(new StringType().setValue(getConsentDomainName(consentDomain)))
- );
+ Bundle.BundleEntryComponent bundleEntryComponent = gIcsResultBundle.getEntry().getFirst();
- 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;
+ var consentAsOne = (Consent) bundleEntryComponent.getResource();
+
+ if (isMiiConsent(consentAsOne)) {
+ return gIcsResultBundle;
}
- 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;
+ if (consentAsOne.getPolicy().stream().noneMatch(p -> p.getUri().equals(BROAD_CONSENT_POLICY))) {
+ consentAsOne.addPolicy(new Consent.ConsentPolicyComponent().setUri(BROAD_CONSENT_POLICY));
}
- @Override
- public Bundle getConsent(String patientId, Date requestDate, ConsentDomain consentDomain) {
- return currentConsentForPersonAndTemplate(patientId, consentDomain, requestDate);
+ if (consentAsOne.getMeta().getProfile().stream()
+ .noneMatch(p -> p.getValue().equals(BROAD_CONSENT_PROFILE_URI))) {
+ consentAsOne.getMeta().addProfile(BROAD_CONSENT_PROFILE_URI);
+ }
+
+ consentAsOne.setPolicyRule(null);
+
+ consentAsOne
+ .getCategory()
+ .removeIf(
+ category ->
+ category.hasCoding(
+ "http://fhir.de/ConsentManagement/CodeSystem/ResultType", "policy"));
+
+ final var miiConsentCategory = new CodeableConcept();
+ miiConsentCategory.addCoding(
+ new Coding()
+ .setSystem(
+ "https://www.medizininformatik-initiative.de/fhir/modul-consent/CodeSystem/mii-cs-consent-consent_category")
+ .setCode("2.16.840.1.113883.3.1937.777.24.2.184"));
+ consentAsOne.addCategory(miiConsentCategory);
+
+ gIcsResultBundle.getEntry().stream()
+ .skip(1)
+ .forEach(
+ c ->
+ consentAsOne
+ .getProvision()
+ .addProvision(
+ ((Consent) c.getResource()).getProvision().getProvisionFirstRep()));
+
+ gIcsResultBundle.getEntry().clear();
+ gIcsResultBundle.addEntry(bundleEntryComponent);
+ return gIcsResultBundle;
+ }
+
+ private static boolean isMiiConsent(Consent consent) {
+ for (var category : consent.getCategory()) {
+ for (var categoryCoding : category.getCoding()) {
+ if ("https://www.medizininformatik-initiative.de/fhir/modul-consent/CodeSystem/mii-cs-consent-consent_category"
+ .equals(categoryCoding.getSystem())
+ && "2.16.840.1.113883.3.1937.777.24.2.184".equals(categoryCoding.getCode())) {
+ return true;
+ }
+ }
}
+ return false;
+ }
+
+ protected Bundle anonymizeBroadConsent(Bundle bundle) {
+ Bundle.BundleEntryComponent bundleEntryComponent = bundle.getEntry().getFirst();
+ hashBundleEntry(bundleEntryComponent);
+ return bundle;
+ }
+
+ private static void hashBundleEntry(Bundle.BundleEntryComponent entry) {
+ String id = entry.getResource().getIdPart();
+ var hash = DigestUtils.sha256Hex("%s_%s".formatted(Random.Default.toString(), id));
+
+ entry.getResource().setId(hash);
+ entry.setFullUrl(entry.getFullUrl().replace(id, hash));
+ var consent = (Consent) entry.getResource();
+ consent
+ .getSource()
+ .setProperty("reference", new StringType("QuestionnaireResponse/%s".formatted(hash)));
+ }
}
diff --git a/src/main/java/dev/dnpm/etl/processor/consent/IConsentService.java b/src/main/java/dev/dnpm/etl/processor/consent/IConsentService.java
index ded3515..148592e 100644
--- a/src/main/java/dev/dnpm/etl/processor/consent/IConsentService.java
+++ b/src/main/java/dev/dnpm/etl/processor/consent/IConsentService.java
@@ -5,23 +5,22 @@ import org.hl7.fhir.r4.model.Bundle;
public interface IConsentService {
- /**
- * 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; <p>if empty patient has not been asked, yet.</p>
- */
- Bundle getConsent(String personIdentifierValue, Date requestDate, ConsentDomain consentDomain);
+ /**
+ * 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;
+ * <p>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/MtbFileConsentService.java b/src/main/java/dev/dnpm/etl/processor/consent/MtbFileConsentService.java
index 24cb8f7..a126a51 100644
--- a/src/main/java/dev/dnpm/etl/processor/consent/MtbFileConsentService.java
+++ b/src/main/java/dev/dnpm/etl/processor/consent/MtbFileConsentService.java
@@ -7,25 +7,25 @@ import org.slf4j.LoggerFactory;
public class MtbFileConsentService implements IConsentService {
- private static final Logger log = LoggerFactory.getLogger(MtbFileConsentService.class);
+ private static final Logger log = LoggerFactory.getLogger(MtbFileConsentService.class);
- public MtbFileConsentService() {
- log.info("ConsentCheckFileBased initialized...");
- }
+ public MtbFileConsentService() {
+ log.info("ConsentCheckFileBased initialized...");
+ }
- @Override
- public TtpConsentStatus getTtpBroadConsentStatus(String personIdentifierValue) {
- return TtpConsentStatus.UNKNOWN_CHECK_FILE;
- }
+ @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();
- }
+ /**
+ * 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/TtpConsentStatus.java b/src/main/java/dev/dnpm/etl/processor/consent/TtpConsentStatus.java
index 2af1683..b92f58d 100644
--- a/src/main/java/dev/dnpm/etl/processor/consent/TtpConsentStatus.java
+++ b/src/main/java/dev/dnpm/etl/processor/consent/TtpConsentStatus.java
@@ -1,38 +1,22 @@
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,
+ /** 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
+ 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/Generator.java b/src/main/java/dev/dnpm/etl/processor/pseudonym/Generator.java
index 8d0d0c1..fce8146 100644
--- a/src/main/java/dev/dnpm/etl/processor/pseudonym/Generator.java
+++ b/src/main/java/dev/dnpm/etl/processor/pseudonym/Generator.java
@@ -21,8 +21,7 @@ package dev.dnpm.etl.processor.pseudonym;
public interface Generator {
- String generate(String id);
-
- String generateGenomDeTan(String id);
+ String generate(String id);
+ String generateGenomDeTan(String id);
}
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 6a2b947..48cdc67 100644
--- a/src/main/java/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGenerator.java
+++ b/src/main/java/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGenerator.java
@@ -39,187 +39,188 @@ 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 FhirContext r4Context;
- private final String gPasUrl;
- private final HttpHeaders httpHeader;
- private final RetryTemplate retryTemplate;
- private final Logger log = LoggerFactory.getLogger(GpasPseudonymGenerator.class);
- private final RestTemplate restTemplate;
- private final @NotNull String genomDeTanDomain;
- private final @NotNull String pidPsnDomain;
- protected final static String createOrGetPsn = "$pseudonymizeAllowCreate";
- protected final static String createMultiDomainPsn = "$pseudonymize-secondary";
- private final static String SINGLE_PSN_PART_NAME = "pseudonym";
- private final static String MULTI_PSN_PART_NAME = "value";
-
- public GpasPseudonymGenerator(GPasConfigProperties gpasCfg, RetryTemplate retryTemplate,
- RestTemplate restTemplate, AppFhirConfig appFhirConfig) {
- this.retryTemplate = retryTemplate;
- this.restTemplate = restTemplate;
- this.gPasUrl = gpasCfg.getUri();
- this.pidPsnDomain = gpasCfg.getPatientDomain();
- this.genomDeTanDomain = gpasCfg.getGenomDeTanDomain();
- this.r4Context = appFhirConfig.fhirContext();
- httpHeader = getHttpHeaders(gpasCfg.getUsername(), gpasCfg.getPassword());
-
- log.debug("{} has been initialized", this.getClass().getName());
-
+ private final FhirContext r4Context;
+ private final String gPasUrl;
+ private final HttpHeaders httpHeader;
+ private final RetryTemplate retryTemplate;
+ private final Logger log = LoggerFactory.getLogger(GpasPseudonymGenerator.class);
+ private final RestTemplate restTemplate;
+ private final @NotNull String genomDeTanDomain;
+ private final @NotNull String pidPsnDomain;
+ protected static final String CREATE_OR_GET_PSN = "$pseudonymizeAllowCreate";
+ protected static final String CREATE_MULTI_DOMAIN_PSN = "$pseudonymize-secondary";
+ private static final String SINGLE_PSN_PART_NAME = "pseudonym";
+ private static final String MULTI_PSN_PART_NAME = "value";
+
+ public GpasPseudonymGenerator(
+ GPasConfigProperties gpasCfg,
+ RetryTemplate retryTemplate,
+ RestTemplate restTemplate,
+ AppFhirConfig appFhirConfig) {
+ this.retryTemplate = retryTemplate;
+ this.restTemplate = restTemplate;
+ this.gPasUrl = gpasCfg.getUri();
+ this.pidPsnDomain = gpasCfg.getPatientDomain();
+ this.genomDeTanDomain = gpasCfg.getGenomDeTanDomain();
+ this.r4Context = appFhirConfig.fhirContext();
+ httpHeader = getHttpHeaders(gpasCfg.getUsername(), gpasCfg.getPassword());
+
+ log.debug("{} has been initialized", this.getClass().getName());
+ }
+
+ @Override
+ public String generate(String id) {
+ return generate(id, PsnDomainType.SINGLE_PSN_DOMAIN);
+ }
+
+ @Override
+ public String generateGenomDeTan(String id) {
+ return generate(id, PsnDomainType.MULTI_PSN_DOMAIN);
+ }
+
+ protected String generate(String id, PsnDomainType domainType) {
+ switch (domainType) {
+ case SINGLE_PSN_DOMAIN -> {
+ final var requestBody = createSinglePsnRequestBody(id, pidPsnDomain);
+ final var responseEntity = getGpasPseudonym(requestBody, CREATE_OR_GET_PSN);
+ final var gPasPseudonymResult =
+ (Parameters) r4Context.newJsonParser().parseResource(responseEntity.getBody());
+
+ return unwrapPseudonym(gPasPseudonymResult, SINGLE_PSN_PART_NAME);
+ }
+ case MULTI_PSN_DOMAIN -> {
+ final var requestBody = createMultiPsnRequestBody(id, genomDeTanDomain);
+ final var responseEntity = getGpasPseudonym(requestBody, CREATE_MULTI_DOMAIN_PSN);
+ final var gPasPseudonymResult =
+ (Parameters) r4Context.newJsonParser().parseResource(responseEntity.getBody());
+
+ return unwrapPseudonym(gPasPseudonymResult, MULTI_PSN_PART_NAME);
+ }
}
+ throw new NotImplementedException(
+ "give domain type '%s' is unexpected and is currently not supported!"
+ .formatted(domainType));
+ }
- @Override
- public String generate(String id) {
- return generate(id, PsnDomainType.SINGLE_PSN_DOMAIN);
- }
+ @NotNull
+ public static String unwrapPseudonym(Parameters gPasPseudonymResult, String targetPartName) {
+ final var parameters = gPasPseudonymResult.getParameter().stream().findFirst();
- @Override
- public String generateGenomDeTan(String id) {
- return generate(id, PsnDomainType.MULTI_PSN_DOMAIN);
+ if (parameters.isEmpty()) {
+ throw new PseudonymRequestFailed("Empty HL7 parameters, cannot find first one");
}
- protected String generate(String id, PsnDomainType domainType) {
- switch (domainType) {
- case SINGLE_PSN_DOMAIN -> {
- final var requestBody = createSinglePsnRequestBody(id, pidPsnDomain);
- final var responseEntity = getGpasPseudonym(requestBody, createOrGetPsn);
- final var gPasPseudonymResult = (Parameters) r4Context.newJsonParser()
- .parseResource(responseEntity.getBody());
-
- return unwrapPseudonym(gPasPseudonymResult, SINGLE_PSN_PART_NAME);
- }
- case MULTI_PSN_DOMAIN -> {
- final var requestBody = createMultiPsnRequestBody(id, genomDeTanDomain);
- final var responseEntity = getGpasPseudonym(requestBody, createMultiDomainPsn);
- final var gPasPseudonymResult = (Parameters) r4Context.newJsonParser()
- .parseResource(responseEntity.getBody());
-
- return unwrapPseudonym(gPasPseudonymResult, MULTI_PSN_PART_NAME);
- }
- }
- throw new NotImplementedException(
- "give domain type '%s' is unexpected and is currently not supported!".formatted(
- domainType));
+ final var identifier =
+ (Identifier)
+ parameters.get().getPart().stream()
+ .filter(a -> a.getName().equals(targetPartName))
+ .findFirst()
+ .orElseGet(ParametersParameterComponent::new)
+ .getValue();
+
+ // pseudonym
+ return sanitizeValue(identifier.getValue());
+ }
+
+ /**
+ * Allow only filename friendly values
+ *
+ * @param psnValue GAPS pseudonym value
+ * @return cleaned up value
+ */
+ public static String sanitizeValue(String psnValue) {
+ // pattern to match forbidden characters
+ String forbiddenCharsRegex = "[\\\\/:*?\"<>|;]";
+
+ // Replace all forbidden characters with underscores
+ return psnValue.replaceAll(forbiddenCharsRegex, "_");
+ }
+
+ @NotNull
+ protected ResponseEntity<String> getGpasPseudonym(String gPasRequestBody, String apiEndpoint) {
+
+ HttpEntity<String> requestEntity = new HttpEntity<>(gPasRequestBody, this.httpHeader);
+
+ try {
+ var targetUrl = buildRequestUrl(apiEndpoint);
+ ResponseEntity<String> responseEntity =
+ retryTemplate.execute(
+ ctx ->
+ restTemplate.exchange(targetUrl, HttpMethod.POST, requestEntity, String.class));
+ if (responseEntity.getStatusCode().is2xxSuccessful()) {
+ log.debug("API request succeeded. Response: {}", responseEntity.getStatusCode());
+ return responseEntity;
+ }
+ } catch (BadRequest e) {
+ String msg =
+ "gPas or request configuration is incorrect. Please check both." + e.getMessage();
+ log.error(msg);
+ throw new PseudonymRequestFailed(msg, e);
+ } catch (Unauthorized e) {
+ var msg =
+ "gPas access credentials are invalid check your configuration. msg: '%s"
+ .formatted(e.getMessage());
+ log.error(msg);
+ throw new PseudonymRequestFailed(msg, e);
+ } catch (Exception unexpected) {
+ throw new PseudonymRequestFailed(
+ "API request due unexpected error unsuccessful gPas unsuccessful.", unexpected);
}
-
- @NotNull
- public static String unwrapPseudonym(Parameters gPasPseudonymResult, String targetPartName) {
- final var parameters = gPasPseudonymResult.getParameter().stream().findFirst();
-
- if (parameters.isEmpty()) {
- throw new PseudonymRequestFailed("Empty HL7 parameters, cannot find first one");
- }
-
- final var identifier = (Identifier) parameters.get().getPart().stream()
- .filter(a -> a.getName().equals(targetPartName))
- .findFirst()
- .orElseGet(ParametersParameterComponent::new).getValue();
-
- // pseudonym
- return sanitizeValue(identifier.getValue());
+ throw new PseudonymRequestFailed(
+ "API request due unexpected error unsuccessful gPas unsuccessful.");
+ }
+
+ protected URI buildRequestUrl(String apiEndpoint) throws URISyntaxException {
+ var gPasUrl1 = gPasUrl;
+ if (gPasUrl.lastIndexOf("/") == gPasUrl.length() - 1) {
+ gPasUrl1 = gPasUrl.substring(0, gPasUrl.length() - 1);
}
-
- /**
- * Allow only filename friendly values
- *
- * @param psnValue GAPS pseudonym value
- * @return cleaned up value
- */
- public static String sanitizeValue(String psnValue) {
- // pattern to match forbidden characters
- String forbiddenCharsRegex = "[\\\\/:*?\"<>|;]";
-
- // Replace all forbidden characters with underscores
- return psnValue.replaceAll(forbiddenCharsRegex, "_");
+ var urlBuilder = new URIBuilder(new URI(gPasUrl1)).appendPath(apiEndpoint);
+
+ return urlBuilder.build();
+ }
+
+ protected String createSinglePsnRequestBody(String id, String targetDomain) {
+ final var requestParameters = new Parameters();
+ requestParameters
+ .addParameter()
+ .setName("target")
+ .setValue(new StringType().setValue(targetDomain));
+ requestParameters.addParameter().setName("original").setValue(new StringType().setValue(id));
+ final IParser iParser = r4Context.newJsonParser();
+ return iParser.encodeResourceToString(requestParameters);
+ }
+
+ protected String createMultiPsnRequestBody(String id, String targetDomain) {
+ final var param = new Parameters();
+ ParametersParameterComponent targetParam = param.addParameter().setName("original");
+ targetParam.addPart(
+ new ParametersParameterComponent()
+ .setName("target")
+ .setValue(new StringType(targetDomain)));
+ targetParam.addPart(
+ new ParametersParameterComponent().setName("value").setValue(new StringType(id)));
+ targetParam.addPart(
+ new ParametersParameterComponent().setName("count").setValue(new StringType("1")));
+
+ final IParser iParser = r4Context.newJsonParser();
+ return iParser.encodeResourceToString(param);
+ }
+
+ @NotNull
+ protected HttpHeaders getHttpHeaders(String gPasUserName, String gPasPassword) {
+ var headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_JSON);
+
+ if (StringUtils.isBlank(gPasUserName) || StringUtils.isBlank(gPasPassword)) {
+ return headers;
}
- @NotNull
- protected ResponseEntity<String> getGpasPseudonym(String gPasRequestBody, String apiEndpoint) {
-
- HttpEntity<String> requestEntity = new HttpEntity<>(gPasRequestBody, this.httpHeader);
-
- try {
- var targetUrl = buildRequestUrl(apiEndpoint);
- ResponseEntity<String> responseEntity = retryTemplate.execute(
- ctx -> restTemplate.exchange(targetUrl, HttpMethod.POST, requestEntity,
- String.class));
- if (responseEntity.getStatusCode().is2xxSuccessful()) {
- log.debug("API request succeeded. Response: {}", responseEntity.getStatusCode());
- 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);
- }
- } catch (Exception unexpected) {
- throw new PseudonymRequestFailed(
- "API request due unexpected error unsuccessful gPas unsuccessful.", unexpected);
- }
- throw new PseudonymRequestFailed(
- "API request due unexpected error unsuccessful gPas unsuccessful.");
-
- }
-
- protected URI buildRequestUrl(String apiEndpoint) throws URISyntaxException {
- var gPasUrl1 = gPasUrl;
- if (gPasUrl.lastIndexOf("/") == gPasUrl.length() - 1) {
- gPasUrl1 = gPasUrl.substring(0, gPasUrl.length() - 1);
- }
- var urlBuilder = new URIBuilder(new URI(gPasUrl1)).appendPath(apiEndpoint);
-
- return urlBuilder.build();
- }
-
- protected String createSinglePsnRequestBody(String id, String targetDomain) {
- final var requestParameters = new Parameters();
- requestParameters.addParameter().setName("target")
- .setValue(new StringType().setValue(targetDomain));
- requestParameters.addParameter().setName("original")
- .setValue(new StringType().setValue(id));
- final IParser iParser = r4Context.newJsonParser();
- return iParser.encodeResourceToString(requestParameters);
- }
-
- protected String createMultiPsnRequestBody(String id, String targetDomain) {
- final var param = new Parameters();
- ParametersParameterComponent targetParam = param.addParameter().setName("original");
- targetParam.addPart(
- new ParametersParameterComponent().setName("target")
- .setValue(new StringType(targetDomain)));
- targetParam.addPart(
- new ParametersParameterComponent().setName("value").setValue(new StringType(id)));
- targetParam
- .addPart(new ParametersParameterComponent().setName("count").setValue(
- new StringType("1")));
-
- final IParser iParser = r4Context.newJsonParser();
- return iParser.encodeResourceToString(param);
- }
-
-
- @NotNull
- protected HttpHeaders getHttpHeaders(String gPasUserName, String gPasPassword) {
- var headers = new HttpHeaders();
- headers.setContentType(MediaType.APPLICATION_JSON);
-
- if (StringUtils.isBlank(gPasUserName) || StringUtils.isBlank(gPasPassword)) {
- return headers;
- }
-
- headers.setBasicAuth(gPasUserName, gPasPassword);
- return headers;
- }
+ headers.setBasicAuth(gPasUserName, gPasPassword);
+ return headers;
+ }
}
diff --git a/src/main/java/dev/dnpm/etl/processor/pseudonym/PseudonymRequestFailed.java b/src/main/java/dev/dnpm/etl/processor/pseudonym/PseudonymRequestFailed.java
index 79b4ba6..397455a 100644
--- a/src/main/java/dev/dnpm/etl/processor/pseudonym/PseudonymRequestFailed.java
+++ b/src/main/java/dev/dnpm/etl/processor/pseudonym/PseudonymRequestFailed.java
@@ -2,11 +2,11 @@ package dev.dnpm.etl.processor.pseudonym;
public class PseudonymRequestFailed extends RuntimeException {
- public PseudonymRequestFailed(String message) {
- super(message);
- }
+ public PseudonymRequestFailed(String message) {
+ super(message);
+ }
- public PseudonymRequestFailed(String message, Throwable cause) {
- super(message, cause);
- }
+ public PseudonymRequestFailed(String message, Throwable cause) {
+ super(message, cause);
+ }
}
diff --git a/src/main/java/dev/dnpm/etl/processor/pseudonym/PsnDomainType.java b/src/main/java/dev/dnpm/etl/processor/pseudonym/PsnDomainType.java
index a0fbc93..55cb212 100644
--- a/src/main/java/dev/dnpm/etl/processor/pseudonym/PsnDomainType.java
+++ b/src/main/java/dev/dnpm/etl/processor/pseudonym/PsnDomainType.java
@@ -1,12 +1,8 @@
package dev.dnpm.etl.processor.pseudonym;
public enum PsnDomainType {
- /**
- * one pseudonym per original value
- */
- SINGLE_PSN_DOMAIN,
- /**
- * multiple pseudonymes for one original value
- */
- MULTI_PSN_DOMAIN
+ /** one pseudonym per original value */
+ SINGLE_PSN_DOMAIN,
+ /** multiple pseudonymes for one original value */
+ MULTI_PSN_DOMAIN
}
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 395dbd2..fc0727f 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt
@@ -47,6 +47,7 @@ data class PseudonymizeConfigProperties(
@ConfigurationProperties(GPasConfigProperties.NAME)
data class GPasConfigProperties(
val uri: String?,
+ val soapEndpoint: String?,
val patientDomain: String = "etl-processor",
val genomDeTanDomain: String = "ccdn",
val username: String?,
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 b4fad3e..de302fd 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt
@@ -20,19 +20,17 @@
package dev.dnpm.etl.processor.config
import com.fasterxml.jackson.databind.ObjectMapper
-import dev.dnpm.etl.processor.consent.MtbFileConsentService
import dev.dnpm.etl.processor.consent.GicsConsentService
import dev.dnpm.etl.processor.consent.IConsentService
+import dev.dnpm.etl.processor.consent.MtbFileConsentService
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.pseudonym.*
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.apache.cxf.jaxws.JaxWsProxyFactoryBean
import org.slf4j.LoggerFactory
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
@@ -85,6 +83,37 @@ class AppConfiguration {
}
@ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "GPAS")
+ @ConditionalOnProperty(value = ["app.pseudonymize.gpas.soap-endpoint"])
+ @Bean
+ fun gpasSoapProxyFactoryBean(gpasConfigProperties: GPasConfigProperties): JaxWsProxyFactoryBean {
+ val proxyFactory = JaxWsProxyFactoryBean()
+ proxyFactory.serviceClass = GpasSoapService::class.java
+ proxyFactory.address = gpasConfigProperties.soapEndpoint
+ return proxyFactory
+ }
+
+ @ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "GPAS")
+ @ConditionalOnProperty(value = ["app.pseudonymize.gpas.soap-endpoint"])
+ @Bean
+ fun gpasSoapProxy(gpasConfigProperties: GPasConfigProperties): GpasSoapService {
+ return gpasSoapProxyFactoryBean(gpasConfigProperties).create() as GpasSoapService
+ }
+
+ @ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "GPAS")
+ @ConditionalOnProperty(value = ["app.pseudonymize.gpas.soap-endpoint"])
+ @Bean
+ fun gpasSoapPseudonymGenerator(
+ configProperties: GPasConfigProperties,
+ retryTemplate: RetryTemplate,
+ gpasSoapService: GpasSoapService,
+ appFhirConfig: AppFhirConfig
+ ): Generator {
+ logger.info("Selected 'GpasSoapPseudonym Generator'")
+ return GpasSoapPseudonymGenerator(configProperties, retryTemplate, gpasSoapService, appFhirConfig)
+ }
+
+ @ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "GPAS")
+ @ConditionalOnProperty(value = ["app.pseudonymize.gpas.uri"])
@Bean
fun gpasPseudonymGenerator(
configProperties: GPasConfigProperties,
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/Request.kt b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/Request.kt
index 36c9705..f2509dd 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/Request.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/Request.kt
@@ -30,6 +30,7 @@ import org.springframework.data.relational.core.mapping.Table
import org.springframework.data.repository.CrudRepository
import org.springframework.data.repository.PagingAndSortingRepository
import java.time.Instant
+import java.time.temporal.ChronoUnit
import java.util.*
@Table("request")
@@ -65,6 +66,12 @@ data class Request(
processedAt: Instant
) :
this(null, uuid, patientPseudonym, pid, fingerprint, type, status, processedAt)
+
+ fun isPendingUnknown(): Boolean {
+ return this.status == RequestStatus.UNKNOWN && this.processedAt.isBefore(
+ Instant.now().minus(10, ChronoUnit.MINUTES)
+ )
+ }
}
@JvmRecord
@@ -90,19 +97,23 @@ interface RequestRepository : CrudRepository<Request, Long>, PagingAndSortingRep
@Query("SELECT count(*) AS count, status FROM request WHERE type = 'MTB_FILE' GROUP BY status ORDER BY status, count DESC;")
fun countStates(): List<CountedState>
- @Query("SELECT count(*) AS count, status FROM (" +
- "SELECT status, rank() OVER (PARTITION BY patient_pseudonym ORDER BY processed_at DESC) AS rank FROM request " +
- "WHERE type = 'MTB_FILE' AND status NOT IN ('DUPLICATION') " +
- ") rank WHERE rank = 1 GROUP BY status ORDER BY status, count DESC;")
+ @Query(
+ "SELECT count(*) AS count, status FROM (" +
+ "SELECT status, rank() OVER (PARTITION BY patient_pseudonym ORDER BY processed_at DESC) AS rank FROM request " +
+ "WHERE type = 'MTB_FILE' AND status NOT IN ('DUPLICATION') " +
+ ") rank WHERE rank = 1 GROUP BY status ORDER BY status, count DESC;"
+ )
fun findPatientUniqueStates(): List<CountedState>
@Query("SELECT count(*) AS count, status FROM request WHERE type = 'DELETE' GROUP BY status ORDER BY status, count DESC;")
fun countDeleteStates(): List<CountedState>
- @Query("SELECT count(*) AS count, status FROM (" +
- "SELECT status, rank() OVER (PARTITION BY patient_pseudonym ORDER BY processed_at DESC) AS rank FROM request " +
- "WHERE type = 'DELETE'" +
- ") rank WHERE rank = 1 GROUP BY status ORDER BY status, count DESC;")
+ @Query(
+ "SELECT count(*) AS count, status FROM (" +
+ "SELECT status, rank() OVER (PARTITION BY patient_pseudonym ORDER BY processed_at DESC) AS rank FROM request " +
+ "WHERE type = 'DELETE'" +
+ ") rank WHERE rank = 1 GROUP BY status ORDER BY status, count DESC;"
+ )
fun findPatientUniqueDeleteStates(): List<CountedState>
-} \ No newline at end of file
+}
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/GpasSoapPseudonymGenerator.kt b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/GpasSoapPseudonymGenerator.kt
new file mode 100644
index 0000000..8215d23
--- /dev/null
+++ b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/GpasSoapPseudonymGenerator.kt
@@ -0,0 +1,26 @@
+package dev.dnpm.etl.processor.pseudonym
+
+import dev.dnpm.etl.processor.config.AppFhirConfig
+import dev.dnpm.etl.processor.config.GPasConfigProperties
+import org.springframework.retry.support.RetryTemplate
+
+class GpasSoapPseudonymGenerator(
+ private val gpasCfg: GPasConfigProperties,
+ private val retryTemplate: RetryTemplate,
+ private val gpasSoapService: GpasSoapService,
+ private val appFhirConfig: AppFhirConfig
+) : Generator {
+
+ override fun generate(id: String): String {
+ return retryTemplate.execute<String, Exception> {
+ gpasSoapService.getOrCreatePseudonymFor(id, gpasCfg.patientDomain)
+ }
+ }
+
+ override fun generateGenomDeTan(id: String): String {
+ return retryTemplate.execute<String, Exception> {
+ gpasSoapService.createPseudonymsFor(id, gpasCfg.genomDeTanDomain, 1).first()
+ }
+ }
+}
+
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/GpasSoapService.kt b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/GpasSoapService.kt
new file mode 100644
index 0000000..0909924
--- /dev/null
+++ b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/GpasSoapService.kt
@@ -0,0 +1,31 @@
+package dev.dnpm.etl.processor.pseudonym
+
+import jakarta.jws.WebMethod
+import jakarta.jws.WebParam
+import jakarta.jws.WebResult
+import jakarta.jws.WebService
+import jakarta.xml.bind.annotation.XmlElementWrapper
+
+@WebService(
+ name = "PSNManagerBeanService",
+ targetNamespace ="http://psn.ttp.ganimed.icmvc.emau.org/"
+)
+interface GpasSoapService {
+
+ @WebMethod(operationName = "getOrCreatePseudonymFor")
+ @WebResult(name = "psn")
+ fun getOrCreatePseudonymFor(
+ @WebParam(name = "value") value: String,
+ @WebParam(name = "domainName") domainName: String
+ ): String
+
+ @WebMethod(operationName = "createPseudonymsFor")
+ @WebResult(name = "psn")
+ @XmlElementWrapper(name = "return")
+ fun createPseudonymsFor(
+ @WebParam(name = "value") value: String,
+ @WebParam(name = "domainName") domainName: String,
+ @WebParam(name = "number") minNumber: Int
+ ): List<String>
+
+} \ No newline at end of file
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/services/ConsentProcessor.kt b/src/main/kotlin/dev/dnpm/etl/processor/services/ConsentProcessor.kt
index 2ed21eb..b420d1f 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/services/ConsentProcessor.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/services/ConsentProcessor.kt
@@ -14,7 +14,6 @@ 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
@@ -137,15 +136,7 @@ class ConsentProcessor(
}
val provisionComponent: ProvisionComponent = provisions.first()
-
- var provisionCode: String? = null
- if (provisionComponent.code != null && provisionComponent.code.isNotEmpty()) {
- val codableConcept: CodeableConcept = provisionComponent.code.first()
- if (codableConcept.coding != null && codableConcept.coding.isNotEmpty()) {
- provisionCode = codableConcept.coding.first().code
- }
- }
-
+ val provisionCode = getProvisionCode(provisionComponent)
if (provisionCode != null) {
try {
val modelProjectConsentPurpose =
@@ -177,6 +168,17 @@ class ConsentProcessor(
}
}
+ private fun getProvisionCode(provisionComponent: ProvisionComponent): String? {
+ var provisionCode: String? = null
+ if (provisionComponent.code != null && provisionComponent.code.isNotEmpty()) {
+ val codableConcept: CodeableConcept = provisionComponent.code.first()
+ if (codableConcept.coding != null && codableConcept.coding.isNotEmpty()) {
+ provisionCode = codableConcept.coding.first().code
+ }
+ }
+ return provisionCode
+ }
+
private fun setGenomDeSubmissionType(mtbFile: Mtb) {
if (appConfigProperties.genomDeTestSubmission) {
mtbFile.metadata.type = MvhSubmissionType.TEST
@@ -230,17 +232,21 @@ class ConsentProcessor(
entry.resource.isResource && entry.resource.resourceType == ResourceType.Consent
val consentIsActive = (entry.resource as Consent).status == ConsentState.ACTIVE
- isConsentResource && consentIsActive && checkCoding(
- targetCode, targetSystem, (entry.resource as Consent).policyRule.coding
- ) && isRequestDateInRange(requestDate, (entry.resource as Consent).provision.period)
+ val provisions = (entry.resource as Consent).provision.provision
+
+ val isValidCoding = checkProvisionExist(
+ targetCode, targetSystem, provisions
+ )
+
+ isConsentResource && consentIsActive && isValidCoding && isRequestDateInRange(requestDate, (entry.resource as Consent).provision.period)
}.map { entry: BundleEntryComponent ->
val consent = (entry.getResource() as Consent)
consent.provision.provision.filter { subProvision ->
isRequestDateInRange(requestDate, subProvision.period)
// search coding entries of current provision for code and system
- subProvision.code.map { c -> c.coding }.flatten().firstOrNull { code ->
+ subProvision.code.map { c -> c.coding }.flatten().any { code ->
targetCode.equals(code.code) && targetSystem.equals(code.system)
- } != null
+ }
}.map { subProvision ->
subProvision
}
@@ -252,16 +258,14 @@ class ConsentProcessor(
return Consent.ConsentProvisionType.NULL
}
- fun checkCoding(
+ fun checkProvisionExist(
researchAllowedPolicyOid: String?,
researchAllowedPolicySystem: String?,
- policyRules: Collection<Coding>
+ provisions: Collection<ProvisionComponent>
): Boolean {
- return policyRules.find { code ->
- researchAllowedPolicySystem.equals(code.getSystem()) && (researchAllowedPolicyOid.equals(
- code.getCode()
- ))
- } != null
+ return provisions.any { provision ->
+ provision.code.any { codeableConcept -> codeableConcept.coding.any { it.system == researchAllowedPolicySystem && it.code == researchAllowedPolicyOid } }
+ }
}
fun isRequestDateInRange(requestDate: Date?, provPeriod: Period): Boolean {
diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html
index f48e3dc..a419dda 100644
--- a/src/main/resources/templates/index.html
+++ b/src/main/resources/templates/index.html
@@ -52,7 +52,8 @@
<td th:if="${request.status.value.contains('success')}" class="bg-green"><small>[[ ${request.status} ]]</small></td>
<td th:if="${request.status.value.contains('warning')}" class="bg-yellow"><small>[[ ${request.status} ]]</small></td>
<td th:if="${request.status.value.contains('error')}" class="bg-red"><small>[[ ${request.status} ]]</small></td>
- <td th:if="${request.status.value == 'unknown'}" class="bg-gray"><small>[[ ${request.status} ]]</small></td>
+ <td th:if="${request.status.value == 'unknown' and not request.isPendingUnknown()}" class="bg-gray"><small>[[ ${request.status} ]]</small></td>
+ <td th:if="${request.status.value == 'unknown' and request.isPendingUnknown()}" class="bg-yellow"><small>⏰ [[ ${request.status} ]] ⏰</small></td>
<td th:if="${request.status.value == 'duplication'}" class="bg-gray"><small>[[ ${request.status} ]]</small></td>
<td th:if="${request.status.value == 'no-consent'}" class="bg-blue"><small>[[ ${request.status} ]]</small></td>
<td th:style="${request.type.value == 'delete'} ? 'color: red;'"><small>[[ ${request.type} ]]</small></td>
diff --git a/src/test/java/dev/dnpm/etl/processor/consent/GicsConsentServiceTest.java b/src/test/java/dev/dnpm/etl/processor/consent/GicsConsentServiceTest.java
index 6fa8f08..bbc53be 100644
--- a/src/test/java/dev/dnpm/etl/processor/consent/GicsConsentServiceTest.java
+++ b/src/test/java/dev/dnpm/etl/processor/consent/GicsConsentServiceTest.java
@@ -1,9 +1,20 @@
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.withServerError;
+import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
+
+import ca.uhn.fhir.context.FhirContext;
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.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.util.Date;
+import java.util.Objects;
+import org.apache.commons.io.IOUtils;
import org.hl7.fhir.r4.model.*;
import org.hl7.fhir.r4.model.OperationOutcome.IssueSeverity;
import org.hl7.fhir.r4.model.OperationOutcome.IssueType;
@@ -20,162 +31,209 @@ import org.springframework.test.context.TestPropertySource;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.web.client.RestTemplate;
-import java.time.Instant;
-import java.util.Date;
-
-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.withServerError;
-import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
-
@ContextConfiguration(classes = {AppConfiguration.class, ObjectMapper.class})
-@TestPropertySource(properties = {
- "app.consent.service=gics",
- "app.consent.gics.uri=http://localhost:8090/ttp-fhir/fhir/gics"
-})
+@TestPropertySource(
+ properties = {
+ "app.consent.service=gics",
+ "app.consent.gics.uri=http://localhost:8090/ttp-fhir/fhir/gics"
+ })
@RestClientTest
class GicsConsentServiceTest {
- static final String GICS_BASE_URI = "http://localhost:8090/ttp-fhir/fhir/gics";
+ static final String GICS_BASE_URI = "http://localhost:8090/ttp-fhir/fhir/gics";
- MockRestServiceServer mockRestServiceServer;
- AppFhirConfig appFhirConfig;
- GIcsConfigProperties gIcsConfigProperties;
+ MockRestServiceServer mockRestServiceServer;
+ AppFhirConfig appFhirConfig;
+ GIcsConfigProperties gIcsConfigProperties;
- GicsConsentService gicsConsentService;
+ GicsConsentService gicsConsentService;
- @BeforeEach
- void setUp(
- @Autowired AppFhirConfig appFhirConfig,
- @Autowired GIcsConfigProperties gIcsConfigProperties
- ) {
- this.appFhirConfig = appFhirConfig;
- this.gIcsConfigProperties = gIcsConfigProperties;
+ @BeforeEach
+ void setUp(
+ @Autowired AppFhirConfig appFhirConfig,
+ @Autowired GIcsConfigProperties gIcsConfigProperties) {
+ this.appFhirConfig = appFhirConfig;
+ this.gIcsConfigProperties = gIcsConfigProperties;
- var restTemplate = new RestTemplate();
+ var restTemplate = new RestTemplate();
- this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate);
- this.gicsConsentService = new GicsConsentService(
+ this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate);
+ this.gicsConsentService =
+ new GicsConsentService(
this.gIcsConfigProperties,
RetryTemplate.builder().maxAttempts(1).build(),
restTemplate,
- this.appFhirConfig
- );
- }
+ this.appFhirConfig);
+ }
- @Test
- void shouldReturnTtpBroadConsentStatus() {
- final Parameters consentedResponse = new Parameters()
+ @Test
+ void shouldReturnTtpBroadConsentStatus() {
+ final Parameters consentedResponse =
+ 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(consentedResponse),
- MediaType.APPLICATION_JSON
- )
- );
-
- var consentStatus = gicsConsentService.getTtpBroadConsentStatus("123456");
- assertThat(consentStatus).isEqualTo(TtpConsentStatus.BROAD_CONSENT_GIVEN);
- }
-
- @Test
- void shouldReturnRevokedConsent() {
- final Parameters revokedResponse = new Parameters()
+ .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(consentedResponse),
+ MediaType.APPLICATION_JSON));
+
+ var consentStatus = gicsConsentService.getTtpBroadConsentStatus("123456");
+ assertThat(consentStatus).isEqualTo(TtpConsentStatus.BROAD_CONSENT_GIVEN);
+ }
+
+ @Test
+ void shouldReturnRevokedConsent() {
+ final Parameters revokedResponse =
+ 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(revokedResponse),
- MediaType.APPLICATION_JSON)
- );
-
- var consentStatus = gicsConsentService.getTtpBroadConsentStatus("123456");
- assertThat(consentStatus).isEqualTo(TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED);
- }
-
-
- @Test
- void shouldReturnInvalidParameterResponse() {
- final OperationOutcome responseWithErrorOutcome = new OperationOutcome()
+ .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(revokedResponse),
+ MediaType.APPLICATION_JSON));
+
+ var consentStatus = gicsConsentService.getTtpBroadConsentStatus("123456");
+ assertThat(consentStatus).isEqualTo(TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED);
+ }
+
+ @Test
+ void shouldReturnInvalidParameterResponse() {
+ final OperationOutcome responseWithErrorOutcome =
+ 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(responseWithErrorOutcome),
- MediaType.APPLICATION_JSON
- )
- );
-
- var consentStatus = gicsConsentService.getTtpBroadConsentStatus("123456");
- assertThat(consentStatus).isEqualTo(TtpConsentStatus.FAILED_TO_ASK);
- }
-
- @Test
- void shouldReturnRequestError() {
- mockRestServiceServer
- .expect(
- requestTo(GICS_BASE_URI + GicsConsentService.IS_CONSENTED_ENDPOINT)
- )
- .andRespond(
- withServerError()
- );
-
- var consentStatus = gicsConsentService.getTtpBroadConsentStatus("123456");
- assertThat(consentStatus).isEqualTo(TtpConsentStatus.FAILED_TO_ASK);
- }
-
- @Test
- void buildRequestParameterCurrentPolicyStatesForPersonTest() {
- String pid = "12345678";
- var result = gicsConsentService
- .buildRequestParameterCurrentPolicyStatesForPerson(
- pid,
- Date.from(Instant.now()),
- ConsentDomain.MODELLVORHABEN_64E
- );
-
- assertThat(result.getParameter())
- .as("should contain 3 parameter resources")
- .hasSize(3);
-
- assertThat(((StringType) result.getParameter("domain").getValue()).getValue())
- .isEqualTo(
- gIcsConfigProperties.getGenomDeConsentDomainName()
- );
-
- assertThat(((Identifier) result.getParameter("personIdentifier").getValue()).getValue())
- .isEqualTo(
- pid
- );
- }
-
-
+ .setDiagnostics("Invalid policy parameter..."));
+
+ mockRestServiceServer
+ .expect(requestTo(GICS_BASE_URI + GicsConsentService.IS_CONSENTED_ENDPOINT))
+ .andRespond(
+ withSuccess(
+ appFhirConfig
+ .fhirContext()
+ .newJsonParser()
+ .encodeResourceToString(responseWithErrorOutcome),
+ MediaType.APPLICATION_JSON));
+
+ var consentStatus = gicsConsentService.getTtpBroadConsentStatus("123456");
+ assertThat(consentStatus).isEqualTo(TtpConsentStatus.FAILED_TO_ASK);
+ }
+
+ @Test
+ void shouldReturnRequestError() {
+ mockRestServiceServer
+ .expect(requestTo(GICS_BASE_URI + GicsConsentService.IS_CONSENTED_ENDPOINT))
+ .andRespond(withServerError());
+
+ var consentStatus = gicsConsentService.getTtpBroadConsentStatus("123456");
+ assertThat(consentStatus).isEqualTo(TtpConsentStatus.FAILED_TO_ASK);
+ }
+
+ @Test
+ void buildRequestParameterCurrentPolicyStatesForPersonTest() {
+ String pid = "12345678";
+ var result =
+ gicsConsentService.buildRequestParameterCurrentPolicyStatesForPerson(
+ pid, Date.from(Instant.now()), ConsentDomain.MODELLVORHABEN_64E);
+
+ assertThat(result.getParameter()).as("should contain 3 parameter resources").hasSize(3);
+
+ assertThat(((StringType) result.getParameter("domain").getValue()).getValue())
+ .isEqualTo(gIcsConfigProperties.getGenomDeConsentDomainName());
+
+ assertThat(((Identifier) result.getParameter("personIdentifier").getValue()).getValue())
+ .isEqualTo(pid);
+ }
+
+ @Test
+ void convertGicsResultToMiiBroadConsent() throws Exception {
+ var fhirJsonParser = FhirContext.forR4().newJsonParser();
+ fhirJsonParser.setPrettyPrint(true);
+
+ var gicsInputStream =
+ Objects.requireNonNull(
+ this.getClass()
+ .getClassLoader()
+ .getResourceAsStream("fake_broadConsent_gics_response_permit.json"));
+ var gicsConsentBundle =
+ (Bundle)
+ fhirJsonParser.parseResource(IOUtils.toString(gicsInputStream, StandardCharsets.UTF_8));
+
+ var miiInputStream =
+ Objects.requireNonNull(
+ this.getClass()
+ .getClassLoader()
+ .getResourceAsStream("fake_broadConsent_mii_response_permit.json"));
+ var miiConsent = IOUtils.toString(miiInputStream, StandardCharsets.UTF_8);
+
+ var actual = gicsConsentService.convertGicsResultToMiiBroadConsent(gicsConsentBundle);
+
+ assertThat(fhirJsonParser.encodeToString(actual)).isEqualTo(miiConsent);
+ }
+
+ @Test
+ void convertedMiiBroadConsentShouldNotContainPatientId() throws Exception {
+ var fhirJsonParser = FhirContext.forR4().newJsonParser();
+ fhirJsonParser.setPrettyPrint(true);
+
+ var miiInputStream =
+ Objects.requireNonNull(
+ this.getClass()
+ .getClassLoader()
+ .getResourceAsStream("fake_broadConsent_mii_response_permit.json"));
+ var miiConsentBundle =
+ (Bundle)
+ fhirJsonParser.parseResource(IOUtils.toString(miiInputStream, StandardCharsets.UTF_8));
+
+ var currentPatientId = miiConsentBundle.getEntry().getFirst().getResource().getIdPart();
+
+ var actual = gicsConsentService.anonymizeBroadConsent(miiConsentBundle);
+ assertThat(fhirJsonParser.encodeToString(actual)).doesNotContain(currentPatientId);
+ }
+
+ @Test
+ void miiBroadConsentShouldNotBeConvertedAgain() throws Exception {
+ var fhirJsonParser = FhirContext.forR4().newJsonParser();
+ fhirJsonParser.setPrettyPrint(true);
+
+ var gicsInputStream =
+ Objects.requireNonNull(
+ this.getClass()
+ .getClassLoader()
+ .getResourceAsStream("fake_broadConsent_mii_response_permit.json"));
+ var gicsConsentBundle =
+ (Bundle)
+ fhirJsonParser.parseResource(IOUtils.toString(gicsInputStream, StandardCharsets.UTF_8));
+
+ var miiInputStream =
+ Objects.requireNonNull(
+ this.getClass()
+ .getClassLoader()
+ .getResourceAsStream("fake_broadConsent_mii_response_permit.json"));
+ var miiConsent = IOUtils.toString(miiInputStream, StandardCharsets.UTF_8);
+
+ var actual = gicsConsentService.convertGicsResultToMiiBroadConsent(gicsConsentBundle);
+
+ assertThat(fhirJsonParser.encodeToString(actual)).isEqualTo(miiConsent);
+ }
}
diff --git a/src/test/kotlin/dev/dnpm/etl/processor/consent/ConsentProcessorTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/consent/ConsentProcessorTest.kt
new file mode 100644
index 0000000..5a86a29
--- /dev/null
+++ b/src/test/kotlin/dev/dnpm/etl/processor/consent/ConsentProcessorTest.kt
@@ -0,0 +1,58 @@
+package dev.dnpm.etl.processor.consent
+
+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.services.ConsentProcessor
+import org.assertj.core.api.Assertions.assertThat
+import org.hl7.fhir.r4.model.Bundle
+import org.hl7.fhir.r4.model.Consent
+import org.junit.jupiter.api.BeforeEach
+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 java.util.*
+
+@ExtendWith(MockitoExtension::class)
+class ConsentProcessorTest {
+
+ lateinit var consentProcessor: ConsentProcessor
+
+ val objectMapper = ObjectMapper()
+ val fhirContext = FhirContext.forR4()
+
+ @BeforeEach
+ fun setup(
+ @Mock consentService: IConsentService
+ ) {
+ val appConfigProperties = AppConfigProperties()
+ val gIcsConfigProperties = GIcsConfigProperties("http://localhost")
+
+ this.consentProcessor = ConsentProcessor(
+ appConfigProperties,
+ gIcsConfigProperties,
+ objectMapper,
+ fhirContext,
+ consentService
+ )
+ }
+
+ @ParameterizedTest
+ @CsvSource(value = [
+ "permittedConsentBundle.json,permit",
+ "deniedConsentBundle.json,deny"
+ ])
+ fun checkGetProvisionTypeByPolicyCode(filename: String, expected: String) {
+ val bundle = fhirContext.newJsonParser().parseResource(
+ this.javaClass.classLoader.getResourceAsStream(filename)
+ )
+ assertThat(bundle).isInstanceOf(Bundle::class.java)
+
+ val actual = consentProcessor.getProvisionTypeByPolicyCode(bundle as Bundle, Date(), ConsentDomain.BROAD_CONSENT)
+
+ assertThat(actual).isEqualTo(Consent.ConsentProvisionType.valueOf(expected.uppercase()))
+ }
+} \ No newline at end of file
diff --git a/src/test/kotlin/dev/dnpm/etl/processor/monitoring/ConnectionCheckServiceTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/monitoring/ConnectionCheckServiceTest.kt
index 788ca6a..d34d2b8 100644
--- a/src/test/kotlin/dev/dnpm/etl/processor/monitoring/ConnectionCheckServiceTest.kt
+++ b/src/test/kotlin/dev/dnpm/etl/processor/monitoring/ConnectionCheckServiceTest.kt
@@ -3,20 +3,22 @@ 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 org.assertj.core.api.Assertions.assertThat
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.mockito.Mock
import org.mockito.junit.jupiter.MockitoExtension
import org.springframework.http.HttpMethod
import org.springframework.http.MediaType
import org.springframework.test.web.client.MockRestServiceServer
import org.springframework.test.web.client.match.MockRestRequestMatchers.method
import org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo
+import org.springframework.test.web.client.response.MockRestResponseCreators.withServerError
import org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess
import org.springframework.web.client.RestTemplate
import reactor.core.publisher.Sinks
+import reactor.test.StepVerifier
@ExtendWith(MockitoExtension::class)
class ConnectionCheckServiceTest {
@@ -26,19 +28,18 @@ class ConnectionCheckServiceTest {
lateinit var mockRestServiceServer: MockRestServiceServer
lateinit var service: RestConnectionCheckService
+ lateinit var sink: Sinks.Many<ConnectionCheckResult>
@BeforeEach
- fun setUp(
- @Mock sink: Sinks.Many<ConnectionCheckResult>
- ) {
+ fun setUp() {
val restTemplate = RestTemplate()
- this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
-
val restTargetProperties = RestTargetProperties(
"http://localhost/api",
"user",
"password",
)
+ this.sink = Sinks.many().multicast().onBackpressureBuffer()
+ this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
this.service = RestConnectionCheckService(restTemplate, restTargetProperties, sink)
}
@@ -53,9 +54,51 @@ class ConnectionCheckServiceTest {
)
this.service.check()
-
this.mockRestServiceServer.verify()
+ }
+ @Test
+ fun shouldEmitAvailable() {
+ this.mockRestServiceServer
+ .expect(method(HttpMethod.GET))
+ .andRespond(
+ withSuccess("OK", MediaType.APPLICATION_JSON),
+ )
+
+ val verifier = StepVerifier.create(sink.asFlux())
+ .assertNext {
+ assertThat(it.available).isTrue()
+ }
+ .expectComplete()
+ .verifyLater()
+
+ this.service.check()
+
+ this.sink.emitComplete(Sinks.EmitFailureHandler.FAIL_FAST)
+
+ verifier.verify()
+ }
+
+ @Test
+ fun shouldEmitUnavailable() {
+ this.mockRestServiceServer
+ .expect(method(HttpMethod.GET))
+ .andRespond(
+ withServerError()
+ )
+
+ val verifier = StepVerifier.create(sink.asFlux())
+ .assertNext {
+ assertThat(it.available).isFalse()
+ }
+ .expectComplete()
+ .verifyLater()
+
+ this.service.check()
+
+ this.sink.emitComplete(Sinks.EmitFailureHandler.FAIL_FAST)
+
+ verifier.verify()
}
}
@@ -64,21 +107,21 @@ class ConnectionCheckServiceTest {
lateinit var mockRestServiceServer: MockRestServiceServer
lateinit var service: GPasConnectionCheckService
+ lateinit var sink: Sinks.Many<ConnectionCheckResult>
@BeforeEach
- fun setUp(
- @Mock sink: Sinks.Many<ConnectionCheckResult>
- ) {
+ fun setUp() {
val restTemplate = RestTemplate()
- this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
-
val gpasTargetProperties = GPasConfigProperties(
"http://localhost/gpas",
+ null,
"patientDomain",
"genomDeTanDomain",
"username",
"password",
)
+ this.sink = Sinks.many().multicast().onBackpressureBuffer()
+ this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
this.service = GPasConnectionCheckService(restTemplate, gpasTargetProperties, sink)
}
@@ -95,7 +138,50 @@ class ConnectionCheckServiceTest {
this.service.check()
this.mockRestServiceServer.verify()
+ }
+
+ @Test
+ fun shouldEmitAvailable() {
+ this.mockRestServiceServer
+ .expect(method(HttpMethod.GET))
+ .andRespond(
+ withSuccess("OK", MediaType.APPLICATION_JSON),
+ )
+
+ val verifier = StepVerifier.create(sink.asFlux())
+ .assertNext {
+ assertThat(it.available).isTrue()
+ }
+ .expectComplete()
+ .verifyLater()
+
+ this.service.check()
+
+ this.sink.emitComplete(Sinks.EmitFailureHandler.FAIL_FAST)
+ verifier.verify()
+ }
+
+ @Test
+ fun shouldEmitUnavailable() {
+ this.mockRestServiceServer
+ .expect(method(HttpMethod.GET))
+ .andRespond(
+ withServerError()
+ )
+
+ val verifier = StepVerifier.create(sink.asFlux())
+ .assertNext {
+ assertThat(it.available).isFalse()
+ }
+ .expectComplete()
+ .verifyLater()
+
+ this.service.check()
+
+ this.sink.emitComplete(Sinks.EmitFailureHandler.FAIL_FAST)
+
+ verifier.verify()
}
}
@@ -104,19 +190,19 @@ class ConnectionCheckServiceTest {
lateinit var mockRestServiceServer: MockRestServiceServer
lateinit var service: GIcsConnectionCheckService
+ lateinit var sink: Sinks.Many<ConnectionCheckResult>
@BeforeEach
- fun setUp(
- @Mock sink: Sinks.Many<ConnectionCheckResult>
- ) {
+ fun setUp() {
val restTemplate = RestTemplate()
- this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
val gicsTargetProperties = GIcsConfigProperties(
"http://localhost/gics",
"username",
"password",
)
+ this.sink = Sinks.many().multicast().onBackpressureBuffer()
+ this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
this.service = GIcsConnectionCheckService(restTemplate, gicsTargetProperties, sink)
}
@@ -135,6 +221,50 @@ class ConnectionCheckServiceTest {
this.mockRestServiceServer.verify()
}
+
+ @Test
+ fun shouldEmitAvailable() {
+ this.mockRestServiceServer
+ .expect(method(HttpMethod.GET))
+ .andRespond(
+ withSuccess("OK", MediaType.APPLICATION_JSON),
+ )
+
+ val verifier = StepVerifier.create(sink.asFlux())
+ .assertNext {
+ assertThat(it.available).isTrue()
+ }
+ .expectComplete()
+ .verifyLater()
+
+ this.service.check()
+
+ this.sink.emitComplete(Sinks.EmitFailureHandler.FAIL_FAST)
+
+ verifier.verify()
+ }
+
+ @Test
+ fun shouldEmitUnavailable() {
+ this.mockRestServiceServer
+ .expect(method(HttpMethod.GET))
+ .andRespond(
+ withServerError()
+ )
+
+ val verifier = StepVerifier.create(sink.asFlux())
+ .assertNext {
+ assertThat(it.available).isFalse()
+ }
+ .expectComplete()
+ .verifyLater()
+
+ this.service.check()
+
+ this.sink.emitComplete(Sinks.EmitFailureHandler.FAIL_FAST)
+
+ verifier.verify()
+ }
}
} \ No newline at end of file
diff --git a/src/test/kotlin/dev/dnpm/etl/processor/services/ConsentProcessorTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/services/ConsentProcessorTest.kt
index 4d414c5..bbc8b1a 100644
--- a/src/test/kotlin/dev/dnpm/etl/processor/services/ConsentProcessorTest.kt
+++ b/src/test/kotlin/dev/dnpm/etl/processor/services/ConsentProcessorTest.kt
@@ -80,7 +80,7 @@ class ConsentProcessorTest {
val checkResult = consentProcessor.consentGatedCheckAndTryEmbedding(inputMtb)
assertThat(checkResult).isTrue
- assertThat(inputMtb.metadata.researchConsents).hasSize(26)
+ assertThat(inputMtb.metadata.researchConsents).isNotEmpty
}
companion object {
diff --git a/src/test/resources/deniedConsentBundle.json b/src/test/resources/deniedConsentBundle.json
new file mode 100644
index 0000000..3487c57
--- /dev/null
+++ b/src/test/resources/deniedConsentBundle.json
@@ -0,0 +1,692 @@
+{
+ "resourceType": "Bundle",
+ "type": "collection",
+ "entry": [
+ {
+ "fullUrl": "http://gics:8080/ttp-fhir/fhir/gics/Consent/XXXX",
+ "resource": {
+ "resourceType": "Consent",
+ "id": "XXXX",
+ "meta": {
+ "lastUpdated": "2025-10-31T14:21:04.630+01:00",
+ "profile": [
+ "http://fhir.de/ConsentManagement/StructureDefinition/Consent",
+ "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/VVVVV"
+ }
+ },
+ {
+ "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": "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/psn-pat-here",
+ "display": "Patienten-ID PID-HERE"
+ },
+ "dateTime": "2025-10-13T00:00:00+01:00",
+ "organization": [
+ {
+ "display": "MII"
+ }
+ ],
+ "sourceReference": {
+ "reference": "QuestionnaireResponse/XXXX"
+ },
+ "policy": [
+ {
+ "uri": "urn:oid:2.16.840.1.113883.3.1937.777.24.2.1791"
+ }
+ ],
+ "provision": {
+ "type": "deny",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2055-10-13T00:00:00+01:00"
+ },
+ "provision": [
+ {
+ "type": "deny",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2055-10-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy",
+ "code": "IDAT_erheben",
+ "display": "Erfassung neuer identifizierender Daten (IDAT)"
+ },
+ {
+ "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"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "deny",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2055-10-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/MII",
+ "code": "KKDAT_5J_retro_speichern_verarbeiten",
+ "display": "Retrospektive Krankenkassendaten (KKDAT) aus fünf Jahren vor Einwilligung speichern und codiert verarbeiten zu Zwecken med. Forschung in der verantwortlichen Stelle"
+ },
+ {
+ "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.12",
+ "display": "KKDAT 5J retrospektiv speichern verarbeiten"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "deny",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2055-10-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/MII",
+ "code": "BIOMAT_wissenschaftlich_nutzen_EU_DSGVO_konform",
+ "display": "Bereitstellung umcodierter Biomaterialien (BIOMAT) für wissenschaftliche Nutzung und Analysen zu Zwecken med. Forschung an ext. Forscher"
+ },
+ {
+ "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.22",
+ "display": "BIOMAT wissenschaftlich nutzen EU DSGVO NIVEAU"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "deny",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2055-10-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/MII",
+ "code": "KKDAT_5J_retro_wissenschaftlich_nutzen",
+ "display": "Bereitstellung umcodierter retrospektiver Krankenkassendaten (KKDAT) für wissenschaftliche Nutzung zu Zwecken med. Forschung an externe Forscher"
+ },
+ {
+ "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.13",
+ "display": "KKDAT 5J retrospektiv wissenschaftlich nutzen"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "deny",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2055-10-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy",
+ "code": "BIOMAT_Analysedaten_zusammenfuehren_Dritte",
+ "display": "Zusammenführen von auf Biomaterialien (BIOMAT) basierenden Analysedaten mit Analysedaten Dritter, sofern dort ebenfalls eine Einwilligung vorliegt"
+ },
+ {
+ "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.23",
+ "display": "BIOMAT Analysedaten zusammenfuehren Dritte"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "deny",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2055-10-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/MII",
+ "code": "Rekontaktierung_Zusatzbefund",
+ "display": "Rekontaktierung bezüglich Zusatzbefund im Rahmen der am Standort dafür entwickelten Prozesse und der im Nutzungsantrag angegebenen Bedingungen"
+ },
+ {
+ "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"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "deny",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2055-10-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/MII",
+ "code": "KKDAT_5J_pro_wissenschaftlich_nutzen",
+ "display": "Bereitstellung umcodierter prospektiver Krankenkassendaten (KKDAT) aus fünf Jahren ab Einwilligung zu Zwecken med. Forschung an ext. Forscher"
+ },
+ {
+ "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.17",
+ "display": "KKDAT 5J prospektiv wissenschaftlich nutzen"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "deny",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2055-10-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy",
+ "code": "BIOMAT_lagern_verarbeiten",
+ "display": "Lagerung und Verarbeitung von Biomaterialien innerhalb der verantwortlichen Stelle (BIOMAT)"
+ },
+ {
+ "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.20",
+ "display": "BIOMAT lagern verarbeiten"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "deny",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2055-10-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/MII",
+ "code": "KKDAT_5J_retro_uebertragen",
+ "display": "Krankenkassendaten (KKDAT) der letzten fünf Kalenderjahre vor Datum Unterschrift übertragen"
+ },
+ {
+ "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.11",
+ "display": "KKDAT 5J retrospektiv uebertragen"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "deny",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2055-10-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/MII",
+ "code": "KKDAT_KVNR_5J_retro_uebertragen",
+ "display": "Erlaubnis zur retrospektiven Übermittlung der KVNr., MII-Pseudonym und Zeitraum Datenübermittlung (von:5 Jahre vor Datum Unterschrift; bis: Datum Unterschrift) an zuständige Stelle"
+ },
+ {
+ "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.38",
+ "display": "KKDAT 5J retrospektiv uebertragen KVNR"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "deny",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2030-03-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy",
+ "code": "MDAT_erheben",
+ "display": "Erfassung medizinischer Daten (MDAT)"
+ },
+ {
+ "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"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "deny",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2055-10-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/MII",
+ "code": "KKDAT_5J_pro_speichern_verarbeiten",
+ "display": "Prospektive Krankenkassendaten (KKDAT) aus fünf Jahren ab Einwilligung speichern und codiert verarbeiten zu Zwecken der med. Forschung in der verantwortlichen Stelle"
+ },
+ {
+ "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.16",
+ "display": "KKDAT 5J prospektiv speichern verarbeiten"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "deny",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2055-10-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/MII",
+ "code": "MDAT_wissenschaftlich_nutzen_EU_DSGVO_konform",
+ "display": "Bereitstellung umcodierter medizinischer Daten (MDAT) für wissenschaftliche Nutzung zu Zwecken med. Forschung an externe Forscher"
+ },
+ {
+ "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"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "deny",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2055-10-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy",
+ "code": "Rekontaktierung_Verknuepfung_Datenbanken",
+ "display": "Rekontaktierung zur Verknüpfung von Patientendaten mit Daten anderer Datenbanken"
+ },
+ {
+ "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 Verknüpfung Datenbanken"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "deny",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2055-10-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy",
+ "code": "Rekontaktierung_weitere_Studien",
+ "display": "Rekontaktierung bezüglich Information zu neuen Forschungsvorhaben oder Studien"
+ },
+ {
+ "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"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "deny",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2055-10-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/MII",
+ "code": "IDAT_bereitstellen_EU_DSGVO_konform",
+ "display": "Herausgabe identifizierender Daten (IDAT) an unabhängige Treuhandstelle zur weiteren Verarbeitung"
+ },
+ {
+ "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"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "deny",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2055-10-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy",
+ "code": "IDAT_speichern_verarbeiten",
+ "display": "Speicherung und Verarbeitung identifizierender Daten (IDAT) zu Zwecken med. Forschung in der verantwortlichen Stelle"
+ },
+ {
+ "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"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "deny",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2055-10-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy",
+ "code": "MDAT_speichern_verarbeiten",
+ "display": "Speicherung und Verarbeitung von medizinischen codierten Daten zu Zwecken med. Forschung innerhalb der verantwortlichen Stelle (MDAT)"
+ },
+ {
+ "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"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "deny",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2030-03-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy",
+ "code": "BIOMAT_erheben",
+ "display": "Gewinnung von Biomaterialien (BIOMAT)"
+ },
+ {
+ "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.19",
+ "display": "BIOMAT erheben"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "deny",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2055-10-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/MII",
+ "code": "Rekontaktierung_Ergebnisse_erheblicher_Bedeutung",
+ "display": "Rekontaktierung des Betroffenen bei Ergebnissen von erheblicher Bedeutung"
+ },
+ {
+ "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"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "deny",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2030-03-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/MII",
+ "code": "KKDAT_KVNR_5J_pro_uebertragen",
+ "display": "Erlaubnis zur prospektiven Übermittlung der KVNr., MII-Pseudonym und Zeitraum Datenübermittlung (von: Datum Unterschrift; bis: max. 5 Kalenderjahre nach Unterschrift) an zuständige Stelle"
+ },
+ {
+ "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.39",
+ "display": "KKDAT 5J prospektiv uebertragen KVNR"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "deny",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2030-03-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy",
+ "code": "BIOMAT_Zusatzmengen_entnehmen",
+ "display": "Entnahme zusätzlicher Mengen von Biomaterialien (BIOMAT) in den in der Einwilligung beschriebenen Grenzen"
+ },
+ {
+ "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.25",
+ "display": "BIOMAT Zusatzmengen entnehmen"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "deny",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2055-10-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy",
+ "code": "Rekontaktierung_weitere_Erhebung",
+ "display": "Rekontaktierung bezüglich Erhebung zusätzlicher Daten"
+ },
+ {
+ "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"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "deny",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2055-10-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy",
+ "code": "IDAT_zusammenfuehren_Dritte",
+ "display": "Zusammenführung identifizierender Daten (IDAT) über die unabhängige Treuhandstelle mit Dritten Forschungspartnern, sofern dort eine Einwilligung vorliegt"
+ },
+ {
+ "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"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "deny",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2055-10-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy",
+ "code": "MDAT_zusammenfuehren_Dritte",
+ "display": "Zusammenführung medizinischer Daten (MDAT) mit Dritten Forschungspartnern, sofern dort eine Einwilligung vorliegt"
+ },
+ {
+ "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"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "deny",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2030-03-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/MII",
+ "code": "KKDAT_5J_pro_uebertragen",
+ "display": "Prospektive Krankenkassendaten (KKDAT) für fünf Kalenderjahre nach Datum Unterschrift übertragen"
+ },
+ {
+ "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.15",
+ "display": "KKDAT 5J prospektiv uebertragen"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ ]
+}
diff --git a/src/test/resources/fake_broadConsent_mii_response_permit.json b/src/test/resources/fake_broadConsent_mii_response_permit.json
new file mode 100644
index 0000000..53f13b5
--- /dev/null
+++ b/src/test/resources/fake_broadConsent_mii_response_permit.json
@@ -0,0 +1,513 @@
+{
+ "resourceType": "Bundle",
+ "type": "collection",
+ "total": 26,
+ "entry": [ {
+ "fullUrl": "http://localhost:8080/ttp-fhir/fhir/gics/Consent/7d3456c2-79b1-11f0-ab27-6ed0ed82d0fd",
+ "resource": {
+ "resourceType": "Consent",
+ "id": "7d3456c2-79b1-11f0-ab27-6ed0ed82d0fd",
+ "meta": {
+ "lastUpdated": "2025-08-15T11:13:59.143+02:00",
+ "profile": [ "http://fhir.de/ConsentManagement/StructureDefinition/Consent", "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/3c3ffec5-79b1-11f0-ab27-6ed0ed82d0fd"
+ }
+ }, {
+ "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": "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/7d2da57f-79b1-11f0-ab27-6ed0ed82d0fd",
+ "display": "Patienten-ID 644bae7a-56f6-4ee8-b02f-c532e65af5b1"
+ },
+ "dateTime": "2025-08-15T00:00:00+02:00",
+ "organization": [ {
+ "display": "MII"
+ } ],
+ "sourceReference": {
+ "reference": "QuestionnaireResponse/7d314bc5-79b1-11f0-ab27-6ed0ed82d0fd"
+ },
+ "policy": [ {
+ "uri": "urn:oid:2.16.840.1.113883.3.1937.777.24.2.1791"
+ } ],
+ "provision": {
+ "type": "deny",
+ "period": {
+ "start": "2025-08-15T00:00:00+02:00",
+ "end": "2055-08-15T00:00:00+02:00"
+ },
+ "provision": [ {
+ "type": "permit",
+ "period": {
+ "start": "2025-08-15T00:00:00+02:00",
+ "end": "2055-08-15T00:00:00+02:00"
+ },
+ "code": [ {
+ "coding": [ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy",
+ "code": "IDAT_erheben",
+ "display": "Erfassung neuer identifizierender Daten (IDAT)"
+ }, {
+ "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"
+ } ]
+ } ]
+ }, {
+ "type": "permit",
+ "period": {
+ "start": "2025-08-15T00:00:00+02:00",
+ "end": "2055-08-15T00:00:00+02:00"
+ },
+ "code": [ {
+ "coding": [ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/MII",
+ "code": "KKDAT_5J_retro_speichern_verarbeiten",
+ "display": "Retrospektive Krankenkassendaten (KKDAT) aus fünf Jahren vor Einwilligung speichern und codiert verarbeiten zu Zwecken med. Forschung in der verantwortlichen Stelle"
+ }, {
+ "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.12",
+ "display": "KKDAT 5J retrospektiv speichern verarbeiten"
+ } ]
+ } ]
+ }, {
+ "type": "permit",
+ "period": {
+ "start": "2025-08-15T00:00:00+02:00",
+ "end": "2055-08-15T00:00:00+02:00"
+ },
+ "code": [ {
+ "coding": [ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/MII",
+ "code": "BIOMAT_wissenschaftlich_nutzen_EU_DSGVO_konform",
+ "display": "Bereitstellung umcodierter Biomaterialien (BIOMAT) für wissenschaftliche Nutzung und Analysen zu Zwecken med. Forschung an ext. Forscher"
+ }, {
+ "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.22",
+ "display": "BIOMAT wissenschaftlich nutzen EU DSGVO NIVEAU"
+ } ]
+ } ]
+ }, {
+ "type": "permit",
+ "period": {
+ "start": "2025-08-15T00:00:00+02:00",
+ "end": "2055-08-15T00:00:00+02:00"
+ },
+ "code": [ {
+ "coding": [ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/MII",
+ "code": "KKDAT_5J_retro_wissenschaftlich_nutzen",
+ "display": "Bereitstellung umcodierter retrospektiver Krankenkassendaten (KKDAT) für wissenschaftliche Nutzung zu Zwecken med. Forschung an externe Forscher"
+ }, {
+ "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.13",
+ "display": "KKDAT 5J retrospektiv wissenschaftlich nutzen"
+ } ]
+ } ]
+ }, {
+ "type": "permit",
+ "period": {
+ "start": "2025-08-15T00:00:00+02:00",
+ "end": "2055-08-15T00:00:00+02:00"
+ },
+ "code": [ {
+ "coding": [ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy",
+ "code": "BIOMAT_Analysedaten_zusammenfuehren_Dritte",
+ "display": "Zusammenführen von auf Biomaterialien (BIOMAT) basierenden Analysedaten mit Analysedaten Dritter, sofern dort ebenfalls eine Einwilligung vorliegt"
+ }, {
+ "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.23",
+ "display": "BIOMAT Analysedaten zusammenfuehren Dritte"
+ } ]
+ } ]
+ }, {
+ "type": "permit",
+ "period": {
+ "start": "2025-08-15T00:00:00+02:00",
+ "end": "2055-08-15T00:00:00+02:00"
+ },
+ "code": [ {
+ "coding": [ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/MII",
+ "code": "Rekontaktierung_Zusatzbefund",
+ "display": "Rekontaktierung bezüglich Zusatzbefund im Rahmen der am Standort dafür entwickelten Prozesse und der im Nutzungsantrag angegebenen Bedingungen"
+ }, {
+ "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"
+ } ]
+ } ]
+ }, {
+ "type": "permit",
+ "period": {
+ "start": "2025-08-15T00:00:00+02:00",
+ "end": "2055-08-15T00:00:00+02:00"
+ },
+ "code": [ {
+ "coding": [ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/MII",
+ "code": "KKDAT_5J_pro_wissenschaftlich_nutzen",
+ "display": "Bereitstellung umcodierter prospektiver Krankenkassendaten (KKDAT) aus fünf Jahren ab Einwilligung zu Zwecken med. Forschung an ext. Forscher"
+ }, {
+ "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.17",
+ "display": "KKDAT 5J prospektiv wissenschaftlich nutzen"
+ } ]
+ } ]
+ }, {
+ "type": "permit",
+ "period": {
+ "start": "2025-08-15T00:00:00+02:00",
+ "end": "2055-08-15T00:00:00+02:00"
+ },
+ "code": [ {
+ "coding": [ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy",
+ "code": "BIOMAT_lagern_verarbeiten",
+ "display": "Lagerung und Verarbeitung von Biomaterialien innerhalb der verantwortlichen Stelle (BIOMAT)"
+ }, {
+ "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.20",
+ "display": "BIOMAT lagern verarbeiten"
+ } ]
+ } ]
+ }, {
+ "type": "permit",
+ "period": {
+ "start": "2025-08-15T00:00:00+02:00",
+ "end": "2055-08-15T00:00:00+02:00"
+ },
+ "code": [ {
+ "coding": [ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/MII",
+ "code": "KKDAT_5J_retro_uebertragen",
+ "display": "Krankenkassendaten (KKDAT) der letzten fünf Kalenderjahre vor Datum Unterschrift übertragen"
+ }, {
+ "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.11",
+ "display": "KKDAT 5J retrospektiv uebertragen"
+ } ]
+ } ]
+ }, {
+ "type": "permit",
+ "period": {
+ "start": "2025-08-15T00:00:00+02:00",
+ "end": "2055-08-15T00:00:00+02:00"
+ },
+ "code": [ {
+ "coding": [ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/MII",
+ "code": "KKDAT_KVNR_5J_retro_uebertragen",
+ "display": "Erlaubnis zur retrospektiven Übermittlung der KVNr., MII-Pseudonym und Zeitraum Datenübermittlung (von:5 Jahre vor Datum Unterschrift; bis: Datum Unterschrift) an zuständige Stelle"
+ }, {
+ "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.38",
+ "display": "KKDAT 5J retrospektiv uebertragen KVNR"
+ } ]
+ } ]
+ }, {
+ "type": "permit",
+ "period": {
+ "start": "2025-08-15T00:00:00+02:00",
+ "end": "2030-08-15T00:00:00+02:00"
+ },
+ "code": [ {
+ "coding": [ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy",
+ "code": "MDAT_erheben",
+ "display": "Erfassung medizinischer Daten (MDAT)"
+ }, {
+ "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"
+ } ]
+ } ]
+ }, {
+ "type": "permit",
+ "period": {
+ "start": "2025-08-15T00:00:00+02:00",
+ "end": "2055-08-15T00:00:00+02:00"
+ },
+ "code": [ {
+ "coding": [ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/MII",
+ "code": "KKDAT_5J_pro_speichern_verarbeiten",
+ "display": "Prospektive Krankenkassendaten (KKDAT) aus fünf Jahren ab Einwilligung speichern und codiert verarbeiten zu Zwecken der med. Forschung in der verantwortlichen Stelle"
+ }, {
+ "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.16",
+ "display": "KKDAT 5J prospektiv speichern verarbeiten"
+ } ]
+ } ]
+ }, {
+ "type": "permit",
+ "period": {
+ "start": "2025-08-15T00:00:00+02:00",
+ "end": "2055-08-15T00:00:00+02:00"
+ },
+ "code": [ {
+ "coding": [ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/MII",
+ "code": "MDAT_wissenschaftlich_nutzen_EU_DSGVO_konform",
+ "display": "Bereitstellung umcodierter medizinischer Daten (MDAT) für wissenschaftliche Nutzung zu Zwecken med. Forschung an externe Forscher"
+ }, {
+ "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"
+ } ]
+ } ]
+ }, {
+ "type": "deny",
+ "period": {
+ "start": "2025-08-15T00:00:00+02:00",
+ "end": "2055-08-15T00:00:00+02:00"
+ },
+ "code": [ {
+ "coding": [ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy",
+ "code": "Rekontaktierung_Verknuepfung_Datenbanken",
+ "display": "Rekontaktierung zur Verknüpfung von Patientendaten mit Daten anderer Datenbanken"
+ }, {
+ "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 Verknüpfung Datenbanken"
+ } ]
+ } ]
+ }, {
+ "type": "permit",
+ "period": {
+ "start": "2025-08-15T00:00:00+02:00",
+ "end": "2055-08-15T00:00:00+02:00"
+ },
+ "code": [ {
+ "coding": [ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy",
+ "code": "Rekontaktierung_weitere_Studien",
+ "display": "Rekontaktierung bezüglich Information zu neuen Forschungsvorhaben oder Studien"
+ }, {
+ "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"
+ } ]
+ } ]
+ }, {
+ "type": "permit",
+ "period": {
+ "start": "2025-08-15T00:00:00+02:00",
+ "end": "2055-08-15T00:00:00+02:00"
+ },
+ "code": [ {
+ "coding": [ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/MII",
+ "code": "IDAT_bereitstellen_EU_DSGVO_konform",
+ "display": "Herausgabe identifizierender Daten (IDAT) an unabhängige Treuhandstelle zur weiteren Verarbeitung"
+ }, {
+ "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"
+ } ]
+ } ]
+ }, {
+ "type": "permit",
+ "period": {
+ "start": "2025-08-15T00:00:00+02:00",
+ "end": "2055-08-15T00:00:00+02:00"
+ },
+ "code": [ {
+ "coding": [ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy",
+ "code": "IDAT_speichern_verarbeiten",
+ "display": "Speicherung und Verarbeitung identifizierender Daten (IDAT) zu Zwecken med. Forschung in der verantwortlichen Stelle"
+ }, {
+ "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"
+ } ]
+ } ]
+ }, {
+ "type": "permit",
+ "period": {
+ "start": "2025-08-15T00:00:00+02:00",
+ "end": "2055-08-15T00:00:00+02:00"
+ },
+ "code": [ {
+ "coding": [ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy",
+ "code": "MDAT_speichern_verarbeiten",
+ "display": "Speicherung und Verarbeitung von medizinischen codierten Daten zu Zwecken med. Forschung innerhalb der verantwortlichen Stelle (MDAT)"
+ }, {
+ "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"
+ } ]
+ } ]
+ }, {
+ "type": "permit",
+ "period": {
+ "start": "2025-08-15T00:00:00+02:00",
+ "end": "2030-08-15T00:00:00+02:00"
+ },
+ "code": [ {
+ "coding": [ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy",
+ "code": "BIOMAT_erheben",
+ "display": "Gewinnung von Biomaterialien (BIOMAT)"
+ }, {
+ "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.19",
+ "display": "BIOMAT erheben"
+ } ]
+ } ]
+ }, {
+ "type": "permit",
+ "period": {
+ "start": "2025-08-15T00:00:00+02:00",
+ "end": "2055-08-15T00:00:00+02:00"
+ },
+ "code": [ {
+ "coding": [ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/MII",
+ "code": "Rekontaktierung_Ergebnisse_erheblicher_Bedeutung",
+ "display": "Rekontaktierung des Betroffenen bei Ergebnissen von erheblicher Bedeutung"
+ }, {
+ "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"
+ } ]
+ } ]
+ }, {
+ "type": "permit",
+ "period": {
+ "start": "2025-08-15T00:00:00+02:00",
+ "end": "2030-08-15T00:00:00+02:00"
+ },
+ "code": [ {
+ "coding": [ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/MII",
+ "code": "KKDAT_KVNR_5J_pro_uebertragen",
+ "display": "Erlaubnis zur prospektiven Übermittlung der KVNr., MII-Pseudonym und Zeitraum Datenübermittlung (von: Datum Unterschrift; bis: max. 5 Kalenderjahre nach Unterschrift) an zuständige Stelle"
+ }, {
+ "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.39",
+ "display": "KKDAT 5J prospektiv uebertragen KVNR"
+ } ]
+ } ]
+ }, {
+ "type": "permit",
+ "period": {
+ "start": "2025-08-15T00:00:00+02:00",
+ "end": "2030-08-15T00:00:00+02:00"
+ },
+ "code": [ {
+ "coding": [ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy",
+ "code": "BIOMAT_Zusatzmengen_entnehmen",
+ "display": "Entnahme zusätzlicher Mengen von Biomaterialien (BIOMAT) in den in der Einwilligung beschriebenen Grenzen"
+ }, {
+ "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.25",
+ "display": "BIOMAT Zusatzmengen entnehmen"
+ } ]
+ } ]
+ }, {
+ "type": "permit",
+ "period": {
+ "start": "2025-08-15T00:00:00+02:00",
+ "end": "2055-08-15T00:00:00+02:00"
+ },
+ "code": [ {
+ "coding": [ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy",
+ "code": "Rekontaktierung_weitere_Erhebung",
+ "display": "Rekontaktierung bezüglich Erhebung zusätzlicher Daten"
+ }, {
+ "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"
+ } ]
+ } ]
+ }, {
+ "type": "permit",
+ "period": {
+ "start": "2025-08-15T00:00:00+02:00",
+ "end": "2055-08-15T00:00:00+02:00"
+ },
+ "code": [ {
+ "coding": [ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy",
+ "code": "IDAT_zusammenfuehren_Dritte",
+ "display": "Zusammenführung identifizierender Daten (IDAT) über die unabhängige Treuhandstelle mit Dritten Forschungspartnern, sofern dort eine Einwilligung vorliegt"
+ }, {
+ "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"
+ } ]
+ } ]
+ }, {
+ "type": "permit",
+ "period": {
+ "start": "2025-08-15T00:00:00+02:00",
+ "end": "2055-08-15T00:00:00+02:00"
+ },
+ "code": [ {
+ "coding": [ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy",
+ "code": "MDAT_zusammenfuehren_Dritte",
+ "display": "Zusammenführung medizinischer Daten (MDAT) mit Dritten Forschungspartnern, sofern dort eine Einwilligung vorliegt"
+ }, {
+ "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"
+ } ]
+ } ]
+ }, {
+ "type": "permit",
+ "period": {
+ "start": "2025-08-15T00:00:00+02:00",
+ "end": "2030-08-15T00:00:00+02:00"
+ },
+ "code": [ {
+ "coding": [ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/MII",
+ "code": "KKDAT_5J_pro_uebertragen",
+ "display": "Prospektive Krankenkassendaten (KKDAT) für fünf Kalenderjahre nach Datum Unterschrift übertragen"
+ }, {
+ "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.15",
+ "display": "KKDAT 5J prospektiv uebertragen"
+ } ]
+ } ]
+ } ]
+ }
+ }
+ } ]
+} \ No newline at end of file
diff --git a/src/test/resources/permittedConsentBundle.json b/src/test/resources/permittedConsentBundle.json
new file mode 100644
index 0000000..20ca3de
--- /dev/null
+++ b/src/test/resources/permittedConsentBundle.json
@@ -0,0 +1,692 @@
+{
+ "resourceType": "Bundle",
+ "type": "collection",
+ "entry": [
+ {
+ "fullUrl": "http://gics:8080/ttp-fhir/fhir/gics/Consent/XXXX",
+ "resource": {
+ "resourceType": "Consent",
+ "id": "XXXX",
+ "meta": {
+ "lastUpdated": "2025-10-31T14:21:04.630+01:00",
+ "profile": [
+ "http://fhir.de/ConsentManagement/StructureDefinition/Consent",
+ "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/VVVVV"
+ }
+ },
+ {
+ "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": "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/psn-pat-here",
+ "display": "Patienten-ID PID-HERE"
+ },
+ "dateTime": "2025-10-13T00:00:00+01:00",
+ "organization": [
+ {
+ "display": "MII"
+ }
+ ],
+ "sourceReference": {
+ "reference": "QuestionnaireResponse/XXXX"
+ },
+ "policy": [
+ {
+ "uri": "urn:oid:2.16.840.1.113883.3.1937.777.24.2.1791"
+ }
+ ],
+ "provision": {
+ "type": "deny",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2055-10-13T00:00:00+01:00"
+ },
+ "provision": [
+ {
+ "type": "permit",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2055-10-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy",
+ "code": "IDAT_erheben",
+ "display": "Erfassung neuer identifizierender Daten (IDAT)"
+ },
+ {
+ "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"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "permit",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2055-10-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/MII",
+ "code": "KKDAT_5J_retro_speichern_verarbeiten",
+ "display": "Retrospektive Krankenkassendaten (KKDAT) aus fünf Jahren vor Einwilligung speichern und codiert verarbeiten zu Zwecken med. Forschung in der verantwortlichen Stelle"
+ },
+ {
+ "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.12",
+ "display": "KKDAT 5J retrospektiv speichern verarbeiten"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "permit",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2055-10-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/MII",
+ "code": "BIOMAT_wissenschaftlich_nutzen_EU_DSGVO_konform",
+ "display": "Bereitstellung umcodierter Biomaterialien (BIOMAT) für wissenschaftliche Nutzung und Analysen zu Zwecken med. Forschung an ext. Forscher"
+ },
+ {
+ "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.22",
+ "display": "BIOMAT wissenschaftlich nutzen EU DSGVO NIVEAU"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "permit",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2055-10-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/MII",
+ "code": "KKDAT_5J_retro_wissenschaftlich_nutzen",
+ "display": "Bereitstellung umcodierter retrospektiver Krankenkassendaten (KKDAT) für wissenschaftliche Nutzung zu Zwecken med. Forschung an externe Forscher"
+ },
+ {
+ "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.13",
+ "display": "KKDAT 5J retrospektiv wissenschaftlich nutzen"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "permit",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2055-10-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy",
+ "code": "BIOMAT_Analysedaten_zusammenfuehren_Dritte",
+ "display": "Zusammenführen von auf Biomaterialien (BIOMAT) basierenden Analysedaten mit Analysedaten Dritter, sofern dort ebenfalls eine Einwilligung vorliegt"
+ },
+ {
+ "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.23",
+ "display": "BIOMAT Analysedaten zusammenfuehren Dritte"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "permit",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2055-10-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/MII",
+ "code": "Rekontaktierung_Zusatzbefund",
+ "display": "Rekontaktierung bezüglich Zusatzbefund im Rahmen der am Standort dafür entwickelten Prozesse und der im Nutzungsantrag angegebenen Bedingungen"
+ },
+ {
+ "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"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "permit",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2055-10-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/MII",
+ "code": "KKDAT_5J_pro_wissenschaftlich_nutzen",
+ "display": "Bereitstellung umcodierter prospektiver Krankenkassendaten (KKDAT) aus fünf Jahren ab Einwilligung zu Zwecken med. Forschung an ext. Forscher"
+ },
+ {
+ "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.17",
+ "display": "KKDAT 5J prospektiv wissenschaftlich nutzen"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "permit",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2055-10-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy",
+ "code": "BIOMAT_lagern_verarbeiten",
+ "display": "Lagerung und Verarbeitung von Biomaterialien innerhalb der verantwortlichen Stelle (BIOMAT)"
+ },
+ {
+ "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.20",
+ "display": "BIOMAT lagern verarbeiten"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "permit",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2055-10-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/MII",
+ "code": "KKDAT_5J_retro_uebertragen",
+ "display": "Krankenkassendaten (KKDAT) der letzten fünf Kalenderjahre vor Datum Unterschrift übertragen"
+ },
+ {
+ "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.11",
+ "display": "KKDAT 5J retrospektiv uebertragen"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "permit",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2055-10-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/MII",
+ "code": "KKDAT_KVNR_5J_retro_uebertragen",
+ "display": "Erlaubnis zur retrospektiven Übermittlung der KVNr., MII-Pseudonym und Zeitraum Datenübermittlung (von:5 Jahre vor Datum Unterschrift; bis: Datum Unterschrift) an zuständige Stelle"
+ },
+ {
+ "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.38",
+ "display": "KKDAT 5J retrospektiv uebertragen KVNR"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "permit",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2030-03-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy",
+ "code": "MDAT_erheben",
+ "display": "Erfassung medizinischer Daten (MDAT)"
+ },
+ {
+ "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"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "permit",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2055-10-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/MII",
+ "code": "KKDAT_5J_pro_speichern_verarbeiten",
+ "display": "Prospektive Krankenkassendaten (KKDAT) aus fünf Jahren ab Einwilligung speichern und codiert verarbeiten zu Zwecken der med. Forschung in der verantwortlichen Stelle"
+ },
+ {
+ "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.16",
+ "display": "KKDAT 5J prospektiv speichern verarbeiten"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "permit",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2055-10-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/MII",
+ "code": "MDAT_wissenschaftlich_nutzen_EU_DSGVO_konform",
+ "display": "Bereitstellung umcodierter medizinischer Daten (MDAT) für wissenschaftliche Nutzung zu Zwecken med. Forschung an externe Forscher"
+ },
+ {
+ "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"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "permit",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2055-10-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy",
+ "code": "Rekontaktierung_Verknuepfung_Datenbanken",
+ "display": "Rekontaktierung zur Verknüpfung von Patientendaten mit Daten anderer Datenbanken"
+ },
+ {
+ "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 Verknüpfung Datenbanken"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "permit",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2055-10-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy",
+ "code": "Rekontaktierung_weitere_Studien",
+ "display": "Rekontaktierung bezüglich Information zu neuen Forschungsvorhaben oder Studien"
+ },
+ {
+ "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"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "permit",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2055-10-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/MII",
+ "code": "IDAT_bereitstellen_EU_DSGVO_konform",
+ "display": "Herausgabe identifizierender Daten (IDAT) an unabhängige Treuhandstelle zur weiteren Verarbeitung"
+ },
+ {
+ "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"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "permit",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2055-10-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy",
+ "code": "IDAT_speichern_verarbeiten",
+ "display": "Speicherung und Verarbeitung identifizierender Daten (IDAT) zu Zwecken med. Forschung in der verantwortlichen Stelle"
+ },
+ {
+ "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"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "permit",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2055-10-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy",
+ "code": "MDAT_speichern_verarbeiten",
+ "display": "Speicherung und Verarbeitung von medizinischen codierten Daten zu Zwecken med. Forschung innerhalb der verantwortlichen Stelle (MDAT)"
+ },
+ {
+ "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"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "permit",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2030-03-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy",
+ "code": "BIOMAT_erheben",
+ "display": "Gewinnung von Biomaterialien (BIOMAT)"
+ },
+ {
+ "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.19",
+ "display": "BIOMAT erheben"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "permit",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2055-10-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/MII",
+ "code": "Rekontaktierung_Ergebnisse_erheblicher_Bedeutung",
+ "display": "Rekontaktierung des Betroffenen bei Ergebnissen von erheblicher Bedeutung"
+ },
+ {
+ "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"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "permit",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2030-03-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/MII",
+ "code": "KKDAT_KVNR_5J_pro_uebertragen",
+ "display": "Erlaubnis zur prospektiven Übermittlung der KVNr., MII-Pseudonym und Zeitraum Datenübermittlung (von: Datum Unterschrift; bis: max. 5 Kalenderjahre nach Unterschrift) an zuständige Stelle"
+ },
+ {
+ "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.39",
+ "display": "KKDAT 5J prospektiv uebertragen KVNR"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "permit",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2030-03-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy",
+ "code": "BIOMAT_Zusatzmengen_entnehmen",
+ "display": "Entnahme zusätzlicher Mengen von Biomaterialien (BIOMAT) in den in der Einwilligung beschriebenen Grenzen"
+ },
+ {
+ "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.25",
+ "display": "BIOMAT Zusatzmengen entnehmen"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "permit",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2055-10-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy",
+ "code": "Rekontaktierung_weitere_Erhebung",
+ "display": "Rekontaktierung bezüglich Erhebung zusätzlicher Daten"
+ },
+ {
+ "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"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "permit",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2055-10-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy",
+ "code": "IDAT_zusammenfuehren_Dritte",
+ "display": "Zusammenführung identifizierender Daten (IDAT) über die unabhängige Treuhandstelle mit Dritten Forschungspartnern, sofern dort eine Einwilligung vorliegt"
+ },
+ {
+ "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"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "permit",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2055-10-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy",
+ "code": "MDAT_zusammenfuehren_Dritte",
+ "display": "Zusammenführung medizinischer Daten (MDAT) mit Dritten Forschungspartnern, sofern dort eine Einwilligung vorliegt"
+ },
+ {
+ "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"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "permit",
+ "period": {
+ "start": "2025-10-13T00:00:00+01:00",
+ "end": "2030-03-13T00:00:00+01:00"
+ },
+ "code": [
+ {
+ "coding": [
+ {
+ "system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/MII",
+ "code": "KKDAT_5J_pro_uebertragen",
+ "display": "Prospektive Krankenkassendaten (KKDAT) für fünf Kalenderjahre nach Datum Unterschrift übertragen"
+ },
+ {
+ "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.15",
+ "display": "KKDAT 5J prospektiv uebertragen"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ ]
+}