From caf23ac0f581b67b14f7f51dceb470daed209a24 Mon Sep 17 00:00:00 2001 From: Marcus Aspin Date: Tue, 17 Dec 2024 11:49:13 +0000 Subject: [PATCH] PI-2700 Remove existing registrations in the same group (#4499) --- .../hmpps/datetime/DeliusDateTimeFormatter.kt | 3 + .../justice/digital/hmpps/data/DataLoader.kt | 20 +++--- .../data/generator/RegistrationGenerator.kt | 8 +++ .../justice/digital/hmpps/IntegrationTest.kt | 11 ++-- .../delius/person/entity/Registration.kt | 33 +++++++++- .../digital/hmpps/service/RiskService.kt | 62 +++++++++++-------- 6 files changed, 97 insertions(+), 40 deletions(-) diff --git a/libs/commons/src/main/kotlin/uk/gov/justice/digital/hmpps/datetime/DeliusDateTimeFormatter.kt b/libs/commons/src/main/kotlin/uk/gov/justice/digital/hmpps/datetime/DeliusDateTimeFormatter.kt index 589cac8dd2..85633f9bfd 100644 --- a/libs/commons/src/main/kotlin/uk/gov/justice/digital/hmpps/datetime/DeliusDateTimeFormatter.kt +++ b/libs/commons/src/main/kotlin/uk/gov/justice/digital/hmpps/datetime/DeliusDateTimeFormatter.kt @@ -2,8 +2,11 @@ package uk.gov.justice.digital.hmpps.datetime import java.time.ZoneId import java.time.format.DateTimeFormatter +import java.time.temporal.Temporal val DeliusDateTimeFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss") val DeliusDateFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("dd/MM/yyyy") +fun Temporal.toDeliusDate(): String = DeliusDateFormatter.format(this) +fun Temporal.toDeliusDateTime(): String = DeliusDateTimeFormatter.format(this) val EuropeLondon: ZoneId = ZoneId.of("Europe/London") diff --git a/projects/assessment-summary-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/DataLoader.kt b/projects/assessment-summary-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/DataLoader.kt index fcbfc7e776..19388d0043 100644 --- a/projects/assessment-summary-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/DataLoader.kt +++ b/projects/assessment-summary-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/DataLoader.kt @@ -15,6 +15,7 @@ import uk.gov.justice.digital.hmpps.enum.RiskType import uk.gov.justice.digital.hmpps.integrations.delius.contact.entity.ContactType import uk.gov.justice.digital.hmpps.integrations.delius.person.entity.Event import uk.gov.justice.digital.hmpps.integrations.delius.person.entity.Person +import uk.gov.justice.digital.hmpps.integrations.delius.person.entity.RegisterType import uk.gov.justice.digital.hmpps.set import uk.gov.justice.digital.hmpps.user.AuditUserRepository import java.time.LocalDate @@ -44,6 +45,8 @@ class DataLoader( saveAll(ReferenceDataGenerator.LEVELS_DATASET, *ReferenceDataGenerator.LEVELS.toTypedArray()) saveAll(*ContactGenerator.TYPES.values.toTypedArray()) saveAll(*RegistrationGenerator.TYPES.values.toTypedArray()) + saveAll(RegistrationGenerator.ALT_TYPE) + saveAll(RegistrationGenerator.DUPLICATE_GROUP) saveAll(*ReferenceDataGenerator.REQ_MAIN_CATS.toTypedArray()) saveAll( ReferenceDataGenerator.DOMAIN_EVENT_TYPE_DATASET, @@ -65,10 +68,11 @@ class DataLoader( PersonGenerator.PRISON_ASSESSMENT.withEvent(custodial = true) PersonGenerator.NO_EXISTING_RISKS.withEvent() PersonGenerator.EXISTING_RISKS.withEvent().withRisks( - RiskType.CHILDREN to RiskLevel.H, - RiskType.STAFF to RiskLevel.V, - RiskType.KNOWN_ADULT to RiskLevel.M, - RiskType.PUBLIC to RiskLevel.M, + RegistrationGenerator.TYPES[RiskType.CHILDREN.code]!! to RiskLevel.H, + RegistrationGenerator.TYPES[RiskType.STAFF.code]!! to RiskLevel.V, + RegistrationGenerator.TYPES[RiskType.KNOWN_ADULT.code]!! to RiskLevel.M, + RegistrationGenerator.TYPES[RiskType.PUBLIC.code]!! to RiskLevel.M, + RegistrationGenerator.ALT_TYPE to null, ) PersonGenerator.FEATURE_FLAG.withEvent().withRiskOfSeriousHarm(V) } @@ -107,11 +111,11 @@ class DataLoader( return this } - private fun Person.withRisks(vararg risks: Pair): Person { + private fun Person.withRisks(vararg risks: Pair): Person { risks.forEach { risk -> - val type = RegistrationGenerator.TYPES[risk.first.code] - val level = ReferenceDataGenerator.LEVELS.single { it.code == risk.second.code } - val contact = entityManager.merge(ContactGenerator.generateContact(this, type!!.registrationContactType!!)) + val type = risk.first + val level = ReferenceDataGenerator.LEVELS.singleOrNull { it.code == risk.second?.code } + val contact = entityManager.merge(ContactGenerator.generateContact(this, type.registrationContactType!!)) val registration = RegistrationGenerator.generate(this.id, LocalDate.parse("2023-06-14"), contact, type, level) val reviewContact = entityManager.merge(ContactGenerator.generateContact(this, type.reviewContactType!!)) diff --git a/projects/assessment-summary-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/RegistrationGenerator.kt b/projects/assessment-summary-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/RegistrationGenerator.kt index c3f1f1104d..d49aaa4250 100644 --- a/projects/assessment-summary-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/RegistrationGenerator.kt +++ b/projects/assessment-summary-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/RegistrationGenerator.kt @@ -5,6 +5,7 @@ import uk.gov.justice.digital.hmpps.enum.RiskOfSeriousHarmType import uk.gov.justice.digital.hmpps.enum.RiskType import uk.gov.justice.digital.hmpps.integrations.delius.contact.entity.Contact import uk.gov.justice.digital.hmpps.integrations.delius.contact.entity.ContactType +import uk.gov.justice.digital.hmpps.integrations.delius.person.entity.RegisterDuplicateGroup import uk.gov.justice.digital.hmpps.integrations.delius.person.entity.RegisterType import uk.gov.justice.digital.hmpps.integrations.delius.person.entity.Registration import uk.gov.justice.digital.hmpps.integrations.delius.referencedata.entity.ReferenceData @@ -17,6 +18,13 @@ object RegistrationGenerator { RiskType.entries.map { generateType(it.code, "Amber", "Risk to ${it.name.lowercase()}") } ).flatten().associateBy { it.code } + val ALT_TYPE = generateType("ALT1", "Amber", "ALT Risk to prisoner") + + val DUPLICATE_GROUP = RegisterDuplicateGroup( + types = listOf(TYPES[RiskType.PRISONER.code]!!, ALT_TYPE), + id = IdGenerator.getAndIncrement() + ) + val RiskOfSeriousHarmType.colour get() = when (this) { RiskOfSeriousHarmType.V -> "Red" diff --git a/projects/assessment-summary-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/IntegrationTest.kt b/projects/assessment-summary-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/IntegrationTest.kt index f7f5f21955..6db9497cc6 100644 --- a/projects/assessment-summary-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/IntegrationTest.kt +++ b/projects/assessment-summary-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/IntegrationTest.kt @@ -30,10 +30,7 @@ import uk.gov.justice.digital.hmpps.integrations.delius.assessment.entity.OasysA import uk.gov.justice.digital.hmpps.integrations.delius.contact.entity.ContactRepository import uk.gov.justice.digital.hmpps.integrations.delius.contact.entity.ContactType import uk.gov.justice.digital.hmpps.integrations.delius.domainevent.entity.DomainEventRepository -import uk.gov.justice.digital.hmpps.integrations.delius.person.entity.Person -import uk.gov.justice.digital.hmpps.integrations.delius.person.entity.PersonRepository -import uk.gov.justice.digital.hmpps.integrations.delius.person.entity.RegistrationRepository -import uk.gov.justice.digital.hmpps.integrations.delius.person.entity.getByCrn +import uk.gov.justice.digital.hmpps.integrations.delius.person.entity.* import uk.gov.justice.digital.hmpps.message.HmppsDomainEvent import uk.gov.justice.digital.hmpps.message.Notification import uk.gov.justice.digital.hmpps.message.PersonIdentifier @@ -351,6 +348,9 @@ internal class IntegrationTest { assertThat(riskToChildrenBefore.reviews, hasSize(1)) val riskToPrisonerBefore = registrationRepository.findByPersonIdAndTypeCode(person.id, RiskType.PRISONER.code) assertThat(riskToPrisonerBefore, hasSize(0)) + val altRiskToPrisonerBefore = + registrationRepository.findByPersonIdAndTypeCode(person.id, RegistrationGenerator.ALT_TYPE.code).single() + assertThat(altRiskToPrisonerBefore.level?.code, nullValue()) val riskToStaffBefore = registrationRepository.findByPersonIdAndTypeCode(person.id, RiskType.STAFF.code).single() assertThat(riskToStaffBefore.level?.code, equalTo(RiskLevel.V.code)) @@ -386,6 +386,9 @@ internal class IntegrationTest { assertThat(riskToPrisoner.level?.code, equalTo(RiskLevel.M.code)) assertThat(riskToPrisoner.reviews, hasSize(1)) assertThat(domainEvents.ofType(RiskType.PRISONER), hasSize(1)) + val altRiskToPrisoner = + registrationRepository.findByPersonIdAndTypeCode(person.id, RegistrationGenerator.ALT_TYPE.code) + assertThat(altRiskToPrisoner, empty()) val riskToStaff = registrationRepository.findByPersonIdAndTypeCode(person.id, RiskType.STAFF.code).single() assertThat(riskToStaff.level?.code, equalTo(RiskLevel.V.code)) diff --git a/projects/assessment-summary-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/person/entity/Registration.kt b/projects/assessment-summary-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/person/entity/Registration.kt index 2fc8905d08..3f9450bca2 100644 --- a/projects/assessment-summary-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/person/entity/Registration.kt +++ b/projects/assessment-summary-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/person/entity/Registration.kt @@ -7,6 +7,7 @@ import org.hibernate.type.YesNoConverter import org.springframework.data.jpa.domain.support.AuditingEntityListener import org.springframework.data.jpa.repository.EntityGraph import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query import uk.gov.justice.digital.hmpps.exception.NotFoundException import uk.gov.justice.digital.hmpps.integrations.delius.contact.entity.Contact import uk.gov.justice.digital.hmpps.integrations.delius.contact.entity.ContactType @@ -121,6 +122,23 @@ class RegisterType( val id: Long ) +@Immutable +@Entity +@Table(name = "r_register_duplicate_group") +class RegisterDuplicateGroup( + @ManyToMany + @JoinTable( + name = "r_register_type_dup_grp", + joinColumns = [JoinColumn(name = "register_group_id")], + inverseJoinColumns = [JoinColumn(name = "register_type_id")] + ) + val types: List, + + @Id + @Column(name = "register_group_id") + val id: Long +) + @Entity @Table(name = "registration_review") @SQLRestriction("soft_deleted = 0") @@ -202,12 +220,25 @@ interface RegistrationRepository : JpaRepository { fun findByPersonIdAndTypeFlagCode(personId: Long, flagCode: String): List @EntityGraph(attributePaths = ["contact", "type.flag", "type.registrationContactType", "type.reviewContactType", "reviews.contact"]) - fun findByPersonIdAndTypeCode(personId: Long, typeCode: String): List + fun findByPersonIdAndTypeCodeIn(personId: Long, typeCodes: List): List } +fun RegistrationRepository.findByPersonIdAndTypeCode(personId: Long, typeCode: String) = + findByPersonIdAndTypeCodeIn(personId, listOf(typeCode)) + interface RegisterTypeRepository : JpaRepository { @EntityGraph(attributePaths = ["flag", "registrationContactType", "reviewContactType"]) fun findByCode(code: String): RegisterType? + + @Query( + """ + select distinct t2.code + from RegisterDuplicateGroup g + join g.types t1 on t1.code = :code + join g.types t2 on t2.code <> :code + """ + ) + fun findOtherTypesInGroup(code: String): List } fun RegisterTypeRepository.getByCode(code: String) = diff --git a/projects/assessment-summary-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/service/RiskService.kt b/projects/assessment-summary-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/service/RiskService.kt index 8914d7c2e7..a4837d31f9 100644 --- a/projects/assessment-summary-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/service/RiskService.kt +++ b/projects/assessment-summary-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/service/RiskService.kt @@ -1,7 +1,7 @@ package uk.gov.justice.digital.hmpps.service import org.springframework.stereotype.Service -import uk.gov.justice.digital.hmpps.datetime.DeliusDateFormatter +import uk.gov.justice.digital.hmpps.datetime.toDeliusDate import uk.gov.justice.digital.hmpps.enum.RiskLevel import uk.gov.justice.digital.hmpps.enum.RiskOfSeriousHarmType import uk.gov.justice.digital.hmpps.enum.RiskType @@ -59,39 +59,41 @@ class RiskService( private fun recordOtherRisks(person: Person, summary: AssessmentSummary): List { if (!featureFlags.enabled("assessment-summary-additional-risks")) return emptyList() return RiskType.entries.flatMap { riskType -> - val desiredLevel = riskType.riskLevel(summary) ?: return@flatMap emptyList() + val riskLevel = riskType.riskLevel(summary) ?: return@flatMap emptyList() val registrations = registrationRepository.findByPersonIdAndTypeCode(person.id, riskType.code).toMutableList() - val (matchingRegistrations, registrationsToRemove) = registrations.partition { existing -> - existing.level != null && existing.level.code == desiredLevel.code && desiredLevel != RiskLevel.L - } + // Deregister existing registrations if OASys identified the level as low risk + if (riskLevel == RiskLevel.L) return@flatMap registrations.map { person.removeRegistration(it) } - val deRegEvents = registrationsToRemove.map { - val contact = contactService.createContact(ContactDetail(DEREGISTRATION, notes = it.notes()), person) - it.deregister(contact) - it.deRegEvent(person.crn) - } - val regEvent = if (matchingRegistrations.isEmpty() && desiredLevel != RiskLevel.L) { + // Remove any existing registrations with a different level + val (matchingRegistrations, registrationsToRemove) = registrations + .partition { it.level != null && it.level.code == riskLevel.code } + val events = registrationsToRemove.map { person.removeRegistration(it) }.toMutableList() + + // Add registration with the identified level if it doesn't already exist + if (matchingRegistrations.isEmpty()) { val type = registerTypeRepository.getByCode(riskType.code) - val level = referenceDataRepository.registerLevel(desiredLevel.code) + val level = referenceDataRepository.registerLevel(riskLevel.code) val notes = - "The OASys assessment of ${summary.furtherInformation.pOAssessmentDesc} on ${ - DeliusDateFormatter.format( - summary.dateCompleted - ) - } identified the ${type.description} to be ${level.description}" - val registration = registrations.addRegistration(person, type, level, notes) - registration.regEvent(person.crn) - } else null - - if (desiredLevel != RiskLevel.L) { - // Always add a review if medium or higher, regardless of whether the register level has changed - registrationRepository.findByPersonIdAndTypeCode(person.id, riskType.code).singleOrNull() - ?.let { it.withReview(it.reviewContact(person)) } + "The OASys assessment of ${summary.furtherInformation.pOAssessmentDesc} on ${summary.dateCompleted.toDeliusDate()} identified the ${type.description} to be ${level.description}" + events += registrations.addRegistration(person, type, level, notes).regEvent(person.crn) + + // Registrations in the type's duplicate group should also be removed + registerTypeRepository.findOtherTypesInGroup(riskType.code) + .let { registrationRepository.findByPersonIdAndTypeCodeIn(person.id, it) } + .forEach { + val duplicateGroupNotes = + "De-registered when a ${type.description} registration was added on ${summary.dateCompleted.toDeliusDate()}." + events += person.removeRegistration(it, notes = duplicateGroupNotes) + } } - return@flatMap listOfNotNull(regEvent) + deRegEvents + // Always add a review, regardless of whether the register level has changed + registrationRepository.findByPersonIdAndTypeCode(person.id, riskType.code).singleOrNull() + ?.let { it.withReview(it.reviewContact(person)) } + + return@flatMap events } } @@ -128,6 +130,12 @@ class RiskService( return registration } + private fun Person.removeRegistration(it: Registration, notes: String? = null): HmppsDomainEvent { + val contact = contactService.createContact(ContactDetail(DEREGISTRATION, notes = notes ?: it.notes()), this) + it.deregister(contact) + return it.deRegEvent(crn) + } + private fun Registration.reviewContact(person: Person) = contactService.createContact( ContactDetail( ContactType.Code.REGISTRATION_REVIEW, @@ -140,7 +148,7 @@ class RiskService( fun reviewNotes(type: RegisterType, nextReviewDate: LocalDate?) = listOfNotNull( "Type: ${type.flag.description} - ${type.description}", - nextReviewDate?.let { "Next Review Date: ${DeliusDateFormatter.format(it)}" } + nextReviewDate?.let { "Next Review Date: ${it.toDeliusDate()}" } ).joinToString(System.lineSeparator()) fun Registration.notes(): String = reviewNotes(type, nextReviewDate)