From 2da173ab69e282d4b21c2e05197f0a324254697a Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 8 Dec 2025 17:30:05 +0200 Subject: [PATCH 1/3] =?UTF-8?q?[MS-1271]=20Identification=20results=20now?= =?UTF-8?q?=20always=20return=20the=20MF-ID=20credential=20matches=20first?= =?UTF-8?q?,=20ignoring=20the=20confidence=20threshold.=20This=20way=20the?= =?UTF-8?q?=20callout=20app=20=CB=86can=20display=20the=20credential=20hol?= =?UTF-8?q?der=20GUIDs=20alongside=20their=20match=20score?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../response/CreateIdentifyResponseUseCase.kt | 66 +++++++------ .../CreateIdentifyResponseUseCaseTest.kt | 93 ++++--------------- 2 files changed, 56 insertions(+), 103 deletions(-) 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 8e80443f5b..46b0c98aed 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,6 +1,5 @@ package com.simprints.feature.orchestrator.usecases.response -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.ProjectConfiguration @@ -13,9 +12,6 @@ import com.simprints.infra.orchestration.data.responses.AppMatchResult import com.simprints.infra.orchestration.data.responses.AppResponse import java.io.Serializable import javax.inject.Inject -import kotlin.collections.ifEmpty -import kotlin.collections.map -import kotlin.collections.take internal class CreateIdentifyResponseUseCase @Inject constructor( private val eventRepository: SessionEventRepository, @@ -25,26 +21,33 @@ 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 faceMatchResults = getFaceMatchResults(results, projectConfiguration) + val bestFaceConfidence = faceMatchResults.firstOrNull()?.confidenceScore ?: 0 + + val fingerprintMatchResults = getFingerprintResults(results, projectConfiguration) + val bestFingerprintConfidence = fingerprintMatchResults.firstOrNull()?.confidenceScore ?: 0 - val fingerprintResults = credentialFingerprintMatchResults + getFingerprintResults(results, projectConfiguration) - val bestFingerprintConfidence = fingerprintResults.firstOrNull()?.confidenceScore ?: 0 + val isUsingFingerprintResults = bestFingerprintConfidence > bestFaceConfidence + val bestMatcherIdentifications = if (isUsingFingerprintResults) { + fingerprintMatchResults + } else { + faceMatchResults + } + val allCredentialResults = credentialResultsMapper(results, projectConfiguration, isFace = true) + + credentialResultsMapper(results, projectConfiguration, isFace = false) + .sortedByDescending(AppMatchResult::confidenceScore) + + // Return the results with the credential results on top, followed by highest confidence score 1:N match results + val identifications = (allCredentialResults + bestMatcherIdentifications) + .distinctBy(AppMatchResult::guid) + .take(projectConfiguration.identification.maxNbOfReturnedCandidates) 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 = identifications, ) } @@ -58,7 +61,6 @@ internal class CreateIdentifyResponseUseCase @Inject constructor( ?.let { fingerprintDecisionPolicy -> fingerprintMatchResult.results.mapToMatchResults( decisionPolicy = fingerprintDecisionPolicy, - projectConfiguration = projectConfiguration, isCredentialMatch = false, verificationMatchThreshold = null, ) @@ -75,7 +77,6 @@ internal class CreateIdentifyResponseUseCase @Inject constructor( ?.let { faceDecisionPolicy -> faceMatchResult.results.mapToMatchResults( decisionPolicy = faceDecisionPolicy, - projectConfiguration = projectConfiguration, isCredentialMatch = false, verificationMatchThreshold = null, ) @@ -85,17 +86,23 @@ internal class CreateIdentifyResponseUseCase @Inject constructor( 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) + val results = if (isCredentialMatch) { + // Credential matches are returned regardless of confidence score + this + } else { + // Attempt to include only high confidence matches. + this + .filter { it.confidence >= decisionPolicy.low } + .sortedByDescending { it.confidence } + .let { goodResults -> + goodResults + .filter { it.confidence >= decisionPolicy.high } + .ifEmpty { goodResults } + } + } + return results .map { AppMatchResult( guid = it.subjectId, @@ -142,9 +149,8 @@ internal class CreateIdentifyResponseUseCase @Inject constructor( return@let matches .mapToMatchResults( decisionPolicy = decisionPolicy, - projectConfiguration = projectConfiguration, isCredentialMatch = true, verificationMatchThreshold = verificationMatchThreshold, - ).sortedByDescending(AppMatchResult::confidenceScore) + ) }.orEmpty() } 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 22d51a8680..3a88e66ed6 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 @@ -39,6 +39,7 @@ class CreateIdentifyResponseUseCaseTest { fun `Returns no identifications if no decision policy`() = runTest { val result = useCase( mockk { + every { identification.maxNbOfReturnedCandidates } returns 2 every { multifactorId?.allowedExternalCredentials } returns null every { face?.getSdkConfiguration((any()))?.decisionPolicy } returns null every { fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns null @@ -204,77 +205,15 @@ class CreateIdentifyResponseUseCaseTest { } @Test - fun `Returns only face credential results sorted by confidence descending`() = runTest { - val (faceSmallConfidence, smallConfidence) = "faceSmallConfidence" to 50f - val (faceBigConfidence, bigConfidence) = "faceBigConfidence" to 99f - val faceMatches = listOf( - mockk { - every { verificationThreshold } returns 0.0f - every { matchResult } returns FaceMatchResult.Item( - subjectId = faceSmallConfidence, - confidence = smallConfidence, - ) - every { faceBioSdk } returns FaceConfiguration.BioSdk.RANK_ONE - every { fingerprintBioSdk } returns null - }, - mockk { - every { matchResult } returns FaceMatchResult.Item( - subjectId = faceBigConfidence, - confidence = bigConfidence, - ) - every { faceBioSdk } returns FaceConfiguration.BioSdk.RANK_ONE - every { fingerprintBioSdk } returns null - }, - ) - - val fingerprintMatches = listOf( - mockk { - every { matchResult } returns FingerprintMatchResult.Item( - subjectId = "fingerprintSubjectId", - confidence = 90f, - ) - every { faceBioSdk } returns null - every { fingerprintBioSdk } returns FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER - }, - ) - - val result = useCase( - 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 - }, - results = listOf( - mockk { - every { matchResults } returns faceMatches + fingerprintMatches - }, - ), - ) - - assertThat((result as AppIdentifyResponse).identifications).isNotEmpty() - assertThat(result.identifications.map { it.guid }).isEqualTo(listOf(faceBigConfidence, faceSmallConfidence)) - assertThat(result.identifications.map { it.confidenceScore }).isEqualTo(listOf(bigConfidence.toInt(), smallConfidence.toInt())) - } - - @Test - fun `Returns only fingerprint credential results sorted by confidence descending`() = runTest { - val (fingerprintSmallConfidence, smallConfidence) = "fingerprintSmallConfidence" to 50f - val (fingerprintBigConfidence, bigConfidence) = "fingerprintBigConfidence" to 99f + fun `Returns both fingerprint and face credential results sorted by confidence descending`() = runTest { + val (fingerprintSmallConfidenceGUID, smallConfidence) = "fingerprintSmallConfidenceGUID" to 50f + val (fingerprintBigConfidenceGUID, fingerprintBigConfidence) = "fingerprintBigConfidenceGUID" to 99f + val (faceBigConfidenceGUID, faceBigConfidence) = "faceBigConfidenceGUID" to fingerprintBigConfidence - 1 val fingerprintMatches = listOf( mockk { every { verificationThreshold } returns 0.0f every { matchResult } returns FingerprintMatchResult.Item( - subjectId = fingerprintSmallConfidence, + subjectId = fingerprintSmallConfidenceGUID, confidence = smallConfidence, ) every { faceBioSdk } returns null @@ -282,8 +221,8 @@ class CreateIdentifyResponseUseCaseTest { }, mockk { every { matchResult } returns FingerprintMatchResult.Item( - subjectId = fingerprintBigConfidence, - confidence = bigConfidence, + subjectId = fingerprintBigConfidenceGUID, + confidence = fingerprintBigConfidence, ) every { faceBioSdk } returns null every { fingerprintBioSdk } returns FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER @@ -293,8 +232,8 @@ class CreateIdentifyResponseUseCaseTest { val faceMatches = listOf( mockk { every { matchResult } returns FaceMatchResult.Item( - subjectId = "faceSubjectId", - confidence = 90f, + subjectId = faceBigConfidenceGUID, + confidence = faceBigConfidence, ) every { faceBioSdk } returns FaceConfiguration.BioSdk.RANK_ONE every { fingerprintBioSdk } returns null @@ -325,8 +264,16 @@ class CreateIdentifyResponseUseCaseTest { ) assertThat((result as AppIdentifyResponse).identifications).isNotEmpty() - assertThat(result.identifications.map { it.guid }).isEqualTo(listOf(fingerprintBigConfidence, fingerprintSmallConfidence)) - assertThat(result.identifications.map { it.confidenceScore }).isEqualTo(listOf(bigConfidence.toInt(), smallConfidence.toInt())) + assertThat( + result.identifications.map { + it.guid + }, + ).isEqualTo(listOf(faceBigConfidenceGUID, fingerprintBigConfidenceGUID, fingerprintSmallConfidenceGUID)) + assertThat( + result.identifications.map { + it.confidenceScore + }, + ).isEqualTo(listOf(faceBigConfidence.toInt(), fingerprintBigConfidence.toInt(), smallConfidence.toInt())) } @Test From 07582f5c31f09bb1d6cad1a6f0735219ff0b68e5 Mon Sep 17 00:00:00 2001 From: alex Date: Tue, 9 Dec 2025 10:49:40 +0200 Subject: [PATCH 2/3] [MS-1271] Adding a test case covering scenario where both credential matches and 1:N matches are returned in the identification response. All credentials are also now properly sorted by confidence score DESC --- .../response/CreateIdentifyResponseUseCase.kt | 7 +- .../CreateIdentifyResponseUseCaseTest.kt | 80 +++++++++++++++++++ 2 files changed, 84 insertions(+), 3 deletions(-) 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 46b0c98aed..62e759acf9 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 @@ -35,9 +35,10 @@ internal class CreateIdentifyResponseUseCase @Inject constructor( } else { faceMatchResults } - val allCredentialResults = credentialResultsMapper(results, projectConfiguration, isFace = true) + - credentialResultsMapper(results, projectConfiguration, isFace = false) - .sortedByDescending(AppMatchResult::confidenceScore) + val allCredentialResults = ( + credentialResultsMapper(results, projectConfiguration, isFace = true) + + credentialResultsMapper(results, projectConfiguration, isFace = false) + ).sortedByDescending(AppMatchResult::confidenceScore) // Return the results with the credential results on top, followed by highest confidence score 1:N match results val identifications = (allCredentialResults + bestMatcherIdentifications) 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 3a88e66ed6..1fe09aac58 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 @@ -370,6 +370,86 @@ class CreateIdentifyResponseUseCaseTest { assertThat(result.identifications.first().confidenceScore).isEqualTo(credentialConfidence.toInt()) } + @Test + fun `Returns credential results prioritized over match results when max candidates is limited`() = runTest { + val credentialConfidence1 = 85f + val credentialConfidence2 = 90f + val credentialConfidence3 = 80f + val credentialGuid1 = "credentialGuid1; confidence=$credentialConfidence1" + val credentialGuid2 = "credentialGuid2; confidence=$credentialConfidence2" + val credentialGuid3 = "credentialGuid3; confidence=$credentialConfidence3" + + val matchConfidence1 = 95f + val matchConfidence2 = 92f + val matchConfidence3 = 88f + val matchConfidence4 = 83f + val matchConfidence5 = 78f + val targetMatchGuid1 = "0" // based on id assigned in 'createFaceMatchResult' + val targetMatchGuid2 = "1" // based on id assigned in 'createFaceMatchResult' + + val maxNbOfReturnedCandidates = 5 + + val credentialFaceMatches = listOf( + mockk { + every { matchResult } returns FaceMatchResult.Item( + subjectId = credentialGuid1, + confidence = credentialConfidence1, + ) + every { faceBioSdk } returns FaceConfiguration.BioSdk.RANK_ONE + every { fingerprintBioSdk } returns null + }, + mockk { + every { matchResult } returns FaceMatchResult.Item( + subjectId = credentialGuid2, + confidence = credentialConfidence2, + ) + every { faceBioSdk } returns FaceConfiguration.BioSdk.RANK_ONE + every { fingerprintBioSdk } returns null + }, + mockk { + every { matchResult } returns FaceMatchResult.Item( + subjectId = credentialGuid3, + confidence = credentialConfidence3, + ) + every { faceBioSdk } returns FaceConfiguration.BioSdk.RANK_ONE + every { fingerprintBioSdk } returns null + }, + ) + + val faceMatchResults = + createFaceMatchResult(matchConfidence1, matchConfidence2, matchConfidence3, matchConfidence4, matchConfidence5) + + val result = useCase( + mockk { + every { identification.maxNbOfReturnedCandidates } returns maxNbOfReturnedCandidates + 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 + }, + results = listOf( + mockk { + every { matchResults } returns credentialFaceMatches + }, + faceMatchResults, + ), + ) + + assertThat((result as AppIdentifyResponse).identifications).hasSize(maxNbOfReturnedCandidates) + assertThat(result.identifications.map { it.guid }).isEqualTo( + listOf(credentialGuid2, credentialGuid1, credentialGuid3, targetMatchGuid1, targetMatchGuid2), + ) + assertThat(result.identifications.map { it.confidenceScore }).isEqualTo( + listOf( + credentialConfidence2.toInt(), + credentialConfidence1.toInt(), + credentialConfidence3.toInt(), + matchConfidence1.toInt(), + matchConfidence2.toInt(), + ), + ) + } + private fun createFaceMatchResult(vararg confidences: Float): Serializable = FaceMatchResult( confidences.mapIndexed { i, confidence -> FaceMatchResult.Item(subjectId = "$i", confidence = confidence) }, FaceConfiguration.BioSdk.RANK_ONE, From 143933d5dae3a9765a5ea5055423c588474676e4 Mon Sep 17 00:00:00 2001 From: alex Date: Tue, 9 Dec 2025 11:14:11 +0200 Subject: [PATCH 3/3] [MS-1271] Fixing tests --- .../usecases/response/CreateIdentifyResponseUseCaseTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 1fe09aac58..ce45e0c6bd 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 @@ -268,12 +268,12 @@ class CreateIdentifyResponseUseCaseTest { result.identifications.map { it.guid }, - ).isEqualTo(listOf(faceBigConfidenceGUID, fingerprintBigConfidenceGUID, fingerprintSmallConfidenceGUID)) + ).isEqualTo(listOf(fingerprintBigConfidenceGUID, faceBigConfidenceGUID, fingerprintSmallConfidenceGUID)) assertThat( result.identifications.map { it.confidenceScore }, - ).isEqualTo(listOf(faceBigConfidence.toInt(), fingerprintBigConfidence.toInt(), smallConfidence.toInt())) + ).isEqualTo(listOf(fingerprintBigConfidence.toInt(), faceBigConfidence.toInt(), smallConfidence.toInt())) } @Test