summaryrefslogtreecommitdiff
path: root/src/main/kotlin/dev/dnpm
diff options
context:
space:
mode:
authorPaul-Christian Volkmer2026-01-06 16:34:12 +0100
committerGitHub2026-01-06 15:34:12 +0000
commit7be91444a867774362eb5b57bdd246fb50189e7d (patch)
tree6a325575bf19e4016ead259a92803b110071eb4f /src/main/kotlin/dev/dnpm
parent2a106a49d91699d0699af1134c41a43b942b85e8 (diff)
feat: block further initial submissions (#232)
Diffstat (limited to 'src/main/kotlin/dev/dnpm')
-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
9 files changed, 179 insertions, 33 deletions
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"
+ }
}