Skip to content

Commit

Permalink
MOP-186 create encouragement offence (#279)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
mg-moj authored Sep 30, 2024
1 parent 513dc1a commit 2e8ee2d
Show file tree
Hide file tree
Showing 7 changed files with 247 additions and 6 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,6 @@ sonar-project.properties

#Helm
**/Chart.lock

#Env files
.env
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 </dev/null | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' >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 </dev/null | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' >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`

Expand Down
1 change: 0 additions & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions run-local.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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}'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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<FeatureToggle>) {
featureToggles.forEach {
Expand Down Expand Up @@ -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<FeatureToggle> =
featureToggleRepository.findAll().map { transform(it) }
Expand Down
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -21,13 +35,15 @@ class AdminServiceTest {
private val offenceReactivatedInNomisRepository = mock<OffenceReactivatedInNomisRepository>()
private val prisonApiClient = mock<PrisonApiClient>()
private val prisonApiUserClient = mock<PrisonApiUserClient>()
private val eventToRaiseRepository = mock<EventToRaiseRepository>()

private val adminService = AdminService(
featureToggleRepository,
offenceRepository,
offenceReactivatedInNomisRepository,
prisonApiClient,
prisonApiUserClient,
eventToRaiseRepository,
)

@Test
Expand Down Expand Up @@ -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<uk.gov.justice.digital.hmpps.manageoffencesapi.model.external.prisonapi.Offence>,
): RestResponsePage<uk.gov.justice.digital.hmpps.manageoffencesapi.model.external.prisonapi.Offence> =
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",
)
}
}

0 comments on commit 2e8ee2d

Please sign in to comment.