Skip to content

Commit

Permalink
Added endpoint for accredited programmes to get risk predictions (#4177)
Browse files Browse the repository at this point in the history
  • Loading branch information
anthony-britton-moj authored Aug 16, 2024
1 parent 3036ab1 commit da4b4d9
Show file tree
Hide file tree
Showing 6 changed files with 378 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
{
"assessments": [
{
"assessmentPk": 10144385,
"assessmentType": "LAYER1",
"dateCompleted": "2024-02-21T10:39:07",
"assessorSignedDate": "2024-02-21T10:38:20",
"initiationDate": "2024-02-21T10:37:11",
"assessmentStatus": "COMPLETE",
"superStatus": "COMPLETE",
"laterWIPAssessmentExists": true,
"latestWIPDate": "2024-04-18T14:08:19",
"laterSignLockAssessmentExists": false,
"latestSignLockDate": null,
"laterPartCompUnsignedAssessmentExists": false,
"latestPartCompUnsignedDate": "2024-01-22T13:05:54",
"laterPartCompSignedAssessmentExists": false,
"latestPartCompSignedDate": null,
"laterCompleteAssessmentExists": false,
"latestCompleteDate": "2024-02-21T10:39:07",
"OGP": {
"ogpStWesc": null,
"ogpDyWesc": null,
"ogpTotWesc": null,
"ogp1Year": null,
"ogp2Year": null,
"ogpRisk": null
},
"OVP": {
"ovpStWesc": null,
"ovpDyWesc": null,
"ovpTotWesc": null,
"ovp1Year": null,
"ovp2Year": null,
"ovpRisk": null,
"ovpPrevWesc": null,
"ovpNonVioWesc": null,
"ovpAgeWesc": null,
"ovpVioWesc": null,
"ovpSexWesc": null
},
"OGRS": {
"ogrs31Year": 8,
"ogrs32Year": 15,
"ogrs3RiskRecon": "Low"
},
"RSR": {
"rsrStaticOrDynamic": "STATIC",
"rsrExceptionError": null,
"rsrAlgorithmVersion": 4,
"rsrPercentageScore": 1.46,
"scoreLevel": "Low"
},
"OSP": {
"ospImagePercentageScore": 0.11,
"ospContactPercentageScore": 1.07,
"ospImageScoreLevel": "Low",
"ospContactScoreLevel": "Medium",
"ospIndirectImagesChildrenPercentageScore": null,
"ospDirectContactPercentageScore": null,
"ospIndirectImagesChildrenScoreLevel": null,
"ospDirectContactScoreLevel": null
}
},
{
"assessmentPk": 90123456,
"assessmentType": "LAYER3",
"dateCompleted": "2023-12-19T16:57:25",
"assessorSignedDate": "2023-12-19T16:55:32",
"initiationDate": "2023-12-19T13:41:04",
"assessmentStatus": "COMPLETE",
"superStatus": "COMPLETE",
"laterWIPAssessmentExists": true,
"latestWIPDate": "2024-04-18T14:08:19",
"laterSignLockAssessmentExists": false,
"latestSignLockDate": null,
"laterPartCompUnsignedAssessmentExists": true,
"latestPartCompUnsignedDate": "2024-01-22T13:05:54",
"laterPartCompSignedAssessmentExists": false,
"latestPartCompSignedDate": null,
"laterCompleteAssessmentExists": true,
"latestCompleteDate": "2024-02-21T10:39:07",
"OGP": {
"ogpStWesc": 38,
"ogpDyWesc": 7,
"ogpTotWesc": 45,
"ogp1Year": 29,
"ogp2Year": 42,
"ogpRisk": "High"
},
"OVP": {
"ovpStWesc": 34,
"ovpDyWesc": 12,
"ovpTotWesc": 46,
"ovp1Year": 23,
"ovp2Year": 36,
"ovpRisk": "Medium",
"ovpPrevWesc": 5,
"ovpNonVioWesc": 3,
"ovpAgeWesc": 10,
"ovpVioWesc": 11,
"ovpSexWesc": 5
},
"OGRS": {
"ogrs31Year": 45,
"ogrs32Year": 63,
"ogrs3RiskRecon": "High"
},
"RSR": {
"rsrStaticOrDynamic": "DYNAMIC",
"rsrExceptionError": null,
"rsrAlgorithmVersion": 4,
"rsrPercentageScore": 3.45,
"scoreLevel": "Low"
},
"OSP": {
"ospImagePercentageScore": 0.11,
"ospContactPercentageScore": 2,
"ospImageScoreLevel": "Low",
"ospContactScoreLevel": "High",
"ospIndirectImagesChildrenPercentageScore": null,
"ospDirectContactPercentageScore": null,
"ospIndirectImagesChildrenScoreLevel": null,
"ospDirectContactScoreLevel": null
}
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,24 @@
"Content-Type": "application/json"
}
}
},
{
"request": {
"method": "GET",
"url": "/eor/oasys/ass/allrisk/T123456/ALLOW",
"headers": {
"Authorization": {
"matches": "^Bearer oasys.token$"
}
}
},
"response": {
"status": 200,
"bodyFileName": "risk-predictors-T123456.json",
"headers": {
"Content-Type": "application/json"
}
}
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
import uk.gov.justice.digital.hmpps.controller.AssessmentSummary
import uk.gov.justice.digital.hmpps.controller.Timeline
import uk.gov.justice.digital.hmpps.controller.*
import uk.gov.justice.digital.hmpps.telemetry.TelemetryService
import uk.gov.justice.digital.hmpps.test.MockMvcExtensions.contentAsJson
import uk.gov.justice.digital.hmpps.test.MockMvcExtensions.withToken
import java.math.BigDecimal
import java.time.LocalDateTime

@AutoConfigureMockMvc
Expand Down Expand Up @@ -78,4 +78,37 @@ internal class IntegrationTest {
.perform(get("/assessments/90123451/section/sectionroshsumm").withToken())
.andExpect(status().isNotFound)
}

@Test
fun `get risk predictors returns correct response`() {
val prediction = mockMvc
.perform(
get("/assessments/90123456/risk-predictors")
.queryParam("crn", "T123456").withToken()
).andExpect(status().is2xxSuccessful)
.andReturn().response.contentAsJson<RiskPrediction>()

assertThat(
prediction, equalTo(
RiskPrediction(
LocalDateTime.parse("2023-12-19T16:57:25"),
"COMPLETE",
YearPredictor(BigDecimal(45), BigDecimal(63), ScoreLevel.HIGH),
YearPredictor(BigDecimal(23), BigDecimal(36), ScoreLevel.MEDIUM),
YearPredictor(BigDecimal(29), BigDecimal(42), ScoreLevel.HIGH),
RsrPredictor(ScoreLevel.LOW, BigDecimal("3.45")),
SexualPredictor(
BigDecimal("0.11"),
BigDecimal("2"),
ScoreLevel.LOW,
ScoreLevel.HIGH,
null,
null,
null,
null
)
)
)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import org.springframework.web.bind.annotation.*
import org.springframework.web.client.HttpClientErrorException
import uk.gov.justice.digital.hmpps.advice.ErrorResponse
import uk.gov.justice.digital.hmpps.integrations.oasys.OrdsClient
import uk.gov.justice.digital.hmpps.integrations.oasys.getRiskPredictors

@RestController
@RequestMapping("assessments")
Expand All @@ -21,6 +22,11 @@ class AssessmentController(private val ordsClient: OrdsClient) {
fun getSection(@PathVariable id: Long, @PathVariable name: String): JsonNode =
ordsClient.getSection(id, name.lowercase()).asResponse()

@PreAuthorize("hasRole('PROBATION_API__ACCREDITED_PROGRAMMES__ASSESSMENT')")
@GetMapping("/{id}/risk-predictors")
fun getRiskPredictors(@PathVariable id: Long, @RequestParam crn: String): RiskPrediction =
ordsClient.getRiskPredictors(crn, id)

@ExceptionHandler
fun handleNotFound(e: HttpClientErrorException) = ResponseEntity
.status(e.statusCode)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package uk.gov.justice.digital.hmpps.controller

import uk.gov.justice.digital.hmpps.controller.ScoreLevel.Companion.of
import java.math.BigDecimal
import java.time.LocalDateTime

data class RiskPrediction(
val completedDate: LocalDateTime? = null,
val assessmentStatus: String? = null,
val groupReconvictionScore: YearPredictor? = null,
val violencePredictorScore: YearPredictor? = null,
val generalPredictorScore: YearPredictor? = null,
val riskOfSeriousRecidivismScore: RsrPredictor? = null,
val sexualPredictorScore: SexualPredictor? = null,
)

data class YearPredictor(
val oneYear: BigDecimal? = null,
val twoYears: BigDecimal? = null,
val scoreLevel: ScoreLevel? = null,
) {
companion object {
fun of(oneYear: BigDecimal?, twoYears: BigDecimal?, scoreLevel: String?): YearPredictor? =
if (listOfNotNull(oneYear, twoYears, scoreLevel).isEmpty()) null else YearPredictor(
oneYear,
twoYears,
of(scoreLevel)
)
}
}

data class RsrPredictor(
val scoreLevel: ScoreLevel? = null,
val percentageScore: BigDecimal? = null,
) {
companion object {
fun of(scoreLevel: String?, percentageScore: BigDecimal?): RsrPredictor? =
if (scoreLevel == null && percentageScore == null) null else RsrPredictor(of(scoreLevel), percentageScore)
}
}

data class SexualPredictor(
val ospIndecentPercentageScore: BigDecimal?,
val ospContactPercentageScore: BigDecimal?,
val ospIndecentPercentageScoreLevel: ScoreLevel?,
val ospContactPercentageScoreLevel: ScoreLevel?,
val ospIndirectImagePercentageScore: BigDecimal?,
val ospDirectContactPercentageScore: BigDecimal?,
val ospIndirectImagePercentageScoreLevel: ScoreLevel?,
val ospDirectContactPercentageScoreLevel: ScoreLevel?
) {
companion object {
fun of(
ospIndecentPercentageScore: BigDecimal?,
ospContactPercentageScore: BigDecimal?,
ospIndecentPercentageScoreLevel: String?,
ospContactPercentageScoreLevel: String?,
ospIndirectImagePercentageScore: BigDecimal?,
ospDirectContactPercentageScore: BigDecimal?,
ospIndirectImagePercentageScoreLevel: String?,
ospDirectContactPercentageScoreLevel: String?
): SexualPredictor? =
if (listOfNotNull(
ospIndecentPercentageScore,
ospContactPercentageScore,
ospIndecentPercentageScoreLevel,
ospContactPercentageScoreLevel,
ospIndirectImagePercentageScore,
ospDirectContactPercentageScore,
ospIndirectImagePercentageScoreLevel,
ospDirectContactPercentageScoreLevel
).isEmpty()
) null
else SexualPredictor(
ospIndecentPercentageScore,
ospContactPercentageScore,
of(ospIndecentPercentageScoreLevel),
of(ospContactPercentageScoreLevel),
ospIndirectImagePercentageScore,
ospDirectContactPercentageScore,
of(ospIndirectImagePercentageScoreLevel),
of(ospDirectContactPercentageScoreLevel)
)
}
}

enum class ScoreLevel(val type: String) {
LOW("Low"), MEDIUM("Medium"), HIGH("High"), VERY_HIGH("Very High"), NOT_APPLICABLE("Not Applicable");

companion object {
fun of(type: String?): ScoreLevel? {
return entries.firstOrNull { value -> value.type == type }
}
}
}
Loading

0 comments on commit da4b4d9

Please sign in to comment.