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

Dynamic risks endpoint for PND #446

Merged
merged 16 commits into from
Jun 20, 2024
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
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.RequestParam
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.DynamicRisk
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApiError
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.GetDynamicRisksForPersonService
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.internal.AuditService
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.util.PaginatedResponse
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.util.paginateWith

@RestController
@RequestMapping("/v1/persons")
class DynamicRisksController(
@Autowired val getDynamicRisksForPersonService: GetDynamicRisksForPersonService,
@Autowired val auditService: AuditService,
) {
@GetMapping("{encodedHmppsId}/risks/dynamic")
fun getDynamicRisks(
@PathVariable encodedHmppsId: String,
@RequestParam(required = false, defaultValue = "1", name = "page") page: Int,
@RequestParam(required = false, defaultValue = "10", name = "perPage") perPage: Int,
): PaginatedResponse<DynamicRisk> {
val hmppsId = encodedHmppsId.decodeUrlCharacters()
val response = getDynamicRisksForPersonService.execute(hmppsId)

if (response.hasError(UpstreamApiError.Type.ENTITY_NOT_FOUND)) {
throw EntityNotFoundException("Could not find person with id: $hmppsId")
}
auditService.createEvent("GET_DYNAMIC_RISKS", mapOf("hmppsId" to hmppsId))
return response.data.paginateWith(page, perPage)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import org.springframework.stereotype.Component
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.extensions.WebClientWrapper
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.extensions.WebClientWrapper.WebClientWrapperResponse
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.CommunityOffenderManager
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.DynamicRisk
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.MappaDetail
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Offence
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Response
Expand Down Expand Up @@ -69,6 +70,29 @@ class NDeliusGateway(
}
}

fun getDynamicRisksForPerson(id: String): Response<List<DynamicRisk>> {
val result =
webClient.request<NDeliusSupervisions>(
HttpMethod.GET,
"/case/$id/supervisions",
authenticationHeader(),
UpstreamApi.NDELIUS,
)

return when (result) {
is WebClientWrapperResponse.Success -> {
Response(data = result.data.dynamicRisks.map { it.toDynamicRisk() })
}

is WebClientWrapperResponse.Error -> {
Response(
data = emptyList(),
errors = result.errors,
)
}
}
}

fun getMappaDetailForPerson(id: String): Response<MappaDetail?> {
val result =
webClient.request<NDeliusSupervisions>(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps

data class DynamicRisk(
val code: String? = null,
val description: String? = null,
val startDate: String? = null,
val reviewDate: String? = null,
val notes: String? = null,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.ndelius

import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.DynamicRisk

data class NDeliusDynamicRisk(
val code: String? = null,
val description: String? = null,
val startDate: String? = null,
val reviewDate: String? = null,
val notes: String? = null,
) {
fun toDynamicRisk(): DynamicRisk =
DynamicRisk(
code = this.code,
description = this.description,
startDate = this.startDate,
reviewDate = this.reviewDate,
notes = this.notes,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ data class NDeliusSupervisions(
val communityManager: NDeliusCommunityManager,
val mappaDetail: NDeliusMappaDetail? = null,
val supervisions: List<NDeliusSupervision>,
val dynamicRisks: List<NDeliusDynamicRisk>,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
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.NDeliusGateway
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.DynamicRisk
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Response

@Service
class GetDynamicRisksForPersonService(
@Autowired val nDeliusGateway: NDeliusGateway,
@Autowired val getPersonService: GetPersonService,
) {
fun execute(hmppsId: String): Response<List<DynamicRisk>> {
val personResponse = getPersonService.execute(hmppsId = hmppsId)
val deliusCrn = personResponse.data?.identifiers?.deliusCrn

var nDeliusDynamicRisks: Response<List<DynamicRisk>> = Response(data = emptyList())

if (deliusCrn != null) {
val allNDeliusDynamicRisks = nDeliusGateway.getDynamicRisksForPerson(deliusCrn)
val filteredNDeliusDynamicRisks =
allNDeliusDynamicRisks.data.filter {
it.code in
listOf(
"RCCO", "RCPR", "REG22", "RVLN", "ALT8", "STRG", "AVIS", "ALT1", "WEAP",
"RHRH", "RLRH", "RMRH", "RVHR", "RCHD", "REG15", "REG16", "REG17",
"ALT4", "AVS2", "ALT7", "ALSH",
)
}
nDeliusDynamicRisks = Response(data = filteredNDeliusDynamicRisks, errors = allNDeliusDynamicRisks.errors)
}

return Response(
data = nDeliusDynamicRisks.data,
errors = personResponse.errors + nDeliusDynamicRisks.errors,
)
}
}
1 change: 1 addition & 0 deletions src/main/resources/application-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ authorisation:
- "/v1/persons/.*/sentences/latest-key-dates-and-adjustments"
- "/v1/persons/.*/risks/scores"
- "/v1/persons/.*/risks/serious-harm"
- "/v1/persons/.*/risks/dynamic"
- "/v1/persons/.*/licences/conditions"
- "/v1/persons/.*/person-responsible-officer"
event-service:
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 @@ -44,6 +44,7 @@ authorisation:
- "/v1/persons/.*/protected-characteristics"
- "/v1/persons/.*/risks/mappadetail"
- "/v1/persons/.*/risks/categories"
- "/v1/persons/.*/risks/dynamic"
- "/v1/persons/.*/case-notes"
- "/v1/persons/.*/person-responsible-officer"
- "/v1/persons/.*/risk-management-plan"
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 @@ -45,6 +45,7 @@ authorisation:
- "/v1/persons/.*/risks/scores"
- "/v1/persons/.*/needs"
- "/v1/persons/.*/risks/serious-harm"
- "/v1/persons/.*/risks/dynamic"
- "/v1/persons/.*/reported-adjudications"
- "/v1/persons/.*/adjudications"
- "/v1/persons/.*/licences/conditions"
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 @@ -53,6 +53,7 @@ authorisation:
- "/v1/persons/.*/risks/scores"
- "/v1/persons/.*/needs"
- "/v1/persons/.*/risks/serious-harm"
- "/v1/persons/.*/risks/dynamic"
- "/v1/persons/.*/reported-adjudications"
- "/v1/epf/person-details/.*/\\.*+[^/]*$"
- "/v1/persons/.*/adjudications"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
package uk.gov.justice.digital.hmpps.hmppsintegrationapi.controllers.v1.person

import io.kotest.assertions.json.shouldContainJsonKeyValue
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.DynamicRisk
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.GetDynamicRisksForPersonService
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.internal.AuditService
import java.net.URLEncoder
import java.nio.charset.StandardCharsets

@WebMvcTest(controllers = [DynamicRisksController::class])
@ActiveProfiles("test")
internal class DynamicRisksControllerTest(
@Autowired var springMockMvc: MockMvc,
@MockBean val getDynamicRisksForPersonService: GetDynamicRisksForPersonService,
@MockBean val auditService: AuditService,
) : DescribeSpec(
{
val hmppsId = "9999/11111A"
val encodedHmppsId = URLEncoder.encode(hmppsId, StandardCharsets.UTF_8)
val path = "/v1/persons/$encodedHmppsId/risks/dynamic"
val mockMvc = IntegrationAPIMockMvc(springMockMvc)

describe("GET $path") {
beforeTest {
Mockito.reset(getDynamicRisksForPersonService)
Mockito.reset(auditService)
whenever(getDynamicRisksForPersonService.execute(hmppsId)).thenReturn(
Response(
data =
listOf(
DynamicRisk(
code = "AVIS",
description = "Subject has a ViSOR record",
startDate = "2023-09-08",
reviewDate = "2026-04-29",
notes = "Nothing to say",
),
DynamicRisk(
code = "RHRH",
description = "High Risk of Harm",
startDate = "2022-09-01",
reviewDate = "2024-12-23",
notes = "A lot of notes",
),
),
),
)
}

it("returns a 200 OK status code") {
val result = mockMvc.performAuthorised(path)

result.response.status.shouldBe(HttpStatus.OK.value())
}

it("logs audit") {
mockMvc.performAuthorised(path)

verify(
auditService,
VerificationModeFactory.times(1),
).createEvent("GET_DYNAMIC_RISKS", mapOf("hmppsId" to hmppsId))
}

it("gets the dynamic risks for a person with the matching ID") {
mockMvc.performAuthorised(path)

verify(getDynamicRisksForPersonService, VerificationModeFactory.times(1)).execute(hmppsId)
}

it("returns the dynamic risks for a person with the matching ID") {
val result = mockMvc.performAuthorised(path)

result.response.contentAsString.shouldContain(
"""
"data": [
{
"code": "AVIS",
"description": "Subject has a ViSOR record",
"startDate": "2023-09-08",
"reviewDate": "2026-04-29",
"notes": "Nothing to say"
},
{
"code": "RHRH",
"description": "High Risk of Harm",
"startDate": "2022-09-01",
"reviewDate": "2024-12-23",
"notes": "A lot of notes"
}
]
""".removeWhitespaceAndNewlines(),
)
}

it("returns an empty list when no dynamic risks are found") {
val hmppsIdForPersonWithNoDynamicRisks = "0123/12345B"
val encodedHmppsIdForPersonWithNoDynamicRisks =
URLEncoder.encode(hmppsIdForPersonWithNoDynamicRisks, StandardCharsets.UTF_8)
val dynamicRisksPath = "/v1/persons/$encodedHmppsIdForPersonWithNoDynamicRisks/risks/dynamic"

whenever(getDynamicRisksForPersonService.execute(hmppsIdForPersonWithNoDynamicRisks)).thenReturn(
Response(
data = emptyList(),
),
)

val result = mockMvc.performAuthorised(dynamicRisksPath)

result.response.contentAsString.shouldContain("\"data\":[]".removeWhitespaceAndNewlines())
}

it("returns a 404 NOT FOUND status code when person isn't found in the upstream API") {
whenever(getDynamicRisksForPersonService.execute(hmppsId)).thenReturn(
Response(
data = emptyList(),
errors =
listOf(
UpstreamApiError(
causedBy = UpstreamApi.NOMIS,
type = UpstreamApiError.Type.ENTITY_NOT_FOUND,
),
),
),
)

val result = mockMvc.performAuthorised(path)

result.response.status.shouldBe(HttpStatus.NOT_FOUND.value())
}

it("returns paginated results") {
whenever(getDynamicRisksForPersonService.execute(hmppsId)).thenReturn(
Response(
data =
List(20) {
DynamicRisk(
code = "XNR",
description = "Not For Release",
startDate = "2022-08-01",
reviewDate = "2025-08-01",
notes = "Notes all written here",
)
},
),
)

val result = mockMvc.performAuthorised("$path?page=1&perPage=10")

result.response.contentAsString.shouldContainJsonKeyValue("$.pagination.page", 1)
result.response.contentAsString.shouldContainJsonKeyValue("$.pagination.totalPages", 2)
}

it("fails with the appropriate error when an upstream service is down") {
whenever(getDynamicRisksForPersonService.execute(hmppsId)).doThrow(
WebClientResponseException(500, "MockError", null, null, null, null),
)

val response = mockMvc.performAuthorised("$path?page=1&perPage=10")

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
Expand Up @@ -86,7 +86,8 @@ class GetCommunityOffenderManagerForPersonTest(
{
"communityManager": {},
"mappaDetail": {},
"supervisions": []
"supervisions": [],
"dynamicRisks": []
}
""",
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ class GetMappaDetailForPersonTest(
{
"communityManager": {},
"mappaDetail": {},
"supervisions": []
"supervisions": [],
"dynamicRisks": []
}
""",
)
Expand Down
Loading
Loading