Add public country search API backed by country_reference
All checks were successful
build-and-deploy / build-deploy (push) Successful in 36s

This commit is contained in:
androidlover5842
2026-02-07 16:47:11 +05:30
parent 71c10193a3
commit c39188d453
5 changed files with 160 additions and 0 deletions

View File

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

View File

@@ -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<CountrySearchResponse> {
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?
)

View File

@@ -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()
)

View File

@@ -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<CountryReference, Long> {
@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<CountrySearchRow>
}
interface CountrySearchRow {
val name: String
val officialName: String?
val isoAlpha2: String?
val isoAlpha3: String?
}

View File

@@ -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")
}
}