Skip to content

Commit

Permalink
Merge branch 'main' into MAN-108-record-if-complied
Browse files Browse the repository at this point in the history
  • Loading branch information
achimber-moj authored Nov 19, 2024
2 parents fcdc174 + d9d185c commit 88881f8
Show file tree
Hide file tree
Showing 9 changed files with 110 additions and 65 deletions.
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 @@ -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() && (custody.locationChangeDate == null || 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

0 comments on commit 88881f8

Please sign in to comment.