diff --git a/feature/client-api/src/main/java/com/simprints/feature/clientapi/usecases/GetEnrolmentCreationEventForSubjectUseCase.kt b/feature/client-api/src/main/java/com/simprints/feature/clientapi/usecases/GetEnrolmentCreationEventForSubjectUseCase.kt index 766cabd0d3..f0dc8fad66 100644 --- a/feature/client-api/src/main/java/com/simprints/feature/clientapi/usecases/GetEnrolmentCreationEventForSubjectUseCase.kt +++ b/feature/client-api/src/main/java/com/simprints/feature/clientapi/usecases/GetEnrolmentCreationEventForSubjectUseCase.kt @@ -54,7 +54,7 @@ internal class GetEnrolmentCreationEventForSubjectUseCase @Inject constructor( projectId = projectId, moduleId = moduleId, attendantId = attendantId, - biometricReferences = EnrolmentRecordCreationEvent.buildBiometricReferences(fingerprintSamples, faceSamples, encoder), + biometricReferences = EnrolmentRecordCreationEvent.buildBiometricReferences(samples, encoder), externalCredentials = externalCredentials, ) diff --git a/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/screen/usecase/BuildSubjectUseCase.kt b/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/screen/usecase/BuildSubjectUseCase.kt index da9cbc03d8..2fc46b034f 100644 --- a/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/screen/usecase/BuildSubjectUseCase.kt +++ b/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/screen/usecase/BuildSubjectUseCase.kt @@ -1,6 +1,5 @@ package com.simprints.feature.enrollast.screen.usecase -import com.simprints.core.domain.common.Modality import com.simprints.core.domain.sample.CaptureSample import com.simprints.core.domain.sample.Sample import com.simprints.core.tools.time.TimeHelper @@ -31,7 +30,6 @@ internal class BuildSubjectUseCase @Inject constructor( val captureResult = params.steps .filterIsInstance() .flatMap { result -> result.results.map { toSample(result.referenceId, it) } } - .groupBy { it.modality } return subjectFactory.buildSubject( subjectId = subjectId, @@ -39,8 +37,7 @@ internal class BuildSubjectUseCase @Inject constructor( attendantId = params.userId, moduleId = params.moduleId, createdAt = Date(timeHelper.now().ms), - fingerprintSamples = captureResult[Modality.FINGERPRINT].orEmpty(), - faceSamples = captureResult[Modality.FACE].orEmpty(), + samples = captureResult, externalCredentials = externalCredentials, ) } diff --git a/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/screen/usecase/CheckForDuplicateEnrolmentsUseCase.kt b/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/screen/usecase/CheckForDuplicateEnrolmentsUseCase.kt index 818b85987d..119e66be3f 100644 --- a/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/screen/usecase/CheckForDuplicateEnrolmentsUseCase.kt +++ b/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/screen/usecase/CheckForDuplicateEnrolmentsUseCase.kt @@ -2,9 +2,8 @@ package com.simprints.feature.enrollast.screen.usecase import com.simprints.feature.enrollast.EnrolLastBiometricStepResult import com.simprints.feature.enrollast.screen.EnrolLastState -import com.simprints.infra.config.store.models.FaceConfiguration -import com.simprints.infra.config.store.models.FingerprintConfiguration import com.simprints.infra.config.store.models.ProjectConfiguration +import com.simprints.infra.config.store.models.getModalitySdkConfig import com.simprints.infra.logging.LoggingConstants.CrashReportTag.ENROLMENT import com.simprints.infra.logging.Simber import javax.inject.Inject @@ -15,12 +14,10 @@ internal class CheckForDuplicateEnrolmentsUseCase @Inject constructor() { steps: List, ): EnrolLastState.ErrorType? { if (!projectConfig.general.duplicateBiometricEnrolmentCheck) return null - - val fingerprintResponse = getFingerprintMatchResult(steps) - val faceResponse = getFaceMatchResult(steps) + val matchResults = getFingerprintMatchResult(steps) return when { - fingerprintResponse == null && faceResponse == null -> { + matchResults.isEmpty() -> { Simber.e( "No match response. Must be either fingerprint, face or both", MissingMatchResultException(), @@ -29,7 +26,7 @@ internal class CheckForDuplicateEnrolmentsUseCase @Inject constructor() { EnrolLastState.ErrorType.NO_MATCH_RESULTS } - isAnyResponseWithHighConfidence(projectConfig, fingerprintResponse, faceResponse) -> { + isAnyResponseWithHighConfidence(projectConfig, matchResults) -> { Simber.i("There is a subject with confidence score above the high confidence level", tag = ENROLMENT) EnrolLastState.ErrorType.DUPLICATE_ENROLMENTS } @@ -40,35 +37,18 @@ internal class CheckForDuplicateEnrolmentsUseCase @Inject constructor() { private fun getFingerprintMatchResult(steps: List) = steps .filterIsInstance() - .lastOrNull { it.sdk is FingerprintConfiguration.BioSdk } - - private fun getFaceMatchResult(steps: List) = steps - .filterIsInstance() - .lastOrNull { it.sdk is FaceConfiguration.BioSdk } private fun isAnyResponseWithHighConfidence( configuration: ProjectConfiguration, - fingerprintResponse: EnrolLastBiometricStepResult.MatchResult?, - faceResponse: EnrolLastBiometricStepResult.MatchResult?, - ): Boolean { - val fingerprintThreshold = fingerprintResponse?.let { - configuration.fingerprint - ?.getSdkConfiguration(fingerprintResponse.sdk) - ?.decisionPolicy - ?.high - ?.toFloat() - } ?: Float.MAX_VALUE - - val faceThreshold = faceResponse?.let { - configuration.face - ?.getSdkConfiguration(faceResponse.sdk) - ?.decisionPolicy - ?.high - ?.toFloat() - } ?: Float.MAX_VALUE - - return fingerprintResponse?.results?.any { it.confidence >= fingerprintThreshold } == true || - faceResponse?.results?.any { it.confidence >= faceThreshold } == true + responses: List, + ): Boolean = responses.any { result -> + val threshold = configuration + .getModalitySdkConfig(result.sdk) + ?.decisionPolicy + ?.high + ?.toFloat() ?: Float.MAX_VALUE + + result.results.any { it.confidence >= threshold } } private class MissingMatchResultException : IllegalStateException("No match response in duplicate check.") diff --git a/feature/enrol-last-biometric/src/test/java/com/simprints/feature/enrollast/screen/usecase/BuildSubjectUseCaseTest.kt b/feature/enrol-last-biometric/src/test/java/com/simprints/feature/enrollast/screen/usecase/BuildSubjectUseCaseTest.kt index 5f624dadef..91f1ce72da 100644 --- a/feature/enrol-last-biometric/src/test/java/com/simprints/feature/enrollast/screen/usecase/BuildSubjectUseCaseTest.kt +++ b/feature/enrol-last-biometric/src/test/java/com/simprints/feature/enrollast/screen/usecase/BuildSubjectUseCaseTest.kt @@ -45,8 +45,7 @@ class BuildSubjectUseCaseTest { fun `has no samples if no steps provided`() { val result = useCase(createParams(steps = emptyList(), scannedCredential = scannedCredential), isAddingCredential = false) - assertThat(result.fingerprintSamples).isEmpty() - assertThat(result.faceSamples).isEmpty() + assertThat(result.samples).isEmpty() } @Test @@ -63,8 +62,7 @@ class BuildSubjectUseCaseTest { isAddingCredential = false, ) - assertThat(result.fingerprintSamples).isEmpty() - assertThat(result.faceSamples).isEmpty() + assertThat(result.samples).isEmpty() } @Test @@ -87,8 +85,8 @@ class BuildSubjectUseCaseTest { isAddingCredential = false, ) - assertThat(result.fingerprintSamples).isNotEmpty() - assertThat(result.fingerprintSamples.first().identifier).isEqualTo(SampleIdentifier.RIGHT_THUMB) + assertThat(result.samples).isNotEmpty() + assertThat(result.samples.first().identifier).isEqualTo(SampleIdentifier.RIGHT_THUMB) } @Test @@ -117,8 +115,8 @@ class BuildSubjectUseCaseTest { isAddingCredential = false, ) - assertThat(result.fingerprintSamples).isNotEmpty() - assertThat(result.fingerprintSamples.size).isEqualTo(10) + assertThat(result.samples).isNotEmpty() + assertThat(result.samples.size).isEqualTo(10) } @Test @@ -135,8 +133,8 @@ class BuildSubjectUseCaseTest { isAddingCredential = false, ) - assertThat(result.faceSamples).isNotEmpty() - assertThat(result.faceSamples.first().format).isEqualTo("first") + assertThat(result.samples).isNotEmpty() + assertThat(result.samples.first().format).isEqualTo("first") } @Test diff --git a/feature/enrol-last-biometric/src/test/java/com/simprints/feature/enrollast/screen/usecase/CheckDuplicateEnrolmentsErrorsUseCaseTest.kt b/feature/enrol-last-biometric/src/test/java/com/simprints/feature/enrollast/screen/usecase/CheckDuplicateEnrolmentsErrorsUseCaseTest.kt index 8f49fb9676..2aa18828c5 100644 --- a/feature/enrol-last-biometric/src/test/java/com/simprints/feature/enrollast/screen/usecase/CheckDuplicateEnrolmentsErrorsUseCaseTest.kt +++ b/feature/enrol-last-biometric/src/test/java/com/simprints/feature/enrollast/screen/usecase/CheckDuplicateEnrolmentsErrorsUseCaseTest.kt @@ -136,10 +136,12 @@ class CheckDuplicateEnrolmentsErrorsUseCaseTest { ): ProjectConfiguration = mockk(relaxed = true) { every { general.duplicateBiometricEnrolmentCheck } returns checkEnabled // cannot mock Int? directly due to Java inter-op issues, so mocking decision policy instead - every { fingerprint?.getSdkConfiguration(any())?.decisionPolicy } returns highConfidence?.let { + every { fingerprint?.secugenSimMatcher?.decisionPolicy } returns highConfidence?.let { + DecisionPolicy(0, 0, it) + } + every { face?.rankOne?.decisionPolicy } returns highConfidence?.let { DecisionPolicy(0, 0, it) } - every { face?.getSdkConfiguration(any())?.decisionPolicy } returns highConfidence?.let { DecisionPolicy(0, 0, it) } } private fun matchResult(confidence: Float) = MatchComparisonResult("subjectId", confidence) 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 index f153415cf6..d32383efb6 100644 --- 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 @@ -1,10 +1,11 @@ package com.simprints.feature.externalcredential import com.simprints.core.ExcludedFromGeneratedTestCoverageReports +import com.simprints.core.domain.common.AgeGroup import com.simprints.core.domain.common.FlowType +import com.simprints.core.domain.common.Modality import com.simprints.core.domain.sample.CaptureSample import com.simprints.feature.externalcredential.model.ExternalCredentialParams -import com.simprints.infra.config.store.models.AgeGroup @ExcludedFromGeneratedTestCoverageReports("Navigation class") object ExternalCredentialContract { @@ -15,14 +16,12 @@ object ExternalCredentialContract { flowType: FlowType, ageGroup: AgeGroup?, probeReferenceId: String? = null, - faceSamples: List = emptyList(), - fingerprintSamples: List = emptyList(), + samples: Map> = emptyMap(), ) = ExternalCredentialParams( subjectId = subjectId, flowType = flowType, ageGroup = ageGroup, probeReferenceId = probeReferenceId, - faceSamples = faceSamples, - fingerprintSamples = fingerprintSamples, + samples = samples, ) } diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/model/CredentialMatch.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/model/CredentialMatch.kt index 7fadc7c165..1580892ff4 100644 --- a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/model/CredentialMatch.kt +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/model/CredentialMatch.kt @@ -2,11 +2,10 @@ package com.simprints.feature.externalcredential.model import androidx.annotation.Keep import com.simprints.core.ExcludedFromGeneratedTestCoverageReports +import com.simprints.core.domain.common.ModalitySdkType import com.simprints.core.domain.sample.MatchComparisonResult import com.simprints.core.domain.step.StepResult import com.simprints.core.domain.tokenization.TokenizableString -import com.simprints.infra.config.store.models.FaceConfiguration -import com.simprints.infra.config.store.models.FingerprintConfiguration @Keep @ExcludedFromGeneratedTestCoverageReports("Data class") @@ -14,8 +13,7 @@ data class CredentialMatch( val credential: TokenizableString.Tokenized, val matchResult: MatchComparisonResult, val verificationThreshold: Float, - val faceBioSdk: FaceConfiguration.BioSdk?, - val fingerprintBioSdk: FingerprintConfiguration.BioSdk?, + val bioSdk: ModalitySdkType, ) : StepResult { val isVerificationSuccessful = matchResult.confidence >= verificationThreshold } 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 index 461fc5f77c..85db7e24c1 100644 --- 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 @@ -2,10 +2,11 @@ package com.simprints.feature.externalcredential.model import androidx.annotation.Keep import com.simprints.core.ExcludedFromGeneratedTestCoverageReports +import com.simprints.core.domain.common.AgeGroup import com.simprints.core.domain.common.FlowType +import com.simprints.core.domain.common.Modality import com.simprints.core.domain.sample.CaptureSample import com.simprints.core.domain.step.StepParams -import com.simprints.infra.config.store.models.AgeGroup @Keep @ExcludedFromGeneratedTestCoverageReports("Data class") @@ -14,6 +15,5 @@ data class ExternalCredentialParams( val flowType: FlowType, val ageGroup: AgeGroup?, val probeReferenceId: String?, - val faceSamples: List, - val fingerprintSamples: List, + val samples: Map>, ) : StepParams diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/usecase/CreateMatchParamsUseCase.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/usecase/CreateMatchParamsUseCase.kt index b7ee13f92b..13413bde66 100644 --- a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/usecase/CreateMatchParamsUseCase.kt +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/usecase/CreateMatchParamsUseCase.kt @@ -1,12 +1,11 @@ package com.simprints.feature.externalcredential.screens.search.usecase +import com.simprints.core.domain.common.AgeGroup import com.simprints.core.domain.common.FlowType import com.simprints.core.domain.common.Modality import com.simprints.core.domain.sample.CaptureSample -import com.simprints.infra.config.store.models.AgeGroup import com.simprints.infra.config.store.models.ProjectConfiguration -import com.simprints.infra.config.store.models.determineFaceSDKs -import com.simprints.infra.config.store.models.determineFingerprintSDKs +import com.simprints.infra.config.store.models.getSdkListForAgeGroup import com.simprints.infra.enrolment.records.repository.domain.models.BiometricDataSource import com.simprints.infra.enrolment.records.repository.domain.models.SubjectQuery import com.simprints.infra.matching.MatchParams @@ -18,39 +17,20 @@ internal class CreateMatchParamsUseCase @Inject constructor() { flowType: FlowType, probeReferenceId: String?, projectConfiguration: ProjectConfiguration, - faceSamples: List, - fingerprintSamples: List, + samples: Map>, ageGroup: AgeGroup?, ): List = projectConfiguration.general.matchingModalities .map { modality -> - when (modality) { - Modality.FACE -> - projectConfiguration - .determineFaceSDKs(ageGroup) - .map { - MatchParams( - probeReferenceId = probeReferenceId.orEmpty(), - flowType = flowType, - queryForCandidates = SubjectQuery(subjectId = candidateSubjectId), - bioSdk = it, - probeFaceSamples = faceSamples, - biometricDataSource = BiometricDataSource.Simprints, // [MS-1167] No CoSync in initial MF-ID implementation - ) - } - - Modality.FINGERPRINT -> - projectConfiguration - .determineFingerprintSDKs(ageGroup) - .map { - MatchParams( - probeReferenceId = probeReferenceId.orEmpty(), - flowType = flowType, - queryForCandidates = SubjectQuery(subjectId = candidateSubjectId), - bioSdk = it, - probeFingerprintSamples = fingerprintSamples, - biometricDataSource = BiometricDataSource.Simprints, // [MS-1167] No CoSync in initial MF-ID implementation - ) - } + val modalityProbes = samples[modality].orEmpty() + projectConfiguration.getSdkListForAgeGroup(modality, ageGroup).map { + MatchParams( + probeReferenceId = probeReferenceId.orEmpty(), + flowType = flowType, + queryForCandidates = SubjectQuery(subjectId = candidateSubjectId), + bioSdk = it, + probeSamples = modalityProbes, + biometricDataSource = BiometricDataSource.Simprints, // [MS-1167] No CoSync in initial MF-ID implementation + ) } }.flatten() } diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/usecase/MatchCandidatesUseCase.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/usecase/MatchCandidatesUseCase.kt index b8e74dec63..6aba463c74 100644 --- a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/usecase/MatchCandidatesUseCase.kt +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/usecase/MatchCandidatesUseCase.kt @@ -7,11 +7,12 @@ import com.simprints.infra.config.store.models.FaceConfiguration import com.simprints.infra.config.store.models.FingerprintConfiguration import com.simprints.infra.config.store.models.Project import com.simprints.infra.config.store.models.ProjectConfiguration +import com.simprints.infra.config.store.models.getModalitySdkConfig import com.simprints.infra.enrolment.records.repository.domain.models.Subject import com.simprints.infra.matching.usecase.FaceMatcherUseCase import com.simprints.infra.matching.usecase.FingerprintMatcherUseCase import com.simprints.infra.matching.usecase.MatcherUseCase.MatcherState -import kotlinx.coroutines.flow.last +import kotlinx.coroutines.flow.lastOrNull import javax.inject.Inject internal class MatchCandidatesUseCase @Inject constructor( @@ -31,48 +32,27 @@ internal class MatchCandidatesUseCase @Inject constructor( flowType = externalCredentialParams.flowType, probeReferenceId = externalCredentialParams.probeReferenceId, projectConfiguration = projectConfig, - faceSamples = externalCredentialParams.faceSamples, - fingerprintSamples = externalCredentialParams.fingerprintSamples, + samples = externalCredentialParams.samples, ageGroup = externalCredentialParams.ageGroup, ) matchParams - .mapNotNull { matchParams -> - when { - matchParams.probeFaceSamples.isNotEmpty() -> { - val faceSdk = matchParams.bioSdk - projectConfig.face?.getSdkConfiguration(faceSdk)?.verificationMatchThreshold?.let { matchThreshold -> - (faceMatcher(matchParams, project).last() as? MatcherState.Success) - ?.comparisonResults - .orEmpty() - .map { result -> - CredentialMatch( - credential = credential, - matchResult = result, - verificationThreshold = matchThreshold, - faceBioSdk = faceSdk as FaceConfiguration.BioSdk, - fingerprintBioSdk = null, - ) - } - } - } - - else -> { - val fingerprintSdk = matchParams.bioSdk - projectConfig.fingerprint?.getSdkConfiguration(fingerprintSdk)?.verificationMatchThreshold?.let { matchThreshold -> - (fingerprintMatcher(matchParams, project).last() as? MatcherState.Success) - ?.comparisonResults - .orEmpty() - .map { result -> - CredentialMatch( - credential = credential, - matchResult = result, - verificationThreshold = matchThreshold, - faceBioSdk = null, - fingerprintBioSdk = fingerprintSdk as FingerprintConfiguration.BioSdk, - ) - } - } - } + .mapNotNull { matchParam -> + val matchThreshold = projectConfig + .getModalitySdkConfig(matchParam.bioSdk) + ?.verificationMatchThreshold + ?: return@mapNotNull null + val lastMatchSuccess = when (matchParam.bioSdk) { + is FaceConfiguration.BioSdk -> faceMatcher(matchParam, project).lastOrNull() as? MatcherState.Success + is FingerprintConfiguration.BioSdk -> fingerprintMatcher(matchParam, project).lastOrNull() as? MatcherState.Success + else -> null + } + lastMatchSuccess?.comparisonResults?.map { result -> + CredentialMatch( + credential = credential, + matchResult = result, + verificationThreshold = matchThreshold, + bioSdk = matchParam.bioSdk, + ) } }.flatten() } diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/usecase/ResetExternalCredentialsInSessionUseCase.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/usecase/ResetExternalCredentialsInSessionUseCase.kt index 1c90b0a55f..23c96223b2 100644 --- a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/usecase/ResetExternalCredentialsInSessionUseCase.kt +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/usecase/ResetExternalCredentialsInSessionUseCase.kt @@ -32,8 +32,7 @@ class ResetExternalCredentialsInSessionUseCase @Inject() constructor( val credentialsToRemove = enrolmentUpdateEvents.map { SubjectAction.Update( subjectId = it.payload.subjectId, - faceSamplesToAdd = emptyList(), - fingerprintSamplesToAdd = emptyList(), + samplesToAdd = emptyList(), referenceIdsToRemove = emptyList(), externalCredentialsToAdd = emptyList(), externalCredentialIdsToRemove = it.payload.externalCredentialIdsToAdd, @@ -45,8 +44,7 @@ class ResetExternalCredentialsInSessionUseCase @Inject() constructor( listOf( SubjectAction.Update( subjectId = subjectId, - faceSamplesToAdd = emptyList(), - fingerprintSamplesToAdd = emptyList(), + samplesToAdd = emptyList(), referenceIdsToRemove = emptyList(), externalCredentialsToAdd = listOf(scannedCredential.toExternalCredential(validSubjectId)), externalCredentialIdsToRemove = emptyList(), diff --git a/feature/external-credential/src/test/java/com/simprints/feature/externalcredential/screens/controller/ExternalCredentialViewModelTest.kt b/feature/external-credential/src/test/java/com/simprints/feature/externalcredential/screens/controller/ExternalCredentialViewModelTest.kt index 669d457236..05ef90863b 100644 --- a/feature/external-credential/src/test/java/com/simprints/feature/externalcredential/screens/controller/ExternalCredentialViewModelTest.kt +++ b/feature/external-credential/src/test/java/com/simprints/feature/externalcredential/screens/controller/ExternalCredentialViewModelTest.kt @@ -228,7 +228,6 @@ internal class ExternalCredentialViewModelTest { flowType = flowType, ageGroup = null, probeReferenceId = null, - faceSamples = emptyList(), - fingerprintSamples = emptyList(), + samples = emptyMap(), ) } diff --git a/feature/external-credential/src/test/java/com/simprints/feature/externalcredential/screens/search/usecase/CreateMatchParamsUseCaseTest.kt b/feature/external-credential/src/test/java/com/simprints/feature/externalcredential/screens/search/usecase/CreateMatchParamsUseCaseTest.kt index da65fb27a8..26b051d547 100644 --- a/feature/external-credential/src/test/java/com/simprints/feature/externalcredential/screens/search/usecase/CreateMatchParamsUseCaseTest.kt +++ b/feature/external-credential/src/test/java/com/simprints/feature/externalcredential/screens/search/usecase/CreateMatchParamsUseCaseTest.kt @@ -1,16 +1,15 @@ package com.simprints.feature.externalcredential.screens.search.usecase import com.google.common.truth.Truth.* +import com.simprints.core.domain.common.AgeGroup import com.simprints.core.domain.common.FlowType import com.simprints.core.domain.common.Modality import com.simprints.core.domain.sample.CaptureSample -import com.simprints.infra.config.store.models.AgeGroup import com.simprints.infra.config.store.models.FaceConfiguration import com.simprints.infra.config.store.models.FingerprintConfiguration import com.simprints.infra.config.store.models.GeneralConfiguration import com.simprints.infra.config.store.models.ProjectConfiguration -import com.simprints.infra.config.store.models.determineFaceSDKs -import com.simprints.infra.config.store.models.determineFingerprintSDKs +import com.simprints.infra.config.store.models.getSdkListForAgeGroup import com.simprints.infra.enrolment.records.repository.domain.models.BiometricDataSource import io.mockk.* import io.mockk.impl.annotations.MockK @@ -46,11 +45,10 @@ internal class CreateMatchParamsUseCaseTest { @Test fun `creates correct MatchParams for face modality`() { - every { projectConfiguration.determineFaceSDKs(ageGroup) } returns listOf( + every { projectConfiguration.getSdkListForAgeGroup(Modality.FACE, ageGroup) } returns listOf( FaceConfiguration.BioSdk.RANK_ONE, FaceConfiguration.BioSdk.SIM_FACE, ) - every { projectConfiguration.determineFingerprintSDKs(ageGroup) } returns emptyList() every { generalConfiguration.matchingModalities } returns listOf(Modality.FACE) val result = useCase( @@ -58,8 +56,7 @@ internal class CreateMatchParamsUseCaseTest { flowType = flowType, probeReferenceId = probeReferenceId, projectConfiguration = projectConfiguration, - faceSamples = listOf(faceSample), - fingerprintSamples = emptyList(), + samples = mapOf(Modality.FACE to listOf(faceSample)), ageGroup = ageGroup, ) @@ -69,7 +66,7 @@ internal class CreateMatchParamsUseCaseTest { assertThat(matchParams.flowType).isEqualTo(flowType) assertThat(matchParams.queryForCandidates.subjectId).isEqualTo(subjectId) assertThat(matchParams.biometricDataSource).isEqualTo(BiometricDataSource.Simprints) - assertThat(matchParams.probeFaceSamples).containsExactly(faceSample) + assertThat(matchParams.probeSamples).containsExactly(faceSample) assertThat(matchParams.bioSdk).isNotNull() } assertThat(result[0].bioSdk).isEqualTo(FaceConfiguration.BioSdk.RANK_ONE) @@ -78,8 +75,7 @@ internal class CreateMatchParamsUseCaseTest { @Test fun `creates correct MatchParams for fingerprint modality`() { - every { projectConfiguration.determineFaceSDKs(ageGroup) } returns emptyList() - every { projectConfiguration.determineFingerprintSDKs(ageGroup) } returns listOf( + every { projectConfiguration.getSdkListForAgeGroup(Modality.FINGERPRINT, ageGroup) } returns listOf( FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER, FingerprintConfiguration.BioSdk.NEC, ) @@ -90,8 +86,7 @@ internal class CreateMatchParamsUseCaseTest { flowType = flowType, probeReferenceId = probeReferenceId, projectConfiguration = projectConfiguration, - faceSamples = emptyList(), - fingerprintSamples = listOf(fingerprintSample), + samples = mapOf(Modality.FINGERPRINT to listOf(fingerprintSample)), ageGroup = ageGroup, ) @@ -101,7 +96,7 @@ internal class CreateMatchParamsUseCaseTest { assertThat(matchParams.flowType).isEqualTo(flowType) assertThat(matchParams.queryForCandidates.subjectId).isEqualTo(subjectId) assertThat(matchParams.biometricDataSource).isEqualTo(BiometricDataSource.Simprints) - assertThat(matchParams.probeFingerprintSamples).containsExactly(fingerprintSample) + assertThat(matchParams.probeSamples).containsExactly(fingerprintSample) assertThat(matchParams.bioSdk).isNotNull() } assertThat(result[0].bioSdk).isEqualTo(FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER) @@ -114,10 +109,10 @@ internal class CreateMatchParamsUseCaseTest { Modality.FACE, Modality.FINGERPRINT, ) - every { projectConfiguration.determineFaceSDKs(ageGroup) } returns listOf( + every { projectConfiguration.getSdkListForAgeGroup(Modality.FACE, ageGroup) } returns listOf( FaceConfiguration.BioSdk.RANK_ONE, ) - every { projectConfiguration.determineFingerprintSDKs(ageGroup) } returns listOf( + every { projectConfiguration.getSdkListForAgeGroup(Modality.FINGERPRINT, ageGroup) } returns listOf( FingerprintConfiguration.BioSdk.NEC, ) @@ -126,8 +121,10 @@ internal class CreateMatchParamsUseCaseTest { flowType = flowType, probeReferenceId = probeReferenceId, projectConfiguration = projectConfiguration, - faceSamples = listOf(faceSample), - fingerprintSamples = listOf(fingerprintSample), + samples = mapOf( + Modality.FINGERPRINT to listOf(fingerprintSample), + Modality.FACE to listOf(faceSample), + ), ageGroup = ageGroup, ) @@ -136,20 +133,19 @@ internal class CreateMatchParamsUseCaseTest { val faceMatch = result.find { it.bioSdk is FaceConfiguration.BioSdk } assertThat(faceMatch).isNotNull() assertThat(faceMatch?.bioSdk).isEqualTo(FaceConfiguration.BioSdk.RANK_ONE) - assertThat(faceMatch?.probeFaceSamples).containsExactly(faceSample) + assertThat(faceMatch?.probeSamples).containsExactly(faceSample) val fingerprintMatch = result.find { it.bioSdk is FingerprintConfiguration.BioSdk } assertThat(fingerprintMatch).isNotNull() assertThat(fingerprintMatch?.bioSdk).isEqualTo(FingerprintConfiguration.BioSdk.NEC) - assertThat(fingerprintMatch?.probeFingerprintSamples).containsExactly(fingerprintSample) + assertThat(fingerprintMatch?.probeSamples).containsExactly(fingerprintSample) } @Test fun `handles null ageGroup`() { - every { projectConfiguration.determineFaceSDKs(null) } returns listOf( + every { projectConfiguration.getSdkListForAgeGroup(Modality.FACE, null) } returns listOf( FaceConfiguration.BioSdk.RANK_ONE, ) - every { projectConfiguration.determineFingerprintSDKs(null) } returns emptyList() every { generalConfiguration.matchingModalities } returns listOf(Modality.FACE) val result = useCase( @@ -157,8 +153,7 @@ internal class CreateMatchParamsUseCaseTest { flowType = flowType, probeReferenceId = probeReferenceId, projectConfiguration = projectConfiguration, - faceSamples = listOf(faceSample), - fingerprintSamples = emptyList(), + samples = mapOf(Modality.FACE to listOf(faceSample)), ageGroup = null, ) @@ -167,16 +162,17 @@ internal class CreateMatchParamsUseCaseTest { @Test fun `returns empty list when no SDKs available`() { - every { projectConfiguration.determineFaceSDKs(ageGroup) } returns emptyList() - every { projectConfiguration.determineFingerprintSDKs(ageGroup) } returns emptyList() + every { projectConfiguration.getSdkListForAgeGroup(any(), ageGroup) } returns emptyList() val result = useCase( candidateSubjectId = subjectId, flowType = flowType, probeReferenceId = probeReferenceId, projectConfiguration = projectConfiguration, - faceSamples = listOf(faceSample), - fingerprintSamples = listOf(fingerprintSample), + samples = mapOf( + Modality.FINGERPRINT to listOf(fingerprintSample), + Modality.FACE to listOf(faceSample), + ), ageGroup = ageGroup, ) @@ -185,11 +181,10 @@ internal class CreateMatchParamsUseCaseTest { @Test fun `creates multiple MatchParams for multiple face SDKs`() { - every { projectConfiguration.determineFaceSDKs(ageGroup) } returns listOf( + every { projectConfiguration.getSdkListForAgeGroup(Modality.FACE, ageGroup) } returns listOf( FaceConfiguration.BioSdk.RANK_ONE, FaceConfiguration.BioSdk.SIM_FACE, ) - every { projectConfiguration.determineFingerprintSDKs(ageGroup) } returns emptyList() every { generalConfiguration.matchingModalities } returns listOf(Modality.FACE) val faceSamples = listOf(faceSample, mockk(relaxed = true)) @@ -199,14 +194,15 @@ internal class CreateMatchParamsUseCaseTest { flowType = flowType, probeReferenceId = probeReferenceId, projectConfiguration = projectConfiguration, - faceSamples = faceSamples, - fingerprintSamples = emptyList(), + samples = mapOf( + Modality.FACE to faceSamples, + ), ageGroup = ageGroup, ) assertThat(result).hasSize(2) result.forEach { matchParams -> - assertThat(matchParams.probeFaceSamples).hasSize(2) + assertThat(matchParams.probeSamples).hasSize(2) } } } diff --git a/feature/external-credential/src/test/java/com/simprints/feature/externalcredential/screens/search/usecase/MatchCandidatesUseCaseTest.kt b/feature/external-credential/src/test/java/com/simprints/feature/externalcredential/screens/search/usecase/MatchCandidatesUseCaseTest.kt index 1069e4826d..68b9f80e6b 100644 --- a/feature/external-credential/src/test/java/com/simprints/feature/externalcredential/screens/search/usecase/MatchCandidatesUseCaseTest.kt +++ b/feature/external-credential/src/test/java/com/simprints/feature/externalcredential/screens/search/usecase/MatchCandidatesUseCaseTest.kt @@ -1,16 +1,18 @@ package com.simprints.feature.externalcredential.screens.search.usecase import com.google.common.truth.Truth.* +import com.simprints.core.domain.common.AgeGroup import com.simprints.core.domain.common.FlowType +import com.simprints.core.domain.common.Modality import com.simprints.core.domain.sample.CaptureSample import com.simprints.core.domain.sample.MatchComparisonResult import com.simprints.core.domain.tokenization.asTokenizableEncrypted import com.simprints.feature.externalcredential.model.ExternalCredentialParams -import com.simprints.infra.config.store.models.AgeGroup import com.simprints.infra.config.store.models.FaceConfiguration import com.simprints.infra.config.store.models.FingerprintConfiguration import com.simprints.infra.config.store.models.Project import com.simprints.infra.config.store.models.ProjectConfiguration +import com.simprints.infra.config.store.models.getModalitySdkConfig import com.simprints.infra.enrolment.records.repository.domain.models.Subject import com.simprints.infra.matching.MatchParams import com.simprints.infra.matching.usecase.FaceMatcherUseCase @@ -94,8 +96,10 @@ internal class MatchCandidatesUseCaseTest { every { subject.subjectId } returns subjectId every { externalCredentialParams.probeReferenceId } returns probeReferenceId every { externalCredentialParams.flowType } returns FlowType.VERIFY - every { externalCredentialParams.faceSamples } returns listOf(faceSample) - every { externalCredentialParams.fingerprintSamples } returns listOf(fingerprintSample) + every { externalCredentialParams.samples } returns mapOf( + Modality.FACE to listOf(faceSample), + Modality.FINGERPRINT to listOf(fingerprintSample), + ) every { externalCredentialParams.ageGroup } returns ageGroup coEvery { @@ -104,16 +108,15 @@ internal class MatchCandidatesUseCaseTest { flowType = any(), probeReferenceId = any(), projectConfiguration = any(), - faceSamples = any(), - fingerprintSamples = any(), + samples = any(), ageGroup = any(), ) } returns listOf(matchParams) every { projectConfig.face } returns faceConfig every { projectConfig.fingerprint } returns fingerprintConfig - every { faceConfig.getSdkConfiguration(FaceConfiguration.BioSdk.RANK_ONE) } returns faceSdkConfig + every { projectConfig.getModalitySdkConfig(FaceConfiguration.BioSdk.RANK_ONE) } returns faceSdkConfig every { faceSdkConfig.verificationMatchThreshold } returns verificationMatchThreshold - every { fingerprintConfig.getSdkConfiguration(FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER) } returns fingerprintSdkConfig + every { projectConfig.getModalitySdkConfig(FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER) } returns fingerprintSdkConfig every { fingerprintSdkConfig.verificationMatchThreshold } returns verificationMatchThreshold every { matcherSuccess.comparisonResults } returns listOf(matchResultItem) coEvery { faceMatcher(matchParams, project) } returns flowOf(matcherSuccess) @@ -122,12 +125,10 @@ internal class MatchCandidatesUseCaseTest { private fun initMatchParams(isFace: Boolean) { if (isFace) { - every { matchParams.probeFingerprintSamples } returns emptyList() - every { matchParams.probeFaceSamples } returns listOf(faceSample) + every { matchParams.probeSamples } returns listOf(faceSample) every { matchParams.bioSdk } returns FaceConfiguration.BioSdk.RANK_ONE } else { - every { matchParams.probeFingerprintSamples } returns listOf(fingerprintSample) - every { matchParams.probeFaceSamples } returns emptyList() + every { matchParams.probeSamples } returns listOf(fingerprintSample) every { matchParams.bioSdk } returns FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER } } @@ -147,8 +148,7 @@ internal class MatchCandidatesUseCaseTest { assertThat(result[0].credential).isEqualTo(credential) assertThat(result[0].matchResult).isEqualTo(matchResultItem) assertThat(result[0].verificationThreshold).isEqualTo(verificationMatchThreshold) - assertThat(result[0].faceBioSdk).isEqualTo(FaceConfiguration.BioSdk.RANK_ONE) - assertThat(result[0].fingerprintBioSdk).isNull() + assertThat(result[0].bioSdk).isEqualTo(FaceConfiguration.BioSdk.RANK_ONE) } @Test @@ -166,8 +166,7 @@ internal class MatchCandidatesUseCaseTest { assertThat(result[0].credential).isEqualTo(credential) assertThat(result[0].matchResult).isEqualTo(matchResultItem) assertThat(result[0].verificationThreshold).isEqualTo(verificationMatchThreshold) - assertThat(result[0].faceBioSdk).isNull() - assertThat(result[0].fingerprintBioSdk).isEqualTo(FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER) + assertThat(result[0].bioSdk).isEqualTo(FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER) } @Test @@ -186,7 +185,7 @@ internal class MatchCandidatesUseCaseTest { @Test fun `returns empty list when face SDK configuration is null`() = runTest { initMatchParams(isFace = true) - every { faceConfig.getSdkConfiguration(FaceConfiguration.BioSdk.RANK_ONE) } returns null + every { projectConfig.getModalitySdkConfig(FaceConfiguration.BioSdk.RANK_ONE) } returns null val result = useCase.invoke( candidates = listOf(subject), @@ -202,7 +201,7 @@ internal class MatchCandidatesUseCaseTest { @Test fun `returns empty list when fingerprint SDK configuration is null`() = runTest { initMatchParams(isFace = false) - every { fingerprintConfig.getSdkConfiguration(FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER) } returns null + every { projectConfig.getModalitySdkConfig(FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER) } returns null val result = useCase.invoke( candidates = listOf(subject), diff --git a/feature/external-credential/src/test/java/com/simprints/feature/externalcredential/usecase/ResetExternalCredentialsInSessionUseCaseTest.kt b/feature/external-credential/src/test/java/com/simprints/feature/externalcredential/usecase/ResetExternalCredentialsInSessionUseCaseTest.kt index f3abb776c6..a29a37d6f3 100644 --- a/feature/external-credential/src/test/java/com/simprints/feature/externalcredential/usecase/ResetExternalCredentialsInSessionUseCaseTest.kt +++ b/feature/external-credential/src/test/java/com/simprints/feature/externalcredential/usecase/ResetExternalCredentialsInSessionUseCaseTest.kt @@ -76,8 +76,7 @@ internal class ResetExternalCredentialsInSessionUseCaseTest { val updateAction = actions.first() as SubjectAction.Update assertThat(updateAction.subjectId).isEqualTo(SUBJECT_ID) assertThat(updateAction.externalCredentialsToAdd).hasSize(1) - assertThat(updateAction.faceSamplesToAdd).isEmpty() - assertThat(updateAction.fingerprintSamplesToAdd).isEmpty() + assertThat(updateAction.samplesToAdd).isEmpty() assertThat(updateAction.referenceIdsToRemove).isEmpty() } diff --git a/feature/matcher/src/main/java/com/simprints/matcher/MatchContract.kt b/feature/matcher/src/main/java/com/simprints/matcher/MatchContract.kt index 9606843f82..8ded5aeb1b 100644 --- a/feature/matcher/src/main/java/com/simprints/matcher/MatchContract.kt +++ b/feature/matcher/src/main/java/com/simprints/matcher/MatchContract.kt @@ -12,8 +12,7 @@ object MatchContract { fun getParams( referenceId: String = "", - fingerprintSamples: List = emptyList(), - faceSamples: List = emptyList(), + probeSamples: List = emptyList(), bioSdk: ModalitySdkType, flowType: FlowType, subjectQuery: SubjectQuery, @@ -21,8 +20,7 @@ object MatchContract { ) = MatchParams( probeReferenceId = referenceId, bioSdk = bioSdk, - probeFaceSamples = faceSamples, - probeFingerprintSamples = fingerprintSamples, + probeSamples = probeSamples, flowType = flowType, queryForCandidates = subjectQuery, biometricDataSource = biometricDataSource, diff --git a/feature/matcher/src/main/java/com/simprints/matcher/screen/MatchFragment.kt b/feature/matcher/src/main/java/com/simprints/matcher/screen/MatchFragment.kt index 641769454c..edfe03c2d7 100644 --- a/feature/matcher/src/main/java/com/simprints/matcher/screen/MatchFragment.kt +++ b/feature/matcher/src/main/java/com/simprints/matcher/screen/MatchFragment.kt @@ -61,7 +61,7 @@ internal class MatchFragment : Fragment(R.layout.fragment_matcher) { ) { super.onViewCreated(view, savedInstanceState) applySystemBarInsets(view) - Simber.i("MatchFragment started (isFace=${params.isFaceMatch()})", tag = ORCHESTRATION) + Simber.i("MatchFragment started (sdk=${params.bioSdk})", tag = ORCHESTRATION) findNavController().handleResult( this, diff --git a/feature/matcher/src/main/java/com/simprints/matcher/screen/MatchViewModel.kt b/feature/matcher/src/main/java/com/simprints/matcher/screen/MatchViewModel.kt index b90b151d8d..1c2e989d84 100644 --- a/feature/matcher/src/main/java/com/simprints/matcher/screen/MatchViewModel.kt +++ b/feature/matcher/src/main/java/com/simprints/matcher/screen/MatchViewModel.kt @@ -11,7 +11,7 @@ import com.simprints.core.tools.time.TimeHelper import com.simprints.infra.authstore.AuthStore import com.simprints.infra.config.store.models.DecisionPolicy import com.simprints.infra.config.store.models.FaceConfiguration -import com.simprints.infra.config.store.models.FingerprintConfiguration +import com.simprints.infra.config.store.models.getModalitySdkConfig import com.simprints.infra.config.sync.ConfigManager import com.simprints.infra.matching.MatchParams import com.simprints.infra.matching.MatchResult @@ -55,9 +55,8 @@ internal class MatchViewModel @Inject constructor( isInitialized = true val startTime = timeHelper.now() - val isFaceMatch = params.isFaceMatch() - val matcherUseCase = when { - isFaceMatch -> faceMatcher + val matcherUseCase = when (params.bioSdk) { + is FaceConfiguration.BioSdk -> faceMatcher else -> fingerprintMatcher } val project = configManager.getProject(authStore.signedInProjectId) @@ -101,15 +100,11 @@ internal class MatchViewModel @Inject constructor( } } - private suspend fun getDecisionPolicy(params: MatchParams): DecisionPolicy { - val config = configManager.getProjectConfiguration() - val policy = when (params.bioSdk) { - is FaceConfiguration.BioSdk -> config.face?.getSdkConfiguration(params.bioSdk)?.decisionPolicy - is FingerprintConfiguration.BioSdk -> config.fingerprint?.getSdkConfiguration(params.bioSdk)?.decisionPolicy - else -> null - } - return policy ?: fallbackDecisionPolicy() - } + private suspend fun getDecisionPolicy(params: MatchParams): DecisionPolicy = configManager + .getProjectConfiguration() + .getModalitySdkConfig(params.bioSdk) + ?.decisionPolicy + ?: fallbackDecisionPolicy() private fun setMatchState( candidatesMatched: Int, diff --git a/feature/matcher/src/test/java/com/simprints/matcher/screen/MatchViewModelTest.kt b/feature/matcher/src/test/java/com/simprints/matcher/screen/MatchViewModelTest.kt index 0d7304ab2c..fffb306ae2 100644 --- a/feature/matcher/src/test/java/com/simprints/matcher/screen/MatchViewModelTest.kt +++ b/feature/matcher/src/test/java/com/simprints/matcher/screen/MatchViewModelTest.kt @@ -110,7 +110,7 @@ internal class MatchViewModelTest { viewModel.setupMatch( MatchParams( probeReferenceId = "referenceId", - probeFaceSamples = listOf(getFaceSample()), + probeSamples = listOf(getFaceSample()), bioSdk = FaceConfiguration.BioSdk.RANK_ONE, flowType = FlowType.ENROL, queryForCandidates = mockk {}, @@ -134,7 +134,7 @@ internal class MatchViewModelTest { configManager .getProjectConfiguration() .face - ?.getSdkConfiguration(any()) + ?.rankOne ?.decisionPolicy } returns DecisionPolicy(20, 35, 50) @@ -182,7 +182,7 @@ internal class MatchViewModelTest { viewModel.setupMatch( MatchParams( probeReferenceId = "referenceId", - probeFaceSamples = listOf(getFaceSample()), + probeSamples = listOf(getFaceSample()), bioSdk = FaceConfiguration.BioSdk.RANK_ONE, flowType = FlowType.ENROL, queryForCandidates = mockk {}, @@ -223,7 +223,7 @@ internal class MatchViewModelTest { configManager .getProjectConfiguration() .fingerprint - ?.getSdkConfiguration(any()) + ?.secugenSimMatcher ?.decisionPolicy } returns DecisionPolicy(200, 350, 500) @@ -273,7 +273,7 @@ internal class MatchViewModelTest { viewModel.setupMatch( MatchParams( probeReferenceId = "referenceId", - probeFingerprintSamples = listOf(getFingerprintSample()), + probeSamples = listOf(getFingerprintSample()), bioSdk = SECUGEN_SIM_MATCHER, flowType = FlowType.ENROL, queryForCandidates = mockk {}, @@ -314,7 +314,7 @@ internal class MatchViewModelTest { configManager .getProjectConfiguration() .face - ?.getSdkConfiguration(any()) + ?.rankOne ?.decisionPolicy } returns null @@ -338,7 +338,7 @@ internal class MatchViewModelTest { viewModel.setupMatch( MatchParams( probeReferenceId = "referenceId", - probeFaceSamples = listOf(getFaceSample()), + probeSamples = listOf(getFaceSample()), bioSdk = FaceConfiguration.BioSdk.RANK_ONE, flowType = FlowType.ENROL, queryForCandidates = mockk {}, @@ -377,7 +377,7 @@ internal class MatchViewModelTest { val states = viewModel.matchState.test() val matchParams = MatchParams( probeReferenceId = "referenceId", - probeFaceSamples = listOf(getFaceSample()), + probeSamples = listOf(getFaceSample()), bioSdk = FaceConfiguration.BioSdk.RANK_ONE, flowType = FlowType.ENROL, queryForCandidates = mockk {}, 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 cf9380d8e7..ccc0fc1f77 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 @@ -315,14 +315,14 @@ internal class OrchestratorViewModel @Inject constructor( currentStep.id == StepId.FACE_CAPTURE && result is CaptureIdentity -> { params.copy( probeReferenceId = result.referenceId, - faceSamples = result.samples, + samples = params.samples + (Modality.FACE to result.samples), ) } currentStep.id == StepId.FINGERPRINT_CAPTURE && result is CaptureIdentity -> { params.copy( probeReferenceId = result.referenceId, - fingerprintSamples = result.samples, + samples = params.samples + (Modality.FINGERPRINT to result.samples), ) } diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/cache/OrchestratorCache.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/cache/OrchestratorCache.kt index c88c9e02b1..f8c92b30f5 100644 --- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/cache/OrchestratorCache.kt +++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/cache/OrchestratorCache.kt @@ -3,6 +3,7 @@ package com.simprints.feature.orchestrator.cache import androidx.core.content.edit import com.fasterxml.jackson.core.type.TypeReference import com.fasterxml.jackson.databind.module.SimpleModule +import com.simprints.core.domain.common.AgeGroup import com.simprints.core.domain.step.StepParams import com.simprints.core.domain.step.StepResult import com.simprints.core.domain.tokenization.TokenizableString @@ -12,7 +13,6 @@ import com.simprints.core.tools.json.JsonHelper import com.simprints.feature.orchestrator.steps.Step import com.simprints.feature.orchestrator.steps.StepParamsMixin import com.simprints.feature.orchestrator.steps.StepResultMixin -import com.simprints.infra.config.store.models.AgeGroup import com.simprints.infra.security.SecurityManager import javax.inject.Inject import javax.inject.Singleton diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/steps/MatchStepStubPayload.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/steps/MatchStepStubPayload.kt index dd98bd3d24..2698aa32f3 100644 --- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/steps/MatchStepStubPayload.kt +++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/steps/MatchStepStubPayload.kt @@ -26,7 +26,7 @@ internal data class MatchStepStubPayload( samples: List, ) = MatchContract.getParams( referenceId = referenceId, - faceSamples = samples, + probeSamples = samples, bioSdk = bioSdk, flowType = flowType, subjectQuery = subjectQuery, @@ -38,7 +38,7 @@ internal data class MatchStepStubPayload( samples: List, ) = MatchContract.getParams( referenceId = referenceId, - fingerprintSamples = samples, + probeSamples = samples, bioSdk = bioSdk, flowType = flowType, subjectQuery = subjectQuery, diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/steps/Step.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/steps/Step.kt index 696f6e20cb..3051833d3e 100644 --- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/steps/Step.kt +++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/steps/Step.kt @@ -4,6 +4,7 @@ import androidx.annotation.IdRes import androidx.annotation.Keep import com.fasterxml.jackson.annotation.JsonSubTypes import com.fasterxml.jackson.annotation.JsonTypeInfo +import com.simprints.core.domain.common.AgeGroup import com.simprints.core.domain.sample.CaptureIdentity import com.simprints.core.domain.sample.CaptureSample import com.simprints.core.domain.sample.MatchComparisonResult @@ -18,6 +19,7 @@ import com.simprints.feature.enrollast.EnrolLastBiometricResult import com.simprints.feature.enrollast.EnrolLastBiometricStepResult import com.simprints.feature.exitform.ExitFormResult import com.simprints.feature.externalcredential.ExternalCredentialSearchResult +import com.simprints.feature.externalcredential.model.CredentialMatch import com.simprints.feature.externalcredential.model.ExternalCredentialParams import com.simprints.feature.fetchsubject.FetchSubjectParams import com.simprints.feature.fetchsubject.FetchSubjectResult @@ -55,6 +57,7 @@ import java.io.Serializable JsonSubTypes.Type(value = ValidateSubjectPoolResult::class, name = "ValidateSubjectPoolResult"), JsonSubTypes.Type(value = SelectSubjectAgeGroupResult::class, name = "SelectSubjectAgeGroupResult"), JsonSubTypes.Type(value = ExternalCredentialSearchResult::class, name = "ExternalCredentialSearchResult"), + JsonSubTypes.Type(value = CredentialMatch::class, name = " CredentialMatch"), // Common data types JsonSubTypes.Type(value = CaptureIdentity::class, name = "CaptureIdentity"), JsonSubTypes.Type(value = CaptureSample::class, name = "CaptureSample"), @@ -91,13 +94,14 @@ abstract class StepResultMixin : StepResult name = "EnrolLastBiometricStepResult.CaptureResult", ), JsonSubTypes.Type(value = ExternalCredentialParams::class, name = "ExternalCredentialParams"), - JsonSubTypes.Type(value = ExternalCredentialSearchResult::class, name = "ExternalCredentialSearchResult"), - JsonSubTypes.Type(value = MatchResult::class, name = "MatchResult"), // Additional types that are used in top-level params JsonSubTypes.Type(value = CaptureSample::class, name = "CaptureSample"), JsonSubTypes.Type(value = MatchComparisonResult::class, name = "MatchComparisonResult"), JsonSubTypes.Type(value = BiometricDataSource::class, name = "BiometricDataSource"), + JsonSubTypes.Type(value = BiometricDataSource.CommCare::class, name = "BiometricDataSource.CommCare"), + JsonSubTypes.Type(value = BiometricDataSource.Simprints::class, name = "BiometricDataSource.Simprints"), JsonSubTypes.Type(value = SubjectQuery::class, name = "SubjectQuery"), + JsonSubTypes.Type(value = AgeGroup::class, name = "AgeGroup"), JsonSubTypes.Type(value = FingerprintConfiguration.BioSdk::class, name = "FingerprintConfiguration.BioSdk"), JsonSubTypes.Type(value = FaceConfiguration.BioSdk::class, name = "FaceConfiguration.BioSdk"), ) diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateConfirmIdentityResponseUseCase.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateConfirmIdentityResponseUseCase.kt index de055b99a1..8328c40ee9 100644 --- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateConfirmIdentityResponseUseCase.kt +++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateConfirmIdentityResponseUseCase.kt @@ -10,7 +10,7 @@ import javax.inject.Inject internal class CreateConfirmIdentityResponseUseCase @Inject constructor() { operator fun invoke(results: List): AppResponse = results - .filterIsInstance(SelectSubjectResult::class.java) + .filterIsInstance() .lastOrNull() ?.let { AppConfirmationResponse(true, externalCredential = it.savedCredential) } ?: AppErrorResponse(AppErrorReason.UNEXPECTED_ERROR) diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateEnrolLastBiometricResponseUseCase.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateEnrolLastBiometricResponseUseCase.kt index 5b1d5602a3..b6b6412383 100644 --- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateEnrolLastBiometricResponseUseCase.kt +++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateEnrolLastBiometricResponseUseCase.kt @@ -9,7 +9,7 @@ import javax.inject.Inject internal class CreateEnrolLastBiometricResponseUseCase @Inject constructor() { operator fun invoke(results: List) = results - .filterIsInstance(EnrolLastBiometricResult::class.java) + .filterIsInstance() .lastOrNull() ?.let { result -> result.newSubjectId?.let { guid -> 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 3a74335b54..7ec8ae7237 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 @@ -1,6 +1,5 @@ package com.simprints.feature.orchestrator.usecases.response -import com.simprints.core.domain.common.Modality import com.simprints.core.domain.response.AppErrorReason import com.simprints.core.domain.sample.CaptureIdentity import com.simprints.feature.externalcredential.ExternalCredentialSearchResult @@ -26,8 +25,6 @@ internal class CreateEnrolResponseUseCase @Inject constructor( project: Project, enrolmentSubjectId: String, ): AppResponse { - val fingerprintCapture = results.filterIsInstance().lastOrNull { it.modality == Modality.FINGERPRINT } - val faceCapture = results.filterIsInstance().lastOrNull { it.modality == Modality.FACE } val credentialResult = results.filterIsInstance().lastOrNull() val externalCredential = credentialResult?.scannedCredential?.toExternalCredential(enrolmentSubjectId) @@ -37,8 +34,7 @@ internal class CreateEnrolResponseUseCase @Inject constructor( projectId = request.projectId, attendantId = request.userId, moduleId = request.moduleId, - fingerprintResponse = fingerprintCapture, - faceResponse = faceCapture, + captures = results.filterIsInstance(), externalCredential = externalCredential, ) enrolSubject(subject, project) diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateIdentifyResponseUseCase.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateIdentifyResponseUseCase.kt index 2b99a2ed93..6170bdf803 100644 --- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateIdentifyResponseUseCase.kt +++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateIdentifyResponseUseCase.kt @@ -1,11 +1,10 @@ package com.simprints.feature.orchestrator.usecases.response -import com.simprints.core.domain.sample.MatchComparisonResult +import com.simprints.core.domain.common.ModalitySdkType +import com.simprints.core.domain.response.AppMatchConfidence import com.simprints.feature.externalcredential.ExternalCredentialSearchResult -import com.simprints.infra.config.store.models.DecisionPolicy -import com.simprints.infra.config.store.models.FaceConfiguration -import com.simprints.infra.config.store.models.FingerprintConfiguration import com.simprints.infra.config.store.models.ProjectConfiguration +import com.simprints.infra.config.store.models.getModalitySdkConfig import com.simprints.infra.events.session.SessionEventRepository import com.simprints.infra.matching.MatchResult import com.simprints.infra.orchestration.data.responses.AppIdentifyResponse @@ -22,154 +21,79 @@ internal class CreateIdentifyResponseUseCase @Inject constructor( results: List, ): AppResponse { val isMultiFactorIdEnabled = projectConfiguration.multifactorId?.allowedExternalCredentials?.isNotEmpty() ?: false - val credentialFaceMatchResults = credentialResultsMapper(results, projectConfiguration, isFace = true) - val credentialFingerprintMatchResults = credentialResultsMapper(results, projectConfiguration, isFace = false) val currentSessionId = eventRepository.getCurrentSessionScope().id - - val faceResults = credentialFaceMatchResults + getFaceMatchResults(results, projectConfiguration) - val bestFaceConfidence = faceResults.firstOrNull()?.confidenceScore ?: 0 - - val fingerprintResults = credentialFingerprintMatchResults + getFingerprintResults(results, projectConfiguration) - val bestFingerprintConfidence = fingerprintResults.firstOrNull()?.confidenceScore ?: 0 - return AppIdentifyResponse( sessionId = currentSessionId, isMultiFactorIdEnabled = isMultiFactorIdEnabled, // Return the results with the highest confidence score - identifications = if (bestFingerprintConfidence > bestFaceConfidence) { - fingerprintResults.distinctBy(AppMatchResult::guid) - } else { - faceResults.distinctBy(AppMatchResult::guid) - }, + identifications = getResults(results, projectConfiguration), ) } - private fun getFingerprintResults( + /** + * Combines all of the matching results per SDK and returns up to [maxNbOfReturnedCandidates] results from the SDK with + * the highest overall score in descending order. Credential matches take precedence over direct matches. + * + * If there are any matches of [AppMatchConfidence.HIGH], only those will be returned, + * otherwise everything above [AppMatchConfidence.NONE] is returned. + */ + private fun getResults( results: List, projectConfiguration: ProjectConfiguration, - ) = results - .filterIsInstance() - .lastOrNull { it.sdk is FingerprintConfiguration.BioSdk } - ?.let { fingerprintMatchResult -> - projectConfiguration.fingerprint - ?.getSdkConfiguration(fingerprintMatchResult.sdk) - ?.decisionPolicy - ?.let { fingerprintDecisionPolicy -> - val matches = fingerprintMatchResult.results - val goodResults = matches - .filter { it.confidence >= fingerprintDecisionPolicy.low } - .sortedByDescending { it.confidence } - // Attempt to include only high confidence matches - goodResults - .filter { it.confidence >= fingerprintDecisionPolicy.high } - .ifEmpty { goodResults } - .take(projectConfiguration.identification.maxNbOfReturnedCandidates) - .map { - AppMatchResult( - guid = it.subjectId, - confidenceScore = it.confidence, - decisionPolicy = fingerprintDecisionPolicy, - isCredentialMatch = false, - ) - } - } - } ?: emptyList() + ): List { + val credentialResultsDescending = mapCredentialSearchResultsPerSdk(results, projectConfiguration) + val matchResultResultsDescending = mapMatchResultsPerSdk(results, projectConfiguration) + + return (credentialResultsDescending.keys + matchResultResultsDescending.keys) + .associateWith { credentialResultsDescending[it].orEmpty() + matchResultResultsDescending[it].orEmpty() } + .filterValues { it.isNotEmpty() } + .maxByOrNull { (_, values) -> values.maxOfOrNull { it.confidenceScore } ?: 0 } + ?.let { (_, results) -> + val goodResults = results.filter { it.matchConfidence != AppMatchConfidence.NONE } + // Attempt to include only high confidence matches + goodResults + .filter { it.matchConfidence == AppMatchConfidence.HIGH } + .ifEmpty { goodResults } + .take(projectConfiguration.identification.maxNbOfReturnedCandidates) + .distinctBy(AppMatchResult::guid) + }.orEmpty() + } - private fun getFaceMatchResults( + private fun mapCredentialSearchResultsPerSdk( results: List, projectConfiguration: ProjectConfiguration, - ) = results - .filterIsInstance() - .lastOrNull { it.sdk is FaceConfiguration.BioSdk } - ?.let { faceMatchResult -> - projectConfiguration.face - ?.getSdkConfiguration(faceMatchResult.sdk) - ?.decisionPolicy - ?.let { faceDecisionPolicy -> - val matches = faceMatchResult.results - val goodResults = matches - .filter { it.confidence >= faceDecisionPolicy.low } - .sortedByDescending { it.confidence } - // Attempt to include only high confidence matches - goodResults - .filter { it.confidence >= faceDecisionPolicy.high } - .ifEmpty { goodResults } - .take(projectConfiguration.identification.maxNbOfReturnedCandidates) - .map { - AppMatchResult( - guid = it.subjectId, - confidenceScore = it.confidence, - decisionPolicy = faceDecisionPolicy, - isCredentialMatch = false, - ) - } - } - } ?: emptyList() + ): Map> = results + .filterIsInstance() + // Mapping the result to the common final type and pairing it with the sdk for later grouping + .flatMap { credentialSearchResult -> + credentialSearchResult.matchResults.mapNotNull { credentialMatchResult -> + val sdk = credentialMatchResult.bioSdk + val policy = projectConfiguration.getModalitySdkConfig(sdk)?.decisionPolicy ?: return@mapNotNull null + val matchResult = credentialMatchResult.matchResult - private fun List.mapToMatchResults( - decisionPolicy: DecisionPolicy, - verificationMatchThreshold: Float?, - projectConfiguration: ProjectConfiguration, - isCredentialMatch: Boolean, - ): List { - val goodResults = this - .filter { it.confidence >= decisionPolicy.low } - .sortedByDescending { it.confidence } - // Attempt to include only high confidence matches - return goodResults - .filter { it.confidence >= decisionPolicy.high } - .ifEmpty { goodResults } - .take(projectConfiguration.identification.maxNbOfReturnedCandidates) - .map { - AppMatchResult( - guid = it.subjectId, - confidenceScore = it.confidence, - decisionPolicy = decisionPolicy, - isCredentialMatch = isCredentialMatch, - verificationMatchThreshold = verificationMatchThreshold, - ) + sdk to AppMatchResult(matchResult.subjectId, matchResult.confidence, policy, true) } - } + }.groupDescendingResultsBySdk() - /** - * Checks if any of the [results] items is an instance of [ExternalCredentialSearchResult]. If such item is found, then returns a list - * of candidates whose verification matching score between the taken biometric probe, and a biometric probe linked to is above - * project's verification threshold. - * - * @return list of [AppMatchResult] containing possible verification matches - */ - private fun credentialResultsMapper( + private fun mapMatchResultsPerSdk( results: List, projectConfiguration: ProjectConfiguration, - isFace: Boolean, - ) = results - .filterIsInstance() - .firstOrNull() - ?.let { credentialSearchResult -> - val credentialMatchItems = credentialSearchResult.matchResults.map { it.matchResult } - val faceMatchItems = credentialSearchResult.matchResults.filter { it.faceBioSdk != null }.map { it.matchResult } - val fingerMatchItems = credentialSearchResult.matchResults.filter { it.fingerprintBioSdk != null }.map { it.matchResult } - val (decisionPolicy, verificationMatchThreshold) = if (isFace) { - credentialSearchResult.matchResults.find { it.faceBioSdk != null }?.faceBioSdk?.let { sdk -> - val config = projectConfiguration.face?.getSdkConfiguration(sdk) - config?.decisionPolicy to config?.verificationMatchThreshold - } - } else { - credentialSearchResult.matchResults.find { it.fingerprintBioSdk != null }?.fingerprintBioSdk?.let { sdk -> - val config = projectConfiguration.fingerprint?.getSdkConfiguration(sdk) - config?.decisionPolicy to config?.verificationMatchThreshold - } - } ?: (null to null) + ): Map> = results + .filterIsInstance() + .flatMap { matchResult -> + val policy = projectConfiguration + .getModalitySdkConfig(matchResult.sdk) + ?.decisionPolicy + ?: return@flatMap emptyList() + + matchResult.results.map { + matchResult.sdk to AppMatchResult(it.subjectId, it.confidence, policy, false) + } + }.groupDescendingResultsBySdk() - if (decisionPolicy == null) return@let emptyList() - val matches = if (isFace) faceMatchItems else fingerMatchItems - return@let matches - .mapToMatchResults( - decisionPolicy = decisionPolicy, - projectConfiguration = projectConfiguration, - isCredentialMatch = true, - verificationMatchThreshold = verificationMatchThreshold, - ).sortedByDescending(AppMatchResult::confidenceScore) - }.orEmpty() + private fun List>.groupDescendingResultsBySdk() = groupBy( + { (sdk, _) -> sdk }, + { (_, resultsPerSdk) -> resultsPerSdk }, + ).mapValues { (_, results) -> results.sortedByDescending { it.confidenceScore } } } diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateVerifyResponseUseCase.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateVerifyResponseUseCase.kt index 4999a30667..ba6bc1ea25 100644 --- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateVerifyResponseUseCase.kt +++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateVerifyResponseUseCase.kt @@ -1,9 +1,8 @@ package com.simprints.feature.orchestrator.usecases.response import com.simprints.core.domain.response.AppErrorReason -import com.simprints.infra.config.store.models.FaceConfiguration -import com.simprints.infra.config.store.models.FingerprintConfiguration import com.simprints.infra.config.store.models.ProjectConfiguration +import com.simprints.infra.config.store.models.getModalitySdkConfig import com.simprints.infra.logging.LoggingConstants.CrashReportTag.FINGER_MATCHING import com.simprints.infra.logging.Simber import com.simprints.infra.matching.MatchResult @@ -18,61 +17,30 @@ internal class CreateVerifyResponseUseCase @Inject constructor() { operator fun invoke( projectConfiguration: ProjectConfiguration, results: List, - ): AppResponse = listOfNotNull( - getFingerprintMatchResults(projectConfiguration, results), - getFaceMatchResults(projectConfiguration, results), - ).maxByOrNull { it.confidenceScore } + ): AppResponse = getMatchResults(projectConfiguration, results) + .maxByOrNull { it.confidenceScore } ?.let { AppVerifyResponse(it) } ?: AppErrorResponse(AppErrorReason.UNEXPECTED_ERROR).also { // if subject enrolled with an SDK and the user tries to verify with another SDK Simber.i("No match results found", tag = FINGER_MATCHING) } - private fun getFingerprintMatchResults( + private fun getMatchResults( projectConfiguration: ProjectConfiguration, results: List, ) = results .filterIsInstance() - .lastOrNull { it.sdk is FingerprintConfiguration.BioSdk } - ?.let { fingerprintMatchResult -> - projectConfiguration.fingerprint - ?.getSdkConfiguration(fingerprintMatchResult.sdk) - ?.let { sdkConfiguration -> - fingerprintMatchResult.results - .maxByOrNull { it.confidence } - ?.let { - AppMatchResult( - guid = it.subjectId, - confidenceScore = it.confidence, - decisionPolicy = sdkConfiguration.decisionPolicy, - verificationMatchThreshold = sdkConfiguration.verificationMatchThreshold, - isCredentialMatch = false, - ) - } - } - } - - private fun getFaceMatchResults( - projectConfiguration: ProjectConfiguration, - results: List, - ) = results - .filterIsInstance() - .lastOrNull { it.sdk is FaceConfiguration.BioSdk } - ?.let { faceMatchResult -> - projectConfiguration.face - ?.getSdkConfiguration(faceMatchResult.sdk) - ?.let { faceConfiguration -> - faceMatchResult.results - .maxByOrNull { it.confidence } - ?.let { - AppMatchResult( - guid = it.subjectId, - confidenceScore = it.confidence, - decisionPolicy = faceConfiguration.decisionPolicy, - verificationMatchThreshold = faceConfiguration.verificationMatchThreshold, - isCredentialMatch = false, - ) - } + .mapNotNull { matchResult -> + projectConfiguration.getModalitySdkConfig(matchResult.sdk)?.let { sdkConfiguration -> + matchResult.results.maxByOrNull { it.confidence }?.let { + AppMatchResult( + guid = it.subjectId, + confidenceScore = it.confidence, + decisionPolicy = sdkConfiguration.decisionPolicy, + verificationMatchThreshold = sdkConfiguration.verificationMatchThreshold, + isCredentialMatch = false, + ) } + } } } diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/IsNewEnrolmentUseCase.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/IsNewEnrolmentUseCase.kt index c23bad1e6c..674f501ff9 100644 --- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/IsNewEnrolmentUseCase.kt +++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/IsNewEnrolmentUseCase.kt @@ -1,9 +1,8 @@ package com.simprints.feature.orchestrator.usecases.response import com.simprints.feature.externalcredential.ExternalCredentialSearchResult -import com.simprints.infra.config.store.models.FaceConfiguration -import com.simprints.infra.config.store.models.FingerprintConfiguration import com.simprints.infra.config.store.models.ProjectConfiguration +import com.simprints.infra.config.store.models.getModalitySdkConfig import com.simprints.infra.matching.MatchResult import java.io.Serializable import javax.inject.Inject @@ -32,38 +31,22 @@ internal class IsNewEnrolmentUseCase @Inject constructor() { return true } - val faceResult = results.lastOrNull { it is MatchResult && it.sdk is FaceConfiguration.BioSdk } as? MatchResult - val fingerprintResult = results.lastOrNull { it is MatchResult && it.sdk is FingerprintConfiguration.BioSdk } as? MatchResult + val mathcResults = results + .filterIsInstance() + .takeUnless { it.isEmpty() } // Missing match results is "valid" to allow creating new records. + ?: return true - val isNewFaceEnrolment = isNewEnrolmentFaceResult(projectConfiguration, faceResult) - val isNewFingerprintEnrolment = isValidEnrolmentFingerprintResult(projectConfiguration, fingerprintResult) - - return isNewFaceEnrolment && isNewFingerprintEnrolment + return mathcResults.all { isResultBelowMediumThreshold(projectConfiguration, it) } } - // Missing results and configuration are ignored as "valid" to allow creating new records. - private fun isValidEnrolmentFingerprintResult( - projectConfiguration: ProjectConfiguration, - fingerprintResult: MatchResult?, - ): Boolean = fingerprintResult?.let { - projectConfiguration.fingerprint - ?.getSdkConfiguration(fingerprintResult.sdk) - ?.decisionPolicy - ?.medium - ?.toFloat() - ?.let { threshold -> fingerprintResult.results.all { it.confidence < threshold } } - } != false - - // Missing results and configuration are ignored as "valid" to allow creating new records. - private fun isNewEnrolmentFaceResult( + // Missing configuration are ignored as "valid" to allow creating new records. + private fun isResultBelowMediumThreshold( projectConfiguration: ProjectConfiguration, - faceResult: MatchResult?, - ): Boolean = faceResult?.let { - projectConfiguration.face - ?.getSdkConfiguration(faceResult.sdk) - ?.decisionPolicy - ?.medium - ?.toFloat() - ?.let { threshold -> faceResult.results.all { it.confidence < threshold } } - } != false + matchResult: MatchResult, + ): Boolean = projectConfiguration + .getModalitySdkConfig(matchResult.sdk) + ?.decisionPolicy + ?.medium + ?.toFloat() + ?.let { threshold -> matchResult.results.isNotEmpty() && matchResult.results.all { it.confidence < threshold } } != false } 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 0cfe44d2cd..bf21988de4 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 @@ -1,5 +1,6 @@ package com.simprints.feature.orchestrator.usecases.steps +import com.simprints.core.domain.common.AgeGroup import com.simprints.core.domain.common.FlowType import com.simprints.core.domain.common.Modality import com.simprints.face.capture.FaceCaptureContract @@ -21,12 +22,12 @@ import com.simprints.feature.selectsubject.SelectSubjectContract import com.simprints.feature.setup.SetupContract import com.simprints.feature.validatepool.ValidateSubjectPoolContract import com.simprints.fingerprint.capture.FingerprintCaptureContract -import com.simprints.infra.config.store.models.AgeGroup +import com.simprints.infra.config.store.models.FaceConfiguration +import com.simprints.infra.config.store.models.FingerprintConfiguration import com.simprints.infra.config.store.models.ProjectConfiguration import com.simprints.infra.config.store.models.allowedAgeRanges -import com.simprints.infra.config.store.models.determineFaceSDKs -import com.simprints.infra.config.store.models.determineFingerprintSDKs import com.simprints.infra.config.store.models.experimental +import com.simprints.infra.config.store.models.getSdkListForAgeGroup import com.simprints.infra.config.store.models.isAgeRestricted import com.simprints.infra.config.store.models.sortedUniqueAgeGroups import com.simprints.infra.enrolment.records.repository.domain.models.BiometricDataSource @@ -399,10 +400,9 @@ internal class BuildStepsUseCase @Inject constructor( projectConfiguration: ProjectConfiguration, ageGroup: AgeGroup?, flowType: FlowType, - ): List = when (modality) { - Modality.FINGERPRINT -> { - projectConfiguration.determineFingerprintSDKs(ageGroup).map { bioSDK -> - + ): List = projectConfiguration.getSdkListForAgeGroup(modality, ageGroup).mapNotNull { bioSDK -> + when (bioSDK) { + is FingerprintConfiguration.BioSdk -> { val sdkConfiguration = projectConfiguration.fingerprint?.getSdkConfiguration(bioSDK) // TODO: fingersToCollect can be read directly from FingerprintCapture @@ -417,10 +417,8 @@ internal class BuildStepsUseCase @Inject constructor( params = FingerprintCaptureContract.getParams(flowType, fingersToCollect, bioSDK), ) } - } - Modality.FACE -> { - projectConfiguration.determineFaceSDKs(ageGroup).map { bioSDK -> + is FaceConfiguration.BioSdk -> { val sdkConfiguration = projectConfiguration.face?.getSdkConfiguration(bioSDK) // TODO: samplesToCapture can be read directly from FaceCapture @@ -432,6 +430,8 @@ internal class BuildStepsUseCase @Inject constructor( params = FaceCaptureContract.getParams(samplesToCapture, bioSDK), ) } + + else -> null } } @@ -450,9 +450,11 @@ internal class BuildStepsUseCase @Inject constructor( ): List = projectConfiguration.general.matchingModalities .flatMap { modality -> buildMatcherStepsForModality(modality, projectConfiguration, ageGroup, flowType, subjectQuery, biometricDataSource) - }.takeIf { it.isNotEmpty() } ?: projectConfiguration.general.modalities.flatMap { modality -> - buildMatcherStepsForModality(modality, projectConfiguration, ageGroup, flowType, subjectQuery, biometricDataSource) - } + }.ifEmpty { + projectConfiguration.general.modalities.flatMap { modality -> + buildMatcherStepsForModality(modality, projectConfiguration, ageGroup, flowType, subjectQuery, biometricDataSource) + } + } private fun buildMatcherStepsForModality( modality: Modality, @@ -461,38 +463,30 @@ internal class BuildStepsUseCase @Inject constructor( flowType: FlowType, subjectQuery: SubjectQuery, biometricDataSource: BiometricDataSource, - ): List = when (modality) { - Modality.FINGERPRINT -> { - projectConfiguration.determineFingerprintSDKs(ageGroup).map { bioSDK -> - Step( - id = StepId.FINGERPRINT_MATCHER, - navigationActionId = R.id.action_orchestratorFragment_to_matcher, - destinationId = MatchContract.DESTINATION, - params = MatchStepStubPayload.getMatchStubParams( - flowType = flowType, - subjectQuery = subjectQuery, - biometricDataSource = biometricDataSource, - bioSdk = bioSDK, - ), - ) - } - } + ): List = projectConfiguration.getSdkListForAgeGroup(modality, ageGroup).mapNotNull { bioSDK -> + val paramStub = MatchStepStubPayload.getMatchStubParams( + flowType = flowType, + subjectQuery = subjectQuery, + biometricDataSource = biometricDataSource, + bioSdk = bioSDK, + ) - Modality.FACE -> { - projectConfiguration.determineFaceSDKs(ageGroup).map { bioSDK -> - // Face bio SDK is currently ignored until we add a second one - Step( - id = StepId.FACE_MATCHER, - navigationActionId = R.id.action_orchestratorFragment_to_matcher, - destinationId = MatchContract.DESTINATION, - params = MatchStepStubPayload.getMatchStubParams( - flowType = flowType, - subjectQuery = subjectQuery, - biometricDataSource = biometricDataSource, - bioSdk = bioSDK, - ), - ) - } + when (bioSDK) { + is FingerprintConfiguration.BioSdk -> Step( + id = StepId.FINGERPRINT_MATCHER, + navigationActionId = R.id.action_orchestratorFragment_to_matcher, + destinationId = MatchContract.DESTINATION, + params = paramStub, + ) + + is FaceConfiguration.BioSdk -> Step( + id = StepId.FACE_MATCHER, + navigationActionId = R.id.action_orchestratorFragment_to_matcher, + destinationId = MatchContract.DESTINATION, + params = paramStub, + ) + + else -> null } } 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 2cc7e2c078..87d7382965 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 @@ -4,6 +4,7 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.test.ext.junit.runners.* import com.google.common.truth.Truth.* import com.jraska.livedata.test +import com.simprints.core.domain.common.AgeGroup import com.simprints.core.domain.common.FlowType import com.simprints.core.domain.common.Modality import com.simprints.core.domain.response.AppErrorReason @@ -34,7 +35,6 @@ import com.simprints.feature.selectagegroup.SelectSubjectAgeGroupResult import com.simprints.feature.setup.LocationStore import com.simprints.feature.setup.SetupResult import com.simprints.fingerprint.capture.FingerprintCaptureContract -import com.simprints.infra.config.store.models.AgeGroup import com.simprints.infra.config.store.models.FaceConfiguration import com.simprints.infra.config.store.models.FingerprintConfiguration.BioSdk.NEC import com.simprints.infra.config.store.models.FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER @@ -323,9 +323,9 @@ internal class OrchestratorViewModelTest { val params = step.params?.let { it as? MatchParams } assertThat(params).isNotNull() assertThat(params?.bioSdk).isEqualTo(SECUGEN_SIM_MATCHER) - assertThat(params?.probeFingerprintSamples?.size).isEqualTo(2) - assertThat(params?.probeFingerprintSamples?.get(0)?.format).isEqualTo(format) - assertThat(params?.probeFingerprintSamples?.get(1)?.format).isEqualTo(format) + assertThat(params?.probeSamples?.size).isEqualTo(2) + assertThat(params?.probeSamples?.get(0)?.format).isEqualTo(format) + assertThat(params?.probeSamples?.get(1)?.format).isEqualTo(format) } } @@ -455,7 +455,7 @@ internal class OrchestratorViewModelTest { ) val externalCredentialParams = mockk(relaxed = true) { - every { copy(probeReferenceId = any(), fingerprintSamples = any()) } returns this + every { copy(probeReferenceId = any(), samples = any()) } returns this } coEvery { stepsBuilder.build(any(), any(), any(), any()) } returns listOf( @@ -477,7 +477,7 @@ internal class OrchestratorViewModelTest { verify { externalCredentialParams.copy( probeReferenceId = fingerprintReferenceId, - fingerprintSamples = expectedFingerprintSamples, + samples = mapOf(Modality.FINGERPRINT to expectedFingerprintSamples), ) } } 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 9345de2281..93436264d1 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 @@ -3,6 +3,7 @@ package com.simprints.feature.orchestrator.cache import android.content.SharedPreferences import androidx.test.ext.junit.runners.* import com.google.common.truth.Truth.* +import com.simprints.core.domain.common.AgeGroup import com.simprints.core.domain.common.FlowType import com.simprints.core.domain.common.Modality import com.simprints.core.domain.externalcredential.ExternalCredentialType @@ -45,7 +46,6 @@ import com.simprints.feature.setup.SetupResult import com.simprints.feature.validatepool.ValidateSubjectPoolFragmentParams import com.simprints.feature.validatepool.ValidateSubjectPoolResult import com.simprints.fingerprint.capture.FingerprintCaptureParams -import com.simprints.infra.config.store.models.AgeGroup import com.simprints.infra.config.store.models.FaceConfiguration import com.simprints.infra.config.store.models.FingerprintConfiguration import com.simprints.infra.enrolment.records.repository.domain.models.BiometricDataSource @@ -185,22 +185,24 @@ class OrchestratorCacheIntegrationTest { flowType = FlowType.IDENTIFY, ageGroup = AgeGroup(1, 2), probeReferenceId = "referenceId", - faceSamples = listOf( - CaptureSample( - captureEventId = GUID1, - identifier = SampleIdentifier.LEFT_THUMB, - template = byteArrayOf(1, 2, 3), - modality = Modality.FACE, - format = "format", + samples = mapOf( + Modality.FACE to listOf( + CaptureSample( + captureEventId = GUID1, + identifier = SampleIdentifier.LEFT_THUMB, + template = byteArrayOf(1, 2, 3), + modality = Modality.FACE, + format = "format", + ), ), - ), - fingerprintSamples = listOf( - CaptureSample( - captureEventId = GUID1, - identifier = SampleIdentifier.LEFT_THUMB, - template = byteArrayOf(1, 2, 3), - modality = Modality.FINGERPRINT, - format = "format", + Modality.FINGERPRINT to listOf( + CaptureSample( + captureEventId = GUID1, + identifier = SampleIdentifier.LEFT_THUMB, + template = byteArrayOf(1, 2, 3), + modality = Modality.FINGERPRINT, + format = "format", + ), ), ), ), @@ -223,8 +225,7 @@ class OrchestratorCacheIntegrationTest { credential = "credential".asTokenizableEncrypted(), matchResult = MatchComparisonResult("subjectId", 0.5f), verificationThreshold = 55f, - faceBioSdk = FaceConfiguration.BioSdk.RANK_ONE, - fingerprintBioSdk = FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER, + bioSdk = FaceConfiguration.BioSdk.RANK_ONE, ), ), ), @@ -277,7 +278,7 @@ class OrchestratorCacheIntegrationTest { queryForCandidates = SubjectQuery(), biometricDataSource = BiometricDataSource.CommCare("name"), bioSdk = FingerprintConfiguration.BioSdk.NEC, - probeFingerprintSamples = listOf( + probeSamples = listOf( CaptureSample( captureEventId = GUID1, identifier = SampleIdentifier.LEFT_THUMB, @@ -337,7 +338,7 @@ class OrchestratorCacheIntegrationTest { queryForCandidates = SubjectQuery(), biometricDataSource = BiometricDataSource.Simprints, bioSdk = FaceConfiguration.BioSdk.RANK_ONE, - probeFaceSamples = listOf( + probeSamples = listOf( CaptureSample( captureEventId = GUID1, identifier = SampleIdentifier.LEFT_THUMB, diff --git a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/cache/OrchestratorCacheTest.kt b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/cache/OrchestratorCacheTest.kt index 955cef9830..6207ecf928 100644 --- a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/cache/OrchestratorCacheTest.kt +++ b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/cache/OrchestratorCacheTest.kt @@ -3,9 +3,9 @@ package com.simprints.feature.orchestrator.cache import android.content.SharedPreferences import com.fasterxml.jackson.core.type.TypeReference import com.google.common.truth.Truth.assertThat +import com.simprints.core.domain.common.AgeGroup import com.simprints.core.tools.json.JsonHelper import com.simprints.feature.orchestrator.steps.Step -import com.simprints.infra.config.store.models.AgeGroup import com.simprints.infra.security.SecurityManager import io.mockk.MockKAnnotations import io.mockk.every diff --git a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/MapRefusalOrErrorResultUseCaseTest.kt b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/MapRefusalOrErrorResultUseCaseTest.kt index ba81d3826c..18606bf17b 100644 --- a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/MapRefusalOrErrorResultUseCaseTest.kt +++ b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/MapRefusalOrErrorResultUseCaseTest.kt @@ -1,6 +1,7 @@ package com.simprints.feature.orchestrator.usecases import com.google.common.truth.Truth.* +import com.simprints.core.domain.common.AgeGroup import com.simprints.core.domain.common.Modality import com.simprints.core.domain.sample.CaptureIdentity import com.simprints.feature.alert.AlertResult @@ -11,7 +12,6 @@ import com.simprints.feature.selectagegroup.SelectSubjectAgeGroupResult import com.simprints.feature.setup.SetupResult import com.simprints.feature.validatepool.ValidateSubjectPoolResult import com.simprints.fingerprint.connect.FingerprintConnectResult -import com.simprints.infra.config.store.models.AgeGroup import com.simprints.infra.config.store.models.ProjectConfiguration import com.simprints.infra.events.session.SessionEventRepository import com.simprints.infra.orchestration.data.responses.AppErrorResponse 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 58d5b3c622..320fec47de 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 @@ -59,8 +59,7 @@ internal class CreateEnrolResponseUseCaseTest { projectId = any(), attendantId = any(), moduleId = any(), - fingerprintResponse = any(), - faceResponse = any(), + captures = any(), externalCredential = any(), ) } returns mockk { every { subjectId } returns "guid" } @@ -87,8 +86,7 @@ internal class CreateEnrolResponseUseCaseTest { projectId = any(), attendantId = any(), moduleId = any(), - fingerprintResponse = null, - faceResponse = null, + captures = emptyList(), externalCredential = null, ) } throws MissingCaptureException() @@ -114,8 +112,7 @@ internal class CreateEnrolResponseUseCaseTest { projectId = any(), attendantId = any(), moduleId = any(), - fingerprintResponse = any(), - faceResponse = any(), + captures = any(), externalCredential = any(), ) } returns mockk { every { subjectId } returns enrolmentSubjectId } @@ -136,8 +133,7 @@ internal class CreateEnrolResponseUseCaseTest { projectId = projectId, attendantId = any(), moduleId = any(), - fingerprintResponse = any(), - faceResponse = null, + captures = any(), externalCredential = match { it.value == credentialEncrypted && it.type == externalCredentialType }, ) } diff --git a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/CreateIdentifyResponseUseCaseTest.kt b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/CreateIdentifyResponseUseCaseTest.kt index d542ad7bb5..649c14d63a 100644 --- a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/CreateIdentifyResponseUseCaseTest.kt +++ b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/CreateIdentifyResponseUseCaseTest.kt @@ -37,8 +37,8 @@ class CreateIdentifyResponseUseCaseTest { val result = useCase( mockk { every { multifactorId?.allowedExternalCredentials } returns null - every { face?.getSdkConfiguration((any()))?.decisionPolicy } returns null - every { fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns null + every { face?.rankOne?.decisionPolicy } returns null + every { fingerprint?.secugenSimMatcher?.decisionPolicy } returns null }, results = listOf(createFaceMatchResult(10f, 20f, 30f)), ) @@ -52,8 +52,8 @@ class CreateIdentifyResponseUseCaseTest { mockk { every { multifactorId?.allowedExternalCredentials } returns null every { identification.maxNbOfReturnedCandidates } returns 2 - every { face?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy(20, 50, 100) - every { fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns null + every { face?.rankOne?.decisionPolicy } returns DecisionPolicy(20, 50, 100) + every { fingerprint?.secugenSimMatcher?.decisionPolicy } returns null }, results = listOf(createFaceMatchResult(10f, 20f, 30f)), ) @@ -68,8 +68,8 @@ class CreateIdentifyResponseUseCaseTest { mockk { every { multifactorId?.allowedExternalCredentials } returns null every { identification.maxNbOfReturnedCandidates } returns 2 - every { face?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy(20, 50, 100) - every { fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns null + every { face?.rankOne?.decisionPolicy } returns DecisionPolicy(20, 50, 100) + every { fingerprint?.secugenSimMatcher?.decisionPolicy } returns null }, results = listOf(createFaceMatchResult(20f, 25f, 30f, 40f)), ) @@ -84,8 +84,8 @@ class CreateIdentifyResponseUseCaseTest { mockk { every { multifactorId?.allowedExternalCredentials } returns null every { identification.maxNbOfReturnedCandidates } returns 2 - every { face?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy(20, 50, 100) - every { fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns null + every { face?.rankOne?.decisionPolicy } returns DecisionPolicy(20, 50, 100) + every { fingerprint?.secugenSimMatcher?.decisionPolicy } returns null }, results = listOf(createFaceMatchResult(15f, 30f, 100f)), ) @@ -100,8 +100,8 @@ class CreateIdentifyResponseUseCaseTest { mockk { every { multifactorId?.allowedExternalCredentials } returns null every { identification.maxNbOfReturnedCandidates } returns 2 - every { face?.getSdkConfiguration((any()))?.decisionPolicy } returns null - every { fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy( + every { face?.rankOne?.decisionPolicy } returns null + every { fingerprint?.secugenSimMatcher?.decisionPolicy } returns DecisionPolicy( 20, 50, 100, @@ -120,8 +120,8 @@ class CreateIdentifyResponseUseCaseTest { mockk { every { multifactorId?.allowedExternalCredentials } returns null every { identification.maxNbOfReturnedCandidates } returns 2 - every { face?.getSdkConfiguration((any()))?.decisionPolicy } returns null - every { fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy( + every { face?.rankOne?.decisionPolicy } returns null + every { fingerprint?.secugenSimMatcher?.decisionPolicy } returns DecisionPolicy( 20, 50, 100, @@ -140,8 +140,8 @@ class CreateIdentifyResponseUseCaseTest { mockk { every { multifactorId?.allowedExternalCredentials } returns null every { identification.maxNbOfReturnedCandidates } returns 2 - every { face?.getSdkConfiguration((any()))?.decisionPolicy } returns null - every { fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy( + every { face?.rankOne?.decisionPolicy } returns null + every { fingerprint?.secugenSimMatcher?.decisionPolicy } returns DecisionPolicy( 20, 50, 100, @@ -160,8 +160,8 @@ class CreateIdentifyResponseUseCaseTest { mockk { every { multifactorId?.allowedExternalCredentials } returns null every { identification.maxNbOfReturnedCandidates } returns 2 - every { face?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy(20, 50, 100) - every { fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy( + every { face?.rankOne?.decisionPolicy } returns DecisionPolicy(20, 50, 100) + every { fingerprint?.secugenSimMatcher?.decisionPolicy } returns DecisionPolicy( 20, 50, 100, @@ -183,8 +183,8 @@ class CreateIdentifyResponseUseCaseTest { mockk { every { multifactorId?.allowedExternalCredentials } returns null every { identification.maxNbOfReturnedCandidates } returns 2 - every { face?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy(20, 50, 100) - every { fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy( + every { face?.rankOne?.decisionPolicy } returns DecisionPolicy(20, 50, 100) + every { fingerprint?.secugenSimMatcher?.decisionPolicy } returns DecisionPolicy( 20, 50, 100, @@ -211,16 +211,14 @@ class CreateIdentifyResponseUseCaseTest { subjectId = faceSmallConfidence, confidence = smallConfidence, ) - every { faceBioSdk } returns FaceConfiguration.BioSdk.RANK_ONE - every { fingerprintBioSdk } returns null + every { bioSdk } returns FaceConfiguration.BioSdk.RANK_ONE }, mockk { every { matchResult } returns MatchComparisonResult( subjectId = faceBigConfidence, confidence = bigConfidence, ) - every { faceBioSdk } returns FaceConfiguration.BioSdk.RANK_ONE - every { fingerprintBioSdk } returns null + every { bioSdk } returns FaceConfiguration.BioSdk.RANK_ONE }, ) @@ -230,8 +228,7 @@ class CreateIdentifyResponseUseCaseTest { subjectId = "fingerprintSubjectId", confidence = 90f, ) - every { faceBioSdk } returns null - every { fingerprintBioSdk } returns FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER + every { bioSdk } returns FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER }, ) @@ -239,17 +236,10 @@ class CreateIdentifyResponseUseCaseTest { mockk { every { multifactorId?.allowedExternalCredentials } returns null every { identification.maxNbOfReturnedCandidates } returns 5 - every { face?.getSdkConfiguration(FaceConfiguration.BioSdk.RANK_ONE)?.decisionPolicy } returns DecisionPolicy(20, 50, 100) - every { face?.getSdkConfiguration(FaceConfiguration.BioSdk.RANK_ONE)?.verificationMatchThreshold } returns 0.0f - every { fingerprint?.getSdkConfiguration(FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER)?.decisionPolicy } returns - DecisionPolicy(20, 50, 100) - every { - fingerprint - ?.getSdkConfiguration( - FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER, - )?.verificationMatchThreshold - } returns - 0.0f + every { face?.rankOne?.decisionPolicy } returns DecisionPolicy(20, 50, 100) + every { fingerprint?.secugenSimMatcher?.decisionPolicy } returns DecisionPolicy(20, 50, 100) + every { face?.rankOne?.verificationMatchThreshold } returns 0.0f + every { fingerprint?.secugenSimMatcher?.verificationMatchThreshold } returns 0.0f }, results = listOf( mockk { @@ -274,16 +264,14 @@ class CreateIdentifyResponseUseCaseTest { subjectId = fingerprintSmallConfidence, confidence = smallConfidence, ) - every { faceBioSdk } returns null - every { fingerprintBioSdk } returns FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER + every { bioSdk } returns FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER }, mockk { every { matchResult } returns MatchComparisonResult( subjectId = fingerprintBigConfidence, confidence = bigConfidence, ) - every { faceBioSdk } returns null - every { fingerprintBioSdk } returns FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER + every { bioSdk } returns FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER }, ) @@ -293,8 +281,7 @@ class CreateIdentifyResponseUseCaseTest { subjectId = "faceSubjectId", confidence = 90f, ) - every { faceBioSdk } returns FaceConfiguration.BioSdk.RANK_ONE - every { fingerprintBioSdk } returns null + every { bioSdk } returns FaceConfiguration.BioSdk.RANK_ONE }, ) @@ -302,17 +289,10 @@ class CreateIdentifyResponseUseCaseTest { mockk { every { multifactorId?.allowedExternalCredentials } returns null every { identification.maxNbOfReturnedCandidates } returns 5 - every { face?.getSdkConfiguration(FaceConfiguration.BioSdk.RANK_ONE)?.decisionPolicy } returns DecisionPolicy(20, 50, 100) - every { face?.getSdkConfiguration(FaceConfiguration.BioSdk.RANK_ONE)?.verificationMatchThreshold } returns 0.0f - every { fingerprint?.getSdkConfiguration(FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER)?.decisionPolicy } returns - DecisionPolicy(20, 50, 100) - every { - fingerprint - ?.getSdkConfiguration( - FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER, - )?.verificationMatchThreshold - } returns - 0.0f + every { face?.rankOne?.decisionPolicy } returns DecisionPolicy(20, 50, 100) + every { face?.rankOne?.verificationMatchThreshold } returns 0.0f + every { fingerprint?.secugenSimMatcher?.decisionPolicy } returns DecisionPolicy(20, 50, 100) + every { fingerprint?.secugenSimMatcher?.verificationMatchThreshold } returns 0.0f }, results = listOf( mockk { @@ -338,8 +318,7 @@ class CreateIdentifyResponseUseCaseTest { subjectId = sharedGuid, confidence = credentialConfidence, ) - every { faceBioSdk } returns FaceConfiguration.BioSdk.RANK_ONE - every { fingerprintBioSdk } returns null + every { bioSdk } returns FaceConfiguration.BioSdk.RANK_ONE }, ) @@ -347,9 +326,9 @@ class CreateIdentifyResponseUseCaseTest { mockk { every { identification.maxNbOfReturnedCandidates } returns 5 every { multifactorId?.allowedExternalCredentials } returns null - every { face?.getSdkConfiguration(FaceConfiguration.BioSdk.RANK_ONE)?.decisionPolicy } returns DecisionPolicy(20, 50, 100) - every { face?.getSdkConfiguration(FaceConfiguration.BioSdk.RANK_ONE)?.verificationMatchThreshold } returns 0.0f - every { fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns null + every { face?.rankOne?.decisionPolicy } returns DecisionPolicy(20, 50, 100) + every { face?.rankOne?.verificationMatchThreshold } returns 0.0f + every { fingerprint?.secugenSimMatcher?.decisionPolicy } returns null }, results = listOf( mockk { @@ -380,8 +359,7 @@ class CreateIdentifyResponseUseCaseTest { subjectId = sharedGuid, confidence = credentialConfidence, ) - every { faceBioSdk } returns null - every { fingerprintBioSdk } returns FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER + every { bioSdk } returns FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER }, ) @@ -389,16 +367,9 @@ class CreateIdentifyResponseUseCaseTest { mockk { every { multifactorId?.allowedExternalCredentials } returns null every { identification.maxNbOfReturnedCandidates } returns 5 - every { face?.getSdkConfiguration((any()))?.decisionPolicy } returns null - every { fingerprint?.getSdkConfiguration(FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER)?.decisionPolicy } returns - DecisionPolicy(20, 50, 100) - every { - fingerprint - ?.getSdkConfiguration( - FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER, - )?.verificationMatchThreshold - } returns - 0.0f + every { face?.rankOne?.decisionPolicy } returns null + every { fingerprint?.secugenSimMatcher?.decisionPolicy } returns DecisionPolicy(20, 50, 100) + every { fingerprint?.secugenSimMatcher?.verificationMatchThreshold } returns 0.0f }, results = listOf( mockk { diff --git a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/CreateVerifyResponseUseCaseTest.kt b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/CreateVerifyResponseUseCaseTest.kt index 6510596a3b..15d982fd83 100644 --- a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/CreateVerifyResponseUseCaseTest.kt +++ b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/CreateVerifyResponseUseCaseTest.kt @@ -26,7 +26,7 @@ class CreateVerifyResponseUseCaseTest { val result = useCase( mockk { every { face } returns null - every { fingerprint?.getSdkConfiguration((any())) } returns null + every { fingerprint?.secugenSimMatcher } returns null }, results = listOf(createFaceMatchResult(10f, 20f, 30f)), ) @@ -38,10 +38,10 @@ class CreateVerifyResponseUseCaseTest { fun `Returns face matches with highest score`() { val result = useCase( mockk { - every { face?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy(10, 20, 30) - every { fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns null - every { face?.getSdkConfiguration((any()))?.verificationMatchThreshold } returns null - every { fingerprint?.getSdkConfiguration((any()))?.verificationMatchThreshold } returns null + every { face?.rankOne?.decisionPolicy } returns DecisionPolicy(10, 20, 30) + every { fingerprint?.secugenSimMatcher?.decisionPolicy } returns null + every { face?.rankOne?.verificationMatchThreshold } returns null + every { fingerprint?.secugenSimMatcher?.verificationMatchThreshold } returns null }, results = listOf(createFaceMatchResult(10f, 50f, 100f)), ) @@ -53,13 +53,13 @@ class CreateVerifyResponseUseCaseTest { fun `Returns fingerprint matches with highest score`() { val result = useCase( mockk { - every { face?.getSdkConfiguration((any()))?.decisionPolicy } returns null - every { fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy( + every { face?.rankOne?.decisionPolicy } returns null + every { fingerprint?.secugenSimMatcher?.decisionPolicy } returns DecisionPolicy( 10, 20, 30, ) - every { fingerprint?.getSdkConfiguration((any()))?.verificationMatchThreshold } returns 50f + every { fingerprint?.secugenSimMatcher?.verificationMatchThreshold } returns 50f }, results = listOf(createFingerprintMatchResult(10f, 50f, 100f)), ) @@ -71,14 +71,14 @@ class CreateVerifyResponseUseCaseTest { fun `Returns matches with highest face match score`() { val result = useCase( mockk { - every { face?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy(10, 20, 30) - every { fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy( + every { face?.rankOne?.decisionPolicy } returns DecisionPolicy(10, 20, 30) + every { fingerprint?.secugenSimMatcher?.decisionPolicy } returns DecisionPolicy( 10, 20, 30, ) - every { face?.getSdkConfiguration((any()))?.verificationMatchThreshold } returns 50f - every { fingerprint?.getSdkConfiguration((any()))?.verificationMatchThreshold } returns 50f + every { face?.rankOne?.verificationMatchThreshold } returns 50f + every { fingerprint?.secugenSimMatcher?.verificationMatchThreshold } returns 50f }, results = listOf( createFaceMatchResult(10f, 50f, 105f), @@ -93,14 +93,14 @@ class CreateVerifyResponseUseCaseTest { fun `Returns matches with highest fingerprint match score`() { val result = useCase( mockk { - every { face?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy(10, 20, 30) - every { fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy( + every { face?.rankOne?.decisionPolicy } returns DecisionPolicy(10, 20, 30) + every { fingerprint?.secugenSimMatcher?.decisionPolicy } returns DecisionPolicy( 10, 20, 30, ) - every { face?.getSdkConfiguration((any()))?.verificationMatchThreshold } returns 50f - every { fingerprint?.getSdkConfiguration((any()))?.verificationMatchThreshold } returns 50f + every { face?.rankOne?.verificationMatchThreshold } returns 50f + every { fingerprint?.secugenSimMatcher?.verificationMatchThreshold } returns 50f }, results = listOf( createFaceMatchResult(10f, 50f, 100f), @@ -115,8 +115,8 @@ class CreateVerifyResponseUseCaseTest { fun `When face verificationMatchThreshold is null - verificationSuccess is null`() { val result = useCase( mockk { - every { face?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy(10, 20, 30) - every { face?.getSdkConfiguration((any()))?.verificationMatchThreshold } returns null + every { face?.rankOne?.decisionPolicy } returns DecisionPolicy(10, 20, 30) + every { face?.rankOne?.verificationMatchThreshold } returns null }, results = listOf( createFaceMatchResult(10f, 50f, 100f), @@ -130,12 +130,12 @@ class CreateVerifyResponseUseCaseTest { fun `When fingerprint verificationMatchThreshold is null - verificationSuccess is null`() { val result = useCase( mockk { - every { fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy( + every { fingerprint?.secugenSimMatcher?.decisionPolicy } returns DecisionPolicy( 10, 20, 30, ) - every { fingerprint?.getSdkConfiguration((any()))?.verificationMatchThreshold } returns null + every { fingerprint?.secugenSimMatcher?.verificationMatchThreshold } returns null }, results = listOf( createFingerprintMatchResult(10f, 50f, 100f), @@ -149,8 +149,8 @@ class CreateVerifyResponseUseCaseTest { fun `When face match score is above verificationMatchThreshold - verificationSuccess is true`() { val result = useCase( mockk { - every { face?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy(10, 20, 30) - every { face?.getSdkConfiguration((any()))?.verificationMatchThreshold } returns 50f + every { face?.rankOne?.decisionPolicy } returns DecisionPolicy(10, 20, 30) + every { face?.rankOne?.verificationMatchThreshold } returns 50f }, results = listOf( createFaceMatchResult(51f), @@ -164,12 +164,12 @@ class CreateVerifyResponseUseCaseTest { fun `When fingerprint match score is above verificationMatchThreshold - verificationSuccess is true`() { val result = useCase( mockk { - every { fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy( + every { fingerprint?.secugenSimMatcher?.decisionPolicy } returns DecisionPolicy( 10, 20, 30, ) - every { fingerprint?.getSdkConfiguration((any()))?.verificationMatchThreshold } returns 50f + every { fingerprint?.secugenSimMatcher?.verificationMatchThreshold } returns 50f }, results = listOf( createFingerprintMatchResult(51f), @@ -183,8 +183,8 @@ class CreateVerifyResponseUseCaseTest { fun `When face match score is equal to verificationMatchThreshold - verificationSuccess is true`() { val result = useCase( mockk { - every { face?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy(10, 20, 30) - every { face?.getSdkConfiguration((any()))?.verificationMatchThreshold } returns 50f + every { face?.rankOne?.decisionPolicy } returns DecisionPolicy(10, 20, 30) + every { face?.rankOne?.verificationMatchThreshold } returns 50f }, results = listOf( createFaceMatchResult(50f), @@ -198,12 +198,12 @@ class CreateVerifyResponseUseCaseTest { fun `When fingerprint match score is equal to verificationMatchThreshold - verificationSuccess is true`() { val result = useCase( mockk { - every { fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy( + every { fingerprint?.secugenSimMatcher?.decisionPolicy } returns DecisionPolicy( 10, 20, 30, ) - every { fingerprint?.getSdkConfiguration((any()))?.verificationMatchThreshold } returns 50f + every { fingerprint?.secugenSimMatcher?.verificationMatchThreshold } returns 50f }, results = listOf( createFingerprintMatchResult(50f), @@ -217,8 +217,8 @@ class CreateVerifyResponseUseCaseTest { fun `When face match score is below verificationMatchThreshold - verificationSuccess is false`() { val result = useCase( mockk { - every { face?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy(10, 20, 30) - every { face?.getSdkConfiguration((any()))?.verificationMatchThreshold } returns 50f + every { face?.rankOne?.decisionPolicy } returns DecisionPolicy(10, 20, 30) + every { face?.rankOne?.verificationMatchThreshold } returns 50f }, results = listOf( createFaceMatchResult(49f), @@ -232,12 +232,12 @@ class CreateVerifyResponseUseCaseTest { fun `When fingerprint match score is below verificationMatchThreshold - verificationSuccess is false`() { val result = useCase( mockk { - every { fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy( + every { fingerprint?.secugenSimMatcher?.decisionPolicy } returns DecisionPolicy( 10, 20, 30, ) - every { fingerprint?.getSdkConfiguration((any()))?.verificationMatchThreshold } returns 50f + every { fingerprint?.secugenSimMatcher?.verificationMatchThreshold } returns 50f }, results = listOf( createFingerprintMatchResult(49f), diff --git a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/IsNewEnrolmentUseCaseTest.kt b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/IsNewEnrolmentUseCaseTest.kt index 0333ee7924..50d74b0669 100644 --- a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/IsNewEnrolmentUseCaseTest.kt +++ b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/IsNewEnrolmentUseCaseTest.kt @@ -24,8 +24,8 @@ internal class IsNewEnrolmentUseCaseTest { fun setUp() { MockKAnnotations.init(this, relaxUnitFun = true) - every { projectConfiguration.face?.getSdkConfiguration((any()))?.decisionPolicy } returns faceConfidenceDecisionPolicy - every { projectConfiguration.fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns fingerprintConfidenceDecisionPolicy + every { projectConfiguration.face?.rankOne?.decisionPolicy } returns faceConfidenceDecisionPolicy + every { projectConfiguration.fingerprint?.secugenSimMatcher?.decisionPolicy } returns fingerprintConfidenceDecisionPolicy useCase = IsNewEnrolmentUseCase() } @@ -166,8 +166,7 @@ internal class IsNewEnrolmentUseCaseTest { every { subjectId } returns "subjectId" every { confidence } returns 50f } - every { faceBioSdk } returns FaceConfiguration.BioSdk.RANK_ONE - every { fingerprintBioSdk } returns null + every { bioSdk } returns FaceConfiguration.BioSdk.RANK_ONE }, ) 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 0d51ba0e9e..83098ec320 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,17 +1,17 @@ package com.simprints.feature.orchestrator.usecases.steps import com.google.common.truth.Truth.* +import com.simprints.core.domain.common.AgeGroup import com.simprints.core.domain.common.Modality import com.simprints.core.domain.externalcredential.ExternalCredentialType -import com.simprints.feature.externalcredential.screens.search.model.ScannedCredential import com.simprints.core.domain.sample.SampleIdentifier +import com.simprints.feature.externalcredential.screens.search.model.ScannedCredential import com.simprints.feature.orchestrator.cache.OrchestratorCache import com.simprints.feature.orchestrator.exceptions.SubjectAgeNotSupportedException import com.simprints.feature.orchestrator.steps.Step import com.simprints.feature.orchestrator.steps.StepId import com.simprints.feature.orchestrator.usecases.MapStepsForLastBiometricEnrolUseCase import com.simprints.feature.selectsubject.SelectSubjectParams -import com.simprints.infra.config.store.models.AgeGroup import com.simprints.infra.config.store.models.FaceConfiguration import com.simprints.infra.config.store.models.FingerprintConfiguration import com.simprints.infra.config.store.models.FingerprintConfiguration.BioSdk.NEC diff --git a/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/SelectSubjectAgeGroupResult.kt b/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/SelectSubjectAgeGroupResult.kt index c085a6004d..45bdb8560c 100644 --- a/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/SelectSubjectAgeGroupResult.kt +++ b/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/SelectSubjectAgeGroupResult.kt @@ -1,8 +1,8 @@ package com.simprints.feature.selectagegroup import androidx.annotation.Keep +import com.simprints.core.domain.common.AgeGroup import com.simprints.core.domain.step.StepResult -import com.simprints.infra.config.store.models.AgeGroup @Keep data class SelectSubjectAgeGroupResult( diff --git a/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/AgeGroupAdapter.kt b/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/AgeGroupAdapter.kt index ecbd0b9a19..afe57b5e55 100644 --- a/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/AgeGroupAdapter.kt +++ b/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/AgeGroupAdapter.kt @@ -7,8 +7,8 @@ import android.widget.ImageView import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import com.simprints.core.ExcludedFromGeneratedTestCoverageReports +import com.simprints.core.domain.common.AgeGroup import com.simprints.feature.selectagegroup.R -import com.simprints.infra.config.store.models.AgeGroup import com.simprints.infra.resources.R as IDR // The age groups should be sorted as follows: Newborn, Baby, Child, Adult diff --git a/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/AgeGroupDisplayName.kt b/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/AgeGroupDisplayName.kt index 110c2630f8..009e5a3d02 100644 --- a/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/AgeGroupDisplayName.kt +++ b/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/AgeGroupDisplayName.kt @@ -1,7 +1,7 @@ package com.simprints.feature.selectagegroup.screen import android.content.Context -import com.simprints.infra.config.store.models.AgeGroup +import com.simprints.core.domain.common.AgeGroup import com.simprints.infra.resources.R as IDR /** diff --git a/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/BuildAgeGroupsUseCase.kt b/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/BuildAgeGroupsUseCase.kt index 3a229675f6..267988c8a0 100644 --- a/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/BuildAgeGroupsUseCase.kt +++ b/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/BuildAgeGroupsUseCase.kt @@ -1,7 +1,7 @@ package com.simprints.feature.selectagegroup.screen +import com.simprints.core.domain.common.AgeGroup import com.simprints.infra.config.store.ConfigRepository -import com.simprints.infra.config.store.models.AgeGroup import com.simprints.infra.config.store.models.sortedUniqueAgeGroups import javax.inject.Inject diff --git a/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/SelectSubjectAgeGroupFragment.kt b/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/SelectSubjectAgeGroupFragment.kt index 409a6ad6d2..b9042201c3 100644 --- a/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/SelectSubjectAgeGroupFragment.kt +++ b/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/SelectSubjectAgeGroupFragment.kt @@ -8,19 +8,19 @@ import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager +import com.simprints.core.domain.common.AgeGroup import com.simprints.core.livedata.LiveDataEventObserver import com.simprints.feature.exitform.ExitFormContract import com.simprints.feature.exitform.ExitFormResult import com.simprints.feature.selectagegroup.R import com.simprints.feature.selectagegroup.SelectSubjectAgeGroupResult import com.simprints.feature.selectagegroup.databinding.FragmentAgeGroupSelectionBinding -import com.simprints.infra.config.store.models.AgeGroup import com.simprints.infra.logging.LoggingConstants.CrashReportTag.ORCHESTRATION import com.simprints.infra.logging.Simber -import com.simprints.infra.uibase.view.applySystemBarInsets import com.simprints.infra.uibase.navigation.finishWithResult import com.simprints.infra.uibase.navigation.handleResult import com.simprints.infra.uibase.navigation.navigateSafely +import com.simprints.infra.uibase.view.applySystemBarInsets import com.simprints.infra.uibase.viewbinding.viewBinding import dagger.hilt.android.AndroidEntryPoint diff --git a/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/SelectSubjectAgeGroupViewModel.kt b/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/SelectSubjectAgeGroupViewModel.kt index 218cfe4982..235c387f1c 100644 --- a/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/SelectSubjectAgeGroupViewModel.kt +++ b/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/SelectSubjectAgeGroupViewModel.kt @@ -5,12 +5,12 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.simprints.core.SessionCoroutineScope +import com.simprints.core.domain.common.AgeGroup import com.simprints.core.livedata.LiveDataEvent import com.simprints.core.livedata.LiveDataEventWithContent import com.simprints.core.livedata.send import com.simprints.core.tools.time.TimeHelper import com.simprints.core.tools.time.Timestamp -import com.simprints.infra.config.store.models.AgeGroup import com.simprints.infra.events.event.domain.models.AgeGroupSelectionEvent import com.simprints.infra.events.session.SessionEventRepository import com.simprints.infra.logging.LoggingConstants.CrashReportTag.SESSION diff --git a/feature/select-subject-age-group/src/test/java/com/simprints/feature/selectagegroup/screen/AgeGroupDisplayModelTest.kt b/feature/select-subject-age-group/src/test/java/com/simprints/feature/selectagegroup/screen/AgeGroupDisplayModelTest.kt index 79436224e5..b53653f448 100644 --- a/feature/select-subject-age-group/src/test/java/com/simprints/feature/selectagegroup/screen/AgeGroupDisplayModelTest.kt +++ b/feature/select-subject-age-group/src/test/java/com/simprints/feature/selectagegroup/screen/AgeGroupDisplayModelTest.kt @@ -3,7 +3,7 @@ package com.simprints.feature.selectagegroup.screen import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.google.common.truth.Truth -import com.simprints.infra.config.store.models.AgeGroup +import com.simprints.core.domain.common.AgeGroup import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltTestApplication import io.mockk.MockKAnnotations diff --git a/feature/select-subject-age-group/src/test/java/com/simprints/feature/selectagegroup/screen/BuildAgeGroupsDescriptionUseCaseTest.kt b/feature/select-subject-age-group/src/test/java/com/simprints/feature/selectagegroup/screen/BuildAgeGroupsDescriptionUseCaseTest.kt index 1527093eee..d8bf0be164 100644 --- a/feature/select-subject-age-group/src/test/java/com/simprints/feature/selectagegroup/screen/BuildAgeGroupsDescriptionUseCaseTest.kt +++ b/feature/select-subject-age-group/src/test/java/com/simprints/feature/selectagegroup/screen/BuildAgeGroupsDescriptionUseCaseTest.kt @@ -2,8 +2,8 @@ package com.simprints.feature.selectagegroup.screen import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat +import com.simprints.core.domain.common.AgeGroup import com.simprints.infra.config.store.ConfigRepository -import com.simprints.infra.config.store.models.AgeGroup import com.simprints.infra.config.store.models.allowedAgeRanges import io.mockk.MockKAnnotations import io.mockk.coEvery diff --git a/feature/select-subject-age-group/src/test/java/com/simprints/feature/selectagegroup/screen/SelectSubjectAgeGroupViewModelTest.kt b/feature/select-subject-age-group/src/test/java/com/simprints/feature/selectagegroup/screen/SelectSubjectAgeGroupViewModelTest.kt index 6f7f1bed6f..4eee0df186 100644 --- a/feature/select-subject-age-group/src/test/java/com/simprints/feature/selectagegroup/screen/SelectSubjectAgeGroupViewModelTest.kt +++ b/feature/select-subject-age-group/src/test/java/com/simprints/feature/selectagegroup/screen/SelectSubjectAgeGroupViewModelTest.kt @@ -3,8 +3,8 @@ package com.simprints.feature.selectagegroup.screen import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.google.common.truth.Truth import com.jraska.livedata.test +import com.simprints.core.domain.common.AgeGroup import com.simprints.core.tools.time.TimeHelper -import com.simprints.infra.config.store.models.AgeGroup import com.simprints.infra.events.session.SessionEventRepository import com.simprints.testtools.common.coroutines.TestCoroutineRule import com.simprints.testtools.common.livedata.getOrAwaitValue diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/FaceConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/FaceConfiguration.kt index 4a1f779f18..a9f434de7d 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/FaceConfiguration.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/FaceConfiguration.kt @@ -1,7 +1,7 @@ package com.simprints.infra.config.store.local.models +import com.simprints.core.domain.common.AgeGroup import com.simprints.infra.config.store.exceptions.InvalidProtobufEnumException -import com.simprints.infra.config.store.models.AgeGroup import com.simprints.infra.config.store.models.FaceConfiguration internal fun FaceConfiguration.toProto(): ProtoFaceConfiguration = ProtoFaceConfiguration diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/FingerprintConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/FingerprintConfiguration.kt index b6abb13e81..50739ddadf 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/FingerprintConfiguration.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/FingerprintConfiguration.kt @@ -1,8 +1,8 @@ package com.simprints.infra.config.store.local.models +import com.simprints.core.domain.common.AgeGroup import com.simprints.infra.config.store.exceptions.InvalidProtobufEnumException import com.simprints.infra.config.store.local.models.ProtoFingerprintConfiguration.ProtoMaxCaptureAttempts -import com.simprints.infra.config.store.models.AgeGroup import com.simprints.infra.config.store.models.FingerprintConfiguration import com.simprints.infra.config.store.models.MaxCaptureAttempts @@ -121,7 +121,7 @@ internal fun ProtoAllowedAgeRange.toDomain() = AgeGroup(startInclusive, if (hasE internal fun AgeGroup.toProto() = ProtoAllowedAgeRange .newBuilder() - .also { - it.setStartInclusive(startInclusive) - if (endExclusive != null) it.setEndExclusive(endExclusive) + .also { builder -> + builder.setStartInclusive(startInclusive) + endExclusive?.let { builder.setEndExclusive(it) } }.build() diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/FaceConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/FaceConfiguration.kt index 43d2bbc7a7..48ecbcbb3a 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/FaceConfiguration.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/FaceConfiguration.kt @@ -1,5 +1,6 @@ package com.simprints.infra.config.store.models +import com.simprints.core.domain.common.AgeGroup import com.simprints.core.domain.common.ModalitySdkType data class FaceConfiguration( @@ -11,11 +12,11 @@ data class FaceConfiguration( val nbOfImagesToCapture: Int, val qualityThreshold: Float, val imageSavingStrategy: ImageSavingStrategy, - val decisionPolicy: DecisionPolicy, + override val decisionPolicy: DecisionPolicy, val version: String, - val allowedAgeRange: AgeGroup = AgeGroup(0, null), - val verificationMatchThreshold: Float? = null, - ) + override val allowedAgeRange: AgeGroup = AgeGroup(0, null), + override val verificationMatchThreshold: Float? = null, + ) : ModalitySdkConfiguration fun getSdkConfiguration(sdk: ModalitySdkType): FaceSdkConfiguration? = when (sdk) { BioSdk.RANK_ONE -> rankOne diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/FingerprintConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/FingerprintConfiguration.kt index aa8bfb4b78..81dbc7dbdc 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/FingerprintConfiguration.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/FingerprintConfiguration.kt @@ -1,5 +1,6 @@ package com.simprints.infra.config.store.models +import com.simprints.core.domain.common.AgeGroup import com.simprints.core.domain.common.ModalitySdkType import com.simprints.core.domain.sample.SampleIdentifier @@ -12,18 +13,18 @@ data class FingerprintConfiguration( ) { data class FingerprintSdkConfiguration( val fingersToCapture: List, - val decisionPolicy: DecisionPolicy, + override val decisionPolicy: DecisionPolicy, val comparisonStrategyForVerification: FingerComparisonStrategy, val vero1: Vero1Configuration? = null, val vero2: Vero2Configuration? = null, - val allowedAgeRange: AgeGroup = AgeGroup(0, null), - val verificationMatchThreshold: Float? = null, + override val allowedAgeRange: AgeGroup = AgeGroup(0, null), + override val verificationMatchThreshold: Float? = null, /** * Allowed amount of 'No Finger Detected' scans before proceeding further */ val maxCaptureAttempts: MaxCaptureAttempts?, val version: String = "", - ) + ) : ModalitySdkConfiguration enum class VeroGeneration { VERO_1, diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/ModalitySdkConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/ModalitySdkConfiguration.kt new file mode 100644 index 0000000000..edfd30b081 --- /dev/null +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/ModalitySdkConfiguration.kt @@ -0,0 +1,12 @@ +package com.simprints.infra.config.store.models + +import com.simprints.core.domain.common.AgeGroup + +/** + * Common parts of SDK configurations that are used across modules. + */ +interface ModalitySdkConfiguration { + val decisionPolicy: DecisionPolicy + val verificationMatchThreshold: Float? + val allowedAgeRange: AgeGroup +} diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/ProjectConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/ProjectConfiguration.kt index 2a76ab1e7e..d2a656afc0 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/ProjectConfiguration.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/ProjectConfiguration.kt @@ -1,5 +1,9 @@ package com.simprints.infra.config.store.models +import com.simprints.core.domain.common.AgeGroup +import com.simprints.core.domain.common.Modality +import com.simprints.core.domain.common.ModalitySdkType + data class ProjectConfiguration( val id: String, val projectId: String, @@ -106,36 +110,46 @@ fun ProjectConfiguration.isProjectWithPeriodicallyUpSync(): Boolean = fun ProjectConfiguration.isModuleSelectionAvailable(): Boolean = isProjectWithModuleSync() && !isProjectWithPeriodicallyUpSync() -fun ProjectConfiguration.determineFaceSDKs(ageGroup: AgeGroup?): List { +fun ProjectConfiguration.getSdkListForAgeGroup( + modality: Modality, + ageGroup: AgeGroup?, +): List { if (!isAgeRestricted()) { - return face?.allowedSDKs.orEmpty() + return when (modality) { + Modality.FACE -> face?.allowedSDKs.orEmpty() + Modality.FINGERPRINT -> fingerprint?.allowedSDKs.orEmpty() + } } return buildList { ageGroup?.let { age -> - if (face?.rankOne?.allowedAgeRange?.contains(age) == true) { - add(FaceConfiguration.BioSdk.RANK_ONE) - } - if (face?.simFace?.allowedAgeRange?.contains(age) == true) { - add(FaceConfiguration.BioSdk.SIM_FACE) + when (modality) { + Modality.FACE -> { + if (face?.rankOne?.allowedAgeRange?.contains(age) == true) { + add(FaceConfiguration.BioSdk.RANK_ONE) + } + if (face?.simFace?.allowedAgeRange?.contains(age) == true) { + add(FaceConfiguration.BioSdk.SIM_FACE) + } + } + + Modality.FINGERPRINT -> { + if (fingerprint?.secugenSimMatcher?.allowedAgeRange?.contains(age) == true) { + add(FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER) + } + if (fingerprint?.nec?.allowedAgeRange?.contains(age) == true) { + add(FingerprintConfiguration.BioSdk.NEC) + } + } } } } } -fun ProjectConfiguration.determineFingerprintSDKs(ageGroup: AgeGroup?): List { - if (!isAgeRestricted()) { - return fingerprint?.allowedSDKs.orEmpty() - } - - return buildList { - ageGroup?.let { age -> - if (fingerprint?.secugenSimMatcher?.allowedAgeRange?.contains(age) == true) { - add(FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER) - } - if (fingerprint?.nec?.allowedAgeRange?.contains(age) == true) { - add(FingerprintConfiguration.BioSdk.NEC) - } - } - } +fun ProjectConfiguration.getModalitySdkConfig(bioSdk: ModalitySdkType): ModalitySdkConfiguration? = when (bioSdk) { + FaceConfiguration.BioSdk.RANK_ONE -> face?.rankOne + FaceConfiguration.BioSdk.SIM_FACE -> face?.simFace + FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER -> fingerprint?.secugenSimMatcher + FingerprintConfiguration.BioSdk.NEC -> fingerprint?.nec + else -> null } diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiAllowedAgeRange.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiAllowedAgeRange.kt index 86c69daa9e..4383232303 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiAllowedAgeRange.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiAllowedAgeRange.kt @@ -1,7 +1,7 @@ package com.simprints.infra.config.store.remote.models import androidx.annotation.Keep -import com.simprints.infra.config.store.models.AgeGroup +import com.simprints.core.domain.common.AgeGroup @Keep data class ApiAllowedAgeRange( diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiFaceConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiFaceConfiguration.kt index bc2b6cfe84..53b7c11578 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiFaceConfiguration.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiFaceConfiguration.kt @@ -1,7 +1,7 @@ package com.simprints.infra.config.store.remote.models import androidx.annotation.Keep -import com.simprints.infra.config.store.models.AgeGroup +import com.simprints.core.domain.common.AgeGroup import com.simprints.infra.config.store.models.FaceConfiguration @Keep diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiFingerprintConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiFingerprintConfiguration.kt index 211b44f911..42c13773f0 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiFingerprintConfiguration.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiFingerprintConfiguration.kt @@ -1,8 +1,8 @@ package com.simprints.infra.config.store.remote.models import androidx.annotation.Keep +import com.simprints.core.domain.common.AgeGroup import com.simprints.core.domain.sample.SampleIdentifier -import com.simprints.infra.config.store.models.AgeGroup import com.simprints.infra.config.store.models.FingerprintConfiguration @Keep diff --git a/infra/config-store/src/test/java/com/simprints/infra/config/store/local/models/FingerprintConfigurationTest.kt b/infra/config-store/src/test/java/com/simprints/infra/config/store/local/models/FingerprintConfigurationTest.kt index 5503ed0def..614501df9a 100644 --- a/infra/config-store/src/test/java/com/simprints/infra/config/store/local/models/FingerprintConfigurationTest.kt +++ b/infra/config-store/src/test/java/com/simprints/infra/config/store/local/models/FingerprintConfigurationTest.kt @@ -1,8 +1,8 @@ package com.simprints.infra.config.store.local.models import com.google.common.truth.Truth.* +import com.simprints.core.domain.common.AgeGroup import com.simprints.core.domain.sample.SampleIdentifier -import com.simprints.infra.config.store.models.AgeGroup import com.simprints.infra.config.store.models.FingerprintConfiguration import com.simprints.infra.config.store.testtools.fingerprintConfiguration import com.simprints.infra.config.store.testtools.protoFingerprintConfiguration diff --git a/infra/config-store/src/test/java/com/simprints/infra/config/store/models/ProjectConfigurationTest.kt b/infra/config-store/src/test/java/com/simprints/infra/config/store/models/ProjectConfigurationTest.kt index 2a3a26ca5c..d5c96eb6fe 100644 --- a/infra/config-store/src/test/java/com/simprints/infra/config/store/models/ProjectConfigurationTest.kt +++ b/infra/config-store/src/test/java/com/simprints/infra/config/store/models/ProjectConfigurationTest.kt @@ -1,6 +1,9 @@ package com.simprints.infra.config.store.models import com.google.common.truth.Truth.* +import com.simprints.core.domain.common.AgeGroup +import com.simprints.core.domain.common.Modality +import com.simprints.core.domain.common.ModalitySdkType import com.simprints.infra.config.store.models.UpSynchronizationConfiguration.CoSyncUpSynchronizationConfiguration import com.simprints.infra.config.store.models.UpSynchronizationConfiguration.SimprintsUpSynchronizationConfiguration import com.simprints.infra.config.store.models.UpSynchronizationConfiguration.UpSynchronizationKind.ALL @@ -385,9 +388,21 @@ class ProjectConfigurationTest { fun `isAgeRestricted should return false when all are empty`() { // Arrange val projectConfiguration = projectConfiguration.copy( - face = faceConfiguration.copy(rankOne = faceSdkConfiguration.copy(allowedAgeRange = AgeGroup(0, null))), + face = faceConfiguration.copy( + rankOne = faceSdkConfiguration.copy( + allowedAgeRange = AgeGroup( + 0, + null, + ), + ), + ), fingerprint = fingerprintConfiguration.copy( - secugenSimMatcher = fingerprintConfiguration.secugenSimMatcher?.copy(allowedAgeRange = AgeGroup(0, null)), + secugenSimMatcher = fingerprintConfiguration.secugenSimMatcher?.copy( + allowedAgeRange = AgeGroup( + 0, + null, + ), + ), nec = null, ), ) @@ -499,7 +514,7 @@ class ProjectConfigurationTest { AgeGroup(faceAgeRange.startInclusive, secugenSimMatcherAgeRange.startInclusive), AgeGroup(secugenSimMatcherAgeRange.startInclusive, faceAgeRange.endExclusive), AgeGroup(faceAgeRange.endExclusive!!, secugenSimMatcherAgeRange.endExclusive!!), - AgeGroup(secugenSimMatcherAgeRange.endExclusive, null), + AgeGroup(secugenSimMatcherAgeRange.endExclusive!!, null), ) assertThat(result).isEqualTo(expected) @@ -691,70 +706,118 @@ class ProjectConfigurationTest { @Test fun `determineFaceSDKs returns all allowed SDKs when not age restricted`() { val config = createAgeUnrestrictedFaceConfig() - val result = config.determineFaceSDKs(AgeGroup(25, 30)) + val result = config.getSdkListForAgeGroup(Modality.FACE, AgeGroup(25, 30)) assertThat(result).containsExactly(FaceConfiguration.BioSdk.RANK_ONE, FaceConfiguration.BioSdk.SIM_FACE) } @Test fun `determineFaceSDKs returns empty list when age group is null and age restricted`() { - val config = createAgeRestrictedFaceConfig(rankOneRange = AgeGroup(10, 20), simFaceRange = AgeGroup(20, 30)) - val result = config.determineFaceSDKs(null) + val config = createAgeRestrictedFaceConfig( + rankOneRange = AgeGroup( + 10, + 20, + ), + simFaceRange = AgeGroup(20, 30), + ) + val result = config.getSdkListForAgeGroup(Modality.FACE, null) assertThat(result).isEmpty() } @Test fun `determineFaceSDKs returns only RankOne when age group matches RankOne range`() { - val config = createAgeRestrictedFaceConfig(rankOneRange = AgeGroup(10, 20), simFaceRange = AgeGroup(20, 30)) - val result = config.determineFaceSDKs(AgeGroup(10, 20)) + val config = createAgeRestrictedFaceConfig( + rankOneRange = AgeGroup( + 10, + 20, + ), + simFaceRange = AgeGroup(20, 30), + ) + val result = config.getSdkListForAgeGroup(Modality.FACE, AgeGroup(10, 20)) assertThat(result).containsExactly(FaceConfiguration.BioSdk.RANK_ONE) } @Test fun `determineFaceSDKs returns only SimFace when age group matches SimFace range`() { - val config = createAgeRestrictedFaceConfig(rankOneRange = AgeGroup(10, 20), simFaceRange = AgeGroup(20, 30)) - val result = config.determineFaceSDKs(AgeGroup(20, 30)) + val config = createAgeRestrictedFaceConfig( + rankOneRange = AgeGroup( + 10, + 20, + ), + simFaceRange = AgeGroup(20, 30), + ) + val result = config.getSdkListForAgeGroup(Modality.FACE, AgeGroup(20, 30)) assertThat(result).containsExactly(FaceConfiguration.BioSdk.SIM_FACE) } @Test fun `determineFaceSDKs returns both SDKs when age group matches both ranges`() { - val config = createAgeRestrictedFaceConfig(rankOneRange = AgeGroup(10, 30), simFaceRange = AgeGroup(15, 25)) - val result = config.determineFaceSDKs(AgeGroup(15, 25)) + val config = createAgeRestrictedFaceConfig( + rankOneRange = AgeGroup( + 10, + 30, + ), + simFaceRange = AgeGroup(15, 25), + ) + val result = config.getSdkListForAgeGroup(Modality.FACE, AgeGroup(15, 25)) assertThat(result).containsExactly(FaceConfiguration.BioSdk.RANK_ONE, FaceConfiguration.BioSdk.SIM_FACE) } @Test fun `determineFaceSDKs returns empty list when age group matches no ranges`() { - val config = createAgeRestrictedFaceConfig(rankOneRange = AgeGroup(10, 20), simFaceRange = AgeGroup(20, 30)) - val result = config.determineFaceSDKs(AgeGroup(30, 40)) + val config = createAgeRestrictedFaceConfig( + rankOneRange = AgeGroup( + 10, + 20, + ), + simFaceRange = AgeGroup(20, 30), + ) + val result = config.getSdkListForAgeGroup(Modality.FACE, AgeGroup(30, 40)) assertThat(result).isEmpty() } @Test fun `determineFingerprintSDKs returns all allowed SDKs when not age restricted`() { val config = createAgeUnrestrictedFingerprintConfig() - val result = config.determineFingerprintSDKs(AgeGroup(25, 30)) + val result = config.getSdkListForAgeGroup(Modality.FINGERPRINT, AgeGroup(25, 30)) assertThat(result).containsExactly(FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER, FingerprintConfiguration.BioSdk.NEC) } @Test fun `determineFingerprintSDKs returns empty list when age group is null and age restricted`() { - val config = createAgeRestrictedFingerprintConfig(secugenRange = AgeGroup(10, 20), necRange = AgeGroup(20, 30)) - val result = config.determineFingerprintSDKs(null) + val config = createAgeRestrictedFingerprintConfig( + secugenRange = AgeGroup( + 10, + 20, + ), + necRange = AgeGroup(20, 30), + ) + val result = config.getSdkListForAgeGroup(Modality.FINGERPRINT, null) assertThat(result).isEmpty() } @Test fun `determineFingerprintSDKs returns only SecugenSimMatcher when age group matches SecugenSimMatcher range`() { - val config = createAgeRestrictedFingerprintConfig(secugenRange = AgeGroup(10, 20), necRange = AgeGroup(20, 30)) - val result = config.determineFingerprintSDKs(AgeGroup(10, 20)) + val config = createAgeRestrictedFingerprintConfig( + secugenRange = AgeGroup( + 10, + 20, + ), + necRange = AgeGroup(20, 30), + ) + val result = config.getSdkListForAgeGroup(Modality.FINGERPRINT, AgeGroup(10, 20)) assertThat(result).containsExactly(FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER) } @Test fun `determineFingerprintSDKs returns empty list when age group matches no ranges`() { - val config = createAgeRestrictedFingerprintConfig(secugenRange = AgeGroup(10, 20), necRange = AgeGroup(20, 30)) - val result = config.determineFingerprintSDKs(AgeGroup(30, 40)) + val config = createAgeRestrictedFingerprintConfig( + secugenRange = AgeGroup( + 10, + 20, + ), + necRange = AgeGroup(20, 30), + ) + val result = config.getSdkListForAgeGroup(Modality.FINGERPRINT, AgeGroup(30, 40)) assertThat(result).isEmpty() } @@ -779,7 +842,12 @@ class ProjectConfigurationTest { private fun createAgeUnrestrictedFingerprintConfig() = projectConfiguration.copy( fingerprint = fingerprintConfiguration.copy( allowedSDKs = listOf(FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER, FingerprintConfiguration.BioSdk.NEC), - secugenSimMatcher = fingerprintConfiguration.secugenSimMatcher?.copy(allowedAgeRange = AgeGroup(0, null)), + secugenSimMatcher = fingerprintConfiguration.secugenSimMatcher?.copy( + allowedAgeRange = AgeGroup( + 0, + null, + ), + ), nec = fingerprintConfiguration.nec?.copy(allowedAgeRange = AgeGroup(0, null)), ), ) @@ -793,4 +861,28 @@ class ProjectConfigurationTest { nec = fingerprintConfiguration.nec?.copy(allowedAgeRange = necRange), ), ) + + @Test + fun `getModalitySdkConfig returns correct SDK configuration`() { + // Marking sdks using the common interface field to use in equality checks + val config = projectConfiguration.copy( + face = faceConfiguration.copy( + rankOne = faceSdkConfiguration.copy(verificationMatchThreshold = 1f), + simFace = faceSdkConfiguration.copy(verificationMatchThreshold = 5f), + ), + fingerprint = fingerprintConfiguration.copy( + secugenSimMatcher = fingerprintSdkConfiguration.copy(verificationMatchThreshold = 20f), + nec = fingerprintSdkConfiguration.copy(verificationMatchThreshold = 30f), + ), + ) + + mapOf( + FaceConfiguration.BioSdk.SIM_FACE to 5f, + FaceConfiguration.BioSdk.RANK_ONE to 1f, + FingerprintConfiguration.BioSdk.NEC to 30f, + FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER to 20f, + ).forEach { (type, expected) -> + assertThat(config.getModalitySdkConfig(type)?.verificationMatchThreshold).isEqualTo(expected) + } + } } diff --git a/infra/config-store/src/test/java/com/simprints/infra/config/store/remote/models/ApiAllowedAgeRangeTest.kt b/infra/config-store/src/test/java/com/simprints/infra/config/store/remote/models/ApiAllowedAgeRangeTest.kt index ce1a456d36..b6868ab45e 100644 --- a/infra/config-store/src/test/java/com/simprints/infra/config/store/remote/models/ApiAllowedAgeRangeTest.kt +++ b/infra/config-store/src/test/java/com/simprints/infra/config/store/remote/models/ApiAllowedAgeRangeTest.kt @@ -1,7 +1,7 @@ package com.simprints.infra.config.store.remote.models import com.google.common.truth.Truth.assertThat -import com.simprints.infra.config.store.models.AgeGroup +import com.simprints.core.domain.common.AgeGroup import org.junit.Test class ApiAllowedAgeRangeTest { diff --git a/infra/config-store/src/test/java/com/simprints/infra/config/store/remote/models/ApiFaceConfigurationTest.kt b/infra/config-store/src/test/java/com/simprints/infra/config/store/remote/models/ApiFaceConfigurationTest.kt index 593ffadfe7..36df4efc2a 100644 --- a/infra/config-store/src/test/java/com/simprints/infra/config/store/remote/models/ApiFaceConfigurationTest.kt +++ b/infra/config-store/src/test/java/com/simprints/infra/config/store/remote/models/ApiFaceConfigurationTest.kt @@ -1,7 +1,7 @@ package com.simprints.infra.config.store.remote.models import com.google.common.truth.Truth.assertThat -import com.simprints.infra.config.store.models.AgeGroup +import com.simprints.core.domain.common.AgeGroup import com.simprints.infra.config.store.models.FaceConfiguration import com.simprints.infra.config.store.testtools.apiFaceConfiguration import com.simprints.infra.config.store.testtools.faceConfiguration diff --git a/infra/config-store/src/test/java/com/simprints/infra/config/store/remote/models/ApiFingerprintConfigurationTest.kt b/infra/config-store/src/test/java/com/simprints/infra/config/store/remote/models/ApiFingerprintConfigurationTest.kt index b6078af2cf..d56b56e080 100644 --- a/infra/config-store/src/test/java/com/simprints/infra/config/store/remote/models/ApiFingerprintConfigurationTest.kt +++ b/infra/config-store/src/test/java/com/simprints/infra/config/store/remote/models/ApiFingerprintConfigurationTest.kt @@ -1,8 +1,8 @@ package com.simprints.infra.config.store.remote.models import com.google.common.truth.Truth.* +import com.simprints.core.domain.common.AgeGroup import com.simprints.core.domain.sample.SampleIdentifier -import com.simprints.infra.config.store.models.AgeGroup import com.simprints.infra.config.store.models.FingerprintConfiguration import com.simprints.infra.config.store.models.MaxCaptureAttempts import com.simprints.infra.config.store.models.Vero1Configuration 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 0bad324692..c9521af088 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 @@ -1,5 +1,6 @@ package com.simprints.infra.config.store.testtools +import com.simprints.core.domain.common.AgeGroup import com.simprints.core.domain.common.Modality import com.simprints.core.domain.externalcredential.ExternalCredentialType import com.simprints.core.domain.sample.SampleIdentifier @@ -26,7 +27,6 @@ import com.simprints.infra.config.store.local.models.ProtoUpSyncBatchSizes import com.simprints.infra.config.store.local.models.ProtoUpSynchronizationConfiguration import com.simprints.infra.config.store.local.models.ProtoVero1Configuration import com.simprints.infra.config.store.local.models.ProtoVero2Configuration -import com.simprints.infra.config.store.models.AgeGroup import com.simprints.infra.config.store.models.ConsentConfiguration import com.simprints.infra.config.store.models.DecisionPolicy import com.simprints.infra.config.store.models.DeviceConfiguration diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/AgeGroup.kt b/infra/core/src/main/java/com/simprints/core/domain/common/AgeGroup.kt similarity index 76% rename from infra/config-store/src/main/java/com/simprints/infra/config/store/models/AgeGroup.kt rename to infra/core/src/main/java/com/simprints/core/domain/common/AgeGroup.kt index d1fb54b3d6..29132b3e01 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/AgeGroup.kt +++ b/infra/core/src/main/java/com/simprints/core/domain/common/AgeGroup.kt @@ -1,9 +1,10 @@ -package com.simprints.infra.config.store.models +package com.simprints.core.domain.common import androidx.annotation.Keep import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonTypeInfo +import com.simprints.core.domain.step.StepParams import java.io.Serializable @Keep @@ -11,8 +12,11 @@ import java.io.Serializable data class AgeGroup( @JsonProperty("startInclusive") val startInclusive: Int, @JsonProperty("endExclusive") val endExclusive: Int?, -) : Serializable { - @JsonIgnore // prevents Jackson isEmpty unwanted serialization bug, see https://stackoverflow.com/questions/69616587/why-does-jackson-add-an-empty-false-into-the-json +) : StepParams, + Serializable { + // prevents Jackson isEmpty unwanted serialization bug, + // see https://stackoverflow.com/questions/69616587/why-does-jackson-add-an-empty-false-into-the-json + @JsonIgnore fun isEmpty() = startInclusive == 0 && (endExclusive == null || endExclusive == 0) fun includes(age: Int): Boolean { diff --git a/infra/config-store/src/test/java/com/simprints/infra/config/store/models/AgeGroupTest.kt b/infra/core/src/test/java/com/simprints/core/domain/common/AgeGroupTest.kt similarity index 74% rename from infra/config-store/src/test/java/com/simprints/infra/config/store/models/AgeGroupTest.kt rename to infra/core/src/test/java/com/simprints/core/domain/common/AgeGroupTest.kt index c5a115ad4a..8329d897bd 100644 --- a/infra/config-store/src/test/java/com/simprints/infra/config/store/models/AgeGroupTest.kt +++ b/infra/core/src/test/java/com/simprints/core/domain/common/AgeGroupTest.kt @@ -1,129 +1,128 @@ -package com.simprints.infra.config.store.models +package com.simprints.core.domain.common -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue +import org.junit.Assert import org.junit.Test class AgeGroupTest { @Test fun `should return empty when the age group is 0, 0`() { val ageGroup = AgeGroup(0, 0) - assertTrue(ageGroup.isEmpty()) + Assert.assertTrue(ageGroup.isEmpty()) } @Test fun `should return empty when the age group is 0, null`() { val ageGroup = AgeGroup(0, null) - assertTrue(ageGroup.isEmpty()) + Assert.assertTrue(ageGroup.isEmpty()) } @Test fun `should return not empty when the age group is 0, 1`() { val ageGroup = AgeGroup(0, 1) - assertFalse(ageGroup.isEmpty()) + Assert.assertFalse(ageGroup.isEmpty()) } @Test fun `should return not empty when the age group is 1, 0`() { val ageGroup = AgeGroup(1, 0) - assertFalse(ageGroup.isEmpty()) + Assert.assertFalse(ageGroup.isEmpty()) } @Test fun `should return not empty when the age group is 1, null`() { val ageGroup = AgeGroup(1, null) - assertFalse(ageGroup.isEmpty()) + Assert.assertFalse(ageGroup.isEmpty()) } @Test fun `should return true when the age is included in the age group`() { val ageGroup = AgeGroup(0, 10) - assertTrue(ageGroup.includes(5)) + Assert.assertTrue(ageGroup.includes(5)) } @Test fun `should return false when the age is not included in the age group`() { val ageGroup = AgeGroup(0, 10) - assertFalse(ageGroup.includes(15)) + Assert.assertFalse(ageGroup.includes(15)) } @Test fun `should return true when endExclusive is null and age is greater than startInclusive`() { val ageGroup = AgeGroup(5, null) - assertTrue(ageGroup.includes(10)) + Assert.assertTrue(ageGroup.includes(10)) } @Test fun `should return true when endExclusive is null and age is equal to startInclusive`() { val ageGroup = AgeGroup(5, null) - assertTrue(ageGroup.includes(5)) + Assert.assertTrue(ageGroup.includes(5)) } @Test fun `should return false when endExclusive is null and age is less than startInclusive`() { val ageGroup = AgeGroup(5, null) - assertFalse(ageGroup.includes(4)) + Assert.assertFalse(ageGroup.includes(4)) } @Test fun `should return false when endExclusive is not null and age is equal to endExclusive`() { val ageGroup = AgeGroup(0, 10) - assertFalse(ageGroup.includes(10)) + Assert.assertFalse(ageGroup.includes(10)) } @Test fun `should return true when the age group contains the other age group`() { val ageGroup = AgeGroup(0, 10) val otherAgeGroup = AgeGroup(5, 8) - assertTrue(ageGroup.contains(otherAgeGroup)) + Assert.assertTrue(ageGroup.contains(otherAgeGroup)) } @Test fun `should return true when the age group contains the other age group 2`() { val ageGroup = AgeGroup(0, 10) val otherAgeGroup = AgeGroup(0, 9) - assertTrue(ageGroup.contains(otherAgeGroup)) + Assert.assertTrue(ageGroup.contains(otherAgeGroup)) } @Test fun `should return true when the age group is the same as the other age group`() { val ageGroup = AgeGroup(0, 10) val otherAgeGroup = AgeGroup(0, 10) - assertTrue(ageGroup.contains(otherAgeGroup)) + Assert.assertTrue(ageGroup.contains(otherAgeGroup)) } @Test fun `should return false when the age group does not contain the other age group`() { val ageGroup = AgeGroup(0, 10) val otherAgeGroup = AgeGroup(5, 15) - assertFalse(ageGroup.contains(otherAgeGroup)) + Assert.assertFalse(ageGroup.contains(otherAgeGroup)) } @Test fun `should return false when otherRange endExclusive is null and is contained within ageGroup`() { val ageGroup = AgeGroup(5, 10) val otherAgeGroup = AgeGroup(7, null) - assertFalse(ageGroup.contains(otherAgeGroup)) + Assert.assertFalse(ageGroup.contains(otherAgeGroup)) } @Test fun `should return true when otherRange endExclusive is null and is contained within ageGroup`() { val ageGroup = AgeGroup(0, null) val otherAgeGroup = AgeGroup(5, null) - assertTrue(ageGroup.contains(otherAgeGroup)) + Assert.assertTrue(ageGroup.contains(otherAgeGroup)) } @Test fun `should return false when otherRange startInclusive is less than startInclusive and otherRange endExclusive is within ageGroup range`() { val ageGroup = AgeGroup(5, 10) val otherAgeGroup = AgeGroup(4, 9) - assertFalse(ageGroup.contains(otherAgeGroup)) + Assert.assertFalse(ageGroup.contains(otherAgeGroup)) } @Test fun `should return false when otherRange startInclusive is within ageGroup range and otherRange endExclusive is greater than endExclusive`() { val ageGroup = AgeGroup(5, 10) val otherAgeGroup = AgeGroup(6, 11) - assertFalse(ageGroup.contains(otherAgeGroup)) + Assert.assertFalse(ageGroup.contains(otherAgeGroup)) } } diff --git a/infra/enrolment-records/repository/src/androidTest/kotlin/com/simprints/infra/enrolment/records/repository/local/RealmEnrolmentRecordLocalDataSourceIntegrationTest.kt b/infra/enrolment-records/repository/src/androidTest/kotlin/com/simprints/infra/enrolment/records/repository/local/RealmEnrolmentRecordLocalDataSourceIntegrationTest.kt index 02a39ae6ff..7f4aa067a3 100644 --- a/infra/enrolment-records/repository/src/androidTest/kotlin/com/simprints/infra/enrolment/records/repository/local/RealmEnrolmentRecordLocalDataSourceIntegrationTest.kt +++ b/infra/enrolment-records/repository/src/androidTest/kotlin/com/simprints/infra/enrolment/records/repository/local/RealmEnrolmentRecordLocalDataSourceIntegrationTest.kt @@ -5,7 +5,6 @@ import com.google.common.truth.Truth.* import com.simprints.core.domain.common.Modality import com.simprints.core.domain.externalcredential.ExternalCredential import com.simprints.core.domain.externalcredential.ExternalCredentialType -import com.simprints.core.domain.sample.Identity import com.simprints.core.domain.sample.Sample import com.simprints.core.domain.sample.SampleIdentifier import com.simprints.core.domain.tokenization.asTokenizableEncrypted @@ -235,7 +234,7 @@ class RealmEnrolmentRecordLocalDataSourceIntegrationTest { // Given val subjectId = UUID.randomUUID().toString() val originalSubject = createTestSubject(subjectId) - originalSubject.faceSamples = listOf( + originalSubject.samples = listOf( Sample( template = byteArrayOf(), format = "ISO", @@ -255,15 +254,13 @@ class RealmEnrolmentRecordLocalDataSourceIntegrationTest { val updateAction = SubjectAction.Update( subjectId, - faceSamplesToAdd = listOf( + samplesToAdd = listOf( Sample( template = byteArrayOf(1, 2, 3), format = "ISO", referenceId = "ref2", modality = Modality.FACE, ), - ), - fingerprintSamplesToAdd = listOf( Sample( template = byteArrayOf(4, 5, 6), format = "ISO", @@ -309,11 +306,11 @@ class RealmEnrolmentRecordLocalDataSourceIntegrationTest { } @Test - fun givenManySubjectsWithFaceSamples_whenLoadFaceIdentitiesIsCalledWithRanges_thenReturnsBatchedFaceIdentities() = runTest { + fun givenManySubjectsWithFaceSamples_whenLoadIdentitiesIsCalledWithRanges_thenReturnsBatchedIdentities() = runTest { // Given val subjects = (1..10).map { i -> createTestSubject(subjectId = UUID.randomUUID().toString()).apply { - faceSamples = listOf( + samples = listOf( Sample( template = byteArrayOf(i.toByte()), format = "ISO", @@ -328,12 +325,12 @@ class RealmEnrolmentRecordLocalDataSourceIntegrationTest { mockk(), ) - val query = SubjectQuery(faceSampleFormat = "ISO") + val query = SubjectQuery(format = "ISO") val ranges = listOf(0..2, 3..5, 6..9) // 3 batches val loadedCandidates = mutableListOf() // When - val channel = dataSource.loadFaceIdentities( + val channel = dataSource.loadIdentities( query = query, ranges = ranges, dataSource = mockk(), @@ -342,7 +339,7 @@ class RealmEnrolmentRecordLocalDataSourceIntegrationTest { onCandidateLoaded = { loadedCandidates.add(Unit) }, ) - val results = mutableListOf>() + val results = mutableListOf() for (batch in channel) { results.add(batch) } @@ -356,54 +353,53 @@ class RealmEnrolmentRecordLocalDataSourceIntegrationTest { } @Test - fun givenManySubjectsWithFingerprintSamples_whenLoadFingerprintIdentitiesIsCalledWithRanges_thenReturnsBatchedFingerprintIdentities() = - runTest { - // Given - val subjects = (1..10).map { i -> - createTestSubject(subjectId = UUID.randomUUID().toString()).apply { - fingerprintSamples = listOf( - Sample( - template = byteArrayOf(i.toByte()), - format = "ISO", - referenceId = "ref$i", - identifier = SampleIdentifier.LEFT_THUMB, - modality = Modality.FINGERPRINT, - ), - ) - } - } - dataSource.performActions( - subjects.map { SubjectAction.Creation(it) }, - mockk(), - ) - - val query = SubjectQuery(fingerprintSampleFormat = "ISO") - val ranges = listOf(0..2, 3..5, 6..9) // 3 batches - val loadedCandidates = mutableListOf() - - // When - val channel = dataSource.loadFingerprintIdentities( - query = query, - ranges = ranges, - dataSource = mockk(), - project = mockk(), - scope = this, - onCandidateLoaded = { loadedCandidates.add(Unit) }, - ) - - val results = mutableListOf>() - for (batch in channel) { - results.add(batch) + fun givenManySubjectsWithFingerprintSamples_whenFingerprintIdentitiesIsCalledWithRanges_thenReturnsBatchedIdentities() = runTest { + // Given + val subjects = (1..10).map { i -> + createTestSubject(subjectId = UUID.randomUUID().toString()).apply { + samples = listOf( + Sample( + template = byteArrayOf(i.toByte()), + format = "ISO", + referenceId = "ref$i", + identifier = SampleIdentifier.LEFT_THUMB, + modality = Modality.FINGERPRINT, + ), + ) } + } + dataSource.performActions( + subjects.map { SubjectAction.Creation(it) }, + mockk(), + ) + + val query = SubjectQuery(format = "ISO") + val ranges = listOf(0..2, 3..5, 6..9) // 3 batches + val loadedCandidates = mutableListOf() - // Then - assertThat(results).hasSize(3) - assertThat(results[0].identities).hasSize(3) - assertThat(results[1].identities).hasSize(3) - assertThat(results[2].identities).hasSize(4) - assertThat(loadedCandidates).hasSize(10) + // When + val channel = dataSource.loadIdentities( + query = query, + ranges = ranges, + dataSource = mockk(), + project = mockk(), + scope = this, + onCandidateLoaded = { loadedCandidates.add(Unit) }, + ) + + val results = mutableListOf() + for (batch in channel) { + results.add(batch) } + // Then + assertThat(results).hasSize(3) + assertThat(results[0].identities).hasSize(3) + assertThat(results[1].identities).hasSize(3) + assertThat(results[2].identities).hasSize(4) + assertThat(loadedCandidates).hasSize(10) + } + @Test fun givenManySubjects_whenLoadAllSubjectsInBatchesIsCalled_thenReturnsSubjectsInBatches() = runTest { // Given diff --git a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/EnrolmentRecordRepositoryImpl.kt b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/EnrolmentRecordRepositoryImpl.kt index 7dd4a975be..43a6d5d0d1 100644 --- a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/EnrolmentRecordRepositoryImpl.kt +++ b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/EnrolmentRecordRepositoryImpl.kt @@ -108,30 +108,14 @@ internal class EnrolmentRecordRepositoryImpl @Inject constructor( override suspend fun getLocalDBInfo(): String = selectEnrolmentRecordLocalDataSource().getLocalDBInfo() - override suspend fun loadFingerprintIdentities( + override suspend fun loadIdentities( query: SubjectQuery, ranges: List, dataSource: BiometricDataSource, project: Project, scope: CoroutineScope, onCandidateLoaded: suspend () -> Unit, - ) = fromIdentityDataSource(dataSource).loadFingerprintIdentities( - query = query, - ranges = ranges, - dataSource = dataSource, - project = project, - scope = scope, - onCandidateLoaded = onCandidateLoaded, - ) - - override suspend fun loadFaceIdentities( - query: SubjectQuery, - ranges: List, - dataSource: BiometricDataSource, - project: Project, - scope: CoroutineScope, - onCandidateLoaded: suspend () -> Unit, - ) = fromIdentityDataSource(dataSource).loadFaceIdentities( + ) = fromIdentityDataSource(dataSource).loadIdentities( query = query, ranges = ranges, dataSource = dataSource, diff --git a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/IdentityDataSource.kt b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/IdentityDataSource.kt index 0828fa9ea6..068749d68f 100644 --- a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/IdentityDataSource.kt +++ b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/IdentityDataSource.kt @@ -1,6 +1,5 @@ package com.simprints.infra.enrolment.records.repository -import com.simprints.core.domain.sample.Identity import com.simprints.infra.config.store.models.Project import com.simprints.infra.enrolment.records.repository.domain.models.BiometricDataSource import com.simprints.infra.enrolment.records.repository.domain.models.IdentityBatch @@ -14,23 +13,14 @@ interface IdentityDataSource { dataSource: BiometricDataSource = BiometricDataSource.Simprints, ): Int - suspend fun loadFingerprintIdentities( + suspend fun loadIdentities( query: SubjectQuery, ranges: List, dataSource: BiometricDataSource, project: Project, scope: CoroutineScope, onCandidateLoaded: suspend () -> Unit, - ): ReceiveChannel> - - suspend fun loadFaceIdentities( - query: SubjectQuery, - ranges: List, - dataSource: BiometricDataSource, - project: Project, - scope: CoroutineScope, - onCandidateLoaded: suspend () -> Unit, - ): ReceiveChannel> + ): ReceiveChannel /** * Loads identities concurrently using the provided dispatcher and parallelism level. diff --git a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/commcare/CommCareIdentityDataSource.kt b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/commcare/CommCareIdentityDataSource.kt index 11cc9fd2f7..a6c0619363 100644 --- a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/commcare/CommCareIdentityDataSource.kt +++ b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/commcare/CommCareIdentityDataSource.kt @@ -59,27 +59,85 @@ internal class CommCareIdentityDataSource @Inject constructor( private fun getCaseDataUri(packageName: String): Uri = "content://$packageName.case/casedb/data".toUri() - private suspend fun loadFingerprintIdentities( + override suspend fun loadIdentities( + query: SubjectQuery, + ranges: List, + dataSource: BiometricDataSource, + project: Project, + scope: CoroutineScope, + onCandidateLoaded: suspend () -> Unit, + ): ReceiveChannel = loadIdentitiesConcurrently( + ranges = ranges, + scope = scope, + ) { range -> + val startTime = timeHelper.now() + val identities = loadIdentities( + query = query, + range = range, + project = project, + dataSource = dataSource, + onCandidateLoaded = onCandidateLoaded, + ) + val endTime = timeHelper.now() + IdentityBatch(identities, startTime, endTime) + } + + private fun loadIdentitiesConcurrently( + ranges: List, + scope: CoroutineScope, + load: suspend (IntRange) -> IdentityBatch, + ): ReceiveChannel { + val channel = Channel(availableProcessors) + val semaphore = Semaphore(availableProcessors) + scope.launch(dispatcher) { + ranges + .map { range -> + async { semaphore.withPermit { channel.send(load(range)) } } + }.joinAll() + channel.close() + } + return channel + } + + private suspend fun loadIdentities( query: SubjectQuery, range: IntRange, dataSource: BiometricDataSource, project: Project, onCandidateLoaded: suspend () -> Unit, ): List = loadEnrolmentRecordCreationEvents(range, dataSource.callerPackageName(), query, project, onCandidateLoaded) - .filter { erce -> - erce.payload.biometricReferences.any { it is FingerprintReference && it.format == query.fingerprintSampleFormat } - }.map { + .filter { erce -> erce.payload.biometricReferences.any { it.format == query.format } } + .map { erce -> Identity( - it.payload.subjectId, - it.payload.biometricReferences.filterIsInstance().flatMap { fingerprintReference -> - fingerprintReference.templates.map { fingerprintTemplate -> - Sample( - identifier = fingerprintTemplate.finger, - template = encoder.base64ToBytes(fingerprintTemplate.template), - format = fingerprintReference.format, - referenceId = fingerprintReference.id, - modality = Modality.FINGERPRINT, - ) + erce.payload.subjectId, + erce.payload.biometricReferences.flatMap { reference -> + when (reference) { + is FaceReference -> reference.templates.mapNotNull { faceTemplate -> + if (reference.format != query.format) { + null + } else { + Sample( + template = encoder.base64ToBytes(faceTemplate.template), + format = reference.format, + referenceId = reference.id, + modality = Modality.FACE, + ) + } + } + + is FingerprintReference -> reference.templates.mapNotNull { fingerprintTemplate -> + if (reference.format != query.format) { + null + } else { + Sample( + identifier = fingerprintTemplate.finger, + template = encoder.base64ToBytes(fingerprintTemplate.template), + format = reference.format, + referenceId = reference.id, + modality = Modality.FINGERPRINT, + ) + } + } } }, ) @@ -112,9 +170,8 @@ internal class CommCareIdentityDataSource @Inject constructor( do { caseMetadataCursor.getString(caseMetadataCursor.getColumnIndexOrThrow(COLUMN_CASE_ID))?.let { caseId -> enrolmentRecordCreationEvents - .addAll( - loadEnrolmentRecordCreationEvents(caseId, callerPackageName, query, project), - ).also { onCandidateLoaded() } + .addAll(loadEnrolmentRecordCreationEvents(caseId, callerPackageName, query, project)) + .also { onCandidateLoaded() } } } while (caseMetadataCursor.moveToNext() && caseMetadataCursor.position <= range.last) } @@ -126,31 +183,6 @@ internal class CommCareIdentityDataSource @Inject constructor( return enrolmentRecordCreationEvents } - private suspend fun loadFaceIdentities( - query: SubjectQuery, - range: IntRange, - dataSource: BiometricDataSource, - project: Project, - onCandidateLoaded: suspend () -> Unit, - ): List = loadEnrolmentRecordCreationEvents(range, dataSource.callerPackageName(), query, project, onCandidateLoaded) - .filter { erce -> - erce.payload.biometricReferences.any { it is FaceReference && it.format == query.faceSampleFormat } - }.map { - Identity( - it.payload.subjectId, - it.payload.biometricReferences.filterIsInstance().flatMap { faceReference -> - faceReference.templates.map { faceTemplate -> - Sample( - template = encoder.base64ToBytes(faceTemplate.template), - format = faceReference.format, - referenceId = faceReference.id, - modality = Modality.FACE, - ) - } - }, - ) - } - private fun loadEnrolmentRecordCreationEvents( caseId: String, callerPackageName: String, @@ -269,73 +301,6 @@ internal class CommCareIdentityDataSource @Inject constructor( count } - override suspend fun loadFaceIdentities( - query: SubjectQuery, - ranges: List, - dataSource: BiometricDataSource, - project: Project, - scope: CoroutineScope, - onCandidateLoaded: suspend () -> Unit, - ): ReceiveChannel> = loadIdentitiesConcurrently( - ranges = ranges, - scope = scope, - ) { range -> - val startTime = timeHelper.now() - val identities = loadFaceIdentities( - query = query, - range = range, - project = project, - dataSource = dataSource, - onCandidateLoaded = onCandidateLoaded, - ) - val endTime = timeHelper.now() - IdentityBatch(identities, startTime, endTime) - } - - override suspend fun loadFingerprintIdentities( - query: SubjectQuery, - ranges: List, - dataSource: BiometricDataSource, - project: Project, - scope: CoroutineScope, - onCandidateLoaded: suspend () -> Unit, - ): ReceiveChannel> = loadIdentitiesConcurrently( - ranges = ranges, - scope = scope, - ) { range -> - val startTime = timeHelper.now() - val identities = loadFingerprintIdentities( - query = query, - range = range, - project = project, - dataSource = dataSource, - onCandidateLoaded = onCandidateLoaded, - ) - val endTime = timeHelper.now() - IdentityBatch(identities, startTime, endTime) - } - - private fun loadIdentitiesConcurrently( - ranges: List, - scope: CoroutineScope, - load: suspend (IntRange) -> IdentityBatch, - ): ReceiveChannel> { - val channel = Channel>(availableProcessors) - val semaphore = Semaphore(availableProcessors) - scope.launch(dispatcher) { - ranges - .map { range -> - async { - semaphore.withPermit { - channel.send(load(range)) - } - } - }.joinAll() - channel.close() - } - return channel - } - companion object { const val COLUMN_CASE_ID = "case_id" const val COLUMN_DATUM_ID = "datum_id" diff --git a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/IdentityBatch.kt b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/IdentityBatch.kt index f79262e008..e52f5f6978 100644 --- a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/IdentityBatch.kt +++ b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/IdentityBatch.kt @@ -1,9 +1,10 @@ package com.simprints.infra.enrolment.records.repository.domain.models +import com.simprints.core.domain.sample.Identity import com.simprints.core.tools.time.Timestamp -data class IdentityBatch( - val identities: List, +data class IdentityBatch( + val identities: List, val loadingStartTime: Timestamp, val loadingEndTime: Timestamp, ) diff --git a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/Subject.kt b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/Subject.kt index befdef7283..1e6f50cac6 100644 --- a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/Subject.kt +++ b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/Subject.kt @@ -15,7 +15,6 @@ data class Subject( val moduleId: TokenizableString, val createdAt: Date? = null, val updatedAt: Date? = null, - var fingerprintSamples: List = emptyList(), - var faceSamples: List = emptyList(), + var samples: List = emptyList(), var externalCredentials: List = emptyList(), ) : Parcelable diff --git a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/SubjectAction.kt b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/SubjectAction.kt index 1ec3d43782..b9ce0ea0e6 100644 --- a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/SubjectAction.kt +++ b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/SubjectAction.kt @@ -12,8 +12,7 @@ sealed class SubjectAction { data class Update( val subjectId: String, - val faceSamplesToAdd: List, - val fingerprintSamplesToAdd: List, + val samplesToAdd: List, val referenceIdsToRemove: List, val externalCredentialsToAdd: List, val externalCredentialIdsToRemove: List, diff --git a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/SubjectQuery.kt b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/SubjectQuery.kt index 3d982dd3db..3a2e5f29b0 100644 --- a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/SubjectQuery.kt +++ b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/SubjectQuery.kt @@ -10,8 +10,7 @@ data class SubjectQuery( val subjectId: String? = null, val subjectIds: List? = null, val attendantId: TokenizableString? = null, - val fingerprintSampleFormat: String? = null, - val faceSampleFormat: String? = null, + val format: String? = null, val hasUntokenizedFields: Boolean? = null, val moduleId: TokenizableString? = null, val sort: Boolean = false, diff --git a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/RealmEnrolmentRecordLocalDataSource.kt b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/RealmEnrolmentRecordLocalDataSource.kt index d5340e2935..db45ff1d43 100644 --- a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/RealmEnrolmentRecordLocalDataSource.kt +++ b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/RealmEnrolmentRecordLocalDataSource.kt @@ -1,6 +1,7 @@ package com.simprints.infra.enrolment.records.repository.local import com.simprints.core.DispatcherIO +import com.simprints.core.domain.common.Modality import com.simprints.core.domain.sample.Identity import com.simprints.core.tools.time.TimeHelper import com.simprints.infra.config.store.models.Project @@ -68,15 +69,15 @@ internal class RealmEnrolmentRecordLocalDataSource @Inject constructor( .map { dbSubject -> dbSubject.toDomain() } } - override suspend fun loadFaceIdentities( + override suspend fun loadIdentities( query: SubjectQuery, ranges: List, dataSource: BiometricDataSource, project: Project, scope: CoroutineScope, onCandidateLoaded: suspend () -> Unit, - ): ReceiveChannel> { - val channel = Channel>(CHANNEL_CAPACITY) + ): ReceiveChannel { + val channel = Channel(CHANNEL_CAPACITY) scope.launch(dispatcherIO) { ranges.forEach { range -> val startTime = timeHelper.now() @@ -86,9 +87,10 @@ internal class RealmEnrolmentRecordLocalDataSource @Inject constructor( mapper = { dbSubject -> Identity( subjectId = dbSubject.subjectId.toString(), - samples = dbSubject.faceSamples - .filter { it.format == query.faceSampleFormat } - .map { it.toDomain() }, + samples = ( + dbSubject.faceSamples.map { it.toDomain() } + + dbSubject.fingerprintSamples.map { it.toDomain() } + ).filter { it.format == query.format }, ) }, onCandidateLoaded = onCandidateLoaded, @@ -101,51 +103,19 @@ internal class RealmEnrolmentRecordLocalDataSource @Inject constructor( return channel } - override suspend fun loadFingerprintIdentities( - query: SubjectQuery, - ranges: List, - dataSource: BiometricDataSource, - project: Project, - scope: CoroutineScope, - onCandidateLoaded: suspend () -> Unit, - ): ReceiveChannel> { - val channel = Channel>(CHANNEL_CAPACITY) - scope.launch(dispatcherIO) { - ranges.forEach { range -> - val startTime = timeHelper.now() - val identities = loadIdentitiesRange( - query = query, - range = range, - mapper = { dbSubject -> - Identity( - subjectId = dbSubject.subjectId.toString(), - samples = dbSubject.fingerprintSamples - .filter { it.format == query.fingerprintSampleFormat } - .map { it.toDomain() }, - ) - }, - onCandidateLoaded = onCandidateLoaded, - ) - val endTime = timeHelper.now() - channel.send(IdentityBatch(identities, startTime, endTime)) - } - channel.close() - } - return channel - } - - private suspend fun loadIdentitiesRange( + private suspend fun loadIdentitiesRange( query: SubjectQuery, range: IntRange, - mapper: (DbSubject) -> T, + mapper: (DbSubject) -> Identity, onCandidateLoaded: suspend () -> Unit, - ): List = realmWrapper.readRealm { realm -> + ): List = realmWrapper.readRealm { realm -> realm .query(DbSubject::class) .buildRealmQueryForSubject(query) // subList's second parameter is exclusive, so we need to add 1 to the last index - .find { it.subList(range.first, range.last + 1) } - .map { dbSubject -> + .find { + it.subList(range.first, range.last + 1) + }.map { dbSubject -> onCandidateLoaded() mapper(dbSubject) } @@ -246,10 +216,14 @@ internal class RealmEnrolmentRecordLocalDataSource @Inject constructor( // Append new samples to the list of samples that remain after removing dbSubject.faceSamples = ( - faceSamplesMap[false].orEmpty() + action.faceSamplesToAdd.map { it.toRealmFaceDb() } + faceSamplesMap[false].orEmpty() + action.samplesToAdd + .filter { it.modality == Modality.FACE } + .map { it.toRealmFaceDb() } ).toRealmList() dbSubject.fingerprintSamples = ( - fingerprintSamplesMap[false].orEmpty() + action.fingerprintSamplesToAdd.map { it.toRealmFingerprintDb() } + fingerprintSamplesMap[false].orEmpty() + action.samplesToAdd + .filter { it.modality == Modality.FINGERPRINT } + .map { it.toRealmFingerprintDb() } ).toRealmList() dbSubject.externalCredentials = allExternalCredentials.toRealmList() @@ -317,16 +291,11 @@ internal class RealmEnrolmentRecordLocalDataSource @Inject constructor( if (query.moduleId != null) { realmQuery = realmQuery.query("$MODULE_ID_FIELD == $0", query.moduleId.value) } - if (query.fingerprintSampleFormat != null) { - realmQuery = realmQuery.query( - "ANY $FINGERPRINT_SAMPLES_FIELD.$FORMAT_FIELD == $0", - query.fingerprintSampleFormat, - ) - } - if (query.faceSampleFormat != null) { + if (query.format != null) { realmQuery = realmQuery.query( - "ANY $FACE_SAMPLES_FIELD.$FORMAT_FIELD == $0", - query.faceSampleFormat, + "ANY $FINGERPRINT_SAMPLES_FIELD.$FORMAT_FIELD == $0 OR ANY $FACE_SAMPLES_FIELD.$FORMAT_FIELD == $1", + query.format, + query.format, ) } if (query.afterSubjectId != null) { diff --git a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/RoomEnrolmentRecordLocalDataSource.kt b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/RoomEnrolmentRecordLocalDataSource.kt index 292586d900..e9af15d33c 100644 --- a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/RoomEnrolmentRecordLocalDataSource.kt +++ b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/RoomEnrolmentRecordLocalDataSource.kt @@ -2,10 +2,8 @@ package com.simprints.infra.enrolment.records.repository.local import androidx.room.withTransaction import com.simprints.core.DispatcherIO -import com.simprints.core.domain.common.Modality import com.simprints.core.domain.sample.Identity import com.simprints.core.domain.sample.Sample -import com.simprints.core.domain.sample.SampleIdentifier import com.simprints.core.tools.time.TimeHelper import com.simprints.infra.config.store.models.Project import com.simprints.infra.config.store.models.TokenKeyType @@ -22,7 +20,9 @@ import com.simprints.infra.enrolment.records.room.store.SubjectDao import com.simprints.infra.enrolment.records.room.store.SubjectsDatabase import com.simprints.infra.enrolment.records.room.store.SubjectsDatabaseFactory import com.simprints.infra.enrolment.records.room.store.models.DbBiometricTemplate +import com.simprints.infra.enrolment.records.room.store.models.DbModality import com.simprints.infra.enrolment.records.room.store.models.DbSubject +import com.simprints.infra.enrolment.records.room.store.models.toDomain import com.simprints.infra.logging.LoggingConstants.CrashReportTag.ROOM_RECORDS_DB import com.simprints.infra.logging.Simber import kotlinx.coroutines.CoroutineDispatcher @@ -82,64 +82,30 @@ internal class RoomEnrolmentRecordLocalDataSource @Inject constructor( } /** - * Loads face identities in paged ranges. + * Loads identities in paged ranges. */ - override suspend fun loadFaceIdentities( + override suspend fun loadIdentities( query: SubjectQuery, ranges: List, dataSource: BiometricDataSource, project: Project, scope: CoroutineScope, onCandidateLoaded: suspend () -> Unit, - ): ReceiveChannel> = loadBiometricIdentitiesPaged( + ): ReceiveChannel = loadBiometricIdentitiesPaged( query = query, ranges = ranges, - format = requireNotNull(query.faceSampleFormat) { "faceSampleFormat required" }, + format = requireNotNull(query.format) { "format required" }, createIdentity = { subjectId, templates -> Identity( subjectId = subjectId, samples = templates.map { sample -> Sample( - template = sample.templateData, id = sample.uuid, - format = sample.format, - referenceId = sample.referenceId, - modality = Modality.FACE, - ) - }, - ) - }, - onCandidateLoaded = onCandidateLoaded, - scope = scope, - ) - - /** - * Loads fingerprint identities in paged ranges. - */ - override suspend fun loadFingerprintIdentities( - query: SubjectQuery, - ranges: List, - dataSource: BiometricDataSource, - project: Project, - scope: CoroutineScope, - onCandidateLoaded: suspend () -> Unit, - ): ReceiveChannel> = loadBiometricIdentitiesPaged( - query = query, - ranges = ranges, - format = requireNotNull(query.fingerprintSampleFormat) { "fingerprintSampleFormat required" }, - createIdentity = { subjectId, templates -> - Identity( - subjectId = subjectId, - samples = templates.map { sample -> - Sample( - identifier = sample.identifier - ?.let { DbSampleIdentifier.fromId(it)?.toDomain() } - ?: SampleIdentifier.NONE, template = sample.templateData, - id = sample.uuid, + identifier = DbSampleIdentifier.fromId(sample.identifier).toDomain(), format = sample.format, referenceId = sample.referenceId, - modality = Modality.FINGERPRINT, + modality = DbModality.fromId(sample.modality).toDomain(), ) }, ) @@ -148,17 +114,17 @@ internal class RoomEnrolmentRecordLocalDataSource @Inject constructor( scope = scope, ) - private fun loadBiometricIdentitiesPaged( + private fun loadBiometricIdentitiesPaged( query: SubjectQuery, ranges: List, format: String, - createIdentity: (String, List) -> T, + createIdentity: (String, List) -> Identity, onCandidateLoaded: suspend () -> Unit, scope: CoroutineScope, - ): ReceiveChannel> { + ): ReceiveChannel { var afterSubjectId: String? = null var lastOffset = 0 - val channel = Channel>(CHANNEL_CAPACITY) + val channel = Channel(CHANNEL_CAPACITY) scope.launch(dispatcherIO) { ranges .forEach { range -> @@ -174,9 +140,7 @@ internal class RoomEnrolmentRecordLocalDataSource @Inject constructor( createIdentity = createIdentity, onCandidateLoaded = onCandidateLoaded, ) - afterSubjectId = identities.lastOrNull()?.let { - (it as? Identity)?.subjectId ?: (it as? Identity)?.subjectId - } + afterSubjectId = identities.lastOrNull()?.subjectId lastOffset = range.last + 1 val endTime = timeHelper.now() channel.send(IdentityBatch(identities, startTime, endTime)) @@ -186,13 +150,13 @@ internal class RoomEnrolmentRecordLocalDataSource @Inject constructor( return channel } - private suspend fun loadBiometricIdentities( + private suspend fun loadBiometricIdentities( query: SubjectQuery, pageSize: Int, format: String?, - createIdentity: (subjectId: String, samples: List) -> T, + createIdentity: (subjectId: String, samples: List) -> Identity, onCandidateLoaded: suspend () -> Unit, - ): List = withContext(dispatcherIO) { + ): List = withContext(dispatcherIO) { requireNotNull(format) { "Appropriate sampleFormat is required for loading biometric identities." } subjectDao .loadSamples(queryBuilder.buildBiometricTemplatesQuery(query, pageSize)) @@ -263,8 +227,8 @@ internal class RoomEnrolmentRecordLocalDataSource @Inject constructor( subject: DomainSubject, project: Project, ) { - require(subject.faceSamples.isNotEmpty() || subject.fingerprintSamples.isNotEmpty()) { - val errorMsg = "Subject should include at least one of the face or fingerprint samples" + require(subject.samples.isNotEmpty()) { + val errorMsg = "Subject should include at least one sample" Simber.i( "[createSubject] $errorMsg for subjectId: ${subject.subjectId}", tag = ROOM_RECORDS_DB, @@ -290,13 +254,9 @@ internal class RoomEnrolmentRecordLocalDataSource @Inject constructor( updatedAt = subject.updatedAt?.time, ) subjectDao.insertSubject(dbSubject) - subject.fingerprintSamples.takeIf { it.isNotEmpty() }?.let { samples -> - val dbFingerprints = samples.map { it.toRoomDb(subject.subjectId) } - subjectDao.insertBiometricSamples(dbFingerprints) - } - subject.faceSamples.takeIf { it.isNotEmpty() }?.let { samples -> - val dbFaces = samples.map { it.toRoomDb(subject.subjectId) } - subjectDao.insertBiometricSamples(dbFaces) + subject.samples.takeIf { it.isNotEmpty() }?.let { samples -> + val dbSample = samples.map { it.toRoomDb(subject.subjectId) } + subjectDao.insertBiometricSamples(dbSample) } subject.externalCredentials.takeIf { it.isNotEmpty() }?.let { credentials -> val dbExternalCredentials = credentials.map { it.toRoomDb() } @@ -310,26 +270,21 @@ internal class RoomEnrolmentRecordLocalDataSource @Inject constructor( val referencesToDelete = action.referenceIdsToRemove.toSet() require( referencesToDelete.size != dbSubject.biometricTemplates.size || - action.faceSamplesToAdd.isNotEmpty() || - action.fingerprintSamplesToAdd.isNotEmpty() || + action.samplesToAdd.isNotEmpty() || action.externalCredentialsToAdd.isNotEmpty() || action.externalCredentialIdsToRemove.isNotEmpty(), ) { - val errorMsg = - "Cannot delete all samples for subject ${action.subjectId} without adding new ones" + val errorMsg = "Cannot delete all samples for subject ${action.subjectId} without adding new ones" Simber.i("[updateSubject] $errorMsg", tag = ROOM_RECORDS_DB) errorMsg } dbSubject.biometricTemplates.filter { it.referenceId in referencesToDelete }.forEach { subjectDao.deleteBiometricSample(it.uuid) } - val templatesToAdd = - action.faceSamplesToAdd.map { it.toRoomDb(action.subjectId) } + - action.fingerprintSamplesToAdd.map { it.toRoomDb(action.subjectId) } + val templatesToAdd = action.samplesToAdd.map { it.toRoomDb(action.subjectId) } if (templatesToAdd.isNotEmpty()) { subjectDao.insertBiometricSamples(templatesToAdd) } - dbSubject.externalCredentials.filter { it.id in action.externalCredentialIdsToRemove }.forEach { subjectDao.deleteExternalCredentialById(it.id) } diff --git a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/RoomEnrolmentRecordQueryBuilder.kt b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/RoomEnrolmentRecordQueryBuilder.kt index c997829a51..7e87f3b570 100644 --- a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/RoomEnrolmentRecordQueryBuilder.kt +++ b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/RoomEnrolmentRecordQueryBuilder.kt @@ -25,7 +25,7 @@ internal class RoomEnrolmentRecordQueryBuilder @Inject constructor() { */ fun buildSubjectQuery(query: SubjectQuery): SimpleSQLiteQuery { // require format not to be set for subject query and guide to use the buildBiometricTemplatesQuery instead - require(query.fingerprintSampleFormat == null && query.faceSampleFormat == null) { + require(query.format == null) { "Cannot set format for subject query, use buildBiometricTemplatesQuery instead" } val (whereClause, args) = buildWhereClause(query) @@ -43,9 +43,8 @@ internal class RoomEnrolmentRecordQueryBuilder @Inject constructor() { fun buildCountQuery(query: SubjectQuery): SimpleSQLiteQuery { val (whereClause, args) = buildWhereClause(query) - val specificFormat = query.fingerprintSampleFormat ?: query.faceSampleFormat - val sql = if (specificFormat != null) { + val sql = if (query.format != null) { "SELECT COUNT(DISTINCT S.$SUBJECT_ID_COLUMN) FROM $SUBJECT_TABLE_NAME S INNER JOIN $TEMPLATE_TABLE_NAME T" + " using(subjectId) $whereClause" } else { @@ -59,8 +58,7 @@ internal class RoomEnrolmentRecordQueryBuilder @Inject constructor() { pageSize: Int, ): SimpleSQLiteQuery { // require format to be set for biometric templates query - val format = query.fingerprintSampleFormat ?: query.faceSampleFormat - require(format != null) { + require(query.format != null) { "Must set format for biometric templates query, use buildSubjectQuery or buildCountQuery instead" } val updatedQuery = query.copy(sort = true) @@ -77,14 +75,14 @@ internal class RoomEnrolmentRecordQueryBuilder @Inject constructor() { $whereClause $orderByClause LIMIT $pageSize - ) B USING(subjectId) where A.format ='$format' + ) B USING(subjectId) where A.format ='${query.format}' """.trimIndent() return SimpleSQLiteQuery(sql, args.toTypedArray()) } fun buildDeleteQuery(query: SubjectQuery): SimpleSQLiteQuery { - require(query.faceSampleFormat == null && query.fingerprintSampleFormat == null) { - val errorMsg = "faceSampleFormat and fingerprintSampleFormat are not supported for deletion" + require(query.format == null) { + val errorMsg = "format is not supported for deletion" Simber.i("[delete] $errorMsg", tag = ROOM_RECORDS_DB) errorMsg } @@ -106,9 +104,6 @@ internal class RoomEnrolmentRecordQueryBuilder @Inject constructor() { ): Pair> { val clauses = mutableListOf() val args = mutableListOf() - require(!(query.fingerprintSampleFormat != null && query.faceSampleFormat != null)) { - "Cannot set both fingerprintSampleFormat and faceSampleFormat" - } // to achieve the highest performance, we should not use OR in the where clause // subject id params are mutually exclusive, so only one of them will be set at a time when { @@ -140,11 +135,7 @@ internal class RoomEnrolmentRecordQueryBuilder @Inject constructor() { clauses.add("${subjectAlias}$MODULE_ID_COLUMN = ?") args.add(it.value) } - query.faceSampleFormat?.let { - clauses.add("${templateAlias}$FORMAT_COLUMN = ?") - args.add(it) - } - query.fingerprintSampleFormat?.let { + query.format?.let { clauses.add("${templateAlias}$FORMAT_COLUMN = ?") args.add(it) } diff --git a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/DbSampleIdentifier.kt b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/DbSampleIdentifier.kt index d6ab99e37a..b0a4568542 100644 --- a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/DbSampleIdentifier.kt +++ b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/DbSampleIdentifier.kt @@ -18,7 +18,7 @@ enum class DbSampleIdentifier( ; companion object Companion { - fun fromId(id: Int) = DbSampleIdentifier.entries.firstOrNull { it.id == id } + fun fromId(id: Int?) = DbSampleIdentifier.entries.firstOrNull { it.id == id } } } diff --git a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RealmSubjectConverter.kt b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RealmSubjectConverter.kt index f8d1e5843d..3b9eb6ee0b 100644 --- a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RealmSubjectConverter.kt +++ b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RealmSubjectConverter.kt @@ -1,5 +1,6 @@ package com.simprints.infra.enrolment.records.repository.local.models +import com.simprints.core.domain.common.Modality import com.simprints.core.domain.externalcredential.ExternalCredential import com.simprints.core.domain.sample.Sample import com.simprints.core.domain.tokenization.asTokenizableEncrypted @@ -28,8 +29,8 @@ internal fun RealmSubject.toDomain(): Subject { moduleId = moduleId, createdAt = createdAt?.toDate(), updatedAt = updatedAt?.toDate(), - fingerprintSamples = fingerprintSamples.map(DbFingerprintSample::toDomain), - faceSamples = faceSamples.map(DbFaceSample::toDomain), + samples = fingerprintSamples.map(DbFingerprintSample::toDomain) + + faceSamples.map(DbFaceSample::toDomain), externalCredentials = externalCredentials.map(DbExternalCredential::toDomain), ) } @@ -41,10 +42,16 @@ internal fun Subject.toRealmDb(): RealmSubject = RealmSubject().also { subject - subject.moduleId = moduleId.value subject.createdAt = createdAt?.toRealmInstant() subject.updatedAt = updatedAt?.toRealmInstant() - subject.fingerprintSamples = - fingerprintSamples.map(Sample::toRealmFingerprintDb).toRealmList() - subject.faceSamples = faceSamples.map(Sample::toRealmFaceDb).toRealmList() subject.isModuleIdTokenized = moduleId.isTokenized() subject.isAttendantIdTokenized = attendantId.isTokenized() subject.externalCredentials = externalCredentials.map(ExternalCredential::toRealmDb).toRealmList() + + subject.fingerprintSamples = samples + .filter { it.modality == Modality.FINGERPRINT } + .map(Sample::toRealmFingerprintDb) + .toRealmList() + subject.faceSamples = samples + .filter { it.modality == Modality.FACE } + .map(Sample::toRealmFaceDb) + .toRealmList() } diff --git a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RoomSubjectConverter.kt b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RoomSubjectConverter.kt index 631805e442..a14111bdd9 100644 --- a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RoomSubjectConverter.kt +++ b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RoomSubjectConverter.kt @@ -2,7 +2,6 @@ package com.simprints.infra.enrolment.records.repository.local.models import com.simprints.core.domain.tokenization.asTokenizableEncrypted import com.simprints.infra.enrolment.records.repository.domain.models.Subject -import com.simprints.infra.enrolment.records.room.store.models.DbModality import com.simprints.infra.enrolment.records.room.store.models.SubjectBiometrics import java.util.Date @@ -13,8 +12,7 @@ internal fun SubjectBiometrics.toDomain() = Subject( moduleId = subject.moduleId.asTokenizableEncrypted(), createdAt = subject.createdAt?.toDate(), updatedAt = subject.updatedAt?.toDate(), - fingerprintSamples = biometricTemplates.filter { it.modality == DbModality.FINGERPRINT.id }.map { it.toSample() }, - faceSamples = biometricTemplates.filter { it.modality == DbModality.FACE.id }.map { it.toSample() }, + samples = biometricTemplates.map { it.toSample() }, externalCredentials = externalCredentials.map { it.toDomain() }, ) diff --git a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/remote/models/ApiEnrolmentRecord.kt b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/remote/models/ApiEnrolmentRecord.kt index 9353ec3f1c..cf1a0cd678 100644 --- a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/remote/models/ApiEnrolmentRecord.kt +++ b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/remote/models/ApiEnrolmentRecord.kt @@ -1,6 +1,7 @@ package com.simprints.infra.enrolment.records.repository.remote.models import androidx.annotation.Keep +import com.simprints.core.domain.common.Modality import com.simprints.core.domain.sample.Sample import com.simprints.core.tools.utils.EncodingUtils import com.simprints.infra.enrolment.records.repository.domain.models.Subject @@ -19,23 +20,15 @@ internal fun Subject.toEnrolmentRecord(encoder: EncodingUtils): ApiEnrolmentReco subjectId, moduleId.value, attendantId.value, - buildBiometricReferences(fingerprintSamples, faceSamples, encoder), + buildBiometricReferences(samples, encoder), ) internal fun buildBiometricReferences( - fingerprintSamples: List, - faceSamples: List, + samples: List, encoder: EncodingUtils, -): List { - val biometricReferences = mutableListOf() - - fingerprintSamples.toFingerprintApi(encoder)?.let { - biometricReferences.add(it) - } - - faceSamples.toFaceApi(encoder)?.let { - biometricReferences.add(it) +): List = samples.groupBy { it.modality }.mapNotNull { (modality, modalitySamples) -> + when (modality) { + Modality.FINGERPRINT -> modalitySamples.toFingerprintApi(encoder) + Modality.FACE -> modalitySamples.toFaceApi(encoder) } - - return biometricReferences } diff --git a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/EnrolmentRecordRepositoryImplTest.kt b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/EnrolmentRecordRepositoryImplTest.kt index 839071e228..f23befbb6f 100644 --- a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/EnrolmentRecordRepositoryImplTest.kt +++ b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/EnrolmentRecordRepositoryImplTest.kt @@ -198,8 +198,7 @@ class EnrolmentRecordRepositoryImplTest { moduleId = moduleIdRaw, createdAt = Date(), updatedAt = null, - fingerprintSamples = emptyList(), - faceSamples = emptyList(), + samples = emptyList(), ) every { project.id } returns projectId coEvery { localDataSource.load(any()) } returns listOf(subject) @@ -240,8 +239,7 @@ class EnrolmentRecordRepositoryImplTest { moduleId = moduleIdRaw, createdAt = Date(), updatedAt = null, - fingerprintSamples = emptyList(), - faceSamples = emptyList(), + samples = emptyList(), ) every { project.id } returns projectId coEvery { localDataSource.load(any()) } returns listOf(subject) @@ -288,7 +286,7 @@ class EnrolmentRecordRepositoryImplTest { val expectedRange = listOf(0..10) val expectedFingerprintIdentities = listOf() coEvery { - localDataSource.loadFingerprintIdentities( + localDataSource.loadIdentities( expectedSubjectQuery, expectedRange, any(), @@ -300,7 +298,7 @@ class EnrolmentRecordRepositoryImplTest { val fingerprintIdentities = mutableListOf() repository - .loadFingerprintIdentities( + .loadIdentities( query = expectedSubjectQuery, ranges = expectedRange, dataSource = BiometricDataSource.Simprints, @@ -313,7 +311,7 @@ class EnrolmentRecordRepositoryImplTest { assert(fingerprintIdentities == expectedFingerprintIdentities) coVerify(exactly = 1) { - localDataSource.loadFingerprintIdentities( + localDataSource.loadIdentities( expectedSubjectQuery, expectedRange, any(), @@ -330,7 +328,7 @@ class EnrolmentRecordRepositoryImplTest { val expectedRange = listOf(0..10) val expectedFingerprintIdentities = listOf() coEvery { - commCareDataSource.loadFingerprintIdentities( + commCareDataSource.loadIdentities( expectedSubjectQuery, expectedRange, any(), @@ -341,7 +339,7 @@ class EnrolmentRecordRepositoryImplTest { } returns createTestChannel(expectedFingerprintIdentities) val fingerprintIdentities = mutableListOf() repository - .loadFingerprintIdentities( + .loadIdentities( query = expectedSubjectQuery, ranges = expectedRange, dataSource = BiometricDataSource.CommCare(""), @@ -354,7 +352,7 @@ class EnrolmentRecordRepositoryImplTest { assert(fingerprintIdentities == expectedFingerprintIdentities) coVerify(exactly = 1) { - commCareDataSource.loadFingerprintIdentities( + commCareDataSource.loadIdentities( expectedSubjectQuery, expectedRange, any(), @@ -371,7 +369,7 @@ class EnrolmentRecordRepositoryImplTest { val expectedRange = listOf(0..10) val expectedFaceIdentities = listOf() coEvery { - localDataSource.loadFaceIdentities( + localDataSource.loadIdentities( expectedSubjectQuery, expectedRange, any(), @@ -383,7 +381,7 @@ class EnrolmentRecordRepositoryImplTest { val faceIdentities = mutableListOf() repository - .loadFaceIdentities( + .loadIdentities( query = expectedSubjectQuery, ranges = expectedRange, dataSource = BiometricDataSource.Simprints, @@ -396,7 +394,7 @@ class EnrolmentRecordRepositoryImplTest { assert(faceIdentities == expectedFaceIdentities) coVerify(exactly = 1) { - localDataSource.loadFaceIdentities( + localDataSource.loadIdentities( expectedSubjectQuery, expectedRange, any(), @@ -413,7 +411,7 @@ class EnrolmentRecordRepositoryImplTest { val expectedRange = listOf(0..10) val expectedFaceIdentities = listOf() coEvery { - commCareDataSource.loadFaceIdentities( + commCareDataSource.loadIdentities( expectedSubjectQuery, expectedRange, any(), @@ -426,7 +424,7 @@ class EnrolmentRecordRepositoryImplTest { val faceIdentities = mutableListOf() repository - .loadFaceIdentities( + .loadIdentities( query = expectedSubjectQuery, ranges = expectedRange, dataSource = BiometricDataSource.CommCare(""), @@ -439,7 +437,7 @@ class EnrolmentRecordRepositoryImplTest { assert(faceIdentities == expectedFaceIdentities) coVerify(exactly = 1) { - commCareDataSource.loadFaceIdentities( + commCareDataSource.loadIdentities( expectedSubjectQuery, expectedRange, any(), @@ -450,8 +448,8 @@ class EnrolmentRecordRepositoryImplTest { } } - private fun createTestChannel(vararg lists: List): ReceiveChannel> { - val channel = Channel>(lists.size) + private fun createTestChannel(vararg lists: List): ReceiveChannel { + val channel = Channel(lists.size) runBlocking { var time = 0L for (list in lists) { diff --git a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/commcare/CommCareIdentityDataSourceTest.kt b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/commcare/CommCareIdentityDataSourceTest.kt index 76427f95dc..c6d4cdfcc7 100644 --- a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/commcare/CommCareIdentityDataSourceTest.kt +++ b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/commcare/CommCareIdentityDataSourceTest.kt @@ -230,7 +230,7 @@ class CommCareIdentityDataSourceTest { } @Test - fun testLoadFingerprintIdentities() = runTest { + fun `test loadIdentities with fingerprint records`() = runTest { every { mockMetadataCursor.count } returns expectedFingerprintIdentities.size every { mockMetadataCursor.moveToPosition(0) } returns true every { mockMetadataCursor.moveToNext() } returnsMany listOf(true, false) @@ -249,12 +249,12 @@ class CommCareIdentityDataSourceTest { ) val templateFormat = "ISO_19794_2" - val query = SubjectQuery(fingerprintSampleFormat = templateFormat) + val query = SubjectQuery(format = templateFormat) val range = 0..expectedFingerprintIdentities.size val actualIdentities = mutableListOf() dataSource - .loadFingerprintIdentities( + .loadIdentities( query = query, ranges = listOf(range), project = project, @@ -286,7 +286,7 @@ class CommCareIdentityDataSourceTest { } @Test - fun testLoadFaceIdentities() = runTest { + fun `test loadIdentities with face records`() = runTest { every { mockMetadataCursor.count } returns expectedFaceIdentities.size every { mockMetadataCursor.moveToPosition(0) } returns true every { mockMetadataCursor.moveToNext() } returnsMany listOf(true, false) @@ -305,7 +305,7 @@ class CommCareIdentityDataSourceTest { ) val templateFormat = "ROC_1_23" val query = SubjectQuery( - faceSampleFormat = templateFormat, + format = templateFormat, attendantId = TokenizableString.Tokenized( value = "AdySMrjuy7uq0Dcxov3rUFIw66uXTFrKd0BnzSr9MYXl5maWEpyKQT8AUdcPuVHUWpOkO88=", ), @@ -315,7 +315,7 @@ class CommCareIdentityDataSourceTest { val range = 0..expectedFaceIdentities.size val actualIdentities = mutableListOf() dataSource - .loadFaceIdentities( + .loadIdentities( query = query, ranges = listOf(range), project = project, @@ -343,7 +343,7 @@ class CommCareIdentityDataSourceTest { } @Test - fun `test loadFingerprintIdentities returns only identities with fingerprint references`() = runTest { + fun `test loadIdentities returns only identities with fingerprint references`() = runTest { every { mockMetadataCursor.count } returns expectedFingerprintIdentities.size + 1 every { mockMetadataCursor.moveToPosition(0) } returns true every { mockMetadataCursor.moveToNext() } returnsMany listOf(true, true, false) @@ -364,11 +364,11 @@ class CommCareIdentityDataSourceTest { SUBJECT_ACTIONS_FACE_1, ) val templateFormat = "NEC_1_5" - val query = SubjectQuery(fingerprintSampleFormat = templateFormat) + val query = SubjectQuery(format = templateFormat) val range = 0..expectedFingerprintIdentities.size val actualIdentities = mutableListOf() dataSource - .loadFingerprintIdentities( + .loadIdentities( query = query, ranges = listOf(range), project = project, @@ -400,7 +400,7 @@ class CommCareIdentityDataSourceTest { } @Test - fun `test loadFaceIdentities returns only identities with face references`() = runTest { + fun `test loadIdentities returns only identities with face references`() = runTest { every { mockMetadataCursor.count } returns expectedFaceIdentities.size + 1 every { mockMetadataCursor.moveToPosition(0) } returns true every { mockMetadataCursor.moveToNext() } returnsMany listOf(true, true, false) @@ -422,11 +422,11 @@ class CommCareIdentityDataSourceTest { ) val templateFormat = "ROC_1_23" - val query = SubjectQuery(faceSampleFormat = templateFormat) + val query = SubjectQuery(format = templateFormat) val range = 0..expectedFaceIdentities.size val actualIdentities = mutableListOf() dataSource - .loadFaceIdentities( + .loadIdentities( query = query, ranges = listOf(range), project = project, @@ -454,7 +454,7 @@ class CommCareIdentityDataSourceTest { } @Test - fun `test loadFingerprintIdentities returns only fingerprint references for dual modality identities`() = runTest { + fun `test loadIdentities returns only fingerprint references for dual modality identities`() = runTest { every { mockMetadataCursor.count } returns expectedFingerprintIdentities.size every { mockMetadataCursor.moveToPosition(0) } returns true every { mockMetadataCursor.moveToNext() } returnsMany listOf(true, false) @@ -472,11 +472,11 @@ class CommCareIdentityDataSourceTest { SUBJECT_ACTIONS_FINGERPRINT_AND_FACE_2, ) val templateFormat = "ISO_19794_2" - val query = SubjectQuery(fingerprintSampleFormat = templateFormat) + val query = SubjectQuery(format = templateFormat) val range = 0..expectedFingerprintIdentities.size val actualIdentities = mutableListOf() dataSource - .loadFingerprintIdentities( + .loadIdentities( query = query, ranges = listOf(range), project = project, @@ -508,7 +508,7 @@ class CommCareIdentityDataSourceTest { } @Test - fun `test loadFaceIdentities returns only face references for dual modality identities`() = runTest { + fun `test loadIdentities returns only face references for dual modality identities`() = runTest { every { mockMetadataCursor.count } returns expectedFaceIdentities.size every { mockMetadataCursor.moveToPosition(0) } returns true every { mockMetadataCursor.moveToNext() } returnsMany listOf(true, false) @@ -526,11 +526,11 @@ class CommCareIdentityDataSourceTest { SUBJECT_ACTIONS_FINGERPRINT_AND_FACE_2, ) val templateFormat = "ROC_1_23" - val query = SubjectQuery(faceSampleFormat = templateFormat) + val query = SubjectQuery(format = templateFormat) val range = 0..expectedFaceIdentities.size val actualIdentities = mutableListOf() dataSource - .loadFaceIdentities( + .loadIdentities( query = query, ranges = listOf(range), project = project, @@ -586,7 +586,7 @@ class CommCareIdentityDataSourceTest { val range = 0..0 val actualIdentities = mutableListOf() dataSource - .loadFingerprintIdentities( + .loadIdentities( query = query, ranges = listOf(range), project = project, @@ -618,7 +618,7 @@ class CommCareIdentityDataSourceTest { val range = 2..3 val actualIdentities = mutableListOf() dataSource - .loadFingerprintIdentities( + .loadIdentities( query = query, ranges = listOf(range), project = project, @@ -663,11 +663,11 @@ class CommCareIdentityDataSourceTest { ) val templateFormat = "ISO_19794_2" - val query = SubjectQuery(fingerprintSampleFormat = templateFormat) + val query = SubjectQuery(format = templateFormat) val range = expectedFingerprintIdentities.indices val actualIdentities = mutableListOf() dataSource - .loadFingerprintIdentities( + .loadIdentities( query = query, ranges = listOf(range), project = project, @@ -710,7 +710,7 @@ class CommCareIdentityDataSourceTest { val range = 0..2 val actualIdentities = mutableListOf() dataSource - .loadFingerprintIdentities( + .loadIdentities( query = query, ranges = listOf(range), project = project, @@ -750,7 +750,7 @@ class CommCareIdentityDataSourceTest { val range = 0..2 val actualIdentities = mutableListOf() dataSource - .loadFingerprintIdentities( + .loadIdentities( query = query, ranges = listOf(range), project = project, @@ -794,7 +794,7 @@ class CommCareIdentityDataSourceTest { val range = 0..2 val actualIdentities = mutableListOf() dataSource - .loadFingerprintIdentities( + .loadIdentities( query = query, ranges = listOf(range), project = project, @@ -825,7 +825,7 @@ class CommCareIdentityDataSourceTest { val range = 0..2 val actualIdentities = mutableListOf() dataSource - .loadFingerprintIdentities( + .loadIdentities( query = query, ranges = listOf(range), project = project, @@ -861,7 +861,7 @@ class CommCareIdentityDataSourceTest { val range = 0..2 val actualIdentities = mutableListOf() dataSource - .loadFingerprintIdentities( + .loadIdentities( query = query, ranges = listOf(range), project = project, @@ -924,7 +924,7 @@ class CommCareIdentityDataSourceTest { } @Test - fun `loadFingerprintIdentities with case ID calls onCandidateLoaded`() = runTest { + fun `loadIdentities with case ID calls onCandidateLoaded`() = runTest { val testCaseId = "test-case-id" var onCandidateLoadedCalled = false every { extractCommCareCaseIdUseCase.invoke(any()) } returns testCaseId @@ -936,7 +936,7 @@ class CommCareIdentityDataSourceTest { val query = SubjectQuery(metadata = "test-metadata") dataSource - .loadFingerprintIdentities( + .loadIdentities( query = query, ranges = listOf(0..1), project = project, diff --git a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/RealmEnrolmentRecordLocalDataSourceTest.kt b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/RealmEnrolmentRecordLocalDataSourceTest.kt index dea9e53373..3acb0c6973 100644 --- a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/RealmEnrolmentRecordLocalDataSourceTest.kt +++ b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/RealmEnrolmentRecordLocalDataSourceTest.kt @@ -147,7 +147,7 @@ class RealmEnrolmentRecordLocalDataSourceTest { val people = mutableListOf() enrolmentRecordLocalDataSource - .loadFingerprintIdentities( + .loadIdentities( SubjectQuery(), listOf(IntRange(0, 20)), BiometricDataSource.Simprints, @@ -162,43 +162,22 @@ class RealmEnrolmentRecordLocalDataSourceTest { } @Test - fun `correctly query supported fingerprint format`() = runTest { + fun `correctly query supported format`() = runTest { val format = "SupportedFormat" enrolmentRecordLocalDataSource - .loadFingerprintIdentities( - SubjectQuery(fingerprintSampleFormat = format), + .loadIdentities( + SubjectQuery(format = format), listOf(IntRange(0, 20)), BiometricDataSource.Simprints, project, this, onCandidateLoaded, ).consumeEach { } - verify { realmQuery.query( - "ANY ${FINGERPRINT_SAMPLES_FIELD}.${FORMAT_FIELD} == $0", + "ANY ${FINGERPRINT_SAMPLES_FIELD}.${FORMAT_FIELD} == $0 OR ANY $FACE_SAMPLES_FIELD.$FORMAT_FIELD == $1", format, - ) - } - } - - @Test - fun `correctly query supported face format`() = runTest { - val format = "SupportedFormat" - - enrolmentRecordLocalDataSource - .loadFingerprintIdentities( - SubjectQuery(faceSampleFormat = format), - listOf(IntRange(0, 20)), - BiometricDataSource.Simprints, - project, - this, - onCandidateLoaded, - ).consumeEach { } - verify { - realmQuery.query( - "ANY ${FACE_SAMPLES_FIELD}.${FORMAT_FIELD} == $0", format, ) } @@ -211,7 +190,7 @@ class RealmEnrolmentRecordLocalDataSourceTest { val people = mutableListOf() enrolmentRecordLocalDataSource - .loadFaceIdentities( + .loadIdentities( SubjectQuery(), listOf(IntRange(0, 20)), BiometricDataSource.Simprints, @@ -277,15 +256,15 @@ class RealmEnrolmentRecordLocalDataSourceTest { fun performSubjectCreationAction_deletesOldSamples() = runTest { val faceReferenceId = "faceToDelete" val fingerReferenceId = "fingerToDelete" - every { realmSingleQuery.find() } returns getRandomSubject() - .copy( - faceSamples = listOf( - getRandomFaceSample(referenceId = faceReferenceId), - ), - fingerprintSamples = listOf( - getRandomFingerprintSample(referenceId = fingerReferenceId), - ), - ).toRealmDb() + every { realmSingleQuery.find() } returns getRandomSubject( + faceSamples = listOf( + getRandomFaceSample(referenceId = faceReferenceId), + ), + fingerprintSamples = listOf( + getRandomFingerprintSample(referenceId = fingerReferenceId), + ), + ).toRealmDb() + val subject = getFakePerson() enrolmentRecordLocalDataSource.performActions( @@ -321,8 +300,7 @@ class RealmEnrolmentRecordLocalDataSourceTest { listOf( SubjectAction.Update( subject.subjectId.toString(), - faceSamplesToAdd = listOf(getRandomFaceSample()), - fingerprintSamplesToAdd = listOf(getRandomFingerprintSample()), + samplesToAdd = listOf(getRandomFaceSample(), getRandomFingerprintSample()), referenceIdsToRemove = listOf(faceReferenceId, fingerReferenceId), externalCredentialsToAdd = listOf(), externalCredentialIdsToRemove = listOf(), @@ -332,9 +310,10 @@ class RealmEnrolmentRecordLocalDataSourceTest { ) val peopleCount = enrolmentRecordLocalDataSource.count() assertThat(peopleCount).isEqualTo(1) + verify { - mutableRealm.delete(match { it.referenceId == faceReferenceId }) mutableRealm.delete(match { it.referenceId == fingerReferenceId }) + mutableRealm.delete(match { it.referenceId == faceReferenceId }) mutableRealm.copyToRealm( match { // one old + one new @@ -364,8 +343,7 @@ class RealmEnrolmentRecordLocalDataSourceTest { listOf( SubjectAction.Update( subject.subjectId.toString(), - faceSamplesToAdd = listOf(), - fingerprintSamplesToAdd = listOf(), + samplesToAdd = listOf(), referenceIdsToRemove = listOf(), externalCredentialsToAdd = listOf(), externalCredentialIdsToRemove = listOf("id1"), @@ -514,8 +492,7 @@ class RealmEnrolmentRecordLocalDataSourceTest { projectId = projectId, attendantId = userId.asTokenizableRaw(), moduleId = moduleId.asTokenizableRaw(), - faceSamples = faceSamples, - fingerprintSamples = fingerprintSamples, + samples = fingerprintSamples + faceSamples, externalCredentials = externalCredentials, ) diff --git a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/RoomEnrolmentRecordLocalDataSourceTest.kt b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/RoomEnrolmentRecordLocalDataSourceTest.kt index b6e8ad9d82..5a1e75b04c 100644 --- a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/RoomEnrolmentRecordLocalDataSourceTest.kt +++ b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/RoomEnrolmentRecordLocalDataSourceTest.kt @@ -124,8 +124,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { projectId = PROJECT_1_ID, attendantId = ATTENDANT_1_ID, moduleId = MODULE_1_ID, - faceSamples = listOf(faceSample1), - fingerprintSamples = emptyList(), + samples = listOf(faceSample1), createdAt = date, updatedAt = date, externalCredentials = listOf(getExternalCredential("subj-001")), @@ -135,8 +134,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { projectId = PROJECT_1_ID, attendantId = ATTENDANT_1_ID, moduleId = MODULE_1_ID, - faceSamples = emptyList(), - fingerprintSamples = listOf(fingerprintSample1), + samples = listOf(fingerprintSample1), createdAt = date, updatedAt = date, externalCredentials = listOf(getExternalCredential("subj-002")), @@ -146,8 +144,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { projectId = PROJECT_1_ID, attendantId = ATTENDANT_1_ID, moduleId = MODULE_2_ID, - faceSamples = listOf(faceSample2), - fingerprintSamples = listOf(fingerprintSample2), + samples = listOf(faceSample2, fingerprintSample2), externalCredentials = listOf(getExternalCredential("subj-003")), ) private val subject4P2WithBoth = Subject( @@ -155,8 +152,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { projectId = PROJECT_2_ID, attendantId = ATTENDANT_2_ID, moduleId = MODULE_2_ID, - faceSamples = listOf(faceSample3), - fingerprintSamples = listOf(fingerprintSample3), + samples = listOf(faceSample3, fingerprintSample3), createdAt = date, updatedAt = date, externalCredentials = listOf(getExternalCredential("subj-004")), @@ -167,8 +163,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { projectId = PROJECT_2_ID, attendantId = ATTENDANT_2_ID, moduleId = MODULE_3_ID, - faceSamples = listOf(faceSample3.copy(id = UUID.randomUUID().toString())), - fingerprintSamples = emptyList(), + samples = listOf(faceSample3.copy(id = UUID.randomUUID().toString())), createdAt = Date(date.time + 1000), // Slightly different time updatedAt = Date(date.time + 1000), externalCredentials = listOf(getExternalCredential("subj-005")), @@ -179,8 +174,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { projectId = PROJECT_2_ID, attendantId = ATTENDANT_1_ID, moduleId = MODULE_3_ID, - faceSamples = emptyList(), - fingerprintSamples = listOf(fingerprintSample3.copy(id = UUID.randomUUID().toString())), + samples = listOf(fingerprintSample3.copy(id = UUID.randomUUID().toString())), createdAt = Date(date.time + 2000), // Different time updatedAt = Date(date.time + 2000), externalCredentials = listOf(getExternalCredential("subj-006")), @@ -264,9 +258,8 @@ class RoomEnrolmentRecordLocalDataSourceTest { assertThat(loaded).hasSize(1) val createdSubject = loaded[0] assertThat(createdSubject.subjectId).isEqualTo(subject1P1WithFace.subjectId) - assertThat(createdSubject.faceSamples).hasSize(1) - assertThat(createdSubject.faceSamples).containsExactly(faceSample1) - assertThat(createdSubject.fingerprintSamples).isEmpty() + assertThat(createdSubject.samples).hasSize(1) + assertThat(createdSubject.samples).containsExactly(faceSample1) assertThat(createdSubject.createdAt).isNotNull() assertThat(createdSubject.updatedAt).isNotNull() } @@ -284,9 +277,8 @@ class RoomEnrolmentRecordLocalDataSourceTest { assertThat(loaded).hasSize(1) val createdSubject = loaded[0] assertThat(createdSubject.subjectId).isEqualTo(subject2P1WithFinger.subjectId) - assertThat(createdSubject.faceSamples).isEmpty() - assertThat(createdSubject.fingerprintSamples).hasSize(1) - assertThat(createdSubject.fingerprintSamples).containsExactly(fingerprintSample1) + assertThat(createdSubject.samples).hasSize(1) + assertThat(createdSubject.samples).containsExactly(fingerprintSample1) assertThat(createdSubject.createdAt).isNotNull() assertThat(createdSubject.updatedAt).isNotNull() } @@ -304,10 +296,8 @@ class RoomEnrolmentRecordLocalDataSourceTest { assertThat(loaded).hasSize(1) val createdSubject = loaded[0] assertThat(createdSubject.subjectId).isEqualTo(subject3P1WithBoth.subjectId) - assertThat(createdSubject.faceSamples).hasSize(1) - assertThat(createdSubject.faceSamples).containsExactly(faceSample2) - assertThat(createdSubject.fingerprintSamples).hasSize(1) - assertThat(createdSubject.fingerprintSamples).containsExactly(fingerprintSample2) + assertThat(createdSubject.samples).hasSize(2) + assertThat(createdSubject.samples).containsExactly(faceSample2, fingerprintSample2) } @Test(expected = IllegalArgumentException::class) // Reverted to JUnit exception check @@ -354,14 +344,12 @@ class RoomEnrolmentRecordLocalDataSourceTest { dataSource.performActions(listOf(SubjectAction.Creation(subject1P1WithFace)), project) val initialSubject = dataSource.load(SubjectQuery(subjectId = subject1P1WithFace.subjectId)).first() - assertThat(initialSubject.faceSamples).hasSize(1) - assertThat(initialSubject.fingerprintSamples).isEmpty() + assertThat(initialSubject.samples).hasSize(1) // Original Update action instantiation style maintained val updateAction = SubjectAction.Update( subjectId = subject1P1WithFace.subjectId, - faceSamplesToAdd = listOf(faceSample2), - fingerprintSamplesToAdd = listOf(fingerprintSample1), + samplesToAdd = listOf(fingerprintSample1, faceSample2), referenceIdsToRemove = listOf(), externalCredentialsToAdd = listOf(), externalCredentialIdsToRemove = listOf(), @@ -374,10 +362,8 @@ class RoomEnrolmentRecordLocalDataSourceTest { val loaded = dataSource.load(SubjectQuery(subjectId = subject1P1WithFace.subjectId)) assertThat(loaded).hasSize(1) val updatedSubject = loaded[0] - assertThat(updatedSubject.faceSamples).hasSize(2) - assertThat(updatedSubject.faceSamples).containsExactly(faceSample1, faceSample2) - assertThat(updatedSubject.fingerprintSamples).hasSize(1) - assertThat(updatedSubject.fingerprintSamples).containsExactly(fingerprintSample1) + assertThat(updatedSubject.samples).hasSize(3) + assertThat(updatedSubject.samples).containsExactly(faceSample1, faceSample2, fingerprintSample1) } @Test @@ -387,14 +373,12 @@ class RoomEnrolmentRecordLocalDataSourceTest { dataSource.performActions(listOf(SubjectAction.Creation(subjectToUpdate)), project) val initial = dataSource.load(SubjectQuery(subjectId = subjectToUpdate.subjectId)).first() - assertThat(initial.faceSamples).hasSize(1) - assertThat(initial.fingerprintSamples).hasSize(1) + assertThat(initial.samples).hasSize(2) // Original Update action instantiation style maintained val updateAction = SubjectAction.Update( subjectId = subjectToUpdate.subjectId, - faceSamplesToAdd = listOf(), // Explicitly empty as in original - fingerprintSamplesToAdd = listOf(), // Explicitly empty as in original + samplesToAdd = listOf(), // Explicitly empty as in original referenceIdsToRemove = listOf(faceSample2.referenceId), externalCredentialsToAdd = listOf(), externalCredentialIdsToRemove = listOf(), @@ -406,9 +390,8 @@ class RoomEnrolmentRecordLocalDataSourceTest { // Then val loaded = dataSource.load(SubjectQuery(subjectId = subjectToUpdate.subjectId)).first() - assertThat(loaded.faceSamples).isEmpty() - assertThat(loaded.fingerprintSamples).hasSize(1) - assertThat(loaded.fingerprintSamples).containsExactly(fingerprintSample2) + assertThat(loaded.samples).hasSize(1) + assertThat(loaded.samples).containsExactly(fingerprintSample2) } @Test @@ -418,14 +401,12 @@ class RoomEnrolmentRecordLocalDataSourceTest { dataSource.performActions(listOf(SubjectAction.Creation(subjectToUpdate)), project) val initial = dataSource.load(SubjectQuery(subjectId = subjectToUpdate.subjectId)).first() - assertThat(initial.faceSamples).hasSize(1) - assertThat(initial.fingerprintSamples).hasSize(1) + assertThat(initial.samples).hasSize(2) // Original Update action instantiation style maintained val updateAction = SubjectAction.Update( subjectId = subjectToUpdate.subjectId, - faceSamplesToAdd = listOf(), - fingerprintSamplesToAdd = listOf(), + samplesToAdd = listOf(), referenceIdsToRemove = listOf(fingerprintSample2.referenceId), externalCredentialsToAdd = listOf(), externalCredentialIdsToRemove = listOf(), @@ -437,9 +418,8 @@ class RoomEnrolmentRecordLocalDataSourceTest { // Then val loaded = dataSource.load(SubjectQuery(subjectId = subjectToUpdate.subjectId)).first() - assertThat(loaded.faceSamples).hasSize(1) - assertThat(loaded.faceSamples).containsExactly(faceSample2) - assertThat(loaded.fingerprintSamples).isEmpty() + assertThat(loaded.samples).hasSize(1) + assertThat(loaded.samples).containsExactly(faceSample2) } @Test(expected = IllegalArgumentException::class) // Reverted to JUnit exception check @@ -448,14 +428,12 @@ class RoomEnrolmentRecordLocalDataSourceTest { dataSource.performActions(listOf(SubjectAction.Creation(subject1P1WithFace)), project) val initial = dataSource.load(SubjectQuery(subjectId = subject1P1WithFace.subjectId)).first() - assertThat(initial.faceSamples).hasSize(1) - assertThat(initial.fingerprintSamples).isEmpty() + assertThat(initial.samples).hasSize(1) // Original Update action instantiation style maintained val updateAction = SubjectAction.Update( subjectId = subject1P1WithFace.subjectId, - faceSamplesToAdd = listOf(), - fingerprintSamplesToAdd = listOf(), + samplesToAdd = listOf(), referenceIdsToRemove = listOf(faceSample1.referenceId), externalCredentialsToAdd = listOf(), externalCredentialIdsToRemove = listOf(), @@ -471,16 +449,13 @@ class RoomEnrolmentRecordLocalDataSourceTest { fun `performActions - Update - should fail when removing last fingerprint sample`() = runTest { // Given: Subject with only a fingerprint sample dataSource.performActions(listOf(SubjectAction.Creation(subject2P1WithFinger)), project) - val initial = - dataSource.load(SubjectQuery(subjectId = subject2P1WithFinger.subjectId)).first() - assertThat(initial.faceSamples).isEmpty() - assertThat(initial.fingerprintSamples).hasSize(1) + val initial = dataSource.load(SubjectQuery(subjectId = subject2P1WithFinger.subjectId)).first() + assertThat(initial.samples).hasSize(1) // Original Update action instantiation style maintained val updateAction = SubjectAction.Update( subjectId = subject2P1WithFinger.subjectId, - faceSamplesToAdd = listOf(), - fingerprintSamplesToAdd = listOf(), + samplesToAdd = listOf(), referenceIdsToRemove = listOf(fingerprintSample1.referenceId), externalCredentialsToAdd = listOf(), externalCredentialIdsToRemove = listOf(), @@ -500,8 +475,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { // Original Update action instantiation style maintained val updateAction = SubjectAction.Update( subjectId = subject1P1WithFace.subjectId, - faceSamplesToAdd = listOf(faceSample1, faceSample2), - fingerprintSamplesToAdd = listOf(fingerprintSample1), + samplesToAdd = listOf(faceSample1, faceSample2, fingerprintSample1), referenceIdsToRemove = listOf(), externalCredentialsToAdd = listOf(), externalCredentialIdsToRemove = listOf(), @@ -515,11 +489,8 @@ class RoomEnrolmentRecordLocalDataSourceTest { assertThat(loaded).hasSize(1) val finalSubject = loaded[0] - assertThat(finalSubject.faceSamples).hasSize(2) - assertThat(finalSubject.faceSamples).containsExactly(faceSample1, faceSample2) - - assertThat(finalSubject.fingerprintSamples).hasSize(1) - assertThat(finalSubject.fingerprintSamples).containsExactly(fingerprintSample1) + assertThat(finalSubject.samples).hasSize(3) + assertThat(finalSubject.samples).containsExactly(faceSample1, faceSample2, fingerprintSample1) } @Test @@ -531,8 +502,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { val updateAction = SubjectAction.Update( subjectId = nonExistentSubjectId, - faceSamplesToAdd = listOf(faceSample1), // Try to add samples - fingerprintSamplesToAdd = listOf(), + samplesToAdd = listOf(faceSample1), // Try to add samples referenceIdsToRemove = listOf(), externalCredentialsToAdd = listOf(), externalCredentialIdsToRemove = listOf(), @@ -574,8 +544,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { // Then assertThat(loaded).hasSize(1) assertThat(loaded[0].subjectId).isEqualTo(subject1P1WithFace.subjectId) - assertThat(loaded[0].faceSamples).containsExactly(faceSample1) - assertThat(loaded[0].fingerprintSamples).isEmpty() + assertThat(loaded[0].samples).containsExactly(faceSample1) } @Test @@ -635,14 +604,12 @@ class RoomEnrolmentRecordLocalDataSourceTest { var loadedSubject = dataSource.load(SubjectQuery(subjectId = subject1P1WithFace.subjectId)).firstOrNull() assertThat(loadedSubject).isNotNull() - assertThat(loadedSubject!!.faceSamples).hasSize(1) - assertThat(loadedSubject.fingerprintSamples).isEmpty() + assertThat(loadedSubject!!.samples).hasSize(1) // --- Update (Original style maintained) --- val updateAction = SubjectAction.Update( subjectId = subject1P1WithFace.subjectId, - faceSamplesToAdd = listOf(), - fingerprintSamplesToAdd = listOf(fingerprintSample1), + samplesToAdd = listOf(fingerprintSample1), referenceIdsToRemove = listOf(), externalCredentialsToAdd = listOf(), externalCredentialIdsToRemove = listOf(), @@ -651,9 +618,8 @@ class RoomEnrolmentRecordLocalDataSourceTest { loadedSubject = dataSource.load(SubjectQuery(subjectId = subject1P1WithFace.subjectId)).firstOrNull() assertThat(loadedSubject).isNotNull() - assertThat(loadedSubject!!.faceSamples).hasSize(1) - assertThat(loadedSubject.fingerprintSamples).hasSize(1) - assertThat(loadedSubject.fingerprintSamples).containsExactly(fingerprintSample1) + assertThat(loadedSubject!!.samples).hasSize(2) + assertThat(loadedSubject.samples).containsExactly(fingerprintSample1, faceSample1) // --- Delete --- val deleteAction = SubjectAction.Deletion(subjectId = subject1P1WithFace.subjectId) @@ -725,22 +691,22 @@ class RoomEnrolmentRecordLocalDataSourceTest { val countRoc1P1 = dataSource.count( SubjectQuery( projectId = PROJECT_1_ID, - faceSampleFormat = ROC_1_FORMAT, + format = ROC_1_FORMAT, ), ) val countRoc3P1 = dataSource.count( SubjectQuery( projectId = PROJECT_1_ID, - faceSampleFormat = ROC_3_FORMAT, + format = ROC_3_FORMAT, ), ) val countRoc1P2 = dataSource.count( SubjectQuery( projectId = PROJECT_2_ID, - faceSampleFormat = ROC_1_FORMAT, + format = ROC_1_FORMAT, ), ) - val countAllRoc1 = dataSource.count(SubjectQuery(faceSampleFormat = ROC_1_FORMAT)) + val countAllRoc1 = dataSource.count(SubjectQuery(format = ROC_1_FORMAT)) // Then assertThat(countRoc1P1).isEqualTo(1) @@ -758,22 +724,22 @@ class RoomEnrolmentRecordLocalDataSourceTest { val countNecP1 = dataSource.count( SubjectQuery( projectId = PROJECT_1_ID, - fingerprintSampleFormat = NEC_FORMAT, + format = NEC_FORMAT, ), ) val countIsoP1 = dataSource.count( SubjectQuery( projectId = PROJECT_1_ID, - fingerprintSampleFormat = ISO_FORMAT, + format = ISO_FORMAT, ), ) val countNecP2 = dataSource.count( SubjectQuery( projectId = PROJECT_2_ID, - fingerprintSampleFormat = NEC_FORMAT, + format = NEC_FORMAT, ), ) - val countAllNec = dataSource.count(SubjectQuery(fingerprintSampleFormat = NEC_FORMAT)) + val countAllNec = dataSource.count(SubjectQuery(format = NEC_FORMAT)) // Then assertThat(countNecP1).isEqualTo(1) @@ -801,13 +767,13 @@ class RoomEnrolmentRecordLocalDataSourceTest { } @Test(expected = IllegalArgumentException::class) // Reverted to JUnit exception check - fun `loadFingerprintIdentities - should throw exception if format is missing in query`() = runTest { + fun `loadIdentities - should throw exception if format is missing in query`() = runTest { // Given setupInitialData() val queryWithoutFormat = SubjectQuery(projectId = PROJECT_1_ID) // When - dataSource.loadFingerprintIdentities( + dataSource.loadIdentities( // This call will throw query = queryWithoutFormat, ranges = listOf(0..10), @@ -823,17 +789,17 @@ class RoomEnrolmentRecordLocalDataSourceTest { } @Test - fun `loadFingerprintIdentities - should load identities matching format for the specified project`() = runTest { + fun `loadIdentities - should load identities matching fingerprint format for the specified project`() = runTest { // Given setupInitialData() val project2Mock: Project = mockk { every { id } returns PROJECT_2_ID } // When - Query P1 for NEC val loadedP1Nec = dataSource - .loadFingerprintIdentities( + .loadIdentities( query = SubjectQuery( projectId = PROJECT_1_ID, - fingerprintSampleFormat = NEC_FORMAT, + format = NEC_FORMAT, ), ranges = listOf(0..10), project = project, @@ -845,10 +811,10 @@ class RoomEnrolmentRecordLocalDataSourceTest { .identities // When - Query P1 for ISO val loadedP1Iso = dataSource - .loadFingerprintIdentities( + .loadIdentities( query = SubjectQuery( projectId = PROJECT_1_ID, - fingerprintSampleFormat = ISO_FORMAT, + format = ISO_FORMAT, ), ranges = listOf(0..10), project = project, @@ -860,10 +826,10 @@ class RoomEnrolmentRecordLocalDataSourceTest { .identities // When - Query P2 for NEC val loadedP2Nec = dataSource - .loadFingerprintIdentities( + .loadIdentities( query = SubjectQuery( projectId = PROJECT_2_ID, - fingerprintSampleFormat = NEC_FORMAT, + format = NEC_FORMAT, ), ranges = listOf(0..10), project = project2Mock, @@ -879,14 +845,14 @@ class RoomEnrolmentRecordLocalDataSourceTest { assertThat(loadedP1Nec[0].subjectId).isEqualTo(subject2P1WithFinger.subjectId) assertThat(loadedP1Nec[0].samples).hasSize(1) assertThat(loadedP1Nec[0].samples[0].format).isEqualTo(NEC_FORMAT) - assertThat(loadedP1Nec[0].samples).isEqualTo(subject2P1WithFinger.fingerprintSamples) + assertThat(loadedP1Nec[0].samples).isEqualTo(subject2P1WithFinger.samples) // Then - P1 ISO assertThat(loadedP1Iso).hasSize(1) assertThat(loadedP1Iso[0].subjectId).isEqualTo(subject3P1WithBoth.subjectId) assertThat(loadedP1Iso[0].samples).hasSize(1) assertThat(loadedP1Iso[0].samples[0].format).isEqualTo(ISO_FORMAT) - assertThat(loadedP1Iso[0].samples).isEqualTo(subject3P1WithBoth.fingerprintSamples) + assertThat(loadedP1Iso[0].samples).isEqualTo(subject3P1WithBoth.samples.filter { it.modality == Modality.FINGERPRINT }) // Then - P2 NEC assertThat(loadedP2Nec).hasSize(2) @@ -898,11 +864,11 @@ class RoomEnrolmentRecordLocalDataSourceTest { } @Test - fun `loadFingerprintIdentities - should respect the range parameter for specific format`() = runTest { + fun `loadIdentities - should respect the range parameter for specific fingerprint format`() = runTest { // Given: More data val subject5P1WithNec = subject2P1WithFinger.copy( subjectId = "subj-005", - fingerprintSamples = listOf( + samples = listOf( fingerprintSample1.copy( id = "fp-uuid-5", referenceId = "ref-fp-5", @@ -912,7 +878,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { ) val subject6P1WithNec = subject2P1WithFinger.copy( subjectId = "subj-006", - fingerprintSamples = listOf( + samples = listOf( fingerprintSample1.copy( id = "fp-uuid-6", referenceId = "ref-fp-6", @@ -930,12 +896,12 @@ class RoomEnrolmentRecordLocalDataSourceTest { ) val baseQuery = SubjectQuery( projectId = PROJECT_1_ID, - fingerprintSampleFormat = NEC_FORMAT, + format = NEC_FORMAT, ) // When val loadedRanges = dataSource - .loadFingerprintIdentities( + .loadIdentities( query = baseQuery, ranges = listOf( 0..0, @@ -949,7 +915,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { val loadedFirstTwo = dataSource - .loadFingerprintIdentities( + .loadIdentities( query = baseQuery, ranges = listOf( 0..1, @@ -963,7 +929,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { .identities val loadedAll = dataSource - .loadFingerprintIdentities( + .loadIdentities( query = baseQuery, ranges = listOf(0..10), project = project, @@ -994,16 +960,16 @@ class RoomEnrolmentRecordLocalDataSourceTest { } @Test - fun `loadFingerprintIdentities - with query format matching nothing - should return empty list`() = runTest { + fun `loadIdentities - with query format matching nothing - should return empty list`() = runTest { // Given setupInitialData() // When val loadedIdentities = dataSource - .loadFingerprintIdentities( + .loadIdentities( query = SubjectQuery( projectId = PROJECT_1_ID, - fingerprintSampleFormat = UNUSED_FORMAT, + format = UNUSED_FORMAT, ), ranges = listOf(0..10), project = project, @@ -1019,38 +985,15 @@ class RoomEnrolmentRecordLocalDataSourceTest { verify(exactly = 0) { mockCallback() } } - @Test(expected = IllegalArgumentException::class) // Reverted to JUnit exception check - fun `loadFaceIdentities - should throw exception if format is missing in query`() = runTest { - // Given - setupInitialData() - val queryWithoutFormat = SubjectQuery(projectId = PROJECT_1_ID) - - // When - dataSource - .loadFaceIdentities( - // This call will throw - query = queryWithoutFormat, - ranges = listOf(0..10), - project = project, - dataSource = Simprints, - scope = this, - onCandidateLoaded = mockCallback, - ).toList() - .first() - - // Then: Handled by expected exception - verify(exactly = 0) { mockCallback() } - } - @Test - fun `loadFaceIdentities - should load identities matching format for the specified project`() = runTest { + fun `loadIdentities - should load identities matching face format for the specified project`() = runTest { // Given setupInitialData() // When val loadedP1Roc1 = dataSource - .loadFaceIdentities( - query = SubjectQuery(projectId = PROJECT_1_ID, faceSampleFormat = ROC_1_FORMAT), + .loadIdentities( + query = SubjectQuery(projectId = PROJECT_1_ID, format = ROC_1_FORMAT), ranges = listOf(0..10), project = project, dataSource = Simprints, @@ -1060,8 +1003,8 @@ class RoomEnrolmentRecordLocalDataSourceTest { .first() .identities val loadedP1Roc3 = dataSource - .loadFaceIdentities( - query = SubjectQuery(projectId = PROJECT_1_ID, faceSampleFormat = ROC_3_FORMAT), + .loadIdentities( + query = SubjectQuery(projectId = PROJECT_1_ID, format = ROC_3_FORMAT), ranges = listOf(0..10), project = project, dataSource = Simprints, @@ -1071,8 +1014,8 @@ class RoomEnrolmentRecordLocalDataSourceTest { .first() .identities val loadedP2Roc1 = dataSource - .loadFaceIdentities( - query = SubjectQuery(projectId = PROJECT_2_ID, faceSampleFormat = ROC_1_FORMAT), + .loadIdentities( + query = SubjectQuery(projectId = PROJECT_2_ID, format = ROC_1_FORMAT), ranges = listOf(0..10), project = project, dataSource = Simprints, @@ -1087,14 +1030,14 @@ class RoomEnrolmentRecordLocalDataSourceTest { assertThat(loadedP1Roc1[0].subjectId).isEqualTo(subject1P1WithFace.subjectId) assertThat(loadedP1Roc1[0].samples).hasSize(1) assertThat(loadedP1Roc1[0].samples[0].format).isEqualTo(ROC_1_FORMAT) - assertThat(loadedP1Roc1[0].samples).isEqualTo(subject1P1WithFace.faceSamples) + assertThat(loadedP1Roc1[0].samples).isEqualTo(subject1P1WithFace.samples) // Then - P1 ROC_3 assertThat(loadedP1Roc3).hasSize(1) assertThat(loadedP1Roc3[0].subjectId).isEqualTo(subject3P1WithBoth.subjectId) assertThat(loadedP1Roc3[0].samples).hasSize(1) assertThat(loadedP1Roc3[0].samples[0].format).isEqualTo(ROC_3_FORMAT) - assertThat(loadedP1Roc3[0].samples).isEqualTo(subject3P1WithBoth.faceSamples) + assertThat(loadedP1Roc3[0].samples).isEqualTo(subject3P1WithBoth.samples.filter { it.modality == Modality.FACE }) // Then - P2 ROC_1 assertThat(loadedP2Roc1).hasSize(2) @@ -1104,16 +1047,16 @@ class RoomEnrolmentRecordLocalDataSourceTest { } @Test - fun `loadFaceIdentities - should respect the range parameter for specific format`() = runTest { + fun `loadIdentities - should respect the range parameter for specific face format`() = runTest { // Given: More data val subject5P1WithRoc1 = subject1P1WithFace.copy( subjectId = "subj-005", - faceSamples = listOf(faceSample1.copy(id = "face-uuid-5", referenceId = "ref-face-5")), + samples = listOf(faceSample1.copy(id = "face-uuid-5", referenceId = "ref-face-5")), createdAt = Date(date.time + 1000), ) val subject6P1WithRoc1 = subject1P1WithFace.copy( subjectId = "subj-006", - faceSamples = listOf(faceSample1.copy(id = "face-uuid-6", referenceId = "ref-face-6")), + samples = listOf(faceSample1.copy(id = "face-uuid-6", referenceId = "ref-face-6")), createdAt = Date(date.time + 2000), ) setupInitialData() @@ -1125,11 +1068,11 @@ class RoomEnrolmentRecordLocalDataSourceTest { project, ) val baseQuery = - SubjectQuery(projectId = PROJECT_1_ID, faceSampleFormat = ROC_1_FORMAT, sort = true) + SubjectQuery(projectId = PROJECT_1_ID, format = ROC_1_FORMAT, sort = true) // When val loadedRanges = dataSource - .loadFaceIdentities( + .loadIdentities( query = baseQuery, ranges = listOf( 0..0, @@ -1143,7 +1086,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { val loadedFirstTwo = dataSource - .loadFaceIdentities( + .loadIdentities( query = baseQuery, ranges = listOf( 0..1, @@ -1156,7 +1099,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { .first() .identities val loadedAll = dataSource - .loadFaceIdentities( + .loadIdentities( query = baseQuery, ranges = listOf(0..10), project = project, @@ -1187,33 +1130,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { } @Test - fun `loadFaceIdentities - with query format matching nothing - should return empty list`() = runTest { - // Given - setupInitialData() - - // When - val loadedIdentities = dataSource - .loadFaceIdentities( - query = SubjectQuery( - projectId = PROJECT_1_ID, - faceSampleFormat = UNUSED_FORMAT, - ), - ranges = listOf(0..10), - project = project, - dataSource = Simprints, - scope = this, - onCandidateLoaded = mockCallback, - ).toList() - .first() - .identities - - // Then - assertThat(loadedIdentities).isEmpty() - verify(exactly = 0) { mockCallback() } - } - - @Test - fun `loadFaceIdentities - by attendantId and moduleId - should return correct identities`() = runTest { + fun `loadIdentities - by attendantId and moduleId and face format - should return correct identities`() = runTest { // Given setupInitialData() @@ -1222,7 +1139,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { projectId = PROJECT_1_ID, attendantId = ATTENDANT_1_ID, moduleId = MODULE_1_ID, - faceSampleFormat = ROC_1_FORMAT, + format = ROC_1_FORMAT, ) // Query for Project 1, Attendant 1, Module 2, Format ROC_3 @@ -1230,7 +1147,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { projectId = PROJECT_1_ID, attendantId = ATTENDANT_1_ID, moduleId = MODULE_2_ID, - faceSampleFormat = ROC_3_FORMAT, + format = ROC_3_FORMAT, ) // Query for Project 1, Attendant 1, Module 1, Format ROC_3 (should be empty) @@ -1238,12 +1155,12 @@ class RoomEnrolmentRecordLocalDataSourceTest { projectId = PROJECT_1_ID, attendantId = ATTENDANT_1_ID, moduleId = MODULE_1_ID, - faceSampleFormat = ROC_3_FORMAT, + format = ROC_3_FORMAT, ) // When val loadedP1A1M1Roc1 = dataSource - .loadFaceIdentities( + .loadIdentities( queryP1A1M1Roc1, listOf(0..10), project = project, @@ -1254,7 +1171,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { .first() .identities val loadedP1A1M2Roc3 = dataSource - .loadFaceIdentities( + .loadIdentities( queryP1A1M2Roc3, listOf( 0..10, @@ -1268,7 +1185,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { .identities val loadedP1A1M1Roc3Empty = dataSource - .loadFaceIdentities( + .loadIdentities( queryP1A1M1Roc3Empty, listOf( 0..10, @@ -1294,7 +1211,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { } @Test - fun `loadFingerprintIdentities - by attendantId and moduleId - should return correct identities`() = runTest { + fun `loadIdentities - by attendantId and moduleId and fingerprint format - should return correct identities`() = runTest { // Given setupInitialData() @@ -1303,7 +1220,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { projectId = PROJECT_1_ID, attendantId = ATTENDANT_1_ID, moduleId = MODULE_1_ID, - fingerprintSampleFormat = NEC_FORMAT, + format = NEC_FORMAT, ) // Query for Project 1, Attendant 1, Module 3, Format NEC @@ -1311,7 +1228,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { projectId = PROJECT_1_ID, attendantId = ATTENDANT_1_ID, moduleId = MODULE_3_ID, - fingerprintSampleFormat = NEC_FORMAT, + format = NEC_FORMAT, ) // Query for Project 1, Attendant 1, Module 2, Format ISO @@ -1319,7 +1236,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { projectId = PROJECT_1_ID, attendantId = ATTENDANT_1_ID, moduleId = MODULE_2_ID, - fingerprintSampleFormat = ISO_FORMAT, + format = ISO_FORMAT, ) // Query for Project 1, Attendant 2, Module 1, Format NEC (should be empty) @@ -1327,12 +1244,12 @@ class RoomEnrolmentRecordLocalDataSourceTest { projectId = PROJECT_1_ID, attendantId = ATTENDANT_2_ID, moduleId = MODULE_1_ID, - fingerprintSampleFormat = NEC_FORMAT, + format = NEC_FORMAT, ) // When val loadedP1A1M1Nec = dataSource - .loadFingerprintIdentities( + .loadIdentities( queryP1A1M1Nec, listOf( 0..10, @@ -1345,7 +1262,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { .first() .identities val loadedP1A1M3Nec = dataSource - .loadFingerprintIdentities( + .loadIdentities( queryP1A1M3Nec, listOf( 0..10, @@ -1358,7 +1275,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { .first() .identities val loadedP1A1M2Iso = dataSource - .loadFingerprintIdentities( + .loadIdentities( queryP1A1M2Iso, listOf( 0..10, @@ -1371,7 +1288,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { .first() .identities val loadedP1A2M1NecEmpty = dataSource - .loadFingerprintIdentities( + .loadIdentities( queryP1A2M1NecEmpty, listOf( 0..10, @@ -1477,7 +1394,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { assertThat(loadedSubject1).isNotNull() assertThat(loadedSubject1?.attendantId).isEqualTo(ATTENDANT_1_ID) assertThat(loadedSubject1?.moduleId).isEqualTo(MODULE_1_ID) - assertThat(loadedSubject1?.faceSamples).isEqualTo(subject1P1WithFace.faceSamples) + assertThat(loadedSubject1?.samples).isEqualTo(subject1P1WithFace.samples) } @Test @@ -1600,7 +1517,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { setupInitialData() val queryToDeleteModule1WithFormat = SubjectQuery( moduleId = MODULE_1_ID, - faceSampleFormat = ROC_1_FORMAT, // Adding format which is not allowed for delete + format = ROC_1_FORMAT, // Adding format which is not allowed for delete ) // When @@ -1651,8 +1568,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { fun `performActions - Update - should succeed when removing all samples but adding external credentials`() = runTest { dataSource.performActions(listOf(SubjectAction.Creation(subject3P1WithBoth)), project) val initial = dataSource.load(SubjectQuery(subjectId = subject3P1WithBoth.subjectId)).first() - assertThat(initial.faceSamples).hasSize(1) - assertThat(initial.fingerprintSamples).hasSize(1) + assertThat(initial.samples).hasSize(2) assertThat(initial.externalCredentials).hasSize(1) val newExternalCredential = ExternalCredential( @@ -1664,8 +1580,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { val updateAction = SubjectAction.Update( subjectId = subject3P1WithBoth.subjectId, - faceSamplesToAdd = listOf(), - fingerprintSamplesToAdd = listOf(), + samplesToAdd = listOf(), referenceIdsToRemove = listOf(faceSample2.referenceId, fingerprintSample2.referenceId), // Remove all samples externalCredentialsToAdd = listOf(newExternalCredential), externalCredentialIdsToRemove = listOf(), @@ -1673,8 +1588,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { dataSource.performActions(listOf(updateAction), project) val loaded = dataSource.load(SubjectQuery(subjectId = subject3P1WithBoth.subjectId)).first() - assertThat(loaded.faceSamples).isEmpty() - assertThat(loaded.fingerprintSamples).isEmpty() + assertThat(loaded.samples).isEmpty() assertThat(loaded.externalCredentials).hasSize(2) assertThat(loaded.externalCredentials).contains(newExternalCredential) } @@ -1701,8 +1615,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { val updateAction = SubjectAction.Update( subjectId = subject3P1WithBoth.subjectId, - faceSamplesToAdd = listOf(), - fingerprintSamplesToAdd = listOf(), + samplesToAdd = listOf(), referenceIdsToRemove = listOf(), externalCredentialsToAdd = listOf(), externalCredentialIdsToRemove = listOf("credential-id-1"), diff --git a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/RoomEnrolmentRecordQueryBuilderTest.kt b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/RoomEnrolmentRecordQueryBuilderTest.kt index aa90089724..3c71b11bb9 100644 --- a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/RoomEnrolmentRecordQueryBuilderTest.kt +++ b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/RoomEnrolmentRecordQueryBuilderTest.kt @@ -152,17 +152,8 @@ class RoomEnrolmentRecordQueryBuilderTest { } @Test - fun `buildSubjectQuery throws error if fingerprintSampleFormat is set`() { - val subjectQuery = SubjectQuery(fingerprintSampleFormat = "ISO_19794_2_2005") - val exception = assertThrows { - queryBuilder.buildSubjectQuery(subjectQuery) - } - assertThat(exception.message).isEqualTo("Cannot set format for subject query, use buildBiometricTemplatesQuery instead") - } - - @Test - fun `buildSubjectQuery throws error if faceSampleFormat is set`() { - val subjectQuery = SubjectQuery(faceSampleFormat = "RAW") + fun `buildSubjectQuery throws error if format is set`() { + val subjectQuery = SubjectQuery(format = "ISO_19794_2_2005") val exception = assertThrows { queryBuilder.buildSubjectQuery(subjectQuery) } @@ -207,23 +198,9 @@ class RoomEnrolmentRecordQueryBuilderTest { } @Test - fun `buildCountQuery with fingerprintSampleFormat`() { + fun `buildCountQuery with format`() { val format = "ISO_FP" - val subjectQuery = SubjectQuery(fingerprintSampleFormat = format) - val expectedSql = - "SELECT COUNT(DISTINCT S.$SUBJECT_ID_COLUMN) FROM $SUBJECT_TABLE_NAME S INNER JOIN $TEMPLATE_TABLE_NAME T" + - " using(subjectId) WHERE T.$FORMAT_COLUMN = ?" - - val resultQuery = queryBuilder.buildCountQuery(subjectQuery) - - assertThat(resultQuery.sql).isEqualTo(expectedSql) - assertThat(getArgs(resultQuery)).isEqualTo(arrayOf(format)) - } - - @Test - fun `buildCountQuery with faceSampleFormat`() { - val format = "RAW_FACE" - val subjectQuery = SubjectQuery(faceSampleFormat = format) + val subjectQuery = SubjectQuery(format = format) val expectedSql = "SELECT COUNT(DISTINCT S.$SUBJECT_ID_COLUMN) FROM $SUBJECT_TABLE_NAME S INNER JOIN $TEMPLATE_TABLE_NAME T" + " using(subjectId) WHERE T.$FORMAT_COLUMN = ?" @@ -235,10 +212,10 @@ class RoomEnrolmentRecordQueryBuilderTest { } @Test - fun `buildCountQuery with projectId and fingerprintSampleFormat`() { + fun `buildCountQuery with projectId and format`() { val projectId = "p1" val format = "ISO_FP" - val subjectQuery = SubjectQuery(projectId = projectId, fingerprintSampleFormat = format) + val subjectQuery = SubjectQuery(projectId = projectId, format = format) val expectedSql = "SELECT COUNT(DISTINCT S.$SUBJECT_ID_COLUMN) FROM $SUBJECT_TABLE_NAME S INNER JOIN $TEMPLATE_TABLE_NAME T" + " using(subjectId) WHERE S.$PROJECT_ID_COLUMN = ? AND T.$FORMAT_COLUMN = ?" @@ -249,15 +226,6 @@ class RoomEnrolmentRecordQueryBuilderTest { assertThat(getArgs(resultQuery)).isEqualTo(arrayOf(projectId, format)) } - @Test - fun `buildCountQuery throws if both fingerprint and face formats set`() { - val subjectQuery = SubjectQuery(fingerprintSampleFormat = "FP_FORMAT", faceSampleFormat = "FACE_FORMAT") - val exception = assertThrows { - queryBuilder.buildCountQuery(subjectQuery) - } - assertThat(exception.message).isEqualTo("Cannot set both fingerprintSampleFormat and faceSampleFormat") - } - @Test fun `buildBiometricTemplatesQuery throws error if format is not set`() { val subjectQuery = SubjectQuery() @@ -271,10 +239,10 @@ class RoomEnrolmentRecordQueryBuilderTest { } @Test - fun `buildBiometricTemplatesQuery with fingerprintSampleFormat`() { + fun `buildBiometricTemplatesQuery with format`() { val format = "ISO_FP_TEMPLATE" val pageSize = 10 - val subjectQuery = SubjectQuery(fingerprintSampleFormat = format) + val subjectQuery = SubjectQuery(format = format) val expectedSql = """ SELECT A.* @@ -295,37 +263,11 @@ class RoomEnrolmentRecordQueryBuilderTest { assertThat(getArgs(resultQuery)).isEqualTo(arrayOf(format)) } - @Test - fun `buildBiometricTemplatesQuery with faceSampleFormat and projectId`() { - val format = "RAW_FACE_TEMPLATE" - val projectId = "projX" - val pageSize = 5 - val subjectQuery = SubjectQuery(faceSampleFormat = format, projectId = projectId) - val expectedSql = - """ - SELECT A.* - FROM $TEMPLATE_TABLE_NAME A - INNER JOIN ( - SELECT distinct S.subjectId - FROM $SUBJECT_TABLE_NAME S INNER JOIN $TEMPLATE_TABLE_NAME T - USING(subjectId) - WHERE S.$PROJECT_ID_COLUMN = ? AND T.$FORMAT_COLUMN = ? - ORDER BY S.$SUBJECT_ID_COLUMN ASC - LIMIT $pageSize - ) B USING(subjectId) where A.format ='$format' - """.trimIndent() - - val resultQuery = queryBuilder.buildBiometricTemplatesQuery(subjectQuery, pageSize) - - assertThat(resultQuery.sql).isEqualTo(expectedSql) - assertThat(getArgs(resultQuery)).isEqualTo(arrayOf(projectId, format)) - } - @Test fun `buildBiometricTemplatesQuery uses sort true internally`() { val format = "ANY_FORMAT" val pageSize = 15 - val subjectQuery = SubjectQuery(fingerprintSampleFormat = format, sort = false) + val subjectQuery = SubjectQuery(format = format, sort = false) val expectedSql = """ SELECT A.* @@ -347,31 +289,12 @@ class RoomEnrolmentRecordQueryBuilderTest { } @Test - fun `buildBiometricTemplatesQuery throws if both fingerprint and face formats set`() { - val subjectQuery = SubjectQuery(fingerprintSampleFormat = "FP_FORMAT", faceSampleFormat = "FACE_FORMAT") - val pageSize = 10 - val exception = assertThrows { - queryBuilder.buildBiometricTemplatesQuery(subjectQuery, pageSize) - } - assertThat(exception.message).isEqualTo("Cannot set both fingerprintSampleFormat and faceSampleFormat") - } - - @Test - fun `buildDeleteQuery throws error if fingerprintSampleFormat is set`() { - val subjectQuery = SubjectQuery(fingerprintSampleFormat = "ISO_19794_2_2005") - val exception = assertThrows { - queryBuilder.buildDeleteQuery(subjectQuery) - } - assertThat(exception.message).isEqualTo("faceSampleFormat and fingerprintSampleFormat are not supported for deletion") - } - - @Test - fun `buildDeleteQuery throws error if faceSampleFormat is set`() { - val subjectQuery = SubjectQuery(faceSampleFormat = "RAW") + fun `buildDeleteQuery throws error if format is set`() { + val subjectQuery = SubjectQuery(format = "ISO_19794_2_2005") val exception = assertThrows { queryBuilder.buildDeleteQuery(subjectQuery) } - assertThat(exception.message).isEqualTo("faceSampleFormat and fingerprintSampleFormat are not supported for deletion") + assertThat(exception.message).isEqualTo("format is not supported for deletion") } @Test diff --git a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/models/DbSubjectTest.kt b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/models/DbSubjectTest.kt index 4ace060947..ba4ce21f7e 100644 --- a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/models/DbSubjectTest.kt +++ b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/models/DbSubjectTest.kt @@ -48,8 +48,7 @@ class DbSubjectTest { moduleId = MODULE_ID, createdAt = Date(0), updatedAt = Date(1500), - fingerprintSamples = listOf(fingerprintSample), - faceSamples = listOf(faceSample), + samples = listOf(fingerprintSample, faceSample), ) val dbSubject = domainSubject.toRealmDb() @@ -104,9 +103,9 @@ class DbSubjectTest { assertThat(updatedAt).isEqualTo(Date(1500)) assertThat(moduleId).isEqualTo(MODULE_ID) assertThat(projectId).isEqualTo(PROJECT_ID) - assertThat(fingerprintSamples.first().id).isEqualTo(fingerprintSample.id) - assertThat(fingerprintSamples.first().referenceId).isEqualTo(REFERENCE_ID) - assertThat(faceSamples.first().referenceId).isEqualTo(REFERENCE_ID) + assertThat(samples.first { it.modality == Modality.FINGERPRINT }.id).isEqualTo(fingerprintSample.id) + assertThat(samples.first { it.modality == Modality.FINGERPRINT }.referenceId).isEqualTo(REFERENCE_ID) + assertThat(samples.first { it.modality == Modality.FACE }.referenceId).isEqualTo(REFERENCE_ID) } } } diff --git a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/remote/EnrolmentRecordRemoteDataSourceImplTest.kt b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/remote/EnrolmentRecordRemoteDataSourceImplTest.kt index 981786d2c1..cd97b91c64 100644 --- a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/remote/EnrolmentRecordRemoteDataSourceImplTest.kt +++ b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/remote/EnrolmentRecordRemoteDataSourceImplTest.kt @@ -68,7 +68,7 @@ class EnrolmentRecordRemoteDataSourceImplTest { projectId = PROJECT_ID, moduleId = MODULE_ID, attendantId = ATTENDANT_ID, - fingerprintSamples = listOf( + samples = listOf( Sample( identifier = SampleIdentifier.LEFT_3RD_FINGER, template = FINGERPRINT_TEMPLATE, @@ -76,8 +76,6 @@ class EnrolmentRecordRemoteDataSourceImplTest { referenceId = "5289df73-7df5-3326-bcdd-22597afb1fac", modality = Modality.FINGERPRINT, ), - ), - faceSamples = listOf( Sample( template = FACE_TEMPLATE, format = "faceTemplateFormat", 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 6e9ba73d98..7c431c6a15 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 @@ -29,8 +29,7 @@ class SubjectFactory @Inject constructor( projectId = projectId, attendantId = attendantId, moduleId = moduleId, - fingerprintSamples = extractFingerprintSamplesFromBiometricReferences(this.biometricReferences), - faceSamples = extractFaceSamplesFromBiometricReferences(this.biometricReferences), + samples = extractSamplesFromBiometricReferences(this.biometricReferences), externalCredentials = payload.externalCredentials, ) } @@ -41,8 +40,7 @@ class SubjectFactory @Inject constructor( projectId = projectId, attendantId = attendantId, moduleId = moduleId, - fingerprintSamples = extractFingerprintSamplesFromBiometricReferences(this.biometricReferences), - faceSamples = extractFaceSamplesFromBiometricReferences(this.biometricReferences), + samples = extractSamplesFromBiometricReferences(this.biometricReferences), externalCredentials = externalCredential?.let { listOf(it) } ?: emptyList(), ) } @@ -52,17 +50,13 @@ class SubjectFactory @Inject constructor( payload: EnrolmentRecordUpdatePayload, ): Subject { val removedBiometricReferences = payload.biometricReferencesRemoved.toSet() // to make lookup O(1) - val addedFaceSamples = extractFaceSamplesFromBiometricReferences(payload.biometricReferencesAdded) - val addedFingerprintSamples = extractFingerprintSamplesFromBiometricReferences(payload.biometricReferencesAdded) + val addedSamples = extractSamplesFromBiometricReferences(payload.biometricReferencesAdded) val externalCredentialsAdded = payload.externalCredentialsAdded return existingSubject.copy( - faceSamples = existingSubject.faceSamples + samples = existingSubject.samples .filterNot { it.referenceId in removedBiometricReferences } - .plus(addedFaceSamples), - fingerprintSamples = existingSubject.fingerprintSamples - .filterNot { it.referenceId in removedBiometricReferences } - .plus(addedFingerprintSamples), + .plus(addedSamples), externalCredentials = existingSubject.externalCredentials .plus(externalCredentialsAdded) .distinctBy { it.value.value }, @@ -74,8 +68,7 @@ class SubjectFactory @Inject constructor( projectId: String, attendantId: TokenizableString, moduleId: TokenizableString, - fingerprintResponse: CaptureIdentity?, - faceResponse: CaptureIdentity?, + captures: List, externalCredential: ExternalCredential?, ): Subject = buildSubject( subjectId = subjectId, @@ -83,8 +76,7 @@ class SubjectFactory @Inject constructor( attendantId = attendantId, moduleId = moduleId, createdAt = Date(timeHelper.now().ms), - fingerprintSamples = fingerprintResponse?.let { extractFingerprintSamples(it) }.orEmpty(), - faceSamples = faceResponse?.let { extractFaceSamples(it) }.orEmpty(), + samples = captures.flatMap { extractCaptureSamples(it) }, externalCredentials = externalCredential?.let { listOf(it) } ?: emptyList(), ) @@ -95,8 +87,7 @@ class SubjectFactory @Inject constructor( moduleId: TokenizableString, createdAt: Date? = null, updatedAt: Date? = null, - fingerprintSamples: List = emptyList(), - faceSamples: List = emptyList(), + samples: List = emptyList(), externalCredentials: List = emptyList(), ) = Subject( subjectId = subjectId, @@ -105,34 +96,27 @@ class SubjectFactory @Inject constructor( moduleId = moduleId, createdAt = createdAt, updatedAt = updatedAt, - fingerprintSamples = fingerprintSamples, - faceSamples = faceSamples, + samples = samples, externalCredentials = externalCredentials, ) - private fun extractFingerprintSamples(fingerprintResponse: CaptureIdentity) = fingerprintResponse.samples.map { sample -> + private fun extractCaptureSamples(response: CaptureIdentity) = response.samples.map { sample -> Sample( identifier = sample.identifier, template = sample.template, format = sample.format, - referenceId = fingerprintResponse.referenceId, + referenceId = response.referenceId, modality = sample.modality, ) } - private fun extractFaceSamples(faceResponse: CaptureIdentity) = faceResponse.samples.map { - Sample( - template = it.template, - format = it.format, - referenceId = faceResponse.referenceId, - modality = it.modality, - ) - } - - fun extractFingerprintSamplesFromBiometricReferences(biometricReferences: List?) = biometricReferences - ?.filterIsInstance() - ?.map { reference -> reference.templates.map { buildFingerprintSample(it, reference.format, reference.id) } } - ?.flatten() + fun extractSamplesFromBiometricReferences(biometricReferences: List?) = biometricReferences + ?.map { reference -> + when (reference) { + is FingerprintReference -> reference.templates.map { buildFingerprintSample(it, reference.format, reference.id) } + is FaceReference -> reference.templates.map { buildFaceSample(it, reference.format, reference.id) } + } + }?.flatten() ?: emptyList() private fun buildFingerprintSample( @@ -147,12 +131,6 @@ class SubjectFactory @Inject constructor( modality = Modality.FINGERPRINT, ) - fun extractFaceSamplesFromBiometricReferences(biometricReferences: List?) = biometricReferences - ?.filterIsInstance() - ?.map { reference -> reference.templates.map { buildFaceSample(it, reference.format, reference.id) } } - ?.flatten() - ?: emptyList() - private fun buildFaceSample( template: FaceTemplate, format: String, diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/tasks/BaseEventDownSyncTask.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/tasks/BaseEventDownSyncTask.kt index 20427b199a..cf2a034b7e 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/tasks/BaseEventDownSyncTask.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/tasks/BaseEventDownSyncTask.kt @@ -202,7 +202,7 @@ internal abstract class BaseEventDownSyncTask( private fun handleSubjectCreationEvent(event: EnrolmentRecordCreationEvent): List { val subject = subjectFactory.buildSubjectFromCreationPayload(event.payload) - return if (subject.fingerprintSamples.isNotEmpty() || subject.faceSamples.isNotEmpty()) { + return if (subject.samples.isNotEmpty()) { listOf(Creation(subject)) } else { emptyList() @@ -259,7 +259,7 @@ internal abstract class BaseEventDownSyncTask( private fun createASubjectActionFromRecordCreation(enrolmentRecordCreation: EnrolmentRecordCreationInMove?): Creation? = enrolmentRecordCreation?.let { val subject = subjectFactory.buildSubjectFromMovePayload(it) - if (subject.fingerprintSamples.isNotEmpty() || subject.faceSamples.isNotEmpty()) { + if (subject.samples.isNotEmpty()) { Creation(subject) } else { null @@ -273,8 +273,7 @@ internal abstract class BaseEventDownSyncTask( listOf( SubjectAction.Update( subjectId = subjectId, - faceSamplesToAdd = subjectFactory.extractFaceSamplesFromBiometricReferences(biometricReferencesAdded), - fingerprintSamplesToAdd = subjectFactory.extractFingerprintSamplesFromBiometricReferences(biometricReferencesAdded), + samplesToAdd = subjectFactory.extractSamplesFromBiometricReferences(biometricReferencesAdded), referenceIdsToRemove = biometricReferencesRemoved, externalCredentialsToAdd = externalCredentialsAdded, externalCredentialIdsToRemove = emptyList(), // Only used locally to ensure a single credential is linked per session diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/CommCareEventSyncTaskTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/CommCareEventSyncTaskTest.kt index 5af99c2c14..2a6bee52b7 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/CommCareEventSyncTaskTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/CommCareEventSyncTaskTest.kt @@ -332,7 +332,7 @@ class CommCareEventSyncTaskTest { projectId = "projectId", attendantId = "moduleId".asTokenizableRaw(), moduleId = "attendantId".asTokenizableRaw(), - faceSamples = listOf( + samples = listOf( Sample( template = byteArrayOf(), format = "format", diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/SimprintsEventDownSyncTaskTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/SimprintsEventDownSyncTaskTest.kt index e92f4184c8..20da78d3ce 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/SimprintsEventDownSyncTaskTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/SimprintsEventDownSyncTaskTest.kt @@ -555,7 +555,7 @@ class SimprintsEventDownSyncTaskTest { projectId = "projectId", attendantId = "moduleId".asTokenizableRaw(), moduleId = "attendantId".asTokenizableRaw(), - faceSamples = listOf( + samples = listOf( Sample( template = byteArrayOf(), format = "format", 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 55870750d0..6a239d4f7a 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 @@ -70,7 +70,7 @@ class SubjectFactoryTest { projectId = PROJECT_ID, attendantId = ATTENDANT_ID, moduleId = MODULE_ID, - fingerprintSamples = listOf( + samples = listOf( Sample( identifier = IDENTIFIER, template = BASE_64_BYTES, @@ -78,8 +78,6 @@ class SubjectFactoryTest { referenceId = REFERENCE_ID, modality = Modality.FINGERPRINT, ), - ), - faceSamples = listOf( Sample( template = BASE_64_BYTES, format = REFERENCE_FORMAT, @@ -108,7 +106,7 @@ class SubjectFactoryTest { projectId = PROJECT_ID, attendantId = ATTENDANT_ID, moduleId = MODULE_ID, - fingerprintSamples = listOf( + samples = listOf( Sample( identifier = IDENTIFIER, template = BASE_64_BYTES, @@ -116,8 +114,6 @@ class SubjectFactoryTest { referenceId = REFERENCE_ID, modality = Modality.FINGERPRINT, ), - ), - faceSamples = listOf( Sample( template = BASE_64_BYTES, format = REFERENCE_FORMAT, @@ -136,7 +132,7 @@ class SubjectFactoryTest { projectId = PROJECT_ID, attendantId = ATTENDANT_ID, moduleId = MODULE_ID, - fingerprintSamples = listOf( + samples = listOf( Sample( identifier = IDENTIFIER, template = BASE_64_BYTES, @@ -151,8 +147,6 @@ class SubjectFactoryTest { referenceId = "referenceId-finger-2", modality = Modality.FINGERPRINT, ), - ), - faceSamples = listOf( Sample( template = BASE_64_BYTES, format = REFERENCE_FORMAT, @@ -198,7 +192,7 @@ class SubjectFactoryTest { projectId = PROJECT_ID, attendantId = ATTENDANT_ID, moduleId = MODULE_ID, - fingerprintSamples = listOf( + samples = listOf( Sample( identifier = IDENTIFIER, template = BASE_64_BYTES, @@ -213,8 +207,6 @@ class SubjectFactoryTest { referenceId = "referenceId-finger-5", modality = Modality.FINGERPRINT, ), - ), - faceSamples = listOf( Sample( template = BASE_64_BYTES, format = REFERENCE_FORMAT, @@ -230,7 +222,9 @@ class SubjectFactoryTest { ), externalCredentials = listOf(EXTERNAL_CREDENTIAL), ) - assertThat(result).isEqualTo(expected) + assertThat(result.subjectId).isEqualTo(expected.subjectId) + assertThat(result.samples.size).isEqualTo(expected.samples.size) + assertThat(result.samples).containsExactlyElementsIn(expected.samples) } @Test @@ -243,7 +237,7 @@ class SubjectFactoryTest { attendantId = ATTENDANT_ID, moduleId = MODULE_ID, createdAt = Date(0L), - fingerprintSamples = listOf( + samples = listOf( Sample( identifier = IDENTIFIER, template = BASE_64_BYTES, @@ -251,8 +245,6 @@ class SubjectFactoryTest { referenceId = REFERENCE_ID, modality = Modality.FINGERPRINT, ), - ), - faceSamples = listOf( Sample( template = BASE_64_BYTES, format = REFERENCE_FORMAT, @@ -268,28 +260,30 @@ class SubjectFactoryTest { projectId = expected.projectId, attendantId = expected.attendantId, moduleId = expected.moduleId, - fingerprintResponse = CaptureIdentity( - GUID1, - Modality.FINGERPRINT, - listOf( - CaptureSample( - captureEventId = GUID1, - identifier = IDENTIFIER, - template = BASE_64_BYTES, - format = REFERENCE_FORMAT, - modality = Modality.FINGERPRINT, + captures = listOf( + CaptureIdentity( + GUID1, + Modality.FINGERPRINT, + listOf( + CaptureSample( + captureEventId = GUID1, + identifier = IDENTIFIER, + template = BASE_64_BYTES, + format = REFERENCE_FORMAT, + modality = Modality.FINGERPRINT, + ), ), ), - ), - faceResponse = CaptureIdentity( - GUID1, - Modality.FACE, - listOf( - CaptureSample( - captureEventId = GUID1, - template = BASE_64_BYTES, - format = REFERENCE_FORMAT, - modality = Modality.FACE, + CaptureIdentity( + GUID1, + Modality.FACE, + listOf( + CaptureSample( + captureEventId = GUID1, + template = BASE_64_BYTES, + format = REFERENCE_FORMAT, + modality = Modality.FACE, + ), ), ), ), @@ -305,7 +299,7 @@ class SubjectFactoryTest { projectId = PROJECT_ID, attendantId = ATTENDANT_ID, moduleId = MODULE_ID, - fingerprintSamples = listOf( + samples = listOf( Sample( identifier = IDENTIFIER, template = BASE_64_BYTES, @@ -313,8 +307,6 @@ class SubjectFactoryTest { referenceId = REFERENCE_ID, modality = Modality.FINGERPRINT, ), - ), - faceSamples = listOf( Sample( template = BASE_64_BYTES, format = REFERENCE_FORMAT, @@ -330,8 +322,7 @@ class SubjectFactoryTest { projectId = expected.projectId, attendantId = expected.attendantId, moduleId = expected.moduleId, - fingerprintSamples = expected.fingerprintSamples, - faceSamples = expected.faceSamples, + samples = expected.samples, externalCredentials = expected.externalCredentials, ) assertThat(result).isEqualTo(expected) diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/BiometricReference.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/BiometricReference.kt index c8eed959ce..1a1f9b1a20 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/BiometricReference.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/BiometricReference.kt @@ -16,6 +16,7 @@ import com.simprints.infra.events.event.domain.models.subject.BiometricReference @Keep sealed class BiometricReference( open val id: String, + open val format: String, val type: BiometricReferenceType, ) @@ -23,17 +24,17 @@ sealed class BiometricReference( data class FaceReference( override val id: String, val templates: List, - val format: String, + override val format: String, val metadata: Map? = null, -) : BiometricReference(id, BiometricReferenceType.FACE_REFERENCE) +) : BiometricReference(id, format, BiometricReferenceType.FACE_REFERENCE) @ExcludedFromGeneratedTestCoverageReports("Domain model") data class FingerprintReference( override val id: String, val templates: List, - val format: String, + override val format: String, val metadata: Map? = null, -) : BiometricReference(id, BiometricReferenceType.FINGERPRINT_REFERENCE) +) : BiometricReference(id, format, BiometricReferenceType.FINGERPRINT_REFERENCE) enum class BiometricReferenceType { // a constant key is required to serialise/deserialize diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordCreationEvent.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordCreationEvent.kt index e6bf478f36..064295b2f7 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordCreationEvent.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordCreationEvent.kt @@ -2,6 +2,7 @@ package com.simprints.infra.events.event.domain.models.subject import androidx.annotation.Keep import com.simprints.core.ExcludedFromGeneratedTestCoverageReports +import com.simprints.core.domain.common.Modality import com.simprints.core.domain.externalcredential.ExternalCredential import com.simprints.core.domain.sample.Sample import com.simprints.core.domain.tokenization.TokenizableString @@ -45,56 +46,44 @@ data class EnrolmentRecordCreationEvent( companion object { fun buildBiometricReferences( - fingerprintSamples: List, - faceSamples: List, + samples: List, encoder: EncodingUtils, - ): List { - val biometricReferences = mutableListOf() - - buildFingerprintReference(fingerprintSamples, encoder)?.let { - biometricReferences.add(it) - } - - buildFaceReference(faceSamples, encoder)?.let { - biometricReferences.add(it) + ): List = samples.groupBy { it.modality }.mapNotNull { (modality, modalitySamples) -> + if (modalitySamples.isNotEmpty()) { + when (modality) { + Modality.FACE -> buildFingerprintReference(modalitySamples, encoder) + Modality.FINGERPRINT -> buildFaceReference(modalitySamples, encoder) + } + } else { + null } - - return biometricReferences } private fun buildFingerprintReference( fingerprintSamples: List, encoder: EncodingUtils, - ) = if (fingerprintSamples.isNotEmpty()) { - FingerprintReference( - fingerprintSamples.first().referenceId, - fingerprintSamples.map { - FingerprintTemplate( - encoder.byteArrayToBase64(it.template), - it.identifier, - ) - }, - fingerprintSamples.first().format, - ) - } else { - null - } + ) = FingerprintReference( + fingerprintSamples.first().referenceId, + fingerprintSamples.map { + FingerprintTemplate( + encoder.byteArrayToBase64(it.template), + it.identifier, + ) + }, + fingerprintSamples.first().format, + ) private fun buildFaceReference( faceSamples: List, encoder: EncodingUtils, - ) = if (faceSamples.isNotEmpty()) { - FaceReference( - faceSamples.first().referenceId, - faceSamples.map { - FaceTemplate( - encoder.byteArrayToBase64(it.template), - ) - }, - faceSamples.first().format, - ) - } else { - null - } + ) = FaceReference( + faceSamples.first().referenceId, + faceSamples.map { + FaceTemplate( + encoder.byteArrayToBase64(it.template), + ) + }, + faceSamples.first().format, + ) } } diff --git a/infra/matching/src/main/java/com/simprints/infra/matching/MatchParams.kt b/infra/matching/src/main/java/com/simprints/infra/matching/MatchParams.kt index 8ee2417870..b22dfd329d 100644 --- a/infra/matching/src/main/java/com/simprints/infra/matching/MatchParams.kt +++ b/infra/matching/src/main/java/com/simprints/infra/matching/MatchParams.kt @@ -6,7 +6,6 @@ import com.simprints.core.domain.common.ModalitySdkType import com.simprints.core.domain.sample.CaptureSample import com.simprints.core.domain.step.StepParams import com.simprints.infra.config.store.models.FaceConfiguration -import com.simprints.infra.config.store.models.FingerprintConfiguration import com.simprints.infra.enrolment.records.repository.domain.models.BiometricDataSource import com.simprints.infra.enrolment.records.repository.domain.models.SubjectQuery @@ -14,11 +13,8 @@ import com.simprints.infra.enrolment.records.repository.domain.models.SubjectQue data class MatchParams( val probeReferenceId: String, val bioSdk: ModalitySdkType, - val probeFaceSamples: List = emptyList(), - val probeFingerprintSamples: List = emptyList(), + val probeSamples: List = emptyList(), val flowType: FlowType, val queryForCandidates: SubjectQuery, val biometricDataSource: BiometricDataSource, -) : StepParams { - fun isFaceMatch() = probeFaceSamples.isNotEmpty() -} +) : StepParams diff --git a/infra/matching/src/main/java/com/simprints/infra/matching/usecase/FaceMatcherUseCase.kt b/infra/matching/src/main/java/com/simprints/infra/matching/usecase/FaceMatcherUseCase.kt index de82db7fcc..7e0cf05608 100644 --- a/infra/matching/src/main/java/com/simprints/infra/matching/usecase/FaceMatcherUseCase.kt +++ b/infra/matching/src/main/java/com/simprints/infra/matching/usecase/FaceMatcherUseCase.kt @@ -46,12 +46,12 @@ class FaceMatcherUseCase @Inject constructor( } val bioSdk = resolveFaceBioSdk(matchParams.bioSdk) - if (matchParams.probeFaceSamples.isEmpty()) { + if (matchParams.probeSamples.isEmpty()) { send(MatcherState.Success(emptyList(), emptyList(), 0, bioSdk.matcherName())) return@channelFlow } val queryWithSupportedFormat = matchParams.queryForCandidates.copy( - faceSampleFormat = bioSdk.templateFormat(), + format = bioSdk.templateFormat(), ) val expectedCandidates = enrolmentRecordRepository.count( queryWithSupportedFormat, @@ -72,7 +72,7 @@ class FaceMatcherUseCase @Inject constructor( val ranges = createRanges(expectedCandidates) val resultSet = MatchResultSet() val candidatesChannel = enrolmentRecordRepository - .loadFaceIdentities( + .loadIdentities( query = queryWithSupportedFormat, ranges = ranges, dataSource = matchParams.biometricDataSource, @@ -83,12 +83,12 @@ class FaceMatcherUseCase @Inject constructor( this@channelFlow.send(MatcherState.CandidateLoaded) } - val batchInfo = consumeAndMatch(candidatesChannel, matchParams.probeFaceSamples, resultSet, bioSdk) + val batchInfo = consumeAndMatch(candidatesChannel, matchParams.probeSamples, resultSet, bioSdk) send(MatcherState.Success(resultSet.toList(), batchInfo, loadedCandidates.get(), bioSdk.matcherName())) }.flowOn(dispatcherBG) private suspend fun consumeAndMatch( - candidatesChannel: ReceiveChannel>, + candidatesChannel: ReceiveChannel, samples: List, resultSet: MatchResultSet, bioSdk: FaceBioSDK, diff --git a/infra/matching/src/main/java/com/simprints/infra/matching/usecase/FingerprintMatcherUseCase.kt b/infra/matching/src/main/java/com/simprints/infra/matching/usecase/FingerprintMatcherUseCase.kt index ce83a733d8..975fc4c737 100644 --- a/infra/matching/src/main/java/com/simprints/infra/matching/usecase/FingerprintMatcherUseCase.kt +++ b/infra/matching/src/main/java/com/simprints/infra/matching/usecase/FingerprintMatcherUseCase.kt @@ -48,14 +48,14 @@ class FingerprintMatcherUseCase @Inject constructor( } val bioSdkWrapper = resolveBioSdkWrapper(matchParams.bioSdk) - if (matchParams.probeFingerprintSamples.isEmpty()) { + if (matchParams.probeSamples.isEmpty()) { send(MatcherState.Success(emptyList(), emptyList(), 0, bioSdkWrapper.matcherName)) return@channelFlow } // Only candidates with supported template format are considered val queryWithSupportedFormat = matchParams.queryForCandidates.copy( - fingerprintSampleFormat = bioSdkWrapper.supportedTemplateFormat, + format = bioSdkWrapper.supportedTemplateFormat, ) val expectedCandidates = enrolmentRecordRepository.count(queryWithSupportedFormat, dataSource = matchParams.biometricDataSource) if (expectedCandidates == 0) { @@ -72,7 +72,7 @@ class FingerprintMatcherUseCase @Inject constructor( val loadedCandidates = AtomicInteger(0) val ranges = createRanges(expectedCandidates) // if number of ranges less than the number of cores then use the number of ranges - val channel = enrolmentRecordRepository.loadFingerprintIdentities( + val channel = enrolmentRecordRepository.loadIdentities( query = queryWithSupportedFormat, ranges = ranges, dataSource = matchParams.biometricDataSource, @@ -87,7 +87,7 @@ class FingerprintMatcherUseCase @Inject constructor( val batchInfo = consumeAndMatch( channel = channel, - samples = matchParams.probeFingerprintSamples, + samples = matchParams.probeSamples, resultSet = resultSet, bioSdk = matchParams.bioSdk, bioSdkWrapper = bioSdkWrapper, @@ -99,7 +99,7 @@ class FingerprintMatcherUseCase @Inject constructor( }.flowOn(dispatcherBG) private suspend fun consumeAndMatch( - channel: ReceiveChannel>, + channel: ReceiveChannel, samples: List, resultSet: MatchResultSet, bioSdk: FingerprintConfiguration.BioSdk, diff --git a/infra/matching/src/test/java/com/simprints/infra/matching/usecase/FaceMatcherUseCaseTest.kt b/infra/matching/src/test/java/com/simprints/infra/matching/usecase/FaceMatcherUseCaseTest.kt index 099cb37175..0c984ce674 100644 --- a/infra/matching/src/test/java/com/simprints/infra/matching/usecase/FaceMatcherUseCaseTest.kt +++ b/infra/matching/src/test/java/com/simprints/infra/matching/usecase/FaceMatcherUseCaseTest.kt @@ -102,7 +102,7 @@ internal class FaceMatcherUseCaseTest { .invoke( MatchParams( probeReferenceId = "referenceId", - probeFaceSamples = listOf( + probeSamples = listOf( CaptureSample( captureEventId = "faceId", template = byteArrayOf(1, 2, 3), @@ -149,7 +149,7 @@ internal class FaceMatcherUseCaseTest { coEvery { enrolmentRecordRepository.count(any(), any()) } returns 1 coEvery { createRangesUseCase(any()) } returns listOf(0..99) coEvery { - enrolmentRecordRepository.loadFaceIdentities(any(), any(), any(), any(), any(), any()) + enrolmentRecordRepository.loadIdentities(any(), any(), any(), any(), any(), any()) } answers { // Call the onCandidateLoaded callback (5th parameter) val onCandidateLoaded: suspend () -> Unit = arg(5) @@ -166,7 +166,7 @@ internal class FaceMatcherUseCaseTest { .invoke( matchParams = MatchParams( probeReferenceId = "referenceId", - probeFaceSamples = listOf( + probeSamples = listOf( CaptureSample( captureEventId = "faceId", template = byteArrayOf(1, 2, 3), diff --git a/infra/matching/src/test/java/com/simprints/infra/matching/usecase/FingerprintMatcherUseCaseTest.kt b/infra/matching/src/test/java/com/simprints/infra/matching/usecase/FingerprintMatcherUseCaseTest.kt index 5f5f1b838e..aeebdd2a7a 100644 --- a/infra/matching/src/test/java/com/simprints/infra/matching/usecase/FingerprintMatcherUseCaseTest.kt +++ b/infra/matching/src/test/java/com/simprints/infra/matching/usecase/FingerprintMatcherUseCaseTest.kt @@ -85,7 +85,7 @@ internal class FingerprintMatcherUseCaseTest { .invoke( MatchParams( probeReferenceId = "referenceId", - probeFingerprintSamples = emptyList(), + probeSamples = emptyList(), bioSdk = SECUGEN_SIM_MATCHER, flowType = FlowType.VERIFY, queryForCandidates = SubjectQuery(), @@ -109,15 +109,16 @@ internal class FingerprintMatcherUseCaseTest { @Test fun `Skips matching if there are no candidates`() = runTest { coEvery { enrolmentRecordRepository.count(any()) } returns 0 - coEvery { enrolmentRecordRepository.loadFingerprintIdentities(any(), any(), any(), project, any(), any()) } returns - createTestChannel(emptyList()) + coEvery { + enrolmentRecordRepository.loadIdentities(any(), any(), any(), project, any(), any()) + } returns createTestChannel(emptyList()) coEvery { bioSdkWrapper.match(any(), any(), any()) } returns listOf() val results = useCase .invoke( MatchParams( probeReferenceId = "referenceId", - probeFingerprintSamples = listOf( + probeSamples = listOf( CaptureSample( captureEventId = "fingerprintId", template = byteArrayOf(1, 2, 3), @@ -151,7 +152,7 @@ internal class FingerprintMatcherUseCaseTest { coEvery { enrolmentRecordRepository.count(any(), any()) } returns 100 coEvery { createRangesUseCase(any()) } returns listOf(0..99) coEvery { - enrolmentRecordRepository.loadFingerprintIdentities( + enrolmentRecordRepository.loadIdentities( any(), any(), any(), @@ -184,7 +185,7 @@ internal class FingerprintMatcherUseCaseTest { .invoke( matchParams = MatchParams( probeReferenceId = "referenceId", - probeFingerprintSamples = listOf( + probeSamples = listOf( CaptureSample( captureEventId = "fingerprintId", template = byteArrayOf(1, 2, 3), @@ -212,8 +213,8 @@ internal class FingerprintMatcherUseCaseTest { ) } -fun createTestChannel(vararg lists: List): ReceiveChannel> { - val channel = Channel>(lists.size) +fun createTestChannel(vararg lists: List): ReceiveChannel { + val channel = Channel(lists.size) runBlocking { var time = 0L for (list in lists) { diff --git a/infra/matching/src/test/java/com/simprints/infra/matching/usecase/SaveMatchEventUseCaseTest.kt b/infra/matching/src/test/java/com/simprints/infra/matching/usecase/SaveMatchEventUseCaseTest.kt index dd4ec47135..cec8168abd 100644 --- a/infra/matching/src/test/java/com/simprints/infra/matching/usecase/SaveMatchEventUseCaseTest.kt +++ b/infra/matching/src/test/java/com/simprints/infra/matching/usecase/SaveMatchEventUseCaseTest.kt @@ -73,7 +73,7 @@ class SaveMatchEventUseCaseTest { flowType = FlowType.VERIFY, bioSdk = FaceConfiguration.BioSdk.RANK_ONE, queryForCandidates = SubjectQuery(subjectId = "subjectId"), - probeFaceSamples = listOf( + probeSamples = listOf( CaptureSample( captureEventId = "faceId", template = byteArrayOf(1, 2, 3), @@ -118,7 +118,7 @@ class SaveMatchEventUseCaseTest { probeReferenceId = "referenceId", flowType = FlowType.VERIFY, queryForCandidates = SubjectQuery(subjectId = "subjectId"), - probeFingerprintSamples = listOf( + probeSamples = listOf( CaptureSample( captureEventId = "fingerprintId", template = byteArrayOf(1, 2, 3), @@ -167,8 +167,7 @@ class SaveMatchEventUseCaseTest { endTime = Timestamp(2L), matchParams = MatchParams( probeReferenceId = "referenceId", - probeFaceSamples = emptyList(), - probeFingerprintSamples = emptyList(), + probeSamples = emptyList(), bioSdk = FaceConfiguration.BioSdk.RANK_ONE, flowType = FlowType.IDENTIFY, queryForCandidates = SubjectQuery(), @@ -306,7 +305,7 @@ class SaveMatchEventUseCaseTest { MatchParams( probeReferenceId = "referenceId", bioSdk = FaceConfiguration.BioSdk.RANK_ONE, - probeFaceSamples = emptyList(), + probeSamples = emptyList(), flowType = FlowType.IDENTIFY, queryForCandidates = SubjectQuery(), biometricDataSource = BiometricDataSource.Simprints, diff --git a/infra/sync/src/test/java/com/simprints/infra/sync/config/testtools/Models.kt b/infra/sync/src/test/java/com/simprints/infra/sync/config/testtools/Models.kt index 4c8bf507be..d56314875d 100644 --- a/infra/sync/src/test/java/com/simprints/infra/sync/config/testtools/Models.kt +++ b/infra/sync/src/test/java/com/simprints/infra/sync/config/testtools/Models.kt @@ -1,10 +1,10 @@ package com.simprints.infra.sync.config.testtools +import com.simprints.core.domain.common.AgeGroup import com.simprints.core.domain.common.Modality import com.simprints.core.domain.externalcredential.ExternalCredentialType import com.simprints.core.domain.sample.SampleIdentifier import com.simprints.core.domain.tokenization.asTokenizableEncrypted -import com.simprints.infra.config.store.models.AgeGroup import com.simprints.infra.config.store.models.ConsentConfiguration import com.simprints.infra.config.store.models.DecisionPolicy import com.simprints.infra.config.store.models.DownSynchronizationConfiguration diff --git a/testing/data-generator/src/main/java/com/simprints/feature/datagenerator/enrollmentrecords/InsertEnrollmentRecordsUseCase.kt b/testing/data-generator/src/main/java/com/simprints/feature/datagenerator/enrollmentrecords/InsertEnrollmentRecordsUseCase.kt index bafacc528b..817c317466 100644 --- a/testing/data-generator/src/main/java/com/simprints/feature/datagenerator/enrollmentrecords/InsertEnrollmentRecordsUseCase.kt +++ b/testing/data-generator/src/main/java/com/simprints/feature/datagenerator/enrollmentrecords/InsertEnrollmentRecordsUseCase.kt @@ -50,11 +50,10 @@ internal class InsertEnrollmentRecordsUseCase @Inject constructor( moduleId = tokenizedModuleId, createdAt = creationDate, updatedAt = updateDate, - fingerprintSamples = generateFingerprintTemplates( + samples = generateFingerprintTemplates( templatesPerFormat = templatesPerFormat, fingerOrder = fingerOrder, - ), - faceSamples = generateFaceSamples( + ) + generateFaceSamples( templatesPerFormat = templatesPerFormat, ), ) diff --git a/testing/data-generator/src/test/java/com/simprints/feature/datagenerator/InsertEnrollmentRecordsUseCaseTest.kt b/testing/data-generator/src/test/java/com/simprints/feature/datagenerator/InsertEnrollmentRecordsUseCaseTest.kt index ded730f120..fb24923edb 100644 --- a/testing/data-generator/src/test/java/com/simprints/feature/datagenerator/InsertEnrollmentRecordsUseCaseTest.kt +++ b/testing/data-generator/src/test/java/com/simprints/feature/datagenerator/InsertEnrollmentRecordsUseCaseTest.kt @@ -219,10 +219,8 @@ internal class InsertEnrollmentRecordsUseCaseTest { // Then val subject = subjectActionsSlot.captured.first().subject - assertThat(subject.faceSamples).hasSize(2) - assertThat(subject.faceSamples.all { it.format == "SIM_FACE_BASE_1" }).isTrue() - assertThat(subject.fingerprintSamples).hasSize(6) - assertThat(subject.fingerprintSamples.all { it.format == "NEC_1_5" }).isTrue() + assertThat(subject.samples.count { it.format == "SIM_FACE_BASE_1" }).isEqualTo(2) + assertThat(subject.samples.count { it.format == "NEC_1_5" }).isEqualTo(6) } @Test @@ -247,9 +245,9 @@ internal class InsertEnrollmentRecordsUseCaseTest { // Then val subject = subjectActionsSlot.captured.first().subject - assertThat(subject.fingerprintSamples).hasSize(2) - assertThat(subject.fingerprintSamples[0].identifier).isEqualTo(SampleIdentifier.LEFT_THUMB) - assertThat(subject.fingerprintSamples[1].identifier).isEqualTo(SampleIdentifier.LEFT_THUMB) + assertThat(subject.samples).hasSize(2) + assertThat(subject.samples[0].identifier).isEqualTo(SampleIdentifier.LEFT_THUMB) + assertThat(subject.samples[1].identifier).isEqualTo(SampleIdentifier.LEFT_THUMB) } @Test @@ -279,7 +277,7 @@ internal class InsertEnrollmentRecordsUseCaseTest { // Then val subject = subjectActionsSlot.captured.first().subject - val fingers = subject.fingerprintSamples.map { it.identifier } + val fingers = subject.samples.map { it.identifier } assertThat(fingers).hasSize(4) assertThat(fingers) .containsExactly(