From 3887251130ed8abfaceb36c663c79ba409cd8b1b Mon Sep 17 00:00:00 2001 From: Rob Booth Date: Fri, 20 Dec 2024 21:55:06 +0000 Subject: [PATCH] Implement logic to fetch matching Space Bookings A new query in the SpaceBookingEntity retrieves the matching space bookings for the given premises and day, filtering and ordering as specified. --- .../controller/cas1/Cas1PremisesController.kt | 20 +- .../jpa/entity/Cas1SpaceBookingEntity.kt | 70 ++++ .../cas1/Cas1SpaceBookingDaySummaryService.kt | 91 +++++ .../cas1/Cas1PremisesDayTransformer.kt | 4 +- .../Cas1SpaceBookingDaySummaryTransformer.kt | 31 ++ src/main/resources/static/cas1-schemas.yml | 3 - .../static/codegen/built-cas1-api-spec.yml | 3 - .../integration/cas1/Cas1PremisesTest.kt | 370 +++++++++++++++++- .../Cas1SpaceBookingDaySummaryServiceTest.kt | 168 ++++++++ .../cas1/Cas1PremisesDayTransformerTest.kt | 27 +- ...s1SpaceBookingDaySummaryTransformerTest.kt | 99 +++++ 11 files changed, 856 insertions(+), 30 deletions(-) create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas1/Cas1SpaceBookingDaySummaryService.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas1/Cas1SpaceBookingDaySummaryTransformer.kt create mode 100644 src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas1/Cas1SpaceBookingDaySummaryServiceTest.kt create mode 100644 src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/transformer/cas1/Cas1SpaceBookingDaySummaryTransformerTest.kt diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas1/Cas1PremisesController.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas1/Cas1PremisesController.kt index f64d41da91..7ce6a82853 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas1/Cas1PremisesController.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas1/Cas1PremisesController.kt @@ -15,6 +15,7 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApprovedPremi import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.UserPermission import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.UserAccessService import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas1.Cas1PremisesService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas1.Cas1SpaceBookingDaySummaryService import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas1.Cas1PremiseCapacitySummaryTransformer import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas1.Cas1PremisesDayTransformer import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas1.Cas1PremisesTransformer @@ -29,6 +30,7 @@ class Cas1PremisesController( val cas1PremisesTransformer: Cas1PremisesTransformer, val cas1PremiseCapacityTransformer: Cas1PremiseCapacitySummaryTransformer, private val cas1PremisesDayTransformer: Cas1PremisesDayTransformer, + private val cas1SpaceBookingDaySummaryService: Cas1SpaceBookingDaySummaryService, ) : PremisesCas1Delegate { override fun getPremisesById(premisesId: UUID): ResponseEntity { @@ -97,14 +99,28 @@ class Cas1PremisesController( userAccessService.ensureCurrentUserHasPermission(UserPermission.CAS1_SPACE_BOOKING_VIEW) val premiseSummaryInfo = cas1PremisesService.getPremisesSummary(premisesId) - val premiseCapacity = cas1PremisesService.getPremiseCapacity(premisesId, date, date) + val premiseCapacity = cas1PremisesService.getPremiseCapacity(premisesId, date, date, null) val premisesCapacitySummary = cas1PremiseCapacityTransformer.toCas1PremiseCapacitySummary( premiseSummaryInfo = extractEntityFromCasResult(premiseSummaryInfo), premiseCapacity = extractEntityFromCasResult(premiseCapacity), ) + val spaceBookingDaySummaries = extractEntityFromCasResult( + cas1SpaceBookingDaySummaryService.getBookingDaySummaries( + premisesId = premisesId, + date = date, + bookingsCriteriaFilter = bookingsCriteriaFilter, + bookingsSortBy = bookingsSortBy ?: Cas1SpaceBookingDaySummarySortField.PERSON_NAME, + bookingsSortDirection = bookingsSortDirection ?: SortDirection.desc, + ), + ) + return ResponseEntity.ok().body( - cas1PremisesDayTransformer.toCas1PremisesDaySummary(date, premisesCapacitySummary), + cas1PremisesDayTransformer.toCas1PremisesDaySummary( + date, + premisesCapacitySummary, + spaceBookingDaySummaries, + ), ) } } diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/Cas1SpaceBookingEntity.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/Cas1SpaceBookingEntity.kt index 9ad6eb6e5e..11b59f83d6 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/Cas1SpaceBookingEntity.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/Cas1SpaceBookingEntity.kt @@ -14,10 +14,12 @@ import jakarta.persistence.Table import jakarta.persistence.Version import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable +import org.springframework.data.domain.Sort import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Modifying import org.springframework.data.jpa.repository.Query import org.springframework.stereotype.Repository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas1SpaceBookingEntity.Constants import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.CharacteristicRepository.Constants.CAS1_PROPERTY_NAME_ARSON_SUITABLE import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.CharacteristicRepository.Constants.CAS1_PROPERTY_NAME_ENSUITE import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.CharacteristicRepository.Constants.CAS1_PROPERTY_NAME_SINGLE_ROOM @@ -117,6 +119,64 @@ interface Cas1SpaceBookingRepository : JpaRepository + @Query( + value = """ + SELECT + Cast(b.id as varchar), + b.crn as crn, + b.canonical_arrival_date as canonicalArrivalDate, + b.canonical_departure_date as canonicalDepartureDate, + apa.risk_ratings -> 'tier' -> 'value' ->> 'level' as tier, + CASE + WHEN + apa.id IS NOT NULL THEN apa.name + ELSE + oa.name + END as personName, + CASE + WHEN + apa.id IS NOT NULL THEN apa.release_type + ELSE + null + END as releaseType, + ( + SELECT STRING_AGG (characteristics.property_name, ',') + FROM cas1_space_bookings_criteria sbc + LEFT OUTER JOIN characteristics ON characteristics.id = sbc.characteristic_id + WHERE sbc.space_booking_id = b.id + GROUP by sbc.space_booking_id + ) AS characteristicsPropertyNames + FROM cas1_space_bookings b + LEFT JOIN approved_premises_applications apa + ON + b.approved_premises_application_id = apa.id + LEFT JOIN offline_applications oa + ON + b.offline_application_id = oa.id + WHERE + b.canonical_arrival_date <= :daySummaryDate AND + b.canonical_departure_date >= :daySummaryDate AND + b.premises_id = :premisesId AND + b.cancellation_occurred_at IS NULL AND ( + :bookingsCriteriaCount = 0 OR ( + SELECT COUNT(*) + FROM + CAS1_SPACE_BOOKINGS_CRITERIA sbc + WHERE b.id = sbc.space_booking_id AND + sbc.characteristic_id IN (:bookingsCriteriaFilter) + ) = :bookingsCriteriaCount + ) + """, + nativeQuery = true, + ) + fun findAllPremisesBookingsForDate( + premisesId: UUID, + daySummaryDate: LocalDate, + bookingsCriteriaFilter: List?, + sort: Sort, + bookingsCriteriaCount: Int = bookingsCriteriaFilter?.size ?: 0, + ): List + @Query( value = """ SELECT @@ -166,6 +226,16 @@ interface Cas1SpaceBookingRepository : JpaRepository +} + interface Cas1SpaceBookingSearchResult { val id: UUID val crn: String diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas1/Cas1SpaceBookingDaySummaryService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas1/Cas1SpaceBookingDaySummaryService.kt new file mode 100644 index 0000000000..ddffc0be9c --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas1/Cas1SpaceBookingDaySummaryService.kt @@ -0,0 +1,91 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas1 + +import org.springframework.data.domain.Sort +import org.springframework.stereotype.Service +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas1SpaceBookingCharacteristic +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas1SpaceBookingDaySummary +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas1SpaceBookingDaySummarySortField +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.ServiceName +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.SortDirection +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas1SpaceBookingDaySummarySearchResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas1SpaceBookingRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.PersonSummaryInfoResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.forCrn +import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.CasResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.CharacteristicService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.OffenderService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.UserAccessService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.UserService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas1LimitedAccessStrategy +import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.PersonTransformer +import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas1.Cas1SpaceBookingDaySummaryTransformer +import java.time.LocalDate +import java.util.UUID + +@Service +class Cas1SpaceBookingDaySummaryService( + val userAccessService: UserAccessService, + private val cas1SpaceBookingRepository: Cas1SpaceBookingRepository, + private val characteristicService: CharacteristicService, + private val cas1SpaceBookingDaySummaryTransformer: Cas1SpaceBookingDaySummaryTransformer, + private val offenderService: OffenderService, + private val userService: UserService, + private val cas1PremisesService: Cas1PremisesService, +) { + + fun getBookingDaySummaries( + premisesId: UUID, + date: LocalDate, + bookingsCriteriaFilter: List?, + bookingsSortBy: Cas1SpaceBookingDaySummarySortField, + bookingsSortDirection: SortDirection, + ): CasResult> { + if (cas1PremisesService.findPremiseById(premisesId) == null) return CasResult.NotFound("premises", premisesId.toString()) + + val sort = Sort.by( + when (bookingsSortDirection) { + SortDirection.desc -> Sort.Direction.DESC + SortDirection.asc -> Sort.Direction.ASC + }, + bookingsSortBy.value, + ) + + val spaceBookingsForDate = cas1SpaceBookingRepository.findAllPremisesBookingsForDate( + premisesId = premisesId, + daySummaryDate = date, + bookingsCriteriaFilter = getBookingCharacteristicIds(bookingsCriteriaFilter), + sort = sort, + ) + + val offenderSummaries = getOffenderSummariesForBookings(spaceBookingsForDate) + + val spaceBookingDaySummaries = + spaceBookingsForDate.map { bookingSummary -> + cas1SpaceBookingDaySummaryTransformer.toCas1SpaceBookingDaySummary( + bookingSummary, + PersonTransformer() + .personSummaryInfoToPersonSummary( + offenderSummaries.forCrn(bookingSummary.crn), + ), + ) + } + return CasResult.Success( + spaceBookingDaySummaries, + ) + } + + private fun getBookingCharacteristicIds(bookingsCriteriaFilter: List?) = + bookingsCriteriaFilter?.let { bookingCriteria -> + val characteristics = bookingCriteria.map { it.value } + characteristicService.getCharacteristicsByPropertyNames(characteristics, ServiceName.approvedPremises) + .map { characteristic -> characteristic.id } + } + + private fun getOffenderSummariesForBookings(spaceBookings: List): List { + val user = userService.getUserForRequest() + return offenderService.getPersonSummaryInfoResults( + crns = spaceBookings.map { it.crn }.toSet(), + limitedAccessStrategy = user.cas1LimitedAccessStrategy(), + ) + } +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas1/Cas1PremisesDayTransformer.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas1/Cas1PremisesDayTransformer.kt index a6efc263ea..c436fce496 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas1/Cas1PremisesDayTransformer.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas1/Cas1PremisesDayTransformer.kt @@ -3,6 +3,7 @@ package uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas1 import org.springframework.stereotype.Component import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas1PremiseCapacity import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas1PremisesDaySummary +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas1SpaceBookingDaySummary import java.time.LocalDate @Component @@ -11,13 +12,14 @@ class Cas1PremisesDayTransformer { fun toCas1PremisesDaySummary( date: LocalDate, premisesCapacity: Cas1PremiseCapacity, + spaceBookings: List, ) = Cas1PremisesDaySummary( forDate = date, previousDate = date.minusDays(1), nextDate = date.plusDays(1), capacity = premisesCapacity.capacity.first(), - spaceBookings = emptyList(), + spaceBookings = spaceBookings, outOfServiceBeds = emptyList(), ) } diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas1/Cas1SpaceBookingDaySummaryTransformer.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas1/Cas1SpaceBookingDaySummaryTransformer.kt new file mode 100644 index 0000000000..d40522be9a --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas1/Cas1SpaceBookingDaySummaryTransformer.kt @@ -0,0 +1,31 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas1 + +import org.springframework.stereotype.Component +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas1SpaceBookingCharacteristic +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas1SpaceBookingDaySummary +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.PersonSummary +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas1SpaceBookingDaySummarySearchResult + +@Component +class Cas1SpaceBookingDaySummaryTransformer { + + fun toCas1SpaceBookingDaySummary( + jpa: Cas1SpaceBookingDaySummarySearchResult, + personSummary: PersonSummary, + ) = Cas1SpaceBookingDaySummary( + id = jpa.id, + canonicalArrivalDate = jpa.canonicalArrivalDate, + canonicalDepartureDate = jpa.canonicalDepartureDate, + tier = jpa.tier, + releaseType = jpa.releaseType, + essentialCharacteristics = characteristicEntityToCharacteristic(jpa.characteristicsPropertyNames), + person = personSummary, + ) + + private fun characteristicEntityToCharacteristic(characteristics: List?): List = + characteristics?.let { characteristicList -> + characteristicList[0].split(",").map { characteristic -> + Cas1SpaceBookingCharacteristic.entries.first { it.value == characteristic } + } + } ?: emptyList() +} diff --git a/src/main/resources/static/cas1-schemas.yml b/src/main/resources/static/cas1-schemas.yml index bd78c5df0d..601b99b2c1 100644 --- a/src/main/resources/static/cas1-schemas.yml +++ b/src/main/resources/static/cas1-schemas.yml @@ -530,7 +530,6 @@ components: - canonicalArrivalDate - canonicalDepartureDate - releaseType - - spaceType x-enum-varnames: - PERSON_NAME - TIER @@ -597,9 +596,7 @@ components: - person - canonicalArrivalDate - canonicalDepartureDate - - tier - spaceType - - releaseType - essentialCharacteristics Cas1SpaceBookingSummaryStatus: type: string diff --git a/src/main/resources/static/codegen/built-cas1-api-spec.yml b/src/main/resources/static/codegen/built-cas1-api-spec.yml index 0b3747dfbf..069d1e866e 100644 --- a/src/main/resources/static/codegen/built-cas1-api-spec.yml +++ b/src/main/resources/static/codegen/built-cas1-api-spec.yml @@ -6577,7 +6577,6 @@ components: - canonicalArrivalDate - canonicalDepartureDate - releaseType - - spaceType x-enum-varnames: - PERSON_NAME - TIER @@ -6644,9 +6643,7 @@ components: - person - canonicalArrivalDate - canonicalDepartureDate - - tier - spaceType - - releaseType - essentialCharacteristics Cas1SpaceBookingSummaryStatus: type: string diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas1/Cas1PremisesTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas1/Cas1PremisesTest.kt index cc711e1780..f57cb76237 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas1/Cas1PremisesTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas1/Cas1PremisesTest.kt @@ -8,18 +8,34 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas1PremiseCap import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas1PremisesBasicSummary import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas1PremisesDaySummary import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas1PremisesSummary +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.FullPersonSummary +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.PersonSummaryDiscriminator +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.ServiceName +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.Cas1SpaceBookingEntityFactory import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.InitialiseDatabasePerClassTestBase import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.IntegrationTestBase +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenAPlacementRequest import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenAProbationRegion import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenAUser import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenAnApArea +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenAnOffender +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenAnOfflineApplication import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenAnOutOfServiceBed +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.httpmocks.apDeliusContextAddListCaseSummaryToBulkResponse import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApAreaEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApprovedPremisesApplicationEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApprovedPremisesEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApprovedPremisesGender +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas1SpaceBookingEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.CharacteristicRepository.Constants.CAS1_PROPERTY_NAME_ARSON_SUITABLE +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.CharacteristicRepository.Constants.CAS1_PROPERTY_NAME_ENSUITE +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.CharacteristicRepository.Constants.CAS1_PROPERTY_NAME_SINGLE_ROOM +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.PlacementRequestEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.UserRole.CAS1_ASSESSOR import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.UserRole.CAS1_CRU_MEMBER import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.UserRole.CAS1_FUTURE_MANAGER +import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.deliuscontext.CaseSummary +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.asCaseSummary import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.bodyAsListOfObjects import java.time.LocalDate import java.util.UUID @@ -441,16 +457,31 @@ class Cas1PremisesTest : IntegrationTestBase() { inner class GetDaySummary : InitialiseDatabasePerClassTestBase() { lateinit var premises: ApprovedPremisesEntity + lateinit var spaceBookingEarly: Cas1SpaceBookingEntity + lateinit var spaceBookingLate: Cas1SpaceBookingEntity + lateinit var spaceBookingOfflineApplication: Cas1SpaceBookingEntity + lateinit var applicationA: ApprovedPremisesApplicationEntity + lateinit var placementRequestA: PlacementRequestEntity + lateinit var applicationB: ApprovedPremisesApplicationEntity + lateinit var placementRequestB: PlacementRequestEntity + lateinit var offenderA: CaseSummary + lateinit var offenderB: CaseSummary + lateinit var offenderOffline: CaseSummary val summaryDate = LocalDate.now() + val tierA = "A2" + val tierB = "B3" + @SuppressWarnings("UnusedPrivateProperty") @BeforeAll fun setupTestData() { val region = givenAProbationRegion( apArea = givenAnApArea(name = "The ap area name"), ) + val premisesId = UUID.randomUUID() premises = approvedPremisesEntityFactory.produceAndPersist { + withId(premisesId) withName("the premises name") withApCode("the ap code") withPostcode("the postcode") @@ -458,10 +489,131 @@ class Cas1PremisesTest : IntegrationTestBase() { withYieldedLocalAuthorityArea { localAuthorityEntityFactory.produceAndPersist() } withManagerDetails("manager details") } + + bedEntityFactory.produceAndPersistMultiple(5) { + withYieldedRoom { + roomEntityFactory.produceAndPersist { + withYieldedPremises { premises } + } + } + } + + val (user) = givenAUser() + val (offenderAAA) = givenAnOffender(offenderDetailsConfigBlock = { + withCrn("crn1") + withFirstName("firstNameAAA") + withLastName("lastNameAAA") + withCurrentRestriction(false) + }) + offenderA = offenderAAA.asCaseSummary() + + val (offenderBBB) = givenAnOffender(offenderDetailsConfigBlock = { + withCrn("crn2") + withFirstName("firstNameBBB") + withLastName("lastNameBBB") + withCurrentRestriction(false) + }) + offenderB = offenderBBB.asCaseSummary() + val (offenderOfflineApplication) = givenAnOffender(offenderDetailsConfigBlock = { + withCrn("offline_crn") + withFirstName("mister") + withLastName("offline") + withCurrentRestriction(false) + }) + offenderOffline = offenderOfflineApplication.asCaseSummary() + + val pRequestA = givenAPlacementRequest( + placementRequestAllocatedTo = user, + assessmentAllocatedTo = user, + createdByUser = user, + crn = offenderA.crn, + name = "$offenderA.firstName $offenderA.lastName", + tier = tierA, + ) + placementRequestA = pRequestA.first + applicationA = pRequestA.second + + val pRequestB = givenAPlacementRequest( + placementRequestAllocatedTo = user, + assessmentAllocatedTo = user, + createdByUser = user, + crn = offenderB.crn, + name = "$offenderB.firstName $offenderB.lastName", + tier = tierB, + ) + placementRequestB = pRequestB.first + applicationB = pRequestB.second + + spaceBookingEarly = createSpaceBooking( + crn = offenderA.crn, + placementRequest = this.placementRequestA, + application = applicationA, + ) { + withCrn(offenderA.crn) + withPremises(premises) + withCanonicalArrivalDate(LocalDate.now().minusDays(3)) + withCanonicalDepartureDate(LocalDate.now().plusDays(3)) + withCreatedBy(user) + withCriteria( + findCharacteristic(CAS1_PROPERTY_NAME_ARSON_SUITABLE), + ) + } + + spaceBookingLate = createSpaceBooking( + crn = offenderA.crn, + placementRequest = this.placementRequestA, + application = applicationB, + ) { + withCrn(offenderB.crn) + withPremises(premises) + withCanonicalArrivalDate(LocalDate.now().minusDays(6)) + withCanonicalDepartureDate(LocalDate.now().plusDays(6)) + withCreatedBy(user) + withCriteria( + findCharacteristic(CAS1_PROPERTY_NAME_ARSON_SUITABLE), + findCharacteristic(CAS1_PROPERTY_NAME_ENSUITE), + findCharacteristic(CAS1_PROPERTY_NAME_SINGLE_ROOM), + ) + } + + val spaceBookingCancelled = createSpaceBooking( + crn = offenderA.crn, + placementRequest = this.placementRequestA, + application = applicationB, + ) { + withCrn(offenderB.crn) + withPremises(premises) + withCanonicalArrivalDate(LocalDate.now().minusDays(2)) + withCanonicalDepartureDate(LocalDate.now().plusDays(2)) + withCreatedBy(user) + withCriteria( + findCharacteristic(CAS1_PROPERTY_NAME_ARSON_SUITABLE), + findCharacteristic(CAS1_PROPERTY_NAME_ENSUITE), + findCharacteristic(CAS1_PROPERTY_NAME_SINGLE_ROOM), + ) + withCancellationOccurredAt(LocalDate.now()) + } + + spaceBookingOfflineApplication = createSpaceBookingWithOfflineApplication( + crn = offenderOffline.crn, + firstName = offenderOffline.name.forename, + lastName = offenderOffline.name.surname, + premises = premises, + ) { + withPremises(premises) + withCanonicalArrivalDate(LocalDate.now().minusDays(2)) + withCanonicalDepartureDate(LocalDate.now().plusDays(2)) + withActualDepartureDate(null) + withCriteria( + findCharacteristic(CAS1_PROPERTY_NAME_SINGLE_ROOM), + ) + } + + apDeliusContextAddListCaseSummaryToBulkResponse(listOf(offenderA, offenderB, offenderOffline)) } @Test - fun `Returns 403 Forbidden if user does not have correct role`() { + fun `returns 403 Forbidden if user does not have correct role`() { val (_, jwt) = givenAUser(roles = listOf(CAS1_ASSESSOR)) webTestClient.get() @@ -473,7 +625,7 @@ class Cas1PremisesTest : IntegrationTestBase() { } @Test - fun `Returns 404 if premise doesn't exist`() { + fun `returns 404 if premise doesn't exist`() { val (_, jwt) = givenAUser(roles = listOf(CAS1_CRU_MEMBER)) webTestClient.get() @@ -485,19 +637,104 @@ class Cas1PremisesTest : IntegrationTestBase() { } @Test - fun `Returns premises day summary`() { + fun `returns premises day summary with no filters applied`() { val (_, jwt) = givenAUser(roles = listOf(CAS1_CRU_MEMBER)) - bedEntityFactory.produceAndPersistMultiple(5) { - withYieldedRoom { - roomEntityFactory.produceAndPersist { - withYieldedPremises { premises } - } - } - } + val summaries = webTestClient.get() + .uri("/cas1/premises/${premises.id}/day-summary/$summaryDate") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isOk + .returnResult(Cas1PremisesDaySummary::class.java).responseBody.blockFirst()!! + + assertThat(summaries.forDate).isEqualTo(summaryDate) + assertThat(summaries.nextDate).isEqualTo(summaryDate.plusDays(1)) + assertThat(summaries.previousDate).isEqualTo(summaryDate.minusDays(1)) + assertThat(summaries.spaceBookings.size).isEqualTo(3) + assertThat(summaries.outOfServiceBeds).isEmpty() + + val summaryBookingOffline = summaries.spaceBookings[0] + assertThat(summaryBookingOffline.id).isEqualTo(spaceBookingOfflineApplication.id) + assertThat(summaryBookingOffline.tier).isEqualTo(null) + assertThat(summaryBookingOffline.releaseType).isEqualTo(null) + assertThat(summaryBookingOffline.canonicalArrivalDate).isEqualTo(summaryBookingOffline.canonicalArrivalDate) + assertThat(summaryBookingOffline.canonicalDepartureDate).isEqualTo(summaryBookingOffline.canonicalDepartureDate) + assertThat(summaryBookingOffline.essentialCharacteristics.size).isEqualTo(1) + assertThat(summaryBookingOffline.essentialCharacteristics[0].value).isEqualTo(CAS1_PROPERTY_NAME_SINGLE_ROOM) + + val summaryBooking1 = summaries.spaceBookings[1] + assertThat(summaryBooking1.id).isEqualTo(spaceBookingLate.id) + assertThat(summaryBooking1.tier).isEqualTo(tierB) + assertThat(summaryBooking1.canonicalArrivalDate).isEqualTo(spaceBookingLate.canonicalArrivalDate) + assertThat(summaryBooking1.canonicalDepartureDate).isEqualTo(spaceBookingLate.canonicalDepartureDate) + assertThat(summaryBooking1.essentialCharacteristics.size).isEqualTo(3) + assertThat(summaryBooking1.essentialCharacteristics[0].value).isEqualTo(CAS1_PROPERTY_NAME_ARSON_SUITABLE) + assertThat(summaryBooking1.essentialCharacteristics[1].value).isEqualTo(CAS1_PROPERTY_NAME_ENSUITE) + assertThat(summaryBooking1.essentialCharacteristics[2].value).isEqualTo(CAS1_PROPERTY_NAME_SINGLE_ROOM) + assertThat(summaryBooking1.releaseType).isEqualTo(applicationB.releaseType) + + val summaryBooking2 = summaries.spaceBookings[2] + assertThat(summaryBooking2.id).isEqualTo(spaceBookingEarly.id) + assertThat(summaryBooking2.tier).isEqualTo(tierA) + assertThat(summaryBooking2.canonicalArrivalDate).isEqualTo(spaceBookingEarly.canonicalArrivalDate) + assertThat(summaryBooking2.canonicalDepartureDate).isEqualTo(spaceBookingEarly.canonicalDepartureDate) + assertThat(summaryBooking2.essentialCharacteristics.size).isEqualTo(1) + assertThat(summaryBooking2.essentialCharacteristics[0].value).isEqualTo(CAS1_PROPERTY_NAME_ARSON_SUITABLE) + assertThat(summaryBooking2.releaseType).isEqualTo(applicationA.releaseType) + + val offender1 = summaryBookingOffline.person + assertThat(offender1.crn).isEqualTo(offenderOffline.crn) + assertThat(offender1.personType).isEqualTo(PersonSummaryDiscriminator.fullPersonSummary) + assertThat(offender1).isInstanceOf(FullPersonSummary::class.java) + assertThat((offender1 as FullPersonSummary).name).isEqualTo("${offenderOffline.name.forename} ${offenderOffline.name.surname}") + + val offender2 = summaryBooking1.person + assertThat(offender2.crn).isEqualTo(offenderB.crn) + assertThat(offender2.personType).isEqualTo(PersonSummaryDiscriminator.fullPersonSummary) + assertThat(offender2).isInstanceOf(FullPersonSummary::class.java) + assertThat((offender2 as FullPersonSummary).name).isEqualTo("${offenderB.name.forename} ${offenderB.name.surname}") + + val offender3 = summaryBooking2.person + assertThat(offender3.crn).isEqualTo(offenderA.crn) + assertThat(offender3.personType).isEqualTo(PersonSummaryDiscriminator.fullPersonSummary) + assertThat(offender3).isInstanceOf(FullPersonSummary::class.java) + assertThat((offender3 as FullPersonSummary).name).isEqualTo("${offenderA.name.forename} ${offenderA.name.surname}") + + val capacity = summaries.capacity + assertThat(capacity.date).isEqualTo(summaryDate) + assertThat(capacity.totalBedCount).isEqualTo(5) + assertThat(capacity.availableBedCount).isEqualTo(5) + assertThat(capacity.bookingCount).isEqualTo(3) + assertThat(capacity.characteristicAvailability.count()).isEqualTo(6) + } + + @Test + fun `returns premises day summaries when filters applied with matching bookings`() { + val (_, jwt) = givenAUser(roles = listOf(CAS1_CRU_MEMBER)) val summary = webTestClient.get() - .uri("/cas1/premises/${premises.id}/day-summary/$summaryDate") + .uri("/cas1/premises/${premises.id}/day-summary/$summaryDate?bookingsCriteriaFilter=hasEnSuite") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isOk + .returnResult(Cas1PremisesDaySummary::class.java).responseBody.blockFirst()!! + + assertThat(summary.forDate).isEqualTo(summaryDate) + assertThat(summary.nextDate).isEqualTo(summaryDate.plusDays(1)) + assertThat(summary.previousDate).isEqualTo(summaryDate.minusDays(1)) + assertThat(summary.spaceBookings.size).isEqualTo(1) + val summaryBooking = summary.spaceBookings[0] + assertThat(summaryBooking.essentialCharacteristics[1].value).isEqualTo(CAS1_PROPERTY_NAME_ENSUITE) + } + + @Test + fun `returns no space booking summaries when filters applied with no matching bookings`() { + val (_, jwt) = givenAUser(roles = listOf(CAS1_CRU_MEMBER)) + + val summary = webTestClient.get() + .uri("/cas1/premises/${premises.id}/day-summary/$summaryDate?bookingsCriteriaFilter=isStepFreeDesignated") .header("Authorization", "Bearer $jwt") .exchange() .expectStatus() @@ -509,12 +746,111 @@ class Cas1PremisesTest : IntegrationTestBase() { assertThat(summary.previousDate).isEqualTo(summaryDate.minusDays(1)) assertThat(summary.spaceBookings).isEmpty() assertThat(summary.outOfServiceBeds).isEmpty() - val capacity = summary.capacity - assertThat(capacity.date).isEqualTo(summaryDate) - assertThat(capacity.totalBedCount).isEqualTo(5) - assertThat(capacity.availableBedCount).isEqualTo(5) - assertThat(capacity.bookingCount).isEqualTo(0) - assertThat(capacity.characteristicAvailability.count()).isEqualTo(6) + } + + @Test + fun `returns ordered space booking summaries when order by person name ascending`() { + val (_, jwt) = givenAUser(roles = listOf(CAS1_CRU_MEMBER)) + + val summaries = webTestClient.get() + .uri("/cas1/premises/${premises.id}/day-summary/$summaryDate?bookingsSortDirection=asc&bookingsSortBy=personName") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isOk + .returnResult(Cas1PremisesDaySummary::class.java).responseBody.blockFirst()!! + + assertThat(summaries.spaceBookings.size).isEqualTo(3) + + val offender1 = summaries.spaceBookings[0].person + assertThat((offender1 as FullPersonSummary).name).isEqualTo("firstNameAAA lastNameAAA") + val offender2 = summaries.spaceBookings[1].person + assertThat((offender2 as FullPersonSummary).name).isEqualTo("firstNameBBB lastNameBBB") + val offender3 = summaries.spaceBookings[2].person + assertThat((offender3 as FullPersonSummary).name).isEqualTo("mister offline") + } + + @Test + fun `returns ordered space booking summaries when order by tier ascending`() { + val (_, jwt) = givenAUser(roles = listOf(CAS1_CRU_MEMBER)) + + val summaries = webTestClient.get() + .uri("/cas1/premises/${premises.id}/day-summary/$summaryDate?bookingsSortDirection=asc&bookingsSortBy=tier") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isOk + .returnResult(Cas1PremisesDaySummary::class.java).responseBody.blockFirst()!! + + assertThat(summaries.spaceBookings.size).isEqualTo(3) + + assertThat(summaries.spaceBookings[0].tier).isEqualTo(tierA) + assertThat(summaries.spaceBookings[1].tier).isEqualTo(tierB) + assertThat(summaries.spaceBookings[2].tier).isNull() + } + + @Test + fun `returns ordered space booking summaries when order by tier descending`() { + val (_, jwt) = givenAUser(roles = listOf(CAS1_CRU_MEMBER)) + + val summaries = webTestClient.get() + .uri("/cas1/premises/${premises.id}/day-summary/$summaryDate?bookingsSortDirection=desc&bookingsSortBy=tier") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isOk + .returnResult(Cas1PremisesDaySummary::class.java).responseBody.blockFirst()!! + + assertThat(summaries.spaceBookings.size).isEqualTo(3) + + assertThat(summaries.spaceBookings[0].tier).isNull() + assertThat(summaries.spaceBookings[1].tier).isEqualTo(tierB) + assertThat(summaries.spaceBookings[2].tier).isEqualTo(tierA) + } + + private fun findCharacteristic(propertyName: String) = + characteristicRepository.findByPropertyName(propertyName, ServiceName.approvedPremises.value)!! + + private fun createSpaceBooking( + crn: String, + placementRequest: PlacementRequestEntity, + application: ApprovedPremisesApplicationEntity, + configuration: Cas1SpaceBookingEntityFactory.() -> Unit, + ): Cas1SpaceBookingEntity { + val (user) = givenAUser() + + return cas1SpaceBookingEntityFactory.produceAndPersist { + withCrn(crn) + withPlacementRequest(placementRequest) + withApplication(application) + withCreatedBy(user) + + configuration.invoke(this) + } + } + + private fun createSpaceBookingWithOfflineApplication( + crn: String, + firstName: String, + lastName: String, + premises: ApprovedPremisesEntity, + configuration: Cas1SpaceBookingEntityFactory.() -> Unit, + ): Cas1SpaceBookingEntity { + val (user) = givenAUser() + val offlineApplication = givenAnOfflineApplication( + crn = crn, + name = "$firstName $lastName", + ) + return cas1SpaceBookingEntityFactory.produceAndPersist { + withCrn(crn) + withPremises(premises) + withPlacementRequest(null) + withApplication(null) + withOfflineApplication(offlineApplication) + withCreatedBy(user) + + configuration.invoke(this) + } } } } diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas1/Cas1SpaceBookingDaySummaryServiceTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas1/Cas1SpaceBookingDaySummaryServiceTest.kt new file mode 100644 index 0000000000..7c0244906a --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas1/Cas1SpaceBookingDaySummaryServiceTest.kt @@ -0,0 +1,168 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.unit.service.cas1 + +import io.mockk.every +import io.mockk.junit5.MockKExtension +import io.mockk.mockk +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.data.domain.Sort +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas1SpaceBookingCharacteristic +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas1SpaceBookingDaySummary +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas1SpaceBookingDaySummarySortField +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.ServiceName +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.SortDirection +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ApprovedPremisesEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.CaseSummaryFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.CharacteristicEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.UserEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas1SpaceBookingRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.PersonSummaryInfoResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.CasResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.CharacteristicService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.OffenderService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.UserAccessService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.UserService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas1.Cas1PremisesService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas1.Cas1SpaceBookingDaySummaryService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.PersonTransformer +import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas1.Cas1SpaceBookingDaySummaryTransformer +import uk.gov.justice.digital.hmpps.approvedpremisesapi.unit.transformer.cas1.Cas1SpaceBookingDaySummarySearchResultImpl +import java.time.LocalDate +import java.util.UUID + +@ExtendWith(MockKExtension::class) +class Cas1SpaceBookingDaySummaryServiceTest { + + private val userAccessService = mockk() + private val cas1SpaceBookingRepository = mockk() + private val characteristicService = mockk() + private val cas1SpaceBookingDaySummaryTransformer = mockk() + private val offenderService = mockk() + private val userService = mockk() + private val cas1PremisesService = mockk() + + private val service = Cas1SpaceBookingDaySummaryService( + userAccessService, + cas1SpaceBookingRepository, + characteristicService, + cas1SpaceBookingDaySummaryTransformer, + offenderService, + userService, + cas1PremisesService, + ) + + @Nested + inner class GetBookingDaySummaries { + + @Test + fun `returns not found error if premises with the given Id does not exist`() { + every { cas1PremisesService.findPremiseById(any()) } returns null + + val result = service.getBookingDaySummaries( + UUID.randomUUID(), + LocalDate.now(), + emptyList(), + Cas1SpaceBookingDaySummarySortField.PERSON_NAME, + SortDirection.desc, + ) + + assertThat(result).isInstanceOf(CasResult.NotFound::class.java) + assertThat((result as CasResult.NotFound).entityType).isEqualTo("premises") + } + } + + @Test + fun `returns booking day summaries when bookings exist`() { + val premisesId = UUID.randomUUID() + val date = LocalDate.now() + val sort = Sort.by(Sort.Direction.DESC, Cas1SpaceBookingDaySummarySortField.PERSON_NAME.value) + + val spaceBooking1Summary = Cas1SpaceBookingDaySummarySearchResultImpl( + UUID.randomUUID(), + "crn1", + LocalDate.now().minusDays(3), + LocalDate.now().plusDays(3), + tier = "A1", + releaseType = "rotl", + characteristicsPropertyNames = listOf("isArsonSuitable", "hasEnSuite"), + ) + val spaceBooking2Summary = Cas1SpaceBookingDaySummarySearchResultImpl( + UUID.randomUUID(), + "crn2", + LocalDate.now().minusDays(2), + LocalDate.now().plusDays(2), + tier = "A2", + releaseType = "licence", + characteristicsPropertyNames = listOf("isSingle"), + ) + val spaceBookingSummaries = listOf(spaceBooking1Summary, spaceBooking2Summary) + + val person1CaseSummary = CaseSummaryFactory().produce() + val person2CaseSummary = CaseSummaryFactory().produce() + val personSummaries = listOf( + PersonSummaryInfoResult.Success.Full("crn1", person1CaseSummary), + PersonSummaryInfoResult.Success.Full("crn2", person2CaseSummary), + ) + val person1Summary = PersonTransformer() + .personSummaryInfoToPersonSummary( + PersonSummaryInfoResult.Success.Full("crn1", person1CaseSummary), + ) + val person2Summary = PersonTransformer() + .personSummaryInfoToPersonSummary( + PersonSummaryInfoResult.Success.Full("crn2", person2CaseSummary), + ) + + val booking1DaySummary = Cas1SpaceBookingDaySummary( + id = UUID.randomUUID(), + person = person1Summary, + canonicalArrivalDate = LocalDate.now().minusDays(6), + canonicalDepartureDate = LocalDate.now().plusDays(6), + tier = "A2", + releaseType = "rotl", + essentialCharacteristics = listOf(Cas1SpaceBookingCharacteristic.IS_SINGLE, Cas1SpaceBookingCharacteristic.HAS_EN_SUITE), + ) + + val booking2DaySummary = Cas1SpaceBookingDaySummary( + id = UUID.randomUUID(), + person = person2Summary, + canonicalArrivalDate = LocalDate.now().minusDays(3), + canonicalDepartureDate = LocalDate.now().plusDays(36), + tier = "B3", + releaseType = "licence", + essentialCharacteristics = listOf(Cas1SpaceBookingCharacteristic.IS_SINGLE), + ) + + val roomCharacteristic = CharacteristicEntityFactory() + .withPropertyName("isArsonSuitable") + .withModelScope("room") + .withServiceScope("approved-premises") + .produce() + + val user = UserEntityFactory() + .withDefaults() + .produce() + + every { cas1PremisesService.findPremiseById(premisesId) } returns ApprovedPremisesEntityFactory().withDefaults().produce() + every { cas1SpaceBookingRepository.findAllPremisesBookingsForDate(premisesId, date, any(), sort, any()) } returns spaceBookingSummaries + every { characteristicService.getCharacteristicsByPropertyNames(any(), ServiceName.approvedPremises) } returns listOf(roomCharacteristic) + every { userService.getUserForRequest() } returns user + every { offenderService.getPersonSummaryInfoResults(any(), any()) } returns personSummaries + every { cas1SpaceBookingDaySummaryTransformer.toCas1SpaceBookingDaySummary(spaceBookingSummaries[0], any()) } returns booking1DaySummary + every { cas1SpaceBookingDaySummaryTransformer.toCas1SpaceBookingDaySummary(spaceBookingSummaries[1], any()) } returns booking2DaySummary + + val result = service.getBookingDaySummaries( + premisesId, + LocalDate.now(), + listOf(Cas1SpaceBookingCharacteristic.IS_SINGLE), + Cas1SpaceBookingDaySummarySortField.PERSON_NAME, + SortDirection.desc, + ) + + assertThat(result).isInstanceOf(CasResult.Success::class.java) + result as CasResult.Success + val daySummaries = result.value + assertThat(daySummaries.size).isEqualTo(2) + } +} diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/transformer/cas1/Cas1PremisesDayTransformerTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/transformer/cas1/Cas1PremisesDayTransformerTest.kt index 241e8417fb..b77f22373c 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/transformer/cas1/Cas1PremisesDayTransformerTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/transformer/cas1/Cas1PremisesDayTransformerTest.kt @@ -10,7 +10,10 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas1PremiseCap import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas1PremiseCapacityForDay import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas1PremiseCharacteristicAvailability import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas1PremisesSummary -import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas1SpaceCharacteristic +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas1SpaceBookingCharacteristic +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas1SpaceBookingDaySummary +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.PersonSummaryDiscriminator +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.RestrictedPersonSummary import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ApprovedPremisesEntityFactory import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas1.Cas1PremisesDayTransformer import java.time.LocalDate @@ -48,12 +51,27 @@ class Cas1PremisesDayTransformerTest { availableBedCount = 3, bookingCount = 4, characteristicAvailability = listOf( - Cas1PremiseCharacteristicAvailability(Cas1SpaceCharacteristic.isSingle, 10, 4), - Cas1PremiseCharacteristicAvailability(Cas1SpaceCharacteristic.isWheelchairAccessible, 20, 8), + Cas1PremiseCharacteristicAvailability(Cas1SpaceBookingCharacteristic.IS_SINGLE, 10, 4), + Cas1PremiseCharacteristicAvailability(Cas1SpaceBookingCharacteristic.IS_WHEELCHAIR_DESIGNATED, 20, 8), ), ), ) + val spaceBookings = listOf( + Cas1SpaceBookingDaySummary( + id = UUID.randomUUID(), + person = RestrictedPersonSummary( + crn = "crn", + personType = PersonSummaryDiscriminator.restrictedPersonSummary, + ), + canonicalArrivalDate = currentSearchDay.minusDays(1), + canonicalDepartureDate = currentSearchDay.plusDays(1), + tier = "Tier 1", + releaseType = "rotl", + essentialCharacteristics = listOf(Cas1SpaceBookingCharacteristic.IS_SINGLE, Cas1SpaceBookingCharacteristic.HAS_EN_SUITE), + ), + ) + val premiseCapacity = Cas1PremiseCapacity( premise = cas1PremisesSummary, startDate = currentSearchDay, @@ -64,13 +82,14 @@ class Cas1PremisesDayTransformerTest { val result = transformer.toCas1PremisesDaySummary( currentSearchDay, premiseCapacity, + spaceBookings, ) assertThat(result.forDate).isEqualTo(currentSearchDay) assertThat(result.previousDate).isEqualTo(currentSearchDay.minusDays(1)) assertThat(result.nextDate).isEqualTo(currentSearchDay.plusDays(1)) assertThat(result.capacity).isEqualTo(capacity[0]) - assertThat(result.spaceBookings).isEmpty() + assertThat(result.spaceBookings).isEqualTo(spaceBookings) assertThat(result.outOfServiceBeds).isEmpty() } } diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/transformer/cas1/Cas1SpaceBookingDaySummaryTransformerTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/transformer/cas1/Cas1SpaceBookingDaySummaryTransformerTest.kt new file mode 100644 index 0000000000..1fd4f74778 --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/transformer/cas1/Cas1SpaceBookingDaySummaryTransformerTest.kt @@ -0,0 +1,99 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.unit.transformer.cas1 + +import io.mockk.junit5.MockKExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas1SpaceBookingCharacteristic +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.FullPersonSummary +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.CaseSummaryFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas1SpaceBookingDaySummarySearchResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.PersonSummaryInfoResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.PersonTransformer +import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas1.Cas1SpaceBookingDaySummaryTransformer +import java.time.LocalDate +import java.util.UUID + +@ExtendWith(MockKExtension::class) +class Cas1SpaceBookingDaySummaryTransformerTest { + + private val transformer = Cas1SpaceBookingDaySummaryTransformer() + + @ParameterizedTest + @EnumSource(value = Cas1SpaceBookingCharacteristic::class) + fun `toCas1SpaceBookingDaySummary`(characteristic: Cas1SpaceBookingCharacteristic) { + val spaceBookingSummary = + Cas1SpaceBookingDaySummarySearchResultImpl( + UUID.randomUUID(), + "crn1", + LocalDate.now().minusDays(3), + LocalDate.now().plusDays(3), + tier = "A1", + releaseType = "rotl", + characteristicsPropertyNames = listOf(characteristic.value), + ) + val personSummary = PersonTransformer() + .personSummaryInfoToPersonSummary( + PersonSummaryInfoResult.Success.Full("crn1", CaseSummaryFactory().produce()), + ) + + val result = transformer.toCas1SpaceBookingDaySummary( + spaceBookingSummary, + personSummary, + ) + + assertThat(result.id).isEqualTo(spaceBookingSummary.id) + assertThat(result.canonicalArrivalDate).isEqualTo(spaceBookingSummary.canonicalArrivalDate) + assertThat(result.canonicalDepartureDate).isEqualTo(spaceBookingSummary.canonicalDepartureDate) + assertThat(result.tier).isEqualTo(spaceBookingSummary.tier) + assertThat(result.releaseType).isEqualTo(spaceBookingSummary.releaseType) + assertThat(result.essentialCharacteristics.size).isEqualTo(1) + assertThat(result.essentialCharacteristics[0]).isEqualTo(characteristic) + val offender = result.person as FullPersonSummary + assertThat(offender.crn).isEqualTo(personSummary.crn) + assertThat(offender.name).isEqualTo((personSummary as FullPersonSummary).name) + } + + @Test + fun `toCas1SpaceBookingDaySummary with multiple characteristics`() { + val spaceBookingSummary = + Cas1SpaceBookingDaySummarySearchResultImpl( + UUID.randomUUID(), + "crn1", + LocalDate.now().minusDays(3), + LocalDate.now().plusDays(3), + tier = "A1", + releaseType = "rotl", + characteristicsPropertyNames = listOf("isArsonSuitable,hasEnSuite,isSingle,isStepFreeDesignated,isSuitedForSexOffenders,isWheelchairDesignated"), + ) + val personSummary = PersonTransformer() + .personSummaryInfoToPersonSummary( + PersonSummaryInfoResult.Success.Full("crn1", CaseSummaryFactory().produce()), + ) + + val result = transformer.toCas1SpaceBookingDaySummary( + spaceBookingSummary, + personSummary, + ) + + assertThat(result.essentialCharacteristics.size).isEqualTo(6) + assertThat(result.essentialCharacteristics[0]).isEqualTo(Cas1SpaceBookingCharacteristic.IS_ARSON_SUITABLE) + assertThat(result.essentialCharacteristics[1]).isEqualTo(Cas1SpaceBookingCharacteristic.HAS_EN_SUITE) + assertThat(result.essentialCharacteristics[2]).isEqualTo(Cas1SpaceBookingCharacteristic.IS_SINGLE) + assertThat(result.essentialCharacteristics[3]).isEqualTo(Cas1SpaceBookingCharacteristic.IS_STEP_FREE_DESIGNATED) + assertThat(result.essentialCharacteristics[4]).isEqualTo(Cas1SpaceBookingCharacteristic.IS_SUITED_FOR_SEX_OFFENDERS) + assertThat(result.essentialCharacteristics[5]).isEqualTo(Cas1SpaceBookingCharacteristic.IS_WHEELCHAIR_DESIGNATED) + } +} + +data class Cas1SpaceBookingDaySummarySearchResultImpl( + override val id: UUID, + override val crn: String, + override val canonicalArrivalDate: LocalDate, + override val canonicalDepartureDate: LocalDate, + override val tier: String, + override val releaseType: String, + override val characteristicsPropertyNames: List, +) : Cas1SpaceBookingDaySummarySearchResult