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

VB-5220 - Migrate a prisoner balance #50

Merged
merged 13 commits into from
Mar 12, 2025
Merged
Show file tree
Hide file tree
Changes from 5 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 @@ -9,11 +9,13 @@ import java.time.LocalDate
@Entity
@Table(name = "PRISONER_DETAILS")
data class PrisonerDetails(

@Id
@Column(nullable = false)
val prisonerId: String,

@Column(nullable = false)
val lastAllocatedDate: LocalDate,
val lastVoAllocatedDate: LocalDate,

@Column(nullable = true)
val lastPvoAllocatedDate: LocalDate?,
)
Original file line number Diff line number Diff line change
@@ -1,8 +1,30 @@
package uk.gov.justice.digital.hmpps.visitallocationapi.repository

import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Modifying
import org.springframework.data.jpa.repository.Query
import org.springframework.stereotype.Repository
import org.springframework.transaction.annotation.Transactional
import uk.gov.justice.digital.hmpps.visitallocationapi.model.entity.PrisonerDetails
import java.time.LocalDate

@Repository
interface PrisonerDetailsRepository : JpaRepository<PrisonerDetails, Long>
interface PrisonerDetailsRepository : JpaRepository<PrisonerDetails, Long> {
fun findByPrisonerId(prisonerId: String): PrisonerDetails?

@Transactional
@Modifying
@Query(
value = "UPDATE prisoner_details SET last_vo_allocated_date = :newLastAllocatedDate WHERE prisoner_id = :prisonerId",
nativeQuery = true,
)
fun updatePrisonerLastVoAllocatedDate(prisonerId: String, newLastAllocatedDate: LocalDate)

@Transactional
@Modifying
@Query(
value = "UPDATE prisoner_details SET last_pvo_allocated_date = :newLastAllocatedDate WHERE prisoner_id = :prisonerId",
nativeQuery = true,
)
fun updatePrisonerLastPvoAllocatedDate(prisonerId: String, newLastAllocatedDate: LocalDate)
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ interface VisitOrderRepository : JpaRepository<VisitOrder, Long> {
WHERE prisoner_id = :prisonerId
AND type = 'PVO'
AND status = 'AVAILABLE'
AND created_timestamp < CURRENT_TIMESTAMP - INTERVAL '28 days'
AND CAST(created_timestamp AS DATE) < CURRENT_DATE - INTERVAL '28 days'
""",
nativeQuery = true,
)
Expand All @@ -74,9 +74,9 @@ interface VisitOrderRepository : JpaRepository<VisitOrder, Long> {
UPDATE visit_order
SET status = 'ACCUMULATED'
WHERE prisoner_id = :prisonerId
AND type = :#{#type.name()}
AND type = 'VO'
AND status = 'AVAILABLE'
AND created_timestamp < CURRENT_TIMESTAMP - INTERVAL '28 days'
AND CAST(created_timestamp AS DATE) < CURRENT_DATE - INTERVAL '28 days'
""",
nativeQuery = true,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import uk.gov.justice.digital.hmpps.visitallocationapi.enums.VisitOrderType
import uk.gov.justice.digital.hmpps.visitallocationapi.model.entity.VisitOrder
import uk.gov.justice.digital.hmpps.visitallocationapi.repository.VisitOrderAllocationPrisonJobRepository
import uk.gov.justice.digital.hmpps.visitallocationapi.repository.VisitOrderRepository
import java.time.LocalDate
import java.time.LocalDateTime

@Transactional
Expand All @@ -23,6 +24,7 @@ class AllocationService(
private val incentivesClient: IncentivesClient,
private val visitOrderRepository: VisitOrderRepository,
private val visitOrderAllocationPrisonJobRepository: VisitOrderAllocationPrisonJobRepository,
private val prisonerDetailsService: PrisonerDetailsService,
private val prisonerRetryService: PrisonerRetryService,
@Value("\${max.visit-orders:26}") val maxAccumulatedVisitOrders: Int,
) {
Expand Down Expand Up @@ -82,11 +84,20 @@ class AllocationService(

visitOrderRepository.saveAll(visitOrders)

updateLastAllocatedDates(prisoner, visitOrders)

LOG.info(
"Successfully generated ${visitOrders.size} visit orders for prisoner prisoner $prisonerId: " + "${visitOrders.count { it.type == VisitOrderType.PVO }} PVOs and ${visitOrders.count { it.type == VisitOrderType.VO }} VOs",
)
}

private fun updateLastAllocatedDates(prisoner: PrisonerDto, visitOrders: MutableList<VisitOrder>) {
prisonerDetailsService.updateVoLastCreatedDateOrCreatePrisoner(prisonerId = prisoner.prisonerId, LocalDate.now())
if (visitOrders.any { it.type == VisitOrderType.PVO }) {
prisonerDetailsService.updatePvoLastCreatedDate(prisonerId = prisoner.prisonerId, LocalDate.now())
}
}

private fun processPrisonerAccumulation(prisonerId: String) {
LOG.info("Entered AllocationService - processPrisonerAccumulation with prisonerId: $prisonerId")

Expand Down Expand Up @@ -120,19 +131,19 @@ class AllocationService(
)

private fun isDueVO(prisonerId: String): Boolean {
val lastVODate = visitOrderRepository.findLastAllocatedDate(prisonerId, VisitOrderType.VO)
return lastVODate == null || lastVODate <= LocalDateTime.now().minusDays(14)
val lastVODate = prisonerDetailsService.getPrisoner(prisonerId)?.lastVoAllocatedDate
return lastVODate == null || lastVODate <= LocalDate.now().minusDays(14)
}

private fun isDuePVO(prisonerId: String): Boolean {
val lastPVODate = visitOrderRepository.findLastAllocatedDate(prisonerId, VisitOrderType.PVO)
val lastPVODate = prisonerDetailsService.getPrisoner(prisonerId)?.lastPvoAllocatedDate

// If they haven't been given a PVO before, we wait until their VO due date to allocate it.
// If they haven't been given a PVO before, we wait until their VO due date to allocate it, to align the dates.
if (lastPVODate == null) {
return isDueVO(prisonerId)
}

return lastPVODate <= LocalDateTime.now().minusDays(28)
return lastPVODate <= LocalDate.now().minusDays(28)
}

private fun generateVos(prisoner: PrisonerDto, prisonIncentivesForPrisonerLevel: PrisonIncentiveAmountsDto): List<VisitOrder> {
Expand All @@ -143,7 +154,6 @@ class AllocationService(
visitOrders.add(createVisitOrder(prisoner.prisonerId, VisitOrderType.VO))
}
}

return visitOrders.toList()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,13 @@ class NomisSyncService(

private fun migrateLastAllocatedDate(migrationDto: VisitAllocationPrisonerMigrationDto) {
LOG.info("Migrating prisoner ${migrationDto.prisonerId} details (last allocated date - ${migrationDto.lastVoAllocationDate})")
prisonerDetailsRepository.save(PrisonerDetails(prisonerId = migrationDto.prisonerId, lastAllocatedDate = migrationDto.lastVoAllocationDate))

val lastPvoAllocatedDate = if (migrationDto.pvoBalance != 0) {
migrationDto.lastVoAllocationDate
} else {
null
}

prisonerDetailsRepository.save(PrisonerDetails(prisonerId = migrationDto.prisonerId, lastVoAllocatedDate = migrationDto.lastVoAllocationDate, lastPvoAllocatedDate = lastPvoAllocatedDate))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package uk.gov.justice.digital.hmpps.visitallocationapi.service

import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import uk.gov.justice.digital.hmpps.visitallocationapi.model.entity.PrisonerDetails
import uk.gov.justice.digital.hmpps.visitallocationapi.repository.PrisonerDetailsRepository
import java.time.LocalDate

@Transactional
@Service
class PrisonerDetailsService(private val prisonerDetailsRepository: PrisonerDetailsRepository) {
companion object {
val LOG: Logger = LoggerFactory.getLogger(this::class.java)
}

fun getPrisoner(prisonerId: String): PrisonerDetails? = prisonerDetailsRepository.findByPrisonerId(prisonerId)

fun updateVoLastCreatedDateOrCreatePrisoner(prisonerId: String, newLastAllocatedDate: LocalDate) {
LOG.info("Entered PrisonerDetailsService updateVoLastCreatedDateOrCreatePrisoner for prisoner $prisonerId with date $newLastAllocatedDate")
// Check if the prisoner exists
val prisonerDetails = prisonerDetailsRepository.findByPrisonerId(prisonerId)

if (prisonerDetails != null) {
LOG.info("Prisoner $prisonerId not found, creating new record")
// If prisoner exists, update the record
prisonerDetailsRepository.updatePrisonerLastVoAllocatedDate(prisonerId, newLastAllocatedDate)
} else {
// If prisoner does not exist, create a new record
val newPrisoner = PrisonerDetails(
prisonerId = prisonerId,
lastVoAllocatedDate = newLastAllocatedDate,
lastPvoAllocatedDate = null,
)
prisonerDetailsRepository.save(newPrisoner)
}
}

fun updatePvoLastCreatedDate(prisonerId: String, newLastAllocatedDate: LocalDate) {
LOG.info("Entered PrisonerDetailsService updatePvoLastCreatedDate for prisoner $prisonerId with date $newLastAllocatedDate")
prisonerDetailsRepository.updatePrisonerLastPvoAllocatedDate(prisonerId, newLastAllocatedDate)
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
CREATE TABLE prisoner_details
(
prisoner_id VARCHAR(80) NOT NULL PRIMARY KEY,
last_allocated_date DATE NOT NULL
prisoner_id VARCHAR(80) NOT NULL PRIMARY KEY,
last_vo_allocated_date DATE NOT NULL,
last_pvo_allocated_date DATE
);
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ class NomisSyncControllerTest : IntegrationTestBase() {
val prisonerDetails = prisonerDetailsRepository.findAll()
assertThat(prisonerDetails.size).isEqualTo(1)
assertThat(prisonerDetails.first().prisonerId).isEqualTo(prisonerMigrationDto.prisonerId)
assertThat(prisonerDetails.first().lastAllocatedDate).isEqualTo(prisonerMigrationDto.lastVoAllocationDate)
assertThat(prisonerDetails.first().lastVoAllocatedDate).isEqualTo(prisonerMigrationDto.lastVoAllocationDate)

val changeLog = changeLogRepository.findAll()
assertThat(changeLog.size).isEqualTo(1)
Expand Down Expand Up @@ -100,7 +100,7 @@ class NomisSyncControllerTest : IntegrationTestBase() {
val prisonerDetails = prisonerDetailsRepository.findAll()
assertThat(prisonerDetails.size).isEqualTo(1)
assertThat(prisonerDetails.first().prisonerId).isEqualTo(prisonerMigrationDto.prisonerId)
assertThat(prisonerDetails.first().lastAllocatedDate).isEqualTo(prisonerMigrationDto.lastVoAllocationDate)
assertThat(prisonerDetails.first().lastVoAllocatedDate).isEqualTo(prisonerMigrationDto.lastVoAllocationDate)

val changeLog = changeLogRepository.findAll()
assertThat(changeLog.size).isEqualTo(1)
Expand Down Expand Up @@ -136,7 +136,7 @@ class NomisSyncControllerTest : IntegrationTestBase() {
val prisonerDetails = prisonerDetailsRepository.findAll()
assertThat(prisonerDetails.size).isEqualTo(1)
assertThat(prisonerDetails.first().prisonerId).isEqualTo(prisonerMigrationDto.prisonerId)
assertThat(prisonerDetails.first().lastAllocatedDate).isEqualTo(prisonerMigrationDto.lastVoAllocationDate)
assertThat(prisonerDetails.first().lastVoAllocatedDate).isEqualTo(prisonerMigrationDto.lastVoAllocationDate)

val changeLog = changeLogRepository.findAll()
assertThat(changeLog.size).isEqualTo(1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.Mock
import org.mockito.Mockito.lenient
import org.mockito.Mockito.verify
import org.mockito.junit.jupiter.MockitoExtension
import org.mockito.kotlin.any
Expand All @@ -20,10 +19,12 @@ import uk.gov.justice.digital.hmpps.visitallocationapi.dto.incentives.PrisonerIn
import uk.gov.justice.digital.hmpps.visitallocationapi.dto.prisoner.search.PrisonerDto
import uk.gov.justice.digital.hmpps.visitallocationapi.enums.VisitOrderStatus
import uk.gov.justice.digital.hmpps.visitallocationapi.enums.VisitOrderType
import uk.gov.justice.digital.hmpps.visitallocationapi.model.entity.PrisonerDetails
import uk.gov.justice.digital.hmpps.visitallocationapi.model.entity.VisitOrder
import uk.gov.justice.digital.hmpps.visitallocationapi.repository.VisitOrderAllocationPrisonJobRepository
import uk.gov.justice.digital.hmpps.visitallocationapi.repository.VisitOrderRepository
import uk.gov.justice.digital.hmpps.visitallocationapi.service.AllocationService
import uk.gov.justice.digital.hmpps.visitallocationapi.service.PrisonerDetailsService
import uk.gov.justice.digital.hmpps.visitallocationapi.service.PrisonerRetryService
import java.time.LocalDate

Expand All @@ -39,6 +40,9 @@ class AllocationServiceTest {
@Mock
private lateinit var visitOrderRepository: VisitOrderRepository

@Mock
private lateinit var prisonerDetailsService: PrisonerDetailsService

@Mock
private lateinit var visitOrderAllocationPrisonJobRepository: VisitOrderAllocationPrisonJobRepository

Expand All @@ -49,7 +53,7 @@ class AllocationServiceTest {

@BeforeEach
fun setUp() {
allocationService = AllocationService(prisonerSearchClient, incentivesClient, visitOrderRepository, visitOrderAllocationPrisonJobRepository, prisonerRetryService, 26)
allocationService = AllocationService(prisonerSearchClient, incentivesClient, visitOrderRepository, visitOrderAllocationPrisonJobRepository, prisonerDetailsService, prisonerRetryService, 26)
}

// --- Start Allocation Tests --- \\
Expand Down Expand Up @@ -149,6 +153,7 @@ class AllocationServiceTest {
val prisonerId = "AA123456"
val prisonId = "MDI"
val prisoner = PrisonerDto(prisonerId = prisonerId, prisonId = prisonId)
val prisonerDetails = PrisonerDetails(prisonerId = prisonerId, lastVoAllocatedDate = LocalDate.now().minusDays(14), null)
val prisonerIncentive = PrisonerIncentivesDto(iepCode = "STD")
val prisonIncentiveAmounts = PrisonIncentiveAmountsDto(visitOrders = 2, privilegedVisitOrders = 0, levelCode = "STD")

Expand All @@ -164,6 +169,7 @@ class AllocationServiceTest {
)
whenever(incentivesClient.getPrisonIncentiveLevels(prisonId)).thenReturn(listOf(prisonIncentiveAmounts))
whenever(incentivesClient.getPrisonerIncentiveReviewHistory(prisoner.prisonerId)).thenReturn(prisonerIncentive)
whenever(prisonerDetailsService.getPrisoner(prisonerId)).thenReturn(prisonerDetails)

// Begin test
runBlocking {
Expand Down Expand Up @@ -194,6 +200,7 @@ class AllocationServiceTest {
val prisonerId = "AA123456"
val prisonId = "MDI"
val prisoner = PrisonerDto(prisonerId = prisonerId, prisonId = prisonId)
val prisonerDetails = PrisonerDetails(prisonerId = prisonerId, lastVoAllocatedDate = LocalDate.now().minusDays(14), LocalDate.now().minusDays(14))
val prisonerIncentive = PrisonerIncentivesDto(iepCode = "STD")
val prisonIncentiveAmounts = PrisonIncentiveAmountsDto(visitOrders = 2, privilegedVisitOrders = 1, levelCode = "STD")

Expand All @@ -210,8 +217,7 @@ class AllocationServiceTest {
whenever(incentivesClient.getPrisonIncentiveLevels(prisonId)).thenReturn(listOf(prisonIncentiveAmounts))
whenever(incentivesClient.getPrisonerIncentiveReviewHistory(prisoner.prisonerId)).thenReturn(prisonerIncentive)

lenient().whenever(visitOrderRepository.findLastAllocatedDate(prisoner.prisonerId, VisitOrderType.VO)).thenReturn(LocalDate.now().minusDays(14).atStartOfDay())
lenient().whenever(visitOrderRepository.findLastAllocatedDate(prisoner.prisonerId, VisitOrderType.PVO)).thenReturn(LocalDate.now().minusDays(14).atStartOfDay())
whenever(prisonerDetailsService.getPrisoner(prisonerId)).thenReturn(prisonerDetails)

// Begin test
runBlocking {
Expand Down Expand Up @@ -242,6 +248,7 @@ class AllocationServiceTest {
val prisonerId = "AA123456"
val prisonId = "MDI"
val prisoner = PrisonerDto(prisonerId = prisonerId, prisonId = prisonId)
val prisonerDetails = PrisonerDetails(prisonerId = prisonerId, lastVoAllocatedDate = LocalDate.now().minusDays(10), null)
val prisonerIncentive = PrisonerIncentivesDto(iepCode = "ENH")
val prisonIncentiveAmounts = PrisonIncentiveAmountsDto(visitOrders = 3, privilegedVisitOrders = 2, levelCode = "ENH")

Expand All @@ -258,8 +265,7 @@ class AllocationServiceTest {
whenever(incentivesClient.getPrisonIncentiveLevels(prisonId)).thenReturn(listOf(prisonIncentiveAmounts))
whenever(incentivesClient.getPrisonerIncentiveReviewHistory(prisoner.prisonerId)).thenReturn(prisonerIncentive)

whenever(visitOrderRepository.findLastAllocatedDate(prisoner.prisonerId, VisitOrderType.VO)).thenReturn(LocalDate.now().minusDays(10).atStartOfDay())
whenever(visitOrderRepository.findLastAllocatedDate(prisoner.prisonerId, VisitOrderType.PVO)).thenReturn(null)
whenever(prisonerDetailsService.getPrisoner(prisonerId)).thenReturn(prisonerDetails)

// Begin test
runBlocking {
Expand Down Expand Up @@ -287,6 +293,7 @@ class AllocationServiceTest {
val prisonerId = "AA123456"
val prisonId = "MDI"
val prisoner = PrisonerDto(prisonerId = prisonerId, prisonId = prisonId)
val prisonerDetails = PrisonerDetails(prisonerId = prisonerId, lastVoAllocatedDate = LocalDate.now().minusDays(14), LocalDate.now().minusDays(28))
val prisonerIncentive = PrisonerIncentivesDto(iepCode = "STD")
val prisonIncentiveAmounts = PrisonIncentiveAmountsDto(visitOrders = 2, privilegedVisitOrders = 1, levelCode = "STD")

Expand All @@ -303,8 +310,7 @@ class AllocationServiceTest {
whenever(incentivesClient.getPrisonIncentiveLevels(prisonId)).thenReturn(listOf(prisonIncentiveAmounts))
whenever(incentivesClient.getPrisonerIncentiveReviewHistory(prisoner.prisonerId)).thenReturn(prisonerIncentive)

lenient().whenever(visitOrderRepository.findLastAllocatedDate(prisoner.prisonerId, VisitOrderType.VO)).thenReturn(LocalDate.now().minusDays(14).atStartOfDay())
lenient().whenever(visitOrderRepository.findLastAllocatedDate(prisoner.prisonerId, VisitOrderType.PVO)).thenReturn(LocalDate.now().minusDays(28).atStartOfDay())
whenever(prisonerDetailsService.getPrisoner(prisonerId)).thenReturn(prisonerDetails)

// Begin test
runBlocking {
Expand Down Expand Up @@ -338,6 +344,7 @@ class AllocationServiceTest {
val prisonerId = "AA123456"
val prisonId = "MDI"
val prisoner = PrisonerDto(prisonerId = prisonerId, prisonId = prisonId)
val prisonerDetails = PrisonerDetails(prisonerId = prisonerId, lastVoAllocatedDate = LocalDate.now().minusDays(1), LocalDate.now().minusDays(14))
val prisonerIncentive = PrisonerIncentivesDto(iepCode = "STD")
val prisonIncentiveAmounts = PrisonIncentiveAmountsDto(visitOrders = 2, privilegedVisitOrders = 1, levelCode = "STD")

Expand All @@ -354,8 +361,7 @@ class AllocationServiceTest {
whenever(incentivesClient.getPrisonIncentiveLevels(prisonId)).thenReturn(listOf(prisonIncentiveAmounts))
whenever(incentivesClient.getPrisonerIncentiveReviewHistory(prisoner.prisonerId)).thenReturn(prisonerIncentive)

lenient().whenever(visitOrderRepository.findLastAllocatedDate(prisoner.prisonerId, VisitOrderType.VO)).thenReturn(LocalDate.now().minusDays(1).atStartOfDay())
lenient().whenever(visitOrderRepository.findLastAllocatedDate(prisoner.prisonerId, VisitOrderType.PVO)).thenReturn(LocalDate.now().minusDays(14).atStartOfDay())
whenever(prisonerDetailsService.getPrisoner(prisonerId)).thenReturn(prisonerDetails)

whenever(visitOrderRepository.countAllVisitOrders(prisoner.prisonerId, VisitOrderType.VO, VisitOrderStatus.ACCUMULATED)).thenReturn(4)

Expand All @@ -381,6 +387,7 @@ class AllocationServiceTest {
val prisonerId = "AA123456"
val prisonId = "MDI"
val prisoner = PrisonerDto(prisonerId = prisonerId, prisonId = prisonId)
val prisonerDetails = PrisonerDetails(prisonerId = prisonerId, lastVoAllocatedDate = LocalDate.now().minusDays(1), LocalDate.now().minusDays(14))
val prisonerIncentive = PrisonerIncentivesDto(iepCode = "STD")
val prisonIncentiveAmounts = PrisonIncentiveAmountsDto(visitOrders = 2, privilegedVisitOrders = 1, levelCode = "STD")

Expand All @@ -397,8 +404,7 @@ class AllocationServiceTest {
whenever(incentivesClient.getPrisonIncentiveLevels(prisonId)).thenReturn(listOf(prisonIncentiveAmounts))
whenever(incentivesClient.getPrisonerIncentiveReviewHistory(prisoner.prisonerId)).thenReturn(prisonerIncentive)

lenient().whenever(visitOrderRepository.findLastAllocatedDate(prisoner.prisonerId, VisitOrderType.VO)).thenReturn(LocalDate.now().minusDays(1).atStartOfDay())
lenient().whenever(visitOrderRepository.findLastAllocatedDate(prisoner.prisonerId, VisitOrderType.PVO)).thenReturn(LocalDate.now().minusDays(14).atStartOfDay())
whenever(prisonerDetailsService.getPrisoner(prisonerId)).thenReturn(prisonerDetails)

whenever(visitOrderRepository.countAllVisitOrders(prisoner.prisonerId, VisitOrderType.VO, VisitOrderStatus.ACCUMULATED)).thenReturn(28)

Expand Down
Loading
Loading