Skip to content

Commit 79a14ed

Browse files
Merge pull request #383 from ministryofjustice/HIA-665
Thin Slice for Risk Categories
2 parents 251cdb6 + 80551a9 commit 79a14ed

File tree

13 files changed

+459
-21
lines changed

13 files changed

+459
-21
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package uk.gov.justice.digital.hmpps.hmppsintegrationapi.controllers.v1.person
2+
3+
import org.springframework.beans.factory.annotation.Autowired
4+
import org.springframework.web.bind.annotation.GetMapping
5+
import org.springframework.web.bind.annotation.PathVariable
6+
import org.springframework.web.bind.annotation.RequestMapping
7+
import org.springframework.web.bind.annotation.RestController
8+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.exception.EntityNotFoundException
9+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.extensions.decodeUrlCharacters
10+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.RiskCategory
11+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApiError
12+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.GetRiskCategoriesForPersonService
13+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.internal.AuditService
14+
15+
@RestController
16+
@RequestMapping("/v1/persons")
17+
class RiskCategoriesController(
18+
@Autowired val getRiskCategoriesForPersonService: GetRiskCategoriesForPersonService,
19+
@Autowired val auditService: AuditService,
20+
) {
21+
22+
@GetMapping("{encodedHmppsId}/risks/categories")
23+
fun getPersonRiskCategories(
24+
@PathVariable encodedHmppsId: String,
25+
26+
): Map<String, RiskCategory?> {
27+
val hmppsId = encodedHmppsId.decodeUrlCharacters()
28+
val response = getRiskCategoriesForPersonService.execute(hmppsId)
29+
30+
if (response.hasError(UpstreamApiError.Type.ENTITY_NOT_FOUND)) {
31+
throw EntityNotFoundException("Could not find person with id: $hmppsId")
32+
}
33+
auditService.createEvent("GET_PERSON_RISK_CATEGORIES", "Person risk categories with hmpps id: $hmppsId has been retrieved")
34+
return mapOf("data" to response.data)
35+
}
36+
}

src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/gateways/NomisGateway.kt

+34
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Alert
1111
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.ImageMetadata
1212
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Offence
1313
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Response
14+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.RiskCategory
1415
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Sentence
1516
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.SentenceAdjustment
1617
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.SentenceKeyDates
@@ -19,6 +20,7 @@ import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.nomis.NomisAddres
1920
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.nomis.NomisAlert
2021
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.nomis.NomisBooking
2122
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.nomis.NomisImageDetail
23+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.nomis.NomisInmateDetail
2224
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.nomis.NomisOffenceHistoryDetail
2325
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.nomis.NomisOffenderSentence
2426
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.nomis.NomisSentence
@@ -215,11 +217,43 @@ class NomisGateway(@Value("\${services.prison-api.base-url}") baseUrl: String) {
215217
}
216218
}
217219

220+
fun getRiskCategoriesForPerson(id: String): Response<RiskCategory?> {
221+
val result = webClient.request<NomisInmateDetail>(
222+
HttpMethod.GET,
223+
"/api/offenders/$id",
224+
authenticationHeaderForCategories(),
225+
UpstreamApi.NOMIS,
226+
)
227+
228+
return when (result) {
229+
is WebClientWrapperResponse.Success -> {
230+
Response(data = result.data.toRiskCategory())
231+
}
232+
233+
is WebClientWrapperResponse.Error -> {
234+
Response(
235+
data = RiskCategory(),
236+
errors = result.errors,
237+
)
238+
}
239+
}
240+
}
241+
218242
private fun authenticationHeader(): Map<String, String> {
219243
val token = hmppsAuthGateway.getClientToken("NOMIS")
220244

221245
return mapOf(
222246
"Authorization" to "Bearer $token",
223247
)
224248
}
249+
250+
private fun authenticationHeaderForCategories(): Map<String, String> {
251+
val token = hmppsAuthGateway.getClientToken("NOMIS")
252+
val version = "1.0"
253+
254+
return mapOf(
255+
"Authorization" to "Bearer $token",
256+
"version" to version,
257+
)
258+
}
225259
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps
2+
3+
data class RiskAssessment(
4+
val classificationCode: String?,
5+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps
2+
3+
data class RiskCategory(
4+
val offenderNo: String? = null,
5+
val assessments: List<RiskAssessment> = emptyList(),
6+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.nomis
2+
3+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.RiskAssessment
4+
5+
data class NomisAssessment(
6+
val classificationCode: String? = null,
7+
) {
8+
fun toRiskAssessment() = RiskAssessment(
9+
classificationCode = this.classificationCode,
10+
)
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.nomis
2+
3+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.RiskCategory
4+
5+
data class NomisInmateDetail(
6+
val offenderNo: String? = null,
7+
val assessments: List<NomisAssessment> = emptyList(),
8+
) {
9+
fun toRiskCategory(): RiskCategory = RiskCategory(
10+
offenderNo = this.offenderNo,
11+
assessments = this.assessments.map { it.toRiskAssessment() },
12+
)
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package uk.gov.justice.digital.hmpps.hmppsintegrationapi.services
2+
3+
import org.springframework.beans.factory.annotation.Autowired
4+
import org.springframework.stereotype.Service
5+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.gateways.NomisGateway
6+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Response
7+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.RiskCategory
8+
9+
@Service
10+
class GetRiskCategoriesForPersonService(
11+
@Autowired val nomisGateway: NomisGateway,
12+
@Autowired val getPersonService: GetPersonService,
13+
) {
14+
fun execute(hmppsId: String): Response<RiskCategory?> {
15+
val personResponse = getPersonService.execute(hmppsId = hmppsId)
16+
val nomisNumber = personResponse.data?.identifiers?.nomisNumber
17+
18+
var personRiskCategories: Response<RiskCategory?> = Response(data = RiskCategory())
19+
20+
if (nomisNumber != null) {
21+
personRiskCategories = nomisGateway.getRiskCategoriesForPerson(id = nomisNumber)
22+
}
23+
24+
return Response(
25+
data = personRiskCategories.data,
26+
errors = personResponse.errors + personRiskCategories.errors,
27+
)
28+
}
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package uk.gov.justice.digital.hmpps.hmppsintegrationapi.controllers.v1.person
2+
3+
import io.kotest.core.spec.style.DescribeSpec
4+
import io.kotest.matchers.shouldBe
5+
import io.kotest.matchers.string.shouldContain
6+
import org.mockito.Mockito
7+
import org.mockito.internal.verification.VerificationModeFactory
8+
import org.mockito.kotlin.verify
9+
import org.mockito.kotlin.whenever
10+
import org.springframework.beans.factory.annotation.Autowired
11+
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
12+
import org.springframework.boot.test.mock.mockito.MockBean
13+
import org.springframework.http.HttpStatus
14+
import org.springframework.test.context.ActiveProfiles
15+
import org.springframework.test.web.servlet.MockMvc
16+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.extensions.removeWhitespaceAndNewlines
17+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.helpers.IntegrationAPIMockMvc
18+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Response
19+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.RiskAssessment
20+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.RiskCategory
21+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApi
22+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApiError
23+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.GetRiskCategoriesForPersonService
24+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.internal.AuditService
25+
import java.net.URLEncoder
26+
import java.nio.charset.StandardCharsets
27+
28+
@WebMvcTest(controllers = [RiskCategoriesController::class])
29+
@ActiveProfiles("test")
30+
internal class RiskCategoriesControllerTest(
31+
@Autowired var springMockMvc: MockMvc,
32+
@MockBean val getRiskCategoriesForPersonService: GetRiskCategoriesForPersonService,
33+
@MockBean val auditService: AuditService,
34+
) : DescribeSpec(
35+
{
36+
val hmppsId = "9999/11111A"
37+
val encodedHmppsId = URLEncoder.encode(hmppsId, StandardCharsets.UTF_8)
38+
val path = "/v1/persons/$encodedHmppsId/risks/categories"
39+
val mockMvc = IntegrationAPIMockMvc(springMockMvc)
40+
41+
describe("GET $path") {
42+
beforeTest {
43+
Mockito.reset(getRiskCategoriesForPersonService)
44+
whenever(getRiskCategoriesForPersonService.execute(hmppsId)).thenReturn(
45+
Response(
46+
data = RiskCategory(
47+
offenderNo = "A1234AA",
48+
assessments = listOf(RiskAssessment(classificationCode = "C")),
49+
),
50+
),
51+
)
52+
53+
Mockito.reset(auditService)
54+
}
55+
56+
it("returns a 200 OK status code") {
57+
val result = mockMvc.performAuthorised(path)
58+
59+
result.response.status.shouldBe(HttpStatus.OK.value())
60+
}
61+
62+
it("gets the risk categories for a person with the matching ID") {
63+
mockMvc.performAuthorised(path)
64+
verify(getRiskCategoriesForPersonService, VerificationModeFactory.times(1)).execute(hmppsId)
65+
}
66+
67+
it("logs audit") {
68+
mockMvc.performAuthorised(path)
69+
70+
verify(auditService, VerificationModeFactory.times(1)).createEvent("GET_PERSON_RISK_CATEGORIES", "Person risk categories with hmpps id: $hmppsId has been retrieved")
71+
}
72+
73+
it("returns the risk categories for a person with the matching ID") {
74+
val result = mockMvc.performAuthorised(path)
75+
76+
result.response.contentAsString.shouldContain(
77+
"""
78+
"data": {
79+
"offenderNo": "A1234AA",
80+
"assessments": [
81+
{
82+
"classificationCode": "C"
83+
}
84+
]
85+
}
86+
""".removeWhitespaceAndNewlines(),
87+
)
88+
}
89+
90+
it("returns a 404 NOT FOUND status code when person isn't found in the upstream API") {
91+
whenever(getRiskCategoriesForPersonService.execute(hmppsId)).thenReturn(
92+
Response(
93+
data = RiskCategory(),
94+
errors = listOf(
95+
UpstreamApiError(
96+
causedBy = UpstreamApi.NOMIS,
97+
type = UpstreamApiError.Type.ENTITY_NOT_FOUND,
98+
),
99+
),
100+
),
101+
)
102+
103+
val result = mockMvc.performAuthorised(path)
104+
105+
result.response.status.shouldBe(HttpStatus.NOT_FOUND.value())
106+
}
107+
}
108+
},
109+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package uk.gov.justice.digital.hmpps.hmppsintegrationapi.gateways.nomis
2+
3+
import io.kotest.core.spec.style.DescribeSpec
4+
import io.kotest.matchers.collections.shouldHaveSize
5+
import io.kotest.matchers.shouldBe
6+
import org.mockito.Mockito
7+
import org.mockito.internal.verification.VerificationModeFactory
8+
import org.mockito.kotlin.verify
9+
import org.mockito.kotlin.whenever
10+
import org.springframework.boot.test.context.ConfigDataApplicationContextInitializer
11+
import org.springframework.boot.test.mock.mockito.MockBean
12+
import org.springframework.http.HttpStatus
13+
import org.springframework.test.context.ActiveProfiles
14+
import org.springframework.test.context.ContextConfiguration
15+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.extensions.removeWhitespaceAndNewlines
16+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.gateways.HmppsAuthGateway
17+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.gateways.NomisGateway
18+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.mockservers.HmppsAuthMockServer
19+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.mockservers.NomisApiMockServer
20+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApi
21+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApiError
22+
23+
@ActiveProfiles("test")
24+
@ContextConfiguration(
25+
initializers = [ConfigDataApplicationContextInitializer::class],
26+
classes = [NomisGateway::class],
27+
)
28+
class GetRiskCategoriesForPersonTest(
29+
@MockBean val hmppsAuthGateway: HmppsAuthGateway,
30+
val nomisGateway: NomisGateway,
31+
) :
32+
DescribeSpec(
33+
{
34+
val nomisApiMockServer = NomisApiMockServer()
35+
val offenderNo = "A7796DY"
36+
37+
beforeEach {
38+
nomisApiMockServer.start()
39+
nomisApiMockServer.stubGetRiskCategoriesForPerson(
40+
offenderNo,
41+
"""
42+
{
43+
"offenderNo": "A7796DY",
44+
"assessments": [
45+
{
46+
"classificationCode": "C"
47+
}
48+
]
49+
}
50+
""".removeWhitespaceAndNewlines(),
51+
)
52+
53+
Mockito.reset(hmppsAuthGateway)
54+
whenever(hmppsAuthGateway.getClientToken("NOMIS")).thenReturn(HmppsAuthMockServer.TOKEN)
55+
}
56+
57+
afterTest {
58+
nomisApiMockServer.stop()
59+
}
60+
61+
it("authenticates using HMPPS Auth with credentials") {
62+
nomisGateway.getRiskCategoriesForPerson(offenderNo)
63+
64+
verify(hmppsAuthGateway, VerificationModeFactory.times(1)).getClientToken("NOMIS")
65+
}
66+
67+
it("returns an error when 404 Not Found is returned because no person is found") {
68+
nomisApiMockServer.stubGetRiskCategoriesForPerson(offenderNo, "", HttpStatus.NOT_FOUND)
69+
70+
val response = nomisGateway.getRiskCategoriesForPerson(offenderNo)
71+
72+
response.errors.shouldHaveSize(1)
73+
response.errors.first().causedBy.shouldBe(UpstreamApi.NOMIS)
74+
response.errors.first().type.shouldBe(UpstreamApiError.Type.ENTITY_NOT_FOUND)
75+
}
76+
},
77+
)

src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/mockservers/NomisApiMockServer.kt

+14-15
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,6 @@ class NomisApiMockServer : WireMockServer(WIREMOCK_PORT) {
1111
private const val WIREMOCK_PORT = 4000
1212
}
1313

14-
fun stubGetOffender(offenderNo: String, body: String, status: HttpStatus = HttpStatus.OK) {
15-
stubFor(
16-
get("/api/offenders/$offenderNo")
17-
.withHeader(
18-
"Authorization",
19-
matching("Bearer ${HmppsAuthMockServer.TOKEN}"),
20-
).willReturn(
21-
aResponse()
22-
.withHeader("Content-Type", "application/json")
23-
.withStatus(status.value())
24-
.withBody(body.trimIndent()),
25-
),
26-
)
27-
}
28-
2914
fun stubGetOffenderImageDetails(offenderNo: String, body: String, status: HttpStatus = HttpStatus.OK) {
3015
stubFor(
3116
get("/api/images/offenders/$offenderNo")
@@ -160,4 +145,18 @@ class NomisApiMockServer : WireMockServer(WIREMOCK_PORT) {
160145
),
161146
)
162147
}
148+
149+
fun stubGetRiskCategoriesForPerson(offenderNo: String, body: String, status: HttpStatus = HttpStatus.OK) {
150+
stubFor(
151+
get("/api/offenders/$offenderNo")
152+
.withHeader("Authorization", matching("Bearer ${HmppsAuthMockServer.TOKEN}"))
153+
.withHeader("version", matching("1.0"))
154+
.willReturn(
155+
aResponse()
156+
.withHeader("Content-Type", "application/json")
157+
.withStatus(status.value())
158+
.withBody(body.trimIndent()),
159+
),
160+
)
161+
}
163162
}

0 commit comments

Comments
 (0)