diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/ext/ResourceExt.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/ext/ResourceExt.kt index ea7dc69c47..22af423ee0 100644 --- a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/ext/ResourceExt.kt +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/ext/ResourceExt.kt @@ -24,6 +24,7 @@ fun Resources.getCredentialFieldTitle(type: ExternalCredentialType): String = wh ExternalCredentialType.NHISCard -> IDR.string.mfid_nhis_card_credential_field ExternalCredentialType.GhanaIdCard -> IDR.string.mfid_ghana_id_credential_field ExternalCredentialType.QRCode -> IDR.string.mfid_qr_credential_field + ExternalCredentialType.FaydaCard -> IDR.string.mfid_fayda_card_credential_field }.run(::getString) fun Resources.getCredentialTypeString(type: ExternalCredentialType?): String = getCredentialTypeRes(type).run(::getString) @@ -32,5 +33,6 @@ fun Resources.getCredentialTypeRes(type: ExternalCredentialType?): Int = when (t 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 + ExternalCredentialType.FaydaCard -> IDR.string.mfid_type_fayda_card null -> IDR.string.mfid_type_any_document } diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/ExternalCredentialScanOcrViewModel.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/ExternalCredentialScanOcrViewModel.kt index 980acdb274..19a087abea 100644 --- a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/ExternalCredentialScanOcrViewModel.kt +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/ExternalCredentialScanOcrViewModel.kt @@ -102,6 +102,7 @@ internal class ExternalCredentialScanOcrViewModel @AssistedInject constructor( fun getDocumentTypeRes(): Int = when (ocrDocumentType) { OcrDocumentType.NhisCard -> R.string.mfid_type_nhis_card OcrDocumentType.GhanaIdCard -> R.string.mfid_type_ghana_id_card + OcrDocumentType.FaydaCard -> R.string.mfid_type_fayda_card } fun startScanning() { diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/model/OcrDocumentType.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/model/OcrDocumentType.kt index 46c6e8cca5..39ad085301 100644 --- a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/model/OcrDocumentType.kt +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/model/OcrDocumentType.kt @@ -5,15 +5,18 @@ import com.simprints.core.domain.externalcredential.ExternalCredentialType enum class OcrDocumentType { NhisCard, GhanaIdCard, + FaydaCard, } fun ExternalCredentialType.asOcrDocumentType() = when (this) { ExternalCredentialType.NHISCard -> OcrDocumentType.NhisCard ExternalCredentialType.GhanaIdCard -> OcrDocumentType.GhanaIdCard + ExternalCredentialType.FaydaCard -> OcrDocumentType.FaydaCard ExternalCredentialType.QRCode -> throw IllegalArgumentException("Cannot create Ocr Document Type from $this") } fun OcrDocumentType.asExternalCredentialType() = when (this) { OcrDocumentType.NhisCard -> ExternalCredentialType.NHISCard OcrDocumentType.GhanaIdCard -> ExternalCredentialType.GhanaIdCard + OcrDocumentType.FaydaCard -> ExternalCredentialType.FaydaCard } diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/model/OcrScanResult.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/model/OcrScanResult.kt index b19f986a2c..99fc3d7841 100644 --- a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/model/OcrScanResult.kt +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/model/OcrScanResult.kt @@ -36,4 +36,10 @@ internal sealed class OcrScanResult { val dateOfIssue: OcrLine? = null, val dateOfExpiry: OcrLine? = null, ) : OcrScanResult() + + @Serializable + @SerialName("OcrScanResult.FaydaCard") + data class FaydaCard( + override val credential: OcrLine, + ) : OcrScanResult() } diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/model/ScannedMfidDocument.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/model/ScannedMfidDocument.kt index cb37f85a2b..112a121fa3 100644 --- a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/model/ScannedMfidDocument.kt +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/model/ScannedMfidDocument.kt @@ -25,5 +25,6 @@ internal data class ScannedMfidDocument( get() = when (ocrScanResult) { is OcrScanResult.GhanaIdCard -> OcrDocumentType.GhanaIdCard is OcrScanResult.GhanaNhisCard -> OcrDocumentType.NhisCard + is OcrScanResult.FaydaCard -> OcrDocumentType.FaydaCard } } diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/reader/OcrQuery.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/reader/OcrQuery.kt index 3616adf803..8bce54972a 100644 --- a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/reader/OcrQuery.kt +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/reader/OcrQuery.kt @@ -13,6 +13,10 @@ internal class OcrQuery { filters += { line -> regex.matches(line.text) } } + fun matchesCondition(condition: (line: String) -> Boolean) { + filters += { line -> condition(line.text) } + } + fun containsPattern(regex: Regex) { filters += { line -> regex.containsMatchIn(line.text) } } diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/usecase/BuildMfidDocumentUseCase.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/usecase/BuildMfidDocumentUseCase.kt index b9e3db160c..38e00f4bfa 100644 --- a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/usecase/BuildMfidDocumentUseCase.kt +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/usecase/BuildMfidDocumentUseCase.kt @@ -17,6 +17,7 @@ internal class BuildMfidDocumentUseCase @Inject constructor( ): MfidDocument = when (documentType) { OcrDocumentType.NhisCard -> buildNhisCard(scannedDocuments) OcrDocumentType.GhanaIdCard -> buildGhanaIdCard(scannedDocuments) + OcrDocumentType.FaydaCard -> buildFaydaCard(scannedDocuments) } /** @@ -68,6 +69,14 @@ internal class BuildMfidDocumentUseCase @Inject constructor( ) } + private fun buildFaydaCard(scannedDocuments: List): MfidDocument.FaydaCard { + val credential = scannedDocuments + .map { it.ocrScanResult.credential.text } + .let { credentials -> getBestReadoutBasedOnConfidenceUseCase(credentials, targetLength = CREDENTIAL_LENGTH_FAYDA_CARD) } + .asTokenizableRaw() + return MfidDocument.FaydaCard(credential = credential) + } + private fun List.bestReadoutOrNull(ocrLine: (T) -> OcrLine?) = mapNotNull(ocrLine) .takeUnless(List::isEmpty) ?.let { getBestReadoutBasedOnConfidenceUseCase(it.map(OcrLine::text)) } @@ -79,5 +88,8 @@ internal class BuildMfidDocumentUseCase @Inject constructor( // Ghana ID field contains 15 chars: GHA-123456789-0 private const val CREDENTIAL_LENGTH_GHANA_ID = 15 + + // Fayda Alias Number (FAN) contains 16 digits + private const val CREDENTIAL_LENGTH_FAYDA_CARD = 16 } } diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/usecase/FaydaCardOcrReaderUseCase.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/usecase/FaydaCardOcrReaderUseCase.kt new file mode 100644 index 0000000000..2bcbed4165 --- /dev/null +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/usecase/FaydaCardOcrReaderUseCase.kt @@ -0,0 +1,23 @@ +package com.simprints.feature.externalcredential.screens.scanocr.usecase + +import com.simprints.feature.externalcredential.screens.scanocr.model.OcrScanResult +import com.simprints.feature.externalcredential.screens.scanocr.reader.OcrReader +import javax.inject.Inject + +internal class FaydaCardOcrReaderUseCase @Inject constructor() { + operator fun invoke(ocrReader: OcrReader): OcrScanResult.FaydaCard? { + val credential = ocrReader.find { + matchesCondition { line -> + // Removing non-digits from the line readout. Checking if remaining digits match FAN_PATTERN + line.filter(Char::isDigit).matches(FAN_PATTERN) + } + } ?: return null + val credentialText = credential.text.filter(Char::isDigit) + return OcrScanResult.FaydaCard(credential.copy(text = credentialText)) + } + + companion object { + // Fayda Alias Number (FAN): exactly 16 digits after stripping all non-digit characters + val FAN_PATTERN = Regex("^\\d{16}$") + } +} diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/usecase/ScanMfidDocumentUseCase.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/usecase/ScanMfidDocumentUseCase.kt index 2757c6fbde..cd874bac94 100644 --- a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/usecase/ScanMfidDocumentUseCase.kt +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/usecase/ScanMfidDocumentUseCase.kt @@ -17,6 +17,7 @@ internal class ScanMfidDocumentUseCase @Inject constructor( private val readTextFromImage: ReadTextFromImageUseCase, private val ghanaNhisCardOcrReaderUseCase: GhanaNhisCardOcrReaderUseCase, private val ghanaIdCardOcrReaderUseCase: GhanaIdCardOcrReaderUseCase, + private val faydaCardOcrReaderUseCase: FaydaCardOcrReaderUseCase, private val credentialImageRepository: CredentialImageRepository, ) { suspend operator fun invoke( @@ -27,6 +28,7 @@ internal class ScanMfidDocumentUseCase @Inject constructor( val isCapturingAllFields = when (documentType) { OcrDocumentType.NhisCard -> config.nhisCardConfig?.isCapturingAllFields OcrDocumentType.GhanaIdCard -> config.ghanaIdCardConfig?.isCapturingAllFields + OcrDocumentType.FaydaCard -> config.faydaCardConfig?.isCapturingAllFields } ?: false return try { val ocrText = readTextFromImage(bitmap) ?: return null @@ -34,6 +36,7 @@ internal class ScanMfidDocumentUseCase @Inject constructor( val scanResult = when (documentType) { OcrDocumentType.NhisCard -> ghanaNhisCardOcrReaderUseCase(ocrReader, isCapturingAllFields) OcrDocumentType.GhanaIdCard -> ghanaIdCardOcrReaderUseCase(ocrReader, isCapturingAllFields) + OcrDocumentType.FaydaCard -> faydaCardOcrReaderUseCase(ocrReader) } if (scanResult != null) { val savedImagePath = credentialImageRepository.saveCredentialScan(bitmap, imageType = FullDocument) diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/ExternalCredentialSearchViewModel.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/ExternalCredentialSearchViewModel.kt index 57b16f3c17..1d18882c6d 100644 --- a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/ExternalCredentialSearchViewModel.kt +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/ExternalCredentialSearchViewModel.kt @@ -13,6 +13,7 @@ import com.simprints.core.livedata.send import com.simprints.core.tools.time.TimeHelper import com.simprints.feature.externalcredential.ExternalCredentialSearchResult import com.simprints.feature.externalcredential.model.ExternalCredentialParams +import com.simprints.feature.externalcredential.screens.scanocr.usecase.FaydaCardOcrReaderUseCase import com.simprints.feature.externalcredential.screens.scanocr.usecase.GhanaIdCardOcrReaderUseCase import com.simprints.feature.externalcredential.screens.scanocr.usecase.GhanaNhisCardOcrReaderUseCase import com.simprints.feature.externalcredential.screens.search.model.ScannedCredentialResult @@ -161,8 +162,9 @@ internal class ExternalCredentialSearchViewModel @AssistedInject constructor( * alpha-numeric, while the NHIS card memberships contain only digits. */ fun getKeyBoardInputType() = when (scannedCredentialResult.credentialType) { - // NHIS card membership contains only numbers + // NHIS card membership and Fayda FAN contain only numbers ExternalCredentialType.NHISCard -> InputType.TYPE_CLASS_NUMBER + ExternalCredentialType.FaydaCard -> InputType.TYPE_CLASS_NUMBER ExternalCredentialType.GhanaIdCard -> InputType.TYPE_CLASS_TEXT ExternalCredentialType.QRCode -> InputType.TYPE_CLASS_TEXT } @@ -211,6 +213,10 @@ internal class ExternalCredentialSearchViewModel @AssistedInject constructor( // Ghana ID card number pattern is "GHA-123456789-0" GhanaIdCardOcrReaderUseCase.GHANA_ID_PATTERN.matches(credential) } + ExternalCredentialType.FaydaCard -> { + // Fayda Alias Number (FAN): exactly 16 digits + FaydaCardOcrReaderUseCase.FAN_PATTERN.matches(credential) + } ExternalCredentialType.QRCode -> { // No QR code validation as of 2025.4.1 true diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/model/MfidDocument.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/model/MfidDocument.kt index 0ec43fbe79..c95f2c65da 100644 --- a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/model/MfidDocument.kt +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/model/MfidDocument.kt @@ -43,4 +43,10 @@ sealed class MfidDocument : StepResult { data class GhanaQrCode( override val credential: TokenizableString.Raw, ) : MfidDocument() + + @Serializable + @SerialName("MfidDocument.FaydaCard") + data class FaydaCard( + override val credential: TokenizableString.Raw, + ) : MfidDocument() } diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/model/ScannedCredentialResult.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/model/ScannedCredentialResult.kt index 5edf70dcc6..ce7f95ae3f 100644 --- a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/model/ScannedCredentialResult.kt +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/model/ScannedCredentialResult.kt @@ -30,5 +30,6 @@ data class ScannedCredentialResult( is MfidDocument.GhanaIdCard -> ExternalCredentialType.GhanaIdCard is MfidDocument.GhanaNhisCard -> ExternalCredentialType.NHISCard is MfidDocument.GhanaQrCode -> ExternalCredentialType.QRCode + is MfidDocument.FaydaCard -> ExternalCredentialType.FaydaCard } } diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/model/SearchCredentialState.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/model/SearchCredentialState.kt index a446b3815d..63a52b5299 100644 --- a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/model/SearchCredentialState.kt +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/search/model/SearchCredentialState.kt @@ -28,7 +28,7 @@ internal data class SearchCredentialState( // [MS-1484] Editing and confirming the readout value should only be available for the OCR readouts (i.e.: Ghana NHIS card) // QR codes are not readable by humans, hence there is no point of asking the user to confirm the readout, nor to edit the readout val isOcrReadout = when (scannedCredentialResult.credentialType) { - ExternalCredentialType.NHISCard, ExternalCredentialType.GhanaIdCard -> true + ExternalCredentialType.NHISCard, ExternalCredentialType.GhanaIdCard, ExternalCredentialType.FaydaCard -> true ExternalCredentialType.QRCode -> false } return SearchCredentialState( 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 31f0040a85..ee3022dd56 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 @@ -116,6 +116,7 @@ internal class ExternalCredentialSelectFragment : Fragment(R.layout.fragment_ext when (type) { ExternalCredentialType.NHISCard, ExternalCredentialType.GhanaIdCard, + ExternalCredentialType.FaydaCard, -> startOcr(type.asOcrDocumentType()) ExternalCredentialType.QRCode -> startQrScan() diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/select/view/ExternalCredentialTypeAdapter.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/select/view/ExternalCredentialTypeAdapter.kt index 6898a68470..3a60dba2f5 100644 --- a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/select/view/ExternalCredentialTypeAdapter.kt +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/select/view/ExternalCredentialTypeAdapter.kt @@ -35,12 +35,14 @@ internal class ExternalCredentialTypeAdapter( 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 + ExternalCredentialType.FaydaCard -> IDR.string.mfid_type_fayda_card }.run(c::getString) val text = c.resources.getString(IDR.string.mfid_scan_action, credentialTypeText) val image = when (credentialType) { ExternalCredentialType.NHISCard -> R.drawable.ghana_nhis_card ExternalCredentialType.GhanaIdCard -> R.drawable.ghana_id_card ExternalCredentialType.QRCode -> R.drawable.qr_code + ExternalCredentialType.FaydaCard -> R.drawable.fayda_card } binding.documentText.text = text binding.documentImage.setImageResource(image) diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/usecase/ExternalCredentialEventTrackerUseCase.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/usecase/ExternalCredentialEventTrackerUseCase.kt index 7749f950d3..430337be60 100644 --- a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/usecase/ExternalCredentialEventTrackerUseCase.kt +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/usecase/ExternalCredentialEventTrackerUseCase.kt @@ -102,6 +102,7 @@ internal class ExternalCredentialEventTrackerUseCase @Inject constructor( ExternalCredentialType.NHISCard -> NHIS_CARD_ID_LENGTH ExternalCredentialType.GhanaIdCard -> GHANA_ID_CARD_ID_LENGTH ExternalCredentialType.QRCode -> QR_CODE_LENGTH + ExternalCredentialType.FaydaCard -> FAYDA_CARD_FAN_LENGTH } suspend fun saveSelectionEvent( @@ -154,5 +155,6 @@ internal class ExternalCredentialEventTrackerUseCase @Inject constructor( private const val NHIS_CARD_ID_LENGTH = 8 private const val GHANA_ID_CARD_ID_LENGTH = 15 private const val QR_CODE_LENGTH = 6 + private const val FAYDA_CARD_FAN_LENGTH = 16 } } diff --git a/feature/external-credential/src/main/res/drawable/fayda_card.webp b/feature/external-credential/src/main/res/drawable/fayda_card.webp new file mode 100644 index 0000000000..853ba53e36 Binary files /dev/null and b/feature/external-credential/src/main/res/drawable/fayda_card.webp differ diff --git a/feature/external-credential/src/test/java/com/simprints/feature/externalcredential/screens/scanocr/usecase/ScanMfidDocumentUseCaseTest.kt b/feature/external-credential/src/test/java/com/simprints/feature/externalcredential/screens/scanocr/usecase/ScanMfidDocumentUseCaseTest.kt index d0b5df3c8a..c8ef039226 100644 --- a/feature/external-credential/src/test/java/com/simprints/feature/externalcredential/screens/scanocr/usecase/ScanMfidDocumentUseCaseTest.kt +++ b/feature/external-credential/src/test/java/com/simprints/feature/externalcredential/screens/scanocr/usecase/ScanMfidDocumentUseCaseTest.kt @@ -6,6 +6,7 @@ import com.simprints.feature.externalcredential.model.BoundingBox import com.simprints.feature.externalcredential.screens.scanocr.model.OcrDocumentType import com.simprints.feature.externalcredential.screens.scanocr.model.OcrScanResult import com.simprints.feature.externalcredential.screens.scanocr.reader.OcrLine +import com.simprints.infra.config.store.models.FaydaCardConfig import com.simprints.infra.config.store.models.GhanaIdCardConfig import com.simprints.infra.config.store.models.MultiFactorIdConfiguration import com.simprints.infra.config.store.models.NhisCardConfig @@ -31,6 +32,9 @@ internal class ScanMfidDocumentUseCaseTest { @MockK private lateinit var ghanaIdCardOcrReaderUseCase: GhanaIdCardOcrReaderUseCase + @MockK + private lateinit var faydaCardOcrReaderUseCase: FaydaCardOcrReaderUseCase + @MockK private lateinit var credentialImageRepository: CredentialImageRepository @@ -43,18 +47,21 @@ internal class ScanMfidDocumentUseCaseTest { ghanaIdCardConfig = null, nhisCardConfig = NhisCardConfig(isCapturingAllFields = false), qrCodeConfig = null, + faydaCardConfig = null, ) private val ghanaIdConfig = MultiFactorIdConfiguration( allowedExternalCredentials = emptyList(), ghanaIdCardConfig = GhanaIdCardConfig(isCapturingAllFields = false), nhisCardConfig = null, qrCodeConfig = null, + faydaCardConfig = null, ) private val allFieldsConfig = MultiFactorIdConfiguration( allowedExternalCredentials = emptyList(), ghanaIdCardConfig = GhanaIdCardConfig(isCapturingAllFields = true), nhisCardConfig = NhisCardConfig(isCapturingAllFields = true), qrCodeConfig = null, + faydaCardConfig = FaydaCardConfig(isCapturingAllFields = true), ) @Before @@ -64,6 +71,7 @@ internal class ScanMfidDocumentUseCaseTest { readTextFromImage = readTextFromImage, ghanaNhisCardOcrReaderUseCase = ghanaNhisCardOcrReaderUseCase, ghanaIdCardOcrReaderUseCase = ghanaIdCardOcrReaderUseCase, + faydaCardOcrReaderUseCase = faydaCardOcrReaderUseCase, credentialImageRepository = credentialImageRepository, ) coEvery { credentialImageRepository.saveCredentialScan(any(), any()) } returns savedImagePath @@ -199,6 +207,7 @@ internal class ScanMfidDocumentUseCaseTest { ghanaIdCardConfig = null, nhisCardConfig = null, qrCodeConfig = null, + faydaCardConfig = null, ) every { readTextFromImage(bitmap) } returns mockk(relaxed = true) diff --git a/feature/external-credential/src/test/java/com/simprints/feature/externalcredential/screens/search/ExternalCredentialSearchViewModelTest.kt b/feature/external-credential/src/test/java/com/simprints/feature/externalcredential/screens/search/ExternalCredentialSearchViewModelTest.kt index 95e5051491..808f7e45da 100644 --- a/feature/external-credential/src/test/java/com/simprints/feature/externalcredential/screens/search/ExternalCredentialSearchViewModelTest.kt +++ b/feature/external-credential/src/test/java/com/simprints/feature/externalcredential/screens/search/ExternalCredentialSearchViewModelTest.kt @@ -342,4 +342,18 @@ internal class ExternalCredentialSearchViewModelTest { assertThat(viewModel.isCredentialFormatValid("")).isTrue() assertThat(viewModel.isCredentialFormatValid(null)).isFalse() } + + @Test + fun `isCredentialFormatValid validates Fayda card format`() = runTest { + every { mockScannedCredentialResult.credentialType } returns ExternalCredentialType.FaydaCard + + viewModel = createViewModel() + + assertThat(viewModel.isCredentialFormatValid("1234567812345678")).isTrue() + assertThat(viewModel.isCredentialFormatValid("1234 5678 1234 5678")).isFalse() + assertThat(viewModel.isCredentialFormatValid("1234-5678-1234-5678")).isFalse() + assertThat(viewModel.isCredentialFormatValid("123456781234567")).isFalse() + assertThat(viewModel.isCredentialFormatValid("invalid")).isFalse() + assertThat(viewModel.isCredentialFormatValid(null)).isFalse() + } } diff --git a/feature/external-credential/src/test/java/com/simprints/feature/externalcredential/usecase/ExternalCredentialEventTrackerUseCaseTest.kt b/feature/external-credential/src/test/java/com/simprints/feature/externalcredential/usecase/ExternalCredentialEventTrackerUseCaseTest.kt index 15e764fd1f..1806dfdb7d 100644 --- a/feature/external-credential/src/test/java/com/simprints/feature/externalcredential/usecase/ExternalCredentialEventTrackerUseCaseTest.kt +++ b/feature/external-credential/src/test/java/com/simprints/feature/externalcredential/usecase/ExternalCredentialEventTrackerUseCaseTest.kt @@ -224,6 +224,7 @@ class ExternalCredentialEventTrackerUseCaseTest { ExternalCredentialType.NHISCard -> MfidDocument.GhanaNhisCard(credential = RAW_SCANNED_VALUE.asTokenizableRaw()) ExternalCredentialType.GhanaIdCard -> MfidDocument.GhanaIdCard(credential = RAW_SCANNED_VALUE.asTokenizableRaw()) ExternalCredentialType.QRCode -> MfidDocument.GhanaQrCode(credential = RAW_SCANNED_VALUE.asTokenizableRaw()) + ExternalCredentialType.FaydaCard -> MfidDocument.FaydaCard(credential = RAW_SCANNED_VALUE.asTokenizableRaw()) } val scannedResult = ScannedCredentialResult( credentialScanId = SCAN_ID, diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/AppExternalCredentialMapper.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/AppExternalCredentialMapper.kt index da06b75d40..a99b7e115e 100644 --- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/AppExternalCredentialMapper.kt +++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/AppExternalCredentialMapper.kt @@ -11,6 +11,7 @@ internal fun ExternalCredentialSearchResult.Complete?.toAppExternalCredential(): is MfidDocument.GhanaIdCard -> document.toNonCredentialFields() is MfidDocument.GhanaNhisCard -> document.toNonCredentialFields() is MfidDocument.GhanaQrCode -> emptyMap() + is MfidDocument.FaydaCard -> emptyMap() } return AppExternalCredential( id = scannedCredentialResult.credentialScanId, 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 index c2f20a6014..5f567e8e10 100644 --- 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 @@ -7,6 +7,7 @@ internal fun ExternalCredentialType.toProto(): ProtoExternalCredentialType = whe ExternalCredentialType.NHISCard -> ProtoExternalCredentialType.NHIS_CARD ExternalCredentialType.GhanaIdCard -> ProtoExternalCredentialType.GHANA_ID_CARD ExternalCredentialType.QRCode -> ProtoExternalCredentialType.QR_CODE + ExternalCredentialType.FaydaCard -> ProtoExternalCredentialType.FAYDA_CARD } internal fun ProtoExternalCredentialType.toDomain(): ExternalCredentialType = when(this){ @@ -15,4 +16,5 @@ internal fun ProtoExternalCredentialType.toDomain(): ExternalCredentialType = wh ProtoExternalCredentialType.NHIS_CARD -> ExternalCredentialType.NHISCard ProtoExternalCredentialType.GHANA_ID_CARD -> ExternalCredentialType.GhanaIdCard ProtoExternalCredentialType.QR_CODE -> ExternalCredentialType.QRCode + ProtoExternalCredentialType.FAYDA_CARD -> ExternalCredentialType.FaydaCard } 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 index 4a1443e2d6..1d8124419f 100644 --- 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 @@ -1,5 +1,6 @@ package com.simprints.infra.config.store.local.models +import com.simprints.infra.config.store.models.FaydaCardConfig import com.simprints.infra.config.store.models.GhanaIdCardConfig import com.simprints.infra.config.store.models.MultiFactorIdConfiguration import com.simprints.infra.config.store.models.NhisCardConfig @@ -11,6 +12,7 @@ internal fun MultiFactorIdConfiguration.toProto(): ProtoMultiFactorIdConfigurati .also { if (ghanaIdCardConfig != null) it.setGhanaIdCardConfig(ghanaIdCardConfig.toProto()) } .also { if (nhisCardConfig != null) it.setNhisCardConfig(nhisCardConfig.toProto()) } .also { if (qrCodeConfig != null) it.setQrCodeConfig(qrCodeConfig.toProto()) } + .also { if (faydaCardConfig != null) it.setFaydaCardConfig(faydaCardConfig.toProto()) } .build() internal fun GhanaIdCardConfig.toProto(): ProtoGhanaIdCardConfig = ProtoGhanaIdCardConfig @@ -27,11 +29,17 @@ internal fun QrCodeConfig.toProto(): ProtoQrCodeConfig = ProtoQrCodeConfig .newBuilder() .build() +internal fun FaydaCardConfig.toProto(): ProtoFaydaCardConfig = ProtoFaydaCardConfig + .newBuilder() + .setIsCapturingAllFields(isCapturingAllFields) + .build() + internal fun ProtoMultiFactorIdConfiguration.toDomain(): MultiFactorIdConfiguration = MultiFactorIdConfiguration( allowedExternalCredentials = allowedExternalCredentialsList.map { it.toDomain() }, ghanaIdCardConfig = if (hasGhanaIdCardConfig()) ghanaIdCardConfig.toDomain() else null, nhisCardConfig = if (hasNhisCardConfig()) nhisCardConfig.toDomain() else null, qrCodeConfig = if (hasQrCodeConfig()) QrCodeConfig else null, + faydaCardConfig = if (hasFaydaCardConfig()) faydaCardConfig.toDomain() else null, ) internal fun ProtoGhanaIdCardConfig.toDomain() = GhanaIdCardConfig( @@ -41,3 +49,7 @@ internal fun ProtoGhanaIdCardConfig.toDomain() = GhanaIdCardConfig( internal fun ProtoNhisCardConfig.toDomain() = NhisCardConfig( isCapturingAllFields = isCapturingAllFields, ) + +internal fun ProtoFaydaCardConfig.toDomain() = FaydaCardConfig( + isCapturingAllFields = isCapturingAllFields, +) diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/MultiFactorIdConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/MultiFactorIdConfiguration.kt index eb1fa31c65..02cc3d1ab7 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/MultiFactorIdConfiguration.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/MultiFactorIdConfiguration.kt @@ -7,6 +7,7 @@ data class MultiFactorIdConfiguration( val ghanaIdCardConfig: GhanaIdCardConfig?, val nhisCardConfig: NhisCardConfig?, val qrCodeConfig: QrCodeConfig?, + val faydaCardConfig: FaydaCardConfig?, ) data class GhanaIdCardConfig( @@ -18,3 +19,7 @@ data class NhisCardConfig( ) object QrCodeConfig + +data class FaydaCardConfig( + val isCapturingAllFields: Boolean, +) 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 bd1f2bdc5e..03c25e12ce 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 @@ -2,10 +2,12 @@ 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.FaydaCardConfig import com.simprints.infra.config.store.models.GhanaIdCardConfig import com.simprints.infra.config.store.models.MultiFactorIdConfiguration import com.simprints.infra.config.store.models.NhisCardConfig import com.simprints.infra.config.store.models.QrCodeConfig +import com.simprints.infra.config.store.remote.models.ApiExternalCredentialType.FAYDA_CARD import com.simprints.infra.config.store.remote.models.ApiExternalCredentialType.GHANA_CARD import com.simprints.infra.config.store.remote.models.ApiExternalCredentialType.NHIS_CARD import com.simprints.infra.config.store.remote.models.ApiExternalCredentialType.QR_CODE @@ -18,12 +20,14 @@ internal data class ApiMultiFactorIdConfiguration( val ghanaCard: ApiGhanaIdCardConfig? = null, val nhisCard: ApiNhisCardConfig? = null, val qrCode: ApiQrCodeConfig? = null, + val faydaCard: ApiFaydaCardConfig? = null, ) { fun toDomain(): MultiFactorIdConfiguration = MultiFactorIdConfiguration( allowedExternalCredentials = allowedExternalCredentials.map { it.toDomain() }, ghanaIdCardConfig = ghanaCard?.toDomain(), nhisCardConfig = nhisCard?.toDomain(), qrCodeConfig = qrCode?.toDomain(), + faydaCardConfig = faydaCard?.toDomain(), ) } @@ -49,17 +53,27 @@ object ApiQrCodeConfig { fun toDomain() = QrCodeConfig } +@Keep +@Serializable +data class ApiFaydaCardConfig( + val isCapturingAllFields: Boolean, +) { + fun toDomain() = FaydaCardConfig(isCapturingAllFields = isCapturingAllFields) +} + @Keep enum class ApiExternalCredentialType { NHIS_CARD, GHANA_CARD, QR_CODE, + FAYDA_CARD, ; fun toDomain(): ExternalCredentialType = when (this) { NHIS_CARD -> ExternalCredentialType.NHISCard GHANA_CARD -> ExternalCredentialType.GhanaIdCard QR_CODE -> ExternalCredentialType.QRCode + FAYDA_CARD -> ExternalCredentialType.FaydaCard } } @@ -67,4 +81,5 @@ fun ExternalCredentialType.fromDomainToApi(): ApiExternalCredentialType = when ( ExternalCredentialType.NHISCard -> NHIS_CARD ExternalCredentialType.GhanaIdCard -> GHANA_CARD ExternalCredentialType.QRCode -> QR_CODE + ExternalCredentialType.FaydaCard -> FAYDA_CARD } diff --git a/infra/config-store/src/main/proto/project_config.proto b/infra/config-store/src/main/proto/project_config.proto index cf018c7198..dd19b3d1fa 100644 --- a/infra/config-store/src/main/proto/project_config.proto +++ b/infra/config-store/src/main/proto/project_config.proto @@ -22,6 +22,7 @@ enum ProtoExternalCredentialType { NHIS_CARD = 1; GHANA_ID_CARD = 2; QR_CODE = 3; + FAYDA_CARD = 4; } message ProtoMultiFactorIdConfiguration { @@ -29,6 +30,7 @@ message ProtoMultiFactorIdConfiguration { optional ProtoGhanaIdCardConfig ghana_id_card_config = 2; optional ProtoNhisCardConfig nhis_card_config = 3; optional ProtoQrCodeConfig qr_code_config = 4; + optional ProtoFaydaCardConfig fayda_card_config = 5; } message ProtoGhanaIdCardConfig { @@ -43,6 +45,10 @@ message ProtoQrCodeConfig { // Empty, reserved for future configuration } +message ProtoFaydaCardConfig { + bool is_capturing_all_fields = 1; +} + message ProtoGeneralConfiguration { repeated Modality modalities = 1; repeated string language_options = 2; 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 8b3b3b9feb..f4af2b4687 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 @@ -427,6 +427,7 @@ internal val multiFactorIdConfiguration = MultiFactorIdConfiguration( allowedExternalCredentials = listOf(allowedExternalCredential), ghanaIdCardConfig = null, nhisCardConfig = null, + faydaCardConfig = null, qrCodeConfig = null, ) 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 index 01e93df4d2..afd6be00f6 100644 --- 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 @@ -9,4 +9,5 @@ enum class ExternalCredentialType { NHISCard, GhanaIdCard, QRCode, + FaydaCard, } diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/cosync/v1/CoSyncExternalCredentialType.kt b/infra/events/src/main/java/com/simprints/infra/events/event/cosync/v1/CoSyncExternalCredentialType.kt index 1bfa503f14..afc722790d 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/cosync/v1/CoSyncExternalCredentialType.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/cosync/v1/CoSyncExternalCredentialType.kt @@ -16,16 +16,19 @@ enum class CoSyncExternalCredentialType { NHISCard, GhanaIdCard, QRCode, + FaydaCard, } fun ExternalCredentialType.toCoSync(): CoSyncExternalCredentialType = when (this) { ExternalCredentialType.NHISCard -> CoSyncExternalCredentialType.NHISCard ExternalCredentialType.GhanaIdCard -> CoSyncExternalCredentialType.GhanaIdCard ExternalCredentialType.QRCode -> CoSyncExternalCredentialType.QRCode + ExternalCredentialType.FaydaCard -> CoSyncExternalCredentialType.FaydaCard } fun CoSyncExternalCredentialType.toDomain(): ExternalCredentialType = when (this) { CoSyncExternalCredentialType.NHISCard -> ExternalCredentialType.NHISCard CoSyncExternalCredentialType.GhanaIdCard -> ExternalCredentialType.GhanaIdCard CoSyncExternalCredentialType.QRCode -> ExternalCredentialType.QRCode + CoSyncExternalCredentialType.FaydaCard -> ExternalCredentialType.FaydaCard } diff --git a/infra/resources/src/main/res/values/strings.xml b/infra/resources/src/main/res/values/strings.xml index ed545595d7..edf3a54659 100644 --- a/infra/resources/src/main/res/values/strings.xml +++ b/infra/resources/src/main/res/values/strings.xml @@ -498,9 +498,11 @@ NHIS Card Ghana ID Card QR Code + Fayda Card Membership number Personal ID QR Code value + Fayda Alias Number (FAN) a document Skip scanning? 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 d517b76ce0..aab4dbd69b 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 @@ -124,6 +124,7 @@ internal val multiFactorIdConfiguration = MultiFactorIdConfiguration( allowedExternalCredentials = listOf(allowedExternalCredential), ghanaIdCardConfig = null, nhisCardConfig = null, + faydaCardConfig = null, qrCodeConfig = null, ) diff --git a/testing/data-generator/src/main/java/com/simprints/feature/datagenerator/enrollmentrecords/InsertEnrollmentRecordsUseCase.kt b/testing/data-generator/src/main/java/com/simprints/feature/datagenerator/enrollmentrecords/InsertEnrollmentRecordsUseCase.kt index c11150211b..095718229b 100644 --- a/testing/data-generator/src/main/java/com/simprints/feature/datagenerator/enrollmentrecords/InsertEnrollmentRecordsUseCase.kt +++ b/testing/data-generator/src/main/java/com/simprints/feature/datagenerator/enrollmentrecords/InsertEnrollmentRecordsUseCase.kt @@ -182,6 +182,7 @@ internal class InsertEnrollmentRecordsUseCase @Inject constructor( ExternalCredentialType.GhanaIdCard -> "GHA-12345789-0" ExternalCredentialType.NHISCard -> "12345678" ExternalCredentialType.QRCode -> "123456" + ExternalCredentialType.FaydaCard -> "1234567812345678" }.asTokenizableRaw() return tokenizationProcessor.encrypt(