From d67a8c431e5a750f4fa9909d90b0c71f789b3a67 Mon Sep 17 00:00:00 2001 From: Marinov Date: Mon, 19 Oct 2026 19:12:17 +0300 Subject: [PATCH] Fix Fayda FAN false positive on adjacent date-of-birth dates The FAN reader stripped all non-digits from an OCR line before matching 16 digits, so the date-of-birth line "07/02/1998 | 2005/10/17" was read as FAN 0702199820051017. Match 16 digits (optionally whitespace-separated) within the line instead, rejecting punctuation between digits and longer digit runs. Co-Authored-By: Claude Opus 5.5 --- .../usecase/FaydaCardOcrReaderUseCase.kt | 21 +++---- .../usecase/FaydaCardOcrReaderUseCaseTest.kt | 58 +++++++++++++++++-- 2 files changed, 63 insertions(+), 16 deletions(-) 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 index 2bcbed4165..9a41260917 100644 --- 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 @@ -5,19 +5,20 @@ 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)) + operator fun invoke(ocrReader: OcrReader): OcrScanResult.FaydaCard? = ocrReader.ocrText.allLines.firstNotNullOfOrNull { line -> + FAN_OCR_PATTERN.find(line.text)?.let { match -> + OcrScanResult.FaydaCard(line.copy(text = match.value.filter(Char::isDigit))) + } } companion object { - // Fayda Alias Number (FAN): exactly 16 digits after stripping all non-digit characters + // Fayda Alias Number (FAN): exactly 16 digits val FAN_PATTERN = Regex("^\\d{16}$") + + // FAN within an OCR line: 16 digits, optionally split by OCR into up to 4 groups separated by 1-2 whitespace characters, + // and not part of a longer digit run (also across 1-2 whitespace characters). Wider gaps are treated as separate numbers. + // Punctuation between digits and more than 4 groups are not allowed, so that adjacent dates such as + // "07/02/1998 | 2005/10/17" are not read as a FAN, even if OCR reads the separators as spaces ("07 02 1998 2005 10 17"). + val FAN_OCR_PATTERN = Regex("(? + fun `returns matching line and strips whitespace from FAN`() { + validFansWithWhitespace.forEachIndexed { id, fan -> val reader = buildReader(line(id = id, text = fan, top = 100)) assertThat(useCase(reader)?.credential?.text).isEqualTo(fan.filter(Char::isDigit)) } } + @Test + fun `extracts FAN from line containing other text`() { + linesContainingFan.entries.forEachIndexed { id, (text, fan) -> + val reader = buildReader(line(id = id, text = text, top = 100)) + assertThat(useCase(reader)?.credential?.text).isEqualTo(fan) + } + } + @Test fun `returns null for invalid FAN formats`() { invalidFans.forEachIndexed { id, fan -> @@ -59,6 +93,18 @@ internal class FaydaCardOcrReaderUseCaseTest { } } + @Test + fun `skips date of birth line and returns FAN line`() { + val reader = buildReader( + line(id = 0, text = "07/02/1998 | 2005/10/17", top = 100), + line(id = 1, text = "30/07/2026 | 08/04/2034", top = 150), + line(id = 2, text = "8642370540168134", top = 200), + ) + val result = useCase(reader) + assertThat(result?.credential?.id).isEqualTo(2) + assertThat(result?.credential?.text).isEqualTo("8642370540168134") + } + private fun buildReader(vararg lines: OcrLine) = OcrReader( OcrText(allLines = lines.toList()), )