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

APS-1752 Create Seed Job to import Bookings from Delius that don't exist in CAS1 #2763

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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 @@ -286,7 +286,7 @@ data class Cas1SpaceBookingEntity(
@JoinColumn(name = "placement_request_id")
val placementRequest: PlacementRequestEntity?,
/**
* createdAt will only be null for migrated [BookingEntity]s where no 'Booking Made' domain event
* createdBy will only be null for migrated [BookingEntity]s where no 'Booking Made' domain event
* existed for the booking (i.e. those migrated into the system when it went live)
*/
@ManyToOne(fetch = FetchType.LAZY)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import jakarta.persistence.Id
import jakarta.persistence.Table
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.OffenderService
import java.time.OffsetDateTime
import java.util.UUID

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import jakarta.persistence.Entity
import jakarta.persistence.Id
import jakarta.persistence.Table
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query
import org.springframework.stereotype.Repository
import java.time.LocalDate
import java.time.OffsetDateTime
Expand All @@ -12,6 +13,40 @@ import java.util.UUID
@Repository
interface Cas1DeliusBookingImportRepository : JpaRepository<Cas1DeliusBookingImportEntity, UUID> {
fun findByBookingId(id: UUID): Cas1DeliusBookingImportEntity?

/**
* Returns all active bookings created in delius that were not created in CAS1
*
* An active booking is one that:
*
* 1. does not have a departure recorded (and optionally, no arrival recorded)
* 2. does not have a non arrival recorded
*
* We also exclude any bookings where the departure date is before 1/1/2025 or after 1/1/2035.
* This filters out several bookings where the dates have been set incorrectly but are clearly inactive/complete,
* most likely because they were created in older versions of delius that did not capture this data
*
* Note that the [Cas1DeliusBookingImportEntity] table only includes accepted bookings (i.e. not rejected),
* so these are already filtered out
*/
@Query(
"""
FROM Cas1DeliusBookingImportEntity i
WHERE
i.bookingId IS NULL AND
i.premisesQcode = :qCode AND
i.departureDate IS NULL AND
i.nonArrivalReasonCode IS NULL AND
i.expectedDepartureDate > :minExpectedDepartureDate AND
i.expectedDepartureDate < :maxExpectedDepartureDate

""",
)
fun findActiveBookingsCreatedInDelius(
qCode: String,
minExpectedDepartureDate: LocalDate,
maxExpectedDepartureDate: LocalDate,
): List<Cas1DeliusBookingImportEntity>
}

@Entity
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.config.SeedConfig
import uk.gov.justice.digital.hmpps.approvedpremisesapi.seed.cas1.ApStaffUsersSeedJob
import uk.gov.justice.digital.hmpps.approvedpremisesapi.seed.cas1.ApprovedPremisesBookingCancelSeedJob
import uk.gov.justice.digital.hmpps.approvedpremisesapi.seed.cas1.ApprovedPremisesRoomsSeedJob
import uk.gov.justice.digital.hmpps.approvedpremisesapi.seed.cas1.Cas1BackfillActiveSpaceBookingsCreatedInDelius
import uk.gov.justice.digital.hmpps.approvedpremisesapi.seed.cas1.Cas1BookingToSpaceBookingSeedJob
import uk.gov.justice.digital.hmpps.approvedpremisesapi.seed.cas1.Cas1CruManagementAreaSeedJob
import uk.gov.justice.digital.hmpps.approvedpremisesapi.seed.cas1.Cas1DomainEventReplaySeedJob
Expand Down Expand Up @@ -90,6 +91,7 @@ class SeedService(
SeedFileType.approvedPremisesImportDeliusReferrals -> getBean(Cas1ImportDeliusReferralsSeedJob::class)
SeedFileType.approvedPremisesUpdateSpaceBooking -> getBean(Cas1UpdateSpaceBookingSeedJob::class)
SeedFileType.temporaryAccommodationReferralRejection -> getBean(Cas3ReferralRejectionSeedJob::class)
SeedFileType.approvedPremisesBackfillActiveSpaceBookingsCreatedInDelius -> getBean(Cas1BackfillActiveSpaceBookingsCreatedInDelius::class)
}

val seedStarted = LocalDateTime.now()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package uk.gov.justice.digital.hmpps.approvedpremisesapi.seed.cas1

import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import org.springframework.transaction.support.TransactionTemplate
import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.ServiceName
import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApprovedPremisesEntity
import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApprovedPremisesRepository
import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas1SpaceBookingEntity
import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas1SpaceBookingRepository
import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.CharacteristicEntity
import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.OfflineApplicationEntity
import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.OfflineApplicationRepository
import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas1.Cas1DeliusBookingImportEntity
import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas1.Cas1DeliusBookingImportRepository
import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.PersonSummaryInfoResult
import uk.gov.justice.digital.hmpps.approvedpremisesapi.seed.SeedJob
import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.EnvironmentService
import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.OffenderService
import java.time.LocalDate
import java.time.OffsetDateTime
import java.util.UUID

/**
* Before this job is ran, corresponding referrals must be imported from delius into the
* 'cas1_delius_booking_import' table using the [Cas1ImportDeliusReferralsSeedJob] job
*/
@Service
class Cas1BackfillActiveSpaceBookingsCreatedInDelius(
private val approvedPremisesRepository: ApprovedPremisesRepository,
private val cas1DeliusBookingImportRepository: Cas1DeliusBookingImportRepository,
private val cas1BookingManagementInfoService: Cas1BookingManagementInfoService,
private val environmentService: EnvironmentService,
private val offenderService: OffenderService,
private val offlineApplicationRepository: OfflineApplicationRepository,
private val spaceBookingRepository: Cas1SpaceBookingRepository,
private val transactionTemplate: TransactionTemplate,
) : SeedJob<Cas1CreateMissingReferralsSeedCsvRow>(
requiredHeaders = setOf(
"q_code",
),
runInTransaction = false,
) {
private val log = LoggerFactory.getLogger(this::class.java)

override fun deserializeRow(columns: Map<String, String>) = Cas1CreateMissingReferralsSeedCsvRow(
qCode = columns["q_code"]!!.trim(),
)

override fun preSeed() {
if (environmentService.isProd()) {
error("Cannot run seed job in prod")
}
}

override fun processRow(row: Cas1CreateMissingReferralsSeedCsvRow) {
transactionTemplate.executeWithoutResult {
migratePremise(row.qCode)
}
}

@SuppressWarnings("TooGenericExceptionCaught", "MagicNumber")
private fun migratePremise(qCode: String) {
val premises = approvedPremisesRepository.findByQCode(qCode) ?: error("Premises with qcode $qCode not found")

if (!premises.supportsSpaceBookings) {
error("premise ${premises.name} doesn't support space bookings, can't migrate bookings")
}

val referrals = cas1DeliusBookingImportRepository.findActiveBookingsCreatedInDelius(
qCode = qCode,
minExpectedDepartureDate = LocalDate.of(2025, 1, 1),
maxExpectedDepartureDate = LocalDate.of(2035, 1, 1),
)

log.info("Will create ${referrals.size} space bookings for premise ${premises.name} ($qCode)")

if (referrals.isEmpty()) {
return
}

val crnToName = offenderService.getPersonSummaryInfoResultsInBatches(
crns = referrals.map { it.crn }.toSet(),
limitedAccessStrategy = OffenderService.LimitedAccessStrategy.IgnoreLimitedAccess,
).associate { personSummaryInfoResult ->
val crn = personSummaryInfoResult.crn
when (personSummaryInfoResult) {
is PersonSummaryInfoResult.Success.Full -> {
val name = personSummaryInfoResult.summary.name
crn to "${name.forename} ${name.surname}"
}
is PersonSummaryInfoResult.NotFound,
is PersonSummaryInfoResult.Success.Restricted,
is PersonSummaryInfoResult.Unknown,
-> {
log.warn(
"Could not find offender for CRN $crn, " +
"result was ${personSummaryInfoResult::class}. Will not populate name",
)
crn to null
}
}
}

referrals.forEach {
createSpaceBooking(
premises = premises,
deliusReferral = it,
crnToName = crnToName,
)
}

log.info("Have crated ${referrals.size} space bookings for premise ${premises.name} ($qCode)")
}

private fun createSpaceBooking(
premises: ApprovedPremisesEntity,
deliusReferral: Cas1DeliusBookingImportEntity,
crnToName: Map<String, String?>,
) {
val crn = deliusReferral.crn

if (deliusReferral.expectedDepartureDate == null) {
log.warn("No expected departure date defined for crn $crn with arrival ${deliusReferral.arrivalDate}")
return
}

val offlineApplication = offlineApplicationRepository.save(
OfflineApplicationEntity(
id = UUID.randomUUID(),
crn = crn,
service = ServiceName.approvedPremises.value,
createdAt = OffsetDateTime.now(),
eventNumber = deliusReferral.eventNumber,
name = crnToName[crn],
),
)

val managementInfo = cas1BookingManagementInfoService.fromDeliusBookingImport(deliusReferral)

spaceBookingRepository.save(
Cas1SpaceBookingEntity(
id = UUID.randomUUID(),
premises = premises,
application = null,
offlineApplication = offlineApplication,
placementRequest = null,
createdBy = null,
createdAt = OffsetDateTime.now(),
expectedArrivalDate = deliusReferral.expectedArrivalDate,
expectedDepartureDate = deliusReferral.expectedDepartureDate!!,
actualArrivalDate = managementInfo.arrivedAtDate,
actualArrivalTime = managementInfo.arrivedAtTime,
actualDepartureDate = null,
actualDepartureTime = null,
canonicalArrivalDate = managementInfo.arrivedAtDate ?: deliusReferral.expectedArrivalDate,
canonicalDepartureDate = deliusReferral.expectedDepartureDate!!,
crn = deliusReferral.crn,
keyWorkerStaffCode = managementInfo.keyWorkerStaffCode,
keyWorkerName = managementInfo.keyWorkerName,
keyWorkerAssignedAt = null,
cancellationOccurredAt = null,
cancellationRecordedAt = null,
cancellationReason = null,
cancellationReasonNotes = null,
departureMoveOnCategory = null,
departureReason = null,
departureNotes = null,
criteria = emptyList<CharacteristicEntity>().toMutableList(),
nonArrivalReason = null,
nonArrivalConfirmedAt = null,
nonArrivalNotes = null,
deliusEventNumber = deliusReferral.eventNumber,
migratedManagementInfoFrom = managementInfo.source,
),
)
}
}

data class Cas1CreateMissingReferralsSeedCsvRow(
val qCode: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package uk.gov.justice.digital.hmpps.approvedpremisesapi.seed.cas1

import org.springframework.stereotype.Service
import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.ServiceName
import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.BookingEntity
import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.DepartureReasonEntity
import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.DepartureReasonRepository
import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ManagementInfoSource
import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.MoveOnCategoryEntity
import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.MoveOnCategoryRepository
import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.NonArrivalReasonEntity
import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.NonArrivalReasonRepository
import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas1.Cas1DeliusBookingImportEntity
import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.toLocalDate
import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.toLocalDateTime
import java.time.LocalDate
import java.time.LocalTime
import java.time.OffsetDateTime

@Service
class Cas1BookingManagementInfoService(
private val departureReasonRepository: DepartureReasonRepository,
private val moveOnCategoryRepository: MoveOnCategoryRepository,
private val nonArrivalReasonReasonEntity: NonArrivalReasonRepository,
) {

fun fromBooking(booking: BookingEntity) = ManagementInfo(
source = ManagementInfoSource.LEGACY_CAS_1,
arrivedAtDate = booking.arrival?.arrivalDateTime?.toLocalDate(),
arrivedAtTime = booking.arrival?.arrivalDateTime?.toLocalDateTime()?.toLocalTime(),
departedAtDate = booking.departure?.dateTime?.toLocalDate(),
departedAtTime = booking.departure?.dateTime?.toLocalDateTime()?.toLocalTime(),
keyWorkerStaffCode = null,
keyWorkerName = null,
departureReason = booking.departure?.reason,
departureMoveOnCategory = booking.departure?.moveOnCategory,
departureNotes = booking.departure?.notes,
nonArrivalConfirmedAt = booking.nonArrival?.createdAt,
nonArrivalReason = booking.nonArrival?.reason,
nonArrivalNotes = booking.nonArrival?.notes,
)

fun fromDeliusBookingImport(import: Cas1DeliusBookingImportEntity) = ManagementInfo(
source = ManagementInfoSource.DELIUS,
arrivedAtDate = import.arrivalDate,
arrivedAtTime = null,
departedAtDate = import.departureDate,
departedAtTime = null,
keyWorkerStaffCode = import.keyWorkerStaffCode,
keyWorkerName = import.keyWorkerStaffCode?.let { "${import.keyWorkerForename} ${import.keyWorkerSurname}" },
departureReason = import.departureReasonCode?.let { reasonCode ->
departureReasonRepository
.findAllByServiceScope(ServiceName.approvedPremises.value)
.filter { it.legacyDeliusReasonCode == reasonCode }
.maxByOrNull { it.isActive }
?: error("Could not resolve DepartureReason for code $reasonCode")
},
departureMoveOnCategory = import.moveOnCategoryCode?.let { reasonCode ->
moveOnCategoryRepository
.findAllByServiceScope(ServiceName.approvedPremises.value)
.firstOrNull { it.legacyDeliusCategoryCode == reasonCode } ?: error("Could not resolve MoveOnCategory for code $reasonCode")
},
departureNotes = null,
nonArrivalConfirmedAt = import.nonArrivalContactDatetime,
nonArrivalReason = import.nonArrivalReasonCode?.let {
nonArrivalReasonReasonEntity.findByLegacyDeliusReasonCode(it) ?: error("Could not resolve NonArrivalReason for code $it")
},
nonArrivalNotes = import.nonArrivalNotes,
)
}

data class ManagementInfo(
val source: ManagementInfoSource,
val arrivedAtDate: LocalDate?,
val arrivedAtTime: LocalTime?,
val departedAtDate: LocalDate?,
val departedAtTime: LocalTime?,
val keyWorkerStaffCode: String?,
val keyWorkerName: String?,
val departureReason: DepartureReasonEntity?,
val departureMoveOnCategory: MoveOnCategoryEntity?,
val departureNotes: String?,
val nonArrivalConfirmedAt: OffsetDateTime?,
val nonArrivalReason: NonArrivalReasonEntity?,
val nonArrivalNotes: String?,
)
Loading
Loading