Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PI-2625 Ignore historic custodial status and location updates #4416

Merged
merged 1 commit into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@ object NotificationGenerator {
val PRISONER_ETR_IN_CUSTODY = ResourceLoader.notification<HmppsDomainEvent>("prisoner-received-etr-custody")
val PRISONER_ECSLIRC_IN_CUSTODY = ResourceLoader.notification<HmppsDomainEvent>("prisoner-received-ecslirc-custody")
val PRISONER_ADMIN_MERGE = ResourceLoader.notification<HmppsDomainEvent>("prisoner-released-admin-merge")
val PRISONER_RELEASED_HISTORIC = ResourceLoader.notification<HmppsDomainEvent>("prisoner-released-historic")
}
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\":\"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"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,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
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -121,9 +117,8 @@ internal class UpdateStatusActionTest {
ZonedDateTime.now().minusDays(1)
)

assertThrows<IgnorableMessageException> {
action.accept(PrisonerMovementContext(prisonerMovement, custody))
}
val result = action.accept(PrisonerMovementContext(prisonerMovement, custody))
assertThat(result, instanceOf(ActionResult.Ignored::class.java))
}

@ParameterizedTest
Expand Down
Loading