Skip to content

Commit

Permalink
PI-2578 Only apply probation reset suspension date for determinate ca…
Browse files Browse the repository at this point in the history
…ses (#4301)

Also use the event.first_release_date if no ACR is present.
  • Loading branch information
marcus-bcl authored Oct 10, 2024
1 parent 7fc52c5 commit 9a810c9
Show file tree
Hide file tree
Showing 9 changed files with 179 additions and 100 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,7 @@ import uk.gov.justice.digital.hmpps.data.generator.SentenceGenerator.generateCus
import uk.gov.justice.digital.hmpps.data.generator.SentenceGenerator.generateDisposal
import uk.gov.justice.digital.hmpps.data.generator.SentenceGenerator.generateEvent
import uk.gov.justice.digital.hmpps.data.generator.SentenceGenerator.generateOrderManager
import uk.gov.justice.digital.hmpps.data.repository.DatasetRepository
import uk.gov.justice.digital.hmpps.data.repository.DisposalRepository
import uk.gov.justice.digital.hmpps.data.repository.EventRepository
import uk.gov.justice.digital.hmpps.data.repository.OrderManagerRepository
import uk.gov.justice.digital.hmpps.data.repository.*
import uk.gov.justice.digital.hmpps.integrations.delius.custody.date.Custody
import uk.gov.justice.digital.hmpps.integrations.delius.custody.date.CustodyRepository
import uk.gov.justice.digital.hmpps.integrations.delius.custody.date.KeyDateRepository
Expand All @@ -37,6 +34,7 @@ class DataLoader(
private val eventRepository: EventRepository,
private val orderManagerRepository: OrderManagerRepository,
private val disposalRepository: DisposalRepository,
private val disposalTypeRepository: DisposalTypeRepository,
private val custodyRepository: CustodyRepository,
private val keyDateRepository: KeyDateRepository
) : ApplicationListener<ApplicationReadyEvent> {
Expand All @@ -57,6 +55,7 @@ class DataLoader(
referenceDataRepository.save(ReferenceDataGenerator.DEFAULT_CUSTODY_STATUS)
referenceDataRepository.saveAll(ReferenceDataGenerator.KEY_DATE_TYPES.values)
contactTypeRepository.save(ContactTypeGenerator.EDSS)
disposalTypeRepository.save(SentenceGenerator.DEFAULT_DISPOSAL_TYPE)

personRepository.save(PersonGenerator.DEFAULT)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
package uk.gov.justice.digital.hmpps.data.generator

import uk.gov.justice.digital.hmpps.integrations.delius.custody.date.Custody
import uk.gov.justice.digital.hmpps.integrations.delius.custody.date.Disposal
import uk.gov.justice.digital.hmpps.integrations.delius.custody.date.Event
import uk.gov.justice.digital.hmpps.integrations.delius.custody.date.OrderManager
import uk.gov.justice.digital.hmpps.integrations.delius.custody.date.*
import uk.gov.justice.digital.hmpps.integrations.delius.custody.date.reference.ReferenceData
import uk.gov.justice.digital.hmpps.integrations.delius.person.Person
import java.time.LocalDate

object SentenceGenerator {
var DEFAULT_CUSTODY: Custody =
Custody(IdGenerator.getAndIncrement(), ReferenceDataGenerator.DEFAULT_CUSTODY_STATUS, "38339A")

fun generateEvent(person: Person = PersonGenerator.DEFAULT, number: String = "1") =
Event(IdGenerator.getAndIncrement(), number, person)
var DEFAULT_DISPOSAL_TYPE = generateDisposalType()

fun generateEvent(
person: Person = PersonGenerator.DEFAULT,
number: String = "1",
firstReleaseDate: LocalDate? = null
) =
Event(IdGenerator.getAndIncrement(), number, person, firstReleaseDate)

fun generateOrderManager(event: Event, providerId: Long = 1, teamId: Long = 2, staffId: Long = 3) =
OrderManager(IdGenerator.getAndIncrement(), event, providerId, teamId, staffId)

fun generateDisposal(event: Event) = Disposal(IdGenerator.getAndIncrement(), event)
fun generateDisposal(event: Event, type: DisposalType = DEFAULT_DISPOSAL_TYPE) =
Disposal(IdGenerator.getAndIncrement(), event, type)

fun generateDisposalType(requiredInformation: String = "L1") =
DisposalType(IdGenerator.getAndIncrement(), requiredInformation)

fun generateCustodialSentence(
custodyStatus: ReferenceData = ReferenceDataGenerator.DEFAULT_CUSTODY_STATUS,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package uk.gov.justice.digital.hmpps.data.repository

import org.springframework.data.jpa.repository.JpaRepository
import uk.gov.justice.digital.hmpps.integrations.delius.custody.date.Disposal
import uk.gov.justice.digital.hmpps.integrations.delius.custody.date.DisposalType
import uk.gov.justice.digital.hmpps.integrations.delius.custody.date.Event
import uk.gov.justice.digital.hmpps.integrations.delius.custody.date.OrderManager

interface EventRepository : JpaRepository<Event, Long>
interface DisposalRepository : JpaRepository<Disposal, Long>
interface DisposalTypeRepository : JpaRepository<DisposalType, Long>
interface OrderManagerRepository : JpaRepository<OrderManager, Long>
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
package uk.gov.justice.digital.hmpps.integrations.delius.custody.date

import uk.gov.justice.digital.hmpps.integrations.prison.SentenceDetail
import java.time.LocalDate
import kotlin.reflect.KProperty

enum class CustodyDateType(val code: String, val field: KProperty<LocalDate?>) {
LICENCE_EXPIRY_DATE("LED", SentenceDetail::licenceExpiryDate),
AUTOMATIC_CONDITIONAL_RELEASE_DATE("ACR", SentenceDetail::conditionalReleaseDate),
PAROLE_ELIGIBILITY_DATE("PED", SentenceDetail::paroleEligibilityDate),
SENTENCE_EXPIRY_DATE("SED", SentenceDetail::sentenceExpiryDate),
EXPECTED_RELEASE_DATE("EXP", SentenceDetail::confirmedReleaseDate),
HDC_EXPECTED_DATE("HDE", SentenceDetail::homeDetentionCurfewEligibilityDate),
POST_SENTENCE_SUPERVISION_END_DATE("PSSED", SentenceDetail::postSentenceSupervisionEndDate),
SUSPENSION_DATE_IF_RESET("PR1", SentenceDetail::suspensionDateIfReset)
enum class CustodyDateType(val code: String) {
LICENCE_EXPIRY_DATE("LED"),
AUTOMATIC_CONDITIONAL_RELEASE_DATE("ACR"),
PAROLE_ELIGIBILITY_DATE("PED"),
SENTENCE_EXPIRY_DATE("SED"),
EXPECTED_RELEASE_DATE("EXP"),
HDC_EXPECTED_DATE("HDE"),
POST_SENTENCE_SUPERVISION_END_DATE("PSSED"),
SUSPENSION_DATE_IF_RESET("PR1")
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import org.springframework.web.client.RestClientResponseException
import uk.gov.justice.digital.hmpps.flags.FeatureFlags
import uk.gov.justice.digital.hmpps.integrations.delius.custody.date.CustodyDateType.*
import uk.gov.justice.digital.hmpps.integrations.delius.custody.date.contact.ContactService
import uk.gov.justice.digital.hmpps.integrations.delius.custody.date.reference.ReferenceDataRepository
import uk.gov.justice.digital.hmpps.integrations.delius.custody.date.reference.findKeyDateType
Expand All @@ -13,6 +14,8 @@ import uk.gov.justice.digital.hmpps.integrations.prison.Booking
import uk.gov.justice.digital.hmpps.integrations.prison.PrisonApiClient
import uk.gov.justice.digital.hmpps.integrations.prison.SentenceDetail
import uk.gov.justice.digital.hmpps.telemetry.TelemetryService
import java.time.LocalDate
import java.time.temporal.ChronoUnit.DAYS

@Service
@Transactional
Expand Down Expand Up @@ -51,39 +54,47 @@ class CustodyDateUpdateService(
}
val custody = custodyRepository.findCustodyById(custodyRepository.findForUpdate(custodyId))
val updated = calculateKeyDateChanges(sentenceDetail, custody)
.filter { it.type.code != CustodyDateType.SUSPENSION_DATE_IF_RESET.code || featureFlags.enabled("suspension-date-if-reset") }
if (updated.isEmpty()) {
telemetryService.trackEvent(
"KeyDatesUnchanged",
booking.telemetry(clientSource)
)
telemetryService.trackEvent("KeyDatesUnchanged", booking.telemetry(clientSource))
} else {
if (!dryRun) {
updated.ifNotEmpty(keyDateRepository::saveAll)
keyDateRepository.saveAll(updated)
contactService.createForKeyDateChanges(custody, updated)
}
telemetryService.trackEvent(
if (dryRun) "KeyDatesDryRun" else "KeyDatesUpdated",
booking.telemetry(clientSource) +
updated.associateBy({ it.type.code }, { it.date.toString() })
booking.telemetry(clientSource) + updated.associateBy({ it.type.code }, { it.date.toString() })
)
}
}

private fun List<KeyDate>.ifNotEmpty(code: (List<KeyDate>) -> Unit) = code(this)
private fun calculateKeyDateChanges(sentenceDetail: SentenceDetail, custody: Custody) = listOfNotNull(
custody.keyDate(LICENCE_EXPIRY_DATE.code, sentenceDetail.licenceExpiryDate),
custody.keyDate(AUTOMATIC_CONDITIONAL_RELEASE_DATE.code, sentenceDetail.conditionalReleaseDate),
custody.keyDate(PAROLE_ELIGIBILITY_DATE.code, sentenceDetail.paroleEligibilityDate),
custody.keyDate(SENTENCE_EXPIRY_DATE.code, sentenceDetail.sentenceExpiryDate),
custody.keyDate(EXPECTED_RELEASE_DATE.code, sentenceDetail.confirmedReleaseDate),
custody.keyDate(HDC_EXPECTED_DATE.code, sentenceDetail.homeDetentionCurfewEligibilityDate),
custody.keyDate(POST_SENTENCE_SUPERVISION_END_DATE.code, sentenceDetail.postSentenceSupervisionEndDate),
custody.keyDate(SUSPENSION_DATE_IF_RESET.code, suspensionDateIfReset(sentenceDetail, custody)),
)

private fun calculateKeyDateChanges(
sentenceDetail: SentenceDetail,
custody: Custody
): List<KeyDate> = CustodyDateType.entries.mapNotNull { cdt ->
cdt.field.getter.call(sentenceDetail)?.let {
val existing = custody.keyDates.find(cdt.code)
if (existing != null) {
existing.changeDate(it)
} else {
val kdt = referenceDataRepository.findKeyDateType(cdt.code)
KeyDate(null, custody, kdt, it)
}
fun suspensionDateIfReset(sentenceDetail: SentenceDetail, custody: Custody): LocalDate? = custody.disposal
?.takeIf { featureFlags.enabled("suspension-date-if-reset") }
?.takeIf { it.type.determinateSentence }
?.let {
val startDate = sentenceDetail.conditionalReleaseDate ?: it.event.firstReleaseDate ?: return null
val endDate = sentenceDetail.sentenceExpiryDate ?: return null
return if (startDate < endDate) startDate.plusDays(DAYS.between(startDate, endDate) * 2 / 3) else null
}

private fun Custody.keyDate(code: String, date: LocalDate?): KeyDate? = date?.let {
val existing = keyDates.find(code)
return if (existing != null) {
existing.changeDate(date)
} else {
val kdt = referenceDataRepository.findKeyDateType(code)
KeyDate(null, this, kdt, date)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
package uk.gov.justice.digital.hmpps.integrations.delius.custody.date

import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.Id
import jakarta.persistence.JoinColumn
import jakarta.persistence.ManyToOne
import jakarta.persistence.OneToMany
import jakarta.persistence.OneToOne
import jakarta.persistence.Table
import jakarta.persistence.*
import org.hibernate.annotations.Immutable
import org.hibernate.annotations.SQLRestriction
import uk.gov.justice.digital.hmpps.integrations.delius.custody.date.reference.ReferenceData
import uk.gov.justice.digital.hmpps.integrations.delius.person.Person
import java.time.LocalDate

@Immutable
@Entity
Expand All @@ -26,6 +20,9 @@ class Event(
@JoinColumn(name = "offender_id", nullable = false)
val person: Person,

@Column
val firstReleaseDate: LocalDate? = null,

@Column(name = "active_flag", columnDefinition = "NUMBER", nullable = false)
val active: Boolean = true,

Expand All @@ -51,13 +48,31 @@ class Disposal(
@JoinColumn(name = "event_id", updatable = false)
val event: Event,

@ManyToOne
@JoinColumn(name = "disposal_type_id")
val type: DisposalType,

@Column(name = "active_flag", updatable = false, columnDefinition = "NUMBER")
val active: Boolean = true,

@Column(updatable = false, columnDefinition = "NUMBER")
val softDeleted: Boolean = false
)

@Entity
@Immutable
@Table(name = "r_disposal_type")
data class DisposalType(
@Id
@Column(name = "disposal_type_id")
val id: Long,

@Column
val requiredInformation: String,
) {
val determinateSentence: Boolean = requiredInformation == "L1"
}

@Immutable
@Entity
class Custody(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package uk.gov.justice.digital.hmpps.integrations.prison

import com.fasterxml.jackson.annotation.JsonAlias
import java.time.LocalDate
import java.time.temporal.ChronoUnit.DAYS

class SentenceDetail(
val sentenceExpiryDate: LocalDate? = null,
Expand All @@ -16,9 +15,4 @@ class SentenceDetail(
val homeDetentionCurfewEligibilityDate: LocalDate? = null
) {
val conditionalReleaseDate = conditionalReleaseOverrideDate ?: conditionalReleaseDate

val suspensionDateIfReset =
if (conditionalReleaseDate != null && sentenceExpiryDate != null && sentenceExpiryDate > conditionalReleaseDate) {
conditionalReleaseDate.plusDays(DAYS.between(conditionalReleaseDate, sentenceExpiryDate) * 2 / 3)
} else null
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
package uk.gov.justice.digital.hmpps.integrations.delius.custody.date

import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.equalTo
import org.hamcrest.Matchers.nullValue
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments.arguments
import org.junit.jupiter.params.provider.MethodSource
import org.mockito.ArgumentMatchers.anyList
import org.mockito.InjectMocks
import org.mockito.Mock
import org.mockito.junit.jupiter.MockitoExtension
import org.mockito.kotlin.*
import uk.gov.justice.digital.hmpps.data.generator.PersonGenerator
import uk.gov.justice.digital.hmpps.data.generator.SentenceGenerator
import uk.gov.justice.digital.hmpps.data.generator.SentenceGenerator.generateCustodialSentence
import uk.gov.justice.digital.hmpps.data.generator.SentenceGenerator.generateDisposal
import uk.gov.justice.digital.hmpps.data.generator.SentenceGenerator.generateDisposalType
import uk.gov.justice.digital.hmpps.data.generator.SentenceGenerator.generateEvent
import uk.gov.justice.digital.hmpps.flags.FeatureFlags
import uk.gov.justice.digital.hmpps.integrations.delius.custody.date.contact.ContactService
import uk.gov.justice.digital.hmpps.integrations.delius.custody.date.reference.ReferenceDataRepository
Expand Down Expand Up @@ -135,4 +145,89 @@ internal class CustodyDateUpdateServiceTest {
verify(keyDateRepository, never()).saveAll(anyList())
verify(keyDateRepository, never()).deleteAll(anyList())
}

@Test
fun `two-thirds point uses event first release date if no conditional release date is available`() {
whenever(featureFlags.enabled("suspension-date-if-reset")).thenReturn(true)
val event = generateEvent(firstReleaseDate = LocalDate.of(2025, 1, 1))
val disposal = generateDisposal(event)
val custody = generateCustodialSentence(disposal = disposal, bookingRef = "ABC")

val suspensionDateIfReset = custodyDateUpdateService.suspensionDateIfReset(
SentenceDetail(
conditionalReleaseDate = null,
sentenceExpiryDate = LocalDate.of(2026, 1, 1)
), custody
)

assertThat(suspensionDateIfReset, equalTo(LocalDate.of(2025, 9, 1)))
}

@Test
fun `two-thirds point is null when event is not determinate`() {
whenever(featureFlags.enabled("suspension-date-if-reset")).thenReturn(true)
val event = generateEvent()
val disposal = generateDisposal(event, generateDisposalType("L2"))
val custody = generateCustodialSentence(disposal = disposal, bookingRef = "ABC")

val suspensionDateIfReset = custodyDateUpdateService.suspensionDateIfReset(
SentenceDetail(
conditionalReleaseDate = LocalDate.of(2024, 1, 1),
sentenceExpiryDate = LocalDate.of(2025, 1, 1)
), custody
)

assertThat(suspensionDateIfReset, nullValue())
}

@Test
fun `two-thirds point is null when feature flag is disabled`() {
whenever(featureFlags.enabled("suspension-date-if-reset")).thenReturn(false)

val custody = generateCustodialSentence(disposal = generateDisposal(generateEvent()), bookingRef = "ABC")
val suspensionDateIfReset = custodyDateUpdateService.suspensionDateIfReset(
SentenceDetail(
conditionalReleaseDate = LocalDate.of(2025, 1, 1),
sentenceExpiryDate = LocalDate.of(2026, 1, 1),
), custody
)

assertThat(suspensionDateIfReset, nullValue())
}

@ParameterizedTest
@MethodSource("testCases")
fun `check two-thirds point`(
conditionalReleaseDate: LocalDate?,
sentenceExpiryDate: LocalDate?,
expected: LocalDate?
) {
whenever(featureFlags.enabled("suspension-date-if-reset")).thenReturn(true)

val custody = generateCustodialSentence(disposal = generateDisposal(generateEvent()), bookingRef = "ABC")
val suspensionDateIfReset = custodyDateUpdateService.suspensionDateIfReset(
SentenceDetail(
conditionalReleaseDate = conditionalReleaseDate,
sentenceExpiryDate = sentenceExpiryDate
), custody
)

assertThat(suspensionDateIfReset, equalTo(expected))
}

companion object {
@JvmStatic
private fun testCases() = listOf(
arguments(null, LocalDate.of(2025, 1, 1), null),
arguments(LocalDate.of(2025, 1, 1), null, null),
arguments(LocalDate.of(2025, 1, 2), LocalDate.of(2025, 1, 1), null),
arguments(LocalDate.of(2025, 1, 1), LocalDate.of(2025, 1, 1), null),
arguments(LocalDate.of(2025, 1, 1), LocalDate.of(2025, 1, 2), LocalDate.of(2025, 1, 1)),
arguments(LocalDate.of(2025, 1, 1), LocalDate.of(2025, 1, 3), LocalDate.of(2025, 1, 2)),
arguments(LocalDate.of(2025, 1, 1), LocalDate.of(2025, 1, 4), LocalDate.of(2025, 1, 3)),
arguments(LocalDate.of(2025, 1, 1), LocalDate.of(2026, 1, 1), LocalDate.of(2025, 9, 1)),
arguments(LocalDate.of(2028, 2, 29), LocalDate.of(2028, 3, 30), LocalDate.of(2028, 3, 20)),
arguments(LocalDate.of(2099, 6, 30), LocalDate.of(2120, 2, 29), LocalDate.of(2113, 4, 10)),
)
}
}
Loading

0 comments on commit 9a810c9

Please sign in to comment.