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.controller.DocumentPrompts
|
||||||
import com.android.trisolarisserver.db.repo.GuestRepo
|
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.db.repo.BookingRepo
|
||||||
import com.android.trisolarisserver.models.booking.GuestDocument
|
import com.android.trisolarisserver.models.booking.GuestDocument
|
||||||
import com.android.trisolarisserver.models.booking.GuestVehicle
|
import com.android.trisolarisserver.models.booking.GuestVehicle
|
||||||
import com.android.trisolarisserver.repo.GuestVehicleRepo
|
import com.android.trisolarisserver.repo.GuestVehicleRepo
|
||||||
import com.android.trisolarisserver.repo.PropertyRepo
|
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.time.OffsetDateTime
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
@@ -15,12 +18,14 @@ import org.slf4j.LoggerFactory
|
|||||||
class DocumentExtractionService(
|
class DocumentExtractionService(
|
||||||
private val llamaClient: LlamaClient,
|
private val llamaClient: LlamaClient,
|
||||||
private val guestRepo: GuestRepo,
|
private val guestRepo: GuestRepo,
|
||||||
|
private val guestDocumentRepo: GuestDocumentRepo,
|
||||||
private val guestVehicleRepo: GuestVehicleRepo,
|
private val guestVehicleRepo: GuestVehicleRepo,
|
||||||
private val propertyRepo: PropertyRepo,
|
private val propertyRepo: PropertyRepo,
|
||||||
private val paddleOcrClient: PaddleOcrClient,
|
private val paddleOcrClient: PaddleOcrClient,
|
||||||
private val bookingRepo: BookingRepo,
|
private val bookingRepo: BookingRepo,
|
||||||
private val pincodeResolver: PincodeResolver,
|
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 logger = LoggerFactory.getLogger(DocumentExtractionService::class.java)
|
||||||
private val aadhaarRegex = Regex("\\b\\d{4}\\s?\\d{4}\\s?\\d{4}\\b")
|
private val aadhaarRegex = Regex("\\b\\d{4}\\s?\\d{4}\\s?\\d{4}\\b")
|
||||||
@@ -268,6 +273,7 @@ class DocumentExtractionService(
|
|||||||
applyBookingCityUpdates(document, results)
|
applyBookingCityUpdates(document, results)
|
||||||
// Final Aadhaar checksum pass before doc type decision.
|
// Final Aadhaar checksum pass before doc type decision.
|
||||||
markAadhaarIfValid(results)
|
markAadhaarIfValid(results)
|
||||||
|
applyAadhaarVerificationAndMatching(document, results)
|
||||||
logIdNumber("after-aadhaar-checksum", document.id, results)
|
logIdNumber("after-aadhaar-checksum", document.id, results)
|
||||||
results["docType"] = computeDocType(results, handled)
|
results["docType"] = computeDocType(results, handled)
|
||||||
applyGuestUpdates(document, propertyId, results)
|
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 {
|
private fun computeDocType(results: Map<String, String>, handled: Boolean): String {
|
||||||
if (!handled && !(isYes(results["hasAadhar"]) || isYes(results["hasUidai"]))) {
|
if (!handled && !(isYes(results["hasAadhar"]) || isYes(results["hasUidai"]))) {
|
||||||
return "GENERAL"
|
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 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 bhPlateRegex = Regex("^\\d{2}BH\\d{4}[A-Z]{1,2}$")
|
||||||
private val pinCodeRegex = Regex("\\b\\d{6}\\b")
|
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? {
|
private fun extractPinFromValue(value: String?): String? {
|
||||||
if (value.isNullOrBlank()) return null
|
if (value.isNullOrBlank()) return null
|
||||||
val compact = value.replace(Regex("\\s+"), "")
|
val compact = value.replace(Regex("\\s+"), "")
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ interface GuestDocumentRepo : JpaRepository<GuestDocument, UUID> {
|
|||||||
fun findByPropertyIdAndGuestIdOrderByUploadedAtDesc(propertyId: UUID, guestId: UUID): List<GuestDocument>
|
fun findByPropertyIdAndGuestIdOrderByUploadedAtDesc(propertyId: UUID, guestId: UUID): List<GuestDocument>
|
||||||
fun findByIdAndPropertyIdAndGuestId(id: UUID, propertyId: UUID, guestId: UUID): GuestDocument?
|
fun findByIdAndPropertyIdAndGuestId(id: UUID, propertyId: UUID, guestId: UUID): GuestDocument?
|
||||||
fun existsByGuestId(guestId: UUID): Boolean
|
fun existsByGuestId(guestId: UUID): Boolean
|
||||||
|
fun findByBookingIdOrderByUploadedAtDesc(bookingId: UUID): List<GuestDocument>
|
||||||
fun existsByPropertyIdAndGuestIdAndBookingIdAndFileHash(
|
fun existsByPropertyIdAndGuestIdAndBookingIdAndFileHash(
|
||||||
propertyId: UUID,
|
propertyId: UUID,
|
||||||
guestId: UUID,
|
guestId: UUID,
|
||||||
|
|||||||
Reference in New Issue
Block a user