From 610b19fd68acc559b76144e25599f129cc71ddfe Mon Sep 17 00:00:00 2001 From: Sergejs Luhmirins Date: Tue, 2 Jun 2026 12:48:57 +0300 Subject: [PATCH 1/2] MS-1452 Add the ID validation step to Enrol+ flow --- .../usecases/steps/BuildStepsUseCase.kt | 29 +++++++---- .../cache/OrchestratorCacheIntegrationTest.kt | 3 +- .../usecases/steps/BuildStepsUseCaseTest.kt | 27 ++++++++++ .../ValidateSubjectPoolContract.kt | 6 ++- .../ValidateSubjectPoolFragmentParams.kt | 8 ++- .../screen/ValidateSubjectPoolFragment.kt | 6 +-- .../screen/ValidateSubjectPoolState.kt | 2 +- .../screen/ValidateSubjectPoolViewModel.kt | 35 ++++++++++--- .../ValidateSubjectPoolViewModelTest.kt | 49 ++++++++++++++----- .../src/main/res/values-bn/strings.xml | 1 - .../resources/src/main/res/values/strings.xml | 2 +- 11 files changed, 129 insertions(+), 39 deletions(-) diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/steps/BuildStepsUseCase.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/steps/BuildStepsUseCase.kt index 8c0b66d7b3..9a5d060f7a 100644 --- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/steps/BuildStepsUseCase.kt +++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/steps/BuildStepsUseCase.kt @@ -21,6 +21,7 @@ import com.simprints.feature.selectagegroup.SelectSubjectAgeGroupContract import com.simprints.feature.selectsubject.SelectSubjectContract import com.simprints.feature.setup.SetupContract import com.simprints.feature.validatepool.ValidateSubjectPoolContract +import com.simprints.feature.validatepool.ValidateSubjectPoolFragmentParams import com.simprints.fingerprint.capture.FingerprintCaptureContract import com.simprints.infra.config.store.models.ProjectConfiguration import com.simprints.infra.config.store.models.allowedAgeRanges @@ -65,6 +66,7 @@ internal class BuildStepsUseCase @Inject constructor( biometricDataSource = action.biometricDataSource, callerPackageName = action.actionIdentifier.callerPackageName, projectConfiguration = projectConfiguration, + mode = ValidateSubjectPoolFragmentParams.ValidationMode.IDENTIFICATION, ), buildAgeSelectionStepIfNeeded(action, projectConfiguration), buildConsentStepIfNeeded(ConsentType.IDENTIFY, projectConfiguration), @@ -191,16 +193,22 @@ internal class BuildStepsUseCase @Inject constructor( flowType = enrolFlowType, ) } + val matcherSteps = if (projectConfiguration.general.duplicateBiometricEnrolmentCheck) { - buildMatcherSteps( - projectConfiguration, - enrolFlowType, - resolvedAgeGroup, - buildMatcherSubjectQuery(projectConfiguration, action), - BiometricDataSource.fromString( - action.biometricDataSource, - action.actionIdentifier.callerPackageName, - ), + val enrolmentRecordQuery = buildMatcherSubjectQuery(projectConfiguration, action) + + buildValidateIdPoolStep( + enrolmentRecordQuery = enrolmentRecordQuery, + biometricDataSource = action.biometricDataSource, + callerPackageName = action.actionIdentifier.callerPackageName, + projectConfiguration = projectConfiguration, + mode = ValidateSubjectPoolFragmentParams.ValidationMode.ENROL_PLUS, + ) + buildMatcherSteps( + projectConfiguration = projectConfiguration, + flowType = enrolFlowType, + ageGroup = resolvedAgeGroup, + enrolmentRecordQuery = enrolmentRecordQuery, + biometricDataSource = BiometricDataSource.fromString(action.biometricDataSource, action.actionIdentifier.callerPackageName), ) } else { emptyList() @@ -347,6 +355,7 @@ internal class BuildStepsUseCase @Inject constructor( biometricDataSource: String, callerPackageName: String, projectConfiguration: ProjectConfiguration, + mode: ValidateSubjectPoolFragmentParams.ValidationMode, ) = if (projectConfiguration.experimental().idPoolValidationEnabled) { when ( BiometricDataSource.fromString( @@ -359,7 +368,7 @@ internal class BuildStepsUseCase @Inject constructor( id = StepId.VALIDATE_ID_POOL, navigationActionId = R.id.action_orchestratorFragment_to_validateSubjectPool, destinationId = ValidateSubjectPoolContract.DESTINATION, - params = ValidateSubjectPoolContract.getParams(enrolmentRecordQuery), + params = ValidateSubjectPoolContract.getParams(enrolmentRecordQuery, mode), ), ) 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 2ff82fa900..be4831c1b5 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 @@ -44,6 +44,7 @@ import com.simprints.feature.selectsubject.SelectSubjectParams import com.simprints.feature.selectsubject.SelectSubjectResult import com.simprints.feature.setup.SetupResult import com.simprints.feature.validatepool.ValidateSubjectPoolFragmentParams +import com.simprints.feature.validatepool.ValidateSubjectPoolFragmentParams.ValidationMode import com.simprints.feature.validatepool.ValidateSubjectPoolResult import com.simprints.fingerprint.capture.FingerprintCaptureParams import com.simprints.infra.config.store.models.ModalitySdkType @@ -320,7 +321,7 @@ class OrchestratorCacheIntegrationTest { id = StepId.VALIDATE_ID_POOL, navigationActionId = 5, destinationId = 6, - params = ValidateSubjectPoolFragmentParams(EnrolmentRecordQuery()), + params = ValidateSubjectPoolFragmentParams(EnrolmentRecordQuery(), ValidationMode.IDENTIFICATION), status = StepStatus.COMPLETED, result = ValidateSubjectPoolResult(true), ), diff --git a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/steps/BuildStepsUseCaseTest.kt b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/steps/BuildStepsUseCaseTest.kt index cc790c6b77..06a62c556e 100644 --- a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/steps/BuildStepsUseCaseTest.kt +++ b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/steps/BuildStepsUseCaseTest.kt @@ -169,6 +169,33 @@ class BuildStepsUseCaseTest { ) } + @Test + fun `build - enrol action - no age restriction - duplicate check - pool validation - returns expected steps`() = runTest { + val projectConfiguration = mockCommonProjectConfiguration() + every { projectConfiguration.general.duplicateBiometricEnrolmentCheck } returns true + + every { projectConfiguration.custom } returns + mapOf("validateIdentificationPool" to JsonPrimitive(true)) + + val action = mockk(relaxed = true) + every { action.getSubjectAgeIfAvailable() } returns null + + val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId, cachedScannedCredentialResult) + + assertStepOrder( + steps, + StepId.SETUP, + StepId.CONSENT, + StepId.FINGERPRINT_CAPTURE, + StepId.FINGERPRINT_CAPTURE, + StepId.FACE_CAPTURE, + StepId.VALIDATE_ID_POOL, + StepId.FINGERPRINT_MATCHER, + StepId.FINGERPRINT_MATCHER, + StepId.FACE_MATCHER, + ) + } + @Test fun `build - enrol action - no age restriction - duplicate check - matching modalities - returns expected steps`() = runTest { val projectConfiguration = mockCommonProjectConfiguration() diff --git a/feature/validate-subject-pool/src/main/java/com/simprints/feature/validatepool/ValidateSubjectPoolContract.kt b/feature/validate-subject-pool/src/main/java/com/simprints/feature/validatepool/ValidateSubjectPoolContract.kt index 8de51372ab..e2748a367a 100644 --- a/feature/validate-subject-pool/src/main/java/com/simprints/feature/validatepool/ValidateSubjectPoolContract.kt +++ b/feature/validate-subject-pool/src/main/java/com/simprints/feature/validatepool/ValidateSubjectPoolContract.kt @@ -1,9 +1,13 @@ package com.simprints.feature.validatepool +import com.simprints.feature.validatepool.ValidateSubjectPoolFragmentParams.ValidationMode import com.simprints.infra.enrolment.records.repository.domain.models.EnrolmentRecordQuery object ValidateSubjectPoolContract { val DESTINATION = R.id.validateSubjectPoolFragment - fun getParams(enrolmentRecordQuery: EnrolmentRecordQuery) = ValidateSubjectPoolFragmentParams(enrolmentRecordQuery) + fun getParams( + enrolmentRecordQuery: EnrolmentRecordQuery, + mode: ValidationMode = ValidationMode.IDENTIFICATION, + ) = ValidateSubjectPoolFragmentParams(enrolmentRecordQuery, mode) } diff --git a/feature/validate-subject-pool/src/main/java/com/simprints/feature/validatepool/ValidateSubjectPoolFragmentParams.kt b/feature/validate-subject-pool/src/main/java/com/simprints/feature/validatepool/ValidateSubjectPoolFragmentParams.kt index 1a85aeefc6..88301463f3 100644 --- a/feature/validate-subject-pool/src/main/java/com/simprints/feature/validatepool/ValidateSubjectPoolFragmentParams.kt +++ b/feature/validate-subject-pool/src/main/java/com/simprints/feature/validatepool/ValidateSubjectPoolFragmentParams.kt @@ -11,4 +11,10 @@ import kotlinx.serialization.Serializable @SerialName("ValidateSubjectPoolFragmentParams") data class ValidateSubjectPoolFragmentParams( val enrolmentRecordQuery: EnrolmentRecordQuery, -) : StepParams + val mode: ValidationMode, +) : StepParams { + enum class ValidationMode { + IDENTIFICATION, + ENROL_PLUS, + } +} diff --git a/feature/validate-subject-pool/src/main/java/com/simprints/feature/validatepool/screen/ValidateSubjectPoolFragment.kt b/feature/validate-subject-pool/src/main/java/com/simprints/feature/validatepool/screen/ValidateSubjectPoolFragment.kt index b04c3f7e80..2a587125e2 100644 --- a/feature/validate-subject-pool/src/main/java/com/simprints/feature/validatepool/screen/ValidateSubjectPoolFragment.kt +++ b/feature/validate-subject-pool/src/main/java/com/simprints/feature/validatepool/screen/ValidateSubjectPoolFragment.kt @@ -46,9 +46,9 @@ internal class ValidateSubjectPoolFragment : Fragment(R.layout.fragment_validate binding.validationActionsClose.setOnClickListener { finishWithResult(false) } binding.validationActionsContinue.setOnClickListener { finishWithResult(true) } - binding.validationActionsSync.setOnClickListener { viewModel.startSync(params.enrolmentRecordQuery) } + binding.validationActionsSync.setOnClickListener { viewModel.startSync(params.enrolmentRecordQuery, params.mode) } - viewModel.checkIdentificationPool(params.enrolmentRecordQuery) + viewModel.checkIdentificationPool(params.enrolmentRecordQuery, params.mode) } private fun renderState(state: ValidateSubjectPoolState) = when (state) { @@ -58,7 +58,7 @@ internal class ValidateSubjectPoolFragment : Fragment(R.layout.fragment_validate showValidating = true, ) - ValidateSubjectPoolState.UserMismatch -> setViews( + ValidateSubjectPoolState.AttendantMismatch -> setViews( descriptionRes = IDR.string.id_pool_validation_user_mismatch_message, ) diff --git a/feature/validate-subject-pool/src/main/java/com/simprints/feature/validatepool/screen/ValidateSubjectPoolState.kt b/feature/validate-subject-pool/src/main/java/com/simprints/feature/validatepool/screen/ValidateSubjectPoolState.kt index 6bd8658717..baac7b87da 100644 --- a/feature/validate-subject-pool/src/main/java/com/simprints/feature/validatepool/screen/ValidateSubjectPoolState.kt +++ b/feature/validate-subject-pool/src/main/java/com/simprints/feature/validatepool/screen/ValidateSubjectPoolState.kt @@ -5,7 +5,7 @@ internal sealed class ValidateSubjectPoolState { data object Success : ValidateSubjectPoolState() - data object UserMismatch : ValidateSubjectPoolState() + data object AttendantMismatch : ValidateSubjectPoolState() data object ModuleMismatch : ValidateSubjectPoolState() diff --git a/feature/validate-subject-pool/src/main/java/com/simprints/feature/validatepool/screen/ValidateSubjectPoolViewModel.kt b/feature/validate-subject-pool/src/main/java/com/simprints/feature/validatepool/screen/ValidateSubjectPoolViewModel.kt index 827dcceb5f..9669fc3ea2 100644 --- a/feature/validate-subject-pool/src/main/java/com/simprints/feature/validatepool/screen/ValidateSubjectPoolViewModel.kt +++ b/feature/validate-subject-pool/src/main/java/com/simprints/feature/validatepool/screen/ValidateSubjectPoolViewModel.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.viewModelScope import com.simprints.core.livedata.LiveDataEventWithContent import com.simprints.core.livedata.send import com.simprints.core.tools.time.TimeHelper +import com.simprints.feature.validatepool.ValidateSubjectPoolFragmentParams.ValidationMode import com.simprints.feature.validatepool.usecase.HasRecordsUseCase import com.simprints.feature.validatepool.usecase.IsModuleIdNotSyncedUseCase import com.simprints.feature.validatepool.usecase.ShouldSuggestSyncUseCase @@ -29,7 +30,7 @@ internal class ValidateSubjectPoolViewModel @Inject constructor( private val timeHelper: TimeHelper, ) : ViewModel() { private lateinit var cachedQuery: EnrolmentRecordQuery - + private lateinit var cachedMode: ValidationMode val state: LiveData> get() = _state private var _state = MutableLiveData>() @@ -49,7 +50,7 @@ internal class ValidateSubjectPoolViewModel @Inject constructor( syncStatusFlow.collect { syncState -> if (syncState.isSyncReporterCompleted()) { isSyncing = false - checkIdentificationPool(cachedQuery) + checkIdentificationPool(cachedQuery, cachedMode) } } } @@ -62,7 +63,10 @@ internal class ValidateSubjectPoolViewModel @Inject constructor( } } - fun checkIdentificationPool(enrolmentRecordQuery: EnrolmentRecordQuery) = viewModelScope.launch { + fun checkIdentificationPool( + enrolmentRecordQuery: EnrolmentRecordQuery, + mode: ValidationMode, + ) = viewModelScope.launch { if (isSyncing) { // In case of configuration change while syncing, we want to show the sync in progress state instead of default state _state.send(ValidateSubjectPoolState.SyncInProgress) @@ -71,18 +75,35 @@ internal class ValidateSubjectPoolViewModel @Inject constructor( _state.send(ValidateSubjectPoolState.Validating) val validationState = when { + // Check that any record are available as a shortcut hasRecords(enrolmentRecordQuery) -> ValidateSubjectPoolState.Success - enrolmentRecordQuery.attendantId != null && hasRecords(EnrolmentRecordQuery()) -> ValidateSubjectPoolState.UserMismatch - enrolmentRecordQuery.moduleId?.let { isModuleIdNotSynced(it) } == true -> ValidateSubjectPoolState.ModuleMismatch + // Check attendant ID or module ID based on the ID configuration in the project + hasInvalidAttendantId(enrolmentRecordQuery) -> ValidateSubjectPoolState.AttendantMismatch + hasInvalidModuleId(enrolmentRecordQuery) -> ValidateSubjectPoolState.ModuleMismatch + // Check for stale sync for a better result shouldSuggestSync() -> ValidateSubjectPoolState.RequiresSync - else -> ValidateSubjectPoolState.PoolEmpty + // For Identification requests, no-records is a fail state + mode == ValidationMode.IDENTIFICATION -> ValidateSubjectPoolState.PoolEmpty + // For Enrol+, no-records is a success state + else -> ValidateSubjectPoolState.Success } _state.send(validationState) } - fun startSync(enrolmentRecordQuery: EnrolmentRecordQuery) = viewModelScope.launch { + private suspend fun hasInvalidModuleId(enrolmentRecordQuery: EnrolmentRecordQuery): Boolean = + enrolmentRecordQuery.moduleId?.let { isModuleIdNotSynced(it) } == true + + private suspend fun hasInvalidAttendantId(enrolmentRecordQuery: EnrolmentRecordQuery): Boolean = + enrolmentRecordQuery.attendantId != null && hasRecords(EnrolmentRecordQuery()) + + fun startSync( + enrolmentRecordQuery: EnrolmentRecordQuery, + mode: ValidationMode, + ) = viewModelScope.launch { cachedQuery = enrolmentRecordQuery + cachedMode = mode + _state.send(ValidateSubjectPoolState.SyncInProgress) isSyncing = true syncOrchestrator.execute(OneTime.Events.start()) diff --git a/feature/validate-subject-pool/src/test/java/com/simprints/feature/validatepool/screen/ValidateSubjectPoolViewModelTest.kt b/feature/validate-subject-pool/src/test/java/com/simprints/feature/validatepool/screen/ValidateSubjectPoolViewModelTest.kt index 94c139e357..0bab40cca4 100644 --- a/feature/validate-subject-pool/src/test/java/com/simprints/feature/validatepool/screen/ValidateSubjectPoolViewModelTest.kt +++ b/feature/validate-subject-pool/src/test/java/com/simprints/feature/validatepool/screen/ValidateSubjectPoolViewModelTest.kt @@ -5,6 +5,7 @@ import com.google.common.truth.Truth.* import com.jraska.livedata.test import com.simprints.core.domain.tokenization.asTokenizableEncrypted import com.simprints.core.tools.time.TimeHelper +import com.simprints.feature.validatepool.ValidateSubjectPoolFragmentParams.ValidationMode import com.simprints.feature.validatepool.usecase.HasRecordsUseCase import com.simprints.feature.validatepool.usecase.IsModuleIdNotSyncedUseCase import com.simprints.feature.validatepool.usecase.ShouldSuggestSyncUseCase @@ -75,7 +76,7 @@ class ValidateSubjectPoolViewModelTest { fun `when subject pool not empty returns Success `() = runTest { coEvery { hasRecordsUseCase(any()) } returns true - viewModel.checkIdentificationPool(EnrolmentRecordQuery()) + viewModel.checkIdentificationPool(EnrolmentRecordQuery(), ValidationMode.IDENTIFICATION) assertThat(viewModel.state.value?.peekContent()).isEqualTo(ValidateSubjectPoolState.Success) } @@ -86,7 +87,7 @@ class ValidateSubjectPoolViewModelTest { coEvery { hasRecordsUseCase(any()) } returns false coEvery { shouldSuggestSyncUseCase() } returns true - viewModel.checkIdentificationPool(enrolmentRecordQuery) + viewModel.checkIdentificationPool(enrolmentRecordQuery, ValidationMode.IDENTIFICATION) assertThat(viewModel.state.value?.peekContent()).isEqualTo(ValidateSubjectPoolState.RequiresSync) coVerify(exactly = 1) { hasRecordsUseCase(any()) } @@ -99,7 +100,7 @@ class ValidateSubjectPoolViewModelTest { coEvery { hasRecordsUseCase(any()) } returns false coEvery { shouldSuggestSyncUseCase() } returns false - viewModel.checkIdentificationPool(enrolmentRecordQuery) + viewModel.checkIdentificationPool(enrolmentRecordQuery, ValidationMode.IDENTIFICATION) assertThat(viewModel.state.value?.peekContent()).isEqualTo(ValidateSubjectPoolState.PoolEmpty) } @@ -110,9 +111,9 @@ class ValidateSubjectPoolViewModelTest { coEvery { hasRecordsUseCase(any()) } returns true coEvery { hasRecordsUseCase(enrolmentRecordQuery) } returns false - viewModel.checkIdentificationPool(enrolmentRecordQuery) + viewModel.checkIdentificationPool(enrolmentRecordQuery, ValidationMode.IDENTIFICATION) - assertThat(viewModel.state.value?.peekContent()).isEqualTo(ValidateSubjectPoolState.UserMismatch) + assertThat(viewModel.state.value?.peekContent()).isEqualTo(ValidateSubjectPoolState.AttendantMismatch) coVerify(exactly = 0) { isModuleIdNotSyncedUseCase(any()) } coVerify(exactly = 0) { shouldSuggestSyncUseCase() } } @@ -123,7 +124,7 @@ class ValidateSubjectPoolViewModelTest { coEvery { hasRecordsUseCase(any()) } returns false coEvery { shouldSuggestSyncUseCase() } returns true - viewModel.checkIdentificationPool(enrolmentRecordQuery) + viewModel.checkIdentificationPool(enrolmentRecordQuery, ValidationMode.IDENTIFICATION) assertThat(viewModel.state.value?.peekContent()).isEqualTo(ValidateSubjectPoolState.RequiresSync) coVerify(exactly = 0) { isModuleIdNotSyncedUseCase(any()) } @@ -135,7 +136,7 @@ class ValidateSubjectPoolViewModelTest { coEvery { hasRecordsUseCase(any()) } returns false coEvery { shouldSuggestSyncUseCase() } returns false - viewModel.checkIdentificationPool(enrolmentRecordQuery) + viewModel.checkIdentificationPool(enrolmentRecordQuery, ValidationMode.IDENTIFICATION) assertThat(viewModel.state.value?.peekContent()).isEqualTo(ValidateSubjectPoolState.PoolEmpty) coVerify(exactly = 0) { isModuleIdNotSyncedUseCase(any()) } @@ -147,7 +148,7 @@ class ValidateSubjectPoolViewModelTest { coEvery { hasRecordsUseCase(any()) } returns false coEvery { isModuleIdNotSyncedUseCase(any()) } returns true - viewModel.checkIdentificationPool(enrolmentRecordQuery) + viewModel.checkIdentificationPool(enrolmentRecordQuery, ValidationMode.IDENTIFICATION) assertThat(viewModel.state.value?.peekContent()).isEqualTo(ValidateSubjectPoolState.ModuleMismatch) coVerify(exactly = 1) { hasRecordsUseCase(any()) } @@ -161,7 +162,7 @@ class ValidateSubjectPoolViewModelTest { coEvery { isModuleIdNotSyncedUseCase(any()) } returns false coEvery { shouldSuggestSyncUseCase() } returns true - viewModel.checkIdentificationPool(enrolmentRecordQuery) + viewModel.checkIdentificationPool(enrolmentRecordQuery, ValidationMode.IDENTIFICATION) assertThat(viewModel.state.value?.peekContent()).isEqualTo(ValidateSubjectPoolState.RequiresSync) } @@ -173,7 +174,7 @@ class ValidateSubjectPoolViewModelTest { coEvery { isModuleIdNotSyncedUseCase(any()) } returns false coEvery { shouldSuggestSyncUseCase() } returns false - viewModel.checkIdentificationPool(enrolmentRecordQuery) + viewModel.checkIdentificationPool(enrolmentRecordQuery, ValidationMode.IDENTIFICATION) assertThat(viewModel.state.value?.peekContent()).isEqualTo(ValidateSubjectPoolState.PoolEmpty) } @@ -189,7 +190,7 @@ class ValidateSubjectPoolViewModelTest { val result = viewModel.state.test() - viewModel.startSync(enrolmentRecordQuery) + viewModel.startSync(enrolmentRecordQuery, ValidationMode.IDENTIFICATION) syncStatusFlow.value = createSyncStatus(isCompleted = true) job.complete() @@ -210,9 +211,9 @@ class ValidateSubjectPoolViewModelTest { val job = Job() coEvery { hasRecordsUseCase(enrolmentRecordQuery) } returns true every { syncOrchestrator.execute(any()) } returns job - viewModel.startSync(enrolmentRecordQuery) + viewModel.startSync(enrolmentRecordQuery, ValidationMode.IDENTIFICATION) - viewModel.checkIdentificationPool(enrolmentRecordQuery) + viewModel.checkIdentificationPool(enrolmentRecordQuery, ValidationMode.IDENTIFICATION) job.complete() advanceUntilIdle() @@ -220,6 +221,28 @@ class ValidateSubjectPoolViewModelTest { coVerify(exactly = 0) { hasRecordsUseCase(any()) } } + @Test + fun `if ENROL_PLUS when no subjects and synced recently returns Success`() = runTest { + val enrolmentRecordQuery = EnrolmentRecordQuery(projectId = "projectId") + coEvery { hasRecordsUseCase(any()) } returns false + coEvery { shouldSuggestSyncUseCase() } returns false + + viewModel.checkIdentificationPool(enrolmentRecordQuery, ValidationMode.ENROL_PLUS) + + assertThat(viewModel.state.value?.peekContent()).isEqualTo(ValidateSubjectPoolState.Success) + } + + @Test + fun `if ENROL_PLUS when no subjects and not synced recently returns RequiresSync`() = runTest { + val enrolmentRecordQuery = EnrolmentRecordQuery(projectId = "projectId") + coEvery { hasRecordsUseCase(any()) } returns false + coEvery { shouldSuggestSyncUseCase() } returns true + + viewModel.checkIdentificationPool(enrolmentRecordQuery, ValidationMode.ENROL_PLUS) + + assertThat(viewModel.state.value?.peekContent()).isEqualTo(ValidateSubjectPoolState.RequiresSync) + } + private fun createSyncStatus(isCompleted: Boolean): SyncStatus { val reporterStates = if (isCompleted) { listOf( diff --git a/infra/resources/src/main/res/values-bn/strings.xml b/infra/resources/src/main/res/values-bn/strings.xml index 75f6db5b23..d3f87ca9f6 100644 --- a/infra/resources/src/main/res/values-bn/strings.xml +++ b/infra/resources/src/main/res/values-bn/strings.xml @@ -476,7 +476,6 @@ ডিভাইসে কোনো রেকর্ড নেই বন্ধ করুন সিঙ্ক করুন এবং আবার চেষ্টা করুন - সংগ্রহের পর এগিয়ে যান সাবজেক্টের বয়সের কোঠা নির্বাচন করুন diff --git a/infra/resources/src/main/res/values/strings.xml b/infra/resources/src/main/res/values/strings.xml index abe47be2e3..357a539ffa 100644 --- a/infra/resources/src/main/res/values/strings.xml +++ b/infra/resources/src/main/res/values/strings.xml @@ -476,7 +476,7 @@ Sync in progress… Sync and retry Close - Continue capture + Continue Select Subject\'s Age Group From 8d73cb14fc6ffc70be85d363ca70666c300fae24 Mon Sep 17 00:00:00 2001 From: Sergejs Luhmirins Date: Tue, 2 Jun 2026 14:12:00 +0300 Subject: [PATCH 2/2] MS-1452 Reverse the pool validation feature flag --- .../usecases/steps/BuildStepsUseCase.kt | 6 +++--- .../usecases/steps/BuildStepsUseCaseTest.kt | 19 +++++++++++++------ .../ExperimentalProjectConfiguration.kt | 6 +++--- .../ExperimentalProjectConfigurationTest.kt | 12 ++++++------ 4 files changed, 25 insertions(+), 18 deletions(-) diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/steps/BuildStepsUseCase.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/steps/BuildStepsUseCase.kt index 9a5d060f7a..e5b55d6eb6 100644 --- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/steps/BuildStepsUseCase.kt +++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/steps/BuildStepsUseCase.kt @@ -356,7 +356,9 @@ internal class BuildStepsUseCase @Inject constructor( callerPackageName: String, projectConfiguration: ProjectConfiguration, mode: ValidateSubjectPoolFragmentParams.ValidationMode, - ) = if (projectConfiguration.experimental().idPoolValidationEnabled) { + ) = if (projectConfiguration.experimental().disableSubjectPoolValidation) { + emptyList() + } else { when ( BiometricDataSource.fromString( value = biometricDataSource, @@ -374,8 +376,6 @@ internal class BuildStepsUseCase @Inject constructor( is BiometricDataSource.CommCare -> emptyList() } - } else { - emptyList() } /** diff --git a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/steps/BuildStepsUseCaseTest.kt b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/steps/BuildStepsUseCaseTest.kt index 06a62c556e..ec957625ad 100644 --- a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/steps/BuildStepsUseCaseTest.kt +++ b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/steps/BuildStepsUseCaseTest.kt @@ -163,6 +163,7 @@ class BuildStepsUseCaseTest { StepId.FINGERPRINT_CAPTURE, StepId.FINGERPRINT_CAPTURE, StepId.FACE_CAPTURE, + StepId.VALIDATE_ID_POOL, StepId.FINGERPRINT_MATCHER, StepId.FINGERPRINT_MATCHER, StepId.FACE_MATCHER, @@ -170,12 +171,12 @@ class BuildStepsUseCaseTest { } @Test - fun `build - enrol action - no age restriction - duplicate check - pool validation - returns expected steps`() = runTest { + fun `build - enrol action - no age restriction - duplicate check - pool validation disabled - returns expected steps`() = runTest { val projectConfiguration = mockCommonProjectConfiguration() every { projectConfiguration.general.duplicateBiometricEnrolmentCheck } returns true every { projectConfiguration.custom } returns - mapOf("validateIdentificationPool" to JsonPrimitive(true)) + mapOf("disableSubjectPoolValidation" to JsonPrimitive(true)) val action = mockk(relaxed = true) every { action.getSubjectAgeIfAvailable() } returns null @@ -189,7 +190,6 @@ class BuildStepsUseCaseTest { StepId.FINGERPRINT_CAPTURE, StepId.FINGERPRINT_CAPTURE, StepId.FACE_CAPTURE, - StepId.VALIDATE_ID_POOL, StepId.FINGERPRINT_MATCHER, StepId.FINGERPRINT_MATCHER, StepId.FACE_MATCHER, @@ -214,6 +214,7 @@ class BuildStepsUseCaseTest { StepId.FINGERPRINT_CAPTURE, StepId.FINGERPRINT_CAPTURE, StepId.FACE_CAPTURE, + StepId.VALIDATE_ID_POOL, StepId.FACE_MATCHER, ) } @@ -230,6 +231,7 @@ class BuildStepsUseCaseTest { assertStepOrder( steps, StepId.SETUP, + StepId.VALIDATE_ID_POOL, StepId.CONSENT, StepId.FINGERPRINT_CAPTURE, StepId.FINGERPRINT_CAPTURE, @@ -253,6 +255,7 @@ class BuildStepsUseCaseTest { assertStepOrder( steps, StepId.SETUP, + StepId.VALIDATE_ID_POOL, StepId.CONSENT, StepId.FACE_CAPTURE, StepId.FACE_MATCHER, @@ -260,20 +263,19 @@ class BuildStepsUseCaseTest { } @Test - fun `build - identify action - no age restriction - id pool validation - returns expected steps`() = runTest { + fun `build - identify action - no age restriction - pool validation disabled - returns expected steps`() = runTest { val projectConfiguration = mockCommonProjectConfiguration() val action = mockk(relaxed = true) every { action.getSubjectAgeIfAvailable() } returns null every { projectConfiguration.custom } returns - mapOf("validateIdentificationPool" to JsonPrimitive(true)) + mapOf("disableSubjectPoolValidation" to JsonPrimitive(true)) val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId, cachedScannedCredentialResult) assertStepOrder( steps, StepId.SETUP, - StepId.VALIDATE_ID_POOL, StepId.CONSENT, StepId.FINGERPRINT_CAPTURE, StepId.FINGERPRINT_CAPTURE, @@ -419,6 +421,7 @@ class BuildStepsUseCaseTest { assertStepOrder( steps, StepId.SETUP, + StepId.VALIDATE_ID_POOL, StepId.SELECT_SUBJECT_AGE, StepId.CONSENT, ) @@ -490,6 +493,7 @@ class BuildStepsUseCaseTest { StepId.FINGERPRINT_CAPTURE, StepId.FINGERPRINT_CAPTURE, StepId.FACE_CAPTURE, + StepId.VALIDATE_ID_POOL, StepId.FINGERPRINT_MATCHER, StepId.FINGERPRINT_MATCHER, StepId.FACE_MATCHER, @@ -512,6 +516,7 @@ class BuildStepsUseCaseTest { assertStepOrder( steps, StepId.SETUP, + StepId.VALIDATE_ID_POOL, StepId.CONSENT, StepId.FINGERPRINT_CAPTURE, StepId.FINGERPRINT_CAPTURE, @@ -689,6 +694,7 @@ class BuildStepsUseCaseTest { StepId.FINGERPRINT_CAPTURE, StepId.FINGERPRINT_CAPTURE, StepId.FACE_CAPTURE, + StepId.VALIDATE_ID_POOL, StepId.FINGERPRINT_MATCHER, StepId.FINGERPRINT_MATCHER, StepId.FACE_MATCHER, @@ -844,6 +850,7 @@ class BuildStepsUseCaseTest { assertStepOrder( steps, StepId.SETUP, + StepId.VALIDATE_ID_POOL, StepId.CONSENT, StepId.FINGERPRINT_CAPTURE, StepId.FINGERPRINT_CAPTURE, diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/ExperimentalProjectConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/ExperimentalProjectConfiguration.kt index 5a48259297..64c6fca5c9 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/ExperimentalProjectConfiguration.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/ExperimentalProjectConfiguration.kt @@ -13,9 +13,9 @@ import kotlinx.serialization.json.longOrNull data class ExperimentalProjectConfiguration( private val customConfig: Map?, ) { - val idPoolValidationEnabled: Boolean + val disableSubjectPoolValidation: Boolean get() = customConfig - ?.get(ENABLE_ID_POOL_VALIDATION) + ?.get(DISABLE_SUBJECT_POOL_VALIDATION) ?.jsonPrimitive ?.booleanOrNull .let { it == true } @@ -180,7 +180,7 @@ data class ExperimentalProjectConfiguration( .let { it == true } companion object { - internal const val ENABLE_ID_POOL_VALIDATION = "validateIdentificationPool" + internal const val DISABLE_SUBJECT_POOL_VALIDATION = "disableSubjectPoolValidation" internal const val SINGLE_GOOD_QUALITY_FALLBACK_REQUIRED = "singleQualityFallbackRequired" internal const val FACE_AUTO_CAPTURE_ENABLED = "faceAutoCaptureEnabled" internal const val FACE_AUTO_CAPTURE_IMAGING_DURATION_MILLIS = "faceAutoCaptureImagingDurationMillis" diff --git a/infra/config-store/src/test/java/com/simprints/infra/config/store/models/ExperimentalProjectConfigurationTest.kt b/infra/config-store/src/test/java/com/simprints/infra/config/store/models/ExperimentalProjectConfigurationTest.kt index ee074d3442..cde42544f2 100644 --- a/infra/config-store/src/test/java/com/simprints/infra/config/store/models/ExperimentalProjectConfigurationTest.kt +++ b/infra/config-store/src/test/java/com/simprints/infra/config/store/models/ExperimentalProjectConfigurationTest.kt @@ -3,7 +3,7 @@ package com.simprints.infra.config.store.models import com.google.common.truth.Truth.* import com.simprints.infra.config.store.models.ExperimentalProjectConfiguration.Companion.ALLOW_CONFIRMING_GUIDS_NOT_IN_CALLBACK import com.simprints.infra.config.store.models.ExperimentalProjectConfiguration.Companion.CAMERA_FLASH_CONTROLS_ENABLED -import com.simprints.infra.config.store.models.ExperimentalProjectConfiguration.Companion.ENABLE_ID_POOL_VALIDATION +import com.simprints.infra.config.store.models.ExperimentalProjectConfiguration.Companion.DISABLE_SUBJECT_POOL_VALIDATION import com.simprints.infra.config.store.models.ExperimentalProjectConfiguration.Companion.FACE_AUTO_CAPTURE_ENABLED import com.simprints.infra.config.store.models.ExperimentalProjectConfiguration.Companion.FACE_AUTO_CAPTURE_IMAGING_DURATION_MILLIS import com.simprints.infra.config.store.models.ExperimentalProjectConfiguration.Companion.FACE_AUTO_CAPTURE_IMAGING_DURATION_MILLIS_DEFAULT @@ -46,18 +46,18 @@ import org.junit.Test internal class ExperimentalProjectConfigurationTest { @Test - fun `check pool validation flag correctly`() { + fun `check disable pool validation flag correctly`() { mapOf( // Value not present emptyMap() to false, // Value not boolean - mapOf(ENABLE_ID_POOL_VALIDATION to JsonPrimitive(1)) to false, + mapOf(DISABLE_SUBJECT_POOL_VALIDATION to JsonPrimitive(1)) to false, // Value present and FALSE - mapOf(ENABLE_ID_POOL_VALIDATION to JsonPrimitive(false)) to false, + mapOf(DISABLE_SUBJECT_POOL_VALIDATION to JsonPrimitive(false)) to false, // Value present and TRUE - mapOf(ENABLE_ID_POOL_VALIDATION to JsonPrimitive(true)) to true, + mapOf(DISABLE_SUBJECT_POOL_VALIDATION to JsonPrimitive(true)) to true, ).forEach { (config, result) -> - assertThat(ExperimentalProjectConfiguration(config).idPoolValidationEnabled).isEqualTo(result) + assertThat(ExperimentalProjectConfiguration(config).disableSubjectPoolValidation).isEqualTo(result) } }