diff --git a/projects/court-case-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/entity/Entities.kt b/projects/court-case-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/entity/Entities.kt index 4c2eab9b21..9b543a76bd 100644 --- a/projects/court-case-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/entity/Entities.kt +++ b/projects/court-case-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/entity/Entities.kt @@ -3,6 +3,7 @@ package uk.gov.justice.digital.hmpps.data.entity import jakarta.persistence.* import org.hibernate.type.YesNoConverter import java.time.LocalDate +import java.time.LocalDateTime @Entity class ApprovedPremisesReferral( @@ -35,7 +36,9 @@ class InstitutionalReport( @Convert(converter = YesNoConverter::class) val establishment: Boolean, val custodyId: Long, - val dateRequested: LocalDate + val dateRequested: LocalDate, + val dateRequired: LocalDate, + val dateCompleted: LocalDateTime, ) @Entity diff --git a/projects/court-case-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/DocumentEntityGenerator.kt b/projects/court-case-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/DocumentEntityGenerator.kt index 0ba74dd0ae..8a31648c13 100644 --- a/projects/court-case-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/DocumentEntityGenerator.kt +++ b/projects/court-case-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/DocumentEntityGenerator.kt @@ -15,7 +15,9 @@ object DocumentEntityGenerator { institutionReportTypeId = INSTITUTIONAL_REPORT_TYPE.id, custodyId = 1, establishment = true, - dateRequested = LocalDate.of(2000, 1, 2) + dateRequested = LocalDate.of(2000, 1, 2), + dateRequired = LocalDate.of(2000, 1, 2), + dateCompleted = LocalDateTime.of(2000, 1, 2, 0, 0) ) fun generateDocument(personId: Long, primaryKeyId: Long?, type: String, tableName: String?) = diff --git a/projects/court-case-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/UnpaidWorkGenerator.kt b/projects/court-case-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/UnpaidWorkGenerator.kt index 53ff6a1ddd..7474aa4248 100644 --- a/projects/court-case-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/UnpaidWorkGenerator.kt +++ b/projects/court-case-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/UnpaidWorkGenerator.kt @@ -3,17 +3,25 @@ package uk.gov.justice.digital.hmpps.data.generator import uk.gov.justice.digital.hmpps.data.generator.SentenceGenerator.CURRENT_SENTENCE import uk.gov.justice.digital.hmpps.integrations.delius.event.sentence.entity.UpwAppointment import uk.gov.justice.digital.hmpps.integrations.delius.event.sentence.entity.UpwDetails +import java.time.LocalDate object UnpaidWorkGenerator { val UNPAID_WORK_DETAILS_1 = UpwDetails(IdGenerator.getAndIncrement(), CURRENT_SENTENCE, 0, ReferenceDataGenerator.HOURS_WORKED) - val APPT1 = UpwAppointment(IdGenerator.getAndIncrement(), 3, "Y", "Y", 0, UNPAID_WORK_DETAILS_1) - val APPT2 = UpwAppointment(IdGenerator.getAndIncrement(), 4, "Y", "Y", 1, UNPAID_WORK_DETAILS_1) - val APPT3 = UpwAppointment(IdGenerator.getAndIncrement(), 0, "N", "N", 1, UNPAID_WORK_DETAILS_1) - val APPT4 = UpwAppointment(IdGenerator.getAndIncrement(), 0, "N", "Y", 1, UNPAID_WORK_DETAILS_1) - val APPT5 = UpwAppointment(IdGenerator.getAndIncrement(), 0, "N", "Y", 1, UNPAID_WORK_DETAILS_1) - val APPT6 = UpwAppointment(IdGenerator.getAndIncrement(), 0, null, null, 1, UNPAID_WORK_DETAILS_1) - val APPT7 = UpwAppointment(IdGenerator.getAndIncrement(), 0, "Y", "Y", 0, UNPAID_WORK_DETAILS_1) + val APPT1 = + UpwAppointment(IdGenerator.getAndIncrement(), 3, "Y", "Y", 0, LocalDate.now(), 1L, UNPAID_WORK_DETAILS_1) + val APPT2 = + UpwAppointment(IdGenerator.getAndIncrement(), 4, "Y", "Y", 1, LocalDate.now(), 1L, UNPAID_WORK_DETAILS_1) + val APPT3 = + UpwAppointment(IdGenerator.getAndIncrement(), 0, "N", "N", 1, LocalDate.now(), 1L, UNPAID_WORK_DETAILS_1) + val APPT4 = + UpwAppointment(IdGenerator.getAndIncrement(), 0, "N", "Y", 1, LocalDate.now(), 1L, UNPAID_WORK_DETAILS_1) + val APPT5 = + UpwAppointment(IdGenerator.getAndIncrement(), 0, "N", "Y", 1, LocalDate.now(), 1L, UNPAID_WORK_DETAILS_1) + val APPT6 = + UpwAppointment(IdGenerator.getAndIncrement(), 0, null, null, 1, LocalDate.now(), 1L, UNPAID_WORK_DETAILS_1) + val APPT7 = + UpwAppointment(IdGenerator.getAndIncrement(), 0, "Y", "Y", 0, LocalDate.now(), 1L, UNPAID_WORK_DETAILS_1) } \ No newline at end of file diff --git a/projects/court-case-and-delius/src/dev/resources/simulations/__files/get_documents_grouped_C123456.json b/projects/court-case-and-delius/src/dev/resources/simulations/__files/get_documents_grouped_C123456.json new file mode 100644 index 0000000000..ed07a63c16 --- /dev/null +++ b/projects/court-case-and-delius/src/dev/resources/simulations/__files/get_documents_grouped_C123456.json @@ -0,0 +1,22 @@ +{ + "documents": [], + "convictions": [ + { + "convictionId": "83", + "documents": [ + { + "id": "alfrescoId", + "documentName": "filename.txt", + "type": { + "code": "CONVICTION_DOCUMENT", + "description": "Sentence related" + }, + "lastModifiedAt": "2024-08-08T15:56:02.88685", + "createdAt": "2024-08-08T15:56:02.88684", + "parentPrimaryKeyId": 83, + "reportDocumentDates": {} + } + ] + } + ] +} \ No newline at end of file diff --git a/projects/court-case-and-delius/src/dev/resources/simulations/mappings/get-documents.json b/projects/court-case-and-delius/src/dev/resources/simulations/mappings/get-documents.json new file mode 100644 index 0000000000..99663daea3 --- /dev/null +++ b/projects/court-case-and-delius/src/dev/resources/simulations/mappings/get-documents.json @@ -0,0 +1,17 @@ +{ + "mappings": [ + { + "request": { + "method": "GET", + "urlPathTemplate": "/secure/offenders/crn/{crn}/documents/grouped" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "bodyFileName": "get_documents_grouped_C123456.json" + } + } + ] +} diff --git a/projects/court-case-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/DocumentsIntegrationTest.kt b/projects/court-case-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/DocumentsIntegrationTest.kt new file mode 100644 index 0000000000..8bdb5e2c65 --- /dev/null +++ b/projects/court-case-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/DocumentsIntegrationTest.kt @@ -0,0 +1,49 @@ +package uk.gov.justice.digital.hmpps + +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status +import uk.gov.justice.digital.hmpps.api.model.OffenderDocuments +import uk.gov.justice.digital.hmpps.api.resource.advice.ErrorResponse +import uk.gov.justice.digital.hmpps.data.generator.PersonGenerator +import uk.gov.justice.digital.hmpps.test.MockMvcExtensions.contentAsJson +import uk.gov.justice.digital.hmpps.test.MockMvcExtensions.withToken + +@AutoConfigureMockMvc +@SpringBootTest(webEnvironment = RANDOM_PORT) +internal class DocumentsIntegrationTest { + @Autowired + lateinit var mockMvc: MockMvc + + @Test + fun `call documents grouped by CRN`() { + val crn = PersonGenerator.CURRENTLY_MANAGED.crn + + val response = mockMvc + .perform(get("/probation-case/$crn/documents/grouped").withToken()) + .andExpect(status().isOk) + .andReturn().response.contentAsJson() + + assertThat(response.documents.size, equalTo(0)) + assertThat(response.convictions.size, equalTo(1)) + } + + @Test + fun `call documents grouped by CRN invalid filter`() { + val crn = PersonGenerator.CURRENTLY_MANAGED.crn + + val response = mockMvc + .perform(get("/probation-case/$crn/documents/grouped?type=INVALID").withToken()) + .andExpect(status().isBadRequest) + .andReturn().response.contentAsJson() + + assertThat(response.developerMessage, equalTo("type of INVALID was not valid")) + } +} \ No newline at end of file diff --git a/projects/court-case-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/ProxyIntegrationTest.kt b/projects/court-case-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/ProxyIntegrationTest.kt index f8c3afc497..14246670bc 100644 --- a/projects/court-case-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/ProxyIntegrationTest.kt +++ b/projects/court-case-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/ProxyIntegrationTest.kt @@ -261,6 +261,10 @@ internal class ProxyIntegrationTest { "CONVICTION_BY_ID_SENTENCE_STATUS": { "convictionId": "?", "activeOnly": true + }, + "DOCUMENTS_GROUPED": { + "type": null, + "subtype": null } } } @@ -269,7 +273,7 @@ internal class ProxyIntegrationTest { .withToken() ).andExpect(status().is2xxSuccessful).andReturn().response.contentAsJson() - assertThat(res.totalNumberOfRequests, equalTo(14)) + assertThat(res.totalNumberOfRequests, equalTo(15)) assertThat(res.totalNumberOfCrns, equalTo(2)) assertThat(res.currentPageNumber, equalTo(1)) } diff --git a/projects/court-case-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/model/Documents.kt b/projects/court-case-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/model/Documents.kt new file mode 100644 index 0000000000..b2cfe10e70 --- /dev/null +++ b/projects/court-case-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/model/Documents.kt @@ -0,0 +1,38 @@ +package uk.gov.justice.digital.hmpps.api.model + +import java.time.LocalDate +import java.time.LocalDateTime + +data class OffenderDocumentDetail( + val id: String? = null, + val documentName: String? = null, + val author: String? = null, + val type: KeyValue, + val extendedDescription: String? = null, + val lastModifiedAt: LocalDateTime? = null, + val createdAt: LocalDateTime? = null, + val parentPrimaryKeyId: Long? = null, + val subType: KeyValue? = null, + val reportDocumentDates: ReportDocumentDates? = null +) + +data class ReportDocumentDates( + val requestedDate: LocalDate? = null, + val requiredDate: LocalDate? = null, + val completedDate: LocalDateTime? = null +) + +data class ConvictionDocuments( + val convictionId: String, + val documents: List = emptyList() +) + +data class OffenderDocuments( + val documents: List = emptyList(), + val convictions: List = emptyList(), +) + +data class DocumentFilter( + val type: String? = null, + val subtype: String? = null +) diff --git a/projects/court-case-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/model/ProbationRecord.kt b/projects/court-case-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/model/ProbationRecord.kt index a59b09caf8..d59a473d2f 100644 --- a/projects/court-case-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/model/ProbationRecord.kt +++ b/projects/court-case-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/model/ProbationRecord.kt @@ -70,22 +70,12 @@ data class KeyValue( val description: String ) -data class OffenderDocumentDetail( - - val documentName: String, - val author: String?, - val type: DocumentType, - val extendedDescription: String?, - val createdAt: ZonedDateTime?, - val subType: KeyValue? -) - -enum class DocumentType(val description: String) { +enum class DocumentType(val description: String, val subtypes: List = emptyList()) { OFFENDER_DOCUMENT("Offender related"), CONVICTION_DOCUMENT("Sentence related"), CPSPACK_DOCUMENT("Crown Prosecution Service case pack"), PRECONS_DOCUMENT("PNC previous convictions"), - COURT_REPORT_DOCUMENT("Court report"), + COURT_REPORT_DOCUMENT("Court report", listOf(SubType.PSR)), INSTITUTION_REPORT_DOCUMENT("Institution report"), ADDRESS_ASSESSMENT_DOCUMENT("Address assessment related document"), APPROVED_PREMISES_REFERRAL_DOCUMENT("Approved premises referral related document"), @@ -100,6 +90,10 @@ enum class DocumentType(val description: String) { PREVIOUS_CONVICTION("Previous conviction document") } +enum class SubType { + PSR +} + class Breach( val description: String?, val status: String?, diff --git a/projects/court-case-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/proxy/CommunityApiController.kt b/projects/court-case-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/proxy/CommunityApiController.kt index 232363ae58..fb8a80909f 100644 --- a/projects/court-case-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/proxy/CommunityApiController.kt +++ b/projects/court-case-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/proxy/CommunityApiController.kt @@ -11,6 +11,7 @@ import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor import org.springframework.security.access.prepost.PreAuthorize import org.springframework.web.bind.annotation.* import uk.gov.justice.digital.hmpps.api.resource.ConvictionResource +import uk.gov.justice.digital.hmpps.api.resource.DocumentResource import uk.gov.justice.digital.hmpps.api.resource.ProbationRecordResource import uk.gov.justice.digital.hmpps.flags.FeatureFlags import uk.gov.justice.digital.hmpps.telemetry.TelemetryService @@ -24,6 +25,7 @@ class CommunityApiController( private val featureFlags: FeatureFlags, private val communityApiService: CommunityApiService, private val convictionResource: ConvictionResource, + private val documentResource: DocumentResource, private val taskExecutor: ThreadPoolTaskExecutor, private val personRepository: PersonEventRepository, private val telemetryService: TelemetryService, @@ -292,6 +294,27 @@ class CommunityApiController( return proxy(request) } + @GetMapping("/offenders/crn/{crn}/documents/grouped") + fun documentsGrouped( + request: HttpServletRequest, + @PathVariable crn: String, + @RequestParam(required = false) type: String?, + @RequestParam(required = false) subtype: String? + ): Any { + + val params = mutableMapOf() + type?.let { params["type"] = it } + subtype?.let { params["subtype"] = it } + sendComparisonReport( + params, Uri.DOCUMENTS_GROUPED, request + ) + + if (featureFlags.enabled("ccd-document-grouped")) { + return documentResource.getOffenderDocumentsGrouped(crn, type, subtype) + } + return proxy(request) + } + @GetMapping("/**") fun proxy(request: HttpServletRequest): ResponseEntity { val headers = request.headerNames.asSequence().associateWith { request.getHeader(it) }.toMutableMap() diff --git a/projects/court-case-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/proxy/CommunityApiService.kt b/projects/court-case-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/proxy/CommunityApiService.kt index 2c12338bd3..5509214c7a 100644 --- a/projects/court-case-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/proxy/CommunityApiService.kt +++ b/projects/court-case-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/proxy/CommunityApiService.kt @@ -14,6 +14,7 @@ import org.springframework.transaction.annotation.Propagation import org.springframework.transaction.annotation.Transactional import org.springframework.web.client.HttpStatusCodeException import uk.gov.justice.digital.hmpps.api.resource.advice.CommunityApiControllerAdvice +import uk.gov.justice.digital.hmpps.exception.InvalidRequestException import uk.gov.justice.digital.hmpps.exception.NotFoundException import java.io.StringReader import java.lang.reflect.InvocationTargetException @@ -45,6 +46,7 @@ class CommunityApiService( when (val cause = ex.cause) { is AccessDeniedException -> controllerAdvice.handleAccessDenied(cause).body is NotFoundException -> controllerAdvice.handleNotFound(cause).body + is InvalidRequestException -> controllerAdvice.handleInvalidRequest(cause).body else -> throw ex } } @@ -68,10 +70,14 @@ class CommunityApiService( val uri = Uri.valueOf(compare.uri) val comApiUri = compare.params.entries.fold(uri.comApiUrl) { path, (key, value) -> - path.replace( - "{$key}", - value.toString() - ) + if (value != null) { + path.replace( + "{$key}", + value.toString() + ) + } else { + path.replace("$key={$key}", "") + } }.replace(" ", "%20") val ccdJsonString = try { diff --git a/projects/court-case-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/proxy/Compare.kt b/projects/court-case-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/proxy/Compare.kt index 60e54e7187..a8f69e9198 100644 --- a/projects/court-case-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/proxy/Compare.kt +++ b/projects/court-case-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/proxy/Compare.kt @@ -1,6 +1,6 @@ package uk.gov.justice.digital.hmpps.api.proxy data class Compare( - val params: Map, + val params: Map, val uri: String ) diff --git a/projects/court-case-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/proxy/Uri.kt b/projects/court-case-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/proxy/Uri.kt index ebb03ccddc..caf099b30d 100644 --- a/projects/court-case-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/proxy/Uri.kt +++ b/projects/court-case-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/proxy/Uri.kt @@ -85,5 +85,11 @@ enum class Uri( "getConvictionSentenceStatus", listOf("crn", "convictionId"), ), + DOCUMENTS_GROUPED( + "/secure/offenders/crn/{crn}/documents/grouped?type={type}&subtype={subtype}", + "documentResource", + "getOffenderDocumentsGrouped", + listOf("crn", "type", "subtype"), + ), DUMMY("/dummy", "dummyResource", "getDummy", listOf("crn")), } \ No newline at end of file diff --git a/projects/court-case-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/resource/DocumentResource.kt b/projects/court-case-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/resource/DocumentResource.kt new file mode 100644 index 0000000000..834cc59b86 --- /dev/null +++ b/projects/court-case-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/resource/DocumentResource.kt @@ -0,0 +1,19 @@ +package uk.gov.justice.digital.hmpps.api.resource + +import org.springframework.security.access.prepost.PreAuthorize +import org.springframework.web.bind.annotation.* +import uk.gov.justice.digital.hmpps.api.model.DocumentFilter +import uk.gov.justice.digital.hmpps.integrations.delius.service.DocumentService + +@RestController +@RequestMapping("probation-case/{crn}/documents") +@PreAuthorize("hasRole('PROBATION_API__COURT_CASE__CASE_DETAIL')") +class DocumentResource(private val documentService: DocumentService) { + + @GetMapping("/grouped") + fun getOffenderDocumentsGrouped( + @PathVariable crn: String, + @RequestParam(required = false) type: String?, + @RequestParam(required = false) subType: String?, + ) = documentService.getDocumentsGroupedFor(crn, DocumentFilter(type, subType)) +} diff --git a/projects/court-case-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/resource/advice/ControllerAdvice.kt b/projects/court-case-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/resource/advice/ControllerAdvice.kt index ba58a09b84..6df668b675 100644 --- a/projects/court-case-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/resource/advice/ControllerAdvice.kt +++ b/projects/court-case-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/resource/advice/ControllerAdvice.kt @@ -1,5 +1,6 @@ package uk.gov.justice.digital.hmpps.api.resource.advice +import org.springframework.http.HttpStatus import org.springframework.http.HttpStatus.FORBIDDEN import org.springframework.http.HttpStatus.NOT_FOUND import org.springframework.http.ResponseEntity @@ -7,11 +8,19 @@ import org.springframework.security.access.AccessDeniedException import org.springframework.web.bind.annotation.ExceptionHandler import org.springframework.web.bind.annotation.RestControllerAdvice import uk.gov.justice.digital.hmpps.api.resource.ConvictionResource +import uk.gov.justice.digital.hmpps.api.resource.DocumentResource import uk.gov.justice.digital.hmpps.api.resource.ProbationRecordResource +import uk.gov.justice.digital.hmpps.exception.InvalidRequestException import uk.gov.justice.digital.hmpps.exception.NotFoundException -@RestControllerAdvice(basePackageClasses = [ProbationRecordResource::class, ConvictionResource::class]) +@RestControllerAdvice(basePackageClasses = [ProbationRecordResource::class, ConvictionResource::class, DocumentResource::class]) class CommunityApiControllerAdvice { + + @ExceptionHandler(InvalidRequestException::class) + fun handleInvalidRequest(e: InvalidRequestException) = ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .body(ErrorResponse(status = HttpStatus.BAD_REQUEST.value(), developerMessage = e.message)) + @ExceptionHandler(NotFoundException::class) fun handleNotFound(e: NotFoundException) = ResponseEntity .status(NOT_FOUND) diff --git a/projects/court-case-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/entity/Document.kt b/projects/court-case-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/entity/Document.kt index 2d350d79a6..926ae440e9 100644 --- a/projects/court-case-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/entity/Document.kt +++ b/projects/court-case-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/entity/Document.kt @@ -62,10 +62,17 @@ interface Document { val name: String val type: String val tableName: String + val lastModifiedAt: Instant? val createdAt: Instant? + val primaryKeyId: Long? val author: String? val description: String? val eventId: Long? + val subTypeCode: String? + val subTypeDescription: String? + val dateRequested: Instant? + val dateRequired: Instant? + val completedDate: Instant? } fun Document.relatesToEvent() = eventId != null @@ -87,6 +94,24 @@ fun Document.typeDescription() = when (tableName) { else -> error("Un-mapped document type ($tableName/$type)") } +fun Document.typeCode(): String = when (tableName) { + "OFFENDER" -> if (type == "PREVIOUS_CONVICTION") DocumentType.PRECONS_DOCUMENT.name else DocumentType.OFFENDER_DOCUMENT.name + "EVENT" -> if (type == "CPS_PACK") DocumentType.CPSPACK_DOCUMENT.name else DocumentType.CONVICTION_DOCUMENT.name + "COURT_REPORT" -> DocumentType.COURT_REPORT_DOCUMENT.name + "INSTITUTIONAL_REPORT" -> DocumentType.INSTITUTION_REPORT_DOCUMENT.name + "ADDRESSASSESSMENT" -> DocumentType.ADDRESS_ASSESSMENT_DOCUMENT.name + "APPROVED_PREMISES_REFERRAL" -> DocumentType.APPROVED_PREMISES_REFERRAL_DOCUMENT.name + "ASSESSMENT" -> DocumentType.ASSESSMENT_DOCUMENT.name + "CASE_ALLOCATION" -> DocumentType.CASE_ALLOCATION_DOCUMENT.name + "PERSONALCONTACT" -> DocumentType.PERSONAL_CONTACT_DOCUMENT.name + "REFERRAL" -> DocumentType.REFERRAL_DOCUMENT.name + "NSI" -> DocumentType.NSI_DOCUMENT.name + "PERSONAL_CIRCUMSTANCE" -> DocumentType.PERSONAL_CIRCUMSTANCE_DOCUMENT.name + "UPW_APPOINTMENT" -> DocumentType.UPW_APPOINTMENT_DOCUMENT.name + "CONTACT" -> DocumentType.CONTACT_DOCUMENT.name + else -> type +} + interface DocumentRepository : JpaRepository { @Query( @@ -106,7 +131,9 @@ interface DocumentRepository : JpaRepository { document.document_name as name, document.document_type as type, document.table_name as "tableName", + document.last_saved as "lastModifiedAt", document.created_datetime as "createdAt", + document.primary_key_id as "primaryKeyId", case when created_by.user_id is not null then created_by.forename || ' ' || created_by.surname when updated_by.user_id is not null then updated_by.forename || ' ' || updated_by.surname @@ -146,7 +173,27 @@ interface DocumentRepository : JpaRepository { upw_appointment_disposal.event_id, contact.event_id, nsi.event_id - ) as "eventId" + ) as "eventId", + coalesce( + institutional_report_type.code_value, + r_court_report_type.code + ) as "subTypeCode", + coalesce( + institutional_report_type.code_description, + r_court_report_type.description + ) as "subTypeDescription", + coalesce( + institutional_report.date_requested, + court_report.date_requested + ) as "dateRequested", + coalesce( + institutional_report.date_required, + court_report.date_required + ) as "dateRequired", + coalesce( + institutional_report.date_completed, + court_report.completed_date + ) as "completedDate" from document -- the following joins are to get the event_id from the related entities, for event-level documents left join event on document.table_name = 'EVENT' and document.primary_key_id = event.event_id diff --git a/projects/court-case-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/event/sentence/entity/Disposal.kt b/projects/court-case-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/event/sentence/entity/Disposal.kt index eb73ea274a..8ca3988b3f 100644 --- a/projects/court-case-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/event/sentence/entity/Disposal.kt +++ b/projects/court-case-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/event/sentence/entity/Disposal.kt @@ -149,6 +149,10 @@ class UpwAppointment( val softDeleted: Long, + val appointmentDate: LocalDate, + + val upwProjectId: Long, + @JoinColumn(name = "upw_details_id") @ManyToOne val upwDetails: UpwDetails, diff --git a/projects/court-case-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/service/CourtReportService.kt b/projects/court-case-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/service/CourtReportService.kt index 9516c4b0d5..1bac4f26f3 100644 --- a/projects/court-case-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/service/CourtReportService.kt +++ b/projects/court-case-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/service/CourtReportService.kt @@ -45,7 +45,8 @@ fun CourtReport.toCourtReport() = CourtReportMinimal( fun Staff.toStaffHuman() = StaffHuman( forenames = listOfNotNull(forename, forename2).joinToString(" "), surname = surname, - code = code + code = code, + unallocated = isUnallocated() ) fun ReportManager.toReportManager() = uk.gov.justice.digital.hmpps.api.model.conviction.ReportManager( diff --git a/projects/court-case-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/service/DocumentService.kt b/projects/court-case-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/service/DocumentService.kt new file mode 100644 index 0000000000..a348c02944 --- /dev/null +++ b/projects/court-case-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/service/DocumentService.kt @@ -0,0 +1,77 @@ +package uk.gov.justice.digital.hmpps.integrations.delius.service + +import org.springframework.stereotype.Service +import uk.gov.justice.digital.hmpps.api.model.* +import uk.gov.justice.digital.hmpps.datetime.EuropeLondon +import uk.gov.justice.digital.hmpps.exception.InvalidRequestException +import uk.gov.justice.digital.hmpps.integrations.delius.entity.* +import uk.gov.justice.digital.hmpps.integrations.delius.person.entity.PersonRepository +import uk.gov.justice.digital.hmpps.integrations.delius.person.entity.getPerson +import java.time.LocalDateTime + +@Service +class DocumentService( + private val personRepository: PersonRepository, + private val documentRepository: DocumentRepository +) { + + fun getDocumentsGroupedFor(crn: String, filterData: DocumentFilter): OffenderDocuments { + val person = personRepository.getPerson(crn) + + if (filterData.subtype != null && filterData.type == null) { + throw InvalidRequestException("subtype of ${filterData.subtype} was supplied but no type. subtype can only be supplied when a valid type is supplied") + } + val typeFilter = filterData.type?.toEnumOrElseThrow("type of ${filterData.type} was not valid") + val subType = filterData.subtype?.toEnumOrElseThrow("subtype of ${filterData.subtype} was not valid") + subType?.let { + if (!typeFilter!!.subtypes.contains(it)) { + throw InvalidRequestException("subtype of ${filterData.subtype} was not valid for type ${typeFilter.name}") + } + } + + val allDocuments = documentRepository.getPersonAndEventDocuments(person.id) + val (convictionDocuments, offenderDocuments) = allDocuments.partition { it.relatesToEvent() } + val documents = offenderDocuments.map { it.toOffenderDocumentDetail() } + .filter { filter(it, filterData) } + val convictions = convictionDocuments + .groupBy { it.eventId } + .map { + ConvictionDocuments(it.key.toString(), + it.value.map { d -> d.toOffenderDocumentDetail() } + .filter { odd -> filter(odd, filterData) }) + } + + return OffenderDocuments(documents, convictions) + } + + private fun filter(record: OffenderDocumentDetail, filter: DocumentFilter): Boolean { + var include = true + if (filter.type != null) { + (record.type.code == filter.type).also { include = it } + } + if (filter.subtype != null) { + (record.subType?.code == filter.subtype).also { include = it } + } + return include + } +} + +private inline fun > String.toEnumOrElseThrow(message: String) = + T::class.java.enumConstants.firstOrNull { it.name == this } ?: throw InvalidRequestException(message) + +fun Document.toOffenderDocumentDetail() = OffenderDocumentDetail( + id = alfrescoId, + documentName = name, + author = author, + type = KeyValue(typeCode(), typeDescription()), + extendedDescription = description, + lastModifiedAt = LocalDateTime.ofInstant(lastModifiedAt, EuropeLondon), + createdAt = LocalDateTime.ofInstant(createdAt, EuropeLondon), + parentPrimaryKeyId = primaryKeyId, + subType = subTypeDescription?.let { KeyValue(code = subTypeCode, description = it) }, + reportDocumentDates = ReportDocumentDates( + requestedDate = dateRequested?.let { LocalDateTime.ofInstant(it, EuropeLondon).toLocalDate() }, + requiredDate = dateRequired?.let { LocalDateTime.ofInstant(it, EuropeLondon).toLocalDate() }, + completedDate = completedDate?.let { LocalDateTime.ofInstant(it, EuropeLondon) }, + ) +) diff --git a/projects/court-case-and-delius/src/test/kotlin/uk/gov/justice/digital/hmpps/service/DocumentServiceTest.kt b/projects/court-case-and-delius/src/test/kotlin/uk/gov/justice/digital/hmpps/service/DocumentServiceTest.kt new file mode 100644 index 0000000000..7baab817d4 --- /dev/null +++ b/projects/court-case-and-delius/src/test/kotlin/uk/gov/justice/digital/hmpps/service/DocumentServiceTest.kt @@ -0,0 +1,168 @@ +package uk.gov.justice.digital.hmpps.service + +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.kotlin.any +import org.mockito.kotlin.whenever +import uk.gov.justice.digital.hmpps.api.model.DocumentFilter +import uk.gov.justice.digital.hmpps.data.generator.PersonGenerator +import uk.gov.justice.digital.hmpps.exception.InvalidRequestException +import uk.gov.justice.digital.hmpps.integrations.delius.entity.Document +import uk.gov.justice.digital.hmpps.integrations.delius.entity.DocumentRepository +import uk.gov.justice.digital.hmpps.integrations.delius.person.entity.PersonRepository +import uk.gov.justice.digital.hmpps.integrations.delius.service.DocumentService +import java.time.Instant + +@ExtendWith(MockitoExtension::class) +class DocumentServiceTest { + + @Mock + lateinit var personRepository: PersonRepository + + @Mock + lateinit var documentRepository: DocumentRepository + + @InjectMocks + lateinit var documentService: DocumentService + + lateinit var documents: List + + @BeforeEach + fun setUp() { + documents = listOf( + generateDocument("PREVIOUS_CONVICTION", "OFFENDER"), + generateDocument("CPS_PACK", "EVENT", eventId = 4L), + generateDocument("CPS_PACK", "EVENT", eventId = 5L), + generateDocument("TEST", "CASE_ALLOCATION"), + generateDocument("OFFENDER_DOCUMENT", "OFFENDER"), + generateDocument( + "OFFENDER_DOCUMENT", + "COURT_REPORT", + subTypeCode = "PSR", + dateRequested = Instant.now(), + dateRequired = Instant.now(), + completedDate = Instant.now() + ) + ) + + whenever(personRepository.findByCrn(any())).thenReturn(PersonGenerator.CURRENTLY_MANAGED) + } + + @Test + fun ` InvalidRequestException when subtype is supplied with no type`() { + + val filter = DocumentFilter(subtype = "AAA") + val exception = assertThrows { + documentService.getDocumentsGroupedFor("C123456", filter) + } + assertThat( + exception.message, + equalTo("subtype of AAA was supplied but no type. subtype can only be supplied when a valid type is supplied") + ) + } + + @Test + fun ` InvalidRequestException when invalid type is supplied`() { + + val filter = DocumentFilter(type = "WRONG", subtype = "AAA") + val exception = assertThrows { + documentService.getDocumentsGroupedFor("C123456", filter) + } + assertThat(exception.message, equalTo("type of WRONG was not valid")) + } + + @Test + fun ` InvalidRequestException when valid type but invalid subtype`() { + + val filter = DocumentFilter(type = "OFFENDER_DOCUMENT", subtype = "AAA") + val exception = assertThrows { + documentService.getDocumentsGroupedFor("C123456", filter) + } + assertThat(exception.message, equalTo("subtype of AAA was not valid")) + } + + @Test + fun ` InvalidRequestException when valid type, valid subtype but subtype not valid for type`() { + + val filter = DocumentFilter(type = "OFFENDER_DOCUMENT", subtype = "PSR") + val exception = assertThrows { + documentService.getDocumentsGroupedFor("C123456", filter) + } + assertThat(exception.message, equalTo("subtype of PSR was not valid for type OFFENDER_DOCUMENT")) + } + + @Test + fun ` valid type and subtype filter returns results `() { + + whenever(documentRepository.getPersonAndEventDocuments(any())).thenReturn(documents) + + val filter = DocumentFilter(type = "COURT_REPORT_DOCUMENT", subtype = "PSR") + val docs = documentService.getDocumentsGroupedFor("C123456", filter) + + assertThat(docs.documents.size, equalTo(1)) + } + + @Test + fun ` no filter returns all documents `() { + + whenever(documentRepository.getPersonAndEventDocuments(any())).thenReturn(documents) + + val filter = DocumentFilter() + val docs = documentService.getDocumentsGroupedFor("C123456", filter) + + assertThat(docs.documents.size, equalTo(4)) + assertThat(docs.convictions.size, equalTo(2)) + } + + fun generateDocument( + type: String, + tableName: String, + subTypeCode: String? = null, + dateRequested: Instant? = null, + dateRequired: Instant? = null, + completedDate: Instant? = null, + eventId: Long? = null + ) = DocumentTest( + alfrescoId = "usdiuhasduihd9sd9a8d09u", + name = "Doc1", + type = type, + tableName = tableName, + lastModifiedAt = Instant.now(), + createdAt = Instant.now(), + primaryKeyId = 2L, + author = "Test Author", + description = "A description", + eventId = eventId, + subTypeCode = subTypeCode, + subTypeDescription = "subtype description", + dateRequested = dateRequested, + dateRequired = dateRequired, + completedDate = completedDate + ) +} + +data class DocumentTest( + override val alfrescoId: String, + override val name: String, + override val type: String, + override val tableName: String, + override val lastModifiedAt: Instant?, + override val createdAt: Instant?, + override val primaryKeyId: Long?, + override val author: String?, + override val description: String?, + override val eventId: Long?, + override val subTypeCode: String?, + override val subTypeDescription: String?, + override val dateRequested: Instant?, + override val dateRequired: Instant?, + override val completedDate: Instant? +) : Document +