From 7298078077d27380255ad5eb42a30876a4babede Mon Sep 17 00:00:00 2001 From: Paul-Christian Volkmer Date: Thu, 11 Jun 2026 08:11:11 +0200 Subject: feat: count follow-ups to update submission type (#297) This is done in addition to the check by date. The submission type will be set to "followup" if: * It is not an initial submission * There are follow-ups in submission content * The date or the count is less than the last successful submission In the database, existing requests will be initialized with followup_count = -1.--- .../dev/dnpm/etl/processor/monitoring/Request.kt | 1 + .../etl/processor/services/RequestProcessor.kt | 39 +- .../migration/mariadb/V0_16_1_1__FollowUpCount.sql | 2 + .../postgresql/V0_16_1_1__FollowUpCount.sql | 2 + .../etl/processor/services/RequestProcessorTest.kt | 912 ++++++++++++++++----- 5 files changed, 738 insertions(+), 218 deletions(-) create mode 100644 src/main/resources/db/migration/mariadb/V0_16_1_1__FollowUpCount.sql create mode 100644 src/main/resources/db/migration/postgresql/V0_16_1_1__FollowUpCount.sql 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 a1f5c3d..67f450f 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/Request.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/Request.kt @@ -53,6 +53,7 @@ data class Request( @LastModifiedBy var updatedBy: String? = null, @Embedded.Nullable var report: Report? = null, @Column("submission_accepted") var submissionAccepted: Boolean = false, + @Column("followup_count") var followupCount: Int = 0, ) { constructor( uuid: RequestId, 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 4b7afff..00e6b73 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt @@ -120,14 +120,15 @@ class RequestProcessor( ) { requestService.save( Request( - request.requestId, - request.patientPseudonym(), - emptyPatientId(), - fingerprint(request), - RequestType.MTB_FILE, - submissionType, - RequestStatus.BLOCKED_INITIAL, - Tan(request.content.metadata?.transferTan.orEmpty()) + uuid = request.requestId, + patientPseudonym = request.patientPseudonym(), + pid = emptyPatientId(), + fingerprint = fingerprint(request), + type = RequestType.MTB_FILE, + submissionType = submissionType, + status = RequestStatus.BLOCKED_INITIAL, + tan = Tan(request.content.metadata?.transferTan.orEmpty()), + followupCount = request.content.followUps?.size ?: 0, ) ) // Exit - no further processing @@ -157,14 +158,15 @@ class RequestProcessor( requestService.save( Request( - request.requestId, - request.patientPseudonym(), - emptyPatientId(), - fingerprint(request), - RequestType.MTB_FILE, - submissionType, - RequestStatus.UNKNOWN, - Tan(request.content.metadata?.transferTan.orEmpty()), + uuid = request.requestId, + patientPseudonym = request.patientPseudonym(), + pid = emptyPatientId(), + fingerprint = fingerprint(request), + type = RequestType.MTB_FILE, + submissionType = submissionType, + status = RequestStatus.UNKNOWN, + tan = Tan(request.content.metadata?.transferTan.orEmpty()), + followupCount = request.content.followUps?.size ?: 0, ) ) @@ -207,7 +209,10 @@ class RequestProcessor( ?.sortedBy { it.date } ?.lastOrNull { it.date != null } ?: return false - return lastSuccessfulSubmission.processedAt.isBefore( lastFollowUp.date.toInstant()) + // Follow-up after last successful submission by date or ... + return lastSuccessfulSubmission.processedAt.isBefore(lastFollowUp.date.toInstant()) || + // ... more follow-ups than on last successful submission + lastSuccessfulSubmission.followupCount < (request.content.followUps?.size ?: 0) } private fun hasSuccessfulInitialSubmission(patientPseudonym: PatientPseudonym): Boolean { diff --git a/src/main/resources/db/migration/mariadb/V0_16_1_1__FollowUpCount.sql b/src/main/resources/db/migration/mariadb/V0_16_1_1__FollowUpCount.sql new file mode 100644 index 0000000..a0a1fda --- /dev/null +++ b/src/main/resources/db/migration/mariadb/V0_16_1_1__FollowUpCount.sql @@ -0,0 +1,2 @@ +ALTER TABLE request ADD COLUMN followup_count int DEFAULT 0; +UPDATE request SET followup_count = -1; \ No newline at end of file diff --git a/src/main/resources/db/migration/postgresql/V0_16_1_1__FollowUpCount.sql b/src/main/resources/db/migration/postgresql/V0_16_1_1__FollowUpCount.sql new file mode 100644 index 0000000..a0a1fda --- /dev/null +++ b/src/main/resources/db/migration/postgresql/V0_16_1_1__FollowUpCount.sql @@ -0,0 +1,2 @@ +ALTER TABLE request ADD COLUMN followup_count int DEFAULT 0; +UPDATE request SET followup_count = -1; \ No newline at end of file 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 75f1b5d..0fea80a 100644 --- a/src/test/kotlin/dev/dnpm/etl/processor/services/RequestProcessorTest.kt +++ b/src/test/kotlin/dev/dnpm/etl/processor/services/RequestProcessorTest.kt @@ -20,7 +20,6 @@ package dev.dnpm.etl.processor.services -import com.fasterxml.jackson.databind.ObjectMapper import dev.dnpm.etl.processor.* import dev.dnpm.etl.processor.config.AppConfigProperties import dev.dnpm.etl.processor.consent.TtpConsentStatus @@ -35,7 +34,6 @@ import dev.dnpm.etl.processor.output.RestMtbFileSender import dev.dnpm.etl.processor.pseudonym.PseudonymizeService import dev.pcvolkmer.mv64e.mtb.* import org.assertj.core.api.Assertions.assertThat -import org.hl7.fhir.r4.model.Consent import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test @@ -812,6 +810,7 @@ class RequestProcessorTest { verify(requestService, times(1)).save(requestCaptor.capture()) assertThat(requestCaptor.firstValue).isNotNull assertThat(requestCaptor.firstValue.status).isEqualTo(RequestStatus.BLOCKED_INITIAL) + assertThat(requestCaptor.firstValue.followupCount).isEqualTo(0) verify(applicationEventPublisher, times(0)).publishEvent(any()) verify(sender, times(0)).send(any()) @@ -918,6 +917,7 @@ class RequestProcessorTest { assertThat(requestCaptor.firstValue).isNotNull assertThat(requestCaptor.firstValue.status).isEqualTo(RequestStatus.UNKNOWN) assertThat(requestCaptor.firstValue.submissionType).isEqualTo(SubmissionType.ADDITION) + assertThat(requestCaptor.firstValue.followupCount).isEqualTo(0) val eventCaptor = argumentCaptor() verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture()) @@ -1029,6 +1029,7 @@ class RequestProcessorTest { assertThat(requestCaptor.firstValue).isNotNull assertThat(requestCaptor.firstValue.status).isEqualTo(RequestStatus.UNKNOWN) assertThat(requestCaptor.firstValue.submissionType).isEqualTo(SubmissionType.ADDITION) + assertThat(requestCaptor.firstValue.followupCount).isEqualTo(0) val eventCaptor = argumentCaptor() verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture()) @@ -1127,6 +1128,7 @@ class RequestProcessorTest { assertThat(requestCaptor.firstValue).isNotNull assertThat(requestCaptor.firstValue.status).isEqualTo(RequestStatus.UNKNOWN) assertThat(requestCaptor.firstValue.submissionType).isEqualTo(SubmissionType.INITIAL) + assertThat(requestCaptor.firstValue.followupCount).isEqualTo(0) val eventCaptor = argumentCaptor() verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture()) @@ -1137,10 +1139,55 @@ class RequestProcessorTest { assertThat(sendRequestCaptor.firstValue.content.metadata.type).isEqualTo(MvhSubmissionType.INITIAL) } + } + + @Nested + inner class FollowUpSubmission { + + 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 + private lateinit var jsonMapper: JsonMapper + + @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 + this.jsonMapper = JsonMapper() + + requestProcessor = + RequestProcessor( + pseudonymizeService, + transformationService, + sender, + requestService, + jsonMapper, + applicationEventPublisher, + appConfigProperties, + consentProcessor, + ) + } + @Test fun testShouldSendFollowUpMtbFileIfUnacceptedErrorsAndAcceptedInitialFileWasSent() { - // One failed attempt and one successful but not accepted val lastRequests = listOf( Request( @@ -1247,6 +1294,7 @@ class RequestProcessorTest { assertThat(requestCaptor.firstValue).isNotNull assertThat(requestCaptor.firstValue.status).isEqualTo(RequestStatus.UNKNOWN) assertThat(requestCaptor.firstValue.submissionType).isEqualTo(SubmissionType.FOLLOWUP) + assertThat(requestCaptor.firstValue.followupCount).isEqualTo(1) val eventCaptor = argumentCaptor() verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture()) @@ -1261,7 +1309,6 @@ class RequestProcessorTest { @Test fun testShouldSendFollowUpMtbFileAfterAdditionWasSent() { - // One failed attempt and one successful but not accepted val lastRequests = listOf( Request( @@ -1368,6 +1415,7 @@ class RequestProcessorTest { assertThat(requestCaptor.firstValue).isNotNull assertThat(requestCaptor.firstValue.status).isEqualTo(RequestStatus.UNKNOWN) assertThat(requestCaptor.firstValue.submissionType).isEqualTo(SubmissionType.FOLLOWUP) + assertThat(requestCaptor.firstValue.followupCount).isEqualTo(1) val eventCaptor = argumentCaptor() verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture()) @@ -1377,233 +1425,695 @@ class RequestProcessorTest { verify(sender, times(1)).send(sendRequestCaptor.capture()) assertThat(sendRequestCaptor.firstValue.content.metadata.type).isEqualTo(MvhSubmissionType.FOLLOWUP) } - } - @Test - fun testShouldSendFollowUpMtbFileAfterInitialWasSent() { + @Test + fun testShouldSendFollowUpMtbFileAfterInitialWasSent() { - // 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, - Tan.empty(), - Instant.parse("2026-02-10T09:00:00Z"), - submissionAccepted = false, - ), - Request( - 2L, - randomRequestId(), - PatientPseudonym("TEST_12345678901"), - PatientId("P1"), - Fingerprint("initial"), - RequestType.MTB_FILE, - SubmissionType.INITIAL, - RequestStatus.WARNING, - Tan.empty(), - Instant.parse("2026-05-20T09:00:00Z"), - submissionAccepted = true, + val lastRequests = + listOf( + Request( + 1L, + randomRequestId(), + PatientPseudonym("TEST_12345678901"), + PatientId("P1"), + Fingerprint("initial"), + RequestType.MTB_FILE, + SubmissionType.INITIAL, + RequestStatus.ERROR, + Tan.empty(), + Instant.parse("2026-02-10T09:00:00Z"), + submissionAccepted = false, + ), + Request( + 2L, + randomRequestId(), + PatientPseudonym("TEST_12345678901"), + PatientId("P1"), + Fingerprint("initial"), + RequestType.MTB_FILE, + SubmissionType.INITIAL, + RequestStatus.WARNING, + Tan.empty(), + Instant.parse("2026-05-20T09:00:00Z"), + submissionAccepted = true, + ) ) - ) - doAnswer { lastRequests } - .whenever(requestService) - .allRequestsByPatientPseudonym(anyValueClass()) + doAnswer { lastRequests } + .whenever(requestService) + .allRequestsByPatientPseudonym(anyValueClass()) - doAnswer { it.arguments[0] as String } - .whenever(pseudonymizeService) - .patientPseudonym(anyValueClass()) + doAnswer { it.arguments[0] as String } + .whenever(pseudonymizeService) + .patientPseudonym(anyValueClass()) - doAnswer { it.arguments[0] }.whenever(transformationService).transform(any()) + doAnswer { it.arguments[0] }.whenever(transformationService).transform(any()) - doAnswer { MtbFileSender.Response(status = RequestStatus.SUCCESS) } - .whenever(sender) - .send(any()) + doAnswer { MtbFileSender.Response(status = RequestStatus.SUCCESS) } + .whenever(sender) + .send(any()) - whenever(consentProcessor.consentGatedCheckAndTryEmbedding(any())).thenReturn(true) + whenever(consentProcessor.consentGatedCheckAndTryEmbedding(any())).thenReturn(true) - requestProcessor = - RequestProcessor( - pseudonymizeService, - transformationService, - sender, - requestService, - jsonMapper, - applicationEventPublisher, - AppConfigProperties(postInitialSubmissionBlock = true), - consentProcessor, - ) + requestProcessor = + RequestProcessor( + pseudonymizeService, + transformationService, + sender, + requestService, + jsonMapper, + applicationEventPublisher, + AppConfigProperties(postInitialSubmissionBlock = true), + consentProcessor, + ) - val mtbFile = - Mtb.builder() - .patient(Patient.builder().id("123").build()) - .metadata(MvhMetadata.builder().type(MvhSubmissionType.INITIAL).build()) - .episodesOfCare( - listOf( - MtbEpisodeOfCare.builder() - .id("1") - .patient(Reference.builder().id("123").build()) - .period( - PeriodDate.builder() - .start(Date.from(Instant.parse("2026-01-20T00:00:00.00Z"))) - .build() - ) - .build() + val mtbFile = + Mtb.builder() + .patient(Patient.builder().id("123").build()) + .metadata(MvhMetadata.builder().type(MvhSubmissionType.INITIAL).build()) + .episodesOfCare( + listOf( + MtbEpisodeOfCare.builder() + .id("1") + .patient(Reference.builder().id("123").build()) + .period( + PeriodDate.builder() + .start(Date.from(Instant.parse("2026-01-20T00:00:00.00Z"))) + .build() + ) + .build() + ) ) - ) - .followUps( - listOf( - FollowUp.builder() - .patient(Reference.builder().id("123").build()) - .date(Date.from(Instant.parse("2026-05-21T00:00:00.00Z"))) - .lastContactDate(Date.from(Instant.parse("2026-05-21T00:00:00.00Z"))) - .build() + .followUps( + listOf( + FollowUp.builder() + .patient(Reference.builder().id("123").build()) + .date(Date.from(Instant.parse("2026-05-21T00:00:00.00Z"))) + .lastContactDate(Date.from(Instant.parse("2026-05-21T00:00:00.00Z"))) + .build() + ) ) - ) - .build() - - this.requestProcessor.processMtbFile(mtbFile) - - val requestCaptor = argumentCaptor() - verify(requestService, times(1)).save(requestCaptor.capture()) - assertThat(requestCaptor.firstValue).isNotNull - assertThat(requestCaptor.firstValue.status).isEqualTo(RequestStatus.UNKNOWN) - assertThat(requestCaptor.firstValue.submissionType).isEqualTo(SubmissionType.FOLLOWUP) - - val eventCaptor = argumentCaptor() - verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture()) - assertThat(eventCaptor.firstValue.status).isEqualTo(RequestStatus.SUCCESS) + .build() - val sendRequestCaptor = argumentCaptor() - verify(sender, times(1)).send(sendRequestCaptor.capture()) - assertThat(sendRequestCaptor.firstValue.content.metadata.type).isEqualTo(MvhSubmissionType.FOLLOWUP) - } + this.requestProcessor.processMtbFile(mtbFile) - @Test - fun testShouldSendAdditionMtbFileAfterFollowUpWasSent() { + val requestCaptor = argumentCaptor() + verify(requestService, times(1)).save(requestCaptor.capture()) + assertThat(requestCaptor.firstValue).isNotNull + assertThat(requestCaptor.firstValue.status).isEqualTo(RequestStatus.UNKNOWN) + assertThat(requestCaptor.firstValue.submissionType).isEqualTo(SubmissionType.FOLLOWUP) - // 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, - Tan.empty(), - Instant.parse("2026-02-10T09:00:00Z"), - submissionAccepted = false, - ), - Request( - 2L, - randomRequestId(), - PatientPseudonym("TEST_12345678901"), - PatientId("P1"), - Fingerprint("initial"), - RequestType.MTB_FILE, - SubmissionType.INITIAL, - RequestStatus.WARNING, - Tan.empty(), - Instant.parse("2026-02-11T09:00:00Z"), - submissionAccepted = true, - ), - Request( - 3L, - randomRequestId(), - PatientPseudonym("TEST_12345678901"), - PatientId("P1"), - Fingerprint("initial"), - RequestType.MTB_FILE, - SubmissionType.FOLLOWUP, - RequestStatus.WARNING, - Tan.empty(), - Instant.parse("2026-05-20T09:00:00Z"), - submissionAccepted = false, - ) - ) + val eventCaptor = argumentCaptor() + verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture()) + assertThat(eventCaptor.firstValue.status).isEqualTo(RequestStatus.SUCCESS) - doAnswer { lastRequests } - .whenever(requestService) - .allRequestsByPatientPseudonym(anyValueClass()) + val sendRequestCaptor = argumentCaptor() + verify(sender, times(1)).send(sendRequestCaptor.capture()) + assertThat(sendRequestCaptor.firstValue.content.metadata.type).isEqualTo(MvhSubmissionType.FOLLOWUP) + } - doAnswer { it.arguments[0] as String } - .whenever(pseudonymizeService) - .patientPseudonym(anyValueClass()) + @Test + fun testShouldSendAdditionMtbFileAfterFollowUpWasSent() { - doAnswer { it.arguments[0] }.whenever(transformationService).transform(any()) + val lastRequests = + listOf( + Request( + 1L, + randomRequestId(), + PatientPseudonym("TEST_12345678901"), + PatientId("P1"), + Fingerprint("initial"), + RequestType.MTB_FILE, + SubmissionType.INITIAL, + RequestStatus.ERROR, + Tan.empty(), + Instant.parse("2026-02-10T09:00:00Z"), + submissionAccepted = false, + followupCount = 0 + ), + Request( + 2L, + randomRequestId(), + PatientPseudonym("TEST_12345678901"), + PatientId("P1"), + Fingerprint("initial"), + RequestType.MTB_FILE, + SubmissionType.INITIAL, + RequestStatus.WARNING, + Tan.empty(), + Instant.parse("2026-02-11T09:00:00Z"), + submissionAccepted = true, + followupCount = 0 + ), + Request( + 3L, + randomRequestId(), + PatientPseudonym("TEST_12345678901"), + PatientId("P1"), + Fingerprint("initial"), + RequestType.MTB_FILE, + SubmissionType.FOLLOWUP, + RequestStatus.WARNING, + Tan.empty(), + Instant.parse("2026-05-20T09:00:00Z"), + submissionAccepted = false, + followupCount = 1 + ) + ) + + doAnswer { lastRequests } + .whenever(requestService) + .allRequestsByPatientPseudonym(anyValueClass()) + + doAnswer { it.arguments[0] as String } + .whenever(pseudonymizeService) + .patientPseudonym(anyValueClass()) + + doAnswer { it.arguments[0] }.whenever(transformationService).transform(any()) + + doAnswer { MtbFileSender.Response(status = RequestStatus.SUCCESS) } + .whenever(sender) + .send(any()) + + whenever(consentProcessor.consentGatedCheckAndTryEmbedding(any())).thenReturn(true) + + requestProcessor = + RequestProcessor( + pseudonymizeService, + transformationService, + sender, + requestService, + jsonMapper, + applicationEventPublisher, + AppConfigProperties(postInitialSubmissionBlock = true), + consentProcessor, + ) + + val mtbFile = + Mtb.builder() + .patient(Patient.builder().id("123").build()) + .metadata(MvhMetadata.builder().type(MvhSubmissionType.INITIAL).build()) + .episodesOfCare( + listOf( + MtbEpisodeOfCare.builder() + .id("1") + .patient(Reference.builder().id("123").build()) + .period( + PeriodDate.builder() + .start(Date.from(Instant.parse("2026-01-20T00:00:00.00Z"))) + .build() + ) + .build() + ) + ) + .followUps( + listOf( + FollowUp.builder() + .patient(Reference.builder().id("123").build()) + .date(Date.from(Instant.parse("2026-05-20T00:00:00.00Z"))) + .lastContactDate(Date.from(Instant.parse("2026-05-20T00:00:00.00Z"))) + .build() + ) + ) + .build() + + this.requestProcessor.processMtbFile(mtbFile) + + val requestCaptor = argumentCaptor() + verify(requestService, times(1)).save(requestCaptor.capture()) + assertThat(requestCaptor.firstValue).isNotNull + assertThat(requestCaptor.firstValue.status).isEqualTo(RequestStatus.UNKNOWN) + assertThat(requestCaptor.firstValue.submissionType).isEqualTo(SubmissionType.ADDITION) + assertThat(requestCaptor.firstValue.followupCount).isEqualTo(1) + + val eventCaptor = argumentCaptor() + verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture()) + assertThat(eventCaptor.firstValue.status).isEqualTo(RequestStatus.SUCCESS) + + val sendRequestCaptor = argumentCaptor() + verify(sender, times(1)).send(sendRequestCaptor.capture()) + assertThat(sendRequestCaptor.firstValue.content.metadata.type).isEqualTo(MvhSubmissionType.ADDITION) + } + + @Test + fun testShouldSendAdditionalFollowUpMtbFileAfterAdditionWasSentOnSameDay() { + + val lastRequests = + listOf( + Request( + 1L, + randomRequestId(), + PatientPseudonym("TEST_12345678901"), + PatientId("P1"), + Fingerprint("initial"), + RequestType.MTB_FILE, + SubmissionType.INITIAL, + RequestStatus.ERROR, + Tan.empty(), + Instant.parse("2026-02-10T09:00:00Z"), + submissionAccepted = false, + followupCount = 0 + ), + Request( + 2L, + randomRequestId(), + PatientPseudonym("TEST_12345678901"), + PatientId("P1"), + Fingerprint("initial"), + RequestType.MTB_FILE, + SubmissionType.INITIAL, + RequestStatus.WARNING, + Tan.empty(), + Instant.parse("2026-02-11T09:00:00Z"), + submissionAccepted = true, + followupCount = 0 + ), + Request( + 3L, + randomRequestId(), + PatientPseudonym("TEST_12345678901"), + PatientId("P1"), + Fingerprint("initial"), + RequestType.MTB_FILE, + SubmissionType.FOLLOWUP, + RequestStatus.WARNING, + Tan.empty(), + Instant.parse("2026-05-20T09:00:00Z"), + submissionAccepted = false, + followupCount = 1 + ), + Request( + 4L, + randomRequestId(), + PatientPseudonym("TEST_12345678901"), + PatientId("P1"), + Fingerprint("initial"), + RequestType.MTB_FILE, + SubmissionType.ADDITION, + RequestStatus.WARNING, + Tan.empty(), + Instant.parse("2026-06-11T05:41:00Z"), + submissionAccepted = false, + followupCount = 1 + ) + ) + + doAnswer { lastRequests } + .whenever(requestService) + .allRequestsByPatientPseudonym(anyValueClass()) + + doAnswer { it.arguments[0] as String } + .whenever(pseudonymizeService) + .patientPseudonym(anyValueClass()) + + doAnswer { it.arguments[0] }.whenever(transformationService).transform(any()) + + doAnswer { MtbFileSender.Response(status = RequestStatus.SUCCESS) } + .whenever(sender) + .send(any()) + + whenever(consentProcessor.consentGatedCheckAndTryEmbedding(any())).thenReturn(true) + + requestProcessor = + RequestProcessor( + pseudonymizeService, + transformationService, + sender, + requestService, + jsonMapper, + applicationEventPublisher, + AppConfigProperties(postInitialSubmissionBlock = true), + consentProcessor, + ) + + val mtbFile = + Mtb.builder() + .patient(Patient.builder().id("123").build()) + .metadata(MvhMetadata.builder().type(MvhSubmissionType.INITIAL).build()) + .episodesOfCare( + listOf( + MtbEpisodeOfCare.builder() + .id("1") + .patient(Reference.builder().id("123").build()) + .period( + PeriodDate.builder() + .start(Date.from(Instant.parse("2026-01-20T00:00:00.00Z"))) + .build() + ) + .build() + ) + ) + .followUps( + listOf( + FollowUp.builder() + .patient(Reference.builder().id("123").build()) + .date(Date.from(Instant.parse("2026-05-20T00:00:00.00Z"))) + .lastContactDate(Date.from(Instant.parse("2026-05-20T00:00:00.00Z"))) + .build(), + FollowUp.builder() + .patient(Reference.builder().id("123").build()) + .date(Date.from(Instant.parse("2026-06-11T00:00:00.00Z"))) + .lastContactDate(Date.from(Instant.parse("2026-06-11T00:00:00.00Z"))) + .build() + ) + ) + .build() + + this.requestProcessor.processMtbFile(mtbFile) + + val requestCaptor = argumentCaptor() + verify(requestService, times(1)).save(requestCaptor.capture()) + assertThat(requestCaptor.firstValue).isNotNull + assertThat(requestCaptor.firstValue.status).isEqualTo(RequestStatus.UNKNOWN) + assertThat(requestCaptor.firstValue.submissionType).isEqualTo(SubmissionType.FOLLOWUP) + assertThat(requestCaptor.firstValue.followupCount).isEqualTo(2) + + val eventCaptor = argumentCaptor() + verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture()) + assertThat(eventCaptor.firstValue.status).isEqualTo(RequestStatus.SUCCESS) + + val sendRequestCaptor = argumentCaptor() + verify(sender, times(1)).send(sendRequestCaptor.capture()) + assertThat(sendRequestCaptor.firstValue.content.metadata.type).isEqualTo(MvhSubmissionType.FOLLOWUP) + } + + @Test + fun testShouldSendAdditionMtbFileWithoutNewHigherFollowUpsCount() { + + val lastRequests = + listOf( + Request( + 1L, + randomRequestId(), + PatientPseudonym("TEST_12345678901"), + PatientId("P1"), + Fingerprint("initial"), + RequestType.MTB_FILE, + SubmissionType.INITIAL, + RequestStatus.ERROR, + Tan.empty(), + Instant.parse("2026-02-10T09:00:00Z"), + submissionAccepted = false, + followupCount = -1 + ), + Request( + 2L, + randomRequestId(), + PatientPseudonym("TEST_12345678901"), + PatientId("P1"), + Fingerprint("initial"), + RequestType.MTB_FILE, + SubmissionType.INITIAL, + RequestStatus.WARNING, + Tan.empty(), + Instant.parse("2026-02-11T09:00:00Z"), + submissionAccepted = true, + followupCount = -1 + ), + Request( + 3L, + randomRequestId(), + PatientPseudonym("TEST_12345678901"), + PatientId("P1"), + Fingerprint("initial"), + RequestType.MTB_FILE, + SubmissionType.FOLLOWUP, + RequestStatus.WARNING, + Tan.empty(), + Instant.parse("2026-06-11T05:41:00Z"), + submissionAccepted = false, + followupCount = 1 + ) + ) + + doAnswer { lastRequests } + .whenever(requestService) + .allRequestsByPatientPseudonym(anyValueClass()) + + doAnswer { it.arguments[0] as String } + .whenever(pseudonymizeService) + .patientPseudonym(anyValueClass()) + + doAnswer { it.arguments[0] }.whenever(transformationService).transform(any()) + + doAnswer { MtbFileSender.Response(status = RequestStatus.SUCCESS) } + .whenever(sender) + .send(any()) + + whenever(consentProcessor.consentGatedCheckAndTryEmbedding(any())).thenReturn(true) + + requestProcessor = + RequestProcessor( + pseudonymizeService, + transformationService, + sender, + requestService, + jsonMapper, + applicationEventPublisher, + AppConfigProperties(postInitialSubmissionBlock = true), + consentProcessor, + ) + + val mtbFile = + Mtb.builder() + .patient(Patient.builder().id("123").build()) + .metadata(MvhMetadata.builder().type(MvhSubmissionType.INITIAL).build()) + .episodesOfCare( + listOf( + MtbEpisodeOfCare.builder() + .id("1") + .patient(Reference.builder().id("123").build()) + .period( + PeriodDate.builder() + .start(Date.from(Instant.parse("2026-01-20T00:00:00.00Z"))) + .build() + ) + .build() + ) + ) + .followUps( + listOf( + FollowUp.builder() + .patient(Reference.builder().id("123").build()) + .date(Date.from(Instant.parse("2026-06-11T00:00:00.00Z"))) + .lastContactDate(Date.from(Instant.parse("2026-06-11T00:00:00.00Z"))) + .build() + ) + ) + .build() - doAnswer { MtbFileSender.Response(status = RequestStatus.SUCCESS) } - .whenever(sender) - .send(any()) + this.requestProcessor.processMtbFile(mtbFile) - whenever(consentProcessor.consentGatedCheckAndTryEmbedding(any())).thenReturn(true) + val requestCaptor = argumentCaptor() + verify(requestService, times(1)).save(requestCaptor.capture()) + assertThat(requestCaptor.firstValue).isNotNull + assertThat(requestCaptor.firstValue.status).isEqualTo(RequestStatus.UNKNOWN) + assertThat(requestCaptor.firstValue.submissionType).isEqualTo(SubmissionType.ADDITION) + assertThat(requestCaptor.firstValue.followupCount).isEqualTo(1) - requestProcessor = - RequestProcessor( - pseudonymizeService, - transformationService, - sender, - requestService, - jsonMapper, - applicationEventPublisher, - AppConfigProperties(postInitialSubmissionBlock = true), - consentProcessor, - ) + val eventCaptor = argumentCaptor() + verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture()) + assertThat(eventCaptor.firstValue.status).isEqualTo(RequestStatus.SUCCESS) - val mtbFile = - Mtb.builder() - .patient(Patient.builder().id("123").build()) - .metadata(MvhMetadata.builder().type(MvhSubmissionType.INITIAL).build()) - .episodesOfCare( - listOf( - MtbEpisodeOfCare.builder() - .id("1") - .patient(Reference.builder().id("123").build()) - .period( - PeriodDate.builder() - .start(Date.from(Instant.parse("2026-01-20T00:00:00.00Z"))) - .build() - ) - .build() + val sendRequestCaptor = argumentCaptor() + verify(sender, times(1)).send(sendRequestCaptor.capture()) + assertThat(sendRequestCaptor.firstValue.content.metadata.type).isEqualTo(MvhSubmissionType.ADDITION) + } + + @Test + fun testShouldSendInitialMtbFileContainingFollowUpsWithoutSuccessfulInitialSubmission() { + + val lastRequests = + listOf( + Request( + 1L, + randomRequestId(), + PatientPseudonym("TEST_12345678901"), + PatientId("P1"), + Fingerprint("initial"), + RequestType.MTB_FILE, + SubmissionType.INITIAL, + RequestStatus.ERROR, + Tan.empty(), + Instant.parse("2026-02-10T09:00:00Z"), + submissionAccepted = false, + followupCount = 0 ) ) - .followUps( - listOf( - FollowUp.builder() - .patient(Reference.builder().id("123").build()) - .date(Date.from(Instant.parse("2026-05-20T00:00:00.00Z"))) - .lastContactDate(Date.from(Instant.parse("2026-05-20T00:00:00.00Z"))) - .build() + + doAnswer { lastRequests } + .whenever(requestService) + .allRequestsByPatientPseudonym(anyValueClass()) + + doAnswer { it.arguments[0] as String } + .whenever(pseudonymizeService) + .patientPseudonym(anyValueClass()) + + doAnswer { it.arguments[0] }.whenever(transformationService).transform(any()) + + doAnswer { MtbFileSender.Response(status = RequestStatus.SUCCESS) } + .whenever(sender) + .send(any()) + + whenever(consentProcessor.consentGatedCheckAndTryEmbedding(any())).thenReturn(true) + + requestProcessor = + RequestProcessor( + pseudonymizeService, + transformationService, + sender, + requestService, + jsonMapper, + applicationEventPublisher, + AppConfigProperties(postInitialSubmissionBlock = true), + consentProcessor, + ) + + val mtbFile = + Mtb.builder() + .patient(Patient.builder().id("123").build()) + .metadata(MvhMetadata.builder().type(MvhSubmissionType.INITIAL).build()) + .episodesOfCare( + listOf( + MtbEpisodeOfCare.builder() + .id("1") + .patient(Reference.builder().id("123").build()) + .period( + PeriodDate.builder() + .start(Date.from(Instant.parse("2026-01-20T00:00:00.00Z"))) + .build() + ) + .build() + ) + ) + .followUps( + listOf( + FollowUp.builder() + .patient(Reference.builder().id("123").build()) + .date(Date.from(Instant.parse("2026-06-11T00:00:00.00Z"))) + .lastContactDate(Date.from(Instant.parse("2026-06-11T00:00:00.00Z"))) + .build() + ) ) + .build() + + this.requestProcessor.processMtbFile(mtbFile) + + val requestCaptor = argumentCaptor() + verify(requestService, times(1)).save(requestCaptor.capture()) + assertThat(requestCaptor.firstValue).isNotNull + assertThat(requestCaptor.firstValue.status).isEqualTo(RequestStatus.UNKNOWN) + assertThat(requestCaptor.firstValue.submissionType).isEqualTo(SubmissionType.INITIAL) + assertThat(requestCaptor.firstValue.followupCount).isEqualTo(1) + + val eventCaptor = argumentCaptor() + verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture()) + assertThat(eventCaptor.firstValue.status).isEqualTo(RequestStatus.SUCCESS) + + val sendRequestCaptor = argumentCaptor() + verify(sender, times(1)).send(sendRequestCaptor.capture()) + assertThat(sendRequestCaptor.firstValue.content.metadata.type).isEqualTo(MvhSubmissionType.INITIAL) + } + + @Test + fun testShouldSendAdditionMtbFileWithoutFollowUpsAndInitialNegativeCount() { + + val lastRequests = + listOf( + Request( + 1L, + randomRequestId(), + PatientPseudonym("TEST_12345678901"), + PatientId("P1"), + Fingerprint("initial"), + RequestType.MTB_FILE, + SubmissionType.INITIAL, + RequestStatus.ERROR, + Tan.empty(), + Instant.parse("2026-02-10T09:00:00Z"), + submissionAccepted = false, + followupCount = -1 + ), + Request( + 2L, + randomRequestId(), + PatientPseudonym("TEST_12345678901"), + PatientId("P1"), + Fingerprint("initial"), + RequestType.MTB_FILE, + SubmissionType.INITIAL, + RequestStatus.WARNING, + Tan.empty(), + Instant.parse("2026-02-11T09:00:00Z"), + submissionAccepted = true, + followupCount = -1 + ), ) - .build() - this.requestProcessor.processMtbFile(mtbFile) + doAnswer { lastRequests } + .whenever(requestService) + .allRequestsByPatientPseudonym(anyValueClass()) - val requestCaptor = argumentCaptor() - verify(requestService, times(1)).save(requestCaptor.capture()) - assertThat(requestCaptor.firstValue).isNotNull - assertThat(requestCaptor.firstValue.status).isEqualTo(RequestStatus.UNKNOWN) - assertThat(requestCaptor.firstValue.submissionType).isEqualTo(SubmissionType.ADDITION) + doAnswer { it.arguments[0] as String } + .whenever(pseudonymizeService) + .patientPseudonym(anyValueClass()) - val eventCaptor = argumentCaptor() - verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture()) - assertThat(eventCaptor.firstValue.status).isEqualTo(RequestStatus.SUCCESS) + doAnswer { it.arguments[0] }.whenever(transformationService).transform(any()) + + doAnswer { MtbFileSender.Response(status = RequestStatus.SUCCESS) } + .whenever(sender) + .send(any()) + + whenever(consentProcessor.consentGatedCheckAndTryEmbedding(any())).thenReturn(true) + + requestProcessor = + RequestProcessor( + pseudonymizeService, + transformationService, + sender, + requestService, + jsonMapper, + applicationEventPublisher, + AppConfigProperties(postInitialSubmissionBlock = true), + consentProcessor, + ) + + val mtbFile = + Mtb.builder() + .patient(Patient.builder().id("123").build()) + .metadata(MvhMetadata.builder().type(MvhSubmissionType.INITIAL).build()) + .episodesOfCare( + listOf( + MtbEpisodeOfCare.builder() + .id("1") + .patient(Reference.builder().id("123").build()) + .period( + PeriodDate.builder() + .start(Date.from(Instant.parse("2026-01-20T00:00:00.00Z"))) + .build() + ) + .build() + ) + ) + .build() - val sendRequestCaptor = argumentCaptor() - verify(sender, times(1)).send(sendRequestCaptor.capture()) - assertThat(sendRequestCaptor.firstValue.content.metadata.type).isEqualTo(MvhSubmissionType.ADDITION) + this.requestProcessor.processMtbFile(mtbFile) + + val requestCaptor = argumentCaptor() + verify(requestService, times(1)).save(requestCaptor.capture()) + assertThat(requestCaptor.firstValue).isNotNull + assertThat(requestCaptor.firstValue.status).isEqualTo(RequestStatus.UNKNOWN) + assertThat(requestCaptor.firstValue.submissionType).isEqualTo(SubmissionType.ADDITION) + assertThat(requestCaptor.firstValue.followupCount).isEqualTo(0) + + val eventCaptor = argumentCaptor() + verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture()) + assertThat(eventCaptor.firstValue.status).isEqualTo(RequestStatus.SUCCESS) + + val sendRequestCaptor = argumentCaptor() + verify(sender, times(1)).send(sendRequestCaptor.capture()) + assertThat(sendRequestCaptor.firstValue.content.metadata.type).isEqualTo(MvhSubmissionType.ADDITION) + } } @Test -- cgit v1.2.3