From e9e39e645c5bc6e7a2a47e750da9f462e2dad44a Mon Sep 17 00:00:00 2001 From: androidlover5842 Date: Sat, 31 Jan 2026 11:48:02 +0530 Subject: [PATCH] Resolve pin via data.gov.in with fallbacks --- .../component/DataGovPincodeClient.kt | 64 +++++++++++++++++++ .../component/DocumentExtractionService.kt | 23 +++++-- .../component/GoogleGeocodingClient.kt | 28 ++------ .../component/PincodeResolver.kt | 41 ++++++++++++ .../component/PostalPincodeClient.kt | 56 ++++++++++++++++ src/main/resources/application.properties | 4 ++ 6 files changed, 189 insertions(+), 27 deletions(-) create mode 100644 src/main/kotlin/com/android/trisolarisserver/component/DataGovPincodeClient.kt create mode 100644 src/main/kotlin/com/android/trisolarisserver/component/PincodeResolver.kt create mode 100644 src/main/kotlin/com/android/trisolarisserver/component/PostalPincodeClient.kt diff --git a/src/main/kotlin/com/android/trisolarisserver/component/DataGovPincodeClient.kt b/src/main/kotlin/com/android/trisolarisserver/component/DataGovPincodeClient.kt new file mode 100644 index 0000000..8bf1437 --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/component/DataGovPincodeClient.kt @@ -0,0 +1,64 @@ +package com.android.trisolarisserver.component + +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Component +import org.springframework.web.client.RestTemplate +import org.springframework.web.util.UriComponentsBuilder + +@Component +class DataGovPincodeClient( + private val restTemplate: RestTemplate, + private val objectMapper: ObjectMapper, + @Value("\${pincode.datagov.apiKey:}") + private val apiKey: String, + @Value("\${pincode.datagov.baseUrl:https://api.data.gov.in/resource/5c2f62fe-5afa-4119-a499-fec9d604d5bd}") + private val baseUrl: String +) { + private val logger = LoggerFactory.getLogger(DataGovPincodeClient::class.java) + + fun resolve(pinCode: String): PincodeLookupResult { + if (apiKey.isBlank()) return PincodeLookupResult(null, null, "NO_API_KEY", "data.gov.in") + return try { + val url = UriComponentsBuilder.fromUriString(baseUrl) + .queryParam("api-key", apiKey) + .queryParam("format", "json") + .queryParam("filters[pincode]", pinCode) + .toUriString() + val response = restTemplate.getForEntity(url, String::class.java) + val body = response.body ?: return PincodeLookupResult(null, null, "EMPTY_BODY", "data.gov.in") + val resolved = parseCityState(body) + val status = if (resolved == null) "ZERO_RESULTS" else "OK" + PincodeLookupResult(resolved, body, status, "data.gov.in") + } catch (ex: Exception) { + logger.warn("Data.gov.in lookup failed: {}", ex.message) + PincodeLookupResult(null, null, "ERROR", "data.gov.in") + } + } + + private fun parseCityState(body: String): String? { + val root = objectMapper.readTree(body) + val records = root.path("records") + if (!records.isArray || records.isEmpty) return null + val chosen = chooseRecord(records) ?: return null + val district = chosen.path("district").asText(null) + val state = chosen.path("statename").asText(null) + val districtName = district?.let { toTitleCase(it) } + val stateName = state?.let { toTitleCase(it) } + if (districtName.isNullOrBlank() && stateName.isNullOrBlank()) return null + return listOfNotNull(districtName?.ifBlank { null }, stateName?.ifBlank { null }).joinToString(", ") + } + + private fun chooseRecord(records: JsonNode): JsonNode? { + val delivery = records.firstOrNull { it.path("delivery").asText("").equals("Delivery", true) } + return delivery ?: records.firstOrNull() + } + + private fun toTitleCase(value: String): String { + return value.lowercase().split(Regex("\\s+")).joinToString(" ") { word -> + word.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } + } + } +} diff --git a/src/main/kotlin/com/android/trisolarisserver/component/DocumentExtractionService.kt b/src/main/kotlin/com/android/trisolarisserver/component/DocumentExtractionService.kt index d3035e8..b225e39 100644 --- a/src/main/kotlin/com/android/trisolarisserver/component/DocumentExtractionService.kt +++ b/src/main/kotlin/com/android/trisolarisserver/component/DocumentExtractionService.kt @@ -19,7 +19,7 @@ class DocumentExtractionService( private val propertyRepo: PropertyRepo, private val paddleOcrClient: PaddleOcrClient, private val bookingRepo: BookingRepo, - private val googleGeocodingClient: GoogleGeocodingClient + private val pincodeResolver: PincodeResolver ) { private val logger = LoggerFactory.getLogger(DocumentExtractionService::class.java) @@ -432,13 +432,24 @@ class DocumentExtractionService( if (booking.fromCity?.isNotBlank() == true && booking.toCity?.isNotBlank() == true) return val pin = cleanedValue(results[DocumentPrompts.PIN_CODE.first]) ?: return if (!isValidPin(pin)) return - val geocode = googleGeocodingClient.resolveCityState(pin) - geocode.status?.let { results["geoStatus"] = it } - if (geocode.rawResponse != null) { - results["geoResponse"] = geocode.rawResponse.take(4000) + val resolvedResult = pincodeResolver.resolve(pin) + val primary = resolvedResult.primary + results["geoPrimarySource"] = primary.source + primary.status?.let { results["geoPrimaryStatus"] = it } + primary.rawResponse?.let { results["geoPrimaryResponse"] = it.take(4000) } + resolvedResult.secondary?.let { secondary -> + results["geoSecondarySource"] = secondary.source + secondary.status?.let { results["geoSecondaryStatus"] = it } + secondary.rawResponse?.let { results["geoSecondaryResponse"] = it.take(4000) } } - val resolved = geocode.resolvedCityState ?: return + resolvedResult.tertiary?.let { tertiary -> + results["geoTertiarySource"] = tertiary.source + tertiary.status?.let { results["geoTertiaryStatus"] = it } + tertiary.rawResponse?.let { results["geoTertiaryResponse"] = it.take(4000) } + } + val resolved = resolvedResult.resolved()?.resolvedCityState ?: return results["geoResolved"] = resolved + results["geoSource"] = resolvedResult.resolved()?.source ?: "" var updated = false if (booking.fromCity.isNullOrBlank()) { booking.fromCity = resolved diff --git a/src/main/kotlin/com/android/trisolarisserver/component/GoogleGeocodingClient.kt b/src/main/kotlin/com/android/trisolarisserver/component/GoogleGeocodingClient.kt index 29de383..edc929a 100644 --- a/src/main/kotlin/com/android/trisolarisserver/component/GoogleGeocodingClient.kt +++ b/src/main/kotlin/com/android/trisolarisserver/component/GoogleGeocodingClient.kt @@ -68,15 +68,6 @@ class GoogleGeocodingClient( val resultNode = results.firstOrNull { node -> node.path("types").any { it.asText(null) == "postal_code" } } ?: results.first() - val postcodeLocalities = resultNode.path("postcode_localities") - if (postcodeLocalities.isArray) { - val candidates = postcodeLocalities.mapNotNull { it.asText(null) } - val agra = candidates.firstOrNull { it.equals("Agra", ignoreCase = true) } - if (agra != null) { - val stateOnly = extractState(resultNode) - return listOfNotNull(agra, stateOnly).joinToString(", ") - } - } val components = resultNode.path("address_components") if (!components.isArray) return null @@ -101,18 +92,6 @@ class GoogleGeocodingClient( if (preferredCity == null && state.isNullOrBlank()) return null return listOfNotNull(preferredCity, state?.trim()?.ifBlank { null }).joinToString(", ") } - - private fun extractState(resultNode: com.fasterxml.jackson.databind.JsonNode): String? { - val components = resultNode.path("address_components") - if (!components.isArray) return null - for (comp in components) { - val types = comp.path("types").mapNotNull { it.asText(null) }.toSet() - if ("administrative_area_level_1" in types) { - return comp.path("long_name").asText(null) - } - } - return null - } } data class GeocodeResult( @@ -120,3 +99,10 @@ data class GeocodeResult( val rawResponse: String?, val status: String? ) + +data class PincodeLookupResult( + val resolvedCityState: String?, + val rawResponse: String?, + val status: String?, + val source: String +) diff --git a/src/main/kotlin/com/android/trisolarisserver/component/PincodeResolver.kt b/src/main/kotlin/com/android/trisolarisserver/component/PincodeResolver.kt new file mode 100644 index 0000000..48fbb57 --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/component/PincodeResolver.kt @@ -0,0 +1,41 @@ +package com.android.trisolarisserver.component + +import org.springframework.stereotype.Component + +@Component +class PincodeResolver( + private val dataGovPincodeClient: DataGovPincodeClient, + private val postalPincodeClient: PostalPincodeClient, + private val googleGeocodingClient: GoogleGeocodingClient +) { + fun resolve(pinCode: String): PincodeResolveResult { + val primary = dataGovPincodeClient.resolve(pinCode) + if (primary.status == "OK" && primary.resolvedCityState != null) { + return PincodeResolveResult(primary, null, null) + } + + val secondary = postalPincodeClient.resolve(pinCode) + if (secondary.status == "OK" && secondary.resolvedCityState != null) { + return PincodeResolveResult(primary, secondary, null) + } + + val google = googleGeocodingClient.resolveCityState(pinCode) + val tertiary = PincodeLookupResult( + google.resolvedCityState, + google.rawResponse, + google.status, + "google" + ) + return PincodeResolveResult(primary, secondary, tertiary) + } +} + +data class PincodeResolveResult( + val primary: PincodeLookupResult, + val secondary: PincodeLookupResult?, + val tertiary: PincodeLookupResult? +) { + fun resolved(): PincodeLookupResult? { + return sequenceOf(primary, secondary, tertiary).firstOrNull { it?.resolvedCityState != null } + } +} diff --git a/src/main/kotlin/com/android/trisolarisserver/component/PostalPincodeClient.kt b/src/main/kotlin/com/android/trisolarisserver/component/PostalPincodeClient.kt new file mode 100644 index 0000000..02b325d --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/component/PostalPincodeClient.kt @@ -0,0 +1,56 @@ +package com.android.trisolarisserver.component + +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Component +import org.springframework.web.client.RestTemplate +import org.springframework.web.util.UriComponentsBuilder + +@Component +class PostalPincodeClient( + private val restTemplate: RestTemplate, + private val objectMapper: ObjectMapper, + @Value("\${pincode.postal.baseUrl:https://api.postalpincode.in}") + private val baseUrl: String +) { + private val logger = LoggerFactory.getLogger(PostalPincodeClient::class.java) + + fun resolve(pinCode: String): PincodeLookupResult { + return try { + val url = UriComponentsBuilder.fromUriString(baseUrl) + .path("/pincode/{pin}") + .buildAndExpand(pinCode) + .toUriString() + val response = restTemplate.getForEntity(url, String::class.java) + val body = response.body ?: return PincodeLookupResult(null, null, "EMPTY_BODY", "postalpincode.in") + val resolved = parseCityState(body) + val status = if (resolved == null) "ZERO_RESULTS" else "OK" + PincodeLookupResult(resolved, body, status, "postalpincode.in") + } catch (ex: Exception) { + logger.warn("Postalpincode lookup failed: {}", ex.message) + PincodeLookupResult(null, null, "ERROR", "postalpincode.in") + } + } + + private fun parseCityState(body: String): String? { + val root = objectMapper.readTree(body) + if (!root.isArray || root.isEmpty) return null + val first = root.first() + val status = first.path("Status").asText(null) + if (!status.equals("Success", true)) return null + val offices = first.path("PostOffice") + if (!offices.isArray || offices.isEmpty) return null + val office = chooseOffice(offices) ?: return null + val district = office.path("District").asText(null) + val state = office.path("State").asText(null) + if (district.isNullOrBlank() && state.isNullOrBlank()) return null + return listOfNotNull(district?.ifBlank { null }, state?.ifBlank { null }).joinToString(", ") + } + + private fun chooseOffice(offices: JsonNode): JsonNode? { + val delivery = offices.firstOrNull { it.path("DeliveryStatus").asText("").equals("Delivery", true) } + return delivery ?: offices.firstOrNull() + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index c5951e5..6fe6a32 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -33,5 +33,9 @@ ocr.paddle.baseUrl=https://ocr.hoteltrisolaris.in/ ocr.paddle.minScore=0.9 ocr.paddle.minAverageScore=0.75 ocr.paddle.minTextLength=4 + +pincode.datagov.apiKey=579b464db66ec23bdd000001cdd3946e44ce4aad7209ff7b23ac571b +pincode.datagov.baseUrl=https://api.data.gov.in/resource/5c2f62fe-5afa-4119-a499-fec9d604d5bd +pincode.postal.baseUrl=https://api.postalpincode.in google.maps.apiKey=AIzaSyAMuRNFWjccKSmPeR0loQI8etHMDtUIZ_k google.maps.geocode.baseUrl=https://maps.googleapis.com/maps/api/geocode/json