Skip to content

Commit

Permalink
CRS-2297 Version two endpoint. (#2368)
Browse files Browse the repository at this point in the history
  • Loading branch information
ldlharper authored Mar 4, 2025
1 parent 6a43ab2 commit 756a3bf
Show file tree
Hide file tree
Showing 8 changed files with 222 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package uk.gov.justice.hmpps.prison.api.model.calculation

import io.swagger.v3.oas.annotations.media.Schema

@Schema(description = "The active sentence envelope is a prisoner to be calculated as part of a bulk calculation")
data class CalculableSentenceEnvelopeVersion2(
@Schema(description = "Prisoner Identifier", example = "A1234AA", requiredMode = Schema.RequiredMode.REQUIRED)
var prisonerNumber: String,
@Schema(description = "The booking ID")
val bookingId: Long,
)
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
import uk.gov.justice.hmpps.prison.api.model.VisitWithVisitors;
import uk.gov.justice.hmpps.prison.api.model.adjudications.AdjudicationSummary;
import uk.gov.justice.hmpps.prison.api.model.calculation.CalculableSentenceEnvelope;
import uk.gov.justice.hmpps.prison.api.model.calculation.CalculableSentenceEnvelopeVersion2;
import uk.gov.justice.hmpps.prison.api.support.Order;
import uk.gov.justice.hmpps.prison.core.HasWriteScope;
import uk.gov.justice.hmpps.prison.core.ProgrammaticAuthorisation;
Expand All @@ -99,6 +100,7 @@
import uk.gov.justice.hmpps.prison.service.OffenderFixedTermRecallService;
import uk.gov.justice.hmpps.prison.service.OffenderMilitaryRecordService;
import uk.gov.justice.hmpps.prison.service.SentenceEnvelopeService;
import uk.gov.justice.hmpps.prison.service.SentenceEnvelopeServiceVersion2;
import uk.gov.justice.hmpps.prison.service.keyworker.KeyWorkerAllocationService;

import java.time.LocalDate;
Expand Down Expand Up @@ -134,6 +136,7 @@ public class BookingResource {
private final AppointmentsService appointmentsService;
private final OffenderFixedTermRecallService fixedTermRecallService;
private final SentenceEnvelopeService sentenceEnvelopeService;
private final SentenceEnvelopeServiceVersion2 sentenceEnvelopeServiceVersion2;
private final OffenderMilitaryRecordService offenderMilitaryRecordService;

@ApiResponses({
Expand Down Expand Up @@ -913,4 +916,15 @@ public List<CourtEventOutcome> getCourtEventOutcomes(@RequestBody @Parameter(des
public List<CalculableSentenceEnvelope> getCalculableSentenceEnvelopeByOffenderNos(@RequestParam(value = "offenderNo") @Parameter(description = "Filter by a list of offender numbers") final Set<String> offenderNumbers) {
return sentenceEnvelopeService.getCalculableSentenceEnvelopeByOffenderNumbers(offenderNumbers);
}
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK"),
@ApiResponse(responseCode = "400", description = "Invalid request.", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))}),
@ApiResponse(responseCode = "500", description = "Unrecoverable error occurred whilst processing request.", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))})
})
@Operation(summary = "Details of the of the prisoner to be calculated")
@PreAuthorize("hasRole('RELEASE_DATE_MANUAL_COMPARER')")
@GetMapping("/latest/calculable-sentence-envelope/v2")
public List<CalculableSentenceEnvelopeVersion2> getCalculableSentenceEnvelopeByOffenderNosV2(@RequestParam(value = "offenderNo") @Parameter(description = "Filter by a list of offender numbers") final Set<String> offenderNumbers) {
return sentenceEnvelopeServiceVersion2.getCalculableSentenceEnvelopeByOffenderNumbers(offenderNumbers);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,16 @@ import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import uk.gov.justice.hmpps.prison.api.model.ErrorResponse
import uk.gov.justice.hmpps.prison.api.model.calculation.CalculableSentenceEnvelope
import uk.gov.justice.hmpps.prison.api.model.calculation.CalculableSentenceEnvelopeVersion2
import uk.gov.justice.hmpps.prison.security.VerifyAgencyAccess
import uk.gov.justice.hmpps.prison.service.SentenceEnvelopeService
import uk.gov.justice.hmpps.prison.service.SentenceEnvelopeServiceVersion2

@RestController
@Tag(name = "prison")
@Validated
@RequestMapping(value = ["/api/prison"], produces = ["application/json"])
class PrisonResource(private val sentenceEnvelopeService: SentenceEnvelopeService) {
class PrisonResource(private val sentenceEnvelopeService: SentenceEnvelopeService, private val sentenceEnvelopeServiceVersion2: SentenceEnvelopeServiceVersion2) {

@ApiResponses(
ApiResponse(responseCode = "200", description = "OK"),
Expand Down Expand Up @@ -59,4 +61,38 @@ class PrisonResource(private val sentenceEnvelopeService: SentenceEnvelopeServic
@Parameter(description = "Requested limit of the page size (i.e. the number of bookings in response)")
size: Int,
): Page<CalculableSentenceEnvelope> = sentenceEnvelopeService.getCalculableSentenceEnvelopeByEstablishment(agencyId, page, size)

@ApiResponses(
ApiResponse(responseCode = "200", description = "OK"),
ApiResponse(
responseCode = "400",
description = "Invalid request.",
content = [Content(mediaType = "application/json", schema = Schema(implementation = ErrorResponse::class))],
),
ApiResponse(
responseCode = "404",
description = "Requested resource not found.",
content = [Content(mediaType = "application/json", schema = Schema(implementation = ErrorResponse::class))],
),
ApiResponse(
responseCode = "500",
description = "Unrecoverable error occurred whilst processing request.",
content = [Content(mediaType = "application/json", schema = Schema(implementation = ErrorResponse::class))],
),
)
@Operation(summary = "Details of the the person to be calculated at a particular establishment (paged response)")
@PreAuthorize("hasRole('RELEASE_DATE_MANUAL_COMPARER')")
@VerifyAgencyAccess(overrideRoles = ["VIEW_PRISONER_DATA"])
@GetMapping("/{agencyId}/booking/latest/paged/calculable-sentence-envelope/v2")
fun getCalculableSentenceEnvelopeByEstablishmentV2(
@PathVariable
@Parameter(description = "The identifier of the establishment(prison) to get the active bookings for", required = true)
agencyId: String,
@RequestParam(value = "page", defaultValue = "0", required = false)
@Parameter(description = "The page number to retrieve of the paged results (starts at zero)")
page: Int,
@RequestParam(value = "size", defaultValue = "200", required = false)
@Parameter(description = "Requested limit of the page size (i.e. the number of bookings in response)")
size: Int,
): Page<CalculableSentenceEnvelopeVersion2> = sentenceEnvelopeServiceVersion2.getCalculableSentenceEnvelopeByEstablishment(agencyId, page, size)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package uk.gov.justice.hmpps.prison.service

import org.springframework.data.domain.Page
import org.springframework.data.domain.PageImpl
import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Sort
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import uk.gov.justice.hmpps.prison.api.model.calculation.CalculableSentenceEnvelopeVersion2
import uk.gov.justice.hmpps.prison.repository.jpa.model.OffenderBooking
import uk.gov.justice.hmpps.prison.repository.jpa.repository.AgencyLocationRepository
import uk.gov.justice.hmpps.prison.repository.jpa.repository.OffenderBookingId
import uk.gov.justice.hmpps.prison.repository.jpa.repository.OffenderBookingRepository

@Service
@Transactional(readOnly = true)
class SentenceEnvelopeServiceVersion2(
private val agencyLocationRepository: AgencyLocationRepository,
private val offenderBookingRepository: OffenderBookingRepository,
private val bookingService: BookingService,
) {
fun getCalculableSentenceEnvelopeByEstablishment(
caseLoad: String,
pageNumber: Int,
pageSize: Int,
): Page<CalculableSentenceEnvelopeVersion2> {
val agencyLocation = agencyLocationRepository.getReferenceById(caseLoad)
val pageRequest = PageRequest.of(pageNumber, pageSize, Sort.by("bookingId"))
val bookingIds =
offenderBookingRepository.findDistinctByActiveTrueAndLocationAndSentences_statusAndSentences_CalculationType_CalculationTypeNotLikeAndSentences_CalculationType_CategoryNot(
agencyLocation,
"A",
"%AGG%",
"LICENCE",
pageRequest,
)
val activeBookings =
offenderBookingRepository.findAllByBookingIdIn(bookingIds.map(OffenderBookingId::bookingId).toList())
val calculableSentenceEnvelopes =
activeBookings.map { determineCalculableSentenceEnvelope(it) }.sortedBy { it.bookingId }
return PageImpl(calculableSentenceEnvelopes, pageRequest, bookingIds.totalElements)
}

fun getCalculableSentenceEnvelopeByOffenderNumbers(offenderNumbers: Set<String?>): List<CalculableSentenceEnvelopeVersion2> {
val bookingIds =
offenderBookingRepository.findDistinctByActiveTrueAndOffenderNomsIdInAndSentences_statusAndSentences_CalculationType_CalculationTypeNotLikeAndSentences_CalculationType_CategoryNot(
offenderNumbers,
"A",
"%AGG%",
"LICENCE",
)

// ensure that the user has access to each of the bookings
bookingIds.forEach { bookingService.verifyBookingAccess(it.bookingId, "VIEW_PRISONER_DATA") }

val activeBookings = offenderBookingRepository.findAllByBookingIdIn(bookingIds.map(OffenderBookingId::bookingId))
return activeBookings.map { determineCalculableSentenceEnvelope(it) }
}

private fun determineCalculableSentenceEnvelope(offenderBooking: OffenderBooking): CalculableSentenceEnvelopeVersion2 = CalculableSentenceEnvelopeVersion2(
offenderBooking.offender.nomsId,
offenderBooking.bookingId,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import org.junit.jupiter.api.Test
import org.springframework.core.ParameterizedTypeReference
import org.springframework.http.MediaType
import uk.gov.justice.hmpps.prison.api.model.calculation.CalculableSentenceEnvelope
import uk.gov.justice.hmpps.prison.api.model.calculation.CalculableSentenceEnvelopeVersion2
import java.time.LocalDate

@DisplayName("GET /api/bookings/latest/calculable-sentence-envelope")
Expand Down Expand Up @@ -95,6 +96,31 @@ class BookingResourceIntTest_getCalculableSentenceEnvelope : ResourceTest() {
.contains(calculableSentenceEnvelope, fixedRecallCalculableSentenceEnvelope)
}

@Test
fun `Test that version 2 endpoint returns a summary list when authorised`() {
val json = getPrisonResourceAsText("prison_resource_single_calculable_sentence_envelope_v2.json")
val calculableSentenceEnvelope = objectMapper.readValue<CalculableSentenceEnvelopeVersion2>(json)
val fixedRecallCalculableSentenceEnvelope = objectMapper.readValue<CalculableSentenceEnvelopeVersion2>(getPrisonResourceAsText("prison_resource_fixed_recall_calculable_sentence_envelope_v2.json"))

webTestClient.get()
.uri { uriBuilder ->
uriBuilder.path("/api/bookings/latest/calculable-sentence-envelope/v2")
.queryParam("offenderNo", "A1234AB", "A1234AE")
.build()
}
.headers(
setAuthorisation(
listOf("ROLE_RELEASE_DATE_MANUAL_COMPARER"),
),
)
.header("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isOk
.expectBodyList(object : ParameterizedTypeReference<CalculableSentenceEnvelopeVersion2>() {})
.contains(calculableSentenceEnvelope, fixedRecallCalculableSentenceEnvelope)
}

@Test
fun `Test that endpoint returns a forbidden when unauthorised`() {
webTestClient.get()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ import org.springframework.test.context.jdbc.Sql.ExecutionPhase.BEFORE_TEST_METH
import org.springframework.test.context.jdbc.SqlConfig
import org.springframework.test.context.jdbc.SqlConfig.TransactionMode.ISOLATED
import uk.gov.justice.hmpps.prison.api.model.calculation.CalculableSentenceEnvelope
import uk.gov.justice.hmpps.prison.api.model.calculation.CalculableSentenceEnvelopeVersion2
import java.time.LocalDate

@DisplayName("GET /api/prison/{establishmentId}/booking/latest/paged/calculable-sentence-envelope")
class PrisonResourceTest : ResourceTest() {
class PrisonResourceIntTest : ResourceTest() {

@Sql(
scripts = ["/sql/create_offender_details_used_for_calc.sql"],
Expand Down Expand Up @@ -80,6 +81,66 @@ class PrisonResourceTest : ResourceTest() {
assertThat(secondPageResponse.content.size).isEqualTo(1)
}

@Sql(
scripts = ["/sql/create_offender_details_used_for_calc.sql"],
executionPhase = BEFORE_TEST_METHOD,
config = SqlConfig(transactionMode = ISOLATED),
)
@Sql(
scripts = ["/sql/clean_offender_details_used_for_calc.sql"],
executionPhase = AFTER_TEST_METHOD,
config = SqlConfig(transactionMode = ISOLATED),
)
@Test
fun `Test that paginated endpoint version 2 returns the correct calculable sentence and the correct number of pages`() {
val establishment = "LEI"

val json = getPrisonResourceAsText("prison_resource_single_calculable_sentence_envelope_v2.json")
val calculableSentenceEnvelope = objectMapper.readValue<CalculableSentenceEnvelopeVersion2>(json)

val fixedRecallCalculableSentenceEnvelope = objectMapper.readValue<CalculableSentenceEnvelopeVersion2>(getPrisonResourceAsText("prison_resource_fixed_recall_calculable_sentence_envelope_v2.json"))

val firstPageResponse = webTestClient.get()
.uri("/api/prison/{establishment}/booking/latest/paged/calculable-sentence-envelope/v2?page=0&size=3", establishment)
.headers(
setAuthorisation(
listOf("ROLE_RELEASE_DATE_MANUAL_COMPARER"),
),
)
.header("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isOk
.expectBody(object : ParameterizedTypeReference<RestResponsePage<CalculableSentenceEnvelopeVersion2>>() {})
.returnResult()
.responseBody!!

assertThat(firstPageResponse.pageable.pageNumber).isEqualTo(0)
assertThat(firstPageResponse.totalPages).isEqualTo(2)
assertThat(firstPageResponse.isLast).isFalse()
assertThat(firstPageResponse.content).contains(calculableSentenceEnvelope, fixedRecallCalculableSentenceEnvelope)

val secondPageResponse = webTestClient.get()
.uri("/api/prison/{establishment}/booking/latest/paged/calculable-sentence-envelope/v2?page=1&size=3", establishment)
.headers(
setAuthorisation(
listOf("ROLE_RELEASE_DATE_MANUAL_COMPARER"),
),
)
.header("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isOk
.expectBody(object : ParameterizedTypeReference<RestResponsePage<CalculableSentenceEnvelopeVersion2>>() {})
.returnResult()
.responseBody!!

assertThat(secondPageResponse.pageable.pageNumber).isEqualTo(1)
assertThat(secondPageResponse.totalPages).isEqualTo(2)
assertThat(secondPageResponse.isLast).isTrue()
assertThat(secondPageResponse.content.size).isEqualTo(1)
}

private fun getPrisonResourceAsText(path: String): String = getResourceAsText("/${this.javaClass.`package`.name.replace(".", "/")}/$path")

@Suppress("RECEIVER_NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"prisonerNumber": "A1234AE",
"bookingId": -5
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"prisonerNumber": "A1234AB",
"bookingId": -2
}

0 comments on commit 756a3bf

Please sign in to comment.