Track Aadhaar verification and similarity
All checks were successful
build-and-deploy / build-deploy (push) Successful in 40s
All checks were successful
build-and-deploy / build-deploy (push) Successful in 40s
This commit is contained in:
@@ -2,11 +2,14 @@ package com.android.trisolarisserver.component
|
||||
|
||||
import com.android.trisolarisserver.controller.DocumentPrompts
|
||||
import com.android.trisolarisserver.db.repo.GuestRepo
|
||||
import com.android.trisolarisserver.db.repo.GuestDocumentRepo
|
||||
import com.android.trisolarisserver.db.repo.BookingRepo
|
||||
import com.android.trisolarisserver.models.booking.GuestDocument
|
||||
import com.android.trisolarisserver.models.booking.GuestVehicle
|
||||
import com.android.trisolarisserver.repo.GuestVehicleRepo
|
||||
import com.android.trisolarisserver.repo.PropertyRepo
|
||||
import com.fasterxml.jackson.core.type.TypeReference
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.UUID
|
||||
import org.slf4j.LoggerFactory
|
||||
@@ -15,12 +18,14 @@ import org.slf4j.LoggerFactory
|
||||
class DocumentExtractionService(
|
||||
private val llamaClient: LlamaClient,
|
||||
private val guestRepo: GuestRepo,
|
||||
private val guestDocumentRepo: GuestDocumentRepo,
|
||||
private val guestVehicleRepo: GuestVehicleRepo,
|
||||
private val propertyRepo: PropertyRepo,
|
||||
private val paddleOcrClient: PaddleOcrClient,
|
||||
private val bookingRepo: BookingRepo,
|
||||
private val pincodeResolver: PincodeResolver,
|
||||
private val bookingEvents: BookingEvents
|
||||
private val bookingEvents: BookingEvents,
|
||||
private val objectMapper: ObjectMapper
|
||||
) {
|
||||
private val logger = LoggerFactory.getLogger(DocumentExtractionService::class.java)
|
||||
private val aadhaarRegex = Regex("\\b\\d{4}\\s?\\d{4}\\s?\\d{4}\\b")
|
||||
@@ -268,6 +273,7 @@ class DocumentExtractionService(
|
||||
applyBookingCityUpdates(document, results)
|
||||
// Final Aadhaar checksum pass before doc type decision.
|
||||
markAadhaarIfValid(results)
|
||||
applyAadhaarVerificationAndMatching(document, results)
|
||||
logIdNumber("after-aadhaar-checksum", document.id, results)
|
||||
results["docType"] = computeDocType(results, handled)
|
||||
applyGuestUpdates(document, propertyId, results)
|
||||
@@ -321,6 +327,110 @@ class DocumentExtractionService(
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyAadhaarVerificationAndMatching(document: GuestDocument, results: MutableMap<String, String>) {
|
||||
val bookingId = document.booking?.id ?: return
|
||||
val idKey = DocumentPrompts.ID_NUMBER.first
|
||||
val digits = normalizeDigits(cleanedValue(results[idKey]))
|
||||
val hasDigits = digits != null && digits.length == 12
|
||||
val isValid = hasDigits && isValidAadhaar(digits!!)
|
||||
if (hasDigits) {
|
||||
results["aadhaarVerified"] = if (isValid) "YES" else "NO"
|
||||
}
|
||||
|
||||
val docs = guestDocumentRepo.findByBookingIdOrderByUploadedAtDesc(bookingId)
|
||||
val verified = if (isValid) {
|
||||
VerifiedAadhaar(document.id, digits!!)
|
||||
} else {
|
||||
docs.firstNotNullOfOrNull { existing ->
|
||||
extractVerified(existing)
|
||||
}
|
||||
}
|
||||
|
||||
if (verified == null) return
|
||||
|
||||
if (!isValid && hasDigits) {
|
||||
val match = computeAadhaarMatch(digits!!, verified.digits)
|
||||
applyMatchResults(results, match, verified)
|
||||
}
|
||||
|
||||
for (existing in docs) {
|
||||
if (existing.id == document.id) continue
|
||||
val existingDigits = extractAadhaarDigits(existing) ?: continue
|
||||
if (isValidAadhaar(existingDigits)) continue
|
||||
val match = computeAadhaarMatch(existingDigits, verified.digits)
|
||||
val updated = updateExtractedData(existing, match, verified)
|
||||
if (updated) {
|
||||
guestDocumentRepo.save(existing)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractVerified(document: GuestDocument): VerifiedAadhaar? {
|
||||
val digits = extractAadhaarDigits(document) ?: return null
|
||||
if (!isValidAadhaar(digits)) return null
|
||||
return VerifiedAadhaar(document.id, digits)
|
||||
}
|
||||
|
||||
private fun extractAadhaarDigits(document: GuestDocument): String? {
|
||||
val raw = extractFromDocument(document, DocumentPrompts.ID_NUMBER.first) ?: return null
|
||||
val digits = normalizeDigits(cleanedValue(raw)) ?: return null
|
||||
return if (digits.length == 12) digits else null
|
||||
}
|
||||
|
||||
private fun extractFromDocument(document: GuestDocument, key: String): String? {
|
||||
val data = document.extractedData ?: return null
|
||||
return try {
|
||||
val parsed: Map<String, String> = objectMapper.readValue(
|
||||
data,
|
||||
object : TypeReference<Map<String, String>>() {}
|
||||
)
|
||||
parsed[key]
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateExtractedData(
|
||||
document: GuestDocument,
|
||||
match: AadhaarMatch,
|
||||
verified: VerifiedAadhaar
|
||||
): Boolean {
|
||||
val raw = document.extractedData ?: return false
|
||||
val parsed = try {
|
||||
objectMapper.readValue(raw, object : TypeReference<MutableMap<String, String>>() {})
|
||||
} catch (_: Exception) {
|
||||
mutableMapOf()
|
||||
}
|
||||
val changed = applyMatchResults(parsed, match, verified)
|
||||
if (!changed) return false
|
||||
document.extractedData = objectMapper.writeValueAsString(parsed)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun applyMatchResults(
|
||||
results: MutableMap<String, String>,
|
||||
match: AadhaarMatch,
|
||||
verified: VerifiedAadhaar
|
||||
): Boolean {
|
||||
var changed = false
|
||||
val targetMasked = maskAadhaar(verified.digits)
|
||||
changed = setIfChanged(results, "aadhaarMatchOrdered", match.ordered.toString()) || changed
|
||||
changed = setIfChanged(results, "aadhaarMatchUnordered", match.unordered.toString()) || changed
|
||||
changed = setIfChanged(results, "aadhaarMatchSimilar", if (match.similar) "YES" else "NO") || changed
|
||||
changed = setIfChanged(results, "aadhaarMatchWith", targetMasked) || changed
|
||||
verified.id?.let {
|
||||
changed = setIfChanged(results, "aadhaarMatchWithDocId", it.toString()) || changed
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
private fun setIfChanged(results: MutableMap<String, String>, key: String, value: String): Boolean {
|
||||
val current = results[key]
|
||||
if (current == value) return false
|
||||
results[key] = value
|
||||
return true
|
||||
}
|
||||
|
||||
private fun computeDocType(results: Map<String, String>, handled: Boolean): String {
|
||||
if (!handled && !(isYes(results["hasAadhar"]) || isYes(results["hasUidai"]))) {
|
||||
return "GENERAL"
|
||||
@@ -551,6 +661,35 @@ private fun logIdNumber(stage: String, documentId: UUID?, results: Map<String, S
|
||||
private val standardPlateRegex = Regex("^[A-Z]{2}\\d{1,2}[A-Z]{1,3}\\d{3,4}$")
|
||||
private val bhPlateRegex = Regex("^\\d{2}BH\\d{4}[A-Z]{1,2}$")
|
||||
private val pinCodeRegex = Regex("\\b\\d{6}\\b")
|
||||
private data class AadhaarMatch(
|
||||
val ordered: Int,
|
||||
val unordered: Int,
|
||||
val similar: Boolean
|
||||
)
|
||||
|
||||
private data class VerifiedAadhaar(
|
||||
val id: UUID?,
|
||||
val digits: String
|
||||
)
|
||||
|
||||
private fun computeAadhaarMatch(candidate: String, verified: String): AadhaarMatch {
|
||||
val ordered = candidate.zip(verified).count { it.first == it.second }
|
||||
val unordered = unorderedMatchCount(candidate, verified)
|
||||
val similar = ordered >= 8 || unordered >= 8
|
||||
return AadhaarMatch(ordered, unordered, similar)
|
||||
}
|
||||
|
||||
private fun unorderedMatchCount(a: String, b: String): Int {
|
||||
val countsA = IntArray(10)
|
||||
val countsB = IntArray(10)
|
||||
a.forEach { if (it.isDigit()) countsA[it - '0']++ }
|
||||
b.forEach { if (it.isDigit()) countsB[it - '0']++ }
|
||||
var total = 0
|
||||
for (i in 0..9) {
|
||||
total += minOf(countsA[i], countsB[i])
|
||||
}
|
||||
return total
|
||||
}
|
||||
private fun extractPinFromValue(value: String?): String? {
|
||||
if (value.isNullOrBlank()) return null
|
||||
val compact = value.replace(Regex("\\s+"), "")
|
||||
|
||||
@@ -8,6 +8,7 @@ interface GuestDocumentRepo : JpaRepository<GuestDocument, UUID> {
|
||||
fun findByPropertyIdAndGuestIdOrderByUploadedAtDesc(propertyId: UUID, guestId: UUID): List<GuestDocument>
|
||||
fun findByIdAndPropertyIdAndGuestId(id: UUID, propertyId: UUID, guestId: UUID): GuestDocument?
|
||||
fun existsByGuestId(guestId: UUID): Boolean
|
||||
fun findByBookingIdOrderByUploadedAtDesc(bookingId: UUID): List<GuestDocument>
|
||||
fun existsByPropertyIdAndGuestIdAndBookingIdAndFileHash(
|
||||
propertyId: UUID,
|
||||
guestId: UUID,
|
||||
|
||||
Reference in New Issue
Block a user