From b08717d82a0192a0cbde8f1cc5753495e4108a87 Mon Sep 17 00:00:00 2001 From: Marcus Aspin Date: Tue, 19 Nov 2024 16:07:09 +0000 Subject: [PATCH 1/2] PI-2625 Ignore historic custodial status and location updates (#4416) --- .../hmpps/data/generator/EventGenerator.kt | 6 +- .../data/generator/NotificationGenerator.kt | 1 + .../messages/prisoner-released-historic.json | 17 +++ .../digital/hmpps/PcstdIntegrationTest.kt | 20 ++++ .../delius/custody/entity/Custody.kt | 6 +- .../hmpps/messaging/PrisonerMovement.kt | 6 + .../messaging/actions/UpdateLocationAction.kt | 3 +- .../messaging/actions/UpdateStatusAction.kt | 105 +++++++++--------- .../actions/UpdateStatusActionTest.kt | 11 +- 9 files changed, 110 insertions(+), 65 deletions(-) create mode 100644 projects/prison-custody-status-to-delius/src/dev/resources/messages/prisoner-released-historic.json diff --git a/projects/prison-custody-status-to-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/EventGenerator.kt b/projects/prison-custody-status-to-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/EventGenerator.kt index 6c7f2d9c53..38367b30c9 100644 --- a/projects/prison-custody-status-to-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/EventGenerator.kt +++ b/projects/prison-custody-status-to-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/EventGenerator.kt @@ -44,6 +44,8 @@ object EventGenerator { institution: Institution?, custodialStatusCode: CustodialStatusCode = CustodialStatusCode.IN_CUSTODY, disposalDate: ZonedDateTime = ZonedDateTime.of(2022, 5, 1, 0, 0, 0, 0, EuropeLondon), + statusChangeDate: LocalDate = LocalDate.of(2020, 1, 1), + locationChangeDate: LocalDate = LocalDate.of(2020, 1, 1), lengthInDays: Long = 365, disposalCode: String = "DEF" ): Event { @@ -54,8 +56,8 @@ object EventGenerator { status = ReferenceDataGenerator.CUSTODIAL_STATUS[custodialStatusCode]!!, institution = institution, disposal = disposal, - statusChangeDate = LocalDate.now().minusDays(1), - locationChangeDate = LocalDate.now().minusDays(1) + statusChangeDate = statusChangeDate, + locationChangeDate = locationChangeDate ) disposal.custody = custody return event 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 dfbcc1a3e6..fde0fc64ba 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 @@ -25,4 +25,5 @@ object NotificationGenerator { val PRISONER_ETR_IN_CUSTODY = ResourceLoader.notification("prisoner-received-etr-custody") val PRISONER_ECSLIRC_IN_CUSTODY = ResourceLoader.notification("prisoner-received-ecslirc-custody") val PRISONER_ADMIN_MERGE = ResourceLoader.notification("prisoner-released-admin-merge") + val PRISONER_RELEASED_HISTORIC = ResourceLoader.notification("prisoner-released-historic") } diff --git a/projects/prison-custody-status-to-delius/src/dev/resources/messages/prisoner-released-historic.json b/projects/prison-custody-status-to-delius/src/dev/resources/messages/prisoner-released-historic.json new file mode 100644 index 0000000000..b9c3d8c394 --- /dev/null +++ b/projects/prison-custody-status-to-delius/src/dev/resources/messages/prisoner-released-historic.json @@ -0,0 +1,17 @@ +{ + "Type": "Notification", + "MessageId": "682dd2a4-c193-461c-a8e9-cc976a961aa5", + "TopicArn": "", + "Message": "{\"eventType\":\"prison-offender-events.prisoner.released\",\"additionalInformation\":{\"nomsNumber\":\"A0001AA\",\"reason\":\"RELEASED\",\"details\":\"Movement reason code NCS\",\"nomisMovementReasonCode\":\"NCS\",\"currentLocation\":\"OUTSIDE_PRISON\",\"prisonId\":\"WSI\",\"currentPrisonStatus\":\"NOT_UNDER_PRISON_CARE\"},\"version\":1,\"occurredAt\":\"2010-01-01T07:03:50.912169+01:00\",\"publishedAt\":\"2010-01-01T08:00:33.477735848+01:00\",\"description\":\"A prisoner has been released from prison\",\"personReference\":{\"identifiers\":[{\"type\":\"NOMS\",\"value\":\"A0001AA\"}]}}", + "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 1ccf2041f5..10774b826f 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 @@ -691,4 +691,24 @@ class PcstdIntegrationTest : PcstdIntegrationTestBase() { ) } } + + @Test + fun `status and location updates are ignored if the values have been updated more recently in Delius`() { + val notification = NotificationGenerator.PRISONER_RELEASED_HISTORIC + withBooking(BookingGenerator.RELEASED, BookingGenerator.RELEASED.lastMovement(notification.message.occurredAt)) + + channelManager.getChannel(queueName).publishAndWait(notification) + + verifyTelemetry("PrisonerLocationCorrect", "PrisonerStatusCorrect") { + mapOf( + "occurredAt" to notification.message.occurredAt.toString(), + "nomsNumber" to "A0001AA", + "previousInstitution" to "WSI", + "institution" to "OUT", + "reason" to "RELEASED", + "movementReason" to "NCS", + "movementType" to "Released" + ) + } + } } diff --git a/projects/prison-custody-status-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/custody/entity/Custody.kt b/projects/prison-custody-status-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/custody/entity/Custody.kt index e4b9787ed0..a3ab72bd8c 100644 --- a/projects/prison-custody-status-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/custody/entity/Custody.kt +++ b/projects/prison-custody-status-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/custody/entity/Custody.kt @@ -105,12 +105,10 @@ class Custody( dateTime: ZonedDateTime, detail: String, historyType: () -> ReferenceData - ): CustodyHistory? = if (this.status.code == status.code) { - null - } else { + ): CustodyHistory { this.status = status this.statusChangeDate = dateTime.toLocalDate() - CustodyHistory( + return CustodyHistory( date = dateTime, type = historyType(), detail = detail, 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 1c2a684e8a..0c28ebfcbd 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 @@ -74,3 +74,9 @@ fun PrisonerMovement.releaseDateValid(custody: Custody): Boolean { fun PrisonerMovement.receivedDateValid(custody: Custody): Boolean = !occurredAt.isAfter(ZonedDateTime.now()) && (custody.mostRecentRelease()?.date?.let { !occurredAt.isBefore(it) } ?: true) + +fun PrisonerMovement.statusDateValid(custody: Custody): Boolean = + occurredAt <= ZonedDateTime.now() && occurredAt.toLocalDate() >= custody.statusChangeDate + +fun PrisonerMovement.locationDateValid(custody: Custody): Boolean = + occurredAt <= ZonedDateTime.now() && occurredAt.toLocalDate() >= custody.locationChangeDate 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 4bbabc328e..fa823b69ec 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 @@ -119,7 +119,8 @@ class UpdateLocationAction( private fun checkPreconditions(prisonerMovement: PrisonerMovement, custody: Custody): ActionResult? { if ((prisonerMovement is PrisonerMovement.Received && custody.institution?.nomisCdeCode == prisonerMovement.toPrisonId) || (prisonerMovement is PrisonerMovement.Received && !prisonerMovement.receivedDateValid(custody)) || - (prisonerMovement is PrisonerMovement.Released && !prisonerMovement.releaseDateValid(custody)) + (prisonerMovement is PrisonerMovement.Released && !prisonerMovement.releaseDateValid(custody)) || + !prisonerMovement.locationDateValid(custody) ) { return ActionResult.Ignored("PrisonerLocationCorrect", prisonerMovement.telemetryProperties()) } 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 d64e21e23c..88ce12b46f 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 @@ -19,78 +19,83 @@ class UpdateStatusAction( private val custodyHistoryRepository: CustodyHistoryRepository ) : PrisonerMovementAction { override val name: String = "UpdateStatus" - override fun accept(context: PrisonerMovementContext): ActionResult = - when (context.prisonerMovement) { - is PrisonerMovement.Received -> { - inboundStatusChange(context) - } + override fun accept(context: PrisonerMovementContext): ActionResult { + val (prisonerMovement, custody) = context + + val result = checkPreconditions(prisonerMovement, custody) + if (result != null) return result - is PrisonerMovement.Released -> { - outboundStatusChange(context) - } + return when (context.prisonerMovement) { + is PrisonerMovement.Received -> inboundStatusChange(context) + is PrisonerMovement.Released -> outboundStatusChange(context) } + } private fun inboundStatusChange(context: PrisonerMovementContext): ActionResult { val (prisonerMovement, custody) = context - return if (custody.status.canChange() && prisonerMovement.receivedDateValid(custody)) { - val detail = if (custody.canBeRecalled()) "Recall added in custody " else "In custody " - updateStatus( - custody, - CustodialStatusCode.IN_CUSTODY, - prisonerMovement, - detail - ) - } else { - ActionResult.Ignored("PrisonerStatusCorrect", prisonerMovement.telemetryProperties()) - } + val detail = if (custody.canBeRecalled()) "Recall added in custody " else "In custody " + return updateStatus(custody, CustodialStatusCode.IN_CUSTODY, prisonerMovement, detail) } private fun outboundStatusChange(context: PrisonerMovementContext): ActionResult { val (prisonerMovement, custody) = context val statusCode = when { prisonerMovement.isHospitalRelease() || prisonerMovement.isIrcRelease() || prisonerMovement.isAbsconded() -> custody.nextStatus() - else -> if (custody.canBeReleased() && prisonerMovement.releaseDateValid(custody)) { - CustodialStatusCode.RELEASED_ON_LICENCE - } else { - throw IgnorableMessageException("PrisonerStatusCorrect") - } + else -> CustodialStatusCode.RELEASED_ON_LICENCE } - return updateStatus( - custody, - statusCode, - prisonerMovement, - when { - prisonerMovement.isHospitalRelease() -> "Transfer to/from Hospital" - prisonerMovement.isIrcRelease() -> "Transfer to Immigration Removal Centre" - prisonerMovement.isAbsconded() -> "Recall added unlawfully at large " - else -> "Released on Licence" - } - ) + val detail = when { + prisonerMovement.isHospitalRelease() -> "Transfer to/from Hospital" + prisonerMovement.isIrcRelease() -> "Transfer to Immigration Removal Centre" + prisonerMovement.isAbsconded() -> "Recall added unlawfully at large " + else -> "Released on Licence" + } + return updateStatus(custody, statusCode, prisonerMovement, detail) } - private fun Custody.nextStatus() = - when { - canBeRecalled() -> CustodialStatusCode.RECALLED - status.canChange() -> CustodialStatusCode.IN_CUSTODY - else -> throw IgnorableMessageException("PrisonerStatusCorrect") - } + private fun Custody.nextStatus() = when { + canBeRecalled() -> CustodialStatusCode.RECALLED + status.canChange() -> CustodialStatusCode.IN_CUSTODY + else -> throw IgnorableMessageException("PrisonerStatusCorrect") + } private fun updateStatus( custody: Custody, status: CustodialStatusCode, prisonerMovement: PrisonerMovement, detail: String - ): ActionResult = custody.updateStatusAt( - referenceDataRepository.getCustodialStatus(status.code), - prisonerMovement.occurredAt, - detail - ) { - referenceDataRepository.getCustodyEventType(CustodyEventTypeCode.STATUS_CHANGE.code) - }?.let { history -> + ): ActionResult = if (status.code == custody.status.code) { + ActionResult.Ignored("PrisonerStatusCorrect", prisonerMovement.telemetryProperties()) + } else { + val history = custody.updateStatusAt( + referenceDataRepository.getCustodialStatus(status.code), + prisonerMovement.occurredAt, + detail + ) { referenceDataRepository.getCustodyEventType(CustodyEventTypeCode.STATUS_CHANGE.code) } custodyRepository.save(custody) custodyHistoryRepository.save(history) - return ActionResult.Success(ActionResult.Type.StatusUpdated, prisonerMovement.telemetryProperties()) - } ?: ActionResult.Ignored("PrisonerStatusCorrect", prisonerMovement.telemetryProperties()) + ActionResult.Success(ActionResult.Type.StatusUpdated, prisonerMovement.telemetryProperties()) + } + + private fun checkPreconditions(prisonerMovement: PrisonerMovement, custody: Custody): ActionResult? { + if (prisonerMovement is PrisonerMovement.Received && + !(custody.status.canChange() && prisonerMovement.receivedDateValid(custody)) + ) { + return ActionResult.Ignored("PrisonerStatusCorrect", prisonerMovement.telemetryProperties()) + } + + if (prisonerMovement is PrisonerMovement.Released && + !(prisonerMovement.isHospitalRelease() || prisonerMovement.isIrcRelease() || prisonerMovement.isAbsconded()) && + !(custody.canBeReleased() && prisonerMovement.releaseDateValid(custody)) + ) { + return ActionResult.Ignored("PrisonerStatusCorrect", prisonerMovement.telemetryProperties()) + } + + if (!prisonerMovement.statusDateValid(custody)) { + return ActionResult.Ignored("PrisonerStatusCorrect", prisonerMovement.telemetryProperties()) + } + + return null + } } private fun ReferenceData.canChange() = !NO_CHANGE_STATUSES.map { it.code }.contains(code) diff --git a/projects/prison-custody-status-to-delius/src/test/kotlin/uk/gov/justice/digital/hmpps/messaging/actions/UpdateStatusActionTest.kt b/projects/prison-custody-status-to-delius/src/test/kotlin/uk/gov/justice/digital/hmpps/messaging/actions/UpdateStatusActionTest.kt index 52ee949fc8..4f628e7bae 100644 --- a/projects/prison-custody-status-to-delius/src/test/kotlin/uk/gov/justice/digital/hmpps/messaging/actions/UpdateStatusActionTest.kt +++ b/projects/prison-custody-status-to-delius/src/test/kotlin/uk/gov/justice/digital/hmpps/messaging/actions/UpdateStatusActionTest.kt @@ -15,12 +15,8 @@ import org.mockito.junit.jupiter.MockitoExtension import org.mockito.kotlin.any import org.mockito.kotlin.verify import org.mockito.kotlin.whenever -import uk.gov.justice.digital.hmpps.data.generator.CustodyGenerator -import uk.gov.justice.digital.hmpps.data.generator.EventGenerator +import uk.gov.justice.digital.hmpps.data.generator.* import uk.gov.justice.digital.hmpps.data.generator.EventGenerator.previouslyReleasedEvent -import uk.gov.justice.digital.hmpps.data.generator.InstitutionGenerator -import uk.gov.justice.digital.hmpps.data.generator.PersonGenerator -import uk.gov.justice.digital.hmpps.data.generator.ReferenceDataGenerator import uk.gov.justice.digital.hmpps.exception.IgnorableMessageException import uk.gov.justice.digital.hmpps.integrations.delius.custody.entity.Custody import uk.gov.justice.digital.hmpps.integrations.delius.custody.entity.CustodyHistoryRepository @@ -121,9 +117,8 @@ internal class UpdateStatusActionTest { ZonedDateTime.now().minusDays(1) ) - assertThrows { - action.accept(PrisonerMovementContext(prisonerMovement, custody)) - } + val result = action.accept(PrisonerMovementContext(prisonerMovement, custody)) + assertThat(result, instanceOf(ActionResult.Ignored::class.java)) } @ParameterizedTest From d9d185c2e00767bfeac6a4a60c31dfdf1ffcafc3 Mon Sep 17 00:00:00 2001 From: Marcus Aspin Date: Tue, 19 Nov 2024 17:55:44 +0000 Subject: [PATCH 2/2] PI-2625 Handle null location change date (#4443) --- .../uk/gov/justice/digital/hmpps/messaging/PrisonerMovement.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 0c28ebfcbd..2e610986db 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 @@ -79,4 +79,4 @@ fun PrisonerMovement.statusDateValid(custody: Custody): Boolean = occurredAt <= ZonedDateTime.now() && occurredAt.toLocalDate() >= custody.statusChangeDate fun PrisonerMovement.locationDateValid(custody: Custody): Boolean = - occurredAt <= ZonedDateTime.now() && occurredAt.toLocalDate() >= custody.locationChangeDate + occurredAt <= ZonedDateTime.now() && (custody.locationChangeDate == null || occurredAt.toLocalDate() >= custody.locationChangeDate)