From 2e8ee2dfec5a64a47c14b409b302109e9be49341 Mon Sep 17 00:00:00 2001 From: mg-moj Date: Mon, 30 Sep 2024 15:03:16 +0100 Subject: [PATCH] MOP-186 create encouragement offence (#279) * MOP-186 create encouragement offence Add Admin endpoint for creating Enforcement Offence. New offence must be linked to a valid parent offence. Only one Encouragement offence should be included per parent offence. Encouragement offences are not valid for any offence with an end date prior to 2008-02-15. Tests to follow. * MOP-186 create encouragement offence Add comment to parent check and fix parentCode comparison. * MOP-186 create encouragement offence Add tests for Admin service createEncouragementOffence method. Run ktlintFormat and include .env files within .gitignore * MOP-186 create encouragement offence Include parentOffenceId within copied child Offence --- .gitignore | 3 + README.md | 6 +- gradle.properties | 1 - run-local.sh | 4 +- .../resource/AdminController.kt | 13 ++ .../manageoffencesapi/service/AdminService.kt | 70 ++++++++ .../service/AdminServiceTest.kt | 156 ++++++++++++++++++ 7 files changed, 247 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index d86190e0..334b651a 100644 --- a/.gitignore +++ b/.gitignore @@ -70,3 +70,6 @@ sonar-project.properties #Helm **/Chart.lock + +#Env files +.env diff --git a/README.md b/README.md index 610c026b..62b35229 100644 --- a/README.md +++ b/README.md @@ -48,10 +48,10 @@ Use the keytool command. if modifying cacerts pass in the option `-cacerts`, oth ### add cert The SDRS Staging api is publicly open api with root URL at -https://crime-reference-data-api.staging.service.justice.gov.uk +https://sdrs.staging.apps.hmcts.net/ However, in order for it to work with java you have to add it to the truststore, locally this can be achieved by doing the following (using the default password of changeit): -1. Download the cert from https://crime-reference-data-api.staging.service.justice.gov.uk using a browser or the following command: -`openssl s_client -servername crime-reference-data-api.staging.service.justice.gov.uk -connect crime-reference-data-api.staging.service.justice.gov.uk:443 sdrs-staging.pem` +1. Download the cert from https://sdrs.staging.apps.hmcts.net/ using a browser or the following command: +`openssl s_client -servername sdrs.staging.apps.hmcts.net -connect crime-reference-data-api.staging.service.justice.gov.uk:443 sdrs-staging.pem` 2. Then add it to the tuststore using the following command: `keytool -noprompt -storepass changeit -importcert -trustcacerts -cacerts -file sdrs-staging.pem -alias sdrs_staging_cert` diff --git a/gradle.properties b/gradle.properties index ecbb0486..aec96a59 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,4 +6,3 @@ # # To stop the dps-gradle-spring-boot project from overwriting any project specific customisations here, remove the # warning at the top of this file. -kotlin.incremental.useClasspathSnapshot=false diff --git a/run-local.sh b/run-local.sh index 139a4147..bc9cbd4b 100755 --- a/run-local.sh +++ b/run-local.sh @@ -24,8 +24,8 @@ export AWS_REGION=eu-west-1 # Match with ports defined in docker-compose.yml # export HMPPS_AUTH_URL=http://localhost:9090/auth export HMPPS_AUTH_URL=https://sign-in-dev.hmpps.service.justice.gov.uk/auth -export API_BASE_URL_PRISON_API=https://api-dev.prison.service.justice.gov.uk -export API_BASE_URL_SDRS=https://crime-reference-data-api.staging.service.justice.gov.uk +export API_BASE_URL_PRISON_API=https://prison-api-dev.prison.service.justice.gov.uk +export API_BASE_URL_SDRS=https://sdrs.staging.apps.hmcts.net/ # Make the connection without specifying the sslmode=verify-full requirement export SPRING_DATASOURCE_URL='jdbc:postgresql://${DB_SERVER}/${DB_NAME}' diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/manageoffencesapi/resource/AdminController.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/manageoffencesapi/resource/AdminController.kt index 8455d916..c29debae 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/manageoffencesapi/resource/AdminController.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/manageoffencesapi/resource/AdminController.kt @@ -6,12 +6,14 @@ import org.slf4j.LoggerFactory import org.springframework.http.MediaType 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.PostMapping import org.springframework.web.bind.annotation.PutMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController import uk.gov.justice.digital.hmpps.manageoffencesapi.model.FeatureToggle +import uk.gov.justice.digital.hmpps.manageoffencesapi.model.Offence import uk.gov.justice.digital.hmpps.manageoffencesapi.service.AdminService @RestController @@ -60,6 +62,17 @@ class AdminController( return adminService.deactivateNomisOffence(offenceIds) } + @PostMapping(value = ["/nomis/offences/encouragement/{parentOffenceId}"]) + @PreAuthorize("hasRole('ROLE_NOMIS_OFFENCE_ACTIVATOR')") + @Operation( + summary = "Create encouragement offence for parent offence", + description = "Encouragement offence creates a new record with existing parent offence value, but with 'E' suffix to the offence code", + ) + fun createEncouragementOffence(@PathVariable parentOffenceId: Long): Offence { + log.info("Create encouragement offence for parent offence") + return adminService.createEncouragementOffence(parentOffenceId) + } + companion object { val log: Logger = LoggerFactory.getLogger(this::class.java) } diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/manageoffencesapi/service/AdminService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/manageoffencesapi/service/AdminService.kt index be81a614..3549e0df 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/manageoffencesapi/service/AdminService.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/manageoffencesapi/service/AdminService.kt @@ -8,12 +8,17 @@ import org.springframework.security.core.context.SecurityContextHolder import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import uk.gov.justice.digital.hmpps.manageoffencesapi.config.AuthAwareAuthenticationToken +import uk.gov.justice.digital.hmpps.manageoffencesapi.entity.EventToRaise +import uk.gov.justice.digital.hmpps.manageoffencesapi.enum.EventType import uk.gov.justice.digital.hmpps.manageoffencesapi.enum.Feature import uk.gov.justice.digital.hmpps.manageoffencesapi.model.FeatureToggle +import uk.gov.justice.digital.hmpps.manageoffencesapi.model.Offence +import uk.gov.justice.digital.hmpps.manageoffencesapi.repository.EventToRaiseRepository import uk.gov.justice.digital.hmpps.manageoffencesapi.repository.FeatureToggleRepository import uk.gov.justice.digital.hmpps.manageoffencesapi.repository.OffenceReactivatedInNomisRepository import uk.gov.justice.digital.hmpps.manageoffencesapi.repository.OffenceRepository import java.time.LocalDate +import java.time.LocalDateTime @Service class AdminService( @@ -22,7 +27,11 @@ class AdminService( private val offenceReactivatedInNomisRepository: OffenceReactivatedInNomisRepository, private val prisonApiClient: PrisonApiClient, private val prisonApiUserClient: PrisonApiUserClient, + private val eventToRaiseRepository: EventToRaiseRepository, ) { + + private val encouragementOffenceEligibilityStartDate = LocalDate.parse("2008-02-15") + @Transactional fun toggleFeature(featureToggles: List) { featureToggles.forEach { @@ -75,6 +84,67 @@ class AdminService( } } + @Transactional + fun createEncouragementOffence(parentOffenceId: Long): Offence { + val offence = offenceRepository.findById(parentOffenceId) + .orElseThrow { EntityNotFoundException("Offence not found with ID $parentOffenceId") } + + // Parent offence should have no parentCode, or parentCode must be the same as the current Offence (indicating the parent) + if (offence.parentCode !== null && offence.parentCode !== offence.code) { + throw ValidationException("Offence must be a valid parent") + } + + if (offence.endDate !== null && offence.endDate < encouragementOffenceEligibilityStartDate) { + throw ValidationException("Offence must have an end date post $encouragementOffenceEligibilityStartDate") + } + + val children = offenceRepository.findByParentOffenceId(parentOffenceId) + val encouragementCode = offence.code + 'E' + + if (children.any { it.code == encouragementCode }) { + throw ValidationException("Encouragement offence already exists for offence code $encouragementCode") + } + + val prisonRecord = prisonApiClient.findByOffenceCodeStartsWith(offence.code, 0) + + if (prisonRecord.content.isEmpty()) { + throw ValidationException("No prison record was found for offence code ${offence.code}") + } + + val now = LocalDateTime.now() + + val encouragementOffence = offence.copy( + id = -1, + parentOffenceId = offence.id, + code = encouragementCode, + createdDate = now, + lastUpdatedDate = now, + changedDate = now, + description = "Encouragement to ${offence.description}", + cjsTitle = "Encouragement to ${offence.cjsTitle}", + ) + + val newOffence = offenceRepository.save(encouragementOffence) + + prisonApiClient.createOffences( + listOf( + prisonRecord.content.first().copy( + code = encouragementOffence.code, + description = encouragementOffence.description!!, + ), + ), + ) + + eventToRaiseRepository.save( + EventToRaise( + offenceCode = encouragementOffence.code, + eventType = EventType.OFFENCE_CHANGED, + ), + ) + + return transform(newOffence, childOffenceIds = listOf()) + } + @Transactional(readOnly = true) fun getAllToggles(): List = featureToggleRepository.findAll().map { transform(it) } diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/manageoffencesapi/service/AdminServiceTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/manageoffencesapi/service/AdminServiceTest.kt index e6ec5f33..ccdb7e3a 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/manageoffencesapi/service/AdminServiceTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/manageoffencesapi/service/AdminServiceTest.kt @@ -1,17 +1,31 @@ package uk.gov.justice.digital.hmpps.manageoffencesapi.service +import io.hypersistence.utils.hibernate.type.json.internal.JacksonUtil +import jakarta.validation.ValidationException import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.jupiter.api.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.doNothing import org.mockito.kotlin.mock import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.verifyNoMoreInteractions import org.mockito.kotlin.whenever +import uk.gov.justice.digital.hmpps.manageoffencesapi.entity.EventToRaise +import uk.gov.justice.digital.hmpps.manageoffencesapi.entity.Offence +import uk.gov.justice.digital.hmpps.manageoffencesapi.enum.EventType import uk.gov.justice.digital.hmpps.manageoffencesapi.enum.Feature.FULL_SYNC_NOMIS +import uk.gov.justice.digital.hmpps.manageoffencesapi.enum.SdrsCache.OFFENCES_A import uk.gov.justice.digital.hmpps.manageoffencesapi.model.FeatureToggle +import uk.gov.justice.digital.hmpps.manageoffencesapi.model.RestResponsePage +import uk.gov.justice.digital.hmpps.manageoffencesapi.model.external.prisonapi.Statute +import uk.gov.justice.digital.hmpps.manageoffencesapi.repository.EventToRaiseRepository import uk.gov.justice.digital.hmpps.manageoffencesapi.repository.FeatureToggleRepository import uk.gov.justice.digital.hmpps.manageoffencesapi.repository.OffenceReactivatedInNomisRepository import uk.gov.justice.digital.hmpps.manageoffencesapi.repository.OffenceRepository +import java.time.LocalDate +import java.time.LocalDateTime import java.util.Optional import uk.gov.justice.digital.hmpps.manageoffencesapi.entity.FeatureToggle as FeatureToggleEntity @@ -21,6 +35,7 @@ class AdminServiceTest { private val offenceReactivatedInNomisRepository = mock() private val prisonApiClient = mock() private val prisonApiUserClient = mock() + private val eventToRaiseRepository = mock() private val adminService = AdminService( featureToggleRepository, @@ -28,6 +43,7 @@ class AdminServiceTest { offenceReactivatedInNomisRepository, prisonApiClient, prisonApiUserClient, + eventToRaiseRepository, ) @Test @@ -64,4 +80,144 @@ class AdminServiceTest { assertThat(res).isEqualTo(listOf(FeatureToggle(FULL_SYNC_NOMIS, false))) } + + @Test + fun `Create valid Encouragement offence`() { + whenever(offenceRepository.findById(10)).thenReturn(Optional.of(PARENT_OFFENCE)) + whenever(offenceRepository.findByParentOffenceId(10)).thenReturn(listOf(CHILD_OFFENCE_ONE, CHILD_OFFENCE_TWO)) + whenever(prisonApiClient.findByOffenceCodeStartsWith(PARENT_OFFENCE.code, 0)).thenReturn( + createPrisonApiOffencesResponse(1, listOf(NOMIS_OFFENCE_AABB010)), + ) + + val encouragementCode = PARENT_OFFENCE.code + 'E' + val encouragementCjsDesc = "Encouragement to ${PARENT_OFFENCE.description}" + + val savedOffence = PARENT_OFFENCE.copy( + id = 999, + code = encouragementCode, + description = encouragementCjsDesc, + cjsTitle = "Encouragement to ${PARENT_OFFENCE.cjsTitle}", + ) + + whenever(offenceRepository.save(any())).thenReturn(savedOffence) + doNothing().`when`(prisonApiClient).createOffences(any()) + + val res = adminService.createEncouragementOffence(10) + + verify(prisonApiClient).createOffences( + listOf( + NOMIS_OFFENCE_AABB010.copy( + code = encouragementCode, + description = encouragementCjsDesc, + ), + ), + ) + + verify(eventToRaiseRepository).save( + EventToRaise( + offenceCode = PARENT_OFFENCE.code + 'E', + eventType = EventType.OFFENCE_CHANGED, + ), + ) + + assertThat(res).isEqualTo( + transform(savedOffence, childOffenceIds = listOf()), + ) + } + + @Test + fun `Encouragement offence request with existing Encouragement offence should fail`() { + val encouragementCode = PARENT_OFFENCE.code + 'E' + whenever(offenceRepository.findById(10)).thenReturn(Optional.of(PARENT_OFFENCE)) + whenever(offenceRepository.findByParentOffenceId(10)).thenReturn( + listOf( + CHILD_OFFENCE_ONE.copy( + code = encouragementCode, + ), + CHILD_OFFENCE_TWO, + ), + ) + + assertThatThrownBy { + adminService.createEncouragementOffence(10) + }.isInstanceOf(ValidationException::class.java) + .hasMessage("Encouragement offence already exists for offence code $encouragementCode") + } + + @Test + fun `Encouragement offence request with end date prior to 2008-02-15 should fail`() { + val cutOffDate = LocalDate.parse("2008-02-15") + whenever(offenceRepository.findById(10)).thenReturn( + Optional.of( + PARENT_OFFENCE.copy(endDate = LocalDate.parse("2008-02-14")), + ), + ) + + assertThatThrownBy { + adminService.createEncouragementOffence(10) + }.isInstanceOf(ValidationException::class.java) + .hasMessage("Offence must have an end date post $cutOffDate") + } + + @Test + fun `Encouragement offence request with no Prison API record should fail`() { + whenever(offenceRepository.findById(10)).thenReturn(Optional.of(PARENT_OFFENCE)) + whenever(offenceRepository.findByParentOffenceId(10)).thenReturn(listOf(CHILD_OFFENCE_ONE, CHILD_OFFENCE_TWO)) + whenever(prisonApiClient.findByOffenceCodeStartsWith(PARENT_OFFENCE.code, 0)).thenReturn( + createPrisonApiOffencesResponse(0, listOf()), + ) + assertThatThrownBy { + adminService.createEncouragementOffence(10) + }.isInstanceOf(ValidationException::class.java) + .hasMessage("No prison record was found for offence code ${PARENT_OFFENCE.code}") + } + + private fun createPrisonApiOffencesResponse( + totalPages: Int, + content: List, + ): RestResponsePage = + RestResponsePage( + content = content, + number = 1, + size = 1, + totalElements = 0L, + pageable = JacksonUtil.toJsonNode("{}"), + last = true, + totalPages = totalPages, + sort = JacksonUtil.toJsonNode("{}"), + first = true, + numberOfElements = 0, + ) + + companion object { + private val PARENT_OFFENCE = Offence( + id = 10, + parentOffenceId = 10, + code = "AABB010", + description = "Desc", + cjsTitle = "Cjs title", + changedDate = LocalDateTime.now(), + revisionId = 1, + startDate = LocalDate.now(), + endDate = LocalDate.parse("2028-01-01"), + sdrsCache = OFFENCES_A, + ) + private val CHILD_OFFENCE_ONE = PARENT_OFFENCE.copy( + id = 11, + code = "AABB011", + parentOffenceId = 10, + ) + private val CHILD_OFFENCE_TWO = PARENT_OFFENCE.copy( + id = 12, + code = "AABB012", + parentOffenceId = 10, + ) + private val NOMIS_OFFENCE_AABB010 = uk.gov.justice.digital.hmpps.manageoffencesapi.model.external.prisonapi.Offence( + code = "AABB010", + description = "Parent Desc", + statuteCode = Statute(code = "A123", description = "Statute desc", activeFlag = "Y", legislatingBodyCode = "UK"), + severityRanking = "99", + activeFlag = "Y", + ) + } }