Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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,
Expand All @@ -25,26 +21,34 @@ internal class CreateIdentifyResponseUseCase @Inject constructor(
results: List<Serializable>,
): 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,
)
}

Expand All @@ -58,7 +62,6 @@ internal class CreateIdentifyResponseUseCase @Inject constructor(
?.let { fingerprintDecisionPolicy ->
fingerprintMatchResult.results.mapToMatchResults(
decisionPolicy = fingerprintDecisionPolicy,
projectConfiguration = projectConfiguration,
isCredentialMatch = false,
verificationMatchThreshold = null,
)
Expand All @@ -75,7 +78,6 @@ internal class CreateIdentifyResponseUseCase @Inject constructor(
?.let { faceDecisionPolicy ->
faceMatchResult.results.mapToMatchResults(
decisionPolicy = faceDecisionPolicy,
projectConfiguration = projectConfiguration,
isCredentialMatch = false,
verificationMatchThreshold = null,
)
Expand All @@ -85,17 +87,23 @@ internal class CreateIdentifyResponseUseCase @Inject constructor(
private fun List<MatchResultItem>.mapToMatchResults(
decisionPolicy: DecisionPolicy,
verificationMatchThreshold: Float?,
projectConfiguration: ProjectConfiguration,
isCredentialMatch: Boolean,
): List<AppMatchResult> {
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,
Expand Down Expand Up @@ -142,9 +150,8 @@ internal class CreateIdentifyResponseUseCase @Inject constructor(
return@let matches
.mapToMatchResults(
decisionPolicy = decisionPolicy,
projectConfiguration = projectConfiguration,
isCredentialMatch = true,
verificationMatchThreshold = verificationMatchThreshold,
).sortedByDescending(AppMatchResult::confidenceScore)
)
}.orEmpty()
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -204,86 +205,24 @@ 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<CredentialMatch>(
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<CredentialMatch>(
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<ExternalCredentialSearchResult> {
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 {
Comment thread
alexandr-simprints marked this conversation as resolved.
val (fingerprintSmallConfidenceGUID, smallConfidence) = "fingerprintSmallConfidenceGUID" to 50f
val (fingerprintBigConfidenceGUID, fingerprintBigConfidence) = "fingerprintBigConfidenceGUID" to 99f
val (faceBigConfidenceGUID, faceBigConfidence) = "faceBigConfidenceGUID" to fingerprintBigConfidence - 1
val fingerprintMatches = listOf<CredentialMatch>(
mockk {
every { verificationThreshold } returns 0.0f
every { matchResult } returns FingerprintMatchResult.Item(
subjectId = fingerprintSmallConfidence,
subjectId = fingerprintSmallConfidenceGUID,
confidence = smallConfidence,
)
every { faceBioSdk } returns null
every { fingerprintBioSdk } returns FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER
},
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
Expand All @@ -293,8 +232,8 @@ class CreateIdentifyResponseUseCaseTest {
val faceMatches = listOf<CredentialMatch>(
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
Expand Down Expand Up @@ -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(fingerprintBigConfidenceGUID, faceBigConfidenceGUID, fingerprintSmallConfidenceGUID))
assertThat(
result.identifications.map {
it.confidenceScore
},
).isEqualTo(listOf(fingerprintBigConfidence.toInt(), faceBigConfidence.toInt(), smallConfidence.toInt()))
}

@Test
Comment thread
alexandr-simprints marked this conversation as resolved.
Expand Down Expand Up @@ -423,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<CredentialMatch>(
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<ExternalCredentialSearchResult> {
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,
Expand Down