Skip to content

Commit

Permalink
Merge pull request #2738 from ministryofjustice/feature/aps-1745-retu…
Browse files Browse the repository at this point in the history
…rn-characteristics-in-space-search-results

APS 1745 return characteristics in space search results
  • Loading branch information
davidatkinsuk authored Dec 23, 2024
2 parents 9d85443 + 1da0784 commit 90f484d
Show file tree
Hide file tree
Showing 9 changed files with 237 additions and 105 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ import java.util.UUID
interface CharacteristicRepository : JpaRepository<CharacteristicEntity, UUID> {

companion object Constants {
const val CAS1_PROPERTY_NAME_PREMISES_PIPE = "isPIPE"
const val CAS1_PROPERTY_NAME_PREMISES_ESAP = "isESAP"
const val CAS1_PROPERTY_NAME_PREMISES_RECOVERY_FOCUSSED = "isRecoveryFocussed"
const val CAS1_PROPERTY_NAME_PREMISES_SEMI_SPECIALIST_MENTAL_HEALTH = "isSemiSpecialistMentalHealth"

const val CAS1_PROPERTY_NAME_ARSON_SUITABLE = "isArsonSuitable"
const val CAS1_PROPERTY_NAME_ENSUITE = "hasEnSuite"
const val CAS1_PROPERTY_NAME_SINGLE_ROOM = "isSingle"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@ package uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas1

import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate
import org.springframework.stereotype.Repository
import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.CharacteristicRepository.Constants.CAS1_PROPERTY_NAME_PREMISES_ESAP
import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.CharacteristicRepository.Constants.CAS1_PROPERTY_NAME_PREMISES_PIPE
import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.CharacteristicRepository.Constants.CAS1_PROPERTY_NAME_PREMISES_RECOVERY_FOCUSSED
import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.CharacteristicRepository.Constants.CAS1_PROPERTY_NAME_PREMISES_SEMI_SPECIALIST_MENTAL_HEALTH
import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.ApprovedPremisesType
import java.sql.ResultSet
import java.util.UUID

private const val AP_TYPE_FILTER = """
AND result.ap_type IN (:apTypes)
AND result.ap_type=:apType
"""

private const val PREMISES_CHARACTERISTICS_FILTER = """
Expand Down Expand Up @@ -43,42 +47,10 @@ FROM
ap.point::geography
) * 0.000621371 AS distance_in_miles,
CASE
WHEN EXISTS (
SELECT 1
FROM premises_characteristics pc
JOIN characteristics c
ON pc.characteristic_id = c.id
WHERE
c.property_name = 'isPIPE'
AND pc.premises_id = p.id
) THEN 'PIPE'
WHEN EXISTS (
SELECT 1
FROM premises_characteristics pc
JOIN characteristics c
ON pc.characteristic_id = c.id
WHERE
c.property_name = 'isESAP'
AND pc.premises_id = p.id
) THEN 'ESAP'
WHEN EXISTS (
SELECT 1
FROM premises_characteristics pc
JOIN characteristics c
ON pc.characteristic_id = c.id
WHERE
c.property_name = 'isRecoveryFocussed'
AND pc.premises_id = p.id
) THEN 'RFAP'
WHEN EXISTS (
SELECT 1
FROM premises_characteristics pc
JOIN characteristics c
ON pc.characteristic_id = c.id
WHERE
c.property_name = 'isSemiSpecialistMentalHealth'
AND pc.premises_id = p.id
) THEN 'MHAP'
WHEN ('$CAS1_PROPERTY_NAME_PREMISES_PIPE'=ANY(ARRAY_AGG (characteristics.property_name))) THEN 'PIPE'
WHEN ('$CAS1_PROPERTY_NAME_PREMISES_ESAP'=ANY(ARRAY_AGG (characteristics.property_name))) THEN 'ESAP'
WHEN ('$CAS1_PROPERTY_NAME_PREMISES_RECOVERY_FOCUSSED'=ANY(ARRAY_AGG (characteristics.property_name))) THEN 'RFAP'
WHEN ('$CAS1_PROPERTY_NAME_PREMISES_SEMI_SPECIALIST_MENTAL_HEALTH'=ANY(ARRAY_AGG (characteristics.property_name))) THEN 'MHAP'
ELSE 'NORMAL'
END AS ap_type,
p.name AS name,
Expand All @@ -87,14 +59,18 @@ FROM
p.town AS town,
p.postcode AS postcode,
aa.id AS ap_area_id,
aa.name AS ap_area_name
aa.name AS ap_area_name,
ARRAY_AGG (characteristics.property_name) as characteristics
FROM approved_premises ap
INNER JOIN premises p ON ap.premises_id = p.id
INNER JOIN probation_regions pr ON p.probation_region_id = pr.id
INNER JOIN ap_areas aa ON pr.ap_area_id = aa.id
LEFT OUTER JOIN premises_characteristics premises_chars ON premises_chars.premises_id = p.id
LEFT OUTER JOIN characteristics ON characteristics.id = premises_chars.characteristic_id
WHERE
ap.supports_space_bookings = true AND
ap.gender = #SPECIFIED_GENDER#
GROUP BY p.id, ap.point, p.name, p.address_line1, p.address_line2, p.town, p.postcode, aa.id, aa.name
) AS result
WHERE
1 = 1
Expand All @@ -110,14 +86,14 @@ class Cas1SpaceSearchRepository(
) {
fun findAllPremisesWithCharacteristicsByDistance(
targetPostcodeDistrict: String,
apTypes: List<ApprovedPremisesType>,
approvedPremisesType: ApprovedPremisesType?,
isWomensPremises: Boolean,
premisesCharacteristics: List<UUID>,
roomCharacteristics: List<UUID>,
): List<CandidatePremises> {
val (query, parameters) = resolveCandidatePremisesQueryTemplate(
targetPostcodeDistrict,
apTypes,
approvedPremisesType,
isWomensPremises,
premisesCharacteristics,
roomCharacteristics,
Expand All @@ -140,13 +116,28 @@ class Cas1SpaceSearchRepository(
rs.getString("postcode"),
rs.getUUID("ap_area_id"),
rs.getString("ap_area_name"),
toStringList(rs.getArray("characteristics")),
)
}
}

private fun toStringList(array: java.sql.Array?): List<String> {
if (array == null) {
return emptyList()
}

val result = (array.array as Array<String>).toList()

return if (result.size == 1 && result[0] == null) {
emptyList()
} else {
result
}
}

private fun resolveCandidatePremisesQueryTemplate(
targetPostcodeDistrict: String,
apTypes: List<ApprovedPremisesType>,
apType: ApprovedPremisesType?,
isWomensPremises: Boolean,
premisesCharacteristics: List<UUID>,
roomCharacteristics: List<UUID>,
Expand All @@ -156,15 +147,18 @@ class Cas1SpaceSearchRepository(
"outcode" to targetPostcodeDistrict,
)

val transformedApTypes = apTypes.transformForQuery()

when {
transformedApTypes.isEmpty() -> {
apType == null -> {
query = query.replace("#AP_TYPE_FILTER#", "")
}
else -> {
val apTypeName = when (apType) {
ApprovedPremisesType.MHAP_ST_JOSEPHS, ApprovedPremisesType.MHAP_ELLIOTT_HOUSE -> "MHAP"
else -> apType.name
}

query = query.replace("#AP_TYPE_FILTER#", AP_TYPE_FILTER)
params["apTypes"] = transformedApTypes
params["apType"] = apTypeName
}
}

Expand Down Expand Up @@ -203,13 +197,6 @@ class Cas1SpaceSearchRepository(
}

private fun ResultSet.getUUID(columnLabel: String) = UUID.fromString(this.getString(columnLabel))

private fun List<ApprovedPremisesType>.transformForQuery() = this.map {
when (it) {
ApprovedPremisesType.MHAP_ST_JOSEPHS, ApprovedPremisesType.MHAP_ELLIOTT_HOUSE -> "MHAP"
else -> it.name
}
}
}

data class CandidatePremises(
Expand All @@ -223,4 +210,5 @@ data class CandidatePremises(
val postcode: String,
val apAreaId: UUID,
val apAreaName: String,
val characteristics: List<String>,
)
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas1
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas1SpaceSearchParameters
import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas1SpaceSearchRequirements
import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.ServiceName
import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApprovedPremiseApplicationRepository
import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.CharacteristicEntity
Expand All @@ -25,7 +26,7 @@ class Cas1SpaceSearchService(
val application = applicationRepository.findByIdOrNull(searchParameters.applicationId)
?: throw NotFoundProblem(applicationId, "Application")

val requiredCharacteristics = getRequiredCharacteristics(searchParameters)
val requiredCharacteristics = getRequiredCharacteristics(searchParameters.requirements)

return getCandidatePremises(
searchParameters.targetPostcodeDistrict,
Expand All @@ -34,13 +35,17 @@ class Cas1SpaceSearchService(
)
}

private fun getRequiredCharacteristics(searchParameters: Cas1SpaceSearchParameters) = RequiredCharacteristics(
searchParameters.requirements.apTypes?.map { it.asApprovedPremisesType() } ?: listOf(),
getSpaceCharacteristics(searchParameters),
private fun getRequiredCharacteristics(requirements: Cas1SpaceSearchRequirements) = RequiredCharacteristics(
apType = if (requirements.apType != null) {
requirements.apType!!.asApprovedPremisesType()
} else {
requirements.apTypes?.map { it.asApprovedPremisesType() }?.firstOrNull()
},
groupedCharacteristics = getSpaceCharacteristics(requirements),
)

private fun getSpaceCharacteristics(searchParameters: Cas1SpaceSearchParameters): GroupedCharacteristics {
val propertyNames = searchParameters.requirements.spaceCharacteristics?.map { it.value } ?: listOf()
private fun getSpaceCharacteristics(requirements: Cas1SpaceSearchRequirements): GroupedCharacteristics {
val propertyNames = requirements.spaceCharacteristics?.map { it.value } ?: listOf()
val characteristics = characteristicService.getCharacteristicsByPropertyNames(propertyNames, ServiceName.approvedPremises)

return GroupedCharacteristics(
Expand All @@ -56,10 +61,10 @@ class Cas1SpaceSearchService(
): List<CandidatePremises> {
return spaceSearchRepository.findAllPremisesWithCharacteristicsByDistance(
targetPostcodeDistrict,
requiredCharacteristics.apTypes,
requiredCharacteristics.apType,
isWomensPremises,
requiredCharacteristics.space.premisesCharacteristics,
requiredCharacteristics.space.roomCharacteristics,
requiredCharacteristics.groupedCharacteristics.premisesCharacteristics,
requiredCharacteristics.groupedCharacteristics.roomCharacteristics,
)
}

Expand All @@ -71,8 +76,8 @@ class Cas1SpaceSearchService(
}

data class RequiredCharacteristics(
val apTypes: List<ApprovedPremisesType>,
val space: GroupedCharacteristics,
val apType: ApprovedPremisesType?,
val groupedCharacteristics: GroupedCharacteristics,
)

data class GroupedCharacteristics(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas1

import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component
import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas1PremiseCharacteristic
import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas1PremisesSearchResultSummary
import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas1SpaceSearchParameters
import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas1SpaceSearchResults
Expand All @@ -11,6 +13,10 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas1SpaceSearc

@Component
class Cas1SpaceSearchResultsTransformer {

private val log = LoggerFactory.getLogger(this::class.java)

@SuppressWarnings("SwallowedException")
fun transformDomainToApi(searchParameters: Cas1SpaceSearchParameters, results: List<CandidatePremises>) =
Cas1SpaceSearchResults(
resultsCount = results.size,
Expand All @@ -29,7 +35,15 @@ class Cas1SpaceSearchResultsTransformer {
id = candidatePremises.apAreaId,
name = candidatePremises.apAreaName,
),
premisesCharacteristics = listOf(),
premisesCharacteristics = emptyList(),
characteristics = candidatePremises.characteristics.mapNotNull {
try {
Cas1PremiseCharacteristic.valueOf(it)
} catch (e: IllegalArgumentException) {
log.warn("Couldn't find an enum entry for propertyName $it")
null
}
},
),
distanceInMiles = candidatePremises.distanceInMiles.toBigDecimal(),
spacesAvailable = listOf(),
Expand Down
41 changes: 40 additions & 1 deletion src/main/resources/static/cas1-schemas.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,20 @@ components:
$ref: '_shared.yml#/components/schemas/NamedId'
premisesCharacteristics:
type: array
deprecated: true
description: "This is not populated. Instead, use 'characteristics'"
items:
$ref: '_shared.yml#/components/schemas/CharacteristicPair'
characteristics:
type: array
items:
$ref: '#/components/schemas/Cas1PremiseCharacteristic'
required:
- id
- apType
- name
- apArea
- characteristics
Cas1PremisesSummary:
type: object
properties:
Expand Down Expand Up @@ -118,6 +130,30 @@ components:
required:
- startInclusive
- endInclusive
Cas1PremiseCharacteristic:
type: string
description: All premise characteristics
enum:
- acceptsChildSexOffenders
- acceptsHateCrimeOffenders
- acceptsNonSexualChildOffenders
- acceptsSexOffenders
- additionalRestrictions
- hasBrailleSignage
- hasHearingLoop
- hasLift
- hasStepFreeAccessToCommunalAreas
- hasTactileFlooring
- hasWheelChairAccessibleBathrooms
- hasWideAccessToCommunalAreas
- hasWideStepFreeAccess
- isCatered
- isESAP
- isIAP
- isPIPE
- isRecoveryFocussed
- isSuitableForVulnerable
- isSemiSpecialistMentalHealth
Cas1SpaceCharacteristic:
type: string
description: All of the characteristics of both premises and rooms
Expand Down Expand Up @@ -226,9 +262,13 @@ components:
type: object
properties:
apTypes:
deprecated: true
description: "Searching on multiple types is not supported, instead use apType. If this is used, the first type wll be matched, and type 'normal' will be ignored"
type: array
items:
$ref: '_shared.yml#/components/schemas/ApType'
apType:
$ref: '_shared.yml#/components/schemas/ApType'
spaceCharacteristics:
type: array
items:
Expand All @@ -240,7 +280,6 @@ components:
items:
$ref: '_shared.yml#/components/schemas/Gender'
required:
- apType
- gender
Cas1SpaceSearchParameters:
type: object
Expand Down
Loading

0 comments on commit 90f484d

Please sign in to comment.