From d2dfe856d593140103ed98221e2a2534e0b0038f Mon Sep 17 00:00:00 2001 From: alex Date: Wed, 14 Jan 2026 11:45:11 +0200 Subject: [PATCH 1/2] [MS-1290] Each credential match now produces a OneToOne event --- .../model/CredentialMatch.kt | 2 + .../ExternalCredentialSearchViewModel.kt | 17 +++- .../search/usecase/MatchCandidatesUseCase.kt | 5 +- .../ExternalCredentialEventTrackerUseCase.kt | 37 ++++++++ .../ExternalCredentialSearchViewModelTest.kt | 1 + .../usecase/MatchCandidatesUseCaseTest.kt | 2 + ...ternalCredentialEventTrackerUseCaseTest.kt | 90 +++++++++++++++++++ 7 files changed, 150 insertions(+), 4 deletions(-) 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 3fd619dac9..ce9d9c8636 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 @@ -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 } diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/ExternalCredentialSearchViewModel.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/ExternalCredentialSearchViewModel.kt index d13785ac91..442883f432 100644 --- a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/ExternalCredentialSearchViewModel.kt +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/ExternalCredentialSearchViewModel.kt @@ -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 + SearchState.Searching -> null + + // button is not displayed during search 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 @@ -148,6 +153,9 @@ internal class ExternalCredentialSearchViewModel @AssistedInject constructor( val projectConfig = configManager.getProjectConfiguration() val matches = matchCandidatesUseCase(candidates, credential, externalCredentialParams, project, projectConfig) eventsTracker.saveSearchEvent(startTime, scannedCredential.credentialScanId, candidates) + matches.forEach { match -> + eventsTracker.saveMatchEvent(startTime, match) + } updateState { state -> state.copy(searchState = SearchState.CredentialLinked(matchResults = matches)) } } @@ -159,8 +167,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 + ExternalCredentialType.NHISCard -> InputType.TYPE_CLASS_NUMBER + + // NHIS card membership contains only numbers ExternalCredentialType.GhanaIdCard -> InputType.TYPE_CLASS_TEXT + ExternalCredentialType.QRCode -> InputType.TYPE_CLASS_TEXT } 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 b63d6e6d79..fef7ba340b 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 @@ -24,10 +24,11 @@ internal class MatchCandidatesUseCase @Inject constructor( project: Project, projectConfig: ProjectConfiguration, ): List = 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, @@ -46,6 +47,7 @@ internal class MatchCandidatesUseCase @Inject constructor( CredentialMatch( credential = credential, matchResult = result, + probeReferenceId = probeReferenceId, verificationThreshold = matchThreshold, faceBioSdk = faceSdk, fingerprintBioSdk = null, @@ -64,6 +66,7 @@ internal class MatchCandidatesUseCase @Inject constructor( CredentialMatch( credential = credential, matchResult = result, + probeReferenceId = probeReferenceId, verificationThreshold = matchThreshold, faceBioSdk = null, fingerprintBioSdk = fingerprintSdk, diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/usecase/ExternalCredentialEventTrackerUseCase.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/usecase/ExternalCredentialEventTrackerUseCase.kt index cb936b120c..415bf8c944 100644 --- a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/usecase/ExternalCredentialEventTrackerUseCase.kt +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/usecase/ExternalCredentialEventTrackerUseCase.kt @@ -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 @@ -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, @@ -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, @@ -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 diff --git a/feature/external-credential/src/test/java/com/simprints/feature/externalcredential/screens/search/ExternalCredentialSearchViewModelTest.kt b/feature/external-credential/src/test/java/com/simprints/feature/externalcredential/screens/search/ExternalCredentialSearchViewModelTest.kt index e72cf4d9a9..16b3f98296 100644 --- a/feature/external-credential/src/test/java/com/simprints/feature/externalcredential/screens/search/ExternalCredentialSearchViewModelTest.kt +++ b/feature/external-credential/src/test/java/com/simprints/feature/externalcredential/screens/search/ExternalCredentialSearchViewModelTest.kt @@ -138,6 +138,7 @@ internal class ExternalCredentialSearchViewModelTest { assertThat(searchState.matchResults).hasSize(1) assertThat(searchState.matchResults.first()).isEqualTo(candidateMatch) coVerify { eventsTracker.saveSearchEvent(any(), any(), any()) } + coVerify { eventsTracker.saveMatchEvent(any(), any()) } } @Test 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 b7e07b2fe1..816d83c055 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 @@ -150,6 +150,7 @@ 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") } @Test @@ -169,6 +170,7 @@ 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") } @Test diff --git a/feature/external-credential/src/test/java/com/simprints/feature/externalcredential/usecase/ExternalCredentialEventTrackerUseCaseTest.kt b/feature/external-credential/src/test/java/com/simprints/feature/externalcredential/usecase/ExternalCredentialEventTrackerUseCaseTest.kt index e006fdcfcb..f35681013b 100644 --- a/feature/external-credential/src/test/java/com/simprints/feature/externalcredential/usecase/ExternalCredentialEventTrackerUseCaseTest.kt +++ b/feature/external-credential/src/test/java/com/simprints/feature/externalcredential/usecase/ExternalCredentialEventTrackerUseCaseTest.kt @@ -6,9 +6,13 @@ import com.simprints.core.domain.tokenization.asTokenizableEncrypted import com.simprints.core.domain.tokenization.asTokenizableRaw 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.infra.authstore.AuthStore +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.TokenKeyType import com.simprints.infra.config.store.tokenization.TokenizationProcessor import com.simprints.infra.config.sync.ConfigManager @@ -18,6 +22,8 @@ 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.ExternalCredentialSelectionEvent import com.simprints.infra.events.event.domain.models.ExternalCredentialSelectionEvent.SkipReason +import com.simprints.infra.events.event.domain.models.FingerComparisonStrategy +import com.simprints.infra.events.event.domain.models.OneToOneMatchEvent import com.simprints.infra.events.session.SessionEventRepository import io.mockk.* import io.mockk.impl.annotations.MockK @@ -167,6 +173,49 @@ class ExternalCredentialEventTrackerUseCaseTest { } } + @Test + fun `saveMatchEvent should save match event for face SDK`() = runTest { + val match = makeCredentialMatch( + faceSdk = FACE_SDK, + fingerprintSdk = null, + ) + + useCase.saveMatchEvent(START_TIME, match) + + verifyMatchEvent( + expectedMatcher = FACE_SDK.name, + expectedFingerStrategy = null, + ) + } + + @Test + fun `saveMatchEvent should save match event for fingerprint SDK`() = runTest { + val fingerprintConfig = mockk { + every { comparisonStrategyForVerification } returns + FingerprintConfiguration.FingerComparisonStrategy.SAME_FINGER + } + + val projectConfig = mockk { + every { fingerprint } returns mockk { + every { getSdkConfiguration(FINGERPRINT_SDK) } returns fingerprintConfig + } + } + + coEvery { configManager.getProjectConfiguration() } returns projectConfig + + val match = makeCredentialMatch( + faceSdk = null, + fingerprintSdk = FINGERPRINT_SDK, + ) + + useCase.saveMatchEvent(START_TIME, match) + + verifyMatchEvent( + expectedMatcher = FINGERPRINT_SDK.name, + expectedFingerStrategy = FingerComparisonStrategy.SAME_FINGER, + ) + } + private fun makeScannedCredential(type: ExternalCredentialType) = ScannedCredential( credentialScanId = "test-scan-id", credential = RAW_SCANNED_VALUE.asTokenizableEncrypted(), @@ -179,6 +228,43 @@ class ExternalCredentialEventTrackerUseCaseTest { scannedValue = RAW_SCANNED_VALUE.asTokenizableRaw(), ) + private fun makeCredentialMatch( + faceSdk: FaceConfiguration.BioSdk?, + fingerprintSdk: FingerprintConfiguration.BioSdk?, + ): CredentialMatch { + val matchResult = mockk { + every { subjectId } returns SUBJECT_ID + every { confidence } returns CONFIDENCE + } + + return CredentialMatch( + credential = RAW_SCANNED_VALUE.asTokenizableEncrypted(), + matchResult = matchResult, + probeReferenceId = PROBE_REFERENCE_ID, + verificationThreshold = 0.5f, + faceBioSdk = faceSdk, + fingerprintBioSdk = fingerprintSdk, + ) + } + + private fun verifyMatchEvent( + expectedMatcher: String, + expectedFingerStrategy: FingerComparisonStrategy?, + ) { + val slot = slot() + coVerify(exactly = 1) { eventRepository.addOrUpdateEvent(capture(slot)) } + + with(slot.captured.payload as OneToOneMatchEvent.OneToOneMatchPayload.OneToOneMatchPayloadV4) { + assertThat(createdAt).isEqualTo(START_TIME) + assertThat(endedAt).isEqualTo(END_TIME) + assertThat(candidateId).isEqualTo(SUBJECT_ID) + assertThat(matcher).isEqualTo(expectedMatcher) + assertThat(result?.score).isEqualTo(CONFIDENCE) + assertThat(fingerComparisonStrategy).isEqualTo(expectedFingerStrategy) + assertThat(probeBiometricReferenceId).isEqualTo(PROBE_REFERENCE_ID) + } + } + companion object Companion { private val START_TIME = Timestamp(0L) private val SCAN_START_TIME = Timestamp(3L) @@ -189,5 +275,9 @@ class ExternalCredentialEventTrackerUseCaseTest { private const val RAW_SCANNED_VALUE = "scanned-value" private const val DEFAULT_DISTANCE = 7 private const val SELECTION_ID = "selection_id" + private const val CONFIDENCE = 0.9f + private const val PROBE_REFERENCE_ID = "probe-ref-id" + private val FACE_SDK = FaceConfiguration.BioSdk.RANK_ONE + private val FINGERPRINT_SDK = FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER } } From 102aea0a127a98e1841a92f45366f9a6750105ce Mon Sep 17 00:00:00 2001 From: alex Date: Wed, 14 Jan 2026 13:32:38 +0200 Subject: [PATCH 2/2] [MS-1290] Moving the OneToOne match events from ViewModel to the MatchCandidatesUseCase to properly reflect the start and end time --- .../search/ExternalCredentialSearchViewModel.kt | 12 ++++-------- .../search/usecase/MatchCandidatesUseCase.kt | 14 ++++++++++++-- .../ExternalCredentialSearchViewModelTest.kt | 1 - .../search/usecase/MatchCandidatesUseCaseTest.kt | 11 +++++++++++ .../cache/OrchestratorCacheIntegrationTest.kt | 1 + 5 files changed, 28 insertions(+), 11 deletions(-) diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/ExternalCredentialSearchViewModel.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/ExternalCredentialSearchViewModel.kt index 442883f432..e0c2289fc2 100644 --- a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/ExternalCredentialSearchViewModel.kt +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/ExternalCredentialSearchViewModel.kt @@ -104,9 +104,9 @@ internal class ExternalCredentialSearchViewModel @AssistedInject constructor( searchState: SearchState, flowType: FlowType, ): Int? = when (searchState) { + // button is not displayed during search SearchState.Searching -> null - // button is not displayed during search is SearchState.CredentialLinked -> when (flowType) { FlowType.ENROL -> { IDR.string.mfid_action_enrol_anyway @@ -140,22 +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) - matches.forEach { match -> - eventsTracker.saveMatchEvent(startTime, match) - } updateState { state -> state.copy(searchState = SearchState.CredentialLinked(matchResults = matches)) } } @@ -167,9 +163,9 @@ internal class ExternalCredentialSearchViewModel @AssistedInject constructor( * alpha-numeric, while the NHIS card memberships contain only digits. */ fun getKeyBoardInputType() = when (scannedCredential.credentialType) { + // NHIS card membership contains only numbers ExternalCredentialType.NHISCard -> InputType.TYPE_CLASS_NUMBER - // NHIS card membership contains only numbers ExternalCredentialType.GhanaIdCard -> InputType.TYPE_CLASS_TEXT ExternalCredentialType.QRCode -> InputType.TYPE_CLASS_TEXT 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 fef7ba340b..f0fdb9c070 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 @@ -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 @@ -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, @@ -40,11 +44,12 @@ 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, @@ -52,6 +57,8 @@ internal class MatchCandidatesUseCase @Inject constructor( faceBioSdk = faceSdk, fingerprintBioSdk = null, ) + eventsTracker.saveMatchEvent(startTime, match) + return@map match } } } @@ -59,11 +66,12 @@ internal class MatchCandidatesUseCase @Inject constructor( 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, @@ -71,6 +79,8 @@ internal class MatchCandidatesUseCase @Inject constructor( faceBioSdk = null, fingerprintBioSdk = fingerprintSdk, ) + eventsTracker.saveMatchEvent(startTime, match) + return@map match } } } diff --git a/feature/external-credential/src/test/java/com/simprints/feature/externalcredential/screens/search/ExternalCredentialSearchViewModelTest.kt b/feature/external-credential/src/test/java/com/simprints/feature/externalcredential/screens/search/ExternalCredentialSearchViewModelTest.kt index 16b3f98296..e72cf4d9a9 100644 --- a/feature/external-credential/src/test/java/com/simprints/feature/externalcredential/screens/search/ExternalCredentialSearchViewModelTest.kt +++ b/feature/external-credential/src/test/java/com/simprints/feature/externalcredential/screens/search/ExternalCredentialSearchViewModelTest.kt @@ -138,7 +138,6 @@ internal class ExternalCredentialSearchViewModelTest { assertThat(searchState.matchResults).hasSize(1) assertThat(searchState.matchResults.first()).isEqualTo(candidateMatch) coVerify { eventsTracker.saveSearchEvent(any(), any(), any()) } - coVerify { eventsTracker.saveMatchEvent(any(), any()) } } @Test 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 816d83c055..f1b8897a50 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 @@ -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 @@ -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" @@ -88,6 +95,8 @@ internal class MatchCandidatesUseCaseTest { createMatchParamsUseCase = createMatchParamsUseCase, faceMatcher = faceMatcher, fingerprintMatcher = fingerprintMatcher, + timeHelper = timeHelper, + eventsTracker = eventsTracker, ) every { subject.subjectId } returns subjectId @@ -151,6 +160,7 @@ internal class MatchCandidatesUseCaseTest { 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 @@ -171,6 +181,7 @@ internal class MatchCandidatesUseCaseTest { 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 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 e450f188d0..6f3e90454e 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 @@ -230,6 +230,7 @@ class OrchestratorCacheIntegrationTest { verificationThreshold = 55f, faceBioSdk = FaceConfiguration.BioSdk.RANK_ONE, fingerprintBioSdk = FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER, + probeReferenceId = "probeReferenceId", ), ), ),