From 90d1378e12c862f5bc7715ac87ae14cedf71529a Mon Sep 17 00:00:00 2001 From: Jakub Lidke Date: Thu, 10 Jul 2025 15:15:14 +0200 Subject: [PATCH] fix: pseudonymize patient reference at embedded consent resources --- .../etl/processor/pseudonym/extensions.kt | 35 +++++- .../processor/services/RequestProcessor.kt | 26 +---- .../etl/processor/pseudonym/ExtensionsTest.kt | 41 +++++++ .../services/TransformationServiceTest.kt | 107 +++++++++--------- 4 files changed, 127 insertions(+), 82 deletions(-) diff --git a/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/extensions.kt b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/extensions.kt index 5400178..b2b4dc7 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/extensions.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/extensions.kt @@ -21,7 +21,9 @@ package dev.dnpm.etl.processor.pseudonym import de.ukw.ccc.bwhc.dto.MtbFile import dev.dnpm.etl.processor.PatientId +import dev.pcvolkmer.mv64e.mtb.ModelProjectConsent import dev.pcvolkmer.mv64e.mtb.Mtb +import dev.pcvolkmer.mv64e.mtb.MvhMetadata import org.apache.commons.codec.digest.DigestUtils import org.hl7.fhir.r4.model.Consent @@ -291,12 +293,13 @@ infix fun Mtb.pseudonymizeWith(pseudonymizeService: PseudonymizeService) { it.patient.id = patientPseudonym } - // FIXME: MUST CREATE TESTCASE - NEEDS TESTING!! - this.metadata?.researchConsents?.forEach { it -> { - val consent = it as? Consent - consent?.patient?.reference = "Patient/$patientPseudonym" - consent?.patient?.display = null - } + this.metadata?.researchConsents?.forEach { it -> + val entry = it ?: return@forEach + val key = entry.keys.first() + val consent = entry[key] as? Consent ?: return@forEach + val patRef= "Patient/$patientPseudonym" + consent.patient?.setReference(patRef) + consent.patient?.display = null } } @@ -326,3 +329,23 @@ infix fun Mtb.anonymizeContentWith(pseudonymizeService: PseudonymizeService) { // TODO all other properties } + +fun Mtb.ensureMetaDataIsInitialized() { + // init metadata if necessary + if (this.metadata == null) { + val mvhMetadata = MvhMetadata.builder().build() + this.metadata = mvhMetadata + } + if (this.metadata.researchConsents == null) { + this.metadata.researchConsents = mutableListOf() + } + if (this.metadata.modelProjectConsent == null) { + this.metadata.modelProjectConsent = ModelProjectConsent() + this.metadata.modelProjectConsent.provisions = mutableListOf() + } else + if (this.metadata.modelProjectConsent.provisions != null) { + // make sure list can be changed + this.metadata.modelProjectConsent.provisions = + this.metadata.modelProjectConsent.provisions.toMutableList() + } +} diff --git a/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt b/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt index e2d5e1c..77daf85 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt @@ -33,10 +33,9 @@ import dev.dnpm.etl.processor.monitoring.RequestType import dev.dnpm.etl.processor.output.* import dev.dnpm.etl.processor.pseudonym.PseudonymizeService import dev.dnpm.etl.processor.pseudonym.anonymizeContentWith +import dev.dnpm.etl.processor.pseudonym.ensureMetaDataIsInitialized import dev.dnpm.etl.processor.pseudonym.pseudonymizeWith -import dev.pcvolkmer.mv64e.mtb.ModelProjectConsent import dev.pcvolkmer.mv64e.mtb.Mtb -import dev.pcvolkmer.mv64e.mtb.MvhMetadata import org.apache.commons.codec.binary.Base32 import org.apache.commons.codec.digest.DigestUtils import org.hl7.fhir.r4.model.Consent @@ -97,7 +96,7 @@ class RequestProcessor( return true } - initMetaDataAtMtbFile(mtbFile) + mtbFile.ensureMetaDataIsInitialized() val personIdentifierValue = extractPatientIdentifier(mtbFile) val requestDate = Date.from(Instant.now(Clock.systemUTC())) @@ -146,27 +145,6 @@ class RequestProcessor( return false } - - private fun initMetaDataAtMtbFile(mtbFile: Mtb) { - // init metadata if necessary - if (mtbFile.metadata == null) { - val mvhMetadata = MvhMetadata.builder().build() - mtbFile.metadata = mvhMetadata - } - if (mtbFile.metadata.researchConsents == null) { - mtbFile.metadata.researchConsents = mutableListOf() - } - if (mtbFile.metadata.modelProjectConsent == null) { - mtbFile.metadata.modelProjectConsent = ModelProjectConsent() - mtbFile.metadata.modelProjectConsent.provisions = mutableListOf() - } else - if (mtbFile.metadata.modelProjectConsent.provisions != null) { - // make sure list can be changed - mtbFile.metadata.modelProjectConsent.provisions = - mtbFile.metadata.modelProjectConsent.provisions.toMutableList() - } - } - fun processMtbFile(mtbFile: Mtb, requestId: RequestId) { val pid = PatientId(extractPatientIdentifier(mtbFile)) diff --git a/src/test/kotlin/dev/dnpm/etl/processor/pseudonym/ExtensionsTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/pseudonym/ExtensionsTest.kt index 072b562..b04ada4 100644 --- a/src/test/kotlin/dev/dnpm/etl/processor/pseudonym/ExtensionsTest.kt +++ b/src/test/kotlin/dev/dnpm/etl/processor/pseudonym/ExtensionsTest.kt @@ -22,9 +22,15 @@ package dev.dnpm.etl.processor.pseudonym import com.fasterxml.jackson.databind.ObjectMapper import de.ukw.ccc.bwhc.dto.* import de.ukw.ccc.bwhc.dto.Patient +import dev.dnpm.etl.processor.config.GIcsConfigProperties import dev.dnpm.etl.processor.config.JacksonConfig +import dev.dnpm.etl.processor.consent.BaseConsentService +import dev.dnpm.etl.processor.consent.ConsentDomain +import dev.dnpm.etl.processor.consent.TtpConsentStatus +import dev.dnpm.etl.processor.services.TransformationServiceTest import dev.pcvolkmer.mv64e.mtb.* import org.assertj.core.api.Assertions.assertThat +import org.hl7.fhir.r4.model.Bundle import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows @@ -231,6 +237,8 @@ class ExtensionsTest { }.whenever(pseudonymizeService).patientPseudonym(anyValueClass()) val mtbFile = fakeMtbFile() + mtbFile.ensureMetaDataIsInitialized() + addConsentData(CLEAN_PATIENT_ID,mtbFile) mtbFile.pseudonymizeWith(pseudonymizeService) @@ -238,6 +246,39 @@ class ExtensionsTest { assertThat(mtbFile.serialized()).doesNotContain(CLEAN_PATIENT_ID) } + private fun addConsentData(cleanPatientId: String, mtbFile: Mtb) { + val gIcsConfigProperties = GIcsConfigProperties("","","", true) + + val baseConsentService = object: BaseConsentService(gIcsConfigProperties){ + override fun getTtpBroadConsentStatus(personIdentifierValue: String?): TtpConsentStatus? { + throw NotImplementedError("dummy") + } + + override fun currentConsentForPersonAndTemplate( + personIdentifierValue: String?, + targetConsentDomain: ConsentDomain?, + requestDate: Date? + ): Bundle? { + throw NotImplementedError("dummy") + } + + override fun getProvisionTypeByPolicyCode( + consentBundle: Bundle?, + requestDate: Date?, + consentDomain: ConsentDomain? + ): org.hl7.fhir.r4.model.Consent.ConsentProvisionType? { + throw NotImplementedError("dummy") + } + } + + val bundle = Bundle() + val dummyConsent = TransformationServiceTest.getDummyConsent() + dummyConsent.patient.reference = "Patient/$cleanPatientId" + bundle.addEntry().resource= dummyConsent + + baseConsentService.embedBroadConsentResources(mtbFile,bundle) + } + @Test fun shouldNotThrowExceptionOnNullValues(@Mock pseudonymizeService: PseudonymizeService) { doAnswer { diff --git a/src/test/kotlin/dev/dnpm/etl/processor/services/TransformationServiceTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/services/TransformationServiceTest.kt index a0b9a8c..10be51e 100644 --- a/src/test/kotlin/dev/dnpm/etl/processor/services/TransformationServiceTest.kt +++ b/src/test/kotlin/dev/dnpm/etl/processor/services/TransformationServiceTest.kt @@ -133,18 +133,18 @@ class TransformationServiceTest { mvhMetadata.modelProjectConsent = ModelProjectConsent.builder().date(Date.from(Instant.parse("2025-06-23T00:00:00.00Z"))) .version("1").provisions( - listOf( - Provision.builder().type(ConsentProvision.PERMIT) - .purpose(ModelProjectConsentPurpose.SEQUENCING) - .date(Date.from(Instant.parse("2025-06-23T00:00:00.00Z"))).build(), - Provision.builder().type(ConsentProvision.PERMIT) - .purpose(ModelProjectConsentPurpose.REIDENTIFICATION) - .date(Date.from(Instant.parse("2025-06-23T00:00:00.00Z"))).build(), - Provision.builder().type(ConsentProvision.DENY) - .purpose(ModelProjectConsentPurpose.CASE_IDENTIFICATION) - .date(Date.from(Instant.parse("2025-06-23T00:00:00.00Z"))).build() - ) - ).build() + listOf( + Provision.builder().type(ConsentProvision.PERMIT) + .purpose(ModelProjectConsentPurpose.SEQUENCING) + .date(Date.from(Instant.parse("2025-06-23T00:00:00.00Z"))).build(), + Provision.builder().type(ConsentProvision.PERMIT) + .purpose(ModelProjectConsentPurpose.REIDENTIFICATION) + .date(Date.from(Instant.parse("2025-06-23T00:00:00.00Z"))).build(), + Provision.builder().type(ConsentProvision.DENY) + .purpose(ModelProjectConsentPurpose.CASE_IDENTIFICATION) + .date(Date.from(Instant.parse("2025-06-23T00:00:00.00Z"))).build() + ) + ).build() val consent = getDummyConsent() mvhMetadata.researchConsents = mutableListOf() @@ -156,49 +156,52 @@ class TransformationServiceTest { assertThat(transformed.metadata.modelProjectConsent.date).isNotNull } -} -fun getDummyConsent(): org.hl7.fhir.r4.model.Consent { - val modelVorhabenConsent = org.hl7.fhir.r4.model.Consent() - modelVorhabenConsent.id = "consent 1 id" - modelVorhabenConsent.patient.reference = "Patient/1234-pat1" + companion object { + fun getDummyConsent(): org.hl7.fhir.r4.model.Consent { + val modelVorhabenConsent = org.hl7.fhir.r4.model.Consent() + modelVorhabenConsent.id = "consent 1 id" + modelVorhabenConsent.patient.reference = "Patient/1234-pat1" - modelVorhabenConsent.provision.setType( - org.hl7.fhir.r4.model.Consent.ConsentProvisionType.fromCode( - "deny" - ) - ) - modelVorhabenConsent.provision.period.start = - Date.from(Instant.parse("2025-06-23T00:00:00.00Z")) - modelVorhabenConsent.provision.period.end = Date.from(Instant.parse("3000-01-01T00:00:00.00Z")) - - - val addProvision1 = modelVorhabenConsent.provision.addProvision() - addProvision1.setType(org.hl7.fhir.r4.model.Consent.ConsentProvisionType.fromCode("permit")) - addProvision1.period.start = Date.from(Instant.parse("2025-06-23T00:00:00.00Z")) - addProvision1.period.end = Date.from(Instant.parse("3000-01-01T00:00:00.00Z")) - addProvision1.code.addLast( - CodeableConcept( - Coding( - "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV", - "Teilnahme", - "Teilnahme am Modellvorhaben und Einwilligung zur Genomsequenzierung" + modelVorhabenConsent.provision.setType( + org.hl7.fhir.r4.model.Consent.ConsentProvisionType.fromCode( + "deny" + ) ) - ) - ) + modelVorhabenConsent.provision.period.start = + Date.from(Instant.parse("2025-06-23T00:00:00.00Z")) + modelVorhabenConsent.provision.period.end = + Date.from(Instant.parse("3000-01-01T00:00:00.00Z")) - val addProvision2 = modelVorhabenConsent.provision.addProvision() - addProvision2.setType(org.hl7.fhir.r4.model.Consent.ConsentProvisionType.fromCode("deny")) - addProvision2.period.start = Date.from(Instant.parse("2025-06-23T00:00:00.00Z")) - addProvision2.period.end = Date.from(Instant.parse("3000-01-01T00:00:00.00Z")) - addProvision2.code.addLast( - CodeableConcept( - Coding( - "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV", - "Rekontaktierung", - "Re-Identifizierung meiner Daten über die Vertrauensstelle beim Robert Koch-Institut und in die erneute Kontaktaufnahme durch meine behandelnde Ärztin oder meinen behandelnden Arzt" + + val addProvision1 = modelVorhabenConsent.provision.addProvision() + addProvision1.setType(org.hl7.fhir.r4.model.Consent.ConsentProvisionType.fromCode("permit")) + addProvision1.period.start = Date.from(Instant.parse("2025-06-23T00:00:00.00Z")) + addProvision1.period.end = Date.from(Instant.parse("3000-01-01T00:00:00.00Z")) + addProvision1.code.addLast( + CodeableConcept( + Coding( + "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV", + "Teilnahme", + "Teilnahme am Modellvorhaben und Einwilligung zur Genomsequenzierung" + ) + ) ) - ) - ) - return modelVorhabenConsent + + val addProvision2 = modelVorhabenConsent.provision.addProvision() + addProvision2.setType(org.hl7.fhir.r4.model.Consent.ConsentProvisionType.fromCode("deny")) + addProvision2.period.start = Date.from(Instant.parse("2025-06-23T00:00:00.00Z")) + addProvision2.period.end = Date.from(Instant.parse("3000-01-01T00:00:00.00Z")) + addProvision2.code.addLast( + CodeableConcept( + Coding( + "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV", + "Rekontaktierung", + "Re-Identifizierung meiner Daten über die Vertrauensstelle beim Robert Koch-Institut und in die erneute Kontaktaufnahme durch meine behandelnde Ärztin oder meinen behandelnden Arzt" + ) + ) + ) + return modelVorhabenConsent + } + } } \ No newline at end of file