diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/ReferralRejectionReasonEntity.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/ReferralRejectionReasonEntity.kt index 6c49fa8580..dade47b25b 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/ReferralRejectionReasonEntity.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/ReferralRejectionReasonEntity.kt @@ -13,6 +13,9 @@ import java.util.UUID interface ReferralRejectionReasonRepository : JpaRepository { @Query("SELECT m FROM ReferralRejectionReasonEntity m WHERE m.serviceScope = :serviceName OR m.serviceScope = '*' ORDER BY m.sortOrder") fun findAllByServiceScope(serviceName: String): List + + @Query("SELECT rr FROM ReferralRejectionReasonEntity rr WHERE rr.serviceScope = :serviceName AND rr.name = :name AND rr.isActive = true") + fun findByNameAndActive(name: String, serviceName: String): ReferralRejectionReasonEntity? } @Entity diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/seed/SeedService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/seed/SeedService.kt index a814ba5541..e58d172e93 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/seed/SeedService.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/seed/SeedService.kt @@ -32,6 +32,7 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.seed.cas1.Cas1WithdrawPl import uk.gov.justice.digital.hmpps.approvedpremisesapi.seed.cas2.Cas2ApplicationsSeedJob import uk.gov.justice.digital.hmpps.approvedpremisesapi.seed.cas2.ExternalUsersSeedJob import uk.gov.justice.digital.hmpps.approvedpremisesapi.seed.cas2.NomisUsersSeedJob +import uk.gov.justice.digital.hmpps.approvedpremisesapi.seed.cas3.Cas3ReferralRejectionSeedJob import uk.gov.justice.digital.hmpps.approvedpremisesapi.seed.cas3.Cas3UsersSeedJob import uk.gov.justice.digital.hmpps.approvedpremisesapi.seed.cas3.TemporaryAccommodationBedspaceSeedJob import uk.gov.justice.digital.hmpps.approvedpremisesapi.seed.cas3.TemporaryAccommodationPremisesSeedJob @@ -120,6 +121,7 @@ class SeedService( SeedFileType.approvedPremisesSpacePlanningDryRun -> getBean(Cas1PlanSpacePlanningDryRunSeedJob::class) SeedFileType.approvedPremisesImportDeliusBookingManagementData -> getBean(Cas1ImportDeliusBookingDataSeedJob::class) SeedFileType.approvedPremisesUpdateSpaceBooking -> getBean(Cas1UpdateSpaceBookingSeedJob::class) + SeedFileType.temporaryAccommodationReferralRejection -> getBean(Cas3ReferralRejectionSeedJob::class) } val seedStarted = LocalDateTime.now() diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/seed/cas3/Cas3ReferralRejectionSeedJob.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/seed/cas3/Cas3ReferralRejectionSeedJob.kt new file mode 100644 index 0000000000..5b30dec090 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/seed/cas3/Cas3ReferralRejectionSeedJob.kt @@ -0,0 +1,80 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.seed.cas3 + +import org.slf4j.LoggerFactory +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Component +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.ServiceName +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.AssessmentDecision +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.AssessmentRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ReferralRejectionReasonRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.TemporaryAccommodationAssessmentEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.seed.SeedJob +import java.time.OffsetDateTime +import java.util.UUID + +@Component +class Cas3ReferralRejectionSeedJob( + private val assessmentRepository: AssessmentRepository, + private val referralRejectionReasonRepository: ReferralRejectionReasonRepository, +) : SeedJob( + requiredHeaders = setOf( + "assessment_id", + "rejection_reason", + "rejection_reason_detail", + "is_withdrawn", + ), + runInTransaction = false, +) { + private val log = LoggerFactory.getLogger(this::class.java) + + override fun deserializeRow(columns: Map) = Cas3ReferralRejectionSeedCsvRow( + assessmentId = UUID.fromString(columns["assessment_id"]!!.trim()), + rejectionReason = columns["rejection_reason"]!!.trim(), + rejectionReasonDetail = columns["rejection_reason_detail"]!!.trim(), + isWithdrawn = columns["is_withdrawn"]!!.trim().equals("true", ignoreCase = true), + ) + + override fun processRow(row: Cas3ReferralRejectionSeedCsvRow) { + rejectAssessment(row) + } + + @SuppressWarnings("TooGenericExceptionCaught") + private fun rejectAssessment(row: Cas3ReferralRejectionSeedCsvRow) { + val assessment = + assessmentRepository.findByIdOrNull(row.assessmentId) ?: error("Assessment with id ${row.assessmentId} not found") + + if (assessment.reallocatedAt != null) { + error("The application has been reallocated, this assessment is read only") + } + + if (assessment is TemporaryAccommodationAssessmentEntity) { + val rejectionReason = referralRejectionReasonRepository.findByNameAndActive(row.rejectionReason, ServiceName.temporaryAccommodation.value) + ?: error("Rejection reason ${row.rejectionReason} not found") + + try { + assessment.submittedAt = OffsetDateTime.now() + assessment.decision = AssessmentDecision.REJECTED + assessment.completedAt = null + assessment.referralRejectionReason = rejectionReason + assessment.referralRejectionReasonDetail = row.rejectionReasonDetail + assessment.isWithdrawn = row.isWithdrawn + + assessmentRepository.save(assessment) + } catch (e: Throwable) { + log.error("Failed to update assessment with id ${row.assessmentId}", e) + error("Failed to update assessment with id ${row.assessmentId}") + } + + log.info("Assessment with id ${row.assessmentId} has been successfully rejected") + } else { + error("Assessment with id ${row.assessmentId} is not a temporary accommodation assessment") + } + } +} + +data class Cas3ReferralRejectionSeedCsvRow( + val assessmentId: UUID, + val rejectionReason: String, + val rejectionReasonDetail: String?, + val isWithdrawn: Boolean, +) diff --git a/src/main/resources/static/_shared.yml b/src/main/resources/static/_shared.yml index 7f4a09ba09..b88d50e5c6 100644 --- a/src/main/resources/static/_shared.yml +++ b/src/main/resources/static/_shared.yml @@ -3732,6 +3732,7 @@ components: - approved_premises_space_planning_dry_run - approved_premises_import_delius_booking_management_data - approved_premises_update_space_booking + - temporary_accommodation_referral_rejection SeedFromExcelFileType: type: string enum: diff --git a/src/main/resources/static/codegen/built-api-spec.yml b/src/main/resources/static/codegen/built-api-spec.yml index 7f9e313b24..b97242d9ae 100644 --- a/src/main/resources/static/codegen/built-api-spec.yml +++ b/src/main/resources/static/codegen/built-api-spec.yml @@ -8033,6 +8033,7 @@ components: - approved_premises_space_planning_dry_run - approved_premises_import_delius_booking_management_data - approved_premises_update_space_booking + - temporary_accommodation_referral_rejection SeedFromExcelFileType: type: string enum: diff --git a/src/main/resources/static/codegen/built-cas1-api-spec.yml b/src/main/resources/static/codegen/built-cas1-api-spec.yml index 8f247665a1..d7f46a35c3 100644 --- a/src/main/resources/static/codegen/built-cas1-api-spec.yml +++ b/src/main/resources/static/codegen/built-cas1-api-spec.yml @@ -4954,6 +4954,7 @@ components: - approved_premises_space_planning_dry_run - approved_premises_import_delius_booking_management_data - approved_premises_update_space_booking + - temporary_accommodation_referral_rejection SeedFromExcelFileType: type: string enum: diff --git a/src/main/resources/static/codegen/built-cas2-api-spec.yml b/src/main/resources/static/codegen/built-cas2-api-spec.yml index ca3cd1372d..f5956f26ea 100644 --- a/src/main/resources/static/codegen/built-cas2-api-spec.yml +++ b/src/main/resources/static/codegen/built-cas2-api-spec.yml @@ -4323,6 +4323,7 @@ components: - approved_premises_space_planning_dry_run - approved_premises_import_delius_booking_management_data - approved_premises_update_space_booking + - temporary_accommodation_referral_rejection SeedFromExcelFileType: type: string enum: diff --git a/src/main/resources/static/codegen/built-cas3-api-spec.yml b/src/main/resources/static/codegen/built-cas3-api-spec.yml index b13a685624..e29e549079 100644 --- a/src/main/resources/static/codegen/built-cas3-api-spec.yml +++ b/src/main/resources/static/codegen/built-cas3-api-spec.yml @@ -3831,6 +3831,7 @@ components: - approved_premises_space_planning_dry_run - approved_premises_import_delius_booking_management_data - approved_premises_update_space_booking + - temporary_accommodation_referral_rejection SeedFromExcelFileType: type: string enum: diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/seed/cas3/Cas3ReferralRejectionSeedJobTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/seed/cas3/Cas3ReferralRejectionSeedJobTest.kt new file mode 100644 index 0000000000..81661f4c0c --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/seed/cas3/Cas3ReferralRejectionSeedJobTest.kt @@ -0,0 +1,78 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.seed.cas3 + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.springframework.data.repository.findByIdOrNull +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.SeedFileType +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenAUser +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.seed.SeedTestBase +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.AssessmentDecision +import uk.gov.justice.digital.hmpps.approvedpremisesapi.seed.CsvBuilder +import uk.gov.justice.digital.hmpps.approvedpremisesapi.seed.cas3.Cas3ReferralRejectionSeedCsvRow +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.randomStringLowerCase +import java.time.OffsetDateTime + +class Cas3ReferralRejectionSeedJobTest : SeedTestBase() { + @Test + fun `Reject an assessment update the assessment status to Rejected`() { + val user = givenAUser().first + + val applicationSchema = temporaryAccommodationApplicationJsonSchemaEntityFactory.produceAndPersist { + withPermissiveSchema() + } + + val assessmentSchema = temporaryAccommodationAssessmentJsonSchemaEntityFactory.produceAndPersist { + withPermissiveSchema() + } + + val application = temporaryAccommodationApplicationEntityFactory.produceAndPersist { + withProbationRegion(user.probationRegion) + withCreatedByUser(user) + withApplicationSchema(applicationSchema) + withSubmittedAt(OffsetDateTime.now().minusDays(10)) + } + + val assessment = temporaryAccommodationAssessmentEntityFactory.produceAndPersist { + withApplication(application) + withAllocatedToUser(user) + withAssessmentSchema(assessmentSchema) + } + + val rejectedReason = "Another reason (please add)" + val rejectedReasonDetail = randomStringLowerCase(30) + + withCsv( + "cas3-referral-rejection-csv", + rowsToCsv(listOf(Cas3ReferralRejectionSeedCsvRow(assessment.id, rejectedReason, rejectedReasonDetail, false))), + ) + + seedService.seedData(SeedFileType.temporaryAccommodationReferralRejection, "cas3-referral-rejection-csv.csv") + + val persistedAssessment = temporaryAccommodationAssessmentRepository.findByIdOrNull(assessment.id)!! + assertThat(persistedAssessment).isNotNull + assertThat(persistedAssessment.decision).isEqualTo(AssessmentDecision.REJECTED) + assertThat(persistedAssessment.completedAt).isNull() + assertThat(persistedAssessment.referralRejectionReason?.name).isEqualTo(rejectedReason) + assertThat(persistedAssessment.referralRejectionReasonDetail).isEqualTo(rejectedReasonDetail) + assertThat(persistedAssessment.isWithdrawn).isFalse() + } + + private fun rowsToCsv(rows: List): String { + val builder = CsvBuilder() + .withUnquotedFields( + "assessment_id", + "rejection_reason", + "rejection_reason_detail", + "is_withdrawn", + ) + .newRow() + + rows.forEach { + builder + .withQuotedFields(it.assessmentId, it.rejectionReason, it.rejectionReasonDetail!!, it.isWithdrawn) + .newRow() + } + + return builder.build() + } +} diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/seed/cas3/Cas3ReferralRejectionSeedJobTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/seed/cas3/Cas3ReferralRejectionSeedJobTest.kt new file mode 100644 index 0000000000..dba09cd5c1 --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/seed/cas3/Cas3ReferralRejectionSeedJobTest.kt @@ -0,0 +1,122 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.unit.seed.cas3 + +import io.mockk.every +import io.mockk.mockk +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.jupiter.api.Test +import org.springframework.data.repository.findByIdOrNull +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.ServiceName +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ApprovedPremisesApplicationEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ApprovedPremisesAssessmentEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.TemporaryAccommodationApplicationEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.TemporaryAccommodationAssessmentEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.AssessmentRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ReferralRejectionReasonEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ReferralRejectionReasonRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.seed.cas3.Cas3ReferralRejectionSeedCsvRow +import uk.gov.justice.digital.hmpps.approvedpremisesapi.seed.cas3.Cas3ReferralRejectionSeedJob +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.randomStringLowerCase +import java.time.OffsetDateTime +import java.util.UUID + +class Cas3ReferralRejectionSeedJobTest { + private val assessmentRepository = mockk() + private val referralRejectionReasonRepository = mockk() + + private val seedJob = Cas3ReferralRejectionSeedJob( + assessmentRepository = assessmentRepository, + referralRejectionReasonRepository = referralRejectionReasonRepository, + ) + + @Test + fun `When the assessment is not Temporary Accommodation assessment expect error`() { + val assessmentId = UUID.randomUUID() + + every { assessmentRepository.findByIdOrNull(assessmentId) } returns + ApprovedPremisesAssessmentEntityFactory() + .withApplication( + ApprovedPremisesApplicationEntityFactory() + .withDefaults() + .produce(), + ) + .produce() + + assertThatThrownBy { + seedJob.processRow(Cas3ReferralRejectionSeedCsvRow(assessmentId, "rejection reason", null, false)) + }.hasMessage("Assessment with id $assessmentId is not a temporary accommodation assessment") + } + + @Test + fun `When an assessment doesn't exist expect error`() { + val assessmentId = UUID.randomUUID() + + every { assessmentRepository.findByIdOrNull(assessmentId) } returns null + + assertThatThrownBy { + seedJob.processRow(Cas3ReferralRejectionSeedCsvRow(assessmentId, "rejection reason", randomStringLowerCase(20), false)) + }.hasMessage("Assessment with id $assessmentId not found") + } + + @Test + fun `When the application has been allocated expect error`() { + val assessmentId = UUID.randomUUID() + + every { assessmentRepository.findByIdOrNull(assessmentId) } returns + TemporaryAccommodationAssessmentEntityFactory() + .withApplication( + TemporaryAccommodationApplicationEntityFactory() + .withDefaults() + .produce(), + ) + .withReallocatedAt(OffsetDateTime.now()) + .produce() + + assertThatThrownBy { + seedJob.processRow(Cas3ReferralRejectionSeedCsvRow(assessmentId, "rejection reason", null, false)) + }.hasMessage("The application has been reallocated, this assessment is read only") + } + + @Test + fun `When the rejection reason doesn't exist expect error`() { + val assessmentId = UUID.randomUUID() + val notExistRejectionReason = "not exist rejection reason" + + every { assessmentRepository.findByIdOrNull(assessmentId) } returns + TemporaryAccommodationAssessmentEntityFactory() + .withApplication( + TemporaryAccommodationApplicationEntityFactory() + .withDefaults() + .produce(), + ) + .produce() + + every { referralRejectionReasonRepository.findByNameAndActive(notExistRejectionReason, ServiceName.temporaryAccommodation.value) } returns null + + assertThatThrownBy { + seedJob.processRow(Cas3ReferralRejectionSeedCsvRow(assessmentId, notExistRejectionReason, null, false)) + }.hasMessage("Rejection reason $notExistRejectionReason not found") + } + + @Test + fun `When save an assessment and an exception happened expect logging error`() { + val assessmentId = UUID.randomUUID() + val assessment = TemporaryAccommodationAssessmentEntityFactory() + .withApplication( + TemporaryAccommodationApplicationEntityFactory() + .withDefaults() + .produce(), + ) + .produce() + + every { assessmentRepository.findByIdOrNull(assessmentId) } returns assessment + + every { referralRejectionReasonRepository.findByNameAndActive("rejection reason", ServiceName.temporaryAccommodation.value) } returns + ReferralRejectionReasonEntity(UUID.randomUUID(), "rejection reason", true, ServiceName.temporaryAccommodation.value, 1) + + every { assessmentRepository.save(any()) } throws RuntimeException("Failed to update assessment with id $assessmentId") + + assertThatThrownBy { + seedJob.processRow(Cas3ReferralRejectionSeedCsvRow(assessmentId, "rejection reason", null, false)) + }.hasMessage("Failed to update assessment with id $assessmentId") + } +}