Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pes add cell location endpoint #459

Merged
merged 3 commits into from
Jul 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<String, CellLocation?> {
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)
}
}
Original file line number Diff line number Diff line change
@@ -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,
)
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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<CellLocation?> {
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()),
)
}
}
1 change: 1 addition & 0 deletions src/main/resources/application-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/application-local-docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/application-local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/application-preprod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/application-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/application-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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}",
),
)
}
}
},
)
Original file line number Diff line number Diff line change
@@ -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)
}
},
)
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
)
}
},
)
Loading