Skip to content

Commit

Permalink
Merge pull request #2872 from ministryofjustice/cas2v2/dev
Browse files Browse the repository at this point in the history
Cas2v2/dev
  • Loading branch information
tobybatchmoj authored Jan 28, 2025
2 parents 1702674 + 27e6cbd commit d6ecba3
Show file tree
Hide file tree
Showing 93 changed files with 17,325 additions and 81 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,5 @@ Confluence](https://dsdmoj.atlassian.net/wiki/spaces/AP/pages/4330226204/Environ

## Release process

Our release process aligns with the other CAS teams and as such [lives in Confluence](https://dsdmoj.atlassian.net/wiki/spaces/AP/pages/4247847062/Release+process). The steps are also available in the [PULL_REQUEST_TEMPLATE](/.github/PULL_REQUEST_TEMPLATE.md#release-checklist).
Our release process aligns with the other CAS teams and as such [lives in Confluence](https://dsdmoj.atlassian.net/wiki/spaces/AP/pages/4247847062/Release+process). The steps are also available in the [PULL_REQUEST_TEMPLATE](/.github/PULL_REQUEST_TEMPLATE.md#release-checklist).

15 changes: 15 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,14 @@ registerAdditionalOpenApiGenerateTask(
useTags = true,
)

registerAdditionalOpenApiGenerateTask(
name = "openApiGenerateCas2v2Namespace",
ymlPath = "$rootDir/src/main/resources/static/codegen/built-cas2v2-api-spec.yml",
apiPackageName = "uk.gov.justice.digital.hmpps.approvedpremisesapi.api.cas2v2",
modelPackageName = "uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model",
apiSuffix = "Cas2v2",
)

registerAdditionalOpenApiGenerateTask(
name = "openApiGenerateCas2Namespace",
ymlPath = "$rootDir/src/main/resources/static/codegen/built-cas2-api-spec.yml",
Expand Down Expand Up @@ -329,6 +337,7 @@ tasks.register("openApiPreCompilation") {
.readFileToString(file, "UTF-8")
.replace("_shared.yml#/components", "#/components")
.replace("cas1-schemas.yml#/components", "#/components")
.replace("cas2v2-schemas.yml#/components", "#/components")
FileUtils.writeStringToFile(file, updatedContents, "UTF-8")
}

Expand Down Expand Up @@ -381,6 +390,11 @@ tasks.register("openApiPreCompilation") {
outputFileName = "built-cas2-api-spec.yml",
inputSpec = "cas2-api.yml",
)
buildSpecWithSharedComponentsAppended(
outputFileName = "built-cas2v2-api-spec.yml",
inputSpec = "cas2v2-api.yml",
inputSchemas = "cas2v2-schemas.yml",
)
buildSpecWithSharedComponentsAppended(
outputFileName = "built-cas3-api-spec.yml",
inputSpec = "cas3-api.yml",
Expand All @@ -394,6 +408,7 @@ tasks.get("openApiGenerate").dependsOn(
"openApiPreCompilation",
"openApiGenerateCas1Namespace",
"openApiGenerateCas2Namespace",
"openApiGenerateCas2v2Namespace",
"openApiGenerateCas3Namespace",
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,17 @@ class OAuth2ResourceServerSecurityConfiguration {
authorize(HttpMethod.GET, "/cas2/reference-data/**", hasAnyRole("CAS2_ASSESSOR", "POM"))
authorize(HttpMethod.GET, "/cas2/reports/**", hasRole("CAS2_MI"))
authorize("/cas2/**", hasAnyAuthority("ROLE_POM", "ROLE_LICENCE_CA"))

authorize(HttpMethod.PUT, "/cas2v2/assessments/**", hasRole("CAS2_ASSESSOR"))
authorize(HttpMethod.GET, "/cas2v2/assessments/**", hasAnyRole("CAS2_ASSESSOR", "CAS2_ADMIN"))
authorize(HttpMethod.POST, "/cas2v2/assessments/*/status-updates", hasRole("CAS2_ASSESSOR"))
authorize(HttpMethod.POST, "/cas2v2/assessments/*/notes", hasAnyRole("LICENCE_CA", "POM", "CAS2_ASSESSOR"))
authorize(HttpMethod.GET, "/cas2v2/submissions/**", hasAnyRole("CAS2_ASSESSOR", "CAS2_ADMIN"))
authorize(HttpMethod.POST, "/cas2v2/submissions/*/status-updates", hasRole("CAS2_ASSESSOR"))
authorize(HttpMethod.GET, "/cas2v2/reference-data/**", hasAnyRole("CAS2_ASSESSOR", "POM"))
authorize(HttpMethod.GET, "/cas2v2/reports/**", hasRole("CAS2_MI"))
authorize("/cas2v2/**", hasAnyAuthority("ROLE_POM", "ROLE_LICENCE_CA"))

authorize(HttpMethod.GET, "/cas3-api.yml", permitAll)
authorize(HttpMethod.GET, "/subject-access-request", hasAnyRole("SAR_DATA_ACCESS"))
authorize(anyRequest, hasAuthority("ROLE_PROBATION"))
Expand Down Expand Up @@ -174,6 +185,8 @@ class AuthAwareAuthenticationToken(
return aPrincipal
}

fun authenticationSource(): String = jwt.claims["auth_source"] as String

fun isExternalUser(): Boolean {
return jwt.claims["auth_source"] == "auth"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ class ApplicationsController(
.body(applicationsTransformer.transformJpaToApi(application, personInfo))
}

@Suppress("TooGenericExceptionThrown")
private fun createApplication(
serviceName: ServiceName,
personInfo: PersonInfoResult.Success.Full,
Expand Down Expand Up @@ -241,7 +242,15 @@ class ApplicationsController(
)
}

ServiceName.cas2 -> error("CAS2 now has its own Cas2ApplicationsController")
ServiceName.cas2 -> throw RuntimeException(
"CAS2 now has its own " +
"Cas2ApplicationsController",
)

ServiceName.cas2v2 -> throw RuntimeException(
"CAS2v2 now has its own " +
"Cas2v2ApplicationsController",
)
}

@Transactional
Expand Down Expand Up @@ -448,6 +457,7 @@ class ApplicationsController(
is ValidatableActionResult.GeneralValidationError -> throw BadRequestProblem(errorDetail = validationResult.message)
is ValidatableActionResult.FieldValidationError -> throw BadRequestProblem(invalidParams = validationResult.validationMessages)
is ValidatableActionResult.ConflictError -> throw ConflictProblem(id = validationResult.conflictingEntityId, conflictReason = validationResult.message)

is ValidatableActionResult.Success -> validationResult.entity
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ class AssessmentController(
val domainSummaryStatuses = statuses?.map { assessmentTransformer.transformApiStatusToDomainSummaryState(it) } ?: emptyList()

val (summaries, metadata) = when (xServiceName) {
ServiceName.cas2v2 -> throw UnsupportedOperationException("CAS2v2 not supported")
ServiceName.cas2 -> throw UnsupportedOperationException("CAS2 not supported")
ServiceName.temporaryAccommodation -> {
val (summaries, metadata) = assessmentService.getAssessmentSummariesForUserCAS3(
Expand All @@ -77,10 +78,12 @@ class AssessmentController(
val transformSummaries = when (sortBy) {
AssessmentSortField.assessmentDueAt -> throw BadRequestProblem(errorDetail = "Sorting by due date is not supported for CAS3")
AssessmentSortField.personName -> transformDomainToApi(user, summaries, user.hasQualification(UserQualification.LAO)).sortByName(resolvedSortDirection)

else -> transformDomainToApi(user, summaries)
}
Pair(transformSummaries, metadata)
}

else -> {
val (summaries, metadata) = assessmentService.getVisibleAssessmentSummariesForUserCAS1(user, domainSummaryStatuses, PageCriteria(resolvedSortBy, resolvedSortDirection, page, perPage))
Pair(transformDomainToApi(user, summaries, user.hasQualification(UserQualification.LAO)), metadata)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ class PremisesController(
private val cas1WithdrawableService: Cas1WithdrawableService,
) : PremisesApiDelegate {

@Suppress("TooGenericExceptionThrown")
override fun premisesSummaryGet(
xServiceName: ServiceName,
probationRegionId: UUID?,
Expand All @@ -139,6 +140,8 @@ class PremisesController(
}

ServiceName.cas2 -> throw RuntimeException("CAS2 not supported")
ServiceName.cas2v2 -> throw RuntimeException("CAS2v2 not supported")

ServiceName.temporaryAccommodation -> {
val user = usersService.getUserForRequest()
val summaries = cas3PremisesService.getAllPremisesSummaries(user.probationRegion.id)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package uk.gov.justice.digital.hmpps.approvedpremisesapi.controller.cas2v2

import com.fasterxml.jackson.databind.ObjectMapper
import jakarta.transaction.Transactional
import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Service
import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.cas2v2.ApplicationsCas2v2Delegate
import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Application
import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.NewCas2v2Application
import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.SortDirection
import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.UpdateApplication
import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.UpdateCas2v2Application
import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2v2.Cas2v2ApplicationEntity
import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2v2.Cas2v2ApplicationSummaryEntity
import uk.gov.justice.digital.hmpps.approvedpremisesapi.problem.BadRequestProblem
import uk.gov.justice.digital.hmpps.approvedpremisesapi.problem.ConflictProblem
import uk.gov.justice.digital.hmpps.approvedpremisesapi.problem.ForbiddenProblem
import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.ValidatableActionResult
import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2.OffenderService
import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2v2.Cas2v2ApplicationService
import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2v2.Cas2v2UserService
import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas2v2.Cas2v2ApplicationsTransformer
import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.PageCriteria
import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.ensureEntityFromCasResultIsSuccess
import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.extractEntityFromCasResult
import java.net.URI
import java.util.UUID
import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas2v2ApplicationSummary as ModelCas2v2ApplicationSummary

@Service(
"Cas2v2ApplicationController",
)
class Cas2v2ApplicationController(
private val cas2v2ApplicationService: Cas2v2ApplicationService,
private val cas2v2ApplicationsTransformer: Cas2v2ApplicationsTransformer,
private val objectMapper: ObjectMapper,
private val cas2OffenderService: OffenderService,
private val userService: Cas2v2UserService,
) : ApplicationsCas2v2Delegate {

override fun applicationsGet(
isSubmitted: Boolean?,
page: Int?,
prisonCode: String?,
): ResponseEntity<List<ModelCas2v2ApplicationSummary>> {
val user = userService.getUserForRequest()

prisonCode?.let { if (prisonCode != user.activeNomisCaseloadId) throw ForbiddenProblem() }

val pageCriteria = PageCriteria("createdAt", SortDirection.desc, page)

val (applications, metadata) = cas2v2ApplicationService.getCas2v2Applications(prisonCode, isSubmitted, user, pageCriteria)

return ResponseEntity.ok().headers(
metadata?.toHeaders(),
).body(getPersonNamesAndTransformToSummaries(applications))
}

override fun applicationsApplicationIdGet(applicationId: UUID): ResponseEntity<Application> {
val user = userService.getUserForRequest()

val applicationResult = cas2v2ApplicationService
.getCas2v2ApplicationForUser(
applicationId,
user,
)

val application = extractEntityFromCasResult(applicationResult)
return ResponseEntity.ok(getPersonDetailAndTransform(application))
}

@Transactional
override fun applicationsPost(body: NewCas2v2Application): ResponseEntity<Application> {
val user = userService.getUserForRequest()

val personInfo = cas2OffenderService.getFullInfoForPersonOrThrow(body.crn)

val applicationResult = cas2v2ApplicationService.createCas2v2Application(
body.crn,
user,
body.applicationOrigin,
body.bailHearingDate,
)

val application = when (applicationResult) {
is ValidatableActionResult.GeneralValidationError -> throw BadRequestProblem(errorDetail = applicationResult.message)
is ValidatableActionResult.FieldValidationError -> throw BadRequestProblem(invalidParams = applicationResult.validationMessages)
is ValidatableActionResult.ConflictError -> throw ConflictProblem(id = applicationResult.conflictingEntityId, conflictReason = applicationResult.message)

is ValidatableActionResult.Success -> applicationResult.entity
}

return ResponseEntity
.created(URI.create("/cas2v2/applications/${application.id}"))
.body(cas2v2ApplicationsTransformer.transformJpaToApi(application, personInfo))
}

@Suppress("TooGenericExceptionThrown")
@Transactional
override fun applicationsApplicationIdPut(
applicationId: UUID,
body: UpdateApplication,
): ResponseEntity<Application> {
val user = userService.getUserForRequest()

val serializedData = objectMapper.writeValueAsString(body.data)

val applicationResult = when (body) {
is UpdateCas2v2Application -> cas2v2ApplicationService.updateCas2v2Application(
applicationId = applicationId,
data = serializedData,
user,
body.bailHearingDate,
)

else -> throw RuntimeException("Unsupported UpdateApplication type: ${body::class.qualifiedName}")
}

val entity = extractEntityFromCasResult(applicationResult)
return ResponseEntity.ok(getPersonDetailAndTransform(entity))
}

@Transactional
override fun applicationsApplicationIdAbandonPut(applicationId: UUID): ResponseEntity<Unit> {
val user = userService.getUserForRequest()

val applicationResult = cas2v2ApplicationService.abandonCas2v2Application(applicationId, user)
ensureEntityFromCasResultIsSuccess(applicationResult)
return ResponseEntity.ok(Unit)
}

private fun getPersonNamesAndTransformToSummaries(
applicationSummaries: List<Cas2v2ApplicationSummaryEntity>,
): List<ModelCas2v2ApplicationSummary> {
val crns = applicationSummaries.map { it.crn }

val personNamesMap = cas2OffenderService.getMapOfPersonNamesAndCrns(crns)

return applicationSummaries.map { application ->
cas2v2ApplicationsTransformer.transformJpaSummaryToSummary(application, personNamesMap[application.crn]!!)
}
}

private fun getPersonDetailAndTransform(
application: Cas2v2ApplicationEntity,
): Application {
val personInfo = cas2OffenderService.getFullInfoForPersonOrThrow(application.crn)

return cas2v2ApplicationsTransformer.transformJpaToApi(application, personInfo)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package uk.gov.justice.digital.hmpps.approvedpremisesapi.controller.cas2v2

import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Service
import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.cas2v2.AssessmentsCas2v2Delegate
import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas2v2ApplicationNote
import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas2v2Assessment
import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas2v2AssessmentStatusUpdate
import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.NewCas2ApplicationNote
import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.UpdateCas2Assessment
import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.CasResult
import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2v2.Cas2v2ApplicationNoteService
import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2v2.Cas2v2AssessmentService
import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2v2.Cas2v2StatusUpdateService
import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2v2.Cas2v2UserService
import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas2v2.Cas2v2ApplicationNotesTransformer
import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas2v2.Cas2v2AssessmentsTransformer
import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.extractEntityFromCasResult
import java.net.URI
import java.util.UUID

@Service("Cas2v2AssessmentsController")
class Cas2v2AssessmentsController(
private val cas2v2AssessmentService: Cas2v2AssessmentService,
private val cas2v2ApplicationNoteService: Cas2v2ApplicationNoteService,
private val cas2v2AssessmentsTransformer: Cas2v2AssessmentsTransformer,
private val cas2v2ApplicationNotesTransformer: Cas2v2ApplicationNotesTransformer,
private val cas2v2StatusUpdateService: Cas2v2StatusUpdateService,
private val cas2v2UserService: Cas2v2UserService,
) : AssessmentsCas2v2Delegate {

override fun assessmentsAssessmentIdGet(assessmentId: UUID): ResponseEntity<Cas2v2Assessment> {
val assessmentResult = cas2v2AssessmentService.getAssessment(assessmentId)
val cas2v2AssessmentEntity = extractEntityFromCasResult(assessmentResult)
return ResponseEntity.ok(cas2v2AssessmentsTransformer.transformJpaToApiRepresentation(cas2v2AssessmentEntity))
}

override fun assessmentsAssessmentIdPut(
assessmentId: UUID,
updateCas2Assessment: UpdateCas2Assessment,
): ResponseEntity<Cas2v2Assessment> {
val assessmentResult = cas2v2AssessmentService.updateAssessment(assessmentId, updateCas2Assessment)

val cas2v2AssessmentEntity = extractEntityFromCasResult(assessmentResult)
return ResponseEntity.ok(
cas2v2AssessmentsTransformer.transformJpaToApiRepresentation(cas2v2AssessmentEntity),
)
}

override fun assessmentsAssessmentIdStatusUpdatesPost(
assessmentId: UUID,
cas2v2AssessmentStatusUpdate: Cas2v2AssessmentStatusUpdate,
): ResponseEntity<Unit> {
val result = cas2v2StatusUpdateService.createForAssessment(
assessmentId = assessmentId,
statusUpdate = cas2v2AssessmentStatusUpdate,
assessor = cas2v2UserService.getUserForRequest(),
)

processAuthorisationFor(result).run { processValidation(result) }

return ResponseEntity(HttpStatus.CREATED)
}

override fun assessmentsAssessmentIdNotesPost(
assessmentId: UUID,
body: NewCas2ApplicationNote,
): ResponseEntity<Cas2v2ApplicationNote> {
val noteResult = cas2v2ApplicationNoteService.createAssessmentNote(assessmentId, body)

val note = extractEntityFromCasResult(noteResult)
return ResponseEntity.created(URI.create("/cas2v2/assessments/$assessmentId/notes/${note.id}"))
.body(
cas2v2ApplicationNotesTransformer.transformJpaToApi(note),
)
}

private fun <EntityType> processAuthorisationFor(
result: CasResult<EntityType>,
): Any? {
return extractEntityFromCasResult(result)
}

private fun <EntityType : Any> processValidation(casResult: CasResult<EntityType>): Any {
return extractEntityFromCasResult(casResult)
}
}
Loading

0 comments on commit d6ecba3

Please sign in to comment.