diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/CellLocationController.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/CellLocationController.kt new file mode 100644 index 000000000..b56dbf119 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/CellLocationController.kt @@ -0,0 +1,37 @@ +package uk.gov.justice.digital.hmpps.hmppsintegrationapi.controllers.v1.person + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.exception.EntityNotFoundException +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.extensions.decodeUrlCharacters +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.CellLocation +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApiError +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.GetCellLocationForPersonService +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.internal.AuditService + +@RestController +@RequestMapping("/v1/persons") +class CellLocationController( + @Autowired val auditService: AuditService, + @Autowired val getCellLocationForPersonService: GetCellLocationForPersonService, +) { + @GetMapping("{encodedHmppsId}/cell-location") + fun getPersonCellLocation( + @PathVariable encodedHmppsId: String, + ): Map { + val hmppsId = encodedHmppsId.decodeUrlCharacters() + + val response = getCellLocationForPersonService.execute(hmppsId) + + if (response.hasError(UpstreamApiError.Type.ENTITY_NOT_FOUND)) { + throw EntityNotFoundException("Could not find cell location for id: $hmppsId") + } + + auditService.createEvent("GET_PERSON_CELL_LOCATION", mapOf("hmppsId" to hmppsId)) + + return mapOf("data" to response.data) + } +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/CellLocation.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/CellLocation.kt new file mode 100644 index 000000000..e7229c591 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/CellLocation.kt @@ -0,0 +1,7 @@ +package uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps + +data class CellLocation( + val prisonCode: String? = null, + val prisonName: String? = null, + val cell: String? = null, +) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/prisoneroffendersearch/POSPrisoner.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/prisoneroffendersearch/POSPrisoner.kt index 19addb8b3..5f82ece68 100755 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/prisoneroffendersearch/POSPrisoner.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/prisoneroffendersearch/POSPrisoner.kt @@ -17,6 +17,10 @@ data class POSPrisoner( val bookingId: String? = null, val maritalStatus: String? = null, val croNumber: String? = null, + val prisonId: String? = null, + val prisonName: String? = null, + val cellLocation: String? = null, + val inOutStatus: String? = null, ) { fun toPerson(): Person = Person( diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/GetCellLocationForPersonService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/GetCellLocationForPersonService.kt new file mode 100644 index 000000000..52a37cb7b --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/GetCellLocationForPersonService.kt @@ -0,0 +1,34 @@ +package uk.gov.justice.digital.hmpps.hmppsintegrationapi.services + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.stereotype.Service +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.gateways.PrisonerOffenderSearchGateway +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.CellLocation +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Response + +@Service +class GetCellLocationForPersonService( + @Autowired val getPersonService: GetPersonService, + @Autowired val prisonerOffenderSearchGateway: PrisonerOffenderSearchGateway, +) { + fun execute(hmppsId: String): Response { + val personResponse = getPersonService.execute(hmppsId = hmppsId) + + val prisonResponse = + personResponse.data?.identifiers?.nomisNumber?.let { + prisonerOffenderSearchGateway.getPrisonOffender(nomsNumber = it) + } + + val cellLocation = + if (prisonResponse?.data?.inOutStatus == "IN") { + CellLocation(prisonCode = prisonResponse.data.prisonId, prisonName = prisonResponse.data.prisonName, cell = prisonResponse.data.cellLocation) + } else { + CellLocation() + } + + return Response( + data = cellLocation, + errors = personResponse.errors + (prisonResponse?.errors ?: emptyList()), + ) + } +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 2ac5e03fd..676df3794 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -53,6 +53,7 @@ authorisation: moj-pes: - "/v1/persons/.*/name" - "/v1/hmpps-id/nomis-number/\\.*+[^/]*$" + - "/v1/persons/.*/cell-location" kubernetes-health-check-client: - "/health/liveness" - "/health/readiness" diff --git a/src/main/resources/application-local-docker.yml b/src/main/resources/application-local-docker.yml index bdc0bcc8b..af2765bf2 100644 --- a/src/main/resources/application-local-docker.yml +++ b/src/main/resources/application-local-docker.yml @@ -50,6 +50,7 @@ authorisation: - "/v1/persons/.*/case-notes" - "/v1/persons/.*/person-responsible-officer" - "/v1/persons/.*/risk-management-plan" + - "/v1/persons/.*/cell-location" - "/v1/epf/person-details/.*/\\.*+[^/]*$" - "/v1/hmpps-id/nomis-number/\\.*+[^/]*$" - "/health" diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index f30371733..99cd1fc34 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -57,6 +57,7 @@ authorisation: - "/v1/persons/.*/person-responsible-officer" - "/v1/persons/.*/status-information" - "/v1/persons/.*/risk-management-plan" + - "/v1/persons/.*/cell-location" - "/v1/epf/person-details/.*/\\.*+[^/]*$" - "/v1/hmpps-id/nomis-number/\\.*+[^/]*$" - "/health" diff --git a/src/main/resources/application-preprod.yml b/src/main/resources/application-preprod.yml index 5efa7c5c3..4dd3d0d8d 100644 --- a/src/main/resources/application-preprod.yml +++ b/src/main/resources/application-preprod.yml @@ -31,6 +31,7 @@ authorisation: moj-pes: - "/v1/persons/.*/name" - "/v1/hmpps-id/nomis-number/\\.*+[^/]*$" + - "/v1/persons/.*/cell-location" kubernetes-health-check-client: - "/health/liveness" - "/health/readiness" diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index dd26ff3b0..43a1485e2 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -31,6 +31,7 @@ authorisation: moj-pes: - "/v1/persons/.*/name" - "/v1/hmpps-id/nomis-number/\\.*+[^/]*$" + - "/v1/persons/.*/cell-location" kubernetes-health-check-client: - "/health/liveness" - "/health/readiness" diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml index 16ea33da6..be3df6016 100644 --- a/src/main/resources/application-test.yml +++ b/src/main/resources/application-test.yml @@ -65,6 +65,7 @@ authorisation: - "/v1/persons/.*/risks/mappadetail" - "/v1/persons/.*/risks/categories" - "/v1/persons/.*/risk-management-plan" + - "/v1/persons/.*/cell-location" - "/v1/persons/.*/status-information" - "/v1/hmpps-id/nomis-number/\\.*+[^/]*$" - "/health" diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/CellLocationControllerTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/CellLocationControllerTest.kt new file mode 100644 index 000000000..f116d765b --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/CellLocationControllerTest.kt @@ -0,0 +1,141 @@ +package uk.gov.justice.digital.hmpps.hmppsintegrationapi.controllers.v1.person + +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain +import org.mockito.Mockito +import org.mockito.internal.verification.VerificationModeFactory +import org.mockito.kotlin.doThrow +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.boot.test.mock.mockito.MockBean +import org.springframework.http.HttpStatus +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.web.servlet.MockMvc +import org.springframework.web.reactive.function.client.WebClientResponseException +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.extensions.removeWhitespaceAndNewlines +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.helpers.IntegrationAPIMockMvc +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.CellLocation +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Response +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApi +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApiError +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.GetCellLocationForPersonService +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.internal.AuditService +import java.net.URLEncoder +import java.nio.charset.StandardCharsets + +@WebMvcTest(controllers = [CellLocationController::class]) +@ActiveProfiles("test") +internal class CellLocationControllerTest( + @Autowired var springMockMvc: MockMvc, + @MockBean val getCellLocationForPersonService: GetCellLocationForPersonService, + @MockBean val auditService: AuditService, +) : DescribeSpec( + { + val hmppsId = "9999/11111A" + val encodedHmppsId = URLEncoder.encode(hmppsId, StandardCharsets.UTF_8) + val path = "/v1/persons/$encodedHmppsId/cell-location" + val mockMvc = IntegrationAPIMockMvc(springMockMvc) + + describe("GET $path") { + beforeTest { + Mockito.reset(getCellLocationForPersonService) + Mockito.reset(auditService) + whenever(getCellLocationForPersonService.execute(hmppsId)).thenReturn( + Response( + data = + CellLocation( + cell = "6-2-006", + prisonCode = "MDI", + prisonName = "Moorland (HMP & YOI)", + ), + ), + ) + } + + it("returns a 200 OK status code") { + val result = mockMvc.performAuthorised(path) + + result.response.status.shouldBe(HttpStatus.OK.value()) + } + + it("gets the cell location for a person with the matching ID") { + mockMvc.performAuthorised(path) + + verify(getCellLocationForPersonService, VerificationModeFactory.times(1)).execute(hmppsId) + } + + it("logs audit") { + mockMvc.performAuthorised(path) + + verify( + auditService, + VerificationModeFactory.times(1), + ).createEvent("GET_PERSON_CELL_LOCATION", mapOf("hmppsId" to hmppsId)) + } + + it("returns the cell location for a person with the matching ID") { + val result = mockMvc.performAuthorised(path) + + result.response.contentAsString.shouldContain( + """ + "data": { + "prisonCode": "MDI", + "prisonName": "Moorland (HMP & YOI)", + "cell": "6-2-006" + } + """.removeWhitespaceAndNewlines(), + ) + } + + it("returns null embedded in a JSON object when no cell location is found") { + val hmppsIdForPersonNotInPrison = "0000/11111A" + val encodedHmppsIdForPersoNotInPrison = + URLEncoder.encode(hmppsIdForPersonNotInPrison, StandardCharsets.UTF_8) + val needsPath = "/v1/persons/$encodedHmppsIdForPersoNotInPrison/cell-location" + + whenever(getCellLocationForPersonService.execute(hmppsIdForPersonNotInPrison)).thenReturn(Response(data = null)) + + val result = mockMvc.performAuthorised(needsPath) + + result.response.contentAsString.shouldContain("\"data\":null") + } + + it("returns a 404 NOT FOUND status code when person isn't found in the upstream API") { + whenever(getCellLocationForPersonService.execute(hmppsId)).thenReturn( + Response( + data = null, + errors = + listOf( + UpstreamApiError( + causedBy = UpstreamApi.PRISONER_OFFENDER_SEARCH, + type = UpstreamApiError.Type.ENTITY_NOT_FOUND, + ), + ), + ), + ) + + val result = mockMvc.performAuthorised(path) + + result.response.status.shouldBe(HttpStatus.NOT_FOUND.value()) + } + + it("fails with the appropriate error when an upstream service is down") { + whenever(getCellLocationForPersonService.execute(hmppsId)).doThrow( + WebClientResponseException(500, "MockError", null, null, null, null), + ) + + val response = mockMvc.performAuthorised(path) + + assert(response.response.status == 500) + assert( + response.response.contentAsString.equals( + "{\"status\":500,\"errorCode\":null,\"userMessage\":\"500 MockError\",\"developerMessage\":\"Unable to complete request as an upstream service is not responding\",\"moreInfo\":null}", + ), + ) + } + } + }, + ) diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/GetCellLocationForPersonServiceTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/GetCellLocationForPersonServiceTest.kt new file mode 100644 index 000000000..ae7ffbfa4 --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/GetCellLocationForPersonServiceTest.kt @@ -0,0 +1,80 @@ +package uk.gov.justice.digital.hmpps.hmppsintegrationapi.services + +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.shouldBe +import org.mockito.Mockito +import org.mockito.internal.verification.VerificationModeFactory +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.springframework.boot.test.context.ConfigDataApplicationContextInitializer +import org.springframework.boot.test.mock.mockito.MockBean +import org.springframework.test.context.ContextConfiguration +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.gateways.PrisonerOffenderSearchGateway +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.CellLocation +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Identifiers +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Person +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Response +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApi +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApiError +import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.prisoneroffendersearch.POSPrisoner + +@ContextConfiguration( + initializers = [ConfigDataApplicationContextInitializer::class], + classes = [GetCellLocationForPersonService::class], +) +internal class GetCellLocationForPersonServiceTest( + @MockBean val getPersonService: GetPersonService, + @MockBean val prisonerOffenderSearchGateway: PrisonerOffenderSearchGateway, + private val getCellLocationForPersonService: GetCellLocationForPersonService, +) : DescribeSpec( + { + val hmppsId = "0000/11111A" + val nomisNumber = "A1234AA" + + beforeEach { + Mockito.reset(getPersonService) + + whenever(getPersonService.execute(hmppsId)).thenReturn( + Response(data = Person(firstName = "Qui-gon", lastName = "Jin", identifiers = Identifiers(nomisNumber = nomisNumber))), + ) + + whenever(prisonerOffenderSearchGateway.getPrisonOffender(nomisNumber)).thenReturn( + Response(data = POSPrisoner(firstName = "Qui-gon", lastName = "Jin", inOutStatus = "IN", prisonId = "MDI", prisonName = "Moorland (HMP & YOI)", cellLocation = "6-2-006")), + ) + } + + it("get cell location for person with hmpps Id") { + getCellLocationForPersonService.execute(hmppsId) + + verify(getPersonService, VerificationModeFactory.times(1)).execute(hmppsId) + } + + it("returns a person cell location") { + val response = getCellLocationForPersonService.execute(hmppsId) + + response.data.shouldBe(CellLocation(cell = "6-2-006", prisonCode = "MDI", prisonName = "Moorland (HMP & YOI)")) + } + + it("returns the upstream error when an error occurs") { + whenever(getPersonService.execute(hmppsId)).thenReturn( + Response( + data = null, + errors = + listOf( + UpstreamApiError( + causedBy = UpstreamApi.PROBATION_OFFENDER_SEARCH, + type = UpstreamApiError.Type.ENTITY_NOT_FOUND, + ), + ), + ), + ) + + val response = getCellLocationForPersonService.execute(hmppsId) + + response.errors.shouldHaveSize(1) + response.errors.first().causedBy.shouldBe(UpstreamApi.PROBATION_OFFENDER_SEARCH) + response.errors.first().type.shouldBe(UpstreamApiError.Type.ENTITY_NOT_FOUND) + } + }, + ) diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/smoke/AuthoriseConfigTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/smoke/AuthoriseConfigTest.kt index f1d279930..f6609c8ed 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/smoke/AuthoriseConfigTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/smoke/AuthoriseConfigTest.kt @@ -45,6 +45,7 @@ class AuthoriseConfigTest : DescribeSpec( "/v1/persons/.*/case-notes", "/v1/persons/.*/person-responsible-officer", "/v1/persons/.*/risk-management-plan", + "/v1/persons/.*/cell-location", "/v1/epf/person-details/.*/\\.*+[^/]*${'$'}", "/v1/hmpps-id/nomis-number/\\.*+[^/]*${'$'}", "/health", diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/smoke/person/PersonSmokeTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/smoke/person/PersonSmokeTest.kt index 7ff350d42..5937ca2cc 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/smoke/person/PersonSmokeTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/smoke/person/PersonSmokeTest.kt @@ -219,5 +219,19 @@ class PersonSmokeTest : DescribeSpec( """.removeWhitespaceAndNewlines(), ) } + + it("returns person cell location if in prison") { + val response = httpClient.performAuthorised("$basePath/$encodedHmppsId/cell-location") + + response.statusCode().shouldBe(HttpStatus.OK.value()) + response.body().shouldContain("\"data\":{") + response.body().shouldContain( + """ + "prisonCode":"MDI", + "prisonName":"HMP Leeds", + "cell":"A-1-002" + """.removeWhitespaceAndNewlines(), + ) + } }, )