From 2041f40e3eabf2387f6a0d1bab269c8e48c876ea Mon Sep 17 00:00:00 2001 From: davidatkinsuk Date: Wed, 18 Dec 2024 08:46:53 +0000 Subject: [PATCH 1/3] Reorder members of Cas1BookingDomainEventService Moved private members to the bottom of the class for consistency --- .../cas1/Cas1BookingDomainEventService.kt | 260 +++++++++--------- 1 file changed, 130 insertions(+), 130 deletions(-) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas1/Cas1BookingDomainEventService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas1/Cas1BookingDomainEventService.kt index 8715189168..a24b625c3f 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas1/Cas1BookingDomainEventService.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas1/Cas1BookingDomainEventService.kt @@ -84,26 +84,6 @@ class Cas1BookingDomainEventService( ) } - private fun BookingEntity.toBookingInfo() = BookingInfo( - id = id, - createdAt = createdAt, - crn = crn, - premises = premises as ApprovedPremisesEntity, - arrivalDate = arrivalDate, - departureDate = departureDate, - isSpaceBooking = false, - ) - - private fun Cas1SpaceBookingEntity.toBookingInfo() = BookingInfo( - id = id, - createdAt = createdAt, - crn = crn, - premises = premises, - arrivalDate = canonicalArrivalDate, - departureDate = canonicalDepartureDate, - isSpaceBooking = true, - ) - fun bookingNotMade( user: UserEntity, placementRequest: PlacementRequestEntity, @@ -158,116 +138,6 @@ class Cas1BookingDomainEventService( ) } - private fun getOffenderDetails( - crn: String, - deliusUsername: String, - ignoreLaoRestrictions: Boolean, - ) = when (val offenderDetailsResult = offenderService.getOffenderByCrn(crn, deliusUsername, ignoreLaoRestrictions)) { - is AuthorisableActionResult.Success -> offenderDetailsResult.entity - else -> null - } - - private fun getStaffDetails(deliusUsername: String) = - when (val staffDetailsResult = apDeliusContextApiClient.getStaffDetail(deliusUsername)) { - is ClientResult.Success -> staffDetailsResult.body - is ClientResult.Failure -> staffDetailsResult.throwException() - } - - data class BookingInfo( - val id: UUID, - val createdAt: OffsetDateTime, - val crn: String, - val premises: ApprovedPremisesEntity, - val arrivalDate: LocalDate, - val departureDate: LocalDate, - val isSpaceBooking: Boolean, - ) - - private fun bookingMade( - applicationId: UUID, - eventNumber: String, - bookingInfo: BookingInfo, - user: UserEntity, - applicationSubmittedOn: OffsetDateTime?, - sentenceType: String?, - releaseType: String?, - situation: String?, - placementRequestId: UUID?, - ) { - val domainEventId = UUID.randomUUID() - val crn = bookingInfo.crn - - val offenderDetails = - when (val offenderDetailsResult = offenderService.getOffenderByCrn(crn, user.deliusUsername, true)) { - is AuthorisableActionResult.Success -> offenderDetailsResult.entity - else -> null - } - - val staffDetails = getStaffDetails(user.deliusUsername) - - val approvedPremises = bookingInfo.premises - val bookingCreatedAt = bookingInfo.createdAt - val isSpaceBooking = bookingInfo.isSpaceBooking - - domainEventService.saveBookingMadeDomainEvent( - DomainEvent( - id = domainEventId, - applicationId = applicationId, - crn = crn, - nomsNumber = offenderDetails?.otherIds?.nomsNumber, - occurredAt = bookingCreatedAt.toInstant(), - bookingId = if (isSpaceBooking) { - null - } else { - bookingInfo.id - }, - cas1SpaceBookingId = if (isSpaceBooking) { - bookingInfo.id - } else { - null - }, - data = BookingMadeEnvelope( - id = domainEventId, - timestamp = bookingCreatedAt.toInstant(), - eventType = EventType.bookingMade, - eventDetails = BookingMade( - applicationId = applicationId, - applicationUrl = applicationUrlTemplate.resolve("id", applicationId.toString()), - bookingId = bookingInfo.id, - personReference = PersonReference( - crn = crn, - noms = offenderDetails?.otherIds?.nomsNumber ?: "Unknown NOMS Number", - ), - deliusEventNumber = eventNumber, - createdAt = bookingCreatedAt.toInstant(), - bookedBy = BookingMadeBookedBy( - staffMember = staffDetails.toStaffMember(), - cru = Cru( - name = user.apArea?.name ?: "Unknown CRU", - ), - ), - premises = Premises( - id = approvedPremises.id, - name = approvedPremises.name, - apCode = approvedPremises.apCode, - legacyApCode = approvedPremises.qCode, - localAuthorityAreaName = approvedPremises.localAuthorityArea!!.name, - ), - arrivalOn = bookingInfo.arrivalDate, - departureOn = bookingInfo.departureDate, - applicationSubmittedOn = applicationSubmittedOn?.toInstant(), - releaseType = releaseType, - sentenceType = sentenceType, - situation = situation, - ), - ), - metadata = mapOfNonNullValues( - MetaDataName.CAS1_PLACEMENT_REQUEST_ID to placementRequestId?.toString(), - ), - ), - ) - } - fun bookingChanged( booking: BookingEntity, changedBy: UserEntity, @@ -367,6 +237,91 @@ class Cas1BookingDomainEventService( ), ) + private fun bookingMade( + applicationId: UUID, + eventNumber: String, + bookingInfo: BookingInfo, + user: UserEntity, + applicationSubmittedOn: OffsetDateTime?, + sentenceType: String?, + releaseType: String?, + situation: String?, + placementRequestId: UUID?, + ) { + val domainEventId = UUID.randomUUID() + val crn = bookingInfo.crn + + val offenderDetails = + when (val offenderDetailsResult = offenderService.getOffenderByCrn(crn, user.deliusUsername, true)) { + is AuthorisableActionResult.Success -> offenderDetailsResult.entity + else -> null + } + + val staffDetails = getStaffDetails(user.deliusUsername) + + val approvedPremises = bookingInfo.premises + val bookingCreatedAt = bookingInfo.createdAt + val isSpaceBooking = bookingInfo.isSpaceBooking + + domainEventService.saveBookingMadeDomainEvent( + DomainEvent( + id = domainEventId, + applicationId = applicationId, + crn = crn, + nomsNumber = offenderDetails?.otherIds?.nomsNumber, + occurredAt = bookingCreatedAt.toInstant(), + bookingId = if (isSpaceBooking) { + null + } else { + bookingInfo.id + }, + cas1SpaceBookingId = if (isSpaceBooking) { + bookingInfo.id + } else { + null + }, + data = BookingMadeEnvelope( + id = domainEventId, + timestamp = bookingCreatedAt.toInstant(), + eventType = EventType.bookingMade, + eventDetails = BookingMade( + applicationId = applicationId, + applicationUrl = applicationUrlTemplate.resolve("id", applicationId.toString()), + bookingId = bookingInfo.id, + personReference = PersonReference( + crn = crn, + noms = offenderDetails?.otherIds?.nomsNumber ?: "Unknown NOMS Number", + ), + deliusEventNumber = eventNumber, + createdAt = bookingCreatedAt.toInstant(), + bookedBy = BookingMadeBookedBy( + staffMember = staffDetails.toStaffMember(), + cru = Cru( + name = user.apArea?.name ?: "Unknown CRU", + ), + ), + premises = Premises( + id = approvedPremises.id, + name = approvedPremises.name, + apCode = approvedPremises.apCode, + legacyApCode = approvedPremises.qCode, + localAuthorityAreaName = approvedPremises.localAuthorityArea!!.name, + ), + arrivalOn = bookingInfo.arrivalDate, + departureOn = bookingInfo.departureDate, + applicationSubmittedOn = applicationSubmittedOn?.toInstant(), + releaseType = releaseType, + sentenceType = sentenceType, + situation = situation, + ), + ), + metadata = mapOfNonNullValues( + MetaDataName.CAS1_PLACEMENT_REQUEST_ID to placementRequestId?.toString(), + ), + ), + ) + } + private fun bookingCancelled( cancellationInfo: CancellationInfo, ) { @@ -442,6 +397,51 @@ class Cas1BookingDomainEventService( ) } + private fun getOffenderDetails( + crn: String, + deliusUsername: String, + ignoreLaoRestrictions: Boolean, + ) = when (val offenderDetailsResult = offenderService.getOffenderByCrn(crn, deliusUsername, ignoreLaoRestrictions)) { + is AuthorisableActionResult.Success -> offenderDetailsResult.entity + else -> null + } + + private fun getStaffDetails(deliusUsername: String) = + when (val staffDetailsResult = apDeliusContextApiClient.getStaffDetail(deliusUsername)) { + is ClientResult.Success -> staffDetailsResult.body + is ClientResult.Failure -> staffDetailsResult.throwException() + } + + private data class BookingInfo( + val id: UUID, + val createdAt: OffsetDateTime, + val crn: String, + val premises: ApprovedPremisesEntity, + val arrivalDate: LocalDate, + val departureDate: LocalDate, + val isSpaceBooking: Boolean, + ) + + private fun BookingEntity.toBookingInfo() = BookingInfo( + id = id, + createdAt = createdAt, + crn = crn, + premises = premises as ApprovedPremisesEntity, + arrivalDate = arrivalDate, + departureDate = departureDate, + isSpaceBooking = false, + ) + + private fun Cas1SpaceBookingEntity.toBookingInfo() = BookingInfo( + id = id, + createdAt = createdAt, + crn = crn, + premises = premises, + arrivalDate = canonicalArrivalDate, + departureDate = canonicalDepartureDate, + isSpaceBooking = true, + ) + private data class CancellationInfo( val bookingId: UUID, val applicationFacade: Cas1ApplicationFacade, From 770fd905379a36530bb2b980429aa3d6d3de25d2 Mon Sep 17 00:00:00 2001 From: davidatkinsuk Date: Wed, 18 Dec 2024 11:40:37 +0000 Subject: [PATCH 2/3] Add previous dates to booking changed domain event This commit updates the CAS1 booking changed domain event to capture the previous date values, if changed. It populates these values for booking changes. --- .../jpa/entity/DomainEventEntity.kt | 4 +++ .../service/BookingService.kt | 22 ++++++++++++--- .../cas1/Cas1BookingDomainEventService.kt | 5 ++++ .../resources/static/domain-events-api.yml | 10 +++++++ .../unit/service/BookingServiceTest.kt | 28 +++++++++++-------- .../Cas1BookingCas1DomainEventServiceTest.kt | 14 ++++++++-- 6 files changed, 64 insertions(+), 19 deletions(-) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/DomainEventEntity.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/DomainEventEntity.kt index 991bd43deb..bcb48581ed 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/DomainEventEntity.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/DomainEventEntity.kt @@ -255,6 +255,10 @@ enum class DomainEventType( Cas1EventType.bookingChanged.value, "An Approved Premises Booking has been changed", TimelineEventType.approvedPremisesBookingChanged, + schemaVersions = listOf( + DEFAULT_DOMAIN_EVENT_SCHEMA_VERSION, + DomainEventSchemaVersion(2, "Captures previous set values, if changed."), + ), ), APPROVED_PREMISES_BOOKING_KEYWORKER_ASSIGNED( DomainEventCas.CAS1, diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/BookingService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/BookingService.kt index a2408f242c..30fe7c8e72 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/BookingService.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/BookingService.kt @@ -416,6 +416,7 @@ class BookingService( private fun shouldCreateDomainEventForBooking(booking: BookingEntity, user: UserEntity?) = booking.service == ServiceName.approvedPremises.value && user != null && (booking.application != null || booking.offlineApplication?.eventNumber != null) + @SuppressWarnings("CyclomaticComplexMethod") @Transactional fun createDateChange( booking: BookingEntity, @@ -456,11 +457,14 @@ class BookingService( } } + val previousArrivalDate = booking.arrivalDate + val previousDepartureDate = booking.departureDate + val dateChangeEntity = dateChangeRepository.save( DateChangeEntity( id = UUID.randomUUID(), - previousDepartureDate = booking.departureDate, - previousArrivalDate = booking.arrivalDate, + previousArrivalDate = previousArrivalDate, + previousDepartureDate = previousDepartureDate, newArrivalDate = effectiveNewArrivalDate, newDepartureDate = effectiveNewDepartureDate, changedAt = OffsetDateTime.now(), @@ -471,8 +475,8 @@ class BookingService( updateBooking( booking.apply { - arrivalDate = dateChangeEntity.newArrivalDate - departureDate = dateChangeEntity.newDepartureDate + arrivalDate = effectiveNewArrivalDate + departureDate = effectiveNewDepartureDate dateChanges.add(dateChangeEntity) }, ) @@ -482,6 +486,16 @@ class BookingService( booking = booking, changedBy = user, bookingChangedAt = OffsetDateTime.now(), + previousArrivalDateIfChanged = if (previousArrivalDate != effectiveNewArrivalDate) { + previousArrivalDate + } else { + null + }, + previousDepartureDateIfChanged = if (previousDepartureDate != effectiveNewDepartureDate) { + previousDepartureDate + } else { + null + }, ) } diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas1/Cas1BookingDomainEventService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas1/Cas1BookingDomainEventService.kt index a24b625c3f..046ca8c237 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas1/Cas1BookingDomainEventService.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas1/Cas1BookingDomainEventService.kt @@ -142,6 +142,8 @@ class Cas1BookingDomainEventService( booking: BookingEntity, changedBy: UserEntity, bookingChangedAt: OffsetDateTime, + previousArrivalDateIfChanged: LocalDate?, + previousDepartureDateIfChanged: LocalDate?, ) { val domainEventId = UUID.randomUUID() val applicationFacade = booking.cas1ApplicationFacade @@ -169,6 +171,7 @@ class Cas1BookingDomainEventService( nomsNumber = offenderDetails?.otherIds?.nomsNumber, occurredAt = bookingChangedAt.toInstant(), bookingId = booking.id, + schemaVersion = 2, data = BookingChangedEnvelope( id = domainEventId, timestamp = bookingChangedAt.toInstant(), @@ -193,6 +196,8 @@ class Cas1BookingDomainEventService( ), arrivalOn = booking.arrivalDate, departureOn = booking.departureDate, + previousArrivalOn = previousArrivalDateIfChanged, + previousDepartureOn = previousDepartureDateIfChanged, ), ), ), diff --git a/src/main/resources/static/domain-events-api.yml b/src/main/resources/static/domain-events-api.yml index 701ef533c9..d6f0786c14 100644 --- a/src/main/resources/static/domain-events-api.yml +++ b/src/main/resources/static/domain-events-api.yml @@ -1241,6 +1241,16 @@ components: type: string example: '2023-04-30' format: date + previousArrivalOn: + type: string + description: Only set if the expected arrival on has changed + example: '2023-01-30' + format: date + previousDepartureOn: + type: string + description: Only set if the expected departure on has changed + example: '2023-01-30' + format: date required: - applicationId - applicationUrl diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/BookingServiceTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/BookingServiceTest.kt index 0d96db2b61..d64d067998 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/BookingServiceTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/BookingServiceTest.kt @@ -2244,7 +2244,7 @@ class BookingServiceTest { } @Test - fun `emits domain event when booking has associated application`() { + fun `emits domain event when booking has associated application, only arrival date changed`() { val application = ApprovedPremisesApplicationEntityFactory() .withCreatedByUser(user) .withSubmittedAt(OffsetDateTime.now()) @@ -2264,10 +2264,10 @@ class BookingServiceTest { every { mockDateChangeRepository.save(any()) } answers { it.invocation.args[0] as DateChangeEntity } every { mockBookingRepository.save(any()) } answers { it.invocation.args[0] as BookingEntity } - every { mockCas1BookingDomainEventService.bookingChanged(any(), any(), any()) } just Runs + every { mockCas1BookingDomainEventService.bookingChanged(any(), any(), any(), any(), any()) } just Runs - val newArrivalDate = LocalDate.parse("2023-07-18") - val newDepartureDate = LocalDate.parse("2023-07-22") + val newArrivalDate = LocalDate.parse("2023-07-15") + val newDepartureDate = LocalDate.parse("2023-07-16") val result = bookingService.createDateChange( booking = booking, @@ -2284,8 +2284,8 @@ class BookingServiceTest { match { it.booking.id == booking.id && it.changedByUser == user && - it.newArrivalDate == LocalDate.parse("2023-07-18") && - it.newDepartureDate == LocalDate.parse("2023-07-22") && + it.newArrivalDate == LocalDate.parse("2023-07-15") && + it.newDepartureDate == LocalDate.parse("2023-07-16") && it.previousArrivalDate == LocalDate.parse("2023-07-14") && it.previousDepartureDate == LocalDate.parse("2023-07-16") }, @@ -2296,8 +2296,8 @@ class BookingServiceTest { mockBookingRepository.save( match { it.id == booking.id && - it.arrivalDate == LocalDate.parse("2023-07-18") && - it.departureDate == LocalDate.parse("2023-07-22") + it.arrivalDate == LocalDate.parse("2023-07-15") && + it.departureDate == LocalDate.parse("2023-07-16") }, ) } @@ -2307,12 +2307,14 @@ class BookingServiceTest { booking = booking, changedBy = user, bookingChangedAt = any(), + previousArrivalDateIfChanged = LocalDate.of(2023, 7, 14), + previousDepartureDateIfChanged = null, ) } } @Test - fun `emits domain event when booking has associated offline application with an event number`() { + fun `emits domain event when booking has associated offline application with an event number, only departure date changed`() { val application = OfflineApplicationEntityFactory() .withEventNumber("123") .produce() @@ -2331,9 +2333,9 @@ class BookingServiceTest { every { mockDateChangeRepository.save(any()) } answers { it.invocation.args[0] as DateChangeEntity } every { mockBookingRepository.save(any()) } answers { it.invocation.args[0] as BookingEntity } - every { mockCas1BookingDomainEventService.bookingChanged(any(), any(), any()) } just Runs + every { mockCas1BookingDomainEventService.bookingChanged(any(), any(), any(), any(), any()) } just Runs - val newArrivalDate = LocalDate.parse("2023-07-18") + val newArrivalDate = LocalDate.parse("2023-07-14") val newDepartureDate = LocalDate.parse("2023-07-22") val result = bookingService.createDateChange( @@ -2351,6 +2353,8 @@ class BookingServiceTest { booking = booking, changedBy = user, bookingChangedAt = any(), + previousArrivalDateIfChanged = null, + previousDepartureDateIfChanged = LocalDate.of(2023, 7, 16), ) } } @@ -2375,7 +2379,7 @@ class BookingServiceTest { every { mockDateChangeRepository.save(any()) } answers { it.invocation.args[0] as DateChangeEntity } every { mockBookingRepository.save(any()) } answers { it.invocation.args[0] as BookingEntity } - every { mockCas1BookingDomainEventService.bookingChanged(any(), any(), any()) } just Runs + every { mockCas1BookingDomainEventService.bookingChanged(any(), any(), any(), any(), any()) } just Runs val newArrivalDate = LocalDate.parse("2023-07-18") val newDepartureDate = LocalDate.parse("2023-07-22") diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas1/Cas1BookingCas1DomainEventServiceTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas1/Cas1BookingCas1DomainEventServiceTest.kt index a280261ad8..730a9b5ea9 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas1/Cas1BookingCas1DomainEventServiceTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas1/Cas1BookingCas1DomainEventServiceTest.kt @@ -290,7 +290,7 @@ class Cas1BookingCas1DomainEventServiceTest { } @Nested - inner class BookingAmended { + inner class BookingChanged { private val changedBy = UserEntityFactory() .withDefaults() @@ -306,7 +306,7 @@ class Cas1BookingCas1DomainEventServiceTest { .produce() @Test - fun `success for application`() { + fun `success for application, no date changes`() { val application = ApprovedPremisesApplicationEntityFactory() .withCreatedByUser(changedBy) .withSubmittedAt(OffsetDateTime.now()) @@ -343,6 +343,8 @@ class Cas1BookingCas1DomainEventServiceTest { booking, changedBy, bookingChangedAt = createdAt, + previousArrivalDateIfChanged = null, + previousDepartureDateIfChanged = null, ) val domainEventArgument = slot>() @@ -384,10 +386,12 @@ class Cas1BookingCas1DomainEventServiceTest { assertThat(data.arrivalOn).isEqualTo(LocalDate.of(2012, 12, 12)) assertThat(data.departureOn).isEqualTo(LocalDate.of(2099, 11, 11)) + assertThat(data.previousArrivalOn).isNull() + assertThat(data.previousDepartureOn).isNull() } @Test - fun `success for offline application`() { + fun `success for offline application, date changes`() { val offlineApplication = OfflineApplicationEntityFactory() .withEventNumber("offline app event number") .produce() @@ -422,6 +426,8 @@ class Cas1BookingCas1DomainEventServiceTest { booking, changedBy, bookingChangedAt = createdAt, + previousArrivalDateIfChanged = LocalDate.of(2012, 12, 11), + previousDepartureDateIfChanged = LocalDate.of(2099, 11, 10), ) val domainEventArgument = slot>() @@ -463,6 +469,8 @@ class Cas1BookingCas1DomainEventServiceTest { assertThat(data.arrivalOn).isEqualTo(LocalDate.of(2012, 12, 12)) assertThat(data.departureOn).isEqualTo(LocalDate.of(2099, 11, 11)) + assertThat(data.previousArrivalOn).isEqualTo(LocalDate.of(2012, 12, 11)) + assertThat(data.previousDepartureOn).isEqualTo(LocalDate.of(2099, 11, 10)) } } From 08082aa5cb4854aa7df93461c9ed8ea828ce8ca8 Mon Sep 17 00:00:00 2001 From: davidatkinsuk Date: Wed, 18 Dec 2024 14:00:07 +0000 Subject: [PATCH 3/3] Use CasResult for Booking Changed Function --- .../controller/PremisesController.kt | 2 +- .../service/BookingService.kt | 7 +- .../unit/service/BookingServiceTest.kt | 49 +++++++------- .../unit/util/CasResultAssertions.kt | 65 +++++++++++++++++++ 4 files changed, 94 insertions(+), 29 deletions(-) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/PremisesController.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/PremisesController.kt index 01c5d4af3d..00c584bd1b 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/PremisesController.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/PremisesController.kt @@ -679,7 +679,7 @@ class PremisesController( newDepartureDate = body.newDepartureDate, ) - val dateChange = extractResultEntityOrThrow(result) + val dateChange = extractEntityFromCasResult(result) return ResponseEntity.ok(dateChangeTransformer.transformJpaToApi(dateChange)) } diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/BookingService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/BookingService.kt index 30fe7c8e72..7220732dda 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/BookingService.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/BookingService.kt @@ -35,6 +35,7 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.UserQualifica import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.serviceScopeMatches import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.PersonInfoResult import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.validated +import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.validatedCasResult import uk.gov.justice.digital.hmpps.approvedpremisesapi.problem.InternalServerErrorProblem import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.AuthorisableActionResult import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.CasResult @@ -423,7 +424,7 @@ class BookingService( user: UserEntity, newArrivalDate: LocalDate?, newDepartureDate: LocalDate?, - ) = validated { + ) = validatedCasResult { val effectiveNewArrivalDate = newArrivalDate ?: booking.arrivalDate val effectiveNewDepartureDate = newDepartureDate ?: booking.departureDate @@ -439,11 +440,11 @@ class BookingService( ?: throw InternalServerErrorProblem("No bed ID present on Booking: ${booking.id}") getBookingWithConflictingDates(effectiveNewArrivalDate, expectedLastUnavailableDate, booking.id, bedId)?.let { - return@validated it.id hasConflictError "A Booking already exists for dates from ${it.arrivalDate} to ${it.lastUnavailableDate} which overlaps with the desired dates" + return@validatedCasResult it.id hasConflictError "A Booking already exists for dates from ${it.arrivalDate} to ${it.lastUnavailableDate} which overlaps with the desired dates" } getLostBedWithConflictingDates(effectiveNewArrivalDate, expectedLastUnavailableDate, null, bedId)?.let { - return@validated it.id hasConflictError "A Lost Bed already exists for dates from ${it.startDate} to ${it.endDate} which overlaps with the desired dates" + return@validatedCasResult it.id hasConflictError "A Lost Bed already exists for dates from ${it.startDate} to ${it.endDate} which overlaps with the desired dates" } } diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/BookingServiceTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/BookingServiceTest.kt index d64d067998..9239c870f7 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/BookingServiceTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/BookingServiceTest.kt @@ -87,6 +87,7 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas1.Cas1Booking import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas1.WithdrawableEntityType import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas1.WithdrawalContext import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas1.WithdrawalTriggeredByUser +import uk.gov.justice.digital.hmpps.approvedpremisesapi.unit.util.assertThat import java.time.LocalDate import java.time.OffsetDateTime import java.util.UUID @@ -2020,9 +2021,9 @@ class BookingServiceTest { newDepartureDate = LocalDate.parse("2023-07-14"), ) - assertThat(result is ValidatableActionResult.ConflictError).isTrue - result as ValidatableActionResult.ConflictError - assertThat(result.message).contains("A Booking already exists") + assertThat(result) + .isConflictError() + .hasMessageContaining("A Booking already exists") } @Test @@ -2052,9 +2053,9 @@ class BookingServiceTest { newDepartureDate = LocalDate.parse("2023-07-14"), ) - assertThat(result is ValidatableActionResult.ConflictError).isTrue - result as ValidatableActionResult.ConflictError - assertThat(result.message).contains("A Lost Bed already exists") + assertThat(result) + .isConflictError() + .hasMessageContaining("A Lost Bed already exists") } @Test @@ -2076,9 +2077,9 @@ class BookingServiceTest { newDepartureDate = LocalDate.parse("2023-07-14"), ) - assertThat(result is ValidatableActionResult.FieldValidationError).isTrue - result as ValidatableActionResult.FieldValidationError - assertThat(result.validationMessages).containsEntry("$.newDepartureDate", "beforeBookingArrivalDate") + assertThat(result) + .isFieldValidationError() + .hasMessage("$.newDepartureDate", "beforeBookingArrivalDate") } @Test @@ -2105,9 +2106,9 @@ class BookingServiceTest { newDepartureDate = LocalDate.parse("2023-07-16"), ) - assertThat(result is ValidatableActionResult.FieldValidationError).isTrue - result as ValidatableActionResult.FieldValidationError - assertThat(result.validationMessages).containsEntry("$.newArrivalDate", "arrivalDateCannotBeChangedOnArrivedBooking") + assertThat(result) + .isFieldValidationError() + .hasMessage("$.newArrivalDate", "arrivalDateCannotBeChangedOnArrivedBooking") } @Test @@ -2137,9 +2138,7 @@ class BookingServiceTest { newDepartureDate = LocalDate.parse("2023-07-16"), ) - assertThat(result is ValidatableActionResult.GeneralValidationError).isTrue - result as ValidatableActionResult.GeneralValidationError - assertThat(result.message).isEqualTo("This Booking is cancelled and as such cannot be modified") + assertThat(result).isGeneralValidationError("This Booking is cancelled and as such cannot be modified") } @Test @@ -2168,8 +2167,8 @@ class BookingServiceTest { newDepartureDate = LocalDate.parse("2023-07-15"), ) - assertThat(result is ValidatableActionResult.Success).isTrue - result as ValidatableActionResult.Success + assertThat(result).isSuccess() + result as CasResult.Success verify { mockDateChangeRepository.save( @@ -2216,8 +2215,8 @@ class BookingServiceTest { newDepartureDate = LocalDate.parse("2023-07-22"), ) - assertThat(result is ValidatableActionResult.Success).isTrue - result as ValidatableActionResult.Success + assertThat(result).isSuccess() + result as CasResult.Success verify { mockDateChangeRepository.save( @@ -2276,8 +2275,8 @@ class BookingServiceTest { newDepartureDate = newDepartureDate, ) - assertThat(result is ValidatableActionResult.Success).isTrue - result as ValidatableActionResult.Success + assertThat(result).isSuccess() + result as CasResult.Success verify { mockDateChangeRepository.save( @@ -2345,8 +2344,8 @@ class BookingServiceTest { newDepartureDate = newDepartureDate, ) - assertThat(result is ValidatableActionResult.Success).isTrue - result as ValidatableActionResult.Success + assertThat(result).isSuccess() + result as CasResult.Success verify(exactly = 1) { mockCas1BookingDomainEventService.bookingChanged( @@ -2391,8 +2390,8 @@ class BookingServiceTest { newDepartureDate = newDepartureDate, ) - assertThat(result is ValidatableActionResult.Success).isTrue - result as ValidatableActionResult.Success + assertThat(result).isSuccess() + result as CasResult.Success verify { mockDateChangeRepository.save( diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/util/CasResultAssertions.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/util/CasResultAssertions.kt index eeaea523e9..ee2b2257ce 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/util/CasResultAssertions.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/util/CasResultAssertions.kt @@ -18,6 +18,21 @@ class CasResultAssertions(actual: CasResult) : AbstractAssert { + if (actual !is CasResult.ConflictError) { + failWithMessage("Expected CasResult.ConflictError but was <%s>", actual.javaClass.simpleName, actual) + } + return CasResultConflictErrorAssertions(actual as CasResult.ConflictError) + } + + fun isFieldValidationError(): CasResultFieldValidationErrorAssertions { + if (actual !is CasResult.FieldValidationError) { + failWithMessage("Expected CasResult.FieldValidationError but was <%s>", actual.javaClass.simpleName, actual) + } + return CasResultFieldValidationErrorAssertions(actual as CasResult.FieldValidationError) + } + + @Deprecated("Use the chained version isFieldValidationError()") fun isFieldValidationError(field: String, expectedMessage: String): CasResultAssertions { if (actual !is CasResult.FieldValidationError) { failWithMessage("Expected CasResult.FieldValidationError but was <%s>", actual.javaClass.simpleName, actual) @@ -86,3 +101,53 @@ class CasSuccessResultAssertions(actual: CasResult.Success) : AbstractAsse check.invoke(actual.value) } } + +class CasResultConflictErrorAssertions(actual: CasResult.ConflictError) : + AbstractAssert, CasResult.ConflictError>( + actual, + CasResultConflictErrorAssertions::class.java, + ) { + fun hasEntityId(expected: Any): CasResultConflictErrorAssertions { + val value = actual.conflictingEntityId + if (value != expected) { + failWithMessage("Expected ConflictError Entity Id value to be <%s> but was:<%s>", expected, value) + } + return this + } + + fun hasMessage(expected: Any): CasResultConflictErrorAssertions { + val value = actual.message + if (value != expected) { + failWithMessage("Expected ConflictError Message value to be <%s> but was:<%s>", expected, value) + } + return this + } + + fun hasMessageContaining(expected: String): CasResultConflictErrorAssertions { + val value = actual.message + assertThat(value).contains(expected) + return this + } +} + +class CasResultFieldValidationErrorAssertions(actual: CasResult.FieldValidationError) : + AbstractAssert, CasResult.FieldValidationError>( + actual, + CasResultFieldValidationErrorAssertions::class.java, + ) { + fun hasMessage(field: String, expectedMessage: String): CasResultFieldValidationErrorAssertions { + val validationMessages = actual.validationMessages + + if (!validationMessages.containsKey(field)) { + failWithMessage("Expected field <%s> not found in validation messages", field) + } else if (validationMessages[field] != expectedMessage) { + failWithMessage( + "Expected field <%s> to have message <%s> but was <%s>", + field, + expectedMessage, + validationMessages[field], + ) + } + return this + } +}