Skip to content

Commit

Permalink
PI-1633 - Process UAL (Absconded) (#2714)
Browse files Browse the repository at this point in the history
  • Loading branch information
anthony-britton-moj authored Nov 20, 2023
1 parent 0d67049 commit 0a4108e
Show file tree
Hide file tree
Showing 13 changed files with 124 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ class DataLoader(
createIrcInCustody()
createReleasablePerson(PersonGenerator.RELEASABLE_ECSL_ACTIVE)
createReleasablePerson(PersonGenerator.RELEASABLE_ECSL_INACTIVE)
createAbsconded()
}

private fun createReferenceData() {
Expand Down Expand Up @@ -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<LicenceConditionCategory, Long>
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ object NotificationGenerator {
val PRISONER_IRC_IN_CUSTODY = ResourceLoader.notification<HmppsDomainEvent>("prisoner-received-irc-custody")
val PRISONER_RELEASED_ECSL_ACTIVE = ResourceLoader.notification<HmppsDomainEvent>("prisoner-released-ecsl-active")
val PRISONER_RELEASED_ECSL_INACTIVE = ResourceLoader.notification<HmppsDomainEvent>("prisoner-released-ecsl-inactive")
val PRISONER_ABSCONDED = ResourceLoader.notification<HmppsDomainEvent>("prisoner-absconded")
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
}
}
}

0 comments on commit 0a4108e

Please sign in to comment.