summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul-Christian Volkmer2026-06-11 08:11:11 +0200
committerGitHub2026-06-11 06:11:11 +0000
commit7298078077d27380255ad5eb42a30876a4babede (patch)
tree015f86e027a5ba8098cb52234965cb59f097d902
parent20d1182da9d8f9900d5580b8f4525847e03cbaba (diff)
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.
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/monitoring/Request.kt1
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt39
-rw-r--r--src/main/resources/db/migration/mariadb/V0_16_1_1__FollowUpCount.sql2
-rw-r--r--src/main/resources/db/migration/postgresql/V0_16_1_1__FollowUpCount.sql2
-rw-r--r--src/test/kotlin/dev/dnpm/etl/processor/services/RequestProcessorTest.kt900
5 files changed, 732 insertions, 212 deletions
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<DnpmV2MtbFileRequest>())
@@ -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<ResponseEvent>()
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<ResponseEvent>()
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<ResponseEvent>()
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<ResponseEvent>()
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<ResponseEvent>()
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<Mtb>())
+ doAnswer { it.arguments[0] }.whenever(transformationService).transform(any<Mtb>())
- doAnswer { MtbFileSender.Response(status = RequestStatus.SUCCESS) }
- .whenever(sender)
- .send(any<DnpmV2MtbFileRequest>())
+ doAnswer { MtbFileSender.Response(status = RequestStatus.SUCCESS) }
+ .whenever(sender)
+ .send(any<DnpmV2MtbFileRequest>())
- 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()
+ )
+ )
+ .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.UNKNOWN)
+ assertThat(requestCaptor.firstValue.submissionType).isEqualTo(SubmissionType.FOLLOWUP)
+
+ val eventCaptor = argumentCaptor<ResponseEvent>()
+ verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture())
+ assertThat(eventCaptor.firstValue.status).isEqualTo(RequestStatus.SUCCESS)
+
+ val sendRequestCaptor = argumentCaptor<DnpmV2MtbFileRequest>()
+ verify(sender, times(1)).send(sendRequestCaptor.capture())
+ assertThat(sendRequestCaptor.firstValue.content.metadata.type).isEqualTo(MvhSubmissionType.FOLLOWUP)
+ }
+
+ @Test
+ fun testShouldSendAdditionMtbFileAfterFollowUpWasSent() {
+
+ 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
)
)
- .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()
+
+ 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>())
+
+ doAnswer { MtbFileSender.Response(status = RequestStatus.SUCCESS) }
+ .whenever(sender)
+ .send(any<DnpmV2MtbFileRequest>())
+
+ 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<Request>()
+ 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<ResponseEvent>()
+ verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture())
+ assertThat(eventCaptor.firstValue.status).isEqualTo(RequestStatus.SUCCESS)
+
+ val sendRequestCaptor = argumentCaptor<DnpmV2MtbFileRequest>()
+ 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
)
)
- .build()
- this.requestProcessor.processMtbFile(mtbFile)
+ doAnswer { lastRequests }
+ .whenever(requestService)
+ .allRequestsByPatientPseudonym(anyValueClass())
- val requestCaptor = argumentCaptor<Request>()
- 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)
+ doAnswer { it.arguments[0] as String }
+ .whenever(pseudonymizeService)
+ .patientPseudonym(anyValueClass())
- val eventCaptor = argumentCaptor<ResponseEvent>()
- verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture())
- assertThat(eventCaptor.firstValue.status).isEqualTo(RequestStatus.SUCCESS)
+ doAnswer { it.arguments[0] }.whenever(transformationService).transform(any<Mtb>())
- val sendRequestCaptor = argumentCaptor<DnpmV2MtbFileRequest>()
- verify(sender, times(1)).send(sendRequestCaptor.capture())
- assertThat(sendRequestCaptor.firstValue.content.metadata.type).isEqualTo(MvhSubmissionType.FOLLOWUP)
- }
+ doAnswer { MtbFileSender.Response(status = RequestStatus.SUCCESS) }
+ .whenever(sender)
+ .send(any<DnpmV2MtbFileRequest>())
- @Test
- fun testShouldSendAdditionMtbFileAfterFollowUpWasSent() {
+ whenever(consentProcessor.consentGatedCheckAndTryEmbedding(any())).thenReturn(true)
- // 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,
+ requestProcessor =
+ RequestProcessor(
+ pseudonymizeService,
+ transformationService,
+ sender,
+ requestService,
+ jsonMapper,
+ applicationEventPublisher,
+ AppConfigProperties(postInitialSubmissionBlock = true),
+ consentProcessor,
)
- )
- doAnswer { lastRequests }
- .whenever(requestService)
- .allRequestsByPatientPseudonym(anyValueClass())
+ 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()
- doAnswer { it.arguments[0] as String }
- .whenever(pseudonymizeService)
- .patientPseudonym(anyValueClass())
+ this.requestProcessor.processMtbFile(mtbFile)
- doAnswer { it.arguments[0] }.whenever(transformationService).transform(any<Mtb>())
+ val requestCaptor = argumentCaptor<Request>()
+ 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)
- doAnswer { MtbFileSender.Response(status = RequestStatus.SUCCESS) }
- .whenever(sender)
- .send(any<DnpmV2MtbFileRequest>())
+ val eventCaptor = argumentCaptor<ResponseEvent>()
+ verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture())
+ assertThat(eventCaptor.firstValue.status).isEqualTo(RequestStatus.SUCCESS)
- whenever(consentProcessor.consentGatedCheckAndTryEmbedding(any())).thenReturn(true)
+ val sendRequestCaptor = argumentCaptor<DnpmV2MtbFileRequest>()
+ verify(sender, times(1)).send(sendRequestCaptor.capture())
+ assertThat(sendRequestCaptor.firstValue.content.metadata.type).isEqualTo(MvhSubmissionType.FOLLOWUP)
+ }
- requestProcessor =
- RequestProcessor(
- pseudonymizeService,
- transformationService,
- sender,
- requestService,
- jsonMapper,
- applicationEventPublisher,
- AppConfigProperties(postInitialSubmissionBlock = true),
- consentProcessor,
- )
+ @Test
+ fun testShouldSendAdditionMtbFileWithoutNewHigherFollowUpsCount() {
- 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 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
)
)
- .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<Mtb>())
+
+ doAnswer { MtbFileSender.Response(status = RequestStatus.SUCCESS) }
+ .whenever(sender)
+ .send(any<DnpmV2MtbFileRequest>())
+
+ 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<Request>()
+ 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<ResponseEvent>()
+ verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture())
+ assertThat(eventCaptor.firstValue.status).isEqualTo(RequestStatus.SUCCESS)
+
+ val sendRequestCaptor = argumentCaptor<DnpmV2MtbFileRequest>()
+ 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
)
)
- .build()
- this.requestProcessor.processMtbFile(mtbFile)
+ doAnswer { lastRequests }
+ .whenever(requestService)
+ .allRequestsByPatientPseudonym(anyValueClass())
- val requestCaptor = argumentCaptor<Request>()
- 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<ResponseEvent>()
- verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture())
- assertThat(eventCaptor.firstValue.status).isEqualTo(RequestStatus.SUCCESS)
+ doAnswer { it.arguments[0] }.whenever(transformationService).transform(any<Mtb>())
+
+ doAnswer { MtbFileSender.Response(status = RequestStatus.SUCCESS) }
+ .whenever(sender)
+ .send(any<DnpmV2MtbFileRequest>())
+
+ 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<Request>()
+ 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<ResponseEvent>()
+ verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture())
+ assertThat(eventCaptor.firstValue.status).isEqualTo(RequestStatus.SUCCESS)
- val sendRequestCaptor = argumentCaptor<DnpmV2MtbFileRequest>()
- verify(sender, times(1)).send(sendRequestCaptor.capture())
- assertThat(sendRequestCaptor.firstValue.content.metadata.type).isEqualTo(MvhSubmissionType.ADDITION)
+ val sendRequestCaptor = argumentCaptor<DnpmV2MtbFileRequest>()
+ 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
+ ),
+ )
+
+ 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>())
+
+ doAnswer { MtbFileSender.Response(status = RequestStatus.SUCCESS) }
+ .whenever(sender)
+ .send(any<DnpmV2MtbFileRequest>())
+
+ 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()
+
+ 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.UNKNOWN)
+ assertThat(requestCaptor.firstValue.submissionType).isEqualTo(SubmissionType.ADDITION)
+ assertThat(requestCaptor.firstValue.followupCount).isEqualTo(0)
+
+ val eventCaptor = argumentCaptor<ResponseEvent>()
+ verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture())
+ assertThat(eventCaptor.firstValue.status).isEqualTo(RequestStatus.SUCCESS)
+
+ val sendRequestCaptor = argumentCaptor<DnpmV2MtbFileRequest>()
+ verify(sender, times(1)).send(sendRequestCaptor.capture())
+ assertThat(sendRequestCaptor.firstValue.content.metadata.type).isEqualTo(MvhSubmissionType.ADDITION)
+ }
}
@Test