Track Aadhaar verification and similarity
All checks were successful
build-and-deploy / build-deploy (push) Successful in 40s

This commit is contained in:
androidlover5842
2026-01-31 23:33:02 +05:30
parent f5c6406e31
commit aab9b02659
2 changed files with 141 additions and 1 deletions

View File

@@ -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+"), "")

View File

@@ -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,