diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/seed/cas1/Cas1ApplicationSeedService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/seed/cas1/Cas1ApplicationSeedService.kt index a2ffb65eb2..d2ece80428 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/seed/cas1/Cas1ApplicationSeedService.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/seed/cas1/Cas1ApplicationSeedService.kt @@ -1,5 +1,8 @@ package uk.gov.justice.digital.hmpps.approvedpremisesapi.seed.cas1 +import com.google.common.cache.CacheBuilder +import com.google.common.cache.CacheLoader +import com.google.common.cache.LoadingCache import org.json.JSONObject import org.slf4j.LoggerFactory import org.springframework.stereotype.Service @@ -16,14 +19,18 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.ServiceName import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.SituationOption import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.SubmitApprovedPremisesApplication import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApprovedPremisesApplicationEntity +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.AssessmentRepository import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.BookingEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.BookingRepository import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas1SpaceBookingEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas1SpaceBookingEntity.Companion.CHARACTERISTICS_OF_INTEREST 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.CharacteristicRepository import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.OfflineApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.PlacementRequestRepository import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.PostcodeDistrictRepository import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.PersonInfoResult import uk.gov.justice.digital.hmpps.approvedpremisesapi.problem.ForbiddenProblem @@ -35,6 +42,7 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.EnvironmentServi import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.OffenderService import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.UserService import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.UserService.GetUserResponse +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas1.Cas1SpaceBookingService import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.ensureEntityFromCasResultIsSuccess import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.extractEntityFromCasResult import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.extractEntityFromNestedAuthorisableValidatableActionResult @@ -42,6 +50,8 @@ import java.io.IOException import java.time.LocalDate import java.time.OffsetDateTime import java.util.UUID +import java.util.concurrent.TimeUnit +import kotlin.random.Random @SuppressWarnings("MagicNumber", "TooGenericExceptionCaught") @Service @@ -50,19 +60,42 @@ class Cas1ApplicationSeedService( private val bookingRepository: BookingRepository, private val applicationTimelineNoteService: ApplicationTimelineNoteService, private val spaceBookingRepository: Cas1SpaceBookingRepository, - private val offenderService: OffenderService, private val applicationService: ApplicationService, private val userService: UserService, private val environmentService: EnvironmentService, private val assessmentService: AssessmentService, private val assessmentRepository: AssessmentRepository, private val postcodeDistrictRepository: PostcodeDistrictRepository, + private val cache: Cas1ApplicationSeedServiceCaches, + private val spaceBookingService: Cas1SpaceBookingService, + private val placementRequestRepository: PlacementRequestRepository, + private val premisesRepository: ApprovedPremisesRepository, + private val characteristicsRepository: CharacteristicRepository, ) { - private val log = LoggerFactory.getLogger(this::class.java) + companion object { + private val log = LoggerFactory.getLogger(this::class.java) + + val ASSESSMENT_DATA_JSON = loadFixtureAsResource("assessment_data.json") + val ASSESSMENT_DOCUMENT_JSON = loadFixtureAsResource("assessment_document.json") + val APPLICATION_DATA_JSON = loadFixtureAsResource("application_data.json") + val APPLICATION_DOCUMENT_JSON: MutableMap = JSONObject(loadFixtureAsResource("application_document.json")).toMap() + + private fun loadFixtureAsResource(filename: String): String { + val path = "db/seed/local+dev+test/cas1_application_data/$filename" + + try { + return this::class.java.classLoader.getResource(path)?.readText() ?: "" + } catch (e: IOException) { + log.warn("Failed to load seed fixture $path: " + e.message!!) + return "{}" + } + } + } enum class ApplicationState { PENDING_SUBMISSION, AUTHORISED, + BOOKED, } @SuppressWarnings("TooGenericExceptionCaught") @@ -71,12 +104,13 @@ class Cas1ApplicationSeedService( crn: String, createIfExistingApplicationForCrn: Boolean = false, state: ApplicationState, + premisesQCode: String? = null, ) { if (environmentService.isNotATestEnvironment()) { error("Cannot create test applications as not in a test environment") } - createApplicationInternal(deliusUserName, crn, createIfExistingApplicationForCrn, state) + createApplicationInternal(deliusUserName, crn, createIfExistingApplicationForCrn, state, premisesQCode) } fun createOfflineApplicationWithBooking(deliusUserName: String, crn: String) { @@ -92,6 +126,7 @@ class Cas1ApplicationSeedService( crn: String, createIfExistingApplicationForCrn: Boolean, state: ApplicationState, + premisesQCode: String?, ) { if (!createIfExistingApplicationForCrn && applicationService.getApplicationsForCrn(crn, ServiceName.approvedPremises).isNotEmpty()) { log.info("Already have CAS1 application for $crn, not seeding a new application") @@ -109,6 +144,16 @@ class Cas1ApplicationSeedService( submitApplication(application) assessAndAcceptApplication(application) } + ApplicationState.BOOKED -> { + val application = createApplicationPendingSubmission(deliusUserName, crn) + submitApplication(application) + assessAndAcceptApplication(application) + val premises = premisesRepository.findByQCode(premisesQCode!!)!! + createSpaceBooking( + application, + premises, + ) + } } } @@ -116,7 +161,7 @@ class Cas1ApplicationSeedService( deliusUserName: String, crn: String, ): ApprovedPremisesApplicationEntity { - val personInfo = getPersonInfo(crn) + val personInfo = cache.getPersonInfo(crn) val createdByUser = (userService.getExistingUserOrCreate(deliusUserName) as GetUserResponse.Success).user val newApplicationEntity = extractEntityFromCasResult( @@ -141,7 +186,7 @@ class Cas1ApplicationSeedService( apType = ApType.normal, releaseType = "licence", arrivalDate = LocalDate.of(2025, 12, 12), - data = loadFixtureAsResource("application_data.json"), + data = APPLICATION_DATA_JSON, isInapplicable = false, noticeType = Cas1ApplicationTimelinessCategory.standard, ), @@ -167,7 +212,7 @@ class Cas1ApplicationSeedService( applicationId = application.id, submitApplication = SubmitApprovedPremisesApplication( apType = ApType.normal, - translatedDocument = JSONObject(loadFixtureAsResource("application_document.json")).toMap(), + translatedDocument = APPLICATION_DOCUMENT_JSON, caseManagerIsNotApplicant = false, isWomensApplication = false, releaseType = ReleaseTypeOption.licence, @@ -204,7 +249,7 @@ class Cas1ApplicationSeedService( assessmentService.updateAssessment( assessmentId = getAssessmentId(application), updatingUser = assessor, - data = loadFixtureAsResource("assessment_data.json"), + data = ASSESSMENT_DATA_JSON, ), ) @@ -212,7 +257,7 @@ class Cas1ApplicationSeedService( assessmentService.acceptAssessment( acceptingUser = assessor, assessmentId = getAssessmentId(application), - document = loadFixtureAsResource("assessment_document.json"), + document = ASSESSMENT_DOCUMENT_JSON, placementRequirements = PlacementRequirements( gender = Gender.male, type = ApType.normal, @@ -231,6 +276,27 @@ class Cas1ApplicationSeedService( ) } + @SuppressWarnings("MagicNumber") + private fun createSpaceBooking( + application: ApprovedPremisesApplicationEntity, + premises: ApprovedPremisesEntity, + ) { + val arrivalDate = LocalDate.now().minusDays(Random.nextLong(0, 7)) + val departureDate = arrivalDate.plusDays(Random.nextLong(1, 365)) + val characteristics = CHARACTERISTICS_OF_INTEREST.shuffled().take(Random.nextInt(0, CHARACTERISTICS_OF_INTEREST.size - 1)) + + ensureEntityFromCasResultIsSuccess( + spaceBookingService.createNewBooking( + premisesId = premises.id, + placementRequestId = placementRequestRepository.findByApplication(application).first().id, + arrivalDate = arrivalDate, + departureDate = departureDate, + createdBy = application.createdByUser, + characteristics = characteristicsRepository.findAllWherePropertyNameIn(characteristics, "approved-premises"), + ), + ) + } + private fun getAssessmentId(application: ApprovedPremisesApplicationEntity) = assessmentRepository.findByApplicationIdAndReallocatedAtNull(applicationId = application.id)!!.id private fun createOfflineApplicationInternal(deliusUserName: String, crn: String) { @@ -239,7 +305,7 @@ class Cas1ApplicationSeedService( return } - val personInfo = getPersonInfo(crn) + val personInfo = cache.getPersonInfo(crn) val offenderDetail = personInfo.offenderDetailSummary val offlineApplication = applicationService.createOfflineApplication( @@ -327,32 +393,37 @@ class Cas1ApplicationSeedService( ), ) } +} - private fun getPersonInfo(crn: String) = - when ( - val personInfoResult = offenderService.getPersonInfoResult( - crn = crn, - deliusUsername = null, - ignoreLaoRestrictions = true, - ) - ) { - is PersonInfoResult.NotFound, is PersonInfoResult.Unknown -> throw NotFoundProblem( - personInfoResult.crn, - "Offender", - ) - - is PersonInfoResult.Success.Restricted -> throw ForbiddenProblem() - is PersonInfoResult.Success.Full -> personInfoResult - } +@SuppressWarnings("MagicNumber") +@Service +class Cas1ApplicationSeedServiceCaches( + private val offenderService: OffenderService, +) { - private fun loadFixtureAsResource(filename: String): String { - val path = "db/seed/local+dev+test/cas1_application_data/$filename" + var personInfoCache: LoadingCache = CacheBuilder.newBuilder() + .maximumSize(10000) + .expireAfterWrite(1, TimeUnit.MINUTES) + .build(object : CacheLoader() { + override fun load(crn: String): PersonInfoResult.Success.Full { + return when ( + val personInfoResult = offenderService.getPersonInfoResult( + crn = crn, + deliusUsername = null, + ignoreLaoRestrictions = true, + ) + ) { + is PersonInfoResult.NotFound, is PersonInfoResult.Unknown -> throw NotFoundProblem( + personInfoResult.crn, + "Offender", + ) + + is PersonInfoResult.Success.Restricted -> throw ForbiddenProblem() + is PersonInfoResult.Success.Full -> personInfoResult + } + } + }) - try { - return this::class.java.classLoader.getResource(path)?.readText() ?: "" - } catch (e: IOException) { - log.warn("Failed to load seed fixture $path: " + e.message!!) - return "{}" - } - } + fun getPersonInfo(crn: String): PersonInfoResult.Success.Full = + personInfoCache.get(crn) } diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/seed/cas1/Cas1CreateTestApplicationsSeedJob.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/seed/cas1/Cas1CreateTestApplicationsSeedJob.kt index 4740f3706c..c14e82b056 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/seed/cas1/Cas1CreateTestApplicationsSeedJob.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/seed/cas1/Cas1CreateTestApplicationsSeedJob.kt @@ -20,6 +20,7 @@ class Cas1CreateTestApplicationsSeedJob( "crn", "count", "state", + "premises_qcode", ), runInTransaction = false, ) { @@ -34,6 +35,7 @@ class Cas1CreateTestApplicationsSeedJob( crn = seedColumns.getStringOrNull("crn")!!, count = seedColumns.getIntOrNull("count")!!, state = Cas1ApplicationSeedService.ApplicationState.valueOf(seedColumns.getStringOrNull("state")!!), + premisesQCode = seedColumns.getStringOrNull("premises_qcode"), ) } @@ -56,6 +58,7 @@ class Cas1CreateTestApplicationsSeedJob( crn = crn, createIfExistingApplicationForCrn = true, state = row.state, + premisesQCode = row.premisesQCode, ) } } @@ -71,4 +74,5 @@ data class Cas1CreateTestApplicationsSeedCsvRow( val crn: String, val count: Int, val state: Cas1ApplicationSeedService.ApplicationState, + val premisesQCode: String?, ) diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas1/seed/SeedCas1CreateTestApplicationSeedTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas1/seed/SeedCas1CreateTestApplicationSeedTest.kt index 6b6a44a297..527ba55ea8 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas1/seed/SeedCas1CreateTestApplicationSeedTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas1/seed/SeedCas1CreateTestApplicationSeedTest.kt @@ -5,6 +5,7 @@ import org.junit.jupiter.api.Test import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.SeedFileType import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.NeedsDetailsFactory import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenAUser +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenAnApprovedPremises import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenAnOffender import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.httpmocks.apDeliusContextMockSuccessfulTeamsManagingCaseCall import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.httpmocks.apOASysContextMockSuccessfulNeedsDetailsCall @@ -29,6 +30,8 @@ class SeedCas1CreateTestApplicationSeedTest : SeedTestBase() { apOASysContextMockSuccessfulNeedsDetailsCall(crn, NeedsDetailsFactory().produce()) govUKBankHolidaysAPIMockSuccessfullCallWithEmptyResponse() + val premises = givenAnApprovedPremises(supportsSpaceBookings = true) + postCodeDistrictFactory.produceAndPersist() withCsv( @@ -38,7 +41,8 @@ class SeedCas1CreateTestApplicationSeedTest : SeedTestBase() { creatorUsername = user.deliusUsername, crn = crn, count = 1, - state = Cas1ApplicationSeedService.ApplicationState.AUTHORISED, + state = Cas1ApplicationSeedService.ApplicationState.BOOKED, + premisesQCode = premises.qCode, ), ).toCsv(), ) @@ -56,6 +60,7 @@ class SeedCas1CreateTestApplicationSeedTest : SeedTestBase() { "crn", "count", "state", + "premises_qcode", ) .newRow() @@ -65,6 +70,7 @@ class SeedCas1CreateTestApplicationSeedTest : SeedTestBase() { .withQuotedField(it.crn) .withQuotedField(it.count) .withQuotedField(it.state) + .withQuotedField(it.premisesQCode ?: "") .newRow() }