diff options
33 files changed, 3135 insertions, 758 deletions
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index e0951e0..65b5e78 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -31,4 +31,5 @@ jobs: run: | ./gradlew bootBuildImage docker tag ghcr.io/${{ github.repository }} ghcr.io/${{ github.repository }}:${{ github.ref_name }} + docker push ghcr.io/${{ github.repository }} docker push ghcr.io/${{ github.repository }}:${{ github.ref_name }}
\ No newline at end of file @@ -115,6 +115,11 @@ Ab Version 2025.1 (Multi-Pseudonym Support) * `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. diff --git a/build.gradle.kts b/build.gradle.kts index a2d2eda..c543344 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,3 +1,5 @@ +import net.ltgt.gradle.errorprone.errorprone +import net.ltgt.gradle.nullaway.nullaway import org.gradle.api.tasks.testing.logging.TestLogEvent import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.tasks.KotlinCompile @@ -7,22 +9,26 @@ plugins { war id("org.springframework.boot") version "3.5.7" 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" + id("net.ltgt.errorprone") version "4.3.0" + id("net.ltgt.nullaway") version "2.3.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.1.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" ) @@ -88,8 +94,12 @@ dependencies { implementation("org.webjars:webjars-locator:${versions["webjars-locator"]}") implementation("org.webjars.npm:echarts:${versions["echarts"]}") implementation("org.webjars.npm:htmx.org:${versions["htmx.org"]}") + implementation("org.jspecify:jspecify:1.0.0") // 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") @@ -113,6 +123,20 @@ dependencies { integrationTestImplementation("org.springframework:spring-webflux") // Fix for CVE-2024-25710 integrationTestImplementation("org.apache.commons:commons-compress:1.27.1") + + errorprone("com.google.errorprone:error_prone_core:2.43.0") + errorprone("com.uber.nullaway:nullaway:0.12.11") +} + +tasks.withType<JavaCompile> { + options.errorprone.nullaway { + error() + annotatedPackages.add("dev.dnpm.etl") + } + options.errorprone.disableAllChecks = true + options.errorprone { + disableAllWarnings = true + } } tasks.withType<KotlinCompile> { @@ -127,6 +151,7 @@ tasks.withType<Test> { testLogging { events(TestLogEvent.FAILED, TestLogEvent.PASSED, TestLogEvent.SKIPPED) } + dependsOn(tasks.spotlessCheck) } tasks.register<Test>("integrationTest") { @@ -171,3 +196,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 10f2359..578810e 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, null, "test", "test2", null, @@ -64,54 +67,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": [ @@ -145,4 +153,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 cc0491d..a69ba53 100644 --- a/src/main/java/dev/dnpm/etl/processor/consent/GicsConsentService.java +++ b/src/main/java/dev/dnpm/etl/processor/consent/GicsConsentService.java @@ -4,11 +4,17 @@ 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.net.URISyntaxException; +import java.util.Date; +import kotlin.random.Random; +import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.lang3.StringUtils; import org.apache.hc.core5.net.URIBuilder; import org.hl7.fhir.r4.model.*; import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent; -import org.jetbrains.annotations.NotNull; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpEntity; @@ -20,10 +26,6 @@ import org.springframework.retry.support.RetryTemplate; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.Date; - /** * Service to request Consent from remote gICS installation * @@ -31,294 +33,372 @@ 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; - } - - private URI endpointUri(String endpoint) throws URISyntaxException { - if (null == this.gIcsConfigProperties.getUri()) { - throw new URISyntaxException("null", "URI must not be null"); - } - var gPasUrl1 = this.gIcsConfigProperties.getUri(); - if (this.gIcsConfigProperties.getUri().lastIndexOf("/") - == this.gIcsConfigProperties.getUri().length() - 1) { - gPasUrl1 = - this.gIcsConfigProperties - .getUri() - .substring(0, this.gIcsConfigProperties.getUri().length() - 1); - } - var urlBuilder = new URIBuilder(new URI(gPasUrl1)).appendPath(endpoint); + return result; + } - return urlBuilder.build(); + private URI endpointUri(String endpoint) throws URISyntaxException { + if (null == this.gIcsConfigProperties.getUri()) { + throw new URISyntaxException("null", "URI must not be null"); } + var gPasUrl1 = this.gIcsConfigProperties.getUri(); + if (this.gIcsConfigProperties.getUri().lastIndexOf("/") + == this.gIcsConfigProperties.getUri().length() - 1) { + gPasUrl1 = + this.gIcsConfigProperties + .getUri() + .substring(0, this.gIcsConfigProperties.getUri().length() - 1); + } + var urlBuilder = new URIBuilder(new URI(gPasUrl1)).appendPath(endpoint); - private HttpHeaders headersWithHttpBasicAuth() { - assert this.gIcsConfigProperties.getUri() != null; + return urlBuilder.build(); + } - var headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_XML); + private HttpHeaders headersWithHttpBasicAuth() { + assert this.gIcsConfigProperties.getUri() != null; - if ( - StringUtils.isBlank(this.gIcsConfigProperties.getUsername()) - || StringUtils.isBlank(this.gIcsConfigProperties.getPassword()) - ) { - return headers; - } + var headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_XML); - headers.setBasicAuth(this.gIcsConfigProperties.getUsername(), this.gIcsConfigProperties.getPassword()); - return headers; + if (StringUtils.isBlank(this.gIcsConfigProperties.getUsername()) + || StringUtils.isBlank(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; - } catch (URISyntaxException e) { + headers.setBasicAuth( + this.gIcsConfigProperties.getUsername(), this.gIcsConfigProperties.getPassword()); + return headers; + } + + @Nullable + 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; + } catch (URISyntaxException e) { var msg = String.format("Invalid URI for consents status request: '%s", e.getMessage()); log.error(msg); return null; } - } + } - @Override - public TtpConsentStatus getTtpBroadConsentStatus(String personIdentifierValue) { - var consentStatusResponse = callGicsApi( + @Override + @NonNull + public TtpConsentStatus getTtpBroadConsentStatus(@NonNull String personIdentifierValue) { + var consentStatusResponse = + callGicsApi( getFhirRequestParameters(personIdentifierValue), - GicsConsentService.IS_CONSENTED_ENDPOINT - ); - return evaluateConsentResponse(consentStatusResponse); + GicsConsentService.IS_CONSENTED_ENDPOINT); + return evaluateConsentResponse(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."); + } + 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); + } + } + + @NonNull + 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)); + + return requestParameter; + } + + private TtpConsentStatus evaluateConsentResponse(@Nullable String consentStatusResponse) { + if (consentStatusResponse == null) { + return TtpConsentStatus.FAILED_TO_ASK; } + try { + var response = fhirContext.newJsonParser().parseResource(consentStatusResponse); + + if (response instanceof Parameters responseParameters) { - 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."); + 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 + @NonNull + public Bundle getConsent( + @NonNull String patientId, @NonNull Date requestDate, @NonNull 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..f9230ec 100644 --- a/src/main/java/dev/dnpm/etl/processor/consent/IConsentService.java +++ b/src/main/java/dev/dnpm/etl/processor/consent/IConsentService.java @@ -2,26 +2,27 @@ package dev.dnpm.etl.processor.consent; import java.util.Date; import org.hl7.fhir.r4.model.Bundle; +import org.jspecify.annotations.NullMarked; +@NullMarked 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..cc8107f 100644 --- a/src/main/java/dev/dnpm/etl/processor/consent/MtbFileConsentService.java +++ b/src/main/java/dev/dnpm/etl/processor/consent/MtbFileConsentService.java @@ -2,30 +2,35 @@ package dev.dnpm.etl.processor.consent; import java.util.Date; import org.hl7.fhir.r4.model.Bundle; +import org.jspecify.annotations.NonNull; import org.slf4j.Logger; 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 + @NonNull + public TtpConsentStatus getTtpBroadConsentStatus(@NonNull 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 + @NonNull + public Bundle getConsent( + @NonNull String personIdentifierValue, + @NonNull Date requestDate, + @NonNull 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..e72a8f8 100644 --- a/src/main/java/dev/dnpm/etl/processor/pseudonym/Generator.java +++ b/src/main/java/dev/dnpm/etl/processor/pseudonym/Generator.java @@ -19,10 +19,12 @@ package dev.dnpm.etl.processor.pseudonym; -public interface Generator { +import org.jspecify.annotations.NullMarked; - String generate(String id); +@NullMarked +public interface Generator { - 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..478c5c9 100644 --- a/src/main/java/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGenerator.java +++ b/src/main/java/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGenerator.java @@ -32,194 +32,202 @@ import org.hl7.fhir.r4.model.Identifier; import org.hl7.fhir.r4.model.Parameters; import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent; import org.hl7.fhir.r4.model.StringType; -import org.jetbrains.annotations.NotNull; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.*; import org.springframework.retry.support.RetryTemplate; import org.springframework.web.client.HttpClientErrorException.BadRequest; import org.springframework.web.client.HttpClientErrorException.Unauthorized; -import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; public class GpasPseudonymGenerator implements Generator { - private final 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 @Nullable String gPasUrl; + private final HttpHeaders httpHeader; + private final RetryTemplate retryTemplate; + private final Logger log = LoggerFactory.getLogger(GpasPseudonymGenerator.class); + private final RestTemplate restTemplate; + private final @NonNull String genomDeTanDomain; + private final @NonNull 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 + @NonNull + public String generate(@NonNull String id) { + return generate(id, PsnDomainType.SINGLE_PSN_DOMAIN); + } + + @Override + @NonNull + public String generateGenomDeTan(@NonNull 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); - } + @NonNull + 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, "_"); + } + + @NonNull + 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); } + throw new PseudonymRequestFailed( + "API request due unexpected error unsuccessful gPas unsuccessful."); + } - @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()); + protected URI buildRequestUrl(String apiEndpoint) throws URISyntaxException { + if (null == gPasUrl) { + throw new URISyntaxException("null", "URI must not be null"); } - - /** - * 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 gPasUrl1 = gPasUrl; + if (gPasUrl.lastIndexOf("/") == gPasUrl.length() - 1) { + gPasUrl1 = gPasUrl.substring(0, gPasUrl.length() - 1); } - - @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."); - + 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); + } + + @NonNull + protected HttpHeaders getHttpHeaders( + @Nullable String gPasUserName, @Nullable String gPasPassword) { + var headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + if (StringUtils.isBlank(gPasUserName) || StringUtils.isBlank(gPasPassword)) { + return headers; } - 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 e5db63e..ee33114 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt @@ -48,6 +48,7 @@ data class PseudonymizeConfigProperties( @ConfigurationProperties(GPasConfigProperties.NAME) data class GPasConfigProperties( val uri: String?, + val soapEndpoint: String?, @get:DeprecatedConfigurationProperty(since = "0.12") val pidDomain: String?, val patientDomain: String = pidDomain ?: "etl-processor", 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/ConnectionCheckService.kt b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/ConnectionCheckService.kt index bf8b8bd..6e97865 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/ConnectionCheckService.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/ConnectionCheckService.kt @@ -80,7 +80,7 @@ sealed class ConnectionCheckResult { class KafkaConnectionCheckService( private val consumer: Consumer<String, String>, - @Qualifier("connectionCheckUpdateProducer") + @param:Qualifier("connectionCheckUpdateProducer") private val connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult> ) : OutputConnectionCheckService { @@ -120,7 +120,7 @@ class KafkaConnectionCheckService( class RestConnectionCheckService( private val restTemplate: RestTemplate, private val restTargetProperties: RestTargetProperties, - @Qualifier("connectionCheckUpdateProducer") + @param:Qualifier("connectionCheckUpdateProducer") private val connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult> ) : OutputConnectionCheckService { @@ -171,7 +171,7 @@ class RestConnectionCheckService( class GPasConnectionCheckService( private val restTemplate: RestTemplate, private val gPasConfigProperties: GPasConfigProperties, - @Qualifier("connectionCheckUpdateProducer") + @param:Qualifier("connectionCheckUpdateProducer") private val connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult> ) : ConnectionCheckService { @@ -229,7 +229,7 @@ class GPasConnectionCheckService( class GIcsConnectionCheckService( private val restTemplate: RestTemplate, private val gIcsConfigProperties: GIcsConfigProperties, - @Qualifier("connectionCheckUpdateProducer") + @param:Qualifier("connectionCheckUpdateProducer") private val connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult> ) : ConnectionCheckService { 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/AnonymizingGenerator.kt b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/AnonymizingGenerator.kt index 0537cbb..dcb438f 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/AnonymizingGenerator.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/AnonymizingGenerator.kt @@ -35,7 +35,7 @@ class AnonymizingGenerator : Generator { } @OptIn(ExperimentalStdlibApi::class) - override fun generateGenomDeTan(id: String?): String { + override fun generateGenomDeTan(id: String): String { val bytes = ByteArray(64 / 2) getSecureRandom().nextBytes(bytes) 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/kotlin/dev/dnpm/etl/processor/services/kafka/KafkaResponseProcessor.kt b/src/main/kotlin/dev/dnpm/etl/processor/services/kafka/KafkaResponseProcessor.kt index 12e824d..fd6a9b4 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/services/kafka/KafkaResponseProcessor.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/services/kafka/KafkaResponseProcessor.kt @@ -73,9 +73,9 @@ class KafkaResponseProcessor( } data class ResponseBody( - @JsonProperty("request_id") @JsonAlias("requestId") val requestId: String, - @JsonProperty("status_code") @JsonAlias("statusCode") val statusCode: Int, - @JsonProperty("status_body") @JsonAlias("statusBody") val statusBody: Map<String, Any> + @param:JsonProperty("request_id") @param:JsonAlias("requestId") val requestId: String, + @param:JsonProperty("status_code") @param:JsonAlias("statusCode") val statusCode: Int, + @param:JsonProperty("status_body") @param:JsonAlias("statusBody") val statusBody: Map<String, Any> ) }
\ No newline at end of file diff --git a/src/main/kotlin/dev/dnpm/etl/processor/web/ConfigController.kt b/src/main/kotlin/dev/dnpm/etl/processor/web/ConfigController.kt index ea89e98..44571d4 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/web/ConfigController.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/web/ConfigController.kt @@ -40,7 +40,7 @@ import reactor.core.publisher.Sinks @Controller @RequestMapping(path = ["configs"]) class ConfigController( - @Qualifier("connectionCheckUpdateProducer") + @param:Qualifier("connectionCheckUpdateProducer") private val connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>, private val transformationService: TransformationService, private val pseudonymGenerator: Generator, diff --git a/src/main/kotlin/dev/dnpm/etl/processor/web/StatisticsRestController.kt b/src/main/kotlin/dev/dnpm/etl/processor/web/StatisticsRestController.kt index c034cb4..8372274 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/web/StatisticsRestController.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/web/StatisticsRestController.kt @@ -39,7 +39,7 @@ import java.time.temporal.ChronoUnit @RestController @RequestMapping(path = ["/statistics"]) class StatisticsRestController( - @Qualifier("statisticsUpdateProducer") + @param:Qualifier("statisticsUpdateProducer") private val statisticsUpdateProducer: Sinks.Many<Any>, private val requestService: RequestService ) { 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 c5b269d..e450e4d 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,22 @@ package dev.dnpm.etl.processor.consent; +import static dev.dnpm.etl.processor.consent.GicsConsentService.IS_CONSENTED_ENDPOINT; +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.net.URI; +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.apache.hc.core5.net.URIBuilder; import org.hl7.fhir.r4.model.*; import org.hl7.fhir.r4.model.OperationOutcome.IssueSeverity; @@ -21,168 +34,207 @@ import org.springframework.test.context.TestPropertySource; import org.springframework.test.web.client.MockRestServiceServer; import org.springframework.web.client.RestTemplate; -import java.net.URI; -import java.time.Instant; -import java.util.Date; - -import static dev.dnpm.etl.processor.consent.GicsConsentService.IS_CONSENTED_ENDPOINT; -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; - static URI expectedGicsConsentedEndpoint() throws Exception { + static URI expectedGicsConsentedEndpoint() throws Exception { return new URIBuilder(URI.create(GICS_BASE_URI)).appendPath(IS_CONSENTED_ENDPOINT).build(); } @BeforeEach - void setUp( - @Autowired AppFhirConfig appFhirConfig, - @Autowired GIcsConfigProperties gIcsConfigProperties - ) { - this.appFhirConfig = appFhirConfig; - this.gIcsConfigProperties = gIcsConfigProperties; + 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() throws Exception { - final Parameters consentedResponse = new Parameters() + @Test + void shouldReturnTtpBroadConsentStatus() throws Exception { + final Parameters consentedResponse = + new Parameters() .addParameter( new ParametersParameterComponent() .setName("consented") - .setValue(new BooleanType().setValue(true)) - ); - - mockRestServiceServer - .expect( - requestTo( - expectedGicsConsentedEndpoint()) - ) - .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() throws Exception { - final Parameters revokedResponse = new Parameters() + .setValue(new BooleanType().setValue(true))); + + mockRestServiceServer + .expect(requestTo(expectedGicsConsentedEndpoint())) + .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() throws Exception { + final Parameters revokedResponse = + new Parameters() .addParameter( new ParametersParameterComponent() .setName("consented") - .setValue(new BooleanType().setValue(false)) - ); - - mockRestServiceServer - .expect( - requestTo( - expectedGicsConsentedEndpoint()) - ) - .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() throws Exception { - final OperationOutcome responseWithErrorOutcome = new OperationOutcome() + .setValue(new BooleanType().setValue(false))); + + mockRestServiceServer + .expect(requestTo(expectedGicsConsentedEndpoint())) + .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() throws Exception { + final OperationOutcome responseWithErrorOutcome = + new OperationOutcome() .addIssue( new OperationOutcomeIssueComponent() .setSeverity(IssueSeverity.ERROR) .setCode(IssueType.PROCESSING) - .setDiagnostics("Invalid policy parameter...") - ); - - mockRestServiceServer - .expect( - requestTo(expectedGicsConsentedEndpoint()) - ) - .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() throws Exception { - mockRestServiceServer - .expect( - requestTo(expectedGicsConsentedEndpoint()) - ) - .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(expectedGicsConsentedEndpoint())) + .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() throws Exception { + mockRestServiceServer + .expect(requestTo(expectedGicsConsentedEndpoint())) + .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 7205714..a6d855c 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,22 +107,22 @@ 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, + null, "patientDomain", "genomDeTanDomain", "username", "password", ) + this.sink = Sinks.many().multicast().onBackpressureBuffer() + this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate) this.service = GPasConnectionCheckService(restTemplate, gpasTargetProperties, sink) } @@ -96,7 +139,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() } } @@ -105,19 +191,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) } @@ -136,6 +222,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" + } + ] + } + ] + } + ] + } + } + } + ] +} |
