diff --git a/src/main/kotlin/com/android/trisolarisserver/component/DocumentExtractionService.kt b/src/main/kotlin/com/android/trisolarisserver/component/DocumentExtractionService.kt index 629d58d..fa955db 100644 --- a/src/main/kotlin/com/android/trisolarisserver/component/DocumentExtractionService.kt +++ b/src/main/kotlin/com/android/trisolarisserver/component/DocumentExtractionService.kt @@ -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) { + 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 = objectMapper.readValue( + data, + object : TypeReference>() {} + ) + 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>() {}) + } 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, + 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, key: String, value: String): Boolean { + val current = results[key] + if (current == value) return false + results[key] = value + return true + } + private fun computeDocType(results: Map, 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= 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+"), "") diff --git a/src/main/kotlin/com/android/trisolarisserver/db/repo/GuestDocumentRepo.kt b/src/main/kotlin/com/android/trisolarisserver/db/repo/GuestDocumentRepo.kt index 1948eec..7ddf2cd 100644 --- a/src/main/kotlin/com/android/trisolarisserver/db/repo/GuestDocumentRepo.kt +++ b/src/main/kotlin/com/android/trisolarisserver/db/repo/GuestDocumentRepo.kt @@ -8,6 +8,7 @@ interface GuestDocumentRepo : JpaRepository { fun findByPropertyIdAndGuestIdOrderByUploadedAtDesc(propertyId: UUID, guestId: UUID): List fun findByIdAndPropertyIdAndGuestId(id: UUID, propertyId: UUID, guestId: UUID): GuestDocument? fun existsByGuestId(guestId: UUID): Boolean + fun findByBookingIdOrderByUploadedAtDesc(bookingId: UUID): List fun existsByPropertyIdAndGuestIdAndBookingIdAndFileHash( propertyId: UUID, guestId: UUID,