From 846849c1de8b8f1cfb68ac8a5ce53a4ec7c5e709 Mon Sep 17 00:00:00 2001 From: alex Date: Wed, 10 Sep 2025 15:51:05 +0300 Subject: [PATCH 1/9] [MS-1127] Changing type of API credential to follow the BFSID style --- .../store/remote/models/ApiMultiFactorIdConfiguration.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiMultiFactorIdConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiMultiFactorIdConfiguration.kt index 47a1501851..e85b27bb49 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiMultiFactorIdConfiguration.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiMultiFactorIdConfiguration.kt @@ -15,10 +15,10 @@ internal data class ApiMultiFactorIdConfiguration( @Keep enum class ApiExternalCredentialType { - NHISCard, GhanaIdCard, QRCode; + NHIS_CARD, GhanaIdCard, QRCode; fun toDomain(): ExternalCredentialType = when (this) { - NHISCard -> ExternalCredentialType.NHISCard + NHIS_CARD -> ExternalCredentialType.NHISCard GhanaIdCard -> ExternalCredentialType.GhanaIdCard QRCode -> ExternalCredentialType.QRCode } From 3ed24725a63ee60638a49d88e343acc1742531cb Mon Sep 17 00:00:00 2001 From: alex Date: Wed, 10 Sep 2025 19:07:26 +0300 Subject: [PATCH 2/9] [MS-1127] Creating fundamental base for MF-ID navigation in Enrol or Identify workflow --- feature/external-credential/.gitignore | 1 + feature/external-credential/build.gradle.kts | 12 ++ .../src/main/AndroidManifest.xml | 4 + .../ExternalCredentialContract.kt | 11 ++ .../model/ExternalCredentialParams.kt | 11 ++ .../ExternalCredentialControllerFragment.kt | 52 ++++++++ .../ExternalCredentialScanOcrFragment.kt | 9 ++ .../ExternalCredentialScanQrFragment.kt | 9 ++ .../ExternalCredentialSearchFragment.kt | 9 ++ .../ExternalCredentialSelectFragment.kt | 9 ++ .../skip/ExternalCredentialSkipFragment.kt | 9 ++ ...ragment_external_credential_controller.xml | 7 ++ .../fragment_external_credential_scan_ocr.xml | 13 ++ .../fragment_external_credential_scan_qr.xml | 12 ++ .../fragment_external_credential_search.xml | 12 ++ .../fragment_external_credential_select.xml | 13 ++ .../fragment_external_credential_skip.xml | 12 ++ .../navigation/graph_external_credential.xml | 20 +++ .../graph_external_credential_internal.xml | 64 ++++++++++ feature/orchestrator/build.gradle.kts | 1 + .../orchestrator/OrchestratorViewModel.kt | 26 ++-- .../feature/orchestrator/steps/StepId.kt | 1 + .../response/AppResponseBuilderUseCase.kt | 10 +- .../response/CreateEnrolResponseUseCase.kt | 2 + .../usecases/steps/BuildStepsUseCase.kt | 119 ++++++++++++------ .../res/navigation/graph_orchestration.xml | 8 +- .../orchestrator/OrchestratorViewModelTest.kt | 28 ++--- .../response/AppResponseBuilderUseCaseTest.kt | 20 +-- .../CreateEnrolResponseUseCaseTest.kt | 15 ++- .../usecases/steps/BuildStepsUseCaseTest.kt | 65 +++++----- .../eventsync/sync/common/SubjectFactory.kt | 2 +- .../sync/down/tasks/SubjectFactoryTest.kt | 1 + settings.gradle.kts | 1 + 33 files changed, 474 insertions(+), 114 deletions(-) create mode 100644 feature/external-credential/.gitignore create mode 100644 feature/external-credential/build.gradle.kts create mode 100644 feature/external-credential/src/main/AndroidManifest.xml create mode 100644 feature/external-credential/src/main/java/com/simprints/feature/externalcredential/ExternalCredentialContract.kt create mode 100644 feature/external-credential/src/main/java/com/simprints/feature/externalcredential/model/ExternalCredentialParams.kt create mode 100644 feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/controller/ExternalCredentialControllerFragment.kt create mode 100644 feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/ExternalCredentialScanOcrFragment.kt create mode 100644 feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanqr/ExternalCredentialScanQrFragment.kt create mode 100644 feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/ExternalCredentialSearchFragment.kt create mode 100644 feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/select/ExternalCredentialSelectFragment.kt create mode 100644 feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/skip/ExternalCredentialSkipFragment.kt create mode 100644 feature/external-credential/src/main/res/layout/fragment_external_credential_controller.xml create mode 100644 feature/external-credential/src/main/res/layout/fragment_external_credential_scan_ocr.xml create mode 100644 feature/external-credential/src/main/res/layout/fragment_external_credential_scan_qr.xml create mode 100644 feature/external-credential/src/main/res/layout/fragment_external_credential_search.xml create mode 100644 feature/external-credential/src/main/res/layout/fragment_external_credential_select.xml create mode 100644 feature/external-credential/src/main/res/layout/fragment_external_credential_skip.xml create mode 100644 feature/external-credential/src/main/res/navigation/graph_external_credential.xml create mode 100644 feature/external-credential/src/main/res/navigation/graph_external_credential_internal.xml diff --git a/feature/external-credential/.gitignore b/feature/external-credential/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/feature/external-credential/.gitignore @@ -0,0 +1 @@ +/build diff --git a/feature/external-credential/build.gradle.kts b/feature/external-credential/build.gradle.kts new file mode 100644 index 0000000000..2db2841b64 --- /dev/null +++ b/feature/external-credential/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + id("simprints.feature") + id("kotlin-parcelize") +} + +android { + namespace = "com.simprints.feature.externalcredential" +} + +dependencies { + implementation(project(":feature:exit-form")) +} diff --git a/feature/external-credential/src/main/AndroidManifest.xml b/feature/external-credential/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..e100076157 --- /dev/null +++ b/feature/external-credential/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/ExternalCredentialContract.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/ExternalCredentialContract.kt new file mode 100644 index 0000000000..5928a86987 --- /dev/null +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/ExternalCredentialContract.kt @@ -0,0 +1,11 @@ +package com.simprints.feature.externalcredential + +import com.simprints.core.domain.common.FlowType +import com.simprints.feature.externalcredential.model.ExternalCredentialParams + +object ExternalCredentialContract { + val DESTINATION = R.id.externalCredentialControllerFragment + + fun getParams(subjectId: String?, flowType: FlowType) = ExternalCredentialParams(subjectId = subjectId, flowType = flowType) + +} diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/model/ExternalCredentialParams.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/model/ExternalCredentialParams.kt new file mode 100644 index 0000000000..0d8fbef752 --- /dev/null +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/model/ExternalCredentialParams.kt @@ -0,0 +1,11 @@ +package com.simprints.feature.externalcredential.model + +import androidx.annotation.Keep +import com.simprints.core.domain.common.FlowType +import com.simprints.core.domain.step.StepParams + +@Keep +data class ExternalCredentialParams( + val subjectId: String?, + val flowType: FlowType +) : StepParams diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/controller/ExternalCredentialControllerFragment.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/controller/ExternalCredentialControllerFragment.kt new file mode 100644 index 0000000000..e95d488dc3 --- /dev/null +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/controller/ExternalCredentialControllerFragment.kt @@ -0,0 +1,52 @@ +package com.simprints.feature.externalcredential.screens.controller + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.Fragment +import androidx.navigation.NavController +import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs +import com.simprints.feature.exitform.ExitFormContract +import com.simprints.feature.exitform.ExitFormResult +import com.simprints.feature.externalcredential.GraphExternalCredentialInternalDirections +import com.simprints.feature.externalcredential.R +import com.simprints.infra.uibase.navigation.finishWithResult +import com.simprints.infra.uibase.navigation.handleResult +import com.simprints.infra.uibase.navigation.navigateSafely +import dagger.hilt.android.AndroidEntryPoint +import kotlin.getValue + +@AndroidEntryPoint +internal class ExternalCredentialControllerFragment : Fragment(R.layout.fragment_external_credential_controller) { + private val args: ExternalCredentialControllerFragmentArgs by navArgs() + + private val hostFragment: Fragment? + get() = childFragmentManager.findFragmentById(R.id.external_credential_host_fragment) + + private val internalNavController: NavController? + get() = hostFragment?.findNavController() + + private val currentlyDisplayedInternalFragment: Fragment? + get() = hostFragment?.childFragmentManager?.fragments?.first() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + findNavController().handleResult( + this, + R.id.externalCredentialControllerFragment, + ExitFormContract.DESTINATION, + ) { + val option = it.submittedOption() + if (option != null) { + findNavController().finishWithResult(this, it) + } else { + internalNavController?.navigateSafely( + currentlyDisplayedInternalFragment, + GraphExternalCredentialInternalDirections.actionGlobalExternalCredentialSelect() + ) + } + } + internalNavController?.setGraph(R.navigation.graph_external_credential_internal) + } +} diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/ExternalCredentialScanOcrFragment.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/ExternalCredentialScanOcrFragment.kt new file mode 100644 index 0000000000..c055da0ae6 --- /dev/null +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/ExternalCredentialScanOcrFragment.kt @@ -0,0 +1,9 @@ +package com.simprints.feature.externalcredential.screens.scanocr + +import androidx.fragment.app.Fragment +import com.simprints.feature.externalcredential.R +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +internal class ExternalCredentialScanOcrFragment : Fragment(R.layout.fragment_external_credential_scan_ocr) { +} diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanqr/ExternalCredentialScanQrFragment.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanqr/ExternalCredentialScanQrFragment.kt new file mode 100644 index 0000000000..93c452cc05 --- /dev/null +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanqr/ExternalCredentialScanQrFragment.kt @@ -0,0 +1,9 @@ +package com.simprints.feature.externalcredential.screens.scanqr + +import androidx.fragment.app.Fragment +import com.simprints.feature.externalcredential.R +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +internal class ExternalCredentialScanQrFragment : Fragment(R.layout.fragment_external_credential_scan_qr) { +} diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/ExternalCredentialSearchFragment.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/ExternalCredentialSearchFragment.kt new file mode 100644 index 0000000000..8a46cf5717 --- /dev/null +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/ExternalCredentialSearchFragment.kt @@ -0,0 +1,9 @@ +package com.simprints.feature.externalcredential.screens.search + +import androidx.fragment.app.Fragment +import com.simprints.feature.externalcredential.R +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +internal class ExternalCredentialSearchFragment : Fragment(R.layout.fragment_external_credential_search) { +} diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/select/ExternalCredentialSelectFragment.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/select/ExternalCredentialSelectFragment.kt new file mode 100644 index 0000000000..ffd14f1e37 --- /dev/null +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/select/ExternalCredentialSelectFragment.kt @@ -0,0 +1,9 @@ +package com.simprints.feature.externalcredential.screens.select + +import androidx.fragment.app.Fragment +import com.simprints.feature.externalcredential.R +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +internal class ExternalCredentialSelectFragment : Fragment(R.layout.fragment_external_credential_select) { +} diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/skip/ExternalCredentialSkipFragment.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/skip/ExternalCredentialSkipFragment.kt new file mode 100644 index 0000000000..d347a4cace --- /dev/null +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/skip/ExternalCredentialSkipFragment.kt @@ -0,0 +1,9 @@ +package com.simprints.feature.externalcredential.screens.skip + +import androidx.fragment.app.Fragment +import com.simprints.feature.externalcredential.R +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class ExternalCredentialSkipFragment : Fragment(R.layout.fragment_external_credential_skip) { +} diff --git a/feature/external-credential/src/main/res/layout/fragment_external_credential_controller.xml b/feature/external-credential/src/main/res/layout/fragment_external_credential_controller.xml new file mode 100644 index 0000000000..2da94202c5 --- /dev/null +++ b/feature/external-credential/src/main/res/layout/fragment_external_credential_controller.xml @@ -0,0 +1,7 @@ + + + diff --git a/feature/external-credential/src/main/res/layout/fragment_external_credential_scan_ocr.xml b/feature/external-credential/src/main/res/layout/fragment_external_credential_scan_ocr.xml new file mode 100644 index 0000000000..1f9f2b618b --- /dev/null +++ b/feature/external-credential/src/main/res/layout/fragment_external_credential_scan_ocr.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/feature/external-credential/src/main/res/layout/fragment_external_credential_scan_qr.xml b/feature/external-credential/src/main/res/layout/fragment_external_credential_scan_qr.xml new file mode 100644 index 0000000000..187f72538b --- /dev/null +++ b/feature/external-credential/src/main/res/layout/fragment_external_credential_scan_qr.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/feature/external-credential/src/main/res/layout/fragment_external_credential_search.xml b/feature/external-credential/src/main/res/layout/fragment_external_credential_search.xml new file mode 100644 index 0000000000..dda0301c71 --- /dev/null +++ b/feature/external-credential/src/main/res/layout/fragment_external_credential_search.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/feature/external-credential/src/main/res/layout/fragment_external_credential_select.xml b/feature/external-credential/src/main/res/layout/fragment_external_credential_select.xml new file mode 100644 index 0000000000..fc81eec866 --- /dev/null +++ b/feature/external-credential/src/main/res/layout/fragment_external_credential_select.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/feature/external-credential/src/main/res/layout/fragment_external_credential_skip.xml b/feature/external-credential/src/main/res/layout/fragment_external_credential_skip.xml new file mode 100644 index 0000000000..1860c12b36 --- /dev/null +++ b/feature/external-credential/src/main/res/layout/fragment_external_credential_skip.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/feature/external-credential/src/main/res/navigation/graph_external_credential.xml b/feature/external-credential/src/main/res/navigation/graph_external_credential.xml new file mode 100644 index 0000000000..1d0865429c --- /dev/null +++ b/feature/external-credential/src/main/res/navigation/graph_external_credential.xml @@ -0,0 +1,20 @@ + + + + + + + + + + diff --git a/feature/external-credential/src/main/res/navigation/graph_external_credential_internal.xml b/feature/external-credential/src/main/res/navigation/graph_external_credential_internal.xml new file mode 100644 index 0000000000..0e988e1024 --- /dev/null +++ b/feature/external-credential/src/main/res/navigation/graph_external_credential_internal.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/feature/orchestrator/build.gradle.kts b/feature/orchestrator/build.gradle.kts index a300a9dc22..b84bb6dc9b 100644 --- a/feature/orchestrator/build.gradle.kts +++ b/feature/orchestrator/build.gradle.kts @@ -22,6 +22,7 @@ dependencies { implementation(project(":feature:matcher")) implementation(project(":feature:validate-subject-pool")) implementation(project(":feature:select-subject-age-group")) + implementation(project(":feature:external-credential")) implementation(project(":face:capture")) 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 c06f979bd5..ef6f57f5f9 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 @@ -46,6 +46,7 @@ import com.simprints.matcher.MatchParams import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import java.io.Serializable +import java.util.UUID import javax.inject.Inject @HiltViewModel @@ -61,6 +62,9 @@ internal class OrchestratorViewModel @Inject constructor( private val mapStepsForLastBiometrics: MapStepsForLastBiometricEnrolUseCase, ) : ViewModel() { var isRequestProcessed = false + + // [MS-1127] MF-ID: during enrolment, the same 'subjectId' needs to be used during the entire workflow + private val enrolmentSubjectId = UUID.randomUUID().toString() private var modalities = emptySet() private var steps = emptyList() private var actionRequest: ActionRequest? = null @@ -83,7 +87,11 @@ internal class OrchestratorViewModel @Inject constructor( // In case of a follow-up action, we should restore completed steps from cache // and add new ones to the list. This way all session steps are available throughout // the app for reference (i.e. have we already captured face in this session?) - steps = cache.steps + stepsBuilder.build(action, projectConfiguration) + steps = cache.steps + stepsBuilder.build( + action = action, + projectConfiguration = projectConfiguration, + enrolmentSubjectId = enrolmentSubjectId + ) Simber.i("Steps to execute: ${steps.joinToString { it.id.toString() }}", tag = ORCHESTRATION) } catch (_: SubjectAgeNotSupportedException) { handleErrorResponse(AppErrorResponse(AppErrorReason.AGE_GROUP_NOT_SUPPORTED)) @@ -115,9 +123,10 @@ internal class OrchestratorViewModel @Inject constructor( if (result is SelectSubjectAgeGroupResult) { val captureAndMatchSteps = stepsBuilder.buildCaptureAndMatchStepsForAgeGroup( - actionRequest!!, - projectConfiguration, - result.ageGroup, + action = actionRequest!!, + projectConfiguration = projectConfiguration, + ageGroup = result.ageGroup, + enrolmentSubjectId = enrolmentSubjectId ) steps = steps + captureAndMatchSteps } @@ -195,10 +204,11 @@ internal class OrchestratorViewModel @Inject constructor( val projectConfiguration = configManager.getProjectConfiguration() val project = configManager.getProject(projectConfiguration.projectId) val appResponse = appResponseBuilder( - projectConfiguration, - actionRequest, - steps.mapNotNull { it.result }, - project, + projectConfiguration = projectConfiguration, + request = actionRequest, + results = steps.mapNotNull { it.result }, + project = project, + enrolmentSubjectId = enrolmentSubjectId, ) updateDailyActivity(appResponse) diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/steps/StepId.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/steps/StepId.kt index 279839e355..8562f02bbc 100644 --- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/steps/StepId.kt +++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/steps/StepId.kt @@ -16,6 +16,7 @@ internal object StepId { const val CONFIRM_IDENTITY = STEP_BASE_CORE + 5 const val VALIDATE_ID_POOL = STEP_BASE_CORE + 6 const val SELECT_SUBJECT_AGE = STEP_BASE_CORE + 7 + const val EXTERNAL_CREDENTIAL = STEP_BASE_CORE + 8 // Face step ids private const val STEP_BASE_FINGERPRINT = 300 diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/AppResponseBuilderUseCase.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/AppResponseBuilderUseCase.kt index c00db35481..69f2839a59 100644 --- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/AppResponseBuilderUseCase.kt +++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/AppResponseBuilderUseCase.kt @@ -21,10 +21,16 @@ internal class AppResponseBuilderUseCase @Inject constructor( projectConfiguration: ProjectConfiguration, request: ActionRequest?, results: List, - project: Project + project: Project, + enrolmentSubjectId: String, ): AppResponse = when (request) { is ActionRequest.EnrolActionRequest -> if (isNewEnrolment(projectConfiguration, results)) { - handleEnrolment(request, results, project) + handleEnrolment( + request = request, + results = results, + project = project, + enrolmentSubjectId = enrolmentSubjectId + ) } else { handleIdentify(projectConfiguration, results) } diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateEnrolResponseUseCase.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateEnrolResponseUseCase.kt index acfa8b4e72..6cf73f2567 100644 --- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateEnrolResponseUseCase.kt +++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateEnrolResponseUseCase.kt @@ -23,6 +23,7 @@ internal class CreateEnrolResponseUseCase @Inject constructor( request: ActionRequest.EnrolActionRequest, results: List, project: Project, + enrolmentSubjectId: String, ): AppResponse { val fingerprintCapture = results.filterIsInstance(FingerprintCaptureResult::class.java).lastOrNull() val faceCapture = results.filterIsInstance(FaceCaptureResult::class.java).lastOrNull() @@ -31,6 +32,7 @@ internal class CreateEnrolResponseUseCase @Inject constructor( return try { val subject = subjectFactory.buildSubjectFromCaptureResults( + subjectId = enrolmentSubjectId, projectId = request.projectId, attendantId = request.userId, moduleId = request.moduleId, 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 42ada62779..6fe6bbf940 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 @@ -5,6 +5,7 @@ import com.simprints.face.capture.FaceCaptureContract import com.simprints.feature.consent.ConsentContract import com.simprints.feature.consent.ConsentType import com.simprints.feature.enrollast.EnrolLastBiometricContract +import com.simprints.feature.externalcredential.ExternalCredentialContract import com.simprints.feature.fetchsubject.FetchSubjectContract import com.simprints.feature.orchestrator.R import com.simprints.feature.orchestrator.cache.OrchestratorCache @@ -43,12 +44,13 @@ internal class BuildStepsUseCase @Inject constructor( suspend fun build( action: ActionRequest, projectConfiguration: ProjectConfiguration, + enrolmentSubjectId: String, ) = when (action) { is ActionRequest.EnrolActionRequest -> listOf( buildSetupStep(), buildAgeSelectionStepIfNeeded(action, projectConfiguration), buildConsentStepIfNeeded(ConsentType.ENROL, projectConfiguration), - buildCaptureAndMatchStepsForEnrol(action, projectConfiguration), + buildCaptureAndMatchStepsForEnrol(action, projectConfiguration, enrolmentSubjectId = enrolmentSubjectId), ) is ActionRequest.IdentifyActionRequest -> { @@ -65,9 +67,10 @@ internal class BuildStepsUseCase @Inject constructor( buildAgeSelectionStepIfNeeded(action, projectConfiguration), buildConsentStepIfNeeded(ConsentType.IDENTIFY, projectConfiguration), buildCaptureAndMatchStepsForIdentify( - action, - projectConfiguration, + action = action, + projectConfiguration = projectConfiguration, subjectQuery = subjectQuery, + enrolmentSubjectId = enrolmentSubjectId ), ) } @@ -99,18 +102,21 @@ internal class BuildStepsUseCase @Inject constructor( action: ActionRequest, projectConfiguration: ProjectConfiguration, ageGroup: AgeGroup, + enrolmentSubjectId: String, ): List = when (action) { is ActionRequest.EnrolActionRequest -> buildCaptureAndMatchStepsForEnrol( action, projectConfiguration, ageGroup, + enrolmentSubjectId ) is ActionRequest.IdentifyActionRequest -> buildCaptureAndMatchStepsForIdentify( - action, - projectConfiguration, - ageGroup, + action = action, + projectConfiguration = projectConfiguration, + ageGroup = ageGroup, subjectQuery = buildMatcherSubjectQuery(projectConfiguration, action), + enrolmentSubjectId = enrolmentSubjectId ) is ActionRequest.VerifyActionRequest -> buildCaptureAndMatchStepsForVerify( @@ -122,35 +128,63 @@ internal class BuildStepsUseCase @Inject constructor( else -> emptyList() } + private fun buildExternalCredentialStepIfNeeded( + enrolmentSubjectId: String, + projectConfiguration: ProjectConfiguration, + flowType: FlowType + ): List { + val isExternalCredentialEnabled = projectConfiguration.multifactorId?.allowedExternalCredentials?.isNotEmpty() ?: false + if (!isExternalCredentialEnabled) return emptyList() + + return when (flowType) { + FlowType.ENROL, FlowType.IDENTIFY -> { + listOf( + Step( + id = StepId.EXTERNAL_CREDENTIAL, + navigationActionId = R.id.action_orchestratorFragment_to_externalCredential, + destinationId = ExternalCredentialContract.DESTINATION, + params = ExternalCredentialContract.getParams(subjectId = enrolmentSubjectId, flowType = flowType), + ) + ) + } + + FlowType.VERIFY -> emptyList() + } + } + private suspend fun buildCaptureAndMatchStepsForEnrol( action: ActionRequest.EnrolActionRequest, projectConfiguration: ProjectConfiguration, ageGroup: AgeGroup? = null, + enrolmentSubjectId: String, ): List { val action = fallbackToCommCareDataSourceIfNeeded(action, projectConfiguration) val resolvedAgeGroup = ageGroup ?: ageGroupFromSubjectAge(action, projectConfiguration) - return listOf( - buildCaptureSteps( + val captureSteps = buildCaptureSteps( + projectConfiguration, + FlowType.ENROL, + resolvedAgeGroup, + ) + val externalCredentialStep = when { + captureSteps.isEmpty() -> emptyList() + else -> buildExternalCredentialStepIfNeeded(enrolmentSubjectId, projectConfiguration, FlowType.ENROL) + } + val matcherSteps = if (projectConfiguration.general.duplicateBiometricEnrolmentCheck) { + buildMatcherSteps( projectConfiguration, FlowType.ENROL, resolvedAgeGroup, - ), - if (projectConfiguration.general.duplicateBiometricEnrolmentCheck) { - buildMatcherSteps( - projectConfiguration, - FlowType.ENROL, - resolvedAgeGroup, - buildMatcherSubjectQuery(projectConfiguration, action), - BiometricDataSource.fromString( - action.biometricDataSource, - action.actionIdentifier.callerPackageName, - ), - ) - } else { - emptyList() - }, - ).flatten() + buildMatcherSubjectQuery(projectConfiguration, action), + BiometricDataSource.fromString( + action.biometricDataSource, + action.actionIdentifier.callerPackageName, + ), + ) + } else { + emptyList() + } + return captureSteps + externalCredentialStep + matcherSteps } private suspend fun buildCaptureAndMatchStepsForIdentify( @@ -158,27 +192,30 @@ internal class BuildStepsUseCase @Inject constructor( projectConfiguration: ProjectConfiguration, ageGroup: AgeGroup? = null, subjectQuery: SubjectQuery, + enrolmentSubjectId: String ): List { val action = fallbackToCommCareDataSourceIfNeeded(action, projectConfiguration) val resolvedAgeGroup = ageGroup ?: ageGroupFromSubjectAge(action, projectConfiguration) - - return listOf( - buildCaptureSteps( - projectConfiguration, - FlowType.IDENTIFY, - resolvedAgeGroup, - ), - buildMatcherSteps( - projectConfiguration, - FlowType.IDENTIFY, - resolvedAgeGroup, - subjectQuery, - BiometricDataSource.fromString( - action.biometricDataSource, - action.actionIdentifier.callerPackageName, - ), + val captureSteps = buildCaptureSteps( + projectConfiguration, + FlowType.IDENTIFY, + resolvedAgeGroup, + ) + val externalCredentialStep = when { + captureSteps.isEmpty() -> emptyList() + else -> buildExternalCredentialStepIfNeeded(enrolmentSubjectId, projectConfiguration, FlowType.IDENTIFY) + } + val matcherSteps = buildMatcherSteps( + projectConfiguration, + FlowType.IDENTIFY, + resolvedAgeGroup, + subjectQuery, + BiometricDataSource.fromString( + action.biometricDataSource, + action.actionIdentifier.callerPackageName, ), - ).flatten() + ) + return captureSteps + externalCredentialStep + matcherSteps } private fun buildCaptureAndMatchStepsForVerify( diff --git a/feature/orchestrator/src/main/res/navigation/graph_orchestration.xml b/feature/orchestrator/src/main/res/navigation/graph_orchestration.xml index 56a33ef8da..43cb91cf24 100644 --- a/feature/orchestrator/src/main/res/navigation/graph_orchestration.xml +++ b/feature/orchestrator/src/main/res/navigation/graph_orchestration.xml @@ -77,6 +77,10 @@ + app:destination="@id/graph_validate_subject_pool" /> + + + 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 0efa8da3a9..4038614599 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 @@ -112,7 +112,7 @@ internal class OrchestratorViewModelTest { @Test fun `Starts executing steps when action when received`() = runTest { - coEvery { stepsBuilder.build(any(), any()) } returns listOf( + coEvery { stepsBuilder.build(any(), any(), any()) } returns listOf( createMockStep(StepId.SETUP), ) @@ -125,7 +125,7 @@ internal class OrchestratorViewModelTest { @Test fun `Executes next steps after step result`() = runTest { - coEvery { stepsBuilder.build(any(), any()) } returns listOf( + coEvery { stepsBuilder.build(any(), any(), any()) } returns listOf( createMockStep(StepId.SETUP), createMockStep(StepId.CONSENT), ) @@ -142,12 +142,12 @@ internal class OrchestratorViewModelTest { @Test fun `Returns response when all steps executed`() = runTest { - coEvery { stepsBuilder.build(any(), any()) } returns listOf( + coEvery { stepsBuilder.build(any(), any(), any()) } returns listOf( createMockStep(StepId.SETUP), createMockStep(StepId.CONSENT), ) coEvery { mapRefusalOrErrorResult(any(), any()) } returns null - coEvery { appResponseBuilder(any(), any(), any(), any()) } returns mockk() + coEvery { appResponseBuilder(any(), any(), any(), any(), any()) } returns mockk() coJustRun { dailyActivityUseCase(any()) } justRun { addCallbackEvent(any()) } @@ -160,7 +160,7 @@ internal class OrchestratorViewModelTest { @Test fun `Returns response when error result received`() = runTest { - coEvery { stepsBuilder.build(any(), any()) } returns listOf( + coEvery { stepsBuilder.build(any(), any(), any()) } returns listOf( createMockStep(StepId.SETUP), createMockStep(StepId.CONSENT), ) @@ -174,7 +174,7 @@ internal class OrchestratorViewModelTest { @Test fun `Returns AGE_GROUP_NOT_SUPPORTED response when step builder throws SubjectAgeNotSupportedException`() = runTest { - coEvery { stepsBuilder.build(any(), any()) } throws SubjectAgeNotSupportedException() + coEvery { stepsBuilder.build(any(), any(), any()) } throws SubjectAgeNotSupportedException() viewModel.handleAction(mockk()) @@ -187,7 +187,7 @@ internal class OrchestratorViewModelTest { @Test fun `Appends capture and match steps upon receiving SelectSubjectAgeGroupResult`() = runTest { - coEvery { stepsBuilder.build(any(), any()) } returns listOf( + coEvery { stepsBuilder.build(any(), any(), any()) } returns listOf( createMockStep(StepId.SELECT_SUBJECT_AGE), ) coEvery { mapRefusalOrErrorResult(any(), any()) } returns null @@ -202,12 +202,12 @@ internal class OrchestratorViewModelTest { ), ), ) - coEvery { stepsBuilder.buildCaptureAndMatchStepsForAgeGroup(any(), any(), any()) } returns captureAndMatchSteps + coEvery { stepsBuilder.buildCaptureAndMatchStepsForAgeGroup(any(), any(), any(), any()) } returns captureAndMatchSteps viewModel.handleAction(mockk()) viewModel.handleResult(SelectSubjectAgeGroupResult(AgeGroup(0, 1))) - coVerify { stepsBuilder.buildCaptureAndMatchStepsForAgeGroup(any(), any(), any()) } + coVerify { stepsBuilder.buildCaptureAndMatchStepsForAgeGroup(any(), any(), any(), any()) } viewModel.currentStep.test().value().peekContent()?.let { step -> assertThat(step.id).isEqualTo(StepId.FACE_CAPTURE) } @@ -215,7 +215,7 @@ internal class OrchestratorViewModelTest { @Test fun `Updates face matcher step payload when receiving face capture`() = runTest { - coEvery { stepsBuilder.build(any(), any()) } returns listOf( + coEvery { stepsBuilder.build(any(), any(), any()) } returns listOf( createMockStep(StepId.FACE_CAPTURE), createMockStep( StepId.FACE_MATCHER, @@ -238,7 +238,7 @@ internal class OrchestratorViewModelTest { @Test fun `Updates fingerprint matcher step payload when receiving fingerprint capture`() = runTest { - coEvery { stepsBuilder.build(any(), any()) } returns listOf( + coEvery { stepsBuilder.build(any(), any(), any()) } returns listOf( createMockStep(StepId.FINGERPRINT_CAPTURE), createMockStep( StepId.FINGERPRINT_MATCHER, @@ -261,7 +261,7 @@ internal class OrchestratorViewModelTest { @Test fun `Updates the correct fingerprint match step when multiple fingerprint SDKs are used`() = runTest { - coEvery { stepsBuilder.build(any(), any()) } returns listOf( + coEvery { stepsBuilder.build(any(), any(), any()) } returns listOf( createMockStep( StepId.FINGERPRINT_CAPTURE, FingerprintCaptureContract.getParams( @@ -350,7 +350,7 @@ internal class OrchestratorViewModelTest { val originalSteps = listOf( createMockStep(StepId.FINGERPRINT_CAPTURE), ) - coEvery { stepsBuilder.build(any(), any()) } returns originalSteps + coEvery { stepsBuilder.build(any(), any(), any()) } returns originalSteps val savedSteps = listOf( createMockStep(StepId.SETUP), createMockStep(StepId.CONSENT), @@ -414,7 +414,7 @@ internal class OrchestratorViewModelTest { TokenizableString.Tokenized("moduleId"), listOf(mockk()), ) - coEvery { stepsBuilder.build(any(), any()) } returns listOf( + coEvery { stepsBuilder.build(any(), any(), any()) } returns listOf( captureStep, enrolLastStep, ) diff --git a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/AppResponseBuilderUseCaseTest.kt b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/AppResponseBuilderUseCaseTest.kt index 9ad9e2d728..1ff982ba2e 100644 --- a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/AppResponseBuilderUseCaseTest.kt +++ b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/AppResponseBuilderUseCaseTest.kt @@ -33,12 +33,13 @@ internal class AppResponseBuilderUseCaseTest { lateinit var handleEnrolLastBiometric: CreateEnrolLastBiometricResponseUseCase private lateinit var useCase: AppResponseBuilderUseCase + private lateinit var enrolmentSubjectId: String @Before fun setUp() { MockKAnnotations.init(this, relaxUnitFun = true) - coEvery { handleEnrolment.invoke(any(), any(), any()) } returns mockk() + coEvery { handleEnrolment.invoke(any(), any(), any(), any()) } returns mockk() coEvery { handleIdentify.invoke(any(), any()) } returns mockk() every { handleVerify.invoke(any(), any()) } returns mockk() every { handleConfirmIdentity.invoke(any()) } returns mockk() @@ -52,48 +53,49 @@ internal class AppResponseBuilderUseCaseTest { handleConfirmIdentity, handleEnrolLastBiometric, ) + enrolmentSubjectId = "enrolmentSubjectId" } @Test fun `Handles as enrolment for new enrolment action`() = runTest { every { isNewEnrolment(any(), any()) } returns true - useCase(mockk(), mockk(), mockk(), mockk()) - coVerify { handleEnrolment.invoke(any(), any(), any()) } + useCase(mockk(), mockk(), mockk(), mockk(), enrolmentSubjectId) + coVerify { handleEnrolment.invoke(any(), any(), any(), any()) } } @Test fun `Handles as identification for enrolment action with existing item`() = runTest { every { isNewEnrolment(any(), any()) } returns false - useCase(mockk(), mockk(), mockk(), mockk()) + useCase(mockk(), mockk(), mockk(), mockk(), enrolmentSubjectId) coVerify { handleIdentify.invoke(any(), any()) } } @Test fun `Handles as identification for identification action`() = runTest { - useCase(mockk(), mockk(), mockk(), mockk()) + useCase(mockk(), mockk(), mockk(), mockk(), enrolmentSubjectId) coVerify { handleIdentify.invoke(any(), any()) } } @Test fun `Handles as verification for verification action`() = runTest { - useCase(mockk(), mockk(), mockk(), mockk()) + useCase(mockk(), mockk(), mockk(), mockk(), enrolmentSubjectId) coVerify { handleVerify.invoke(any(), any()) } } @Test fun `Handles as confirmIdentity for confirm action`() = runTest { - useCase(mockk(), mockk(), mockk(), mockk()) + useCase(mockk(), mockk(), mockk(), mockk(), enrolmentSubjectId) coVerify { handleConfirmIdentity.invoke(any()) } } @Test fun `Handles as enrol last biometric for enrol last action`() = runTest { - useCase(mockk(), mockk(), mockk(), mockk()) + useCase(mockk(), mockk(), mockk(), mockk(), enrolmentSubjectId) coVerify { handleEnrolLastBiometric.invoke(any()) } } @Test fun `Handles null request`() = runTest { - assertThat(useCase(mockk(), null, mockk(), mockk())).isInstanceOf(AppErrorResponse::class.java) + assertThat(useCase(mockk(), null, mockk(), mockk(), enrolmentSubjectId)).isInstanceOf(AppErrorResponse::class.java) } } 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 27100d1399..1484774120 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 @@ -29,6 +29,8 @@ internal class CreateEnrolResponseUseCaseTest { @MockK lateinit var project: Project + private val enrolmentSubjectId = "enrolmentSubjectId" + private val action = mockk { every { projectId } returns "projectId" every { userId } returns "userId".asTokenizableRaw() @@ -50,24 +52,26 @@ internal class CreateEnrolResponseUseCaseTest { fun `Converts correct results to response`() = runTest { every { subjectFactory.buildSubjectFromCaptureResults( + subjectId = any(), projectId = any(), attendantId = any(), moduleId = any(), fingerprintResponse = any(), faceResponse = any(), - externalCredential = any() + externalCredential = any(), ) } returns mockk { every { subjectId } returns "guid" } assertThat( useCase( - action, - listOf( + request = action, + results = listOf( FingerprintCaptureResult("", emptyList()), FaceCaptureResult("", emptyList()), mockk(), ), - project + project = project, + enrolmentSubjectId = enrolmentSubjectId ), ).isInstanceOf(AppEnrolResponse::class.java) } @@ -76,6 +80,7 @@ internal class CreateEnrolResponseUseCaseTest { fun `Returns error if no valid response`() = runTest { every { subjectFactory.buildSubjectFromCaptureResults( + subjectId = any(), projectId = any(), attendantId = any(), moduleId = any(), @@ -85,6 +90,6 @@ internal class CreateEnrolResponseUseCaseTest { ) } throws MissingCaptureException() - assertThat(useCase(action, emptyList(), project)).isInstanceOf(AppErrorResponse::class.java) + assertThat(useCase(action, emptyList(), project, enrolmentSubjectId)).isInstanceOf(AppErrorResponse::class.java) } } 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 12a3cb5a65..6328e20227 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 @@ -47,12 +47,13 @@ class BuildStepsUseCaseTest { private lateinit var nec: FingerprintConfiguration.FingerprintSdkConfiguration private lateinit var useCase: BuildStepsUseCase + private lateinit var enrolmentSubjectId: String @Before fun setup() { MockKAnnotations.init(this) useCase = BuildStepsUseCase(buildMatcherSubjectQuery, cache, mapStepsForLastBiometrics, fallbackToCommCareDataSourceIfNeeded) - + enrolmentSubjectId = "enrolmentSubjectId" // Setup fallback use case to return the input actions unchanged by default coEvery { fallbackToCommCareDataSourceIfNeeded(any(), any()) } answers { firstArg() } coEvery { fallbackToCommCareDataSourceIfNeeded(any(), any()) } answers { firstArg() } @@ -110,7 +111,7 @@ class BuildStepsUseCaseTest { val action = mockk(relaxed = true) every { action.getSubjectAgeIfAvailable() } returns null - val steps = useCase.build(action, projectConfiguration) + val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId) assertStepOrder( steps, @@ -130,7 +131,7 @@ class BuildStepsUseCaseTest { val action = mockk(relaxed = true) every { action.getSubjectAgeIfAvailable() } returns null - val steps = useCase.build(action, projectConfiguration) + val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId) assertStepOrder( steps, @@ -149,7 +150,7 @@ class BuildStepsUseCaseTest { val action = mockk(relaxed = true) every { action.getSubjectAgeIfAvailable() } returns null - val steps = useCase.build(action, projectConfiguration) + val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId) assertStepOrder( steps, @@ -173,7 +174,7 @@ class BuildStepsUseCaseTest { val action = mockk(relaxed = true) every { action.getSubjectAgeIfAvailable() } returns null - val steps = useCase.build(action, projectConfiguration) + val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId) assertStepOrder( steps, @@ -193,7 +194,7 @@ class BuildStepsUseCaseTest { val action = mockk(relaxed = true) every { action.getSubjectAgeIfAvailable() } returns null - val steps = useCase.build(action, projectConfiguration) + val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId) assertStepOrder( steps, @@ -216,7 +217,7 @@ class BuildStepsUseCaseTest { val action = mockk(relaxed = true) every { action.getSubjectAgeIfAvailable() } returns null - val steps = useCase.build(action, projectConfiguration) + val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId) assertStepOrder( steps, @@ -235,7 +236,7 @@ class BuildStepsUseCaseTest { every { action.getSubjectAgeIfAvailable() } returns null every { projectConfiguration.experimental().idPoolValidationEnabled } returns true - val steps = useCase.build(action, projectConfiguration) + val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId) assertStepOrder( steps, @@ -258,7 +259,7 @@ class BuildStepsUseCaseTest { val action = mockk(relaxed = true) every { action.getSubjectAgeIfAvailable() } returns null - val steps = useCase.build(action, projectConfiguration) + val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId) assertStepOrder( steps, @@ -282,7 +283,7 @@ class BuildStepsUseCaseTest { val action = mockk(relaxed = true) every { action.getSubjectAgeIfAvailable() } returns null - val steps = useCase.build(action, projectConfiguration) + val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId) assertStepOrder( steps, @@ -301,7 +302,7 @@ class BuildStepsUseCaseTest { val action = mockk(relaxed = true) every { action.getSubjectAgeIfAvailable() } returns null - val steps = useCase.build(action, projectConfiguration) + val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId) assertStepOrder( steps, @@ -320,7 +321,7 @@ class BuildStepsUseCaseTest { Step(StepId.FACE_CAPTURE, mockk(relaxed = true), mockk(relaxed = true), mockk(relaxed = true)), ) - val steps = useCase.build(action, projectConfiguration) + val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId) assertStepOrder( steps, @@ -339,7 +340,7 @@ class BuildStepsUseCaseTest { Step(StepId.FACE_CAPTURE, mockk(relaxed = true), mockk(relaxed = true), mockk(relaxed = true)), ) - val steps = useCase.build(action, projectConfiguration) + val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId) assertStepOrder( steps, @@ -360,7 +361,7 @@ class BuildStepsUseCaseTest { val action = mockk(relaxed = true) every { action.getSubjectAgeIfAvailable() } returns null - val steps = useCase.build(action, projectConfiguration) + val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId) assertStepOrder( steps, @@ -381,7 +382,7 @@ class BuildStepsUseCaseTest { val action = mockk(relaxed = true) every { action.getSubjectAgeIfAvailable() } returns null - val steps = useCase.build(action, projectConfiguration) + val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId) assertStepOrder( steps, @@ -402,7 +403,7 @@ class BuildStepsUseCaseTest { val action = mockk(relaxed = true) every { action.getSubjectAgeIfAvailable() } returns null - val steps = useCase.build(action, projectConfiguration) + val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId) assertStepOrder( steps, @@ -424,7 +425,7 @@ class BuildStepsUseCaseTest { val action = mockk(relaxed = true) every { action.getSubjectAgeIfAvailable() } returns 25 // Subject age within the supported range - val steps = useCase.build(action, projectConfiguration) + val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId) assertStepOrder( steps, @@ -448,7 +449,7 @@ class BuildStepsUseCaseTest { val action = mockk(relaxed = true) every { action.getSubjectAgeIfAvailable() } returns 25 // Subject age within the supported range - val steps = useCase.build(action, projectConfiguration) + val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId) assertStepOrder( steps, @@ -474,7 +475,7 @@ class BuildStepsUseCaseTest { val action = mockk(relaxed = true) every { action.getSubjectAgeIfAvailable() } returns 25 // Subject age within the supported range - val steps = useCase.build(action, projectConfiguration) + val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId) assertStepOrder( steps, @@ -501,7 +502,7 @@ class BuildStepsUseCaseTest { every { getSubjectAgeIfAvailable() } returns 25 every { biometricDataSource } returns "COMMCARE" } - val steps = useCase.build(action, projectConfiguration) + val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId) assertStepOrder( steps, @@ -527,7 +528,7 @@ class BuildStepsUseCaseTest { val action = mockk(relaxed = true) every { action.getSubjectAgeIfAvailable() } returns 25 // Subject age within the supported range - val steps = useCase.build(action, projectConfiguration) + val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId) assertStepOrder( steps, @@ -554,7 +555,7 @@ class BuildStepsUseCaseTest { val action = mockk(relaxed = true) every { action.getSubjectAgeIfAvailable() } returns 25 // Subject age within the supported range every { action.biometricDataSource } returns "COMMCARE" - val steps = useCase.build(action, projectConfiguration) + val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId) assertStepOrder( steps, @@ -582,7 +583,7 @@ class BuildStepsUseCaseTest { every { action.getSubjectAgeIfAvailable() } returns 20 // Subject age not supported by any SDK assertThrows(SubjectAgeNotSupportedException::class.java) { - runBlocking { useCase.build(action, projectConfiguration) } + runBlocking { useCase.build(action, projectConfiguration, enrolmentSubjectId) } } } @@ -598,7 +599,7 @@ class BuildStepsUseCaseTest { every { action.getSubjectAgeIfAvailable() } returns 20 // Subject age not supported by any SDK assertThrows(SubjectAgeNotSupportedException::class.java) { - runBlocking { useCase.build(action, projectConfiguration) } + runBlocking { useCase.build(action, projectConfiguration, enrolmentSubjectId) } } } @@ -614,7 +615,7 @@ class BuildStepsUseCaseTest { every { action.getSubjectAgeIfAvailable() } returns 20 // Subject age not supported by any SDK assertThrows(SubjectAgeNotSupportedException::class.java) { - runBlocking { useCase.build(action, projectConfiguration) } + runBlocking { useCase.build(action, projectConfiguration, enrolmentSubjectId) } } } @@ -628,7 +629,7 @@ class BuildStepsUseCaseTest { val action = mockk(relaxed = true) - val steps = useCase.buildCaptureAndMatchStepsForAgeGroup(action, projectConfiguration, ageGroup) + val steps = useCase.buildCaptureAndMatchStepsForAgeGroup(action, projectConfiguration, ageGroup, enrolmentSubjectId) assertStepOrder( steps, @@ -649,7 +650,7 @@ class BuildStepsUseCaseTest { val action = mockk(relaxed = true) - val steps = useCase.buildCaptureAndMatchStepsForAgeGroup(action, projectConfiguration, ageGroup) + val steps = useCase.buildCaptureAndMatchStepsForAgeGroup(action, projectConfiguration, ageGroup, enrolmentSubjectId) assertStepOrder( steps, @@ -672,7 +673,7 @@ class BuildStepsUseCaseTest { val action = mockk(relaxed = true) - val steps = useCase.buildCaptureAndMatchStepsForAgeGroup(action, projectConfiguration, ageGroup) + val steps = useCase.buildCaptureAndMatchStepsForAgeGroup(action, projectConfiguration, ageGroup, enrolmentSubjectId) assertStepOrder( steps, @@ -695,7 +696,7 @@ class BuildStepsUseCaseTest { val action = mockk(relaxed = true) - val steps = useCase.buildCaptureAndMatchStepsForAgeGroup(action, projectConfiguration, ageGroup) + val steps = useCase.buildCaptureAndMatchStepsForAgeGroup(action, projectConfiguration, ageGroup, enrolmentSubjectId) assertStepOrder( steps, @@ -719,7 +720,7 @@ class BuildStepsUseCaseTest { val action = mockk(relaxed = true) - val steps = useCase.buildCaptureAndMatchStepsForAgeGroup(action, projectConfiguration, ageGroup) + val steps = useCase.buildCaptureAndMatchStepsForAgeGroup(action, projectConfiguration, ageGroup, enrolmentSubjectId) assertStepOrder( steps, @@ -740,7 +741,7 @@ class BuildStepsUseCaseTest { val action = mockk(relaxed = true) - val steps = useCase.buildCaptureAndMatchStepsForAgeGroup(action, projectConfiguration, AgeGroup(18, 60)) + val steps = useCase.buildCaptureAndMatchStepsForAgeGroup(action, projectConfiguration, AgeGroup(18, 60), enrolmentSubjectId) assertEquals(0, steps.size) } @@ -751,7 +752,7 @@ class BuildStepsUseCaseTest { val action = mockk(relaxed = true) - val steps = useCase.buildCaptureAndMatchStepsForAgeGroup(action, projectConfiguration, AgeGroup(18, 60)) + val steps = useCase.buildCaptureAndMatchStepsForAgeGroup(action, projectConfiguration, AgeGroup(18, 60), enrolmentSubjectId) assertEquals(0, steps.size) } diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/common/SubjectFactory.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/common/SubjectFactory.kt index 68d2bde6e9..45ebcc9f33 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/common/SubjectFactory.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/common/SubjectFactory.kt @@ -68,6 +68,7 @@ class SubjectFactory @Inject constructor( } fun buildSubjectFromCaptureResults( + subjectId: String, projectId: String, attendantId: TokenizableString, moduleId: TokenizableString, @@ -75,7 +76,6 @@ class SubjectFactory @Inject constructor( faceResponse: FaceCaptureResult?, externalCredential: ExternalCredential?, ): Subject { - val subjectId = UUID.randomUUID().toString() return buildSubject( subjectId = subjectId, projectId = projectId, 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 3135e8e3f5..35154ae213 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 @@ -251,6 +251,7 @@ class SubjectFactoryTest { ) val result = factory.buildSubjectFromCaptureResults( + subjectId = expected.subjectId, projectId = expected.projectId, attendantId = expected.attendantId, moduleId = expected.moduleId, diff --git a/settings.gradle.kts b/settings.gradle.kts index 8d15045037..a4fd410e7e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -114,6 +114,7 @@ include( ":feature:fetch-subject", ":feature:select-subject", ":feature:enrol-last-biometric", + ":feature:external-credential", ":feature:dashboard", ":feature:troubleshooting", ":feature:alert", From bc215dd731aba025c57eebfaccf6a900798b922c Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 11 Sep 2025 14:06:39 +0300 Subject: [PATCH 3/9] =?UTF-8?q?[MS-1127]=20Fixing=20room=20migrations,=20a?= =?UTF-8?q?dding=20ID=20field=CB=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../enrolment/records/room/store/migration/RoomMigrations.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/migration/RoomMigrations.kt b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/migration/RoomMigrations.kt index 106bbdc2a3..46aac888cc 100644 --- a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/migration/RoomMigrations.kt +++ b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/migration/RoomMigrations.kt @@ -14,6 +14,7 @@ val MIGRATION_1_2 = object : Migration(1, 2) { db.execSQL( """ CREATE TABLE IF NOT EXISTS `DbExternalCredential` ( + `id` TEXT NOT NULL, `value` TEXT NOT NULL, `subjectId` TEXT NOT NULL, `type` TEXT NOT NULL, From 724e6ac7c077f63b15d43c79b1044f57b018e1c5 Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 11 Sep 2025 14:18:39 +0300 Subject: [PATCH 4/9] [MS-1127] Adding version 2 for generated SubjectDatabase schema --- .../2.json | 230 ++++++++++++++++++ 1 file changed, 230 insertions(+) create mode 100644 infra/enrolment-records/room-store/schemas/com.simprints.infra.enrolment.records.room.store.SubjectsDatabase/2.json diff --git a/infra/enrolment-records/room-store/schemas/com.simprints.infra.enrolment.records.room.store.SubjectsDatabase/2.json b/infra/enrolment-records/room-store/schemas/com.simprints.infra.enrolment.records.room.store.SubjectsDatabase/2.json new file mode 100644 index 0000000000..c2b7bd5f10 --- /dev/null +++ b/infra/enrolment-records/room-store/schemas/com.simprints.infra.enrolment.records.room.store.SubjectsDatabase/2.json @@ -0,0 +1,230 @@ +{ + "formatVersion": 1, + "database": { + "version": 2, + "identityHash": "95e64b85f208336cee0a08fc5e94b2d6", + "entities": [ + { + "tableName": "DbSubject", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`subjectId` TEXT NOT NULL, `projectId` TEXT NOT NULL, `attendantId` TEXT NOT NULL, `moduleId` TEXT NOT NULL, `createdAt` INTEGER, `updatedAt` INTEGER, PRIMARY KEY(`subjectId`))", + "fields": [ + { + "fieldPath": "subjectId", + "columnName": "subjectId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "projectId", + "columnName": "projectId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "attendantId", + "columnName": "attendantId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "moduleId", + "columnName": "moduleId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "subjectId" + ] + }, + "indices": [ + { + "name": "index_DbSubject_projectId_subjectId", + "unique": false, + "columnNames": [ + "projectId", + "subjectId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DbSubject_projectId_subjectId` ON `${TABLE_NAME}` (`projectId`, `subjectId`)" + }, + { + "name": "index_DbSubject_projectId_moduleId_subjectId", + "unique": false, + "columnNames": [ + "projectId", + "moduleId", + "subjectId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DbSubject_projectId_moduleId_subjectId` ON `${TABLE_NAME}` (`projectId`, `moduleId`, `subjectId`)" + }, + { + "name": "index_DbSubject_projectId_attendantId_subjectId", + "unique": false, + "columnNames": [ + "projectId", + "attendantId", + "subjectId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DbSubject_projectId_attendantId_subjectId` ON `${TABLE_NAME}` (`projectId`, `attendantId`, `subjectId`)" + } + ] + }, + { + "tableName": "DbBiometricTemplate", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` TEXT NOT NULL, `subjectId` TEXT NOT NULL, `identifier` INTEGER, `templateData` BLOB NOT NULL, `format` TEXT NOT NULL, `referenceId` TEXT NOT NULL, `modality` INTEGER NOT NULL, PRIMARY KEY(`uuid`), FOREIGN KEY(`subjectId`) REFERENCES `DbSubject`(`subjectId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subjectId", + "columnName": "subjectId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "identifier", + "columnName": "identifier", + "affinity": "INTEGER" + }, + { + "fieldPath": "templateData", + "columnName": "templateData", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "format", + "columnName": "format", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "referenceId", + "columnName": "referenceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "modality", + "columnName": "modality", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uuid" + ] + }, + "indices": [ + { + "name": "index_DbBiometricTemplate_format_subjectId", + "unique": false, + "columnNames": [ + "format", + "subjectId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DbBiometricTemplate_format_subjectId` ON `${TABLE_NAME}` (`format`, `subjectId`)" + }, + { + "name": "index_DbBiometricTemplate_subjectId", + "unique": false, + "columnNames": [ + "subjectId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DbBiometricTemplate_subjectId` ON `${TABLE_NAME}` (`subjectId`)" + } + ], + "foreignKeys": [ + { + "table": "DbSubject", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "subjectId" + ], + "referencedColumns": [ + "subjectId" + ] + } + ] + }, + { + "tableName": "DbExternalCredential", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `value` TEXT NOT NULL, `subjectId` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`value`, `subjectId`), FOREIGN KEY(`subjectId`) REFERENCES `DbSubject`(`subjectId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subjectId", + "columnName": "subjectId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "value", + "subjectId" + ] + }, + "foreignKeys": [ + { + "table": "DbSubject", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "subjectId" + ], + "referencedColumns": [ + "subjectId" + ] + } + ] + } + ], + "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, '95e64b85f208336cee0a08fc5e94b2d6')" + ] + } +} \ No newline at end of file From e74f1c4eba469f8e3351251decc7f933ec9d5f63 Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 11 Sep 2025 14:27:55 +0300 Subject: [PATCH 5/9] [MS-1127] Adding index for Foreign Key check speedup --- .../records/room/store/models/DbExternalCredential.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/DbExternalCredential.kt b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/DbExternalCredential.kt index 33d36197ed..7175998ebb 100644 --- a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/DbExternalCredential.kt +++ b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/DbExternalCredential.kt @@ -3,6 +3,7 @@ package com.simprints.infra.enrolment.records.room.store.models import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.ForeignKey +import androidx.room.Index import com.simprints.infra.enrolment.records.room.store.models.DbExternalCredential.Companion.EXTERNAL_CREDENTIAL_TABLE_NAME import com.simprints.infra.enrolment.records.room.store.models.DbExternalCredential.Companion.EXTERNAL_CREDENTIAL_VALUE_COLUMN import com.simprints.infra.enrolment.records.room.store.models.DbSubject.Companion.SUBJECT_ID_COLUMN @@ -17,7 +18,8 @@ import com.simprints.infra.enrolment.records.room.store.models.DbSubject.Compani childColumns = [SUBJECT_ID_COLUMN], onDelete = ForeignKey.CASCADE, ) - ] + ], + indices = [Index(SUBJECT_ID_COLUMN)] ) data class DbExternalCredential( // The ID is only used by BFSID for analytics. The primary key should be a composite of value+subjectId From f2498ce290b71639f026a8658a7d5871d9227861 Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 11 Sep 2025 15:09:05 +0300 Subject: [PATCH 6/9] =?UTF-8?q?[MS-1127]=20Fixing=20migrations=CB=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../1.json | 48 +------------------ .../2.json | 15 +++++- .../room/store/migration/RoomMigrations.kt | 1 + 3 files changed, 16 insertions(+), 48 deletions(-) diff --git a/infra/enrolment-records/room-store/schemas/com.simprints.infra.enrolment.records.room.store.SubjectsDatabase/1.json b/infra/enrolment-records/room-store/schemas/com.simprints.infra.enrolment.records.room.store.SubjectsDatabase/1.json index 4df59295d1..58b0f3827c 100644 --- a/infra/enrolment-records/room-store/schemas/com.simprints.infra.enrolment.records.room.store.SubjectsDatabase/1.json +++ b/infra/enrolment-records/room-store/schemas/com.simprints.infra.enrolment.records.room.store.SubjectsDatabase/1.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "527fcc2c704906558681cb31beddb0c3", + "identityHash": "94bee827928a2618c6873579bc6bc63a", "entities": [ { "tableName": "DbSubject", @@ -170,55 +170,11 @@ ] } ] - }, - { - "tableName": "DbExternalCredential", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`value` TEXT NOT NULL, `subjectId` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`value`, `subjectId`), FOREIGN KEY(`subjectId`) REFERENCES `DbSubject`(`subjectId`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "value", - "columnName": "value", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "subjectId", - "columnName": "subjectId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "type", - "columnName": "type", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "value", - "subjectId" - ] - }, - "foreignKeys": [ - { - "table": "DbSubject", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "subjectId" - ], - "referencedColumns": [ - "subjectId" - ] - } - ] } ], "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, '527fcc2c704906558681cb31beddb0c3')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '94bee827928a2618c6873579bc6bc63a')" ] } } diff --git a/infra/enrolment-records/room-store/schemas/com.simprints.infra.enrolment.records.room.store.SubjectsDatabase/2.json b/infra/enrolment-records/room-store/schemas/com.simprints.infra.enrolment.records.room.store.SubjectsDatabase/2.json index c2b7bd5f10..6673ade6ff 100644 --- a/infra/enrolment-records/room-store/schemas/com.simprints.infra.enrolment.records.room.store.SubjectsDatabase/2.json +++ b/infra/enrolment-records/room-store/schemas/com.simprints.infra.enrolment.records.room.store.SubjectsDatabase/2.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 2, - "identityHash": "95e64b85f208336cee0a08fc5e94b2d6", + "identityHash": "8dad14b775cb4eea6cb7293fbcf84b30", "entities": [ { "tableName": "DbSubject", @@ -207,6 +207,17 @@ "subjectId" ] }, + "indices": [ + { + "name": "index_DbExternalCredential_subjectId", + "unique": false, + "columnNames": [ + "subjectId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DbExternalCredential_subjectId` ON `${TABLE_NAME}` (`subjectId`)" + } + ], "foreignKeys": [ { "table": "DbSubject", @@ -224,7 +235,7 @@ ], "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, '95e64b85f208336cee0a08fc5e94b2d6')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '8dad14b775cb4eea6cb7293fbcf84b30')" ] } } \ No newline at end of file diff --git a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/migration/RoomMigrations.kt b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/migration/RoomMigrations.kt index 46aac888cc..6278faf23b 100644 --- a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/migration/RoomMigrations.kt +++ b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/migration/RoomMigrations.kt @@ -23,6 +23,7 @@ val MIGRATION_1_2 = object : Migration(1, 2) { ) """.trimIndent() ) + db.execSQL("CREATE INDEX IF NOT EXISTS `index_DbExternalCredential_subjectId` ON `DbExternalCredential` (`subjectId`)") } } From a1e03a7b4add1a8d10e42a73b0c25f133ce01d43 Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 11 Sep 2025 15:20:48 +0300 Subject: [PATCH 7/9] [MS-1127] Fixing tests --- .../java/com/simprints/infra/config/store/testtools/Models.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/config-store/src/test/java/com/simprints/infra/config/store/testtools/Models.kt b/infra/config-store/src/test/java/com/simprints/infra/config/store/testtools/Models.kt index 14f9581d8b..3be90e32a5 100644 --- a/infra/config-store/src/test/java/com/simprints/infra/config/store/testtools/Models.kt +++ b/infra/config-store/src/test/java/com/simprints/infra/config/store/testtools/Models.kt @@ -469,7 +469,7 @@ internal val protoSynchronizationConfiguration = ProtoSynchronizationConfigurati .build(), ).build() -internal val apiAllowedExternalCredential = ApiExternalCredentialType.NHISCard +internal val apiAllowedExternalCredential = ApiExternalCredentialType.NHIS_CARD internal val apiMultiFactorIdConfiguration = ApiMultiFactorIdConfiguration( allowedExternalCredentials = listOf(apiAllowedExternalCredential) From 88f44314ad703de9c16cf9c56a7067c274eef276 Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 11 Sep 2025 15:49:38 +0300 Subject: [PATCH 8/9] =?UTF-8?q?[MS-1127]=20Renaming=20subject=20migrations?= =?UTF-8?q?=20to=20explicit=20class=CB=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../records/room/store/migration/Migration1to2Test.kt | 2 +- .../infra/enrolment/records/room/store/SubjectsDatabase.kt | 4 ++-- .../migration/{RoomMigrations.kt => SubjectMigration1to2.kt} | 4 +++- 3 files changed, 6 insertions(+), 4 deletions(-) rename infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/migration/{RoomMigrations.kt => SubjectMigration1to2.kt} (81%) diff --git a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/room/store/migration/Migration1to2Test.kt b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/room/store/migration/Migration1to2Test.kt index 5cda6f65fe..278023f419 100644 --- a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/room/store/migration/Migration1to2Test.kt +++ b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/room/store/migration/Migration1to2Test.kt @@ -25,7 +25,7 @@ class Migration1to2Test { name = TEST_DB, version = 2, validateDroppedTables = true, - MIGRATION_1_2 + SubjectMigration1to2() ) // Verify external credentials table exists diff --git a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/SubjectsDatabase.kt b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/SubjectsDatabase.kt index 422c0dc33c..157af2c0ab 100644 --- a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/SubjectsDatabase.kt +++ b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/SubjectsDatabase.kt @@ -6,7 +6,7 @@ import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase import com.simprints.infra.enrolment.records.room.store.SubjectsDatabase.Companion.SUBJECT_DB_VERSION -import com.simprints.infra.enrolment.records.room.store.migration.MIGRATION_1_2 +import com.simprints.infra.enrolment.records.room.store.migration.SubjectMigration1to2 import com.simprints.infra.enrolment.records.room.store.models.DbBiometricTemplate import com.simprints.infra.enrolment.records.room.store.models.DbExternalCredential import com.simprints.infra.enrolment.records.room.store.models.DbSubject @@ -35,7 +35,7 @@ abstract class SubjectsDatabase : RoomDatabase() { ): SubjectsDatabase { val builder = Room .databaseBuilder(context, SubjectsDatabase::class.java, dbName) - .addMigrations(MIGRATION_1_2) + .addMigrations(SubjectMigration1to2()) if (BuildConfig.DB_ENCRYPTION) { builder.openHelperFactory(factory) } diff --git a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/migration/RoomMigrations.kt b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/migration/SubjectMigration1to2.kt similarity index 81% rename from infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/migration/RoomMigrations.kt rename to infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/migration/SubjectMigration1to2.kt index 6278faf23b..c7a250110b 100644 --- a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/migration/RoomMigrations.kt +++ b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/migration/SubjectMigration1to2.kt @@ -2,6 +2,7 @@ package com.simprints.infra.enrolment.records.room.store.migration import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase +import com.simprints.core.ExcludedFromGeneratedTestCoverageReports /** * Schema version 1 -> 2 @@ -9,7 +10,8 @@ import androidx.sqlite.db.SupportSQLiteDatabase * Changes: * - Adding [DbExternalCredential] entity * */ -val MIGRATION_1_2 = object : Migration(1, 2) { +@ExcludedFromGeneratedTestCoverageReports("Covered indirectly in the migration tests") +class SubjectMigration1to2 : Migration(1, 2) { override fun migrate(db: SupportSQLiteDatabase) { db.execSQL( """ From c8106eefac09577bd95530e029351f00b0521ee2 Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 11 Sep 2025 17:55:02 +0300 Subject: [PATCH 9/9] [MS-1127] Adding test coverage for BuildStepsUseCaseTest.kt --- .../usecases/steps/BuildStepsUseCaseTest.kt | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) 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 6328e20227..f74d4cee84 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 @@ -1,5 +1,6 @@ package com.simprints.feature.orchestrator.usecases.steps +import com.simprints.core.domain.externalcredential.ExternalCredentialType import com.simprints.feature.orchestrator.cache.OrchestratorCache import com.simprints.feature.orchestrator.exceptions.SubjectAgeNotSupportedException import com.simprints.feature.orchestrator.steps.Step @@ -756,4 +757,95 @@ class BuildStepsUseCaseTest { assertEquals(0, steps.size) } + + @Test + fun `build external credential not enabled - no external credential step`() = runTest { + val projectConfiguration = mockCommonProjectConfiguration() + every { projectConfiguration.multifactorId?.allowedExternalCredentials } returns emptyList() + + val action = mockk(relaxed = true) + every { action.getSubjectAgeIfAvailable() } returns null + + val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId) + + // Should not contain EXTERNAL_CREDENTIAL step + assertStepOrder( + steps, + StepId.SETUP, + StepId.CONSENT, + StepId.FINGERPRINT_CAPTURE, + StepId.FINGERPRINT_CAPTURE, + StepId.FACE_CAPTURE, + ) + } + + @Test + fun `build enrol action - external credential enabled - returns external credential step`() = runTest { + val projectConfiguration = mockCommonProjectConfiguration() + every { projectConfiguration.multifactorId?.allowedExternalCredentials } returns ExternalCredentialType.entries + + val action = mockk(relaxed = true) + every { action.getSubjectAgeIfAvailable() } returns null + + val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId) + + assertStepOrder( + steps, + StepId.SETUP, + StepId.CONSENT, + StepId.FINGERPRINT_CAPTURE, + StepId.FINGERPRINT_CAPTURE, + StepId.FACE_CAPTURE, + StepId.EXTERNAL_CREDENTIAL, + ) + } + + @Test + fun `build identify action - external credential enabled - returns external credential step`() = runTest { + val projectConfiguration = mockCommonProjectConfiguration() + every { projectConfiguration.multifactorId?.allowedExternalCredentials } returns ExternalCredentialType.entries + + val action = mockk(relaxed = true) + every { action.getSubjectAgeIfAvailable() } returns null + + val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId) + + assertStepOrder( + steps, + StepId.SETUP, + StepId.CONSENT, + StepId.FINGERPRINT_CAPTURE, + StepId.FINGERPRINT_CAPTURE, + StepId.FACE_CAPTURE, + StepId.EXTERNAL_CREDENTIAL, + StepId.FINGERPRINT_MATCHER, + StepId.FINGERPRINT_MATCHER, + StepId.FACE_MATCHER, + ) + } + + @Test + fun `build verify action - external credential enabled - no external credential step`() = runTest { + val projectConfiguration = mockCommonProjectConfiguration() + every { projectConfiguration.multifactorId?.allowedExternalCredentials } returns ExternalCredentialType.entries + + val action = mockk(relaxed = true) + every { action.getSubjectAgeIfAvailable() } returns null + + val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId) + + // Should not contain EXTERNAL_CREDENTIAL step for VERIFY flow + assertStepOrder( + steps, + StepId.SETUP, + StepId.FETCH_GUID, + StepId.CONSENT, + StepId.FINGERPRINT_CAPTURE, + StepId.FINGERPRINT_CAPTURE, + StepId.FACE_CAPTURE, + StepId.FINGERPRINT_MATCHER, + StepId.FINGERPRINT_MATCHER, + StepId.FACE_MATCHER, + ) + } }