From 84a0044edb7fdaed1dbe43e3522e4da9e23f7673 Mon Sep 17 00:00:00 2001 From: alex Date: Tue, 29 Jul 2025 12:22:48 +0300 Subject: [PATCH 001/139] =?UTF-8?q?WIP=20[CORE-3421]=20first=20steps=20of?= =?UTF-8?q?=20adding=20the=20external=20credentials=20support=CB=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infra/config/store/models/Project.kt | 1 + .../store/models/ProjectConfiguration.kt | 1 + .../models/SearchAndVerifyConfiguration.kt | 7 ++++ .../remote/models/ApiProjectConfiguration.kt | 1 + .../models/ApiSearchAndVerifyConfiguration.kt | 27 +++++++++++++++ .../externalcredential/ExternalCredential.kt | 14 ++++++++ .../ExternalCredentialType.kt | 5 +++ .../ExternalCredentialRepository.kt | 8 +++++ .../repository/domain/models/Subject.kt | 2 ++ .../repository/domain/models/SubjectAction.kt | 2 ++ .../repository/domain/models/SubjectQuery.kt | 1 + .../RoomEnrolmentRecordLocalDataSource.kt | 4 +++ .../models/RoomExternalCredentialConverter.kt | 22 +++++++++++++ .../local/models/RoomSubjectConverter.kt | 1 + .../records/room/store/SubjectDao.kt | 12 +++++++ .../records/room/store/SubjectsDatabase.kt | 2 ++ .../room/store/models/DbBiometricTemplate.kt | 1 + .../room/store/models/DbExternalCredential.kt | 33 +++++++++++++++++++ .../room/store/models/SubjectBiometrics.kt | 6 ++++ .../models/SubjectExternalCredentials.kt | 14 ++++++++ .../ApiEnrolmentRecordCreationPayload.kt | 2 ++ .../ApiEnrolmentRecordUpdatePayload.kt | 4 +++ .../models/subject/ApiExternalCredential.kt | 18 ++++++++++ .../subject/EnrolmentRecordCreationEvent.kt | 2 ++ .../subject/EnrolmentRecordUpdateEvent.kt | 10 ++++-- 25 files changed, 197 insertions(+), 3 deletions(-) create mode 100644 infra/config-store/src/main/java/com/simprints/infra/config/store/models/SearchAndVerifyConfiguration.kt create mode 100644 infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiSearchAndVerifyConfiguration.kt create mode 100644 infra/core/src/main/java/com/simprints/core/domain/externalcredential/ExternalCredential.kt create mode 100644 infra/core/src/main/java/com/simprints/core/domain/externalcredential/ExternalCredentialType.kt create mode 100644 infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/ExternalCredentialRepository.kt create mode 100644 infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RoomExternalCredentialConverter.kt create mode 100644 infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/DbExternalCredential.kt create mode 100644 infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/SubjectExternalCredentials.kt create mode 100644 infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiExternalCredential.kt diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/Project.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/Project.kt index f9a97c7a52..acb5c4c235 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/Project.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/Project.kt @@ -14,5 +14,6 @@ data class Project( enum class TokenKeyType { AttendantId, ModuleId, + ExternalCredential, Unknown, } diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/ProjectConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/ProjectConfiguration.kt index de6e8d9aaf..39ea770b3f 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/ProjectConfiguration.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/ProjectConfiguration.kt @@ -10,6 +10,7 @@ data class ProjectConfiguration( val consent: ConsentConfiguration, val identification: IdentificationConfiguration, val synchronization: SynchronizationConfiguration, + val searchAndVerify: SearchAndVerifyConfiguration?, val custom: Map?, ) diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/SearchAndVerifyConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/SearchAndVerifyConfiguration.kt new file mode 100644 index 0000000000..ee29b0528f --- /dev/null +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/SearchAndVerifyConfiguration.kt @@ -0,0 +1,7 @@ +package com.simprints.infra.config.store.models + +import com.simprints.core.domain.externalcredential.ExternalCredentialType + +data class SearchAndVerifyConfiguration( + val allowedExternalCredentials: List +) diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiProjectConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiProjectConfiguration.kt index e4b589bcd7..c782b25f79 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiProjectConfiguration.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiProjectConfiguration.kt @@ -14,6 +14,7 @@ internal data class ApiProjectConfiguration( val consent: ApiConsentConfiguration, val identification: ApiIdentificationConfiguration, val synchronization: ApiSynchronizationConfiguration, + val searchAndVerify: ApiSearchAndVerifyConfiguration?, val custom: Map?, ) { fun toDomain(): ProjectConfiguration = ProjectConfiguration( diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiSearchAndVerifyConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiSearchAndVerifyConfiguration.kt new file mode 100644 index 0000000000..8b3eb3ca29 --- /dev/null +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiSearchAndVerifyConfiguration.kt @@ -0,0 +1,27 @@ +package com.simprints.infra.config.store.remote.models + +import androidx.annotation.Keep +import com.simprints.core.domain.tokenization.asTokenizableEncrypted +import com.simprints.infra.config.store.models.DownSynchronizationConfiguration +import com.simprints.infra.config.store.models.DownSynchronizationConfiguration.Companion.DEFAULT_DOWN_SYNC_MAX_AGE +import com.simprints.infra.config.store.models.Frequency +import com.simprints.infra.config.store.models.SampleSynchronizationConfiguration +import com.simprints.infra.config.store.models.SynchronizationConfiguration +import com.simprints.infra.config.store.models.UpSynchronizationConfiguration + +@Keep +internal data class ApiSearchAndVerifyConfiguration( + val allowedExternalCredentials: List +) + +enum class ApiExternalCredentialType { + NHISCard, GhanaIdCard, QRCode +} + +fun mapToString(a: ApiExternalCredentialType) { + return when(a) + { + ApiExternalCredentialType.NHISCard -> TODO() + ApiExternalCredentialType.GhanaIdCard -> TODO() + ApiExternalCredentialType.QRCode -> TODO() + }} diff --git a/infra/core/src/main/java/com/simprints/core/domain/externalcredential/ExternalCredential.kt b/infra/core/src/main/java/com/simprints/core/domain/externalcredential/ExternalCredential.kt new file mode 100644 index 0000000000..478508ce21 --- /dev/null +++ b/infra/core/src/main/java/com/simprints/core/domain/externalcredential/ExternalCredential.kt @@ -0,0 +1,14 @@ +package com.simprints.core.domain.externalcredential + +import android.os.Parcelable +import com.simprints.core.ExcludedFromGeneratedTestCoverageReports +import com.simprints.core.domain.tokenization.TokenizableString +import kotlinx.parcelize.Parcelize + +@Parcelize +@ExcludedFromGeneratedTestCoverageReports("Data class with generated code") +data class ExternalCredential( + val value: TokenizableString.Tokenized, + val subjectId: String, + val type: ExternalCredentialType, +) : Parcelable diff --git a/infra/core/src/main/java/com/simprints/core/domain/externalcredential/ExternalCredentialType.kt b/infra/core/src/main/java/com/simprints/core/domain/externalcredential/ExternalCredentialType.kt new file mode 100644 index 0000000000..1481ce4af3 --- /dev/null +++ b/infra/core/src/main/java/com/simprints/core/domain/externalcredential/ExternalCredentialType.kt @@ -0,0 +1,5 @@ +package com.simprints.core.domain.externalcredential + +enum class ExternalCredentialType { + NHISCard, GhanaIdCard, QRCode +} diff --git a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/ExternalCredentialRepository.kt b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/ExternalCredentialRepository.kt new file mode 100644 index 0000000000..aac8a37e6a --- /dev/null +++ b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/ExternalCredentialRepository.kt @@ -0,0 +1,8 @@ +package com.simprints.infra.enrolment.records.repository + +import javax.inject.Inject + +internal class ExternalCredentialRepository @Inject constructor( + +){ +} diff --git a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/Subject.kt b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/Subject.kt index 23d307709b..3cc6060579 100644 --- a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/Subject.kt +++ b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/Subject.kt @@ -1,6 +1,7 @@ package com.simprints.infra.enrolment.records.repository.domain.models import android.os.Parcelable +import com.simprints.core.domain.externalcredential.ExternalCredential import com.simprints.core.domain.face.FaceSample import com.simprints.core.domain.fingerprint.FingerprintSample import com.simprints.core.domain.tokenization.TokenizableString @@ -17,4 +18,5 @@ data class Subject( val updatedAt: Date? = null, var fingerprintSamples: List = emptyList(), var faceSamples: List = emptyList(), + var externalCredentials: List = emptyList(), ) : Parcelable diff --git a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/SubjectAction.kt b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/SubjectAction.kt index 421a6f11c6..7953eac800 100644 --- a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/SubjectAction.kt +++ b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/SubjectAction.kt @@ -1,6 +1,7 @@ package com.simprints.infra.enrolment.records.repository.domain.models import androidx.annotation.Keep +import com.simprints.core.domain.externalcredential.ExternalCredential import com.simprints.core.domain.face.FaceSample import com.simprints.core.domain.fingerprint.FingerprintSample @@ -14,6 +15,7 @@ sealed class SubjectAction { val subjectId: String, val faceSamplesToAdd: List, val fingerprintSamplesToAdd: List, + val externalCredentialsToAdd: List, val referenceIdsToRemove: List, ) : SubjectAction() diff --git a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/SubjectQuery.kt b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/SubjectQuery.kt index 73f3af0d43..2151ecb31e 100644 --- a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/SubjectQuery.kt +++ b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/domain/models/SubjectQuery.kt @@ -17,4 +17,5 @@ data class SubjectQuery( val sort: Boolean = false, val afterSubjectId: String? = null, val metadata: String? = null, + val externalCredential: String? = null, ) : StepParams 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 61aed68169..505d9d323f 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 @@ -289,6 +289,10 @@ internal class RoomEnrolmentRecordLocalDataSource @Inject constructor( val dbFaces = samples.map { it.toRoomDb(subject.subjectId) } subjectDao.insertBiometricSamples(dbFaces) } + subject.externalCredentials.takeIf { it.isNotEmpty() }?.let { credentials -> + val dbExternalCredentials = credentials.map { it.toRoomDb() } + subjectDao.insertExternalCredentials(dbExternalCredentials) + } } private suspend fun updateSubject(action: SubjectAction.Update) { diff --git a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RoomExternalCredentialConverter.kt b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RoomExternalCredentialConverter.kt new file mode 100644 index 0000000000..d43b2fe5af --- /dev/null +++ b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RoomExternalCredentialConverter.kt @@ -0,0 +1,22 @@ +package com.simprints.infra.enrolment.records.repository.local.models + +import com.simprints.core.domain.externalcredential.ExternalCredential +import com.simprints.core.domain.externalcredential.ExternalCredentialType +import com.simprints.core.domain.face.FaceSample +import com.simprints.core.domain.fingerprint.FingerprintSample +import com.simprints.infra.enrolment.records.room.store.models.DbBiometricTemplate +import com.simprints.infra.enrolment.records.room.store.models.DbExternalCredential +import com.simprints.infra.enrolment.records.room.store.models.Modality + +internal fun DbExternalCredential.toDomain(): ExternalCredential = ExternalCredential( + value = value, + subjectId = subjectId, + type = ExternalCredentialType.valueOf(type) +) + +internal fun ExternalCredential.toRoomDb(): DbExternalCredential = DbExternalCredential( + value = value, + subjectId = subjectId, + type = type.name +) + diff --git a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RoomSubjectConverter.kt b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RoomSubjectConverter.kt index 385e341830..456b54b7e6 100644 --- a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RoomSubjectConverter.kt +++ b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RoomSubjectConverter.kt @@ -15,6 +15,7 @@ internal fun SubjectBiometrics.toDomain() = Subject( updatedAt = subject.updatedAt?.toDate(), fingerprintSamples = biometricTemplates.filter { it.modality == Modality.FINGERPRINT.id }.map { it.toFingerprintSample() }, faceSamples = biometricTemplates.filter { it.modality == Modality.FACE.id }.map { it.toFaceSample() }, + externalCredentials = externalCredentials.map { it.toDomain() }, ) fun Long.toDate() = Date(this) diff --git a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/SubjectDao.kt b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/SubjectDao.kt index d7836eb8cf..d682a67b2a 100644 --- a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/SubjectDao.kt +++ b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/SubjectDao.kt @@ -8,23 +8,32 @@ import androidx.room.Query import androidx.room.RawQuery import androidx.sqlite.db.SupportSQLiteQuery import com.simprints.infra.enrolment.records.room.store.models.DbBiometricTemplate +import com.simprints.infra.enrolment.records.room.store.models.DbExternalCredential import com.simprints.infra.enrolment.records.room.store.models.DbSubject import com.simprints.infra.enrolment.records.room.store.models.SubjectBiometrics +import com.simprints.infra.enrolment.records.room.store.models.SubjectExternalCredentials @Dao interface SubjectDao { + /*Remaining method*/ @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertSubject(subject: DbSubject) @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertBiometricSamples(samples: List) + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertExternalCredentials(value: List) + @Query("DELETE FROM DbSubject WHERE subjectId = :subjectId") suspend fun deleteSubject(subjectId: String) @Query("DELETE FROM DbBiometricTemplate WHERE uuid = :uuid") suspend fun deleteBiometricSample(uuid: String) + @Query("DELETE FROM DbDbExternalCredential WHERE value = :value") + suspend fun deleteExternalCredential(value: String) + @RawQuery suspend fun deleteSubjects(query: SupportSQLiteQuery): Int @@ -34,6 +43,9 @@ interface SubjectDao { @RawQuery suspend fun loadSubjects(query: SupportSQLiteQuery): List + @Query("SELECT * FROM DbDbExternalCredential") + suspend fun getAllExternalCredentials(): List + @RawQuery suspend fun countSubjects(query: SupportSQLiteQuery): Int diff --git a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/SubjectsDatabase.kt b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/SubjectsDatabase.kt index 16f004f62d..7b4e9f3641 100644 --- a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/SubjectsDatabase.kt +++ b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/SubjectsDatabase.kt @@ -6,6 +6,7 @@ import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase import com.simprints.infra.enrolment.records.room.store.models.DbBiometricTemplate +import com.simprints.infra.enrolment.records.room.store.models.DbExternalCredential import com.simprints.infra.enrolment.records.room.store.models.DbSubject import net.zetetic.database.sqlcipher.SupportOpenHelperFactory import javax.inject.Singleton @@ -15,6 +16,7 @@ import javax.inject.Singleton entities = [ DbSubject::class, DbBiometricTemplate::class, + DbExternalCredential::class, ], version = 1, exportSchema = true, diff --git a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/DbBiometricTemplate.kt b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/DbBiometricTemplate.kt index 7590c579bd..91c0075666 100644 --- a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/DbBiometricTemplate.kt +++ b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/DbBiometricTemplate.kt @@ -39,3 +39,4 @@ data class DbBiometricTemplate( const val FORMAT_COLUMN = "format" } } + diff --git a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/DbExternalCredential.kt b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/DbExternalCredential.kt new file mode 100644 index 0000000000..7468c1dbc2 --- /dev/null +++ b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/DbExternalCredential.kt @@ -0,0 +1,33 @@ +package com.simprints.infra.enrolment.records.room.store.models + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import com.simprints.infra.enrolment.records.room.store.models.DbExternalCredential.Companion.EXTERNAL_CREDENTIAL_TABLE_NAME +import com.simprints.infra.enrolment.records.room.store.models.DbExternalCredential.Companion.EXTERNAL_CREDENTIAL_VALUE_COLUMN +import com.simprints.infra.enrolment.records.room.store.models.DbSubject.Companion.SUBJECT_ID_COLUMN + +@Entity( + tableName = EXTERNAL_CREDENTIAL_TABLE_NAME, + primaryKeys = [EXTERNAL_CREDENTIAL_VALUE_COLUMN, SUBJECT_ID_COLUMN], + foreignKeys = [ + ForeignKey( + entity = DbSubject::class, + parentColumns = [SUBJECT_ID_COLUMN], + childColumns = [SUBJECT_ID_COLUMN], + onDelete = ForeignKey.CASCADE, + ) + ] +) +data class DbExternalCredential( + @ColumnInfo(name = EXTERNAL_CREDENTIAL_VALUE_COLUMN) + val value: String, + @ColumnInfo(name = SUBJECT_ID_COLUMN) + val subjectId: String, + val type: String, +) { + companion object { + const val EXTERNAL_CREDENTIAL_VALUE_COLUMN = "value" + const val EXTERNAL_CREDENTIAL_TABLE_NAME = "DbDbExternalCredential" + } +} diff --git a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/SubjectBiometrics.kt b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/SubjectBiometrics.kt index 1ea2f722d2..d267894645 100644 --- a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/SubjectBiometrics.kt +++ b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/SubjectBiometrics.kt @@ -11,4 +11,10 @@ data class SubjectBiometrics( entityColumn = SUBJECT_ID_COLUMN, ) val biometricTemplates: List, + @Relation( + parentColumn = SUBJECT_ID_COLUMN, + entityColumn = SUBJECT_ID_COLUMN, + ) + /** New field */ + val externalCredentials: List, ) diff --git a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/SubjectExternalCredentials.kt b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/SubjectExternalCredentials.kt new file mode 100644 index 0000000000..f710dfa45a --- /dev/null +++ b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/SubjectExternalCredentials.kt @@ -0,0 +1,14 @@ +package com.simprints.infra.enrolment.records.room.store.models + +import androidx.room.Embedded +import androidx.room.Relation +import com.simprints.infra.enrolment.records.room.store.models.DbSubject.Companion.SUBJECT_ID_COLUMN + +data class SubjectExternalCredentials( + @Embedded val subject: DbSubject, + @Relation( + parentColumn = SUBJECT_ID_COLUMN, + entityColumn = SUBJECT_ID_COLUMN, + ) + val externalCredentials: List, +) 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 94f749f810..3332ea9773 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,6 +4,7 @@ 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 @@ -16,6 +17,7 @@ internal data class ApiEnrolmentRecordCreationPayload( val moduleId: String, val attendantId: String, val biometricReferences: List?, + val externalCredential: ApiExternalCredential?, ) : ApiEnrolmentRecordEventPayload(ApiEnrolmentRecordPayloadType.EnrolmentRecordCreation) internal fun ApiEnrolmentRecordCreationPayload.fromApiToDomain() = EnrolmentRecordCreationEvent.EnrolmentRecordCreationPayload( 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 57014fc4ef..414c9ab056 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,6 +1,7 @@ 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 @@ -10,10 +11,13 @@ internal data class ApiEnrolmentRecordUpdatePayload( val subjectId: String, val biometricReferencesAdded: List?, val biometricReferencesRemoved: List?, + val externalCredentialAdded: ApiExternalCredential?, ) : ApiEnrolmentRecordEventPayload(ApiEnrolmentRecordPayloadType.EnrolmentRecordUpdate) internal fun ApiEnrolmentRecordUpdatePayload.fromApiToDomain() = EnrolmentRecordUpdateEvent.EnrolmentRecordUpdatePayload( subjectId, biometricReferencesAdded?.map { it.fromApiToDomain() }.orEmpty(), biometricReferencesRemoved.orEmpty(), + externalCredentialAdded?.fromApiToDomain(subjectId), ) + 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 new file mode 100644 index 0000000000..e1890996e3 --- /dev/null +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiExternalCredential.kt @@ -0,0 +1,18 @@ +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 + +data class ApiExternalCredential( + val id: String, + val type: String, + val value: String, +) + + +internal fun ApiExternalCredential.fromApiToDomain(subjectId: String) = ExternalCredential( + value = value.asTokenizableEncrypted(), + subjectId = subjectId, + type = ExternalCredentialType.valueOf(type) +) diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordCreationEvent.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordCreationEvent.kt index e583230b9b..d9375fd3a1 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordCreationEvent.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordCreationEvent.kt @@ -1,6 +1,7 @@ package com.simprints.infra.events.event.domain.models.subject import androidx.annotation.Keep +import com.simprints.core.domain.externalcredential.ExternalCredential import com.simprints.core.domain.face.FaceSample import com.simprints.core.domain.fingerprint.FingerprintSample import com.simprints.core.domain.tokenization.TokenizableString @@ -36,6 +37,7 @@ data class EnrolmentRecordCreationEvent( val moduleId: TokenizableString, val attendantId: TokenizableString, val biometricReferences: List, + val externalCredential: ExternalCredential? ) companion object { 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 4ca63c97f8..cd50c3ffd0 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 @@ -1,6 +1,7 @@ package com.simprints.infra.events.event.domain.models.subject import androidx.annotation.Keep +import com.simprints.core.domain.externalcredential.ExternalCredential import java.util.UUID @Keep @@ -12,12 +13,14 @@ data class EnrolmentRecordUpdateEvent( subjectId: String, biometricReferencesAdded: List, biometricReferencesRemoved: List, + externalCredentialAdded: ExternalCredential?, ) : this( UUID.randomUUID().toString(), EnrolmentRecordUpdatePayload( - subjectId, - biometricReferencesAdded, - biometricReferencesRemoved, + subjectId = subjectId, + biometricReferencesAdded = biometricReferencesAdded, + biometricReferencesRemoved = biometricReferencesRemoved, + externalCredentialAdded = externalCredentialAdded, ), ) @@ -26,5 +29,6 @@ data class EnrolmentRecordUpdateEvent( val subjectId: String, val biometricReferencesAdded: List, val biometricReferencesRemoved: List, + val externalCredentialAdded: ExternalCredential?, ) } From 9504059324b191098019a189b2147d8b04a5aa5b Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 4 Aug 2025 14:06:05 +0300 Subject: [PATCH 002/139] =?UTF-8?q?[WIP]=20Adding=20external=20credentials?= =?UTF-8?q?=20to=20the=20Realm=20DB=CB=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../store/models/DbExternalCredential.kt | 21 +++++++++++++++++++ .../records/realm/store/models/DbSubject.kt | 1 + .../RealmEnrolmentRecordLocalDataSource.kt | 16 ++++++++++++-- .../models/RealmFingerprintSampleConverter.kt | 8 +++++++ 4 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 infra/enrolment-records/realm-store/src/main/java/com/simprints/infra/enrolment/records/realm/store/models/DbExternalCredential.kt diff --git a/infra/enrolment-records/realm-store/src/main/java/com/simprints/infra/enrolment/records/realm/store/models/DbExternalCredential.kt b/infra/enrolment-records/realm-store/src/main/java/com/simprints/infra/enrolment/records/realm/store/models/DbExternalCredential.kt new file mode 100644 index 0000000000..d076cca478 --- /dev/null +++ b/infra/enrolment-records/realm-store/src/main/java/com/simprints/infra/enrolment/records/realm/store/models/DbExternalCredential.kt @@ -0,0 +1,21 @@ +package com.simprints.infra.enrolment.records.realm.store.models + +import androidx.annotation.Keep +import com.simprints.core.ExcludedFromGeneratedTestCoverageReports +import io.realm.kotlin.types.RealmObject +import io.realm.kotlin.types.annotations.PrimaryKey + +@Keep +@ExcludedFromGeneratedTestCoverageReports("Data model definition for Realm table") +class DbExternalCredential : RealmObject { + @PrimaryKey + var id: String = "" + get() = "$value$SEPARATOR$subjectId" + var value: String = "" + var subjectId: String = "" + var type: String = "" + + companion object { + const val SEPARATOR = "|" + } +} diff --git a/infra/enrolment-records/realm-store/src/main/java/com/simprints/infra/enrolment/records/realm/store/models/DbSubject.kt b/infra/enrolment-records/realm-store/src/main/java/com/simprints/infra/enrolment/records/realm/store/models/DbSubject.kt index 612e8bb8a7..4824b59a16 100644 --- a/infra/enrolment-records/realm-store/src/main/java/com/simprints/infra/enrolment/records/realm/store/models/DbSubject.kt +++ b/infra/enrolment-records/realm-store/src/main/java/com/simprints/infra/enrolment/records/realm/store/models/DbSubject.kt @@ -22,6 +22,7 @@ class DbSubject : RealmObject { var fingerprintSamples: RealmList = realmListOf() var faceSamples: RealmList = realmListOf() + var externalCredentials: RealmList = realmListOf() var isAttendantIdTokenized: Boolean = false var isModuleIdTokenized: Boolean = false 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 b3b2e3037d..1fe1969bc6 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 @@ -211,6 +211,12 @@ internal class RealmEnrolmentRecordLocalDataSource @Inject constructor( .filterNot { it.id in faceSampleIds } .takeIf { it.isNotEmpty() } ?.forEach { realm.delete(it) } + + val externalCredentialIds = newSubject.externalCredentials.map { it.id }.toSet() + dbSubject.externalCredentials + .filterNot { it.id in externalCredentialIds } + .takeIf { it.isNotEmpty() } + ?.forEach { realm.delete(it) } } realm.copyToRealm(newSubject, updatePolicy = UpdatePolicy.ALL) @@ -220,16 +226,22 @@ internal class RealmEnrolmentRecordLocalDataSource @Inject constructor( val dbSubject: DbSubject? = realm.findSubject(RealmUUID.from(action.subjectId)) if (dbSubject != null) { val referencesToDelete = action.referenceIdsToRemove.toSet() // to make lookup O(1) + val externalCredentialsToAdd = action.externalCredentialsToAdd.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 externalCredentialsMap = +// dbSubject.externalCredentials.groupBy { it.value in externalCredentialsToAdd.map { it.value.value } } + + val allExternalCredentials = (dbSubject.externalCredentials + action.externalCredentialsToAdd.map { it.toRealmDb() }).distinctBy { it.id }.toSet() // 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) } fingerprintSamplesMap[true]?.forEach { realm.delete(it) } diff --git a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RealmFingerprintSampleConverter.kt b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RealmFingerprintSampleConverter.kt index 6b2be097ba..ebfab4e594 100644 --- a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RealmFingerprintSampleConverter.kt +++ b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RealmFingerprintSampleConverter.kt @@ -1,7 +1,9 @@ package com.simprints.infra.enrolment.records.repository.local.models +import com.simprints.core.domain.externalcredential.ExternalCredential import com.simprints.core.domain.fingerprint.FingerprintSample import com.simprints.infra.enrolment.records.realm.store.models.DbFingerprintSample as RealmFingerprintSample +import com.simprints.infra.enrolment.records.realm.store.models.DbExternalCredential as RealmExternalCredential internal fun RealmFingerprintSample.toDomain(): FingerprintSample = FingerprintSample( id = id, @@ -18,3 +20,9 @@ internal fun FingerprintSample.toRealmDb(): RealmFingerprintSample = RealmFinger sample.template = template sample.format = format } + +internal fun ExternalCredential.toRealmDb(): RealmExternalCredential = RealmExternalCredential().also { sample -> + sample.value = value.value + sample.subjectId = subjectId + sample.type = type.toString() +} From f279637b81a7dfb431271e0c1f2a321b0c1b76aa Mon Sep 17 00:00:00 2001 From: alex Date: Sun, 10 Aug 2025 17:37:12 +0300 Subject: [PATCH 003/139] =?UTF-8?q?[CORE-3421]=20Adding=20external=20crede?= =?UTF-8?q?ntial=20support=20on=20Domain,=20DB=20(Realm,=20Room)=20and=20A?= =?UTF-8?q?PI=20levels=20=CB=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...EnrolmentCreationEventForSubjectUseCase.kt | 12 +++++---- .../store/local/ConfigLocalDataSourceImpl.kt | 1 + .../migrations/models/OldProjectConfig.kt | 1 + .../local/models/ExternalCredentialType.kt | 18 +++++++++++++ .../models/MutliFactorIdConfiguration.kt | 12 +++++++++ .../local/models/ProjectConfiguration.kt | 22 ++++++++------- ...ation.kt => MultiFactorIdConfiguration.kt} | 2 +- .../store/models/ProjectConfiguration.kt | 2 +- .../models/ApiMultiFactorIdConfiguration.kt | 25 +++++++++++++++++ .../remote/models/ApiProjectConfiguration.kt | 23 ++++++++-------- .../models/ApiSearchAndVerifyConfiguration.kt | 27 ------------------- .../src/main/proto/project_config.proto | 12 +++++++++ .../records/realm/store/config/RealmConfig.kt | 4 ++- .../RealmEnrolmentRecordLocalDataSource.kt | 4 --- .../RoomEnrolmentRecordLocalDataSource.kt | 3 +++ .../models/RoomExternalCredentialConverter.kt | 9 +++---- .../1.json | 2 +- .../remote/models/ApiEnrolmentPayloadV2.kt | 1 + .../remote/models/ApiEnrolmentPayloadV4.kt | 1 + .../models/callout/ApiCalloutPayloadV2.kt | 1 + .../models/callout/ApiCalloutPayloadV3.kt | 1 + .../ApiEnrolmentRecordCreationPayload.kt | 11 ++++---- .../sync/down/tasks/EventDownSyncTask.kt | 1 + ...nrolmentRecordCreationEventDeserializer.kt | 12 +++++---- .../subject/EnrolmentRecordCreationEvent.kt | 12 +++++---- 25 files changed, 137 insertions(+), 82 deletions(-) create mode 100644 infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/ExternalCredentialType.kt create mode 100644 infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/MutliFactorIdConfiguration.kt rename infra/config-store/src/main/java/com/simprints/infra/config/store/models/{SearchAndVerifyConfiguration.kt => MultiFactorIdConfiguration.kt} (82%) create mode 100644 infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiMultiFactorIdConfiguration.kt delete mode 100644 infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiSearchAndVerifyConfiguration.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 6661aa5441..40359a9ac5 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 @@ -47,11 +47,13 @@ internal class GetEnrolmentCreationEventForSubjectUseCase @Inject constructor( } private fun Subject.fromSubjectToEnrolmentCreationEvent() = EnrolmentRecordCreationEvent( - subjectId, - projectId, - moduleId, - attendantId, - EnrolmentRecordCreationEvent.buildBiometricReferences(fingerprintSamples, faceSamples, encoder), + subjectId = subjectId, + projectId = projectId, + 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 + externalCredential = externalCredentials.firstOrNull() ) companion object { diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/ConfigLocalDataSourceImpl.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/ConfigLocalDataSourceImpl.kt index 633ab8e1f3..69ebd618fa 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/ConfigLocalDataSourceImpl.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/ConfigLocalDataSourceImpl.kt @@ -238,6 +238,7 @@ internal class ConfigLocalDataSourceImpl @Inject constructor( ), ), custom = null, + multifactorId = null ).toProto() val defaultDeviceConfiguration: ProtoDeviceConfiguration = DeviceConfiguration( language = "", diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/migrations/models/OldProjectConfig.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/migrations/models/OldProjectConfig.kt index 75352715fc..38d5957162 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/migrations/models/OldProjectConfig.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/migrations/models/OldProjectConfig.kt @@ -75,6 +75,7 @@ internal data class OldProjectConfig( consent = consentConfiguration(), identification = identificationConfiguration(), synchronization = synchronizationConfiguration(), + multifactorId = null, custom = null, ) diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/ExternalCredentialType.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/ExternalCredentialType.kt new file mode 100644 index 0000000000..c2f20a6014 --- /dev/null +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/ExternalCredentialType.kt @@ -0,0 +1,18 @@ +package com.simprints.infra.config.store.local.models + +import com.simprints.core.domain.externalcredential.ExternalCredentialType +import com.simprints.infra.config.store.exceptions.InvalidProtobufEnumException + +internal fun ExternalCredentialType.toProto(): ProtoExternalCredentialType = when(this){ + ExternalCredentialType.NHISCard -> ProtoExternalCredentialType.NHIS_CARD + ExternalCredentialType.GhanaIdCard -> ProtoExternalCredentialType.GHANA_ID_CARD + ExternalCredentialType.QRCode -> ProtoExternalCredentialType.QR_CODE +} + +internal fun ProtoExternalCredentialType.toDomain(): ExternalCredentialType = when(this){ + ProtoExternalCredentialType.UNRECOGNIZED, + ProtoExternalCredentialType.EXTERNAL_CREDENTIAL_TYPE_UNSPECIFIED -> throw InvalidProtobufEnumException("invalid External credential $name") + ProtoExternalCredentialType.NHIS_CARD -> ExternalCredentialType.NHISCard + ProtoExternalCredentialType.GHANA_ID_CARD -> ExternalCredentialType.GhanaIdCard + ProtoExternalCredentialType.QR_CODE -> ExternalCredentialType.QRCode +} diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/MutliFactorIdConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/MutliFactorIdConfiguration.kt new file mode 100644 index 0000000000..100fa4afe0 --- /dev/null +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/MutliFactorIdConfiguration.kt @@ -0,0 +1,12 @@ +package com.simprints.infra.config.store.local.models + +import com.simprints.infra.config.store.models.MultiFactorIdConfiguration + +internal fun MultiFactorIdConfiguration.toProto(): ProtoMultiFactorIdConfiguration = ProtoMultiFactorIdConfiguration + .newBuilder() + .addAllAllowedExternalCredentials(allowedExternalCredentials.map { it.toProto() }) + .build() + +internal fun ProtoMultiFactorIdConfiguration.toDomain(): MultiFactorIdConfiguration = MultiFactorIdConfiguration( + allowedExternalCredentials = allowedExternalCredentialsList.map { it.toDomain() } +) diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/ProjectConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/ProjectConfiguration.kt index 9579957ae7..93acadfa1a 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/ProjectConfiguration.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/ProjectConfiguration.kt @@ -15,6 +15,7 @@ internal fun ProjectConfiguration.toProto(): ProtoProjectConfiguration = ProtoPr .also { if (face != null) it.face = face.toProto() if (fingerprint != null) it.fingerprint = fingerprint.toProto() + if (multifactorId != null) it.multiFactorId = multifactorId.toProto() }.also { if (custom != null) { try { @@ -28,16 +29,17 @@ internal fun ProjectConfiguration.toProto(): ProtoProjectConfiguration = ProtoPr }.build() internal fun ProtoProjectConfiguration.toDomain(): ProjectConfiguration = ProjectConfiguration( - id, - projectId, - updatedAt, - general.toDomain(), - hasFace().let { if (it) face.toDomain() else null }, - hasFingerprint().let { if (it) fingerprint.toDomain() else null }, - consent.toDomain(), - identification.toDomain(), - synchronization.toDomain(), - customJson?.takeIf { it.isNotBlank() }?.let { + id = id, + projectId = projectId, + updatedAt = updatedAt, + general = general.toDomain(), + face = hasFace().let { if (it) face.toDomain() else null }, + fingerprint = hasFingerprint().let { if (it) fingerprint.toDomain() else null }, + consent = consent.toDomain(), + identification = identification.toDomain(), + synchronization = synchronization.toDomain(), + multifactorId = multiFactorId?.toDomain(), + custom = customJson?.takeIf { it.isNotBlank() }?.let { try { JsonHelper.fromJson(it) } catch (_: Exception) { diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/SearchAndVerifyConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/MultiFactorIdConfiguration.kt similarity index 82% rename from infra/config-store/src/main/java/com/simprints/infra/config/store/models/SearchAndVerifyConfiguration.kt rename to infra/config-store/src/main/java/com/simprints/infra/config/store/models/MultiFactorIdConfiguration.kt index ee29b0528f..f315afa5f9 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/SearchAndVerifyConfiguration.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/MultiFactorIdConfiguration.kt @@ -2,6 +2,6 @@ package com.simprints.infra.config.store.models import com.simprints.core.domain.externalcredential.ExternalCredentialType -data class SearchAndVerifyConfiguration( +data class MultiFactorIdConfiguration( val allowedExternalCredentials: List ) diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/ProjectConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/ProjectConfiguration.kt index 39ea770b3f..88af76ad8b 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/ProjectConfiguration.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/ProjectConfiguration.kt @@ -10,7 +10,7 @@ data class ProjectConfiguration( val consent: ConsentConfiguration, val identification: IdentificationConfiguration, val synchronization: SynchronizationConfiguration, - val searchAndVerify: SearchAndVerifyConfiguration?, + val multifactorId: MultiFactorIdConfiguration?, val custom: Map?, ) 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 new file mode 100644 index 0000000000..47a1501851 --- /dev/null +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiMultiFactorIdConfiguration.kt @@ -0,0 +1,25 @@ +package com.simprints.infra.config.store.remote.models + +import androidx.annotation.Keep +import com.simprints.core.domain.externalcredential.ExternalCredentialType +import com.simprints.infra.config.store.models.MultiFactorIdConfiguration + +@Keep +internal data class ApiMultiFactorIdConfiguration( + val allowedExternalCredentials: List +) { + fun toDomain(): MultiFactorIdConfiguration = MultiFactorIdConfiguration( + allowedExternalCredentials = allowedExternalCredentials.map { it.toDomain() } + ) +} + +@Keep +enum class ApiExternalCredentialType { + NHISCard, GhanaIdCard, QRCode; + + fun toDomain(): ExternalCredentialType = when (this) { + NHISCard -> ExternalCredentialType.NHISCard + GhanaIdCard -> ExternalCredentialType.GhanaIdCard + QRCode -> ExternalCredentialType.QRCode + } +} diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiProjectConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiProjectConfiguration.kt index c782b25f79..696d4444fb 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiProjectConfiguration.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiProjectConfiguration.kt @@ -14,19 +14,20 @@ internal data class ApiProjectConfiguration( val consent: ApiConsentConfiguration, val identification: ApiIdentificationConfiguration, val synchronization: ApiSynchronizationConfiguration, - val searchAndVerify: ApiSearchAndVerifyConfiguration?, + val multiFactorId: ApiMultiFactorIdConfiguration?, val custom: Map?, ) { fun toDomain(): ProjectConfiguration = ProjectConfiguration( - id, - projectId, - updatedAt, - general.toDomain(), - face?.toDomain(), - fingerprint?.toDomain(), - consent.toDomain(), - identification.toDomain(), - synchronization.toDomain(), - custom, + id = id, + projectId = projectId, + updatedAt = updatedAt, + general = general.toDomain(), + face = face?.toDomain(), + fingerprint = fingerprint?.toDomain(), + consent = consent.toDomain(), + identification = identification.toDomain(), + synchronization = synchronization.toDomain(), + multifactorId = multiFactorId?.toDomain(), + custom = custom, ) } diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiSearchAndVerifyConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiSearchAndVerifyConfiguration.kt deleted file mode 100644 index 8b3eb3ca29..0000000000 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiSearchAndVerifyConfiguration.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.simprints.infra.config.store.remote.models - -import androidx.annotation.Keep -import com.simprints.core.domain.tokenization.asTokenizableEncrypted -import com.simprints.infra.config.store.models.DownSynchronizationConfiguration -import com.simprints.infra.config.store.models.DownSynchronizationConfiguration.Companion.DEFAULT_DOWN_SYNC_MAX_AGE -import com.simprints.infra.config.store.models.Frequency -import com.simprints.infra.config.store.models.SampleSynchronizationConfiguration -import com.simprints.infra.config.store.models.SynchronizationConfiguration -import com.simprints.infra.config.store.models.UpSynchronizationConfiguration - -@Keep -internal data class ApiSearchAndVerifyConfiguration( - val allowedExternalCredentials: List -) - -enum class ApiExternalCredentialType { - NHISCard, GhanaIdCard, QRCode -} - -fun mapToString(a: ApiExternalCredentialType) { - return when(a) - { - ApiExternalCredentialType.NHISCard -> TODO() - ApiExternalCredentialType.GhanaIdCard -> TODO() - ApiExternalCredentialType.QRCode -> TODO() - }} diff --git a/infra/config-store/src/main/proto/project_config.proto b/infra/config-store/src/main/proto/project_config.proto index b03ea409ce..69615d2745 100644 --- a/infra/config-store/src/main/proto/project_config.proto +++ b/infra/config-store/src/main/proto/project_config.proto @@ -14,6 +14,18 @@ message ProtoProjectConfiguration { string updated_at = 8; optional string customJson = 9; string id = 10; + optional ProtoMultiFactorIdConfiguration multiFactorId = 11; +} + +enum ProtoExternalCredentialType { + EXTERNAL_CREDENTIAL_TYPE_UNSPECIFIED = 0; + NHIS_CARD = 1; + GHANA_ID_CARD = 2; + QR_CODE = 3; +} + +message ProtoMultiFactorIdConfiguration { + repeated ProtoExternalCredentialType allowed_external_credentials = 1; } message ProtoGeneralConfiguration { diff --git a/infra/enrolment-records/realm-store/src/main/java/com/simprints/infra/enrolment/records/realm/store/config/RealmConfig.kt b/infra/enrolment-records/realm-store/src/main/java/com/simprints/infra/enrolment/records/realm/store/config/RealmConfig.kt index 5a967181e1..ff6e0b7881 100644 --- a/infra/enrolment-records/realm-store/src/main/java/com/simprints/infra/enrolment/records/realm/store/config/RealmConfig.kt +++ b/infra/enrolment-records/realm-store/src/main/java/com/simprints/infra/enrolment/records/realm/store/config/RealmConfig.kt @@ -4,6 +4,7 @@ import androidx.annotation.Keep import com.simprints.core.ExcludedFromGeneratedTestCoverageReports import com.simprints.infra.enrolment.records.realm.store.BuildConfig import com.simprints.infra.enrolment.records.realm.store.migration.RealmMigrations +import com.simprints.infra.enrolment.records.realm.store.models.DbExternalCredential import com.simprints.infra.enrolment.records.realm.store.models.DbFaceSample import com.simprints.infra.enrolment.records.realm.store.models.DbFingerprintSample import com.simprints.infra.enrolment.records.realm.store.models.DbProject @@ -26,6 +27,7 @@ class RealmConfig @Inject constructor() { DbFaceSample::class, DbSubject::class, DbProject::class, + DbExternalCredential::class, ), ).name("$databaseName.realm") .schemaVersion(REALM_SCHEMA_VERSION) @@ -36,6 +38,6 @@ class RealmConfig @Inject constructor() { .build() companion object { - private const val REALM_SCHEMA_VERSION: Long = 17 + private const val REALM_SCHEMA_VERSION: Long = 18 } } 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 1fe1969bc6..1b751691a4 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 @@ -226,12 +226,8 @@ internal class RealmEnrolmentRecordLocalDataSource @Inject constructor( val dbSubject: DbSubject? = realm.findSubject(RealmUUID.from(action.subjectId)) if (dbSubject != null) { val referencesToDelete = action.referenceIdsToRemove.toSet() // to make lookup O(1) - val externalCredentialsToAdd = action.externalCredentialsToAdd.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 externalCredentialsMap = -// dbSubject.externalCredentials.groupBy { it.value in externalCredentialsToAdd.map { it.value.value } } - val allExternalCredentials = (dbSubject.externalCredentials + action.externalCredentialsToAdd.map { it.toRealmDb() }).distinctBy { it.id }.toSet() // Append new samples to the list of samples that remain after removing 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 505d9d323f..bf76f891a5 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 @@ -318,6 +318,9 @@ internal class RoomEnrolmentRecordLocalDataSource @Inject constructor( if (templatesToAdd.isNotEmpty()) { subjectDao.insertBiometricSamples(templatesToAdd) } + if (action.externalCredentialsToAdd.isNotEmpty()) { + subjectDao.insertExternalCredentials(action.externalCredentialsToAdd.map { it.toRoomDb() }) + } } else { Simber.e( "[updateSubject] Subject ${action.subjectId} not found for update", diff --git a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RoomExternalCredentialConverter.kt b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RoomExternalCredentialConverter.kt index d43b2fe5af..9a1822ffaa 100644 --- a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RoomExternalCredentialConverter.kt +++ b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RoomExternalCredentialConverter.kt @@ -2,20 +2,17 @@ package com.simprints.infra.enrolment.records.repository.local.models import com.simprints.core.domain.externalcredential.ExternalCredential import com.simprints.core.domain.externalcredential.ExternalCredentialType -import com.simprints.core.domain.face.FaceSample -import com.simprints.core.domain.fingerprint.FingerprintSample -import com.simprints.infra.enrolment.records.room.store.models.DbBiometricTemplate +import com.simprints.core.domain.tokenization.asTokenizableEncrypted import com.simprints.infra.enrolment.records.room.store.models.DbExternalCredential -import com.simprints.infra.enrolment.records.room.store.models.Modality internal fun DbExternalCredential.toDomain(): ExternalCredential = ExternalCredential( - value = value, + value = value.asTokenizableEncrypted(), subjectId = subjectId, type = ExternalCredentialType.valueOf(type) ) internal fun ExternalCredential.toRoomDb(): DbExternalCredential = DbExternalCredential( - value = value, + value = value.value, subjectId = subjectId, type = type.name ) diff --git a/infra/enrolment-records/room-store/schemas/com.simprints.infra.enrolment.records.room.store.SubjectsDatabase/1.json b/infra/enrolment-records/room-store/schemas/com.simprints.infra.enrolment.records.room.store.SubjectsDatabase/1.json index bbe29315e1..58b0f3827c 100644 --- a/infra/enrolment-records/room-store/schemas/com.simprints.infra.enrolment.records.room.store.SubjectsDatabase/1.json +++ b/infra/enrolment-records/room-store/schemas/com.simprints.infra.enrolment.records.room.store.SubjectsDatabase/1.json @@ -177,4 +177,4 @@ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '94bee827928a2618c6873579bc6bc63a')" ] } -} \ No newline at end of file +} diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEnrolmentPayloadV2.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEnrolmentPayloadV2.kt index 92fd4a0598..f18557c181 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEnrolmentPayloadV2.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEnrolmentPayloadV2.kt @@ -25,6 +25,7 @@ internal data class ApiEnrolmentPayloadV2( override fun getTokenizedFieldJsonPath(tokenKeyType: TokenKeyType): String? = when (tokenKeyType) { TokenKeyType.AttendantId -> "attendantId" TokenKeyType.ModuleId -> "moduleId" + TokenKeyType.ExternalCredential -> "externalCredential" TokenKeyType.Unknown -> null } } 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 519e6ed6df..1cff72ca80 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 @@ -25,6 +25,7 @@ internal data class ApiEnrolmentPayloadV4( override fun getTokenizedFieldJsonPath(tokenKeyType: TokenKeyType): String? = when (tokenKeyType) { TokenKeyType.AttendantId -> "attendantId" TokenKeyType.ModuleId -> "moduleId" + TokenKeyType.ExternalCredential -> "externalCredential" TokenKeyType.Unknown -> null } } diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/callout/ApiCalloutPayloadV2.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/callout/ApiCalloutPayloadV2.kt index 0475985a95..58bb4a4553 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/callout/ApiCalloutPayloadV2.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/callout/ApiCalloutPayloadV2.kt @@ -73,6 +73,7 @@ internal data class ApiCalloutPayloadV2( override fun getTokenizedFieldJsonPath(tokenKeyType: TokenKeyType): String? = when (tokenKeyType) { TokenKeyType.AttendantId -> "callout.userId" TokenKeyType.ModuleId -> "callout.moduleId" + TokenKeyType.ExternalCredential -> "callout.externalCredential" TokenKeyType.Unknown -> null } } diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/callout/ApiCalloutPayloadV3.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/callout/ApiCalloutPayloadV3.kt index ab9f1a7cae..d703c11275 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/callout/ApiCalloutPayloadV3.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/callout/ApiCalloutPayloadV3.kt @@ -76,6 +76,7 @@ internal data class ApiCalloutPayloadV3( override fun getTokenizedFieldJsonPath(tokenKeyType: TokenKeyType): String? = when (tokenKeyType) { TokenKeyType.AttendantId -> "callout.userId" TokenKeyType.ModuleId -> "callout.moduleId" + TokenKeyType.ExternalCredential -> "callout.externalCredential" TokenKeyType.Unknown -> null } } 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 3332ea9773..774d68c9d3 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 @@ -21,9 +21,10 @@ internal data class ApiEnrolmentRecordCreationPayload( ) : ApiEnrolmentRecordEventPayload(ApiEnrolmentRecordPayloadType.EnrolmentRecordCreation) internal fun ApiEnrolmentRecordCreationPayload.fromApiToDomain() = EnrolmentRecordCreationEvent.EnrolmentRecordCreationPayload( - subjectId, - projectId, - moduleId.asTokenizableEncrypted(), - attendantId.asTokenizableEncrypted(), - biometricReferences?.map { it.fromApiToDomain() } ?: emptyList(), + subjectId = subjectId, + projectId = projectId, + moduleId = moduleId.asTokenizableEncrypted(), + attendantId = attendantId.asTokenizableEncrypted(), + biometricReferences = biometricReferences?.map { it.fromApiToDomain() } ?: emptyList(), + externalCredential = externalCredential?.fromApiToDomain(subjectId) ) diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/tasks/EventDownSyncTask.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/tasks/EventDownSyncTask.kt index 1539651746..f3e09e0c18 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/tasks/EventDownSyncTask.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/tasks/EventDownSyncTask.kt @@ -266,6 +266,7 @@ internal class EventDownSyncTask @Inject constructor( faceSamplesToAdd = subjectFactory.extractFaceSamplesFromBiometricReferences(biometricReferencesAdded), fingerprintSamplesToAdd = subjectFactory.extractFingerprintSamplesFromBiometricReferences(biometricReferencesAdded), referenceIdsToRemove = biometricReferencesRemoved, + externalCredentialsToAdd = externalCredentialAdded?.let { listOf(it) } ?: emptyList() ), ) } diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/cosync/CoSyncEnrolmentRecordCreationEventDeserializer.kt b/infra/events/src/main/java/com/simprints/infra/events/event/cosync/CoSyncEnrolmentRecordCreationEventDeserializer.kt index e3fc850615..6f26ffe9ca 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/cosync/CoSyncEnrolmentRecordCreationEventDeserializer.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/cosync/CoSyncEnrolmentRecordCreationEventDeserializer.kt @@ -50,11 +50,13 @@ class CoSyncEnrolmentRecordCreationEventDeserializer : return EnrolmentRecordCreationEvent( id, EnrolmentRecordCreationEvent.EnrolmentRecordCreationPayload( - subjectId, - projectId, - moduleId, - attendantId, - biometricReferences, + subjectId = subjectId, + projectId = projectId, + moduleId = moduleId, + attendantId = attendantId, + biometricReferences = biometricReferences, + // TODO [CORE-3421] Update when CoSync supports external credentials (MfID) + externalCredential = null ), ) } diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordCreationEvent.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordCreationEvent.kt index d9375fd3a1..9a89986e15 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordCreationEvent.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordCreationEvent.kt @@ -19,14 +19,16 @@ data class EnrolmentRecordCreationEvent( moduleId: TokenizableString, attendantId: TokenizableString, biometricReferences: List, + externalCredential: ExternalCredential?, ) : this( UUID.randomUUID().toString(), EnrolmentRecordCreationPayload( - subjectId, - projectId, - moduleId, - attendantId, - biometricReferences, + subjectId = subjectId, + projectId = projectId, + moduleId = moduleId, + attendantId = attendantId, + biometricReferences = biometricReferences, + externalCredential = externalCredential, ), ) From d61fb62f0c8dd0e98c1bee23e7203bc951fe3bee Mon Sep 17 00:00:00 2001 From: alex Date: Sun, 10 Aug 2025 18:18:00 +0300 Subject: [PATCH 004/139] =?UTF-8?q?[CORE-3421]=20Fixing=20tests=CB=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...RealmEnrolmentRecordLocalDataSourceTest.kt | 1 + .../RoomEnrolmentRecordLocalDataSourceTest.kt | 8 +++ .../1.json | 50 +++++++++++++++++-- .../models/ApiEnrolmentPayloadV2Test.kt | 3 +- .../ApiEnrolmentRecordCreationEventTest.kt | 32 ++++++++---- .../ApiEnrolmentRecordUpdateEventTest.kt | 25 +++++++--- .../sync/down/tasks/EventDownSyncTaskTest.kt | 29 ++++++++--- .../sync/down/tasks/SubjectFactoryTest.kt | 12 +++++ 8 files changed, 132 insertions(+), 28 deletions(-) diff --git a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/RealmEnrolmentRecordLocalDataSourceTest.kt b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/RealmEnrolmentRecordLocalDataSourceTest.kt index 8508074a68..f16a5fbe70 100644 --- a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/RealmEnrolmentRecordLocalDataSourceTest.kt +++ b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/RealmEnrolmentRecordLocalDataSourceTest.kt @@ -316,6 +316,7 @@ class RealmEnrolmentRecordLocalDataSourceTest { faceSamplesToAdd = listOf(getRandomFaceSample()), fingerprintSamplesToAdd = listOf(getRandomFingerprintSample()), referenceIdsToRemove = listOf(faceReferenceId, fingerReferenceId), + externalCredentialsToAdd = listOf(), ), ), project, diff --git a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/RoomEnrolmentRecordLocalDataSourceTest.kt b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/RoomEnrolmentRecordLocalDataSourceTest.kt index c916779534..d1c1644f7e 100644 --- a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/RoomEnrolmentRecordLocalDataSourceTest.kt +++ b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/RoomEnrolmentRecordLocalDataSourceTest.kt @@ -334,6 +334,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { faceSamplesToAdd = listOf(faceSample2), fingerprintSamplesToAdd = listOf(fingerprintSample1), referenceIdsToRemove = listOf(), + externalCredentialsToAdd = listOf(), ) // When @@ -365,6 +366,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { faceSamplesToAdd = listOf(), // Explicitly empty as in original fingerprintSamplesToAdd = listOf(), // Explicitly empty as in original referenceIdsToRemove = listOf(faceSample2.referenceId), + externalCredentialsToAdd = listOf(), ) // When @@ -394,6 +396,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { faceSamplesToAdd = listOf(), fingerprintSamplesToAdd = listOf(), referenceIdsToRemove = listOf(fingerprintSample2.referenceId), + externalCredentialsToAdd = listOf(), ) // When @@ -422,6 +425,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { faceSamplesToAdd = listOf(), fingerprintSamplesToAdd = listOf(), referenceIdsToRemove = listOf(faceSample1.referenceId), + externalCredentialsToAdd = listOf(), ) // When @@ -445,6 +449,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { faceSamplesToAdd = listOf(), fingerprintSamplesToAdd = listOf(), referenceIdsToRemove = listOf(fingerprintSample1.referenceId), + externalCredentialsToAdd = listOf(), ) // When @@ -464,6 +469,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { faceSamplesToAdd = listOf(faceSample1, faceSample2), fingerprintSamplesToAdd = listOf(fingerprintSample1), referenceIdsToRemove = listOf(), + externalCredentialsToAdd = listOf(), ) // When @@ -493,6 +499,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { faceSamplesToAdd = listOf(faceSample1), // Try to add samples fingerprintSamplesToAdd = listOf(), referenceIdsToRemove = listOf(), + externalCredentialsToAdd = listOf(), ) // When @@ -601,6 +608,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { faceSamplesToAdd = listOf(), fingerprintSamplesToAdd = listOf(fingerprintSample1), referenceIdsToRemove = listOf(), + externalCredentialsToAdd = listOf(), ) dataSource.performActions(listOf(updateAction), project) loadedSubject = diff --git a/infra/enrolment-records/room-store/schemas/com.simprints.infra.enrolment.records.room.store.SubjectsDatabase/1.json b/infra/enrolment-records/room-store/schemas/com.simprints.infra.enrolment.records.room.store.SubjectsDatabase/1.json index 58b0f3827c..365053e35b 100644 --- a/infra/enrolment-records/room-store/schemas/com.simprints.infra.enrolment.records.room.store.SubjectsDatabase/1.json +++ b/infra/enrolment-records/room-store/schemas/com.simprints.infra.enrolment.records.room.store.SubjectsDatabase/1.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "94bee827928a2618c6873579bc6bc63a", + "identityHash": "527fcc2c704906558681cb31beddb0c3", "entities": [ { "tableName": "DbSubject", @@ -170,11 +170,55 @@ ] } ] + }, + { + "tableName": "DbDbExternalCredential", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`value` TEXT NOT NULL, `subjectId` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`value`, `subjectId`), FOREIGN KEY(`subjectId`) REFERENCES `DbSubject`(`subjectId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subjectId", + "columnName": "subjectId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "value", + "subjectId" + ] + }, + "foreignKeys": [ + { + "table": "DbSubject", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "subjectId" + ], + "referencedColumns": [ + "subjectId" + ] + } + ] } ], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '94bee827928a2618c6873579bc6bc63a')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '527fcc2c704906558681cb31beddb0c3')" ] } -} +} \ No newline at end of file diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/ApiEnrolmentPayloadV2Test.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/ApiEnrolmentPayloadV2Test.kt index 523977bddb..de5b9695a7 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/ApiEnrolmentPayloadV2Test.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/ApiEnrolmentPayloadV2Test.kt @@ -14,7 +14,8 @@ class ApiEnrolmentPayloadV2Test { when (it) { TokenKeyType.AttendantId -> assertThat(result).isEqualTo("attendantId") TokenKeyType.ModuleId -> assertThat(result).isEqualTo("moduleId") - else -> assertThat(result).isNull() + TokenKeyType.ExternalCredential -> assertThat(result).isEqualTo("externalCredential") + TokenKeyType.Unknown -> assertThat(result).isNull() } } } 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 72bda216d3..74fa6fa139 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 @@ -1,6 +1,8 @@ package com.simprints.infra.eventsync.event.remote.models.subject import com.google.common.truth.Truth.assertThat +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.events.event.domain.models.subject.EnrolmentRecordCreationEvent @@ -14,11 +16,11 @@ class ApiEnrolmentRecordCreationEventTest { @Test fun convert_EnrolmentRecordCreationEvent() { val apiPayload = ApiEnrolmentRecordCreationPayload( - "subjectId", - "projectId", - "moduleId", - "attendantId", - listOf( + subjectId = "subjectId", + projectId = "projectId", + moduleId = "moduleId", + attendantId = "attendantId", + biometricReferences = listOf( ApiFingerprintReference( "fpRefId", listOf( @@ -27,13 +29,18 @@ class ApiEnrolmentRecordCreationEventTest { "NEC_1", ), ), + externalCredential = ApiExternalCredential( + id = "id", + type = ExternalCredentialType.NHISCard.toString(), + value = "value" + ), ) val expectedPayload = EnrolmentRecordCreationEvent.EnrolmentRecordCreationPayload( - "subjectId", - "projectId", - "moduleId".asTokenizableEncrypted(), - "attendantId".asTokenizableEncrypted(), - listOf( + subjectId = "subjectId", + projectId = "projectId", + moduleId = "moduleId".asTokenizableEncrypted(), + attendantId = "attendantId".asTokenizableEncrypted(), + biometricReferences = listOf( FingerprintReference( "fpRefId", listOf( @@ -42,6 +49,11 @@ class ApiEnrolmentRecordCreationEventTest { "NEC_1", ), ), + externalCredential = ExternalCredential( + 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/remote/models/subject/ApiEnrolmentRecordUpdateEventTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordUpdateEventTest.kt index 030c7bf493..e3ed24eef0 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 @@ -1,7 +1,10 @@ package com.simprints.infra.eventsync.event.remote.models.subject import com.google.common.truth.Truth.assertThat +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.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 @@ -17,8 +20,8 @@ class ApiEnrolmentRecordUpdateEventTest { @Test fun convert_EnrolmentRecordUpdateEvent() { val apiPayload = ApiEnrolmentRecordUpdatePayload( - "subjectId", - listOf( + subjectId = "subjectId", + biometricReferencesAdded = listOf( ApiFingerprintReference( "fpRefId", listOf( @@ -32,11 +35,16 @@ class ApiEnrolmentRecordUpdateEventTest { "ROC_3", ), ), - listOf("fpRefId2"), + biometricReferencesRemoved = listOf("fpRefId2"), + externalCredentialAdded = ApiExternalCredential( + id = "id", + type = ExternalCredentialType.NHISCard.toString(), + value = "value" + ) ) val expectedPayload = EnrolmentRecordUpdateEvent.EnrolmentRecordUpdatePayload( - "subjectId", - listOf( + subjectId = "subjectId", + biometricReferencesAdded = listOf( FingerprintReference( "fpRefId", listOf(FingerprintTemplate("template", IFingerIdentifier.LEFT_THUMB)), @@ -48,7 +56,12 @@ class ApiEnrolmentRecordUpdateEventTest { "ROC_3", ), ), - listOf("fpRefId2"), + biometricReferencesRemoved = listOf("fpRefId2"), + externalCredentialAdded = ExternalCredential( + 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/sync/down/tasks/EventDownSyncTaskTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/EventDownSyncTaskTest.kt index a7d6b01de8..3962485c2b 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/EventDownSyncTaskTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/EventDownSyncTaskTest.kt @@ -1,7 +1,10 @@ package com.simprints.infra.eventsync.sync.down.tasks import com.google.common.truth.Truth.assertThat +import com.simprints.core.domain.externalcredential.ExternalCredential +import com.simprints.core.domain.externalcredential.ExternalCredentialType import com.simprints.core.domain.face.FaceSample +import com.simprints.core.domain.tokenization.asTokenizableEncrypted import com.simprints.core.domain.tokenization.asTokenizableRaw import com.simprints.core.tools.time.TimeHelper import com.simprints.infra.authstore.exceptions.RemoteDbNotSignedInException @@ -58,11 +61,16 @@ class EventDownSyncTaskTest { "attendantId", ) val ENROLMENT_RECORD_CREATION = EnrolmentRecordCreationEvent( - "subjectId", - "projectId", - "moduleId".asTokenizableRaw(), - "attendantId".asTokenizableRaw(), - listOf(FaceReference("id", listOf(FaceTemplate("template")), "format")), + subjectId = "subjectId", + projectId = "projectId", + moduleId = "moduleId".asTokenizableRaw(), + attendantId = "attendantId".asTokenizableRaw(), + biometricReferences = listOf(FaceReference("id", listOf(FaceTemplate("template")), "format")), + externalCredential = ExternalCredential( + value = "value".asTokenizableEncrypted(), + subjectId = "subjectId", + type = ExternalCredentialType.NHISCard + ), ) val ENROLMENT_RECORD_MOVE_MODULE = EnrolmentRecordMoveEvent( EnrolmentRecordMoveEvent.EnrolmentRecordCreationInMove( @@ -110,9 +118,14 @@ class EventDownSyncTaskTest { ), ) val ENROLMENT_RECORD_UPDATE = EnrolmentRecordUpdateEvent( - "subjectId", - listOf(FaceReference("id", listOf(FaceTemplate("template")), "format")), - listOf("referenceIdToDelete"), + subjectId = "subjectId", + biometricReferencesAdded = listOf(FaceReference("id", listOf(FaceTemplate("template")), "format")), + biometricReferencesRemoved = listOf("referenceIdToDelete"), + externalCredentialAdded = ExternalCredential( + value = "value".asTokenizableEncrypted(), + subjectId = "subjectId", + 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 684a61f533..29fe72c30c 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 @@ -1,9 +1,12 @@ package com.simprints.infra.eventsync.sync.down.tasks import com.google.common.truth.Truth.assertThat +import com.simprints.core.domain.externalcredential.ExternalCredential +import com.simprints.core.domain.externalcredential.ExternalCredentialType import com.simprints.core.domain.face.FaceSample import com.simprints.core.domain.fingerprint.FingerprintSample import com.simprints.core.domain.fingerprint.IFingerIdentifier +import com.simprints.core.domain.tokenization.asTokenizableEncrypted import com.simprints.core.domain.tokenization.asTokenizableRaw import com.simprints.core.tools.time.TimeHelper import com.simprints.core.tools.utils.EncodingUtils @@ -61,6 +64,7 @@ class SubjectFactoryTest { attendantId = ATTENDANT_ID, moduleId = MODULE_ID, biometricReferences = listOf(FINGERPRINT_REFERENCE, faceReference), + externalCredential = null ) val result = factory.buildSubjectFromCreationPayload(payload) val expected = Subject( @@ -177,6 +181,7 @@ class SubjectFactoryTest { templates = listOf(FaceTemplate(template = BASE_64_BYTES.toString())), ), ), + externalCredentialAdded = EXTERNAL_CREDENTIAL ) val result = factory.buildSubjectFromUpdatePayload(subject, payload) @@ -326,6 +331,8 @@ class SubjectFactoryTest { private const val REFERENCE_ID = "fpRefId" private const val REFERENCE_FORMAT = "NEC_1" private const val TEMPLATE_NAME = "template" + private val EXTERNAL_CREDENTIAL_VALUE = "value".asTokenizableEncrypted() + private val EXTERNAL_CREDENTIAL_TYPE = ExternalCredentialType.NHISCard private val IDENTIFIER = IFingerIdentifier.LEFT_THUMB private const val QUALITY = 10 private val FINGERPRINT_REFERENCE = FingerprintReference( @@ -343,5 +350,10 @@ class SubjectFactoryTest { templates = listOf(FaceTemplate(TEMPLATE_NAME)), format = REFERENCE_FORMAT, ) + private val EXTERNAL_CREDENTIAL = ExternalCredential( + value = EXTERNAL_CREDENTIAL_VALUE, + subjectId = SUBJECT_ID, + type = EXTERNAL_CREDENTIAL_TYPE + ) } } From 0660608816f0fc722910f8c688ce8442685d75fe Mon Sep 17 00:00:00 2001 From: alex Date: Sun, 10 Aug 2025 18:37:29 +0300 Subject: [PATCH 005/139] =?UTF-8?q?[CORE-3421]=20Fixing=20tests=CB=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../authenticator/ProjectAuthenticatorTest.kt | 19 +++--- .../authenticator/SignerManagerTest.kt | 21 +++--- .../local/ConfigLocalDataSourceImplTest.kt | 22 ++++--- .../models/ApiProjectConfigurationTest.kt | 44 +++++++------ .../infra/config/store/testtools/Models.kt | 65 +++++++++++++------ 5 files changed, 102 insertions(+), 69 deletions(-) diff --git a/infra/auth-logic/src/test/java/com/simprints/infra/authlogic/authenticator/ProjectAuthenticatorTest.kt b/infra/auth-logic/src/test/java/com/simprints/infra/authlogic/authenticator/ProjectAuthenticatorTest.kt index 968f04c4dc..e38b099a26 100644 --- a/infra/auth-logic/src/test/java/com/simprints/infra/authlogic/authenticator/ProjectAuthenticatorTest.kt +++ b/infra/auth-logic/src/test/java/com/simprints/infra/authlogic/authenticator/ProjectAuthenticatorTest.kt @@ -164,9 +164,9 @@ class ProjectAuthenticatorTest { } returns Token("", "", "", "") coEvery { configManager.getProjectConfiguration() } returns ProjectConfiguration( - "id", - PROJECT_ID, - "", + id = "id", + projectId = PROJECT_ID, + updatedAt = "", general = GeneralConfiguration( modalities = mockk(), matchingModalities = mockk(), @@ -176,12 +176,13 @@ class ProjectAuthenticatorTest { duplicateBiometricEnrolmentCheck = false, settingsPassword = mockk(), ), - mockk(), - mockk(), - mockk(), - mockk(), - mockk(), - mockk(), + face = mockk(), + fingerprint = mockk(), + consent = mockk(), + identification = mockk(), + synchronization = mockk(), + multifactorId = mockk(), + custom = mockk(), ) coEvery { configManager.getPrivacyNotice(any(), any()) } returns emptyFlow() diff --git a/infra/auth-logic/src/test/java/com/simprints/infra/authlogic/authenticator/SignerManagerTest.kt b/infra/auth-logic/src/test/java/com/simprints/infra/authlogic/authenticator/SignerManagerTest.kt index 5ba9343990..af8a2c6dc8 100644 --- a/infra/auth-logic/src/test/java/com/simprints/infra/authlogic/authenticator/SignerManagerTest.kt +++ b/infra/auth-logic/src/test/java/com/simprints/infra/authlogic/authenticator/SignerManagerTest.kt @@ -227,16 +227,17 @@ internal class SignerManagerTest { tokenizationKeys = emptyMap(), ), ProjectConfiguration( - "id", - DEFAULT_PROJECT_ID, - "", - mockk(), - mockk(), - mockk(), - mockk(), - mockk(), - mockk(), - null, + id = "id", + projectId = DEFAULT_PROJECT_ID, + updatedAt = "", + general = mockk(), + face = mockk(), + fingerprint = mockk(), + consent = mockk(), + identification = mockk(), + synchronization = mockk(), + multifactorId = mockk(), + custom = mockk(), ), ), ) diff --git a/infra/config-store/src/test/java/com/simprints/infra/config/store/local/ConfigLocalDataSourceImplTest.kt b/infra/config-store/src/test/java/com/simprints/infra/config/store/local/ConfigLocalDataSourceImplTest.kt index 68378448af..6b70f71501 100644 --- a/infra/config-store/src/test/java/com/simprints/infra/config/store/local/ConfigLocalDataSourceImplTest.kt +++ b/infra/config-store/src/test/java/com/simprints/infra/config/store/local/ConfigLocalDataSourceImplTest.kt @@ -17,6 +17,7 @@ import com.simprints.infra.config.store.testtools.consentConfiguration import com.simprints.infra.config.store.testtools.faceConfiguration import com.simprints.infra.config.store.testtools.generalConfiguration import com.simprints.infra.config.store.testtools.identificationConfiguration +import com.simprints.infra.config.store.testtools.multiFactorIdConfiguration import com.simprints.infra.config.store.testtools.project import com.simprints.infra.config.store.testtools.projectConfiguration import com.simprints.infra.config.store.testtools.synchronizationConfiguration @@ -173,16 +174,17 @@ class ConfigLocalDataSourceImplTest { fun `should save the project configuration and update the device configuration correctly with an empty list of fingersToCollect if fingerprint config is missing`() = runTest { val projectConfigurationToSave = ProjectConfiguration( - "id", - "projectId", - "updatedAt", - generalConfiguration, - faceConfiguration, - null, - consentConfiguration, - identificationConfiguration, - synchronizationConfiguration, - null, + id = "id", + projectId = "projectId", + updatedAt = "updatedAt", + general = generalConfiguration, + face = faceConfiguration, + fingerprint = null, + consent = consentConfiguration, + identification = identificationConfiguration, + synchronization = synchronizationConfiguration, + multifactorId = multiFactorIdConfiguration, + custom = null, ) configLocalDataSourceImpl.saveProjectConfiguration(projectConfigurationToSave) diff --git a/infra/config-store/src/test/java/com/simprints/infra/config/store/remote/models/ApiProjectConfigurationTest.kt b/infra/config-store/src/test/java/com/simprints/infra/config/store/remote/models/ApiProjectConfigurationTest.kt index f5b2a3e4e2..1e48265958 100644 --- a/infra/config-store/src/test/java/com/simprints/infra/config/store/remote/models/ApiProjectConfigurationTest.kt +++ b/infra/config-store/src/test/java/com/simprints/infra/config/store/remote/models/ApiProjectConfigurationTest.kt @@ -5,12 +5,14 @@ import com.simprints.infra.config.store.models.ProjectConfiguration import com.simprints.infra.config.store.testtools.apiConsentConfiguration import com.simprints.infra.config.store.testtools.apiGeneralConfiguration import com.simprints.infra.config.store.testtools.apiIdentificationConfiguration +import com.simprints.infra.config.store.testtools.apiMultiFactorIdConfiguration import com.simprints.infra.config.store.testtools.apiProjectConfiguration import com.simprints.infra.config.store.testtools.apiSynchronizationConfiguration import com.simprints.infra.config.store.testtools.consentConfiguration import com.simprints.infra.config.store.testtools.customKeyMap import com.simprints.infra.config.store.testtools.generalConfiguration import com.simprints.infra.config.store.testtools.identificationConfiguration +import com.simprints.infra.config.store.testtools.multiFactorIdConfiguration import com.simprints.infra.config.store.testtools.projectConfiguration import com.simprints.infra.config.store.testtools.synchronizationConfiguration import org.junit.Test @@ -24,28 +26,30 @@ class ApiProjectConfigurationTest { @Test fun `should map correctly the model when both fingerprint and face are missing`() { val apiProjectConfiguration = ApiProjectConfiguration( - "id", - "projectId", - "updatedAt", - apiGeneralConfiguration, - null, - null, - apiConsentConfiguration, - apiIdentificationConfiguration, - apiSynchronizationConfiguration, - customKeyMap, + id = "id", + projectId = "projectId", + updatedAt = "updatedAt", + general = apiGeneralConfiguration, + face = null, + fingerprint = null, + consent = apiConsentConfiguration, + identification = apiIdentificationConfiguration, + synchronization = apiSynchronizationConfiguration, + multiFactorId = apiMultiFactorIdConfiguration, + custom = customKeyMap, ) val projectConfiguration = ProjectConfiguration( - "id", - "projectId", - "updatedAt", - generalConfiguration, - null, - null, - consentConfiguration, - identificationConfiguration, - synchronizationConfiguration, - customKeyMap, + id = "id", + projectId = "projectId", + updatedAt = "updatedAt", + general = generalConfiguration, + face = null, + fingerprint = null, + consent = consentConfiguration, + identification = identificationConfiguration, + synchronization = synchronizationConfiguration, + multifactorId = multiFactorIdConfiguration, + custom = customKeyMap, ) assertThat(apiProjectConfiguration.toDomain()).isEqualTo(projectConfiguration) diff --git a/infra/config-store/src/test/java/com/simprints/infra/config/store/testtools/Models.kt b/infra/config-store/src/test/java/com/simprints/infra/config/store/testtools/Models.kt index 229040bc55..b141abc1b5 100644 --- a/infra/config-store/src/test/java/com/simprints/infra/config/store/testtools/Models.kt +++ b/infra/config-store/src/test/java/com/simprints/infra/config/store/testtools/Models.kt @@ -1,17 +1,20 @@ package com.simprints.infra.config.store.testtools +import com.simprints.core.domain.externalcredential.ExternalCredentialType import com.simprints.core.domain.tokenization.asTokenizableEncrypted import com.simprints.infra.config.store.local.models.ProtoAllowedAgeRange import com.simprints.infra.config.store.local.models.ProtoConsentConfiguration import com.simprints.infra.config.store.local.models.ProtoDecisionPolicy import com.simprints.infra.config.store.local.models.ProtoDeviceConfiguration import com.simprints.infra.config.store.local.models.ProtoDownSynchronizationConfiguration +import com.simprints.infra.config.store.local.models.ProtoExternalCredentialType import com.simprints.infra.config.store.local.models.ProtoFaceConfiguration import com.simprints.infra.config.store.local.models.ProtoFinger import com.simprints.infra.config.store.local.models.ProtoFingerprintConfiguration import com.simprints.infra.config.store.local.models.ProtoFingerprintConfiguration.ProtoMaxCaptureAttempts import com.simprints.infra.config.store.local.models.ProtoGeneralConfiguration import com.simprints.infra.config.store.local.models.ProtoIdentificationConfiguration +import com.simprints.infra.config.store.local.models.ProtoMultiFactorIdConfiguration import com.simprints.infra.config.store.local.models.ProtoProject import com.simprints.infra.config.store.local.models.ProtoProjectConfiguration import com.simprints.infra.config.store.local.models.ProtoSampleSynchronizationConfiguration @@ -35,6 +38,7 @@ import com.simprints.infra.config.store.models.Frequency import com.simprints.infra.config.store.models.GeneralConfiguration import com.simprints.infra.config.store.models.IdentificationConfiguration import com.simprints.infra.config.store.models.MaxCaptureAttempts +import com.simprints.infra.config.store.models.MultiFactorIdConfiguration import com.simprints.infra.config.store.models.Project import com.simprints.infra.config.store.models.ProjectConfiguration import com.simprints.infra.config.store.models.ProjectState @@ -49,12 +53,14 @@ import com.simprints.infra.config.store.remote.models.ApiAllowedAgeRange import com.simprints.infra.config.store.remote.models.ApiConsentConfiguration import com.simprints.infra.config.store.remote.models.ApiDecisionPolicy import com.simprints.infra.config.store.remote.models.ApiDeviceState +import com.simprints.infra.config.store.remote.models.ApiExternalCredentialType import com.simprints.infra.config.store.remote.models.ApiFaceConfiguration import com.simprints.infra.config.store.remote.models.ApiFaceConfiguration.ApiFaceSdkConfiguration import com.simprints.infra.config.store.remote.models.ApiFingerprintConfiguration import com.simprints.infra.config.store.remote.models.ApiGeneralConfiguration import com.simprints.infra.config.store.remote.models.ApiIdentificationConfiguration import com.simprints.infra.config.store.remote.models.ApiMaxCaptureAttempts +import com.simprints.infra.config.store.remote.models.ApiMultiFactorIdConfiguration import com.simprints.infra.config.store.remote.models.ApiProject import com.simprints.infra.config.store.remote.models.ApiProjectConfiguration import com.simprints.infra.config.store.remote.models.ApiProjectState @@ -405,6 +411,16 @@ internal val synchronizationConfiguration = SynchronizationConfiguration( ), ) +internal val allowedExternalCredential = ExternalCredentialType.NHISCard + +internal val multiFactorIdConfiguration = MultiFactorIdConfiguration( + allowedExternalCredentials = listOf(allowedExternalCredential) +) + +internal val protoMultiFactorIdConfiguration = ProtoMultiFactorIdConfiguration + .newBuilder() + .addAllowedExternalCredentials(ProtoExternalCredentialType.NHIS_CARD) + internal val protoSynchronizationConfiguration = ProtoSynchronizationConfiguration .newBuilder() .setUp( @@ -451,6 +467,12 @@ internal val protoSynchronizationConfiguration = ProtoSynchronizationConfigurati .build(), ).build() +internal val apiAllowedExternalCredential = ApiExternalCredentialType.NHISCard + +internal val apiMultiFactorIdConfiguration = ApiMultiFactorIdConfiguration( + allowedExternalCredentials = listOf(apiAllowedExternalCredential) +) + internal val customKeyMap: Map? = mapOf( "key1" to 7, "key2" to 4.2, @@ -460,29 +482,31 @@ internal val customKeyMap: Map? = mapOf( internal const val PROTO_CUSTOM_KEY_MAP_JSON = "{\"key1\":7,\"key2\":4.2,\"key3\":false,\"key4\":\"test\"}" internal val apiProjectConfiguration = ApiProjectConfiguration( - "id", - "projectId", - "updatedAt", - apiGeneralConfiguration, - apiFaceConfiguration, - apiFingerprintConfiguration, - apiConsentConfiguration, - apiIdentificationConfiguration, - apiSynchronizationConfiguration, - customKeyMap, + id = "id", + projectId = "projectId", + updatedAt = "updatedAt", + general = apiGeneralConfiguration, + face = apiFaceConfiguration, + fingerprint = apiFingerprintConfiguration, + consent = apiConsentConfiguration, + identification = apiIdentificationConfiguration, + synchronization = apiSynchronizationConfiguration, + multiFactorId = apiMultiFactorIdConfiguration, + custom = customKeyMap, ) internal val projectConfiguration = ProjectConfiguration( - "id", - "projectId", - "updatedAt", - generalConfiguration, - faceConfiguration, - fingerprintConfiguration, - consentConfiguration, - identificationConfiguration, - synchronizationConfiguration, - customKeyMap, + id = "id", + projectId = "projectId", + updatedAt = "updatedAt", + general = generalConfiguration, + face = faceConfiguration, + fingerprint = fingerprintConfiguration, + consent = consentConfiguration, + identification = identificationConfiguration, + synchronization = synchronizationConfiguration, + multifactorId = multiFactorIdConfiguration, + custom = customKeyMap ) internal val protoProjectConfiguration = ProtoProjectConfiguration @@ -496,6 +520,7 @@ internal val protoProjectConfiguration = ProtoProjectConfiguration .setConsent(protoConsentConfiguration) .setIdentification(protoIdentificationConfiguration) .setSynchronization(protoSynchronizationConfiguration) + .setMultiFactorId(protoMultiFactorIdConfiguration) .setCustomJson(PROTO_CUSTOM_KEY_MAP_JSON) .build() From 5317e06d5131defc1b2e5f6dda447104cc47ded5 Mon Sep 17 00:00:00 2001 From: alex Date: Sun, 10 Aug 2025 18:57:03 +0300 Subject: [PATCH 006/139] =?UTF-8?q?[CORE-3421]=20Fixing=20tests=CB=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infra/sync/config/testtools/Models.kt | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/infra/sync/src/test/java/com/simprints/infra/sync/config/testtools/Models.kt b/infra/sync/src/test/java/com/simprints/infra/sync/config/testtools/Models.kt index aa055eb0b9..2810ef06ed 100644 --- a/infra/sync/src/test/java/com/simprints/infra/sync/config/testtools/Models.kt +++ b/infra/sync/src/test/java/com/simprints/infra/sync/config/testtools/Models.kt @@ -1,5 +1,6 @@ package com.simprints.infra.sync.config.testtools +import com.simprints.core.domain.externalcredential.ExternalCredentialType import com.simprints.core.domain.tokenization.asTokenizableEncrypted import com.simprints.infra.config.store.models.AgeGroup import com.simprints.infra.config.store.models.ConsentConfiguration @@ -12,6 +13,7 @@ import com.simprints.infra.config.store.models.Frequency import com.simprints.infra.config.store.models.GeneralConfiguration import com.simprints.infra.config.store.models.IdentificationConfiguration import com.simprints.infra.config.store.models.MaxCaptureAttempts +import com.simprints.infra.config.store.models.MultiFactorIdConfiguration import com.simprints.infra.config.store.models.Project import com.simprints.infra.config.store.models.ProjectConfiguration import com.simprints.infra.config.store.models.ProjectState @@ -114,6 +116,12 @@ internal val simprintsDownSyncConfigurationConfiguration = DownSynchronizationCo frequency = Frequency.PERIODICALLY, ) +internal val allowedExternalCredential = ExternalCredentialType.NHISCard + +internal val multiFactorIdConfiguration = MultiFactorIdConfiguration( + allowedExternalCredentials = listOf(allowedExternalCredential) +) + internal val synchronizationConfiguration = SynchronizationConfiguration( up = UpSynchronizationConfiguration( simprints = simprintsUpSyncConfigurationConfiguration, @@ -131,16 +139,17 @@ internal val identificationConfiguration = IdentificationConfiguration(4, IdentificationConfiguration.PoolType.PROJECT) internal val projectConfiguration = ProjectConfiguration( - "id", - "projectId", - "updatedAt", - generalConfiguration, - faceConfiguration, - fingerprintConfiguration, - consentConfiguration, - identificationConfiguration, - synchronizationConfiguration, - null, + id = "id", + projectId = "projectId", + updatedAt = "updatedAt", + general = generalConfiguration, + face = faceConfiguration, + fingerprint = fingerprintConfiguration, + consent = consentConfiguration, + identification = identificationConfiguration, + synchronization = synchronizationConfiguration, + multifactorId = multiFactorIdConfiguration, + custom = null ) internal const val TOKENIZATION_JSON = From 54f7ddf37f5309b3de742c93db672ec3c50b893f Mon Sep 17 00:00:00 2001 From: alex Date: Sun, 10 Aug 2025 19:30:37 +0300 Subject: [PATCH 007/139] =?UTF-8?q?[CORE-3421]=20Fixing=20tests=CB=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../enrollast/screen/usecase/BuildSubjectUseCase.kt | 7 +++++++ .../usecases/response/CreateEnrolResponseUseCase.kt | 4 ++++ .../local/RealmEnrolmentRecordLocalDataSourceTest.kt | 12 ++++++++++++ .../event/remote/models/ApiEnrolmentPayloadV4.kt | 2 ++ .../remote/models/callout/ApiCalloutPayloadV2.kt | 2 ++ .../remote/models/callout/ApiCalloutPayloadV3.kt | 2 ++ .../eventsync/sync/down/tasks/SubjectFactory.kt | 7 +++++++ .../models/subject/EnrolmentRecordCreationEvent.kt | 2 ++ .../models/subject/EnrolmentRecordUpdateEvent.kt | 2 ++ 9 files changed, 40 insertions(+) diff --git a/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/screen/usecase/BuildSubjectUseCase.kt b/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/screen/usecase/BuildSubjectUseCase.kt index ba78166670..b537dda31c 100644 --- a/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/screen/usecase/BuildSubjectUseCase.kt +++ b/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/screen/usecase/BuildSubjectUseCase.kt @@ -1,5 +1,6 @@ package com.simprints.feature.enrollast.screen.usecase +import com.simprints.core.domain.externalcredential.ExternalCredential import com.simprints.core.domain.face.FaceSample import com.simprints.core.domain.fingerprint.FingerprintSample import com.simprints.core.domain.fingerprint.IFingerIdentifier @@ -31,8 +32,14 @@ internal class BuildSubjectUseCase @Inject constructor( faceSamples = getFaceCaptureResult(params.steps) ?.let { result -> result.results.map { faceSample(result.referenceId, it) } } .orEmpty(), + externalCredential = getExternalCredentialResult(params.steps) ) + // TODO [CORE-3421] When an external credential can be extracted from the UI-level steps, extract it here + private fun getExternalCredentialResult(steps: List): ExternalCredential? { + return null + } + private fun getFingerprintCaptureResult(steps: List) = steps .filterIsInstance() .firstOrNull() diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateEnrolResponseUseCase.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateEnrolResponseUseCase.kt index 69d2b57198..53f4dc9a2f 100644 --- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateEnrolResponseUseCase.kt +++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateEnrolResponseUseCase.kt @@ -1,5 +1,6 @@ package com.simprints.feature.orchestrator.usecases.response +import com.simprints.core.domain.externalcredential.ExternalCredential import com.simprints.core.domain.response.AppErrorReason import com.simprints.face.capture.FaceCaptureResult import com.simprints.fingerprint.capture.FingerprintCaptureResult @@ -25,6 +26,8 @@ internal class CreateEnrolResponseUseCase @Inject constructor( ): AppResponse { val fingerprintCapture = results.filterIsInstance(FingerprintCaptureResult::class.java).lastOrNull() val faceCapture = results.filterIsInstance(FaceCaptureResult::class.java).lastOrNull() + // TODO [CORE-3421] When an external credential can be extracted from the UI-level steps, extract it here + val externalCredential: ExternalCredential? = null return try { val subject = subjectFactory.buildSubjectFromCaptureResults( @@ -33,6 +36,7 @@ internal class CreateEnrolResponseUseCase @Inject constructor( moduleId = request.moduleId, fingerprintResponse = fingerprintCapture, faceResponse = faceCapture, + externalCredential = externalCredential, ) enrolSubject(subject, project) diff --git a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/RealmEnrolmentRecordLocalDataSourceTest.kt b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/RealmEnrolmentRecordLocalDataSourceTest.kt index f16a5fbe70..dd6bdc4ff4 100644 --- a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/RealmEnrolmentRecordLocalDataSourceTest.kt +++ b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/RealmEnrolmentRecordLocalDataSourceTest.kt @@ -1,6 +1,8 @@ package com.simprints.infra.enrolment.records.repository.local 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 import com.simprints.core.domain.fingerprint.FingerprintSample import com.simprints.core.domain.fingerprint.IFingerIdentifier @@ -431,6 +433,9 @@ class RealmEnrolmentRecordLocalDataSourceTest { getRandomFaceSample(), ), fingerprintSamples: List = listOf(), + externalCredentials: List = listOf( + getRandomExternalCredential() + ), ): Subject = Subject( subjectId = patientId, projectId = projectId, @@ -438,6 +443,7 @@ class RealmEnrolmentRecordLocalDataSourceTest { moduleId = moduleId.asTokenizableRaw(), faceSamples = faceSamples, fingerprintSamples = fingerprintSamples, + externalCredentials = externalCredentials ) private fun getRandomFaceSample( @@ -449,4 +455,10 @@ class RealmEnrolmentRecordLocalDataSourceTest { id: String = UUID.randomUUID().toString(), referenceId: String = "referenceId", ) = FingerprintSample(IFingerIdentifier.LEFT_3RD_FINGER, Random.nextBytes(64), "fingerprintTemplateFormat", referenceId, id) + + private fun getRandomExternalCredential() = ExternalCredential( + value = "value".asTokenizableEncrypted(), + subjectId = "subjectId", + type = ExternalCredentialType.NHISCard + ) } 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 1cff72ca80..28368915c6 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 @@ -1,10 +1,12 @@ package com.simprints.infra.eventsync.event.remote.models import androidx.annotation.Keep +import com.simprints.core.ExcludedFromGeneratedTestCoverageReports import com.simprints.infra.config.store.models.TokenKeyType import com.simprints.infra.events.event.domain.models.EnrolmentEventV4 @Keep +@ExcludedFromGeneratedTestCoverageReports("Data class") internal data class ApiEnrolmentPayloadV4( override val startTime: ApiTimestamp, val subjectId: String, diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/callout/ApiCalloutPayloadV2.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/callout/ApiCalloutPayloadV2.kt index 58bb4a4553..45441ef474 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/callout/ApiCalloutPayloadV2.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/callout/ApiCalloutPayloadV2.kt @@ -3,6 +3,7 @@ package com.simprints.infra.eventsync.event.remote.models.callout import androidx.annotation.Keep import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.annotation.JsonInclude.Include +import com.simprints.core.ExcludedFromGeneratedTestCoverageReports import com.simprints.infra.config.store.models.TokenKeyType import com.simprints.infra.events.event.domain.models.callout.ConfirmationCalloutEventV2.ConfirmationCalloutPayload import com.simprints.infra.events.event.domain.models.callout.EnrolmentCalloutEventV2.EnrolmentCalloutPayload @@ -15,6 +16,7 @@ import com.simprints.infra.eventsync.event.remote.models.fromDomainToApi @Keep @JsonInclude(Include.NON_NULL) +@ExcludedFromGeneratedTestCoverageReports("Data class") internal data class ApiCalloutPayloadV2( override val startTime: ApiTimestamp, val callout: ApiCallout, diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/callout/ApiCalloutPayloadV3.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/callout/ApiCalloutPayloadV3.kt index d703c11275..373e28cd0f 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/callout/ApiCalloutPayloadV3.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/callout/ApiCalloutPayloadV3.kt @@ -3,6 +3,7 @@ package com.simprints.infra.eventsync.event.remote.models.callout import androidx.annotation.Keep import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.annotation.JsonInclude.Include +import com.simprints.core.ExcludedFromGeneratedTestCoverageReports import com.simprints.infra.config.store.models.TokenKeyType import com.simprints.infra.events.event.domain.models.callout.ConfirmationCalloutEventV3.ConfirmationCalloutPayload import com.simprints.infra.events.event.domain.models.callout.EnrolmentCalloutEventV3.EnrolmentCalloutPayload @@ -15,6 +16,7 @@ import com.simprints.infra.eventsync.event.remote.models.fromDomainToApi @Keep @JsonInclude(Include.NON_NULL) +@ExcludedFromGeneratedTestCoverageReports("Data class") internal data class ApiCalloutPayloadV3( override val startTime: ApiTimestamp, val callout: ApiCallout, diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/tasks/SubjectFactory.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/tasks/SubjectFactory.kt index 8c5ce2496e..6b360f69ec 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/tasks/SubjectFactory.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/tasks/SubjectFactory.kt @@ -1,5 +1,6 @@ package com.simprints.infra.eventsync.sync.down.tasks +import com.simprints.core.domain.externalcredential.ExternalCredential import com.simprints.core.domain.face.FaceSample import com.simprints.core.domain.fingerprint.FingerprintSample import com.simprints.core.domain.tokenization.TokenizableString @@ -32,6 +33,7 @@ class SubjectFactory @Inject constructor( moduleId = moduleId, fingerprintSamples = extractFingerprintSamplesFromBiometricReferences(this.biometricReferences), faceSamples = extractFaceSamplesFromBiometricReferences(this.biometricReferences), + externalCredential = payload.externalCredential, ) } @@ -43,6 +45,7 @@ class SubjectFactory @Inject constructor( moduleId = moduleId, fingerprintSamples = extractFingerprintSamplesFromBiometricReferences(this.biometricReferences), faceSamples = extractFaceSamplesFromBiometricReferences(this.biometricReferences), + externalCredential = null, ) } @@ -70,6 +73,7 @@ class SubjectFactory @Inject constructor( moduleId: TokenizableString, fingerprintResponse: FingerprintCaptureResult?, faceResponse: FaceCaptureResult?, + externalCredential: ExternalCredential?, ): Subject { val subjectId = UUID.randomUUID().toString() return buildSubject( @@ -80,6 +84,7 @@ class SubjectFactory @Inject constructor( createdAt = Date(timeHelper.now().ms), fingerprintSamples = fingerprintResponse?.let { extractFingerprintSamples(it) }.orEmpty(), faceSamples = faceResponse?.let { extractFaceSamples(it) }.orEmpty(), + externalCredential = externalCredential, ) } @@ -92,6 +97,7 @@ class SubjectFactory @Inject constructor( updatedAt: Date? = null, fingerprintSamples: List = emptyList(), faceSamples: List = emptyList(), + externalCredential: ExternalCredential? = null, ) = Subject( subjectId = subjectId, projectId = projectId, @@ -101,6 +107,7 @@ class SubjectFactory @Inject constructor( updatedAt = updatedAt, fingerprintSamples = fingerprintSamples, faceSamples = faceSamples, + externalCredentials = externalCredential?.let { listOf(it) } ?: emptyList() ) private fun extractFingerprintSamples(fingerprintResponse: FingerprintCaptureResult) = diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordCreationEvent.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordCreationEvent.kt index 9a89986e15..3fc585095a 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordCreationEvent.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordCreationEvent.kt @@ -1,6 +1,7 @@ package com.simprints.infra.events.event.domain.models.subject import androidx.annotation.Keep +import com.simprints.core.ExcludedFromGeneratedTestCoverageReports import com.simprints.core.domain.externalcredential.ExternalCredential import com.simprints.core.domain.face.FaceSample import com.simprints.core.domain.fingerprint.FingerprintSample @@ -9,6 +10,7 @@ import com.simprints.core.tools.utils.EncodingUtils import java.util.UUID @Keep +@ExcludedFromGeneratedTestCoverageReports("Data class") data class EnrolmentRecordCreationEvent( override val id: String, val payload: EnrolmentRecordCreationPayload, 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 cd50c3ffd0..34c9196462 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 @@ -1,10 +1,12 @@ package com.simprints.infra.events.event.domain.models.subject import androidx.annotation.Keep +import com.simprints.core.ExcludedFromGeneratedTestCoverageReports import com.simprints.core.domain.externalcredential.ExternalCredential import java.util.UUID @Keep +@ExcludedFromGeneratedTestCoverageReports("Data class") data class EnrolmentRecordUpdateEvent( override val id: String, val payload: EnrolmentRecordUpdatePayload, From d80695984c5822505f70cc07a0b0b70c707dfdf3 Mon Sep 17 00:00:00 2001 From: alex Date: Sun, 10 Aug 2025 19:48:21 +0300 Subject: [PATCH 008/139] =?UTF-8?q?[CORE-3421]=20Fixing=20tests=CB=86?= =?UTF-8?q?=CB=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RoomEnrolmentRecordLocalDataSourceTest.kt | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/RoomEnrolmentRecordLocalDataSourceTest.kt b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/RoomEnrolmentRecordLocalDataSourceTest.kt index d1c1644f7e..d1302a8cd3 100644 --- a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/RoomEnrolmentRecordLocalDataSourceTest.kt +++ b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/RoomEnrolmentRecordLocalDataSourceTest.kt @@ -3,6 +3,8 @@ package com.simprints.infra.enrolment.records.repository.local import android.content.Context import androidx.test.core.app.ApplicationProvider import com.google.common.truth.Truth.assertThat +import com.simprints.core.domain.externalcredential.ExternalCredential +import com.simprints.core.domain.externalcredential.ExternalCredentialType import com.simprints.core.domain.face.FaceSample import com.simprints.core.domain.fingerprint.FingerprintSample import com.simprints.core.domain.fingerprint.IFingerIdentifier @@ -65,6 +67,14 @@ class RoomEnrolmentRecordLocalDataSourceTest { // --- Test Data --- private val date = Date() // Use a fixed date for consistent timestamps in tests + // External credentials + private val externalCredential = ExternalCredential( + value = "value".asTokenizableEncrypted(), + subjectId = "subjectId", + type = ExternalCredentialType.NHISCard + ) + + // Samples defined first private val faceSample1 = FaceSample( template = byteArrayOf(1, 2, 3), @@ -116,6 +126,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { fingerprintSamples = emptyList(), createdAt = date, updatedAt = date, + externalCredentials = listOf(getExternalCredential("subj-001")) ) private val subject2P1WithFinger = Subject( subjectId = "subj-002", @@ -126,6 +137,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { fingerprintSamples = listOf(fingerprintSample1), createdAt = date, updatedAt = date, + externalCredentials = listOf(getExternalCredential("subj-002")) ) private val subject3P1WithBoth = Subject( subjectId = "subj-003", @@ -134,6 +146,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { moduleId = MODULE_2_ID, faceSamples = listOf(faceSample2), fingerprintSamples = listOf(fingerprintSample2), + externalCredentials = listOf(getExternalCredential("subj-003")) ) private val subject4P2WithBoth = Subject( subjectId = "subj-004", @@ -144,6 +157,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { fingerprintSamples = listOf(fingerprintSample3), createdAt = date, updatedAt = date, + externalCredentials = listOf(getExternalCredential("subj-004")) ) private val subject5P2WithFace = Subject( // Added subject @@ -155,6 +169,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { fingerprintSamples = emptyList(), createdAt = Date(date.time + 1000), // Slightly different time updatedAt = Date(date.time + 1000), + externalCredentials = listOf(getExternalCredential("subj-005")) ) private val subject6P2WithFinger = Subject( // Added subject @@ -166,6 +181,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { fingerprintSamples = listOf(fingerprintSample3.copy(id = UUID.randomUUID().toString())), createdAt = Date(date.time + 2000), // Different time updatedAt = Date(date.time + 2000), + externalCredentials = listOf(getExternalCredential("subj-006")) ) private val subjectInvalidNoSamples = Subject( subjectId = "subj-invalid", @@ -174,6 +190,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { moduleId = MODULE_1_ID, createdAt = date, updatedAt = date, + externalCredentials = listOf(getExternalCredential("subj-invalid")) ) private val project: Project = mockk() @@ -213,6 +230,12 @@ class RoomEnrolmentRecordLocalDataSourceTest { SubjectAction.Creation(subject6P2WithFinger), ) + private fun getExternalCredential(subjectId: String) = ExternalCredential( + value = "value".asTokenizableEncrypted(), + subjectId = subjectId, + type = ExternalCredentialType.NHISCard + ) + private suspend fun setupInitialData() { dataSource.performActions( initialData, From 6aedb12aebd404507d7fd7d9e384bb80063b8772 Mon Sep 17 00:00:00 2001 From: alex Date: Sun, 10 Aug 2025 20:26:51 +0300 Subject: [PATCH 009/139] [CORE-3421] Fixing tests --- .../response/CreateEnrolResponseUseCaseTest.kt | 18 ++++++++++++++++-- .../sync/down/tasks/SubjectFactoryTest.kt | 4 ++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/CreateEnrolResponseUseCaseTest.kt b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/CreateEnrolResponseUseCaseTest.kt index 476ef8afec..4112ef3859 100644 --- a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/CreateEnrolResponseUseCaseTest.kt +++ b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/CreateEnrolResponseUseCaseTest.kt @@ -49,7 +49,14 @@ internal class CreateEnrolResponseUseCaseTest { @Test fun `Converts correct results to response`() = runTest { every { - subjectFactory.buildSubjectFromCaptureResults(any(), any(), any(), any(), any()) + subjectFactory.buildSubjectFromCaptureResults( + projectId = any(), + attendantId = any(), + moduleId = any(), + fingerprintResponse = any(), + faceResponse = any(), + externalCredential = any() + ) } returns mockk { every { subjectId } returns "guid" } assertThat( @@ -68,7 +75,14 @@ internal class CreateEnrolResponseUseCaseTest { @Test fun `Returns error if no valid response`() = runTest { every { - subjectFactory.buildSubjectFromCaptureResults(any(), any(), any(), null, null) + subjectFactory.buildSubjectFromCaptureResults( + projectId = any(), + attendantId = any(), + moduleId = any(), + fingerprintResponse = null, + faceResponse = null, + externalCredential = null + ) } throws MissingCaptureException() assertThat(useCase(action, emptyList(), project)).isInstanceOf(AppErrorResponse::class.java) 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 29fe72c30c..0b470f6a68 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 @@ -245,6 +245,7 @@ class SubjectFactoryTest { referenceId = REFERENCE_ID, ), ), + externalCredentials = listOf(EXTERNAL_CREDENTIAL) ) val result = factory.buildSubjectFromCaptureResults( @@ -282,6 +283,7 @@ class SubjectFactoryTest { ), ), ), + externalCredential = EXTERNAL_CREDENTIAL ) assertThat(result).isEqualTo(expected) } @@ -308,6 +310,7 @@ class SubjectFactoryTest { referenceId = REFERENCE_ID, ), ), + externalCredentials = listOf(EXTERNAL_CREDENTIAL) ) val result = factory.buildSubject( @@ -317,6 +320,7 @@ class SubjectFactoryTest { moduleId = expected.moduleId, fingerprintSamples = expected.fingerprintSamples, faceSamples = expected.faceSamples, + externalCredential = expected.externalCredentials.first() ) assertThat(result).isEqualTo(expected) } From 23a6c01807efc22309ac52977b8e6315da96b8eb Mon Sep 17 00:00:00 2001 From: alex Date: Tue, 12 Aug 2025 12:30:54 +0300 Subject: [PATCH 010/139] =?UTF-8?q?[CORE-3421]=20Adding=201->2=20Room=20mi?= =?UTF-8?q?gration=CB=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/build.gradle.kts | 1 + .../room/store/migration/Migration1to2Test.kt | 42 +++++++++++++++++++ .../records/room/store/SubjectsDatabase.kt | 7 +++- .../room/store/migration/RoomMigrations.kt | 27 ++++++++++++ 4 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/room/store/migration/Migration1to2Test.kt create mode 100644 infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/migration/RoomMigrations.kt diff --git a/infra/enrolment-records/repository/build.gradle.kts b/infra/enrolment-records/repository/build.gradle.kts index e87c549e8a..085e5cbe1f 100644 --- a/infra/enrolment-records/repository/build.gradle.kts +++ b/infra/enrolment-records/repository/build.gradle.kts @@ -17,4 +17,5 @@ dependencies { implementation(libs.libsimprints) implementation(libs.retrofit.core) implementation(libs.jackson.core) + implementation(libs.testing.androidX.room) } diff --git a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/room/store/migration/Migration1to2Test.kt b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/room/store/migration/Migration1to2Test.kt new file mode 100644 index 0000000000..7d10aa6065 --- /dev/null +++ b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/room/store/migration/Migration1to2Test.kt @@ -0,0 +1,42 @@ +package com.simprints.infra.enrolment.records.room.store.migration + +import androidx.room.testing.MigrationTestHelper +import androidx.test.ext.junit.runners.* +import androidx.test.platform.app.* +import com.google.common.truth.Truth.* +import com.simprints.infra.enrolment.records.room.store.SubjectsDatabase +import org.junit.Rule +import org.junit.runner.RunWith +import kotlin.test.Test + +@RunWith(AndroidJUnit4::class) +class Migration1to2Test { + + @get:Rule + val helper = MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + SubjectsDatabase::class.java, + ) + + @Test + fun `when migrating from 1 to 2 then external credential table should be added`() { + val db1 = helper.createDatabase(name = TEST_DB, version = 1) + val db2 = helper.runMigrationsAndValidate( + name = TEST_DB, + version = 2, + validateDroppedTables = true, + MIGRATION_1_2 + ) + + // Verify external credentials table exists + val cursor = db2.query("SELECT name FROM sqlite_master WHERE name='DbDbExternalCredential'") + assertThat(cursor.count).isEqualTo(1) + cursor.close() + db1.close() + db2.close() + } + + companion object { + private const val TEST_DB = "migration-test" + } +} diff --git a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/SubjectsDatabase.kt b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/SubjectsDatabase.kt index 7b4e9f3641..ca12b4fca9 100644 --- a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/SubjectsDatabase.kt +++ b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/SubjectsDatabase.kt @@ -5,6 +5,7 @@ import androidx.annotation.Keep import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase +import com.simprints.infra.enrolment.records.room.store.migration.MIGRATION_1_2 import com.simprints.infra.enrolment.records.room.store.models.DbBiometricTemplate import com.simprints.infra.enrolment.records.room.store.models.DbExternalCredential import com.simprints.infra.enrolment.records.room.store.models.DbSubject @@ -18,7 +19,7 @@ import javax.inject.Singleton DbBiometricTemplate::class, DbExternalCredential::class, ], - version = 1, + version = 2, exportSchema = true, ) @Keep @@ -31,7 +32,9 @@ abstract class SubjectsDatabase : RoomDatabase() { factory: SupportOpenHelperFactory, dbName: String, ): SubjectsDatabase { - val builder = Room.databaseBuilder(context, SubjectsDatabase::class.java, dbName) + val builder = Room + .databaseBuilder(context, SubjectsDatabase::class.java, dbName) + .addMigrations(MIGRATION_1_2) if (BuildConfig.DB_ENCRYPTION) { builder.openHelperFactory(factory) } diff --git a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/migration/RoomMigrations.kt b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/migration/RoomMigrations.kt new file mode 100644 index 0000000000..e59ce0f544 --- /dev/null +++ b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/migration/RoomMigrations.kt @@ -0,0 +1,27 @@ +package com.simprints.infra.enrolment.records.room.store.migration + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +/** + * Schema version 1 -> 2 + * + * Changes: + * - Adding [DbExternalCredential] entity + * */ +val MIGRATION_1_2 = object : Migration(1, 2) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( + """ + CREATE TABLE IF NOT EXISTS `DbDbExternalCredential` ( + `value` TEXT NOT NULL, + `subjectId` TEXT NOT NULL, + `type` TEXT NOT NULL, + PRIMARY KEY(`value`, `subjectId`), + FOREIGN KEY(`subjectId`) REFERENCES `DbSubject`(`subjectId`) ON DELETE CASCADE + ) + """.trimIndent() + ) + } +} + From 7c2c5ff6627fc9a6cdf383a279f38004439f53b5 Mon Sep 17 00:00:00 2001 From: alex Date: Tue, 12 Aug 2025 12:44:55 +0300 Subject: [PATCH 011/139] [CORE-3421] Moving Room DB version to a static field so that it can be dynamically referenced in the tests --- .../local/RoomEnrolmentRecordLocalDataSourceTest.kt | 3 ++- .../infra/enrolment/records/room/store/SubjectsDatabase.kt | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/RoomEnrolmentRecordLocalDataSourceTest.kt b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/RoomEnrolmentRecordLocalDataSourceTest.kt index d1302a8cd3..5a37523cff 100644 --- a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/RoomEnrolmentRecordLocalDataSourceTest.kt +++ b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/RoomEnrolmentRecordLocalDataSourceTest.kt @@ -15,6 +15,7 @@ import com.simprints.infra.enrolment.records.repository.domain.models.BiometricD 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.SubjectQuery +import com.simprints.infra.enrolment.records.room.store.SubjectsDatabase.Companion.SUBJECT_DB_VERSION import com.simprints.infra.enrolment.records.room.store.SubjectsDatabaseFactory import com.simprints.infra.security.keyprovider.LocalDbKey import com.simprints.testtools.common.coroutines.TestCoroutineRule @@ -1484,7 +1485,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { val result = dataSource.getLocalDBInfo() // Then assertThat(result).contains("Database Name: db-subjects") - assertThat(result).contains("Database Version: 1") + assertThat(result).contains("Database Version: $SUBJECT_DB_VERSION") assertThat(result).contains("Is Encrypted: false") // db not encrypted in tests assertThat(result).contains("Number of Subjects: 6") } diff --git a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/SubjectsDatabase.kt b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/SubjectsDatabase.kt index ca12b4fca9..422c0dc33c 100644 --- a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/SubjectsDatabase.kt +++ b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/SubjectsDatabase.kt @@ -5,6 +5,7 @@ import androidx.annotation.Keep import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase +import com.simprints.infra.enrolment.records.room.store.SubjectsDatabase.Companion.SUBJECT_DB_VERSION import com.simprints.infra.enrolment.records.room.store.migration.MIGRATION_1_2 import com.simprints.infra.enrolment.records.room.store.models.DbBiometricTemplate import com.simprints.infra.enrolment.records.room.store.models.DbExternalCredential @@ -19,7 +20,7 @@ import javax.inject.Singleton DbBiometricTemplate::class, DbExternalCredential::class, ], - version = 2, + version = SUBJECT_DB_VERSION, exportSchema = true, ) @Keep @@ -40,5 +41,6 @@ abstract class SubjectsDatabase : RoomDatabase() { } return builder.build() } + const val SUBJECT_DB_VERSION = 2 } } From 3036a3aac00ccc0cdf615094c375208a80c476b4 Mon Sep 17 00:00:00 2001 From: alex Date: Tue, 12 Aug 2025 13:22:17 +0300 Subject: [PATCH 012/139] [CORE-3421] Updating tests --- .../infra/eventsync/sync/down/tasks/EventDownSyncTaskTest.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/EventDownSyncTaskTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/EventDownSyncTaskTest.kt index 3962485c2b..0933803c1f 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/EventDownSyncTaskTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/EventDownSyncTaskTest.kt @@ -532,6 +532,9 @@ class EventDownSyncTaskTest { faceSamples = listOf( FaceSample(byteArrayOf(), "format", "referenceId"), ), + externalCredentials = listOf( + ExternalCredential(value = "value".asTokenizableEncrypted(), subjectId = "subjectId", type = ExternalCredentialType.NHISCard) + ) ), ) From f0d359ca3867b7d83d8136be3c185b9731d7cd51 Mon Sep 17 00:00:00 2001 From: alex Date: Tue, 12 Aug 2025 13:22:31 +0300 Subject: [PATCH 013/139] [CORE-3421] Removing unused classes --- .../repository/ExternalCredentialRepository.kt | 8 -------- .../enrolment/records/room/store/SubjectDao.kt | 1 - .../store/models/SubjectExternalCredentials.kt | 14 -------------- 3 files changed, 23 deletions(-) delete mode 100644 infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/ExternalCredentialRepository.kt delete mode 100644 infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/SubjectExternalCredentials.kt diff --git a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/ExternalCredentialRepository.kt b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/ExternalCredentialRepository.kt deleted file mode 100644 index aac8a37e6a..0000000000 --- a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/ExternalCredentialRepository.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.simprints.infra.enrolment.records.repository - -import javax.inject.Inject - -internal class ExternalCredentialRepository @Inject constructor( - -){ -} diff --git a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/SubjectDao.kt b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/SubjectDao.kt index d682a67b2a..e331ae3ebc 100644 --- a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/SubjectDao.kt +++ b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/SubjectDao.kt @@ -11,7 +11,6 @@ import com.simprints.infra.enrolment.records.room.store.models.DbBiometricTempla import com.simprints.infra.enrolment.records.room.store.models.DbExternalCredential import com.simprints.infra.enrolment.records.room.store.models.DbSubject import com.simprints.infra.enrolment.records.room.store.models.SubjectBiometrics -import com.simprints.infra.enrolment.records.room.store.models.SubjectExternalCredentials @Dao interface SubjectDao { diff --git a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/SubjectExternalCredentials.kt b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/SubjectExternalCredentials.kt deleted file mode 100644 index f710dfa45a..0000000000 --- a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/SubjectExternalCredentials.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.simprints.infra.enrolment.records.room.store.models - -import androidx.room.Embedded -import androidx.room.Relation -import com.simprints.infra.enrolment.records.room.store.models.DbSubject.Companion.SUBJECT_ID_COLUMN - -data class SubjectExternalCredentials( - @Embedded val subject: DbSubject, - @Relation( - parentColumn = SUBJECT_ID_COLUMN, - entityColumn = SUBJECT_ID_COLUMN, - ) - val externalCredentials: List, -) From e445a48028295c8b1835d2c602238decca83d0a0 Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 25 Aug 2025 17:05:17 +0300 Subject: [PATCH 014/139] [CORE-3421] External credentials are now stored as list on the EnrolmentRecordCreationEvent. Renaming `DbDbExternalCredential` to `DbExternalCredential` --- ...GetEnrolmentCreationEventForSubjectUseCase.kt | 2 +- .../screen/usecase/BuildSubjectUseCase.kt | 2 +- .../room/store/migration/Migration1to2Test.kt | 2 +- .../1.json | 4 ++-- .../enrolment/records/room/store/SubjectDao.kt | 4 ++-- .../room/store/migration/RoomMigrations.kt | 2 +- .../room/store/models/DbExternalCredential.kt | 2 +- .../subject/ApiEnrolmentRecordCreationPayload.kt | 2 +- .../eventsync/sync/down/tasks/SubjectFactory.kt | 10 +++++----- .../sync/down/tasks/EventDownSyncTaskTest.kt | 16 +++++++++++----- ...ncEnrolmentRecordCreationEventDeserializer.kt | 2 +- .../subject/EnrolmentRecordCreationEvent.kt | 6 +++--- 12 files changed, 30 insertions(+), 24 deletions(-) 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 40359a9ac5..cba8da95f5 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 @@ -53,7 +53,7 @@ internal class GetEnrolmentCreationEventForSubjectUseCase @Inject constructor( 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 - externalCredential = externalCredentials.firstOrNull() + externalCredentials = externalCredentials ) companion object { diff --git a/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/screen/usecase/BuildSubjectUseCase.kt b/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/screen/usecase/BuildSubjectUseCase.kt index b537dda31c..b232c0c3e4 100644 --- a/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/screen/usecase/BuildSubjectUseCase.kt +++ b/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/screen/usecase/BuildSubjectUseCase.kt @@ -32,7 +32,7 @@ internal class BuildSubjectUseCase @Inject constructor( faceSamples = getFaceCaptureResult(params.steps) ?.let { result -> result.results.map { faceSample(result.referenceId, it) } } .orEmpty(), - externalCredential = getExternalCredentialResult(params.steps) + externalCredentials = getExternalCredentialResult(params.steps)?.let { listOf(it) } ?: emptyList() ) // TODO [CORE-3421] When an external credential can be extracted from the UI-level steps, extract it here diff --git a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/room/store/migration/Migration1to2Test.kt b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/room/store/migration/Migration1to2Test.kt index 7d10aa6065..5cda6f65fe 100644 --- a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/room/store/migration/Migration1to2Test.kt +++ b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/room/store/migration/Migration1to2Test.kt @@ -29,7 +29,7 @@ class Migration1to2Test { ) // Verify external credentials table exists - val cursor = db2.query("SELECT name FROM sqlite_master WHERE name='DbDbExternalCredential'") + val cursor = db2.query("SELECT name FROM sqlite_master WHERE name='DbExternalCredential'") assertThat(cursor.count).isEqualTo(1) cursor.close() db1.close() diff --git a/infra/enrolment-records/room-store/schemas/com.simprints.infra.enrolment.records.room.store.SubjectsDatabase/1.json b/infra/enrolment-records/room-store/schemas/com.simprints.infra.enrolment.records.room.store.SubjectsDatabase/1.json index 365053e35b..4df59295d1 100644 --- a/infra/enrolment-records/room-store/schemas/com.simprints.infra.enrolment.records.room.store.SubjectsDatabase/1.json +++ b/infra/enrolment-records/room-store/schemas/com.simprints.infra.enrolment.records.room.store.SubjectsDatabase/1.json @@ -172,7 +172,7 @@ ] }, { - "tableName": "DbDbExternalCredential", + "tableName": "DbExternalCredential", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`value` TEXT NOT NULL, `subjectId` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`value`, `subjectId`), FOREIGN KEY(`subjectId`) REFERENCES `DbSubject`(`subjectId`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { @@ -221,4 +221,4 @@ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '527fcc2c704906558681cb31beddb0c3')" ] } -} \ No newline at end of file +} diff --git a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/SubjectDao.kt b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/SubjectDao.kt index e331ae3ebc..0648f165f2 100644 --- a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/SubjectDao.kt +++ b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/SubjectDao.kt @@ -30,7 +30,7 @@ interface SubjectDao { @Query("DELETE FROM DbBiometricTemplate WHERE uuid = :uuid") suspend fun deleteBiometricSample(uuid: String) - @Query("DELETE FROM DbDbExternalCredential WHERE value = :value") + @Query("DELETE FROM DbExternalCredential WHERE value = :value") suspend fun deleteExternalCredential(value: String) @RawQuery @@ -42,7 +42,7 @@ interface SubjectDao { @RawQuery suspend fun loadSubjects(query: SupportSQLiteQuery): List - @Query("SELECT * FROM DbDbExternalCredential") + @Query("SELECT * FROM DbExternalCredential") suspend fun getAllExternalCredentials(): List @RawQuery diff --git a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/migration/RoomMigrations.kt b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/migration/RoomMigrations.kt index e59ce0f544..106bbdc2a3 100644 --- a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/migration/RoomMigrations.kt +++ b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/migration/RoomMigrations.kt @@ -13,7 +13,7 @@ val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(db: SupportSQLiteDatabase) { db.execSQL( """ - CREATE TABLE IF NOT EXISTS `DbDbExternalCredential` ( + CREATE TABLE IF NOT EXISTS `DbExternalCredential` ( `value` TEXT NOT NULL, `subjectId` TEXT NOT NULL, `type` TEXT NOT NULL, diff --git a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/DbExternalCredential.kt b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/DbExternalCredential.kt index 7468c1dbc2..f476ef190c 100644 --- a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/DbExternalCredential.kt +++ b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/DbExternalCredential.kt @@ -28,6 +28,6 @@ data class DbExternalCredential( ) { companion object { const val EXTERNAL_CREDENTIAL_VALUE_COLUMN = "value" - const val EXTERNAL_CREDENTIAL_TABLE_NAME = "DbDbExternalCredential" + const val EXTERNAL_CREDENTIAL_TABLE_NAME = "DbExternalCredential" } } 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 774d68c9d3..dc5b0e7178 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 @@ -26,5 +26,5 @@ internal fun ApiEnrolmentRecordCreationPayload.fromApiToDomain() = EnrolmentReco moduleId = moduleId.asTokenizableEncrypted(), attendantId = attendantId.asTokenizableEncrypted(), biometricReferences = biometricReferences?.map { it.fromApiToDomain() } ?: emptyList(), - externalCredential = externalCredential?.fromApiToDomain(subjectId) + externalCredentials = externalCredential?.let { listOf(it.fromApiToDomain(subjectId)) } ?: emptyList() ) diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/tasks/SubjectFactory.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/tasks/SubjectFactory.kt index 6b360f69ec..38eaf5d2e6 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/tasks/SubjectFactory.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/tasks/SubjectFactory.kt @@ -33,7 +33,7 @@ class SubjectFactory @Inject constructor( moduleId = moduleId, fingerprintSamples = extractFingerprintSamplesFromBiometricReferences(this.biometricReferences), faceSamples = extractFaceSamplesFromBiometricReferences(this.biometricReferences), - externalCredential = payload.externalCredential, + externalCredentials = payload.externalCredentials, ) } @@ -45,7 +45,7 @@ class SubjectFactory @Inject constructor( moduleId = moduleId, fingerprintSamples = extractFingerprintSamplesFromBiometricReferences(this.biometricReferences), faceSamples = extractFaceSamplesFromBiometricReferences(this.biometricReferences), - externalCredential = null, + externalCredentials = emptyList(), ) } @@ -84,7 +84,7 @@ class SubjectFactory @Inject constructor( createdAt = Date(timeHelper.now().ms), fingerprintSamples = fingerprintResponse?.let { extractFingerprintSamples(it) }.orEmpty(), faceSamples = faceResponse?.let { extractFaceSamples(it) }.orEmpty(), - externalCredential = externalCredential, + externalCredentials = externalCredential?.let { listOf(it) } ?: emptyList(), ) } @@ -97,7 +97,7 @@ class SubjectFactory @Inject constructor( updatedAt: Date? = null, fingerprintSamples: List = emptyList(), faceSamples: List = emptyList(), - externalCredential: ExternalCredential? = null, + externalCredentials: List = emptyList(), ) = Subject( subjectId = subjectId, projectId = projectId, @@ -107,7 +107,7 @@ class SubjectFactory @Inject constructor( updatedAt = updatedAt, fingerprintSamples = fingerprintSamples, faceSamples = faceSamples, - externalCredentials = externalCredential?.let { listOf(it) } ?: emptyList() + externalCredentials = externalCredentials ) private fun extractFingerprintSamples(fingerprintResponse: FingerprintCaptureResult) = diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/EventDownSyncTaskTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/EventDownSyncTaskTest.kt index 0933803c1f..57b852bfca 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/EventDownSyncTaskTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/EventDownSyncTaskTest.kt @@ -66,10 +66,12 @@ class EventDownSyncTaskTest { moduleId = "moduleId".asTokenizableRaw(), attendantId = "attendantId".asTokenizableRaw(), biometricReferences = listOf(FaceReference("id", listOf(FaceTemplate("template")), "format")), - externalCredential = ExternalCredential( - value = "value".asTokenizableEncrypted(), - subjectId = "subjectId", - type = ExternalCredentialType.NHISCard + externalCredentials = listOf( + ExternalCredential( + value = "value".asTokenizableEncrypted(), + subjectId = "subjectId", + type = ExternalCredentialType.NHISCard + ) ), ) val ENROLMENT_RECORD_MOVE_MODULE = EnrolmentRecordMoveEvent( @@ -533,7 +535,11 @@ class EventDownSyncTaskTest { FaceSample(byteArrayOf(), "format", "referenceId"), ), externalCredentials = listOf( - ExternalCredential(value = "value".asTokenizableEncrypted(), subjectId = "subjectId", type = ExternalCredentialType.NHISCard) + ExternalCredential( + value = "value".asTokenizableEncrypted(), + subjectId = "subjectId", + type = ExternalCredentialType.NHISCard + ) ) ), ) diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/cosync/CoSyncEnrolmentRecordCreationEventDeserializer.kt b/infra/events/src/main/java/com/simprints/infra/events/event/cosync/CoSyncEnrolmentRecordCreationEventDeserializer.kt index 6f26ffe9ca..df5939165b 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/cosync/CoSyncEnrolmentRecordCreationEventDeserializer.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/cosync/CoSyncEnrolmentRecordCreationEventDeserializer.kt @@ -56,7 +56,7 @@ class CoSyncEnrolmentRecordCreationEventDeserializer : attendantId = attendantId, biometricReferences = biometricReferences, // TODO [CORE-3421] Update when CoSync supports external credentials (MfID) - externalCredential = null + externalCredentials = emptyList() ), ) } diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordCreationEvent.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordCreationEvent.kt index 3fc585095a..3e17b20424 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordCreationEvent.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordCreationEvent.kt @@ -21,7 +21,7 @@ data class EnrolmentRecordCreationEvent( moduleId: TokenizableString, attendantId: TokenizableString, biometricReferences: List, - externalCredential: ExternalCredential?, + externalCredentials: List, ) : this( UUID.randomUUID().toString(), EnrolmentRecordCreationPayload( @@ -30,7 +30,7 @@ data class EnrolmentRecordCreationEvent( moduleId = moduleId, attendantId = attendantId, biometricReferences = biometricReferences, - externalCredential = externalCredential, + externalCredentials = externalCredentials, ), ) @@ -41,7 +41,7 @@ data class EnrolmentRecordCreationEvent( val moduleId: TokenizableString, val attendantId: TokenizableString, val biometricReferences: List, - val externalCredential: ExternalCredential? + val externalCredentials: List ) companion object { From b372abe4556b75991acba58d904fe2e3ec784aa8 Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 25 Aug 2025 17:13:57 +0300 Subject: [PATCH 015/139] [CORE-3421] Moving external credential Realm mapper to its separate file --- .../local/models/RealmExternalCredentialConverter.kt | 10 ++++++++++ .../local/models/RealmFingerprintSampleConverter.kt | 8 -------- 2 files changed, 10 insertions(+), 8 deletions(-) create mode 100644 infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RealmExternalCredentialConverter.kt diff --git a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RealmExternalCredentialConverter.kt b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RealmExternalCredentialConverter.kt new file mode 100644 index 0000000000..b002d06ac6 --- /dev/null +++ b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RealmExternalCredentialConverter.kt @@ -0,0 +1,10 @@ +package com.simprints.infra.enrolment.records.repository.local.models + +import com.simprints.core.domain.externalcredential.ExternalCredential +import com.simprints.infra.enrolment.records.realm.store.models.DbExternalCredential as RealmExternalCredential + +internal fun ExternalCredential.toRealmDb(): RealmExternalCredential = RealmExternalCredential().also { sample -> + sample.value = value.value + sample.subjectId = subjectId + sample.type = type.toString() +} diff --git a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RealmFingerprintSampleConverter.kt b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RealmFingerprintSampleConverter.kt index ebfab4e594..6b2be097ba 100644 --- a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RealmFingerprintSampleConverter.kt +++ b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RealmFingerprintSampleConverter.kt @@ -1,9 +1,7 @@ package com.simprints.infra.enrolment.records.repository.local.models -import com.simprints.core.domain.externalcredential.ExternalCredential import com.simprints.core.domain.fingerprint.FingerprintSample import com.simprints.infra.enrolment.records.realm.store.models.DbFingerprintSample as RealmFingerprintSample -import com.simprints.infra.enrolment.records.realm.store.models.DbExternalCredential as RealmExternalCredential internal fun RealmFingerprintSample.toDomain(): FingerprintSample = FingerprintSample( id = id, @@ -20,9 +18,3 @@ internal fun FingerprintSample.toRealmDb(): RealmFingerprintSample = RealmFinger sample.template = template sample.format = format } - -internal fun ExternalCredential.toRealmDb(): RealmExternalCredential = RealmExternalCredential().also { sample -> - sample.value = value.value - sample.subjectId = subjectId - sample.type = type.toString() -} From b715b1bfa140055621f0a909dea6e1549f45d96b Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 25 Aug 2025 17:35:16 +0300 Subject: [PATCH 016/139] =?UTF-8?q?[CORE-3421]=20EnrolmentRecordMoveEvent?= =?UTF-8?q?=20now=20contains=20external=20credential=CB=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../records/room/store/SubjectDao.kt | 1 - .../subject/ApiEnrolmentRecordMovePayload.kt | 20 ++++++++++--------- .../sync/down/tasks/SubjectFactory.kt | 2 +- .../subject/EnrolmentRecordMoveEvent.kt | 2 ++ 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/SubjectDao.kt b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/SubjectDao.kt index 0648f165f2..ffdba547e4 100644 --- a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/SubjectDao.kt +++ b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/SubjectDao.kt @@ -14,7 +14,6 @@ import com.simprints.infra.enrolment.records.room.store.models.SubjectBiometrics @Dao interface SubjectDao { - /*Remaining method*/ @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertSubject(subject: DbSubject) diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordMovePayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordMovePayload.kt index 87a8b7d67a..53e5492ab7 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordMovePayload.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordMovePayload.kt @@ -31,25 +31,27 @@ internal data class ApiEnrolmentRecordMovePayload( val moduleId: String, val attendantId: String, val biometricReferences: List?, + val externalCredential: ApiExternalCredential?, ) } internal fun ApiEnrolmentRecordMovePayload.fromApiToDomain() = EnrolmentRecordMoveEvent.EnrolmentRecordMovePayload( with(enrolmentRecordCreation) { EnrolmentRecordCreationInMove( - subjectId, - projectId, - moduleId.asTokenizableEncrypted(), - attendantId.asTokenizableEncrypted(), - biometricReferences?.map { it.fromApiToDomain() }, + subjectId = subjectId, + projectId = projectId, + moduleId = moduleId.asTokenizableEncrypted(), + attendantId = attendantId.asTokenizableEncrypted(), + biometricReferences = biometricReferences?.map { it.fromApiToDomain() }, + externalCredential = externalCredential?.fromApiToDomain(subjectId) ) }, enrolmentRecordDeletion.let { EnrolmentRecordDeletionInMove( - it.subjectId, - it.projectId, - it.moduleId.asTokenizableEncrypted(), - it.attendantId.asTokenizableEncrypted(), + subjectId = it.subjectId, + projectId = it.projectId, + moduleId = it.moduleId.asTokenizableEncrypted(), + attendantId = it.attendantId.asTokenizableEncrypted(), ) }, ) diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/tasks/SubjectFactory.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/tasks/SubjectFactory.kt index 38eaf5d2e6..920e0cf22e 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/tasks/SubjectFactory.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/tasks/SubjectFactory.kt @@ -45,7 +45,7 @@ class SubjectFactory @Inject constructor( moduleId = moduleId, fingerprintSamples = extractFingerprintSamplesFromBiometricReferences(this.biometricReferences), faceSamples = extractFaceSamplesFromBiometricReferences(this.biometricReferences), - externalCredentials = emptyList(), + externalCredentials = externalCredential?.let { listOf(it) } ?: emptyList(), ) } diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordMoveEvent.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordMoveEvent.kt index 32dcd17da6..21392b5452 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordMoveEvent.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordMoveEvent.kt @@ -1,6 +1,7 @@ package com.simprints.infra.events.event.domain.models.subject import androidx.annotation.Keep +import com.simprints.core.domain.externalcredential.ExternalCredential import com.simprints.core.domain.tokenization.TokenizableString import java.util.UUID @@ -35,5 +36,6 @@ data class EnrolmentRecordMoveEvent( val moduleId: TokenizableString, val attendantId: TokenizableString, val biometricReferences: List?, + val externalCredential: ExternalCredential?, ) } From 16e166a641f1b7fb95a848e1f07b11b0a45f84b7 Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 25 Aug 2025 17:39:19 +0300 Subject: [PATCH 017/139] [CORE-3421] Reverting addition of the external credentials in SubjectBiometrics class --- .../records/room/store/models/SubjectBiometrics.kt | 6 ------ 1 file changed, 6 deletions(-) diff --git a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/SubjectBiometrics.kt b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/SubjectBiometrics.kt index d267894645..1ea2f722d2 100644 --- a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/SubjectBiometrics.kt +++ b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/SubjectBiometrics.kt @@ -11,10 +11,4 @@ data class SubjectBiometrics( entityColumn = SUBJECT_ID_COLUMN, ) val biometricTemplates: List, - @Relation( - parentColumn = SUBJECT_ID_COLUMN, - entityColumn = SUBJECT_ID_COLUMN, - ) - /** New field */ - val externalCredentials: List, ) From a3ed7f645ee52312752995d06b2c0482ecbc9f9b Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 25 Aug 2025 18:26:22 +0300 Subject: [PATCH 018/139] [CORE-3421] Addition of the external credentials back in SubjectBiometrics class. Otherwise, it creates more overhead to populate the credentials field --- .../enrolment/records/room/store/models/SubjectBiometrics.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/SubjectBiometrics.kt b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/SubjectBiometrics.kt index 1ea2f722d2..d35bff3420 100644 --- a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/SubjectBiometrics.kt +++ b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/SubjectBiometrics.kt @@ -11,4 +11,9 @@ data class SubjectBiometrics( entityColumn = SUBJECT_ID_COLUMN, ) val biometricTemplates: List, + @Relation( + parentColumn = SUBJECT_ID_COLUMN, + entityColumn = SUBJECT_ID_COLUMN, + ) + val externalCredentials: List, ) From 37d8cb43b07b53425d525c2dc5c7bf3b020149d9 Mon Sep 17 00:00:00 2001 From: alex Date: Tue, 26 Aug 2025 10:59:07 +0300 Subject: [PATCH 019/139] [CORE-3421] Fixing tests --- .../ApiEnrolmentRecordCreationEventTest.kt | 10 ++++++---- .../subject/ApiEnrolmentRecordMoveEventTest.kt | 12 ++++++++++++ .../sync/down/tasks/EventDownSyncTaskTest.kt | 15 +++++++++++++++ .../sync/down/tasks/SubjectFactoryTest.kt | 5 +++-- 4 files changed, 36 insertions(+), 6 deletions(-) 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 74fa6fa139..769eee6696 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 @@ -49,10 +49,12 @@ class ApiEnrolmentRecordCreationEventTest { "NEC_1", ), ), - externalCredential = ExternalCredential( - value = "value".asTokenizableEncrypted(), - subjectId = "subjectId", - type = ExternalCredentialType.NHISCard + externalCredentials = listOf( + ExternalCredential( + value = "value".asTokenizableEncrypted(), + subjectId = "subjectId", + 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 c1ae48e35e..8c13aa8acb 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 @@ -1,6 +1,8 @@ package com.simprints.infra.eventsync.event.remote.models.subject import com.google.common.truth.Truth.assertThat +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.events.event.domain.models.subject.EnrolmentRecordMoveEvent @@ -28,6 +30,11 @@ class ApiEnrolmentRecordMoveEventTest { "NEC_1", ), ), + ApiExternalCredential( + value = "value", + id = "subjectId", + type = ExternalCredentialType.NHISCard.toString() + ) ), ApiEnrolmentRecordMovePayload.ApiEnrolmentRecordDeletionInMove( "subjectId", @@ -51,6 +58,11 @@ class ApiEnrolmentRecordMoveEventTest { "NEC_1", ), ), + ExternalCredential( + value = "value".asTokenizableEncrypted(), + subjectId = "subjectId", + type = ExternalCredentialType.NHISCard + ) ), EnrolmentRecordMoveEvent.EnrolmentRecordDeletionInMove( "subjectId", diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/EventDownSyncTaskTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/EventDownSyncTaskTest.kt index 57b852bfca..5de7881756 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/EventDownSyncTaskTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/EventDownSyncTaskTest.kt @@ -81,6 +81,11 @@ class EventDownSyncTaskTest { DEFAULT_MODULE_ID_2, "attendantId".asTokenizableRaw(), listOf(FaceReference("id", listOf(FaceTemplate("template")), "format")), + ExternalCredential( + value = "value".asTokenizableEncrypted(), + subjectId = "subjectId", + type = ExternalCredentialType.NHISCard + ) ), EnrolmentRecordMoveEvent.EnrolmentRecordDeletionInMove( "subjectId", @@ -96,6 +101,11 @@ class EventDownSyncTaskTest { "moduleId".asTokenizableRaw(), DEFAULT_USER_ID, listOf(FaceReference("id", listOf(FaceTemplate("template")), "format")), + ExternalCredential( + value = "value".asTokenizableEncrypted(), + subjectId = "subjectId", + type = ExternalCredentialType.NHISCard + ) ), EnrolmentRecordMoveEvent.EnrolmentRecordDeletionInMove( "subjectId", @@ -111,6 +121,11 @@ class EventDownSyncTaskTest { "moduleId".asTokenizableRaw(), DEFAULT_USER_ID_2, listOf(FaceReference("id", listOf(FaceTemplate("template")), "format")), + ExternalCredential( + value = "value".asTokenizableEncrypted(), + subjectId = "subjectId", + type = ExternalCredentialType.NHISCard + ) ), EnrolmentRecordMoveEvent.EnrolmentRecordDeletionInMove( "subjectId", 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 0b470f6a68..12f17d952f 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 @@ -64,7 +64,7 @@ class SubjectFactoryTest { attendantId = ATTENDANT_ID, moduleId = MODULE_ID, biometricReferences = listOf(FINGERPRINT_REFERENCE, faceReference), - externalCredential = null + externalCredentials = emptyList() ) val result = factory.buildSubjectFromCreationPayload(payload) val expected = Subject( @@ -99,6 +99,7 @@ class SubjectFactoryTest { attendantId = ATTENDANT_ID, moduleId = MODULE_ID, biometricReferences = listOf(FINGERPRINT_REFERENCE, faceReference), + externalCredential = null ) val result = factory.buildSubjectFromMovePayload(payload) @@ -320,7 +321,7 @@ class SubjectFactoryTest { moduleId = expected.moduleId, fingerprintSamples = expected.fingerprintSamples, faceSamples = expected.faceSamples, - externalCredential = expected.externalCredentials.first() + externalCredentials = expected.externalCredentials ) assertThat(result).isEqualTo(expected) } From 05d2a00096a19116cedd2882f04491aaee6cacce Mon Sep 17 00:00:00 2001 From: alex Date: Tue, 26 Aug 2025 14:27:47 +0300 Subject: [PATCH 020/139] [CORE-3421] Adding test coverage for ExternalCredentialType --- .../models/ExternalCredentialTypeTest.kt | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 infra/config-store/src/test/java/com/simprints/infra/config/store/models/ExternalCredentialTypeTest.kt diff --git a/infra/config-store/src/test/java/com/simprints/infra/config/store/models/ExternalCredentialTypeTest.kt b/infra/config-store/src/test/java/com/simprints/infra/config/store/models/ExternalCredentialTypeTest.kt new file mode 100644 index 0000000000..45d8d0e2d4 --- /dev/null +++ b/infra/config-store/src/test/java/com/simprints/infra/config/store/models/ExternalCredentialTypeTest.kt @@ -0,0 +1,43 @@ +package com.simprints.infra.config.store.models + +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import com.simprints.core.domain.externalcredential.ExternalCredentialType +import com.simprints.infra.config.store.exceptions.InvalidProtobufEnumException +import com.simprints.infra.config.store.local.models.ProtoExternalCredentialType +import com.simprints.infra.config.store.local.models.toDomain +import com.simprints.infra.config.store.local.models.toProto +import com.simprints.testtools.common.syntax.assertThrows + +class ExternalCredentialTypeMapperTest { + + @Test + fun `should map correctly the model to proto`() { + val pairs = listOf( + ExternalCredentialType.NHISCard to ProtoExternalCredentialType.NHIS_CARD, + ExternalCredentialType.GhanaIdCard to ProtoExternalCredentialType.GHANA_ID_CARD, + ExternalCredentialType.QRCode to ProtoExternalCredentialType.QR_CODE + ) + + pairs.forEach { (domain, proto) -> + assertThat(proto).isEqualTo(domain.toProto()) + } + } + + @Test + fun `should map correctly the model from proto`() { + val pairs = listOf( + ProtoExternalCredentialType.NHIS_CARD to ExternalCredentialType.NHISCard, + ProtoExternalCredentialType.GHANA_ID_CARD to ExternalCredentialType.GhanaIdCard, + ProtoExternalCredentialType.QR_CODE to ExternalCredentialType.QRCode + ) + + pairs.forEach { (proto, domain) -> + assertThat(domain).isEqualTo(proto.toDomain()) + } + + assertThrows { + ProtoExternalCredentialType.UNRECOGNIZED.toDomain() + } + } +} From 95de9567eb764b41cc51ec6099bbb6f9d99e8bfc Mon Sep 17 00:00:00 2001 From: alex Date: Wed, 10 Sep 2025 15:20:20 +0300 Subject: [PATCH 021/139] =?UTF-8?q?[CORE-3404]=20Adding=20'id'=20field=20t?= =?UTF-8?q?o=20the=20ext=CB=86ernal=20credential=20but=20keeping=20the=20u?= =?UTF-8?q?niqueness=20by=20combining=20value+subjectId?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../externalcredential/ExternalCredential.kt | 9 ++- .../store/models/DbExternalCredential.kt | 5 -- ...entRecordLocalDataSourceIntegrationTest.kt | 17 +++++ .../RealmExternalCredentialConverter.kt | 10 +++ .../local/models/RealmSubjectConverter.kt | 4 + .../models/RoomExternalCredentialConverter.kt | 2 + ...RealmEnrolmentRecordLocalDataSourceTest.kt | 1 + .../RoomEnrolmentRecordLocalDataSourceTest.kt | 2 + .../room/store/models/DbExternalCredential.kt | 2 + .../models/subject/ApiExternalCredential.kt | 1 + .../ApiEnrolmentRecordCreationEventTest.kt | 1 + .../ApiEnrolmentRecordMoveEventTest.kt | 3 +- .../ApiEnrolmentRecordUpdateEventTest.kt | 1 + .../down/tasks/CommCareEventSyncTaskTest.kt | 75 +++++++++++++------ .../tasks/SimprintsEventDownSyncTaskTest.kt | 6 ++ .../sync/down/tasks/SubjectFactoryTest.kt | 2 + 16 files changed, 108 insertions(+), 33 deletions(-) diff --git a/infra/core/src/main/java/com/simprints/core/domain/externalcredential/ExternalCredential.kt b/infra/core/src/main/java/com/simprints/core/domain/externalcredential/ExternalCredential.kt index 478508ce21..aceb534d21 100644 --- a/infra/core/src/main/java/com/simprints/core/domain/externalcredential/ExternalCredential.kt +++ b/infra/core/src/main/java/com/simprints/core/domain/externalcredential/ExternalCredential.kt @@ -1,14 +1,15 @@ package com.simprints.core.domain.externalcredential -import android.os.Parcelable +import androidx.annotation.Keep import com.simprints.core.ExcludedFromGeneratedTestCoverageReports import com.simprints.core.domain.tokenization.TokenizableString -import kotlinx.parcelize.Parcelize +import java.io.Serializable -@Parcelize +@Keep @ExcludedFromGeneratedTestCoverageReports("Data class with generated code") data class ExternalCredential( + val id: String, val value: TokenizableString.Tokenized, val subjectId: String, val type: ExternalCredentialType, -) : Parcelable +) : Serializable diff --git a/infra/enrolment-records/realm-store/src/main/java/com/simprints/infra/enrolment/records/realm/store/models/DbExternalCredential.kt b/infra/enrolment-records/realm-store/src/main/java/com/simprints/infra/enrolment/records/realm/store/models/DbExternalCredential.kt index d076cca478..8728e08141 100644 --- a/infra/enrolment-records/realm-store/src/main/java/com/simprints/infra/enrolment/records/realm/store/models/DbExternalCredential.kt +++ b/infra/enrolment-records/realm-store/src/main/java/com/simprints/infra/enrolment/records/realm/store/models/DbExternalCredential.kt @@ -10,12 +10,7 @@ import io.realm.kotlin.types.annotations.PrimaryKey class DbExternalCredential : RealmObject { @PrimaryKey var id: String = "" - get() = "$value$SEPARATOR$subjectId" var value: String = "" var subjectId: String = "" var type: String = "" - - companion object { - const val SEPARATOR = "|" - } } diff --git a/infra/enrolment-records/repository/src/androidTest/kotlin/com/simprints/infra/enrolment/records/repository/local/RealmEnrolmentRecordLocalDataSourceIntegrationTest.kt b/infra/enrolment-records/repository/src/androidTest/kotlin/com/simprints/infra/enrolment/records/repository/local/RealmEnrolmentRecordLocalDataSourceIntegrationTest.kt index a25a98436b..00ac46d6ae 100644 --- a/infra/enrolment-records/repository/src/androidTest/kotlin/com/simprints/infra/enrolment/records/repository/local/RealmEnrolmentRecordLocalDataSourceIntegrationTest.kt +++ b/infra/enrolment-records/repository/src/androidTest/kotlin/com/simprints/infra/enrolment/records/repository/local/RealmEnrolmentRecordLocalDataSourceIntegrationTest.kt @@ -2,9 +2,12 @@ package com.simprints.infra.enrolment.records.repository.local import androidx.test.core.app.* 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 import com.simprints.core.domain.fingerprint.FingerprintSample import com.simprints.core.domain.fingerprint.IFingerIdentifier +import com.simprints.core.domain.tokenization.asTokenizableEncrypted import com.simprints.core.domain.tokenization.asTokenizableRaw import com.simprints.core.tools.time.TimeHelper import com.simprints.core.tools.time.Timestamp @@ -260,6 +263,14 @@ class RealmEnrolmentRecordLocalDataSourceIntegrationTest { ), ), referenceIdsToRemove = listOf("ref1"), + externalCredentialsToAdd = listOf( + ExternalCredential( + id = "id", + value = "value".asTokenizableEncrypted(), + subjectId = "subjectId", + type = ExternalCredentialType.NHISCard + ) + ) ) val project = mockk() // When @@ -278,6 +289,12 @@ class RealmEnrolmentRecordLocalDataSourceIntegrationTest { assertThat(savedSubject?.faceSamples?.first()?.referenceId).isEqualTo("ref2") assertThat(savedSubject?.fingerprintSamples).hasSize(1) assertThat(savedSubject?.fingerprintSamples?.first()?.referenceId).isEqualTo("ref3") + savedSubject?.externalCredentials?.first()?.let { + assertThat(it.id).isEqualTo("id") + assertThat(it.value).isEqualTo("value") + assertThat(it.subjectId).isEqualTo("subjectId") + assertThat(it.type).isEqualTo(ExternalCredentialType.NHISCard.toString()) + } } @Test diff --git a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RealmExternalCredentialConverter.kt b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RealmExternalCredentialConverter.kt index b002d06ac6..8f3e2bb4cd 100644 --- a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RealmExternalCredentialConverter.kt +++ b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RealmExternalCredentialConverter.kt @@ -1,10 +1,20 @@ package com.simprints.infra.enrolment.records.repository.local.models 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.enrolment.records.realm.store.models.DbExternalCredential as RealmExternalCredential internal fun ExternalCredential.toRealmDb(): RealmExternalCredential = RealmExternalCredential().also { sample -> + sample.id = id sample.value = value.value sample.subjectId = subjectId sample.type = type.toString() } + +internal fun RealmExternalCredential.toDomain(): ExternalCredential = ExternalCredential( + id = this.id, + value = this.value.asTokenizableEncrypted(), + subjectId = this.subjectId, + type = ExternalCredentialType.valueOf(this.type) +) diff --git a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RealmSubjectConverter.kt b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RealmSubjectConverter.kt index cb93aa32b7..5b4ceff415 100644 --- a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RealmSubjectConverter.kt +++ b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RealmSubjectConverter.kt @@ -1,10 +1,12 @@ package com.simprints.infra.enrolment.records.repository.local.models +import com.simprints.core.domain.externalcredential.ExternalCredential import com.simprints.core.domain.face.FaceSample import com.simprints.core.domain.fingerprint.FingerprintSample import com.simprints.core.domain.tokenization.asTokenizableEncrypted import com.simprints.core.domain.tokenization.asTokenizableRaw import com.simprints.core.domain.tokenization.isTokenized +import com.simprints.infra.enrolment.records.realm.store.models.DbExternalCredential import com.simprints.infra.enrolment.records.realm.store.models.DbFaceSample import com.simprints.infra.enrolment.records.realm.store.models.DbFingerprintSample import com.simprints.infra.enrolment.records.realm.store.models.toDate @@ -29,6 +31,7 @@ internal fun RealmSubject.toDomain(): Subject { updatedAt = updatedAt?.toDate(), fingerprintSamples = fingerprintSamples.map(DbFingerprintSample::toDomain), faceSamples = faceSamples.map(DbFaceSample::toDomain), + externalCredentials = externalCredentials.map(DbExternalCredential::toDomain) ) } @@ -44,4 +47,5 @@ internal fun Subject.toRealmDb(): RealmSubject = RealmSubject().also { subject - subject.faceSamples = faceSamples.map(FaceSample::toRealmDb).toRealmList() subject.isModuleIdTokenized = moduleId.isTokenized() subject.isAttendantIdTokenized = attendantId.isTokenized() + subject.externalCredentials = externalCredentials.map(ExternalCredential::toRealmDb).toRealmList() } diff --git a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RoomExternalCredentialConverter.kt b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RoomExternalCredentialConverter.kt index 9a1822ffaa..b12caae012 100644 --- a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RoomExternalCredentialConverter.kt +++ b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/local/models/RoomExternalCredentialConverter.kt @@ -6,12 +6,14 @@ import com.simprints.core.domain.tokenization.asTokenizableEncrypted import com.simprints.infra.enrolment.records.room.store.models.DbExternalCredential internal fun DbExternalCredential.toDomain(): ExternalCredential = ExternalCredential( + id = id, value = value.asTokenizableEncrypted(), subjectId = subjectId, type = ExternalCredentialType.valueOf(type) ) internal fun ExternalCredential.toRoomDb(): DbExternalCredential = DbExternalCredential( + id = id, value = value.value, subjectId = subjectId, type = type.name diff --git a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/RealmEnrolmentRecordLocalDataSourceTest.kt b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/RealmEnrolmentRecordLocalDataSourceTest.kt index 3b3b568d19..4230cc46b7 100644 --- a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/RealmEnrolmentRecordLocalDataSourceTest.kt +++ b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/RealmEnrolmentRecordLocalDataSourceTest.kt @@ -462,6 +462,7 @@ class RealmEnrolmentRecordLocalDataSourceTest { ) = FingerprintSample(IFingerIdentifier.LEFT_3RD_FINGER, Random.nextBytes(64), "fingerprintTemplateFormat", referenceId, id) private fun getRandomExternalCredential() = ExternalCredential( + id = "id", value = "value".asTokenizableEncrypted(), subjectId = "subjectId", type = ExternalCredentialType.NHISCard diff --git a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/RoomEnrolmentRecordLocalDataSourceTest.kt b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/RoomEnrolmentRecordLocalDataSourceTest.kt index 071248ab9a..464f53e411 100644 --- a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/RoomEnrolmentRecordLocalDataSourceTest.kt +++ b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/RoomEnrolmentRecordLocalDataSourceTest.kt @@ -73,6 +73,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { // External credentials private val externalCredential = ExternalCredential( + id = "id", value = "value".asTokenizableEncrypted(), subjectId = "subjectId", type = ExternalCredentialType.NHISCard @@ -238,6 +239,7 @@ class RoomEnrolmentRecordLocalDataSourceTest { ) private fun getExternalCredential(subjectId: String) = ExternalCredential( + id = "id", value = "value".asTokenizableEncrypted(), subjectId = subjectId, type = ExternalCredentialType.NHISCard diff --git a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/DbExternalCredential.kt b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/DbExternalCredential.kt index f476ef190c..33d36197ed 100644 --- a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/DbExternalCredential.kt +++ b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/DbExternalCredential.kt @@ -20,6 +20,8 @@ import com.simprints.infra.enrolment.records.room.store.models.DbSubject.Compani ] ) data class DbExternalCredential( + // The ID is only used by BFSID for analytics. The primary key should be a composite of value+subjectId + val id: String, @ColumnInfo(name = EXTERNAL_CREDENTIAL_VALUE_COLUMN) val value: String, @ColumnInfo(name = SUBJECT_ID_COLUMN) 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 e1890996e3..e0815cac93 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 @@ -12,6 +12,7 @@ data class ApiExternalCredential( internal fun ApiExternalCredential.fromApiToDomain(subjectId: String) = ExternalCredential( + id = id, value = value.asTokenizableEncrypted(), subjectId = subjectId, type = ExternalCredentialType.valueOf(type) 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 769eee6696..e5911dcfbc 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 @@ -51,6 +51,7 @@ class ApiEnrolmentRecordCreationEventTest { ), externalCredentials = 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/event/remote/models/subject/ApiEnrolmentRecordMoveEventTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/subject/ApiEnrolmentRecordMoveEventTest.kt index 8c13aa8acb..a8d04ff6b6 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 @@ -31,8 +31,8 @@ class ApiEnrolmentRecordMoveEventTest { ), ), ApiExternalCredential( + id = "id", value = "value", - id = "subjectId", type = ExternalCredentialType.NHISCard.toString() ) ), @@ -59,6 +59,7 @@ class ApiEnrolmentRecordMoveEventTest { ), ), ExternalCredential( + id = "id", value = "value".asTokenizableEncrypted(), subjectId = "subjectId", type = ExternalCredentialType.NHISCard 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 e3ed24eef0..8c6a45e736 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 @@ -58,6 +58,7 @@ class ApiEnrolmentRecordUpdateEventTest { ), biometricReferencesRemoved = listOf("fpRefId2"), externalCredentialAdded = 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/CommCareEventSyncTaskTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/CommCareEventSyncTaskTest.kt index 4f2ce42871..f2c2416b78 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 @@ -1,7 +1,10 @@ package com.simprints.infra.eventsync.sync.down.tasks import com.google.common.truth.Truth.assertThat +import com.simprints.core.domain.externalcredential.ExternalCredential +import com.simprints.core.domain.externalcredential.ExternalCredentialType import com.simprints.core.domain.face.FaceSample +import com.simprints.core.domain.tokenization.asTokenizableEncrypted import com.simprints.core.domain.tokenization.asTokenizableRaw import com.simprints.core.tools.time.TimeHelper import com.simprints.infra.config.store.models.DeviceConfiguration @@ -57,46 +60,72 @@ class CommCareEventSyncTaskTest { "attendantId", ) val ENROLMENT_RECORD_CREATION = EnrolmentRecordCreationEvent( - "subjectId", - "projectId", - "moduleId".asTokenizableRaw(), - "attendantId".asTokenizableRaw(), - listOf(FaceReference("id", listOf(FaceTemplate("template")), "format")), + subjectId = "subjectId", + projectId = "projectId", + moduleId = "moduleId".asTokenizableRaw(), + attendantId = "attendantId".asTokenizableRaw(), + biometricReferences = listOf(FaceReference("id", listOf(FaceTemplate("template")), "format")), + externalCredentials = listOf( + ExternalCredential( + id = "id", + value = "value".asTokenizableEncrypted(), + subjectId = "subjectId", + type = ExternalCredentialType.NHISCard + ) + ), ) val ENROLMENT_RECORD_MOVE_MODULE = EnrolmentRecordMoveEvent( EnrolmentRecordMoveEvent.EnrolmentRecordCreationInMove( - "subjectId", - "projectId", - DEFAULT_MODULE_ID_2, - "attendantId".asTokenizableRaw(), - listOf(FaceReference("id", listOf(FaceTemplate("template")), "format")), + subjectId = "subjectId", + projectId = "projectId", + moduleId = DEFAULT_MODULE_ID_2, + attendantId = "attendantId".asTokenizableRaw(), + biometricReferences = listOf(FaceReference("id", listOf(FaceTemplate("template")), "format")), + externalCredential = ExternalCredential( + id = "id", + value = "value".asTokenizableEncrypted(), + subjectId = "subjectId", + type = ExternalCredentialType.NHISCard + ) ), EnrolmentRecordMoveEvent.EnrolmentRecordDeletionInMove( - "subjectId", - "projectId", - DEFAULT_MODULE_ID, - "attendantId".asTokenizableRaw(), + subjectId = "subjectId", + projectId = "projectId", + moduleId = DEFAULT_MODULE_ID, + attendantId = "attendantId".asTokenizableRaw(), ), ) val ENROLMENT_RECORD_MOVE_ATTENDANT = EnrolmentRecordMoveEvent( EnrolmentRecordMoveEvent.EnrolmentRecordCreationInMove( - "subjectId", - "projectId", - "moduleId".asTokenizableRaw(), - DEFAULT_USER_ID, - listOf(FaceReference("id", listOf(FaceTemplate("template")), "format")), + subjectId = "subjectId", + projectId = "projectId", + moduleId = "moduleId".asTokenizableRaw(), + attendantId = DEFAULT_USER_ID, + biometricReferences = listOf(FaceReference("id", listOf(FaceTemplate("template")), "format")), + externalCredential = ExternalCredential( + id = "id", + value = "value".asTokenizableEncrypted(), + subjectId = "subjectId", + type = ExternalCredentialType.NHISCard + ) ), EnrolmentRecordMoveEvent.EnrolmentRecordDeletionInMove( - "subjectId", - "projectId", - "moduleId".asTokenizableRaw(), - DEFAULT_USER_ID_2, + subjectId = "subjectId", + projectId = "projectId", + moduleId = "moduleId".asTokenizableRaw(), + attendantId = DEFAULT_USER_ID_2, ), ) 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 + ), ) } 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 9e4d98fb51..42c1a4c137 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 @@ -69,6 +69,7 @@ class SimprintsEventDownSyncTaskTest { biometricReferences = listOf(FaceReference("id", listOf(FaceTemplate("template")), "format")), externalCredentials = listOf( ExternalCredential( + id = "id", value = "value".asTokenizableEncrypted(), subjectId = "subjectId", type = ExternalCredentialType.NHISCard @@ -83,6 +84,7 @@ class SimprintsEventDownSyncTaskTest { "attendantId".asTokenizableRaw(), listOf(FaceReference("id", listOf(FaceTemplate("template")), "format")), ExternalCredential( + id = "id", value = "value".asTokenizableEncrypted(), subjectId = "subjectId", type = ExternalCredentialType.NHISCard @@ -103,6 +105,7 @@ class SimprintsEventDownSyncTaskTest { DEFAULT_USER_ID, listOf(FaceReference("id", listOf(FaceTemplate("template")), "format")), ExternalCredential( + id = "id", value = "value".asTokenizableEncrypted(), subjectId = "subjectId", type = ExternalCredentialType.NHISCard @@ -123,6 +126,7 @@ class SimprintsEventDownSyncTaskTest { DEFAULT_USER_ID_2, listOf(FaceReference("id", listOf(FaceTemplate("template")), "format")), ExternalCredential( + id = "id", value = "value".asTokenizableEncrypted(), subjectId = "subjectId", type = ExternalCredentialType.NHISCard @@ -140,6 +144,7 @@ class SimprintsEventDownSyncTaskTest { biometricReferencesAdded = listOf(FaceReference("id", listOf(FaceTemplate("template")), "format")), biometricReferencesRemoved = listOf("referenceIdToDelete"), externalCredentialAdded = ExternalCredential( + id = "id", value = "value".asTokenizableEncrypted(), subjectId = "subjectId", type = ExternalCredentialType.NHISCard @@ -552,6 +557,7 @@ class SimprintsEventDownSyncTaskTest { ), externalCredentials = 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/SubjectFactoryTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/SubjectFactoryTest.kt index 920fed0b39..3135e8e3f5 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 @@ -331,6 +331,7 @@ class SubjectFactoryTest { private lateinit var factory: SubjectFactory private const val PROJECT_ID = "projectId" private const val SUBJECT_ID = "subjectId" + private const val EXTERNAL_CREDENTIAL_ID = "credentialId" private val ATTENDANT_ID = "encryptedAttendantId".asTokenizableRaw() private val MODULE_ID = "encryptedModuleId".asTokenizableRaw() private val BASE_64_BYTES = byteArrayOf(1) @@ -357,6 +358,7 @@ class SubjectFactoryTest { format = REFERENCE_FORMAT, ) private val EXTERNAL_CREDENTIAL = ExternalCredential( + id = EXTERNAL_CREDENTIAL_ID, value = EXTERNAL_CREDENTIAL_VALUE, subjectId = SUBJECT_ID, type = EXTERNAL_CREDENTIAL_TYPE From 846849c1de8b8f1cfb68ac8a5ce53a4ec7c5e709 Mon Sep 17 00:00:00 2001 From: alex Date: Wed, 10 Sep 2025 15:51:05 +0300 Subject: [PATCH 022/139] [MS-1127] Changing type of API credential to follow the BFSID style --- .../store/remote/models/ApiMultiFactorIdConfiguration.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 47a1501851..e85b27bb49 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 @@ -15,10 +15,10 @@ internal data class ApiMultiFactorIdConfiguration( @Keep enum class ApiExternalCredentialType { - NHISCard, GhanaIdCard, QRCode; + NHIS_CARD, GhanaIdCard, QRCode; fun toDomain(): ExternalCredentialType = when (this) { - NHISCard -> ExternalCredentialType.NHISCard + NHIS_CARD -> ExternalCredentialType.NHISCard GhanaIdCard -> ExternalCredentialType.GhanaIdCard QRCode -> ExternalCredentialType.QRCode } From 3ed24725a63ee60638a49d88e343acc1742531cb Mon Sep 17 00:00:00 2001 From: alex Date: Wed, 10 Sep 2025 19:07:26 +0300 Subject: [PATCH 023/139] [MS-1127] Creating fundamental base for MF-ID navigation in Enrol or Identify workflow --- feature/external-credential/.gitignore | 1 + feature/external-credential/build.gradle.kts | 12 ++ .../src/main/AndroidManifest.xml | 4 + .../ExternalCredentialContract.kt | 11 ++ .../model/ExternalCredentialParams.kt | 11 ++ .../ExternalCredentialControllerFragment.kt | 52 ++++++++ .../ExternalCredentialScanOcrFragment.kt | 9 ++ .../ExternalCredentialScanQrFragment.kt | 9 ++ .../ExternalCredentialSearchFragment.kt | 9 ++ .../ExternalCredentialSelectFragment.kt | 9 ++ .../skip/ExternalCredentialSkipFragment.kt | 9 ++ ...ragment_external_credential_controller.xml | 7 ++ .../fragment_external_credential_scan_ocr.xml | 13 ++ .../fragment_external_credential_scan_qr.xml | 12 ++ .../fragment_external_credential_search.xml | 12 ++ .../fragment_external_credential_select.xml | 13 ++ .../fragment_external_credential_skip.xml | 12 ++ .../navigation/graph_external_credential.xml | 20 +++ .../graph_external_credential_internal.xml | 64 ++++++++++ feature/orchestrator/build.gradle.kts | 1 + .../orchestrator/OrchestratorViewModel.kt | 26 ++-- .../feature/orchestrator/steps/StepId.kt | 1 + .../response/AppResponseBuilderUseCase.kt | 10 +- .../response/CreateEnrolResponseUseCase.kt | 2 + .../usecases/steps/BuildStepsUseCase.kt | 119 ++++++++++++------ .../res/navigation/graph_orchestration.xml | 8 +- .../orchestrator/OrchestratorViewModelTest.kt | 28 ++--- .../response/AppResponseBuilderUseCaseTest.kt | 20 +-- .../CreateEnrolResponseUseCaseTest.kt | 15 ++- .../usecases/steps/BuildStepsUseCaseTest.kt | 65 +++++----- .../eventsync/sync/common/SubjectFactory.kt | 2 +- .../sync/down/tasks/SubjectFactoryTest.kt | 1 + settings.gradle.kts | 1 + 33 files changed, 474 insertions(+), 114 deletions(-) create mode 100644 feature/external-credential/.gitignore create mode 100644 feature/external-credential/build.gradle.kts create mode 100644 feature/external-credential/src/main/AndroidManifest.xml create mode 100644 feature/external-credential/src/main/java/com/simprints/feature/externalcredential/ExternalCredentialContract.kt create mode 100644 feature/external-credential/src/main/java/com/simprints/feature/externalcredential/model/ExternalCredentialParams.kt create mode 100644 feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/controller/ExternalCredentialControllerFragment.kt create mode 100644 feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/ExternalCredentialScanOcrFragment.kt create mode 100644 feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanqr/ExternalCredentialScanQrFragment.kt create mode 100644 feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/ExternalCredentialSearchFragment.kt create mode 100644 feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/select/ExternalCredentialSelectFragment.kt create mode 100644 feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/skip/ExternalCredentialSkipFragment.kt create mode 100644 feature/external-credential/src/main/res/layout/fragment_external_credential_controller.xml create mode 100644 feature/external-credential/src/main/res/layout/fragment_external_credential_scan_ocr.xml create mode 100644 feature/external-credential/src/main/res/layout/fragment_external_credential_scan_qr.xml create mode 100644 feature/external-credential/src/main/res/layout/fragment_external_credential_search.xml create mode 100644 feature/external-credential/src/main/res/layout/fragment_external_credential_select.xml create mode 100644 feature/external-credential/src/main/res/layout/fragment_external_credential_skip.xml create mode 100644 feature/external-credential/src/main/res/navigation/graph_external_credential.xml create mode 100644 feature/external-credential/src/main/res/navigation/graph_external_credential_internal.xml diff --git a/feature/external-credential/.gitignore b/feature/external-credential/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/feature/external-credential/.gitignore @@ -0,0 +1 @@ +/build diff --git a/feature/external-credential/build.gradle.kts b/feature/external-credential/build.gradle.kts new file mode 100644 index 0000000000..2db2841b64 --- /dev/null +++ b/feature/external-credential/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + id("simprints.feature") + id("kotlin-parcelize") +} + +android { + namespace = "com.simprints.feature.externalcredential" +} + +dependencies { + implementation(project(":feature:exit-form")) +} diff --git a/feature/external-credential/src/main/AndroidManifest.xml b/feature/external-credential/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..e100076157 --- /dev/null +++ b/feature/external-credential/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/ExternalCredentialContract.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/ExternalCredentialContract.kt new file mode 100644 index 0000000000..5928a86987 --- /dev/null +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/ExternalCredentialContract.kt @@ -0,0 +1,11 @@ +package com.simprints.feature.externalcredential + +import com.simprints.core.domain.common.FlowType +import com.simprints.feature.externalcredential.model.ExternalCredentialParams + +object ExternalCredentialContract { + val DESTINATION = R.id.externalCredentialControllerFragment + + fun getParams(subjectId: String?, flowType: FlowType) = ExternalCredentialParams(subjectId = subjectId, flowType = flowType) + +} diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/model/ExternalCredentialParams.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/model/ExternalCredentialParams.kt new file mode 100644 index 0000000000..0d8fbef752 --- /dev/null +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/model/ExternalCredentialParams.kt @@ -0,0 +1,11 @@ +package com.simprints.feature.externalcredential.model + +import androidx.annotation.Keep +import com.simprints.core.domain.common.FlowType +import com.simprints.core.domain.step.StepParams + +@Keep +data class ExternalCredentialParams( + val subjectId: String?, + val flowType: FlowType +) : StepParams diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/controller/ExternalCredentialControllerFragment.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/controller/ExternalCredentialControllerFragment.kt new file mode 100644 index 0000000000..e95d488dc3 --- /dev/null +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/controller/ExternalCredentialControllerFragment.kt @@ -0,0 +1,52 @@ +package com.simprints.feature.externalcredential.screens.controller + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.Fragment +import androidx.navigation.NavController +import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs +import com.simprints.feature.exitform.ExitFormContract +import com.simprints.feature.exitform.ExitFormResult +import com.simprints.feature.externalcredential.GraphExternalCredentialInternalDirections +import com.simprints.feature.externalcredential.R +import com.simprints.infra.uibase.navigation.finishWithResult +import com.simprints.infra.uibase.navigation.handleResult +import com.simprints.infra.uibase.navigation.navigateSafely +import dagger.hilt.android.AndroidEntryPoint +import kotlin.getValue + +@AndroidEntryPoint +internal class ExternalCredentialControllerFragment : Fragment(R.layout.fragment_external_credential_controller) { + private val args: ExternalCredentialControllerFragmentArgs by navArgs() + + private val hostFragment: Fragment? + get() = childFragmentManager.findFragmentById(R.id.external_credential_host_fragment) + + private val internalNavController: NavController? + get() = hostFragment?.findNavController() + + private val currentlyDisplayedInternalFragment: Fragment? + get() = hostFragment?.childFragmentManager?.fragments?.first() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + findNavController().handleResult( + this, + R.id.externalCredentialControllerFragment, + ExitFormContract.DESTINATION, + ) { + val option = it.submittedOption() + if (option != null) { + findNavController().finishWithResult(this, it) + } else { + internalNavController?.navigateSafely( + currentlyDisplayedInternalFragment, + GraphExternalCredentialInternalDirections.actionGlobalExternalCredentialSelect() + ) + } + } + internalNavController?.setGraph(R.navigation.graph_external_credential_internal) + } +} diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/ExternalCredentialScanOcrFragment.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/ExternalCredentialScanOcrFragment.kt new file mode 100644 index 0000000000..c055da0ae6 --- /dev/null +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/ExternalCredentialScanOcrFragment.kt @@ -0,0 +1,9 @@ +package com.simprints.feature.externalcredential.screens.scanocr + +import androidx.fragment.app.Fragment +import com.simprints.feature.externalcredential.R +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +internal class ExternalCredentialScanOcrFragment : Fragment(R.layout.fragment_external_credential_scan_ocr) { +} diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanqr/ExternalCredentialScanQrFragment.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanqr/ExternalCredentialScanQrFragment.kt new file mode 100644 index 0000000000..93c452cc05 --- /dev/null +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanqr/ExternalCredentialScanQrFragment.kt @@ -0,0 +1,9 @@ +package com.simprints.feature.externalcredential.screens.scanqr + +import androidx.fragment.app.Fragment +import com.simprints.feature.externalcredential.R +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +internal class ExternalCredentialScanQrFragment : Fragment(R.layout.fragment_external_credential_scan_qr) { +} diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/ExternalCredentialSearchFragment.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/ExternalCredentialSearchFragment.kt new file mode 100644 index 0000000000..8a46cf5717 --- /dev/null +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/ExternalCredentialSearchFragment.kt @@ -0,0 +1,9 @@ +package com.simprints.feature.externalcredential.screens.search + +import androidx.fragment.app.Fragment +import com.simprints.feature.externalcredential.R +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +internal class ExternalCredentialSearchFragment : Fragment(R.layout.fragment_external_credential_search) { +} 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 new file mode 100644 index 0000000000..ffd14f1e37 --- /dev/null +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/select/ExternalCredentialSelectFragment.kt @@ -0,0 +1,9 @@ +package com.simprints.feature.externalcredential.screens.select + +import androidx.fragment.app.Fragment +import com.simprints.feature.externalcredential.R +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +internal class ExternalCredentialSelectFragment : Fragment(R.layout.fragment_external_credential_select) { +} diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/skip/ExternalCredentialSkipFragment.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/skip/ExternalCredentialSkipFragment.kt new file mode 100644 index 0000000000..d347a4cace --- /dev/null +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/skip/ExternalCredentialSkipFragment.kt @@ -0,0 +1,9 @@ +package com.simprints.feature.externalcredential.screens.skip + +import androidx.fragment.app.Fragment +import com.simprints.feature.externalcredential.R +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class ExternalCredentialSkipFragment : Fragment(R.layout.fragment_external_credential_skip) { +} diff --git a/feature/external-credential/src/main/res/layout/fragment_external_credential_controller.xml b/feature/external-credential/src/main/res/layout/fragment_external_credential_controller.xml new file mode 100644 index 0000000000..2da94202c5 --- /dev/null +++ b/feature/external-credential/src/main/res/layout/fragment_external_credential_controller.xml @@ -0,0 +1,7 @@ + + + diff --git a/feature/external-credential/src/main/res/layout/fragment_external_credential_scan_ocr.xml b/feature/external-credential/src/main/res/layout/fragment_external_credential_scan_ocr.xml new file mode 100644 index 0000000000..1f9f2b618b --- /dev/null +++ b/feature/external-credential/src/main/res/layout/fragment_external_credential_scan_ocr.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/feature/external-credential/src/main/res/layout/fragment_external_credential_scan_qr.xml b/feature/external-credential/src/main/res/layout/fragment_external_credential_scan_qr.xml new file mode 100644 index 0000000000..187f72538b --- /dev/null +++ b/feature/external-credential/src/main/res/layout/fragment_external_credential_scan_qr.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/feature/external-credential/src/main/res/layout/fragment_external_credential_search.xml b/feature/external-credential/src/main/res/layout/fragment_external_credential_search.xml new file mode 100644 index 0000000000..dda0301c71 --- /dev/null +++ b/feature/external-credential/src/main/res/layout/fragment_external_credential_search.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/feature/external-credential/src/main/res/layout/fragment_external_credential_select.xml b/feature/external-credential/src/main/res/layout/fragment_external_credential_select.xml new file mode 100644 index 0000000000..fc81eec866 --- /dev/null +++ b/feature/external-credential/src/main/res/layout/fragment_external_credential_select.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/feature/external-credential/src/main/res/layout/fragment_external_credential_skip.xml b/feature/external-credential/src/main/res/layout/fragment_external_credential_skip.xml new file mode 100644 index 0000000000..1860c12b36 --- /dev/null +++ b/feature/external-credential/src/main/res/layout/fragment_external_credential_skip.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/feature/external-credential/src/main/res/navigation/graph_external_credential.xml b/feature/external-credential/src/main/res/navigation/graph_external_credential.xml new file mode 100644 index 0000000000..1d0865429c --- /dev/null +++ b/feature/external-credential/src/main/res/navigation/graph_external_credential.xml @@ -0,0 +1,20 @@ + + + + + + + + + + diff --git a/feature/external-credential/src/main/res/navigation/graph_external_credential_internal.xml b/feature/external-credential/src/main/res/navigation/graph_external_credential_internal.xml new file mode 100644 index 0000000000..0e988e1024 --- /dev/null +++ b/feature/external-credential/src/main/res/navigation/graph_external_credential_internal.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/feature/orchestrator/build.gradle.kts b/feature/orchestrator/build.gradle.kts index a300a9dc22..b84bb6dc9b 100644 --- a/feature/orchestrator/build.gradle.kts +++ b/feature/orchestrator/build.gradle.kts @@ -22,6 +22,7 @@ dependencies { implementation(project(":feature:matcher")) implementation(project(":feature:validate-subject-pool")) implementation(project(":feature:select-subject-age-group")) + implementation(project(":feature:external-credential")) implementation(project(":face:capture")) diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/OrchestratorViewModel.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/OrchestratorViewModel.kt index c06f979bd5..ef6f57f5f9 100644 --- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/OrchestratorViewModel.kt +++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/OrchestratorViewModel.kt @@ -46,6 +46,7 @@ import com.simprints.matcher.MatchParams import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import java.io.Serializable +import java.util.UUID import javax.inject.Inject @HiltViewModel @@ -61,6 +62,9 @@ internal class OrchestratorViewModel @Inject constructor( private val mapStepsForLastBiometrics: MapStepsForLastBiometricEnrolUseCase, ) : ViewModel() { var isRequestProcessed = false + + // [MS-1127] MF-ID: during enrolment, the same 'subjectId' needs to be used during the entire workflow + private val enrolmentSubjectId = UUID.randomUUID().toString() private var modalities = emptySet() private var steps = emptyList() private var actionRequest: ActionRequest? = null @@ -83,7 +87,11 @@ internal class OrchestratorViewModel @Inject constructor( // In case of a follow-up action, we should restore completed steps from cache // and add new ones to the list. This way all session steps are available throughout // the app for reference (i.e. have we already captured face in this session?) - steps = cache.steps + stepsBuilder.build(action, projectConfiguration) + steps = cache.steps + stepsBuilder.build( + action = action, + projectConfiguration = projectConfiguration, + enrolmentSubjectId = enrolmentSubjectId + ) Simber.i("Steps to execute: ${steps.joinToString { it.id.toString() }}", tag = ORCHESTRATION) } catch (_: SubjectAgeNotSupportedException) { handleErrorResponse(AppErrorResponse(AppErrorReason.AGE_GROUP_NOT_SUPPORTED)) @@ -115,9 +123,10 @@ internal class OrchestratorViewModel @Inject constructor( if (result is SelectSubjectAgeGroupResult) { val captureAndMatchSteps = stepsBuilder.buildCaptureAndMatchStepsForAgeGroup( - actionRequest!!, - projectConfiguration, - result.ageGroup, + action = actionRequest!!, + projectConfiguration = projectConfiguration, + ageGroup = result.ageGroup, + enrolmentSubjectId = enrolmentSubjectId ) steps = steps + captureAndMatchSteps } @@ -195,10 +204,11 @@ internal class OrchestratorViewModel @Inject constructor( val projectConfiguration = configManager.getProjectConfiguration() val project = configManager.getProject(projectConfiguration.projectId) val appResponse = appResponseBuilder( - projectConfiguration, - actionRequest, - steps.mapNotNull { it.result }, - project, + projectConfiguration = projectConfiguration, + request = actionRequest, + results = steps.mapNotNull { it.result }, + project = project, + enrolmentSubjectId = enrolmentSubjectId, ) updateDailyActivity(appResponse) diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/steps/StepId.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/steps/StepId.kt index 279839e355..8562f02bbc 100644 --- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/steps/StepId.kt +++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/steps/StepId.kt @@ -16,6 +16,7 @@ internal object StepId { const val CONFIRM_IDENTITY = STEP_BASE_CORE + 5 const val VALIDATE_ID_POOL = STEP_BASE_CORE + 6 const val SELECT_SUBJECT_AGE = STEP_BASE_CORE + 7 + const val EXTERNAL_CREDENTIAL = STEP_BASE_CORE + 8 // Face step ids private const val STEP_BASE_FINGERPRINT = 300 diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/AppResponseBuilderUseCase.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/AppResponseBuilderUseCase.kt index c00db35481..69f2839a59 100644 --- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/AppResponseBuilderUseCase.kt +++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/AppResponseBuilderUseCase.kt @@ -21,10 +21,16 @@ internal class AppResponseBuilderUseCase @Inject constructor( projectConfiguration: ProjectConfiguration, request: ActionRequest?, results: List, - project: Project + project: Project, + enrolmentSubjectId: String, ): AppResponse = when (request) { is ActionRequest.EnrolActionRequest -> if (isNewEnrolment(projectConfiguration, results)) { - handleEnrolment(request, results, project) + handleEnrolment( + request = request, + results = results, + project = project, + enrolmentSubjectId = enrolmentSubjectId + ) } else { handleIdentify(projectConfiguration, results) } diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateEnrolResponseUseCase.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateEnrolResponseUseCase.kt index acfa8b4e72..6cf73f2567 100644 --- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateEnrolResponseUseCase.kt +++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateEnrolResponseUseCase.kt @@ -23,6 +23,7 @@ internal class CreateEnrolResponseUseCase @Inject constructor( request: ActionRequest.EnrolActionRequest, results: List, project: Project, + enrolmentSubjectId: String, ): AppResponse { val fingerprintCapture = results.filterIsInstance(FingerprintCaptureResult::class.java).lastOrNull() val faceCapture = results.filterIsInstance(FaceCaptureResult::class.java).lastOrNull() @@ -31,6 +32,7 @@ internal class CreateEnrolResponseUseCase @Inject constructor( return try { val subject = subjectFactory.buildSubjectFromCaptureResults( + subjectId = enrolmentSubjectId, projectId = request.projectId, attendantId = request.userId, moduleId = request.moduleId, diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/steps/BuildStepsUseCase.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/steps/BuildStepsUseCase.kt index 42ada62779..6fe6bbf940 100644 --- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/steps/BuildStepsUseCase.kt +++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/steps/BuildStepsUseCase.kt @@ -5,6 +5,7 @@ import com.simprints.face.capture.FaceCaptureContract import com.simprints.feature.consent.ConsentContract import com.simprints.feature.consent.ConsentType import com.simprints.feature.enrollast.EnrolLastBiometricContract +import com.simprints.feature.externalcredential.ExternalCredentialContract import com.simprints.feature.fetchsubject.FetchSubjectContract import com.simprints.feature.orchestrator.R import com.simprints.feature.orchestrator.cache.OrchestratorCache @@ -43,12 +44,13 @@ internal class BuildStepsUseCase @Inject constructor( suspend fun build( action: ActionRequest, projectConfiguration: ProjectConfiguration, + enrolmentSubjectId: String, ) = when (action) { is ActionRequest.EnrolActionRequest -> listOf( buildSetupStep(), buildAgeSelectionStepIfNeeded(action, projectConfiguration), buildConsentStepIfNeeded(ConsentType.ENROL, projectConfiguration), - buildCaptureAndMatchStepsForEnrol(action, projectConfiguration), + buildCaptureAndMatchStepsForEnrol(action, projectConfiguration, enrolmentSubjectId = enrolmentSubjectId), ) is ActionRequest.IdentifyActionRequest -> { @@ -65,9 +67,10 @@ internal class BuildStepsUseCase @Inject constructor( buildAgeSelectionStepIfNeeded(action, projectConfiguration), buildConsentStepIfNeeded(ConsentType.IDENTIFY, projectConfiguration), buildCaptureAndMatchStepsForIdentify( - action, - projectConfiguration, + action = action, + projectConfiguration = projectConfiguration, subjectQuery = subjectQuery, + enrolmentSubjectId = enrolmentSubjectId ), ) } @@ -99,18 +102,21 @@ internal class BuildStepsUseCase @Inject constructor( action: ActionRequest, projectConfiguration: ProjectConfiguration, ageGroup: AgeGroup, + enrolmentSubjectId: String, ): List = when (action) { is ActionRequest.EnrolActionRequest -> buildCaptureAndMatchStepsForEnrol( action, projectConfiguration, ageGroup, + enrolmentSubjectId ) is ActionRequest.IdentifyActionRequest -> buildCaptureAndMatchStepsForIdentify( - action, - projectConfiguration, - ageGroup, + action = action, + projectConfiguration = projectConfiguration, + ageGroup = ageGroup, subjectQuery = buildMatcherSubjectQuery(projectConfiguration, action), + enrolmentSubjectId = enrolmentSubjectId ) is ActionRequest.VerifyActionRequest -> buildCaptureAndMatchStepsForVerify( @@ -122,35 +128,63 @@ internal class BuildStepsUseCase @Inject constructor( else -> emptyList() } + private fun buildExternalCredentialStepIfNeeded( + enrolmentSubjectId: String, + projectConfiguration: ProjectConfiguration, + flowType: FlowType + ): List { + val isExternalCredentialEnabled = projectConfiguration.multifactorId?.allowedExternalCredentials?.isNotEmpty() ?: false + if (!isExternalCredentialEnabled) return emptyList() + + return when (flowType) { + FlowType.ENROL, FlowType.IDENTIFY -> { + listOf( + Step( + id = StepId.EXTERNAL_CREDENTIAL, + navigationActionId = R.id.action_orchestratorFragment_to_externalCredential, + destinationId = ExternalCredentialContract.DESTINATION, + params = ExternalCredentialContract.getParams(subjectId = enrolmentSubjectId, flowType = flowType), + ) + ) + } + + FlowType.VERIFY -> emptyList() + } + } + private suspend fun buildCaptureAndMatchStepsForEnrol( action: ActionRequest.EnrolActionRequest, projectConfiguration: ProjectConfiguration, ageGroup: AgeGroup? = null, + enrolmentSubjectId: String, ): List { val action = fallbackToCommCareDataSourceIfNeeded(action, projectConfiguration) val resolvedAgeGroup = ageGroup ?: ageGroupFromSubjectAge(action, projectConfiguration) - return listOf( - buildCaptureSteps( + val captureSteps = buildCaptureSteps( + projectConfiguration, + FlowType.ENROL, + resolvedAgeGroup, + ) + val externalCredentialStep = when { + captureSteps.isEmpty() -> emptyList() + else -> buildExternalCredentialStepIfNeeded(enrolmentSubjectId, projectConfiguration, FlowType.ENROL) + } + val matcherSteps = if (projectConfiguration.general.duplicateBiometricEnrolmentCheck) { + buildMatcherSteps( projectConfiguration, FlowType.ENROL, resolvedAgeGroup, - ), - if (projectConfiguration.general.duplicateBiometricEnrolmentCheck) { - buildMatcherSteps( - projectConfiguration, - FlowType.ENROL, - resolvedAgeGroup, - buildMatcherSubjectQuery(projectConfiguration, action), - BiometricDataSource.fromString( - action.biometricDataSource, - action.actionIdentifier.callerPackageName, - ), - ) - } else { - emptyList() - }, - ).flatten() + buildMatcherSubjectQuery(projectConfiguration, action), + BiometricDataSource.fromString( + action.biometricDataSource, + action.actionIdentifier.callerPackageName, + ), + ) + } else { + emptyList() + } + return captureSteps + externalCredentialStep + matcherSteps } private suspend fun buildCaptureAndMatchStepsForIdentify( @@ -158,27 +192,30 @@ internal class BuildStepsUseCase @Inject constructor( projectConfiguration: ProjectConfiguration, ageGroup: AgeGroup? = null, subjectQuery: SubjectQuery, + enrolmentSubjectId: String ): List { val action = fallbackToCommCareDataSourceIfNeeded(action, projectConfiguration) val resolvedAgeGroup = ageGroup ?: ageGroupFromSubjectAge(action, projectConfiguration) - - return listOf( - buildCaptureSteps( - projectConfiguration, - FlowType.IDENTIFY, - resolvedAgeGroup, - ), - buildMatcherSteps( - projectConfiguration, - FlowType.IDENTIFY, - resolvedAgeGroup, - subjectQuery, - BiometricDataSource.fromString( - action.biometricDataSource, - action.actionIdentifier.callerPackageName, - ), + val captureSteps = buildCaptureSteps( + projectConfiguration, + FlowType.IDENTIFY, + resolvedAgeGroup, + ) + val externalCredentialStep = when { + captureSteps.isEmpty() -> emptyList() + else -> buildExternalCredentialStepIfNeeded(enrolmentSubjectId, projectConfiguration, FlowType.IDENTIFY) + } + val matcherSteps = buildMatcherSteps( + projectConfiguration, + FlowType.IDENTIFY, + resolvedAgeGroup, + subjectQuery, + BiometricDataSource.fromString( + action.biometricDataSource, + action.actionIdentifier.callerPackageName, ), - ).flatten() + ) + return captureSteps + externalCredentialStep + matcherSteps } private fun buildCaptureAndMatchStepsForVerify( diff --git a/feature/orchestrator/src/main/res/navigation/graph_orchestration.xml b/feature/orchestrator/src/main/res/navigation/graph_orchestration.xml index 56a33ef8da..43cb91cf24 100644 --- a/feature/orchestrator/src/main/res/navigation/graph_orchestration.xml +++ b/feature/orchestrator/src/main/res/navigation/graph_orchestration.xml @@ -77,6 +77,10 @@ + app:destination="@id/graph_validate_subject_pool" /> + + + diff --git a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/OrchestratorViewModelTest.kt b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/OrchestratorViewModelTest.kt index 0efa8da3a9..4038614599 100644 --- a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/OrchestratorViewModelTest.kt +++ b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/OrchestratorViewModelTest.kt @@ -112,7 +112,7 @@ internal class OrchestratorViewModelTest { @Test fun `Starts executing steps when action when received`() = runTest { - coEvery { stepsBuilder.build(any(), any()) } returns listOf( + coEvery { stepsBuilder.build(any(), any(), any()) } returns listOf( createMockStep(StepId.SETUP), ) @@ -125,7 +125,7 @@ internal class OrchestratorViewModelTest { @Test fun `Executes next steps after step result`() = runTest { - coEvery { stepsBuilder.build(any(), any()) } returns listOf( + coEvery { stepsBuilder.build(any(), any(), any()) } returns listOf( createMockStep(StepId.SETUP), createMockStep(StepId.CONSENT), ) @@ -142,12 +142,12 @@ internal class OrchestratorViewModelTest { @Test fun `Returns response when all steps executed`() = runTest { - coEvery { stepsBuilder.build(any(), any()) } returns listOf( + coEvery { stepsBuilder.build(any(), any(), any()) } returns listOf( createMockStep(StepId.SETUP), createMockStep(StepId.CONSENT), ) coEvery { mapRefusalOrErrorResult(any(), any()) } returns null - coEvery { appResponseBuilder(any(), any(), any(), any()) } returns mockk() + coEvery { appResponseBuilder(any(), any(), any(), any(), any()) } returns mockk() coJustRun { dailyActivityUseCase(any()) } justRun { addCallbackEvent(any()) } @@ -160,7 +160,7 @@ internal class OrchestratorViewModelTest { @Test fun `Returns response when error result received`() = runTest { - coEvery { stepsBuilder.build(any(), any()) } returns listOf( + coEvery { stepsBuilder.build(any(), any(), any()) } returns listOf( createMockStep(StepId.SETUP), createMockStep(StepId.CONSENT), ) @@ -174,7 +174,7 @@ internal class OrchestratorViewModelTest { @Test fun `Returns AGE_GROUP_NOT_SUPPORTED response when step builder throws SubjectAgeNotSupportedException`() = runTest { - coEvery { stepsBuilder.build(any(), any()) } throws SubjectAgeNotSupportedException() + coEvery { stepsBuilder.build(any(), any(), any()) } throws SubjectAgeNotSupportedException() viewModel.handleAction(mockk()) @@ -187,7 +187,7 @@ internal class OrchestratorViewModelTest { @Test fun `Appends capture and match steps upon receiving SelectSubjectAgeGroupResult`() = runTest { - coEvery { stepsBuilder.build(any(), any()) } returns listOf( + coEvery { stepsBuilder.build(any(), any(), any()) } returns listOf( createMockStep(StepId.SELECT_SUBJECT_AGE), ) coEvery { mapRefusalOrErrorResult(any(), any()) } returns null @@ -202,12 +202,12 @@ internal class OrchestratorViewModelTest { ), ), ) - coEvery { stepsBuilder.buildCaptureAndMatchStepsForAgeGroup(any(), any(), any()) } returns captureAndMatchSteps + coEvery { stepsBuilder.buildCaptureAndMatchStepsForAgeGroup(any(), any(), any(), any()) } returns captureAndMatchSteps viewModel.handleAction(mockk()) viewModel.handleResult(SelectSubjectAgeGroupResult(AgeGroup(0, 1))) - coVerify { stepsBuilder.buildCaptureAndMatchStepsForAgeGroup(any(), any(), any()) } + coVerify { stepsBuilder.buildCaptureAndMatchStepsForAgeGroup(any(), any(), any(), any()) } viewModel.currentStep.test().value().peekContent()?.let { step -> assertThat(step.id).isEqualTo(StepId.FACE_CAPTURE) } @@ -215,7 +215,7 @@ internal class OrchestratorViewModelTest { @Test fun `Updates face matcher step payload when receiving face capture`() = runTest { - coEvery { stepsBuilder.build(any(), any()) } returns listOf( + coEvery { stepsBuilder.build(any(), any(), any()) } returns listOf( createMockStep(StepId.FACE_CAPTURE), createMockStep( StepId.FACE_MATCHER, @@ -238,7 +238,7 @@ internal class OrchestratorViewModelTest { @Test fun `Updates fingerprint matcher step payload when receiving fingerprint capture`() = runTest { - coEvery { stepsBuilder.build(any(), any()) } returns listOf( + coEvery { stepsBuilder.build(any(), any(), any()) } returns listOf( createMockStep(StepId.FINGERPRINT_CAPTURE), createMockStep( StepId.FINGERPRINT_MATCHER, @@ -261,7 +261,7 @@ internal class OrchestratorViewModelTest { @Test fun `Updates the correct fingerprint match step when multiple fingerprint SDKs are used`() = runTest { - coEvery { stepsBuilder.build(any(), any()) } returns listOf( + coEvery { stepsBuilder.build(any(), any(), any()) } returns listOf( createMockStep( StepId.FINGERPRINT_CAPTURE, FingerprintCaptureContract.getParams( @@ -350,7 +350,7 @@ internal class OrchestratorViewModelTest { val originalSteps = listOf( createMockStep(StepId.FINGERPRINT_CAPTURE), ) - coEvery { stepsBuilder.build(any(), any()) } returns originalSteps + coEvery { stepsBuilder.build(any(), any(), any()) } returns originalSteps val savedSteps = listOf( createMockStep(StepId.SETUP), createMockStep(StepId.CONSENT), @@ -414,7 +414,7 @@ internal class OrchestratorViewModelTest { TokenizableString.Tokenized("moduleId"), listOf(mockk()), ) - coEvery { stepsBuilder.build(any(), any()) } returns listOf( + coEvery { stepsBuilder.build(any(), any(), any()) } returns listOf( captureStep, enrolLastStep, ) diff --git a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/AppResponseBuilderUseCaseTest.kt b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/AppResponseBuilderUseCaseTest.kt index 9ad9e2d728..1ff982ba2e 100644 --- a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/AppResponseBuilderUseCaseTest.kt +++ b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/AppResponseBuilderUseCaseTest.kt @@ -33,12 +33,13 @@ internal class AppResponseBuilderUseCaseTest { lateinit var handleEnrolLastBiometric: CreateEnrolLastBiometricResponseUseCase private lateinit var useCase: AppResponseBuilderUseCase + private lateinit var enrolmentSubjectId: String @Before fun setUp() { MockKAnnotations.init(this, relaxUnitFun = true) - coEvery { handleEnrolment.invoke(any(), any(), any()) } returns mockk() + coEvery { handleEnrolment.invoke(any(), any(), any(), any()) } returns mockk() coEvery { handleIdentify.invoke(any(), any()) } returns mockk() every { handleVerify.invoke(any(), any()) } returns mockk() every { handleConfirmIdentity.invoke(any()) } returns mockk() @@ -52,48 +53,49 @@ internal class AppResponseBuilderUseCaseTest { handleConfirmIdentity, handleEnrolLastBiometric, ) + enrolmentSubjectId = "enrolmentSubjectId" } @Test fun `Handles as enrolment for new enrolment action`() = runTest { every { isNewEnrolment(any(), any()) } returns true - useCase(mockk(), mockk(), mockk(), mockk()) - coVerify { handleEnrolment.invoke(any(), any(), any()) } + useCase(mockk(), mockk(), mockk(), mockk(), enrolmentSubjectId) + coVerify { handleEnrolment.invoke(any(), any(), any(), any()) } } @Test fun `Handles as identification for enrolment action with existing item`() = runTest { every { isNewEnrolment(any(), any()) } returns false - useCase(mockk(), mockk(), mockk(), mockk()) + useCase(mockk(), mockk(), mockk(), mockk(), enrolmentSubjectId) coVerify { handleIdentify.invoke(any(), any()) } } @Test fun `Handles as identification for identification action`() = runTest { - useCase(mockk(), mockk(), mockk(), mockk()) + useCase(mockk(), mockk(), mockk(), mockk(), enrolmentSubjectId) coVerify { handleIdentify.invoke(any(), any()) } } @Test fun `Handles as verification for verification action`() = runTest { - useCase(mockk(), mockk(), mockk(), mockk()) + useCase(mockk(), mockk(), mockk(), mockk(), enrolmentSubjectId) coVerify { handleVerify.invoke(any(), any()) } } @Test fun `Handles as confirmIdentity for confirm action`() = runTest { - useCase(mockk(), mockk(), mockk(), mockk()) + useCase(mockk(), mockk(), mockk(), mockk(), enrolmentSubjectId) coVerify { handleConfirmIdentity.invoke(any()) } } @Test fun `Handles as enrol last biometric for enrol last action`() = runTest { - useCase(mockk(), mockk(), mockk(), mockk()) + useCase(mockk(), mockk(), mockk(), mockk(), enrolmentSubjectId) coVerify { handleEnrolLastBiometric.invoke(any()) } } @Test fun `Handles null request`() = runTest { - assertThat(useCase(mockk(), null, mockk(), mockk())).isInstanceOf(AppErrorResponse::class.java) + assertThat(useCase(mockk(), null, mockk(), mockk(), enrolmentSubjectId)).isInstanceOf(AppErrorResponse::class.java) } } diff --git a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/CreateEnrolResponseUseCaseTest.kt b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/CreateEnrolResponseUseCaseTest.kt index 27100d1399..1484774120 100644 --- a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/CreateEnrolResponseUseCaseTest.kt +++ b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/CreateEnrolResponseUseCaseTest.kt @@ -29,6 +29,8 @@ internal class CreateEnrolResponseUseCaseTest { @MockK lateinit var project: Project + private val enrolmentSubjectId = "enrolmentSubjectId" + private val action = mockk { every { projectId } returns "projectId" every { userId } returns "userId".asTokenizableRaw() @@ -50,24 +52,26 @@ internal class CreateEnrolResponseUseCaseTest { fun `Converts correct results to response`() = runTest { every { subjectFactory.buildSubjectFromCaptureResults( + subjectId = any(), projectId = any(), attendantId = any(), moduleId = any(), fingerprintResponse = any(), faceResponse = any(), - externalCredential = any() + externalCredential = any(), ) } returns mockk { every { subjectId } returns "guid" } assertThat( useCase( - action, - listOf( + request = action, + results = listOf( FingerprintCaptureResult("", emptyList()), FaceCaptureResult("", emptyList()), mockk(), ), - project + project = project, + enrolmentSubjectId = enrolmentSubjectId ), ).isInstanceOf(AppEnrolResponse::class.java) } @@ -76,6 +80,7 @@ internal class CreateEnrolResponseUseCaseTest { fun `Returns error if no valid response`() = runTest { every { subjectFactory.buildSubjectFromCaptureResults( + subjectId = any(), projectId = any(), attendantId = any(), moduleId = any(), @@ -85,6 +90,6 @@ internal class CreateEnrolResponseUseCaseTest { ) } throws MissingCaptureException() - assertThat(useCase(action, emptyList(), project)).isInstanceOf(AppErrorResponse::class.java) + assertThat(useCase(action, emptyList(), project, enrolmentSubjectId)).isInstanceOf(AppErrorResponse::class.java) } } diff --git a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/steps/BuildStepsUseCaseTest.kt b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/steps/BuildStepsUseCaseTest.kt index 12a3cb5a65..6328e20227 100644 --- a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/steps/BuildStepsUseCaseTest.kt +++ b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/steps/BuildStepsUseCaseTest.kt @@ -47,12 +47,13 @@ class BuildStepsUseCaseTest { private lateinit var nec: FingerprintConfiguration.FingerprintSdkConfiguration private lateinit var useCase: BuildStepsUseCase + private lateinit var enrolmentSubjectId: String @Before fun setup() { MockKAnnotations.init(this) useCase = BuildStepsUseCase(buildMatcherSubjectQuery, cache, mapStepsForLastBiometrics, fallbackToCommCareDataSourceIfNeeded) - + enrolmentSubjectId = "enrolmentSubjectId" // Setup fallback use case to return the input actions unchanged by default coEvery { fallbackToCommCareDataSourceIfNeeded(any(), any()) } answers { firstArg() } coEvery { fallbackToCommCareDataSourceIfNeeded(any(), any()) } answers { firstArg() } @@ -110,7 +111,7 @@ class BuildStepsUseCaseTest { val action = mockk(relaxed = true) every { action.getSubjectAgeIfAvailable() } returns null - val steps = useCase.build(action, projectConfiguration) + val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId) assertStepOrder( steps, @@ -130,7 +131,7 @@ class BuildStepsUseCaseTest { val action = mockk(relaxed = true) every { action.getSubjectAgeIfAvailable() } returns null - val steps = useCase.build(action, projectConfiguration) + val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId) assertStepOrder( steps, @@ -149,7 +150,7 @@ class BuildStepsUseCaseTest { val action = mockk(relaxed = true) every { action.getSubjectAgeIfAvailable() } returns null - val steps = useCase.build(action, projectConfiguration) + val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId) assertStepOrder( steps, @@ -173,7 +174,7 @@ class BuildStepsUseCaseTest { val action = mockk(relaxed = true) every { action.getSubjectAgeIfAvailable() } returns null - val steps = useCase.build(action, projectConfiguration) + val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId) assertStepOrder( steps, @@ -193,7 +194,7 @@ class BuildStepsUseCaseTest { val action = mockk(relaxed = true) every { action.getSubjectAgeIfAvailable() } returns null - val steps = useCase.build(action, projectConfiguration) + val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId) assertStepOrder( steps, @@ -216,7 +217,7 @@ class BuildStepsUseCaseTest { val action = mockk(relaxed = true) every { action.getSubjectAgeIfAvailable() } returns null - val steps = useCase.build(action, projectConfiguration) + val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId) assertStepOrder( steps, @@ -235,7 +236,7 @@ class BuildStepsUseCaseTest { every { action.getSubjectAgeIfAvailable() } returns null every { projectConfiguration.experimental().idPoolValidationEnabled } returns true - val steps = useCase.build(action, projectConfiguration) + val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId) assertStepOrder( steps, @@ -258,7 +259,7 @@ class BuildStepsUseCaseTest { val action = mockk(relaxed = true) every { action.getSubjectAgeIfAvailable() } returns null - val steps = useCase.build(action, projectConfiguration) + val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId) assertStepOrder( steps, @@ -282,7 +283,7 @@ class BuildStepsUseCaseTest { val action = mockk(relaxed = true) every { action.getSubjectAgeIfAvailable() } returns null - val steps = useCase.build(action, projectConfiguration) + val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId) assertStepOrder( steps, @@ -301,7 +302,7 @@ class BuildStepsUseCaseTest { val action = mockk(relaxed = true) every { action.getSubjectAgeIfAvailable() } returns null - val steps = useCase.build(action, projectConfiguration) + val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId) assertStepOrder( steps, @@ -320,7 +321,7 @@ class BuildStepsUseCaseTest { Step(StepId.FACE_CAPTURE, mockk(relaxed = true), mockk(relaxed = true), mockk(relaxed = true)), ) - val steps = useCase.build(action, projectConfiguration) + val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId) assertStepOrder( steps, @@ -339,7 +340,7 @@ class BuildStepsUseCaseTest { Step(StepId.FACE_CAPTURE, mockk(relaxed = true), mockk(relaxed = true), mockk(relaxed = true)), ) - val steps = useCase.build(action, projectConfiguration) + val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId) assertStepOrder( steps, @@ -360,7 +361,7 @@ class BuildStepsUseCaseTest { val action = mockk(relaxed = true) every { action.getSubjectAgeIfAvailable() } returns null - val steps = useCase.build(action, projectConfiguration) + val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId) assertStepOrder( steps, @@ -381,7 +382,7 @@ class BuildStepsUseCaseTest { val action = mockk(relaxed = true) every { action.getSubjectAgeIfAvailable() } returns null - val steps = useCase.build(action, projectConfiguration) + val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId) assertStepOrder( steps, @@ -402,7 +403,7 @@ class BuildStepsUseCaseTest { val action = mockk(relaxed = true) every { action.getSubjectAgeIfAvailable() } returns null - val steps = useCase.build(action, projectConfiguration) + val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId) assertStepOrder( steps, @@ -424,7 +425,7 @@ class BuildStepsUseCaseTest { val action = mockk(relaxed = true) every { action.getSubjectAgeIfAvailable() } returns 25 // Subject age within the supported range - val steps = useCase.build(action, projectConfiguration) + val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId) assertStepOrder( steps, @@ -448,7 +449,7 @@ class BuildStepsUseCaseTest { val action = mockk(relaxed = true) every { action.getSubjectAgeIfAvailable() } returns 25 // Subject age within the supported range - val steps = useCase.build(action, projectConfiguration) + val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId) assertStepOrder( steps, @@ -474,7 +475,7 @@ class BuildStepsUseCaseTest { val action = mockk(relaxed = true) every { action.getSubjectAgeIfAvailable() } returns 25 // Subject age within the supported range - val steps = useCase.build(action, projectConfiguration) + val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId) assertStepOrder( steps, @@ -501,7 +502,7 @@ class BuildStepsUseCaseTest { every { getSubjectAgeIfAvailable() } returns 25 every { biometricDataSource } returns "COMMCARE" } - val steps = useCase.build(action, projectConfiguration) + val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId) assertStepOrder( steps, @@ -527,7 +528,7 @@ class BuildStepsUseCaseTest { val action = mockk(relaxed = true) every { action.getSubjectAgeIfAvailable() } returns 25 // Subject age within the supported range - val steps = useCase.build(action, projectConfiguration) + val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId) assertStepOrder( steps, @@ -554,7 +555,7 @@ class BuildStepsUseCaseTest { val action = mockk(relaxed = true) every { action.getSubjectAgeIfAvailable() } returns 25 // Subject age within the supported range every { action.biometricDataSource } returns "COMMCARE" - val steps = useCase.build(action, projectConfiguration) + val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId) assertStepOrder( steps, @@ -582,7 +583,7 @@ class BuildStepsUseCaseTest { every { action.getSubjectAgeIfAvailable() } returns 20 // Subject age not supported by any SDK assertThrows(SubjectAgeNotSupportedException::class.java) { - runBlocking { useCase.build(action, projectConfiguration) } + runBlocking { useCase.build(action, projectConfiguration, enrolmentSubjectId) } } } @@ -598,7 +599,7 @@ class BuildStepsUseCaseTest { every { action.getSubjectAgeIfAvailable() } returns 20 // Subject age not supported by any SDK assertThrows(SubjectAgeNotSupportedException::class.java) { - runBlocking { useCase.build(action, projectConfiguration) } + runBlocking { useCase.build(action, projectConfiguration, enrolmentSubjectId) } } } @@ -614,7 +615,7 @@ class BuildStepsUseCaseTest { every { action.getSubjectAgeIfAvailable() } returns 20 // Subject age not supported by any SDK assertThrows(SubjectAgeNotSupportedException::class.java) { - runBlocking { useCase.build(action, projectConfiguration) } + runBlocking { useCase.build(action, projectConfiguration, enrolmentSubjectId) } } } @@ -628,7 +629,7 @@ class BuildStepsUseCaseTest { val action = mockk(relaxed = true) - val steps = useCase.buildCaptureAndMatchStepsForAgeGroup(action, projectConfiguration, ageGroup) + val steps = useCase.buildCaptureAndMatchStepsForAgeGroup(action, projectConfiguration, ageGroup, enrolmentSubjectId) assertStepOrder( steps, @@ -649,7 +650,7 @@ class BuildStepsUseCaseTest { val action = mockk(relaxed = true) - val steps = useCase.buildCaptureAndMatchStepsForAgeGroup(action, projectConfiguration, ageGroup) + val steps = useCase.buildCaptureAndMatchStepsForAgeGroup(action, projectConfiguration, ageGroup, enrolmentSubjectId) assertStepOrder( steps, @@ -672,7 +673,7 @@ class BuildStepsUseCaseTest { val action = mockk(relaxed = true) - val steps = useCase.buildCaptureAndMatchStepsForAgeGroup(action, projectConfiguration, ageGroup) + val steps = useCase.buildCaptureAndMatchStepsForAgeGroup(action, projectConfiguration, ageGroup, enrolmentSubjectId) assertStepOrder( steps, @@ -695,7 +696,7 @@ class BuildStepsUseCaseTest { val action = mockk(relaxed = true) - val steps = useCase.buildCaptureAndMatchStepsForAgeGroup(action, projectConfiguration, ageGroup) + val steps = useCase.buildCaptureAndMatchStepsForAgeGroup(action, projectConfiguration, ageGroup, enrolmentSubjectId) assertStepOrder( steps, @@ -719,7 +720,7 @@ class BuildStepsUseCaseTest { val action = mockk(relaxed = true) - val steps = useCase.buildCaptureAndMatchStepsForAgeGroup(action, projectConfiguration, ageGroup) + val steps = useCase.buildCaptureAndMatchStepsForAgeGroup(action, projectConfiguration, ageGroup, enrolmentSubjectId) assertStepOrder( steps, @@ -740,7 +741,7 @@ class BuildStepsUseCaseTest { val action = mockk(relaxed = true) - val steps = useCase.buildCaptureAndMatchStepsForAgeGroup(action, projectConfiguration, AgeGroup(18, 60)) + val steps = useCase.buildCaptureAndMatchStepsForAgeGroup(action, projectConfiguration, AgeGroup(18, 60), enrolmentSubjectId) assertEquals(0, steps.size) } @@ -751,7 +752,7 @@ class BuildStepsUseCaseTest { val action = mockk(relaxed = true) - val steps = useCase.buildCaptureAndMatchStepsForAgeGroup(action, projectConfiguration, AgeGroup(18, 60)) + val steps = useCase.buildCaptureAndMatchStepsForAgeGroup(action, projectConfiguration, AgeGroup(18, 60), enrolmentSubjectId) assertEquals(0, steps.size) } 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 68d2bde6e9..45ebcc9f33 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 @@ -68,6 +68,7 @@ class SubjectFactory @Inject constructor( } fun buildSubjectFromCaptureResults( + subjectId: String, projectId: String, attendantId: TokenizableString, moduleId: TokenizableString, @@ -75,7 +76,6 @@ class SubjectFactory @Inject constructor( faceResponse: FaceCaptureResult?, externalCredential: ExternalCredential?, ): Subject { - val subjectId = UUID.randomUUID().toString() return buildSubject( subjectId = subjectId, projectId = projectId, 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 3135e8e3f5..35154ae213 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 @@ -251,6 +251,7 @@ class SubjectFactoryTest { ) val result = factory.buildSubjectFromCaptureResults( + subjectId = expected.subjectId, projectId = expected.projectId, attendantId = expected.attendantId, moduleId = expected.moduleId, diff --git a/settings.gradle.kts b/settings.gradle.kts index 8d15045037..a4fd410e7e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -114,6 +114,7 @@ include( ":feature:fetch-subject", ":feature:select-subject", ":feature:enrol-last-biometric", + ":feature:external-credential", ":feature:dashboard", ":feature:troubleshooting", ":feature:alert", From bc215dd731aba025c57eebfaccf6a900798b922c Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 11 Sep 2025 14:06:39 +0300 Subject: [PATCH 024/139] =?UTF-8?q?[MS-1127]=20Fixing=20room=20migrations,?= =?UTF-8?q?=20adding=20ID=20field=CB=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../enrolment/records/room/store/migration/RoomMigrations.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/migration/RoomMigrations.kt b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/migration/RoomMigrations.kt index 106bbdc2a3..46aac888cc 100644 --- a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/migration/RoomMigrations.kt +++ b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/migration/RoomMigrations.kt @@ -14,6 +14,7 @@ val MIGRATION_1_2 = object : Migration(1, 2) { db.execSQL( """ CREATE TABLE IF NOT EXISTS `DbExternalCredential` ( + `id` TEXT NOT NULL, `value` TEXT NOT NULL, `subjectId` TEXT NOT NULL, `type` TEXT NOT NULL, From 724e6ac7c077f63b15d43c79b1044f57b018e1c5 Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 11 Sep 2025 14:18:39 +0300 Subject: [PATCH 025/139] [MS-1127] Adding version 2 for generated SubjectDatabase schema --- .../2.json | 230 ++++++++++++++++++ 1 file changed, 230 insertions(+) create mode 100644 infra/enrolment-records/room-store/schemas/com.simprints.infra.enrolment.records.room.store.SubjectsDatabase/2.json diff --git a/infra/enrolment-records/room-store/schemas/com.simprints.infra.enrolment.records.room.store.SubjectsDatabase/2.json b/infra/enrolment-records/room-store/schemas/com.simprints.infra.enrolment.records.room.store.SubjectsDatabase/2.json new file mode 100644 index 0000000000..c2b7bd5f10 --- /dev/null +++ b/infra/enrolment-records/room-store/schemas/com.simprints.infra.enrolment.records.room.store.SubjectsDatabase/2.json @@ -0,0 +1,230 @@ +{ + "formatVersion": 1, + "database": { + "version": 2, + "identityHash": "95e64b85f208336cee0a08fc5e94b2d6", + "entities": [ + { + "tableName": "DbSubject", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`subjectId` TEXT NOT NULL, `projectId` TEXT NOT NULL, `attendantId` TEXT NOT NULL, `moduleId` TEXT NOT NULL, `createdAt` INTEGER, `updatedAt` INTEGER, PRIMARY KEY(`subjectId`))", + "fields": [ + { + "fieldPath": "subjectId", + "columnName": "subjectId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "projectId", + "columnName": "projectId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "attendantId", + "columnName": "attendantId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "moduleId", + "columnName": "moduleId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "subjectId" + ] + }, + "indices": [ + { + "name": "index_DbSubject_projectId_subjectId", + "unique": false, + "columnNames": [ + "projectId", + "subjectId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DbSubject_projectId_subjectId` ON `${TABLE_NAME}` (`projectId`, `subjectId`)" + }, + { + "name": "index_DbSubject_projectId_moduleId_subjectId", + "unique": false, + "columnNames": [ + "projectId", + "moduleId", + "subjectId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DbSubject_projectId_moduleId_subjectId` ON `${TABLE_NAME}` (`projectId`, `moduleId`, `subjectId`)" + }, + { + "name": "index_DbSubject_projectId_attendantId_subjectId", + "unique": false, + "columnNames": [ + "projectId", + "attendantId", + "subjectId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DbSubject_projectId_attendantId_subjectId` ON `${TABLE_NAME}` (`projectId`, `attendantId`, `subjectId`)" + } + ] + }, + { + "tableName": "DbBiometricTemplate", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` TEXT NOT NULL, `subjectId` TEXT NOT NULL, `identifier` INTEGER, `templateData` BLOB NOT NULL, `format` TEXT NOT NULL, `referenceId` TEXT NOT NULL, `modality` INTEGER NOT NULL, PRIMARY KEY(`uuid`), FOREIGN KEY(`subjectId`) REFERENCES `DbSubject`(`subjectId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subjectId", + "columnName": "subjectId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "identifier", + "columnName": "identifier", + "affinity": "INTEGER" + }, + { + "fieldPath": "templateData", + "columnName": "templateData", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "format", + "columnName": "format", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "referenceId", + "columnName": "referenceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "modality", + "columnName": "modality", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uuid" + ] + }, + "indices": [ + { + "name": "index_DbBiometricTemplate_format_subjectId", + "unique": false, + "columnNames": [ + "format", + "subjectId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DbBiometricTemplate_format_subjectId` ON `${TABLE_NAME}` (`format`, `subjectId`)" + }, + { + "name": "index_DbBiometricTemplate_subjectId", + "unique": false, + "columnNames": [ + "subjectId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DbBiometricTemplate_subjectId` ON `${TABLE_NAME}` (`subjectId`)" + } + ], + "foreignKeys": [ + { + "table": "DbSubject", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "subjectId" + ], + "referencedColumns": [ + "subjectId" + ] + } + ] + }, + { + "tableName": "DbExternalCredential", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `value` TEXT NOT NULL, `subjectId` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`value`, `subjectId`), FOREIGN KEY(`subjectId`) REFERENCES `DbSubject`(`subjectId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subjectId", + "columnName": "subjectId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "value", + "subjectId" + ] + }, + "foreignKeys": [ + { + "table": "DbSubject", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "subjectId" + ], + "referencedColumns": [ + "subjectId" + ] + } + ] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '95e64b85f208336cee0a08fc5e94b2d6')" + ] + } +} \ No newline at end of file From e74f1c4eba469f8e3351251decc7f933ec9d5f63 Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 11 Sep 2025 14:27:55 +0300 Subject: [PATCH 026/139] [MS-1127] Adding index for Foreign Key check speedup --- .../records/room/store/models/DbExternalCredential.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/DbExternalCredential.kt b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/DbExternalCredential.kt index 33d36197ed..7175998ebb 100644 --- a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/DbExternalCredential.kt +++ b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/models/DbExternalCredential.kt @@ -3,6 +3,7 @@ package com.simprints.infra.enrolment.records.room.store.models import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.ForeignKey +import androidx.room.Index import com.simprints.infra.enrolment.records.room.store.models.DbExternalCredential.Companion.EXTERNAL_CREDENTIAL_TABLE_NAME import com.simprints.infra.enrolment.records.room.store.models.DbExternalCredential.Companion.EXTERNAL_CREDENTIAL_VALUE_COLUMN import com.simprints.infra.enrolment.records.room.store.models.DbSubject.Companion.SUBJECT_ID_COLUMN @@ -17,7 +18,8 @@ import com.simprints.infra.enrolment.records.room.store.models.DbSubject.Compani childColumns = [SUBJECT_ID_COLUMN], onDelete = ForeignKey.CASCADE, ) - ] + ], + indices = [Index(SUBJECT_ID_COLUMN)] ) data class DbExternalCredential( // The ID is only used by BFSID for analytics. The primary key should be a composite of value+subjectId From f2498ce290b71639f026a8658a7d5871d9227861 Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 11 Sep 2025 15:09:05 +0300 Subject: [PATCH 027/139] =?UTF-8?q?[MS-1127]=20Fixing=20migrations=CB=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../1.json | 48 +------------------ .../2.json | 15 +++++- .../room/store/migration/RoomMigrations.kt | 1 + 3 files changed, 16 insertions(+), 48 deletions(-) diff --git a/infra/enrolment-records/room-store/schemas/com.simprints.infra.enrolment.records.room.store.SubjectsDatabase/1.json b/infra/enrolment-records/room-store/schemas/com.simprints.infra.enrolment.records.room.store.SubjectsDatabase/1.json index 4df59295d1..58b0f3827c 100644 --- a/infra/enrolment-records/room-store/schemas/com.simprints.infra.enrolment.records.room.store.SubjectsDatabase/1.json +++ b/infra/enrolment-records/room-store/schemas/com.simprints.infra.enrolment.records.room.store.SubjectsDatabase/1.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "527fcc2c704906558681cb31beddb0c3", + "identityHash": "94bee827928a2618c6873579bc6bc63a", "entities": [ { "tableName": "DbSubject", @@ -170,55 +170,11 @@ ] } ] - }, - { - "tableName": "DbExternalCredential", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`value` TEXT NOT NULL, `subjectId` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`value`, `subjectId`), FOREIGN KEY(`subjectId`) REFERENCES `DbSubject`(`subjectId`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "value", - "columnName": "value", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "subjectId", - "columnName": "subjectId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "type", - "columnName": "type", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "value", - "subjectId" - ] - }, - "foreignKeys": [ - { - "table": "DbSubject", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "subjectId" - ], - "referencedColumns": [ - "subjectId" - ] - } - ] } ], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '527fcc2c704906558681cb31beddb0c3')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '94bee827928a2618c6873579bc6bc63a')" ] } } diff --git a/infra/enrolment-records/room-store/schemas/com.simprints.infra.enrolment.records.room.store.SubjectsDatabase/2.json b/infra/enrolment-records/room-store/schemas/com.simprints.infra.enrolment.records.room.store.SubjectsDatabase/2.json index c2b7bd5f10..6673ade6ff 100644 --- a/infra/enrolment-records/room-store/schemas/com.simprints.infra.enrolment.records.room.store.SubjectsDatabase/2.json +++ b/infra/enrolment-records/room-store/schemas/com.simprints.infra.enrolment.records.room.store.SubjectsDatabase/2.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 2, - "identityHash": "95e64b85f208336cee0a08fc5e94b2d6", + "identityHash": "8dad14b775cb4eea6cb7293fbcf84b30", "entities": [ { "tableName": "DbSubject", @@ -207,6 +207,17 @@ "subjectId" ] }, + "indices": [ + { + "name": "index_DbExternalCredential_subjectId", + "unique": false, + "columnNames": [ + "subjectId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DbExternalCredential_subjectId` ON `${TABLE_NAME}` (`subjectId`)" + } + ], "foreignKeys": [ { "table": "DbSubject", @@ -224,7 +235,7 @@ ], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '95e64b85f208336cee0a08fc5e94b2d6')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '8dad14b775cb4eea6cb7293fbcf84b30')" ] } } \ No newline at end of file diff --git a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/migration/RoomMigrations.kt b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/migration/RoomMigrations.kt index 46aac888cc..6278faf23b 100644 --- a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/migration/RoomMigrations.kt +++ b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/migration/RoomMigrations.kt @@ -23,6 +23,7 @@ val MIGRATION_1_2 = object : Migration(1, 2) { ) """.trimIndent() ) + db.execSQL("CREATE INDEX IF NOT EXISTS `index_DbExternalCredential_subjectId` ON `DbExternalCredential` (`subjectId`)") } } From a1e03a7b4add1a8d10e42a73b0c25f133ce01d43 Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 11 Sep 2025 15:20:48 +0300 Subject: [PATCH 028/139] [MS-1127] Fixing tests --- .../java/com/simprints/infra/config/store/testtools/Models.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/config-store/src/test/java/com/simprints/infra/config/store/testtools/Models.kt b/infra/config-store/src/test/java/com/simprints/infra/config/store/testtools/Models.kt index 14f9581d8b..3be90e32a5 100644 --- a/infra/config-store/src/test/java/com/simprints/infra/config/store/testtools/Models.kt +++ b/infra/config-store/src/test/java/com/simprints/infra/config/store/testtools/Models.kt @@ -469,7 +469,7 @@ internal val protoSynchronizationConfiguration = ProtoSynchronizationConfigurati .build(), ).build() -internal val apiAllowedExternalCredential = ApiExternalCredentialType.NHISCard +internal val apiAllowedExternalCredential = ApiExternalCredentialType.NHIS_CARD internal val apiMultiFactorIdConfiguration = ApiMultiFactorIdConfiguration( allowedExternalCredentials = listOf(apiAllowedExternalCredential) From 88f44314ad703de9c16cf9c56a7067c274eef276 Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 11 Sep 2025 15:49:38 +0300 Subject: [PATCH 029/139] =?UTF-8?q?[MS-1127]=20Renaming=20subject=20migrat?= =?UTF-8?q?ions=20to=20explicit=20class=CB=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../records/room/store/migration/Migration1to2Test.kt | 2 +- .../infra/enrolment/records/room/store/SubjectsDatabase.kt | 4 ++-- .../migration/{RoomMigrations.kt => SubjectMigration1to2.kt} | 4 +++- 3 files changed, 6 insertions(+), 4 deletions(-) rename infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/migration/{RoomMigrations.kt => SubjectMigration1to2.kt} (81%) diff --git a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/room/store/migration/Migration1to2Test.kt b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/room/store/migration/Migration1to2Test.kt index 5cda6f65fe..278023f419 100644 --- a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/room/store/migration/Migration1to2Test.kt +++ b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/room/store/migration/Migration1to2Test.kt @@ -25,7 +25,7 @@ class Migration1to2Test { name = TEST_DB, version = 2, validateDroppedTables = true, - MIGRATION_1_2 + SubjectMigration1to2() ) // Verify external credentials table exists diff --git a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/SubjectsDatabase.kt b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/SubjectsDatabase.kt index 422c0dc33c..157af2c0ab 100644 --- a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/SubjectsDatabase.kt +++ b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/SubjectsDatabase.kt @@ -6,7 +6,7 @@ import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase import com.simprints.infra.enrolment.records.room.store.SubjectsDatabase.Companion.SUBJECT_DB_VERSION -import com.simprints.infra.enrolment.records.room.store.migration.MIGRATION_1_2 +import com.simprints.infra.enrolment.records.room.store.migration.SubjectMigration1to2 import com.simprints.infra.enrolment.records.room.store.models.DbBiometricTemplate import com.simprints.infra.enrolment.records.room.store.models.DbExternalCredential import com.simprints.infra.enrolment.records.room.store.models.DbSubject @@ -35,7 +35,7 @@ abstract class SubjectsDatabase : RoomDatabase() { ): SubjectsDatabase { val builder = Room .databaseBuilder(context, SubjectsDatabase::class.java, dbName) - .addMigrations(MIGRATION_1_2) + .addMigrations(SubjectMigration1to2()) if (BuildConfig.DB_ENCRYPTION) { builder.openHelperFactory(factory) } diff --git a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/migration/RoomMigrations.kt b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/migration/SubjectMigration1to2.kt similarity index 81% rename from infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/migration/RoomMigrations.kt rename to infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/migration/SubjectMigration1to2.kt index 6278faf23b..c7a250110b 100644 --- a/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/migration/RoomMigrations.kt +++ b/infra/enrolment-records/room-store/src/main/java/com/simprints/infra/enrolment/records/room/store/migration/SubjectMigration1to2.kt @@ -2,6 +2,7 @@ package com.simprints.infra.enrolment.records.room.store.migration import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase +import com.simprints.core.ExcludedFromGeneratedTestCoverageReports /** * Schema version 1 -> 2 @@ -9,7 +10,8 @@ import androidx.sqlite.db.SupportSQLiteDatabase * Changes: * - Adding [DbExternalCredential] entity * */ -val MIGRATION_1_2 = object : Migration(1, 2) { +@ExcludedFromGeneratedTestCoverageReports("Covered indirectly in the migration tests") +class SubjectMigration1to2 : Migration(1, 2) { override fun migrate(db: SupportSQLiteDatabase) { db.execSQL( """ From c8106eefac09577bd95530e029351f00b0521ee2 Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 11 Sep 2025 17:55:02 +0300 Subject: [PATCH 030/139] [MS-1127] Adding test coverage for BuildStepsUseCaseTest.kt --- .../usecases/steps/BuildStepsUseCaseTest.kt | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/steps/BuildStepsUseCaseTest.kt b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/steps/BuildStepsUseCaseTest.kt index 6328e20227..f74d4cee84 100644 --- a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/steps/BuildStepsUseCaseTest.kt +++ b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/steps/BuildStepsUseCaseTest.kt @@ -1,5 +1,6 @@ package com.simprints.feature.orchestrator.usecases.steps +import com.simprints.core.domain.externalcredential.ExternalCredentialType import com.simprints.feature.orchestrator.cache.OrchestratorCache import com.simprints.feature.orchestrator.exceptions.SubjectAgeNotSupportedException import com.simprints.feature.orchestrator.steps.Step @@ -756,4 +757,95 @@ class BuildStepsUseCaseTest { assertEquals(0, steps.size) } + + @Test + fun `build external credential not enabled - no external credential step`() = runTest { + val projectConfiguration = mockCommonProjectConfiguration() + every { projectConfiguration.multifactorId?.allowedExternalCredentials } returns emptyList() + + val action = mockk(relaxed = true) + every { action.getSubjectAgeIfAvailable() } returns null + + val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId) + + // Should not contain EXTERNAL_CREDENTIAL step + assertStepOrder( + steps, + StepId.SETUP, + StepId.CONSENT, + StepId.FINGERPRINT_CAPTURE, + StepId.FINGERPRINT_CAPTURE, + StepId.FACE_CAPTURE, + ) + } + + @Test + fun `build enrol action - external credential enabled - returns external credential step`() = runTest { + val projectConfiguration = mockCommonProjectConfiguration() + every { projectConfiguration.multifactorId?.allowedExternalCredentials } returns ExternalCredentialType.entries + + val action = mockk(relaxed = true) + every { action.getSubjectAgeIfAvailable() } returns null + + val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId) + + assertStepOrder( + steps, + StepId.SETUP, + StepId.CONSENT, + StepId.FINGERPRINT_CAPTURE, + StepId.FINGERPRINT_CAPTURE, + StepId.FACE_CAPTURE, + StepId.EXTERNAL_CREDENTIAL, + ) + } + + @Test + fun `build identify action - external credential enabled - returns external credential step`() = runTest { + val projectConfiguration = mockCommonProjectConfiguration() + every { projectConfiguration.multifactorId?.allowedExternalCredentials } returns ExternalCredentialType.entries + + val action = mockk(relaxed = true) + every { action.getSubjectAgeIfAvailable() } returns null + + val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId) + + assertStepOrder( + steps, + StepId.SETUP, + StepId.CONSENT, + StepId.FINGERPRINT_CAPTURE, + StepId.FINGERPRINT_CAPTURE, + StepId.FACE_CAPTURE, + StepId.EXTERNAL_CREDENTIAL, + StepId.FINGERPRINT_MATCHER, + StepId.FINGERPRINT_MATCHER, + StepId.FACE_MATCHER, + ) + } + + @Test + fun `build verify action - external credential enabled - no external credential step`() = runTest { + val projectConfiguration = mockCommonProjectConfiguration() + every { projectConfiguration.multifactorId?.allowedExternalCredentials } returns ExternalCredentialType.entries + + val action = mockk(relaxed = true) + every { action.getSubjectAgeIfAvailable() } returns null + + val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId) + + // Should not contain EXTERNAL_CREDENTIAL step for VERIFY flow + assertStepOrder( + steps, + StepId.SETUP, + StepId.FETCH_GUID, + StepId.CONSENT, + StepId.FINGERPRINT_CAPTURE, + StepId.FINGERPRINT_CAPTURE, + StepId.FACE_CAPTURE, + StepId.FINGERPRINT_MATCHER, + StepId.FINGERPRINT_MATCHER, + StepId.FACE_MATCHER, + ) + } } From 43c9f876c0d94a866b65d03610c80258fc2832de Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 18 Sep 2025 19:14:08 +0300 Subject: [PATCH 031/139] [MS-1163] Implementation of the External Credential selection screen --- feature/external-credential/build.gradle.kts | 2 + .../ExternalCredentialControllerFragment.kt | 27 +++ .../controller/ExternalCredentialState.kt | 13 ++ .../controller/ExternalCredentialViewModel.kt | 37 +++++ .../ExternalCredentialSelectFragment.kt | 154 ++++++++++++++++++ .../ExternalCredentialSelectViewModel.kt | 29 ++++ .../view/ExternalCredentialTypeAdapter.kt | 47 ++++++ .../src/main/res/drawable/ghana_id_card.webp | Bin 0 -> 5048 bytes .../main/res/drawable/ghana_nhis_card.webp | Bin 0 -> 6138 bytes .../src/main/res/drawable/qr_code.webp | Bin 0 -> 3818 bytes .../main/res/layout-land/item_document.xml | 41 +++++ .../res/layout/dialog_skip_scan_confirm.xml | 65 ++++++++ .../fragment_external_credential_select.xml | 52 +++++- .../src/main/res/layout/item_document.xml | 41 +++++ .../graph_external_credential_internal.xml | 9 +- .../resources/src/main/res/values/strings.xml | 13 ++ 16 files changed, 519 insertions(+), 11 deletions(-) create mode 100644 feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/controller/ExternalCredentialState.kt create mode 100644 feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/controller/ExternalCredentialViewModel.kt create mode 100644 feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/select/ExternalCredentialSelectViewModel.kt create mode 100644 feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/select/view/ExternalCredentialTypeAdapter.kt create mode 100644 feature/external-credential/src/main/res/drawable/ghana_id_card.webp create mode 100644 feature/external-credential/src/main/res/drawable/ghana_nhis_card.webp create mode 100644 feature/external-credential/src/main/res/drawable/qr_code.webp create mode 100644 feature/external-credential/src/main/res/layout-land/item_document.xml create mode 100644 feature/external-credential/src/main/res/layout/dialog_skip_scan_confirm.xml create mode 100644 feature/external-credential/src/main/res/layout/item_document.xml diff --git a/feature/external-credential/build.gradle.kts b/feature/external-credential/build.gradle.kts index 2db2841b64..61fa3b3d79 100644 --- a/feature/external-credential/build.gradle.kts +++ b/feature/external-credential/build.gradle.kts @@ -8,5 +8,7 @@ android { } dependencies { + implementation(project(":infra:config-store")) + implementation(project(":infra:config-sync")) implementation(project(":feature:exit-form")) } diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/controller/ExternalCredentialControllerFragment.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/controller/ExternalCredentialControllerFragment.kt index e95d488dc3..c377a49fc7 100644 --- a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/controller/ExternalCredentialControllerFragment.kt +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/controller/ExternalCredentialControllerFragment.kt @@ -2,7 +2,9 @@ package com.simprints.feature.externalcredential.screens.controller import android.os.Bundle import android.view.View +import androidx.activity.addCallback import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels import androidx.navigation.NavController import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs @@ -19,6 +21,7 @@ import kotlin.getValue @AndroidEntryPoint internal class ExternalCredentialControllerFragment : Fragment(R.layout.fragment_external_credential_controller) { private val args: ExternalCredentialControllerFragmentArgs by navArgs() + private val viewModel: ExternalCredentialViewModel by activityViewModels() private val hostFragment: Fragment? get() = childFragmentManager.findFragmentById(R.id.external_credential_host_fragment) @@ -48,5 +51,29 @@ internal class ExternalCredentialControllerFragment : Fragment(R.layout.fragment } } internalNavController?.setGraph(R.navigation.graph_external_credential_internal) + + initObservers() + initListeners() + } + + private fun initObservers() { + viewModel.stateLiveData.observe(viewLifecycleOwner) { + } + } + + private fun initListeners() { + requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) { + when (internalNavController?.currentDestination?.id) { + R.id.externalCredentialSelectFragment -> { + // Exit form navigation + findNavController().navigateSafely( + this@ExternalCredentialControllerFragment, + R.id.action_global_refusalFragment, + ) + } + + else -> internalNavController?.popBackStack() + } + } } } diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/controller/ExternalCredentialState.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/controller/ExternalCredentialState.kt new file mode 100644 index 0000000000..609900800f --- /dev/null +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/controller/ExternalCredentialState.kt @@ -0,0 +1,13 @@ +package com.simprints.feature.externalcredential.screens.controller + +import com.simprints.core.domain.externalcredential.ExternalCredentialType + +internal data class ExternalCredentialState( + val selectedType: ExternalCredentialType? +) { + companion object { + val EMPTY = ExternalCredentialState( + selectedType = null + ) + } +} 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 new file mode 100644 index 0000000000..1cb2f215dd --- /dev/null +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/controller/ExternalCredentialViewModel.kt @@ -0,0 +1,37 @@ +package com.simprints.feature.externalcredential.screens.controller + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.simprints.core.domain.externalcredential.ExternalCredentialType +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import com.simprints.infra.resources.R as IDR + +@HiltViewModel +internal class ExternalCredentialViewModel @Inject internal constructor( + +) : ViewModel() { + + private var state: ExternalCredentialState = ExternalCredentialState.EMPTY + set(value) { + field = value + _stateLiveData.postValue(value) + } + private val _stateLiveData = MutableLiveData() + val stateLiveData: LiveData = _stateLiveData + + fun setSelectedExternalCredentialType(selectedType: ExternalCredentialType?) { + updateState { it.copy(selectedType = selectedType) } + } + + private fun updateState(state: (ExternalCredentialState) -> ExternalCredentialState) { + this.state = state(this.state) + } + + fun mapTypeToStringResource(type: ExternalCredentialType) = when(type) { + ExternalCredentialType.NHISCard -> IDR.string.mfid_type_nhis_card + ExternalCredentialType.GhanaIdCard -> IDR.string.mfid_type_ghana_id_card + ExternalCredentialType.QRCode -> IDR.string.mfid_type_qr_code + } +} 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 ffd14f1e37..65235eac9e 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 @@ -1,9 +1,163 @@ package com.simprints.feature.externalcredential.screens.select +import android.app.Dialog +import android.os.Bundle +import android.view.View +import android.widget.Button +import android.widget.TextView import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.simprints.core.domain.externalcredential.ExternalCredentialType import com.simprints.feature.externalcredential.R +import com.simprints.feature.externalcredential.databinding.FragmentExternalCredentialSelectBinding +import com.simprints.feature.externalcredential.screens.controller.ExternalCredentialViewModel +import com.simprints.feature.externalcredential.screens.select.view.ExternalCredentialTypeAdapter +import com.simprints.infra.logging.LoggingConstants.CrashReportTag.ORCHESTRATION +import com.simprints.infra.logging.Simber +import com.simprints.infra.uibase.navigation.navigateSafely +import com.simprints.infra.uibase.view.applySystemBarInsets +import com.simprints.infra.uibase.viewbinding.viewBinding import dagger.hilt.android.AndroidEntryPoint +import kotlin.getValue +import com.simprints.infra.resources.R as IDR @AndroidEntryPoint internal class ExternalCredentialSelectFragment : Fragment(R.layout.fragment_external_credential_select) { + + private val mainViewModel by viewModels() + private val viewModel by viewModels() + private val binding by viewBinding(FragmentExternalCredentialSelectBinding::bind) + + private var dialog: Dialog? = null + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + applySystemBarInsets(view) + Simber.i("ExternalCredentialSelectFragment started", tag = ORCHESTRATION) + + observeChanges() + viewModel.loadExternalCredentials() + } + + override fun onDestroy() { + dismissDialog() + super.onDestroy() + } + + private fun dismissDialog() { + dialog?.dismiss() + dialog = null + } + + private fun initListeners(types: List) { + binding.skipScanning.setOnClickListener { + displaySkipScanningConfirmationDialog( + credentialTypes = types, + onConfirm = { + dismissDialog() + findNavController().navigateSafely( + this, + ExternalCredentialSelectFragmentDirections.actionExternalCredentialSelectFragmentToExternalCredentialSkip(), + ) + }, + onCancel = ::dismissDialog + ) + } + } + + private fun initViews(types: List) { + binding.title.text = when (types.size) { + 1 -> { + val documentType = getString(mainViewModel.mapTypeToStringResource(types.first())) + getString(IDR.string.mfid_scanner_selection_title_specific).format(documentType) + } + + else -> getString(IDR.string.mfid_scanner_selection_title_generic) + } + } + + private fun observeChanges() { + viewModel.externalCredentialTypes.observe(viewLifecycleOwner) { externalCredentialTypes -> + updateSelectedCredentialType(null) + fillRecyclerView(externalCredentialTypes) + initViews(externalCredentialTypes) + initListeners(externalCredentialTypes) + } + } + + private fun fillRecyclerView(types: List) { + with(binding.documentsRecyclerView) { + layoutManager = LinearLayoutManager(requireContext()) + adapter = ExternalCredentialTypeAdapter(types) { selectedType -> + updateSelectedCredentialType(selectedType) + navigateToScanner(selectedType) + } + } + } + + private fun updateSelectedCredentialType(type: ExternalCredentialType?) { + mainViewModel.setSelectedExternalCredentialType(type) + } + + private fun navigateToScanner(type: ExternalCredentialType) { + when (type) { + ExternalCredentialType.NHISCard -> startOcr() + ExternalCredentialType.GhanaIdCard -> startOcr() + ExternalCredentialType.QRCode -> startQrScan() + } + } + + private fun startQrScan() { + findNavController().navigateSafely( + this, + ExternalCredentialSelectFragmentDirections.actionExternalCredentialSelectFragmentToExternalCredentialScanQr(), + ) + } + + private fun displaySkipScanningConfirmationDialog( + credentialTypes: List, + onConfirm: () -> Unit, + onCancel: () -> Unit + ) { + dialog?.dismiss() + dialog = BottomSheetDialog(requireContext()) + val view = layoutInflater.inflate(R.layout.dialog_skip_scan_confirm, null) + val bodyText = view.findViewById(R.id.skipDialogBodyText) + val cancelButton = view.findViewById