Skip to content

Commit

Permalink
PI-1962 Add support for new OSP levels (#3419)
Browse files Browse the repository at this point in the history
  • Loading branch information
marcus-bcl authored Mar 4, 2024
1 parent 8d2ae62 commit f5a0106
Show file tree
Hide file tree
Showing 11 changed files with 205 additions and 68 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,27 @@ package uk.gov.justice.digital.hmpps.flags

import io.flipt.api.FliptClient
import io.flipt.api.evaluation.models.EvaluationRequest
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service

@Service
class FeatureFlags(
private val client: FliptClient?
) {
companion object {
private val log: Logger = LoggerFactory.getLogger(this::class.java)
}

fun enabled(key: String) = try {
client == null || client.evaluation()
.evaluateBoolean(EvaluationRequest.builder().namespaceKey("probation-integration").flagKey(key).build())
.isEnabled
if (client == null) {
log.warn("Flipt client not configured, all feature flags enabled.")
true
} else {
client.evaluation()
.evaluateBoolean(EvaluationRequest.builder().namespaceKey("probation-integration").flagKey(key).build())
.isEnabled
}
} catch (e: Exception) {
throw FeatureFlagException(key, e)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import uk.gov.justice.digital.hmpps.resourceloader.ResourceLoader

object MessageGenerator {
val RSR_SCORES_DETERMINED = ResourceLoader.message<HmppsDomainEvent>("rsr-scores-determined")
val RSR_SCORES_DETERMINED_WITHOUT_OSPIIC_OSPDC =
ResourceLoader.message<HmppsDomainEvent>("rsr-scores-determined-null-osp")
val OGRS_SCORES_DETERMINED = ResourceLoader.message<HmppsDomainEvent>("ogrs-scores-determined")
val OGRS_SCORES_DETERMINED_UPDATE = ResourceLoader.message<HmppsDomainEvent>("ogrs-scores-determined-update")
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
drop schema if exists pkg_triggersupport cascade;

create schema pkg_triggersupport;
create alias pkg_triggersupport.procUpdateCAS as 'void stub(String p_crn, Integer p_event_number, java.util.Date p_rsr_assessor_date, Double p_rsr_score, String p_rsr_level_code, Double p_osp_score_i, Double p_osp_score_c, String p_osp_level_i_code, String p_osp_level_c_code) {}';
create
alias pkg_triggersupport.procUpdateCAS as 'void stub(' ||
'String p_crn, ' ||
'Integer p_event_number, ' ||
'java.util.Date p_rsr_assessor_date, ' ||
'Double p_rsr_score, ' ||
'String p_rsr_level_code, ' ||
'Double p_osp_score_i, ' ||
'Double p_osp_score_c, ' ||
'String p_osp_level_i_code, ' ||
'String p_osp_level_c_code, ' ||
'String p_osp_level_iic_code, ' ||
'String p_osp_level_dc_code) {}';
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
-- As of 30/09/2022, the procedure is not yet available in the Delius OracleDB so I've left this SQL here to help with local testing.

create or replace package pkg_triggersupport as
procedure procUpdateCas(
p_crn in varchar2,
Expand All @@ -11,7 +9,9 @@ create or replace package pkg_triggersupport as
p_osp_score_i in float,
p_osp_level_i_code in varchar2,
p_osp_score_c in float,
p_osp_level_c_code in varchar2
p_osp_level_c_code in varchar2,
p_osp_level_iic_code in varchar2 default null,
p_osp_level_dc_code in varchar2 default null
);
end pkg_triggersupport;
grant execute on pkg_triggersupport to delius_app_schema;
Expand All @@ -26,7 +26,9 @@ create or replace package body pkg_triggersupport as
p_osp_score_i in float,
p_osp_level_i_code in varchar2,
p_osp_score_c in float,
p_osp_level_c_code in varchar2
p_osp_level_c_code in varchar2,
p_osp_level_iic_code in varchar2 default null,
p_osp_level_dc_code in varchar2 default null
) is
begin
-- For testing:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"Type": "Notification",
"MessageId": "b04098df-3d67-416f-87eb-f63782f713fb",
"TopicArn": "",
"Message": "{\"eventType\":\"risk-assessment.scores.rsr.determined\",\"version\":1,\"description\":\"Risk assessment scores have been determined\",\"detailUrl\":\"https://some-url-where-we-can-get-more-info-this-might-not-exist\",\"occurredAt\":\"2022-09-22T12:16:04+01:00\",\"additionalInformation\":{\"RSRScore\":45.33,\"RSRBand\":\"H\",\"RSRStaticOrDynamic\":\"STATIC\",\"OSPIndecentScore\":5.79,\"OSPIndecentBand\":\"H\",\"OSPContactScore\":38.7,\"OSPContactBand\":\"V\",\"EventNumber\":1,\"AssessmentDate\":\"2022-09-22T12:16:04+01:00\"},\"personReference\":{\"identifiers\":[{\"type\":\"CRN\",\"value\":\"X552020\"}]}}",
"Timestamp": "2022-05-04T08:06:46.704Z",
"SignatureVersion": "1",
"Signature": "",
"SigningCertURL": "",
"UnsubscribeURL": "",
"MessageAttributes": {
"eventType": {
"Type": "String",
"Value": "risk-assessment.scores.determined"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"Type": "Notification",
"MessageId": "b04098df-3d67-416f-87eb-f63782f713fb",
"TopicArn": "",
"Message": "{\"eventType\":\"risk-assessment.scores.rsr.determined\",\"version\":1,\"description\":\"Risk assessment scores have been determined\",\"detailUrl\":\"https://some-url-where-we-can-get-more-info-this-might-not-exist\",\"occurredAt\":\"2022-09-22T12:16:04+01:00\",\"additionalInformation\":{\"RSRScore\":45.33,\"RSRBand\":\"H\",\"RSRStaticOrDynamic\":\"STATIC\",\"OSPIndecentScore\":5.79,\"OSPIndecentBand\":\"H\",\"OSPContactScore\":38.7,\"OSPContactBand\":\"V\",\"EventNumber\":1,\"AssessmentDate\":\"2022-09-22T12:16:04+01:00\"},\"personReference\":{\"identifiers\":[{\"type\":\"CRN\",\"value\":\"X552020\"}]}}",
"Message": "{\"eventType\":\"risk-assessment.scores.rsr.determined\",\"version\":1,\"description\":\"Risk assessment scores have been determined\",\"detailUrl\":\"https://some-url-where-we-can-get-more-info-this-might-not-exist\",\"occurredAt\":\"2022-09-22T12:16:04+01:00\",\"additionalInformation\":{\"RSRScore\":45.33,\"RSRBand\":\"H\",\"RSRStaticOrDynamic\":\"STATIC\",\"OSPIndecentScore\":5.79,\"OSPIndecentBand\":\"H\",\"OSPIndirectIndecentScore\":15.32,\"OSPIndirectIndecentBand\":\"V\",\"OSPContactScore\":38.7,\"OSPContactBand\":\"V\",\"OSPDirectContactScore\":21.2,\"OSPDirectContactBand\":\"H\",\"EventNumber\":1,\"AssessmentDate\":\"2022-09-22T12:16:04+01:00\"},\"personReference\":{\"identifiers\":[{\"type\":\"CRN\",\"value\":\"X552020\"}]}}",
"Timestamp": "2022-05-04T08:06:46.704Z",
"SignatureVersion": "1",
"Signature": "",
Expand All @@ -11,7 +11,7 @@
"MessageAttributes": {
"eventType": {
"Type": "String",
"Value": "risk-assessment.scores.rsr.determined"
"Value": "risk-assessment.scores.determined"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,7 @@ package uk.gov.justice.digital.hmpps
import org.assertj.core.api.Assertions.assertThat
import org.hamcrest.MatcherAssert
import org.hamcrest.Matchers
import org.junit.jupiter.api.MethodOrderer
import org.junit.jupiter.api.Order
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestMethodOrder
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.*
import org.mockito.kotlin.verify
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value
Expand Down Expand Up @@ -68,6 +64,16 @@ internal class IntegrationTest {
verify(telemetryService).trackEvent("RsrScoresUpdated", notification.message.telemetryProperties())
}

@Test
fun `handles null OSP-IIC and OSP-DC bands`() {
val notification = Notification(
message = MessageGenerator.RSR_SCORES_DETERMINED_WITHOUT_OSPIIC_OSPDC,
attributes = MessageAttributes("risk-assessment.scores.determined")
)
channelManager.getChannel(queueName).publishAndWait(notification)
verify(telemetryService).trackEvent("RsrScoresUpdated", notification.message.telemetryProperties())
}

@Test
@Order(1)
fun `successfully add OGRS assessment`() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ import org.springframework.jdbc.core.SqlParameter
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource
import org.springframework.jdbc.core.simple.SimpleJdbcCall
import org.springframework.stereotype.Service
import uk.gov.justice.digital.hmpps.flags.FeatureFlags
import uk.gov.justice.digital.hmpps.messaging.RiskAssessment
import java.sql.SQLException
import java.sql.Types
import java.time.ZonedDateTime

@Service
class RiskScoreService(jdbcTemplate: JdbcTemplate) {
private val updateRsrScoresProcedure = SimpleJdbcCall(jdbcTemplate)
class RiskScoreService(jdbcTemplate: JdbcTemplate, val featureFlags: FeatureFlags) {
private val updateRsrAndOspScoresProcedure = SimpleJdbcCall(jdbcTemplate)
.withCatalogName("pkg_triggersupport")
.withProcedureName("procUpdateCAS")
.withoutProcedureColumnMetaDataAccess()
Expand All @@ -26,45 +27,62 @@ class RiskScoreService(jdbcTemplate: JdbcTemplate) {
SqlParameter("p_osp_score_i", Types.NUMERIC),
SqlParameter("p_osp_score_c", Types.NUMERIC),
SqlParameter("p_osp_level_i_code", Types.VARCHAR),
SqlParameter("p_osp_level_c_code", Types.VARCHAR)
SqlParameter("p_osp_level_c_code", Types.VARCHAR),
)

fun updateRsrScores(
private val SimpleJdbcCall.withIndirectIndecentAndDirectContact
get() = declareParameters(
SqlParameter("p_osp_level_iic_code", Types.VARCHAR),
SqlParameter("p_osp_level_dc_code", Types.VARCHAR),
)

fun updateRsrAndOspScores(
crn: String,
eventNumber: Int?,
assessmentDate: ZonedDateTime,
rsr: RiskAssessment,
ospIndecent: RiskAssessment,
ospContact: RiskAssessment
ospIndirectIndecent: RiskAssessment?,
ospContact: RiskAssessment,
ospDirectContact: RiskAssessment?,
) {
try {
updateRsrScoresProcedure.execute(
MapSqlParameterSource()
.addValue("p_crn", crn)
.addValue("p_event_number", eventNumber)
.addValue("p_rsr_assessor_date", assessmentDate)
.addValue("p_rsr_score", rsr.score)
.addValue("p_rsr_level_code", rsr.band)
.addValue("p_osp_score_i", ospIndecent.score)
.addValue("p_osp_score_c", ospContact.score)
.addValue("p_osp_level_i_code", ospIndecent.band)
.addValue("p_osp_level_c_code", ospContact.band)
)
val params = MapSqlParameterSource()
.addValue("p_crn", crn)
.addValue("p_event_number", eventNumber)
.addValue("p_rsr_assessor_date", assessmentDate)
.addValue("p_rsr_score", rsr.score)
.addValue("p_rsr_level_code", rsr.band)
.addValue("p_osp_score_i", ospIndecent.score)
.addValue("p_osp_score_c", ospContact.score)
.addValue("p_osp_level_i_code", ospIndecent.band)
.addValue("p_osp_level_c_code", ospContact.band)

if (featureFlags.enabled("osp-indirect-indecent-and-direct-contact")) {
updateRsrAndOspScoresProcedure.withIndirectIndecentAndDirectContact.execute(
params
.addValue("p_osp_level_iic_code", ospIndirectIndecent?.band)
.addValue("p_osp_level_dc_code", ospDirectContact?.band)
)
} else {
updateRsrAndOspScoresProcedure.execute(params)
}
} catch (e: UncategorizedSQLException) {
e.sqlException.takeIf { it.isValidationError() }
?.parsedValidationMessage()
?.takeIf { it.isDeliusValidationMessage() }
e.sqlException.takeIf { it.isValidationError }
?.parsedValidationMessage
?.takeIf { it.isDeliusValidationMessage }
?.let { throw DeliusValidationError(it) }
throw e
}
}

private fun SQLException.isValidationError() = errorCode == 20000
private fun SQLException.parsedValidationMessage() = message
?.replace(Regex("\\n.*"), "") // take the first line
?.replace(Regex("\\[[^]]++]\\s*"), "") // remove anything inside square brackets
?.removePrefix("ORA-20000: INTERNAL ERROR: An unexpected error in PL/SQL: ERROR : ") // remove Oracle prefix
?.trim()
private val SQLException.isValidationError get() = errorCode == 20000
private val SQLException.parsedValidationMessage
get() = message
?.replace(Regex("\\n.*"), "") // take the first line
?.replace(Regex("\\[[^]]++]\\s*"), "") // remove anything inside square brackets
?.removePrefix("ORA-20000: INTERNAL ERROR: An unexpected error in PL/SQL: ERROR : ") // remove Oracle prefix
?.trim()

private fun String.isDeliusValidationMessage() = DeliusValidationError.isKnownValidationMessage(this)
private val String.isDeliusValidationMessage get() = DeliusValidationError.isKnownValidationMessage(this)
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,16 @@ class Handler(
when (message.eventType) {
"risk-assessment.scores.rsr.determined" -> {
try {
riskScoreService.updateRsrScores(
riskScoreService.updateRsrAndOspScores(
message.personReference.findCrn()
?: throw IllegalArgumentException("Missing CRN in ${message.personReference}"),
message.additionalInformation["EventNumber"] as Int?,
message.assessmentDate(),
message.rsr(),
message.ospIndecent(),
message.ospContact()
message.ospIndirectIndecent(),
message.ospContact(),
message.ospDirectContact(),
)
telemetryService.trackEvent("RsrScoresUpdated", message.telemetryProperties())
} catch (e: DeliusValidationError) {
Expand Down Expand Up @@ -95,14 +97,28 @@ fun HmppsDomainEvent.rsr() = RiskAssessment(

fun HmppsDomainEvent.ospIndecent() = RiskAssessment(
additionalInformation["OSPIndecentScore"] as Double,
additionalInformation["OSPIndecentBand"] as String
additionalInformation["OSPIndecentBand"] as String,
)

fun HmppsDomainEvent.ospIndirectIndecent() = additionalInformation["OSPIndecentIndirectBand"]?.let {
RiskAssessment(
additionalInformation["OSPIndirectIndecentScore"] as Double,
additionalInformation["OSPIndirectIndecentBand"] as String,
)
}

fun HmppsDomainEvent.ospContact() = RiskAssessment(
additionalInformation["OSPContactScore"] as Double,
additionalInformation["OSPContactBand"] as String
additionalInformation["OSPContactBand"] as String,
)

fun HmppsDomainEvent.ospDirectContact() = additionalInformation["OSPDirectContactBand"]?.let {
RiskAssessment(
additionalInformation["OSPDirectContactScore"] as Double,
additionalInformation["OSPDirectContactBand"] as String,
)
}

fun HmppsDomainEvent.ogrsScore() = OgrsScore(
additionalInformation["OGRS3Yr1"] as Int,
additionalInformation["OGRS3Yr2"] as Int
Expand Down
Loading

0 comments on commit f5a0106

Please sign in to comment.