1
0
mirror of https://github.com/pcvolkmer/etl-processor.git synced 2025-07-01 14:12:55 +00:00

chore: some refactoring merge ttp consent status missing and rejected into one CONSENT_MISSING_OR_REJECTED

This commit is contained in:
Jakub Lidke
2025-04-28 15:10:20 +02:00
parent 8517ba749c
commit 41d0a38c1d
15 changed files with 117 additions and 70 deletions

View File

@ -22,10 +22,9 @@ package dev.dnpm.etl.processor.input
import com.fasterxml.jackson.databind.ObjectMapper
import de.ukw.ccc.bwhc.dto.*
import dev.dnpm.etl.processor.anyValueClass
import dev.dnpm.etl.processor.config.AppFhirConfig
import dev.dnpm.etl.processor.config.AppSecurityConfiguration
import dev.dnpm.etl.processor.consent.ConsentCheckedIgnored
import dev.dnpm.etl.processor.consent.ConsentStatus
import dev.dnpm.etl.processor.consent.TtpConsentStatus
import dev.dnpm.etl.processor.consent.ICheckConsent
import dev.dnpm.etl.processor.security.TokenRepository
import dev.dnpm.etl.processor.security.UserRoleRepository
@ -34,7 +33,6 @@ 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.Mockito
import org.mockito.junit.jupiter.MockitoExtension
import org.mockito.kotlin.*
import org.springframework.beans.factory.annotation.Autowired
@ -145,7 +143,7 @@ class MtbFileRestControllerTest {
status { isAccepted() }
}
verify(requestProcessor, times(1)).processDeletion(anyValueClass(), eq(ConsentStatus.IGNORED))
verify(requestProcessor, times(1)).processDeletion(anyValueClass(), eq(TtpConsentStatus.IGNORED))
}
@Test

View File

@ -3,7 +3,7 @@ package dev.dnpm.etl.processor.consent;
public class ConsentCheckedIgnored implements ICheckConsent{
@Override
public ConsentStatus isConsented(String personIdentifierValue) {
return ConsentStatus.IGNORED;
public TtpConsentStatus isConsented(String personIdentifierValue) {
return TtpConsentStatus.IGNORED;
}
}

View File

@ -1,8 +0,0 @@
package dev.dnpm.etl.processor.consent;
public enum ConsentStatus {
CONSENTED,
CONSENT_MISSING,
FAILED_TO_ASK,
IGNORED, CONSENT_REJECTED
}

View File

@ -4,6 +4,7 @@ import ca.uhn.fhir.context.FhirContext;
import dev.dnpm.etl.processor.config.AppFhirConfig;
import dev.dnpm.etl.processor.config.GIcsConfigProperties;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.r4.model.BooleanType;
import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.Identifier;
import org.hl7.fhir.r4.model.Parameters;
@ -88,6 +89,18 @@ public class GicsConsentService implements ICheckConsent {
.setSystem(configProperties.getPolicySystem())));
result.addParameter(new ParametersParameterComponent().setName("version")
.setValue(new StringType().setValue(configProperties.getParameterVersion())));
/* add config parameter with:
* ignoreVersionNumber -> true
* unknownStateIsConsideredAsDecline -> true
*/
var config = new ParametersParameterComponent().setName("config").addPart(
new ParametersParameterComponent().setName("ignoreVersionNumber")
.setValue(new BooleanType().setValue(true))).addPart(
new ParametersParameterComponent().setName("unknownStateIsConsideredAsDecline")
.setValue(new BooleanType().setValue(true)));
result.addParameter(config);
return result;
}
@ -127,7 +140,7 @@ public class GicsConsentService implements ICheckConsent {
}
@Override
public ConsentStatus isConsented(String personIdentifierValue) {
public TtpConsentStatus isConsented(String personIdentifierValue) {
var parameter = GicsConsentService.getIsConsentedParam(gIcsConfigProperties,
personIdentifierValue);
@ -136,9 +149,9 @@ public class GicsConsentService implements ICheckConsent {
}
private ConsentStatus evaluateConsentResponse(String consentStatusResponse) {
private TtpConsentStatus evaluateConsentResponse(String consentStatusResponse) {
if (consentStatusResponse == null) {
return ConsentStatus.FAILED_TO_ASK;
return TtpConsentStatus.FAILED_TO_ASK;
}
var responseParameters = fhirContext.newJsonParser()
.parseResource(Parameters.class, consentStatusResponse);
@ -146,12 +159,12 @@ public class GicsConsentService implements ICheckConsent {
var responseValue = responseParameters.getParameter("consented").getValue();
var isConsented = responseValue.castToBoolean(responseValue);
if (!isConsented.hasValue()) {
return ConsentStatus.FAILED_TO_ASK;
return TtpConsentStatus.FAILED_TO_ASK;
}
if (isConsented.booleanValue()) {
return ConsentStatus.CONSENTED;
return TtpConsentStatus.CONSENTED;
} else {
return ConsentStatus.CONSENT_MISSING;
return TtpConsentStatus.CONSENT_MISSING_OR_REJECTED;
}
}
}

View File

@ -3,6 +3,6 @@ package dev.dnpm.etl.processor.consent;
public interface ICheckConsent {
ConsentStatus isConsented(String personIdentifierValue);
TtpConsentStatus isConsented(String personIdentifierValue);
}

View File

@ -0,0 +1,18 @@
package dev.dnpm.etl.processor.consent;
public enum TtpConsentStatus {
/**
* Valid consent found
*/
CONSENTED,
CONSENT_MISSING_OR_REJECTED,
/**
* Due technical problems consent status is unknown
*/
FAILED_TO_ASK,
/**
* We assume received files are consented
*/
IGNORED
}

View File

@ -84,8 +84,8 @@ class AppConfiguration {
@ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "GPAS")
@Bean
fun gpasPseudonymGenerator(configProperties: GPasConfigProperties, retryTemplate: RetryTemplate, restTemplate: RestTemplate): Generator {
return GpasPseudonymGenerator(configProperties, retryTemplate, restTemplate)
fun gpasPseudonymGenerator(configProperties: GPasConfigProperties, retryTemplate: RetryTemplate, restTemplate: RestTemplate, appFhirConfig: AppFhirConfig): Generator {
return GpasPseudonymGenerator(configProperties, retryTemplate, restTemplate, appFhirConfig)
}
@ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "BUILDIN", matchIfMissing = true)

View File

@ -25,7 +25,7 @@ import de.ukw.ccc.bwhc.dto.MtbFile
import dev.dnpm.etl.processor.CustomMediaType
import dev.dnpm.etl.processor.PatientId
import dev.dnpm.etl.processor.RequestId
import dev.dnpm.etl.processor.consent.ConsentStatus
import dev.dnpm.etl.processor.consent.TtpConsentStatus
import dev.dnpm.etl.processor.services.RequestProcessor
import org.apache.kafka.clients.consumer.ConsumerRecord
import org.slf4j.LoggerFactory
@ -66,7 +66,7 @@ class KafkaInputListener(
} else {
RequestId("")
}
// fixme: add consent check
if (mtbFile.consent.status == Consent.Status.ACTIVE) {
logger.debug("Accepted MTB File for processing")
if (requestId.isBlank()) {
@ -77,12 +77,12 @@ class KafkaInputListener(
} else {
logger.debug("Accepted MTB File and process deletion")
if (requestId.isBlank()) {
requestProcessor.processDeletion(patientId, ConsentStatus.IGNORED)
requestProcessor.processDeletion(patientId, TtpConsentStatus.IGNORED)
} else {
requestProcessor.processDeletion(
patientId,
requestId,
ConsentStatus.IGNORED
TtpConsentStatus.IGNORED
)
}
}

View File

@ -24,7 +24,7 @@ import de.ukw.ccc.bwhc.dto.MtbFile
import dev.dnpm.etl.processor.CustomMediaType
import dev.dnpm.etl.processor.PatientId
import dev.dnpm.etl.processor.consent.ICheckConsent
import dev.dnpm.etl.processor.consent.ConsentStatus
import dev.dnpm.etl.processor.consent.TtpConsentStatus
import dev.dnpm.etl.processor.services.RequestProcessor
import dev.pcvolkmer.mv64e.mtb.Mtb
import org.slf4j.LoggerFactory
@ -47,20 +47,26 @@ class MtbFileRestController(
@PostMapping( consumes = [ MediaType.APPLICATION_JSON_VALUE ] )
fun mtbFile(@RequestBody mtbFile: MtbFile): ResponseEntity<Unit> {
var consentStatus = constService.isConsented(mtbFile.patient.id)
var ttpConsentStatus = constService.isConsented(mtbFile.patient.id)
if (mtbFile.consent.status == Consent.Status.ACTIVE && (consentStatus.equals(ConsentStatus.CONSENTED) || consentStatus.equals(
ConsentStatus.IGNORED
// received status REJECTED overrides TTP value. Also in case of disabled consent service,
// we need to override IGNORED status
if (mtbFile.consent.status == Consent.Status.REJECTED) ttpConsentStatus =
TtpConsentStatus.CONSENT_MISSING_OR_REJECTED
val isConsentOK = mtbFile.consent.status == Consent.Status.ACTIVE && (ttpConsentStatus.equals(
TtpConsentStatus.CONSENTED
) || ttpConsentStatus.equals(
TtpConsentStatus.IGNORED
))
) {
if (isConsentOK) {
logger.debug("Accepted MTB File (bwHC V1) for processing")
requestProcessor.processMtbFile(mtbFile)
} else {
logger.debug("Accepted MTB File (bwHC V1) and process deletion")
if (mtbFile.consent.status == Consent.Status.REJECTED) consentStatus =
ConsentStatus.CONSENT_REJECTED
val patientId = PatientId(mtbFile.patient.id)
requestProcessor.processDeletion(patientId, consentStatus)
requestProcessor.processDeletion(patientId, ttpConsentStatus)
}
return ResponseEntity.accepted().build()
}
@ -75,7 +81,7 @@ class MtbFileRestController(
@DeleteMapping(path = ["{patientId}"])
fun deleteData(@PathVariable patientId: String): ResponseEntity<Unit> {
logger.debug("Accepted patient ID to process deletion")
requestProcessor.processDeletion(PatientId(patientId), ConsentStatus.IGNORED)
requestProcessor.processDeletion(PatientId(patientId), TtpConsentStatus.IGNORED)
return ResponseEntity.accepted().build()
}

View File

@ -25,5 +25,5 @@ enum class RequestStatus(val value: String) {
ERROR("error"),
UNKNOWN("unknown"),
DUPLICATION("duplication"),
CONSENTMISSING("no-consent")
NO_CONSENT("no-consent")
}

View File

@ -23,7 +23,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
import de.ukw.ccc.bwhc.dto.MtbFile
import dev.dnpm.etl.processor.*
import dev.dnpm.etl.processor.config.AppConfigProperties
import dev.dnpm.etl.processor.consent.ConsentStatus
import dev.dnpm.etl.processor.consent.TtpConsentStatus
import dev.dnpm.etl.processor.monitoring.Report
import dev.dnpm.etl.processor.monitoring.Request
import dev.dnpm.etl.processor.monitoring.RequestStatus
@ -129,19 +129,18 @@ class RequestProcessor(
&& lastMtbFileRequestForPatient.fingerprint == fingerprint(pseudonymizedMtbFileRequest)
}
fun processDeletion(patientId: PatientId, isConsented: ConsentStatus) {
fun processDeletion(patientId: PatientId, isConsented: TtpConsentStatus) {
processDeletion(patientId, randomRequestId(), isConsented)
}
fun processDeletion(patientId: PatientId, requestId: RequestId, isConsented: ConsentStatus) {
fun processDeletion(patientId: PatientId, requestId: RequestId, isConsented: TtpConsentStatus) {
try {
val patientPseudonym = pseudonymizeService.patientPseudonym(patientId)
val requestStatus: RequestStatus = when (isConsented) {
ConsentStatus.CONSENT_MISSING -> RequestStatus.CONSENTMISSING
ConsentStatus.FAILED_TO_ASK -> RequestStatus.ERROR
ConsentStatus.CONSENTED, ConsentStatus.IGNORED,
ConsentStatus.CONSENT_REJECTED -> RequestStatus.UNKNOWN
TtpConsentStatus.CONSENT_MISSING_OR_REJECTED -> RequestStatus.NO_CONSENT
TtpConsentStatus.FAILED_TO_ASK -> RequestStatus.ERROR
TtpConsentStatus.CONSENTED, TtpConsentStatus.IGNORED -> RequestStatus.UNKNOWN
}
requestService.save(

View File

@ -70,6 +70,12 @@ class ResponseProcessor(
)
}
RequestStatus.NO_CONSENT -> {
it.report = Report(
"Einwilligung Status fehlt, widerrufen oder ungeklärt."
)
}
else -> {
logger.error("Cannot process response: Unknown response!")
return@ifPresentOrElse

View File

@ -23,8 +23,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
import de.ukw.ccc.bwhc.dto.Consent
import de.ukw.ccc.bwhc.dto.MtbFile
import de.ukw.ccc.bwhc.dto.Patient
import dev.dnpm.etl.processor.anyValueClass
import dev.dnpm.etl.processor.consent.ConsentStatus
import dev.dnpm.etl.processor.consent.TtpConsentStatus
import dev.dnpm.etl.processor.CustomMediaType
import dev.dnpm.etl.processor.services.RequestProcessor
import org.apache.kafka.clients.consumer.ConsumerRecord
@ -36,10 +35,7 @@ import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.Mock
import org.mockito.junit.jupiter.MockitoExtension
import org.mockito.kotlin.any
import org.mockito.kotlin.eq
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.*
import java.util.*
@ExtendWith(MockitoExtension::class)
@ -98,7 +94,7 @@ class KafkaInputListenerTest {
verify(requestProcessor, times(1)).processDeletion(
anyValueClass(),
eq(ConsentStatus.IGNORED)
eq(TtpConsentStatus.IGNORED)
)
}
@ -152,7 +148,8 @@ class KafkaInputListenerTest {
Optional.empty()
)
)
verify(requestProcessor, times(1)).processDeletion(anyValueClass(), anyValueClass(), eq(ConsentStatus.IGNORED)
verify(requestProcessor, times(1)).processDeletion(anyValueClass(), anyValueClass(), eq(
TtpConsentStatus.IGNORED))
}
@Test
@ -183,7 +180,8 @@ class KafkaInputListenerTest {
Optional.empty()
)
)
verify(requestProcessor, times(0)).processDeletion(anyValueClass(), anyValueClass(), eq(ConsentStatus.IGNORED)
verify(requestProcessor, times(0)).processDeletion(anyValueClass(), anyValueClass(), eq(
TtpConsentStatus.IGNORED))
}
}

View File

@ -21,9 +21,8 @@ package dev.dnpm.etl.processor.input
import com.fasterxml.jackson.databind.ObjectMapper
import de.ukw.ccc.bwhc.dto.*
import dev.dnpm.etl.processor.anyValueClass
import dev.dnpm.etl.processor.consent.ConsentCheckedIgnored
import dev.dnpm.etl.processor.consent.ConsentStatus
import dev.dnpm.etl.processor.consent.TtpConsentStatus
import dev.dnpm.etl.processor.CustomMediaType
import dev.dnpm.etl.processor.services.RequestProcessor
import dev.pcvolkmer.mv64e.mtb.Mtb
@ -81,7 +80,8 @@ class MtbFileRestControllerTest {
@Test
fun shouldProcessPostRequestWithRejectedConsent() {
mockMvc.post("/mtbfile") {
content = objectMapper.writeValueAsString(bwhcMtbFileContent(Consent.Status.REJECTED))
content =
objectMapper.writeValueAsString(bwhcMtbFileContent(Consent.Status.REJECTED))
contentType = MediaType.APPLICATION_JSON
}.andExpect {
status {
@ -89,7 +89,10 @@ class MtbFileRestControllerTest {
}
}
verify(requestProcessor, times(1)).processDeletion(anyValueClass())
verify(requestProcessor, times(1)).processDeletion(
anyValueClass(),
org.mockito.kotlin.eq(TtpConsentStatus.CONSENT_MISSING_OR_REJECTED)
)
}
@Test
@ -100,7 +103,10 @@ class MtbFileRestControllerTest {
}
}
verify(requestProcessor, times(1)).processDeletion(anyValueClass())
verify(requestProcessor, times(1)).processDeletion(
anyValueClass(),
org.mockito.kotlin.eq(TtpConsentStatus.IGNORED)
)
}
}
@ -116,7 +122,7 @@ class MtbFileRestControllerTest {
@Mock requestProcessor: RequestProcessor
) {
this.requestProcessor = requestProcessor
val controller = MtbFileRestController(requestProcessor)
val controller = MtbFileRestController(requestProcessor, ConsentCheckedIgnored())
this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build()
}
@ -137,7 +143,8 @@ class MtbFileRestControllerTest {
@Test
fun shouldProcessPostRequestWithRejectedConsent() {
mockMvc.post("/mtb") {
content = objectMapper.writeValueAsString(bwhcMtbFileContent(Consent.Status.REJECTED))
content =
objectMapper.writeValueAsString(bwhcMtbFileContent(Consent.Status.REJECTED))
contentType = MediaType.APPLICATION_JSON
}.andExpect {
status {
@ -145,7 +152,11 @@ class MtbFileRestControllerTest {
}
}
verify(requestProcessor, times(1)).processDeletion(anyValueClass())
verify(requestProcessor, times(1)).processDeletion(
anyValueClass(), org.mockito.kotlin.eq(
TtpConsentStatus.CONSENT_MISSING_OR_REJECTED
)
)
}
@Test
@ -156,7 +167,11 @@ class MtbFileRestControllerTest {
}
}
verify(requestProcessor, times(1)).processDeletion(anyValueClass())
verify(requestProcessor, times(1)).processDeletion(
anyValueClass(), org.mockito.kotlin.eq(
TtpConsentStatus.IGNORED
)
)
}
}
@ -172,13 +187,15 @@ class MtbFileRestControllerTest {
@Mock requestProcessor: RequestProcessor
) {
this.requestProcessor = requestProcessor
val controller = MtbFileRestController(requestProcessor)
val controller = MtbFileRestController(requestProcessor, ConsentCheckedIgnored())
this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build()
}
@Test
fun shouldRespondPostRequest() {
val mtbFileContent = ClassPathResource("mv64e-mtb-fake-patient.json").inputStream.readAllBytes().toString(Charsets.UTF_8)
val mtbFileContent =
ClassPathResource("mv64e-mtb-fake-patient.json").inputStream.readAllBytes()
.toString(Charsets.UTF_8)
mockMvc.post("/mtb") {
content = mtbFileContent

View File

@ -25,7 +25,7 @@ import dev.dnpm.etl.processor.Fingerprint
import dev.dnpm.etl.processor.PatientId
import dev.dnpm.etl.processor.PatientPseudonym
import dev.dnpm.etl.processor.config.AppConfigProperties
import dev.dnpm.etl.processor.consent.ConsentStatus
import dev.dnpm.etl.processor.consent.TtpConsentStatus
import dev.dnpm.etl.processor.monitoring.Request
import dev.dnpm.etl.processor.monitoring.RequestStatus
import dev.dnpm.etl.processor.monitoring.RequestType
@ -344,7 +344,7 @@ class RequestProcessorTest {
MtbFileSender.Response(status = RequestStatus.UNKNOWN)
}.whenever(sender).send(any<DeleteRequest>())
this.requestProcessor.processDeletion(TEST_PATIENT_ID, isConsented = ConsentStatus.IGNORED)
this.requestProcessor.processDeletion(TEST_PATIENT_ID, isConsented = TtpConsentStatus.IGNORED)
val requestCaptor = argumentCaptor<Request>()
verify(requestService, times(1)).save(requestCaptor.capture())
@ -362,7 +362,7 @@ class RequestProcessorTest {
MtbFileSender.Response(status = RequestStatus.SUCCESS)
}.whenever(sender).send(any<DeleteRequest>())
this.requestProcessor.processDeletion(TEST_PATIENT_ID, isConsented = ConsentStatus.IGNORED)
this.requestProcessor.processDeletion(TEST_PATIENT_ID, isConsented = TtpConsentStatus.IGNORED)
val eventCaptor = argumentCaptor<ResponseEvent>()
verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture())
@ -380,7 +380,7 @@ class RequestProcessorTest {
MtbFileSender.Response(status = RequestStatus.ERROR)
}.whenever(sender).send(any<DeleteRequest>())
this.requestProcessor.processDeletion(TEST_PATIENT_ID, isConsented = ConsentStatus.IGNORED)
this.requestProcessor.processDeletion(TEST_PATIENT_ID, isConsented = TtpConsentStatus.IGNORED)
val eventCaptor = argumentCaptor<ResponseEvent>()
verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture())
@ -392,7 +392,7 @@ class RequestProcessorTest {
fun testShouldSendDeleteRequestWithPseudonymErrorAndSaveErrorRequestStatus() {
doThrow(RuntimeException()).whenever(pseudonymizeService).patientPseudonym(anyValueClass())
this.requestProcessor.processDeletion(TEST_PATIENT_ID, isConsented = ConsentStatus.IGNORED)
this.requestProcessor.processDeletion(TEST_PATIENT_ID, isConsented = TtpConsentStatus.IGNORED)
val requestCaptor = argumentCaptor<Request>()
verify(requestService, times(1)).save(requestCaptor.capture())