From 84e67f26a5be994bcd54e338fad1ab5a9f2b18dc Mon Sep 17 00:00:00 2001 From: Marcus Aspin Date: Wed, 23 Oct 2024 17:03:11 +0000 Subject: [PATCH] PI-2543 Initial POC API for retrieving upcoming unpaid work appointments --- .../build.gradle.kts | 1 + .../justice/digital/hmpps/IntegrationTest.kt | 46 ++++++- .../digital/hmpps/config/CsvMapperConfig.kt | 9 ++ .../digital/hmpps/controller/ApiController.kt | 26 ++-- .../digital/hmpps/entity/UpwAppointment.kt | 14 +++ .../hmpps/model/UnpaidWorkAppointment.kt | 40 ++++++ .../repository/UpwAppointmentRepository.kt | 119 ++++++++++++++++++ .../src/main/resources/application.yml | 2 +- 8 files changed, 245 insertions(+), 12 deletions(-) create mode 100644 projects/appointment-reminders-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/config/CsvMapperConfig.kt create mode 100644 projects/appointment-reminders-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/entity/UpwAppointment.kt create mode 100644 projects/appointment-reminders-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/model/UnpaidWorkAppointment.kt create mode 100644 projects/appointment-reminders-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/repository/UpwAppointmentRepository.kt diff --git a/projects/appointment-reminders-and-delius/build.gradle.kts b/projects/appointment-reminders-and-delius/build.gradle.kts index a089a2f210..fdab8d46fc 100644 --- a/projects/appointment-reminders-and-delius/build.gradle.kts +++ b/projects/appointment-reminders-and-delius/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-csv") implementation(libs.springdoc) dev(project(":libs:dev-tools")) diff --git a/projects/appointment-reminders-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/IntegrationTest.kt b/projects/appointment-reminders-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/IntegrationTest.kt index d9006c74c5..673b2507d6 100644 --- a/projects/appointment-reminders-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/IntegrationTest.kt +++ b/projects/appointment-reminders-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/IntegrationTest.kt @@ -1,6 +1,9 @@ package uk.gov.justice.digital.hmpps import org.junit.jupiter.api.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.whenever import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc import org.springframework.boot.test.context.SpringBootTest @@ -8,9 +11,11 @@ import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDO import org.springframework.boot.test.mock.mockito.MockBean import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status +import uk.gov.justice.digital.hmpps.model.UnpaidWorkAppointment +import uk.gov.justice.digital.hmpps.repository.UpwAppointmentRepository import uk.gov.justice.digital.hmpps.test.MockMvcExtensions.withToken -import uk.gov.justice.digital.hmpps.telemetry.TelemetryService @AutoConfigureMockMvc @SpringBootTest(webEnvironment = RANDOM_PORT) @@ -19,12 +24,45 @@ internal class IntegrationTest { lateinit var mockMvc: MockMvc @MockBean - lateinit var telemetryService: TelemetryService + lateinit var upwAppointmentRepository: UpwAppointmentRepository @Test - fun `API call retuns a success response`() { + fun `returns csv report`() { + whenever(upwAppointmentRepository.getUnpaidWorkAppointments(any(), eq("N56"), any())).thenReturn( + listOf( + UnpaidWorkAppointment( + crn = "A123456", + firstName = "Test", + mobileNumber = "07000000000", + appointmentDate = "01/01/2000", + appointmentTimes = "08:00, 11:00", + nextWorkSessionProjectType = "Group session", + today = "01/01/2000", + sendSmsForDay = "01/01/2000", + fullName = "Test Test", + numberOfEvents = "1", + activeUpwRequirements = "1", + custodialStatus = null, + currentRemandStatus = null, + allowSms = "Y", + originalMobileNumber = "070 0000 0000", + upwMinutesRemaining = "123" + ) + ) + ) + mockMvc - .perform(get("/example/123").withToken()) + .perform(get("/upw-appointments.csv?providerCode=N56").withToken()) .andExpect(status().is2xxSuccessful) + .andExpect(content().contentTypeCompatibleWith("text/csv;charset=UTF-8")) + .andExpect( + content().string( + """ + crn,firstName,mobileNumber,appointmentDate,appointmentTimes,"nextWorkSessionProjectType",today,sendSmsForDay,fullName,numberOfEvents,activeUpwRequirements,custodialStatus,currentRemandStatus,allowSms,originalMobileNumber,upwMinutesRemaining + A123456,Test,07000000000,01/01/2000,"08:00, 11:00","Group session",01/01/2000,01/01/2000,"Test Test",1,1,,,Y,"070 0000 0000",123 + + """.trimIndent() + ) + ) } } diff --git a/projects/appointment-reminders-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/config/CsvMapperConfig.kt b/projects/appointment-reminders-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/config/CsvMapperConfig.kt new file mode 100644 index 0000000000..55b96d6ce6 --- /dev/null +++ b/projects/appointment-reminders-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/config/CsvMapperConfig.kt @@ -0,0 +1,9 @@ +package uk.gov.justice.digital.hmpps.config + +import com.fasterxml.jackson.dataformat.csv.CsvMapper +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import com.fasterxml.jackson.module.kotlin.registerKotlinModule + +object CsvMapperConfig { + val csvMapper = CsvMapper().also { it.registerKotlinModule().registerModule(JavaTimeModule()) } +} diff --git a/projects/appointment-reminders-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/controller/ApiController.kt b/projects/appointment-reminders-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/controller/ApiController.kt index 6e4f24c46f..c64c360811 100644 --- a/projects/appointment-reminders-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/controller/ApiController.kt +++ b/projects/appointment-reminders-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/controller/ApiController.kt @@ -2,16 +2,28 @@ package uk.gov.justice.digital.hmpps.controller import org.springframework.security.access.prepost.PreAuthorize import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController +import uk.gov.justice.digital.hmpps.config.CsvMapperConfig.csvMapper +import uk.gov.justice.digital.hmpps.model.UnpaidWorkAppointment +import uk.gov.justice.digital.hmpps.repository.UpwAppointmentRepository +import java.time.LocalDate @RestController -class ApiController { - @PreAuthorize("hasRole('EXAMPLE')") - @GetMapping(value = ["/example/{inputId}"]) +class ApiController( + private val upwAppointmentRepository: UpwAppointmentRepository +) { + @GetMapping("/upw-appointments.csv", produces = ["text/csv"]) + @PreAuthorize("hasRole('PROBATION_API__REMINDERS__UPW_APPOINTMENTS')") fun handle( - @PathVariable("inputId") inputId: String - ) { - // TODO Not yet implemented + @RequestParam providerCode: String, + @RequestParam projectTypeCodes: List = + listOf("A", "ES", "G", "I", "IP", "NP1", "NP2", "P", "PL", "UP09"), + @RequestParam date: LocalDate = LocalDate.now().plusDays(2) + ): String { + val results = upwAppointmentRepository.getUnpaidWorkAppointments(date, providerCode, projectTypeCodes) + return csvMapper + .writer(csvMapper.schemaFor(UnpaidWorkAppointment::class.java).withHeader()) + .writeValueAsString(results) } } diff --git a/projects/appointment-reminders-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/entity/UpwAppointment.kt b/projects/appointment-reminders-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/entity/UpwAppointment.kt new file mode 100644 index 0000000000..6e40bbfc3b --- /dev/null +++ b/projects/appointment-reminders-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/entity/UpwAppointment.kt @@ -0,0 +1,14 @@ +package uk.gov.justice.digital.hmpps.entity + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.Id +import org.hibernate.annotations.Immutable + +@Entity +@Immutable +class UpwAppointment( + @Id + @Column(name = "upw_appointment_id") + val id: Long, +) diff --git a/projects/appointment-reminders-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/model/UnpaidWorkAppointment.kt b/projects/appointment-reminders-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/model/UnpaidWorkAppointment.kt new file mode 100644 index 0000000000..6358acf07b --- /dev/null +++ b/projects/appointment-reminders-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/model/UnpaidWorkAppointment.kt @@ -0,0 +1,40 @@ +package uk.gov.justice.digital.hmpps.model + +import com.fasterxml.jackson.annotation.JsonPropertyOrder + +@JsonPropertyOrder( + "crn", + "firstName", + "mobileNumber", + "appointmentDate", + "appointmentTimes", + "nextWorkSessionProjectType", + "today", + "sendSmsForDay", + "fullName", + "numberOfEvents", + "activeUpwRequirements", + "custodialStatus", + "currentRemandStatus", + "allowSms", + "originalMobileNumber", + "upwMinutesRemaining" +) +data class UnpaidWorkAppointment( + val crn: String, + val firstName: String, + val mobileNumber: String, + val appointmentDate: String, + val appointmentTimes: String, + val nextWorkSessionProjectType: String, + val today: String, + val sendSmsForDay: String, + val fullName: String, + val numberOfEvents: String, + val activeUpwRequirements: String, + val custodialStatus: String?, + val currentRemandStatus: String?, + val allowSms: String, + val originalMobileNumber: String, + val upwMinutesRemaining: String +) diff --git a/projects/appointment-reminders-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/repository/UpwAppointmentRepository.kt b/projects/appointment-reminders-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/repository/UpwAppointmentRepository.kt new file mode 100644 index 0000000000..f0b7bfd1bc --- /dev/null +++ b/projects/appointment-reminders-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/repository/UpwAppointmentRepository.kt @@ -0,0 +1,119 @@ +package uk.gov.justice.digital.hmpps.repository + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query +import uk.gov.justice.digital.hmpps.entity.UpwAppointment +import uk.gov.justice.digital.hmpps.model.UnpaidWorkAppointment +import java.time.LocalDate + +interface UpwAppointmentRepository : JpaRepository { + @Query( + """ + select + crn, + any_value(first_name) as first_name, + any_value(mobile_number) as mobile_number, + any_value(to_char(appointment_date_time, 'DD/MM/YYYY')) as appointment_date, + listagg(distinct to_char(appointment_date_time, 'HH24:MI'), ', ') as appointment_times, + listagg(distinct next_work_session_project_type, ', ') as next_work_session_project_type, + to_char(current_date, 'DD/MM/YYYY') as today, + to_char(:date, 'DD/MM/YYYY') as send_sms_for_day, + any_value(full_name) as full_name, + count(distinct event_number) as number_of_events, + listagg(distinct active_upw_requirements, ', ') as active_upw_requirements, + listagg(distinct custodial_status, ', ') as custodial_status, + any_value(current_remand_status) as current_remand_status, + any_value(allow_sms) as allow_sms, + any_value(original_mobile_number) as original_mobile_number, + listagg(distinct upw_minutes_remaining, ', ') as upw_minutes_remaining + from ( + with duplicate_mobile_numbers as ( + select offender.offender_id from offender + join offender duplicate on replace(duplicate.mobile_number, ' ', '') = replace(offender.mobile_number, ' ', '') and duplicate.offender_id <> offender.offender_id and duplicate.soft_deleted = 0 + where offender.soft_deleted = 0 + ) + select + crn, + upw_appointment.upw_appointment_id, + event_number, + first_name, + replace(mobile_number, ' ', '') as mobile_number, + upw_appointment.appointment_date + (upw_appointment.start_time - trunc(upw_appointment.start_time)) as appointment_date_time, + upw_project_type.code_description as next_work_session_project_type, + r_contact_outcome_type.description as outcome_description, + to_char(current_date, 'DD/MM/YYYY') as today, + to_char(:date, 'DD/MM/YYYY') as send_sms_for_day, + first_name || ' ' || surname as full_name, + 1 as number_of_events, + (select count(*) from rqmnt + join r_rqmnt_type_main_category on r_rqmnt_type_main_category.rqmnt_type_main_category_id = rqmnt.rqmnt_type_main_category_id and r_rqmnt_type_main_category.code in ('W', 'W0', 'W1', 'W2') + where rqmnt.disposal_id = disposal.disposal_id and rqmnt.active_flag = 1 and rqmnt.soft_deleted = 0) as active_upw_requirements, + custodial_status.code_description as custodial_status, + case when exists (select 1 from registration + join r_register_type on r_register_type.register_type_id = registration.register_type_id and r_register_type.code in ('IWWO', 'IWWB', 'WRSM') + where registration.offender_id = offender.offender_id and registration.deregistered = 0 and registration.soft_deleted = 0) then 'Y' else 'N' end as warrant, + case when exists (select 1 from registration + join r_register_type on r_register_type.register_type_id = registration.register_type_id and r_register_type.code in ('HUAL') + where registration.offender_id = offender.offender_id and registration.deregistered = 0 and registration.soft_deleted = 0) then 'Y' else 'N' end as unlawfully_at_large, + current_remand_status, + case when exists (select 1 from personal_circumstance + join r_circumstance_type on r_circumstance_type.circumstance_type_id = personal_circumstance.circumstance_type_id and r_circumstance_type.code_value = 'RIC' + where personal_circumstance.offender_id = offender.offender_id and personal_circumstance.soft_deleted = 0 + ) then 'Y' else 'N' end as remanded_in_custody_circumstance, + allow_sms, + mobile_number as original_mobile_number, + (select total_minutes_ordered + positive_adjustments - negative_adjustments - minutes_credited from ( + select + case + when r_disposal_type.pre_cja2003 = 'Y' then disposal.length * 60 + else (select sum(rqmnt.length) * 60 from rqmnt + join r_rqmnt_type_main_category on r_rqmnt_type_main_category.rqmnt_type_main_category_id = rqmnt.rqmnt_type_main_category_id and r_rqmnt_type_main_category.code = 'W' + where rqmnt.disposal_id = disposal.disposal_id and rqmnt.soft_deleted = 0) + end as total_minutes_ordered, + (select coalesce(sum(adjustment_amount), 0) from upw_adjustment where upw_adjustment.upw_details_id = upw_details.upw_details_id and adjustment_type = 'POSITIVE' and upw_adjustment.soft_deleted = 0) + as positive_adjustments, + (select coalesce(sum(adjustment_amount), 0) from upw_adjustment where upw_adjustment.upw_details_id = upw_details.upw_details_id and adjustment_type = 'NEGATIVE' and upw_adjustment.soft_deleted = 0) + as negative_adjustments, + (select coalesce(sum(appts.minutes_credited), 0) from upw_appointment appts where appts.upw_details_id = upw_details.upw_details_id and appts.soft_deleted = 0) + as minutes_credited + from dual + )) as upw_minutes_remaining + from offender + join event on event.offender_id = offender.offender_id and event.active_flag = 1 and event.soft_deleted = 0 + join disposal on disposal.event_id = event.event_id and disposal.active_flag = 1 and disposal.soft_deleted = 0 + join r_disposal_type on r_disposal_type.disposal_type_id = disposal.disposal_type_id + join upw_details on upw_details.disposal_id = disposal.disposal_id and upw_details.soft_deleted = 0 + join upw_appointment on upw_appointment.upw_details_id = upw_details.upw_details_id and upw_appointment.soft_deleted = 0 and trunc(upw_appointment.appointment_date) = :date + left join r_contact_outcome_type on r_contact_outcome_type.contact_outcome_type_id = upw_appointment.contact_outcome_type_id + join upw_project on upw_project.upw_project_id = upw_appointment.upw_project_id + join r_standard_reference_list upw_project_type on upw_project_type.standard_reference_list_id = upw_project.project_type_id and upw_project_type.code_value in :projectTypeCodes + join probation_area on probation_area.probation_area_id = upw_project.probation_area_id and probation_area.code = :providerCode + left join custody on custody.disposal_id = disposal.disposal_id and custody.soft_deleted = 0 + left join r_standard_reference_list custodial_status on custodial_status.standard_reference_list_id = custody.custodial_status_id + where offender.soft_deleted = 0 + -- allow_sms <> 'N' + and (allow_sms is null or allow_sms = 'Y') + -- mobile_number_is_valid = 'Y' + and replace(mobile_number, ' ', '') like '07%' + and length(replace(mobile_number, ' ', '')) = 11 + and validate_conversion(replace(mobile_number, ' ', '') as number) = 1 + -- duplicate_mobile_number_exists = 'N' + and not exists (select 1 from duplicate_mobile_numbers where offender.offender_id = duplicate_mobile_numbers.offender_id) + -- outcome_description = null + and r_contact_outcome_type.description is null + ) + where active_upw_requirements > 0 + and upw_minutes_remaining > 0 + and warrant = 'N' + and unlawfully_at_large = 'N' + and current_remand_status is null or (current_remand_status not like 'Remand%' and current_remand_status not like 'Warrant%' and current_remand_status not like 'UAL%' and current_remand_status <> 'Unlawfully at Large') + and remanded_in_custody_circumstance = 'N' + group by crn + """, nativeQuery = true + ) + fun getUnpaidWorkAppointments( + date: LocalDate, + providerCode: String, + projectTypeCodes: List, + ): List +} \ No newline at end of file diff --git a/projects/appointment-reminders-and-delius/src/main/resources/application.yml b/projects/appointment-reminders-and-delius/src/main/resources/application.yml index 9e45275b50..3c2e8f33a2 100644 --- a/projects/appointment-reminders-and-delius/src/main/resources/application.yml +++ b/projects/appointment-reminders-and-delius/src/main/resources/application.yml @@ -19,7 +19,7 @@ spring: threads.virtual.enabled: true oauth2.roles: - - EXAMPLE + - PROBATION_API__REMINDERS__UPW_APPOINTMENTS springdoc.default-produces-media-type: application/json