diff --git a/build-logic/build_properties.gradle.kts b/build-logic/build_properties.gradle.kts index 50da31676a..56e6ad7b42 100644 --- a/build-logic/build_properties.gradle.kts +++ b/build-logic/build_properties.gradle.kts @@ -16,8 +16,9 @@ extra.apply { * Dev version >= 2024.2.1 is required for receiving biometric sdk age restrictions * Dev version >= 2024.2.2 is required for float quality thresholds * Dev version >= 2024.3.0 is required to receive configuration ID + * Dev version >= 2025.2.0 is required to support enrolment record updates */ - set("VERSION_NAME", "2025.1.0") + set("VERSION_NAME", "2025.2.0") /** * Build type. The version code describes which build type was used for the build. diff --git a/face/capture/src/main/java/com/simprints/face/capture/FaceCaptureResult.kt b/face/capture/src/main/java/com/simprints/face/capture/FaceCaptureResult.kt index 5b49d08657..a5a029f611 100644 --- a/face/capture/src/main/java/com/simprints/face/capture/FaceCaptureResult.kt +++ b/face/capture/src/main/java/com/simprints/face/capture/FaceCaptureResult.kt @@ -6,6 +6,7 @@ import java.io.Serializable @Keep data class FaceCaptureResult( + val referenceId: String, val results: List, ) : Serializable { @Keep diff --git a/face/capture/src/main/java/com/simprints/face/capture/screens/FaceCaptureViewModel.kt b/face/capture/src/main/java/com/simprints/face/capture/screens/FaceCaptureViewModel.kt index 23e1082096..178a7b573f 100644 --- a/face/capture/src/main/java/com/simprints/face/capture/screens/FaceCaptureViewModel.kt +++ b/face/capture/src/main/java/com/simprints/face/capture/screens/FaceCaptureViewModel.kt @@ -36,6 +36,7 @@ import kotlinx.coroutines.flow.last import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import java.util.UUID import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject @@ -184,8 +185,10 @@ internal class FaceCaptureViewModel @Inject constructor( ), ) } + val referenceId = UUID.randomUUID().toString() + eventReporter.addBiometricReferenceCreationEvents(referenceId, items.mapNotNull { it.captureEventId }) - _finishFlowEvent.send(FaceCaptureResult(items)) + _finishFlowEvent.send(FaceCaptureResult(referenceId, items)) } } diff --git a/face/capture/src/main/java/com/simprints/face/capture/usecases/SimpleCaptureEventReporter.kt b/face/capture/src/main/java/com/simprints/face/capture/usecases/SimpleCaptureEventReporter.kt index 806d586ee8..ab3c99bed6 100644 --- a/face/capture/src/main/java/com/simprints/face/capture/usecases/SimpleCaptureEventReporter.kt +++ b/face/capture/src/main/java/com/simprints/face/capture/usecases/SimpleCaptureEventReporter.kt @@ -5,6 +5,7 @@ import com.simprints.core.tools.time.TimeHelper import com.simprints.core.tools.time.Timestamp import com.simprints.core.tools.utils.EncodingUtils import com.simprints.face.capture.models.FaceDetection +import com.simprints.infra.events.event.domain.models.BiometricReferenceCreationEvent import com.simprints.infra.events.event.domain.models.face.FaceCaptureBiometricsEvent import com.simprints.infra.events.event.domain.models.face.FaceCaptureConfirmationEvent import com.simprints.infra.events.event.domain.models.face.FaceCaptureConfirmationEvent.FaceCaptureConfirmationPayload.Result @@ -100,4 +101,18 @@ internal class SimpleCaptureEventReporter @Inject constructor( it.format, ) }!! + + fun addBiometricReferenceCreationEvents( + referenceId: String, + captureIds: List, + ) = sessionCoroutineScope.launch { + eventRepository.addOrUpdateEvent( + BiometricReferenceCreationEvent( + startTime = timeHelper.now(), + referenceId = referenceId, + modality = BiometricReferenceCreationEvent.BiometricReferenceModality.FACE, + captureIds = captureIds, + ), + ) + } } diff --git a/face/capture/src/test/java/com/simprints/face/capture/screens/FaceCaptureViewModelTest.kt b/face/capture/src/test/java/com/simprints/face/capture/screens/FaceCaptureViewModelTest.kt index 0e042b5535..65d011f544 100644 --- a/face/capture/src/test/java/com/simprints/face/capture/screens/FaceCaptureViewModelTest.kt +++ b/face/capture/src/test/java/com/simprints/face/capture/screens/FaceCaptureViewModelTest.kt @@ -122,6 +122,15 @@ class FaceCaptureViewModelTest { coVerify(atLeast = 1) { faceImageUseCase.invoke(any(), any()) } } + @Test + fun `Save biometric reference creation when flow finishes`() { + viewModel.captureFinished(faceDetections) + viewModel.flowFinished() + coVerify(atLeast = 1) { + eventReporter.addBiometricReferenceCreationEvents(any(), any()) + } + } + @Test fun `Recapture requests clears capture list`() { viewModel.captureFinished(faceDetections) diff --git a/face/capture/src/test/java/com/simprints/face/capture/usecases/SimpleCaptureEventReporterTest.kt b/face/capture/src/test/java/com/simprints/face/capture/usecases/SimpleCaptureEventReporterTest.kt index fa8d20f3b0..4465a5f930 100644 --- a/face/capture/src/test/java/com/simprints/face/capture/usecases/SimpleCaptureEventReporterTest.kt +++ b/face/capture/src/test/java/com/simprints/face/capture/usecases/SimpleCaptureEventReporterTest.kt @@ -7,6 +7,8 @@ import com.simprints.core.tools.time.Timestamp import com.simprints.core.tools.utils.EncodingUtils import com.simprints.face.capture.models.FaceDetection import com.simprints.face.infra.basebiosdk.detection.Face +import com.simprints.infra.events.event.domain.models.BiometricReferenceCreationEvent +import com.simprints.infra.events.event.domain.models.BiometricReferenceCreationEvent.BiometricReferenceCreationPayload import com.simprints.infra.events.event.domain.models.face.FaceCaptureBiometricsEvent import com.simprints.infra.events.event.domain.models.face.FaceCaptureConfirmationEvent import com.simprints.infra.events.event.domain.models.face.FaceCaptureEvent @@ -73,8 +75,9 @@ class SimpleCaptureEventReporterTest { eventRepository.addOrUpdateEvent( withArg { assertThat(it).isInstanceOf(FaceCaptureConfirmationEvent::class.java) - assertThat((it.payload as FaceCaptureConfirmationEvent.FaceCaptureConfirmationPayload).result) - .isEqualTo(FaceCaptureConfirmationEvent.FaceCaptureConfirmationPayload.Result.CONTINUE) + assertThat((it.payload as FaceCaptureConfirmationEvent.FaceCaptureConfirmationPayload).result).isEqualTo( + FaceCaptureConfirmationEvent.FaceCaptureConfirmationPayload.Result.CONTINUE, + ) }, ) } @@ -87,8 +90,9 @@ class SimpleCaptureEventReporterTest { eventRepository.addOrUpdateEvent( withArg { assertThat(it).isInstanceOf(FaceCaptureConfirmationEvent::class.java) - assertThat((it.payload as FaceCaptureConfirmationEvent.FaceCaptureConfirmationPayload).result) - .isEqualTo(FaceCaptureConfirmationEvent.FaceCaptureConfirmationPayload.Result.RECAPTURE) + assertThat((it.payload as FaceCaptureConfirmationEvent.FaceCaptureConfirmationPayload).result).isEqualTo( + FaceCaptureConfirmationEvent.FaceCaptureConfirmationPayload.Result.RECAPTURE, + ) }, ) } @@ -182,6 +186,20 @@ class SimpleCaptureEventReporterTest { } } + @Test + fun `Adds reference creation event`() = runTest { + reporter.addBiometricReferenceCreationEvents("id", listOf("id1", "id2", "id3")) + coVerify { + eventRepository.addOrUpdateEvent( + withArg { + assertThat(it).isInstanceOf(BiometricReferenceCreationEvent::class.java) + assertThat((it.payload as BiometricReferenceCreationPayload).modality) + .isEqualTo(BiometricReferenceCreationEvent.BiometricReferenceModality.FACE) + }, + ) + } + } + private fun getDetection(status: FaceDetection.Status) = FaceDetection(mockk(), getFace(), status, mockk(), Timestamp(1L), false, "id", Timestamp(1L)) diff --git a/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/EnrolLastBiometricParams.kt b/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/EnrolLastBiometricParams.kt index 0e3a6f78e2..12df3a0b14 100644 --- a/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/EnrolLastBiometricParams.kt +++ b/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/EnrolLastBiometricParams.kt @@ -39,12 +39,14 @@ sealed class EnrolLastBiometricStepResult : Parcelable { @Keep @Parcelize data class FingerprintCaptureResult( + val referenceId: String, val results: List, ) : EnrolLastBiometricStepResult() @Keep @Parcelize data class FaceCaptureResult( + val referenceId: String, val results: List, ) : EnrolLastBiometricStepResult() } diff --git a/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/screen/EnrolLastBiometricViewModel.kt b/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/screen/EnrolLastBiometricViewModel.kt index 0e26782c12..c34ca748df 100644 --- a/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/screen/EnrolLastBiometricViewModel.kt +++ b/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/screen/EnrolLastBiometricViewModel.kt @@ -17,8 +17,8 @@ import com.simprints.infra.config.sync.ConfigManager import com.simprints.infra.enrolment.records.repository.EnrolmentRecordRepository import com.simprints.infra.enrolment.records.repository.domain.models.Subject import com.simprints.infra.enrolment.records.repository.domain.models.SubjectAction -import com.simprints.infra.events.event.domain.models.EnrolmentEventV2 -import com.simprints.infra.events.event.domain.models.PersonCreationEvent +import com.simprints.infra.events.event.domain.models.BiometricReferenceCreationEvent +import com.simprints.infra.events.event.domain.models.EnrolmentEventV4 import com.simprints.infra.events.session.SessionEventRepository import com.simprints.infra.logging.LoggingConstants.CrashReportTag.ENROLMENT import com.simprints.infra.logging.Simber @@ -85,20 +85,20 @@ internal class EnrolLastBiometricViewModel @Inject constructor( private suspend fun registerEvent(subject: Subject) { Simber.d("Register events for enrolments", tag = ENROLMENT) - val personCreationEvent = eventRepository + val biometricReferenceIds = eventRepository .getEventsInCurrentSession() - .filterIsInstance() + .filterIsInstance() .sortedByDescending { it.payload.createdAt } - .first() + .map { it.payload.id } eventRepository.addOrUpdateEvent( - EnrolmentEventV2( + EnrolmentEventV4( timeHelper.now(), subject.subjectId, subject.projectId, subject.moduleId, subject.attendantId, - personCreationEvent.id, + biometricReferenceIds, ), ) } diff --git a/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/screen/usecase/BuildSubjectUseCase.kt b/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/screen/usecase/BuildSubjectUseCase.kt index 75ab358dd4..1b40ba37b7 100644 --- a/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/screen/usecase/BuildSubjectUseCase.kt +++ b/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/screen/usecase/BuildSubjectUseCase.kt @@ -26,26 +26,30 @@ internal class BuildSubjectUseCase @Inject constructor( params.moduleId, createdAt = Date(timeHelper.now().ms), fingerprintSamples = getFingerprintCaptureResult(params.steps) - ?.map(::fingerprintSample) + ?.let { result -> result.results.map { fingerprintSample(result.referenceId, it) } } + .orEmpty(), + faceSamples = getFaceCaptureResult(params.steps) + ?.let { result -> result.results.map { faceSample(result.referenceId, it) } } .orEmpty(), - faceSamples = getFaceCaptureResult(params.steps)?.map(::faceSample).orEmpty(), ) private fun getFingerprintCaptureResult(steps: List) = steps .filterIsInstance() .firstOrNull() - ?.results private fun getFaceCaptureResult(steps: List) = steps .filterIsInstance() .firstOrNull() - ?.results - private fun fingerprintSample(it: FingerTemplateCaptureResult) = FingerprintSample( - fromDomainToModuleApi(it.finger), - it.template, - it.templateQualityScore, - it.format, + private fun fingerprintSample( + referenceId: String, + result: FingerTemplateCaptureResult, + ) = FingerprintSample( + fromDomainToModuleApi(result.finger), + result.template, + result.templateQualityScore, + result.format, + referenceId, ) private fun fromDomainToModuleApi(finger: Finger) = when (finger) { @@ -61,5 +65,8 @@ internal class BuildSubjectUseCase @Inject constructor( Finger.LEFT_5TH_FINGER -> IFingerIdentifier.LEFT_5TH_FINGER } - private fun faceSample(it: FaceTemplateCaptureResult) = FaceSample(it.template, it.format) + private fun faceSample( + referenceId: String, + result: FaceTemplateCaptureResult, + ) = FaceSample(result.template, result.format, referenceId) } diff --git a/feature/enrol-last-biometric/src/test/java/com/simprints/feature/enrollast/screen/EnrolLastBiometricViewModelTest.kt b/feature/enrol-last-biometric/src/test/java/com/simprints/feature/enrollast/screen/EnrolLastBiometricViewModelTest.kt index 34affcfd60..1ff2a99e34 100644 --- a/feature/enrol-last-biometric/src/test/java/com/simprints/feature/enrollast/screen/EnrolLastBiometricViewModelTest.kt +++ b/feature/enrol-last-biometric/src/test/java/com/simprints/feature/enrollast/screen/EnrolLastBiometricViewModelTest.kt @@ -14,9 +14,10 @@ import com.simprints.infra.config.store.models.ProjectConfiguration import com.simprints.infra.config.sync.ConfigManager import com.simprints.infra.enrolment.records.repository.EnrolmentRecordRepository import com.simprints.infra.enrolment.records.repository.domain.models.Subject -import com.simprints.infra.events.event.domain.models.EnrolmentEventV2 +import com.simprints.infra.events.event.domain.models.BiometricReferenceCreationEvent +import com.simprints.infra.events.event.domain.models.BiometricReferenceCreationEvent.BiometricReferenceCreationPayload +import com.simprints.infra.events.event.domain.models.EnrolmentEventV4 import com.simprints.infra.events.event.domain.models.PersonCreationEvent -import com.simprints.infra.events.event.domain.models.PersonCreationEvent.PersonCreationPayload import com.simprints.infra.events.session.SessionEventRepository import com.simprints.testtools.common.coroutines.TestCoroutineRule import io.mockk.MockKAnnotations @@ -213,30 +214,36 @@ internal class EnrolLastBiometricViewModelTest { } @Test - fun `Uses latest PersonCreationEvent for Enrolment event`() = runTest { - val personCreationEvent1 = mockk { - every { id } returns "personCreationEventId1" - every { payload } returns mockk { + fun `Uses all BiometricReferenceCreationEvent for Enrolment event`() = runTest { + val biometricReferenceCreationEvent1 = mockk { + every { id } returns "biometricReferenceCreationEventId1" + every { payload } returns mockk { every { createdAt } returns Timestamp(1) + every { id } returns "referenceId1" } } - val personCreationId2 = "personCreationEventId2" - val personCreationEvent2 = mockk { - every { id } returns personCreationId2 - every { payload } returns mockk { + val biometricReferenceCreationEvent2 = mockk { + every { id } returns "biometricReferenceCreationEventId2" + every { payload } returns mockk { every { createdAt } returns Timestamp(2) + every { id } returns "referenceId2" } } + coEvery { eventRepository.getEventsInCurrentSession() } returns listOf( - personCreationEvent1, - personCreationEvent2, + biometricReferenceCreationEvent2, + biometricReferenceCreationEvent1, ) viewModel.enrolBiometric(createParams(listOf())) coVerify { eventRepository.addOrUpdateEvent( - match { it is EnrolmentEventV2 && it.payload.personCreationEventId == personCreationId2 }, + withArg { + assertThat(it).isInstanceOf(EnrolmentEventV4::class.java) + assertThat((it.payload as EnrolmentEventV4.EnrolmentPayload).biometricReferenceIds) + .containsExactly("referenceId1", "referenceId2") + }, ) } } diff --git a/feature/enrol-last-biometric/src/test/java/com/simprints/feature/enrollast/screen/usecase/BuildSubjectUseCaseTest.kt b/feature/enrol-last-biometric/src/test/java/com/simprints/feature/enrollast/screen/usecase/BuildSubjectUseCaseTest.kt index f337bafdd3..1f71cf4a47 100644 --- a/feature/enrol-last-biometric/src/test/java/com/simprints/feature/enrollast/screen/usecase/BuildSubjectUseCaseTest.kt +++ b/feature/enrol-last-biometric/src/test/java/com/simprints/feature/enrollast/screen/usecase/BuildSubjectUseCaseTest.kt @@ -68,8 +68,14 @@ class BuildSubjectUseCaseTest { createParams( listOf( EnrolLastBiometricStepResult.FingerprintMatchResult(emptyList(), mockk()), - EnrolLastBiometricStepResult.FingerprintCaptureResult(listOf(mockFingerprintResults(Finger.RIGHT_THUMB))), - EnrolLastBiometricStepResult.FingerprintCaptureResult(listOf(mockFingerprintResults(Finger.LEFT_THUMB))), + EnrolLastBiometricStepResult.FingerprintCaptureResult( + REFERENCE_ID, + listOf(mockFingerprintResults(Finger.RIGHT_THUMB)), + ), + EnrolLastBiometricStepResult.FingerprintCaptureResult( + REFERENCE_ID, + listOf(mockFingerprintResults(Finger.LEFT_THUMB)), + ), ), ), ) @@ -84,6 +90,7 @@ class BuildSubjectUseCaseTest { createParams( listOf( EnrolLastBiometricStepResult.FingerprintCaptureResult( + REFERENCE_ID, listOf( mockFingerprintResults(Finger.RIGHT_5TH_FINGER), mockFingerprintResults(Finger.RIGHT_4TH_FINGER), @@ -111,8 +118,8 @@ class BuildSubjectUseCaseTest { createParams( listOf( EnrolLastBiometricStepResult.FaceMatchResult(emptyList()), - EnrolLastBiometricStepResult.FaceCaptureResult(mockFaceResultsList("first")), - EnrolLastBiometricStepResult.FaceCaptureResult(mockFaceResultsList("second")), + EnrolLastBiometricStepResult.FaceCaptureResult(REFERENCE_ID, mockFaceResultsList("first")), + EnrolLastBiometricStepResult.FaceCaptureResult(REFERENCE_ID, mockFaceResultsList("second")), ), ), ) @@ -133,6 +140,8 @@ class BuildSubjectUseCaseTest { private fun mockFaceResultsList(format: String) = listOf(FaceTemplateCaptureResult(byteArrayOf(), format)) companion object { + private const val REFERENCE_ID = "referenceId" + private const val PROJECT_ID = "projectId" private val USER_ID = "userId".asTokenizableRaw() private val MODULE_ID = "moduleId".asTokenizableRaw() diff --git a/feature/login-check/src/main/java/com/simprints/feature/logincheck/usecases/StartBackgroundSyncUseCase.kt b/feature/login-check/src/main/java/com/simprints/feature/logincheck/usecases/StartBackgroundSyncUseCase.kt index 8369ea556d..0e6a13503d 100644 --- a/feature/login-check/src/main/java/com/simprints/feature/logincheck/usecases/StartBackgroundSyncUseCase.kt +++ b/feature/login-check/src/main/java/com/simprints/feature/logincheck/usecases/StartBackgroundSyncUseCase.kt @@ -10,11 +10,10 @@ internal class StartBackgroundSyncUseCase @Inject constructor( private val configManager: ConfigManager, ) { suspend operator fun invoke() { - syncOrchestrator.scheduleBackgroundWork() - val frequency = configManager.getProjectConfiguration().synchronization.frequency - if (frequency == SynchronizationConfiguration.Frequency.PERIODICALLY_AND_ON_SESSION_START) { - syncOrchestrator.startEventSync() - } + + syncOrchestrator.scheduleBackgroundWork( + withDelay = frequency != SynchronizationConfiguration.Frequency.PERIODICALLY_AND_ON_SESSION_START, + ) } } diff --git a/feature/login-check/src/test/java/com/simprints/feature/logincheck/usecases/StartBackgroundSyncUseCaseTest.kt b/feature/login-check/src/test/java/com/simprints/feature/logincheck/usecases/StartBackgroundSyncUseCaseTest.kt index 41bca6b3b9..b23f046c8c 100644 --- a/feature/login-check/src/test/java/com/simprints/feature/logincheck/usecases/StartBackgroundSyncUseCaseTest.kt +++ b/feature/login-check/src/test/java/com/simprints/feature/logincheck/usecases/StartBackgroundSyncUseCaseTest.kt @@ -7,7 +7,6 @@ import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify import io.mockk.impl.annotations.MockK -import io.mockk.verify import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -38,9 +37,7 @@ class StartBackgroundSyncUseCaseTest { useCase.invoke() - coVerify { - syncOrchestrator.scheduleBackgroundWork() - } + coVerify { syncOrchestrator.scheduleBackgroundWork(any()) } } @Test @@ -50,7 +47,7 @@ class StartBackgroundSyncUseCaseTest { useCase.invoke() - verify { syncOrchestrator.startEventSync() } + coVerify { syncOrchestrator.scheduleBackgroundWork(eq(false)) } } @Test @@ -60,6 +57,6 @@ class StartBackgroundSyncUseCaseTest { useCase.invoke() - verify(exactly = 0) { syncOrchestrator.startEventSync() } + coVerify { syncOrchestrator.scheduleBackgroundWork(eq(true)) } } } diff --git a/feature/matcher/src/main/java/com/simprints/matcher/MatchContract.kt b/feature/matcher/src/main/java/com/simprints/matcher/MatchContract.kt index ef9d263025..b63b8ad98e 100644 --- a/feature/matcher/src/main/java/com/simprints/matcher/MatchContract.kt +++ b/feature/matcher/src/main/java/com/simprints/matcher/MatchContract.kt @@ -10,6 +10,7 @@ object MatchContract { val DESTINATION = R.id.matcherFragment fun getArgs( + referenceId: String = "", fingerprintSamples: List = emptyList(), faceSamples: List = emptyList(), fingerprintSDK: FingerprintConfiguration.BioSdk? = null, @@ -18,6 +19,7 @@ object MatchContract { biometricDataSource: BiometricDataSource, ) = MatchFragmentArgs( MatchParams( + referenceId, faceSamples, fingerprintSamples, fingerprintSDK, diff --git a/feature/matcher/src/main/java/com/simprints/matcher/MatchParams.kt b/feature/matcher/src/main/java/com/simprints/matcher/MatchParams.kt index 497ad40088..110754051a 100644 --- a/feature/matcher/src/main/java/com/simprints/matcher/MatchParams.kt +++ b/feature/matcher/src/main/java/com/simprints/matcher/MatchParams.kt @@ -13,6 +13,7 @@ import kotlinx.parcelize.Parcelize @Keep @Parcelize data class MatchParams( + val probeReferenceId: String, val probeFaceSamples: List = emptyList(), val probeFingerprintSamples: List = emptyList(), val fingerprintSDK: FingerprintConfiguration.BioSdk? = null, diff --git a/feature/matcher/src/main/java/com/simprints/matcher/usecases/SaveMatchEventUseCase.kt b/feature/matcher/src/main/java/com/simprints/matcher/usecases/SaveMatchEventUseCase.kt index 24c65d4830..8efa3d8848 100644 --- a/feature/matcher/src/main/java/com/simprints/matcher/usecases/SaveMatchEventUseCase.kt +++ b/feature/matcher/src/main/java/com/simprints/matcher/usecases/SaveMatchEventUseCase.kt @@ -41,6 +41,7 @@ internal class SaveMatchEventUseCase @Inject constructor( matchParams.queryForCandidates, matchEntries.firstOrNull(), if (matchParams.isFaceMatch()) null else getFingerprintComparisonStrategy(matchParams.fingerprintSDK!!), + matchParams.probeReferenceId, ) } else { getOneToManyEvent( @@ -50,6 +51,7 @@ internal class SaveMatchEventUseCase @Inject constructor( matchParams.queryForCandidates, candidatesCount, matchEntries, + matchParams.probeReferenceId, ) } eventRepository.addOrUpdateEvent(event) @@ -75,13 +77,15 @@ internal class SaveMatchEventUseCase @Inject constructor( queryForCandidates: SubjectQuery, matchEntry: MatchEntry?, fingerComparisonStrategy: FingerComparisonStrategy?, + biometricReferenceId: String, ) = OneToOneMatchEvent( - startTime, - endTime, - queryForCandidates.subjectId!!, - matcherName, - matchEntry, - fingerComparisonStrategy, + createdAt = startTime, + endTime = endTime, + candidateId = queryForCandidates.subjectId!!, + matcher = matcherName, + result = matchEntry, + fingerComparisonStrategy = fingerComparisonStrategy, + probeBiometricReferenceId = biometricReferenceId, ) private fun getOneToManyEvent( @@ -91,15 +95,17 @@ internal class SaveMatchEventUseCase @Inject constructor( queryForCandidates: SubjectQuery, candidatesCount: Int, matchEntries: List, + biometricReferenceId: String, ) = OneToManyMatchEvent( - startTime, - endTime, - OneToManyMatchEvent.OneToManyMatchPayload.MatchPool( + createdAt = startTime, + endTime = endTime, + pool = OneToManyMatchEvent.OneToManyMatchPayload.MatchPool( queryForCandidates.parseQueryAsCoreMatchPoolType(), candidatesCount, ), - matcherName, - matchEntries, + matcher = matcherName, + result = matchEntries, + probeBiometricReferenceId = biometricReferenceId, ) private fun SubjectQuery.parseQueryAsCoreMatchPoolType(): OneToManyMatchEvent.OneToManyMatchPayload.MatchPoolType = when { diff --git a/feature/matcher/src/test/java/com/simprints/matcher/screen/MatchViewModelTest.kt b/feature/matcher/src/test/java/com/simprints/matcher/screen/MatchViewModelTest.kt index 8629422436..8f3d164bd8 100644 --- a/feature/matcher/src/test/java/com/simprints/matcher/screen/MatchViewModelTest.kt +++ b/feature/matcher/src/test/java/com/simprints/matcher/screen/MatchViewModelTest.kt @@ -109,6 +109,7 @@ internal class MatchViewModelTest { viewModel.matchState.test() viewModel.setupMatch( MatchParams( + probeReferenceId = "referenceId", probeFaceSamples = listOf(getFaceSample()), flowType = FlowType.ENROL, queryForCandidates = mockk {}, @@ -153,6 +154,7 @@ internal class MatchViewModelTest { val states = viewModel.matchState.test() viewModel.setupMatch( MatchParams( + probeReferenceId = "referenceId", probeFaceSamples = listOf(getFaceSample()), flowType = FlowType.ENROL, queryForCandidates = mockk {}, @@ -207,6 +209,7 @@ internal class MatchViewModelTest { viewModel.setupMatch( MatchParams( + probeReferenceId = "referenceId", probeFingerprintSamples = listOf(getFingerprintSample()), fingerprintSDK = SECUGEN_SIM_MATCHER, flowType = FlowType.ENROL, diff --git a/feature/matcher/src/test/java/com/simprints/matcher/usecases/FaceMatcherUseCaseTest.kt b/feature/matcher/src/test/java/com/simprints/matcher/usecases/FaceMatcherUseCaseTest.kt index 1ab631ad2b..11fe0458b7 100644 --- a/feature/matcher/src/test/java/com/simprints/matcher/usecases/FaceMatcherUseCaseTest.kt +++ b/feature/matcher/src/test/java/com/simprints/matcher/usecases/FaceMatcherUseCaseTest.kt @@ -65,6 +65,7 @@ internal class FaceMatcherUseCaseTest { val results = useCase.invoke( MatchParams( + probeReferenceId = "referenceId", flowType = FlowType.VERIFY, queryForCandidates = SubjectQuery(), biometricDataSource = BiometricDataSource.Simprints, @@ -89,6 +90,7 @@ internal class FaceMatcherUseCaseTest { val results = useCase.invoke( MatchParams( + probeReferenceId = "referenceId", probeFaceSamples = listOf( MatchParams.FaceSample("faceId", byteArrayOf(1, 2, 3)), ), @@ -127,6 +129,7 @@ internal class FaceMatcherUseCaseTest { val results = useCase.invoke( matchParams = MatchParams( + probeReferenceId = "referenceId", probeFaceSamples = listOf( MatchParams.FaceSample("faceId", byteArrayOf(1, 2, 3)), ), diff --git a/feature/matcher/src/test/java/com/simprints/matcher/usecases/FingerprintMatcherUseCaseTest.kt b/feature/matcher/src/test/java/com/simprints/matcher/usecases/FingerprintMatcherUseCaseTest.kt index 02547326f1..599f1393b1 100644 --- a/feature/matcher/src/test/java/com/simprints/matcher/usecases/FingerprintMatcherUseCaseTest.kt +++ b/feature/matcher/src/test/java/com/simprints/matcher/usecases/FingerprintMatcherUseCaseTest.kt @@ -74,6 +74,7 @@ internal class FingerprintMatcherUseCaseTest { fun `Skips matching if there are no probes`() = runTest { val results = useCase.invoke( MatchParams( + probeReferenceId = "referenceId", probeFingerprintSamples = emptyList(), fingerprintSDK = SECUGEN_SIM_MATCHER, flowType = FlowType.VERIFY, @@ -102,6 +103,7 @@ internal class FingerprintMatcherUseCaseTest { val results = useCase.invoke( MatchParams( + probeReferenceId = "referenceId", probeFingerprintSamples = listOf( MatchParams.FingerprintSample( IFingerIdentifier.LEFT_3RD_FINGER, @@ -161,6 +163,7 @@ internal class FingerprintMatcherUseCaseTest { useCase.invoke( matchParams = MatchParams( + probeReferenceId = "referenceId", probeFingerprintSamples = listOf( MatchParams.FingerprintSample( IFingerIdentifier.LEFT_3RD_FINGER, @@ -179,5 +182,5 @@ internal class FingerprintMatcherUseCaseTest { coVerify { bioSdkWrapper.match(any(), any(), any()) } } - private fun fingerprintSample(finger: IFingerIdentifier) = FingerprintSample(finger, byteArrayOf(1), 42, "format") + private fun fingerprintSample(finger: IFingerIdentifier) = FingerprintSample(finger, byteArrayOf(1), 42, "format", "referenceId") } diff --git a/feature/matcher/src/test/java/com/simprints/matcher/usecases/SaveMatchEventUseCaseTest.kt b/feature/matcher/src/test/java/com/simprints/matcher/usecases/SaveMatchEventUseCaseTest.kt index 3169497cb7..ff852c70f1 100644 --- a/feature/matcher/src/test/java/com/simprints/matcher/usecases/SaveMatchEventUseCaseTest.kt +++ b/feature/matcher/src/test/java/com/simprints/matcher/usecases/SaveMatchEventUseCaseTest.kt @@ -11,7 +11,9 @@ import com.simprints.infra.config.sync.ConfigManager import com.simprints.infra.enrolment.records.repository.domain.models.BiometricDataSource import com.simprints.infra.enrolment.records.repository.domain.models.SubjectQuery import com.simprints.infra.events.event.domain.models.OneToManyMatchEvent +import com.simprints.infra.events.event.domain.models.OneToManyMatchEvent.OneToManyMatchPayload.OneToManyMatchPayloadV3 import com.simprints.infra.events.event.domain.models.OneToOneMatchEvent +import com.simprints.infra.events.event.domain.models.OneToOneMatchEvent.OneToOneMatchPayload.OneToOneMatchPayloadV4 import com.simprints.infra.events.session.SessionEventRepository import com.simprints.matcher.FaceMatchResult import com.simprints.matcher.MatchParams @@ -67,6 +69,7 @@ class SaveMatchEventUseCaseTest { Timestamp(1L), Timestamp(2L), MatchParams( + probeReferenceId = "referenceId", flowType = FlowType.VERIFY, queryForCandidates = SubjectQuery(subjectId = "subjectId"), probeFaceSamples = listOf(MatchParams.FaceSample("faceId", byteArrayOf(1, 2, 3))), @@ -91,6 +94,7 @@ class SaveMatchEventUseCaseTest { assertThat(it.payload.matcher).isEqualTo("faceMatcherName") assertThat(it.payload.result?.candidateId).isEqualTo("guid1") assertThat(it.payload.result?.score).isEqualTo(0.5f) + assertThat((it.payload as OneToOneMatchPayloadV4).probeBiometricReferenceId).isEqualTo("referenceId") }, ) } @@ -102,6 +106,7 @@ class SaveMatchEventUseCaseTest { Timestamp(1L), Timestamp(2L), MatchParams( + probeReferenceId = "referenceId", flowType = FlowType.VERIFY, queryForCandidates = SubjectQuery(subjectId = "subjectId"), probeFingerprintSamples = listOf( @@ -133,6 +138,7 @@ class SaveMatchEventUseCaseTest { assertThat(it.payload.matcher).isEqualTo("faceMatcherName") assertThat(it.payload.result?.candidateId).isEqualTo("guid1") assertThat(it.payload.result?.score).isEqualTo(0.5f) + assertThat((it.payload as OneToOneMatchPayloadV4).probeBiometricReferenceId).isEqualTo("referenceId") }, ) } @@ -144,6 +150,7 @@ class SaveMatchEventUseCaseTest { startTime = Timestamp(1L), endTime = Timestamp(2L), matchParams = MatchParams( + probeReferenceId = "referenceId", probeFaceSamples = emptyList(), probeFingerprintSamples = emptyList(), fingerprintSDK = null, @@ -177,6 +184,7 @@ class SaveMatchEventUseCaseTest { ?.last() ?.candidateId, ).isEqualTo("guid2") + assertThat((it.payload as OneToManyMatchPayloadV3).probeBiometricReferenceId).isEqualTo("referenceId") }, ) } @@ -188,6 +196,7 @@ class SaveMatchEventUseCaseTest { Timestamp(1L), Timestamp(2L), MatchParams( + probeReferenceId = "referenceId", flowType = FlowType.IDENTIFY, queryForCandidates = SubjectQuery(attendantId = "userId".asTokenizableEncrypted()), biometricDataSource = BiometricDataSource.Simprints, @@ -213,6 +222,7 @@ class SaveMatchEventUseCaseTest { Timestamp(1L), Timestamp(2L), MatchParams( + probeReferenceId = "referenceId", flowType = FlowType.IDENTIFY, queryForCandidates = SubjectQuery(moduleId = "moduleId".asTokenizableEncrypted()), biometricDataSource = BiometricDataSource.Simprints, @@ -238,6 +248,7 @@ class SaveMatchEventUseCaseTest { Timestamp(1L), Timestamp(2L), MatchParams( + probeReferenceId = "referenceId", emptyList(), flowType = FlowType.IDENTIFY, queryForCandidates = SubjectQuery(), diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/OrchestratorViewModel.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/OrchestratorViewModel.kt index 64f08e379b..342c0025f8 100644 --- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/OrchestratorViewModel.kt +++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/OrchestratorViewModel.kt @@ -24,10 +24,8 @@ import com.simprints.feature.orchestrator.steps.Step import com.simprints.feature.orchestrator.steps.StepId import com.simprints.feature.orchestrator.steps.StepStatus import com.simprints.feature.orchestrator.usecases.AddCallbackEventUseCase -import com.simprints.feature.orchestrator.usecases.CreatePersonEventUseCase import com.simprints.feature.orchestrator.usecases.MapRefusalOrErrorResultUseCase import com.simprints.feature.orchestrator.usecases.MapStepsForLastBiometricEnrolUseCase -import com.simprints.feature.orchestrator.usecases.ShouldCreatePersonUseCase import com.simprints.feature.orchestrator.usecases.UpdateDailyActivityUseCase import com.simprints.feature.orchestrator.usecases.response.AppResponseBuilderUseCase import com.simprints.feature.orchestrator.usecases.steps.BuildStepsUseCase @@ -55,8 +53,6 @@ internal class OrchestratorViewModel @Inject constructor( private val locationStore: LocationStore, private val stepsBuilder: BuildStepsUseCase, private val mapRefusalOrErrorResult: MapRefusalOrErrorResultUseCase, - private val shouldCreatePerson: ShouldCreatePersonUseCase, - private val createPersonEvent: CreatePersonEventUseCase, private val appResponseBuilder: AppResponseBuilderUseCase, private val addCallbackEvent: AddCallbackEventUseCase, private val updateDailyActivity: UpdateDailyActivityUseCase, @@ -115,11 +111,6 @@ internal class OrchestratorViewModel @Inject constructor( updateMatcherStepPayload(step, result) } - if (shouldCreatePerson(actionRequest, modalities, steps)) { - Simber.i("Creating person event", tag = ORCHESTRATION) - createPersonEvent(steps.mapNotNull { it.result }) - } - if (result is SelectSubjectAgeGroupResult) { val captureAndMatchSteps = stepsBuilder.buildCaptureAndMatchStepsForAgeGroup( actionRequest!!, @@ -226,7 +217,7 @@ internal class OrchestratorViewModel @Inject constructor( .map { MatchParams.FaceSample(it.faceId, it.template) } val newPayload = matchingStep.payload .getParcelable(MatchStepStubPayload.STUB_KEY) - ?.toFaceStepArgs(faceSamples) + ?.toFaceStepArgs(result.referenceId, faceSamples) if (newPayload != null) { matchingStep.payload = newPayload @@ -257,7 +248,7 @@ internal class OrchestratorViewModel @Inject constructor( } val newPayload = matchingStep.payload .getParcelable(MatchStepStubPayload.STUB_KEY) - ?.toFingerprintStepArgs(fingerprintSamples) + ?.toFingerprintStepArgs(result.referenceId, fingerprintSamples) if (newPayload != null) { matchingStep.payload = newPayload diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/steps/MatchStepStubPayload.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/steps/MatchStepStubPayload.kt index 2d9d2601cd..188518d65b 100644 --- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/steps/MatchStepStubPayload.kt +++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/steps/MatchStepStubPayload.kt @@ -23,14 +23,22 @@ internal data class MatchStepStubPayload( val biometricDataSource: BiometricDataSource, val fingerprintSDK: FingerprintConfiguration.BioSdk?, ) : Parcelable { - fun toFaceStepArgs(samples: List) = MatchContract.getArgs( + fun toFaceStepArgs( + referenceId: String, + samples: List, + ) = MatchContract.getArgs( + referenceId = referenceId, faceSamples = samples, flowType = flowType, subjectQuery = subjectQuery, biometricDataSource = biometricDataSource, ) - fun toFingerprintStepArgs(samples: List) = MatchContract.getArgs( + fun toFingerprintStepArgs( + referenceId: String, + samples: List, + ) = MatchContract.getArgs( + referenceId = referenceId, fingerprintSamples = samples, fingerprintSDK = fingerprintSDK, flowType = flowType, diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/CreatePersonEventUseCase.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/CreatePersonEventUseCase.kt deleted file mode 100644 index f525fd7e8d..0000000000 --- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/CreatePersonEventUseCase.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.simprints.feature.orchestrator.usecases - -import com.simprints.core.domain.face.FaceSample -import com.simprints.core.domain.face.uniqueId -import com.simprints.core.domain.fingerprint.FingerprintSample -import com.simprints.core.domain.fingerprint.uniqueId -import com.simprints.core.tools.time.TimeHelper -import com.simprints.face.capture.FaceCaptureResult -import com.simprints.fingerprint.capture.FingerprintCaptureResult -import com.simprints.infra.events.event.domain.models.PersonCreationEvent -import com.simprints.infra.events.session.SessionEventRepository -import java.io.Serializable -import javax.inject.Inject - -internal class CreatePersonEventUseCase @Inject constructor( - private val eventRepository: SessionEventRepository, - private val timeHelper: TimeHelper, -) { - suspend operator fun invoke(results: List) { - val sessionEvents = eventRepository.getEventsInCurrentSession() - val previousPersonCreationEvent = sessionEvents - .filterIsInstance() - .sortedByDescending { it.payload.createdAt } - .firstOrNull() - - val faceCaptures = extractFaceCaptures(results) - val fingerprintCaptures = extractFingerprintCaptures(results) - - val personCreationEvent = build(faceCaptures, fingerprintCaptures, previousPersonCreationEvent) - - if (personCreationEvent.hasBiometricData()) { - eventRepository.addOrUpdateEvent(personCreationEvent) - } - } - - private fun extractFaceCaptures(responses: List) = responses - .filterIsInstance() - .flatMap { it.results } - - private fun extractFingerprintCaptures(responses: List) = responses - .filterIsInstance() - .flatMap { it.results } - - private fun build( - faceSamplesForPersonCreation: List, - fingerprintSamplesForPersonCreation: List, - previousPersonCreationEvent: PersonCreationEvent? = null, - ): PersonCreationEvent { - val fingerprintCaptureIds = fingerprintSamplesForPersonCreation - .mapNotNull { it.captureEventId } - .ifEmpty { null } - val fingerprintReferenceId = fingerprintSamplesForPersonCreation - .mapNotNull { it.sample } - .map { FingerprintSample(it.fingerIdentifier, it.template, it.templateQualityScore, it.format) } - .uniqueId() - - val faceCaptureIds = faceSamplesForPersonCreation - .mapNotNull { it.captureEventId } - .ifEmpty { null } - val faceReferenceId = faceSamplesForPersonCreation - .mapNotNull { it.sample } - .map { FaceSample(it.template, it.format) } - .uniqueId() - - // If the step results of the current callout do not contain a modality but we have a PersonCreationEvent from the - // previous callout (of the same session), we use the modality from the previous callout. This happens when the - // user skips a modality during identification (due to matching modalities configuration) and then captures the - // skipped modality in the following enrol last callout. - return PersonCreationEvent( - startTime = timeHelper.now(), - fingerprintCaptureIds = fingerprintCaptureIds ?: previousPersonCreationEvent?.payload?.fingerprintCaptureIds, - fingerprintReferenceId = fingerprintReferenceId ?: previousPersonCreationEvent?.payload?.fingerprintReferenceId, - faceCaptureIds = faceCaptureIds ?: previousPersonCreationEvent?.payload?.faceCaptureIds, - faceReferenceId = faceReferenceId ?: previousPersonCreationEvent?.payload?.faceReferenceId, - ) - } -} diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/MapStepsForLastBiometricEnrolUseCase.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/MapStepsForLastBiometricEnrolUseCase.kt index 5c1d48f11b..4996599c5b 100644 --- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/MapStepsForLastBiometricEnrolUseCase.kt +++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/MapStepsForLastBiometricEnrolUseCase.kt @@ -22,6 +22,7 @@ internal class MapStepsForLastBiometricEnrolUseCase @Inject constructor() { ) is FingerprintCaptureResult -> EnrolLastBiometricStepResult.FingerprintCaptureResult( + result.referenceId, result.results.mapNotNull { it.sample }.map { FingerTemplateCaptureResult( it.fingerIdentifier.fromModuleApiToDomain(), @@ -38,6 +39,7 @@ internal class MapStepsForLastBiometricEnrolUseCase @Inject constructor() { ) is FaceCaptureResult -> EnrolLastBiometricStepResult.FaceCaptureResult( + result.referenceId, result.results.mapNotNull { it.sample }.map { FaceTemplateCaptureResult(it.template, it.format) }, ) diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/ShouldCreatePersonUseCase.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/ShouldCreatePersonUseCase.kt deleted file mode 100644 index 1b69babb11..0000000000 --- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/ShouldCreatePersonUseCase.kt +++ /dev/null @@ -1,72 +0,0 @@ -package com.simprints.feature.orchestrator.usecases - -import com.simprints.face.capture.FaceCaptureResult -import com.simprints.feature.orchestrator.steps.Step -import com.simprints.feature.orchestrator.steps.StepId -import com.simprints.fingerprint.capture.FingerprintCaptureResult -import com.simprints.infra.config.store.models.GeneralConfiguration -import com.simprints.infra.events.event.domain.models.PersonCreationEvent -import com.simprints.infra.events.session.SessionEventRepository -import com.simprints.infra.logging.LoggingConstants.CrashReportTag.ORCHESTRATION -import com.simprints.infra.logging.Simber -import com.simprints.infra.orchestration.data.ActionRequest -import javax.inject.Inject - -internal class ShouldCreatePersonUseCase @Inject constructor( - private val eventRepository: SessionEventRepository, -) { - suspend operator fun invoke( - actionRequest: ActionRequest?, - modalities: Set, - results: List, - ): Boolean { - if (actionRequest !is ActionRequest.FlowAction && - actionRequest !is ActionRequest.EnrolLastBiometricActionRequest - ) { - return false - } - - if (modalities.isEmpty()) { - Simber.i("Could not create person event - modalities are empty", tag = ORCHESTRATION) - return false - } - - val faceCaptureResults = results.filter { it.id == StepId.FACE_CAPTURE } - val fingerprintCaptureResults = results.filter { it.id == StepId.FINGERPRINT_CAPTURE } - val faceCaptureIsScheduled = faceCaptureResults.isNotEmpty() - val fingerprintCaptureIsScheduled = fingerprintCaptureResults.isNotEmpty() - val faceCaptureIsComplete = faceCaptureResults.all { it.result is FaceCaptureResult } - val fingerprintCaptureIsComplete = fingerprintCaptureResults.all { it.result is FingerprintCaptureResult } - - // Neither face nor fingerprint capture is scheduled so no captures for a PersonCreation event - if (!faceCaptureIsScheduled && !fingerprintCaptureIsScheduled) { - return false - } - - // There are scheduled captures but not all of them are complete - if (!faceCaptureIsComplete || !fingerprintCaptureIsComplete) { - return false - } - - val sessionEvents = eventRepository.getEventsInCurrentSession() - val personCreationEvents = sessionEvents.filterIsInstance() - // We already have the maximum number of PersonCreation events (2) in the session - if (personCreationEvents.size > 1) { - return false - } - - val currentPersonCreationEvent = personCreationEvents.firstOrNull() - // If all scheduled capture steps are complete and we don't yet have a PersonCreation event, - // we should create one - if (currentPersonCreationEvent == null) { - return true - } - - // If we have a PersonCreation event but it's missing a reference that was scheduled for capture - // we should create a new one. This happens when an identification with only one modality is performed - // (due to matching modalities configuration) and the other modality is scheduled for capture in the - // following enrol last request - return (faceCaptureIsScheduled && !currentPersonCreationEvent.hasFaceReference()) || - (fingerprintCaptureIsScheduled && !currentPersonCreationEvent.hasFingerprintReference()) - } -} diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/EnrolSubjectUseCase.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/EnrolSubjectUseCase.kt index f0522293d9..7f318cf907 100644 --- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/EnrolSubjectUseCase.kt +++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/EnrolSubjectUseCase.kt @@ -5,8 +5,8 @@ import com.simprints.infra.config.store.models.Project import com.simprints.infra.enrolment.records.repository.EnrolmentRecordRepository import com.simprints.infra.enrolment.records.repository.domain.models.Subject import com.simprints.infra.enrolment.records.repository.domain.models.SubjectAction -import com.simprints.infra.events.event.domain.models.EnrolmentEventV2 -import com.simprints.infra.events.event.domain.models.PersonCreationEvent +import com.simprints.infra.events.event.domain.models.BiometricReferenceCreationEvent +import com.simprints.infra.events.event.domain.models.EnrolmentEventV4 import com.simprints.infra.events.session.SessionEventRepository import javax.inject.Inject @@ -15,21 +15,24 @@ internal class EnrolSubjectUseCase @Inject constructor( private val timeHelper: TimeHelper, private val enrolmentRecordRepository: EnrolmentRecordRepository, ) { - suspend operator fun invoke(subject: Subject, project: Project) { - val personCreationEvent = eventRepository + suspend operator fun invoke( + subject: Subject, + project: Project, + ) { + val biometricReferenceIds = eventRepository .getEventsInCurrentSession() - .filterIsInstance() + .filterIsInstance() .sortedByDescending { it.payload.createdAt } - .first() + .map { it.payload.id } eventRepository.addOrUpdateEvent( - EnrolmentEventV2( + EnrolmentEventV4( timeHelper.now(), subject.subjectId, subject.projectId, subject.moduleId, subject.attendantId, - personCreationEvent.id, + biometricReferenceIds, ), ) enrolmentRecordRepository.performActions(listOf(SubjectAction.Creation(subject)), project) diff --git a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/OrchestratorViewModelTest.kt b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/OrchestratorViewModelTest.kt index 1555e1c9bd..da5b09c520 100644 --- a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/OrchestratorViewModelTest.kt +++ b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/OrchestratorViewModelTest.kt @@ -20,10 +20,8 @@ import com.simprints.feature.orchestrator.steps.Step import com.simprints.feature.orchestrator.steps.StepId import com.simprints.feature.orchestrator.steps.StepStatus import com.simprints.feature.orchestrator.usecases.AddCallbackEventUseCase -import com.simprints.feature.orchestrator.usecases.CreatePersonEventUseCase import com.simprints.feature.orchestrator.usecases.MapRefusalOrErrorResultUseCase import com.simprints.feature.orchestrator.usecases.MapStepsForLastBiometricEnrolUseCase -import com.simprints.feature.orchestrator.usecases.ShouldCreatePersonUseCase import com.simprints.feature.orchestrator.usecases.UpdateDailyActivityUseCase import com.simprints.feature.orchestrator.usecases.response.AppResponseBuilderUseCase import com.simprints.feature.orchestrator.usecases.steps.BuildStepsUseCase @@ -81,12 +79,6 @@ internal class OrchestratorViewModelTest { @MockK private lateinit var mapRefusalOrErrorResult: MapRefusalOrErrorResultUseCase - @MockK - private lateinit var shouldCreatePerson: ShouldCreatePersonUseCase - - @MockK - private lateinit var createPersonEvent: CreatePersonEventUseCase - @MockK private lateinit var appResponseBuilder: AppResponseBuilderUseCase @@ -111,8 +103,6 @@ internal class OrchestratorViewModelTest { locationStore, stepsBuilder, mapRefusalOrErrorResult, - shouldCreatePerson, - createPersonEvent, appResponseBuilder, addCallbackEvent, dailyActivityUseCase, @@ -140,7 +130,6 @@ internal class OrchestratorViewModelTest { createMockStep(StepId.CONSENT), ) coEvery { mapRefusalOrErrorResult(any(), any()) } returns null - coEvery { shouldCreatePerson(any(), any(), any()) } returns false val stepsObserver = viewModel.currentStep.test() @@ -151,19 +140,6 @@ internal class OrchestratorViewModelTest { .isEqualTo(listOf(StepId.SETUP, StepId.CONSENT)) } - @Test - fun `Creates person if required after step result`() = runTest { - every { stepsBuilder.build(any(), any()) } returns emptyList() - coEvery { mapRefusalOrErrorResult(any(), any()) } returns null - - coEvery { shouldCreatePerson(any(), any(), any()) } returns true - coJustRun { createPersonEvent(any()) } - - viewModel.handleResult(SetupResult(true)) - - coVerify { createPersonEvent(any()) } - } - @Test fun `Returns response when all steps executed`() = runTest { every { stepsBuilder.build(any(), any()) } returns listOf( @@ -171,7 +147,6 @@ internal class OrchestratorViewModelTest { createMockStep(StepId.CONSENT), ) coEvery { mapRefusalOrErrorResult(any(), any()) } returns null - coEvery { shouldCreatePerson(any(), any(), any()) } returns false coEvery { appResponseBuilder(any(), any(), any(), any()) } returns mockk() coJustRun { dailyActivityUseCase(any()) } justRun { addCallbackEvent(any()) } @@ -216,7 +191,6 @@ internal class OrchestratorViewModelTest { createMockStep(StepId.SELECT_SUBJECT_AGE), ) coEvery { mapRefusalOrErrorResult(any(), any()) } returns null - coEvery { shouldCreatePerson(any(), any(), any()) } returns false val captureAndMatchSteps = listOf( createMockStep(StepId.FACE_CAPTURE), createMockStep( @@ -253,10 +227,9 @@ internal class OrchestratorViewModelTest { ), ) coEvery { mapRefusalOrErrorResult(any(), any()) } returns null - coEvery { shouldCreatePerson(any(), any(), any()) } returns false viewModel.handleAction(mockk()) - viewModel.handleResult(FaceCaptureResult(emptyList())) + viewModel.handleResult(FaceCaptureResult("", emptyList())) viewModel.currentStep.test().value().peekContent()?.let { step -> assertThat(step.id).isEqualTo(StepId.FACE_MATCHER) @@ -277,10 +250,9 @@ internal class OrchestratorViewModelTest { ), ) coEvery { mapRefusalOrErrorResult(any(), any()) } returns null - coEvery { shouldCreatePerson(any(), any(), any()) } returns false viewModel.handleAction(mockk()) - viewModel.handleResult(FingerprintCaptureResult(emptyList())) + viewModel.handleResult(FingerprintCaptureResult("", emptyList())) viewModel.currentStep.test().value().peekContent()?.let { step -> assertThat(step.id).isEqualTo(StepId.FINGERPRINT_MATCHER) @@ -326,7 +298,6 @@ internal class OrchestratorViewModelTest { ), ) coEvery { mapRefusalOrErrorResult(any(), any()) } returns null - coEvery { shouldCreatePerson(any(), any(), any()) } returns false val format = "SimMatcher" val sample1 = FingerprintCaptureResult.Sample( IFingerIdentifier.LEFT_INDEX_FINGER, @@ -348,7 +319,7 @@ internal class OrchestratorViewModelTest { ) viewModel.handleAction(mockk()) - viewModel.handleResult(FingerprintCaptureResult(captureResults)) + viewModel.handleResult(FingerprintCaptureResult("", captureResults)) viewModel.currentStep.test().value().peekContent()?.let { step -> assertThat(step.id).isEqualTo(StepId.FINGERPRINT_MATCHER) @@ -456,7 +427,7 @@ internal class OrchestratorViewModelTest { ) viewModel.handleAction(mockk()) - viewModel.handleResult(FingerprintCaptureResult(emptyList())) + viewModel.handleResult(FingerprintCaptureResult("", emptyList())) viewModel.currentStep.test().value().peekContent()?.let { step -> assertThat(step.payload.getParcelable("params")?.steps) diff --git a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/cache/OrchestratorCacheIntegrationTest.kt b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/cache/OrchestratorCacheIntegrationTest.kt index d963d7be9f..57e93b0da1 100644 --- a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/cache/OrchestratorCacheIntegrationTest.kt +++ b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/cache/OrchestratorCacheIntegrationTest.kt @@ -60,6 +60,7 @@ class OrchestratorCacheIntegrationTest { payload = bundleOf("key" to "value"), status = StepStatus.COMPLETED, result = FingerprintCaptureResult( + "", results = listOf( FingerprintCaptureResult.Item( captureEventId = GUID1, diff --git a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/CreatePersonEventUseCaseTest.kt b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/CreatePersonEventUseCaseTest.kt deleted file mode 100644 index 04ae5336c2..0000000000 --- a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/CreatePersonEventUseCaseTest.kt +++ /dev/null @@ -1,190 +0,0 @@ -package com.simprints.feature.orchestrator.usecases - -import com.google.common.truth.Truth.assertThat -import com.simprints.core.domain.fingerprint.IFingerIdentifier -import com.simprints.core.tools.time.TimeHelper -import com.simprints.core.tools.time.Timestamp -import com.simprints.face.capture.FaceCaptureResult -import com.simprints.fingerprint.capture.FingerprintCaptureResult -import com.simprints.infra.events.event.domain.models.PersonCreationEvent -import com.simprints.infra.events.event.domain.models.fingerprint.FingerprintCaptureBiometricsEvent -import com.simprints.infra.events.session.SessionEventRepository -import com.simprints.testtools.common.coroutines.TestCoroutineRule -import io.mockk.MockKAnnotations -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.every -import io.mockk.impl.annotations.MockK -import io.mockk.mockk -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Rule -import org.junit.Test - -internal class CreatePersonEventUseCaseTest { - @get:Rule - val testCoroutineRule = TestCoroutineRule() - - @MockK - lateinit var eventRepository: SessionEventRepository - - @MockK - lateinit var timeHelper: TimeHelper - - private lateinit var useCase: CreatePersonEventUseCase - - @Before - fun setUp() { - MockKAnnotations.init(this, relaxed = true) - - every { timeHelper.now() } returns Timestamp(0L) - - coEvery { eventRepository.getCurrentSessionScope() } returns mockk { - every { id } returns "sessionId" - } - - useCase = CreatePersonEventUseCase(eventRepository, timeHelper) - } - - @Test - fun `Does not create event if no biometric data`() = runTest { - coEvery { eventRepository.getEventsInCurrentSession() } returns listOf() - - useCase(listOf()) - - coVerify(exactly = 0) { eventRepository.addOrUpdateEvent(any()) } - } - - @Test - fun `Create event if there is face biometric data`() = runTest { - coEvery { eventRepository.getEventsInCurrentSession() } returns listOf() - - useCase(listOf(FaceCaptureResult(listOf(createFaceCaptureResultItem())))) - - coVerify { - eventRepository.addOrUpdateEvent( - withArg { - assertThat(it.payload.faceCaptureIds).isEqualTo(listOf(FACE_CAPTURE_ID)) - }, - ) - } - } - - @Test - fun `Create event if there is fingerprint biometric data`() = runTest { - coEvery { eventRepository.getEventsInCurrentSession() } returns listOf( - mockk { - every { payload.id } returns FINGER_CAPTURE_ID - every { payload.fingerprint.template } returns TEMPLATE - }, - ) - - useCase(listOf(FingerprintCaptureResult(listOf(createFingerprintCaptureResultItem())))) - - coVerify { - eventRepository.addOrUpdateEvent( - withArg { - assertThat(it.payload.fingerprintCaptureIds).isEqualTo(listOf(FINGER_CAPTURE_ID)) - }, - ) - } - } - - @Test - fun `Gets fingerprint from previous PersonCreationEvent (when present) if missing in current callout captures`() = runTest { - coEvery { eventRepository.getEventsInCurrentSession() } returns listOf( - mockk { - every { payload.fingerprintCaptureIds } returns listOf(FINGER_CAPTURE_ID) - every { payload.fingerprintReferenceId } returns FINGER_REFERENCE_ID - }, - ) - - useCase(listOf(FaceCaptureResult(listOf(createFaceCaptureResultItem())))) - - coVerify { - eventRepository.addOrUpdateEvent( - withArg { - assertThat(it.payload.faceCaptureIds).isEqualTo(listOf(FACE_CAPTURE_ID)) - assertThat(it.payload.fingerprintCaptureIds).isEqualTo(listOf(FINGER_CAPTURE_ID)) - assertThat(it.payload.fingerprintReferenceId).isEqualTo(FINGER_REFERENCE_ID) - }, - ) - } - } - - @Test - fun `Gets face from previous PersonCreationEvent (when present) if missing in current callout captures`() = runTest { - coEvery { eventRepository.getEventsInCurrentSession() } returns listOf( - mockk { - every { payload.faceCaptureIds } returns listOf(FACE_CAPTURE_ID) - every { payload.faceReferenceId } returns FACE_REFERENCE_ID - }, - ) - - useCase(listOf(FingerprintCaptureResult(listOf(createFingerprintCaptureResultItem())))) - - coVerify { - eventRepository.addOrUpdateEvent( - withArg { - assertThat(it.payload.fingerprintCaptureIds).isEqualTo(listOf(FINGER_CAPTURE_ID)) - assertThat(it.payload.faceCaptureIds).isEqualTo(listOf(FACE_CAPTURE_ID)) - assertThat(it.payload.faceReferenceId).isEqualTo(FACE_REFERENCE_ID) - }, - ) - } - } - - @Test - fun `Uses face from latest PersonCreationEvent (if more than one) if missing in current callout captures`() = runTest { - coEvery { eventRepository.getEventsInCurrentSession() } returns listOf( - mockk { - every { payload.faceCaptureIds } returns listOf(FACE_CAPTURE_ID) - every { payload.faceReferenceId } returns FACE_REFERENCE_ID - every { payload.createdAt } returns Timestamp(2L) - }, - mockk { - every { payload.faceCaptureIds } returns listOf("anotherFaceCaptureId") - every { payload.faceReferenceId } returns "anotherFaceReferenceId" - every { payload.createdAt } returns Timestamp(1L) - }, - ) - - useCase(listOf(FingerprintCaptureResult(listOf(createFingerprintCaptureResultItem())))) - - coVerify { - eventRepository.addOrUpdateEvent( - withArg { - assertThat(it.payload.fingerprintCaptureIds).isEqualTo(listOf(FINGER_CAPTURE_ID)) - assertThat(it.payload.faceCaptureIds).isEqualTo(listOf(FACE_CAPTURE_ID)) - assertThat(it.payload.faceReferenceId).isEqualTo(FACE_REFERENCE_ID) - }, - ) - } - } - - private fun createFingerprintCaptureResultItem() = FingerprintCaptureResult.Item( - captureEventId = FINGER_CAPTURE_ID, - identifier = IFingerIdentifier.RIGHT_THUMB, - sample = FingerprintCaptureResult.Sample( - IFingerIdentifier.RIGHT_THUMB, - TEMPLATE.toByteArray(), - 0, - null, - "format", - ), - ) - - private fun createFaceCaptureResultItem() = FaceCaptureResult.Item( - captureEventId = FACE_CAPTURE_ID, - index = 0, - sample = FaceCaptureResult.Sample(FACE_CAPTURE_ID, TEMPLATE.toByteArray(), null, "format"), - ) - - companion object { - private const val TEMPLATE = "template" - private const val FINGER_CAPTURE_ID = "fingerprintCaptureId" - private const val FINGER_REFERENCE_ID = "fingerReferenceId" - private const val FACE_CAPTURE_ID = "faceCaptureId" - private const val FACE_REFERENCE_ID = "faceReferenceId" - } -} diff --git a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/MapRefusalOrErrorResultUseCaseTest.kt b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/MapRefusalOrErrorResultUseCaseTest.kt index 5600e54ba5..310279d2d3 100644 --- a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/MapRefusalOrErrorResultUseCaseTest.kt +++ b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/MapRefusalOrErrorResultUseCaseTest.kt @@ -63,7 +63,7 @@ class MapRefusalOrErrorResultUseCaseTest { listOf( FetchSubjectResult(found = true), SetupResult(isSuccess = true), - FaceCaptureResult(emptyList()), + FaceCaptureResult("", emptyList()), ).forEach { result -> assertThat(useCase(result, mockk())).isNull() } } diff --git a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/MapStepsForLastBiometricEnrolUseCaseTest.kt b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/MapStepsForLastBiometricEnrolUseCaseTest.kt index c597a9450d..eb4f9cdbc4 100644 --- a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/MapStepsForLastBiometricEnrolUseCaseTest.kt +++ b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/MapStepsForLastBiometricEnrolUseCaseTest.kt @@ -51,6 +51,7 @@ internal class MapStepsForLastBiometricEnrolUseCaseTest { val result = useCase( listOf( FaceCaptureResult( + "referenceId", results = listOf( FaceCaptureResult.Item(captureEventId = null, index = 0, sample = null), FaceCaptureResult.Item( @@ -70,8 +71,9 @@ internal class MapStepsForLastBiometricEnrolUseCaseTest { assertThat(result.first()).isEqualTo( EnrolLastBiometricStepResult.FaceCaptureResult( - listOf( - FaceTemplateCaptureResult( + referenceId = "referenceId", + results = listOf( + element = FaceTemplateCaptureResult( template = byteArrayOf(), format = "format", ), @@ -98,6 +100,7 @@ internal class MapStepsForLastBiometricEnrolUseCaseTest { val result = useCase( listOf( FingerprintCaptureResult( + "referenceId", results = listOf( FingerprintCaptureResult.Item(null, IFingerIdentifier.LEFT_THUMB, null), FingerprintCaptureResult.Item( @@ -118,7 +121,8 @@ internal class MapStepsForLastBiometricEnrolUseCaseTest { assertThat(result.first()).isEqualTo( EnrolLastBiometricStepResult.FingerprintCaptureResult( - listOf( + referenceId = "referenceId", + results = listOf( FingerTemplateCaptureResult( template = byteArrayOf(), templateQualityScore = 0, diff --git a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/ShouldCreatePersonUseCaseTest.kt b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/ShouldCreatePersonUseCaseTest.kt deleted file mode 100644 index 353a0deba4..0000000000 --- a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/ShouldCreatePersonUseCaseTest.kt +++ /dev/null @@ -1,392 +0,0 @@ -package com.simprints.feature.orchestrator.usecases - -import android.os.Bundle -import com.google.common.truth.Truth.assertThat -import com.simprints.core.domain.tokenization.asTokenizableRaw -import com.simprints.core.tools.time.Timestamp -import com.simprints.face.capture.FaceCaptureResult -import com.simprints.feature.orchestrator.steps.Step -import com.simprints.feature.orchestrator.steps.StepId -import com.simprints.feature.orchestrator.steps.StepStatus -import com.simprints.fingerprint.capture.FingerprintCaptureResult -import com.simprints.infra.config.store.models.GeneralConfiguration -import com.simprints.infra.events.event.domain.models.PersonCreationEvent -import com.simprints.infra.events.session.SessionEventRepository -import com.simprints.infra.orchestration.data.ActionRequest -import com.simprints.infra.orchestration.data.ActionRequestIdentifier -import io.mockk.MockKAnnotations -import io.mockk.coEvery -import io.mockk.impl.annotations.MockK -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Test -import java.io.Serializable - -class ShouldCreatePersonUseCaseTest { - @MockK - lateinit var eventRepository: SessionEventRepository - - private lateinit var useCase: ShouldCreatePersonUseCase - - @Before - fun setUp() { - MockKAnnotations.init(this, relaxed = true) - - useCase = ShouldCreatePersonUseCase(eventRepository) - } - - @Test - fun `Returns false if no action`() = runTest { - assertThat( - useCase( - actionRequest = null, - modalities = setOf(GeneralConfiguration.Modality.FINGERPRINT), - results = listOf( - createStep(StepId.FINGERPRINT_CAPTURE, FingerprintCaptureResult(emptyList())), - ), - ), - ).isFalse() - } - - @Test - fun `Returns true if action is Enrol`() = runTest { - assertThat( - useCase( - actionRequest = ActionRequest.EnrolActionRequest( - actionIdentifier = ActionRequestIdentifier.fromIntentAction(0L, ""), - projectId = "", - userId = "".asTokenizableRaw(), - moduleId = "".asTokenizableRaw(), - biometricDataSource = "", - metadata = "", - unknownExtras = emptyMap(), - ), - modalities = setOf(GeneralConfiguration.Modality.FINGERPRINT), - results = listOf( - createStep(StepId.FINGERPRINT_CAPTURE, FingerprintCaptureResult(emptyList())), - ), - ), - ).isTrue() - } - - @Test - fun `Returns true if action is Identify`() = runTest { - assertThat( - useCase( - actionRequest = ActionRequest.IdentifyActionRequest( - actionIdentifier = ActionRequestIdentifier.fromIntentAction(0L, ""), - projectId = "", - userId = "".asTokenizableRaw(), - moduleId = "".asTokenizableRaw(), - biometricDataSource = "", - metadata = "", - unknownExtras = emptyMap(), - ), - modalities = setOf(GeneralConfiguration.Modality.FINGERPRINT), - results = listOf( - createStep(StepId.FINGERPRINT_CAPTURE, FingerprintCaptureResult(emptyList())), - ), - ), - ).isTrue() - } - - @Test - fun `Returns true if actions is Verify`() = runTest { - assertThat( - useCase( - actionRequest = ActionRequest.VerifyActionRequest( - actionIdentifier = ActionRequestIdentifier.fromIntentAction(0L, ""), - projectId = "", - userId = "".asTokenizableRaw(), - moduleId = "".asTokenizableRaw(), - biometricDataSource = "", - metadata = "", - verifyGuid = "", - unknownExtras = emptyMap(), - ), - modalities = setOf(GeneralConfiguration.Modality.FINGERPRINT), - results = listOf( - createStep(StepId.FINGERPRINT_CAPTURE, FingerprintCaptureResult(emptyList())), - ), - ), - ).isTrue() - } - - @Test - fun `Returns false if followup action is ConfirmIdentity`() = runTest { - assertThat( - useCase( - actionRequest = ActionRequest.ConfirmIdentityActionRequest( - actionIdentifier = ActionRequestIdentifier.fromIntentAction(0L, ""), - projectId = "", - userId = "".asTokenizableRaw(), - sessionId = "", - selectedGuid = "", - metadata = "", - unknownExtras = emptyMap(), - ), - modalities = setOf(GeneralConfiguration.Modality.FINGERPRINT), - results = listOf( - createStep(StepId.FINGERPRINT_CAPTURE, FingerprintCaptureResult(emptyList())), - ), - ), - ).isFalse() - } - - @Test - fun `Returns true if followup action is Enrol last biometric`() = runTest { - assertThat( - useCase( - actionRequest = ActionRequest.EnrolLastBiometricActionRequest( - actionIdentifier = ActionRequestIdentifier.fromIntentAction(0L, ""), - projectId = "", - userId = "".asTokenizableRaw(), - moduleId = "".asTokenizableRaw(), - metadata = "", - sessionId = "", - unknownExtras = emptyMap(), - ), - modalities = setOf(GeneralConfiguration.Modality.FINGERPRINT), - results = listOf( - createStep(StepId.FINGERPRINT_CAPTURE, FingerprintCaptureResult(emptyList())), - ), - ), - ).isTrue() - } - - @Test - fun `Returns false if no modalities`() = runTest { - assertThat( - useCase( - actionRequest = flowAction, - modalities = emptySet(), - results = listOf( - createStep(StepId.FINGERPRINT_CAPTURE, FingerprintCaptureResult(emptyList())), - ), - ), - ).isFalse() - } - - @Test - fun `Returns false when only fingerprint required and no results`() = runTest { - assertThat( - useCase( - actionRequest = flowAction, - modalities = setOf(GeneralConfiguration.Modality.FINGERPRINT), - results = listOf(createStep(StepId.FINGERPRINT_CAPTURE, null)), - ), - ).isFalse() - } - - @Test - fun `Returns false when only face required and no results`() = runTest { - assertThat( - useCase( - actionRequest = flowAction, - modalities = setOf(GeneralConfiguration.Modality.FACE), - results = listOf(createStep(StepId.FACE_CAPTURE, null)), - ), - ).isFalse() - } - - @Test - fun `Returns false when both modalities required but there are no capture results`() = runTest { - assertThat( - useCase( - actionRequest = flowAction, - modalities = setOf( - GeneralConfiguration.Modality.FACE, - GeneralConfiguration.Modality.FINGERPRINT, - ), - results = emptyList(), - ), - ).isFalse() - } - - @Test - fun `Returns true when only fingerprint required and provided`() = runTest { - assertThat( - useCase( - actionRequest = flowAction, - modalities = setOf(GeneralConfiguration.Modality.FINGERPRINT), - results = listOf( - createStep(StepId.FINGERPRINT_CAPTURE, FingerprintCaptureResult(emptyList())), - ), - ), - ).isTrue() - } - - @Test - fun `Returns false when both modalities required and face result missing`() = runTest { - assertThat( - useCase( - actionRequest = flowAction, - modalities = setOf( - GeneralConfiguration.Modality.FACE, - GeneralConfiguration.Modality.FINGERPRINT, - ), - results = listOf( - createStep(StepId.FACE_CAPTURE, null), - createStep(StepId.FINGERPRINT_CAPTURE, FingerprintCaptureResult(emptyList())), - ), - ), - ).isFalse() - } - - @Test - fun `Returns false when both modalities required and fingerprint result missing`() = runTest { - assertThat( - useCase( - actionRequest = flowAction, - modalities = setOf( - GeneralConfiguration.Modality.FACE, - GeneralConfiguration.Modality.FINGERPRINT, - ), - results = listOf( - createStep(StepId.FACE_CAPTURE, FaceCaptureResult(emptyList())), - createStep(StepId.FINGERPRINT_CAPTURE, null), - ), - ), - ).isFalse() - } - - @Test - fun `Returns true when only face required and provided`() = runTest { - assertThat( - useCase( - actionRequest = flowAction, - modalities = setOf(GeneralConfiguration.Modality.FACE), - results = listOf(createStep(StepId.FACE_CAPTURE, FaceCaptureResult(emptyList()))), - ), - ).isTrue() - } - - @Test - fun `Returns true when both modalities required and both results provided`() = runTest { - assertThat( - useCase( - actionRequest = flowAction, - modalities = setOf( - GeneralConfiguration.Modality.FACE, - GeneralConfiguration.Modality.FINGERPRINT, - ), - results = listOf( - createStep(StepId.FACE_CAPTURE, FaceCaptureResult(emptyList())), - createStep(StepId.FINGERPRINT_CAPTURE, FingerprintCaptureResult(emptyList())), - ), - ), - ).isTrue() - } - - @Test - fun `Returns true when there is a PersonCreationEvent but it's missing a face reference that was scheduled for capture`() = runTest { - val faceCaptureResults = listOf(createStep(StepId.FACE_CAPTURE, FaceCaptureResult(emptyList()))) - - val sessionEvents = listOf( - PersonCreationEvent( - startTime = Timestamp(0), - fingerprintCaptureIds = listOf("1", "2"), - fingerprintReferenceId = "123", - faceCaptureIds = null, - faceReferenceId = null, - ), - ) - coEvery { eventRepository.getEventsInCurrentSession() } returns sessionEvents - - assertThat( - useCase( - actionRequest = flowAction, - modalities = setOf( - GeneralConfiguration.Modality.FACE, - GeneralConfiguration.Modality.FINGERPRINT, - ), - results = faceCaptureResults, - ), - ).isTrue() - } - - @Test - fun `Returns true when there is a PersonCreationEvent but it's missing a fingerprint reference that was scheduled for capture`() = - runTest { - val fingerprintCaptureResults = listOf(createStep(StepId.FINGERPRINT_CAPTURE, FingerprintCaptureResult(emptyList()))) - - val sessionEvents = listOf( - PersonCreationEvent( - startTime = Timestamp(0), - fingerprintCaptureIds = null, - fingerprintReferenceId = null, - faceCaptureIds = listOf("1", "2"), - faceReferenceId = "123", - ), - ) - coEvery { eventRepository.getEventsInCurrentSession() } returns sessionEvents - - assertThat( - useCase( - actionRequest = flowAction, - modalities = setOf( - GeneralConfiguration.Modality.FACE, - GeneralConfiguration.Modality.FINGERPRINT, - ), - results = fingerprintCaptureResults, - ), - ).isTrue() - } - - @Test - fun `Returns false when there are already 2 PersonCreation events in the session`() = runTest { - val sessionEvents = listOf( - PersonCreationEvent( - startTime = Timestamp(0), - fingerprintCaptureIds = null, - fingerprintReferenceId = null, - faceCaptureIds = listOf("1", "2"), - faceReferenceId = "123", - ), - PersonCreationEvent( - startTime = Timestamp(0), - fingerprintCaptureIds = listOf("1", "2"), - fingerprintReferenceId = "123", - faceCaptureIds = listOf("1", "2"), - faceReferenceId = "123", - ), - ) - coEvery { eventRepository.getEventsInCurrentSession() } returns sessionEvents - - assertThat( - useCase( - actionRequest = flowAction, - modalities = setOf( - GeneralConfiguration.Modality.FACE, - GeneralConfiguration.Modality.FINGERPRINT, - ), - results = listOf( - createStep(StepId.FACE_CAPTURE, FaceCaptureResult(emptyList())), - createStep(StepId.FINGERPRINT_CAPTURE, FingerprintCaptureResult(emptyList())), - ), - ), - ).isFalse() - } - - private val flowAction = ActionRequest.EnrolActionRequest( - actionIdentifier = ActionRequestIdentifier.fromIntentAction(0L, ""), - projectId = "", - userId = "".asTokenizableRaw(), - moduleId = "".asTokenizableRaw(), - biometricDataSource = "", - metadata = "", - unknownExtras = emptyMap(), - ) - - private fun createStep( - id: Int, - result: Serializable?, - ) = Step( - id = id, - navigationActionId = 1, - destinationId = 1, - payload = Bundle(), - status = StepStatus.COMPLETED, - result = result, - ) -} diff --git a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/CreateEnrolResponseUseCaseTest.kt b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/CreateEnrolResponseUseCaseTest.kt index e3631b6d2b..476ef8afec 100644 --- a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/CreateEnrolResponseUseCaseTest.kt +++ b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/CreateEnrolResponseUseCaseTest.kt @@ -56,8 +56,8 @@ internal class CreateEnrolResponseUseCaseTest { useCase( action, listOf( - FingerprintCaptureResult(emptyList()), - FaceCaptureResult(emptyList()), + FingerprintCaptureResult("", emptyList()), + FaceCaptureResult("", emptyList()), mockk(), ), project diff --git a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/EnrolSubjectUseCaseTest.kt b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/EnrolSubjectUseCaseTest.kt index d3a101c468..29648d3742 100644 --- a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/EnrolSubjectUseCaseTest.kt +++ b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/EnrolSubjectUseCaseTest.kt @@ -8,9 +8,10 @@ import com.simprints.infra.config.store.models.Project import com.simprints.infra.enrolment.records.repository.EnrolmentRecordRepository import com.simprints.infra.enrolment.records.repository.domain.models.Subject import com.simprints.infra.enrolment.records.repository.domain.models.SubjectAction -import com.simprints.infra.events.event.domain.models.EnrolmentEventV2 +import com.simprints.infra.events.event.domain.models.BiometricReferenceCreationEvent +import com.simprints.infra.events.event.domain.models.BiometricReferenceCreationEvent.BiometricReferenceCreationPayload +import com.simprints.infra.events.event.domain.models.EnrolmentEventV4 import com.simprints.infra.events.event.domain.models.PersonCreationEvent -import com.simprints.infra.events.event.domain.models.PersonCreationEvent.PersonCreationPayload import com.simprints.infra.events.session.SessionEventRepository import io.mockk.MockKAnnotations import io.mockk.coEvery @@ -53,9 +54,11 @@ class EnrolSubjectUseCaseTest { } @Test - fun `Adds enrolment V2 event when called`() = runTest { + fun `Adds enrolment V4 event when called`() = runTest { coEvery { eventRepository.getEventsInCurrentSession() } returns listOf( - mockk { every { id } returns "personCreationId" }, + mockk { + every { payload.id } returns "referenceId" + }, ) useCase.invoke( @@ -65,13 +68,13 @@ class EnrolSubjectUseCaseTest { attendantId = "moduleId".asTokenizableRaw(), moduleId = "attendantId".asTokenizableRaw(), ), - project + project, ) coVerify { eventRepository.addOrUpdateEvent( withArg { - assertThat(it).isInstanceOf(EnrolmentEventV2::class.java) + assertThat(it).isInstanceOf(EnrolmentEventV4::class.java) }, ) } @@ -90,7 +93,7 @@ class EnrolSubjectUseCaseTest { attendantId = "moduleId".asTokenizableRaw(), moduleId = "attendantId".asTokenizableRaw(), ), - project + project, ) coVerify { @@ -98,29 +101,28 @@ class EnrolSubjectUseCaseTest { withArg { assertThat(it.first()).isInstanceOf(SubjectAction.Creation::class.java) }, - project + project, ) } } @Test - fun `Uses latest PersonCreationEvent`() = runTest { - val personCreationEvent1 = mockk { - every { id } returns "personCreationEventId1" - every { payload } returns mockk { + fun `Uses all BiometricReferenceCreationEvent`() = runTest { + val biometricReferenceCreationEvent1 = mockk { + every { payload } returns mockk { every { createdAt } returns Timestamp(1) + every { id } returns "referenceId1" } } - val personCreationId2 = "personCreationEventId2" - val personCreationEvent2 = mockk { - every { id } returns personCreationId2 - every { payload } returns mockk { + val biometricReferenceCreationEvent2 = mockk { + every { payload } returns mockk { every { createdAt } returns Timestamp(2) + every { id } returns "referenceId2" } } coEvery { eventRepository.getEventsInCurrentSession() } returns listOf( - personCreationEvent1, - personCreationEvent2, + biometricReferenceCreationEvent2, + biometricReferenceCreationEvent1, ) useCase.invoke( @@ -130,12 +132,16 @@ class EnrolSubjectUseCaseTest { attendantId = "moduleId".asTokenizableRaw(), moduleId = "attendantId".asTokenizableRaw(), ), - project + project, ) coVerify { eventRepository.addOrUpdateEvent( - match { it is EnrolmentEventV2 && it.payload.personCreationEventId == personCreationId2 }, + withArg { + assertThat(it).isInstanceOf(EnrolmentEventV4::class.java) + assertThat((it as EnrolmentEventV4).payload.biometricReferenceIds) + .containsExactly("referenceId1", "referenceId2") + }, ) } } diff --git a/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/FingerprintCaptureResult.kt b/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/FingerprintCaptureResult.kt index c5c2332c57..5fae8aba48 100644 --- a/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/FingerprintCaptureResult.kt +++ b/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/FingerprintCaptureResult.kt @@ -7,6 +7,7 @@ import java.io.Serializable @Keep data class FingerprintCaptureResult( + val referenceId: String, var results: List, ) : Serializable { @Keep diff --git a/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureViewModel.kt b/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureViewModel.kt index cce8bb0df2..daa3ed497e 100644 --- a/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureViewModel.kt +++ b/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureViewModel.kt @@ -24,6 +24,7 @@ import com.simprints.fingerprint.capture.state.CollectFingerprintsState import com.simprints.fingerprint.capture.state.FingerState import com.simprints.fingerprint.capture.state.LiveFeedbackState import com.simprints.fingerprint.capture.state.ScanResult +import com.simprints.fingerprint.capture.usecase.AddBiometricReferenceCreationEventUseCase import com.simprints.fingerprint.capture.usecase.AddCaptureEventsUseCase import com.simprints.fingerprint.capture.usecase.GetNextFingerToAddUseCase import com.simprints.fingerprint.capture.usecase.GetStartStateUseCase @@ -61,6 +62,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import java.util.UUID import javax.inject.Inject import kotlin.math.min @@ -74,6 +76,7 @@ internal class FingerprintCaptureViewModel @Inject constructor( private val getNextFingerToAdd: GetNextFingerToAddUseCase, private val getStartState: GetStartStateUseCase, private val addCaptureEvents: AddCaptureEventsUseCase, + private val addBiometricReferenceCreationEvents: AddBiometricReferenceCreationEventUseCase, private val tracker: FingerprintScanningStatusTracker, private val isNoFingerDetectedLimitReachedUseCase: IsNoFingerDetectedLimitReachedUseCase, @ExternalScope private val externalScope: CoroutineScope, @@ -669,7 +672,10 @@ internal class FingerprintCaptureViewModel @Inject constructor( ), ) } - _finishWithFingerprints.send(FingerprintCaptureResult(resultItems)) + val biometricReferenceId = UUID.randomUUID().toString() + addBiometricReferenceCreationEvents(biometricReferenceId, resultItems.mapNotNull { it.captureEventId }) + + _finishWithFingerprints.send(FingerprintCaptureResult(biometricReferenceId, resultItems)) } private suspend fun saveImageIfExists( diff --git a/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/usecase/AddBiometricReferenceCreationEventUseCase.kt b/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/usecase/AddBiometricReferenceCreationEventUseCase.kt new file mode 100644 index 0000000000..57ad82ef14 --- /dev/null +++ b/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/usecase/AddBiometricReferenceCreationEventUseCase.kt @@ -0,0 +1,29 @@ +package com.simprints.fingerprint.capture.usecase + +import com.simprints.core.SessionCoroutineScope +import com.simprints.core.tools.time.TimeHelper +import com.simprints.infra.events.event.domain.models.BiometricReferenceCreationEvent +import com.simprints.infra.events.session.SessionEventRepository +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import javax.inject.Inject + +internal class AddBiometricReferenceCreationEventUseCase @Inject constructor( + private val timeHelper: TimeHelper, + private val eventRepository: SessionEventRepository, + @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, +) { + operator fun invoke( + referenceId: String, + captureIds: List, + ) = sessionCoroutineScope.launch { + eventRepository.addOrUpdateEvent( + BiometricReferenceCreationEvent( + startTime = timeHelper.now(), + referenceId = referenceId, + modality = BiometricReferenceCreationEvent.BiometricReferenceModality.FINGERPRINT, + captureIds = captureIds, + ), + ) + } +} diff --git a/fingerprint/capture/src/test/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureViewModelTest.kt b/fingerprint/capture/src/test/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureViewModelTest.kt index a9efe989e4..75dd864814 100644 --- a/fingerprint/capture/src/test/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureViewModelTest.kt +++ b/fingerprint/capture/src/test/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureViewModelTest.kt @@ -16,6 +16,7 @@ import com.simprints.fingerprint.capture.state.CollectFingerprintsState import com.simprints.fingerprint.capture.state.FingerState import com.simprints.fingerprint.capture.state.LiveFeedbackState import com.simprints.fingerprint.capture.state.ScanResult +import com.simprints.fingerprint.capture.usecase.AddBiometricReferenceCreationEventUseCase import com.simprints.fingerprint.capture.usecase.AddCaptureEventsUseCase import com.simprints.fingerprint.capture.usecase.GetNextFingerToAddUseCase import com.simprints.fingerprint.capture.usecase.GetStartStateUseCase @@ -103,6 +104,9 @@ class FingerprintCaptureViewModelTest { @MockK private lateinit var addCaptureEventsUseCase: AddCaptureEventsUseCase + @MockK + private lateinit var addBiometricReferenceCreatedEvents: AddBiometricReferenceCreationEventUseCase + @MockK private lateinit var isNoFingerDetectedLimitReachedUseCase: IsNoFingerDetectedLimitReachedUseCase @@ -153,6 +157,7 @@ class FingerprintCaptureViewModelTest { getNextFingerToAdd = getNextFingerToAddUseCase, getStartState = getStartStateUseCase, addCaptureEvents = addCaptureEventsUseCase, + addBiometricReferenceCreationEvents = addBiometricReferenceCreatedEvents, tracker = tracker, isNoFingerDetectedLimitReachedUseCase = isNoFingerDetectedLimitReachedUseCase, externalScope = CoroutineScope(testCoroutineRule.testCoroutineDispatcher), @@ -204,7 +209,7 @@ class FingerprintCaptureViewModelTest { } @Test - fun `test scanner supports image transfer then isImageTransferRequired should be equal to scanningTimeoutMs + imageTransferTimeoutMs`() = + fun `test scanner supports image transfer then isImageTransferRequired should be scanningTimeoutMs + imageTransferTimeoutMs`() = runTest { withImageTransfer() every { scanner.isImageTransferSupported() } returns true @@ -515,6 +520,7 @@ class FingerprintCaptureViewModelTest { vm.handleConfirmFingerprintsAndContinue() coVerify(exactly = 4) { saveImageUseCase.invoke(any(), any(), any(), any()) } + coVerify { addBiometricReferenceCreatedEvents.invoke(any(), any()) } vm.finishWithFingerprints.assertEventReceivedWithContentAssertions { actualFingerprints -> assertThat(actualFingerprints?.results).hasSize(FOUR_FINGERS_IDS.size) assertThat(actualFingerprints?.results?.map { it.identifier }).containsExactlyElementsIn( @@ -572,6 +578,7 @@ class FingerprintCaptureViewModelTest { vm.handleConfirmFingerprintsAndContinue() coVerify(exactly = 2) { saveImageUseCase.invoke(any(), any(), any(), any()) } + coVerify { addBiometricReferenceCreatedEvents.invoke(any(), any()) } vm.finishWithFingerprints.assertEventReceivedWithContentAssertions { actualFingerprints -> assertThat(actualFingerprints?.results).hasSize(TWO_FINGERS_IDS.size) @@ -628,6 +635,7 @@ class FingerprintCaptureViewModelTest { coVerify(exactly = 2) { addCaptureEventsUseCase.invoke(any(), any(), any(), any()) } vm.handleConfirmFingerprintsAndContinue() coVerify(exactly = 0) { saveImageUseCase.invoke(any(), any(), any(), any()) } + coVerify { addBiometricReferenceCreatedEvents.invoke(any(), any()) } vm.finishWithFingerprints.assertEventReceivedWithContentAssertions { actualFingerprints -> assertThat(actualFingerprints?.results).hasSize(TWO_FINGERS_IDS.size) diff --git a/fingerprint/capture/src/test/java/com/simprints/fingerprint/capture/usecase/AddBiometricReferenceCreationEventUseCaseTest.kt b/fingerprint/capture/src/test/java/com/simprints/fingerprint/capture/usecase/AddBiometricReferenceCreationEventUseCaseTest.kt new file mode 100644 index 0000000000..27e3671d60 --- /dev/null +++ b/fingerprint/capture/src/test/java/com/simprints/fingerprint/capture/usecase/AddBiometricReferenceCreationEventUseCaseTest.kt @@ -0,0 +1,56 @@ +package com.simprints.fingerprint.capture.usecase + +import com.google.common.truth.Truth.assertThat +import com.simprints.core.tools.time.TimeHelper +import com.simprints.infra.events.event.domain.models.BiometricReferenceCreationEvent +import com.simprints.infra.events.session.SessionEventRepository +import com.simprints.testtools.common.coroutines.TestCoroutineRule +import io.mockk.MockKAnnotations +import io.mockk.coJustRun +import io.mockk.coVerify +import io.mockk.impl.annotations.MockK +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +internal class AddBiometricReferenceCreationEventUseCaseTest { + @get:Rule + val testCoroutineRule = TestCoroutineRule() + + @MockK + lateinit var timeHelper: TimeHelper + + @MockK + lateinit var eventRepo: SessionEventRepository + + private lateinit var useCase: AddBiometricReferenceCreationEventUseCase + + @Before + fun setUp() { + MockKAnnotations.init(this, relaxed = true) + + coJustRun { eventRepo.addOrUpdateEvent(any()) } + + useCase = AddBiometricReferenceCreationEventUseCase( + timeHelper, + eventRepo, + CoroutineScope(testCoroutineRule.testCoroutineDispatcher), + ) + } + + @Test + fun `Adds reference creation event`() = runTest { + useCase("id", listOf("id1", "id2", "id3")) + coVerify { + eventRepo.addOrUpdateEvent( + withArg { + assertThat(it).isInstanceOf(BiometricReferenceCreationEvent::class.java) + assertThat((it.payload as BiometricReferenceCreationEvent.BiometricReferenceCreationPayload).modality) + .isEqualTo(BiometricReferenceCreationEvent.BiometricReferenceModality.FINGERPRINT) + }, + ) + } + } +} diff --git a/infra/core/src/main/java/com/simprints/core/domain/face/FaceSample.kt b/infra/core/src/main/java/com/simprints/core/domain/face/FaceSample.kt index a4e931f54a..fbf1938b6b 100644 --- a/infra/core/src/main/java/com/simprints/core/domain/face/FaceSample.kt +++ b/infra/core/src/main/java/com/simprints/core/domain/face/FaceSample.kt @@ -10,6 +10,7 @@ import java.util.UUID data class FaceSample( val template: ByteArray, val format: String, + val referenceId: String, val id: String = UUID.randomUUID().toString(), ) : Parcelable { override fun equals(other: Any?): Boolean { @@ -25,19 +26,3 @@ data class FaceSample( override fun hashCode(): Int = template.contentHashCode() } - -// Generates a unique id for a list of samples. -// It concats the templates (sorted by quality score) and creates a UUID from that. Or null if there -// are not templates -fun List.uniqueId(): String? = if (this.isNotEmpty()) { - UUID - .nameUUIDFromBytes( - concatTemplates(), - ).toString() -} else { - null -} - -fun List.concatTemplates(): ByteArray = this.sortedBy { it.template.contentHashCode() }.fold(byteArrayOf()) { acc, sample -> - acc + sample.template -} diff --git a/infra/core/src/main/java/com/simprints/core/domain/fingerprint/FingerprintSample.kt b/infra/core/src/main/java/com/simprints/core/domain/fingerprint/FingerprintSample.kt index f76c00913c..5504dfa29b 100644 --- a/infra/core/src/main/java/com/simprints/core/domain/fingerprint/FingerprintSample.kt +++ b/infra/core/src/main/java/com/simprints/core/domain/fingerprint/FingerprintSample.kt @@ -12,6 +12,7 @@ data class FingerprintSample( val template: ByteArray, val templateQualityScore: Int, val format: String, + val referenceId: String, val id: String = UUID.randomUUID().toString(), ) : Parcelable { override fun equals(other: Any?): Boolean { @@ -34,15 +35,3 @@ data class FingerprintSample( return result } } - -// Generates a unique id for a list of samples. -// It concats the templates (sorted by quality score) and creates a UUID from that. -fun List.uniqueId(): String? = if (this.isNotEmpty()) { - UUID.nameUUIDFromBytes(concatTemplates()).toString() -} else { - null -} - -fun List.concatTemplates(): ByteArray = this.sortedBy { it.templateQualityScore }.fold(byteArrayOf()) { acc, sample -> - acc + sample.template -} diff --git a/infra/core/src/test/java/com/simprints/core/domain/face/FaceSampleTest.kt b/infra/core/src/test/java/com/simprints/core/domain/face/FaceSampleTest.kt deleted file mode 100644 index 44e637efca..0000000000 --- a/infra/core/src/test/java/com/simprints/core/domain/face/FaceSampleTest.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.simprints.core.domain.face - -import com.google.common.truth.Truth.assertThat -import org.junit.Test - -class FaceSampleTest { - @Test - fun testUniqueId() { - assertThat(listOf().uniqueId()).isNull() - assertThat( - listOf( - FaceSample( - template = byteArrayOf(1, 2), - format = "", - ), - ).uniqueId(), - ).isNotNull() - } - - @Test - fun testConcatTemplates() { - val samples = listOf( - FaceSample( - template = byteArrayOf(2), - format = "", - ), - FaceSample( - template = byteArrayOf(1), - format = "", - ), - FaceSample( - template = byteArrayOf(3), - format = "", - ), - ) - assertThat(samples.concatTemplates()).isEqualTo(byteArrayOf(1, 2, 3)) - } -} diff --git a/infra/core/src/test/java/com/simprints/core/domain/fingerprint/FingerprintSampleTest.kt b/infra/core/src/test/java/com/simprints/core/domain/fingerprint/FingerprintSampleTest.kt deleted file mode 100644 index 41a8e0b227..0000000000 --- a/infra/core/src/test/java/com/simprints/core/domain/fingerprint/FingerprintSampleTest.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.simprints.core.domain.fingerprint - -import com.google.common.truth.Truth.assertThat -import org.junit.Test - -class FingerprintSampleTest { - @Test - fun testUniqueId() { - assertThat(listOf().uniqueId()).isNull() - assertThat( - listOf( - FingerprintSample( - fingerIdentifier = IFingerIdentifier.LEFT_3RD_FINGER, - template = byteArrayOf(1, 2), - templateQualityScore = 1, - format = "", - ), - ).uniqueId(), - ).isNotNull() - } - - @Test - fun testConcatTemplates() { - val fingerprintSample = listOf( - FingerprintSample( - fingerIdentifier = IFingerIdentifier.LEFT_3RD_FINGER, - template = byteArrayOf(31, 32), - templateQualityScore = 3, - format = "", - ), - FingerprintSample( - fingerIdentifier = IFingerIdentifier.LEFT_3RD_FINGER, - template = byteArrayOf(1, 2), - templateQualityScore = 1, - format = "", - ), - FingerprintSample( - fingerIdentifier = IFingerIdentifier.LEFT_3RD_FINGER, - template = byteArrayOf(21, 22), - templateQualityScore = 2, - format = "", - ), - ) - assertThat(fingerprintSample.concatTemplates()).isEqualTo(byteArrayOf(1, 2, 21, 22, 31, 32)) - } -} diff --git a/infra/enrolment-records/realm-store/src/main/java/com/simprints/infra/enrolment/records/realm/store/config/RealmConfig.kt b/infra/enrolment-records/realm-store/src/main/java/com/simprints/infra/enrolment/records/realm/store/config/RealmConfig.kt index 4c01402903..c23eb21c38 100644 --- a/infra/enrolment-records/realm-store/src/main/java/com/simprints/infra/enrolment/records/realm/store/config/RealmConfig.kt +++ b/infra/enrolment-records/realm-store/src/main/java/com/simprints/infra/enrolment/records/realm/store/config/RealmConfig.kt @@ -36,6 +36,6 @@ class RealmConfig @Inject constructor() { .build() companion object { - private const val REALM_SCHEMA_VERSION: Long = 15 + private const val REALM_SCHEMA_VERSION: Long = 16 } } diff --git a/infra/enrolment-records/realm-store/src/main/java/com/simprints/infra/enrolment/records/realm/store/models/DbFaceSample.kt b/infra/enrolment-records/realm-store/src/main/java/com/simprints/infra/enrolment/records/realm/store/models/DbFaceSample.kt index b8b85674eb..60e7bff875 100644 --- a/infra/enrolment-records/realm-store/src/main/java/com/simprints/infra/enrolment/records/realm/store/models/DbFaceSample.kt +++ b/infra/enrolment-records/realm-store/src/main/java/com/simprints/infra/enrolment/records/realm/store/models/DbFaceSample.kt @@ -10,6 +10,7 @@ import io.realm.kotlin.types.annotations.PrimaryKey class DbFaceSample : RealmObject { @PrimaryKey var id: String = "" + var referenceId = "" var template: ByteArray = byteArrayOf() var format: String = "" } diff --git a/infra/enrolment-records/realm-store/src/main/java/com/simprints/infra/enrolment/records/realm/store/models/DbFingerprintSample.kt b/infra/enrolment-records/realm-store/src/main/java/com/simprints/infra/enrolment/records/realm/store/models/DbFingerprintSample.kt index cb109a80f6..435ce2fec8 100644 --- a/infra/enrolment-records/realm-store/src/main/java/com/simprints/infra/enrolment/records/realm/store/models/DbFingerprintSample.kt +++ b/infra/enrolment-records/realm-store/src/main/java/com/simprints/infra/enrolment/records/realm/store/models/DbFingerprintSample.kt @@ -10,6 +10,7 @@ import io.realm.kotlin.types.annotations.PrimaryKey class DbFingerprintSample : RealmObject { @PrimaryKey var id: String = "" + var referenceId = "" var fingerIdentifier: Int = -1 var template: ByteArray = byteArrayOf() var templateQualityScore: Int = -1 diff --git a/infra/enrolment-records/realm-store/src/main/java/com/simprints/infra/enrolment/records/realm/store/models/DbSubject.kt b/infra/enrolment-records/realm-store/src/main/java/com/simprints/infra/enrolment/records/realm/store/models/DbSubject.kt index ff507dec32..612e8bb8a7 100644 --- a/infra/enrolment-records/realm-store/src/main/java/com/simprints/infra/enrolment/records/realm/store/models/DbSubject.kt +++ b/infra/enrolment-records/realm-store/src/main/java/com/simprints/infra/enrolment/records/realm/store/models/DbSubject.kt @@ -23,8 +23,6 @@ class DbSubject : RealmObject { var fingerprintSamples: RealmList = realmListOf() var faceSamples: RealmList = realmListOf() - @Deprecated("See SubjectToEventDbMigrationManagerImpl doc") - var toSync: Boolean = false var isAttendantIdTokenized: Boolean = false var isModuleIdTokenized: Boolean = false } diff --git a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/EnrolmentRecordRepositoryImpl.kt b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/EnrolmentRecordRepositoryImpl.kt index 9fbd8460b1..0c89d95022 100644 --- a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/EnrolmentRecordRepositoryImpl.kt +++ b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/EnrolmentRecordRepositoryImpl.kt @@ -129,8 +129,8 @@ internal class EnrolmentRecordRepositoryImpl( dataSource: BiometricDataSource, project: Project, onCandidateLoaded: () -> Unit, - ): List = - fromIdentityDataSource(dataSource).loadFingerprintIdentities(query, range, dataSource, project, onCandidateLoaded) + ): List = fromIdentityDataSource(dataSource) + .loadFingerprintIdentities(query, range, dataSource, project, onCandidateLoaded) override suspend fun loadFaceIdentities( query: SubjectQuery, @@ -138,7 +138,8 @@ internal class EnrolmentRecordRepositoryImpl( dataSource: BiometricDataSource, project: Project, onCandidateLoaded: () -> Unit, - ): List = fromIdentityDataSource(dataSource).loadFaceIdentities(query, range, dataSource, project, onCandidateLoaded) + ): List = fromIdentityDataSource(dataSource) + .loadFaceIdentities(query, range, dataSource, project, onCandidateLoaded) private fun fromIdentityDataSource(dataSource: BiometricDataSource) = when (dataSource) { is BiometricDataSource.Simprints -> localDataSource diff --git a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/commcare/CommCareIdentityDataSource.kt b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/commcare/CommCareIdentityDataSource.kt index 1f204c9f9e..cee89e791b 100644 --- a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/commcare/CommCareIdentityDataSource.kt +++ b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/commcare/CommCareIdentityDataSource.kt @@ -67,6 +67,7 @@ internal class CommCareIdentityDataSource @Inject constructor( templateQualityScore = fingerprintTemplate.quality, template = encoder.base64ToBytes(fingerprintTemplate.template), format = fingerprintReference.format, + referenceId = fingerprintReference.id, ) } }, @@ -144,6 +145,7 @@ internal class CommCareIdentityDataSource @Inject constructor( FaceSample( template = encoder.base64ToBytes(faceTemplate.template), format = faceReference.format, + referenceId = faceReference.id, ) } }, diff --git a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/Subject.kt b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/Subject.kt index 9270a5c566..23d307709b 100644 --- a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/Subject.kt +++ b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/Subject.kt @@ -17,6 +17,4 @@ data class Subject( val updatedAt: Date? = null, var fingerprintSamples: List = emptyList(), var faceSamples: List = emptyList(), - @Deprecated("See SubjectToEventDbMigrationManagerImpl doc") - val toSync: Boolean = false, ) : Parcelable diff --git a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/SubjectAction.kt b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/SubjectAction.kt index 5134ada84e..421a6f11c6 100644 --- a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/SubjectAction.kt +++ b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/SubjectAction.kt @@ -1,6 +1,8 @@ package com.simprints.infra.enrolment.records.repository.domain.models import androidx.annotation.Keep +import com.simprints.core.domain.face.FaceSample +import com.simprints.core.domain.fingerprint.FingerprintSample @Keep sealed class SubjectAction { @@ -8,6 +10,13 @@ sealed class SubjectAction { val subject: Subject, ) : SubjectAction() + data class Update( + val subjectId: String, + val faceSamplesToAdd: List, + val fingerprintSamplesToAdd: List, + val referenceIdsToRemove: List, + ) : SubjectAction() + data class Deletion( val subjectId: String, ) : SubjectAction() diff --git a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/EnrolmentRecordLocalDataSourceImpl.kt b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/EnrolmentRecordLocalDataSourceImpl.kt index 7456378937..086bceca59 100644 --- a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/EnrolmentRecordLocalDataSourceImpl.kt +++ b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/EnrolmentRecordLocalDataSourceImpl.kt @@ -18,7 +18,9 @@ import com.simprints.infra.enrolment.records.repository.local.models.fromDbToDom import com.simprints.infra.enrolment.records.repository.local.models.fromDomainToDb import com.simprints.infra.logging.LoggingConstants.CrashReportTag.REALM_DB import com.simprints.infra.logging.Simber +import io.realm.kotlin.MutableRealm import io.realm.kotlin.UpdatePolicy +import io.realm.kotlin.ext.toRealmList import io.realm.kotlin.query.RealmQuery import io.realm.kotlin.query.Sort import io.realm.kotlin.query.find @@ -129,23 +131,59 @@ internal class EnrolmentRecordLocalDataSourceImpl @Inject constructor( actions.forEach { action -> when (action) { is SubjectAction.Creation -> { - val tokenizedModuleId = action.subject.moduleId.tokenizeIfNecessary(TokenKeyType.ModuleId, project) - val tokenizedAttendantId = action.subject.attendantId.tokenizeIfNecessary(TokenKeyType.AttendantId, project) - realm.copyToRealm( - action.subject.copy(moduleId = tokenizedModuleId, attendantId = tokenizedAttendantId).fromDomainToDb(), - updatePolicy = UpdatePolicy.ALL, - ) + val newSubject = action.subject + .copy( + moduleId = action.subject.moduleId.tokenizeIfNecessary(TokenKeyType.ModuleId, project), + attendantId = action.subject.attendantId.tokenizeIfNecessary(TokenKeyType.AttendantId, project), + ).fromDomainToDb() + val dbSubject: DbSubject? = realm.findSubject(newSubject.subjectId) + + if (dbSubject != null) { + // When updating an existing subject, we must manually delete outdated samples + val fingerprintSampleIds = newSubject.fingerprintSamples.map { it.id }.toSet() + dbSubject.fingerprintSamples + .filterNot { it.id in fingerprintSampleIds } + .takeIf { it.isNotEmpty() } + ?.forEach { realm.delete(it) } + + val faceSampleIds = newSubject.faceSamples.map { it.id }.toSet() + dbSubject.faceSamples + .filterNot { it.id in faceSampleIds } + .takeIf { it.isNotEmpty() } + ?.forEach { realm.delete(it) } + } + + realm.copyToRealm(newSubject, updatePolicy = UpdatePolicy.ALL) + } + + is SubjectAction.Update -> { + val dbSubject: DbSubject? = realm.findSubject(RealmUUID.from(action.subjectId)) + if (dbSubject != null) { + val referencesToDelete = action.referenceIdsToRemove.toSet() // to make lookup O(1) + val faceSamplesMap = dbSubject.faceSamples.groupBy { it.referenceId in referencesToDelete } + val fingerprintSamplesMap = dbSubject.fingerprintSamples.groupBy { it.referenceId in referencesToDelete } + + // Append new samples to the list of samples that remain after removing + dbSubject.faceSamples = ( + faceSamplesMap[false].orEmpty() + action.faceSamplesToAdd.map { it.fromDomainToDb() } + ).toRealmList() + dbSubject.fingerprintSamples = ( + fingerprintSamplesMap[false].orEmpty() + action.fingerprintSamplesToAdd.map { it.fromDomainToDb() } + ).toRealmList() + + faceSamplesMap[true]?.forEach { realm.delete(it) } + fingerprintSamplesMap[true]?.forEach { realm.delete(it) } + realm.copyToRealm(dbSubject, updatePolicy = UpdatePolicy.ALL) + } else { + Simber.i("[SubjectLocalDataSourceImpl] Subject not found for update", tag = REALM_DB) + } } is SubjectAction.Deletion -> realm.delete( realm .query(DbSubject::class) - .buildRealmQueryForSubject( - query = SubjectQuery( - subjectId = - action.subjectId, - ), - ).find(), + .buildRealmQueryForSubject(query = SubjectQuery(subjectId = action.subjectId)) + .find(), ) } } @@ -165,6 +203,9 @@ internal class EnrolmentRecordLocalDataSourceImpl @Inject constructor( is TokenizableString.Tokenized -> this } + private fun MutableRealm.findSubject(subjectId: RealmUUID): DbSubject? = + query(DbSubject::class).query("$SUBJECT_ID_FIELD == $0", subjectId).first().find() + private fun RealmQuery.buildRealmQueryForSubject(query: SubjectQuery): RealmQuery { var realmQuery = this diff --git a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/DbFaceSample.kt b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/DbFaceSample.kt index e115efce86..00ef14df71 100644 --- a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/DbFaceSample.kt +++ b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/DbFaceSample.kt @@ -7,10 +7,12 @@ internal fun DbFaceSample.fromDbToDomain(): FaceSample = FaceSample( id = id, template = template, format = format, + referenceId = referenceId, ) internal fun FaceSample.fromDomainToDb(): DbFaceSample = DbFaceSample().also { sample -> sample.id = id + sample.referenceId = referenceId sample.template = template sample.format = format } diff --git a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/DbFingerprintSample.kt b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/DbFingerprintSample.kt index 74acd52c72..6c7c248558 100644 --- a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/DbFingerprintSample.kt +++ b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/DbFingerprintSample.kt @@ -10,10 +10,12 @@ internal fun DbFingerprintSample.fromDbToDomain(): FingerprintSample = Fingerpri template = template, templateQualityScore = templateQualityScore, format = format, + referenceId = referenceId, ) internal fun FingerprintSample.fromDomainToDb(): DbFingerprintSample = DbFingerprintSample().also { sample -> sample.id = id + sample.referenceId = referenceId sample.fingerIdentifier = fingerIdentifier.ordinal sample.template = template sample.templateQualityScore = templateQualityScore diff --git a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/DbSubject.kt b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/DbSubject.kt index daaa1be6c0..7cace06d8c 100644 --- a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/DbSubject.kt +++ b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/DbSubject.kt @@ -27,7 +27,6 @@ internal fun DbSubject.fromDbToDomain(): Subject { moduleId = moduleId, createdAt = createdAt?.toDate(), updatedAt = updatedAt?.toDate(), - toSync = toSync, fingerprintSamples = fingerprintSamples.map(DbFingerprintSample::fromDbToDomain), faceSamples = faceSamples.map(DbFaceSample::fromDbToDomain), ) @@ -40,7 +39,6 @@ internal fun Subject.fromDomainToDb(): DbSubject = DbSubject().also { subject -> subject.moduleId = moduleId.value subject.createdAt = createdAt?.toRealmInstant() subject.updatedAt = updatedAt?.toRealmInstant() - subject.toSync = toSync subject.fingerprintSamples = fingerprintSamples.map(FingerprintSample::fromDomainToDb).toRealmList() subject.faceSamples = faceSamples.map(FaceSample::fromDomainToDb).toRealmList() diff --git a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/remote/models/face/ApiFaceReference.kt b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/remote/models/face/ApiFaceReference.kt index d15cb9b5cd..6eb6e5f811 100644 --- a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/remote/models/face/ApiFaceReference.kt +++ b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/remote/models/face/ApiFaceReference.kt @@ -2,10 +2,8 @@ package com.simprints.infra.enrolment.records.repository.remote.models.face import androidx.annotation.Keep import com.simprints.core.domain.face.FaceSample -import com.simprints.core.domain.face.concatTemplates import com.simprints.core.tools.utils.EncodingUtils import com.simprints.infra.enrolment.records.repository.remote.models.ApiBiometricReference -import java.util.UUID @Keep internal data class ApiFaceReference( @@ -17,10 +15,8 @@ internal data class ApiFaceReference( internal fun List.toApi(encoder: EncodingUtils): ApiFaceReference? = if (isNotEmpty()) { ApiFaceReference( - UUID.nameUUIDFromBytes(concatTemplates()).toString(), - map { - ApiFaceTemplate(encoder.byteArrayToBase64(it.template)) - }, + first().referenceId, + map { ApiFaceTemplate(encoder.byteArrayToBase64(it.template)) }, first().format, ) } else { diff --git a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/remote/models/fingerprint/ApiFingerprintReference.kt b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/remote/models/fingerprint/ApiFingerprintReference.kt index 27d5dbcc5f..826679e85c 100644 --- a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/remote/models/fingerprint/ApiFingerprintReference.kt +++ b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/remote/models/fingerprint/ApiFingerprintReference.kt @@ -2,10 +2,8 @@ package com.simprints.infra.enrolment.records.repository.remote.models.fingerpri import androidx.annotation.Keep import com.simprints.core.domain.fingerprint.FingerprintSample -import com.simprints.core.domain.fingerprint.concatTemplates import com.simprints.core.tools.utils.EncodingUtils import com.simprints.infra.enrolment.records.repository.remote.models.ApiBiometricReference -import java.util.UUID @Keep internal data class ApiFingerprintReference( @@ -17,7 +15,7 @@ internal data class ApiFingerprintReference( internal fun List.toApi(encoder: EncodingUtils): ApiFingerprintReference? = if (isNotEmpty()) { ApiFingerprintReference( - UUID.nameUUIDFromBytes(concatTemplates()).toString(), + first().referenceId, map { ApiFingerprintTemplate( it.templateQualityScore, diff --git a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/EnrolmentRecordRepositoryImplTest.kt b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/EnrolmentRecordRepositoryImplTest.kt index 67b600aa85..24484216ad 100644 --- a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/EnrolmentRecordRepositoryImplTest.kt +++ b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/EnrolmentRecordRepositoryImplTest.kt @@ -50,7 +50,7 @@ class EnrolmentRecordRepositoryImplTest { } } - private val onCandidateLoaded: () -> Unit = {} + private val onCandidateLoaded: () -> Unit = {} private val tokenizationProcessor = mockk() private val localDataSource = mockk(relaxed = true) private val commCareDataSource = mockk(relaxed = true) @@ -278,18 +278,34 @@ class EnrolmentRecordRepositoryImplTest { val expectedSubjectQuery = SubjectQuery() val expectedRange = 0..10 val expectedFingerprintIdentities = listOf() - coEvery { localDataSource.loadFingerprintIdentities(expectedSubjectQuery, expectedRange, any(), project, onCandidateLoaded) } returns expectedFingerprintIdentities + coEvery { + localDataSource.loadFingerprintIdentities( + expectedSubjectQuery, + expectedRange, + any(), + project, + onCandidateLoaded, + ) + } returns expectedFingerprintIdentities val fingerprintIdentities = repository.loadFingerprintIdentities( query = expectedSubjectQuery, range = expectedRange, dataSource = BiometricDataSource.Simprints, project = project, - onCandidateLoaded = onCandidateLoaded + onCandidateLoaded = onCandidateLoaded, ) assert(fingerprintIdentities == expectedFingerprintIdentities) - coVerify(exactly = 1) { localDataSource.loadFingerprintIdentities(expectedSubjectQuery, expectedRange, any(), project, onCandidateLoaded) } + coVerify(exactly = 1) { + localDataSource.loadFingerprintIdentities( + expectedSubjectQuery, + expectedRange, + any(), + project, + onCandidateLoaded, + ) + } } @Test @@ -297,18 +313,34 @@ class EnrolmentRecordRepositoryImplTest { val expectedSubjectQuery = SubjectQuery() val expectedRange = 0..10 val expectedFingerprintIdentities = listOf() - coEvery { commCareDataSource.loadFingerprintIdentities(expectedSubjectQuery, expectedRange, any(), project, onCandidateLoaded) } returns expectedFingerprintIdentities + coEvery { + commCareDataSource.loadFingerprintIdentities( + expectedSubjectQuery, + expectedRange, + any(), + project, + onCandidateLoaded, + ) + } returns expectedFingerprintIdentities val fingerprintIdentities = repository.loadFingerprintIdentities( query = expectedSubjectQuery, range = expectedRange, dataSource = BiometricDataSource.CommCare(""), project = project, - onCandidateLoaded = onCandidateLoaded + onCandidateLoaded = onCandidateLoaded, ) assert(fingerprintIdentities == expectedFingerprintIdentities) - coVerify(exactly = 1) { commCareDataSource.loadFingerprintIdentities(expectedSubjectQuery, expectedRange, any(), project, onCandidateLoaded) } + coVerify(exactly = 1) { + commCareDataSource.loadFingerprintIdentities( + expectedSubjectQuery, + expectedRange, + any(), + project, + onCandidateLoaded, + ) + } } @Test @@ -316,14 +348,22 @@ class EnrolmentRecordRepositoryImplTest { val expectedSubjectQuery = SubjectQuery() val expectedRange = 0..10 val expectedFaceIdentities = listOf() - coEvery { localDataSource.loadFaceIdentities(expectedSubjectQuery, expectedRange, any(), project, onCandidateLoaded) } returns expectedFaceIdentities + coEvery { + localDataSource.loadFaceIdentities( + expectedSubjectQuery, + expectedRange, + any(), + project, + onCandidateLoaded, + ) + } returns expectedFaceIdentities val faceIdentities = repository.loadFaceIdentities( query = expectedSubjectQuery, range = expectedRange, dataSource = BiometricDataSource.Simprints, project = project, - onCandidateLoaded = onCandidateLoaded + onCandidateLoaded = onCandidateLoaded, ) assert(faceIdentities == expectedFaceIdentities) @@ -335,17 +375,27 @@ class EnrolmentRecordRepositoryImplTest { val expectedSubjectQuery = SubjectQuery() val expectedRange = 0..10 val expectedFaceIdentities = listOf() - coEvery { commCareDataSource.loadFaceIdentities(expectedSubjectQuery, expectedRange, any(), project, onCandidateLoaded) } returns expectedFaceIdentities + coEvery { + commCareDataSource.loadFaceIdentities(expectedSubjectQuery, expectedRange, any(), project, onCandidateLoaded) + } returns expectedFaceIdentities val faceIdentities = repository.loadFaceIdentities( query = expectedSubjectQuery, range = expectedRange, dataSource = BiometricDataSource.CommCare(""), project = project, - onCandidateLoaded = onCandidateLoaded + onCandidateLoaded = onCandidateLoaded, ) assert(faceIdentities == expectedFaceIdentities) - coVerify(exactly = 1) { commCareDataSource.loadFaceIdentities(expectedSubjectQuery, expectedRange, any(), project, onCandidateLoaded) } + coVerify(exactly = 1) { + commCareDataSource.loadFaceIdentities( + expectedSubjectQuery, + expectedRange, + any(), + project, + onCandidateLoaded, + ) + } } } diff --git a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/commcare/CommCareIdentityDataSourceTest.kt b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/commcare/CommCareIdentityDataSourceTest.kt index 6ed9d38faa..33c4cafc41 100644 --- a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/commcare/CommCareIdentityDataSourceTest.kt +++ b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/commcare/CommCareIdentityDataSourceTest.kt @@ -55,12 +55,14 @@ class CommCareIdentityDataSourceTest { templateQualityScore = 99, template = byteArrayOf(), format = "ISO_19794_2", + referenceId = "referenceId", ), FingerprintSample( fingerIdentifier = LEFT_INDEX_FINGER, templateQualityScore = 88, template = byteArrayOf(), format = "ISO_19794_2", + referenceId = "referenceId", ), ), ), @@ -72,12 +74,14 @@ class CommCareIdentityDataSourceTest { templateQualityScore = 77, template = byteArrayOf(), format = "ISO_19794_2", + referenceId = "referenceId", ), FingerprintSample( fingerIdentifier = LEFT_INDEX_FINGER, templateQualityScore = 66, template = byteArrayOf(), format = "ISO_19794_2", + referenceId = "referenceId", ), ), ), @@ -85,11 +89,23 @@ class CommCareIdentityDataSourceTest { val expectedFaceIdentities = listOf( FaceIdentity( subjectId = "b26c91bc-b307-4131-80c3-55090ba5dbf2", - faces = listOf(FaceSample(template = byteArrayOf(), format = "ROC_1_23")), + faces = listOf( + FaceSample( + template = byteArrayOf(), + format = "ROC_1_23", + referenceId = "referenceId", + ), + ), ), FaceIdentity( subjectId = "a961fcb4-8573-4270-a1b2-088e88275b00", - faces = listOf(FaceSample(template = byteArrayOf(), format = "ROC_3")), + faces = listOf( + FaceSample( + template = byteArrayOf(), + format = "ROC_3", + referenceId = "referenceId", + ), + ), ), ) diff --git a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/EnrolmentRecordLocalDataSourceImplTest.kt b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/EnrolmentRecordLocalDataSourceImplTest.kt index 0ee8f8f721..09838d6906 100644 --- a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/EnrolmentRecordLocalDataSourceImplTest.kt +++ b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/EnrolmentRecordLocalDataSourceImplTest.kt @@ -2,11 +2,15 @@ package com.simprints.infra.enrolment.records.repository.local import com.google.common.truth.Truth.assertThat import com.simprints.core.domain.face.FaceSample +import com.simprints.core.domain.fingerprint.FingerprintSample +import com.simprints.core.domain.fingerprint.IFingerIdentifier import com.simprints.core.domain.tokenization.asTokenizableEncrypted import com.simprints.core.domain.tokenization.asTokenizableRaw import com.simprints.infra.config.store.models.Project import com.simprints.infra.config.store.tokenization.TokenizationProcessor import com.simprints.infra.enrolment.records.realm.store.RealmWrapper +import com.simprints.infra.enrolment.records.realm.store.models.DbFaceSample +import com.simprints.infra.enrolment.records.realm.store.models.DbFingerprintSample import com.simprints.infra.enrolment.records.realm.store.models.DbSubject import com.simprints.infra.enrolment.records.repository.domain.models.BiometricDataSource import com.simprints.infra.enrolment.records.repository.domain.models.Subject @@ -28,6 +32,7 @@ import io.mockk.verify import io.realm.kotlin.MutableRealm import io.realm.kotlin.Realm import io.realm.kotlin.query.RealmQuery +import io.realm.kotlin.query.RealmSingleQuery import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -47,6 +52,9 @@ class EnrolmentRecordLocalDataSourceImplTest { @MockK private lateinit var realmQuery: RealmQuery + @MockK + private lateinit var realmSingleQuery: RealmSingleQuery + @MockK private lateinit var tokenizationProcessor: TokenizationProcessor @@ -88,7 +96,13 @@ class EnrolmentRecordLocalDataSourceImplTest { every { realm.query(DbSubject::class) } returns realmQuery every { mutableRealm.query(DbSubject::class) } returns realmQuery - enrolmentRecordLocalDataSource = EnrolmentRecordLocalDataSourceImpl(realmWrapperMock, tokenizationProcessor) + every { realmQuery.query(any(), any()) } returns realmQuery + every { realmQuery.first() } returns realmSingleQuery + + enrolmentRecordLocalDataSource = EnrolmentRecordLocalDataSourceImpl( + realmWrapperMock, + tokenizationProcessor, + ) } @Test @@ -212,10 +226,7 @@ class EnrolmentRecordLocalDataSourceImplTest { val savedPersons = saveFakePeople(getRandomPeople(20)) val fakePerson = savedPersons[0].fromDomainToDb() - val people = - enrolmentRecordLocalDataSource - .load(SubjectQuery(attendantId = savedPersons[0].attendantId)) - .toList() + val people = enrolmentRecordLocalDataSource.load(SubjectQuery(attendantId = savedPersons[0].attendantId)).toList() listOf(fakePerson).zip(people).forEach { (dbSubject, subject) -> assertThat(dbSubject.deepEquals(subject.fromDomainToDb())).isTrue() } @@ -226,10 +237,7 @@ class EnrolmentRecordLocalDataSourceImplTest { val savedPersons = saveFakePeople(getRandomPeople(20)) val fakePerson = savedPersons[0].fromDomainToDb() - val people = - enrolmentRecordLocalDataSource - .load(SubjectQuery(moduleId = fakePerson.moduleId.asTokenizableEncrypted())) - .toList() + val people = enrolmentRecordLocalDataSource.load(SubjectQuery(moduleId = fakePerson.moduleId.asTokenizableEncrypted())).toList() listOf(fakePerson).zip(people).forEach { (dbSubject, subject) -> assertThat(dbSubject.deepEquals(subject.fromDomainToDb())).isTrue() } @@ -238,6 +246,8 @@ class EnrolmentRecordLocalDataSourceImplTest { @Test fun performSubjectCreationAction() = runTest { val subject = getFakePerson() + every { realmSingleQuery.find() } returns null + enrolmentRecordLocalDataSource.performActions( listOf(SubjectAction.Creation(subject.fromDbToDomain())), project, @@ -246,6 +256,75 @@ class EnrolmentRecordLocalDataSourceImplTest { assertThat(peopleCount).isEqualTo(1) } + @Test + fun performSubjectCreationAction_deletesOldSamples() = runTest { + every { realmSingleQuery.find() } returns getRandomSubject() + .copy( + faceSamples = listOf( + getRandomFaceSample("faceToDelete"), + ), + fingerprintSamples = listOf( + getRandomFingerprintSample("fingerToDelete"), + ), + ).fromDomainToDb() + val subject = getFakePerson() + + enrolmentRecordLocalDataSource.performActions( + listOf(SubjectAction.Creation(subject.fromDbToDomain())), + project, + ) + + verify { + mutableRealm.delete(withArg { it.id == "faceToDelete" }) + mutableRealm.delete(withArg { it.id == "faceToDelete" }) + } + val peopleCount = enrolmentRecordLocalDataSource.count() + assertThat(peopleCount).isEqualTo(1) + } + + @Test + fun performSubjectUpdateAction() = runTest { + val subject = getFakePerson() + every { realmSingleQuery.find() } returns getRandomSubject( + faceSamples = listOf( + getRandomFaceSample(referenceId = "faceToDelete"), + getRandomFaceSample(), + ), + fingerprintSamples = listOf( + getRandomFingerprintSample(referenceId = "fingerToDelete"), + getRandomFingerprintSample(), + ), + ).fromDomainToDb() + + enrolmentRecordLocalDataSource.performActions( + listOf( + SubjectAction.Update( + subject.subjectId.toString(), + faceSamplesToAdd = listOf(getRandomFaceSample()), + fingerprintSamplesToAdd = listOf(getRandomFingerprintSample()), + referenceIdsToRemove = listOf("faceToDelete", "fingerToDelete"), + ), + ), + project, + ) + val peopleCount = enrolmentRecordLocalDataSource.count() + assertThat(peopleCount).isEqualTo(1) + verify { + mutableRealm.delete(withArg { it.id == "faceToDelete" }) + mutableRealm.delete(withArg { it.id == "faceToDelete" }) + mutableRealm.copyToRealm( + withArg { + // one old + one new + it.faceSamples.size == 2 && + it.fingerprintSamples.size == 2 && + it.faceSamples.none { it.referenceId == "faceToDelete" } && + it.fingerprintSamples.none { it.referenceId == "fingerToDelete" } + }, + any(), + ) + } + } + @Test fun performSubjectDeletionAction() = runTest { val subject = getFakePerson() @@ -307,15 +386,27 @@ class EnrolmentRecordLocalDataSourceImplTest { projectId: String = UUID.randomUUID().toString(), userId: String = UUID.randomUUID().toString(), moduleId: String = UUID.randomUUID().toString(), - faceSamples: Array = arrayOf( - FaceSample(Random.nextBytes(64), "faceTemplateFormat"), - FaceSample(Random.nextBytes(64), "faceTemplateFormat"), + faceSamples: List = listOf( + getRandomFaceSample(), + getRandomFaceSample(), ), + fingerprintSamples: List = listOf(), ): Subject = Subject( subjectId = patientId, projectId = projectId, attendantId = userId.asTokenizableRaw(), moduleId = moduleId.asTokenizableRaw(), - faceSamples = faceSamples.toList(), + faceSamples = faceSamples, + fingerprintSamples = fingerprintSamples, ) + + private fun getRandomFaceSample( + id: String = UUID.randomUUID().toString(), + referenceId: String = "referenceId", + ) = FaceSample(Random.nextBytes(64), "faceTemplateFormat", referenceId, id) + + private fun getRandomFingerprintSample( + id: String = UUID.randomUUID().toString(), + referenceId: String = "referenceId", + ) = FingerprintSample(IFingerIdentifier.LEFT_3RD_FINGER, Random.nextBytes(64), 42, "fingerprintTemplateFormat", referenceId, id) } diff --git a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/models/DbSubjectTest.kt b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/models/DbSubjectTest.kt index e4a6e223cf..048cae41d2 100644 --- a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/models/DbSubjectTest.kt +++ b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/models/DbSubjectTest.kt @@ -20,6 +20,7 @@ class DbSubjectTest { companion object { private const val GUID = "3f0f8e9a-0a0c-456c-846e-577b1440b6fb" private const val PROJECT_ID = "projectId" + private const val REFERENCE_ID = "referenceId" private val ATTENDANT_ID = "user1".asTokenizableEncrypted() private val MODULE_ID = "module".asTokenizableEncrypted() } @@ -31,8 +32,9 @@ class DbSubjectTest { Random.nextBytes(64), 30, "NEC_1", + REFERENCE_ID, ) - val faceSample = FaceSample(Random.nextBytes(64), "RANK_ONE_1_23") + val faceSample = FaceSample(Random.nextBytes(64), "RANK_ONE_1_23", REFERENCE_ID) val domainSubject = Subject( subjectId = GUID, @@ -55,7 +57,9 @@ class DbSubjectTest { assertThat(createdAt).isEqualTo(RealmInstant.from(0, 0)) assertThat(updatedAt).isEqualTo(RealmInstant.from(1, 500_000_000)) assertThat(fingerprintSamples.first().id).isEqualTo(fingerprintSample.id) + assertThat(fingerprintSamples.first().referenceId).isEqualTo(REFERENCE_ID) assertThat(faceSamples.first().id).isEqualTo(faceSample.id) + assertThat(faceSamples.first().referenceId).isEqualTo(REFERENCE_ID) } } @@ -66,10 +70,12 @@ class DbSubjectTest { template = Random.nextBytes(64) templateQualityScore = 30 format = "NEC_1" + referenceId = REFERENCE_ID } val faceSample = DbFaceSample().apply { template = Random.nextBytes(64) format = "RANK_ONE_1_23" + referenceId = REFERENCE_ID } val dbSubject = DbSubject().apply { @@ -95,7 +101,8 @@ class DbSubjectTest { assertThat(moduleId).isEqualTo(MODULE_ID) assertThat(projectId).isEqualTo(PROJECT_ID) assertThat(fingerprintSamples.first().id).isEqualTo(fingerprintSample.id) - assertThat(faceSamples.first().id).isEqualTo(faceSample.id) + assertThat(fingerprintSamples.first().referenceId).isEqualTo(REFERENCE_ID) + assertThat(faceSamples.first().referenceId).isEqualTo(REFERENCE_ID) } } } diff --git a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/remote/EnrolmentRecordRemoteDataSourceImplTest.kt b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/remote/EnrolmentRecordRemoteDataSourceImplTest.kt index 4d267117e3..70c8e072b5 100644 --- a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/remote/EnrolmentRecordRemoteDataSourceImplTest.kt +++ b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/remote/EnrolmentRecordRemoteDataSourceImplTest.kt @@ -77,9 +77,10 @@ class EnrolmentRecordRemoteDataSourceImplTest { FINGERPRINT_TEMPLATE, 50, "ISO_19794_2", + "5289df73-7df5-3326-bcdd-22597afb1fac", ), ), - faceSamples = listOf(FaceSample(FACE_TEMPLATE, "faceTemplateFormat")), + faceSamples = listOf(FaceSample(FACE_TEMPLATE, "faceTemplateFormat", "b4a3ba90-6413-32b4-a4ea-a841a5a400ec")), ) val expectedRecord = ApiEnrolmentRecord( subjectId = SUBJECT_ID, diff --git a/infra/event-sync/schemas/com.simprints.infra.eventsync.status.EventSyncStatusDatabase/4.json b/infra/event-sync/schemas/com.simprints.infra.eventsync.status.EventSyncStatusDatabase/4.json new file mode 100644 index 0000000000..55cb911755 --- /dev/null +++ b/infra/event-sync/schemas/com.simprints.infra.eventsync.status.EventSyncStatusDatabase/4.json @@ -0,0 +1,84 @@ +{ + "formatVersion": 1, + "database": { + "version": 4, + "identityHash": "fb9ad2ed04bc63d696f394aa7b2351c2", + "entities": [ + { + "tableName": "DbEventsDownSyncOperation", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `lastState` TEXT, `lastEventId` TEXT, `lastUpdatedTime` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastState", + "columnName": "lastState", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastEventId", + "columnName": "lastEventId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdatedTime", + "columnName": "lastUpdatedTime", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "DbEventsUpSyncOperation", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `lastState` TEXT, `lastUpdatedTime` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastState", + "columnName": "lastState", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdatedTime", + "columnName": "lastUpdatedTime", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'fb9ad2ed04bc63d696f394aa7b2351c2')" + ] + } +} \ No newline at end of file diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiBiometricReferenceCreationPayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiBiometricReferenceCreationPayload.kt new file mode 100644 index 0000000000..770e9d891a --- /dev/null +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiBiometricReferenceCreationPayload.kt @@ -0,0 +1,22 @@ +package com.simprints.infra.eventsync.event.remote.models + +import androidx.annotation.Keep +import com.simprints.infra.config.store.models.TokenKeyType +import com.simprints.infra.events.event.domain.models.BiometricReferenceCreationEvent.BiometricReferenceCreationPayload + +@Keep +internal data class ApiBiometricReferenceCreationPayload( + override val startTime: ApiTimestamp, + val id: String, + val modality: String, + val captureIds: List, +) : ApiEventPayload(startTime) { + constructor(domainPayload: BiometricReferenceCreationPayload) : this( + domainPayload.createdAt.fromDomainToApi(), + domainPayload.id, + domainPayload.modality.name, + domainPayload.captureIds, + ) + + override fun getTokenizedFieldJsonPath(tokenKeyType: TokenKeyType): String? = null +} diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEnrolmentPayloadV1.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEnrolmentPayloadV1.kt deleted file mode 100644 index 12e670fdd3..0000000000 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEnrolmentPayloadV1.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.simprints.infra.eventsync.event.remote.models - -import androidx.annotation.Keep -import com.simprints.infra.config.store.models.TokenKeyType -import com.simprints.infra.events.event.domain.models.EnrolmentEventV1 - -@Keep -internal data class ApiEnrolmentPayloadV1( - override val startTime: ApiTimestamp, - val personId: String, -) : ApiEventPayload(startTime) { - constructor(domainPayload: EnrolmentEventV1.EnrolmentPayload) : this( - domainPayload.createdAt.fromDomainToApi(), - domainPayload.personId, - ) - - override fun getTokenizedFieldJsonPath(tokenKeyType: TokenKeyType): String? = null // this payload doesn't have tokenizable fields -} diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEnrolmentPayloadV4.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEnrolmentPayloadV4.kt new file mode 100644 index 0000000000..519e6ed6df --- /dev/null +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEnrolmentPayloadV4.kt @@ -0,0 +1,30 @@ +package com.simprints.infra.eventsync.event.remote.models + +import androidx.annotation.Keep +import com.simprints.infra.config.store.models.TokenKeyType +import com.simprints.infra.events.event.domain.models.EnrolmentEventV4 + +@Keep +internal data class ApiEnrolmentPayloadV4( + override val startTime: ApiTimestamp, + val subjectId: String, + val projectId: String, + val moduleId: String, + val attendantId: String, + val biometricReferenceIds: List, +) : ApiEventPayload(startTime) { + constructor(domainPayload: EnrolmentEventV4.EnrolmentPayload) : this( + domainPayload.createdAt.fromDomainToApi(), + domainPayload.subjectId, + domainPayload.projectId, + domainPayload.moduleId.value, + domainPayload.attendantId.value, + domainPayload.biometricReferenceIds, + ) + + override fun getTokenizedFieldJsonPath(tokenKeyType: TokenKeyType): String? = when (tokenKeyType) { + TokenKeyType.AttendantId -> "attendantId" + TokenKeyType.ModuleId -> "moduleId" + TokenKeyType.Unknown -> null + } +} diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEventPayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEventPayload.kt index 34c0114514..81776dbf76 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEventPayload.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEventPayload.kt @@ -8,17 +8,19 @@ import com.simprints.infra.events.event.domain.models.AgeGroupSelectionEvent import com.simprints.infra.events.event.domain.models.AlertScreenEvent.AlertScreenPayload import com.simprints.infra.events.event.domain.models.AuthenticationEvent.AuthenticationPayload import com.simprints.infra.events.event.domain.models.AuthorizationEvent.AuthorizationPayload +import com.simprints.infra.events.event.domain.models.BiometricReferenceCreationEvent.BiometricReferenceCreationPayload import com.simprints.infra.events.event.domain.models.CandidateReadEvent.CandidateReadPayload import com.simprints.infra.events.event.domain.models.CompletionCheckEvent.CompletionCheckPayload import com.simprints.infra.events.event.domain.models.ConnectivitySnapshotEvent.ConnectivitySnapshotPayload import com.simprints.infra.events.event.domain.models.ConsentEvent.ConsentPayload -import com.simprints.infra.events.event.domain.models.EnrolmentEventV1 import com.simprints.infra.events.event.domain.models.EnrolmentEventV2 +import com.simprints.infra.events.event.domain.models.EnrolmentEventV4 import com.simprints.infra.events.event.domain.models.EventPayload import com.simprints.infra.events.event.domain.models.EventType.AGE_GROUP_SELECTION import com.simprints.infra.events.event.domain.models.EventType.ALERT_SCREEN import com.simprints.infra.events.event.domain.models.EventType.AUTHENTICATION import com.simprints.infra.events.event.domain.models.EventType.AUTHORIZATION +import com.simprints.infra.events.event.domain.models.EventType.BIOMETRIC_REFERENCE_CREATION import com.simprints.infra.events.event.domain.models.EventType.CALLBACK_CONFIRMATION import com.simprints.infra.events.event.domain.models.EventType.CALLBACK_ENROLMENT import com.simprints.infra.events.event.domain.models.EventType.CALLBACK_ERROR @@ -34,8 +36,8 @@ import com.simprints.infra.events.event.domain.models.EventType.CANDIDATE_READ import com.simprints.infra.events.event.domain.models.EventType.COMPLETION_CHECK import com.simprints.infra.events.event.domain.models.EventType.CONNECTIVITY_SNAPSHOT import com.simprints.infra.events.event.domain.models.EventType.CONSENT -import com.simprints.infra.events.event.domain.models.EventType.ENROLMENT_V1 import com.simprints.infra.events.event.domain.models.EventType.ENROLMENT_V2 +import com.simprints.infra.events.event.domain.models.EventType.ENROLMENT_V4 import com.simprints.infra.events.event.domain.models.EventType.EVENT_DOWN_SYNC_REQUEST import com.simprints.infra.events.event.domain.models.EventType.EVENT_UP_SYNC_REQUEST import com.simprints.infra.events.event.domain.models.EventType.FACE_CAPTURE @@ -86,7 +88,7 @@ import com.simprints.infra.events.event.domain.models.face.FaceCaptureConfirmati import com.simprints.infra.events.event.domain.models.face.FaceCaptureEvent import com.simprints.infra.events.event.domain.models.face.FaceFallbackCaptureEvent.FaceFallbackCapturePayload import com.simprints.infra.events.event.domain.models.face.FaceOnboardingCompleteEvent.FaceOnboardingCompletePayload -import com.simprints.infra.events.event.domain.models.fingerprint.FingerprintCaptureBiometricsEvent +import com.simprints.infra.events.event.domain.models.fingerprint.FingerprintCaptureBiometricsEvent.FingerprintCaptureBiometricsPayload import com.simprints.infra.events.event.domain.models.fingerprint.FingerprintCaptureEvent import com.simprints.infra.events.event.domain.models.upsync.EventUpSyncRequestEvent import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.Companion @@ -114,7 +116,7 @@ import com.simprints.infra.eventsync.event.remote.models.upsync.ApiEventUpSyncRe JsonSubTypes.Type(value = ApiCompletionCheckPayload::class, name = Companion.COMPLETION_CHECK_KEY), JsonSubTypes.Type(value = ApiConnectivitySnapshotPayload::class, name = Companion.CONNECTIVITY_SNAPSHOT_KEY), JsonSubTypes.Type(value = ApiConsentPayload::class, name = Companion.CONSENT_KEY), - JsonSubTypes.Type(value = ApiEnrolmentPayloadV2::class, name = Companion.ENROLMENT_KEY), + JsonSubTypes.Type(value = ApiEnrolmentPayloadV4::class, name = Companion.ENROLMENT_KEY), JsonSubTypes.Type(value = ApiFingerprintCapturePayload::class, name = Companion.FINGERPRINT_CAPTURE_KEY), JsonSubTypes.Type(value = ApiFingerprintCaptureBiometricsPayload::class, name = Companion.FINGERPRINT_CAPTURE_BIOMETRICS_KEY), JsonSubTypes.Type(value = ApiGuidSelectionPayload::class, name = Companion.GUID_SELECTION_KEY), @@ -132,6 +134,7 @@ import com.simprints.infra.eventsync.event.remote.models.upsync.ApiEventUpSyncRe JsonSubTypes.Type(value = ApiCalloutPayload::class, name = Companion.CALLBACK_KEY), JsonSubTypes.Type(value = ApiEventDownSyncRequestPayload::class, name = Companion.EVENT_DOWN_SYNC_REQUEST_KEY), JsonSubTypes.Type(value = ApiEventUpSyncRequestPayload::class, name = Companion.EVENT_UP_SYNC_REQUEST_KEY), + JsonSubTypes.Type(value = ApiBiometricReferenceCreationPayload::class, name = Companion.BIOMETRIC_REFERENCE_CREATION_KEY), ) @Keep internal abstract class ApiEventPayload( @@ -143,8 +146,8 @@ internal abstract class ApiEventPayload( internal fun EventPayload.fromDomainToApi(): ApiEventPayload = when (this.type) { AUTHENTICATION -> ApiAuthenticationPayload(this as AuthenticationPayload) CONSENT -> ApiConsentPayload(this as ConsentPayload) - ENROLMENT_V1 -> ApiEnrolmentPayloadV1(this as EnrolmentEventV1.EnrolmentPayload) ENROLMENT_V2 -> ApiEnrolmentPayloadV2(this as EnrolmentEventV2.EnrolmentPayload) + ENROLMENT_V4 -> ApiEnrolmentPayloadV4(this as EnrolmentEventV4.EnrolmentPayload) AUTHORIZATION -> ApiAuthorizationPayload(this as AuthorizationPayload) FINGERPRINT_CAPTURE -> ApiFingerprintCapturePayload(this as FingerprintCaptureEvent.FingerprintCapturePayload) ONE_TO_ONE_MATCH -> ApiOneToOneMatchPayload(this as OneToOneMatchPayload) @@ -177,12 +180,11 @@ internal fun EventPayload.fromDomainToApi(): ApiEventPayload = when (this.type) CALLBACK_VERIFICATION -> ApiCallbackPayload(this as VerificationCallbackPayload) CALLBACK_ERROR -> ApiCallbackPayload(this as ErrorCallbackPayload) CALLBACK_CONFIRMATION -> ApiCallbackPayload(this as ConfirmationCallbackPayload) - FINGERPRINT_CAPTURE_BIOMETRICS -> ApiFingerprintCaptureBiometricsPayload( - this as FingerprintCaptureBiometricsEvent.FingerprintCaptureBiometricsPayload, - ) + FINGERPRINT_CAPTURE_BIOMETRICS -> ApiFingerprintCaptureBiometricsPayload(this as FingerprintCaptureBiometricsPayload) FACE_CAPTURE_BIOMETRICS -> ApiFaceCaptureBiometricsPayload(this as FaceCaptureBiometricsEvent.FaceCaptureBiometricsPayload) EVENT_DOWN_SYNC_REQUEST -> ApiEventDownSyncRequestPayload(this as EventDownSyncRequestEvent.EventDownSyncRequestPayload) EVENT_UP_SYNC_REQUEST -> ApiEventUpSyncRequestPayload(this as EventUpSyncRequestEvent.EventUpSyncRequestPayload) LICENSE_CHECK -> ApiLicenseCheckEventPayload(this as LicenseCheckEvent.LicenseCheckEventPayload) AGE_GROUP_SELECTION -> ApiAgeGroupSelectionPayload(this as AgeGroupSelectionEvent.AgeGroupSelectionPayload) + BIOMETRIC_REFERENCE_CREATION -> ApiBiometricReferenceCreationPayload(this as BiometricReferenceCreationPayload) } diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEventPayloadType.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEventPayloadType.kt index 82602e8129..9f0dc95bdb 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEventPayloadType.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEventPayloadType.kt @@ -6,6 +6,7 @@ import com.simprints.infra.events.event.domain.models.EventType.AGE_GROUP_SELECT import com.simprints.infra.events.event.domain.models.EventType.ALERT_SCREEN import com.simprints.infra.events.event.domain.models.EventType.AUTHENTICATION import com.simprints.infra.events.event.domain.models.EventType.AUTHORIZATION +import com.simprints.infra.events.event.domain.models.EventType.BIOMETRIC_REFERENCE_CREATION import com.simprints.infra.events.event.domain.models.EventType.CALLBACK_CONFIRMATION import com.simprints.infra.events.event.domain.models.EventType.CALLBACK_ENROLMENT import com.simprints.infra.events.event.domain.models.EventType.CALLBACK_ERROR @@ -21,8 +22,8 @@ import com.simprints.infra.events.event.domain.models.EventType.CANDIDATE_READ import com.simprints.infra.events.event.domain.models.EventType.COMPLETION_CHECK import com.simprints.infra.events.event.domain.models.EventType.CONNECTIVITY_SNAPSHOT import com.simprints.infra.events.event.domain.models.EventType.CONSENT -import com.simprints.infra.events.event.domain.models.EventType.ENROLMENT_V1 import com.simprints.infra.events.event.domain.models.EventType.ENROLMENT_V2 +import com.simprints.infra.events.event.domain.models.EventType.ENROLMENT_V4 import com.simprints.infra.events.event.domain.models.EventType.EVENT_DOWN_SYNC_REQUEST import com.simprints.infra.events.event.domain.models.EventType.EVENT_UP_SYNC_REQUEST import com.simprints.infra.events.event.domain.models.EventType.FACE_CAPTURE @@ -147,6 +148,9 @@ internal enum class ApiEventPayloadType { // key added: AGE_GROUP_SELECTION_KEY AgeGroupSelection, + // key added: BIOMETRIC_REFERENCE_CREATION_KEY + BiometricReferenceCreation, + ; companion object { @@ -180,13 +184,14 @@ internal enum class ApiEventPayloadType { const val FINGERPRINT_CAPTURE_BIOMETRICS_KEY = "FingerprintCaptureBiometrics" const val EVENT_DOWN_SYNC_REQUEST_KEY = "EventDownSyncRequest" const val EVENT_UP_SYNC_REQUEST_KEY = "EventUpSyncRequest" + const val BIOMETRIC_REFERENCE_CREATION_KEY = "BiometricReferenceCreation" } } internal fun EventType.fromDomainToApi(): ApiEventPayloadType = when (this) { AUTHENTICATION -> ApiEventPayloadType.Authentication CONSENT -> ApiEventPayloadType.Consent - ENROLMENT_V1, ENROLMENT_V2 -> ApiEventPayloadType.Enrolment + ENROLMENT_V2, ENROLMENT_V4 -> ApiEventPayloadType.Enrolment AUTHORIZATION -> ApiEventPayloadType.Authorization FINGERPRINT_CAPTURE -> ApiEventPayloadType.FingerprintCapture ONE_TO_ONE_MATCH -> ApiEventPayloadType.OneToOneMatch @@ -229,12 +234,13 @@ internal fun EventType.fromDomainToApi(): ApiEventPayloadType = when (this) { EVENT_UP_SYNC_REQUEST -> ApiEventPayloadType.EventUpSyncRequest LICENSE_CHECK -> ApiEventPayloadType.LicenseCheck AGE_GROUP_SELECTION -> ApiEventPayloadType.AgeGroupSelection + BIOMETRIC_REFERENCE_CREATION -> ApiEventPayloadType.BiometricReferenceCreation } internal fun ApiEventPayloadType.fromApiToDomain(): EventType = when (this) { ApiEventPayloadType.Authentication -> AUTHENTICATION ApiEventPayloadType.Consent -> CONSENT - ApiEventPayloadType.Enrolment -> ENROLMENT_V2 + ApiEventPayloadType.Enrolment -> ENROLMENT_V4 ApiEventPayloadType.Authorization -> AUTHORIZATION ApiEventPayloadType.FingerprintCapture -> FINGERPRINT_CAPTURE ApiEventPayloadType.OneToOneMatch -> ONE_TO_MANY_MATCH @@ -262,6 +268,7 @@ internal fun ApiEventPayloadType.fromApiToDomain(): EventType = when (this) { ApiEventPayloadType.EventUpSyncRequest -> EVENT_UP_SYNC_REQUEST ApiEventPayloadType.LicenseCheck -> LICENSE_CHECK ApiEventPayloadType.AgeGroupSelection -> AGE_GROUP_SELECTION + ApiEventPayloadType.BiometricReferenceCreation -> BIOMETRIC_REFERENCE_CREATION ApiEventPayloadType.Callout -> throw UnsupportedOperationException("") ApiEventPayloadType.Callback -> throw UnsupportedOperationException("") } diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiOneToManyMatchPayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiOneToManyMatchPayload.kt index c14f85db49..56f7f29d27 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiOneToManyMatchPayload.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiOneToManyMatchPayload.kt @@ -12,6 +12,7 @@ internal data class ApiOneToManyMatchPayload( val pool: ApiMatchPool, val matcher: String, val result: List?, + val probeBiometricReferenceId: String? = null, ) : ApiEventPayload(startTime) { @Keep data class ApiMatchPool( @@ -30,11 +31,15 @@ internal data class ApiOneToManyMatchPayload( } constructor(domainPayload: OneToManyMatchPayload) : this( - domainPayload.createdAt.fromDomainToApi(), - domainPayload.endedAt?.fromDomainToApi(), - ApiMatchPool(domainPayload.pool), - domainPayload.matcher, - domainPayload.result?.map { ApiMatchEntry(it) }, + startTime = domainPayload.createdAt.fromDomainToApi(), + endTime = domainPayload.endedAt?.fromDomainToApi(), + pool = ApiMatchPool(domainPayload.pool), + matcher = domainPayload.matcher, + result = domainPayload.result?.map { ApiMatchEntry(it) }, + probeBiometricReferenceId = when (domainPayload) { + is OneToManyMatchPayload.OneToManyMatchPayloadV2 -> null + is OneToManyMatchPayload.OneToManyMatchPayloadV3 -> domainPayload.probeBiometricReferenceId + }, ) override fun getTokenizedFieldJsonPath(tokenKeyType: TokenKeyType): String? = null // this payload doesn't have tokenizable fields diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiOneToOneMatchPayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiOneToOneMatchPayload.kt index a0bdcab049..71107450a1 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiOneToOneMatchPayload.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiOneToOneMatchPayload.kt @@ -15,14 +15,19 @@ internal data class ApiOneToOneMatchPayload( val matcher: String, val result: ApiMatchEntry?, val fingerComparisonStrategy: ApiFingerComparisonStrategy?, + val probeBiometricReferenceId: String? = null, ) : ApiEventPayload(startTime) { constructor(domainPayload: OneToOneMatchPayload) : this( - domainPayload.createdAt.fromDomainToApi(), - domainPayload.endedAt?.fromDomainToApi(), - domainPayload.candidateId, - domainPayload.matcher, - domainPayload.result?.let { ApiMatchEntry(it) }, - domainPayload.fingerComparisonStrategy?.fromDomainToApi(), + startTime = domainPayload.createdAt.fromDomainToApi(), + endTime = domainPayload.endedAt?.fromDomainToApi(), + candidateId = domainPayload.candidateId, + matcher = domainPayload.matcher, + result = domainPayload.result?.let { ApiMatchEntry(it) }, + fingerComparisonStrategy = domainPayload.fingerComparisonStrategy?.fromDomainToApi(), + when (domainPayload) { + is OneToOneMatchPayload.OneToOneMatchPayloadV3 -> null + is OneToOneMatchPayload.OneToOneMatchPayloadV4 -> domainPayload.probeBiometricReferenceId + }, ) override fun getTokenizedFieldJsonPath(tokenKeyType: TokenKeyType): String? = null // this payload doesn't have tokenizable fields diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiPersonCreationPayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiPersonCreationPayload.kt index 746f61f762..596f775bd0 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiPersonCreationPayload.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiPersonCreationPayload.kt @@ -5,6 +5,7 @@ import com.simprints.infra.config.store.models.TokenKeyType import com.simprints.infra.events.event.domain.models.PersonCreationEvent.PersonCreationPayload @Keep +@Deprecated("Replaced by ApiBiometricReferenceCreationEvent in 2025.1.0") internal data class ApiPersonCreationPayload( override val startTime: ApiTimestamp, val fingerprintCaptureIds: List?, diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordDeletionPayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordDeletionPayload.kt index d0518524fb..3a5c24922a 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordDeletionPayload.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordDeletionPayload.kt @@ -9,11 +9,7 @@ internal data class ApiEnrolmentRecordDeletionPayload( val projectId: String, val moduleId: String, val attendantId: String, -) : ApiEnrolmentRecordEventPayload(ApiEnrolmentRecordPayloadType.EnrolmentRecordDeletion) { - companion object { - const val ENROLMENT_RECORD_DELETION = "EnrolmentRecordDeletion" - } -} +) : ApiEnrolmentRecordEventPayload(ApiEnrolmentRecordPayloadType.EnrolmentRecordDeletion) internal fun ApiEnrolmentRecordDeletionPayload.fromApiToDomain() = EnrolmentRecordDeletionEvent.EnrolmentRecordDeletionPayload( subjectId, diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordEvent.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordEvent.kt index 73970eae05..14a58316ca 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordEvent.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordEvent.kt @@ -5,6 +5,7 @@ import com.simprints.infra.events.event.domain.models.subject.EnrolmentRecordCre import com.simprints.infra.events.event.domain.models.subject.EnrolmentRecordDeletionEvent import com.simprints.infra.events.event.domain.models.subject.EnrolmentRecordEvent import com.simprints.infra.events.event.domain.models.subject.EnrolmentRecordMoveEvent +import com.simprints.infra.events.event.domain.models.subject.EnrolmentRecordUpdateEvent @Keep internal class ApiEnrolmentRecordEvent( @@ -17,10 +18,17 @@ internal fun ApiEnrolmentRecordEvent.fromApiToDomain(): EnrolmentRecordEvent = w id, (payload as ApiEnrolmentRecordCreationPayload).fromApiToDomain(), ) + ApiEnrolmentRecordPayloadType.EnrolmentRecordDeletion -> EnrolmentRecordDeletionEvent( id, (payload as ApiEnrolmentRecordDeletionPayload).fromApiToDomain(), ) + + ApiEnrolmentRecordPayloadType.EnrolmentRecordUpdate -> EnrolmentRecordUpdateEvent( + id, + (payload as ApiEnrolmentRecordUpdatePayload).fromApiToDomain(), + ) + ApiEnrolmentRecordPayloadType.EnrolmentRecordMove -> EnrolmentRecordMoveEvent( id, (payload as ApiEnrolmentRecordMovePayload).fromApiToDomain(), diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordEventPayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordEventPayload.kt index 5f2220e605..1bf235b3f8 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordEventPayload.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordEventPayload.kt @@ -22,6 +22,10 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo value = ApiEnrolmentRecordMovePayload::class, name = ApiEnrolmentRecordPayloadType.ENROLMENT_RECORD_MOVE_KEY, ), + JsonSubTypes.Type( + value = ApiEnrolmentRecordUpdatePayload::class, + name = ApiEnrolmentRecordPayloadType.ENROLMENT_RECORD_UPDATE_KEY, + ), ) internal abstract class ApiEnrolmentRecordEventPayload( val type: ApiEnrolmentRecordPayloadType, diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordMovePayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordMovePayload.kt index 48e0ba28f6..87a8b7d67a 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordMovePayload.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordMovePayload.kt @@ -32,10 +32,6 @@ internal data class ApiEnrolmentRecordMovePayload( val attendantId: String, val biometricReferences: List?, ) - - companion object { - const val ENROLMENT_RECORD_MOVE = "EnrolmentRecordMove" - } } internal fun ApiEnrolmentRecordMovePayload.fromApiToDomain() = EnrolmentRecordMoveEvent.EnrolmentRecordMovePayload( diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordPayloadType.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordPayloadType.kt index 9c54670936..3df952cc64 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordPayloadType.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordPayloadType.kt @@ -11,6 +11,9 @@ internal enum class ApiEnrolmentRecordPayloadType { // key added: ENROLMENT_RECORD_DELETION_KEY EnrolmentRecordDeletion, + // key added: ENROLMENT_RECORD_UPDATE_KEY + EnrolmentRecordUpdate, + // key added: ENROLMENT_RECORD_MOVE_KEY EnrolmentRecordMove, @@ -20,6 +23,7 @@ internal enum class ApiEnrolmentRecordPayloadType { const val ENROLMENT_RECORD_CREATION_KEY = "EnrolmentRecordCreation" const val ENROLMENT_RECORD_DELETION_KEY = "EnrolmentRecordDeletion" const val ENROLMENT_RECORD_MOVE_KEY = "EnrolmentRecordMove" + const val ENROLMENT_RECORD_UPDATE_KEY = "EnrolmentRecordUpdate" } } @@ -27,4 +31,5 @@ internal fun ApiEnrolmentRecordPayloadType.fromApiToDomain(): EnrolmentRecordEve ApiEnrolmentRecordPayloadType.EnrolmentRecordCreation -> EnrolmentRecordEventType.EnrolmentRecordCreation ApiEnrolmentRecordPayloadType.EnrolmentRecordDeletion -> EnrolmentRecordEventType.EnrolmentRecordDeletion ApiEnrolmentRecordPayloadType.EnrolmentRecordMove -> EnrolmentRecordEventType.EnrolmentRecordMove + ApiEnrolmentRecordPayloadType.EnrolmentRecordUpdate -> EnrolmentRecordEventType.EnrolmentRecordUpdate } diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordUpdatePayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordUpdatePayload.kt new file mode 100644 index 0000000000..57014fc4ef --- /dev/null +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordUpdatePayload.kt @@ -0,0 +1,19 @@ +package com.simprints.infra.eventsync.event.remote.models.subject + +import androidx.annotation.Keep +import com.simprints.infra.events.event.domain.models.subject.EnrolmentRecordUpdateEvent +import com.simprints.infra.eventsync.event.remote.models.subject.biometricref.ApiBiometricReference +import com.simprints.infra.eventsync.event.remote.models.subject.biometricref.fromApiToDomain + +@Keep +internal data class ApiEnrolmentRecordUpdatePayload( + val subjectId: String, + val biometricReferencesAdded: List?, + val biometricReferencesRemoved: List?, +) : ApiEnrolmentRecordEventPayload(ApiEnrolmentRecordPayloadType.EnrolmentRecordUpdate) + +internal fun ApiEnrolmentRecordUpdatePayload.fromApiToDomain() = EnrolmentRecordUpdateEvent.EnrolmentRecordUpdatePayload( + subjectId, + biometricReferencesAdded?.map { it.fromApiToDomain() }.orEmpty(), + biometricReferencesRemoved.orEmpty(), +) diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/status/EventSyncStatusDatabase.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/status/EventSyncStatusDatabase.kt index e4153d3c14..96ae9c2f2a 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/status/EventSyncStatusDatabase.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/status/EventSyncStatusDatabase.kt @@ -11,7 +11,14 @@ import com.simprints.infra.eventsync.status.down.local.DbEventsDownSyncOperation import com.simprints.infra.eventsync.status.up.local.DbEventUpSyncOperationStateDao import com.simprints.infra.eventsync.status.up.local.DbEventsUpSyncOperationState -@Database(entities = [DbEventsDownSyncOperationState::class, DbEventsUpSyncOperationState::class], version = 3, exportSchema = true) +@Database( + entities = [ + DbEventsDownSyncOperationState::class, + DbEventsUpSyncOperationState::class, + ], + version = 4, + exportSchema = true, +) @TypeConverters(Converters::class) @Keep internal abstract class EventSyncStatusDatabase : RoomDatabase() { diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/status/down/domain/EventDownSyncOperation.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/status/down/domain/EventDownSyncOperation.kt index a407a1ea7c..550517b43f 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/status/down/domain/EventDownSyncOperation.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/status/down/domain/EventDownSyncOperation.kt @@ -19,22 +19,9 @@ internal data class EventDownSyncOperation( // Unique key: all request params expect for lastEventId internal fun getUniqueKey(): String = with(this.queryEvent) { - UUID - .nameUUIDFromBytes( - ( - projectId + - (attendantId ?: "") + - (subjectId ?: "") + - (moduleId ?: "") + - modes.joinToString { it.name } + - oldTypes - ).toByteArray(), - ).toString() - } - - companion object { - // We need to keep this old types otherwise the unique key of the down-sync will change - // and we will need to down-sync again from scratch. - internal var oldTypes = "ENROLMENT_RECORD_CREATION, ENROLMENT_RECORD_MOVE, ENROLMENT_RECORD_DELETION" + listOfNotNull(projectId, attendantId, subjectId, moduleId) + .joinToString(separator = "") + .toByteArray() + .let { UUID.nameUUIDFromBytes(it).toString() } } } diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/tasks/EventDownSyncTask.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/tasks/EventDownSyncTask.kt index d3adc1b6a6..1539651746 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/tasks/EventDownSyncTask.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/tasks/EventDownSyncTask.kt @@ -20,6 +20,7 @@ import com.simprints.infra.events.event.domain.models.subject.EnrolmentRecordEve import com.simprints.infra.events.event.domain.models.subject.EnrolmentRecordEventType import com.simprints.infra.events.event.domain.models.subject.EnrolmentRecordMoveEvent import com.simprints.infra.events.event.domain.models.subject.EnrolmentRecordMoveEvent.EnrolmentRecordCreationInMove +import com.simprints.infra.events.event.domain.models.subject.EnrolmentRecordUpdateEvent import com.simprints.infra.eventsync.event.remote.EventRemoteDataSource import com.simprints.infra.eventsync.status.down.EventDownSyncScopeRepository import com.simprints.infra.eventsync.status.down.domain.EventDownSyncOperation @@ -51,7 +52,7 @@ internal class EventDownSyncTask @Inject constructor( scope: CoroutineScope, operation: EventDownSyncOperation, eventScope: EventScope, - project: Project + project: Project, ): Flow = flow { var lastOperation = operation.copy() var count = 0 @@ -152,7 +153,7 @@ internal class EventDownSyncTask @Inject constructor( operation: EventDownSyncOperation, batchOfEventsToProcess: MutableList, lastOperation: EventDownSyncOperation, - project: Project + project: Project, ): EventDownSyncOperation { val actions = batchOfEventsToProcess .map { event -> @@ -168,6 +169,10 @@ internal class EventDownSyncTask @Inject constructor( EnrolmentRecordEventType.EnrolmentRecordMove -> { handleSubjectMoveEvent(operation, event as EnrolmentRecordMoveEvent) } + + EnrolmentRecordEventType.EnrolmentRecordUpdate -> { + handleSubjectUpdateEvent(event as EnrolmentRecordUpdateEvent) + } } }.flatten() @@ -254,6 +259,17 @@ internal class EventDownSyncTask @Inject constructor( private fun handleSubjectDeletionEvent(event: EnrolmentRecordDeletionEvent): List = listOf(Deletion(event.payload.subjectId)) + private fun handleSubjectUpdateEvent(event: EnrolmentRecordUpdateEvent): List = with(event.payload) { + listOf( + SubjectAction.Update( + subjectId = subjectId, + faceSamplesToAdd = subjectFactory.extractFaceSamplesFromBiometricReferences(biometricReferencesAdded), + fingerprintSamplesToAdd = subjectFactory.extractFingerprintSamplesFromBiometricReferences(biometricReferencesAdded), + referenceIdsToRemove = biometricReferencesRemoved, + ), + ) + } + companion object { const val EVENTS_BATCH_SIZE = 200 } diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/tasks/SubjectFactory.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/tasks/SubjectFactory.kt index 1614518e8f..97a52fad1a 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/tasks/SubjectFactory.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/tasks/SubjectFactory.kt @@ -11,6 +11,7 @@ import com.simprints.infra.enrolment.records.repository.domain.models.Subject import com.simprints.infra.events.event.domain.models.subject.BiometricReference import com.simprints.infra.events.event.domain.models.subject.EnrolmentRecordCreationEvent.EnrolmentRecordCreationPayload import com.simprints.infra.events.event.domain.models.subject.EnrolmentRecordMoveEvent.EnrolmentRecordCreationInMove +import com.simprints.infra.events.event.domain.models.subject.EnrolmentRecordUpdateEvent.EnrolmentRecordUpdatePayload import com.simprints.infra.events.event.domain.models.subject.FaceReference import com.simprints.infra.events.event.domain.models.subject.FaceTemplate import com.simprints.infra.events.event.domain.models.subject.FingerprintReference @@ -45,6 +46,24 @@ class SubjectFactory @Inject constructor( ) } + fun buildSubjectFromUpdatePayload( + existingSubject: Subject, + payload: EnrolmentRecordUpdatePayload, + ): Subject { + val removedBiometricReferences = payload.biometricReferencesRemoved.toSet() // to make lookup O(1) + val addedFaceSamples = extractFaceSamplesFromBiometricReferences(payload.biometricReferencesAdded) + val addedFingerprintSamples = extractFingerprintSamplesFromBiometricReferences(payload.biometricReferencesAdded) + + return existingSubject.copy( + faceSamples = existingSubject.faceSamples + .filterNot { it.referenceId in removedBiometricReferences } + .plus(addedFaceSamples), + fingerprintSamples = existingSubject.fingerprintSamples + .filterNot { it.referenceId in removedBiometricReferences } + .plus(addedFingerprintSamples), + ) + } + fun buildSubjectFromCaptureResults( projectId: String, attendantId: TokenizableString, @@ -92,45 +111,46 @@ class SubjectFactory @Inject constructor( sample.template, sample.templateQualityScore, sample.format, + fingerprintResponse.referenceId, ) } } private fun extractFaceSamples(faceResponse: FaceCaptureResult) = faceResponse.results .mapNotNull { it.sample } - .map { FaceSample(it.template, it.format) } + .map { FaceSample(it.template, it.format, faceResponse.referenceId) } - private fun extractFingerprintSamplesFromBiometricReferences(biometricReferences: List?) = biometricReferences + fun extractFingerprintSamplesFromBiometricReferences(biometricReferences: List?) = biometricReferences ?.filterIsInstance() - ?.firstOrNull() - ?.let { reference -> - reference.templates.map { - buildFingerprintSample( - it, - reference.format, - ) - } - } + ?.map { reference -> reference.templates.map { buildFingerprintSample(it, reference.format, reference.id) } } + ?.flatten() ?: emptyList() private fun buildFingerprintSample( template: FingerprintTemplate, format: String, + referenceId: String, ): FingerprintSample = FingerprintSample( fingerIdentifier = template.finger, template = encodingUtils.base64ToBytes(template.template), templateQualityScore = template.quality, format = format, + referenceId = referenceId, ) - private fun extractFaceSamplesFromBiometricReferences(biometricReferences: List?) = biometricReferences + fun extractFaceSamplesFromBiometricReferences(biometricReferences: List?) = biometricReferences ?.filterIsInstance() - ?.firstOrNull() - ?.let { reference -> reference.templates.map { buildFaceSample(it, reference.format) } } + ?.map { reference -> reference.templates.map { buildFaceSample(it, reference.format, reference.id) } } + ?.flatten() ?: emptyList() private fun buildFaceSample( template: FaceTemplate, format: String, - ) = FaceSample(encodingUtils.base64ToBytes(template.template), format) + referenceId: String, + ) = FaceSample( + template = encodingUtils.base64ToBytes(template.template), + format = format, + referenceId = referenceId, + ) } diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/up/tasks/EventUpSyncTask.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/up/tasks/EventUpSyncTask.kt index 4b7bce4ea3..21a61caca8 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/up/tasks/EventUpSyncTask.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/up/tasks/EventUpSyncTask.kt @@ -14,7 +14,9 @@ import com.simprints.infra.config.store.models.canSyncAnalyticsDataToSimprints import com.simprints.infra.config.store.models.canSyncBiometricDataToSimprints import com.simprints.infra.config.sync.ConfigManager import com.simprints.infra.events.EventRepository +import com.simprints.infra.events.event.domain.models.BiometricReferenceCreationEvent import com.simprints.infra.events.event.domain.models.EnrolmentEventV2 +import com.simprints.infra.events.event.domain.models.EnrolmentEventV4 import com.simprints.infra.events.event.domain.models.Event import com.simprints.infra.events.event.domain.models.PersonCreationEvent import com.simprints.infra.events.event.domain.models.face.FaceCaptureBiometricsEvent @@ -315,9 +317,11 @@ internal class EventUpSyncTask @Inject constructor( config.canSyncBiometricDataToSimprints() -> events.filter { it is EnrolmentEventV2 || + it is EnrolmentEventV4 || it is PersonCreationEvent || it is FingerprintCaptureBiometricsEvent || - it is FaceCaptureBiometricsEvent + it is FaceCaptureBiometricsEvent || + it is BiometricReferenceCreationEvent } config.canSyncAnalyticsDataToSimprints() -> events.filterNot { diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/EventValidationUtils.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/EventValidationUtils.kt index dc73c21969..06976423a6 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/EventValidationUtils.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/EventValidationUtils.kt @@ -303,18 +303,22 @@ fun validateConsentEventApiModel(json: JSONObject) { } } -fun validateEnrolmentEventV1ApiModel(json: JSONObject) { - validateCommonParams(json, "Enrolment", 2) +fun validateEnrolmentEventV2ApiModel(json: JSONObject) { + validateCommonParams(json, "Enrolment", 3) with(json.getJSONObject("payload")) { validateTimestamp(getJSONObject("startTime")) - assertThat(getString("personId").isValidGuid()).isTrue() - assertThat(length()).isEqualTo(2) + assertThat(getString("subjectId")).isNotNull() + assertThat(getString("projectId")).isNotNull() + assertThat(getString("moduleId")).isNotNull() + assertThat(getString("attendantId")).isNotNull() + assertThat(getString("personCreationEventId")).isNotNull() + assertThat(length()).isEqualTo(6) } } -fun validateEnrolmentEventV2ApiModel(json: JSONObject) { - validateCommonParams(json, "Enrolment", 3) +fun validateEnrolmentEventV4ApiModel(json: JSONObject) { + validateCommonParams(json, "Enrolment", 4) with(json.getJSONObject("payload")) { validateTimestamp(getJSONObject("startTime")) @@ -322,7 +326,7 @@ fun validateEnrolmentEventV2ApiModel(json: JSONObject) { assertThat(getString("projectId")).isNotNull() assertThat(getString("moduleId")).isNotNull() assertThat(getString("attendantId")).isNotNull() - assertThat(getString("personCreationEventId")).isNotNull() + assertThat(getJSONArray("biometricReferenceIds")).isNotNull() assertThat(length()).isEqualTo(6) } } @@ -378,7 +382,7 @@ fun validateMatchEntryApiModel(json: JSONObject) { } fun validateOneToManyMatchEventApiModel(json: JSONObject) { - validateCommonParams(json, "OneToManyMatch", 2) + validateCommonParams(json, "OneToManyMatch", 3) with(json.getJSONObject("payload")) { validateTimestamp(getJSONObject("startTime")) @@ -393,12 +397,13 @@ fun validateOneToManyMatchEventApiModel(json: JSONObject) { for (i in 0 until matchEntries.length()) { validateMatchEntryApiModel(matchEntries.getJSONObject(i)) } - assertThat(length()).isEqualTo(5) + assertThat(getString("probeBiometricReferenceId").isValidGuid()).isTrue() + assertThat(length()).isEqualTo(6) } } fun validateOneToOneMatchEventApiModel(json: JSONObject) { - validateCommonParams(json, "OneToOneMatch", 3) + validateCommonParams(json, "OneToOneMatch", 4) with(json.getJSONObject("payload")) { validateTimestamp(getJSONObject("startTime")) @@ -413,7 +418,8 @@ fun validateOneToOneMatchEventApiModel(json: JSONObject) { with(getJSONObject("result")) { validateMatchEntryApiModel(this) } - assertThat(length()).isEqualTo(6) + assertThat(getString("probeBiometricReferenceId").isValidGuid()).isTrue() + assertThat(length()).isEqualTo(7) } } @@ -631,4 +637,14 @@ fun validateAgeGroupSelectionEventApiModel(json: JSONObject) { } } +fun validateBiometricReferenceCreationEventApiModel(json: JSONObject) { + validateCommonParams(json, "BiometricReferenceCreation", 1) + with(json.getJSONObject("payload")) { + validateTimestamp(getJSONObject("startTime")) + assertThat(getString("id")).isNotNull() + assertThat(getString("modality")).isNotNull() + assertThat(getString("captureIds")).isNotNull() + } +} + private fun Array.valuesAsStrings(): List = this.map { it.toString() } diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/EventRemoteDataSourceTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/EventRemoteDataSourceTest.kt index 4e9cb3a48b..a54c598655 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/EventRemoteDataSourceTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/EventRemoteDataSourceTest.kt @@ -129,7 +129,7 @@ class EventRemoteDataSourceTest { @Test fun downloadEvents_shouldParseStreamAndEmitBatches() = runTest { val responseStreamWith6Events = - this.javaClass.classLoader?.getResourceAsStream("responses/down_sync_7events.json")!! + this.javaClass.classLoader?.getResourceAsStream("responses/down_sync_8events.json")!! val channel = mockk>(relaxed = true) excludeRecords { channel.isClosedForSend } @@ -148,6 +148,7 @@ class EventRemoteDataSourceTest { EnrolmentRecordEventType.EnrolmentRecordDeletion, EnrolmentRecordEventType.EnrolmentRecordMove, EnrolmentRecordEventType.EnrolmentRecordMove, + EnrolmentRecordEventType.EnrolmentRecordUpdate, ), ) } diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/ApiBiometricReferenceCreationPayloadTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/ApiBiometricReferenceCreationPayloadTest.kt new file mode 100644 index 0000000000..e0dbe019cd --- /dev/null +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/ApiBiometricReferenceCreationPayloadTest.kt @@ -0,0 +1,16 @@ +package com.simprints.infra.eventsync.event.remote.models + +import com.google.common.truth.Truth +import com.simprints.infra.config.store.models.TokenKeyType +import io.mockk.mockk +import org.junit.Test + +class ApiBiometricReferenceCreationPayloadTest { + @Test + fun `when getTokenizedFieldJsonPath is invoked, null is returned`() { + val payload = ApiBiometricReferenceCreationPayload(domainPayload = mockk(relaxed = true)) + TokenKeyType.entries.forEach { + Truth.assertThat(payload.getTokenizedFieldJsonPath(it)).isNull() + } + } +} diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/ApiEnrolmentPayloadV1Test.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/ApiEnrolmentPayloadV1Test.kt deleted file mode 100644 index a0f3b523d4..0000000000 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/ApiEnrolmentPayloadV1Test.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.simprints.infra.eventsync.event.remote.models - -import com.google.common.truth.Truth.assertThat -import com.simprints.infra.config.store.models.TokenKeyType -import io.mockk.mockk -import org.junit.Test - -class ApiEnrolmentPayloadV1Test { - @Test - fun `when getTokenizedFieldJsonPath is invoked, null is returned`() { - val payload = ApiEnrolmentPayloadV1(domainPayload = mockk(relaxed = true)) - TokenKeyType.values().forEach { - assertThat(payload.getTokenizedFieldJsonPath(it)).isNull() - } - } -} diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordUpdateEventTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordUpdateEventTest.kt new file mode 100644 index 0000000000..aeb54ed7d2 --- /dev/null +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordUpdateEventTest.kt @@ -0,0 +1,56 @@ +package com.simprints.infra.eventsync.event.remote.models.subject + +import com.google.common.truth.Truth.assertThat +import com.simprints.core.domain.fingerprint.IFingerIdentifier +import com.simprints.infra.events.event.domain.models.subject.EnrolmentRecordUpdateEvent +import com.simprints.infra.events.event.domain.models.subject.FaceReference +import com.simprints.infra.events.event.domain.models.subject.FaceTemplate +import com.simprints.infra.events.event.domain.models.subject.FingerprintReference +import com.simprints.infra.events.event.domain.models.subject.FingerprintTemplate +import com.simprints.infra.eventsync.event.remote.models.subject.biometricref.face.ApiFaceReference +import com.simprints.infra.eventsync.event.remote.models.subject.biometricref.face.ApiFaceTemplate +import com.simprints.infra.eventsync.event.remote.models.subject.biometricref.fingerprint.ApiFingerprintReference +import com.simprints.infra.eventsync.event.remote.models.subject.biometricref.fingerprint.ApiFingerprintTemplate +import org.junit.Test + +class ApiEnrolmentRecordUpdateEventTest { + @Test + fun convert_EnrolmentRecordUpdateEvent() { + val apiPayload = ApiEnrolmentRecordUpdatePayload( + "subjectId", + listOf( + ApiFingerprintReference( + "fpRefId", + listOf( + ApiFingerprintTemplate(10, "template", IFingerIdentifier.LEFT_THUMB), + ), + "NEC_1", + ), + ApiFaceReference( + "fRefId", + listOf(ApiFaceTemplate("template")), + "ROC_3", + ), + ), + listOf("fpRefId2"), + ) + val expectedPayload = EnrolmentRecordUpdateEvent.EnrolmentRecordUpdatePayload( + "subjectId", + listOf( + FingerprintReference( + "fpRefId", + listOf(FingerprintTemplate(10, "template", IFingerIdentifier.LEFT_THUMB)), + "NEC_1", + ), + FaceReference( + "fRefId", + listOf(FaceTemplate("template")), + "ROC_3", + ), + ), + listOf("fpRefId2"), + ) + + assertThat(apiPayload.fromApiToDomain()).isEqualTo(expectedPayload) + } +} diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/usecases/MapDomainEventToApiUseCaseTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/usecases/MapDomainEventToApiUseCaseTest.kt index 61a8a11156..c35cbcdf72 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/usecases/MapDomainEventToApiUseCaseTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/usecases/MapDomainEventToApiUseCaseTest.kt @@ -15,6 +15,7 @@ import com.simprints.infra.events.sampledata.createAgeGroupSelectionEvent import com.simprints.infra.events.sampledata.createAlertScreenEvent import com.simprints.infra.events.sampledata.createAuthenticationEvent import com.simprints.infra.events.sampledata.createAuthorizationEvent +import com.simprints.infra.events.sampledata.createBiometricReferenceCreationEvent import com.simprints.infra.events.sampledata.createCandidateReadEvent import com.simprints.infra.events.sampledata.createCompletionCheckEvent import com.simprints.infra.events.sampledata.createConfirmationCallbackEvent @@ -23,8 +24,8 @@ import com.simprints.infra.events.sampledata.createConnectivitySnapshotEvent import com.simprints.infra.events.sampledata.createConsentEvent import com.simprints.infra.events.sampledata.createEnrolmentCallbackEvent import com.simprints.infra.events.sampledata.createEnrolmentCalloutEvent -import com.simprints.infra.events.sampledata.createEnrolmentEventV1 import com.simprints.infra.events.sampledata.createEnrolmentEventV2 +import com.simprints.infra.events.sampledata.createEnrolmentEventV4 import com.simprints.infra.events.sampledata.createEventDownSyncRequestEvent import com.simprints.infra.events.sampledata.createEventUpSyncRequestEvent import com.simprints.infra.events.sampledata.createFaceCaptureBiometricsEvent @@ -58,6 +59,7 @@ import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.Age import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.AlertScreen import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.Authentication import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.Authorization +import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.BiometricReferenceCreation import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.Callback import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.Callout import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.CandidateRead @@ -90,6 +92,7 @@ import com.simprints.infra.eventsync.event.validateAgeGroupSelectionEventApiMode import com.simprints.infra.eventsync.event.validateAlertScreenEventApiModel import com.simprints.infra.eventsync.event.validateAuthenticationEventApiModel import com.simprints.infra.eventsync.event.validateAuthorizationEventApiModel +import com.simprints.infra.eventsync.event.validateBiometricReferenceCreationEventApiModel import com.simprints.infra.eventsync.event.validateCallbackV1EventApiModel import com.simprints.infra.eventsync.event.validateCallbackV2EventApiModel import com.simprints.infra.eventsync.event.validateCalloutEventApiModel @@ -99,8 +102,8 @@ import com.simprints.infra.eventsync.event.validateCompletionCheckEventApiModel import com.simprints.infra.eventsync.event.validateConnectivitySnapshotEventApiModel import com.simprints.infra.eventsync.event.validateConsentEventApiModel import com.simprints.infra.eventsync.event.validateDownSyncRequestEventApiModel -import com.simprints.infra.eventsync.event.validateEnrolmentEventV1ApiModel import com.simprints.infra.eventsync.event.validateEnrolmentEventV2ApiModel +import com.simprints.infra.eventsync.event.validateEnrolmentEventV4ApiModel import com.simprints.infra.eventsync.event.validateFaceCaptureBiometricsEventApiModel import com.simprints.infra.eventsync.event.validateFaceCaptureConfirmationEventApiModel import com.simprints.infra.eventsync.event.validateFaceCaptureEventApiModel @@ -315,21 +318,21 @@ internal class MapDomainEventToApiUseCaseTest { } @Test - fun validateEnrolmentV1_enrolmentEventApiModel() { - val event = createEnrolmentEventV1() + fun validateEnrolmentV2_enrolmentEventApiModel() { + val event = createEnrolmentEventV2() val apiEvent = useCase(event, project) val json = JSONObject(jackson.writeValueAsString(apiEvent)) - validateEnrolmentEventV1ApiModel(json) + validateEnrolmentEventV2ApiModel(json) } @Test - fun validateEnrolmentV2_enrolmentEventApiModel() { - val event = createEnrolmentEventV2() + fun validateEnrolmentV4_enrolmentEventApiModel() { + val event = createEnrolmentEventV4() val apiEvent = useCase(event, project) val json = JSONObject(jackson.writeValueAsString(apiEvent)) - validateEnrolmentEventV2ApiModel(json) + validateEnrolmentEventV4ApiModel(json) } @Test @@ -521,6 +524,15 @@ internal class MapDomainEventToApiUseCaseTest { validateAgeGroupSelectionEventApiModel(json) } + @Test + fun validate_biometricReferenceCreationEventApiModel() { + val event = createBiometricReferenceCreationEvent() + val apiEvent = useCase(event, project) + val json = JSONObject(jackson.writeValueAsString(apiEvent)) + + validateBiometricReferenceCreationEventApiModel(json) + } + @Test fun `when event contains tokenized attendant id, then ApiEvent should contain tokenizedField`() { validateUserIdTokenization(attendantId = "attendantId".asTokenizableEncrypted()) @@ -621,6 +633,7 @@ internal class MapDomainEventToApiUseCaseTest { EventUpSyncRequest -> validate_UpSyncRequestEventApiModel() LicenseCheck -> validate_licenseCheckEventApiModel() AgeGroupSelection -> validate_ageGroupSelectionEventApiModel() + BiometricReferenceCreation -> validate_biometricReferenceCreationEventApiModel() null -> TODO() }.safeSealedWhens } diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/status/down/domain/EventDownSyncOperationTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/status/down/domain/EventDownSyncOperationTest.kt index 15efc6e9ef..d52de8cde1 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/status/down/domain/EventDownSyncOperationTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/status/down/domain/EventDownSyncOperationTest.kt @@ -1,7 +1,6 @@ package com.simprints.infra.eventsync.status.down.domain import com.google.common.truth.Truth.assertThat -import com.simprints.infra.events.sampledata.SampleDefaults.DEFAULT_MODES import com.simprints.infra.events.sampledata.SampleDefaults.DEFAULT_MODULE_ID import com.simprints.infra.events.sampledata.SampleDefaults.DEFAULT_MODULE_ID_2 import com.simprints.infra.events.sampledata.SampleDefaults.DEFAULT_PROJECT_ID @@ -9,7 +8,6 @@ import com.simprints.infra.events.sampledata.SampleDefaults.DEFAULT_USER_ID import com.simprints.infra.eventsync.SampleSyncScopes.modulesDownSyncScope import com.simprints.infra.eventsync.SampleSyncScopes.projectDownSyncScope import com.simprints.infra.eventsync.SampleSyncScopes.userDownSyncScope -import com.simprints.infra.eventsync.status.down.domain.EventDownSyncOperation.Companion.oldTypes import org.junit.Test import java.util.UUID @@ -18,9 +16,7 @@ class EventDownSyncOperationTest { fun eventDownSyncOperationForProjectScope_hasAnUniqueKey() { val op = projectDownSyncScope.operations.first() assertThat(op.getUniqueKey()).isEqualTo( - uuidFrom( - "${DEFAULT_PROJECT_ID}${DEFAULT_MODES.joinToString { it.name }}$oldTypes", - ), + uuidFrom(DEFAULT_PROJECT_ID), ) } @@ -28,9 +24,7 @@ class EventDownSyncOperationTest { fun eventDownSyncOperationForUserScope_hasAnUniqueKey() { val op = userDownSyncScope.operations.first() assertThat(op.getUniqueKey()).isEqualTo( - uuidFrom( - "$DEFAULT_PROJECT_ID${DEFAULT_USER_ID.value}${DEFAULT_MODES.joinToString { it.name }}$oldTypes", - ), + uuidFrom("$DEFAULT_PROJECT_ID${DEFAULT_USER_ID.value}"), ) } @@ -40,15 +34,11 @@ class EventDownSyncOperationTest { val op1 = modulesDownSyncScope.operations[1] assertThat(op.getUniqueKey()).isEqualTo( - uuidFrom( - "$DEFAULT_PROJECT_ID${DEFAULT_MODULE_ID.value}${DEFAULT_MODES.joinToString { it.name }}$oldTypes", - ), + uuidFrom("$DEFAULT_PROJECT_ID${DEFAULT_MODULE_ID.value}"), ) assertThat(op1.getUniqueKey()).isEqualTo( - uuidFrom( - "$DEFAULT_PROJECT_ID${DEFAULT_MODULE_ID_2.value}${DEFAULT_MODES.joinToString { it.name }}$oldTypes", - ), + uuidFrom("$DEFAULT_PROJECT_ID${DEFAULT_MODULE_ID_2.value}"), ) } diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/EventDownSyncTaskTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/EventDownSyncTaskTest.kt index 5eae2b5f64..ce832d5650 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/EventDownSyncTaskTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/EventDownSyncTaskTest.kt @@ -1,6 +1,7 @@ package com.simprints.infra.eventsync.sync.down.tasks import com.google.common.truth.Truth.assertThat +import com.simprints.core.domain.face.FaceSample import com.simprints.core.domain.tokenization.asTokenizableRaw import com.simprints.core.tools.time.TimeHelper import com.simprints.infra.authstore.exceptions.RemoteDbNotSignedInException @@ -8,6 +9,7 @@ import com.simprints.infra.config.store.models.DeviceConfiguration import com.simprints.infra.config.store.models.Project import com.simprints.infra.config.sync.ConfigManager import com.simprints.infra.enrolment.records.repository.EnrolmentRecordRepository +import com.simprints.infra.enrolment.records.repository.domain.models.Subject import com.simprints.infra.enrolment.records.repository.domain.models.SubjectAction.Creation import com.simprints.infra.enrolment.records.repository.domain.models.SubjectAction.Deletion import com.simprints.infra.events.EventRepository @@ -17,6 +19,7 @@ import com.simprints.infra.events.event.domain.models.subject.EnrolmentRecordCre import com.simprints.infra.events.event.domain.models.subject.EnrolmentRecordDeletionEvent import com.simprints.infra.events.event.domain.models.subject.EnrolmentRecordEvent import com.simprints.infra.events.event.domain.models.subject.EnrolmentRecordMoveEvent +import com.simprints.infra.events.event.domain.models.subject.EnrolmentRecordUpdateEvent import com.simprints.infra.events.event.domain.models.subject.FaceReference import com.simprints.infra.events.event.domain.models.subject.FaceTemplate import com.simprints.infra.events.sampledata.SampleDefaults.DEFAULT_MODULE_ID @@ -106,6 +109,11 @@ class EventDownSyncTaskTest { DEFAULT_USER_ID, ), ) + val ENROLMENT_RECORD_UPDATE = EnrolmentRecordUpdateEvent( + "subjectId", + listOf(FaceReference("id", listOf(FaceTemplate("template")), "format")), + listOf("referenceIdToDelete"), + ) } private val projectOp = SampleSyncScopes.projectDownSyncScope.operations.first() @@ -458,9 +466,7 @@ class EventDownSyncTaskTest { coVerify { enrolmentRecordRepository.performActions( - listOf( - Deletion(eventToMoveToAttendant2.payload.enrolmentRecordDeletion.subjectId), - ), + listOf(Deletion(eventToMoveToAttendant2.payload.enrolmentRecordDeletion.subjectId)), project, ) } @@ -502,6 +508,60 @@ class EventDownSyncTaskTest { } } + @Test + fun downSync_shouldProcessRecordUpdateEvent_withCreations() = runTest { + coEvery { enrolmentRecordRepository.load(any()) } returns listOf( + Subject( + subjectId = "subjectId", + projectId = "projectId", + attendantId = "moduleId".asTokenizableRaw(), + moduleId = "attendantId".asTokenizableRaw(), + faceSamples = listOf( + FaceSample(byteArrayOf(), "format", "referenceId"), + ), + ), + ) + + val event = ENROLMENT_RECORD_UPDATE + mockProgressEmission(listOf(event)) + + eventDownSyncTask.downSync(this, projectOp, eventScope, project).toList() + + coVerify { + enrolmentRecordRepository.performActions( + withArg { actions -> actions.all { it is Creation } }, + any(), + ) + } + } + + @Test + fun downSync_shouldProcessRecordUpdateEvent_withDeletions() = runTest { + coEvery { enrolmentRecordRepository.load(any()) } returns listOf( + Subject( + subjectId = "subjectId", + projectId = "projectId", + attendantId = "moduleId".asTokenizableRaw(), + moduleId = "attendantId".asTokenizableRaw(), + faceSamples = listOf( + FaceSample(byteArrayOf(), "format", "referenceIdToDelete"), + ), + ), + ) + + val event = ENROLMENT_RECORD_UPDATE + mockProgressEmission(listOf(event)) + + eventDownSyncTask.downSync(this, projectOp, eventScope, project).toList() + + coVerify { + enrolmentRecordRepository.performActions( + withArg { actions -> actions.all { it is Deletion } }, + any(), + ) + } + } + private suspend fun mockProgressEmission(progressEvents: List) { downloadEventsChannel = Channel(capacity = Channel.UNLIMITED) coEvery { eventRemoteDataSource.getEvents(any(), any(), any()) } returns EventDownSyncResult( diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/SubjectFactoryTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/SubjectFactoryTest.kt index 0b09d66248..b0deeeb2fd 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/SubjectFactoryTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/SubjectFactoryTest.kt @@ -12,6 +12,7 @@ import com.simprints.fingerprint.capture.FingerprintCaptureResult import com.simprints.infra.enrolment.records.repository.domain.models.Subject import com.simprints.infra.events.event.domain.models.subject.EnrolmentRecordCreationEvent import com.simprints.infra.events.event.domain.models.subject.EnrolmentRecordMoveEvent +import com.simprints.infra.events.event.domain.models.subject.EnrolmentRecordUpdateEvent.EnrolmentRecordUpdatePayload import com.simprints.infra.events.event.domain.models.subject.FaceReference import com.simprints.infra.events.event.domain.models.subject.FaceTemplate import com.simprints.infra.events.event.domain.models.subject.FingerprintReference @@ -73,12 +74,14 @@ class SubjectFactoryTest { template = BASE_64_BYTES, templateQualityScore = QUALITY, format = REFERENCE_FORMAT, + referenceId = REFERENCE_ID, ), ), faceSamples = listOf( FaceSample( template = BASE_64_BYTES, format = REFERENCE_FORMAT, + referenceId = REFERENCE_ID, ), ), ) @@ -107,12 +110,112 @@ class SubjectFactoryTest { template = BASE_64_BYTES, templateQualityScore = QUALITY, format = REFERENCE_FORMAT, + referenceId = REFERENCE_ID, ), ), faceSamples = listOf( FaceSample( template = BASE_64_BYTES, format = REFERENCE_FORMAT, + referenceId = REFERENCE_ID, + ), + ), + ) + assertThat(result).isEqualTo(expected) + } + + @Test + fun `when buildSubjectFromUpdatePayload is called, correct samples list is created`() { + val subject = Subject( + subjectId = SUBJECT_ID, + projectId = PROJECT_ID, + attendantId = ATTENDANT_ID, + moduleId = MODULE_ID, + fingerprintSamples = listOf( + FingerprintSample( + fingerIdentifier = IDENTIFIER, + template = BASE_64_BYTES, + templateQualityScore = QUALITY, + format = REFERENCE_FORMAT, + referenceId = "referenceId-finger-1", + ), + FingerprintSample( + fingerIdentifier = IDENTIFIER, + template = BASE_64_BYTES, + templateQualityScore = QUALITY, + format = REFERENCE_FORMAT, + referenceId = "referenceId-finger-2", + ), + ), + faceSamples = listOf( + FaceSample( + template = BASE_64_BYTES, + format = REFERENCE_FORMAT, + referenceId = "referenceId-finger-3", + ), + FaceSample( + template = BASE_64_BYTES, + format = REFERENCE_FORMAT, + referenceId = "referenceId-finger-4", + ), + ), + ) + + val payload = EnrolmentRecordUpdatePayload( + subjectId = SUBJECT_ID, + biometricReferencesRemoved = listOf("referenceId-finger-3", "referenceId-finger-2"), + biometricReferencesAdded = listOf( + FingerprintReference( + id = "referenceId-finger-5", + format = REFERENCE_FORMAT, + templates = listOf( + FingerprintTemplate( + quality = QUALITY, + template = BASE_64_BYTES.toString(), + finger = IFingerIdentifier.LEFT_THUMB, + ), + ), + ), + FaceReference( + id = "referenceId-finger-6", + format = REFERENCE_FORMAT, + templates = listOf(FaceTemplate(template = BASE_64_BYTES.toString())), + ), + ), + ) + val result = factory.buildSubjectFromUpdatePayload(subject, payload) + + val expected = Subject( + subjectId = SUBJECT_ID, + projectId = PROJECT_ID, + attendantId = ATTENDANT_ID, + moduleId = MODULE_ID, + fingerprintSamples = listOf( + FingerprintSample( + fingerIdentifier = IDENTIFIER, + template = BASE_64_BYTES, + templateQualityScore = QUALITY, + format = REFERENCE_FORMAT, + referenceId = "referenceId-finger-1", + ), + FingerprintSample( + fingerIdentifier = IDENTIFIER, + template = BASE_64_BYTES, + templateQualityScore = QUALITY, + format = REFERENCE_FORMAT, + referenceId = "referenceId-finger-5", + ), + ), + faceSamples = listOf( + FaceSample( + template = BASE_64_BYTES, + format = REFERENCE_FORMAT, + referenceId = "referenceId-finger-4", + ), + FaceSample( + template = BASE_64_BYTES, + format = REFERENCE_FORMAT, + referenceId = "referenceId-finger-6", ), ), ) @@ -135,12 +238,14 @@ class SubjectFactoryTest { template = BASE_64_BYTES, templateQualityScore = QUALITY, format = REFERENCE_FORMAT, + referenceId = REFERENCE_ID, ), ), faceSamples = listOf( FaceSample( template = BASE_64_BYTES, format = REFERENCE_FORMAT, + referenceId = REFERENCE_ID, ), ), ) @@ -150,6 +255,7 @@ class SubjectFactoryTest { attendantId = expected.attendantId, moduleId = expected.moduleId, fingerprintResponse = FingerprintCaptureResult( + GUID1, listOf( FingerprintCaptureResult.Item( captureEventId = GUID1, @@ -165,6 +271,7 @@ class SubjectFactoryTest { ), ), faceResponse = FaceCaptureResult( + GUID1, listOf( FaceCaptureResult.Item( captureEventId = GUID1, @@ -195,12 +302,14 @@ class SubjectFactoryTest { template = BASE_64_BYTES, templateQualityScore = QUALITY, format = REFERENCE_FORMAT, + referenceId = REFERENCE_ID, ), ), faceSamples = listOf( FaceSample( template = BASE_64_BYTES, format = REFERENCE_FORMAT, + referenceId = REFERENCE_ID, ), ), ) diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/up/tasks/EventUpSyncTaskTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/up/tasks/EventUpSyncTaskTest.kt index da687fc4b3..36e407087b 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/up/tasks/EventUpSyncTaskTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/up/tasks/EventUpSyncTaskTest.kt @@ -25,7 +25,9 @@ import com.simprints.infra.events.sampledata.SampleDefaults.GUID2 import com.simprints.infra.events.sampledata.SampleDefaults.GUID3 import com.simprints.infra.events.sampledata.createAlertScreenEvent import com.simprints.infra.events.sampledata.createAuthenticationEvent +import com.simprints.infra.events.sampledata.createBiometricReferenceCreationEvent import com.simprints.infra.events.sampledata.createEnrolmentEventV2 +import com.simprints.infra.events.sampledata.createEnrolmentEventV4 import com.simprints.infra.events.sampledata.createEventWithSessionId import com.simprints.infra.events.sampledata.createFaceCaptureBiometricsEvent import com.simprints.infra.events.sampledata.createFingerprintCaptureBiometricsEvent @@ -118,7 +120,7 @@ internal class EventUpSyncTaskTest { timeHelper = timeHelper, configManager = configManager, jsonHelper = JsonHelper, - mapDomainEventScopeToApiUseCase = mapDomainEventScopeToApiUseCase + mapDomainEventScopeToApiUseCase = mapDomainEventScopeToApiUseCase, ) } @@ -266,6 +268,7 @@ internal class EventUpSyncTaskTest { createAlertScreenEvent(), // only following should be uploaded createEnrolmentEventV2(), + createEnrolmentEventV4(), createPersonCreationEvent(), createFingerprintCaptureBiometricsEvent(), createFaceCaptureBiometricsEvent(), @@ -277,7 +280,7 @@ internal class EventUpSyncTaskTest { coVerify(exactly = 1) { mapDomainEventScopeToApiUseCase(any(), capture(capturedRequest), any()) } - assertThat(capturedRequest.captured).hasSize(4) + assertThat(capturedRequest.captured).hasSize(5) } @Test @@ -295,7 +298,9 @@ internal class EventUpSyncTaskTest { // only following should be uploaded createPersonCreationEvent(), createEnrolmentEventV2(), + createEnrolmentEventV4(), createAlertScreenEvent(), + createBiometricReferenceCreationEvent(), ) eventUpSyncTask.upSync(operation, eventScope).toList() @@ -304,7 +309,7 @@ internal class EventUpSyncTaskTest { coVerify(exactly = 1) { mapDomainEventScopeToApiUseCase(any(), capture(capturedRequest), any()) } - assertThat(capturedRequest.captured).hasSize(3) + assertThat(capturedRequest.captured).hasSize(5) } @Test @@ -354,6 +359,7 @@ internal class EventUpSyncTaskTest { ) coEvery { eventRepo.getEventsFromScope(GUID2) } returns listOf( createEnrolmentEventV2(), + createEnrolmentEventV4(), createAlertScreenEvent(), ) diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/testtools/RemoteTestingHelper.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/testtools/RemoteTestingHelper.kt index a1a3659ba8..c5a24cf748 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/testtools/RemoteTestingHelper.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/testtools/RemoteTestingHelper.kt @@ -7,13 +7,17 @@ internal class RemoteTestingHelper { fun enforceThatAnyTestHasATest(type: ApiEventPayloadType?) { when (type) { ApiEventPayloadType.Callout, ApiEventPayloadType.Callback, - ApiEventPayloadType.Authentication, ApiEventPayloadType.Consent, ApiEventPayloadType.Enrolment, ApiEventPayloadType.Authorization, ApiEventPayloadType.FingerprintCapture, ApiEventPayloadType.OneToOneMatch, - ApiEventPayloadType.OneToManyMatch, ApiEventPayloadType.PersonCreation, ApiEventPayloadType.AlertScreen, ApiEventPayloadType.GuidSelection, ApiEventPayloadType.ConnectivitySnapshot, ApiEventPayloadType.Refusal, ApiEventPayloadType.CandidateRead, - ApiEventPayloadType.ScannerConnection, ApiEventPayloadType.Vero2InfoSnapshot, ApiEventPayloadType.ScannerFirmwareUpdate, ApiEventPayloadType.InvalidIntent, ApiEventPayloadType.SuspiciousIntent, ApiEventPayloadType.IntentParsing, - ApiEventPayloadType.CompletionCheck, ApiEventPayloadType.FaceOnboardingComplete, ApiEventPayloadType.FaceFallbackCapture, ApiEventPayloadType.FaceCapture, - ApiEventPayloadType.FaceCaptureConfirmation, ApiEventPayloadType.FingerprintCaptureBiometrics, ApiEventPayloadType.FaceCaptureBiometrics, - ApiEventPayloadType.EventDownSyncRequest, ApiEventPayloadType.EventUpSyncRequest, - ApiEventPayloadType.LicenseCheck, ApiEventPayloadType.AgeGroupSelection, + ApiEventPayloadType.Authentication, ApiEventPayloadType.Consent, ApiEventPayloadType.Enrolment, + ApiEventPayloadType.Authorization, ApiEventPayloadType.FingerprintCapture, ApiEventPayloadType.OneToOneMatch, + ApiEventPayloadType.OneToManyMatch, ApiEventPayloadType.PersonCreation, ApiEventPayloadType.AlertScreen, + ApiEventPayloadType.GuidSelection, ApiEventPayloadType.ConnectivitySnapshot, ApiEventPayloadType.Refusal, + ApiEventPayloadType.CandidateRead, ApiEventPayloadType.ScannerConnection, ApiEventPayloadType.Vero2InfoSnapshot, + ApiEventPayloadType.ScannerFirmwareUpdate, ApiEventPayloadType.InvalidIntent, ApiEventPayloadType.SuspiciousIntent, + ApiEventPayloadType.IntentParsing, ApiEventPayloadType.CompletionCheck, ApiEventPayloadType.FaceOnboardingComplete, + ApiEventPayloadType.FaceFallbackCapture, ApiEventPayloadType.FaceCapture, ApiEventPayloadType.FaceCaptureConfirmation, + ApiEventPayloadType.FingerprintCaptureBiometrics, ApiEventPayloadType.FaceCaptureBiometrics, + ApiEventPayloadType.EventDownSyncRequest, ApiEventPayloadType.EventUpSyncRequest, ApiEventPayloadType.LicenseCheck, + ApiEventPayloadType.AgeGroupSelection, ApiEventPayloadType.BiometricReferenceCreation, null, -> { // ADD TEST FOR NEW EVENT IN THIS CLASS diff --git a/infra/event-sync/src/test/resources/responses/down_sync_7events.json b/infra/event-sync/src/test/resources/responses/down_sync_8events.json similarity index 79% rename from infra/event-sync/src/test/resources/responses/down_sync_7events.json rename to infra/event-sync/src/test/resources/responses/down_sync_8events.json index 7a78be498c..0d98025ab3 100644 --- a/infra/event-sync/src/test/resources/responses/down_sync_7events.json +++ b/infra/event-sync/src/test/resources/responses/down_sync_8events.json @@ -357,5 +357,63 @@ "type": "EnrolmentRecordMove", "version": 0 } + }, + { + "id": "a3102202-956b-4444-bf3e-75c39cee35ba", + "labels": { + "attendantId": [ + "user a" + ], + "subjectId": [ + "5d08d867-fa87-41e4-8df0-98c7256950c7" + ] + }, + "payload": { + "subjectId": "5d08d867-fa87-41e4-8df0-98c7256950c7", + "biometricReferencesAdded": [ + { + "format": "NEC_1", + "templates": [ + { + "finger": "LEFT_INDEX_FINGER", + "quality": 55, + "template": "nN33bhY4llJZZFI+3NEsHSDWSDjiNBJCKlc+Pi0HEL/G0TVo3oEHxu+7tOxwze454ExIJnhijFPZxn6pEXjKEJzoaFrN0ncQwsuA7ncQ4BZVfxf7V6YqO6ornrfrv1z0ViYZrItT2yJ5fIdV1NFXb+T2MHxVVQdEs8VZLFI68+QAxLS/gvFKttQ0kjs4UQfGcYSq6tH2vHidTwFtg9F7SZAfzVioC++l7c/7AneN91HBZVCf8q1vChKMXLqaDe7MRIe6nuaRZLGAy/jz5kloBabPFarevp1b0DoFGDTUTzlGDVIwYLPUYqH2myrM2ykbA339GTFZzmYx2+8SQX7px5akcZ35p0bGiNJGAG1odUuqu4DMBMSn4mN3QAzV5BNPNAhPfakbJd+1oeSQOfkKSU0Uh6+qTwW449ZZnjkYRCJn7RI94mFv" + }, + { + "finger": "LEFT_THUMB", + "quality": 56, + "template": "uoHuxFvqWsnNhequ0JkpOoic6D3qLaFhXvL5R5i7xyWpJALAcLBP0LzQbtrvQTYfuhm3pdJa6zkCLH1jGOshLKJsTFqBZk+MPb9+ol0ugGsfSh7UpkLemr3lxgWRCNSzsktzg7cvZvkdv5GIDfi5aObd/MJ5AeNrNXOI6wpuobiQhIeVciBNYsO/MaaNBw8y+Tiqq8eQ1LAz1vo/tuvIWhN45KWEX4BXXePYnPB3J9VNNFsDUDE7Ex6hm5v42dWK53g/yWa1XTDDZHA2yTLWJGrsfDybaVbIPiCJ3NP1ZCAD+iBPdR8B1GzFfkKI31IqZPKEqCvTQUirZDV7pOo6OjozAGAlcscgozA+r2oLqb3agCuimDUpPIP1zwPs+AFIusiwI1wmGEh2zg2Yo7G+wPy3flnrjtJ1tZP08op2k5U0kzJCb2l8KA+G" + }, + { + "finger": "RIGHT_4TH_FINGER", + "quality": 50, + "template": "gnoXCrttqcGy21pE8lHHIvHLZasSfUpWW8/ckM9Sq+DOLOdsueG4SIUdDr9kANtC8Fx+thOPKBO2YMQdfgHplrqlgPql6SB0iGZ7MGV8L8ly3er7QvNyCeNd/NjDPVrJoEAF71XYX7P7WiQLGunkLEsnoD+87H9Id/y4cr6eWTaj7wfXIG/j4ZXIUHn285BfxxJVE76AGc5owXZYX5xcbrWv0CogXj9jEaxh2zhZ/ETX/8ux6HN3K3JatQV8yS6AEB/qCfSPnZjYg1+1tox3iqaguS0zczYyg6Hc7QBpwEgLUhOKAe7adZwDj4fQKX1NHdVcl3WJH/X8MMkP+XzhiD4id00uppoZAN36n1mmXBjY7kdFkBuZn35oPOtFluWdDO8+4x2A8kc5wUhT4xOgkaFNSYuwskqRgHgJQfePImwDE6vzqhuoT/s=" + }, + { + "finger": "RIGHT_5TH_FINGER", + "quality": 44, + "template": "bNzfaVB8BKjhcIrY8Q3TSDG0cRXY3sbMUwRW0b4tc0e7xvv9gkkgMJrzeYHWcc7uGoDG0eI61Q+O9OVsmwdJudPembgkyK3loQRhsjQM4UIpGQu8NkGEbAn+9gwmYtTi1ezkzLbjknq5ktHDUxaWPV0PDhNo76cki5HSQRT7Ex1cbA2oJ5mn8sNeQzIoHEDo3VjJfYLgvWZVuugBcFaCLZw2oHUjfhounWZ900jZUzGP3aw0kv52fjkKdmpkldtbycQwOtzbnFnGXXSxR9MfHeBu64MuvoX3LGhMALtQpp/O+3noRxldRmcdu7mTEzuynmZD4v0lQV4X/tstVjplQ1hBug5zGo2W7ObXwcplpRsj0+/dbtmUf9Cck089DkrLd6PWEQOg0vId0WunisR7YiYUj3GKTfBP76JtrPfCJnsJRQy2+9mcJgM=" + } + ], + "type": "FingerprintReference" + }, + { + "format": "RANK_ONE_1_23", + "templates": [ + { + "template": "xq98xkmiTbqHdoYcjTP0x7nb/iX3vl1ltNev/w+ZCvzksLSsylKx2DmPMKVeuKnsgGjHElgI7YrpJAxIFR7PqmvwNnwr/y4PCr2G1/sfetE9Jv8DAzL0mcb2D02aQzZ1hjiAUEeTnE0aUiWtlSMj/ijDvZfRcwTd4xxfyHAE0GEnRB+yvuCimOeQ3lL0bZkjR6okrt48xEWBkgsPHNV7UujGjNPBiOSFHzwcJaidBHPPVkAuBhIXHdOo4jVedYB+2YBQqfrqeCLgL1jMvUKYvvGXszc3eQUL+vQuBl+pqe1j8Yr0l74cKqABfPrkCcb50Vi1DQUiNWTXvkmY3+/T7w8hFp3gWoFm70A/f8kYiBaVVd0Y4Kb5/Mu1UE6kg6ScG8O+z9GtYkpzXvpqRLoyQj/AJtNltZQ70hx1nccl1lUx6ftchbfuJX8qng==" + }, + { + "template": "xlzqoNui8yoJduJ48HxnyLzt2wR/Ey1m04Gq8+v4GXlJwQgQNPQLLUkZINR9b7+91yLb8CntoVi0DF3It4wNpQOptL2rynVPL5J5NDrJJsOdMEegbOVup4Co4TbbykalKAcQo/BN1TSpjrh3Ho1mstSsg90bpvY7boI0b7Uql5H6W1GUri2NqnLMw/GOIZiHmmXs4fnWXK9t4ah0i1ctrIbseW8VIViiNe9NzE202HJXJX51+nlLdxXbm9m5bQZhGKQxO4YJdmldqCwIQmBW4D6dw45NbJnsxhN+jpIQV5kpYgQXNRqrzeqo54S4Ta9RulZfJIsFpgK6F9eSBqVjn9iyIvDUXfjes6kcsU8dbWq91apxtREacoJh3b0jlcM6jxpJEaGUSVoVTzz68AqvAB7e11rypconzCPg/sg2fs/Pef85ZvvXfqnR" + } + ], + "type": "FaceReference" + } + ], + "biometricReferencesRemoved": [ + "5d08d867-fa87-41e4-8df0-98c7256950c7" + ], + "type": "EnrolmentRecordUpdate" + } } ] diff --git a/infra/events/src/debug/java/com/simprints/infra/events/sampledata/EventFactoryUtils.kt b/infra/events/src/debug/java/com/simprints/infra/events/sampledata/EventFactoryUtils.kt index 744fbd0305..754919954a 100644 --- a/infra/events/src/debug/java/com/simprints/infra/events/sampledata/EventFactoryUtils.kt +++ b/infra/events/src/debug/java/com/simprints/infra/events/sampledata/EventFactoryUtils.kt @@ -16,6 +16,7 @@ import com.simprints.infra.events.event.domain.models.AuthenticationEvent.Authen import com.simprints.infra.events.event.domain.models.AuthorizationEvent import com.simprints.infra.events.event.domain.models.AuthorizationEvent.AuthorizationPayload import com.simprints.infra.events.event.domain.models.AuthorizationEvent.AuthorizationPayload.AuthorizationResult.AUTHORIZED +import com.simprints.infra.events.event.domain.models.BiometricReferenceCreationEvent import com.simprints.infra.events.event.domain.models.CandidateReadEvent import com.simprints.infra.events.event.domain.models.CandidateReadEvent.CandidateReadPayload.LocalResult.FOUND import com.simprints.infra.events.event.domain.models.CandidateReadEvent.CandidateReadPayload.RemoteResult.NOT_FOUND @@ -24,8 +25,8 @@ import com.simprints.infra.events.event.domain.models.ConnectivitySnapshotEvent import com.simprints.infra.events.event.domain.models.ConsentEvent import com.simprints.infra.events.event.domain.models.ConsentEvent.ConsentPayload.Result.ACCEPTED import com.simprints.infra.events.event.domain.models.ConsentEvent.ConsentPayload.Type.INDIVIDUAL -import com.simprints.infra.events.event.domain.models.EnrolmentEventV1 import com.simprints.infra.events.event.domain.models.EnrolmentEventV2 +import com.simprints.infra.events.event.domain.models.EnrolmentEventV4 import com.simprints.infra.events.event.domain.models.Event import com.simprints.infra.events.event.domain.models.EventType import com.simprints.infra.events.event.domain.models.FingerComparisonStrategy @@ -316,9 +317,13 @@ fun createEnrolmentEventV2() = EnrolmentEventV2( GUID2, ) -fun createEnrolmentEventV1() = EnrolmentEventV1( +fun createEnrolmentEventV4() = EnrolmentEventV4( CREATED_AT, GUID1, + DEFAULT_PROJECT_ID, + DEFAULT_MODULE_ID, + DEFAULT_USER_ID, + listOf(GUID1, GUID2), ) fun createFingerprintCaptureEvent() = FingerprintCaptureEvent( @@ -362,6 +367,7 @@ fun createOneToManyMatchEvent() = OneToManyMatchEvent( MatchPool(PROJECT, 100), "RANK_ONE", listOf(MatchEntry(GUID1, 0F)), + GUID2, ) fun createOneToOneMatchEvent() = OneToOneMatchEvent( @@ -371,6 +377,7 @@ fun createOneToOneMatchEvent() = OneToOneMatchEvent( "SIM_AFIS", MatchEntry(GUID1, 10F), FingerComparisonStrategy.CROSS_FINGER_USING_MEAN_OF_MAX, + GUID2, ) fun createPersonCreationEvent() = PersonCreationEvent( @@ -453,3 +460,10 @@ fun createAgeGroupSelectionEvent() = AgeGroupSelectionEvent( endedAt = ENDED_AT, subjectAgeGroup = AgeGroupSelectionEvent.AgeGroup(1, 2), ) + +fun createBiometricReferenceCreationEvent() = BiometricReferenceCreationEvent( + startTime = CREATED_AT, + referenceId = GUID1, + modality = BiometricReferenceCreationEvent.BiometricReferenceModality.FACE, + captureIds = listOf(GUID1, GUID2), +) diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/BiometricReferenceCreationEvent.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/BiometricReferenceCreationEvent.kt new file mode 100644 index 0000000000..9e563d2408 --- /dev/null +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/BiometricReferenceCreationEvent.kt @@ -0,0 +1,57 @@ +package com.simprints.infra.events.event.domain.models + +import androidx.annotation.Keep +import com.simprints.core.domain.tokenization.TokenizableString +import com.simprints.core.tools.time.Timestamp +import com.simprints.infra.config.store.models.TokenKeyType +import com.simprints.infra.events.event.domain.models.EventType.BIOMETRIC_REFERENCE_CREATION +import java.util.UUID + +@Keep +data class BiometricReferenceCreationEvent( + override val id: String = UUID.randomUUID().toString(), + override val payload: BiometricReferenceCreationPayload, + override val type: EventType, + override var scopeId: String? = null, + override var projectId: String? = null, +) : Event() { + constructor( + startTime: Timestamp, + referenceId: String, + modality: BiometricReferenceModality, + captureIds: List, + ) : this( + UUID.randomUUID().toString(), + BiometricReferenceCreationPayload( + createdAt = startTime, + eventVersion = EVENT_VERSION, + id = referenceId, + modality = modality, + captureIds = captureIds, + ), + BIOMETRIC_REFERENCE_CREATION, + ) + + override fun getTokenizableFields(): Map = emptyMap() + + override fun setTokenizedFields(map: Map): Event = this + + data class BiometricReferenceCreationPayload( + override val createdAt: Timestamp, + override val eventVersion: Int, + val id: String, + val modality: BiometricReferenceModality, + val captureIds: List, + override val endedAt: Timestamp? = null, + override val type: EventType = BIOMETRIC_REFERENCE_CREATION, + ) : EventPayload() + + enum class BiometricReferenceModality { + FACE, + FINGERPRINT, + } + + companion object { + const val EVENT_VERSION = 1 + } +} diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/EnrolmentEventV1.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/EnrolmentEventV1.kt index f5599fbb85..e69de29bb2 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/EnrolmentEventV1.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/EnrolmentEventV1.kt @@ -1,46 +0,0 @@ -package com.simprints.infra.events.event.domain.models - -import androidx.annotation.Keep -import com.simprints.core.domain.tokenization.TokenizableString -import com.simprints.core.tools.time.Timestamp -import com.simprints.infra.config.store.models.TokenKeyType -import com.simprints.infra.events.event.domain.models.EventType.ENROLMENT_V1 -import java.util.UUID - -@Keep -@Deprecated("Used only for the migration before 2021.1.0") -data class EnrolmentEventV1( - override val id: String = UUID.randomUUID().toString(), - override val payload: EnrolmentPayload, - override val type: EventType, - override var scopeId: String? = null, - override var projectId: String? = null, -) : Event() { - constructor( - createdAt: Timestamp, - personId: String, - ) : this( - UUID.randomUUID().toString(), - EnrolmentPayload(createdAt, EVENT_VERSION, personId), - ENROLMENT_V1, - ) - - override fun getTokenizableFields(): Map = emptyMap() - - override fun setTokenizedFields(map: Map) = this // No tokenized fields - - @Keep - data class EnrolmentPayload( - override val createdAt: Timestamp, - override val eventVersion: Int, - val personId: String, - override val endedAt: Timestamp? = null, - override val type: EventType = ENROLMENT_V1, - ) : EventPayload() { - override fun toSafeString(): String = "person ID: $personId" - } - - companion object { - const val EVENT_VERSION = 2 - } -} diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/EnrolmentEventV2.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/EnrolmentEventV2.kt index 420f38dc26..a5c40372d8 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/EnrolmentEventV2.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/EnrolmentEventV2.kt @@ -8,6 +8,7 @@ import com.simprints.infra.events.event.domain.models.EventType.ENROLMENT_V2 import java.util.UUID @Keep +@Deprecated("Replaced by v4 in 2025.1.0") data class EnrolmentEventV2( override val id: String = UUID.randomUUID().toString(), override val payload: EnrolmentPayload, diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/EnrolmentEventV4.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/EnrolmentEventV4.kt new file mode 100644 index 0000000000..01cb57dc1f --- /dev/null +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/EnrolmentEventV4.kt @@ -0,0 +1,69 @@ +package com.simprints.infra.events.event.domain.models + +import androidx.annotation.Keep +import com.simprints.core.domain.tokenization.TokenizableString +import com.simprints.core.tools.time.Timestamp +import com.simprints.infra.config.store.models.TokenKeyType +import com.simprints.infra.events.event.domain.models.EventType.ENROLMENT_V4 +import java.util.UUID + +@Keep +data class EnrolmentEventV4( + override val id: String = UUID.randomUUID().toString(), + override val payload: EnrolmentPayload, + override val type: EventType, + override var scopeId: String? = null, + override var projectId: String? = null, +) : Event() { + constructor( + createdAt: Timestamp, + subjectId: String, + projectId: String, + moduleId: TokenizableString, + attendantId: TokenizableString, + biometricReferenceIds: List, + ) : this( + UUID.randomUUID().toString(), + EnrolmentPayload( + createdAt = createdAt, + eventVersion = EVENT_VERSION, + subjectId = subjectId, + projectId = projectId, + moduleId = moduleId, + attendantId = attendantId, + biometricReferenceIds = biometricReferenceIds, + ), + ENROLMENT_V4, + ) + + override fun getTokenizableFields(): Map = mapOf( + TokenKeyType.AttendantId to payload.attendantId, + TokenKeyType.ModuleId to payload.moduleId, + ) + + override fun setTokenizedFields(map: Map) = this.copy( + payload = payload.copy( + attendantId = map[TokenKeyType.AttendantId] ?: payload.attendantId, + moduleId = map[TokenKeyType.ModuleId] ?: payload.moduleId, + ), + ) + + @Keep + data class EnrolmentPayload( + override val createdAt: Timestamp, + override val eventVersion: Int, + val subjectId: String, + val projectId: String, + val moduleId: TokenizableString, + val attendantId: TokenizableString, + val biometricReferenceIds: List, + override val endedAt: Timestamp? = null, + override val type: EventType = ENROLMENT_V4, + ) : EventPayload() { + override fun toSafeString(): String = "subject ID: $subjectId, module ID: $moduleId" + } + + companion object { + const val EVENT_VERSION = 4 + } +} diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/Event.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/Event.kt index 9df1bd7a5e..9c143ed770 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/Event.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/Event.kt @@ -9,6 +9,7 @@ import com.simprints.infra.events.event.domain.models.EventType.Companion.AGE_GR import com.simprints.infra.events.event.domain.models.EventType.Companion.ALERT_SCREEN_KEY import com.simprints.infra.events.event.domain.models.EventType.Companion.AUTHENTICATION_KEY import com.simprints.infra.events.event.domain.models.EventType.Companion.AUTHORIZATION_KEY +import com.simprints.infra.events.event.domain.models.EventType.Companion.BIOMETRIC_REFERENCE_CREATION_KEY import com.simprints.infra.events.event.domain.models.EventType.Companion.CALLBACK_CONFIRMATION_KEY import com.simprints.infra.events.event.domain.models.EventType.Companion.CALLBACK_ENROLMENT_KEY import com.simprints.infra.events.event.domain.models.EventType.Companion.CALLBACK_ERROR_KEY @@ -24,8 +25,8 @@ import com.simprints.infra.events.event.domain.models.EventType.Companion.CANDID import com.simprints.infra.events.event.domain.models.EventType.Companion.COMPLETION_CHECK_KEY import com.simprints.infra.events.event.domain.models.EventType.Companion.CONNECTIVITY_SNAPSHOT_KEY import com.simprints.infra.events.event.domain.models.EventType.Companion.CONSENT_KEY -import com.simprints.infra.events.event.domain.models.EventType.Companion.ENROLMENT_V1_KEY import com.simprints.infra.events.event.domain.models.EventType.Companion.ENROLMENT_V2_KEY +import com.simprints.infra.events.event.domain.models.EventType.Companion.ENROLMENT_V4_KEY import com.simprints.infra.events.event.domain.models.EventType.Companion.EVENT_DOWN_SYNC_REQUEST_KEY import com.simprints.infra.events.event.domain.models.EventType.Companion.EVENT_UP_SYNC_REQUEST_KEY import com.simprints.infra.events.event.domain.models.EventType.Companion.FACE_CAPTURE_BIOMETRICS_KEY @@ -113,8 +114,8 @@ import com.simprints.infra.events.event.domain.models.upsync.EventUpSyncRequestE JsonSubTypes.Type(value = CompletionCheckEvent::class, name = COMPLETION_CHECK_KEY), JsonSubTypes.Type(value = ConnectivitySnapshotEvent::class, name = CONNECTIVITY_SNAPSHOT_KEY), JsonSubTypes.Type(value = ConsentEvent::class, name = CONSENT_KEY), - JsonSubTypes.Type(value = EnrolmentEventV1::class, name = ENROLMENT_V1_KEY), JsonSubTypes.Type(value = EnrolmentEventV2::class, name = ENROLMENT_V2_KEY), + JsonSubTypes.Type(value = EnrolmentEventV4::class, name = ENROLMENT_V4_KEY), JsonSubTypes.Type(value = FingerprintCaptureEvent::class, name = FINGERPRINT_CAPTURE_KEY), JsonSubTypes.Type( value = FingerprintCaptureBiometricsEvent::class, @@ -138,6 +139,7 @@ import com.simprints.infra.events.event.domain.models.upsync.EventUpSyncRequestE JsonSubTypes.Type(value = EventUpSyncRequestEvent::class, name = EVENT_UP_SYNC_REQUEST_KEY), JsonSubTypes.Type(value = LicenseCheckEvent::class, name = LICENSE_CHECK_KEY), JsonSubTypes.Type(value = AgeGroupSelectionEvent::class, name = AGE_GROUP_SELECTION_KEY), + JsonSubTypes.Type(value = BiometricReferenceCreationEvent::class, name = BIOMETRIC_REFERENCE_CREATION_KEY), ) abstract class Event { abstract val id: String diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/EventPayload.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/EventPayload.kt index 518cbff56a..8018878ee1 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/EventPayload.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/EventPayload.kt @@ -7,6 +7,7 @@ import com.simprints.infra.events.event.domain.models.AgeGroupSelectionEvent.Age import com.simprints.infra.events.event.domain.models.AlertScreenEvent.AlertScreenPayload import com.simprints.infra.events.event.domain.models.AuthenticationEvent.AuthenticationPayload import com.simprints.infra.events.event.domain.models.AuthorizationEvent.AuthorizationPayload +import com.simprints.infra.events.event.domain.models.BiometricReferenceCreationEvent.BiometricReferenceCreationPayload import com.simprints.infra.events.event.domain.models.CandidateReadEvent.CandidateReadPayload import com.simprints.infra.events.event.domain.models.CompletionCheckEvent.CompletionCheckPayload import com.simprints.infra.events.event.domain.models.ConnectivitySnapshotEvent.ConnectivitySnapshotPayload @@ -70,8 +71,8 @@ import com.simprints.infra.events.event.domain.models.upsync.EventUpSyncRequestE JsonSubTypes.Type(value = CompletionCheckPayload::class, name = EventType.COMPLETION_CHECK_KEY), JsonSubTypes.Type(value = ConnectivitySnapshotPayload::class, name = EventType.CONNECTIVITY_SNAPSHOT_KEY), JsonSubTypes.Type(value = ConsentPayload::class, name = EventType.CONSENT_KEY), - JsonSubTypes.Type(value = EnrolmentEventV1.EnrolmentPayload::class, name = EventType.ENROLMENT_V1_KEY), JsonSubTypes.Type(value = EnrolmentEventV2.EnrolmentPayload::class, name = EventType.ENROLMENT_V2_KEY), + JsonSubTypes.Type(value = EnrolmentEventV4.EnrolmentPayload::class, name = EventType.ENROLMENT_V4_KEY), JsonSubTypes.Type(value = FingerprintCapturePayload::class, name = EventType.FINGERPRINT_CAPTURE_KEY), JsonSubTypes.Type(value = FingerprintCaptureBiometricsPayload::class, name = EventType.FINGERPRINT_CAPTURE_BIOMETRICS_KEY), JsonSubTypes.Type(value = GuidSelectionPayload::class, name = EventType.GUID_SELECTION_KEY), @@ -89,6 +90,7 @@ import com.simprints.infra.events.event.domain.models.upsync.EventUpSyncRequestE JsonSubTypes.Type(value = EventUpSyncRequestPayload::class, name = Companion.EVENT_UP_SYNC_REQUEST_KEY), JsonSubTypes.Type(value = LicenseCheckEventPayload::class, name = Companion.LICENSE_CHECK_KEY), JsonSubTypes.Type(value = AgeGroupSelectionPayload::class, name = Companion.AGE_GROUP_SELECTION_KEY), + JsonSubTypes.Type(value = BiometricReferenceCreationPayload::class, name = Companion.BIOMETRIC_REFERENCE_CREATION_KEY), ) abstract class EventPayload { abstract val type: EventType diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/EventType.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/EventType.kt index 4031238052..c67dd9f77d 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/EventType.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/EventType.kt @@ -14,12 +14,12 @@ enum class EventType { // key added: CONSENT_KEY CONSENT, - // key added: ENROLMENT_V1_KEY - ENROLMENT_V1, - - // key added: ENROLMENT_V1_KE2 + // key added: ENROLMENT_V2_KEY ENROLMENT_V2, + // key added: ENROLMENT_V4_KEY + ENROLMENT_V4, + // key added: AUTHORIZATION_KEY AUTHORIZATION, @@ -133,6 +133,9 @@ enum class EventType { // key added: AGE_GROUP_SELECTION_KEY AGE_GROUP_SELECTION, + + // key added: BIOMETRIC_REFERENCE_CREATION_KEY + BIOMETRIC_REFERENCE_CREATION, ; companion object { @@ -154,8 +157,8 @@ enum class EventType { const val FACE_CAPTURE_CONFIRMATION_KEY = "FACE_CAPTURE_CONFIRMATION" const val AUTHENTICATION_KEY = "AUTHENTICATION" const val CONSENT_KEY = "CONSENT" - const val ENROLMENT_V1_KEY = "ENROLMENT_V1" const val ENROLMENT_V2_KEY = "ENROLMENT_V2" + const val ENROLMENT_V4_KEY = "ENROLMENT_V4" const val AUTHORIZATION_KEY = "AUTHORIZATION" const val FINGERPRINT_CAPTURE_KEY = "FINGERPRINT_CAPTURE" const val FINGERPRINT_CAPTURE_BIOMETRICS_KEY = "FINGERPRINT_CAPTURE_BIOMETRICS" @@ -178,5 +181,6 @@ enum class EventType { const val EVENT_UP_SYNC_REQUEST_KEY = "EVENT_UP_SYNC_REQUEST" const val LICENSE_CHECK_KEY = "LICENSE_CHECK" const val AGE_GROUP_SELECTION_KEY = "AGE_GROUP_SELECTION" + const val BIOMETRIC_REFERENCE_CREATION_KEY = "BIOMETRIC_REFERENCE_CREATION" } } diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/OneToManyMatchEvent.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/OneToManyMatchEvent.kt index cd712ceb38..ff7f1a0a06 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/OneToManyMatchEvent.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/OneToManyMatchEvent.kt @@ -1,6 +1,8 @@ package com.simprints.infra.events.event.domain.models import androidx.annotation.Keep +import com.fasterxml.jackson.annotation.JsonSubTypes +import com.fasterxml.jackson.annotation.JsonTypeInfo import com.simprints.core.domain.tokenization.TokenizableString import com.simprints.core.tools.time.Timestamp import com.simprints.infra.config.store.models.TokenKeyType @@ -21,28 +23,74 @@ data class OneToManyMatchEvent( pool: OneToManyMatchPayload.MatchPool, matcher: String, result: List?, + probeBiometricReferenceId: String, ) : this( - UUID.randomUUID().toString(), - OneToManyMatchPayload(createdAt, EVENT_VERSION, endTime, pool, matcher, result), - ONE_TO_MANY_MATCH, + id = UUID.randomUUID().toString(), + payload = OneToManyMatchPayload.OneToManyMatchPayloadV3( + createdAt = createdAt, + eventVersion = EVENT_VERSION, + endedAt = endTime, + pool = pool, + matcher = matcher, + result = result, + probeBiometricReferenceId = probeBiometricReferenceId, + ), + type = ONE_TO_MANY_MATCH, ) override fun getTokenizableFields(): Map = emptyMap() override fun setTokenizedFields(map: Map) = this // No tokenized fields + @JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.EXISTING_PROPERTY, + property = "eventVersion", + visible = true, + ) + @JsonSubTypes( + JsonSubTypes.Type( + value = OneToManyMatchPayload.OneToManyMatchPayloadV2::class, + name = EVENT_VERSION_WITHOUT_REFERENCE_ID.toString(), + ), + JsonSubTypes.Type( + value = OneToManyMatchPayload.OneToManyMatchPayloadV3::class, + name = EVENT_VERSION.toString(), + ), + ) @Keep - data class OneToManyMatchPayload( + sealed class OneToManyMatchPayload( override val createdAt: Timestamp, override val eventVersion: Int, override val endedAt: Timestamp?, - val pool: MatchPool, - val matcher: String, - val result: List?, + open val pool: MatchPool, + open val matcher: String, + open val result: List?, override val type: EventType = ONE_TO_MANY_MATCH, ) : EventPayload() { override fun toSafeString(): String = "matcher: $matcher, pool: ${pool.type}, size: ${pool.count}, results: ${result?.size}" + @Keep + data class OneToManyMatchPayloadV2( + override val createdAt: Timestamp, + override val eventVersion: Int, + override val endedAt: Timestamp?, + override val pool: MatchPool, + override val matcher: String, + override val result: List?, + ) : OneToManyMatchPayload(createdAt, eventVersion, endedAt, pool, matcher, result) + + @Keep + data class OneToManyMatchPayloadV3( + override val createdAt: Timestamp, + override val eventVersion: Int, + override val endedAt: Timestamp?, + override val pool: MatchPool, + override val matcher: String, + override val result: List?, + val probeBiometricReferenceId: String, + ) : OneToManyMatchPayload(createdAt, eventVersion, endedAt, pool, matcher, result) + @Keep data class MatchPool( val type: MatchPoolType, @@ -58,6 +106,7 @@ data class OneToManyMatchEvent( } companion object { - const val EVENT_VERSION = 2 + const val EVENT_VERSION_WITHOUT_REFERENCE_ID = 2 + const val EVENT_VERSION = 3 } } diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/OneToOneMatchEvent.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/OneToOneMatchEvent.kt index 155e80ee85..db70b20275 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/OneToOneMatchEvent.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/OneToOneMatchEvent.kt @@ -1,6 +1,8 @@ package com.simprints.infra.events.event.domain.models import androidx.annotation.Keep +import com.fasterxml.jackson.annotation.JsonSubTypes +import com.fasterxml.jackson.annotation.JsonTypeInfo import com.simprints.core.domain.tokenization.TokenizableString import com.simprints.core.tools.time.Timestamp import com.simprints.infra.config.store.models.TokenKeyType @@ -22,9 +24,10 @@ data class OneToOneMatchEvent( matcher: String, result: MatchEntry?, fingerComparisonStrategy: FingerComparisonStrategy?, + probeBiometricReferenceId: String, ) : this( - UUID.randomUUID().toString(), - OneToOneMatchPayload( + id = UUID.randomUUID().toString(), + payload = OneToOneMatchPayload.OneToOneMatchPayloadV4( createdAt = createdAt, eventVersion = EVENT_VERSION, endedAt = endTime, @@ -32,30 +35,71 @@ data class OneToOneMatchEvent( matcher = matcher, result = result, fingerComparisonStrategy = fingerComparisonStrategy, + probeBiometricReferenceId = probeBiometricReferenceId, ), - ONE_TO_ONE_MATCH, + type = ONE_TO_ONE_MATCH, ) override fun getTokenizableFields(): Map = emptyMap() override fun setTokenizedFields(map: Map) = this // No tokenized fields + @JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.EXISTING_PROPERTY, + property = "eventVersion", + visible = true, + ) + @JsonSubTypes( + JsonSubTypes.Type( + value = OneToOneMatchPayload.OneToOneMatchPayloadV3::class, + name = VERSION_WITHOUT_REFERENCE_ID.toString(), + ), + JsonSubTypes.Type( + value = OneToOneMatchPayload.OneToOneMatchPayloadV4::class, + name = EVENT_VERSION.toString(), + ), + ) @Keep - data class OneToOneMatchPayload( + sealed class OneToOneMatchPayload( override val createdAt: Timestamp, override val eventVersion: Int, override var endedAt: Timestamp?, - val candidateId: String, - val matcher: String, - val result: MatchEntry?, - val fingerComparisonStrategy: FingerComparisonStrategy?, + open val candidateId: String, + open val matcher: String, + open val result: MatchEntry?, + open val fingerComparisonStrategy: FingerComparisonStrategy?, override val type: EventType = ONE_TO_ONE_MATCH, ) : EventPayload() { override fun toSafeString(): String = "matcher: $matcher, candidate ID: $candidateId, " + "result: ${result?.score}, finger strategy: $fingerComparisonStrategy" + + @Keep + data class OneToOneMatchPayloadV3( + override val createdAt: Timestamp, + override val eventVersion: Int, + override var endedAt: Timestamp?, + override val candidateId: String, + override val matcher: String, + override val result: MatchEntry?, + override val fingerComparisonStrategy: FingerComparisonStrategy?, + ) : OneToOneMatchPayload(createdAt, eventVersion, endedAt, candidateId, matcher, result, fingerComparisonStrategy) + + @Keep + data class OneToOneMatchPayloadV4( + override val createdAt: Timestamp, + override val eventVersion: Int, + override var endedAt: Timestamp?, + override val candidateId: String, + override val matcher: String, + override val result: MatchEntry?, + override val fingerComparisonStrategy: FingerComparisonStrategy?, + val probeBiometricReferenceId: String, + ) : OneToOneMatchPayload(createdAt, eventVersion, endedAt, candidateId, matcher, result, fingerComparisonStrategy) } companion object { - const val EVENT_VERSION = 3 + const val VERSION_WITHOUT_REFERENCE_ID = 3 + const val EVENT_VERSION = 4 } } diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/PersonCreationEvent.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/PersonCreationEvent.kt index 40348311d8..054d8652fd 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/PersonCreationEvent.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/PersonCreationEvent.kt @@ -8,6 +8,7 @@ import com.simprints.infra.events.event.domain.models.EventType.PERSON_CREATION import java.util.UUID @Keep +@Deprecated("Replaced by BiometricReferenceCreationEvent in 2025.1.0") data class PersonCreationEvent( override val id: String = UUID.randomUUID().toString(), override val payload: PersonCreationPayload, @@ -40,6 +41,7 @@ data class PersonCreationEvent( // At the end of the sequence of capture, we build a Person object used either for enrolment, verification or identification @Keep + @Deprecated("Replaced by BiometricReferenceCreationEvent") data class PersonCreationPayload( override val createdAt: Timestamp, override val eventVersion: Int, diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordCreationEvent.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordCreationEvent.kt index 914dce84f2..c976823440 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordCreationEvent.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordCreationEvent.kt @@ -2,9 +2,7 @@ package com.simprints.infra.events.event.domain.models.subject import androidx.annotation.Keep import com.simprints.core.domain.face.FaceSample -import com.simprints.core.domain.face.uniqueId import com.simprints.core.domain.fingerprint.FingerprintSample -import com.simprints.core.domain.fingerprint.uniqueId import com.simprints.core.domain.tokenization.TokenizableString import com.simprints.core.tools.utils.EncodingUtils import java.util.UUID @@ -64,7 +62,7 @@ data class EnrolmentRecordCreationEvent( encoder: EncodingUtils, ) = if (fingerprintSamples.isNotEmpty()) { FingerprintReference( - fingerprintSamples.uniqueId() ?: "", + fingerprintSamples.first().referenceId, fingerprintSamples.map { FingerprintTemplate( it.templateQualityScore, @@ -83,7 +81,7 @@ data class EnrolmentRecordCreationEvent( encoder: EncodingUtils, ) = if (faceSamples.isNotEmpty()) { FaceReference( - faceSamples.uniqueId() ?: "", + faceSamples.first().referenceId, faceSamples.map { FaceTemplate( encoder.byteArrayToBase64(it.template), diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordEvent.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordEvent.kt index bbe87336eb..63c62d3c64 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordEvent.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordEvent.kt @@ -13,6 +13,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo JsonSubTypes.Type(value = EnrolmentRecordCreationEvent::class, name = "EnrolmentRecordCreation"), JsonSubTypes.Type(value = EnrolmentRecordMoveEvent::class, name = "EnrolmentRecordMove"), JsonSubTypes.Type(value = EnrolmentRecordDeletionEvent::class, name = "EnrolmentRecordDeletion"), + JsonSubTypes.Type(value = EnrolmentRecordUpdateEvent::class, name = "EnrolmentRecordUpdate"), ) @Keep sealed class EnrolmentRecordEvent( diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordEventType.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordEventType.kt index a6e8815508..17448d96a2 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordEventType.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordEventType.kt @@ -7,4 +7,5 @@ enum class EnrolmentRecordEventType { EnrolmentRecordCreation, EnrolmentRecordDeletion, EnrolmentRecordMove, + EnrolmentRecordUpdate, } diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordUpdateEvent.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordUpdateEvent.kt new file mode 100644 index 0000000000..4ca63c97f8 --- /dev/null +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordUpdateEvent.kt @@ -0,0 +1,30 @@ +package com.simprints.infra.events.event.domain.models.subject + +import androidx.annotation.Keep +import java.util.UUID + +@Keep +data class EnrolmentRecordUpdateEvent( + override val id: String, + val payload: EnrolmentRecordUpdatePayload, +) : EnrolmentRecordEvent(id, EnrolmentRecordEventType.EnrolmentRecordUpdate) { + constructor( + subjectId: String, + biometricReferencesAdded: List, + biometricReferencesRemoved: List, + ) : this( + UUID.randomUUID().toString(), + EnrolmentRecordUpdatePayload( + subjectId, + biometricReferencesAdded, + biometricReferencesRemoved, + ), + ) + + @Keep + data class EnrolmentRecordUpdatePayload( + val subjectId: String, + val biometricReferencesAdded: List, + val biometricReferencesRemoved: List, + ) +} diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/validators/EnrolmentEventValidator.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/validators/EnrolmentEventValidator.kt index 7dea5d5a51..38c8cabfbf 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/domain/validators/EnrolmentEventValidator.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/validators/EnrolmentEventValidator.kt @@ -1,8 +1,8 @@ package com.simprints.infra.events.event.domain.validators -import com.simprints.infra.events.event.domain.models.EnrolmentEventV2 +import com.simprints.infra.events.event.domain.models.BiometricReferenceCreationEvent +import com.simprints.infra.events.event.domain.models.EnrolmentEventV4 import com.simprints.infra.events.event.domain.models.Event -import com.simprints.infra.events.event.domain.models.PersonCreationEvent import com.simprints.infra.events.event.domain.models.face.FaceCaptureEvent import com.simprints.infra.events.event.domain.models.fingerprint.FingerprintCaptureEvent import com.simprints.infra.events.exceptions.validator.EnrolmentEventValidatorException @@ -16,17 +16,17 @@ internal class EnrolmentEventValidator : EventValidator { currentEvents: List, eventToAdd: Event, ) { - if (eventToAdd is EnrolmentEventV2) { + if (eventToAdd is EnrolmentEventV4) { val hasFingerprint = currentEvents.any { it is FingerprintCaptureEvent } val hasFace = currentEvents.any { it is FaceCaptureEvent } - val hasPersonCreation = currentEvents.any { it is PersonCreationEvent } + val hasBiometricReference = currentEvents.any { it is BiometricReferenceCreationEvent } if (!hasFingerprint && !hasFace) { throw EnrolmentEventValidatorException("Missing fingerprint or face capture event") } - if (!hasPersonCreation) { - throw EnrolmentEventValidatorException("Missing person creation event") + if (!hasBiometricReference) { + throw EnrolmentEventValidatorException("Missing biometric reference creation event") } } } diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/validators/PersonCreationEventValidator.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/validators/PersonCreationEventValidator.kt deleted file mode 100644 index eacf45295b..0000000000 --- a/infra/events/src/main/java/com/simprints/infra/events/event/domain/validators/PersonCreationEventValidator.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.simprints.infra.events.event.domain.validators - -import com.simprints.infra.events.event.domain.models.Event -import com.simprints.infra.events.event.domain.models.PersonCreationEvent -import com.simprints.infra.events.exceptions.validator.PersonCreationEventException - -internal class PersonCreationEventValidator : EventValidator { - /** - * This validator checks to make sure that no more than the allowed number of PersonCreation events - * are added to the session. - * "Normal" sessions have only one PersonCreation event. - * Sessions that skip a modality (due to Matching Modalities configuration) have two PersonCreation events. - */ - override fun validate( - currentEvents: List, - eventToAdd: Event, - ) { - if (eventToAdd is PersonCreationEvent) { - val existingPersonCreationEventsCount = currentEvents - .filter { it is PersonCreationEvent } - .count { it.id != eventToAdd.id } - if (existingPersonCreationEventsCount > 1) { - throw PersonCreationEventException( - "The session already has the maximum PersonCreationEvents allowed ($existingPersonCreationEventsCount)", - ) - } - } - } -} diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/validators/SessionEventValidatorsFactory.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/validators/SessionEventValidatorsFactory.kt index 67bb34a1be..6d39a76385 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/domain/validators/SessionEventValidatorsFactory.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/validators/SessionEventValidatorsFactory.kt @@ -4,7 +4,6 @@ import javax.inject.Inject internal class SessionEventValidatorsFactory @Inject constructor() { fun build(): Array = arrayOf( - PersonCreationEventValidator(), EnrolmentEventValidator(), ) } diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/local/migrations/EventMigration1to2.kt b/infra/events/src/main/java/com/simprints/infra/events/event/local/migrations/EventMigration1to2.kt index 32be86a3ac..62c96b9bf6 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/local/migrations/EventMigration1to2.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/local/migrations/EventMigration1to2.kt @@ -4,7 +4,6 @@ import android.database.Cursor import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase import com.simprints.core.tools.extentions.getStringWithColumnName -import com.simprints.infra.events.event.domain.models.EventType.ENROLMENT_V1 import com.simprints.infra.logging.LoggingConstants.CrashReportTag.MIGRATION import com.simprints.infra.logging.Simber import org.json.JSONObject @@ -59,10 +58,10 @@ internal class EventMigration1to2 : Migration(1, 2) { ) { val jsonData = it.getStringWithColumnName(DB_EVENT_JSON_FIELD) jsonData?.let { - val originalJson = JSONObject(jsonData).put(DB_EVENT_JSON_EVENT_TYPE, ENROLMENT_V1) + val originalJson = JSONObject(jsonData).put(DB_EVENT_JSON_EVENT_TYPE, "ENROLMENT_V1") val newPayload = originalJson.getJSONObject(DB_EVENT_JSON_EVENT_PAYLOAD).put( DB_EVENT_JSON_EVENT_TYPE, - ENROLMENT_V1, + "ENROLMENT_V1", ) val newJson = originalJson.put(DB_EVENT_JSON_EVENT_PAYLOAD, newPayload) database.execSQL("UPDATE DbEvent SET eventJson = ? WHERE id = ?", arrayOf(newJson, id)) @@ -74,7 +73,7 @@ internal class EventMigration1to2 : Migration(1, 2) { database: SupportSQLiteDatabase, ) { id?.let { - database.execSQL("UPDATE DbEvent SET type = ? WHERE id = ?", arrayOf(ENROLMENT_V1, it)) + database.execSQL("UPDATE DbEvent SET type = ? WHERE id = ?", arrayOf("ENROLMENT_V1", it)) } } diff --git a/infra/events/src/test/java/com/simprints/infra/events/event/domain/models/BiometricReferenceCreationEventTest.kt b/infra/events/src/test/java/com/simprints/infra/events/event/domain/models/BiometricReferenceCreationEventTest.kt new file mode 100644 index 0000000000..cdc82fd457 --- /dev/null +++ b/infra/events/src/test/java/com/simprints/infra/events/event/domain/models/BiometricReferenceCreationEventTest.kt @@ -0,0 +1,34 @@ +package com.simprints.infra.events.event.domain.models + +import com.google.common.truth.Truth.assertThat +import com.simprints.infra.events.event.domain.models.BiometricReferenceCreationEvent.Companion.EVENT_VERSION +import com.simprints.infra.events.event.domain.models.EventType.BIOMETRIC_REFERENCE_CREATION +import com.simprints.infra.events.sampledata.SampleDefaults.CREATED_AT +import com.simprints.infra.events.sampledata.SampleDefaults.GUID1 +import com.simprints.infra.events.sampledata.SampleDefaults.GUID2 +import com.simprints.infra.events.sampledata.SampleDefaults.GUID3 +import org.junit.Test + +class BiometricReferenceCreationEventTest { + @Test + fun create_BiometricReferenceCreationEvent() { + val event = BiometricReferenceCreationEvent( + CREATED_AT, + GUID1, + BiometricReferenceCreationEvent.BiometricReferenceModality.FACE, + listOf(GUID2, GUID3), + ) + + assertThat(event.id).isNotNull() + assertThat(event.type).isEqualTo(BIOMETRIC_REFERENCE_CREATION) + with(event.payload) { + assertThat(createdAt).isEqualTo(CREATED_AT) + assertThat(endedAt).isNull() + assertThat(id).isEqualTo(GUID1) + assertThat(modality).isEqualTo(BiometricReferenceCreationEvent.BiometricReferenceModality.FACE) + assertThat(captureIds).containsExactly(GUID2, GUID3) + assertThat(eventVersion).isEqualTo(EVENT_VERSION) + assertThat(type).isEqualTo(BIOMETRIC_REFERENCE_CREATION) + } + } +} diff --git a/infra/events/src/test/java/com/simprints/infra/events/event/domain/models/EnrolmentEventV1Test.kt b/infra/events/src/test/java/com/simprints/infra/events/event/domain/models/EnrolmentEventV1Test.kt deleted file mode 100644 index dbfd90008a..0000000000 --- a/infra/events/src/test/java/com/simprints/infra/events/event/domain/models/EnrolmentEventV1Test.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.simprints.infra.events.event.domain.models - -import com.google.common.truth.Truth.assertThat -import com.simprints.infra.events.event.domain.models.EventType.ENROLMENT_V1 -import com.simprints.infra.events.sampledata.SampleDefaults.CREATED_AT -import com.simprints.infra.events.sampledata.SampleDefaults.GUID2 -import org.junit.Test - -class EnrolmentEventV1Test { - @Test - fun create_EnrolmentEvent() { - val event = EnrolmentEventV1(CREATED_AT, GUID2) - - assertThat(event.id).isNotNull() - assertThat(event.type).isEqualTo(ENROLMENT_V1) - with(event.payload) { - assertThat(createdAt).isEqualTo(CREATED_AT) - assertThat(eventVersion).isEqualTo(EnrolmentEventV1.EVENT_VERSION) - assertThat(type).isEqualTo(ENROLMENT_V1) - assertThat(personId).isEqualTo(GUID2) - } - } -} diff --git a/infra/events/src/test/java/com/simprints/infra/events/event/domain/models/EnrolmentEventV4Test.kt b/infra/events/src/test/java/com/simprints/infra/events/event/domain/models/EnrolmentEventV4Test.kt new file mode 100644 index 0000000000..15c7bf54be --- /dev/null +++ b/infra/events/src/test/java/com/simprints/infra/events/event/domain/models/EnrolmentEventV4Test.kt @@ -0,0 +1,37 @@ +package com.simprints.infra.events.event.domain.models + +import com.google.common.truth.Truth.assertThat +import com.simprints.infra.events.event.domain.models.EventType.ENROLMENT_V4 +import com.simprints.infra.events.sampledata.SampleDefaults.CREATED_AT +import com.simprints.infra.events.sampledata.SampleDefaults.DEFAULT_MODULE_ID +import com.simprints.infra.events.sampledata.SampleDefaults.DEFAULT_PROJECT_ID +import com.simprints.infra.events.sampledata.SampleDefaults.DEFAULT_USER_ID +import com.simprints.infra.events.sampledata.SampleDefaults.GUID1 +import com.simprints.infra.events.sampledata.SampleDefaults.GUID2 +import org.junit.Test + +class EnrolmentEventV4Test { + @Test + fun create_EnrolmentEvent() { + val event = EnrolmentEventV4( + CREATED_AT, + GUID1, + DEFAULT_PROJECT_ID, + DEFAULT_MODULE_ID, + DEFAULT_USER_ID, + listOf(GUID2), + ) + + assertThat(event.id).isNotNull() + assertThat(event.type).isEqualTo(ENROLMENT_V4) + with(event.payload) { + assertThat(createdAt).isEqualTo(CREATED_AT) + assertThat(eventVersion).isEqualTo(EnrolmentEventV4.EVENT_VERSION) + assertThat(type).isEqualTo(ENROLMENT_V4) + assertThat(projectId).isEqualTo(DEFAULT_PROJECT_ID) + assertThat(moduleId).isEqualTo(DEFAULT_MODULE_ID) + assertThat(attendantId).isEqualTo(DEFAULT_USER_ID) + assertThat(biometricReferenceIds).containsExactly(GUID2) + } + } +} diff --git a/infra/events/src/test/java/com/simprints/infra/events/event/domain/models/EventPayloadTest.kt b/infra/events/src/test/java/com/simprints/infra/events/event/domain/models/EventPayloadTest.kt index 7cf9386f9c..118b4e440f 100644 --- a/infra/events/src/test/java/com/simprints/infra/events/event/domain/models/EventPayloadTest.kt +++ b/infra/events/src/test/java/com/simprints/infra/events/event/domain/models/EventPayloadTest.kt @@ -192,7 +192,6 @@ class EventPayloadTest { ), ), ConsentEvent(CREATED_AT, ENDED_AT, INDIVIDUAL, ACCEPTED), - EnrolmentEventV1(CREATED_AT, GUID2), EnrolmentEventV2( createdAt = CREATED_AT, subjectId = GUID1, @@ -201,6 +200,14 @@ class EventPayloadTest { attendantId = DEFAULT_USER_ID, personCreationEventId = GUID2, ), + EnrolmentEventV4( + createdAt = CREATED_AT, + subjectId = GUID1, + projectId = DEFAULT_PROJECT_ID, + moduleId = DEFAULT_MODULE_ID, + attendantId = DEFAULT_USER_ID, + biometricReferenceIds = listOf(GUID1, GUID2), + ), GuidSelectionEvent(CREATED_AT, GUID1), IntentParsingEvent(CREATED_AT, COMMCARE), InvalidIntentEvent(CREATED_AT, "REGISTER", mapOf("extra_key" to "value")), @@ -210,6 +217,7 @@ class EventPayloadTest { pool = MatchPool(PROJECT, 100), matcher = "MATCHER_NAME", result = listOf(MatchEntry(GUID1, 0F)), + probeBiometricReferenceId = GUID1, ), OneToOneMatchEvent( createdAt = CREATED_AT, @@ -218,6 +226,7 @@ class EventPayloadTest { matcher = "MATCHER_NAME", result = MatchEntry(GUID1, 0F), fingerComparisonStrategy = FingerComparisonStrategy.SAME_FINGER, + probeBiometricReferenceId = GUID1, ), PersonCreationEvent( startTime = CREATED_AT, diff --git a/infra/events/src/test/java/com/simprints/infra/events/event/domain/models/OneToManyMatchEventTest.kt b/infra/events/src/test/java/com/simprints/infra/events/event/domain/models/OneToManyMatchEventTest.kt index 240dce778a..61ed121139 100644 --- a/infra/events/src/test/java/com/simprints/infra/events/event/domain/models/OneToManyMatchEventTest.kt +++ b/infra/events/src/test/java/com/simprints/infra/events/event/domain/models/OneToManyMatchEventTest.kt @@ -1,10 +1,14 @@ package com.simprints.infra.events.event.domain.models +import com.fasterxml.jackson.core.type.TypeReference import com.google.common.truth.Truth.assertThat +import com.simprints.core.tools.json.JsonHelper import com.simprints.infra.events.event.domain.models.EventType.ONE_TO_MANY_MATCH import com.simprints.infra.events.event.domain.models.OneToManyMatchEvent.Companion.EVENT_VERSION import com.simprints.infra.events.event.domain.models.OneToManyMatchEvent.OneToManyMatchPayload.MatchPool import com.simprints.infra.events.event.domain.models.OneToManyMatchEvent.OneToManyMatchPayload.MatchPoolType.PROJECT +import com.simprints.infra.events.event.domain.models.OneToManyMatchEvent.OneToManyMatchPayload.OneToManyMatchPayloadV2 +import com.simprints.infra.events.event.domain.models.OneToManyMatchEvent.OneToManyMatchPayload.OneToManyMatchPayloadV3 import com.simprints.infra.events.sampledata.SampleDefaults.CREATED_AT import com.simprints.infra.events.sampledata.SampleDefaults.ENDED_AT import com.simprints.infra.events.sampledata.SampleDefaults.GUID1 @@ -15,7 +19,7 @@ class OneToManyMatchEventTest { fun create_OneToManyMatchEvent() { val poolArg = MatchPool(PROJECT, 100) val resultArg = listOf(MatchEntry(GUID1, 0F)) - val event = OneToManyMatchEvent(CREATED_AT, ENDED_AT, poolArg, "MATCHER_NAME", resultArg) + val event = OneToManyMatchEvent(CREATED_AT, ENDED_AT, poolArg, "MATCHER_NAME", resultArg, "referenceId") assertThat(event.id).isNotNull() assertThat(event.type).isEqualTo(ONE_TO_MANY_MATCH) @@ -27,6 +31,69 @@ class OneToManyMatchEventTest { assertThat(matcher).isEqualTo("MATCHER_NAME") assertThat(pool).isEqualTo(poolArg) assertThat(result).isEqualTo(resultArg) + assertThat((this as OneToManyMatchPayloadV3).probeBiometricReferenceId).isEqualTo("referenceId") } } + + @Test + fun shouldParse_v2Event_successfully() { + val actualEvent = JsonHelper.fromJson(oldApiJsonEventString, object : TypeReference() {}) + + assertThat(actualEvent.id).isEqualTo("3afb1b9e-b263-4073-b773-6e1dac20d72f") + assertThat(actualEvent.payload.eventVersion).isEqualTo(2) + assertThat(actualEvent.payload).isInstanceOf(OneToManyMatchPayloadV2::class.java) + } + + @Test + fun shouldParse_v3Event_successfully() { + val actualEvent = JsonHelper.fromJson(newApiJsonEventString, object : TypeReference() {}) + + assertThat(actualEvent.id).isEqualTo("3afb1b9e-b263-4073-b773-6e1dac20d72f") + assertThat(actualEvent.payload.eventVersion).isEqualTo(3) + assertThat(actualEvent.payload).isInstanceOf(OneToManyMatchPayloadV3::class.java) + assertThat((actualEvent.payload as OneToManyMatchPayloadV3).probeBiometricReferenceId).isEqualTo("referenceId") + } + + private val oldApiJsonEventString = + """ + { + "id": "3afb1b9e-b263-4073-b773-6e1dac20d72f", + "scopeId": "6dcb3810-4789-4149-8fea-473ffb520958", + "payload": { + "createdAt": {"ms": 1234}, + "eventVersion": 2, + "pool": { + "type": "PROJECT", + "count": 1040 + }, + "matcher": "SIM_AFIS", + "results": [], + "type": "ONE_TO_MANY_MATCH", + "endedAt": {"ms": 4567} + }, + "type": "ONE_TO_MANY_MATCH" + } + """.trimIndent() + + private val newApiJsonEventString = + """ + { + "id": "3afb1b9e-b263-4073-b773-6e1dac20d72f", + "scopeId": "6dcb3810-4789-4149-8fea-473ffb520958", + "payload": { + "createdAt": {"ms": 1234}, + "eventVersion": 3, + "pool": { + "type": "PROJECT", + "count": 1040 + }, + "matcher": "SIM_AFIS", + "results": [], + "probeBiometricReferenceId": "referenceId", + "type": "ONE_TO_MANY_MATCH", + "endedAt": {"ms": 1234} + }, + "type": "ONE_TO_MANY_MATCH" + } + """.trimIndent() } diff --git a/infra/events/src/test/java/com/simprints/infra/events/event/domain/models/OneToOneMatchEventTest.kt b/infra/events/src/test/java/com/simprints/infra/events/event/domain/models/OneToOneMatchEventTest.kt index 50ebf64720..6309aebbd2 100644 --- a/infra/events/src/test/java/com/simprints/infra/events/event/domain/models/OneToOneMatchEventTest.kt +++ b/infra/events/src/test/java/com/simprints/infra/events/event/domain/models/OneToOneMatchEventTest.kt @@ -1,8 +1,12 @@ package com.simprints.infra.events.event.domain.models +import com.fasterxml.jackson.core.type.TypeReference import com.google.common.truth.Truth.assertThat +import com.simprints.core.tools.json.JsonHelper import com.simprints.infra.events.event.domain.models.EventType.ONE_TO_ONE_MATCH import com.simprints.infra.events.event.domain.models.OneToOneMatchEvent.Companion.EVENT_VERSION +import com.simprints.infra.events.event.domain.models.OneToOneMatchEvent.OneToOneMatchPayload.OneToOneMatchPayloadV3 +import com.simprints.infra.events.event.domain.models.OneToOneMatchEvent.OneToOneMatchPayload.OneToOneMatchPayloadV4 import com.simprints.infra.events.sampledata.SampleDefaults.CREATED_AT import com.simprints.infra.events.sampledata.SampleDefaults.ENDED_AT import com.simprints.infra.events.sampledata.SampleDefaults.GUID1 @@ -19,6 +23,7 @@ class OneToOneMatchEventTest { "MATCHER_NAME", resultArg, FingerComparisonStrategy.SAME_FINGER, + "referenceId", ) assertThat(event.id).isNotNull() @@ -32,6 +37,65 @@ class OneToOneMatchEventTest { .isEqualTo(FingerComparisonStrategy.SAME_FINGER) assertThat(type).isEqualTo(ONE_TO_ONE_MATCH) assertThat(result).isEqualTo(resultArg) + assertThat((this as OneToOneMatchPayloadV4).probeBiometricReferenceId).isEqualTo("referenceId") } } + + @Test + fun shouldParse_v3Event_successfully() { + val actualEvent = JsonHelper.fromJson(oldApiJsonEventString, object : TypeReference() {}) + + assertThat(actualEvent.id).isEqualTo("3afb1b9e-b263-4073-b773-6e1dac20d72f") + assertThat(actualEvent.payload.eventVersion).isEqualTo(3) + assertThat(actualEvent.payload).isInstanceOf(OneToOneMatchPayloadV3::class.java) + } + + @Test + fun shouldParse_v4Event_successfully() { + val actualEvent = JsonHelper.fromJson(newApiJsonEventString, object : TypeReference() {}) + + assertThat(actualEvent.id).isEqualTo("3afb1b9e-b263-4073-b773-6e1dac20d72f") + assertThat(actualEvent.payload.eventVersion).isEqualTo(4) + assertThat(actualEvent.payload).isInstanceOf(OneToOneMatchPayloadV4::class.java) + assertThat((actualEvent.payload as OneToOneMatchPayloadV4).probeBiometricReferenceId).isEqualTo("referenceId") + } + + private val oldApiJsonEventString = + """ + { + "id":"3afb1b9e-b263-4073-b773-6e1dac20d72f", + "scopeId":"6dcb3810-4789-4149-8fea-473ffb520958", + "payload":{ + "createdAt":{"ms":1234}, + "eventVersion":3, + "candidateId":"3afb1b9e-b263-4073-b773-6e1dac20d72f", + "matcher":"SIM_AFIS", + "result":{"candidateId":"3afb1b9e-b263-4073-b773-6e1dac20d72f","score":1.0}, + "fingerComparisonStrategy":"SAME_FINGER", + "type":"ONE_TO_ONE_MATCH", + "endedAt":{"ms":4567} + }, + "type":"ONE_TO_ONE_MATCH" + } + """.trimIndent() + + private val newApiJsonEventString = + """ + { + "id":"3afb1b9e-b263-4073-b773-6e1dac20d72f", + "scopeId":"6dcb3810-4789-4149-8fea-473ffb520958", + "payload":{ + "createdAt":{"ms":1234}, + "eventVersion":4, + "candidateId":"3afb1b9e-b263-4073-b773-6e1dac20d72f", + "matcher":"SIM_AFIS", + "result":{"candidateId":"3afb1b9e-b263-4073-b773-6e1dac20d72f","score":1.0}, + "fingerComparisonStrategy":"SAME_FINGER", + "type":"ONE_TO_ONE_MATCH", + "endedAt":{"ms":4567}, + "probeBiometricReferenceId": "referenceId" + }, + "type":"ONE_TO_ONE_MATCH" + } + """.trimIndent() } diff --git a/infra/events/src/test/java/com/simprints/infra/events/event/domain/validators/EnrolmentEventValidatorTest.kt b/infra/events/src/test/java/com/simprints/infra/events/event/domain/validators/EnrolmentEventValidatorTest.kt index 49310b3b91..6afc0075b7 100644 --- a/infra/events/src/test/java/com/simprints/infra/events/event/domain/validators/EnrolmentEventValidatorTest.kt +++ b/infra/events/src/test/java/com/simprints/infra/events/event/domain/validators/EnrolmentEventValidatorTest.kt @@ -2,7 +2,8 @@ package com.simprints.infra.events.event.domain.validators import com.simprints.infra.events.exceptions.validator.EnrolmentEventValidatorException import com.simprints.infra.events.sampledata.SampleDefaults.GUID1 -import com.simprints.infra.events.sampledata.createEnrolmentEventV2 +import com.simprints.infra.events.sampledata.createBiometricReferenceCreationEvent +import com.simprints.infra.events.sampledata.createEnrolmentEventV4 import com.simprints.infra.events.sampledata.createEventWithSessionId import com.simprints.infra.events.sampledata.createFaceCaptureEvent import com.simprints.infra.events.sampledata.createFingerprintCaptureEvent @@ -20,24 +21,24 @@ internal class EnrolmentEventValidatorTest { } @Test - fun validate_shouldValidateIfBiometricCaptureAndPersonCreationIsPresent() { + fun validate_shouldValidateIfBiometricCaptureAndBiometricCreationIsPresent() { val currentEvents = listOf(createFaceCaptureEvent(), createPersonCreationEvent()) - validator.run { validate(currentEvents, createEnrolmentEventV2()) } + validator.run { validate(currentEvents, createBiometricReferenceCreationEvent()) } } @Test fun validate_shouldThrowIfBiometricCaptureIsNotPresent() { assertThrows { - val currentEvents = listOf(createEventWithSessionId(GUID1, GUID1), createPersonCreationEvent()) - validator.validate(currentEvents, createEnrolmentEventV2()) + val currentEvents = listOf(createEventWithSessionId(GUID1, GUID1), createBiometricReferenceCreationEvent()) + validator.validate(currentEvents, createEnrolmentEventV4()) } } @Test - fun validate_shouldThrowIfPersonCreationIsNotPresent() { + fun validate_shouldThrowIfBiometricCreationEventIsNotPresent() { assertThrows { val currentEvents = listOf(createFingerprintCaptureEvent()) - validator.validate(currentEvents, createEnrolmentEventV2()) + validator.validate(currentEvents, createEnrolmentEventV4()) } } } diff --git a/infra/events/src/test/java/com/simprints/infra/events/event/local/migrations/EventMigration1to2Test.kt b/infra/events/src/test/java/com/simprints/infra/events/event/local/migrations/EventMigration1to2Test.kt index 1b1769ae49..e2e6297015 100644 --- a/infra/events/src/test/java/com/simprints/infra/events/event/local/migrations/EventMigration1to2Test.kt +++ b/infra/events/src/test/java/com/simprints/infra/events/event/local/migrations/EventMigration1to2Test.kt @@ -9,7 +9,6 @@ import androidx.test.platform.app.InstrumentationRegistry import com.google.common.truth.Truth.assertThat import com.simprints.core.tools.extentions.getStringWithColumnName import com.simprints.core.tools.utils.randomUUID -import com.simprints.infra.events.event.domain.models.EventType.ENROLMENT_V1 import com.simprints.infra.events.event.local.EventRoomDatabase import com.simprints.infra.events.event.local.migrations.MigrationTestingTools.retrieveCursorWithEventById import com.simprints.testtools.unit.robolectric.ShadowAndroidXMultiDex @@ -110,7 +109,7 @@ class EventMigration1to2Test { id: String, ) { val cursor = retrieveCursorWithEventById(db, id) - assertThat(cursor.getStringWithColumnName("type")).isEqualTo(ENROLMENT_V1.toString()) + assertThat(cursor.getStringWithColumnName("type")).isEqualTo("ENROLMENT_V1") val eventJson = JSONObject(cursor.getStringWithColumnName("eventJson")!!) assertThat(eventJson.getString("type")).isEqualTo("ENROLMENT_V1") diff --git a/infra/events/src/test/resources/all-events/biometric_reference_creation_v1.json b/infra/events/src/test/resources/all-events/biometric_reference_creation_v1.json new file mode 100644 index 0000000000..17f1a8ea9c --- /dev/null +++ b/infra/events/src/test/resources/all-events/biometric_reference_creation_v1.json @@ -0,0 +1,21 @@ +{ + "id": "c96e03c0-f063-4eb0-94ca-833e0a412965", + "type": "BIOMETRIC_REFERENCE_CREATION", + "labels": { + "projectId": "TEST6Oai41ps1pBNrzBL", + "sessionId": "e35c39f9-b81e-48f2-97e7-46ecc8399bb4", + "deviceId": "f2fd8393c0a0be67" + }, + "payload": { + "type": "BIOMETRIC_REFERENCE_CREATION", + "eventVersion": 1, + "createdAt": 9345678901, + "endedAt": 0, + "id": "78d13639-ac13-497f-8d81-4e7b08551c93", + "modality": "FACE", + "captureIds": [ + "4f599e48-bf94-49b9-a752-8992fd77014c", + "bd76b0e6-56d1-4ef2-a119-9a21d90c36dc" + ] + } +} diff --git a/infra/events/src/test/resources/all-events/enrolment_v1.json b/infra/events/src/test/resources/all-events/enrolment_v1.json deleted file mode 100644 index fe2f523662..0000000000 --- a/infra/events/src/test/resources/all-events/enrolment_v1.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "id": "4d5ec0e9-8aba-46c6-bc7c-c84258066fd8", - "type": "ENROLMENT_V1", - "labels": { - "projectId": "TEST6Oai41ps1pBNrzBL", - "sessionId": "e35c39f9-b81e-48f2-97e7-46ecc8399bb4", - "deviceId": "f2fd8393c0a0be67" - }, - "payload": { - "type": "ENROLMENT_V1", - "eventVersion": 1, - "createdAt": 178967890, - "endedAt": 0, - "personId": "ef2d93dc-5afd-453f-9ea4-add4286530f6" - } -} diff --git a/infra/events/src/test/resources/all-events/enrolment_v4.json b/infra/events/src/test/resources/all-events/enrolment_v4.json new file mode 100644 index 0000000000..d4b3824cf1 --- /dev/null +++ b/infra/events/src/test/resources/all-events/enrolment_v4.json @@ -0,0 +1,28 @@ +{ + "id": "d93f16d6-0581-4f8b-8ddf-f3ae8ec3579e", + "type": "ENROLMENT_V4", + "labels": { + "projectId": "TEST6Oai41ps1pBNrzBL", + "sessionId": "e35c39f9-b81e-48f2-97e7-46ecc8399bb4", + "deviceId": "f2fd8393c0a0be67" + }, + "payload": { + "type": "ENROLMENT_V4", + "eventVersion": 4, + "createdAt": 178967890, + "endedAt": 0, + "subjectId": "9aa64aff-46b5-4ab4-90b5-82c0e6d9725f", + "projectId": "TEST6Oai41ps1pBNrzBL", + "moduleId": { + "className": "TokenizableString.Raw", + "value": "module1" + }, + "attendantId": { + "className": "TokenizableString.Raw", + "value": "user1" + }, + "biometricReferenceIds": [ + "94828488-47ce-4c76-8ace-33d69b2a6d75" + ] + } +} diff --git a/infra/sync/src/main/java/com/simprints/infra/sync/SyncOrchestrator.kt b/infra/sync/src/main/java/com/simprints/infra/sync/SyncOrchestrator.kt index d88ea8e8a5..57d67bff67 100644 --- a/infra/sync/src/main/java/com/simprints/infra/sync/SyncOrchestrator.kt +++ b/infra/sync/src/main/java/com/simprints/infra/sync/SyncOrchestrator.kt @@ -3,7 +3,7 @@ package com.simprints.infra.sync import kotlinx.coroutines.flow.Flow interface SyncOrchestrator { - suspend fun scheduleBackgroundWork() + suspend fun scheduleBackgroundWork(withDelay: Boolean = false) suspend fun cancelBackgroundWork() @@ -13,7 +13,7 @@ interface SyncOrchestrator { */ fun refreshConfiguration(): Flow - fun rescheduleEventSync() + fun rescheduleEventSync(withDelay: Boolean = false) fun cancelEventSync() diff --git a/infra/sync/src/main/java/com/simprints/infra/sync/SyncOrchestratorImpl.kt b/infra/sync/src/main/java/com/simprints/infra/sync/SyncOrchestratorImpl.kt index c9b73acabe..ba22289a12 100644 --- a/infra/sync/src/main/java/com/simprints/infra/sync/SyncOrchestratorImpl.kt +++ b/infra/sync/src/main/java/com/simprints/infra/sync/SyncOrchestratorImpl.kt @@ -57,7 +57,7 @@ internal class SyncOrchestratorImpl @Inject constructor( } } - override suspend fun scheduleBackgroundWork() { + override suspend fun scheduleBackgroundWork(withDelay: Boolean) { if (authStore.signedInProjectId.isNotEmpty()) { workManager.schedulePeriodicWorker( SyncConstants.PROJECT_SYNC_WORK_NAME, @@ -72,7 +72,7 @@ internal class SyncOrchestratorImpl @Inject constructor( SyncConstants.FILE_UP_SYNC_REPEAT_INTERVAL, constraints = getImageUploadConstraints(), ) - rescheduleEventSync() + rescheduleEventSync(withDelay) if (shouldScheduleFirmwareUpdate()) { workManager.schedulePeriodicWorker( SyncConstants.FIRMWARE_UPDATE_WORK_NAME, @@ -110,10 +110,11 @@ internal class SyncOrchestratorImpl @Inject constructor( }.map { } // Converts flow emissions to Unit value as we only care about when it happens, not the value } - override fun rescheduleEventSync() { + override fun rescheduleEventSync(withDelay: Boolean) { workManager.schedulePeriodicWorker( SyncConstants.EVENT_SYNC_WORK_NAME, SyncConstants.EVENT_SYNC_WORKER_INTERVAL, + initialDelay = if (withDelay) SyncConstants.EVENT_SYNC_WORKER_INTERVAL else 0, tags = eventSyncManager.getPeriodicWorkTags(), ) } diff --git a/infra/sync/src/test/java/com/simprints/infra/sync/SyncOrchestratorImplTest.kt b/infra/sync/src/test/java/com/simprints/infra/sync/SyncOrchestratorImplTest.kt index 7875468182..fcbeb8ced1 100644 --- a/infra/sync/src/test/java/com/simprints/infra/sync/SyncOrchestratorImplTest.kt +++ b/infra/sync/src/test/java/com/simprints/infra/sync/SyncOrchestratorImplTest.kt @@ -218,6 +218,21 @@ class SyncOrchestratorImplTest { } } + @Test + fun `reschedules event sync worker with correct delay`() = runTest { + every { eventSyncManager.getPeriodicWorkTags() } returns listOf("tag1", "tag2") + + syncOrchestrator.rescheduleEventSync(true) + + verify { + workManager.enqueueUniquePeriodicWork( + EVENT_SYNC_WORK_NAME, + any(), + match { it.workSpec.initialDelay > 0 }, + ) + } + } + @Test fun `cancel event sync worker cancels correct worker`() = runTest { every { eventSyncManager.getAllWorkerTag() } returns "syncWorkers"