Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ internal class BuildMfidDocumentUseCase @Inject constructor(
): MfidDocument = when (documentType) {
OcrDocumentType.NhisCard -> buildNhisCard(scannedDocuments)
OcrDocumentType.GhanaIdCard -> buildGhanaIdCard(scannedDocuments)
OcrDocumentType.FaydaCard -> buildFaydaCard(scannedDocuments)
}

/**
Expand Down Expand Up @@ -68,6 +69,14 @@ internal class BuildMfidDocumentUseCase @Inject constructor(
)
}

private fun buildFaydaCard(scannedDocuments: List<ScannedMfidDocument>): 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 <T> List<T>.bestReadoutOrNull(ocrLine: (T) -> OcrLine?) = mapNotNull(ocrLine)
.takeUnless(List<OcrLine>::isEmpty)
?.let { getBestReadoutBasedOnConfidenceUseCase(it.map(OcrLine::text)) }
Expand All @@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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}$")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Comment on lines 17 to 21
) {
suspend operator fun invoke(
Expand All @@ -27,13 +28,15 @@ 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
val ocrReader = OcrReader(ocrText)
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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)
}
Comment thread
Copilot marked this conversation as resolved.
ExternalCredentialType.QRCode -> {
// No QR code validation as of 2025.4.1
true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines 28 to +31
ExternalCredentialType.QRCode -> false
}
return SearchCredentialState(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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
Expand All @@ -64,6 +71,7 @@ internal class ScanMfidDocumentUseCaseTest {
readTextFromImage = readTextFromImage,
ghanaNhisCardOcrReaderUseCase = ghanaNhisCardOcrReaderUseCase,
ghanaIdCardOcrReaderUseCase = ghanaIdCardOcrReaderUseCase,
faydaCardOcrReaderUseCase = faydaCardOcrReaderUseCase,
credentialImageRepository = credentialImageRepository,
)
coEvery { credentialImageRepository.saveCredentialScan(any(), any()) } returns savedImagePath
Expand Down Expand Up @@ -199,6 +207,7 @@ internal class ScanMfidDocumentUseCaseTest {
ghanaIdCardConfig = null,
nhisCardConfig = null,
qrCodeConfig = null,
faydaCardConfig = null,
)
every { readTextFromImage(bitmap) } returns mockk(relaxed = true)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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){
Expand All @@ -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
}
Loading