From f063852dab29ee37cae6bed4b965995f0baaa87e Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 9 Oct 2025 10:58:08 +0300 Subject: [PATCH 01/31] [MS-1198] Identification response now includes credential information - whether person is linked to the scanned credential, and if yes, whether the verification was successful --- .../feature/clientapi/ClientApiViewModel.kt | 1 + .../response/LibSimprintsResponseMapper.kt | 31 ++++++++++++++++++- .../MapRefusalOrErrorResultUseCase.kt | 2 +- .../response/CreateIdentifyResponseUseCase.kt | 18 ++++++++--- .../response/CreateVerifyResponseUseCase.kt | 6 ++-- .../orchestration/data/ActionResponse.kt | 1 + .../data/responses/AppIdentifyResponse.kt | 1 + .../data/responses/AppMatchResult.kt | 9 ++++++ 8 files changed, 61 insertions(+), 8 deletions(-) diff --git a/feature/client-api/src/main/java/com/simprints/feature/clientapi/ClientApiViewModel.kt b/feature/client-api/src/main/java/com/simprints/feature/clientapi/ClientApiViewModel.kt index ef1799cf24..6af1adc7e2 100644 --- a/feature/client-api/src/main/java/com/simprints/feature/clientapi/ClientApiViewModel.kt +++ b/feature/client-api/src/main/java/com/simprints/feature/clientapi/ClientApiViewModel.kt @@ -140,6 +140,7 @@ class ClientApiViewModel @Inject internal constructor( actionIdentifier = action.actionIdentifier, sessionId = currentSessionId, identifications = identifyResponse.identifications, + isMultiFactorIdEnabled = identifyResponse.isMultiFactorIdEnabled, ), ), ) diff --git a/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/response/LibSimprintsResponseMapper.kt b/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/response/LibSimprintsResponseMapper.kt index ff314dc277..06acfc816a 100644 --- a/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/response/LibSimprintsResponseMapper.kt +++ b/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/response/LibSimprintsResponseMapper.kt @@ -2,6 +2,7 @@ package com.simprints.feature.clientapi.mappers.response import android.os.Bundle import androidx.core.os.bundleOf +import com.simprints.core.BuildConfig import com.simprints.core.DeviceID import com.simprints.core.PackageVersionName import com.simprints.core.domain.externalcredential.ExternalCredential @@ -15,6 +16,7 @@ import com.simprints.libsimprints.contracts.data.Identification import com.simprints.libsimprints.contracts.data.Identification.Companion.toJson import com.simprints.libsimprints.contracts.data.RefusalForm import com.simprints.libsimprints.contracts.data.Verification +import org.json.JSONArray import org.json.JSONObject import javax.inject.Inject import com.simprints.libsimprints.Identification as LegacyIdentification @@ -53,13 +55,18 @@ internal class LibSimprintsResponseMapper @Inject constructor( Constants.SIMPRINTS_BIOMETRICS_COMPLETE_CHECK to true, ).appendDataPerContractVersion(response) { version -> when { - version < VersionsList.INITIAL_REWORK -> putParcelableArrayList( + !BuildConfig.DEBUG && version < VersionsList.INITIAL_REWORK -> putParcelableArrayList( Constants.SIMPRINTS_IDENTIFICATIONS, response.identifications .map { LegacyIdentification(it.guid, it.confidenceScore, LegacyTier.valueOf(it.tier.name)) } .toCollection(ArrayList()), ) + response.isMultiFactorIdEnabled -> putString( + Constants.SIMPRINTS_IDENTIFICATIONS, + response.mapIdentificationsWithCredentials(), + ) + else -> putString( Constants.SIMPRINTS_IDENTIFICATIONS, response.identifications @@ -164,6 +171,21 @@ internal class LibSimprintsResponseMapper @Inject constructor( } } + private fun ActionResponse.IdentifyActionResponse.mapIdentificationsWithCredentials(): String = identifications + .map { identification -> + JSONObject() + .also { json -> + json.put(KEY_GUID, identification.guid) + json.put(KEY_CONFIDENCE_BAND, identification.matchConfidence.name) + json.put(KEY_CONFIDENCE, identification.confidenceScore.toFloat()) + json.put(KEY_IS_LINKED_TO_CREDENTIAL, identification.isLinkedToScannedCredential ?: false) + identification.isCredentialVerified?.let { + json.put(KEY_IS_CREDENTIAL_VERIFIED, it) + } + } + }.run(::JSONArray) + .toString() + private fun AppErrorReason.libSimprintsResultCode() = when (this) { AppErrorReason.UNEXPECTED_ERROR -> Constants.SIMPRINTS_UNEXPECTED_ERROR AppErrorReason.ROOTED_DEVICE -> Constants.SIMPRINTS_ROOTED_DEVICE @@ -204,5 +226,12 @@ internal class LibSimprintsResponseMapper @Inject constructor( internal const val SCANNED_CREDENTIAL = "scannedCredential" internal const val SCANNED_CREDENTIAL_VALUE = "value" internal const val SCANNED_CREDENTIAL_TYPE = "type" + + // TODO [MS-1190] Move implementation to LibSimprints. These constats are copies of com.simprints.libsimprints.contracts.data.Identification + private const val KEY_GUID = "guid" + private const val KEY_CONFIDENCE = "confidence" + private const val KEY_CONFIDENCE_BAND = "confidenceBand" + private const val KEY_IS_LINKED_TO_CREDENTIAL = "isLinkedToCredential" + private const val KEY_IS_CREDENTIAL_VERIFIED = "isVerified" } } diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/MapRefusalOrErrorResultUseCase.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/MapRefusalOrErrorResultUseCase.kt index c4ac6f77e7..619aad1d04 100644 --- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/MapRefusalOrErrorResultUseCase.kt +++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/MapRefusalOrErrorResultUseCase.kt @@ -59,7 +59,7 @@ internal class MapRefusalOrErrorResultUseCase @Inject constructor( is ValidateSubjectPoolResult -> result .takeUnless { it.isValid } - ?.let { AppIdentifyResponse(emptyList(), eventRepository.getCurrentSessionScope().id) } + ?.let { AppIdentifyResponse(emptyList(), eventRepository.getCurrentSessionScope().id, isMultiFactorIdEnabled = false) } is SelectSubjectAgeGroupResult -> result 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 d72392e65f..5e1cb1061c 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 @@ -24,6 +24,7 @@ internal class CreateIdentifyResponseUseCase @Inject constructor( projectConfiguration: ProjectConfiguration, results: List, ): AppResponse { + val isMultiFactorIdEnabled = projectConfiguration.multifactorId?.allowedExternalCredentials?.isNotEmpty() ?: false val credentialFaceMatchResults = credentialResultsMapper(results, projectConfiguration, isFace = true) val credentialFingerprintMatchResults = credentialResultsMapper(results, projectConfiguration, isFace = false) @@ -37,6 +38,7 @@ internal class CreateIdentifyResponseUseCase @Inject constructor( return AppIdentifyResponse( sessionId = currentSessionId, + isMultiFactorIdEnabled = isMultiFactorIdEnabled, // Return the results with the highest confidence score identifications = if (bestFingerprintConfidence > bestFaceConfidence) { fingerprintResults.distinctBy(AppMatchResult::guid) @@ -54,7 +56,7 @@ internal class CreateIdentifyResponseUseCase @Inject constructor( ?.getSdkConfiguration(fingerprintMatchResult.sdk) ?.decisionPolicy ?.let { fingerprintDecisionPolicy -> - fingerprintMatchResult.results.mapToMatchResults(fingerprintDecisionPolicy, projectConfiguration) + fingerprintMatchResult.results.mapToMatchResults(fingerprintDecisionPolicy, projectConfiguration, isCredentialMatch = false) } } ?: emptyList() @@ -66,13 +68,14 @@ internal class CreateIdentifyResponseUseCase @Inject constructor( ?.getSdkConfiguration(faceMatchResult.sdk) ?.decisionPolicy ?.let { faceDecisionPolicy -> - faceMatchResult.results.mapToMatchResults(faceDecisionPolicy, projectConfiguration) + faceMatchResult.results.mapToMatchResults(faceDecisionPolicy, projectConfiguration, isCredentialMatch = false) } } ?: emptyList() private fun List.mapToMatchResults( decisionPolicy: DecisionPolicy, projectConfiguration: ProjectConfiguration, + isCredentialMatch: Boolean, ): List { val goodResults = this .filter { it.confidence >= decisionPolicy.low } @@ -82,7 +85,14 @@ internal class CreateIdentifyResponseUseCase @Inject constructor( .filter { it.confidence >= decisionPolicy.high } .ifEmpty { goodResults } .take(projectConfiguration.identification.maxNbOfReturnedCandidates) - .map { AppMatchResult(it.subjectId, it.confidence, decisionPolicy) } + .map { + AppMatchResult( + guid = it.subjectId, + confidenceScore = it.confidence, + decisionPolicy = decisionPolicy, + isCredentialMatch = isCredentialMatch, + ) + } } /** @@ -119,7 +129,7 @@ internal class CreateIdentifyResponseUseCase @Inject constructor( if (decisionPolicy == null) return@let emptyList() val matches = if (isFace) faceMatchItems else fingerMatchItems return@let matches - .mapToMatchResults(decisionPolicy, projectConfiguration) + .mapToMatchResults(decisionPolicy, projectConfiguration, isCredentialMatch = true) .sortedByDescending(AppMatchResult::confidenceScore) }.orEmpty() } 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 9ceb8ea8ba..bb75ce096d 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 @@ -4,12 +4,12 @@ import com.simprints.core.domain.response.AppErrorReason import com.simprints.infra.config.store.models.ProjectConfiguration import com.simprints.infra.logging.LoggingConstants.CrashReportTag.FINGER_MATCHING import com.simprints.infra.logging.Simber +import com.simprints.infra.matching.FaceMatchResult +import com.simprints.infra.matching.FingerprintMatchResult import com.simprints.infra.orchestration.data.responses.AppErrorResponse import com.simprints.infra.orchestration.data.responses.AppMatchResult import com.simprints.infra.orchestration.data.responses.AppResponse import com.simprints.infra.orchestration.data.responses.AppVerifyResponse -import com.simprints.infra.matching.FaceMatchResult -import com.simprints.infra.matching.FingerprintMatchResult import java.io.Serializable import javax.inject.Inject @@ -45,6 +45,7 @@ internal class CreateVerifyResponseUseCase @Inject constructor() { confidenceScore = it.confidence, decisionPolicy = sdkConfiguration.decisionPolicy, verificationMatchThreshold = sdkConfiguration.verificationMatchThreshold, + isCredentialMatch = false, ) } } @@ -68,6 +69,7 @@ internal class CreateVerifyResponseUseCase @Inject constructor() { confidenceScore = it.confidence, decisionPolicy = faceConfiguration.decisionPolicy, verificationMatchThreshold = faceConfiguration.verificationMatchThreshold, + isCredentialMatch = false, ) } } diff --git a/infra/orchestrator-data/src/main/java/com/simprints/infra/orchestration/data/ActionResponse.kt b/infra/orchestrator-data/src/main/java/com/simprints/infra/orchestration/data/ActionResponse.kt index e7f26b3910..2ad48c7286 100644 --- a/infra/orchestrator-data/src/main/java/com/simprints/infra/orchestration/data/ActionResponse.kt +++ b/infra/orchestrator-data/src/main/java/com/simprints/infra/orchestration/data/ActionResponse.kt @@ -24,6 +24,7 @@ sealed class ActionResponse( override val actionIdentifier: ActionRequestIdentifier, override val sessionId: String, val identifications: List, + val isMultiFactorIdEnabled: Boolean, ) : ActionResponse(actionIdentifier, sessionId) @ExcludedFromGeneratedTestCoverageReports("Data struct") diff --git a/infra/orchestrator-data/src/main/java/com/simprints/infra/orchestration/data/responses/AppIdentifyResponse.kt b/infra/orchestrator-data/src/main/java/com/simprints/infra/orchestration/data/responses/AppIdentifyResponse.kt index a9f348697e..6b22f9f81a 100644 --- a/infra/orchestrator-data/src/main/java/com/simprints/infra/orchestration/data/responses/AppIdentifyResponse.kt +++ b/infra/orchestrator-data/src/main/java/com/simprints/infra/orchestration/data/responses/AppIdentifyResponse.kt @@ -10,4 +10,5 @@ import kotlinx.parcelize.Parcelize data class AppIdentifyResponse( val identifications: List, val sessionId: String, + val isMultiFactorIdEnabled: Boolean, ) : AppResponse() diff --git a/infra/orchestrator-data/src/main/java/com/simprints/infra/orchestration/data/responses/AppMatchResult.kt b/infra/orchestrator-data/src/main/java/com/simprints/infra/orchestration/data/responses/AppMatchResult.kt index f88f5bc87f..76fade9547 100644 --- a/infra/orchestrator-data/src/main/java/com/simprints/infra/orchestration/data/responses/AppMatchResult.kt +++ b/infra/orchestrator-data/src/main/java/com/simprints/infra/orchestration/data/responses/AppMatchResult.kt @@ -14,6 +14,8 @@ data class AppMatchResult( val confidenceScore: Int, val matchConfidence: AppMatchConfidence, val verificationSuccess: Boolean? = null, + val isLinkedToScannedCredential: Boolean? = null, + val isCredentialVerified: Boolean? = null, ) : Parcelable { // Temporarily using match confidence as a proxy for tiers. val tier: AppResponseTier @@ -28,12 +30,19 @@ data class AppMatchResult( guid: String, confidenceScore: Float, decisionPolicy: DecisionPolicy, + isCredentialMatch: Boolean, verificationMatchThreshold: Float? = null, ) : this( guid = guid, confidenceScore = confidenceScore.toInt(), matchConfidence = computeMatchConfidence(confidenceScore.toInt(), decisionPolicy), verificationSuccess = computeVerificationSuccess(confidenceScore.toInt(), verificationMatchThreshold), + isLinkedToScannedCredential = isCredentialMatch, + isCredentialVerified = if (isCredentialMatch) { + computeVerificationSuccess(confidenceScore.toInt(), verificationMatchThreshold) + } else { + null + }, ) companion object { From 6c97652f626302b585504da83958a67fda815aa0 Mon Sep 17 00:00:00 2001 From: alex Date: Sun, 12 Oct 2025 21:10:32 +0300 Subject: [PATCH 02/31] [MS-1199] Adding 'EnrolmentUpdate' event that captures addition of external credentials to subject. Adding ExternalCredentialCapture events that handles the credentails during enrolment --- ...EnrolmentCreationEventForSubjectUseCase.kt | 8 ++- .../screen/EnrolLastBiometricViewModel.kt | 23 +++--- feature/external-credential/build.gradle.kts | 1 + .../controller/ExternalCredentialViewModel.kt | 37 +++++++++- .../usecases/response/EnrolSubjectUseCase.kt | 22 ++++-- .../screen/SelectSubjectViewModel.kt | 22 ++++++ .../models/ApiMultiFactorIdConfiguration.kt | 13 ++-- .../remote/models/ApiEnrolmentPayloadV4.kt | 17 ++--- .../models/ApiEnrolmentUpdatePayload.kt | 20 ++++++ .../event/remote/models/ApiEventPayload.kt | 9 +++ .../remote/models/ApiEventPayloadType.kt | 12 ++++ .../ApiExternalCredentialCapturePayload.kt | 32 +++++++++ ...piExternalCredentialCaptureValuePayload.kt | 28 ++++++++ .../models/subject/ApiExternalCredential.kt | 16 ++++- .../events/sampledata/EventFactoryUtils.kt | 1 + .../event/domain/models/EnrolmentEventV4.kt | 3 + .../domain/models/EnrolmentUpdateEvent.kt | 47 +++++++++++++ .../infra/events/event/domain/models/Event.kt | 6 ++ .../event/domain/models/EventPayload.kt | 6 ++ .../events/event/domain/models/EventType.kt | 12 ++++ .../models/ExternalCredentialCaptureEvent.kt | 70 +++++++++++++++++++ .../ExternalCredentialCaptureValueEvent.kt | 62 ++++++++++++++++ 22 files changed, 432 insertions(+), 35 deletions(-) create mode 100644 infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEnrolmentUpdatePayload.kt create mode 100644 infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiExternalCredentialCapturePayload.kt create mode 100644 infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiExternalCredentialCaptureValuePayload.kt create mode 100644 infra/events/src/main/java/com/simprints/infra/events/event/domain/models/EnrolmentUpdateEvent.kt create mode 100644 infra/events/src/main/java/com/simprints/infra/events/event/domain/models/ExternalCredentialCaptureEvent.kt create mode 100644 infra/events/src/main/java/com/simprints/infra/events/event/domain/models/ExternalCredentialCaptureValueEvent.kt 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 cba8da95f5..766cabd0d3 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 @@ -39,7 +39,10 @@ internal class GetEnrolmentCreationEventForSubjectUseCase @Inject constructor( ?.fromSubjectToEnrolmentCreationEvent() if (recordCreationEvent == null) { - Simber.e("Couldn't find enrolment for subjectActions", IllegalStateException("No enrolment record found for subjectId: $subjectId")) + Simber.e( + "Couldn't find enrolment for subjectActions", + IllegalStateException("No enrolment record found for subjectId: $subjectId"), + ) return null } @@ -52,8 +55,7 @@ internal class GetEnrolmentCreationEventForSubjectUseCase @Inject constructor( moduleId = moduleId, attendantId = attendantId, biometricReferences = EnrolmentRecordCreationEvent.buildBiometricReferences(fingerprintSamples, faceSamples, encoder), - // TODO [CORE-3421] Review if EnrolmentRecordCreationEvent should contain List of external credentials, as it currently doesn't make sense - externalCredentials = externalCredentials + externalCredentials = externalCredentials, ) companion object { diff --git a/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/screen/EnrolLastBiometricViewModel.kt b/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/screen/EnrolLastBiometricViewModel.kt index c580af08cb..0522c2149d 100644 --- a/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/screen/EnrolLastBiometricViewModel.kt +++ b/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/screen/EnrolLastBiometricViewModel.kt @@ -25,6 +25,7 @@ import com.simprints.infra.enrolment.records.repository.domain.models.SubjectAct import com.simprints.infra.enrolment.records.repository.domain.models.SubjectQuery import com.simprints.infra.events.event.domain.models.BiometricReferenceCreationEvent import com.simprints.infra.events.event.domain.models.EnrolmentEventV4 +import com.simprints.infra.events.event.domain.models.ExternalCredentialCaptureValueEvent import com.simprints.infra.events.session.SessionEventRepository import com.simprints.infra.logging.LoggingConstants.CrashReportTag.ENROLMENT import com.simprints.infra.logging.Simber @@ -136,21 +137,27 @@ internal class EnrolLastBiometricViewModel @Inject constructor( private suspend fun registerEvent(subject: Subject) { Simber.d("Register events for enrolments", tag = ENROLMENT) - - val biometricReferenceIds = eventRepository + val events = eventRepository .getEventsInCurrentSession() + + val biometricReferenceIds = events .filterIsInstance() .sortedByDescending { it.payload.createdAt } .map { it.payload.id } + val externalCredentialIds = events + .filterIsInstance() + .map { it.payload.id } + eventRepository.addOrUpdateEvent( EnrolmentEventV4( - timeHelper.now(), - subject.subjectId, - subject.projectId, - subject.moduleId, - subject.attendantId, - biometricReferenceIds, + createdAt = timeHelper.now(), + subjectId = subject.subjectId, + projectId = subject.projectId, + moduleId = subject.moduleId, + attendantId = subject.attendantId, + biometricReferenceIds = biometricReferenceIds, + externalCredentialIds = externalCredentialIds, ), ) } diff --git a/feature/external-credential/build.gradle.kts b/feature/external-credential/build.gradle.kts index e13f5f8a32..411b4dc0e1 100644 --- a/feature/external-credential/build.gradle.kts +++ b/feature/external-credential/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { implementation(project(":infra:enrolment-records:repository")) implementation(project(":infra:auth-store")) implementation(project(":infra:matching")) + implementation(project(":infra:events")) implementation(project(":infra:credential-store")) implementation(libs.androidX.cameraX.view) implementation(libs.mlkit.text.recognition) diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/controller/ExternalCredentialViewModel.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/controller/ExternalCredentialViewModel.kt index d7f60e8678..5cdc2a3709 100644 --- a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/controller/ExternalCredentialViewModel.kt +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/controller/ExternalCredentialViewModel.kt @@ -7,17 +7,26 @@ import androidx.lifecycle.viewModelScope import com.simprints.core.domain.externalcredential.ExternalCredentialType import com.simprints.core.livedata.LiveDataEventWithContent import com.simprints.core.livedata.send +import com.simprints.core.tools.time.TimeHelper import com.simprints.feature.externalcredential.ExternalCredentialSearchResult import com.simprints.feature.externalcredential.model.ExternalCredentialParams +import com.simprints.feature.externalcredential.screens.search.model.toExternalCredential import com.simprints.infra.config.sync.ConfigManager +import com.simprints.infra.events.event.domain.models.ExternalCredentialCaptureEvent +import com.simprints.infra.events.event.domain.models.ExternalCredentialCaptureValueEvent +import com.simprints.infra.events.session.SessionEventRepository +import com.simprints.infra.logging.Simber import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch +import java.util.UUID import javax.inject.Inject import kotlin.collections.orEmpty @HiltViewModel internal class ExternalCredentialViewModel @Inject internal constructor( + private val timeHelper: TimeHelper, private val configManager: ConfigManager, + private val eventRepository: SessionEventRepository, ) : ViewModel() { private var isInitialized = false lateinit var params: ExternalCredentialParams @@ -66,6 +75,32 @@ internal class ExternalCredentialViewModel @Inject internal constructor( } fun finish(result: ExternalCredentialSearchResult) { - _finishEvent.send(result) + viewModelScope.launch { + result.scannedCredential?.let { scannedCredential -> + Simber.d("Saving External Credential Events for $scannedCredential") + val credential = scannedCredential.toExternalCredential(params.subjectId.orEmpty()) + eventRepository.addOrUpdateEvent( + ExternalCredentialCaptureValueEvent( + createdAt = timeHelper.now(), + id = UUID.randomUUID().toString(), + credential = credential, + ), + ) + eventRepository.addOrUpdateEvent( + ExternalCredentialCaptureEvent( + createdAt = timeHelper.now(), + id = UUID.randomUUID().toString(), + endTime = timeHelper.now(), + autoCaptureStartTime = timeHelper.now(), + autoCaptureEndTime = timeHelper.now(), + ocrErrorCount = 0, + capturedTextLength = 0, + credentialTextLength = 0, + selectionId = credential.id, + ), + ) + } + _finishEvent.send(result) + } } } diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/EnrolSubjectUseCase.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/EnrolSubjectUseCase.kt index 7f318cf907..9d7a7c6fdc 100644 --- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/EnrolSubjectUseCase.kt +++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/EnrolSubjectUseCase.kt @@ -7,6 +7,7 @@ import com.simprints.infra.enrolment.records.repository.domain.models.Subject import com.simprints.infra.enrolment.records.repository.domain.models.SubjectAction import com.simprints.infra.events.event.domain.models.BiometricReferenceCreationEvent import com.simprints.infra.events.event.domain.models.EnrolmentEventV4 +import com.simprints.infra.events.event.domain.models.ExternalCredentialCaptureValueEvent import com.simprints.infra.events.session.SessionEventRepository import javax.inject.Inject @@ -19,20 +20,27 @@ internal class EnrolSubjectUseCase @Inject constructor( subject: Subject, project: Project, ) { - val biometricReferenceIds = eventRepository + val events = eventRepository .getEventsInCurrentSession() + + val biometricReferenceIds = events .filterIsInstance() .sortedByDescending { it.payload.createdAt } .map { it.payload.id } + val externalCredentialIds = events + .filterIsInstance() + .map { it.payload.id } + eventRepository.addOrUpdateEvent( EnrolmentEventV4( - timeHelper.now(), - subject.subjectId, - subject.projectId, - subject.moduleId, - subject.attendantId, - biometricReferenceIds, + createdAt = timeHelper.now(), + subjectId = subject.subjectId, + projectId = subject.projectId, + moduleId = subject.moduleId, + attendantId = subject.attendantId, + biometricReferenceIds = biometricReferenceIds, + externalCredentialIds = externalCredentialIds, ), ) enrolmentRecordRepository.performActions(listOf(SubjectAction.Creation(subject)), project) diff --git a/feature/select-subject/src/main/java/com/simprints/feature/selectsubject/screen/SelectSubjectViewModel.kt b/feature/select-subject/src/main/java/com/simprints/feature/selectsubject/screen/SelectSubjectViewModel.kt index 1d04b34ef7..25aafb4cc1 100644 --- a/feature/select-subject/src/main/java/com/simprints/feature/selectsubject/screen/SelectSubjectViewModel.kt +++ b/feature/select-subject/src/main/java/com/simprints/feature/selectsubject/screen/SelectSubjectViewModel.kt @@ -21,6 +21,8 @@ import com.simprints.infra.config.store.tokenization.TokenizationProcessor import com.simprints.infra.config.sync.ConfigManager import com.simprints.infra.enrolment.records.repository.EnrolmentRecordRepository import com.simprints.infra.enrolment.records.repository.domain.models.SubjectQuery +import com.simprints.infra.events.event.domain.models.EnrolmentUpdateEvent +import com.simprints.infra.events.event.domain.models.ExternalCredentialCaptureValueEvent import com.simprints.infra.events.event.domain.models.GuidSelectionEvent import com.simprints.infra.events.session.SessionEventRepository import com.simprints.infra.logging.LoggingConstants.CrashReportTag.SESSION @@ -120,6 +122,7 @@ internal class SelectSubjectViewModel @AssistedInject constructor( viewModelScope.launch { val addedCredential = try { addExternalCredentialToSubjectUseCase(scannedCredential, subjectId = params.subjectId, projectId = params.projectId) + saveCredentialSelectionEvent(params.subjectId) scannedCredential } catch (e: Exception) { Simber.e("Failed to attach scanned credential", e, tag = SESSION) @@ -138,6 +141,25 @@ internal class SelectSubjectViewModel @AssistedInject constructor( _finish.send(SelectSubjectResult(isSubjectIdSaved = true, savedCredential = null)) } + private suspend fun saveCredentialSelectionEvent(subjectId: String) = with(sessionCoroutineScope) { + try { + val externalCredentialIdsToAdd = eventRepository + .getEventsInCurrentSession() + .filterIsInstance() + .map { it.payload.credential.id } + Simber.d("Adding credentials $externalCredentialIdsToAdd to subject $subjectId", tag = SESSION) + eventRepository.addOrUpdateEvent( + EnrolmentUpdateEvent( + timeHelper.now(), + subjectId = subjectId, + externalCredentialIdsToAdd = externalCredentialIdsToAdd, + ), + ) + } catch (t: Throwable) { + Simber.e("Failed to save Enrolment Update Event", t, tag = SESSION) + } + } + private suspend fun saveSelectionEvent(subjectId: String): Boolean = with(sessionCoroutineScope) { try { val event = GuidSelectionEvent(timeHelper.now(), subjectId) diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiMultiFactorIdConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiMultiFactorIdConfiguration.kt index e85b27bb49..31995e63e0 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiMultiFactorIdConfiguration.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiMultiFactorIdConfiguration.kt @@ -6,20 +6,23 @@ import com.simprints.infra.config.store.models.MultiFactorIdConfiguration @Keep internal data class ApiMultiFactorIdConfiguration( - val allowedExternalCredentials: List + val allowedExternalCredentials: List, ) { fun toDomain(): MultiFactorIdConfiguration = MultiFactorIdConfiguration( - allowedExternalCredentials = allowedExternalCredentials.map { it.toDomain() } + allowedExternalCredentials = allowedExternalCredentials.map { it.toDomain() }, ) } @Keep enum class ApiExternalCredentialType { - NHIS_CARD, GhanaIdCard, QRCode; + NHIS_CARD, + GHANA_CARD, + QR_CODE, + ; fun toDomain(): ExternalCredentialType = when (this) { NHIS_CARD -> ExternalCredentialType.NHISCard - GhanaIdCard -> ExternalCredentialType.GhanaIdCard - QRCode -> ExternalCredentialType.QRCode + GHANA_CARD -> ExternalCredentialType.GhanaIdCard + QR_CODE -> ExternalCredentialType.QRCode } } diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEnrolmentPayloadV4.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEnrolmentPayloadV4.kt index 28368915c6..24d4c99723 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEnrolmentPayloadV4.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEnrolmentPayloadV4.kt @@ -14,20 +14,21 @@ internal data class ApiEnrolmentPayloadV4( val moduleId: String, val attendantId: String, val biometricReferenceIds: List, + val externalCredentialIds: List, ) : ApiEventPayload(startTime) { constructor(domainPayload: EnrolmentEventV4.EnrolmentPayload) : this( - domainPayload.createdAt.fromDomainToApi(), - domainPayload.subjectId, - domainPayload.projectId, - domainPayload.moduleId.value, - domainPayload.attendantId.value, - domainPayload.biometricReferenceIds, + startTime = domainPayload.createdAt.fromDomainToApi(), + subjectId = domainPayload.subjectId, + projectId = domainPayload.projectId, + moduleId = domainPayload.moduleId.value, + attendantId = domainPayload.attendantId.value, + biometricReferenceIds = domainPayload.biometricReferenceIds, + externalCredentialIds = domainPayload.externalCredentialIds, ) override fun getTokenizedFieldJsonPath(tokenKeyType: TokenKeyType): String? = when (tokenKeyType) { TokenKeyType.AttendantId -> "attendantId" TokenKeyType.ModuleId -> "moduleId" - TokenKeyType.ExternalCredential -> "externalCredential" - TokenKeyType.Unknown -> null + else -> null } } diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEnrolmentUpdatePayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEnrolmentUpdatePayload.kt new file mode 100644 index 0000000000..2adebe0e1c --- /dev/null +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEnrolmentUpdatePayload.kt @@ -0,0 +1,20 @@ +package com.simprints.infra.eventsync.event.remote.models + +import androidx.annotation.Keep +import com.simprints.infra.config.store.models.TokenKeyType +import com.simprints.infra.events.event.domain.models.EnrolmentUpdateEvent.EnrolmentUpdatePayload + +@Keep +internal data class ApiEnrolmentUpdatePayload( + override val startTime: ApiTimestamp, + val subjectId: String, + val externalCredentialIdsToAdd: List, +) : ApiEventPayload(startTime) { + constructor(domainPayload: EnrolmentUpdatePayload) : this( + domainPayload.createdAt.fromDomainToApi(), + domainPayload.subjectId, + domainPayload.externalCredentialIdsToAdd, + ) + + override fun getTokenizedFieldJsonPath(tokenKeyType: TokenKeyType): String? = null +} diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEventPayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEventPayload.kt index 0bf9a3677c..f553cbfbab 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEventPayload.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEventPayload.kt @@ -13,6 +13,7 @@ import com.simprints.infra.events.event.domain.models.ConnectivitySnapshotEvent. import com.simprints.infra.events.event.domain.models.ConsentEvent.ConsentPayload import com.simprints.infra.events.event.domain.models.EnrolmentEventV2 import com.simprints.infra.events.event.domain.models.EnrolmentEventV4 +import com.simprints.infra.events.event.domain.models.EnrolmentUpdateEvent.EnrolmentUpdatePayload import com.simprints.infra.events.event.domain.models.EventPayload import com.simprints.infra.events.event.domain.models.EventType.AGE_GROUP_SELECTION import com.simprints.infra.events.event.domain.models.EventType.ALERT_SCREEN @@ -39,10 +40,13 @@ import com.simprints.infra.events.event.domain.models.EventType.CANDIDATE_READ import com.simprints.infra.events.event.domain.models.EventType.COMPLETION_CHECK import com.simprints.infra.events.event.domain.models.EventType.CONNECTIVITY_SNAPSHOT import com.simprints.infra.events.event.domain.models.EventType.CONSENT +import com.simprints.infra.events.event.domain.models.EventType.ENROLMENT_UPDATE import com.simprints.infra.events.event.domain.models.EventType.ENROLMENT_V2 import com.simprints.infra.events.event.domain.models.EventType.ENROLMENT_V4 import com.simprints.infra.events.event.domain.models.EventType.EVENT_DOWN_SYNC_REQUEST import com.simprints.infra.events.event.domain.models.EventType.EVENT_UP_SYNC_REQUEST +import com.simprints.infra.events.event.domain.models.EventType.EXTERNAL_CREDENTIAL_CAPTURE +import com.simprints.infra.events.event.domain.models.EventType.EXTERNAL_CREDENTIAL_CAPTURE_VALUE import com.simprints.infra.events.event.domain.models.EventType.FACE_CAPTURE import com.simprints.infra.events.event.domain.models.EventType.FACE_CAPTURE_BIOMETRICS import com.simprints.infra.events.event.domain.models.EventType.FACE_CAPTURE_CONFIRMATION @@ -63,6 +67,8 @@ import com.simprints.infra.events.event.domain.models.EventType.SCANNER_CONNECTI import com.simprints.infra.events.event.domain.models.EventType.SCANNER_FIRMWARE_UPDATE import com.simprints.infra.events.event.domain.models.EventType.SUSPICIOUS_INTENT import com.simprints.infra.events.event.domain.models.EventType.VERO_2_INFO_SNAPSHOT +import com.simprints.infra.events.event.domain.models.ExternalCredentialCaptureEvent.ExternalCredentialCapturePayload +import com.simprints.infra.events.event.domain.models.ExternalCredentialCaptureValueEvent.ExternalCredentialCaptureValuePayload import com.simprints.infra.events.event.domain.models.GuidSelectionEvent.GuidSelectionPayload import com.simprints.infra.events.event.domain.models.IntentParsingEvent.IntentParsingPayload import com.simprints.infra.events.event.domain.models.InvalidIntentEvent.InvalidIntentPayload @@ -170,4 +176,7 @@ internal fun EventPayload.fromDomainToApi(): ApiEventPayload = when (this.type) LICENSE_CHECK -> ApiLicenseCheckEventPayload(this as LicenseCheckEvent.LicenseCheckEventPayload) AGE_GROUP_SELECTION -> ApiAgeGroupSelectionPayload(this as AgeGroupSelectionEvent.AgeGroupSelectionPayload) BIOMETRIC_REFERENCE_CREATION -> ApiBiometricReferenceCreationPayload(this as BiometricReferenceCreationPayload) + ENROLMENT_UPDATE -> ApiEnrolmentUpdatePayload(this as EnrolmentUpdatePayload) + EXTERNAL_CREDENTIAL_CAPTURE -> ApiExternalCredentialCapturePayload(this as ExternalCredentialCapturePayload) + EXTERNAL_CREDENTIAL_CAPTURE_VALUE -> ApiExternalCredentialCaptureValuePayload(this as ExternalCredentialCaptureValuePayload) } diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEventPayloadType.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEventPayloadType.kt index e2a4908b20..befee5bebb 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEventPayloadType.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEventPayloadType.kt @@ -27,10 +27,13 @@ import com.simprints.infra.events.event.domain.models.EventType.CANDIDATE_READ import com.simprints.infra.events.event.domain.models.EventType.COMPLETION_CHECK import com.simprints.infra.events.event.domain.models.EventType.CONNECTIVITY_SNAPSHOT import com.simprints.infra.events.event.domain.models.EventType.CONSENT +import com.simprints.infra.events.event.domain.models.EventType.ENROLMENT_UPDATE import com.simprints.infra.events.event.domain.models.EventType.ENROLMENT_V2 import com.simprints.infra.events.event.domain.models.EventType.ENROLMENT_V4 import com.simprints.infra.events.event.domain.models.EventType.EVENT_DOWN_SYNC_REQUEST import com.simprints.infra.events.event.domain.models.EventType.EVENT_UP_SYNC_REQUEST +import com.simprints.infra.events.event.domain.models.EventType.EXTERNAL_CREDENTIAL_CAPTURE +import com.simprints.infra.events.event.domain.models.EventType.EXTERNAL_CREDENTIAL_CAPTURE_VALUE import com.simprints.infra.events.event.domain.models.EventType.FACE_CAPTURE import com.simprints.infra.events.event.domain.models.EventType.FACE_CAPTURE_BIOMETRICS import com.simprints.infra.events.event.domain.models.EventType.FACE_CAPTURE_CONFIRMATION @@ -88,6 +91,9 @@ internal enum class ApiEventPayloadType { LicenseCheck, AgeGroupSelection, BiometricReferenceCreation, + EnrolmentUpdate, + ExternalCredentialCaptureValue, + ExternalCredentialCapture, } internal fun EventType.fromDomainToApi(): ApiEventPayloadType = when (this) { @@ -143,6 +149,9 @@ internal fun EventType.fromDomainToApi(): ApiEventPayloadType = when (this) { LICENSE_CHECK -> ApiEventPayloadType.LicenseCheck AGE_GROUP_SELECTION -> ApiEventPayloadType.AgeGroupSelection BIOMETRIC_REFERENCE_CREATION -> ApiEventPayloadType.BiometricReferenceCreation + ENROLMENT_UPDATE -> ApiEventPayloadType.EnrolmentUpdate + EXTERNAL_CREDENTIAL_CAPTURE_VALUE -> ApiEventPayloadType.ExternalCredentialCaptureValue + EXTERNAL_CREDENTIAL_CAPTURE -> ApiEventPayloadType.ExternalCredentialCapture } internal fun ApiEventPayloadType.fromApiToDomain(): EventType = when (this) { @@ -180,4 +189,7 @@ internal fun ApiEventPayloadType.fromApiToDomain(): EventType = when (this) { ApiEventPayloadType.BiometricReferenceCreation -> BIOMETRIC_REFERENCE_CREATION ApiEventPayloadType.Callout -> throw UnsupportedOperationException("") ApiEventPayloadType.Callback -> throw UnsupportedOperationException("") + ApiEventPayloadType.EnrolmentUpdate -> ENROLMENT_UPDATE + ApiEventPayloadType.ExternalCredentialCaptureValue -> EXTERNAL_CREDENTIAL_CAPTURE_VALUE + ApiEventPayloadType.ExternalCredentialCapture -> EXTERNAL_CREDENTIAL_CAPTURE } diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiExternalCredentialCapturePayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiExternalCredentialCapturePayload.kt new file mode 100644 index 0000000000..3840145d4f --- /dev/null +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiExternalCredentialCapturePayload.kt @@ -0,0 +1,32 @@ +package com.simprints.infra.eventsync.event.remote.models + +import androidx.annotation.Keep +import com.simprints.infra.config.store.models.TokenKeyType +import com.simprints.infra.events.event.domain.models.ExternalCredentialCaptureEvent.ExternalCredentialCapturePayload + +@Keep +internal data class ApiExternalCredentialCapturePayload( + override val startTime: ApiTimestamp, + val id: String, + val endTime: ApiTimestamp, + val autoCaptureStartTime: ApiTimestamp, + val autoCaptureEndTime: ApiTimestamp, + val ocrErrorCount: Int, + val capturedTextLength: Int, + val credentialTextLength: Int, + val selectionId: String, +) : ApiEventPayload(startTime) { + constructor(domainPayload: ExternalCredentialCapturePayload) : this( + startTime = domainPayload.createdAt.fromDomainToApi(), + id = domainPayload.id, + endTime = domainPayload.endTime.fromDomainToApi(), + autoCaptureStartTime = domainPayload.autoCaptureStartTime.fromDomainToApi(), + autoCaptureEndTime = domainPayload.autoCaptureEndTime.fromDomainToApi(), + ocrErrorCount = domainPayload.ocrErrorCount, + capturedTextLength = domainPayload.capturedTextLength, + credentialTextLength = domainPayload.credentialTextLength, + selectionId = domainPayload.selectionId, + ) + + override fun getTokenizedFieldJsonPath(tokenKeyType: TokenKeyType): String? = null +} diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiExternalCredentialCaptureValuePayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiExternalCredentialCaptureValuePayload.kt new file mode 100644 index 0000000000..48d2a3753a --- /dev/null +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiExternalCredentialCaptureValuePayload.kt @@ -0,0 +1,28 @@ +package com.simprints.infra.eventsync.event.remote.models + +import androidx.annotation.Keep +import com.simprints.infra.config.store.models.TokenKeyType +import com.simprints.infra.events.event.domain.models.ExternalCredentialCaptureValueEvent.ExternalCredentialCaptureValuePayload +import com.simprints.infra.eventsync.event.remote.models.subject.ApiExternalCredential +import com.simprints.infra.eventsync.event.remote.models.subject.fromDomainToApi + +@Keep +internal data class ApiExternalCredentialCaptureValuePayload( + override val startTime: ApiTimestamp, + val id: String, + val credential: ApiExternalCredential, +) : ApiEventPayload(startTime) { + constructor(domainPayload: ExternalCredentialCaptureValuePayload) : this( + startTime = domainPayload.createdAt.fromDomainToApi(), + id = domainPayload.id, + credential = domainPayload.credential.fromDomainToApi(), + ) + + override fun getTokenizedFieldJsonPath(tokenKeyType: TokenKeyType): String? = when (tokenKeyType) { + TokenKeyType.ExternalCredential -> "credential.value" + TokenKeyType.AttendantId, + TokenKeyType.ModuleId, + TokenKeyType.Unknown, + -> null + } +} diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiExternalCredential.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiExternalCredential.kt index e0815cac93..56e211b037 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiExternalCredential.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiExternalCredential.kt @@ -3,17 +3,27 @@ package com.simprints.infra.eventsync.event.remote.models.subject import com.simprints.core.domain.externalcredential.ExternalCredential import com.simprints.core.domain.externalcredential.ExternalCredentialType import com.simprints.core.domain.tokenization.asTokenizableEncrypted +import com.simprints.infra.config.store.remote.models.ApiExternalCredentialType data class ApiExternalCredential( val id: String, - val type: String, + val type: ApiExternalCredentialType, val value: String, ) - internal fun ApiExternalCredential.fromApiToDomain(subjectId: String) = ExternalCredential( id = id, value = value.asTokenizableEncrypted(), subjectId = subjectId, - type = ExternalCredentialType.valueOf(type) + type = type.toDomain(), +) + +internal fun ExternalCredential.fromDomainToApi() = ApiExternalCredential( + id = id, + value = value.value, + type = when (type) { + ExternalCredentialType.NHISCard -> ApiExternalCredentialType.NHIS_CARD + ExternalCredentialType.GhanaIdCard -> ApiExternalCredentialType.GHANA_CARD + ExternalCredentialType.QRCode -> ApiExternalCredentialType.QR_CODE + }, ) diff --git a/infra/events/src/debug/java/com/simprints/infra/events/sampledata/EventFactoryUtils.kt b/infra/events/src/debug/java/com/simprints/infra/events/sampledata/EventFactoryUtils.kt index b12eb638f4..d7007095e6 100644 --- a/infra/events/src/debug/java/com/simprints/infra/events/sampledata/EventFactoryUtils.kt +++ b/infra/events/src/debug/java/com/simprints/infra/events/sampledata/EventFactoryUtils.kt @@ -378,6 +378,7 @@ fun createEnrolmentEventV4() = EnrolmentEventV4( DEFAULT_MODULE_ID, DEFAULT_USER_ID, listOf(GUID1, GUID2), + emptyList(), ) fun createFingerprintCaptureEvent() = FingerprintCaptureEvent( diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/EnrolmentEventV4.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/EnrolmentEventV4.kt index 01cb57dc1f..a223c54076 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/EnrolmentEventV4.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/EnrolmentEventV4.kt @@ -22,6 +22,7 @@ data class EnrolmentEventV4( moduleId: TokenizableString, attendantId: TokenizableString, biometricReferenceIds: List, + externalCredentialIds: List, ) : this( UUID.randomUUID().toString(), EnrolmentPayload( @@ -32,6 +33,7 @@ data class EnrolmentEventV4( moduleId = moduleId, attendantId = attendantId, biometricReferenceIds = biometricReferenceIds, + externalCredentialIds = externalCredentialIds, ), ENROLMENT_V4, ) @@ -57,6 +59,7 @@ data class EnrolmentEventV4( val moduleId: TokenizableString, val attendantId: TokenizableString, val biometricReferenceIds: List, + val externalCredentialIds: List, override val endedAt: Timestamp? = null, override val type: EventType = ENROLMENT_V4, ) : EventPayload() { diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/EnrolmentUpdateEvent.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/EnrolmentUpdateEvent.kt new file mode 100644 index 0000000000..986b02818e --- /dev/null +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/EnrolmentUpdateEvent.kt @@ -0,0 +1,47 @@ +package com.simprints.infra.events.event.domain.models + +import androidx.annotation.Keep +import com.simprints.core.domain.tokenization.TokenizableString +import com.simprints.core.tools.time.Timestamp +import com.simprints.infra.config.store.models.TokenKeyType +import com.simprints.infra.events.event.domain.models.EventType.ENROLMENT_UPDATE +import java.util.UUID + +@Keep +data class EnrolmentUpdateEvent( + override val id: String = UUID.randomUUID().toString(), + override val payload: EnrolmentUpdatePayload, + override val type: EventType, + override var scopeId: String? = null, + override var projectId: String? = null, +) : Event() { + constructor( + createdAt: Timestamp, + subjectId: String, + externalCredentialIdsToAdd: List, + ) : this( + UUID.randomUUID().toString(), + EnrolmentUpdatePayload(createdAt, EVENT_VERSION, subjectId, externalCredentialIdsToAdd), + ENROLMENT_UPDATE, + ) + + override fun getTokenizableFields(): Map = emptyMap() + + override fun setTokenizedFields(map: Map) = this // No tokenized fields + + @Keep + data class EnrolmentUpdatePayload( + override val createdAt: Timestamp, + override val eventVersion: Int, + val subjectId: String, + val externalCredentialIdsToAdd: List, + override val endedAt: Timestamp? = null, + override val type: EventType = ENROLMENT_UPDATE, + ) : EventPayload() { + override fun toSafeString(): String = "subjectId: $subjectId, credentials: $externalCredentialIdsToAdd" + } + + companion object Companion { + const val EVENT_VERSION = 0 + } +} diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/Event.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/Event.kt index 66119a3c73..c95a9f99c9 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/Event.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/Event.kt @@ -30,10 +30,13 @@ import com.simprints.infra.events.event.domain.models.EventType.Companion.CANDID import com.simprints.infra.events.event.domain.models.EventType.Companion.COMPLETION_CHECK_KEY import com.simprints.infra.events.event.domain.models.EventType.Companion.CONNECTIVITY_SNAPSHOT_KEY import com.simprints.infra.events.event.domain.models.EventType.Companion.CONSENT_KEY +import com.simprints.infra.events.event.domain.models.EventType.Companion.ENROLMENT_UPDATE_KEY import com.simprints.infra.events.event.domain.models.EventType.Companion.ENROLMENT_V2_KEY import com.simprints.infra.events.event.domain.models.EventType.Companion.ENROLMENT_V4_KEY import com.simprints.infra.events.event.domain.models.EventType.Companion.EVENT_DOWN_SYNC_REQUEST_KEY import com.simprints.infra.events.event.domain.models.EventType.Companion.EVENT_UP_SYNC_REQUEST_KEY +import com.simprints.infra.events.event.domain.models.EventType.Companion.EXTERNAL_CREDENTIAL_CAPTURE_KEY +import com.simprints.infra.events.event.domain.models.EventType.Companion.EXTERNAL_CREDENTIAL_CAPTURE_VALUE_KEY import com.simprints.infra.events.event.domain.models.EventType.Companion.FACE_CAPTURE_BIOMETRICS_KEY import com.simprints.infra.events.event.domain.models.EventType.Companion.FACE_CAPTURE_CONFIRMATION_KEY import com.simprints.infra.events.event.domain.models.EventType.Companion.FACE_CAPTURE_KEY @@ -137,6 +140,9 @@ import com.simprints.infra.events.event.domain.models.upsync.EventUpSyncRequestE JsonSubTypes.Type(value = AgeGroupSelectionEvent::class, name = AGE_GROUP_SELECTION_KEY), JsonSubTypes.Type(value = BiometricReferenceCreationEvent::class, name = BIOMETRIC_REFERENCE_CREATION_KEY), JsonSubTypes.Type(value = SampleUpSyncRequestEvent::class, name = SAMPLE_UP_SYNC_REQUEST), + JsonSubTypes.Type(value = EnrolmentUpdateEvent::class, name = ENROLMENT_UPDATE_KEY), + JsonSubTypes.Type(value = ExternalCredentialCaptureValueEvent::class, name = EXTERNAL_CREDENTIAL_CAPTURE_VALUE_KEY), + JsonSubTypes.Type(value = ExternalCredentialCaptureEvent::class, name = EXTERNAL_CREDENTIAL_CAPTURE_KEY), ) abstract class Event { abstract val id: String diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/EventPayload.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/EventPayload.kt index 9cd815e5f1..eb7e9553f8 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/EventPayload.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/EventPayload.kt @@ -12,7 +12,10 @@ import com.simprints.infra.events.event.domain.models.CandidateReadEvent.Candida import com.simprints.infra.events.event.domain.models.CompletionCheckEvent.CompletionCheckPayload import com.simprints.infra.events.event.domain.models.ConnectivitySnapshotEvent.ConnectivitySnapshotPayload import com.simprints.infra.events.event.domain.models.ConsentEvent.ConsentPayload +import com.simprints.infra.events.event.domain.models.EnrolmentUpdateEvent.EnrolmentUpdatePayload import com.simprints.infra.events.event.domain.models.EventType.Companion +import com.simprints.infra.events.event.domain.models.ExternalCredentialCaptureEvent.ExternalCredentialCapturePayload +import com.simprints.infra.events.event.domain.models.ExternalCredentialCaptureValueEvent.ExternalCredentialCaptureValuePayload import com.simprints.infra.events.event.domain.models.GuidSelectionEvent.GuidSelectionPayload import com.simprints.infra.events.event.domain.models.IntentParsingEvent.IntentParsingPayload import com.simprints.infra.events.event.domain.models.InvalidIntentEvent.InvalidIntentPayload @@ -115,6 +118,9 @@ import com.simprints.infra.events.event.domain.models.upsync.EventUpSyncRequestE JsonSubTypes.Type(value = AgeGroupSelectionPayload::class, name = Companion.AGE_GROUP_SELECTION_KEY), JsonSubTypes.Type(value = BiometricReferenceCreationPayload::class, name = Companion.BIOMETRIC_REFERENCE_CREATION_KEY), JsonSubTypes.Type(value = SampleUpSyncRequestPayload::class, name = Companion.SAMPLE_UP_SYNC_REQUEST), + JsonSubTypes.Type(value = EnrolmentUpdatePayload::class, name = Companion.ENROLMENT_UPDATE_KEY), + JsonSubTypes.Type(value = ExternalCredentialCaptureValuePayload::class, name = Companion.EXTERNAL_CREDENTIAL_CAPTURE_VALUE_KEY), + JsonSubTypes.Type(value = ExternalCredentialCapturePayload::class, name = Companion.EXTERNAL_CREDENTIAL_CAPTURE_KEY), ) abstract class EventPayload { abstract val type: EventType diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/EventType.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/EventType.kt index 505b0b4a88..e456beffae 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/EventType.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/EventType.kt @@ -154,6 +154,15 @@ enum class EventType { // key added: BIOMETRIC_REFERENCE_CREATION_KEY BIOMETRIC_REFERENCE_CREATION, + + // key added: ENROLMENT_UPDATE_KEY + ENROLMENT_UPDATE, + + // key added: EXTERNAL_CREDENTIAL_CAPTURE_KEY + EXTERNAL_CREDENTIAL_CAPTURE, + + // key added: EXTERNAL_CREDENTIAL_CAPTURE_VALUE_KEY + EXTERNAL_CREDENTIAL_CAPTURE_VALUE, ; companion object { @@ -206,5 +215,8 @@ enum class EventType { const val LICENSE_CHECK_KEY = "LICENSE_CHECK" const val AGE_GROUP_SELECTION_KEY = "AGE_GROUP_SELECTION" const val BIOMETRIC_REFERENCE_CREATION_KEY = "BIOMETRIC_REFERENCE_CREATION" + const val ENROLMENT_UPDATE_KEY = "ENROLMENT_UPDATE" + const val EXTERNAL_CREDENTIAL_CAPTURE_KEY = "EXTERNAL_CREDENTIAL_CAPTURE" + const val EXTERNAL_CREDENTIAL_CAPTURE_VALUE_KEY = "EXTERNAL_CREDENTIAL_CAPTURE_VALUE" } } diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/ExternalCredentialCaptureEvent.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/ExternalCredentialCaptureEvent.kt new file mode 100644 index 0000000000..a2b624832e --- /dev/null +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/ExternalCredentialCaptureEvent.kt @@ -0,0 +1,70 @@ +package com.simprints.infra.events.event.domain.models + +import androidx.annotation.Keep +import com.simprints.core.domain.tokenization.TokenizableString +import com.simprints.core.tools.time.Timestamp +import com.simprints.infra.config.store.models.TokenKeyType +import com.simprints.infra.events.event.domain.models.EventType.EXTERNAL_CREDENTIAL_CAPTURE +import java.util.UUID + +@Keep +data class ExternalCredentialCaptureEvent( + override val id: String = UUID.randomUUID().toString(), + override val payload: ExternalCredentialCapturePayload, + override val type: EventType, + override var scopeId: String? = null, + override var projectId: String? = null, +) : Event() { + constructor( + createdAt: Timestamp, + id: String, + endTime: Timestamp, + autoCaptureStartTime: Timestamp, + autoCaptureEndTime: Timestamp, + ocrErrorCount: Int, + capturedTextLength: Int, + credentialTextLength: Int, + selectionId: String, + ) : this( + id = UUID.randomUUID().toString(), + payload = ExternalCredentialCapturePayload( + createdAt = createdAt, + eventVersion = EVENT_VERSION, + id = id, + endTime = endTime, + autoCaptureStartTime = autoCaptureStartTime, + autoCaptureEndTime = autoCaptureEndTime, + ocrErrorCount = ocrErrorCount, + capturedTextLength = capturedTextLength, + credentialTextLength = credentialTextLength, + selectionId = selectionId, + ), + type = EXTERNAL_CREDENTIAL_CAPTURE, + ) + + override fun getTokenizableFields(): Map = emptyMap() + + override fun setTokenizedFields(map: Map) = this // No tokenized field + + @Keep + data class ExternalCredentialCapturePayload( + override val createdAt: Timestamp, + override val eventVersion: Int, + val id: String, + val endTime: Timestamp, + val autoCaptureStartTime: Timestamp, + val autoCaptureEndTime: Timestamp, + val ocrErrorCount: Int, + val capturedTextLength: Int, + val credentialTextLength: Int, + val selectionId: String, + override val endedAt: Timestamp? = null, + override val type: EventType = EXTERNAL_CREDENTIAL_CAPTURE, + ) : EventPayload() { + override fun toSafeString(): String = "credential capture: $id" + } + + companion object { + const val EVENT_VERSION = 0 + } +} diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/ExternalCredentialCaptureValueEvent.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/ExternalCredentialCaptureValueEvent.kt new file mode 100644 index 0000000000..90270749cb --- /dev/null +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/ExternalCredentialCaptureValueEvent.kt @@ -0,0 +1,62 @@ +package com.simprints.infra.events.event.domain.models + +import androidx.annotation.Keep +import com.simprints.core.domain.externalcredential.ExternalCredential +import com.simprints.core.domain.tokenization.TokenizableString +import com.simprints.core.tools.time.Timestamp +import com.simprints.infra.config.store.models.TokenKeyType +import com.simprints.infra.events.event.domain.models.EventType.EXTERNAL_CREDENTIAL_CAPTURE_VALUE +import java.util.UUID + +@Keep +data class ExternalCredentialCaptureValueEvent( + override val id: String = UUID.randomUUID().toString(), + override val payload: ExternalCredentialCaptureValuePayload, + override val type: EventType, + override var scopeId: String? = null, + override var projectId: String? = null, +) : Event() { + constructor( + createdAt: Timestamp, + id: String, + credential: ExternalCredential, + ) : this( + id = UUID.randomUUID().toString(), + payload = ExternalCredentialCaptureValuePayload( + createdAt = createdAt, + eventVersion = EVENT_VERSION, + id = id, + credential = credential, + ), + type = EXTERNAL_CREDENTIAL_CAPTURE_VALUE, + ) + + override fun getTokenizableFields(): Map = mapOf( + TokenKeyType.ExternalCredential to payload.credential.value, + ) + + override fun setTokenizedFields(map: Map) = this.copy( + payload = payload.copy( + credential = payload.credential.copy( + value = map[TokenKeyType.ExternalCredential] as? TokenizableString.Tokenized + ?: payload.credential.value, + ), + ), + ) + + @Keep + data class ExternalCredentialCaptureValuePayload( + override val createdAt: Timestamp, + override val eventVersion: Int, + val id: String, + val credential: ExternalCredential, + override val endedAt: Timestamp? = null, + override val type: EventType = EXTERNAL_CREDENTIAL_CAPTURE_VALUE, + ) : EventPayload() { + override fun toSafeString(): String = "id $id, credential: $credential" + } + + companion object { + const val EVENT_VERSION = 0 + } +} From dd948350830a35e407541ce97417c2bd5ba43c70 Mon Sep 17 00:00:00 2001 From: alex Date: Sun, 12 Oct 2025 21:49:24 +0300 Subject: [PATCH 03/31] [MFID PREVIEW] Removing tests from the build actions. Leaving only Staging build types. Altering build name for the MFID Preview build --- .github/workflows/deploy-release-workflow.yml | 33 +++++---- .../workflows/reusable-get-version-name.yml | 67 +++++++++++++------ 2 files changed, 64 insertions(+), 36 deletions(-) diff --git a/.github/workflows/deploy-release-workflow.yml b/.github/workflows/deploy-release-workflow.yml index c9efd4e5b8..644053db1f 100644 --- a/.github/workflows/deploy-release-workflow.yml +++ b/.github/workflows/deploy-release-workflow.yml @@ -8,26 +8,25 @@ concurrency: cancel-in-progress: true jobs: +# run-core-tests: +# name: Run Core Tests +# uses: ./.github/workflows/pr-checks.yml +# secrets: inherit - run-core-tests: - name: Run Core Tests - uses: ./.github/workflows/pr-checks.yml - secrets: inherit - - deploy-to-dev: - uses: ./.github/workflows/pipeline-deploy-to-dev.yml - secrets: inherit - needs: - - run-core-tests +# deploy-to-dev: +# uses: ./.github/workflows/pipeline-deploy-to-dev.yml +# secrets: inherit +# needs: +# - run-core-tests deploy-to-staging: uses: ./.github/workflows/pipeline-deploy-to-staging.yml secrets: inherit - needs: - - run-core-tests +# needs: +# - run-core-tests - deploy-to-internal: - uses: ./.github/workflows/pipeline-deploy-to-internal.yml - secrets: inherit - needs: - - run-core-tests +# deploy-to-internal: +# uses: ./.github/workflows/pipeline-deploy-to-internal.yml +# secrets: inherit +# needs: +# - run-core-tests diff --git a/.github/workflows/reusable-get-version-name.yml b/.github/workflows/reusable-get-version-name.yml index 00c74b807e..c68b241933 100644 --- a/.github/workflows/reusable-get-version-name.yml +++ b/.github/workflows/reusable-get-version-name.yml @@ -1,4 +1,47 @@ -# Parse out the version name based on the release type +## Parse out the version name based on the release type +#name: "[Reusable] Get Version Name" +# +#on: +# workflow_call: +# inputs: +# version-source: +# type: string +# description: "Where to get the version from: 'branch' or 'internal'" +# default: "internal" +# +# outputs: +# version-name: +# description: "The version name extracted from the source" +# value: ${{ jobs.extract-version.outputs.version-name }} +# +#jobs: +# extract-version: +# runs-on: ubuntu-latest +# +# outputs: +# version-name: ${{ steps.extract.outputs.version-name }} +# +# steps: +# - name: Checkout if using internal version +# if: ${{ inputs.version-source == 'internal' }} +# uses: actions/checkout@v5 +# +# - name: Extract version +# id: extract +# run: | +# if [ "${{ inputs.version-source }}" = "branch" ]; then +# last_part="${GITHUB_REF_NAME##*/}" +# echo "version-name=$last_part" >> $GITHUB_OUTPUT +# elif [ "${{ inputs.version-source }}" = "internal" ]; then +# VERSION_NAME=$(grep 'set("VERSION_NAME"' build-logic/build_properties.gradle.kts \ +# | sed -E 's/.*set\("VERSION_NAME",[[:space:]]*"([^"]+)".*/\1/') +# echo "version-name=$VERSION_NAME" >> $GITHUB_OUTPUT +# else +# echo "Error: Unknown version-source '${{ inputs.version-source }}'" +# exit 1 +# fi +# [TODO] Remove below, uncomment above. mfid-preview-build-2025-4-0 + name: "[Reusable] Get Version Name" on: @@ -22,21 +65,7 @@ jobs: version-name: ${{ steps.extract.outputs.version-name }} steps: - - name: Checkout if using internal version - if: ${{ inputs.version-source == 'internal' }} - uses: actions/checkout@v5 - - - name: Extract version - id: extract - run: | - if [ "${{ inputs.version-source }}" = "branch" ]; then - last_part="${GITHUB_REF_NAME##*/}" - echo "version-name=$last_part" >> $GITHUB_OUTPUT - elif [ "${{ inputs.version-source }}" = "internal" ]; then - VERSION_NAME=$(grep 'set("VERSION_NAME"' build-logic/build_properties.gradle.kts \ - | sed -E 's/.*set\("VERSION_NAME",[[:space:]]*"([^"]+)".*/\1/') - echo "version-name=$VERSION_NAME" >> $GITHUB_OUTPUT - else - echo "Error: Unknown version-source '${{ inputs.version-source }}'" - exit 1 - fi + - name: Return MFID preview staging version [MS-1118] + id: extract + run: | + echo "version-name=2025.4.0-MFID-preview" >> $GITHUB_OUTPUT From 9b5db4dd1badc8c254d92b358784cf898566da70 Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 13 Oct 2025 11:37:54 +0300 Subject: [PATCH 04/31] [MFID PREVIEW] Passing external credential value instead of tokenization wrapper object to the query builder --- .../repository/local/RoomEnrolmentRecordQueryBuilder.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 711b4a9c41..ae980018d4 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 @@ -149,9 +149,9 @@ internal class RoomEnrolmentRecordQueryBuilder @Inject constructor() { args.add(it) } - query.externalCredential?.let { + query.externalCredential?.value?.let { clauses.add("${credentialAlias}$EXTERNAL_CREDENTIAL_VALUE_COLUMN = ?") - args.add(query.externalCredential) + args.add(it) } var whereClauseResult = if (clauses.isNotEmpty()) "WHERE ${clauses.joinToString(" AND ")}" else "" From a78a48b899b77f45aff25b6077e6044dab5dd44d Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 13 Oct 2025 17:08:11 +0300 Subject: [PATCH 05/31] [MFID PREVIEW] Removing legacy response types for the MFID testing --- .../mappers/response/LibSimprintsResponseMapper.kt | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/response/LibSimprintsResponseMapper.kt b/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/response/LibSimprintsResponseMapper.kt index 06acfc816a..c2033f5b05 100644 --- a/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/response/LibSimprintsResponseMapper.kt +++ b/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/response/LibSimprintsResponseMapper.kt @@ -55,12 +55,13 @@ internal class LibSimprintsResponseMapper @Inject constructor( Constants.SIMPRINTS_BIOMETRICS_COMPLETE_CHECK to true, ).appendDataPerContractVersion(response) { version -> when { - !BuildConfig.DEBUG && version < VersionsList.INITIAL_REWORK -> putParcelableArrayList( - Constants.SIMPRINTS_IDENTIFICATIONS, - response.identifications - .map { LegacyIdentification(it.guid, it.confidenceScore, LegacyTier.valueOf(it.tier.name)) } - .toCollection(ArrayList()), - ) + // TODO [MFID preview] remove commented out code for release versions +// !BuildConfig.DEBUG && version < VersionsList.INITIAL_REWORK -> putParcelableArrayList( +// Constants.SIMPRINTS_IDENTIFICATIONS, +// response.identifications +// .map { LegacyIdentification(it.guid, it.confidenceScore, LegacyTier.valueOf(it.tier.name)) } +// .toCollection(ArrayList()), +// ) response.isMultiFactorIdEnabled -> putString( Constants.SIMPRINTS_IDENTIFICATIONS, From 447dcf32b6256804077b77496007e21a40eb71f6 Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 13 Oct 2025 19:29:17 +0300 Subject: [PATCH 06/31] [MFID PREVIEW] Verification threshold is now explicitly passed to the match results mapper, so that we can accurately create an identification response --- .../response/CreateIdentifyResponseUseCase.kt | 39 +++++++++++++------ 1 file changed, 27 insertions(+), 12 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 5e1cb1061c..8e80443f5b 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 @@ -56,7 +56,12 @@ internal class CreateIdentifyResponseUseCase @Inject constructor( ?.getSdkConfiguration(fingerprintMatchResult.sdk) ?.decisionPolicy ?.let { fingerprintDecisionPolicy -> - fingerprintMatchResult.results.mapToMatchResults(fingerprintDecisionPolicy, projectConfiguration, isCredentialMatch = false) + fingerprintMatchResult.results.mapToMatchResults( + decisionPolicy = fingerprintDecisionPolicy, + projectConfiguration = projectConfiguration, + isCredentialMatch = false, + verificationMatchThreshold = null, + ) } } ?: emptyList() @@ -68,12 +73,18 @@ internal class CreateIdentifyResponseUseCase @Inject constructor( ?.getSdkConfiguration(faceMatchResult.sdk) ?.decisionPolicy ?.let { faceDecisionPolicy -> - faceMatchResult.results.mapToMatchResults(faceDecisionPolicy, projectConfiguration, isCredentialMatch = false) + faceMatchResult.results.mapToMatchResults( + decisionPolicy = faceDecisionPolicy, + projectConfiguration = projectConfiguration, + isCredentialMatch = false, + verificationMatchThreshold = null, + ) } } ?: emptyList() private fun List.mapToMatchResults( decisionPolicy: DecisionPolicy, + verificationMatchThreshold: Float?, projectConfiguration: ProjectConfiguration, isCredentialMatch: Boolean, ): List { @@ -91,6 +102,7 @@ internal class CreateIdentifyResponseUseCase @Inject constructor( confidenceScore = it.confidence, decisionPolicy = decisionPolicy, isCredentialMatch = isCredentialMatch, + verificationMatchThreshold = verificationMatchThreshold, ) } } @@ -113,23 +125,26 @@ internal class CreateIdentifyResponseUseCase @Inject constructor( val credentialMatchItems = credentialSearchResult.matchResults.map { it.matchResult } val faceMatchItems = credentialMatchItems.filterIsInstance() val fingerMatchItems = credentialMatchItems.filterIsInstance() - val decisionPolicy = if (isFace) { + val (decisionPolicy, verificationMatchThreshold) = if (isFace) { credentialSearchResult.matchResults.find { it.faceBioSdk != null }?.faceBioSdk?.let { sdk -> - projectConfiguration.face - ?.getSdkConfiguration(sdk) - ?.decisionPolicy + val config = projectConfiguration.face?.getSdkConfiguration(sdk) + config?.decisionPolicy to config?.verificationMatchThreshold } } else { credentialSearchResult.matchResults.find { it.fingerprintBioSdk != null }?.fingerprintBioSdk?.let { sdk -> - projectConfiguration.fingerprint - ?.getSdkConfiguration(sdk) - ?.decisionPolicy + val config = projectConfiguration.fingerprint?.getSdkConfiguration(sdk) + config?.decisionPolicy to config?.verificationMatchThreshold } - } + } ?: (null to null) + if (decisionPolicy == null) return@let emptyList() val matches = if (isFace) faceMatchItems else fingerMatchItems return@let matches - .mapToMatchResults(decisionPolicy, projectConfiguration, isCredentialMatch = true) - .sortedByDescending(AppMatchResult::confidenceScore) + .mapToMatchResults( + decisionPolicy = decisionPolicy, + projectConfiguration = projectConfiguration, + isCredentialMatch = true, + verificationMatchThreshold = verificationMatchThreshold, + ).sortedByDescending(AppMatchResult::confidenceScore) }.orEmpty() } From e58b44fa616724c8e98901e37dd316545bbdd6d5 Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 13 Oct 2025 20:43:38 +0300 Subject: [PATCH 07/31] [MFID PREVIEW] External credential field is now an array in the ApiEnrolmentRecordCreationPayload.kt --- .../models/subject/ApiEnrolmentRecordCreationPayload.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordCreationPayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordCreationPayload.kt index dc5b0e7178..8a552a4a4e 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordCreationPayload.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordCreationPayload.kt @@ -4,7 +4,6 @@ import androidx.annotation.Keep import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.annotation.JsonInclude.Include import com.simprints.core.domain.tokenization.asTokenizableEncrypted -import com.simprints.infra.config.store.remote.models.ApiExternalCredentialType import com.simprints.infra.events.event.domain.models.subject.EnrolmentRecordCreationEvent import com.simprints.infra.eventsync.event.remote.models.subject.biometricref.ApiBiometricReference import com.simprints.infra.eventsync.event.remote.models.subject.biometricref.fromApiToDomain @@ -17,7 +16,7 @@ internal data class ApiEnrolmentRecordCreationPayload( val moduleId: String, val attendantId: String, val biometricReferences: List?, - val externalCredential: ApiExternalCredential?, + val externalCredentials: List?, ) : ApiEnrolmentRecordEventPayload(ApiEnrolmentRecordPayloadType.EnrolmentRecordCreation) internal fun ApiEnrolmentRecordCreationPayload.fromApiToDomain() = EnrolmentRecordCreationEvent.EnrolmentRecordCreationPayload( @@ -26,5 +25,5 @@ internal fun ApiEnrolmentRecordCreationPayload.fromApiToDomain() = EnrolmentReco moduleId = moduleId.asTokenizableEncrypted(), attendantId = attendantId.asTokenizableEncrypted(), biometricReferences = biometricReferences?.map { it.fromApiToDomain() } ?: emptyList(), - externalCredentials = externalCredential?.let { listOf(it.fromApiToDomain(subjectId)) } ?: emptyList() + externalCredentials = externalCredentials?.map { it.fromApiToDomain(subjectId) } ?: emptyList(), ) From ea63685aa4aab7d282715db15f1b6f8b57fddb16 Mon Sep 17 00:00:00 2001 From: alex Date: Tue, 14 Oct 2025 11:47:00 +0300 Subject: [PATCH 08/31] [MFID PREVIEW BUILD] Creating 'AppExternalCredential' class for app response types. This class uses decrypted credential values --- .../feature/clientapi/ClientApiViewModel.kt | 28 +++++++++++++++++-- .../response/LibSimprintsResponseMapper.kt | 4 +-- .../orchestration/data/ActionResponse.kt | 6 ++-- .../data/responses/AppExternalCredential.kt | 15 ++++++++++ 4 files changed, 46 insertions(+), 7 deletions(-) create mode 100644 infra/orchestrator-data/src/main/java/com/simprints/infra/orchestration/data/responses/AppExternalCredential.kt diff --git a/feature/client-api/src/main/java/com/simprints/feature/clientapi/ClientApiViewModel.kt b/feature/client-api/src/main/java/com/simprints/feature/clientapi/ClientApiViewModel.kt index 6af1adc7e2..63f2395485 100644 --- a/feature/client-api/src/main/java/com/simprints/feature/clientapi/ClientApiViewModel.kt +++ b/feature/client-api/src/main/java/com/simprints/feature/clientapi/ClientApiViewModel.kt @@ -5,6 +5,8 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.simprints.core.domain.externalcredential.ExternalCredential +import com.simprints.core.domain.tokenization.TokenizableString import com.simprints.core.livedata.LiveDataEvent import com.simprints.core.livedata.LiveDataEventWithContent import com.simprints.core.livedata.send @@ -21,6 +23,9 @@ import com.simprints.feature.clientapi.usecases.GetEnrolmentCreationEventForSubj import com.simprints.feature.clientapi.usecases.IsFlowCompletedWithErrorUseCase import com.simprints.feature.clientapi.usecases.SimpleEventReporter import com.simprints.infra.authstore.AuthStore +import com.simprints.infra.config.store.models.Project +import com.simprints.infra.config.store.models.TokenKeyType +import com.simprints.infra.config.store.tokenization.TokenizationProcessor import com.simprints.infra.config.sync.ConfigManager import com.simprints.infra.logging.LoggingConstants.CrashReportTag.ORCHESTRATION import com.simprints.infra.logging.Simber @@ -30,6 +35,7 @@ import com.simprints.infra.orchestration.data.ActionResponse import com.simprints.infra.orchestration.data.responses.AppConfirmationResponse import com.simprints.infra.orchestration.data.responses.AppEnrolResponse import com.simprints.infra.orchestration.data.responses.AppErrorResponse +import com.simprints.infra.orchestration.data.responses.AppExternalCredential import com.simprints.infra.orchestration.data.responses.AppIdentifyResponse import com.simprints.infra.orchestration.data.responses.AppRefusalResponse import com.simprints.infra.orchestration.data.responses.AppVerifyResponse @@ -53,6 +59,7 @@ class ClientApiViewModel @Inject internal constructor( private val configManager: ConfigManager, private val timeHelper: TimeHelper, private val persistentLogger: PersistentLogger, + private val tokenizationProcessor: TokenizationProcessor, ) : ViewModel() { val returnResponse: LiveData> get() = _returnResponse @@ -115,7 +122,7 @@ class ClientApiViewModel @Inject internal constructor( sessionId = currentSessionId, enrolledGuid = enrolResponse.guid, subjectActions = coSyncEnrolmentRecords, - externalCredential = enrolResponse.externalCredential, + externalCredential = enrolResponse.externalCredential?.toAppExternalCredential(tokenizationProcessor, getProject()), ), ), ) @@ -163,7 +170,7 @@ class ClientApiViewModel @Inject internal constructor( actionIdentifier = action.actionIdentifier, sessionId = currentSessionId, confirmed = confirmResponse.identificationOutcome, - externalCredential = confirmResponse.externalCredential, + externalCredential = confirmResponse.externalCredential?.toAppExternalCredential(tokenizationProcessor, getProject()), ), ), ) @@ -265,4 +272,21 @@ class ClientApiViewModel @Inject internal constructor( body = "${action.actionIdentifier}\n$response", ) } + + private fun ExternalCredential.toAppExternalCredential( + tokenizationProcessor: TokenizationProcessor, + project: Project?, + ): AppExternalCredential? { + if (project == null) return null + val decryptedValue = tokenizationProcessor.decrypt( + encrypted = value, + tokenKeyType = TokenKeyType.ExternalCredential, + project = project, + ) as? TokenizableString.Raw ?: return null + return AppExternalCredential( + id = id, + value = decryptedValue, + type = type, + ) + } } diff --git a/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/response/LibSimprintsResponseMapper.kt b/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/response/LibSimprintsResponseMapper.kt index c2033f5b05..5ed192305c 100644 --- a/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/response/LibSimprintsResponseMapper.kt +++ b/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/response/LibSimprintsResponseMapper.kt @@ -5,9 +5,9 @@ import androidx.core.os.bundleOf import com.simprints.core.BuildConfig import com.simprints.core.DeviceID import com.simprints.core.PackageVersionName -import com.simprints.core.domain.externalcredential.ExternalCredential import com.simprints.core.domain.response.AppErrorReason import com.simprints.infra.orchestration.data.ActionResponse +import com.simprints.infra.orchestration.data.responses.AppExternalCredential import com.simprints.libsimprints.Constants import com.simprints.libsimprints.contracts.VersionsList import com.simprints.libsimprints.contracts.data.ConfidenceBand @@ -160,7 +160,7 @@ internal class LibSimprintsResponseMapper @Inject constructor( actions?.let { putString(Constants.SIMPRINTS_COSYNC_SUBJECT_ACTIONS, it) } } - private fun Bundle.appendExternalCredential(credential: ExternalCredential?) = apply { + private fun Bundle.appendExternalCredential(credential: AppExternalCredential?) = apply { if (credential != null) { val credentialJson = JSONObject() diff --git a/infra/orchestrator-data/src/main/java/com/simprints/infra/orchestration/data/ActionResponse.kt b/infra/orchestrator-data/src/main/java/com/simprints/infra/orchestration/data/ActionResponse.kt index 2ad48c7286..cbb24033ad 100644 --- a/infra/orchestrator-data/src/main/java/com/simprints/infra/orchestration/data/ActionResponse.kt +++ b/infra/orchestrator-data/src/main/java/com/simprints/infra/orchestration/data/ActionResponse.kt @@ -1,8 +1,8 @@ package com.simprints.infra.orchestration.data import com.simprints.core.ExcludedFromGeneratedTestCoverageReports -import com.simprints.core.domain.externalcredential.ExternalCredential import com.simprints.core.domain.response.AppErrorReason +import com.simprints.infra.orchestration.data.responses.AppExternalCredential import com.simprints.infra.orchestration.data.responses.AppMatchResult @ExcludedFromGeneratedTestCoverageReports("Data struct") @@ -16,7 +16,7 @@ sealed class ActionResponse( override val sessionId: String, val enrolledGuid: String, val subjectActions: String?, - val externalCredential: ExternalCredential?, + val externalCredential: AppExternalCredential?, ) : ActionResponse(actionIdentifier, sessionId) @ExcludedFromGeneratedTestCoverageReports("Data struct") @@ -32,7 +32,7 @@ sealed class ActionResponse( override val actionIdentifier: ActionRequestIdentifier, override val sessionId: String, val confirmed: Boolean, - val externalCredential: ExternalCredential?, + val externalCredential: AppExternalCredential?, ) : ActionResponse(actionIdentifier, sessionId) @ExcludedFromGeneratedTestCoverageReports("Data struct") diff --git a/infra/orchestrator-data/src/main/java/com/simprints/infra/orchestration/data/responses/AppExternalCredential.kt b/infra/orchestrator-data/src/main/java/com/simprints/infra/orchestration/data/responses/AppExternalCredential.kt new file mode 100644 index 0000000000..532a536dd4 --- /dev/null +++ b/infra/orchestrator-data/src/main/java/com/simprints/infra/orchestration/data/responses/AppExternalCredential.kt @@ -0,0 +1,15 @@ +package com.simprints.infra.orchestration.data.responses + +import androidx.annotation.Keep +import com.simprints.core.ExcludedFromGeneratedTestCoverageReports +import com.simprints.core.domain.externalcredential.ExternalCredentialType +import com.simprints.core.domain.tokenization.TokenizableString +import java.io.Serializable + +@Keep +@ExcludedFromGeneratedTestCoverageReports("Data class") +data class AppExternalCredential( + val id: String, + val value: TokenizableString.Raw, + val type: ExternalCredentialType, +) : Serializable From 20d8d0d09116ed9d28e8a8f2d620ce06fe43b969 Mon Sep 17 00:00:00 2001 From: alex Date: Tue, 14 Oct 2025 14:49:20 +0300 Subject: [PATCH 09/31] [MFID PREVIEW BUILD] Passing payload ID for credential, instead of credential's ID as per protocol --- .../feature/selectsubject/screen/SelectSubjectViewModel.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/feature/select-subject/src/main/java/com/simprints/feature/selectsubject/screen/SelectSubjectViewModel.kt b/feature/select-subject/src/main/java/com/simprints/feature/selectsubject/screen/SelectSubjectViewModel.kt index 25aafb4cc1..46b70e2f7f 100644 --- a/feature/select-subject/src/main/java/com/simprints/feature/selectsubject/screen/SelectSubjectViewModel.kt +++ b/feature/select-subject/src/main/java/com/simprints/feature/selectsubject/screen/SelectSubjectViewModel.kt @@ -146,7 +146,8 @@ internal class SelectSubjectViewModel @AssistedInject constructor( val externalCredentialIdsToAdd = eventRepository .getEventsInCurrentSession() .filterIsInstance() - .map { it.payload.credential.id } + .map { it.payload.id } + Simber.d("Adding credentials $externalCredentialIdsToAdd to subject $subjectId", tag = SESSION) eventRepository.addOrUpdateEvent( EnrolmentUpdateEvent( From b4af02ef27d9c746a85d619a2d2dd09005110f6b Mon Sep 17 00:00:00 2001 From: alex Date: Tue, 14 Oct 2025 15:56:46 +0300 Subject: [PATCH 10/31] [MFID PREVIEW BUILD] Enrolment Record update now uses list of credentials instead of a single one --- .../ApiEnrolmentRecordUpdatePayload.kt | 6 ++-- .../eventsync/sync/common/SubjectFactory.kt | 28 ++++++++++--------- .../sync/down/tasks/BaseEventDownSyncTask.kt | 3 +- .../subject/EnrolmentRecordUpdateEvent.kt | 6 ++-- 4 files changed, 21 insertions(+), 22 deletions(-) diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordUpdatePayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordUpdatePayload.kt index 414c9ab056..2e972b74b0 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordUpdatePayload.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordUpdatePayload.kt @@ -1,7 +1,6 @@ package com.simprints.infra.eventsync.event.remote.models.subject import androidx.annotation.Keep -import com.simprints.core.domain.externalcredential.ExternalCredential import com.simprints.infra.events.event.domain.models.subject.EnrolmentRecordUpdateEvent import com.simprints.infra.eventsync.event.remote.models.subject.biometricref.ApiBiometricReference import com.simprints.infra.eventsync.event.remote.models.subject.biometricref.fromApiToDomain @@ -11,13 +10,12 @@ internal data class ApiEnrolmentRecordUpdatePayload( val subjectId: String, val biometricReferencesAdded: List?, val biometricReferencesRemoved: List?, - val externalCredentialAdded: ApiExternalCredential?, + val externalCredentialsAdded: List?, ) : ApiEnrolmentRecordEventPayload(ApiEnrolmentRecordPayloadType.EnrolmentRecordUpdate) internal fun ApiEnrolmentRecordUpdatePayload.fromApiToDomain() = EnrolmentRecordUpdateEvent.EnrolmentRecordUpdatePayload( subjectId, biometricReferencesAdded?.map { it.fromApiToDomain() }.orEmpty(), biometricReferencesRemoved.orEmpty(), - externalCredentialAdded?.fromApiToDomain(subjectId), + externalCredentialsAdded?.map { it.fromApiToDomain(subjectId) }.orEmpty(), ) - 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 45ebcc9f33..9e28301c3c 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 @@ -56,6 +56,7 @@ class SubjectFactory @Inject constructor( val removedBiometricReferences = payload.biometricReferencesRemoved.toSet() // to make lookup O(1) val addedFaceSamples = extractFaceSamplesFromBiometricReferences(payload.biometricReferencesAdded) val addedFingerprintSamples = extractFingerprintSamplesFromBiometricReferences(payload.biometricReferencesAdded) + val externalCredentialsAdded = payload.externalCredentialsAdded return existingSubject.copy( faceSamples = existingSubject.faceSamples @@ -64,6 +65,9 @@ class SubjectFactory @Inject constructor( fingerprintSamples = existingSubject.fingerprintSamples .filterNot { it.referenceId in removedBiometricReferences } .plus(addedFingerprintSamples), + externalCredentials = existingSubject.externalCredentials + .plus(externalCredentialsAdded) + .distinctBy { it.value.value }, ) } @@ -75,18 +79,16 @@ class SubjectFactory @Inject constructor( fingerprintResponse: FingerprintCaptureResult?, faceResponse: FaceCaptureResult?, externalCredential: ExternalCredential?, - ): Subject { - return buildSubject( - subjectId = subjectId, - projectId = projectId, - attendantId = attendantId, - moduleId = moduleId, - createdAt = Date(timeHelper.now().ms), - fingerprintSamples = fingerprintResponse?.let { extractFingerprintSamples(it) }.orEmpty(), - faceSamples = faceResponse?.let { extractFaceSamples(it) }.orEmpty(), - externalCredentials = externalCredential?.let { listOf(it) } ?: emptyList(), - ) - } + ): Subject = buildSubject( + subjectId = subjectId, + projectId = projectId, + attendantId = attendantId, + moduleId = moduleId, + createdAt = Date(timeHelper.now().ms), + fingerprintSamples = fingerprintResponse?.let { extractFingerprintSamples(it) }.orEmpty(), + faceSamples = faceResponse?.let { extractFaceSamples(it) }.orEmpty(), + externalCredentials = externalCredential?.let { listOf(it) } ?: emptyList(), + ) fun buildSubject( subjectId: String, @@ -107,7 +109,7 @@ class SubjectFactory @Inject constructor( updatedAt = updatedAt, fingerprintSamples = fingerprintSamples, faceSamples = faceSamples, - externalCredentials = externalCredentials + externalCredentials = externalCredentials, ) private fun extractFingerprintSamples(fingerprintResponse: FingerprintCaptureResult) = 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 75f8cfee9a..33e7e19fa3 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 @@ -42,7 +42,6 @@ internal abstract class BaseEventDownSyncTask( protected val timeHelper: TimeHelper, protected val eventRepository: EventRepository, ) { - abstract suspend fun fetchEvents( operation: EventDownSyncOperation, scope: CoroutineScope, @@ -277,7 +276,7 @@ internal abstract class BaseEventDownSyncTask( faceSamplesToAdd = subjectFactory.extractFaceSamplesFromBiometricReferences(biometricReferencesAdded), fingerprintSamplesToAdd = subjectFactory.extractFingerprintSamplesFromBiometricReferences(biometricReferencesAdded), referenceIdsToRemove = biometricReferencesRemoved, - externalCredentialsToAdd = externalCredentialAdded?.let { listOf(it) } ?: emptyList() + externalCredentialsToAdd = externalCredentialsAdded, ), ) } diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordUpdateEvent.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordUpdateEvent.kt index 34c9196462..af513be965 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordUpdateEvent.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordUpdateEvent.kt @@ -15,14 +15,14 @@ data class EnrolmentRecordUpdateEvent( subjectId: String, biometricReferencesAdded: List, biometricReferencesRemoved: List, - externalCredentialAdded: ExternalCredential?, + externalCredentialsAdded: List, ) : this( UUID.randomUUID().toString(), EnrolmentRecordUpdatePayload( subjectId = subjectId, biometricReferencesAdded = biometricReferencesAdded, biometricReferencesRemoved = biometricReferencesRemoved, - externalCredentialAdded = externalCredentialAdded, + externalCredentialsAdded = externalCredentialsAdded, ), ) @@ -31,6 +31,6 @@ data class EnrolmentRecordUpdateEvent( val subjectId: String, val biometricReferencesAdded: List, val biometricReferencesRemoved: List, - val externalCredentialAdded: ExternalCredential?, + val externalCredentialsAdded: List, ) } From a4958da920387c7415945b988adab9d6ed05923f Mon Sep 17 00:00:00 2001 From: alex Date: Tue, 14 Oct 2025 23:21:49 +0300 Subject: [PATCH 11/31] [MFID PREVIEW BUILD] Adding extra logging to be able to trace down the external credentials state in the release builds --- .../ExternalCredentialSearchViewModel.kt | 16 ++++++++++++++- .../RealmEnrolmentRecordLocalDataSource.kt | 14 ++++++++++--- .../RoomEnrolmentRecordLocalDataSource.kt | 5 ++++- .../ApiEnrolmentRecordUpdatePayload.kt | 20 +++++++++++++------ 4 files changed, 44 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 8a704840af..670defde1c 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 @@ -22,6 +22,7 @@ import com.simprints.infra.config.store.tokenization.TokenizationProcessor import com.simprints.infra.config.sync.ConfigManager import com.simprints.infra.enrolment.records.repository.EnrolmentRecordRepository import com.simprints.infra.enrolment.records.repository.domain.models.SubjectQuery +import com.simprints.infra.logging.Simber import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -129,8 +130,21 @@ internal class ExternalCredentialSearchViewModel @AssistedInject constructor( updateState { it.copy(searchState = SearchState.Searching) } val project = configManager.getProject(authStore.signedInProjectId) val candidates = enrolmentRecordRepository.load(SubjectQuery(projectId = project.id, externalCredential = credential)) + Simber.i("Search by credential $credential returned ${candidates.size} candidates") when { - candidates.isEmpty() -> updateState { it.copy(searchState = SearchState.CredentialNotFound) } + candidates.isEmpty() -> { + val allSubjects = + enrolmentRecordRepository.load(SubjectQuery(projectId = project.id)).filter { it.externalCredentials.isNotEmpty() } + Simber.i( + "There are total of ${allSubjects.size} subjects that have credentials.\n${ + allSubjects.map { + "guid: " + it.subjectId + ", credentials: " + it.externalCredentials + "\n" + } + }", + ) + updateState { it.copy(searchState = SearchState.CredentialNotFound) } + } + else -> { val projectConfig = configManager.getProjectConfiguration() val matches = matchCandidatesUseCase(candidates, credential, externalCredentialParams, project, projectConfig) 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 d4106749e1..bf10a8b0b9 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 @@ -236,15 +236,23 @@ internal class RealmEnrolmentRecordLocalDataSource @Inject constructor( val referencesToDelete = action.referenceIdsToRemove.toSet() // to make lookup O(1) val faceSamplesMap = dbSubject.faceSamples.groupBy { it.referenceId in referencesToDelete } val fingerprintSamplesMap = dbSubject.fingerprintSamples.groupBy { it.referenceId in referencesToDelete } - val allExternalCredentials = (dbSubject.externalCredentials + action.externalCredentialsToAdd.map { it.toRealmDb() }).distinctBy { it.id }.toSet() + val allExternalCredentials = + (dbSubject.externalCredentials + action.externalCredentialsToAdd.map { it.toRealmDb() }) + .distinctBy { it.id } + .toSet() + if (action.externalCredentialsToAdd.isNotEmpty()) { + Simber.i( + "[Realm] Adding external credentials to subject ${dbSubject.subjectId}. Credentials: [${action.externalCredentialsToAdd}]", + ) + } // Append new samples to the list of samples that remain after removing dbSubject.faceSamples = ( faceSamplesMap[false].orEmpty() + action.faceSamplesToAdd.map { it.toRealmDb() } - ).toRealmList() + ).toRealmList() dbSubject.fingerprintSamples = ( fingerprintSamplesMap[false].orEmpty() + action.fingerprintSamplesToAdd.map { it.toRealmDb() } - ).toRealmList() + ).toRealmList() dbSubject.externalCredentials = allExternalCredentials.toRealmList() faceSamplesMap[true]?.forEach { realm.delete(it) } 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 e215a7fcc7..37a7d1dba4 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 @@ -308,7 +308,7 @@ internal class RoomEnrolmentRecordLocalDataSource @Inject constructor( referencesToDelete.size != dbSubject.biometricTemplates.size || action.faceSamplesToAdd.isNotEmpty() || action.fingerprintSamplesToAdd.isNotEmpty() || - action.externalCredentialsToAdd.isNotEmpty() + action.externalCredentialsToAdd.isNotEmpty(), ) { val errorMsg = "Cannot delete all samples for subject ${action.subjectId} without adding new ones" @@ -325,6 +325,9 @@ internal class RoomEnrolmentRecordLocalDataSource @Inject constructor( subjectDao.insertBiometricSamples(templatesToAdd) } if (action.externalCredentialsToAdd.isNotEmpty()) { + Simber.i( + "[Room] Adding external credentials to subject ${dbSubject.subject.subjectId}. Credentials: [${action.externalCredentialsToAdd}]", + ) subjectDao.insertExternalCredentials(action.externalCredentialsToAdd.map { it.toRoomDb() }) } } else { diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordUpdatePayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordUpdatePayload.kt index 2e972b74b0..85f899ffac 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordUpdatePayload.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordUpdatePayload.kt @@ -4,6 +4,7 @@ import androidx.annotation.Keep import com.simprints.infra.events.event.domain.models.subject.EnrolmentRecordUpdateEvent import com.simprints.infra.eventsync.event.remote.models.subject.biometricref.ApiBiometricReference import com.simprints.infra.eventsync.event.remote.models.subject.biometricref.fromApiToDomain +import com.simprints.infra.logging.Simber @Keep internal data class ApiEnrolmentRecordUpdatePayload( @@ -13,9 +14,16 @@ internal data class ApiEnrolmentRecordUpdatePayload( val externalCredentialsAdded: List?, ) : ApiEnrolmentRecordEventPayload(ApiEnrolmentRecordPayloadType.EnrolmentRecordUpdate) -internal fun ApiEnrolmentRecordUpdatePayload.fromApiToDomain() = EnrolmentRecordUpdateEvent.EnrolmentRecordUpdatePayload( - subjectId, - biometricReferencesAdded?.map { it.fromApiToDomain() }.orEmpty(), - biometricReferencesRemoved.orEmpty(), - externalCredentialsAdded?.map { it.fromApiToDomain(subjectId) }.orEmpty(), -) +internal fun ApiEnrolmentRecordUpdatePayload.fromApiToDomain(): EnrolmentRecordUpdateEvent.EnrolmentRecordUpdatePayload { + if (externalCredentialsAdded?.isNotEmpty() == true) { + Simber.i( + "ApiEnrolmentRecordUpdatePayload contains ${externalCredentialsAdded.size} credentials. Subject Id=[$subjectId], external credentials: [$externalCredentialsAdded]", + ) + } + return EnrolmentRecordUpdateEvent.EnrolmentRecordUpdatePayload( + subjectId, + biometricReferencesAdded?.map { it.fromApiToDomain() }.orEmpty(), + biometricReferencesRemoved.orEmpty(), + externalCredentialsAdded?.map { it.fromApiToDomain(subjectId) }.orEmpty(), + ) +} From de9d99e1ee5c8911f05042e6a7d36c39725cdbdc Mon Sep 17 00:00:00 2001 From: alex Date: Wed, 15 Oct 2025 13:21:54 +0300 Subject: [PATCH 12/31] [MFID PREVIEW BUILD] Removing extra logging that traced the external credentials state in the release builds --- .../ExternalCredentialSearchViewModel.kt | 16 +-------------- .../RealmEnrolmentRecordLocalDataSource.kt | 5 ----- .../RoomEnrolmentRecordLocalDataSource.kt | 3 --- .../ApiEnrolmentRecordUpdatePayload.kt | 20 ++++++------------- 4 files changed, 7 insertions(+), 37 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 670defde1c..8a704840af 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 @@ -22,7 +22,6 @@ import com.simprints.infra.config.store.tokenization.TokenizationProcessor import com.simprints.infra.config.sync.ConfigManager import com.simprints.infra.enrolment.records.repository.EnrolmentRecordRepository import com.simprints.infra.enrolment.records.repository.domain.models.SubjectQuery -import com.simprints.infra.logging.Simber import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -130,21 +129,8 @@ internal class ExternalCredentialSearchViewModel @AssistedInject constructor( updateState { it.copy(searchState = SearchState.Searching) } val project = configManager.getProject(authStore.signedInProjectId) val candidates = enrolmentRecordRepository.load(SubjectQuery(projectId = project.id, externalCredential = credential)) - Simber.i("Search by credential $credential returned ${candidates.size} candidates") when { - candidates.isEmpty() -> { - val allSubjects = - enrolmentRecordRepository.load(SubjectQuery(projectId = project.id)).filter { it.externalCredentials.isNotEmpty() } - Simber.i( - "There are total of ${allSubjects.size} subjects that have credentials.\n${ - allSubjects.map { - "guid: " + it.subjectId + ", credentials: " + it.externalCredentials + "\n" - } - }", - ) - updateState { it.copy(searchState = SearchState.CredentialNotFound) } - } - + candidates.isEmpty() -> updateState { it.copy(searchState = SearchState.CredentialNotFound) } else -> { val projectConfig = configManager.getProjectConfiguration() val matches = matchCandidatesUseCase(candidates, credential, externalCredentialParams, project, projectConfig) 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 bf10a8b0b9..37427298d0 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 @@ -241,11 +241,6 @@ internal class RealmEnrolmentRecordLocalDataSource @Inject constructor( .distinctBy { it.id } .toSet() - if (action.externalCredentialsToAdd.isNotEmpty()) { - Simber.i( - "[Realm] Adding external credentials to subject ${dbSubject.subjectId}. Credentials: [${action.externalCredentialsToAdd}]", - ) - } // Append new samples to the list of samples that remain after removing dbSubject.faceSamples = ( faceSamplesMap[false].orEmpty() + action.faceSamplesToAdd.map { it.toRealmDb() } 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 37a7d1dba4..5fe58b49e9 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 @@ -325,9 +325,6 @@ internal class RoomEnrolmentRecordLocalDataSource @Inject constructor( subjectDao.insertBiometricSamples(templatesToAdd) } if (action.externalCredentialsToAdd.isNotEmpty()) { - Simber.i( - "[Room] Adding external credentials to subject ${dbSubject.subject.subjectId}. Credentials: [${action.externalCredentialsToAdd}]", - ) subjectDao.insertExternalCredentials(action.externalCredentialsToAdd.map { it.toRoomDb() }) } } else { diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordUpdatePayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordUpdatePayload.kt index 85f899ffac..2e972b74b0 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordUpdatePayload.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordUpdatePayload.kt @@ -4,7 +4,6 @@ import androidx.annotation.Keep import com.simprints.infra.events.event.domain.models.subject.EnrolmentRecordUpdateEvent import com.simprints.infra.eventsync.event.remote.models.subject.biometricref.ApiBiometricReference import com.simprints.infra.eventsync.event.remote.models.subject.biometricref.fromApiToDomain -import com.simprints.infra.logging.Simber @Keep internal data class ApiEnrolmentRecordUpdatePayload( @@ -14,16 +13,9 @@ internal data class ApiEnrolmentRecordUpdatePayload( val externalCredentialsAdded: List?, ) : ApiEnrolmentRecordEventPayload(ApiEnrolmentRecordPayloadType.EnrolmentRecordUpdate) -internal fun ApiEnrolmentRecordUpdatePayload.fromApiToDomain(): EnrolmentRecordUpdateEvent.EnrolmentRecordUpdatePayload { - if (externalCredentialsAdded?.isNotEmpty() == true) { - Simber.i( - "ApiEnrolmentRecordUpdatePayload contains ${externalCredentialsAdded.size} credentials. Subject Id=[$subjectId], external credentials: [$externalCredentialsAdded]", - ) - } - return EnrolmentRecordUpdateEvent.EnrolmentRecordUpdatePayload( - subjectId, - biometricReferencesAdded?.map { it.fromApiToDomain() }.orEmpty(), - biometricReferencesRemoved.orEmpty(), - externalCredentialsAdded?.map { it.fromApiToDomain(subjectId) }.orEmpty(), - ) -} +internal fun ApiEnrolmentRecordUpdatePayload.fromApiToDomain() = EnrolmentRecordUpdateEvent.EnrolmentRecordUpdatePayload( + subjectId, + biometricReferencesAdded?.map { it.fromApiToDomain() }.orEmpty(), + biometricReferencesRemoved.orEmpty(), + externalCredentialsAdded?.map { it.fromApiToDomain(subjectId) }.orEmpty(), +) From 87f4b6e138af6fa4c773c1f0bec7b8d0b32a7730 Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 16 Oct 2025 10:01:01 +0300 Subject: [PATCH 13/31] [MFID PREVIEW] Fixing tests --- .../response/LibSimprintsResponseMapper.kt | 15 +++--- .../clientapi/ClientApiViewModelTest.kt | 10 +++- .../response/CommCareResponseMapperTest.kt | 1 + .../LibSimprintsResponseMapperTest.kt | 5 +- .../mappers/response/OdkResponseMapperTest.kt | 2 + .../ExternalCredentialViewModelTest.kt | 41 ++++++++++------- .../usecases/AddCallbackEventUseCaseTest.kt | 1 + .../UpdateDailyActivityUseCaseTest.kt | 2 +- .../CreateIdentifyResponseUseCaseTest.kt | 40 ++++++++++++++++ .../eventsync/event/EventValidationUtils.kt | 36 ++++++++++++++- .../ApiEnrolmentRecordCreationEventTest.kt | 15 +++--- .../ApiEnrolmentRecordMoveEventTest.kt | 9 ++-- .../ApiEnrolmentRecordUpdateEventTest.kt | 27 ++++++----- .../MapDomainEventToApiUseCaseTest.kt | 39 ++++++++++++++++ .../down/tasks/CommCareEventSyncTaskTest.kt | 30 ++++++------ .../tasks/SimprintsEventDownSyncTaskTest.kt | 46 ++++++++++--------- .../sync/down/tasks/SubjectFactoryTest.kt | 18 ++++---- .../testtools/RemoteTestingHelper.kt | 3 +- .../events/sampledata/EventFactoryUtils.kt | 31 ++++++++++++- .../infra/events/sampledata/SampleDefaults.kt | 12 +++++ .../domain/models/EnrolmentEventV4Test.kt | 3 ++ .../event/domain/models/EventPayloadTest.kt | 2 + .../all-events/enrolment_update_v0.json | 14 ++++++ .../resources/all-events/enrolment_v4.json | 3 ++ .../external_credential_capture_v0.json | 20 ++++++++ .../external_credential_capture_value_v0.json | 19 ++++++++ .../data/responses/AppMatchResultTest.kt | 3 ++ 27 files changed, 352 insertions(+), 95 deletions(-) create mode 100644 infra/events/src/test/resources/all-events/enrolment_update_v0.json create mode 100644 infra/events/src/test/resources/all-events/external_credential_capture_v0.json create mode 100644 infra/events/src/test/resources/all-events/external_credential_capture_value_v0.json diff --git a/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/response/LibSimprintsResponseMapper.kt b/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/response/LibSimprintsResponseMapper.kt index 5ed192305c..d2488ba5f5 100644 --- a/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/response/LibSimprintsResponseMapper.kt +++ b/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/response/LibSimprintsResponseMapper.kt @@ -55,19 +55,18 @@ internal class LibSimprintsResponseMapper @Inject constructor( Constants.SIMPRINTS_BIOMETRICS_COMPLETE_CHECK to true, ).appendDataPerContractVersion(response) { version -> when { - // TODO [MFID preview] remove commented out code for release versions -// !BuildConfig.DEBUG && version < VersionsList.INITIAL_REWORK -> putParcelableArrayList( -// Constants.SIMPRINTS_IDENTIFICATIONS, -// response.identifications -// .map { LegacyIdentification(it.guid, it.confidenceScore, LegacyTier.valueOf(it.tier.name)) } -// .toCollection(ArrayList()), -// ) - response.isMultiFactorIdEnabled -> putString( Constants.SIMPRINTS_IDENTIFICATIONS, response.mapIdentificationsWithCredentials(), ) + version < VersionsList.INITIAL_REWORK -> putParcelableArrayList( + Constants.SIMPRINTS_IDENTIFICATIONS, + response.identifications + .map { LegacyIdentification(it.guid, it.confidenceScore, LegacyTier.valueOf(it.tier.name)) } + .toCollection(ArrayList()), + ) + else -> putString( Constants.SIMPRINTS_IDENTIFICATIONS, response.identifications diff --git a/feature/client-api/src/test/java/com/simprints/feature/clientapi/ClientApiViewModelTest.kt b/feature/client-api/src/test/java/com/simprints/feature/clientapi/ClientApiViewModelTest.kt index 830e788f23..b063ecb54b 100644 --- a/feature/client-api/src/test/java/com/simprints/feature/clientapi/ClientApiViewModelTest.kt +++ b/feature/client-api/src/test/java/com/simprints/feature/clientapi/ClientApiViewModelTest.kt @@ -16,6 +16,7 @@ import com.simprints.feature.clientapi.usecases.GetEnrolmentCreationEventForSubj import com.simprints.feature.clientapi.usecases.IsFlowCompletedWithErrorUseCase import com.simprints.feature.clientapi.usecases.SimpleEventReporter import com.simprints.infra.authstore.AuthStore +import com.simprints.infra.config.store.tokenization.TokenizationProcessor import com.simprints.infra.config.sync.ConfigManager import com.simprints.infra.orchestration.data.ActionRequest import com.simprints.infra.orchestration.data.ActionRequestIdentifier @@ -74,6 +75,9 @@ internal class ClientApiViewModelTest { @MockK lateinit var persistentLogger: PersistentLogger + @MockK + lateinit var tokenizationProcessor: TokenizationProcessor + private lateinit var viewModel: ClientApiViewModel @Before @@ -101,6 +105,7 @@ internal class ClientApiViewModelTest { configManager = configManager, timeHelper = timeHelper, persistentLogger = persistentLogger, + tokenizationProcessor = tokenizationProcessor, ) } @@ -162,7 +167,10 @@ internal class ClientApiViewModelTest { fun `handleIdentifyResponse saves correct events`() = runTest { viewModel.handleIdentifyResponse( mockRequest(), - mockk { every { identifications } returns emptyList() }, + mockk { + every { identifications } returns emptyList() + every { isMultiFactorIdEnabled } returns false + }, ) coVerify { diff --git a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/response/CommCareResponseMapperTest.kt b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/response/CommCareResponseMapperTest.kt index 0ce8ec2849..f9abf3fb49 100644 --- a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/response/CommCareResponseMapperTest.kt +++ b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/response/CommCareResponseMapperTest.kt @@ -60,6 +60,7 @@ class CommCareResponseMapperTest { matchConfidence = AppMatchConfidence.LOW, ), ), + isMultiFactorIdEnabled = false, ), ) diff --git a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/response/LibSimprintsResponseMapperTest.kt b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/response/LibSimprintsResponseMapperTest.kt index 15226739a1..a55ed7f146 100644 --- a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/response/LibSimprintsResponseMapperTest.kt +++ b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/response/LibSimprintsResponseMapperTest.kt @@ -6,6 +6,7 @@ import com.simprints.core.domain.externalcredential.ExternalCredentialType import com.simprints.core.domain.response.AppErrorReason import com.simprints.core.domain.response.AppMatchConfidence import com.simprints.core.domain.tokenization.asTokenizableEncrypted +import com.simprints.core.domain.tokenization.asTokenizableRaw import com.simprints.feature.clientapi.mappers.request.requestFactories.ConfirmIdentityActionFactory import com.simprints.feature.clientapi.mappers.request.requestFactories.EnrolActionFactory import com.simprints.feature.clientapi.mappers.request.requestFactories.EnrolLastBiometricsActionFactory @@ -92,6 +93,7 @@ class LibSimprintsResponseMapperTest { matchConfidence = AppMatchConfidence.LOW, ), ), + isMultiFactorIdEnabled = false, ), ) @@ -119,6 +121,7 @@ class LibSimprintsResponseMapperTest { matchConfidence = AppMatchConfidence.MEDIUM, ), ), + isMultiFactorIdEnabled = false, ), ) @@ -135,7 +138,7 @@ class LibSimprintsResponseMapperTest { @Test fun `correctly maps confirm response`() { - val expectedValue = "expectedValue".asTokenizableEncrypted() + val expectedValue = "expectedValue".asTokenizableRaw() val expectedType = ExternalCredentialType.NHISCard val expectedJson = "{\"$SCANNED_CREDENTIAL_VALUE\":\"$expectedValue\",\"$SCANNED_CREDENTIAL_TYPE\":\"$expectedType\"}" val extras = mapper( diff --git a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/response/OdkResponseMapperTest.kt b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/response/OdkResponseMapperTest.kt index fd0bd91826..6b1349d889 100644 --- a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/response/OdkResponseMapperTest.kt +++ b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/response/OdkResponseMapperTest.kt @@ -54,6 +54,7 @@ class OdkResponseMapperTest { matchConfidence = AppMatchConfidence.LOW, ), ), + isMultiFactorIdEnabled = false, ), ) @@ -73,6 +74,7 @@ class OdkResponseMapperTest { actionIdentifier = IdentifyRequestActionFactory.getIdentifier(), sessionId = "sessionId", identifications = listOf(), + isMultiFactorIdEnabled = false, ), ) 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 6bb1ac635d..374be31488 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 @@ -1,19 +1,18 @@ package com.simprints.feature.externalcredential.screens.controller import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.* import com.jraska.livedata.test import com.simprints.core.domain.common.FlowType import com.simprints.core.domain.externalcredential.ExternalCredentialType +import com.simprints.core.tools.time.TimeHelper import com.simprints.feature.externalcredential.ExternalCredentialSearchResult import com.simprints.feature.externalcredential.model.ExternalCredentialParams import com.simprints.infra.config.sync.ConfigManager +import com.simprints.infra.events.session.SessionEventRepository import com.simprints.testtools.common.coroutines.TestCoroutineRule -import io.mockk.MockKAnnotations -import io.mockk.coEvery -import io.mockk.every +import io.mockk.* import io.mockk.impl.annotations.MockK -import io.mockk.mockk import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Rule @@ -26,6 +25,12 @@ internal class ExternalCredentialViewModelTest { @get:Rule val testCoroutineRule = TestCoroutineRule() + @MockK + lateinit var eventRepository: SessionEventRepository + + @MockK + lateinit var timeHelper: TimeHelper + @MockK private lateinit var configManager: ConfigManager private lateinit var viewModel: ExternalCredentialViewModel @@ -33,7 +38,7 @@ internal class ExternalCredentialViewModelTest { @Before fun setUp() { MockKAnnotations.init(this, relaxed = true) - viewModel = ExternalCredentialViewModel(configManager = configManager) + viewModel = ExternalCredentialViewModel(configManager = configManager, timeHelper = timeHelper, eventRepository = eventRepository) } @Test @@ -87,13 +92,18 @@ internal class ExternalCredentialViewModelTest { } @Test - fun `finish sends result to finishEvent`() { - val observer = viewModel.finishEvent.test() - val mockResult = mockk(relaxed = true) + fun `finish sends result to finishEvent`() = runTest { + val mockResult = mockk(relaxed = true) { + every { scannedCredential } returns null + } viewModel.finish(mockResult) + val observer = viewModel.finishEvent + .test() + .value() + .getContentIfNotHandled() - assertThat(observer.value()).isNotNull() - assertThat(observer.value()?.peekContent()).isEqualTo(mockResult) + assertThat(observer).isNotNull() + assertThat(observer).isEqualTo(mockResult) } @Test @@ -102,26 +112,25 @@ internal class ExternalCredentialViewModelTest { ExternalCredentialType.NHISCard, ExternalCredentialType.GhanaIdCard, ) - setupProjectConfig(allowedCredentials = allowedCredentials) - val viewModel = ExternalCredentialViewModel(configManager = configManager) + val viewModel = setupViewModel(allowedCredentials = allowedCredentials) val observer = viewModel.externalCredentialTypes.test() assertThat(observer.value()).isEqualTo(allowedCredentials) } @Test fun `init block sets empty list if no allowed credentials configured`() = runTest { - setupProjectConfig(allowedCredentials = emptyList()) - val viewModel = ExternalCredentialViewModel(configManager = configManager) + val viewModel = setupViewModel(allowedCredentials = emptyList()) val observer = viewModel.externalCredentialTypes.test() assertThat(observer.value()).isEmpty() } - private fun setupProjectConfig(allowedCredentials: List) { + private fun setupViewModel(allowedCredentials: List): ExternalCredentialViewModel { coEvery { configManager.getProjectConfiguration() } returns mockk { every { multifactorId } returns mockk { every { allowedExternalCredentials } returns allowedCredentials } } + return ExternalCredentialViewModel(configManager = configManager, timeHelper = timeHelper, eventRepository = eventRepository) } private fun createParams( diff --git a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/AddCallbackEventUseCaseTest.kt b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/AddCallbackEventUseCaseTest.kt index d5beabaa42..fae3556619 100644 --- a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/AddCallbackEventUseCaseTest.kt +++ b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/AddCallbackEventUseCaseTest.kt @@ -70,6 +70,7 @@ class AddCallbackEventUseCaseTest { AppIdentifyResponse( listOf(AppMatchResult("guid", 0, AppMatchConfidence.HIGH)), "sessionId", + isMultiFactorIdEnabled = false, ), ) diff --git a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/UpdateDailyActivityUseCaseTest.kt b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/UpdateDailyActivityUseCaseTest.kt index b753742b1f..5d83449464 100644 --- a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/UpdateDailyActivityUseCaseTest.kt +++ b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/UpdateDailyActivityUseCaseTest.kt @@ -59,7 +59,7 @@ class UpdateDailyActivityUseCaseTest { @Test fun `Update daily activity on identify response`() = runTest { - useCase(AppIdentifyResponse(emptyList(), "guid")) + useCase(AppIdentifyResponse(emptyList(), "guid", isMultiFactorIdEnabled = false)) coVerify { recentUserActivityManager.updateRecentUserActivity(any()) } } 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 bba4630fe7..22d51a8680 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 { multifactorId?.allowedExternalCredentials } returns null every { face?.getSdkConfiguration((any()))?.decisionPolicy } returns null every { fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns null }, @@ -52,6 +53,7 @@ class CreateIdentifyResponseUseCaseTest { fun `Returns only face identifications over the low confidence`() = runTest { val result = useCase( 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 @@ -67,6 +69,7 @@ class CreateIdentifyResponseUseCaseTest { fun `Returns exactly N best face identifications over the low confidence`() = runTest { val result = useCase( 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 @@ -82,6 +85,7 @@ class CreateIdentifyResponseUseCaseTest { fun `Returns only high confidence face identifications if there are any`() = runTest { val result = useCase( 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 @@ -97,6 +101,7 @@ class CreateIdentifyResponseUseCaseTest { fun `Returns only fingerprint identifications over the low confidence`() = runTest { val result = useCase( 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( @@ -116,6 +121,7 @@ class CreateIdentifyResponseUseCaseTest { fun `Returns exactly N best fingerprint identifications over the low confidence`() = runTest { val result = useCase( 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( @@ -135,6 +141,7 @@ class CreateIdentifyResponseUseCaseTest { fun `Returns only high confidence fingerprint identifications if there are any`() = runTest { val result = useCase( 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( @@ -154,6 +161,7 @@ class CreateIdentifyResponseUseCaseTest { fun `Returns fingerprint matches if both modalities available and fingerprint has higher confidence`() = runTest { val result = useCase( 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( @@ -176,6 +184,7 @@ class CreateIdentifyResponseUseCaseTest { fun `Returns face matches if both modalities available and face has higher confidence`() = runTest { val result = useCase( 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( @@ -200,6 +209,7 @@ class CreateIdentifyResponseUseCaseTest { val (faceBigConfidence, bigConfidence) = "faceBigConfidence" to 99f val faceMatches = listOf( mockk { + every { verificationThreshold } returns 0.0f every { matchResult } returns FaceMatchResult.Item( subjectId = faceSmallConfidence, confidence = smallConfidence, @@ -230,10 +240,19 @@ class CreateIdentifyResponseUseCaseTest { 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 { @@ -253,6 +272,7 @@ class CreateIdentifyResponseUseCaseTest { val (fingerprintBigConfidence, bigConfidence) = "fingerprintBigConfidence" to 99f val fingerprintMatches = listOf( mockk { + every { verificationThreshold } returns 0.0f every { matchResult } returns FingerprintMatchResult.Item( subjectId = fingerprintSmallConfidence, confidence = smallConfidence, @@ -283,10 +303,19 @@ class CreateIdentifyResponseUseCaseTest { 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 { @@ -320,7 +349,9 @@ class CreateIdentifyResponseUseCaseTest { val result = useCase( 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 }, results = listOf( @@ -349,6 +380,7 @@ class CreateIdentifyResponseUseCaseTest { val credentialFingerprintMatches = listOf( mockk { + every { verificationThreshold } returns 0.0f every { matchResult } returns FingerprintMatchResult.Item( subjectId = sharedGuid, confidence = credentialConfidence, @@ -360,10 +392,18 @@ class CreateIdentifyResponseUseCaseTest { val result = useCase( 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 }, results = listOf( mockk { diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/EventValidationUtils.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/EventValidationUtils.kt index 8d0e66f0f5..5d9bc33e5e 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/EventValidationUtils.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/EventValidationUtils.kt @@ -392,7 +392,8 @@ fun validateEnrolmentEventV4ApiModel(json: JSONObject) { assertThat(getString("moduleId")).isNotNull() assertThat(getString("attendantId")).isNotNull() assertThat(getJSONArray("biometricReferenceIds")).isNotNull() - assertThat(length()).isEqualTo(6) + assertThat(getJSONArray("externalCredentialIds")).isNotNull() + assertThat(length()).isEqualTo(7) } } @@ -724,4 +725,37 @@ fun validateBiometricReferenceCreationEventApiModel(json: JSONObject) { } } +fun validateEnrolmentUpdateEventApiModel(json: JSONObject) { + validateCommonParams(json, "EnrolmentUpdate", 0) + with(json.getJSONObject("payload")) { + validateTimestamp(getJSONObject("startTime")) + assertThat(getString("subjectId")).isNotNull() + assertThat(getString("externalCredentialIdsToAdd")).isNotNull() + } +} + +fun validateExternalCredentialCaptureValueEventApiModel(json: JSONObject) { + validateCommonParams(json, "ExternalCredentialCaptureValue", 0) + with(json.getJSONObject("payload")) { + validateTimestamp(getJSONObject("startTime")) + assertThat(getString("id")).isNotNull() + assertThat(getString("credential")).isNotNull() + } +} + +fun validateExternalCredentialCaptureEventApiModel(json: JSONObject) { + validateCommonParams(json, "ExternalCredentialCapture", 0) + with(json.getJSONObject("payload")) { + validateTimestamp(getJSONObject("startTime")) + assertThat(getString("id")).isNotNull() + assertThat(getString("endTime")).isNotNull() + assertThat(getString("autoCaptureStartTime")).isNotNull() + assertThat(getString("autoCaptureEndTime")).isNotNull() + assertThat(getString("ocrErrorCount")).isNotNull() + assertThat(getString("capturedTextLength")).isNotNull() + assertThat(getString("credentialTextLength")).isNotNull() + assertThat(getString("selectionId")).isNotNull() + } +} + private fun Array.valuesAsStrings(): List = this.map { it.toString() } diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordCreationEventTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordCreationEventTest.kt index e5911dcfbc..1456f98a7f 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordCreationEventTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordCreationEventTest.kt @@ -5,6 +5,7 @@ import com.simprints.core.domain.externalcredential.ExternalCredential import com.simprints.core.domain.externalcredential.ExternalCredentialType import com.simprints.core.domain.fingerprint.IFingerIdentifier import com.simprints.core.domain.tokenization.asTokenizableEncrypted +import com.simprints.infra.config.store.remote.models.ApiExternalCredentialType import com.simprints.infra.events.event.domain.models.subject.EnrolmentRecordCreationEvent import com.simprints.infra.events.event.domain.models.subject.FingerprintReference import com.simprints.infra.events.event.domain.models.subject.FingerprintTemplate @@ -29,10 +30,12 @@ class ApiEnrolmentRecordCreationEventTest { "NEC_1", ), ), - externalCredential = ApiExternalCredential( - id = "id", - type = ExternalCredentialType.NHISCard.toString(), - value = "value" + externalCredentials = listOf( + ApiExternalCredential( + id = "id", + type = ApiExternalCredentialType.NHIS_CARD, + value = "value", + ), ), ) val expectedPayload = EnrolmentRecordCreationEvent.EnrolmentRecordCreationPayload( @@ -54,8 +57,8 @@ class ApiEnrolmentRecordCreationEventTest { id = "id", value = "value".asTokenizableEncrypted(), subjectId = "subjectId", - type = ExternalCredentialType.NHISCard - ) + type = ExternalCredentialType.NHISCard, + ), ), ) diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordMoveEventTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordMoveEventTest.kt index a8d04ff6b6..ab91b997b3 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordMoveEventTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordMoveEventTest.kt @@ -5,6 +5,7 @@ import com.simprints.core.domain.externalcredential.ExternalCredential import com.simprints.core.domain.externalcredential.ExternalCredentialType import com.simprints.core.domain.fingerprint.IFingerIdentifier import com.simprints.core.domain.tokenization.asTokenizableEncrypted +import com.simprints.infra.config.store.remote.models.ApiExternalCredentialType import com.simprints.infra.events.event.domain.models.subject.EnrolmentRecordMoveEvent import com.simprints.infra.events.event.domain.models.subject.FingerprintReference import com.simprints.infra.events.event.domain.models.subject.FingerprintTemplate @@ -33,8 +34,8 @@ class ApiEnrolmentRecordMoveEventTest { ApiExternalCredential( id = "id", value = "value", - type = ExternalCredentialType.NHISCard.toString() - ) + type = ApiExternalCredentialType.NHIS_CARD, + ), ), ApiEnrolmentRecordMovePayload.ApiEnrolmentRecordDeletionInMove( "subjectId", @@ -62,8 +63,8 @@ class ApiEnrolmentRecordMoveEventTest { id = "id", value = "value".asTokenizableEncrypted(), subjectId = "subjectId", - type = ExternalCredentialType.NHISCard - ) + type = ExternalCredentialType.NHISCard, + ), ), EnrolmentRecordMoveEvent.EnrolmentRecordDeletionInMove( "subjectId", diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordUpdateEventTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordUpdateEventTest.kt index 8c6a45e736..98e24e00e4 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordUpdateEventTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordUpdateEventTest.kt @@ -5,6 +5,7 @@ import com.simprints.core.domain.externalcredential.ExternalCredential import com.simprints.core.domain.externalcredential.ExternalCredentialType import com.simprints.core.domain.fingerprint.IFingerIdentifier import com.simprints.core.domain.tokenization.asTokenizableEncrypted +import com.simprints.infra.config.store.remote.models.ApiExternalCredentialType import com.simprints.infra.events.event.domain.models.subject.EnrolmentRecordUpdateEvent import com.simprints.infra.events.event.domain.models.subject.FaceReference import com.simprints.infra.events.event.domain.models.subject.FaceTemplate @@ -36,11 +37,13 @@ class ApiEnrolmentRecordUpdateEventTest { ), ), biometricReferencesRemoved = listOf("fpRefId2"), - externalCredentialAdded = ApiExternalCredential( - id = "id", - type = ExternalCredentialType.NHISCard.toString(), - value = "value" - ) + externalCredentialsAdded = listOf( + ApiExternalCredential( + id = "id", + type = ApiExternalCredentialType.NHIS_CARD, + value = "value", + ), + ), ) val expectedPayload = EnrolmentRecordUpdateEvent.EnrolmentRecordUpdatePayload( subjectId = "subjectId", @@ -57,12 +60,14 @@ class ApiEnrolmentRecordUpdateEventTest { ), ), biometricReferencesRemoved = listOf("fpRefId2"), - externalCredentialAdded = ExternalCredential( - id = "id", - value = "value".asTokenizableEncrypted(), - subjectId = "subjectId", - type = ExternalCredentialType.NHISCard - ) + externalCredentialsAdded = listOf( + ExternalCredential( + id = "id", + value = "value".asTokenizableEncrypted(), + subjectId = "subjectId", + type = ExternalCredentialType.NHISCard, + ), + ), ) assertThat(apiPayload.fromApiToDomain()).isEqualTo(expectedPayload) diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/usecases/MapDomainEventToApiUseCaseTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/usecases/MapDomainEventToApiUseCaseTest.kt index f7fa756e4e..9c7520c990 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/usecases/MapDomainEventToApiUseCaseTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/usecases/MapDomainEventToApiUseCaseTest.kt @@ -28,8 +28,11 @@ import com.simprints.infra.events.sampledata.createEnrolmentCalloutEventV2 import com.simprints.infra.events.sampledata.createEnrolmentCalloutEventV3 import com.simprints.infra.events.sampledata.createEnrolmentEventV2 import com.simprints.infra.events.sampledata.createEnrolmentEventV4 +import com.simprints.infra.events.sampledata.createEnrolmentUpdateEvent import com.simprints.infra.events.sampledata.createEventDownSyncRequestEvent import com.simprints.infra.events.sampledata.createEventUpSyncRequestEvent +import com.simprints.infra.events.sampledata.createExternalCredentialCaptureEvent +import com.simprints.infra.events.sampledata.createExternalCredentialCaptureValueEvent import com.simprints.infra.events.sampledata.createFaceCaptureBiometricsEvent import com.simprints.infra.events.sampledata.createFaceCaptureConfirmationEvent import com.simprints.infra.events.sampledata.createFaceCaptureEvent @@ -73,8 +76,11 @@ import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.Com import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.ConnectivitySnapshot import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.Consent import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.Enrolment +import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.EnrolmentUpdate import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.EventDownSyncRequest import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.EventUpSyncRequest +import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.ExternalCredentialCapture +import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.ExternalCredentialCaptureValue import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.FaceCapture import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.FaceCaptureBiometrics import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.FaceCaptureConfirmation @@ -112,6 +118,9 @@ import com.simprints.infra.eventsync.event.validateConsentEventApiModel import com.simprints.infra.eventsync.event.validateDownSyncRequestEventApiModel import com.simprints.infra.eventsync.event.validateEnrolmentEventV2ApiModel import com.simprints.infra.eventsync.event.validateEnrolmentEventV4ApiModel +import com.simprints.infra.eventsync.event.validateEnrolmentUpdateEventApiModel +import com.simprints.infra.eventsync.event.validateExternalCredentialCaptureEventApiModel +import com.simprints.infra.eventsync.event.validateExternalCredentialCaptureValueEventApiModel import com.simprints.infra.eventsync.event.validateFaceCaptureBiometricsEventApiModel import com.simprints.infra.eventsync.event.validateFaceCaptureConfirmationEventApiModel import com.simprints.infra.eventsync.event.validateFaceCaptureEventApiModel @@ -596,6 +605,33 @@ internal class MapDomainEventToApiUseCaseTest { validateBiometricReferenceCreationEventApiModel(json) } + @Test + fun validate_enrolmentUpdateEventApiModel() { + val event = createEnrolmentUpdateEvent() + val apiEvent = useCase(event, project) + val json = JSONObject(jackson.writeValueAsString(apiEvent)) + + validateEnrolmentUpdateEventApiModel(json) + } + + @Test + fun validate_externalCredentialCaptureValueEventApiModel() { + val event = createExternalCredentialCaptureValueEvent() + val apiEvent = useCase(event, project) + val json = JSONObject(jackson.writeValueAsString(apiEvent)) + + validateExternalCredentialCaptureValueEventApiModel(json) + } + + @Test + fun validate_externalCredentialCaptureEventApiModel() { + val event = createExternalCredentialCaptureEvent() + val apiEvent = useCase(event, project) + val json = JSONObject(jackson.writeValueAsString(apiEvent)) + + validateExternalCredentialCaptureEventApiModel(json) + } + @Test fun `when event contains tokenized attendant id, then ApiEvent should contain tokenizedField`() { validateUserIdTokenization(attendantId = "attendantId".asTokenizableEncrypted()) @@ -698,6 +734,9 @@ internal class MapDomainEventToApiUseCaseTest { LicenseCheck -> validate_licenseCheckEventApiModel() AgeGroupSelection -> validate_ageGroupSelectionEventApiModel() BiometricReferenceCreation -> validate_biometricReferenceCreationEventApiModel() + EnrolmentUpdate -> TODO() + ExternalCredentialCaptureValue -> TODO() + ExternalCredentialCapture -> TODO() null -> TODO() }.safeSealedWhens } 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 f2c2416b78..7a44a0b38f 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 @@ -70,8 +70,8 @@ class CommCareEventSyncTaskTest { id = "id", value = "value".asTokenizableEncrypted(), subjectId = "subjectId", - type = ExternalCredentialType.NHISCard - ) + type = ExternalCredentialType.NHISCard, + ), ), ) val ENROLMENT_RECORD_MOVE_MODULE = EnrolmentRecordMoveEvent( @@ -85,8 +85,8 @@ class CommCareEventSyncTaskTest { id = "id", value = "value".asTokenizableEncrypted(), subjectId = "subjectId", - type = ExternalCredentialType.NHISCard - ) + type = ExternalCredentialType.NHISCard, + ), ), EnrolmentRecordMoveEvent.EnrolmentRecordDeletionInMove( subjectId = "subjectId", @@ -106,8 +106,8 @@ class CommCareEventSyncTaskTest { id = "id", value = "value".asTokenizableEncrypted(), subjectId = "subjectId", - type = ExternalCredentialType.NHISCard - ) + type = ExternalCredentialType.NHISCard, + ), ), EnrolmentRecordMoveEvent.EnrolmentRecordDeletionInMove( subjectId = "subjectId", @@ -117,14 +117,16 @@ class CommCareEventSyncTaskTest { ), ) val ENROLMENT_RECORD_UPDATE = EnrolmentRecordUpdateEvent( - "subjectId", - listOf(FaceReference("id", listOf(FaceTemplate("template")), "format")), - listOf("referenceIdToDelete"), - externalCredentialAdded = ExternalCredential( - id = "id", - value = "value".asTokenizableEncrypted(), - subjectId = "subjectId", - type = ExternalCredentialType.NHISCard + subjectId = "subjectId", + biometricReferencesAdded = listOf(FaceReference("id", listOf(FaceTemplate("template")), "format")), + biometricReferencesRemoved = listOf("referenceIdToDelete"), + externalCredentialsAdded = listOf( + ExternalCredential( + id = "id", + value = "value".asTokenizableEncrypted(), + subjectId = "subjectId", + type = ExternalCredentialType.NHISCard, + ), ), ) } 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 42c1a4c137..b7764841c9 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 @@ -1,6 +1,6 @@ package com.simprints.infra.eventsync.sync.down.tasks -import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.* import com.simprints.core.domain.externalcredential.ExternalCredential import com.simprints.core.domain.externalcredential.ExternalCredentialType import com.simprints.core.domain.face.FaceSample @@ -14,7 +14,9 @@ import com.simprints.infra.config.sync.ConfigManager import com.simprints.infra.enrolment.records.repository.EnrolmentRecordRepository import com.simprints.infra.enrolment.records.repository.domain.models.Subject import com.simprints.infra.enrolment.records.repository.domain.models.SubjectAction -import com.simprints.infra.enrolment.records.repository.domain.models.SubjectAction.* +import com.simprints.infra.enrolment.records.repository.domain.models.SubjectAction.Creation +import com.simprints.infra.enrolment.records.repository.domain.models.SubjectAction.Deletion +import com.simprints.infra.enrolment.records.repository.domain.models.SubjectAction.Update import com.simprints.infra.events.EventRepository import com.simprints.infra.events.event.domain.models.downsync.EventDownSyncRequestEvent import com.simprints.infra.events.event.domain.models.scope.EventScope @@ -40,9 +42,7 @@ import com.simprints.infra.eventsync.sync.common.SubjectFactory import com.simprints.infra.eventsync.sync.down.tasks.BaseEventDownSyncTask.Companion.EVENTS_BATCH_SIZE import com.simprints.testtools.common.coroutines.TestCoroutineRule import com.simprints.testtools.unit.EncodingUtilsImplForTests -import io.mockk.MockKAnnotations -import io.mockk.coEvery -import io.mockk.coVerify +import io.mockk.* import io.mockk.impl.annotations.MockK import kotlinx.coroutines.CancellationException import kotlinx.coroutines.channels.Channel @@ -72,8 +72,8 @@ class SimprintsEventDownSyncTaskTest { id = "id", value = "value".asTokenizableEncrypted(), subjectId = "subjectId", - type = ExternalCredentialType.NHISCard - ) + type = ExternalCredentialType.NHISCard, + ), ), ) val ENROLMENT_RECORD_MOVE_MODULE = EnrolmentRecordMoveEvent( @@ -87,8 +87,8 @@ class SimprintsEventDownSyncTaskTest { id = "id", value = "value".asTokenizableEncrypted(), subjectId = "subjectId", - type = ExternalCredentialType.NHISCard - ) + type = ExternalCredentialType.NHISCard, + ), ), EnrolmentRecordMoveEvent.EnrolmentRecordDeletionInMove( "subjectId", @@ -108,8 +108,8 @@ class SimprintsEventDownSyncTaskTest { id = "id", value = "value".asTokenizableEncrypted(), subjectId = "subjectId", - type = ExternalCredentialType.NHISCard - ) + type = ExternalCredentialType.NHISCard, + ), ), EnrolmentRecordMoveEvent.EnrolmentRecordDeletionInMove( "subjectId", @@ -129,8 +129,8 @@ class SimprintsEventDownSyncTaskTest { id = "id", value = "value".asTokenizableEncrypted(), subjectId = "subjectId", - type = ExternalCredentialType.NHISCard - ) + type = ExternalCredentialType.NHISCard, + ), ), EnrolmentRecordMoveEvent.EnrolmentRecordDeletionInMove( "subjectId", @@ -143,12 +143,14 @@ class SimprintsEventDownSyncTaskTest { subjectId = "subjectId", biometricReferencesAdded = listOf(FaceReference("id", listOf(FaceTemplate("template")), "format")), biometricReferencesRemoved = listOf("referenceIdToDelete"), - externalCredentialAdded = ExternalCredential( - id = "id", - value = "value".asTokenizableEncrypted(), - subjectId = "subjectId", - type = ExternalCredentialType.NHISCard - ) + externalCredentialsAdded = listOf( + ExternalCredential( + id = "id", + value = "value".asTokenizableEncrypted(), + subjectId = "subjectId", + type = ExternalCredentialType.NHISCard, + ), + ), ) } @@ -560,9 +562,9 @@ class SimprintsEventDownSyncTaskTest { id = "id", value = "value".asTokenizableEncrypted(), subjectId = "subjectId", - type = ExternalCredentialType.NHISCard - ) - ) + type = ExternalCredentialType.NHISCard, + ), + ), ), ) 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 35154ae213..fc33c754b5 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 @@ -65,7 +65,7 @@ class SubjectFactoryTest { attendantId = ATTENDANT_ID, moduleId = MODULE_ID, biometricReferences = listOf(FINGERPRINT_REFERENCE, faceReference), - externalCredentials = emptyList() + externalCredentials = emptyList(), ) val result = factory.buildSubjectFromCreationPayload(payload) val expected = Subject( @@ -100,7 +100,7 @@ class SubjectFactoryTest { attendantId = ATTENDANT_ID, moduleId = MODULE_ID, biometricReferences = listOf(FINGERPRINT_REFERENCE, faceReference), - externalCredential = null + externalCredential = null, ) val result = factory.buildSubjectFromMovePayload(payload) @@ -161,6 +161,7 @@ class SubjectFactoryTest { referenceId = "referenceId-finger-4", ), ), + externalCredentials = listOf(EXTERNAL_CREDENTIAL), ) val payload = EnrolmentRecordUpdatePayload( @@ -183,7 +184,7 @@ class SubjectFactoryTest { templates = listOf(FaceTemplate(template = BASE_64_BYTES.toString())), ), ), - externalCredentialAdded = EXTERNAL_CREDENTIAL + externalCredentialsAdded = listOf(EXTERNAL_CREDENTIAL), ) val result = factory.buildSubjectFromUpdatePayload(subject, payload) @@ -218,6 +219,7 @@ class SubjectFactoryTest { referenceId = "referenceId-finger-6", ), ), + externalCredentials = listOf(EXTERNAL_CREDENTIAL), ) assertThat(result).isEqualTo(expected) } @@ -247,7 +249,7 @@ class SubjectFactoryTest { referenceId = REFERENCE_ID, ), ), - externalCredentials = listOf(EXTERNAL_CREDENTIAL) + externalCredentials = listOf(EXTERNAL_CREDENTIAL), ) val result = factory.buildSubjectFromCaptureResults( @@ -286,7 +288,7 @@ class SubjectFactoryTest { ), ), ), - externalCredential = EXTERNAL_CREDENTIAL + externalCredential = EXTERNAL_CREDENTIAL, ) assertThat(result).isEqualTo(expected) } @@ -313,7 +315,7 @@ class SubjectFactoryTest { referenceId = REFERENCE_ID, ), ), - externalCredentials = listOf(EXTERNAL_CREDENTIAL) + externalCredentials = listOf(EXTERNAL_CREDENTIAL), ) val result = factory.buildSubject( @@ -323,7 +325,7 @@ class SubjectFactoryTest { moduleId = expected.moduleId, fingerprintSamples = expected.fingerprintSamples, faceSamples = expected.faceSamples, - externalCredentials = expected.externalCredentials + externalCredentials = expected.externalCredentials, ) assertThat(result).isEqualTo(expected) } @@ -362,7 +364,7 @@ class SubjectFactoryTest { id = EXTERNAL_CREDENTIAL_ID, value = EXTERNAL_CREDENTIAL_VALUE, subjectId = SUBJECT_ID, - type = EXTERNAL_CREDENTIAL_TYPE + type = EXTERNAL_CREDENTIAL_TYPE, ) } } diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/testtools/RemoteTestingHelper.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/testtools/RemoteTestingHelper.kt index d029848cbf..4f00c40d0f 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/testtools/RemoteTestingHelper.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/testtools/RemoteTestingHelper.kt @@ -18,7 +18,8 @@ internal class RemoteTestingHelper { ApiEventPayloadType.FingerprintCaptureBiometrics, ApiEventPayloadType.FaceCaptureBiometrics, ApiEventPayloadType.EventDownSyncRequest, ApiEventPayloadType.EventUpSyncRequest, ApiEventPayloadType.LicenseCheck, ApiEventPayloadType.AgeGroupSelection, ApiEventPayloadType.BiometricReferenceCreation, ApiEventPayloadType.SampleUpSyncRequest, - null, + ApiEventPayloadType.EnrolmentUpdate, ApiEventPayloadType.ExternalCredentialCaptureValue, + ApiEventPayloadType.ExternalCredentialCapture, null, -> { // ADD TEST FOR NEW EVENT IN THIS CLASS } diff --git a/infra/events/src/debug/java/com/simprints/infra/events/sampledata/EventFactoryUtils.kt b/infra/events/src/debug/java/com/simprints/infra/events/sampledata/EventFactoryUtils.kt index d7007095e6..e609e8ece1 100644 --- a/infra/events/src/debug/java/com/simprints/infra/events/sampledata/EventFactoryUtils.kt +++ b/infra/events/src/debug/java/com/simprints/infra/events/sampledata/EventFactoryUtils.kt @@ -27,8 +27,11 @@ import com.simprints.infra.events.event.domain.models.ConsentEvent.ConsentPayloa import com.simprints.infra.events.event.domain.models.ConsentEvent.ConsentPayload.Type.INDIVIDUAL import com.simprints.infra.events.event.domain.models.EnrolmentEventV2 import com.simprints.infra.events.event.domain.models.EnrolmentEventV4 +import com.simprints.infra.events.event.domain.models.EnrolmentUpdateEvent import com.simprints.infra.events.event.domain.models.Event import com.simprints.infra.events.event.domain.models.EventType +import com.simprints.infra.events.event.domain.models.ExternalCredentialCaptureEvent +import com.simprints.infra.events.event.domain.models.ExternalCredentialCaptureValueEvent import com.simprints.infra.events.event.domain.models.FingerComparisonStrategy import com.simprints.infra.events.event.domain.models.GuidSelectionEvent import com.simprints.infra.events.event.domain.models.IntentParsingEvent @@ -87,12 +90,14 @@ import com.simprints.infra.events.event.domain.models.scope.EventScopeType import com.simprints.infra.events.event.domain.models.scope.Location import com.simprints.infra.events.event.domain.models.upsync.EventUpSyncRequestEvent import com.simprints.infra.events.sampledata.SampleDefaults.CREATED_AT +import com.simprints.infra.events.sampledata.SampleDefaults.CREDENTIAL_ID import com.simprints.infra.events.sampledata.SampleDefaults.DEFAULT_BIOMETRIC_DATA_SOURCE import com.simprints.infra.events.sampledata.SampleDefaults.DEFAULT_METADATA import com.simprints.infra.events.sampledata.SampleDefaults.DEFAULT_MODULE_ID import com.simprints.infra.events.sampledata.SampleDefaults.DEFAULT_PROJECT_ID import com.simprints.infra.events.sampledata.SampleDefaults.DEFAULT_USER_ID import com.simprints.infra.events.sampledata.SampleDefaults.ENDED_AT +import com.simprints.infra.events.sampledata.SampleDefaults.EXTERNAL_CREDENTIAL import com.simprints.infra.events.sampledata.SampleDefaults.GUID1 import com.simprints.infra.events.sampledata.SampleDefaults.GUID2 @@ -378,7 +383,7 @@ fun createEnrolmentEventV4() = EnrolmentEventV4( DEFAULT_MODULE_ID, DEFAULT_USER_ID, listOf(GUID1, GUID2), - emptyList(), + listOf(CREDENTIAL_ID), ) fun createFingerprintCaptureEvent() = FingerprintCaptureEvent( @@ -532,3 +537,27 @@ fun createBiometricReferenceCreationEvent() = BiometricReferenceCreationEvent( modality = BiometricReferenceCreationEvent.BiometricReferenceModality.FACE, captureIds = listOf(GUID1, GUID2), ) + +fun createEnrolmentUpdateEvent() = EnrolmentUpdateEvent( + createdAt = CREATED_AT, + subjectId = GUID1, + externalCredentialIdsToAdd = listOf(CREDENTIAL_ID), +) + +fun createExternalCredentialCaptureValueEvent() = ExternalCredentialCaptureValueEvent( + createdAt = CREATED_AT, + id = CREDENTIAL_ID, + credential = EXTERNAL_CREDENTIAL, +) + +fun createExternalCredentialCaptureEvent() = ExternalCredentialCaptureEvent( + createdAt = CREATED_AT, + id = CREDENTIAL_ID, + endTime = CREATED_AT, + autoCaptureStartTime = CREATED_AT, + autoCaptureEndTime = CREATED_AT, + ocrErrorCount = 0, + capturedTextLength = 0, + credentialTextLength = 0, + selectionId = GUID1, +) diff --git a/infra/events/src/debug/java/com/simprints/infra/events/sampledata/SampleDefaults.kt b/infra/events/src/debug/java/com/simprints/infra/events/sampledata/SampleDefaults.kt index 677831bd2d..7a349d360f 100644 --- a/infra/events/src/debug/java/com/simprints/infra/events/sampledata/SampleDefaults.kt +++ b/infra/events/src/debug/java/com/simprints/infra/events/sampledata/SampleDefaults.kt @@ -1,6 +1,9 @@ package com.simprints.infra.events.sampledata +import com.simprints.core.domain.externalcredential.ExternalCredential +import com.simprints.core.domain.externalcredential.ExternalCredentialType import com.simprints.core.domain.modality.Modes +import com.simprints.core.domain.tokenization.asTokenizableEncrypted import com.simprints.core.domain.tokenization.asTokenizableRaw import com.simprints.core.tools.time.Timestamp import com.simprints.infra.events.event.domain.models.callout.BiometricDataSource @@ -28,4 +31,13 @@ object SampleDefaults { val TIME1 = System.currentTimeMillis() val DEFAULT_MODES = listOf(Modes.FINGERPRINT) + const val CREDENTIAL_ID = "CREDENTIAL_ID" + val CREDENTIAL_TYPE = ExternalCredentialType.NHISCard + val CREDENTIAL_VALUE = "CREDENTIAL_VALUE".asTokenizableEncrypted() + val EXTERNAL_CREDENTIAL = ExternalCredential( + id = CREDENTIAL_ID, + value = CREDENTIAL_VALUE, + subjectId = GUID1, + type = CREDENTIAL_TYPE, + ) } diff --git a/infra/events/src/test/java/com/simprints/infra/events/event/domain/models/EnrolmentEventV4Test.kt b/infra/events/src/test/java/com/simprints/infra/events/event/domain/models/EnrolmentEventV4Test.kt index 15c7bf54be..ac1c2d986c 100644 --- a/infra/events/src/test/java/com/simprints/infra/events/event/domain/models/EnrolmentEventV4Test.kt +++ b/infra/events/src/test/java/com/simprints/infra/events/event/domain/models/EnrolmentEventV4Test.kt @@ -3,9 +3,11 @@ package com.simprints.infra.events.event.domain.models import com.google.common.truth.Truth.assertThat import com.simprints.infra.events.event.domain.models.EventType.ENROLMENT_V4 import com.simprints.infra.events.sampledata.SampleDefaults.CREATED_AT +import com.simprints.infra.events.sampledata.SampleDefaults.CREDENTIAL_ID import com.simprints.infra.events.sampledata.SampleDefaults.DEFAULT_MODULE_ID import com.simprints.infra.events.sampledata.SampleDefaults.DEFAULT_PROJECT_ID import com.simprints.infra.events.sampledata.SampleDefaults.DEFAULT_USER_ID +import com.simprints.infra.events.sampledata.SampleDefaults.EXTERNAL_CREDENTIAL import com.simprints.infra.events.sampledata.SampleDefaults.GUID1 import com.simprints.infra.events.sampledata.SampleDefaults.GUID2 import org.junit.Test @@ -20,6 +22,7 @@ class EnrolmentEventV4Test { DEFAULT_MODULE_ID, DEFAULT_USER_ID, listOf(GUID2), + listOf(CREDENTIAL_ID), ) assertThat(event.id).isNotNull() diff --git a/infra/events/src/test/java/com/simprints/infra/events/event/domain/models/EventPayloadTest.kt b/infra/events/src/test/java/com/simprints/infra/events/event/domain/models/EventPayloadTest.kt index 6e1391fc34..ac441a3c13 100644 --- a/infra/events/src/test/java/com/simprints/infra/events/event/domain/models/EventPayloadTest.kt +++ b/infra/events/src/test/java/com/simprints/infra/events/event/domain/models/EventPayloadTest.kt @@ -43,6 +43,7 @@ import com.simprints.infra.events.event.domain.models.fingerprint.FingerprintCap import com.simprints.infra.events.event.domain.models.fingerprint.FingerprintCaptureEvent import com.simprints.infra.events.sampledata.FACE_TEMPLATE_FORMAT import com.simprints.infra.events.sampledata.SampleDefaults.CREATED_AT +import com.simprints.infra.events.sampledata.SampleDefaults.CREDENTIAL_ID import com.simprints.infra.events.sampledata.SampleDefaults.DEFAULT_BIOMETRIC_DATA_SOURCE import com.simprints.infra.events.sampledata.SampleDefaults.DEFAULT_METADATA import com.simprints.infra.events.sampledata.SampleDefaults.DEFAULT_MODULE_ID @@ -217,6 +218,7 @@ class EventPayloadTest { moduleId = DEFAULT_MODULE_ID, attendantId = DEFAULT_USER_ID, biometricReferenceIds = listOf(GUID1, GUID2), + externalCredentialIds = listOf(CREDENTIAL_ID), ), GuidSelectionEvent(CREATED_AT, GUID1), IntentParsingEvent(CREATED_AT, COMMCARE), diff --git a/infra/events/src/test/resources/all-events/enrolment_update_v0.json b/infra/events/src/test/resources/all-events/enrolment_update_v0.json new file mode 100644 index 0000000000..7be0dc2c77 --- /dev/null +++ b/infra/events/src/test/resources/all-events/enrolment_update_v0.json @@ -0,0 +1,14 @@ +{ + "id": "f9cb378f-eb6d-4c6f-86e8-c1621c7e2ddd", + "type": "ENROLMENT_UPDATE", + "payload": { + "type": "ENROLMENT_UPDATE", + "eventVersion": 0, + "subjectId": "9aa64aff-46b5-4ab4-90b5-82c0e6d9725f", + "createdAt": 1078967890, + "endedAt": 0, + "externalCredentialIdsToAdd": [ + "dbc27bdb-cab3-463e-9004-68065e05f812" + ] + } +} diff --git a/infra/events/src/test/resources/all-events/enrolment_v4.json b/infra/events/src/test/resources/all-events/enrolment_v4.json index d4b3824cf1..976b1a39ee 100644 --- a/infra/events/src/test/resources/all-events/enrolment_v4.json +++ b/infra/events/src/test/resources/all-events/enrolment_v4.json @@ -23,6 +23,9 @@ }, "biometricReferenceIds": [ "94828488-47ce-4c76-8ace-33d69b2a6d75" + ], + "externalCredentialIds": [ + "dbc27bdb-cab3-463e-9004-68065e05f812" ] } } diff --git a/infra/events/src/test/resources/all-events/external_credential_capture_v0.json b/infra/events/src/test/resources/all-events/external_credential_capture_v0.json new file mode 100644 index 0000000000..c2d84d45fa --- /dev/null +++ b/infra/events/src/test/resources/all-events/external_credential_capture_v0.json @@ -0,0 +1,20 @@ +{ + "id": "f9cb378f-eb6d-4c6f-86e8-c1621c7e2d12", + "type": "EXTERNAL_CREDENTIAL_CAPTURE", + "payload": { + "type": "EXTERNAL_CREDENTIAL_CAPTURE", + "id": "dbc27bdb-cab3-463e-9004-68065e05f8ab", + "endTime": 178967891, + "autoCaptureStartTime": 178967890, + "createdAt": 178967890, + "autoCaptureEndTime": 178967891, + "endedAt": 178967891, + "ocrErrorCount": 0, + "capturedTextLength": 0, + "credentialTextLength": 0, + "selectionId": "dbc27bdb-cab3-463e-9004-68065e05f8ec", + "externalCredentialIdsToAdd": [ + "dbc27bdb-cab3-463e-9004-68065e05f812" + ] + } +} diff --git a/infra/events/src/test/resources/all-events/external_credential_capture_value_v0.json b/infra/events/src/test/resources/all-events/external_credential_capture_value_v0.json new file mode 100644 index 0000000000..4ebb8bf667 --- /dev/null +++ b/infra/events/src/test/resources/all-events/external_credential_capture_value_v0.json @@ -0,0 +1,19 @@ +{ + "id": "f9cb378f-eb6d-4c6f-86e8-c1621c7e2d34", + "type": "EXTERNAL_CREDENTIAL_CAPTURE_VALUE", + "payload": { + "type": "EXTERNAL_CREDENTIAL_CAPTURE_VALUE", + "id": "dbc27bdb-cab3-463e-9004-68065e05f8ec", + "createdAt": 1078967890, + "endedAt": 0, + "credential": { + "id": "dbc27bdb-cab3-463e-9004-68065e05f812", + "value": { + "className": "TokenizableString.Tokenized", + "value": "value" + }, + "subjectId": "9aa64aff-46b5-4ab4-90b5-82c0e6d9725f", + "type": "NHISCard" + } + } +} diff --git a/infra/orchestrator-data/src/test/java/com/simprints/infra/orchestration/data/responses/AppMatchResultTest.kt b/infra/orchestrator-data/src/test/java/com/simprints/infra/orchestration/data/responses/AppMatchResultTest.kt index 3e59185383..12bf961d1e 100644 --- a/infra/orchestrator-data/src/test/java/com/simprints/infra/orchestration/data/responses/AppMatchResultTest.kt +++ b/infra/orchestrator-data/src/test/java/com/simprints/infra/orchestration/data/responses/AppMatchResultTest.kt @@ -14,12 +14,14 @@ class AppMatchResultTest { guid = "guid", confidenceScore = 25.0f, decisionPolicy = DecisionPolicy(low = 10, medium = 20, high = 30), + isCredentialMatch = false, ), ).isEqualTo( AppMatchResult( guid = "guid", confidenceScore = 25, matchConfidence = AppMatchConfidence.MEDIUM, + isLinkedToScannedCredential = false, ), ) } @@ -44,6 +46,7 @@ class AppMatchResultTest { guid = "guid", confidenceScore = score, decisionPolicy = DecisionPolicy(low = 20, medium = 40, high = 60), + isCredentialMatch = false, ).tier, ).isEqualTo(expected) } From 23b75846cfb6d00a5784192818313d7b5c204e0c Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 16 Oct 2025 13:51:36 +0300 Subject: [PATCH 14/31] [MFID PREVIEW] Fixing tests --- .../external_credential_capture_v0.json | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/infra/events/src/test/resources/all-events/external_credential_capture_v0.json b/infra/events/src/test/resources/all-events/external_credential_capture_v0.json index c2d84d45fa..c9c7d77975 100644 --- a/infra/events/src/test/resources/all-events/external_credential_capture_v0.json +++ b/infra/events/src/test/resources/all-events/external_credential_capture_v0.json @@ -4,10 +4,22 @@ "payload": { "type": "EXTERNAL_CREDENTIAL_CAPTURE", "id": "dbc27bdb-cab3-463e-9004-68065e05f8ab", - "endTime": 178967891, - "autoCaptureStartTime": 178967890, "createdAt": 178967890, - "autoCaptureEndTime": 178967891, + "endTime": { + "ms": 178967891, + "isTrustworthy": false, + "msSinceBoot": null + }, + "autoCaptureStartTime": { + "ms": 178967890, + "isTrustworthy": false, + "msSinceBoot": null + }, + "autoCaptureEndTime": { + "ms": 178967891, + "isTrustworthy": false, + "msSinceBoot": null + }, "endedAt": 178967891, "ocrErrorCount": 0, "capturedTextLength": 0, From 0241dddb0391a57e7d65c02ce93e38096f504e3f Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 20 Oct 2025 16:23:50 +0300 Subject: [PATCH 15/31] [MFID PREVIEW] Removing unnecessary plurals from strings.xml, and renaming confirmation dialog strings to avoid confusion --- .../feature/externalcredential/ext/ResourceExt.kt | 11 +++-------- .../select/ExternalCredentialSelectFragment.kt | 4 ++-- .../main/res/layout/dialog_skip_scan_confirm.xml | 4 ++-- infra/resources/src/main/res/values/strings.xml | 14 ++++---------- 4 files changed, 11 insertions(+), 22 deletions(-) diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/ext/ResourceExt.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/ext/ResourceExt.kt index fe00b7ee70..91e30bc556 100644 --- a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/ext/ResourceExt.kt +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/ext/ResourceExt.kt @@ -7,22 +7,17 @@ import com.simprints.core.domain.externalcredential.ExternalCredentialType import com.simprints.infra.resources.R as IDR fun Resources.getQuantityCredentialString( - @PluralsRes id: Int, + @StringRes id: Int, @StringRes specificCredentialRes: Int, @StringRes multipleCredentialsRes: Int, credentialTypes: List, ): String { - val credentialsAmount = credentialTypes.size - val documentTypeRes = if (credentialsAmount == 1) { + val documentTypeRes = if (credentialTypes.size == 1) { specificCredentialRes } else { multipleCredentialsRes } - return getQuantityString( - id, - credentialsAmount, - getString(documentTypeRes), - ) + return getString(id, documentTypeRes) } fun Resources.getCredentialFieldTitle(type: ExternalCredentialType): String = when (type) { diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/select/ExternalCredentialSelectFragment.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/select/ExternalCredentialSelectFragment.kt index 6ab288a498..e6ff933d30 100644 --- a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/select/ExternalCredentialSelectFragment.kt +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/select/ExternalCredentialSelectFragment.kt @@ -74,7 +74,7 @@ internal class ExternalCredentialSelectFragment : Fragment(R.layout.fragment_ext private fun initViews(types: List) { binding.title.text = resources.getQuantityCredentialString( - id = IDR.plurals.mfid_scan_action, + id = IDR.string.mfid_scan_action, specificCredentialRes = resources.getCredentialTypeRes(types.firstOrNull()), multipleCredentialsRes = IDR.string.mfid_type_any_document, credentialTypes = types, @@ -136,7 +136,7 @@ internal class ExternalCredentialSelectFragment : Fragment(R.layout.fragment_ext val confirmButton = view.findViewById