From c39188d45362b2c7b8f67d205a2ac2202a613c2e Mon Sep 17 00:00:00 2001 From: androidlover5842 Date: Sat, 7 Feb 2026 16:47:11 +0530 Subject: [PATCH] Add public country search API backed by country_reference --- docs/API_REFERENCE.txt | 26 +++++++++++ .../controller/system/CountrySearch.kt | 43 +++++++++++++++++ .../models/property/CountryReference.kt | 43 +++++++++++++++++ .../repo/property/CountryReferenceRepo.kt | 46 +++++++++++++++++++ .../security/PublicEndpoints.kt | 2 + 5 files changed, 160 insertions(+) create mode 100644 src/main/kotlin/com/android/trisolarisserver/controller/system/CountrySearch.kt create mode 100644 src/main/kotlin/com/android/trisolarisserver/models/property/CountryReference.kt create mode 100644 src/main/kotlin/com/android/trisolarisserver/repo/property/CountryReferenceRepo.kt diff --git a/docs/API_REFERENCE.txt b/docs/API_REFERENCE.txt index b819fda..691f5c0 100644 --- a/docs/API_REFERENCE.txt +++ b/docs/API_REFERENCE.txt @@ -148,6 +148,32 @@ AUTH + SYSTEM APIS - 400 Bad Request (q too short) - 401 Unauthorized + +- Country search API is this one: + + GET /geo/countries/search + + What it does: + + - Searches countries from local country_reference table. + - Case-insensitive match on country name, official name, ISO alpha-2, and ISO alpha-3. + - Example: q=IND returns matches like India and related country names. + + Request body: + + - None. + + Query params: + + - q (required, minimum 3 characters) + - limit (optional, default 20, min 1, max 100) + + - Allowed roles: Public endpoint. + + Error Codes + + - 400 Bad Request (q too short) + ================================================================================ PROPERTY + USER APIS ================================================================================ diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/system/CountrySearch.kt b/src/main/kotlin/com/android/trisolarisserver/controller/system/CountrySearch.kt new file mode 100644 index 0000000..dbfa6fa --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/controller/system/CountrySearch.kt @@ -0,0 +1,43 @@ +package com.android.trisolarisserver.controller.system + +import com.android.trisolarisserver.repo.property.CountryReferenceRepo +import org.springframework.data.domain.PageRequest +import org.springframework.http.HttpStatus +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.server.ResponseStatusException + +@RestController +class CountrySearch( + private val countryReferenceRepo: CountryReferenceRepo +) { + @GetMapping("/geo/countries/search") + fun searchCountries( + @RequestParam("q") q: String, + @RequestParam("limit", required = false, defaultValue = "20") limit: Int + ): List { + val query = q.trim() + if (query.length < 3) { + throw ResponseStatusException(HttpStatus.BAD_REQUEST, "q must be at least 3 characters") + } + val boundedLimit = limit.coerceIn(1, 100) + return countryReferenceRepo + .searchCountries(query, PageRequest.of(0, boundedLimit)) + .map { + CountrySearchResponse( + name = it.name, + officialName = it.officialName, + isoAlpha2 = it.isoAlpha2, + isoAlpha3 = it.isoAlpha3 + ) + } + } +} + +data class CountrySearchResponse( + val name: String, + val officialName: String?, + val isoAlpha2: String?, + val isoAlpha3: String? +) diff --git a/src/main/kotlin/com/android/trisolarisserver/models/property/CountryReference.kt b/src/main/kotlin/com/android/trisolarisserver/models/property/CountryReference.kt new file mode 100644 index 0000000..48393bc --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/models/property/CountryReference.kt @@ -0,0 +1,43 @@ +package com.android.trisolarisserver.models.property + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.Table +import java.math.BigDecimal +import java.time.OffsetDateTime + +@Entity +@Table(name = "country_reference") +class CountryReference( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + val id: Long? = null, + + @Column(name = "name", nullable = false) + val name: String, + + @Column(name = "official_name") + val officialName: String? = null, + + @Column(name = "iso_alpha2") + val isoAlpha2: String? = null, + + @Column(name = "iso_alpha3") + val isoAlpha3: String? = null, + + @Column(name = "latitude", precision = 11, scale = 8) + val latitude: BigDecimal? = null, + + @Column(name = "longitude", precision = 11, scale = 8) + val longitude: BigDecimal? = null, + + @Column(name = "source_row_code") + val sourceRowCode: Int? = null, + + @Column(name = "created_at", nullable = false, columnDefinition = "timestamptz") + val createdAt: OffsetDateTime = OffsetDateTime.now() +) diff --git a/src/main/kotlin/com/android/trisolarisserver/repo/property/CountryReferenceRepo.kt b/src/main/kotlin/com/android/trisolarisserver/repo/property/CountryReferenceRepo.kt new file mode 100644 index 0000000..24b7a33 --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/repo/property/CountryReferenceRepo.kt @@ -0,0 +1,46 @@ +package com.android.trisolarisserver.repo.property + +import com.android.trisolarisserver.models.property.CountryReference +import org.springframework.data.domain.Pageable +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query +import org.springframework.data.repository.query.Param + +interface CountryReferenceRepo : JpaRepository { + @Query( + """ + select c.name as name, + c.officialName as officialName, + c.isoAlpha2 as isoAlpha2, + c.isoAlpha3 as isoAlpha3 + from CountryReference c + where trim(c.name) <> '' + and ( + lower(c.name) like lower(concat('%', :query, '%')) + or lower(coalesce(c.officialName, '')) like lower(concat('%', :query, '%')) + or lower(coalesce(c.isoAlpha2, '')) like lower(concat('%', :query, '%')) + or lower(coalesce(c.isoAlpha3, '')) like lower(concat('%', :query, '%')) + ) + order by + case + when lower(c.name) = lower(:query) then 0 + when lower(coalesce(c.isoAlpha2, '')) = lower(:query) then 1 + when lower(coalesce(c.isoAlpha3, '')) = lower(:query) then 2 + when lower(c.name) like lower(concat(:query, '%')) then 3 + else 4 + end, + c.name asc + """ + ) + fun searchCountries( + @Param("query") query: String, + pageable: Pageable + ): List +} + +interface CountrySearchRow { + val name: String + val officialName: String? + val isoAlpha2: String? + val isoAlpha3: String? +} diff --git a/src/main/kotlin/com/android/trisolarisserver/security/PublicEndpoints.kt b/src/main/kotlin/com/android/trisolarisserver/security/PublicEndpoints.kt index bbdf2c3..89f3bf5 100644 --- a/src/main/kotlin/com/android/trisolarisserver/security/PublicEndpoints.kt +++ b/src/main/kotlin/com/android/trisolarisserver/security/PublicEndpoints.kt @@ -14,6 +14,7 @@ internal object PublicEndpoints { private val razorpayReturn = Regex("^/properties/[^/]+/razorpay/return/(success|failure)$") private val guestDocumentFile = Regex("^/properties/[^/]+/guests/[^/]+/documents/[^/]+/file$") private val cancellationPolicy = Regex("^/properties/[^/]+/cancellation-policy$") + private val countrySearch = Regex("^/geo/countries/search$") fun isPublic(request: HttpServletRequest): Boolean { val path = request.requestURI @@ -34,5 +35,6 @@ internal object PublicEndpoints { || iconPngFile.matches(path) || guestDocumentFile.matches(path) || (cancellationPolicy.matches(path) && method == "GET") + || (countrySearch.matches(path) && method == "GET") } }