summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul-Christian Volkmer2026-01-06 16:34:12 +0100
committerGitHub2026-01-06 15:34:12 +0000
commit7be91444a867774362eb5b57bdd246fb50189e7d (patch)
tree6a325575bf19e4016ead259a92803b110071eb4f
parent2a106a49d91699d0699af1134c41a43b942b85e8 (diff)
feat: block further initial submissions (#232)
-rw-r--r--README.md11
-rw-r--r--src/integrationTest/kotlin/dev/dnpm/etl/processor/monitoring/RequestRepositoryTest.kt1
-rw-r--r--src/integrationTest/kotlin/dev/dnpm/etl/processor/output/RestDipMtbFileSenderTest.kt109
-rw-r--r--src/integrationTest/kotlin/dev/dnpm/etl/processor/services/RequestServiceIntegrationTest.kt4
-rw-r--r--src/integrationTest/kotlin/dev/dnpm/etl/processor/web/HomeControllerTest.kt9
-rw-r--r--src/integrationTest/kotlin/dev/dnpm/etl/processor/web/StatisticsRestControllerTest.kt6
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt1
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt56
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/config/AppSecurityConfiguration.kt2
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/monitoring/Request.kt28
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/monitoring/RequestStatus.kt1
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/monitoring/SubmissionType.kt12
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/output/RestMtbFileSender.kt3
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt75
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/web/HomeController.kt34
-rw-r--r--src/main/resources/db/migration/mariadb/V0_5_0__SubmissionType.sql2
-rw-r--r--src/main/resources/db/migration/postgresql/V0_5_0__SubmissionType.sql2
-rw-r--r--src/main/resources/static/style.css17
-rw-r--r--src/main/resources/templates/fragments.html14
-rw-r--r--src/main/resources/templates/index.html11
-rw-r--r--src/test/kotlin/dev/dnpm/etl/processor/services/RequestProcessorTest.kt232
-rw-r--r--src/test/kotlin/dev/dnpm/etl/processor/services/RequestServiceTest.kt11
-rw-r--r--src/test/kotlin/dev/dnpm/etl/processor/services/ResponseProcessorTest.kt2
-rw-r--r--src/test/kotlin/dev/dnpm/etl/processor/services/TransformationServiceTest.kt11
24 files changed, 556 insertions, 98 deletions
diff --git a/README.md b/README.md
index a945762..5b631f7 100644
--- a/README.md
+++ b/README.md
@@ -45,6 +45,17 @@ Zu diesem Zweck muss in gPas eine **Multi-Pseudonym-Domäne** konfiguriert werde
**WICHTIG:** Deaktivierte Pseudonymisierung ist nur für Tests nutzbar. Vorgangsummern sind zufällig
und werden anschließend verworfen.
+#### Blockieren weiterer initialer Submissions
+
+Diese Anwendung blockiert weitere initiale Submissions nach der ersten erfolgreichen Übertragung in DNPM:DIP.
+Sobald für einen Patienten eine Übertragung ohne Issues oder mit maximal Warnungen erfolgte und damit von
+DNPM:DIP akzeptiert wurde, werden weitere Meldungen solange verworfen, bis ein Administrator den Patienten
+wieder freigegeben hat.
+
+**ACHTUNG**: Diese Funktionalität ist in Version 0.12.x noch nicht standardmäßig aktiviert und muss erst aktiviert werden.
+
+`APP_POST_INITIAL_SUBMISSION_BLOCK` -> `true` | `false` (falls fehlt, wird `false` angenommen)
+
#### Test Betriebsbereitschaft
Um die voll Betriebsbereitschaft herzustellen, muss eine erfolgreiche Übertragung mit dem
Submission-Typ *Test* erfolgt sein. Über die Umgebungsvariable wird dieser Übertragungsmodus
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 428a99d..1f561da 100644
--- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/monitoring/RequestRepositoryTest.kt
+++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/monitoring/RequestRepositoryTest.kt
@@ -61,6 +61,7 @@ class RequestRepositoryTest : AbstractTestcontainerTest() {
PatientId("P1"),
Fingerprint("0123456789abcdef1"),
RequestType.MTB_FILE,
+ SubmissionType.TEST,
RequestStatus.WARNING,
Instant.parse("2023-07-07T00:00:00Z"),
)
diff --git a/src/integrationTest/kotlin/dev/dnpm/etl/processor/output/RestDipMtbFileSenderTest.kt b/src/integrationTest/kotlin/dev/dnpm/etl/processor/output/RestDipMtbFileSenderTest.kt
index f6f6a08..d27aa4c 100644
--- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/output/RestDipMtbFileSenderTest.kt
+++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/output/RestDipMtbFileSenderTest.kt
@@ -31,6 +31,8 @@ import dev.dnpm.etl.processor.consent.ConsentEvaluator
import dev.dnpm.etl.processor.monitoring.ReportService
import dev.dnpm.etl.processor.monitoring.RequestStatus
import dev.pcvolkmer.mv64e.mtb.*
+import java.time.Instant
+import java.util.*
import org.assertj.core.api.Assertions.assertThat
import org.hamcrest.CoreMatchers.not
import org.hamcrest.Matchers.containsString
@@ -51,8 +53,6 @@ import org.springframework.test.web.client.MockRestServiceServer
import org.springframework.test.web.client.match.MockRestRequestMatchers.*
import org.springframework.test.web.client.response.MockRestResponseCreators.withStatus
import org.springframework.web.client.RestTemplate
-import java.time.Instant
-import java.util.*
@SpringBootTest
@MockitoBean(types = [ReportService::class])
@@ -70,43 +70,39 @@ import java.util.*
)
class RestDipMtbFileSenderTest {
- @Nested
- inner class DnpmV2ContentRequest {
+ @Nested
+ inner class DnpmV2ContentRequest {
- private lateinit var mockRestServiceServer: MockRestServiceServer
+ private lateinit var mockRestServiceServer: MockRestServiceServer
- private lateinit var restMtbFileSender: RestMtbFileSender
+ private lateinit var restMtbFileSender: RestMtbFileSender
- private var reportService =
- ReportService(ObjectMapper().registerModule(KotlinModule.Builder().build()))
+ private var reportService =
+ ReportService(ObjectMapper().registerModule(KotlinModule.Builder().build()))
- @BeforeEach
- fun setup(
- @Autowired restTemplate: RestTemplate
- ) {
- val restTemplate = restTemplate
- val restTargetProperties = RestTargetProperties("http://localhost:9000/api", null, null)
- val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(1)).build()
+ @BeforeEach
+ fun setup(@Autowired restTemplate: RestTemplate) {
+ val restTemplate = restTemplate
+ val restTargetProperties = RestTargetProperties("http://localhost:9000/api", null, null)
+ val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(1)).build()
- this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
+ this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
- this.restMtbFileSender =
- RestDipMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService)
- }
+ this.restMtbFileSender =
+ RestDipMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService)
+ }
- @Test
- fun shouldNotSendJsonNullValues() {
- this.mockRestServiceServer
- .expect(method(HttpMethod.POST))
- .andExpect(requestTo("http://localhost:9000/api/mtb/etl/patient-record"))
- .andExpect(
- content().string(not(containsString("null")))
- )
- .andRespond {
- withStatus(HttpStatus.OK)
- .contentType(MediaType.APPLICATION_JSON)
- .body(
- """
+ @Test
+ fun shouldNotSendJsonNullValues() {
+ this.mockRestServiceServer
+ .expect(method(HttpMethod.POST))
+ .andExpect(requestTo("http://localhost:9000/api/mtb/etl/patient-record"))
+ .andExpect(content().string(not(containsString("null"))))
+ .andRespond {
+ withStatus(HttpStatus.OK)
+ .contentType(MediaType.APPLICATION_JSON)
+ .body(
+ """
{
"patient": "PID",
"issues": [
@@ -114,33 +110,34 @@ class RestDipMtbFileSenderTest {
]
}
"""
- )
- .createResponse(it)
- }
+ )
+ .createResponse(it)
+ }
- val response = restMtbFileSender.send(DnpmV2MtbFileRequest(RequestId("TEST1234"), dnpmV2MtbFile()))
- assertThat(response.status).isEqualTo(RequestStatus.SUCCESS)
- }
+ val response =
+ restMtbFileSender.send(DnpmV2MtbFileRequest(RequestId("TEST1234"), dnpmV2MtbFile()))
+ assertThat(response.status).isEqualTo(RequestStatus.SUCCESS)
}
+ }
- companion object {
- fun dnpmV2MtbFile(): Mtb {
- return Mtb().apply {
- this.patient =
- Patient().apply {
- this.id = "PID"
- this.birthDate = Date.from(Instant.now())
- this.gender = GenderCoding().apply { this.code = GenderCodingCode.MALE }
- }
- this.episodesOfCare =
- listOf(
- MtbEpisodeOfCare().apply {
- this.id = "1"
- this.patient = Reference().apply { this.id = "PID" }
- this.period = PeriodDate().apply { this.start = Date.from(Instant.now()) }
- }
- )
+ companion object {
+ fun dnpmV2MtbFile(): Mtb {
+ return Mtb().apply {
+ this.patient =
+ Patient().apply {
+ this.id = "PID"
+ this.birthDate = Date.from(Instant.now())
+ this.gender = GenderCoding().apply { this.code = GenderCodingCode.MALE }
}
- }
+ this.episodesOfCare =
+ listOf(
+ MtbEpisodeOfCare().apply {
+ this.id = "1"
+ this.patient = Reference().apply { this.id = "PID" }
+ this.period = PeriodDate().apply { this.start = Date.from(Instant.now()) }
+ }
+ )
+ }
}
+ }
}
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 d9489f2..0b30e94 100644
--- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/services/RequestServiceIntegrationTest.kt
+++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/services/RequestServiceIntegrationTest.kt
@@ -24,6 +24,7 @@ import dev.dnpm.etl.processor.monitoring.Request
import dev.dnpm.etl.processor.monitoring.RequestRepository
import dev.dnpm.etl.processor.monitoring.RequestStatus
import dev.dnpm.etl.processor.monitoring.RequestType
+import dev.dnpm.etl.processor.monitoring.SubmissionType
import dev.dnpm.etl.processor.output.MtbFileSender
import java.time.Instant
import org.assertj.core.api.Assertions.assertThat
@@ -75,6 +76,7 @@ class RequestServiceIntegrationTest : AbstractTestcontainerTest() {
PatientId("P1"),
Fingerprint("0123456789abcdef1"),
RequestType.MTB_FILE,
+ SubmissionType.TEST,
RequestStatus.SUCCESS,
Instant.parse("2023-07-07T02:00:00Z"),
),
@@ -85,6 +87,7 @@ class RequestServiceIntegrationTest : AbstractTestcontainerTest() {
PatientId("P2"),
Fingerprint("0123456789abcdef2"),
RequestType.MTB_FILE,
+ SubmissionType.TEST,
RequestStatus.WARNING,
Instant.parse("2023-08-08T00:00:00Z"),
),
@@ -95,6 +98,7 @@ class RequestServiceIntegrationTest : AbstractTestcontainerTest() {
PatientId("P2"),
Fingerprint("0123456789abcdee1"),
RequestType.DELETE,
+ SubmissionType.TEST,
RequestStatus.SUCCESS,
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 33fc9d2..e329b30 100644
--- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/HomeControllerTest.kt
+++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/HomeControllerTest.kt
@@ -26,6 +26,7 @@ import dev.dnpm.etl.processor.monitoring.Report
import dev.dnpm.etl.processor.monitoring.Request
import dev.dnpm.etl.processor.monitoring.RequestStatus
import dev.dnpm.etl.processor.monitoring.RequestType
+import dev.dnpm.etl.processor.monitoring.SubmissionType
import dev.dnpm.etl.processor.services.RequestService
import java.io.IOException
import java.time.Instant
@@ -113,6 +114,7 @@ class HomeControllerTest {
PatientId("PATIENT1"),
Fingerprint("ashdkasdh"),
RequestType.MTB_FILE,
+ SubmissionType.TEST,
RequestStatus.SUCCESS,
),
Request(
@@ -122,6 +124,7 @@ class HomeControllerTest {
PatientId("PATIENT1"),
Fingerprint("asdasdasd"),
RequestType.MTB_FILE,
+ SubmissionType.TEST,
RequestStatus.ERROR,
),
)
@@ -148,6 +151,7 @@ class HomeControllerTest {
PatientId("PATIENT1"),
Fingerprint("ashdkasdh"),
RequestType.MTB_FILE,
+ SubmissionType.TEST,
RequestStatus.SUCCESS,
Instant.now(),
Report("Test"),
@@ -174,6 +178,7 @@ class HomeControllerTest {
PatientId("PATIENT1"),
Fingerprint("ashdkasdh"),
RequestType.MTB_FILE,
+ SubmissionType.TEST,
RequestStatus.SUCCESS,
),
Request(
@@ -183,6 +188,7 @@ class HomeControllerTest {
PatientId("PATIENT1"),
Fingerprint("asdasdasd"),
RequestType.MTB_FILE,
+ SubmissionType.TEST,
RequestStatus.ERROR,
),
)
@@ -208,6 +214,7 @@ class HomeControllerTest {
PatientId("PATIENT1"),
Fingerprint("ashdkasdh"),
RequestType.MTB_FILE,
+ SubmissionType.TEST,
RequestStatus.SUCCESS,
),
Request(
@@ -217,6 +224,7 @@ class HomeControllerTest {
PatientId("PATIENT1"),
Fingerprint("asdasdasd"),
RequestType.MTB_FILE,
+ SubmissionType.TEST,
RequestStatus.ERROR,
),
)
@@ -286,6 +294,7 @@ class HomeControllerTest {
PatientId("PATIENT1"),
Fingerprint("ashdkasdh"),
RequestType.MTB_FILE,
+ SubmissionType.TEST,
RequestStatus.NO_CONSENT,
)
)
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 16a9464..aabd634 100644
--- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/StatisticsRestControllerTest.kt
+++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/StatisticsRestControllerTest.kt
@@ -28,6 +28,7 @@ import dev.dnpm.etl.processor.monitoring.CountedState
import dev.dnpm.etl.processor.monitoring.Request
import dev.dnpm.etl.processor.monitoring.RequestStatus
import dev.dnpm.etl.processor.monitoring.RequestType
+import dev.dnpm.etl.processor.monitoring.SubmissionType
import dev.dnpm.etl.processor.randomRequestId
import dev.dnpm.etl.processor.services.RequestService
import org.hamcrest.Matchers.equalTo
@@ -185,6 +186,7 @@ class StatisticsRestControllerTest {
PatientId("P1"),
Fingerprint("0123456789abcdef1"),
RequestType.MTB_FILE,
+ SubmissionType.TEST,
RequestStatus.SUCCESS,
Instant
.now()
@@ -200,6 +202,7 @@ class StatisticsRestControllerTest {
PatientId("P2"),
Fingerprint("0123456789abcdef2"),
RequestType.MTB_FILE,
+ SubmissionType.TEST,
RequestStatus.WARNING,
Instant
.now()
@@ -215,6 +218,7 @@ class StatisticsRestControllerTest {
PatientId("P2"),
Fingerprint("0123456789abcdee1"),
RequestType.DELETE,
+ SubmissionType.TEST,
RequestStatus.ERROR,
Instant
.now()
@@ -230,6 +234,7 @@ class StatisticsRestControllerTest {
PatientId("P2"),
Fingerprint("0123456789abcdef2"),
RequestType.MTB_FILE,
+ SubmissionType.TEST,
RequestStatus.DUPLICATION,
Instant
.now()
@@ -245,6 +250,7 @@ class StatisticsRestControllerTest {
PatientId("P2"),
Fingerprint("0123456789abcdef2"),
RequestType.DELETE,
+ SubmissionType.TEST,
RequestStatus.UNKNOWN,
Instant
.now()
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt
index 7d795c8..96e48ea 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt
@@ -29,6 +29,7 @@ data class AppConfigProperties(
var maxRetryAttempts: Int = 3,
var duplicationDetection: Boolean = true,
var genomDeTestSubmission: Boolean = false,
+ var postInitialSubmissionBlock: Boolean = false,
) {
companion object {
const val NAME = "app"
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt
index f21c09b..40c290a 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt
@@ -32,6 +32,8 @@ import dev.dnpm.etl.processor.security.TokenService
import dev.dnpm.etl.processor.services.ConsentProcessor
import dev.dnpm.etl.processor.services.Transformation
import dev.dnpm.etl.processor.services.TransformationService
+import kotlin.time.Duration.Companion.seconds
+import kotlin.time.toJavaDuration
import org.apache.cxf.jaxws.JaxWsProxyFactoryBean
import org.slf4j.LoggerFactory
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition
@@ -58,8 +60,6 @@ import org.springframework.security.provisioning.InMemoryUserDetailsManager
import org.springframework.web.client.HttpClientErrorException
import org.springframework.web.client.RestTemplate
import reactor.core.publisher.Sinks
-import kotlin.time.Duration.Companion.seconds
-import kotlin.time.toJavaDuration
@Configuration
@EnableConfigurationProperties(
@@ -77,12 +77,14 @@ class AppConfiguration {
private val logger = LoggerFactory.getLogger(AppConfiguration::class.java)
- fun stringHttpMessageConverter(): StringHttpMessageConverter {
- return StringHttpMessageConverter()
- }
+ fun stringHttpMessageConverter(): StringHttpMessageConverter {
+ return StringHttpMessageConverter()
+ }
- @Bean
- fun mappingJacksonHttpMessageConverter(objectMapper: ObjectMapper): MappingJackson2HttpMessageConverter {
+ @Bean
+ fun mappingJacksonHttpMessageConverter(
+ objectMapper: ObjectMapper
+ ): MappingJackson2HttpMessageConverter {
val converter = MappingJackson2HttpMessageConverter()
converter.setObjectMapper(objectMapper)
return converter
@@ -91,7 +93,10 @@ class AppConfiguration {
@Bean
fun restTemplate(objectMapper: ObjectMapper): RestTemplate {
return RestTemplateBuilder()
- .messageConverters(stringHttpMessageConverter(), mappingJacksonHttpMessageConverter(objectMapper))
+ .messageConverters(
+ stringHttpMessageConverter(),
+ mappingJacksonHttpMessageConverter(objectMapper),
+ )
.build()
}
@@ -273,16 +278,21 @@ class AppConfiguration {
return GicsConsentService(gIcsConfigProperties, retryTemplate, restTemplate, appFhirConfig)
}
- @Conditional(GicsGetBroadConsentEnabledCondition::class)
- @Bean
- fun gicsGetBroadConsentService(
- gIcsConfigProperties: GIcsConfigProperties,
- retryTemplate: RetryTemplate,
- restTemplate: RestTemplate,
- appFhirConfig: AppFhirConfig,
- ): IConsentService {
- return GicsGetBroadConsentService(gIcsConfigProperties, retryTemplate, restTemplate, appFhirConfig)
- }
+ @Conditional(GicsGetBroadConsentEnabledCondition::class)
+ @Bean
+ fun gicsGetBroadConsentService(
+ gIcsConfigProperties: GIcsConfigProperties,
+ retryTemplate: RetryTemplate,
+ restTemplate: RestTemplate,
+ appFhirConfig: AppFhirConfig,
+ ): IConsentService {
+ return GicsGetBroadConsentService(
+ gIcsConfigProperties,
+ retryTemplate,
+ restTemplate,
+ appFhirConfig,
+ )
+ }
@Conditional(GicsEnabledCondition::class)
@Bean
@@ -336,9 +346,9 @@ class GicsEnabledCondition :
class GicsGetBroadConsentEnabledCondition :
AnyNestedCondition(ConfigurationCondition.ConfigurationPhase.REGISTER_BEAN) {
- @ConditionalOnProperty(name = ["app.consent.service"], havingValue = "gics_get_bc")
- @ConditionalOnProperty(name = ["app.consent.gics.uri"])
- class OnGicsGetBroadConsentServiceSelected {
- // Just for Condition
- }
+ @ConditionalOnProperty(name = ["app.consent.service"], havingValue = "gics_get_bc")
+ @ConditionalOnProperty(name = ["app.consent.gics.uri"])
+ class OnGicsGetBroadConsentServiceSelected {
+ // Just for Condition
+ }
}
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/AppSecurityConfiguration.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/AppSecurityConfiguration.kt
index 6b7cab8..e0f24cf 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppSecurityConfiguration.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppSecurityConfiguration.kt
@@ -94,6 +94,7 @@ class AppSecurityConfiguration(private val securityConfigProperties: SecurityCon
authorize("/mtbfile/**", hasAnyRole("MTBFILE", "ADMIN", "USER"))
authorize("/mtb/**", hasAnyRole("MTBFILE", "ADMIN", "USER"))
authorize("/report/**", hasAnyRole("ADMIN", "USER"))
+ authorize("/submission/**", hasAnyRole("ADMIN", "USER"))
authorize("*.css", permitAll)
authorize("*.ico", permitAll)
authorize("*.jpeg", permitAll)
@@ -161,6 +162,7 @@ class AppSecurityConfiguration(private val securityConfigProperties: SecurityCon
authorize("/mtbfile/**", hasAnyRole("MTBFILE", "ADMIN"))
authorize("/mtb/**", hasAnyRole("MTBFILE", "ADMIN"))
authorize("/report/**", hasRole("ADMIN"))
+ authorize("/submission/**", hasAnyRole("ADMIN"))
authorize(anyRequest, permitAll)
}
httpBasic { realmName = "ETL-Processor" }
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 71731f1..b1cc1ed 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/Request.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/Request.kt
@@ -41,9 +41,11 @@ data class Request(
val pid: PatientId,
@Column("fingerprint") val fingerprint: Fingerprint,
val type: RequestType,
+ @Column("submission_type") val submissionType: SubmissionType,
var status: RequestStatus,
var processedAt: Instant = Instant.now(),
@Embedded.Nullable var report: Report? = null,
+ @Column("submission_accepted") var submissionAccepted: Boolean = false,
) {
constructor(
uuid: RequestId,
@@ -51,8 +53,19 @@ data class Request(
pid: PatientId,
fingerprint: Fingerprint,
type: RequestType,
+ submissionType: SubmissionType,
status: RequestStatus,
- ) : this(null, uuid, patientPseudonym, pid, fingerprint, type, status, Instant.now())
+ ) : this(
+ null,
+ uuid,
+ patientPseudonym,
+ pid,
+ fingerprint,
+ type,
+ submissionType,
+ status,
+ Instant.now(),
+ )
constructor(
uuid: RequestId,
@@ -60,9 +73,20 @@ data class Request(
pid: PatientId,
fingerprint: Fingerprint,
type: RequestType,
+ submissionType: SubmissionType,
status: RequestStatus,
processedAt: Instant,
- ) : this(null, uuid, patientPseudonym, pid, fingerprint, type, status, processedAt)
+ ) : this(
+ null,
+ uuid,
+ patientPseudonym,
+ pid,
+ fingerprint,
+ type,
+ submissionType,
+ status,
+ processedAt,
+ )
fun isPendingUnknown(): Boolean {
return this.status == RequestStatus.UNKNOWN &&
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/RequestStatus.kt b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/RequestStatus.kt
index 5487a05..a0cd4ad 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/RequestStatus.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/RequestStatus.kt
@@ -28,4 +28,5 @@ enum class RequestStatus(
UNKNOWN("unknown"),
DUPLICATION("duplication"),
NO_CONSENT("no-consent"),
+ BLOCKED_INITIAL("blocked-initial"),
}
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/SubmissionType.kt b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/SubmissionType.kt
new file mode 100644
index 0000000..351281e
--- /dev/null
+++ b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/SubmissionType.kt
@@ -0,0 +1,12 @@
+package dev.dnpm.etl.processor.monitoring
+
+enum class SubmissionType(
+ val value: String,
+) {
+ UNKNOWN("unknown"),
+ INITIAL("initial"),
+ ADDITION("addition"),
+ CORRECTION("correction"),
+ FOLLOWUP("followup"),
+ TEST("test"),
+}
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/output/RestMtbFileSender.kt b/src/main/kotlin/dev/dnpm/etl/processor/output/RestMtbFileSender.kt
index 9c22ec0..a2a88b6 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/output/RestMtbFileSender.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/output/RestMtbFileSender.kt
@@ -53,7 +53,8 @@ abstract class RestMtbFileSender(
return retryTemplate.execute<MtbFileSender.Response, Exception> {
val headers = getHttpHeaders(request)
val entityReq = HttpEntity(request.content, headers)
- val response = restTemplate.exchange(sendUrl(), HttpMethod.POST, entityReq, String::class.java)
+ val response =
+ restTemplate.exchange(sendUrl(), HttpMethod.POST, entityReq, String::class.java)
if (!response.statusCode.is2xxSuccessful) {
logger.warn("Error sending to remote system: {}", response.body)
return@execute MtbFileSender.Response(
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 22a25ee..2b80167 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt
@@ -27,6 +27,7 @@ import dev.dnpm.etl.processor.monitoring.Report
import dev.dnpm.etl.processor.monitoring.Request
import dev.dnpm.etl.processor.monitoring.RequestStatus
import dev.dnpm.etl.processor.monitoring.RequestType
+import dev.dnpm.etl.processor.monitoring.SubmissionType
import dev.dnpm.etl.processor.output.DeleteRequest
import dev.dnpm.etl.processor.output.DnpmV2MtbFileRequest
import dev.dnpm.etl.processor.output.MtbFileRequest
@@ -38,6 +39,7 @@ import dev.dnpm.etl.processor.pseudonym.pseudonymizeWith
import dev.pcvolkmer.mv64e.mtb.ConsentProvision
import dev.pcvolkmer.mv64e.mtb.ModelProjectConsentPurpose
import dev.pcvolkmer.mv64e.mtb.Mtb
+import dev.pcvolkmer.mv64e.mtb.MvhSubmissionType
import java.time.Instant
import java.util.*
import org.apache.commons.codec.binary.Base32
@@ -94,6 +96,53 @@ class RequestProcessor(
}
private fun <T> saveAndSend(request: MtbFileRequest<T>) {
+ var submissionType: SubmissionType =
+ when (request) {
+ is DnpmV2MtbFileRequest -> {
+ when (request.content.metadata?.type) {
+ MvhSubmissionType.TEST -> SubmissionType.TEST
+ MvhSubmissionType.INITIAL -> SubmissionType.INITIAL
+ MvhSubmissionType.ADDITION -> SubmissionType.ADDITION
+ MvhSubmissionType.CORRECTION -> SubmissionType.CORRECTION
+ MvhSubmissionType.FOLLOWUP -> SubmissionType.FOLLOWUP
+ else -> SubmissionType.UNKNOWN
+ }
+ }
+ }
+
+ if (
+ appConfigProperties.postInitialSubmissionBlock &&
+ hasSuccessfullInitialSubmission(request.patientPseudonym()) &&
+ hasUnacceptedInitialSubmission(request.patientPseudonym())
+ ) {
+ requestService.save(
+ Request(
+ request.requestId,
+ request.patientPseudonym(),
+ emptyPatientId(),
+ fingerprint(request),
+ RequestType.MTB_FILE,
+ submissionType,
+ RequestStatus.BLOCKED_INITIAL,
+ )
+ )
+ // Exit - no further processing
+ return
+ }
+
+ if (
+ appConfigProperties.postInitialSubmissionBlock &&
+ hasSuccessfullInitialSubmission(request.patientPseudonym()) &&
+ !hasUnacceptedInitialSubmission(request.patientPseudonym())
+ ) {
+ // Use "addition" after "intial" with "Meldebestaetigung"
+ request.content.metadata?.let {
+ logger.warn("Override submission type using 'addition' after first initial submission!")
+ it.type = MvhSubmissionType.ADDITION
+ submissionType = SubmissionType.ADDITION
+ }
+ }
+
requestService.save(
Request(
request.requestId,
@@ -101,6 +150,7 @@ class RequestProcessor(
emptyPatientId(),
fingerprint(request),
RequestType.MTB_FILE,
+ submissionType,
RequestStatus.UNKNOWN,
)
)
@@ -128,6 +178,20 @@ class RequestProcessor(
)
}
+ private fun hasSuccessfullInitialSubmission(patientPseudonym: PatientPseudonym): Boolean {
+ return this.requestService.allRequestsByPatientPseudonym(patientPseudonym).any {
+ it.submissionType == SubmissionType.INITIAL &&
+ (it.status == RequestStatus.SUCCESS || it.status == RequestStatus.WARNING)
+ }
+ }
+
+ private fun hasUnacceptedInitialSubmission(patientPseudonym: PatientPseudonym): Boolean {
+ return this.requestService.allRequestsByPatientPseudonym(patientPseudonym).any {
+ it.submissionType == SubmissionType.INITIAL &&
+ !(it.submissionAccepted || it.status == RequestStatus.BLOCKED_INITIAL)
+ }
+ }
+
private fun <T> isDuplication(pseudonymizedMtbFileRequest: MtbFileRequest<T>): Boolean {
val patientPseudonym =
when (pseudonymizedMtbFileRequest) {
@@ -179,6 +243,7 @@ class RequestProcessor(
emptyPatientId(),
fingerprint(patientPseudonym.value),
RequestType.DELETE,
+ SubmissionType.UNKNOWN,
requestStatus,
)
)
@@ -206,6 +271,7 @@ class RequestProcessor(
fingerprint = Fingerprint.empty(),
status = RequestStatus.ERROR,
type = RequestType.DELETE,
+ submissionType = SubmissionType.UNKNOWN,
report = Report("Fehler bei der Pseudonymisierung"),
)
)
@@ -219,11 +285,8 @@ class RequestProcessor(
}
private fun fingerprint(s: String): Fingerprint {
- return Fingerprint(
- Base32()
- .encodeAsString(DigestUtils.sha256(s))
- .replace("=", "")
- .lowercase()
- )
+ return Fingerprint(Base32().encodeAsString(DigestUtils.sha256(s))
+ .replace("=", "")
+ .lowercase())
}
}
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/web/HomeController.kt b/src/main/kotlin/dev/dnpm/etl/processor/web/HomeController.kt
index 082cd20..ed0f264 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/web/HomeController.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/web/HomeController.kt
@@ -22,6 +22,7 @@ package dev.dnpm.etl.processor.web
import dev.dnpm.etl.processor.NotFoundException
import dev.dnpm.etl.processor.PatientPseudonym
import dev.dnpm.etl.processor.RequestId
+import dev.dnpm.etl.processor.config.AppConfigProperties
import dev.dnpm.etl.processor.monitoring.ReportService
import dev.dnpm.etl.processor.services.RequestService
import org.springframework.data.domain.Pageable
@@ -29,8 +30,10 @@ import org.springframework.data.domain.Sort
import org.springframework.data.web.PageableDefault
import org.springframework.stereotype.Controller
import org.springframework.ui.Model
+import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
+import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestMapping
@Controller
@@ -38,6 +41,7 @@ import org.springframework.web.bind.annotation.RequestMapping
class HomeController(
private val requestService: RequestService,
private val reportService: ReportService,
+ private val appConfigProperties: AppConfigProperties,
) {
@GetMapping
fun index(
@@ -47,7 +51,7 @@ class HomeController(
): String {
val requests = requestService.findAll(pageable)
model.addAttribute("requests", requests)
-
+ model.addAttribute("postInitialSubmissionBlock", appConfigProperties.postInitialSubmissionBlock)
return "index"
}
@@ -76,4 +80,32 @@ class HomeController(
return "report"
}
+
+ @PutMapping(path = ["/submission/{id}/accepted"])
+ fun acceptSubmission(
+ @PathVariable id: RequestId,
+ model: Model,
+ ): String {
+ val request = requestService.findByUuid(id).orElse(null) ?: throw NotFoundException()
+ request.submissionAccepted = true
+ val savedRequest = requestService.save(request)
+
+ model.addAttribute("request", savedRequest)
+
+ return "fragments :: accept-initial"
+ }
+
+ @DeleteMapping(path = ["/submission/{id}/accepted"])
+ fun unacceptSubmission(
+ @PathVariable id: RequestId,
+ model: Model,
+ ): String {
+ val request = requestService.findByUuid(id).orElse(null) ?: throw NotFoundException()
+ request.submissionAccepted = false
+ val savedRequest = requestService.save(request)
+
+ model.addAttribute("request", savedRequest)
+
+ return "fragments :: accept-initial"
+ }
}
diff --git a/src/main/resources/db/migration/mariadb/V0_5_0__SubmissionType.sql b/src/main/resources/db/migration/mariadb/V0_5_0__SubmissionType.sql
new file mode 100644
index 0000000..79f89d8
--- /dev/null
+++ b/src/main/resources/db/migration/mariadb/V0_5_0__SubmissionType.sql
@@ -0,0 +1,2 @@
+ALTER TABLE request ADD COLUMN submission_type varchar(16) not null default 'UNKNOWN';
+ALTER TABLE request ADD COLUMN submission_accepted boolean not null default false; \ No newline at end of file
diff --git a/src/main/resources/db/migration/postgresql/V0_5_0__SubmissionType.sql b/src/main/resources/db/migration/postgresql/V0_5_0__SubmissionType.sql
new file mode 100644
index 0000000..79f89d8
--- /dev/null
+++ b/src/main/resources/db/migration/postgresql/V0_5_0__SubmissionType.sql
@@ -0,0 +1,2 @@
+ALTER TABLE request ADD COLUMN submission_type varchar(16) not null default 'UNKNOWN';
+ALTER TABLE request ADD COLUMN submission_accepted boolean not null default false; \ No newline at end of file
diff --git a/src/main/resources/static/style.css b/src/main/resources/static/style.css
index 83d98c3..33e5d20 100644
--- a/src/main/resources/static/style.css
+++ b/src/main/resources/static/style.css
@@ -360,7 +360,7 @@ form.samplecode-input input:focus-visible {
}
.border {
- padding: 1.5rem;
+ padding: 1rem;
border: 1px solid var(--table-border);
border-radius: .5rem;
background: white;
@@ -382,7 +382,7 @@ table {
}
table.config-table td:first-child {
- width: 24rem;
+ width: 26rem;
min-width: fit-content;
}
@@ -451,10 +451,13 @@ th {
}
td {
- font-family: monospace;
border-bottom: 1px solid var(--bg-gray-op);
}
+td, td > a {
+ font-family: monospace;
+}
+
tr:last-of-type > td {
border-bottom: none;
}
@@ -465,10 +468,9 @@ td > small {
}
td.patient-id {
- width: 32rem;
+ min-width: 20rem;
text-overflow: ellipsis;
overflow: hidden;
- display: block;
}
td.bg-blue, th.bg-blue,
@@ -571,6 +573,11 @@ td.clipboard.clipped {
color: white;
}
+.btn.btn-green {
+ background: var(--bg-green);
+ color: white;
+}
+
.btn.btn-blue {
background: var(--bg-blue);
color: white;
diff --git a/src/main/resources/templates/fragments.html b/src/main/resources/templates/fragments.html
index f3d95c9..8ecd507 100644
--- a/src/main/resources/templates/fragments.html
+++ b/src/main/resources/templates/fragments.html
@@ -35,6 +35,20 @@
</ul>
</nav>
</div>
+ <th:block th:fragment="accept-initial" sec:authorize="hasRole('USER') or hasRole('ADMIN')">
+ <button class="btn" hx-swap="outerHTML" th:hx-delete="@{/submission/{requestId}/accepted(requestId=${request.uuid})}" th:if="${
+ request.submissionType.value == 'initial'
+ and (request.status.value == 'success' or request.status.value == 'warning')
+ and request.submissionAccepted == true}" title="Keine Meldebestätigung - blockieren">
+ 🔒
+ </button>
+ <button class="btn" hx-swap="outerHTML" th:hx-put="@{/submission/{requestId}/accepted(requestId=${request.uuid})}" th:if="${
+ request.submissionType.value == 'initial'
+ and (request.status.value == 'success' or request.status.value == 'warning')
+ and request.submissionAccepted == false}" title="Meldebestätigung vorhanden - nicht weiter blockieren">
+ 🔓
+ </button>
+ </th:block>
<footer th:fragment="footer">
<div class="container">
<div>
diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html
index 5021bc4..a732fa2 100644
--- a/src/main/resources/templates/index.html
+++ b/src/main/resources/templates/index.html
@@ -42,6 +42,7 @@
<tr>
<th>Status</th>
<th>Typ</th>
+ <th sec:authorize="hasRole('USER') or hasRole('ADMIN')" th:if="${postInitialSubmissionBlock}">Aktion</th>
<th>ID</th>
<th>Datum</th>
<th>Patienten-ID</th>
@@ -56,7 +57,14 @@
<td th:if="${request.status.value == 'unknown' and request.isPendingUnknown()}" class="bg-yellow"><small>⏰ [[ ${request.status} ]] ⏰</small></td>
<td th:if="${request.status.value == 'duplication'}" class="bg-gray"><small>[[ ${request.status} ]]</small></td>
<td th:if="${request.status.value == 'no-consent'}" class="bg-blue"><small>[[ ${request.status} ]]</small></td>
- <td th:style="${request.type.value == 'delete'} ? 'color: red;'"><small>[[ ${request.type} ]]</small></td>
+ <td th:if="${request.status.value == 'blocked-initial'}" class="bg-gray"><small>[[ ${request.status} ]]</small></td>
+ <td th:style="${request.type.value == 'delete'} ? 'color: red;'">
+ <small>
+ [[ ${request.type} ]]
+ <th:block th:if="${request.submissionType.value != 'unknown'}">([[ ${request.submissionType} ]])</th:block>
+ </small>
+ </td>
+ <td th:insert="~{fragments :: accept-initial}" sec:authorize="hasRole('USER') or hasRole('ADMIN')" th:if="${postInitialSubmissionBlock}"></td>
<td th:if="not ${request.report}">[[ ${request.uuid} ]]</td>
<td th:if="${request.report}">
<a th:href="@{/report/{id}(id=${request.uuid})}" sec:authorize="hasRole('USER') or hasRole('ADMIN')">[[ ${request.uuid} ]]</a>
@@ -78,6 +86,7 @@
</main>
<footer th:replace="~{fragments.html :: footer}"></footer>
<script th:src="@{/scripts.js}"></script>
+ <script th:src="@{/webjars/htmx.org/dist/htmx.min.js}"></script>
<script>
window.addEventListener('load', () => {
let keyBindings = {
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 4bd3fc1..851c1a1 100644
--- a/src/test/kotlin/dev/dnpm/etl/processor/services/RequestProcessorTest.kt
+++ b/src/test/kotlin/dev/dnpm/etl/processor/services/RequestProcessorTest.kt
@@ -28,6 +28,7 @@ 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
+import dev.dnpm.etl.processor.monitoring.SubmissionType
import dev.dnpm.etl.processor.output.DeleteRequest
import dev.dnpm.etl.processor.output.DnpmV2MtbFileRequest
import dev.dnpm.etl.processor.output.MtbFileSender
@@ -39,6 +40,7 @@ import java.time.Instant
import java.util.*
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.Mock
@@ -104,6 +106,7 @@ class RequestProcessorTest {
PatientId("P1"),
Fingerprint("6vkiti5bk6ikwifpajpt7cygmd3dvw54d6lwfhzlynb3pqtzferq"),
RequestType.MTB_FILE,
+ SubmissionType.TEST,
RequestStatus.SUCCESS,
Instant.parse("2023-08-08T02:00:00Z"),
)
@@ -159,6 +162,7 @@ class RequestProcessorTest {
PatientId("P1"),
Fingerprint("4gcjwtjjtcczybsljxepdfpkaeusvd7g3vogfqpmphyffyzfx7dq"),
RequestType.MTB_FILE,
+ SubmissionType.TEST,
RequestStatus.SUCCESS,
Instant.parse("2023-08-08T02:00:00Z"),
)
@@ -214,6 +218,7 @@ class RequestProcessorTest {
PatientId("P1"),
Fingerprint("different"),
RequestType.MTB_FILE,
+ SubmissionType.TEST,
RequestStatus.SUCCESS,
Instant.parse("2023-08-08T02:00:00Z"),
)
@@ -273,6 +278,7 @@ class RequestProcessorTest {
PatientId("P1"),
Fingerprint("different"),
RequestType.MTB_FILE,
+ SubmissionType.TEST,
RequestStatus.SUCCESS,
Instant.parse("2023-08-08T02:00:00Z"),
)
@@ -339,6 +345,102 @@ class RequestProcessorTest {
}
@Test
+ fun testShouldSendMtbFileAdditionIfInitialFileWasAccepted() {
+
+ // One successful and accepted and one blocked initial
+ val lastRequests =
+ listOf(
+ Request(
+ 1L,
+ randomRequestId(),
+ PatientPseudonym("TEST_12345678901"),
+ PatientId("P1"),
+ Fingerprint("initial"),
+ RequestType.MTB_FILE,
+ SubmissionType.INITIAL,
+ RequestStatus.SUCCESS,
+ Instant.parse("2026-01-05T09:00:00Z"),
+ submissionAccepted = true,
+ ),
+ Request(
+ 2L,
+ randomRequestId(),
+ PatientPseudonym("TEST_12345678901"),
+ PatientId("P1"),
+ Fingerprint("blocked_initial"),
+ RequestType.MTB_FILE,
+ SubmissionType.INITIAL,
+ RequestStatus.BLOCKED_INITIAL,
+ Instant.parse("2026-01-05T10:00:00Z"),
+ submissionAccepted = false,
+ ),
+ )
+
+ doAnswer { lastRequests }
+ .whenever(requestService)
+ .allRequestsByPatientPseudonym(anyValueClass())
+
+ doAnswer { false }
+ .whenever(requestService)
+ .isLastRequestWithKnownStatusDeletion(anyValueClass())
+
+ doAnswer { MtbFileSender.Response(status = RequestStatus.SUCCESS) }
+ .whenever(sender)
+ .send(any<DnpmV2MtbFileRequest>())
+
+ doAnswer { it.arguments[0] as String }
+ .whenever(pseudonymizeService)
+ .patientPseudonym(anyValueClass())
+
+ doAnswer { it.arguments[0] }.whenever(transformationService).transform(any<Mtb>())
+
+ 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<DnpmV2MtbFileRequest>()
+ verify(sender, times(1)).send(requestCaptor.capture())
+ assertThat(requestCaptor.firstValue).isNotNull
+ assertThat(requestCaptor.firstValue.content.metadata.type).isEqualTo(MvhSubmissionType.ADDITION)
+
+ val eventCaptor = argumentCaptor<ResponseEvent>()
+ verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture())
+ assertThat(eventCaptor.firstValue).isNotNull
+ assertThat(eventCaptor.firstValue.status).isEqualTo(RequestStatus.SUCCESS)
+ }
+
+ @Test
fun testShouldSendDeleteRequestAndSaveUnknownRequestStatusAtFirst() {
doAnswer { "PSEUDONYM" }.whenever(pseudonymizeService).patientPseudonym(anyValueClass())
@@ -452,6 +554,136 @@ class RequestProcessorTest {
assertThat(eventCaptor.firstValue.status).isEqualTo(RequestStatus.SUCCESS)
}
+ @Nested
+ inner class WithInitialSubmissionBlock {
+
+ private lateinit var pseudonymizeService: PseudonymizeService
+ private lateinit var transformationService: TransformationService
+ private lateinit var sender: MtbFileSender
+ private lateinit var requestService: RequestService
+ private lateinit var applicationEventPublisher: ApplicationEventPublisher
+ private lateinit var appConfigProperties: AppConfigProperties
+ private lateinit var consentProcessor: ConsentProcessor
+ private lateinit var requestProcessor: RequestProcessor
+
+ @BeforeEach
+ fun setup(
+ @Mock pseudonymizeService: PseudonymizeService,
+ @Mock transformationService: TransformationService,
+ @Mock sender: RestMtbFileSender,
+ @Mock requestService: RequestService,
+ @Mock applicationEventPublisher: ApplicationEventPublisher,
+ @Mock consentProcessor: ConsentProcessor,
+ ) {
+ this.pseudonymizeService = pseudonymizeService
+ this.transformationService = transformationService
+ this.sender = sender
+ this.requestService = requestService
+ this.applicationEventPublisher = applicationEventPublisher
+ this.appConfigProperties = AppConfigProperties()
+ this.consentProcessor = consentProcessor
+
+ val objectMapper = ObjectMapper()
+
+ requestProcessor =
+ RequestProcessor(
+ pseudonymizeService,
+ transformationService,
+ sender,
+ requestService,
+ objectMapper,
+ applicationEventPublisher,
+ appConfigProperties,
+ consentProcessor,
+ )
+ }
+
+ @Test
+ fun testShouldNotSendMtbFileIfInitialFileWasSent() {
+
+ // One failed attempt and one successful but not accepted
+ val lastRequests =
+ listOf(
+ Request(
+ 1L,
+ randomRequestId(),
+ PatientPseudonym("TEST_12345678901"),
+ PatientId("P1"),
+ Fingerprint("initial"),
+ RequestType.MTB_FILE,
+ SubmissionType.INITIAL,
+ RequestStatus.ERROR,
+ Instant.parse("2026-01-05T09:00:00Z"),
+ submissionAccepted = false,
+ ),
+ Request(
+ 2L,
+ randomRequestId(),
+ PatientPseudonym("TEST_12345678901"),
+ PatientId("P1"),
+ Fingerprint("blocked_initial"),
+ RequestType.MTB_FILE,
+ SubmissionType.INITIAL,
+ RequestStatus.SUCCESS,
+ Instant.parse("2026-01-05T10:00:00Z"),
+ submissionAccepted = false,
+ ),
+ )
+
+ doAnswer { lastRequests }
+ .whenever(requestService)
+ .allRequestsByPatientPseudonym(anyValueClass())
+
+ doAnswer { it.arguments[0] as String }
+ .whenever(pseudonymizeService)
+ .patientPseudonym(anyValueClass())
+
+ doAnswer { it.arguments[0] }.whenever(transformationService).transform(any<Mtb>())
+
+ 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())
+ .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<Request>()
+ verify(requestService, times(1)).save(requestCaptor.capture())
+ assertThat(requestCaptor.firstValue).isNotNull
+ assertThat(requestCaptor.firstValue.status).isEqualTo(RequestStatus.BLOCKED_INITIAL)
+
+ verify(applicationEventPublisher, times(0)).publishEvent(any())
+ verify(sender, times(0)).send(any<DnpmV2MtbFileRequest>())
+ }
+ }
+
companion object {
val TEST_PATIENT_ID = PatientId("TEST_12345678901")
}
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 bc0286c..fdb7578 100644
--- a/src/test/kotlin/dev/dnpm/etl/processor/services/RequestServiceTest.kt
+++ b/src/test/kotlin/dev/dnpm/etl/processor/services/RequestServiceTest.kt
@@ -24,6 +24,7 @@ import dev.dnpm.etl.processor.monitoring.Request
import dev.dnpm.etl.processor.monitoring.RequestRepository
import dev.dnpm.etl.processor.monitoring.RequestStatus
import dev.dnpm.etl.processor.monitoring.RequestType
+import dev.dnpm.etl.processor.monitoring.SubmissionType
import java.time.Instant
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
@@ -51,6 +52,7 @@ class RequestServiceTest {
PatientId("PX"),
Fingerprint("dummy"),
RequestType.MTB_FILE,
+ SubmissionType.TEST,
RequestStatus.SUCCESS,
Instant.parse("2023-08-08T02:00:00Z"),
)
@@ -72,6 +74,7 @@ class RequestServiceTest {
PatientId("P1"),
Fingerprint("0123456789abcdef1"),
RequestType.MTB_FILE,
+ SubmissionType.TEST,
RequestStatus.WARNING,
Instant.parse("2023-07-07T00:00:00Z"),
),
@@ -82,6 +85,7 @@ class RequestServiceTest {
PatientId("P1"),
Fingerprint("0123456789abcdefd"),
RequestType.DELETE,
+ SubmissionType.TEST,
RequestStatus.WARNING,
Instant.parse("2023-07-07T02:00:00Z"),
),
@@ -92,6 +96,7 @@ class RequestServiceTest {
PatientId("P1"),
Fingerprint("0123456789abcdef1"),
RequestType.MTB_FILE,
+ SubmissionType.TEST,
RequestStatus.UNKNOWN,
Instant.parse("2023-08-11T00:00:00Z"),
),
@@ -113,6 +118,7 @@ class RequestServiceTest {
PatientId("P1"),
Fingerprint("0123456789abcdef1"),
RequestType.MTB_FILE,
+ SubmissionType.TEST,
RequestStatus.WARNING,
Instant.parse("2023-07-07T00:00:00Z"),
),
@@ -123,6 +129,7 @@ class RequestServiceTest {
PatientId("P1"),
Fingerprint("0123456789abcdef1"),
RequestType.MTB_FILE,
+ SubmissionType.TEST,
RequestStatus.WARNING,
Instant.parse("2023-07-07T02:00:00Z"),
),
@@ -133,6 +140,7 @@ class RequestServiceTest {
PatientId("P1"),
Fingerprint("0123456789abcdef1"),
RequestType.MTB_FILE,
+ SubmissionType.TEST,
RequestStatus.UNKNOWN,
Instant.parse("2023-08-11T00:00:00Z"),
),
@@ -154,6 +162,7 @@ class RequestServiceTest {
PatientId("P1"),
Fingerprint("0123456789abcdef1"),
RequestType.DELETE,
+ SubmissionType.TEST,
RequestStatus.SUCCESS,
Instant.parse("2023-07-07T02:00:00Z"),
),
@@ -164,6 +173,7 @@ class RequestServiceTest {
PatientId("P2"),
Fingerprint("0123456789abcdef2"),
RequestType.MTB_FILE,
+ SubmissionType.TEST,
RequestStatus.WARNING,
Instant.parse("2023-08-08T00:00:00Z"),
),
@@ -200,6 +210,7 @@ class RequestServiceTest {
PatientId("P1"),
Fingerprint("0123456789abcdef1"),
RequestType.DELETE,
+ SubmissionType.TEST,
RequestStatus.SUCCESS,
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 16a5791..804b91c 100644
--- a/src/test/kotlin/dev/dnpm/etl/processor/services/ResponseProcessorTest.kt
+++ b/src/test/kotlin/dev/dnpm/etl/processor/services/ResponseProcessorTest.kt
@@ -23,6 +23,7 @@ import dev.dnpm.etl.processor.*
import dev.dnpm.etl.processor.monitoring.Request
import dev.dnpm.etl.processor.monitoring.RequestStatus
import dev.dnpm.etl.processor.monitoring.RequestType
+import dev.dnpm.etl.processor.monitoring.SubmissionType
import java.time.Instant
import java.util.*
import org.assertj.core.api.Assertions.assertThat
@@ -52,6 +53,7 @@ class ResponseProcessorTest {
PatientId("1"),
Fingerprint("dummyfingerprint"),
RequestType.MTB_FILE,
+ SubmissionType.TEST,
RequestStatus.UNKNOWN,
)
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 a4af214..3af5097 100644
--- a/src/test/kotlin/dev/dnpm/etl/processor/services/TransformationServiceTest.kt
+++ b/src/test/kotlin/dev/dnpm/etl/processor/services/TransformationServiceTest.kt
@@ -24,11 +24,11 @@ import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.node.ObjectNode
import dev.dnpm.etl.processor.config.JacksonConfig
import dev.pcvolkmer.mv64e.mtb.*
+import java.time.Instant
+import java.util.*
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
-import java.time.Instant
-import java.util.*
class TransformationServiceTest {
@@ -149,7 +149,12 @@ class TransformationServiceTest {
)
.build()
val consent = ConsentProcessorTest.getDummyGenomDeConsent()
- val jsonNode = ObjectMapper().readValue(FhirContext.forR4().newJsonParser().encodeToString(consent), ObjectNode::class.java)
+ val jsonNode =
+ ObjectMapper()
+ .readValue(
+ FhirContext.forR4().newJsonParser().encodeToString(consent),
+ ObjectNode::class.java,
+ )
mvhMetadata.researchConsents = mutableListOf()
mvhMetadata.researchConsents.add(MvhMetadata.ResearchConsent.from(jsonNode))