From bf6bfa904e127f51b79cfafb96e1280b50e9615a Mon Sep 17 00:00:00 2001 From: Paul-Christian Volkmer Date: Fri, 6 Mar 2026 13:08:40 +0100 Subject: feat: save TAN for MTB requests (#253) --- .../processor/monitoring/RequestRepositoryTest.kt | 1 + .../services/RequestServiceIntegrationTest.kt | 3 + .../dnpm/etl/processor/web/HomeControllerTest.kt | 1 + .../processor/web/StatisticsRestControllerTest.kt | 6 ++ .../dev/dnpm/etl/processor/monitoring/Request.kt | 5 ++ .../etl/processor/services/RequestProcessor.kt | 4 ++ src/main/kotlin/dev/dnpm/etl/processor/types.kt | 8 +++ .../db/migration/mariadb/V0_15_0_1__Tan.sql | 2 + .../db/migration/postgresql/V0_15_0_1__Tan.sql | 2 + .../etl/processor/services/RequestProcessorTest.kt | 81 +++++++++++++++++++++- .../etl/processor/services/RequestServiceTest.kt | 10 +++ .../processor/services/ResponseProcessorTest.kt | 1 + 12 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 src/main/resources/db/migration/mariadb/V0_15_0_1__Tan.sql create mode 100644 src/main/resources/db/migration/postgresql/V0_15_0_1__Tan.sql (limited to 'src') diff --git a/src/integrationTest/kotlin/dev/dnpm/etl/processor/monitoring/RequestRepositoryTest.kt b/src/integrationTest/kotlin/dev/dnpm/etl/processor/monitoring/RequestRepositoryTest.kt index 98fee3f..17b49eb 100644 --- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/monitoring/RequestRepositoryTest.kt +++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/monitoring/RequestRepositoryTest.kt @@ -63,6 +63,7 @@ class RequestRepositoryTest : AbstractTestcontainerTest() { RequestType.MTB_FILE, SubmissionType.TEST, RequestStatus.WARNING, + Tan.empty(), Instant.parse("2023-07-07T00:00:00Z"), ) diff --git a/src/integrationTest/kotlin/dev/dnpm/etl/processor/services/RequestServiceIntegrationTest.kt b/src/integrationTest/kotlin/dev/dnpm/etl/processor/services/RequestServiceIntegrationTest.kt index 0b30e94..63dd5dc 100644 --- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/services/RequestServiceIntegrationTest.kt +++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/services/RequestServiceIntegrationTest.kt @@ -78,6 +78,7 @@ class RequestServiceIntegrationTest : AbstractTestcontainerTest() { RequestType.MTB_FILE, SubmissionType.TEST, RequestStatus.SUCCESS, + Tan.empty(), Instant.parse("2023-07-07T02:00:00Z"), ), // Should be ignored - wrong patient ID --> @@ -89,6 +90,7 @@ class RequestServiceIntegrationTest : AbstractTestcontainerTest() { RequestType.MTB_FILE, SubmissionType.TEST, RequestStatus.WARNING, + Tan.empty(), Instant.parse("2023-08-08T00:00:00Z"), ), // <-- @@ -100,6 +102,7 @@ class RequestServiceIntegrationTest : AbstractTestcontainerTest() { RequestType.DELETE, SubmissionType.TEST, RequestStatus.SUCCESS, + Tan.empty(), Instant.parse("2023-08-08T02:00:00Z"), ), ) diff --git a/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/HomeControllerTest.kt b/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/HomeControllerTest.kt index 9e85d08..c2b6759 100644 --- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/HomeControllerTest.kt +++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/HomeControllerTest.kt @@ -159,6 +159,7 @@ class HomeControllerTest { RequestType.MTB_FILE, SubmissionType.TEST, RequestStatus.SUCCESS, + Tan.empty(), Instant.now(), Report("Test"), ) diff --git a/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/StatisticsRestControllerTest.kt b/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/StatisticsRestControllerTest.kt index 926f315..2568b1b 100644 --- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/StatisticsRestControllerTest.kt +++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/StatisticsRestControllerTest.kt @@ -22,6 +22,7 @@ package dev.dnpm.etl.processor.web import dev.dnpm.etl.processor.Fingerprint import dev.dnpm.etl.processor.PatientId import dev.dnpm.etl.processor.PatientPseudonym +import dev.dnpm.etl.processor.Tan import dev.dnpm.etl.processor.config.AppConfiguration import dev.dnpm.etl.processor.config.AppSecurityConfiguration import dev.dnpm.etl.processor.monitoring.CountedState @@ -188,6 +189,7 @@ class StatisticsRestControllerTest { RequestType.MTB_FILE, SubmissionType.TEST, RequestStatus.SUCCESS, + Tan.empty(), Instant .now() .atZone(zoneId) @@ -204,6 +206,7 @@ class StatisticsRestControllerTest { RequestType.MTB_FILE, SubmissionType.TEST, RequestStatus.WARNING, + Tan.empty(), Instant .now() .atZone(zoneId) @@ -220,6 +223,7 @@ class StatisticsRestControllerTest { RequestType.DELETE, SubmissionType.TEST, RequestStatus.ERROR, + Tan.empty(), Instant .now() .atZone(zoneId) @@ -236,6 +240,7 @@ class StatisticsRestControllerTest { RequestType.MTB_FILE, SubmissionType.TEST, RequestStatus.DUPLICATION, + Tan.empty(), Instant .now() .atZone(zoneId) @@ -252,6 +257,7 @@ class StatisticsRestControllerTest { RequestType.DELETE, SubmissionType.TEST, RequestStatus.UNKNOWN, + Tan.empty(), Instant .now() .atZone(zoneId) 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 b1cc1ed..9aede20 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/Request.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/Request.kt @@ -43,6 +43,7 @@ data class Request( val type: RequestType, @Column("submission_type") val submissionType: SubmissionType, var status: RequestStatus, + @Column("tan") val tan: Tan = Tan.empty(), var processedAt: Instant = Instant.now(), @Embedded.Nullable var report: Report? = null, @Column("submission_accepted") var submissionAccepted: Boolean = false, @@ -55,6 +56,7 @@ data class Request( type: RequestType, submissionType: SubmissionType, status: RequestStatus, + tan: Tan, ) : this( null, uuid, @@ -64,6 +66,7 @@ data class Request( type, submissionType, status, + tan, Instant.now(), ) @@ -75,6 +78,7 @@ data class Request( type: RequestType, submissionType: SubmissionType, status: RequestStatus, + tan: Tan, processedAt: Instant, ) : this( null, @@ -85,6 +89,7 @@ data class Request( type, submissionType, status, + tan, processedAt, ) 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 b14d6f4..fe1fd3b 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt @@ -107,6 +107,7 @@ class RequestProcessor( RequestType.MTB_FILE, submissionType, RequestStatus.BLOCKED_INITIAL, + Tan(request.content.metadata?.transferTan.orEmpty()) ) ) // Exit - no further processing @@ -135,6 +136,7 @@ class RequestProcessor( RequestType.MTB_FILE, submissionType, RequestStatus.UNKNOWN, + Tan(request.content.metadata?.transferTan.orEmpty()), ) ) @@ -228,6 +230,7 @@ class RequestProcessor( RequestType.DELETE, SubmissionType.UNKNOWN, requestStatus, + Tan.empty() ) ) @@ -256,6 +259,7 @@ class RequestProcessor( type = RequestType.DELETE, submissionType = SubmissionType.UNKNOWN, report = Report("Fehler bei der Pseudonymisierung"), + tan = Tan.empty(), ) ) } diff --git a/src/main/kotlin/dev/dnpm/etl/processor/types.kt b/src/main/kotlin/dev/dnpm/etl/processor/types.kt index 87f5fb2..9244297 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/types.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/types.kt @@ -48,6 +48,14 @@ fun emptyPatientId() = PatientId("") fun emptyPatientPseudonym() = PatientPseudonym("") +@JvmInline value class Tan(val value: String) { + fun isValid() = value.matches(Regex("^[a-fA-F0-9]{64}$")) + + companion object { + fun empty() = Tan("") + } +} + /** * Custom MediaTypes * diff --git a/src/main/resources/db/migration/mariadb/V0_15_0_1__Tan.sql b/src/main/resources/db/migration/mariadb/V0_15_0_1__Tan.sql new file mode 100644 index 0000000..2076163 --- /dev/null +++ b/src/main/resources/db/migration/mariadb/V0_15_0_1__Tan.sql @@ -0,0 +1,2 @@ +ALTER TABLE request ADD COLUMN tan varchar(64) not null default ''; +UPDATE request SET tan = ''; diff --git a/src/main/resources/db/migration/postgresql/V0_15_0_1__Tan.sql b/src/main/resources/db/migration/postgresql/V0_15_0_1__Tan.sql new file mode 100644 index 0000000..2076163 --- /dev/null +++ b/src/main/resources/db/migration/postgresql/V0_15_0_1__Tan.sql @@ -0,0 +1,2 @@ +ALTER TABLE request ADD COLUMN tan varchar(64) not null default ''; +UPDATE request SET tan = ''; diff --git a/src/test/kotlin/dev/dnpm/etl/processor/services/RequestProcessorTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/services/RequestProcessorTest.kt index edd9ffe..afa6872 100644 --- a/src/test/kotlin/dev/dnpm/etl/processor/services/RequestProcessorTest.kt +++ b/src/test/kotlin/dev/dnpm/etl/processor/services/RequestProcessorTest.kt @@ -23,7 +23,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import dev.dnpm.etl.processor.Fingerprint import dev.dnpm.etl.processor.PatientId import dev.dnpm.etl.processor.PatientPseudonym -import dev.dnpm.etl.processor.RequestId +import dev.dnpm.etl.processor.Tan import dev.dnpm.etl.processor.config.AppConfigProperties import dev.dnpm.etl.processor.consent.TtpConsentStatus import dev.dnpm.etl.processor.monitoring.Request @@ -109,6 +109,7 @@ class RequestProcessorTest { RequestType.MTB_FILE, SubmissionType.TEST, RequestStatus.SUCCESS, + Tan.empty(), Instant.parse("2023-08-08T02:00:00Z"), ) } @@ -165,6 +166,7 @@ class RequestProcessorTest { RequestType.MTB_FILE, SubmissionType.TEST, RequestStatus.SUCCESS, + Tan.empty(), Instant.parse("2023-08-08T02:00:00Z"), ) } @@ -221,6 +223,7 @@ class RequestProcessorTest { RequestType.MTB_FILE, SubmissionType.TEST, RequestStatus.SUCCESS, + Tan.empty(), Instant.parse("2023-08-08T02:00:00Z"), ) } @@ -281,6 +284,7 @@ class RequestProcessorTest { RequestType.MTB_FILE, SubmissionType.TEST, RequestStatus.SUCCESS, + Tan.empty(), Instant.parse("2023-08-08T02:00:00Z"), ) } @@ -299,6 +303,10 @@ class RequestProcessorTest { .whenever(pseudonymizeService) .patientPseudonym(anyValueClass()) + doAnswer { "f2ca1bb6c7e907d06dafe4687e579fce76b37e4e93b7605022da52e6ccc26fd2" } + .whenever(pseudonymizeService) + .genomDeTan(anyValueClass()) + doAnswer { it.arguments[0] }.whenever(transformationService).transform(any()) whenever(consentProcessor.consentGatedCheckAndTryEmbedding(any())).thenReturn(true) @@ -360,6 +368,7 @@ class RequestProcessorTest { RequestType.MTB_FILE, SubmissionType.INITIAL, RequestStatus.SUCCESS, + Tan.empty(), Instant.parse("2026-01-05T09:00:00Z"), submissionAccepted = true, ), @@ -372,6 +381,7 @@ class RequestProcessorTest { RequestType.MTB_FILE, SubmissionType.INITIAL, RequestStatus.BLOCKED_INITIAL, + Tan.empty(), Instant.parse("2026-01-05T10:00:00Z"), submissionAccepted = false, ), @@ -393,6 +403,10 @@ class RequestProcessorTest { .whenever(pseudonymizeService) .patientPseudonym(anyValueClass()) + doAnswer { "f2ca1bb6c7e907d06dafe4687e579fce76b37e4e93b7605022da52e6ccc26fd2" } + .whenever(pseudonymizeService) + .genomDeTan(anyValueClass()) + doAnswer { it.arguments[0] }.whenever(transformationService).transform(any()) whenever(consentProcessor.consentGatedCheckAndTryEmbedding(any())).thenReturn(true) @@ -434,6 +448,7 @@ class RequestProcessorTest { verify(sender, times(1)).send(requestCaptor.capture()) assertThat(requestCaptor.firstValue).isNotNull assertThat(requestCaptor.firstValue.content.metadata.type).isEqualTo(MvhSubmissionType.ADDITION) + assertThat(requestCaptor.firstValue.content.metadata.transferTan).isEqualTo("f2ca1bb6c7e907d06dafe4687e579fce76b37e4e93b7605022da52e6ccc26fd2") val eventCaptor = argumentCaptor() verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture()) @@ -597,6 +612,68 @@ class RequestProcessorTest { assertThat(eventCaptor.firstValue.status).isEqualTo(RequestStatus.SUCCESS) } + @Test + fun testShouldSaveRequestWithGenomDeTan() { + + doAnswer { false } + .whenever(requestService) + .isLastRequestWithKnownStatusDeletion(anyValueClass()) + + doAnswer { MtbFileSender.Response(status = RequestStatus.SUCCESS) } + .whenever(sender) + .send(any()) + + doAnswer { it.arguments[0] as String } + .whenever(pseudonymizeService) + .patientPseudonym(anyValueClass()) + + doAnswer { "f2ca1bb6c7e907d06dafe4687e579fce76b37e4e93b7605022da52e6ccc26fd2" } + .whenever(pseudonymizeService) + .genomDeTan(anyValueClass()) + + doAnswer { it.arguments[0] }.whenever(transformationService).transform(any()) + + whenever(consentProcessor.consentGatedCheckAndTryEmbedding(any())).thenReturn(true) + + requestProcessor = + RequestProcessor( + pseudonymizeService, + transformationService, + sender, + requestService, + ObjectMapper(), + applicationEventPublisher, + AppConfigProperties(postInitialSubmissionBlock = true), + consentProcessor, + ) + + val mtbFile = + Mtb.builder() + .patient(Patient.builder().id("123").build()) + .metadata(MvhMetadata()) + .episodesOfCare( + listOf( + MtbEpisodeOfCare.builder() + .id("1") + .patient(Reference.builder().id("123").build()) + .period( + PeriodDate.builder() + .start(Date.from(Instant.parse("2021-01-01T00:00:00.00Z"))) + .build() + ) + .build() + ) + ) + .build() + + this.requestProcessor.processMtbFile(mtbFile) + + val requestCaptor = argumentCaptor() + verify(requestService, times(1)).save(requestCaptor.capture()) + assertThat(requestCaptor.firstValue).isNotNull + assertThat(requestCaptor.firstValue.tan).isEqualTo(Tan("f2ca1bb6c7e907d06dafe4687e579fce76b37e4e93b7605022da52e6ccc26fd2")) + } + @Nested inner class WithInitialSubmissionBlock { @@ -656,6 +733,7 @@ class RequestProcessorTest { RequestType.MTB_FILE, SubmissionType.INITIAL, RequestStatus.ERROR, + Tan.empty(), Instant.parse("2026-01-05T09:00:00Z"), submissionAccepted = false, ), @@ -668,6 +746,7 @@ class RequestProcessorTest { RequestType.MTB_FILE, SubmissionType.INITIAL, RequestStatus.SUCCESS, + Tan.empty(), Instant.parse("2026-01-05T10:00:00Z"), submissionAccepted = false, ), diff --git a/src/test/kotlin/dev/dnpm/etl/processor/services/RequestServiceTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/services/RequestServiceTest.kt index fdb7578..d02a5fe 100644 --- a/src/test/kotlin/dev/dnpm/etl/processor/services/RequestServiceTest.kt +++ b/src/test/kotlin/dev/dnpm/etl/processor/services/RequestServiceTest.kt @@ -54,6 +54,7 @@ class RequestServiceTest { RequestType.MTB_FILE, SubmissionType.TEST, RequestStatus.SUCCESS, + Tan.empty(), Instant.parse("2023-08-08T02:00:00Z"), ) @@ -76,6 +77,7 @@ class RequestServiceTest { RequestType.MTB_FILE, SubmissionType.TEST, RequestStatus.WARNING, + Tan.empty(), Instant.parse("2023-07-07T00:00:00Z"), ), Request( @@ -87,6 +89,7 @@ class RequestServiceTest { RequestType.DELETE, SubmissionType.TEST, RequestStatus.WARNING, + Tan.empty(), Instant.parse("2023-07-07T02:00:00Z"), ), Request( @@ -98,6 +101,7 @@ class RequestServiceTest { RequestType.MTB_FILE, SubmissionType.TEST, RequestStatus.UNKNOWN, + Tan.empty(), Instant.parse("2023-08-11T00:00:00Z"), ), ) @@ -120,6 +124,7 @@ class RequestServiceTest { RequestType.MTB_FILE, SubmissionType.TEST, RequestStatus.WARNING, + Tan.empty(), Instant.parse("2023-07-07T00:00:00Z"), ), Request( @@ -131,6 +136,7 @@ class RequestServiceTest { RequestType.MTB_FILE, SubmissionType.TEST, RequestStatus.WARNING, + Tan.empty(), Instant.parse("2023-07-07T02:00:00Z"), ), Request( @@ -142,6 +148,7 @@ class RequestServiceTest { RequestType.MTB_FILE, SubmissionType.TEST, RequestStatus.UNKNOWN, + Tan.empty(), Instant.parse("2023-08-11T00:00:00Z"), ), ) @@ -164,6 +171,7 @@ class RequestServiceTest { RequestType.DELETE, SubmissionType.TEST, RequestStatus.SUCCESS, + Tan.empty(), Instant.parse("2023-07-07T02:00:00Z"), ), Request( @@ -175,6 +183,7 @@ class RequestServiceTest { RequestType.MTB_FILE, SubmissionType.TEST, RequestStatus.WARNING, + Tan.empty(), Instant.parse("2023-08-08T00:00:00Z"), ), ) @@ -212,6 +221,7 @@ class RequestServiceTest { RequestType.DELETE, SubmissionType.TEST, RequestStatus.SUCCESS, + Tan.empty(), Instant.parse("2023-07-07T02:00:00Z"), ) diff --git a/src/test/kotlin/dev/dnpm/etl/processor/services/ResponseProcessorTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/services/ResponseProcessorTest.kt index 804b91c..3b09cc7 100644 --- a/src/test/kotlin/dev/dnpm/etl/processor/services/ResponseProcessorTest.kt +++ b/src/test/kotlin/dev/dnpm/etl/processor/services/ResponseProcessorTest.kt @@ -55,6 +55,7 @@ class ResponseProcessorTest { RequestType.MTB_FILE, SubmissionType.TEST, RequestStatus.UNKNOWN, + Tan.empty(), ) @BeforeEach -- cgit v1.2.3