diff options
| author | Paul-Christian Volkmer | 2026-01-06 16:34:12 +0100 |
|---|---|---|
| committer | GitHub | 2026-01-06 15:34:12 +0000 |
| commit | 7be91444a867774362eb5b57bdd246fb50189e7d (patch) | |
| tree | 6a325575bf19e4016ead259a92803b110071eb4f /src/main/kotlin/dev | |
| parent | 2a106a49d91699d0699af1134c41a43b942b85e8 (diff) | |
feat: block further initial submissions (#232)
Diffstat (limited to 'src/main/kotlin/dev')
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" + } } |
