1
0
mirror of https://github.com/pcvolkmer/etl-processor.git synced 2025-07-17 12:52:54 +00:00

fix: reimplement broad consent and genomDE consent check and embedding them into mtb data

This commit is contained in:
Jakub Lidke
2025-07-09 13:26:29 +02:00
parent f855df5e5f
commit d343b79caa
12 changed files with 488 additions and 158 deletions

View File

@ -1,5 +1,8 @@
package dev.dnpm.etl.processor.consent; package dev.dnpm.etl.processor.consent;
import java.util.Date;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Consent.ConsentProvisionType;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -12,7 +15,29 @@ public class ConsentCheckFileBased implements ICheckConsent{
} }
@Override @Override
public TtpConsentStatus getTtpConsentStatus(String personIdentifierValue) { public TtpConsentStatus getTtpBroadConsentStatus(String personIdentifierValue) {
return TtpConsentStatus.UNKNOWN_CHECK_FILE; return TtpConsentStatus.UNKNOWN_CHECK_FILE;
} }
@Override
public Bundle getBroadConsent(String personIdentifierValue, Date requestDate) {
return ICheckConsent.super.getBroadConsent(personIdentifierValue, requestDate);
}
@Override
public Bundle getGenomDeConsent(String personIdentifierValue, Date requestDate) {
return ICheckConsent.super.getGenomDeConsent(personIdentifierValue, requestDate);
}
@Override
public Bundle currentConsentForPersonAndTemplate(String personIdentifierValue,
ConsentDomain targetConsentDomain, Date requestDate) {
return new Bundle();
}
@Override
public ConsentProvisionType getProvisionTypeByPolicyCode(Bundle consentBundle,
Date requestDate, ConsentDomain consentDomain) {
return ConsentProvisionType.NULL;
}
} }

View File

@ -1,6 +1,13 @@
package dev.dnpm.etl.processor.consent; package dev.dnpm.etl.processor.consent;
public enum ConsentDomain { public enum ConsentDomain {
/**
* MII Broad consent
*/
BroadConsent, BroadConsent,
/**
* GenomDe Modelvohaben §64e
*/
Modelvorhaben64e Modelvorhaben64e
} }

View File

@ -5,16 +5,24 @@ import ca.uhn.fhir.parser.DataFormatException;
import dev.dnpm.etl.processor.config.AppFhirConfig; import dev.dnpm.etl.processor.config.AppFhirConfig;
import dev.dnpm.etl.processor.config.GIcsConfigProperties; import dev.dnpm.etl.processor.config.GIcsConfigProperties;
import java.util.Date; import java.util.Date;
import java.util.Optional;
import org.apache.commons.lang3.NotImplementedException;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.BooleanType;
import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.Consent;
import org.hl7.fhir.r4.model.Consent.ConsentProvisionType;
import org.hl7.fhir.r4.model.Consent.ConsentState;
import org.hl7.fhir.r4.model.Consent.ProvisionComponent;
import org.hl7.fhir.r4.model.DateType; import org.hl7.fhir.r4.model.DateType;
import org.hl7.fhir.r4.model.Identifier; import org.hl7.fhir.r4.model.Identifier;
import org.hl7.fhir.r4.model.OperationOutcome; import org.hl7.fhir.r4.model.OperationOutcome;
import org.hl7.fhir.r4.model.Parameters; import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent; import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent;
import org.hl7.fhir.r4.model.Period;
import org.hl7.fhir.r4.model.ResourceType;
import org.hl7.fhir.r4.model.StringType; import org.hl7.fhir.r4.model.StringType;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -63,7 +71,7 @@ public class GicsConsentService implements ICheckConsent {
throw new IllegalArgumentException( throw new IllegalArgumentException(
"gICS base URL is empty - should call gICS with false configuration."); "gICS base URL is empty - should call gICS with false configuration.");
} }
url = UriComponentsBuilder.fromUriString(gIcsBaseUri).path(IS_CONSENTED_ENDPOINT) url = UriComponentsBuilder.fromUriString(gIcsBaseUri).path(endpoint)
.toUriString(); .toUriString();
} }
return url; return url;
@ -91,8 +99,8 @@ public class GicsConsentService implements ICheckConsent {
result.addParameter(new ParametersParameterComponent().setName("domain") result.addParameter(new ParametersParameterComponent().setName("domain")
.setValue(new StringType().setValue(configProperties.getBroadConsentDomainName()))); .setValue(new StringType().setValue(configProperties.getBroadConsentDomainName())));
result.addParameter(new ParametersParameterComponent().setName("policy").setValue( result.addParameter(new ParametersParameterComponent().setName("policy").setValue(
new Coding().setCode(configProperties.getPolicyCode()) new Coding().setCode(configProperties.getBroadConsentPolicyCode())
.setSystem(configProperties.getPolicySystem()))); .setSystem(configProperties.getBroadConsentPolicySystem())));
/* /*
* is mandatory parameter, but we ignore it via additional configuration parameter * is mandatory parameter, but we ignore it via additional configuration parameter
@ -152,7 +160,7 @@ public class GicsConsentService implements ICheckConsent {
} }
@Override @Override
public TtpConsentStatus getTtpConsentStatus(String personIdentifierValue) { public TtpConsentStatus getTtpBroadConsentStatus(String personIdentifierValue) {
var parameter = GicsConsentService.getIsConsentedRequestParam(gIcsConfigProperties, var parameter = GicsConsentService.getIsConsentedRequestParam(gIcsConfigProperties,
personIdentifierValue); personIdentifierValue);
@ -199,30 +207,14 @@ public class GicsConsentService implements ICheckConsent {
private String getConsentDomain(ConsentDomain targetConsentDomain) { private String getConsentDomain(ConsentDomain targetConsentDomain) {
String consentDomain; String consentDomain;
switch (targetConsentDomain) { switch (targetConsentDomain) {
case BroadConsent -> { case BroadConsent -> consentDomain = gIcsConfigProperties.getBroadConsentDomainName();
consentDomain = gIcsConfigProperties.getBroadConsentDomainName(); case Modelvorhaben64e -> consentDomain = gIcsConfigProperties.getGenomDeConsentDomainName();
} default -> throw new IllegalArgumentException(
case Modelvorhaben64e -> { "target ConsentDomain is missing but must be provided!");
consentDomain = gIcsConfigProperties.getGnomDeConsentDomainName();
}
default -> {
throw new IllegalArgumentException(
"target ConsentDomain is missing but must be provided!");
}
} }
return consentDomain; return consentDomain;
} }
public Bundle getBroadConsent(String personIdentifierValue, Date requestDate) {
return currentConsentForPersonAndTemplate(personIdentifierValue, ConsentDomain.BroadConsent,
requestDate);
}
public Bundle getGenomDeConsent(String personIdentifierValue, Date requestDate) {
return currentConsentForPersonAndTemplate(personIdentifierValue,
ConsentDomain.Modelvorhaben64e, requestDate);
}
protected static Parameters buildRequestParameterCurrentPolicyStatesForPerson( protected static Parameters buildRequestParameterCurrentPolicyStatesForPerson(
GIcsConfigProperties gIcsConfigProperties, String personIdentifierValue, Date requestDate, GIcsConfigProperties gIcsConfigProperties, String personIdentifierValue, Date requestDate,
String targetDomain) { String targetDomain) {
@ -264,9 +256,9 @@ public class GicsConsentService implements ICheckConsent {
return TtpConsentStatus.FAILED_TO_ASK; return TtpConsentStatus.FAILED_TO_ASK;
} }
if (isConsented.booleanValue()) { if (isConsented.booleanValue()) {
return TtpConsentStatus.CONSENTED; return TtpConsentStatus.BROAD_CONSENT_GIVEN;
} else { } else {
return TtpConsentStatus.CONSENT_MISSING_OR_REJECTED; return TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED;
} }
} else if (response instanceof OperationOutcome outcome) { } else if (response instanceof OperationOutcome outcome) {
log.error("failed to get consent status from ttp. probably configuration error. " log.error("failed to get consent status from ttp. probably configuration error. "
@ -278,4 +270,84 @@ public class GicsConsentService implements ICheckConsent {
} }
return TtpConsentStatus.FAILED_TO_ASK; return TtpConsentStatus.FAILED_TO_ASK;
} }
/**
* @param consentBundle consent resource
* @param requestDate date which must be within validation period of provision
* @return type of provision, will be {@link ConsentProvisionType#NULL} if none is found.
*/
public ConsentProvisionType getProvisionTypeByPolicyCode(Bundle consentBundle,
Date requestDate, ConsentDomain consentDomain) {
String code;
String system;
if (ConsentDomain.BroadConsent == consentDomain) {
code = gIcsConfigProperties.getBroadConsentPolicyCode();
system = gIcsConfigProperties.getBroadConsentPolicySystem();
} else if (ConsentDomain.Modelvorhaben64e == consentDomain) {
code = gIcsConfigProperties.getGenomeDePolicyCode();
system = gIcsConfigProperties.getGenomeDePolicySystem();
} else {
throw new NotImplementedException("unknown consent domain " + consentDomain.name());
}
Optional<ConsentProvisionType> provisionTypeByPolicyCode = getProvisionTypeByPolicyCode(
consentBundle, code,
system, requestDate);
return provisionTypeByPolicyCode.orElse(ConsentProvisionType.NULL);
}
/**
* @param consentBundle consent resource
* @param policyAndProvisionCode policyRule and provision code value
* @param policyAndProvisionSystem policyRule and provision system value
* @param requestDate date which must be within validation period of provision
* @return type of provision, will be {@link ConsentProvisionType#NULL} if none is found.
*/
public Optional<ConsentProvisionType> getProvisionTypeByPolicyCode(Bundle consentBundle,
String policyAndProvisionCode, String policyAndProvisionSystem, Date requestDate) {
return consentBundle.getEntry().stream().filter(entry -> {
if (entry.getResource().getResourceType() != ResourceType.Consent) {
// no consent in bundle
return false;
}
Consent consent = (Consent) entry.getResource();
// consent ist active and its policy rule must fits search criteria
return consent.getStatus() == ConsentState.ACTIVE && checkCoding(
policyAndProvisionCode, policyAndProvisionSystem,
consent.getPolicyRule().getCodingFirstRep()) && isIsRequestDateInRange(requestDate,
consent.getProvision().getPeriod());
}).map(consentWithTargetPolicy -> {
ProvisionComponent provision = ((Consent) consentWithTargetPolicy.getResource()).getProvision();
var provisionComponentByCode = provision.getProvision().stream().filter(prov ->
checkCoding(policyAndProvisionCode, policyAndProvisionSystem,
prov.getCodeFirstRep().getCodingFirstRep()) && isIsRequestDateInRange(
requestDate, prov.getPeriod())
).findFirst();
if (provisionComponentByCode.isPresent()) {
// actual provision we search for
return provisionComponentByCode.get().getType();
}
// no fitting nested provision found - fall back to wrapping provision with default value
return provision.getType();
}).findFirst().or(() -> Optional.of(ConsentProvisionType.NULL));
}
protected static boolean checkCoding(String researchAllowedPolicyOid,
String researchAllowedPolicySystem, Coding coding) {
return coding.getSystem().equals(researchAllowedPolicySystem) && coding.getCode()
.equals(researchAllowedPolicyOid);
}
protected static boolean isIsRequestDateInRange(Date requestdate, Period provPeriod) {
var isRequestDateAfterOrEqualStart = provPeriod.getStart().compareTo(requestdate);
var isRequestDateBeforeOrEqualEnd = provPeriod.getEnd().compareTo(requestdate);
return isRequestDateAfterOrEqualStart <= 0 && isRequestDateBeforeOrEqualEnd >= 0;
}
} }

View File

@ -1,8 +1,61 @@
package dev.dnpm.etl.processor.consent; package dev.dnpm.etl.processor.consent;
import java.util.Date;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Consent.ConsentProvisionType;
public interface ICheckConsent { public interface ICheckConsent {
TtpConsentStatus getTtpConsentStatus(String personIdentifierValue); /**
* 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>
*/
default Bundle getBroadConsent(String personIdentifierValue, Date requestDate) {
return currentConsentForPersonAndTemplate(personIdentifierValue, ConsentDomain.BroadConsent,
requestDate);
}
/**
* Get 'GenomDe Modelvorhaben §64e' 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>
*/
default Bundle getGenomDeConsent(String personIdentifierValue, Date requestDate) {
return currentConsentForPersonAndTemplate(personIdentifierValue, ConsentDomain.BroadConsent,
requestDate);
}
/**
* Get consent policies with respect to a request date
*
* @param personIdentifierValue patient identifier used for consent data
* @param targetConsentDomain domain which should be used to request consent
* @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 currentConsentForPersonAndTemplate(String personIdentifierValue,
ConsentDomain targetConsentDomain, Date requestDate);
/**
*
* @param consentBundle consent resource
* @param requestDate date which must be within validation period of provision
* @return type of provision, will be {@link ConsentProvisionType#NULL} if none is found.
*/
ConsentProvisionType getProvisionTypeByPolicyCode(Bundle consentBundle,
Date requestDate, ConsentDomain consentDomain);
} }

View File

@ -4,17 +4,35 @@ public enum TtpConsentStatus {
/** /**
* Valid consent found * Valid consent found
*/ */
CONSENTED, BROAD_CONSENT_GIVEN,
CONSENT_MISSING_OR_REJECTED,
/** /**
* Due technical problems consent status is unknown * Missing or rejected...actually unknown
*/ */
FAILED_TO_ASK, 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' * Consent status is validate via file property 'consent.status'
*/ */
UNKNOWN_CHECK_FILE UNKNOWN_CHECK_FILE,
/**
* Due technical problems consent status is unknown
*/
FAILED_TO_ASK
} }

View File

@ -87,17 +87,27 @@ data class GIcsConfigProperties(
/** /**
* Domain of Modelvorhaben 64e consent resources * Domain of Modelvorhaben 64e consent resources
**/ **/
val gnomDeConsentDomainName: String = "GenomDE_MV", val genomDeConsentDomainName: String = "GenomDE_MV",
/** /**
* Value to expect in case of positiv consent * Value to expect in case of positiv consent
*/ */
val policyCode: String = "2.16.840.1.113883.3.1937.777.24.5.3.6", val broadConsentPolicyCode: String = "2.16.840.1.113883.3.1937.777.24.5.3.6",
/** /**
* Consent Policy which should be used for consent check * Consent Policy which should be used for consent check
*/ */
val policySystem: String = "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3" val broadConsentPolicySystem: String = "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3",
/**
* Value to expect in case of positiv consent
*/
val genomeDePolicyCode: String = "sequencing",
/**
* Consent Policy which should be used for consent check
*/
val genomeDePolicySystem: String = "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV"
) { ) {
companion object { companion object {
const val NAME = "app.consent.gics" const val NAME = "app.consent.gics"

View File

@ -63,16 +63,16 @@ class MtbFileRestController(
} }
private fun checkConsentStatus(mtbFile: MtbFile): Pair<TtpConsentStatus, Boolean> { private fun checkConsentStatus(mtbFile: MtbFile): Pair<TtpConsentStatus, Boolean> {
var ttpConsentStatus = iCheckConsent.getTtpConsentStatus(mtbFile.patient.id) var ttpConsentStatus = iCheckConsent.getTtpBroadConsentStatus(mtbFile.patient.id)
val isConsentOK = val isConsentOK =
(ttpConsentStatus.equals(TtpConsentStatus.UNKNOWN_CHECK_FILE) && mtbFile.consent.status == Consent.Status.ACTIVE) || (ttpConsentStatus.equals(TtpConsentStatus.UNKNOWN_CHECK_FILE) && mtbFile.consent.status == Consent.Status.ACTIVE) ||
ttpConsentStatus.equals( ttpConsentStatus.equals(
TtpConsentStatus.CONSENTED TtpConsentStatus.BROAD_CONSENT_GIVEN
) )
if (ttpConsentStatus.equals(TtpConsentStatus.UNKNOWN_CHECK_FILE) && mtbFile.consent.status == Consent.Status.REJECTED) { if (ttpConsentStatus.equals(TtpConsentStatus.UNKNOWN_CHECK_FILE) && mtbFile.consent.status == Consent.Status.REJECTED) {
// in case ttp check is disabled - we propagate rejected status anyway // in case ttp check is disabled - we propagate rejected status anyway
ttpConsentStatus = TtpConsentStatus.CONSENT_MISSING_OR_REJECTED ttpConsentStatus = TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED
} }
return Pair(ttpConsentStatus, isConsentOK) return Pair(ttpConsentStatus, isConsentOK)
} }

View File

@ -23,7 +23,8 @@ import com.fasterxml.jackson.databind.ObjectMapper
import de.ukw.ccc.bwhc.dto.MtbFile import de.ukw.ccc.bwhc.dto.MtbFile
import dev.dnpm.etl.processor.* import dev.dnpm.etl.processor.*
import dev.dnpm.etl.processor.config.AppConfigProperties import dev.dnpm.etl.processor.config.AppConfigProperties
import dev.dnpm.etl.processor.consent.GicsConsentService import dev.dnpm.etl.processor.consent.ConsentDomain
import dev.dnpm.etl.processor.consent.ICheckConsent
import dev.dnpm.etl.processor.consent.TtpConsentStatus import dev.dnpm.etl.processor.consent.TtpConsentStatus
import dev.dnpm.etl.processor.monitoring.Report import dev.dnpm.etl.processor.monitoring.Report
import dev.dnpm.etl.processor.monitoring.Request import dev.dnpm.etl.processor.monitoring.Request
@ -44,9 +45,15 @@ import org.apache.commons.codec.digest.DigestUtils
import org.hl7.fhir.instance.model.api.IBaseResource import org.hl7.fhir.instance.model.api.IBaseResource
import org.hl7.fhir.r4.model.Bundle import org.hl7.fhir.r4.model.Bundle
import org.hl7.fhir.r4.model.Consent import org.hl7.fhir.r4.model.Consent
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.context.ApplicationEventPublisher import org.springframework.context.ApplicationEventPublisher
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import java.io.IOException
import java.lang.RuntimeException
import java.time.Clock
import java.time.Instant import java.time.Instant
import java.time.ZoneId
import java.util.* import java.util.*
@Service @Service
@ -58,9 +65,10 @@ class RequestProcessor(
private val objectMapper: ObjectMapper, private val objectMapper: ObjectMapper,
private val applicationEventPublisher: ApplicationEventPublisher, private val applicationEventPublisher: ApplicationEventPublisher,
private val appConfigProperties: AppConfigProperties, private val appConfigProperties: AppConfigProperties,
private val gicsConsentService: GicsConsentService? private val consentService: ICheckConsent?
) { ) {
private var logger: Logger = LoggerFactory.getLogger("RequestProcessor")
fun processMtbFile(mtbFile: MtbFile) { fun processMtbFile(mtbFile: MtbFile) {
processMtbFile(mtbFile, randomRequestId()) processMtbFile(mtbFile, randomRequestId())
} }
@ -77,21 +85,79 @@ class RequestProcessor(
processMtbFile(mtbFile, randomRequestId()) processMtbFile(mtbFile, randomRequestId())
} }
fun processMtbFile(mtbFile: Mtb, requestId: RequestId) { /**
val pid = PatientId(mtbFile.patient.id) * In case an instance of {@link ICheckConsent} is active, consent will be embedded and checked.
*
* Logik:
* * <c>true</c> IF consent check is disabled.
* * <c>true</c> IF broad consent (BC) has been given.
* * <c>true</c> BC has been asked AND declined but genomDe consent has been consented.
* * ELSE <c>false</c> is returned.
*
* @param mtbFile File v2 (will be enriched with consent data)
* @return true if consent is given
*
*/
fun consentGatedCheck(mtbFile: Mtb): Boolean {
if (consentService == null) {
// consent check seems to be disabled
return true
}
addConsentToMtb(mtbFile) initMetaDataAtMtbFile(mtbFile)
mtbFile pseudonymizeWith pseudonymizeService
mtbFile anonymizeContentWith pseudonymizeService val personIdentifierValue = extractPatientIdentifier(mtbFile)
val request = DnpmV2MtbFileRequest(requestId, transformationService.transform(mtbFile)) val requestDate = Date.from(Instant.now(Clock.system(ZoneId.of("ECT"))))
saveAndSend(request, pid)
// 1. Broad consent Entry exists?
// 1.1. -> yes and research consent is given -> send mtb file
// 1.2. -> no -> return status error - consent has not been asked
// 2. -> Broad consent found but rejected -> is GenomDe consent provision 'sequencing' given?
// 2.1 -> yes -> send mtb file
// 2.2 -> no -> warn/info no consent given
/*
* broad consent
*/
val broadConsent = consentService.getBroadConsent(personIdentifierValue, requestDate)
val broadConsentHasBeenAsked = !broadConsent.entry.isEmpty()
// fast exit - if patient has not been asked, we can skip and exit
if (!broadConsentHasBeenAsked) return false
val genomeDeConsent = consentService.getGenomDeConsent(
personIdentifierValue, requestDate
)
addGenomeDbProvisions(mtbFile, genomeDeConsent)
embedBroadConsentResources(mtbFile, broadConsent)
val broadConsentStatus = consentService.getProvisionTypeByPolicyCode(
broadConsent,
requestDate,
ConsentDomain.BroadConsent
)
val genomDeSequencingStatus = consentService.getProvisionTypeByPolicyCode(
genomeDeConsent, requestDate,
ConsentDomain.Modelvorhaben64e
)
if (Consent.ConsentProvisionType.PERMIT == broadConsentStatus) return true
if (Consent.ConsentProvisionType.DENY == broadConsentStatus && Consent.ConsentProvisionType.PERMIT == genomDeSequencingStatus) return true
if (Consent.ConsentProvisionType.NULL == broadConsentStatus) {
// bc not asked
return false
}
return false
} }
fun addConsentToMtb(mtbFile: Mtb) {
if (gicsConsentService == null) return private fun initMetaDataAtMtbFile(mtbFile: Mtb) {
// init metadata if necessary // init metadata if necessary
if (mtbFile.metadata == null) { if (mtbFile.metadata == null) {
val mvhMetadata = MvhMetadata.builder().build(); val mvhMetadata = MvhMetadata.builder().build()
mtbFile.metadata = mvhMetadata mtbFile.metadata = mvhMetadata
if (mtbFile.metadata.researchConsents == null) { if (mtbFile.metadata.researchConsents == null) {
mtbFile.metadata.researchConsents = mutableListOf() mtbFile.metadata.researchConsents = mutableListOf()
@ -101,21 +167,29 @@ class RequestProcessor(
mtbFile.metadata.modelProjectConsent.provisions = mutableListOf() mtbFile.metadata.modelProjectConsent.provisions = mutableListOf()
} }
} }
// fixme Date should be extracted from mtbFile
val consentGnomeDe =
gicsConsentService.getGenomDeConsent(mtbFile.patient.id, Date.from(Instant.now()))
addGenomeDbProvisions(mtbFile, consentGnomeDe)
// fixme Date should be extracted from mtbFile
val broadConsent =
gicsConsentService.getBroadConsent(mtbFile.patient.id, Date.from(Instant.now()))
embedBroadConsentResources(mtbFile, broadConsent)
} }
fun processMtbFile(mtbFile: Mtb, requestId: RequestId) {
val pid = PatientId(extractPatientIdentifier(mtbFile))
if (consentGatedCheck(mtbFile)) {
mtbFile pseudonymizeWith pseudonymizeService
mtbFile anonymizeContentWith pseudonymizeService
val request = DnpmV2MtbFileRequest(requestId, transformationService.transform(mtbFile))
saveAndSend(request, pid)
} else {
logger.warn("consent check failed file will not be processed further!")
applicationEventPublisher.publishEvent(
ResponseEvent(
requestId, Instant.now(), RequestStatus.NO_CONSENT
)
)
}
}
fun embedBroadConsentResources( fun embedBroadConsentResources(
mtbFile: Mtb, mtbFile: Mtb, broadConsent: Bundle
broadConsent: Bundle
) { ) {
broadConsent.entry.forEach { it -> broadConsent.entry.forEach { it ->
mtbFile.metadata.researchConsents.add(mapOf(it.resource.id to it as IBaseResource)) mtbFile.metadata.researchConsents.add(mapOf(it.resource.id to it as IBaseResource))
@ -123,36 +197,36 @@ class RequestProcessor(
} }
fun addGenomeDbProvisions( fun addGenomeDbProvisions(
mtbFile: Mtb, mtbFile: Mtb, consentGnomeDe: Bundle
consentGnomeDe: Bundle
) { ) {
consentGnomeDe.entry.forEach { it -> consentGnomeDe.entry.forEach { it ->
{ {
val consent = it.resource as Consent val consentFhirResource = it.resource as Consent
val provisionComponent = consent.provision.provision.firstOrNull()
// we expect only one provision in collection, therefore get first or none
val provisionComponent = consentFhirResource.provision.provision.firstOrNull()
val provisionCode = val provisionCode =
provisionComponent?.code?.firstOrNull()?.coding?.firstOrNull()?.code provisionComponent?.code?.firstOrNull()?.coding?.firstOrNull()?.code
var isValidCode = true
if (provisionCode != null) { if (provisionCode != null) {
var modelProjectConsentPurpose: ModelProjectConsentPurpose = try {
ModelProjectConsentPurpose.SEQUENCING val modelProjectConsentPurpose: ModelProjectConsentPurpose =
if (provisionCode == "Teilnahme") { ModelProjectConsentPurpose.valueOf(provisionCode)
modelProjectConsentPurpose = ModelProjectConsentPurpose.SEQUENCING mtbFile.metadata.modelProjectConsent.provisions.add(
} else if (provisionCode == "Fallidentifizierung") { Provision.builder().type(
modelProjectConsentPurpose = ModelProjectConsentPurpose.CASE_IDENTIFICATION ConsentProvision.forValue(provisionComponent.type.name)
} else if (provisionCode == "Rekontaktierung") { ).date(provisionComponent.period.start).purpose(
modelProjectConsentPurpose = ModelProjectConsentPurpose.REIDENTIFICATION modelProjectConsentPurpose
} else { ).build()
isValidCode = false )
} catch (ioe: IOException) {
logger.error(
"provision code '$provisionCode' is unknown and cannot be mapped.",
ioe.toString()
)
} }
if (isValidCode) mtbFile.metadata.modelProjectConsent.provisions.add(
Provision.builder().type(
ConsentProvision.forValue(provisionComponent.type.name)
).date(provisionComponent.period.start).purpose(
modelProjectConsentPurpose
).build()
)
} }
} }
} }
} }
@ -172,9 +246,7 @@ class RequestProcessor(
if (appConfigProperties.duplicationDetection && isDuplication(request)) { if (appConfigProperties.duplicationDetection && isDuplication(request)) {
applicationEventPublisher.publishEvent( applicationEventPublisher.publishEvent(
ResponseEvent( ResponseEvent(
request.requestId, request.requestId, Instant.now(), RequestStatus.DUPLICATION
Instant.now(),
RequestStatus.DUPLICATION
) )
) )
return return
@ -206,9 +278,7 @@ class RequestProcessor(
val isLastRequestDeletion = val isLastRequestDeletion =
requestService.isLastRequestWithKnownStatusDeletion(patientPseudonym) requestService.isLastRequestWithKnownStatusDeletion(patientPseudonym)
return null != lastMtbFileRequestForPatient return null != lastMtbFileRequestForPatient && !isLastRequestDeletion && lastMtbFileRequestForPatient.fingerprint == fingerprint(
&& !isLastRequestDeletion
&& lastMtbFileRequestForPatient.fingerprint == fingerprint(
pseudonymizedMtbFileRequest pseudonymizedMtbFileRequest
) )
} }
@ -222,9 +292,12 @@ class RequestProcessor(
val patientPseudonym = pseudonymizeService.patientPseudonym(patientId) val patientPseudonym = pseudonymizeService.patientPseudonym(patientId)
val requestStatus: RequestStatus = when (isConsented) { val requestStatus: RequestStatus = when (isConsented) {
TtpConsentStatus.CONSENT_MISSING_OR_REJECTED -> RequestStatus.NO_CONSENT TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED, TtpConsentStatus.BROAD_CONSENT_MISSING, TtpConsentStatus.BROAD_CONSENT_REJECTED -> RequestStatus.NO_CONSENT
TtpConsentStatus.FAILED_TO_ASK -> RequestStatus.ERROR TtpConsentStatus.FAILED_TO_ASK -> RequestStatus.ERROR
TtpConsentStatus.CONSENTED, TtpConsentStatus.UNKNOWN_CHECK_FILE -> RequestStatus.UNKNOWN TtpConsentStatus.BROAD_CONSENT_GIVEN, TtpConsentStatus.UNKNOWN_CHECK_FILE -> RequestStatus.UNKNOWN
TtpConsentStatus.GENOM_DE_CONSENT_SEQUENCING_PERMIT, TtpConsentStatus.GENOM_DE_CONSENT_MISSING, TtpConsentStatus.GENOM_DE_SEQUENCING_REJECTED -> {
throw RuntimeException("processDelete should never deal with '" + isConsented.name + "' consent status. This is a bug and need to be fixed!")
}
} }
requestService.save( requestService.save(
@ -242,10 +315,7 @@ class RequestProcessor(
applicationEventPublisher.publishEvent( applicationEventPublisher.publishEvent(
ResponseEvent( ResponseEvent(
requestId, requestId, Instant.now(), responseStatus.status, when (responseStatus.status) {
Instant.now(),
responseStatus.status,
when (responseStatus.status) {
RequestStatus.WARNING, RequestStatus.ERROR -> Optional.of(responseStatus.body) RequestStatus.WARNING, RequestStatus.ERROR -> Optional.of(responseStatus.body)
else -> Optional.empty() else -> Optional.empty()
} }
@ -276,10 +346,10 @@ class RequestProcessor(
private fun fingerprint(s: String): Fingerprint { private fun fingerprint(s: String): Fingerprint {
return Fingerprint( return Fingerprint(
Base32().encodeAsString(DigestUtils.sha256(s)) Base32().encodeAsString(DigestUtils.sha256(s)).replace("=", "").lowercase()
.replace("=", "")
.lowercase()
) )
} }
} }
private fun extractPatientIdentifier(mtbFile: Mtb): String = mtbFile.patient.id

View File

@ -4,13 +4,19 @@ 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.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
import ca.uhn.fhir.context.FhirContext;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import dev.dnpm.etl.processor.config.AppConfiguration; import dev.dnpm.etl.processor.config.AppConfiguration;
import dev.dnpm.etl.processor.config.AppFhirConfig; import dev.dnpm.etl.processor.config.AppFhirConfig;
import dev.dnpm.etl.processor.config.GIcsConfigProperties; import dev.dnpm.etl.processor.config.GIcsConfigProperties;
import java.io.IOException;
import java.io.InputStream;
import java.time.Instant; import java.time.Instant;
import java.time.OffsetDateTime;
import java.util.Date; import java.util.Date;
import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.BooleanType;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Consent.ConsentProvisionType;
import org.hl7.fhir.r4.model.Identifier; import org.hl7.fhir.r4.model.Identifier;
import org.hl7.fhir.r4.model.OperationOutcome; import org.hl7.fhir.r4.model.OperationOutcome;
import org.hl7.fhir.r4.model.OperationOutcome.IssueSeverity; import org.hl7.fhir.r4.model.OperationOutcome.IssueSeverity;
@ -21,8 +27,11 @@ import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent;
import org.hl7.fhir.r4.model.StringType; import org.hl7.fhir.r4.model.StringType;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.client.RestClientTest; import org.springframework.boot.test.autoconfigure.web.client.RestClientTest;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.TestPropertySource;
@ -58,7 +67,7 @@ public class GicsConsentServiceTest {
} }
@Test @Test
void getTtpConsentStatus() { void getTtpBroadConsentStatus() {
final Parameters responseConsented = new Parameters().addParameter( final Parameters responseConsented = new Parameters().addParameter(
new ParametersParameterComponent().setName("consented") new ParametersParameterComponent().setName("consented")
.setValue(new BooleanType().setValue(true))); .setValue(new BooleanType().setValue(true)));
@ -70,8 +79,8 @@ public class GicsConsentServiceTest {
.encodeResourceToString(responseConsented), .encodeResourceToString(responseConsented),
MediaType.APPLICATION_JSON)); MediaType.APPLICATION_JSON));
var consentStatus = gicsConsentService.getTtpConsentStatus("123456"); var consentStatus = gicsConsentService.getTtpBroadConsentStatus("123456");
assertThat(consentStatus).isEqualTo(TtpConsentStatus.CONSENTED); assertThat(consentStatus).isEqualTo(TtpConsentStatus.BROAD_CONSENT_GIVEN);
} }
@Test @Test
@ -87,8 +96,8 @@ public class GicsConsentServiceTest {
.encodeResourceToString(responseRevoced), .encodeResourceToString(responseRevoced),
MediaType.APPLICATION_JSON)); MediaType.APPLICATION_JSON));
var consentStatus = gicsConsentService.getTtpConsentStatus("123456"); var consentStatus = gicsConsentService.getTtpBroadConsentStatus("123456");
assertThat(consentStatus).isEqualTo(TtpConsentStatus.CONSENT_MISSING_OR_REJECTED); assertThat(consentStatus).isEqualTo(TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED);
} }
@ -105,7 +114,7 @@ public class GicsConsentServiceTest {
.encodeResourceToString(responseErrorOutcome), .encodeResourceToString(responseErrorOutcome),
MediaType.APPLICATION_JSON)); MediaType.APPLICATION_JSON));
var consentStatus = gicsConsentService.getTtpConsentStatus("123456"); var consentStatus = gicsConsentService.getTtpBroadConsentStatus("123456");
assertThat(consentStatus).isEqualTo(TtpConsentStatus.FAILED_TO_ASK); assertThat(consentStatus).isEqualTo(TtpConsentStatus.FAILED_TO_ASK);
} }
@ -114,11 +123,50 @@ public class GicsConsentServiceTest {
String pid = "12345678"; String pid = "12345678";
var result = GicsConsentService.buildRequestParameterCurrentPolicyStatesForPerson(gIcsConfigProperties, var result = GicsConsentService.buildRequestParameterCurrentPolicyStatesForPerson(gIcsConfigProperties,
pid, Date.from(Instant.now()),gIcsConfigProperties.getGnomDeConsentDomainName()); pid, Date.from(Instant.now()),gIcsConfigProperties.getGenomDeConsentDomainName());
assertThat(result.getParameter().size()).as("should contain 3 parameter resources").isEqualTo(3); assertThat(result.getParameter().size()).as("should contain 3 parameter resources").isEqualTo(3);
assertThat(((StringType)result.getParameter("domain").getValue()).getValue()).isEqualTo(gIcsConfigProperties.getGnomDeConsentDomainName()); assertThat(((StringType)result.getParameter("domain").getValue()).getValue()).isEqualTo(gIcsConfigProperties.getGenomDeConsentDomainName());
assertThat(((Identifier)result.getParameter("personIdentifier").getValue()).getValue()).isEqualTo(pid); assertThat(((Identifier)result.getParameter("personIdentifier").getValue()).getValue()).isEqualTo(pid);
} }
@ParameterizedTest
@CsvSource({
"2.16.840.1.113883.3.1937.777.24.5.3.8,urn:oid:2.16.840.1.113883.3.1937.777.24.5.3,2025-07-23T00:00:00+02:00,PERMIT,expect permit",
"2.16.840.1.113883.3.1937.777.24.5.3.8,urn:oid:2.16.840.1.113883.3.1937.777.24.5.3,2025-06-23T00:00:00+02:00,PERMIT,expect permit date is exactly on start",
"2.16.840.1.113883.3.1937.777.24.5.3.8,urn:oid:2.16.840.1.113883.3.1937.777.24.5.3,2055-06-23T00:00:00+02:00,PERMIT,expect permit date is exactly on end",
"2.16.840.1.113883.3.1937.777.24.5.3.8,urn:oid:2.16.840.1.113883.3.1937.777.24.5.3,2021-06-23T00:00:00+02:00,NULL,date is before start",
"2.16.840.1.113883.3.1937.777.24.5.3.8,urn:oid:2.16.840.1.113883.3.1937.777.24.5.3,2060-06-23T00:00:00+02:00,NULL,date is after end",
"2.16.840.1.113883.3.1937.777.24.5.3.8,XXXX,2025-07-23T00:00:00+02:00,NULL,system not found - therefore expect NULL",
"2.16.840.1.113883.3.1937.777.24.5.3.27,urn:oid:2.16.840.1.113883.3.1937.777.24.5.3,2025-07-23T00:00:00+02:00,DENY,provision is denied"})
void getProvisionTypeByPolicyCode(String code, String system, String timeStamp, String expected,
String desc) {
var testData = getDummyBroadConsent();
Date requestDate = Date.from(OffsetDateTime.parse(timeStamp).toInstant());
var result = gicsConsentService.getProvisionTypeByPolicyCode(testData, code, system, requestDate);
assertThat(result).isNotNull();
assertThat(result).isNotEmpty();
assertThat(result.get()).as(desc).isEqualTo(ConsentProvisionType.valueOf(expected));
}
private Bundle getDummyBroadConsent() {
InputStream bundle;
try {
bundle = new ClassPathResource(
"fake_broadConsent_gics_response_permit.json").getInputStream();
} catch (IOException e) {
throw new RuntimeException(e);
}
return FhirContext.forR4().newJsonParser().parseResource(Bundle.class, bundle);
}
} }

View File

@ -101,7 +101,7 @@ class MtbFileRestControllerTest {
verify(requestProcessor, times(1)).processDeletion( verify(requestProcessor, times(1)).processDeletion(
anyValueClass(), anyValueClass(),
org.mockito.kotlin.eq(TtpConsentStatus.CONSENT_MISSING_OR_REJECTED) org.mockito.kotlin.eq(TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED)
) )
} }
@ -149,7 +149,7 @@ class MtbFileRestControllerTest {
@ValueSource(strings = ["ACTIVE", "REJECTED"]) @ValueSource(strings = ["ACTIVE", "REJECTED"])
fun shouldProcessPostRequest(status: String) { fun shouldProcessPostRequest(status: String) {
whenever(gicsConsentService.getTtpConsentStatus(any())).thenReturn(TtpConsentStatus.CONSENTED) whenever(gicsConsentService.getTtpBroadConsentStatus(any())).thenReturn(TtpConsentStatus.BROAD_CONSENT_GIVEN)
mockMvc.post("/mtbfile") { mockMvc.post("/mtbfile") {
content = content =
@ -169,7 +169,7 @@ class MtbFileRestControllerTest {
@ValueSource(strings = ["ACTIVE", "REJECTED"]) @ValueSource(strings = ["ACTIVE", "REJECTED"])
fun shouldProcessPostRequestWithRejectedConsent(status: String) { fun shouldProcessPostRequestWithRejectedConsent(status: String) {
whenever(gicsConsentService.getTtpConsentStatus(any())).thenReturn(TtpConsentStatus.CONSENT_MISSING_OR_REJECTED) whenever(gicsConsentService.getTtpBroadConsentStatus(any())).thenReturn(TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED)
mockMvc.post("/mtbfile") { mockMvc.post("/mtbfile") {
content = content =
@ -184,7 +184,7 @@ class MtbFileRestControllerTest {
// consent status from ttp should override file consent value // consent status from ttp should override file consent value
verify(requestProcessor, times(1)).processDeletion( verify(requestProcessor, times(1)).processDeletion(
anyValueClass(), anyValueClass(),
org.mockito.kotlin.eq(TtpConsentStatus.CONSENT_MISSING_OR_REJECTED) org.mockito.kotlin.eq(TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED)
) )
} }
@ -201,7 +201,7 @@ class MtbFileRestControllerTest {
anyValueClass(), anyValueClass(),
org.mockito.kotlin.eq(TtpConsentStatus.UNKNOWN_CHECK_FILE) org.mockito.kotlin.eq(TtpConsentStatus.UNKNOWN_CHECK_FILE)
) )
verify(gicsConsentService, times(0)).getTtpConsentStatus(any()) verify(gicsConsentService, times(0)).getTtpBroadConsentStatus(any())
} }
} }
@ -253,7 +253,7 @@ class MtbFileRestControllerTest {
verify(requestProcessor, times(1)).processDeletion( verify(requestProcessor, times(1)).processDeletion(
anyValueClass(), org.mockito.kotlin.eq( anyValueClass(), org.mockito.kotlin.eq(
TtpConsentStatus.CONSENT_MISSING_OR_REJECTED TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED
) )
) )
} }
@ -283,11 +283,12 @@ class MtbFileRestControllerTest {
@BeforeEach @BeforeEach
fun setup( fun setup(
@Mock requestProcessor: RequestProcessor @Mock requestProcessor: RequestProcessor,
@Mock gicsConsentService: GicsConsentService
) { ) {
this.requestProcessor = requestProcessor this.requestProcessor = requestProcessor
val controller = MtbFileRestController(requestProcessor, val controller = MtbFileRestController(requestProcessor,
ConsentCheckFileBased() gicsConsentService
) )
this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build() this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build()
} }

View File

@ -24,12 +24,15 @@ import de.ukw.ccc.bwhc.dto.Diagnosis
import de.ukw.ccc.bwhc.dto.Icd10 import de.ukw.ccc.bwhc.dto.Icd10
import de.ukw.ccc.bwhc.dto.MtbFile import de.ukw.ccc.bwhc.dto.MtbFile
import dev.dnpm.etl.processor.config.JacksonConfig import dev.dnpm.etl.processor.config.JacksonConfig
import dev.pcvolkmer.mv64e.mtb.ConsentProvision
import dev.pcvolkmer.mv64e.mtb.ModelProjectConsent import dev.pcvolkmer.mv64e.mtb.ModelProjectConsent
import dev.pcvolkmer.mv64e.mtb.ModelProjectConsentPurpose
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import dev.pcvolkmer.mv64e.mtb.Mtb import dev.pcvolkmer.mv64e.mtb.Mtb
import dev.pcvolkmer.mv64e.mtb.MvhMetadata import dev.pcvolkmer.mv64e.mtb.MvhMetadata
import dev.pcvolkmer.mv64e.mtb.Provision
import org.hl7.fhir.instance.model.api.IBaseResource import org.hl7.fhir.instance.model.api.IBaseResource
import org.hl7.fhir.r4.model.CodeableConcept import org.hl7.fhir.r4.model.CodeableConcept
import org.hl7.fhir.r4.model.Coding import org.hl7.fhir.r4.model.Coding
@ -124,50 +127,28 @@ class TransformationServiceTest {
@Test @Test
fun shouldTransformConsent() { fun shouldTransformConsent() {
val mvhMetadata = MvhMetadata.builder().transferTan("transfertan12345").build(); val mvhMetadata = MvhMetadata.builder().transferTan("transfertan12345").build()
assertThat(mvhMetadata).isNotNull assertThat(mvhMetadata).isNotNull
mvhMetadata.modelProjectConsent = mvhMetadata.modelProjectConsent =
ModelProjectConsent.builder().date(Date.from(Instant.now())).version("1").build() ModelProjectConsent.builder().date(Date.from(Instant.parse("2025-06-23T00:00:00.00Z")))
val consent1 = org.hl7.fhir.r4.model.Consent() .version("1").provisions(
consent1.id = "consent 1 id" listOf(
consent1.patient.reference = "Patient/1234-pat1" Provision.builder().type(ConsentProvision.PERMIT)
.purpose(ModelProjectConsentPurpose.SEQUENCING)
consent1.provision.setType(org.hl7.fhir.r4.model.Consent.ConsentProvisionType.fromCode("deny")) .date(Date.from(Instant.parse("2025-06-23T00:00:00.00Z"))).build(),
consent1.provision.period.start = Date.from(Instant.parse("2025-06-23T00:00:00.00Z")) Provision.builder().type(ConsentProvision.PERMIT)
consent1.provision.period.end = Date.from(Instant.parse("3000-01-01T00:00:00.00Z")) .purpose(ModelProjectConsentPurpose.REIDENTIFICATION)
.date(Date.from(Instant.parse("2025-06-23T00:00:00.00Z"))).build(),
Provision.builder().type(ConsentProvision.DENY)
val addProvision1 = consent1.provision.addProvision() .purpose(ModelProjectConsentPurpose.CASE_IDENTIFICATION)
addProvision1.setType(org.hl7.fhir.r4.model.Consent.ConsentProvisionType.fromCode("permit")) .date(Date.from(Instant.parse("2025-06-23T00:00:00.00Z"))).build()
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"
) )
) ).build()
) val consent = getDummyConsent()
val addProvision2 = consent1.provision.addProvision() mvhMetadata.researchConsents = mutableListOf()
addProvision2.setType(org.hl7.fhir.r4.model.Consent.ConsentProvisionType.fromCode("deny")) mvhMetadata.researchConsents.add(mapOf(consent.id to consent as IBaseResource))
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"
)
)
)
mvhMetadata.researchConsents = mutableListOf()
mvhMetadata.researchConsents.add(mapOf(consent1.id to consent1 as IBaseResource))
val mtbFile = Mtb.builder().metadata(mvhMetadata).build() val mtbFile = Mtb.builder().metadata(mvhMetadata).build()
@ -175,4 +156,49 @@ class TransformationServiceTest {
assertThat(transformed.metadata.modelProjectConsent.date).isNotNull 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"
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"
)
)
)
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
} }

View File

@ -606,7 +606,7 @@
}, },
"provision": [ "provision": [
{ {
"type": "permit", "type": "deny",
"period": { "period": {
"start": "2025-06-23T00:00:00+02:00", "start": "2025-06-23T00:00:00+02:00",
"end": "2055-06-23T00:00:00+02:00" "end": "2055-06-23T00:00:00+02:00"