From 0a4108e0c5acf5a66897e8ec3f607faabddfd3fd Mon Sep 17 00:00:00 2001 From: Anthony Britton <105213050+anthony-britton-moj@users.noreply.github.com> Date: Mon, 20 Nov 2023 14:36:32 +0000 Subject: [PATCH] PI-1633 - Process UAL (Absconded) (#2714) * PI-1633 --- .../justice/digital/hmpps/data/DataLoader.kt | 11 ++++ .../data/generator/NotificationGenerator.kt | 1 + .../hmpps/data/generator/PersonGenerator.kt | 1 + .../messages/prisoner-absconded.json | 17 +++++++ .../digital/hmpps/PcstdIntegrationTest.kt | 50 +++++++++++++++++++ .../wellknown/InstitutionCode.kt | 1 + .../hmpps/messaging/MovementReasonCodes.kt | 2 + .../hmpps/messaging/PrisonerMovement.kt | 5 ++ .../hmpps/messaging/actions/RecallAction.kt | 11 ++-- .../messaging/actions/UpdateLocationAction.kt | 4 +- .../messaging/actions/UpdateStatusAction.kt | 2 + .../application-prisoner-movement.yml | 10 ++++ .../actions/UpdateLocationActionTest.kt | 17 ++++++- 13 files changed, 124 insertions(+), 8 deletions(-) create mode 100644 projects/prison-custody-status-to-delius/src/dev/resources/messages/prisoner-absconded.json diff --git a/projects/prison-custody-status-to-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/DataLoader.kt b/projects/prison-custody-status-to-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/DataLoader.kt index 17a612b41f..c4426aaaf4 100644 --- a/projects/prison-custody-status-to-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/DataLoader.kt +++ b/projects/prison-custody-status-to-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/DataLoader.kt @@ -96,6 +96,7 @@ class DataLoader( createIrcInCustody() createReleasablePerson(PersonGenerator.RELEASABLE_ECSL_ACTIVE) createReleasablePerson(PersonGenerator.RELEASABLE_ECSL_INACTIVE) + createAbsconded() } private fun createReferenceData() { @@ -268,6 +269,16 @@ class DataLoader( release?.also { releaseRepository.save(it) } recall?.also { recallRepository.save(it) } } + + private fun createAbsconded() { + createPerson(PersonGenerator.ABSCONDED) + createEvent( + EventGenerator.previouslyReleasedEvent( + PersonGenerator.ABSCONDED, + InstitutionGenerator.STANDARD_INSTITUTIONS[InstitutionCode.IN_COMMUNITY] + ) + ) + } } interface LicenceConditionCategoryRepository : JpaRepository diff --git a/projects/prison-custody-status-to-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/NotificationGenerator.kt b/projects/prison-custody-status-to-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/NotificationGenerator.kt index d8a3534e37..547be2d17f 100644 --- a/projects/prison-custody-status-to-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/NotificationGenerator.kt +++ b/projects/prison-custody-status-to-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/NotificationGenerator.kt @@ -17,4 +17,5 @@ object NotificationGenerator { val PRISONER_IRC_IN_CUSTODY = ResourceLoader.notification("prisoner-received-irc-custody") val PRISONER_RELEASED_ECSL_ACTIVE = ResourceLoader.notification("prisoner-released-ecsl-active") val PRISONER_RELEASED_ECSL_INACTIVE = ResourceLoader.notification("prisoner-released-ecsl-inactive") + val PRISONER_ABSCONDED = ResourceLoader.notification("prisoner-absconded") } diff --git a/projects/prison-custody-status-to-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/PersonGenerator.kt b/projects/prison-custody-status-to-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/PersonGenerator.kt index 514a4356d2..4a3baa5bc0 100644 --- a/projects/prison-custody-status-to-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/PersonGenerator.kt +++ b/projects/prison-custody-status-to-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/PersonGenerator.kt @@ -17,6 +17,7 @@ object PersonGenerator { val IRC_IN_CUSTODY = generate(NotificationGenerator.PRISONER_IRC_IN_CUSTODY.nomsId()) val RELEASABLE_ECSL_ACTIVE = generate(NotificationGenerator.PRISONER_RELEASED_ECSL_ACTIVE.nomsId()) val RELEASABLE_ECSL_INACTIVE = generate(NotificationGenerator.PRISONER_RELEASED_ECSL_INACTIVE.nomsId()) + val ABSCONDED = generate(NotificationGenerator.PRISONER_ABSCONDED.nomsId()) fun generate(nomsNumber: String, id: Long = IdGenerator.getAndIncrement()) = Person(id, nomsNumber) } diff --git a/projects/prison-custody-status-to-delius/src/dev/resources/messages/prisoner-absconded.json b/projects/prison-custody-status-to-delius/src/dev/resources/messages/prisoner-absconded.json new file mode 100644 index 0000000000..f9d757e30e --- /dev/null +++ b/projects/prison-custody-status-to-delius/src/dev/resources/messages/prisoner-absconded.json @@ -0,0 +1,17 @@ +{ + "Type": "Notification", + "MessageId": "682dd2a4-c193-461c-a8e9-cc976a961aa5", + "TopicArn": "", + "Message": "{\"eventType\":\"prison-offender-events.prisoner.released\",\"additionalInformation\":{\"nomsNumber\":\"A0017AA\",\"reason\":\"RELEASED\",\"details\":\"Movement reason code UAL\",\"nomisMovementReasonCode\":\"UAL\",\"currentLocation\":\"OUTSIDE_PRISON\",\"prisonId\":\"WSI\",\"currentPrisonStatus\":\"NOT_UNDER_PRISON_CARE\"},\"version\":1,\"occurredAt\":\"2023-11-20T09:30:00.00000000+01:00\",\"publishedAt\":\"2023-11-20T09:30:00.00000000+01:00\",\"description\":\"A prisoner has been released from prison\",\"personReference\":{\"identifiers\":[{\"type\":\"NOMS\",\"value\":\"A0017AA\"}]}}", + "Timestamp": "2022-05-04T07:00:33.487Z", + "SignatureVersion": "1", + "Signature": "", + "SigningCertURL": "", + "UnsubscribeURL": "", + "MessageAttributes": { + "eventType": { + "Type": "String", + "Value": "prison-offender-events.prisoner.released" + } + } +} \ No newline at end of file diff --git a/projects/prison-custody-status-to-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/PcstdIntegrationTest.kt b/projects/prison-custody-status-to-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/PcstdIntegrationTest.kt index 85e822da85..af2440f8d1 100644 --- a/projects/prison-custody-status-to-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/PcstdIntegrationTest.kt +++ b/projects/prison-custody-status-to-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/PcstdIntegrationTest.kt @@ -517,4 +517,54 @@ class PcstdIntegrationTest : PcstdIntegrationTestBase() { ) } } + + @Test + fun `prisoner absconded - unlawfully at large`() { + whenever(featureFlags.enabled("messages_released_absconded")).thenReturn(true) + val notification = NotificationGenerator.PRISONER_ABSCONDED + val nomsNumber = notification.nomsId() + assertFalse(getCustody(nomsNumber).isInCustody()) + + channelManager.getChannel(queueName).publishAndWait(notification) + + val ual = InstitutionGenerator.STANDARD_INSTITUTIONS[InstitutionCode.UNLAWFULLY_AT_LARGE]!! + val custody = getCustody(nomsNumber) + assertTrue(custody.isInCustody()) + assertThat(custody.institution?.code, equalTo(ual.code)) + assertThat(custody.status.code, equalTo(CustodialStatusCode.IN_CUSTODY.code)) + assertThat(custody.statusChangeDate, isCloseTo(notification.message.occurredAt)) + + verifyRecall(custody, notification.message.occurredAt, RecallReason.Code.NOTIFIED_BY_CUSTODIAL_ESTABLISHMENT) + + verifyCustodyHistory( + custody, + CustodyEventTester(CustodyEventTypeCode.STATUS_CHANGE, "Recall added unlawfully at large "), + CustodyEventTester(CustodyEventTypeCode.LOCATION_CHANGE, ual.description) + ) + + verifyContacts( + custody, + 2, + ContactType.Code.BREACH_PRISON_RECALL, + ContactType.Code.CHANGE_OF_INSTITUTION + ) + + licenceConditionRepository.findAll().filter { + it.disposal.id == custody.disposal.id + }.forEach { + assertNotNull(it.terminationDate) + assertThat(it.terminationReason?.code, equalTo("TEST")) + } + + verifyTelemetry("Recalled", "LocationUpdated", "StatusUpdated") { + mapOf( + "occurredAt" to notification.message.occurredAt.toString(), + "nomsNumber" to nomsNumber, + "institution" to "WSI", + "reason" to "RELEASED", + "movementReason" to "UAL", + "movementType" to "Released" + ) + } + } } diff --git a/projects/prison-custody-status-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/referencedata/wellknown/InstitutionCode.kt b/projects/prison-custody-status-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/referencedata/wellknown/InstitutionCode.kt index 8f4615e992..352dcf4bad 100644 --- a/projects/prison-custody-status-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/referencedata/wellknown/InstitutionCode.kt +++ b/projects/prison-custody-status-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/referencedata/wellknown/InstitutionCode.kt @@ -2,6 +2,7 @@ package uk.gov.justice.digital.hmpps.integrations.delius.referencedata.wellknown enum class InstitutionCode(val code: String) { IN_COMMUNITY("COMMUN"), + UNLAWFULLY_AT_LARGE("UATLRG"), UNKNOWN("UNKNOW"), OTHER_SECURE_UNIT("XXX056"), OTHER_IRC("XXX054") diff --git a/projects/prison-custody-status-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/messaging/MovementReasonCodes.kt b/projects/prison-custody-status-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/messaging/MovementReasonCodes.kt index 7d904c26e2..53187ac0ad 100644 --- a/projects/prison-custody-status-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/messaging/MovementReasonCodes.kt +++ b/projects/prison-custody-status-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/messaging/MovementReasonCodes.kt @@ -7,4 +7,6 @@ object MovementReasonCodes { const val DETAINED_MENTAL_HEALTH = "HO" const val RELEASE_MENTAL_HEALTH = "HQ" const val FINAL_DISCHARGE_PSYCHIATRIC = "HP" + const val ABSCONDED = "UAL" + const val ABSCONDED_ECL = "UAL_ECL" } diff --git a/projects/prison-custody-status-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/messaging/PrisonerMovement.kt b/projects/prison-custody-status-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/messaging/PrisonerMovement.kt index 8b67682ab6..70f59eb66a 100644 --- a/projects/prison-custody-status-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/messaging/PrisonerMovement.kt +++ b/projects/prison-custody-status-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/messaging/PrisonerMovement.kt @@ -52,4 +52,9 @@ sealed interface PrisonerMovement { MovementReasonCodes.DEPORTED_NO_SENTENCE, MovementReasonCodes.DEPORTED_LICENCE ) + + fun isAbsconded() = this is Released && reason in listOf( + MovementReasonCodes.ABSCONDED, + MovementReasonCodes.ABSCONDED_ECL + ) } diff --git a/projects/prison-custody-status-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/messaging/actions/RecallAction.kt b/projects/prison-custody-status-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/messaging/actions/RecallAction.kt index 84d34b9cb2..8f3c0f0f3c 100644 --- a/projects/prison-custody-status-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/messaging/actions/RecallAction.kt +++ b/projects/prison-custody-status-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/messaging/actions/RecallAction.kt @@ -116,12 +116,11 @@ class RecallAction( } } - PrisonerMovement.Type.RELEASED -> if (prisonerMovement.isHospitalRelease()) { - RecallReason.Code.TRANSFER_TO_SECURE_HOSPITAL - } else if (prisonerMovement.isIrcRelease()) { - RecallReason.Code.TRANSFER_TO_IRC - } else { - throw IgnorableMessageException("RecallNotSupported", prisonerMovement.telemetryProperties()) + PrisonerMovement.Type.RELEASED -> when { + prisonerMovement.isAbsconded() -> if (statusCode == CustodialStatusCode.CUSTODY_ROTL) RecallReason.Code.END_OF_TEMPORARY_LICENCE else RecallReason.Code.NOTIFIED_BY_CUSTODIAL_ESTABLISHMENT + prisonerMovement.isHospitalRelease() -> RecallReason.Code.TRANSFER_TO_SECURE_HOSPITAL + prisonerMovement.isIrcRelease() -> RecallReason.Code.TRANSFER_TO_IRC + else -> throw IgnorableMessageException("RecallNotSupported", prisonerMovement.telemetryProperties()) } else -> throw IgnorableMessageException("RecallNotSupported", prisonerMovement.telemetryProperties()) diff --git a/projects/prison-custody-status-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/messaging/actions/UpdateLocationAction.kt b/projects/prison-custody-status-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/messaging/actions/UpdateLocationAction.kt index 8342631aa0..80e9fb0656 100644 --- a/projects/prison-custody-status-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/messaging/actions/UpdateLocationAction.kt +++ b/projects/prison-custody-status-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/messaging/actions/UpdateLocationAction.kt @@ -86,12 +86,14 @@ class UpdateLocationAction( institutionRepository.getByCode(InstitutionCode.OTHER_IRC.code) } + isAbsconded() -> institutionRepository.getByCode(InstitutionCode.UNLAWFULLY_AT_LARGE.code) + else -> institutionRepository.getByCode(InstitutionCode.IN_COMMUNITY.code) } private fun createLocationChangeContact(prisonerMovement: PrisonerMovement, custody: Custody) { if (prisonerMovement is PrisonerMovement.Received || - prisonerMovement.isHospitalRelease() || prisonerMovement.isIrcRelease() + prisonerMovement.isHospitalRelease() || prisonerMovement.isIrcRelease() || prisonerMovement.isAbsconded() ) { val notes = """ |Custodial Status: ${custody.status.description} diff --git a/projects/prison-custody-status-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/messaging/actions/UpdateStatusAction.kt b/projects/prison-custody-status-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/messaging/actions/UpdateStatusAction.kt index a04b3a2d69..f92c1cb47a 100644 --- a/projects/prison-custody-status-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/messaging/actions/UpdateStatusAction.kt +++ b/projects/prison-custody-status-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/messaging/actions/UpdateStatusAction.kt @@ -56,6 +56,7 @@ class UpdateStatusAction( private fun outboundStatusChange(context: PrisonerMovementContext): ActionResult { val (prisonerMovement, custody) = context val statusCode = when { + prisonerMovement.isAbsconded() -> CustodialStatusCode.IN_CUSTODY prisonerMovement.isHospitalRelease() || prisonerMovement.isIrcRelease() -> custody.nextStatus() else -> if (custody.canBeReleased()) { CustodialStatusCode.RELEASED_ON_LICENCE @@ -70,6 +71,7 @@ class UpdateStatusAction( when { prisonerMovement.isHospitalRelease() -> "Transfer to/from Hospital" prisonerMovement.isIrcRelease() -> "Transfer to Immigration Removal Centre" + prisonerMovement.isAbsconded() -> if (custody.canBeRecalled()) "Recall added unlawfully at large " else "Absconded unlawfully at large " else -> "Released on Licence" } ) diff --git a/projects/prison-custody-status-to-delius/src/main/resources/application-prisoner-movement.yml b/projects/prison-custody-status-to-delius/src/main/resources/application-prisoner-movement.yml index 38e5e216b5..1c43ee14ba 100644 --- a/projects/prison-custody-status-to-delius/src/main/resources/application-prisoner-movement.yml +++ b/projects/prison-custody-status-to-delius/src/main/resources/application-prisoner-movement.yml @@ -50,6 +50,16 @@ prisoner.movement.configs: - UpdateStatus - UpdateLocation featureFlag: messages_released_ecsl + - types: + - RELEASED + reasons: + - UAL + - UAL_ECL + actionNames: + - Recall + - UpdateStatus + - UpdateLocation + featureFlag: messages_released_absconded - types: - RELEASED actionNames: diff --git a/projects/prison-custody-status-to-delius/src/test/kotlin/uk/gov/justice/digital/hmpps/messaging/actions/UpdateLocationActionTest.kt b/projects/prison-custody-status-to-delius/src/test/kotlin/uk/gov/justice/digital/hmpps/messaging/actions/UpdateLocationActionTest.kt index 3637027dcc..b5cdb8b42a 100644 --- a/projects/prison-custody-status-to-delius/src/test/kotlin/uk/gov/justice/digital/hmpps/messaging/actions/UpdateLocationActionTest.kt +++ b/projects/prison-custody-status-to-delius/src/test/kotlin/uk/gov/justice/digital/hmpps/messaging/actions/UpdateLocationActionTest.kt @@ -67,6 +67,9 @@ internal class UpdateLocationActionTest { if (prisonerMovement.type == RELEASED && prisonerMovement.reason.isBlank()) { whenever(institutionRepository.findByCode(InstitutionCode.IN_COMMUNITY.code)) .thenReturn(InstitutionGenerator.STANDARD_INSTITUTIONS[InstitutionCode.IN_COMMUNITY]) + } else if (prisonerMovement.isAbsconded()) { + whenever(institutionRepository.findByCode(InstitutionCode.UNLAWFULLY_AT_LARGE.code)) + .thenReturn(InstitutionGenerator.STANDARD_INSTITUTIONS[InstitutionCode.UNLAWFULLY_AT_LARGE]) } val res = action.accept(PrisonerMovementContext(prisonerMovement, custody)) @@ -114,7 +117,9 @@ internal class UpdateLocationActionTest { Arguments.of(custody, received), Arguments.of(custody, released.copy(type = RELEASED_TO_HOSPITAL, reason = "HP")), Arguments.of(custody, released.copy(reason = "HO")), - Arguments.of(released(), released) + Arguments.of(released(), released), + Arguments.of(absconded(), released.copy(type = RELEASED, reason = "UAL")), + Arguments.of(absconded(), released.copy(type = RELEASED, reason = "UAL_ECL")) ) private fun custody(): Custody { @@ -132,5 +137,15 @@ internal class UpdateLocationActionTest { ) return requireNotNull(event.disposal?.custody) } + + private fun absconded(): Custody { + val person = PersonGenerator.generate("A1234CD") + val event = custodialEvent( + person, + InstitutionGenerator.STANDARD_INSTITUTIONS[InstitutionCode.UNLAWFULLY_AT_LARGE], + CustodialStatusCode.IN_CUSTODY + ) + return requireNotNull(event.disposal?.custody) + } } }