Skip to content
Open
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
Expand Up @@ -13,9 +13,11 @@ import com.simprints.infra.matching.MatchResultItem
data class CredentialMatch(
val credential: TokenizableString.Tokenized,
val matchResult: MatchResultItem,
val probeReferenceId: String?,
val verificationThreshold: Float,
val faceBioSdk: FaceConfiguration.BioSdk?,
val fingerprintBioSdk: FingerprintConfiguration.BioSdk?,
) : StepResult {
val isVerificationSuccessful = matchResult.confidence >= verificationThreshold
val isFaceMatch = faceBioSdk != null
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import com.simprints.feature.exitform.ExitFormResult
import com.simprints.feature.externalcredential.GraphExternalCredentialInternalDirections
import com.simprints.feature.externalcredential.R
import com.simprints.feature.externalcredential.model.ExternalCredentialParams
import com.simprints.feature.externalcredential.screens.select.ExternalCredentialSelectFragmentDirections
import com.simprints.infra.uibase.navigation.finishWithResult
import com.simprints.infra.uibase.navigation.handleResult
import com.simprints.infra.uibase.navigation.navigateSafely
Expand Down Expand Up @@ -76,15 +77,13 @@ internal class ExternalCredentialControllerFragment : Fragment(R.layout.fragment
private fun initListeners() {
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) {
when (internalNavController?.currentDestination?.id) {
R.id.externalCredentialSelectFragment, R.id.externalCredentialSearch -> {
// Exit form navigation
findNavController().navigateSafely(
this@ExternalCredentialControllerFragment,
R.id.action_global_refusalFragment,
)
R.id.externalCredentialSearch -> {
internalNavController?.navigate(R.id.externalCredentialSkip)
}

else -> internalNavController?.popBackStack()
R.id.externalCredentialSkip, R.id.externalCredentialScanQr, R.id.externalCredentialScanOcr -> {
internalNavController?.popBackStack()
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,14 @@ internal class ExternalCredentialSearchViewModel @AssistedInject constructor(
searchState: SearchState,
flowType: FlowType,
): Int? = when (searchState) {
SearchState.Searching -> null // button is not displayed during search
// button is not displayed during search
SearchState.Searching -> null

is SearchState.CredentialLinked -> when (flowType) {
FlowType.ENROL -> IDR.string.mfid_action_enrol_anyway
FlowType.ENROL -> {
IDR.string.mfid_action_enrol_anyway
}

else -> {
if (searchState.hasSuccessfulVerifications) {
IDR.string.mfid_action_go_to_record
Expand Down Expand Up @@ -135,19 +140,18 @@ internal class ExternalCredentialSearchViewModel @AssistedInject constructor(
private suspend fun searchSubjectsLinkedToCredential(credential: TokenizableString.Tokenized) {
updateState { it.copy(searchState = SearchState.Searching) }
val project = configManager.getProject(authStore.signedInProjectId)
val searchStartTime = timeHelper.now()
val candidates = enrolmentRecordRepository.load(SubjectQuery(projectId = project.id, externalCredential = credential))
eventsTracker.saveSearchEvent(searchStartTime, scannedCredential.credentialScanId, candidates)

val startTime = timeHelper.now()
when {
candidates.isEmpty() -> {
eventsTracker.saveSearchEvent(startTime, scannedCredential.credentialScanId, emptyList())
updateState { it.copy(searchState = SearchState.CredentialNotFound) }
}

else -> {
val projectConfig = configManager.getProjectConfiguration()
val matches = matchCandidatesUseCase(candidates, credential, externalCredentialParams, project, projectConfig)
eventsTracker.saveSearchEvent(startTime, scannedCredential.credentialScanId, candidates)

updateState { state -> state.copy(searchState = SearchState.CredentialLinked(matchResults = matches)) }
}
Expand All @@ -159,8 +163,11 @@ internal class ExternalCredentialSearchViewModel @AssistedInject constructor(
* alpha-numeric, while the NHIS card memberships contain only digits.
*/
fun getKeyBoardInputType() = when (scannedCredential.credentialType) {
ExternalCredentialType.NHISCard -> InputType.TYPE_CLASS_NUMBER // NHIS card membership contains only numbers
// NHIS card membership contains only numbers
ExternalCredentialType.NHISCard -> InputType.TYPE_CLASS_NUMBER

ExternalCredentialType.GhanaIdCard -> InputType.TYPE_CLASS_TEXT

ExternalCredentialType.QRCode -> InputType.TYPE_CLASS_TEXT
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.simprints.feature.externalcredential.screens.search.usecase

import com.simprints.core.domain.tokenization.TokenizableString
import com.simprints.core.tools.time.TimeHelper
import com.simprints.feature.externalcredential.model.CredentialMatch
import com.simprints.feature.externalcredential.model.ExternalCredentialParams
import com.simprints.feature.externalcredential.usecase.ExternalCredentialEventTrackerUseCase
import com.simprints.infra.config.store.models.Project
import com.simprints.infra.config.store.models.ProjectConfiguration
import com.simprints.infra.enrolment.records.repository.domain.models.Subject
Expand All @@ -16,6 +18,8 @@ internal class MatchCandidatesUseCase @Inject constructor(
private val createMatchParamsUseCase: CreateMatchParamsUseCase,
private val faceMatcher: FaceMatcherUseCase,
private val fingerprintMatcher: FingerprintMatcherUseCase,
private val eventsTracker: ExternalCredentialEventTrackerUseCase,
private val timeHelper: TimeHelper,
) {
suspend operator fun invoke(
candidates: List<Subject>,
Expand All @@ -24,10 +28,11 @@ internal class MatchCandidatesUseCase @Inject constructor(
project: Project,
projectConfig: ProjectConfiguration,
): List<CredentialMatch> = candidates.flatMap { candidate ->
val probeReferenceId = externalCredentialParams.probeReferenceId
val matchParams = createMatchParamsUseCase(
candidateSubjectId = candidate.subjectId,
flowType = externalCredentialParams.flowType,
probeReferenceId = externalCredentialParams.probeReferenceId,
probeReferenceId = probeReferenceId,
projectConfiguration = projectConfig,
faceSamples = externalCredentialParams.faceSamples,
fingerprintSamples = externalCredentialParams.fingerprintSamples,
Expand All @@ -39,35 +44,43 @@ internal class MatchCandidatesUseCase @Inject constructor(
matchParams.probeFaceSamples.isNotEmpty() -> {
val faceSdk = matchParams.faceSDK ?: return@mapNotNull null
projectConfig.face?.getSdkConfiguration(faceSdk)?.verificationMatchThreshold?.let { matchThreshold ->
val startTime = timeHelper.now()
(faceMatcher(matchParams, project).last() as? MatcherState.Success)
?.matchResultItems
.orEmpty()
.map { result ->
CredentialMatch(
val match = CredentialMatch(
credential = credential,
matchResult = result,
probeReferenceId = probeReferenceId,
verificationThreshold = matchThreshold,
faceBioSdk = faceSdk,
fingerprintBioSdk = null,
)
eventsTracker.saveMatchEvent(startTime, match)
return@map match
}
}
}

else -> {
val fingerprintSdk = matchParams.fingerprintSDK ?: return@mapNotNull null
projectConfig.fingerprint?.getSdkConfiguration(fingerprintSdk)?.verificationMatchThreshold?.let { matchThreshold ->
val startTime = timeHelper.now()
(fingerprintMatcher(matchParams, project).last() as? MatcherState.Success)
?.matchResultItems
.orEmpty()
.map { result ->
CredentialMatch(
val match = CredentialMatch(
credential = credential,
matchResult = result,
probeReferenceId = probeReferenceId,
verificationThreshold = matchThreshold,
faceBioSdk = null,
fingerprintBioSdk = fingerprintSdk,
)
eventsTracker.saveMatchEvent(startTime, match)
return@map match
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import android.os.Bundle
import android.view.View
import android.widget.Button
import android.widget.TextView
import androidx.activity.addCallback
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
Expand Down Expand Up @@ -71,6 +72,9 @@ internal class ExternalCredentialSelectFragment : Fragment(R.layout.fragment_ext
onCancel = ::dismissDialog,
)
}
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) {
binding.skipScanning.performClick()
}
}

private fun initViews(types: List<ExternalCredentialType>) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import com.simprints.core.domain.externalcredential.ExternalCredential
import com.simprints.core.domain.externalcredential.ExternalCredentialType
import com.simprints.core.tools.time.TimeHelper
import com.simprints.core.tools.time.Timestamp
import com.simprints.feature.externalcredential.model.CredentialMatch
import com.simprints.feature.externalcredential.screens.scanocr.usecase.CalculateLevenshteinDistanceUseCase
import com.simprints.feature.externalcredential.screens.search.model.ScannedCredential
import com.simprints.feature.externalcredential.screens.search.model.toExternalCredential
import com.simprints.infra.authstore.AuthStore
import com.simprints.infra.config.store.models.FingerprintConfiguration
import com.simprints.infra.config.store.models.TokenKeyType
import com.simprints.infra.config.store.tokenization.TokenizationProcessor
import com.simprints.infra.config.sync.ConfigManager
Expand All @@ -18,9 +20,13 @@ import com.simprints.infra.events.event.domain.models.ExternalCredentialConfirma
import com.simprints.infra.events.event.domain.models.ExternalCredentialConfirmationEvent.ExternalCredentialConfirmationResult
import com.simprints.infra.events.event.domain.models.ExternalCredentialSearchEvent
import com.simprints.infra.events.event.domain.models.ExternalCredentialSelectionEvent
import com.simprints.infra.events.event.domain.models.FingerComparisonStrategy
import com.simprints.infra.events.event.domain.models.MatchEntry
import com.simprints.infra.events.event.domain.models.OneToOneMatchEvent
import com.simprints.infra.events.session.SessionEventRepository
import com.simprints.infra.logging.Simber
import javax.inject.Inject
import com.simprints.infra.config.store.models.FingerprintConfiguration.FingerComparisonStrategy as ConfigFingerComparisonStrategy

internal class ExternalCredentialEventTrackerUseCase @Inject constructor(
private val timeHelper: TimeHelper,
Expand All @@ -30,6 +36,25 @@ internal class ExternalCredentialEventTrackerUseCase @Inject constructor(
private val eventRepository: SessionEventRepository,
private val calculateDistance: CalculateLevenshteinDistanceUseCase,
) {
suspend fun saveMatchEvent(
startTime: Timestamp,
match: CredentialMatch,
) {
eventRepository.addOrUpdateEvent(
OneToOneMatchEvent(
createdAt = startTime,
endTime = timeHelper.now(),
candidateId = match.matchResult.subjectId,
matcher = match.faceBioSdk?.name ?: match.fingerprintBioSdk?.name!!,
result = with(match.matchResult) {
MatchEntry(subjectId, confidence)
},
fingerComparisonStrategy = if (match.isFaceMatch) null else getFingerprintComparisonStrategy(match.fingerprintBioSdk!!),
probeBiometricReferenceId = match.probeReferenceId.orEmpty(),
),
)
}

suspend fun saveSearchEvent(
startTime: Timestamp,
externalCredentialId: String,
Expand Down Expand Up @@ -130,6 +155,18 @@ internal class ExternalCredentialEventTrackerUseCase @Inject constructor(
)
}

private suspend fun getFingerprintComparisonStrategy(bioSdk: FingerprintConfiguration.BioSdk) = configManager
.getProjectConfiguration()
.fingerprint
?.getSdkConfiguration(bioSdk)
?.comparisonStrategyForVerification
?.let {
when (it) {
ConfigFingerComparisonStrategy.SAME_FINGER -> FingerComparisonStrategy.SAME_FINGER
ConfigFingerComparisonStrategy.CROSS_FINGER_USING_MEAN_OF_MAX -> FingerComparisonStrategy.CROSS_FINGER_USING_MEAN_OF_MAX
}
}

companion object Companion {
private const val NHIS_CARD_ID_LENGTH = 8
private const val GHANA_ID_CARD_ID_LENGTH = 15
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package com.simprints.feature.externalcredential.screens.search.usecase
import com.google.common.truth.Truth.*
import com.simprints.core.domain.common.FlowType
import com.simprints.core.domain.tokenization.asTokenizableEncrypted
import com.simprints.core.tools.time.TimeHelper
import com.simprints.feature.externalcredential.model.ExternalCredentialParams
import com.simprints.feature.externalcredential.usecase.ExternalCredentialEventTrackerUseCase
import com.simprints.infra.config.store.models.AgeGroup
import com.simprints.infra.config.store.models.FaceConfiguration
import com.simprints.infra.config.store.models.FingerprintConfiguration
Expand Down Expand Up @@ -76,6 +78,11 @@ internal class MatchCandidatesUseCaseTest {
@MockK
private lateinit var matcherSuccess: MatcherState.Success

@MockK
private lateinit var timeHelper: TimeHelper

@MockK
lateinit var eventsTracker: ExternalCredentialEventTrackerUseCase
private val credential = "credential".asTokenizableEncrypted()
private val subjectId = "subjectId"
private val probeReferenceId = "probeReferenceId"
Expand All @@ -88,6 +95,8 @@ internal class MatchCandidatesUseCaseTest {
createMatchParamsUseCase = createMatchParamsUseCase,
faceMatcher = faceMatcher,
fingerprintMatcher = fingerprintMatcher,
timeHelper = timeHelper,
eventsTracker = eventsTracker,
)

every { subject.subjectId } returns subjectId
Expand Down Expand Up @@ -150,6 +159,8 @@ internal class MatchCandidatesUseCaseTest {
assertThat(result[0].verificationThreshold).isEqualTo(verificationMatchThreshold)
assertThat(result[0].faceBioSdk).isEqualTo(FaceConfiguration.BioSdk.RANK_ONE)
assertThat(result[0].fingerprintBioSdk).isNull()
assertThat(result[0].probeReferenceId).isEqualTo("probeReferenceId")
coVerify { eventsTracker.saveMatchEvent(any(), any()) }
}

@Test
Expand All @@ -169,6 +180,8 @@ internal class MatchCandidatesUseCaseTest {
assertThat(result[0].verificationThreshold).isEqualTo(verificationMatchThreshold)
assertThat(result[0].faceBioSdk).isNull()
assertThat(result[0].fingerprintBioSdk).isEqualTo(FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER)
assertThat(result[0].probeReferenceId).isEqualTo("probeReferenceId")
coVerify { eventsTracker.saveMatchEvent(any(), any()) }
}

@Test
Expand Down
Loading