Skip to content

Commit b0dd56c

Browse files
authored
Ground work: prison controller and single prisoner details endpoint (#532)
* Ground work: Additional prison controller and extended getPersonService with query for prisoner excluding probation people * correcting getPersonService unit test * updating prison controller as per pr comments
1 parent b8f4702 commit b0dd56c

File tree

13 files changed

+337
-25
lines changed

13 files changed

+337
-25
lines changed

src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/PersonController.kt

+2-3
Original file line numberDiff line numberDiff line change
@@ -157,12 +157,11 @@ class PersonController(
157157
return DataResponse(response.data)
158158
}
159159

160-
private fun isValidISODateFormat(dateString: String): Boolean {
161-
return try {
160+
private fun isValidISODateFormat(dateString: String): Boolean =
161+
try {
162162
LocalDate.parse(dateString, DateTimeFormatter.ISO_DATE)
163163
true
164164
} catch (e: Exception) {
165165
false
166166
}
167-
}
168167
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package uk.gov.justice.digital.hmpps.hmppsintegrationapi.controllers.v1.prison
2+
3+
import io.swagger.v3.oas.annotations.Operation
4+
import io.swagger.v3.oas.annotations.Parameter
5+
import io.swagger.v3.oas.annotations.media.Content
6+
import io.swagger.v3.oas.annotations.media.Schema
7+
import io.swagger.v3.oas.annotations.responses.ApiResponse
8+
import io.swagger.v3.oas.annotations.tags.Tag
9+
import org.springframework.beans.factory.annotation.Autowired
10+
import org.springframework.web.bind.annotation.GetMapping
11+
import org.springframework.web.bind.annotation.PathVariable
12+
import org.springframework.web.bind.annotation.RequestMapping
13+
import org.springframework.web.bind.annotation.RestController
14+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.exception.EntityNotFoundException
15+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.extensions.decodeUrlCharacters
16+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.DataResponse
17+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Person
18+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApi
19+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApiError.Type.ENTITY_NOT_FOUND
20+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.GetPersonService
21+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.internal.AuditService
22+
23+
@RestController
24+
@RequestMapping("/v1/prison")
25+
@Tag(name = "prison")
26+
class PrisonController(
27+
@Autowired val getPersonService: GetPersonService,
28+
@Autowired val auditService: AuditService,
29+
) {
30+
@GetMapping("/prisoners/{hmppsId}")
31+
@Operation(
32+
summary = "Returns a single prisoners details given an hmppsId, does not query for a probation person.",
33+
responses = [
34+
ApiResponse(responseCode = "200", useReturnTypeSchema = true, description = "Successfully found a prisoner with the provided HMPPS ID."),
35+
ApiResponse(responseCode = "404", content = [Content(schema = Schema(ref = "#/components/schemas/PersonNotFound"))]),
36+
ApiResponse(responseCode = "500", content = [Content(schema = Schema(ref = "#/components/schemas/InternalServerError"))]),
37+
],
38+
)
39+
fun getPerson(
40+
@Parameter(description = "A HMPPS identifier", example = "2008%2F0545166T", required = true) @PathVariable hmppsId: String,
41+
): DataResponse<Person?> {
42+
val decodedHmppsId = hmppsId.decodeUrlCharacters()
43+
44+
val response = getPersonService.getPrisoner(decodedHmppsId)
45+
46+
if (response.hasErrorCausedBy(ENTITY_NOT_FOUND, causedBy = UpstreamApi.PROBATION_OFFENDER_SEARCH)) {
47+
throw EntityNotFoundException("Could not find person with hmppsId: $decodedHmppsId")
48+
}
49+
50+
auditService.createEvent("GET_PERSON_DETAILS", mapOf("hmppsId" to decodedHmppsId))
51+
val data = response.data
52+
return DataResponse(data)
53+
}
54+
}

src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/GetPersonService.kt

+44-6
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ class GetPersonService(
4444
* When it is a noms number then return it.
4545
* When it is a CRN look up the prisoner in probation offender search and then return it
4646
*/
47-
fun getNomisNumber(hmppsId: String): Response<NomisNumber?> {
48-
return when (identifyHmppsId(hmppsId)) {
47+
fun getNomisNumber(hmppsId: String): Response<NomisNumber?> =
48+
when (identifyHmppsId(hmppsId)) {
4949
IdentifierType.NOMS -> Response(data = NomisNumber(hmppsId))
5050

5151
IdentifierType.CRN -> {
@@ -82,7 +82,6 @@ class GetPersonService(
8282
),
8383
)
8484
}
85-
}
8685

8786
fun getCombinedDataForPerson(hmppsId: String): Response<OffenderSearchResponse> {
8887
val probationResponse = probationOffenderSearchGateway.getPerson(id = hmppsId)
@@ -105,8 +104,47 @@ class GetPersonService(
105104
}
106105

107106
fun getPersonFromNomis(nomisNumber: String) = prisonerOffenderSearchGateway.getPrisonOffender(nomisNumber)
108-
}
109107

110-
fun isNomsNumber(id: String?): Boolean {
111-
return id?.matches(Regex("^[A-Z]\\d{4}[A-Z]{2}+$")) == true
108+
fun getPrisoner(hmppsId: String): Response<Person?> {
109+
val prisonerNomisNumber = getNomisNumber(hmppsId)
110+
111+
if (prisonerNomisNumber.errors.isNotEmpty()) {
112+
return Response(
113+
data = null,
114+
errors = prisonerNomisNumber.errors,
115+
)
116+
}
117+
118+
val nomisNumber = prisonerNomisNumber.data?.nomisNumber
119+
120+
val prisonResponse =
121+
try {
122+
getPersonFromNomis(nomisNumber!!)
123+
} catch (e: RuntimeException) {
124+
if (nomisNumber == null) {
125+
return Response(
126+
data = null,
127+
errors = prisonerNomisNumber.errors,
128+
)
129+
}
130+
return Response(
131+
data = null,
132+
errors = listOf(UpstreamApiError(description = e.message ?: "Service error", type = UpstreamApiError.Type.INTERNAL_SERVER_ERROR, causedBy = UpstreamApi.PRISONER_OFFENDER_SEARCH)),
133+
)
134+
}
135+
136+
if (prisonResponse.errors.isNotEmpty()) {
137+
return Response(
138+
data = null,
139+
errors = prisonResponse.errors,
140+
)
141+
}
142+
143+
return Response(
144+
data = prisonResponse.data?.toPerson(),
145+
errors = prisonResponse.errors,
146+
)
147+
}
112148
}
149+
150+
fun isNomsNumber(id: String?): Boolean = id?.matches(Regex("^[A-Z]\\d{4}[A-Z]{2}+$")) == true

src/main/resources/application-dev.yml

+1
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ authorisation:
108108
- "/v1/persons/.*/sentences/latest-key-dates-and-adjustments"
109109
- "/v1/persons/.*/status-information"
110110
- "/v1/persons/[^/]*$"
111+
- "/v1/prison/prisoners/[^/]*$"
111112
kilco:
112113
- "/v1/persons"
113114
- "/v1/persons/[^/]*$"

src/main/resources/application-integration-test.yml

+1
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ authorisation:
7979
- "/health/readiness"
8080
- "/health/liveness"
8181
- "/info"
82+
- "/v1/prison/prisoners/[^/]*$"
8283
config-test:
8384
- "/v1/config/authorisation"
8485
all-access:

src/main/resources/application-local-docker.yml

+1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ authorisation:
5353
- "/health/readiness"
5454
- "/health/liveness"
5555
- "/info"
56+
- "/v1/prison/prisoners/[^/]*$"
5657
config-test:
5758
- "/v1/config/authorisation"
5859
all-access:

src/main/resources/application-local.yml

+1
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ authorisation:
6161
- "/health/liveness"
6262
- "/info"
6363
- "/v1/hmpps/reference-data"
64+
- "/v1/prison/prisoners/[^/]*$"
6465
config-test:
6566
- "/v1/config/authorisation"
6667
all-access:

src/main/resources/application-test.yml

+1
Original file line numberDiff line numberDiff line change
@@ -81,5 +81,6 @@ authorisation:
8181
- "/health/liveness"
8282
- "/info"
8383
- "/v1/hmpps/reference-data"
84+
- "/v1/prison/prisoners/[^/]*$"
8485
config-test:
8586
- "/v1/config/authorisation"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package uk.gov.justice.digital.hmpps.hmppsintegrationapi.controllers.v1.prison
2+
3+
import io.kotest.core.spec.style.DescribeSpec
4+
import io.kotest.matchers.shouldBe
5+
import org.mockito.Mockito
6+
import org.mockito.kotlin.times
7+
import org.mockito.kotlin.verify
8+
import org.mockito.kotlin.whenever
9+
import org.springframework.beans.factory.annotation.Autowired
10+
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
11+
import org.springframework.test.context.ActiveProfiles
12+
import org.springframework.test.context.bean.override.mockito.MockitoBean
13+
import org.springframework.test.web.servlet.MockMvc
14+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.extensions.removeWhitespaceAndNewlines
15+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.helpers.IntegrationAPIMockMvc
16+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Person
17+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Response
18+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApi
19+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApiError
20+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApiError.Type.ENTITY_NOT_FOUND
21+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.GetPersonService
22+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.internal.AuditService
23+
import java.time.LocalDate
24+
25+
@WebMvcTest(controllers = [PrisonController::class])
26+
@ActiveProfiles("test")
27+
internal class PrisonControllerTest(
28+
@Autowired var springMockMvc: MockMvc,
29+
@MockitoBean val getPersonService: GetPersonService,
30+
@MockitoBean val auditService: AuditService,
31+
) : DescribeSpec({
32+
val hmppsId = "200313116M"
33+
val basePath = "/v1/prison"
34+
val mockMvc = IntegrationAPIMockMvc(springMockMvc)
35+
36+
describe("GET $basePath") {
37+
}
38+
39+
afterTest {
40+
Mockito.reset(getPersonService)
41+
Mockito.reset(auditService)
42+
}
43+
44+
it("returns 500 when service throws an exception") {
45+
whenever(getPersonService.getPrisoner(hmppsId)).thenThrow(RuntimeException("Service error"))
46+
47+
val result = mockMvc.performAuthorised("$basePath/prisoners/$hmppsId")
48+
49+
result.response.status.shouldBe(500)
50+
}
51+
52+
it("returns a person with all fields populated") {
53+
whenever(getPersonService.getPrisoner(hmppsId)).thenReturn(
54+
Response(
55+
data =
56+
Person(
57+
firstName = "Barry",
58+
lastName = "Allen",
59+
middleName = "Jonas",
60+
dateOfBirth = LocalDate.parse("2023-03-01"),
61+
gender = "Male",
62+
ethnicity = "Caucasian",
63+
pncId = "PNC123456",
64+
),
65+
),
66+
)
67+
68+
val result = mockMvc.performAuthorised("$basePath/prisoners/$hmppsId")
69+
70+
result.response.contentAsString.shouldBe(
71+
"""
72+
{
73+
"data":{
74+
"firstName":"Barry",
75+
"lastName":"Allen",
76+
"middleName":"Jonas",
77+
"dateOfBirth":"2023-03-01",
78+
"gender":"Male",
79+
"ethnicity":"Caucasian",
80+
"aliases":[],
81+
"identifiers":{
82+
"nomisNumber":null,
83+
"croNumber":null,
84+
"deliusCrn":null
85+
},
86+
"pncId": "PNC123456",
87+
"hmppsId": null,
88+
"contactDetails": null
89+
}
90+
}
91+
""".removeWhitespaceAndNewlines(),
92+
)
93+
}
94+
95+
it("logs audit event") {
96+
whenever(getPersonService.getPrisoner(hmppsId)).thenReturn(
97+
Response(
98+
data =
99+
Person(
100+
firstName = "Barry",
101+
lastName = "Allen",
102+
middleName = "Jonas",
103+
dateOfBirth = LocalDate.parse("2023-03-01"),
104+
gender = "Male",
105+
ethnicity = "Caucasian",
106+
pncId = "PNC123456",
107+
),
108+
),
109+
)
110+
111+
mockMvc.performAuthorised("$basePath/prisoners/$hmppsId")
112+
verify(
113+
auditService,
114+
times(1),
115+
).createEvent(
116+
"GET_PERSON_DETAILS",
117+
mapOf("hmppsId" to hmppsId),
118+
)
119+
}
120+
121+
it("returns 404 when prisoner is not found") {
122+
whenever(getPersonService.getPrisoner(hmppsId)).thenReturn(
123+
Response(
124+
data = null,
125+
errors =
126+
listOf(
127+
UpstreamApiError(
128+
type = ENTITY_NOT_FOUND,
129+
causedBy = UpstreamApi.PROBATION_OFFENDER_SEARCH,
130+
description = "Prisoner not found",
131+
),
132+
),
133+
),
134+
)
135+
136+
val result = mockMvc.performAuthorised("$basePath/prisoners/$hmppsId")
137+
138+
result.response.status.shouldBe(404)
139+
}
140+
})

src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/helpers/IntegrationAPIMockMvc.kt

+1-3
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,5 @@ class IntegrationAPIMockMvc(
2323
return mockMvc.perform(MockMvcRequestBuilders.get(path).header("subject-distinguished-name", subjectDistinguishedName)).andReturn()
2424
}
2525

26-
fun performUnAuthorised(path: String): MvcResult {
27-
return mockMvc.perform(MockMvcRequestBuilders.get(path)).andReturn()
28-
}
26+
fun performUnAuthorised(path: String): MvcResult = mockMvc.perform(MockMvcRequestBuilders.get(path)).andReturn()
2927
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package uk.gov.justice.digital.hmpps.hmppsintegrationapi.integration.prison
2+
3+
import org.junit.jupiter.api.Test
4+
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content
5+
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
6+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.integration.IntegrationTestBase
7+
8+
class PrisonIntegrationTest : IntegrationTestBase() {
9+
private final val hmppsId = "G2996UX"
10+
private final val basePrisonPath = "/v1/prison"
11+
12+
@Test
13+
fun `return a prisoner with all fields populated`() {
14+
callApi("$basePrisonPath/prisoners/$hmppsId")
15+
.andExpect(status().isOk)
16+
.andExpect(content().json(getExpectedResponse("prisoner-response")))
17+
}
18+
}

0 commit comments

Comments
 (0)