From c8de685302c198eb2fed62589ecf7472e9508a7d Mon Sep 17 00:00:00 2001 From: Toby Batch Date: Thu, 28 Nov 2024 10:36:16 +0000 Subject: [PATCH 01/37] feat: added open api scaffold for cas2 to cas2bail routes and delegates --- build.gradle.kts | 24 +- .../controller/cas2bail/.keep | 0 src/main/resources/static/cas2bail-api.yml | 589 ++ .../resources/static/cas2bail-schemas.yml | 4 + .../codegen/built-cas2bail-api-spec.yml | 5779 +++++++++++++++++ .../static/codegen/built-cas3-api-spec.yml | 3 +- 6 files changed, 6397 insertions(+), 2 deletions(-) create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/.keep create mode 100644 src/main/resources/static/cas2bail-api.yml create mode 100644 src/main/resources/static/cas2bail-schemas.yml create mode 100644 src/main/resources/static/codegen/built-cas2bail-api-spec.yml diff --git a/build.gradle.kts b/build.gradle.kts index de34c5f76f..16fcc36d1c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -159,7 +159,14 @@ tasks.bootRun { } tasks.withType { - jvmArgs("--add-opens", "java.base/java.lang=ALL-UNNAMED", "--add-opens", "java.base/java.lang.reflect=ALL-UNNAMED", "--add-opens", "java.base/java.time=ALL-UNNAMED") + jvmArgs( + "--add-opens", + "java.base/java.lang=ALL-UNNAMED", + "--add-opens", + "java.base/java.lang.reflect=ALL-UNNAMED", + "--add-opens", + "java.base/java.time=ALL-UNNAMED" + ) afterEvaluate { if (environment["CI"] != null) { @@ -248,6 +255,14 @@ registerAdditionalOpenApiGenerateTask( useTags = true, ) +registerAdditionalOpenApiGenerateTask( + name = "openApiGenerateCas2bailNamespace", + ymlPath = "$rootDir/src/main/resources/static/codegen/built-cas2bail-api-spec.yml", + apiPackageName = "uk.gov.justice.digital.hmpps.approvedpremisesapi.api.cas2bail", + modelPackageName = "uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model", + apiSuffix = "Cas2bail", +) + registerAdditionalOpenApiGenerateTask( name = "openApiGenerateCas2Namespace", ymlPath = "$rootDir/src/main/resources/static/codegen/built-cas2-api-spec.yml", @@ -327,6 +342,7 @@ tasks.register("openApiPreCompilation") { .readFileToString(file, "UTF-8") .replace("_shared.yml#/components", "#/components") .replace("cas1-schemas.yml#/components", "#/components") + .replace("cas2bail-schemas.yml#/components", "#/components") FileUtils.writeStringToFile(file, updatedContents, "UTF-8") } @@ -379,6 +395,11 @@ tasks.register("openApiPreCompilation") { outputFileName = "built-cas2-api-spec.yml", inputSpec = "cas2-api.yml", ) + buildSpecWithSharedComponentsAppended( + outputFileName = "built-cas2bail-api-spec.yml", + inputSpec = "cas2bail-api.yml", + inputSchemas = "cas2bail-schemas.yml", + ) buildSpecWithSharedComponentsAppended( outputFileName = "built-cas3-api-spec.yml", inputSpec = "cas3-api.yml", @@ -392,6 +413,7 @@ tasks.get("openApiGenerate").dependsOn( "openApiPreCompilation", "openApiGenerateCas1Namespace", "openApiGenerateCas2Namespace", + "openApiGenerateCas2bailNamespace", "openApiGenerateCas3Namespace", ) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/.keep b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/main/resources/static/cas2bail-api.yml b/src/main/resources/static/cas2bail-api.yml new file mode 100644 index 0000000000..90c33adcb5 --- /dev/null +++ b/src/main/resources/static/cas2bail-api.yml @@ -0,0 +1,589 @@ +openapi: 3.0.1 +info: + title: 'Community Accommodation Services: Tier 2 (CAS2 Bail)' + version: 1.0.0 +servers: + - url: /cas2bail +paths: + /applications: + post: + tags: + - Operations on CAS2 Bail applications + summary: Creates a CAS2 Bail application + requestBody: + description: Information to create a blank application with + content: + 'application/json': + schema: + $ref: '_shared.yml#/components/schemas/NewApplication' + required: true + responses: + 201: + description: successful operation + content: + 'application/json': + schema: + $ref: '_shared.yml#/components/schemas/Application' + 400: + description: invalid params + content: + 'application/problem+json': + schema: + $ref: '_shared.yml#/components/schemas/ValidationError' + 401: + $ref: '_shared.yml#/components/responses/401Response' + 403: + $ref: '_shared.yml#/components/responses/403Response' + 404: + description: invalid CRN + content: + 'application/json': + schema: + $ref: '_shared.yml#/components/schemas/Problem' + 500: + $ref: '_shared.yml#/components/responses/500Response' + x-codegen-request-body-name: body + get: + tags: + - Operations on CAS2 Bail applications + summary: List summaries of all CAS2 Bail applications authorised for the logged in user + parameters: + - name: isSubmitted + in: query + description: Returns submitted applications if true, un submitted applications if false, and all applications if absent + schema: + type: boolean + - name: page + in: query + description: Page number of results to return. If blank, returns all results + schema: + type: integer + - name: prisonCode + in: query + description: Prison code of applications to return. If blank, returns all results. + schema: + type: string + responses: + 200: + description: successful operation + content: + 'application/json': + schema: + type: array + items: + $ref: '_shared.yml#/components/schemas/Cas2ApplicationSummary' + headers: + X-Pagination-CurrentPage: + $ref: '_shared.yml#/components/headers/X-Pagination-CurrentPage' + X-Pagination-TotalPages: + $ref: '_shared.yml#/components/headers/X-Pagination-TotalPages' + X-Pagination-TotalResults: + $ref: '_shared.yml#/components/headers/X-Pagination-TotalResults' + X-Pagination-PageSize: + $ref: '_shared.yml#/components/headers/X-Pagination-TotalResults' + 401: + $ref: '_shared.yml#/components/responses/401Response' + 403: + $ref: '_shared.yml#/components/responses/403Response' + 500: + $ref: '_shared.yml#/components/responses/500Response' + + /applications/{applicationId}: + put: + tags: + - Operations on CAS2 Bail applications + summary: Updates a CAS2 Bail application + parameters: + - name: applicationId + in: path + description: ID of the application + required: true + schema: + type: string + format: uuid + requestBody: + description: Information to update the application with + content: + 'application/json': + schema: + $ref: '_shared.yml#/components/schemas/UpdateApplication' + required: true + responses: + 200: + description: successful operation + content: + 'application/json': + schema: + $ref: '_shared.yml#/components/schemas/Application' + 400: + description: invalid params + content: + 'application/problem+json': + schema: + $ref: '_shared.yml#/components/schemas/ValidationError' + 401: + $ref: '_shared.yml#/components/responses/401Response' + 403: + $ref: '_shared.yml#/components/responses/403Response' + 500: + $ref: '_shared.yml#/components/responses/500Response' + x-codegen-request-body-name: body + get: + tags: + - Operations on CAS2 Bail applications + summary: Gets a single CAS2 Bail application by its ID + parameters: + - name: applicationId + in: path + description: ID of the application + required: true + schema: + type: string + format: uuid + responses: + 200: + description: successful operation + content: + 'application/json': + schema: + $ref: '_shared.yml#/components/schemas/Application' + 401: + $ref: '_shared.yml#/components/responses/401Response' + 403: + $ref: '_shared.yml#/components/responses/403Response' + 500: + $ref: '_shared.yml#/components/responses/500Response' + /applications/{applicationId}/abandon: + put: + tags: + - Operations on CAS2 Bail applications + summary: Abandons an in progress CAS2 Bail application + parameters: + - name: applicationId + in: path + description: ID of the application + required: true + schema: + type: string + format: uuid + responses: + 200: + description: successful operation + 401: + $ref: '_shared.yml#/components/responses/401Response' + 409: + description: The application has been submitted + content: + 'application/json': + schema: + $ref: '_shared.yml#/components/schemas/Problem' + 500: + $ref: '_shared.yml#/components/responses/500Response' + /assessments/{assessmentId}: + put: + tags: + - Operations on submitted CAS2 Bail applications (Assessors) + summary: Updates a single CAS2 Bail assessment by its ID + parameters: + - name: assessmentId + in: path + description: ID of the assessment + required: true + schema: + type: string + format: uuid + requestBody: + description: Information to update the assessment with + content: + 'application/json': + schema: + $ref: '_shared.yml#/components/schemas/UpdateCas2Assessment' + required: true + responses: + 200: + description: successful operation + content: + 'application/json': + schema: + $ref: '_shared.yml#/components/schemas/Cas2Assessment' + 401: + $ref: '_shared.yml#/components/responses/401Response' + 403: + $ref: '_shared.yml#/components/responses/403Response' + 404: + description: invalid assessmentId + content: + 'application/json': + schema: + $ref: '_shared.yml#/components/schemas/Problem' + 500: + $ref: '_shared.yml#/components/responses/500Response' + get: + tags: + - Operations on submitted CAS2 Bail applications (Assessors) + summary: Gets a single CAS2 Bail assessment by its ID + parameters: + - name: assessmentId + in: path + description: ID of the assessment + required: true + schema: + type: string + format: uuid + responses: + 200: + description: successful operation + content: + 'application/json': + schema: + $ref: '_shared.yml#/components/schemas/Cas2Assessment' + 401: + $ref: '_shared.yml#/components/responses/401Response' + 403: + $ref: '_shared.yml#/components/responses/403Response' + 404: + description: invalid assessmentId + content: + 'application/json': + schema: + $ref: '_shared.yml#/components/schemas/Problem' + 500: + $ref: '_shared.yml#/components/responses/500Response' + /assessments/{assessmentId}/status-updates: + post: + tags: + - Operations on submitted CAS2 Bail applications (Assessors) + summary: Creates a status update on an assessment + parameters: + - name: assessmentId + in: path + description: ID of the assessment whose status is to be updated + required: true + schema: + type: string + format: uuid + requestBody: + description: Information on the new status to be applied + content: + 'application/json': + schema: + $ref: '_shared.yml#/components/schemas/Cas2AssessmentStatusUpdate' + required: true + responses: + 200: + description: successfully created the status update + 400: + description: status update has already been submitted + content: + 'application/problem+json': + schema: + $ref: '_shared.yml#/components/schemas/ValidationError' + 401: + $ref: '_shared.yml#/components/responses/401Response' + 403: + $ref: '_shared.yml#/components/responses/403Response' + 500: + $ref: '_shared.yml#/components/responses/500Response' + /assessments/{assessmentId}/notes: + post: + tags: + - Operations on CAS2 Bail assessments + summary: Add a note to an assessment + parameters: + - name: assessmentId + in: path + description: ID of the assessment + required: true + schema: + type: string + format: uuid + requestBody: + description: the note to add + content: + 'application/json': + schema: + $ref: '_shared.yml#/components/schemas/NewCas2ApplicationNote' + required: true + responses: + 200: + description: successful operation + content: + 'application/json': + schema: + $ref: '_shared.yml#/components/schemas/Cas2ApplicationNote' + 401: + $ref: '_shared.yml#/components/responses/401Response' + 403: + $ref: '_shared.yml#/components/responses/403Response' + 404: + description: invalid assessmentId + content: + 'application/json': + schema: + $ref: '_shared.yml#/components/schemas/Problem' + 500: + $ref: '_shared.yml#/components/responses/500Response' + x-codegen-request-body-name: body + /submissions: + get: + tags: + - Operations on submitted CAS2 Bail applications (Assessors) + summary: List summaries of all submitted CAS2 Bail applications + parameters: + - name: page + in: query + description: Page number of results to return. If blank, returns all results + schema: + type: integer + responses: + 200: + description: successful operation + content: + 'application/json': + schema: + type: array + items: + $ref: '_shared.yml#/components/schemas/Cas2SubmittedApplicationSummary' + headers: + X-Pagination-CurrentPage: + $ref: '_shared.yml#/components/headers/X-Pagination-CurrentPage' + X-Pagination-TotalPages: + $ref: '_shared.yml#/components/headers/X-Pagination-TotalPages' + X-Pagination-TotalResults: + $ref: '_shared.yml#/components/headers/X-Pagination-TotalResults' + X-Pagination-PageSize: + $ref: '_shared.yml#/components/headers/X-Pagination-TotalResults' + 401: + $ref: '_shared.yml#/components/responses/401Response' + 403: + $ref: '_shared.yml#/components/responses/403Response' + 500: + $ref: '_shared.yml#/components/responses/500Response' + post: + tags: + - Operations on CAS2 Bail applications + summary: Submits a CAS2 Bail Application (creates a SubmittedApplication) + requestBody: + description: Information needed to submit an application + content: + 'application/json': + schema: + $ref: '_shared.yml#/components/schemas/SubmitCas2Application' + required: true + responses: + 200: + description: successfully submitted the application + 400: + description: application has already been submitted + content: + 'application/problem+json': + schema: + $ref: '_shared.yml#/components/schemas/ValidationError' + 401: + $ref: '_shared.yml#/components/responses/401Response' + 403: + $ref: '_shared.yml#/components/responses/403Response' + 500: + $ref: '_shared.yml#/components/responses/500Response' + /submissions/{applicationId}: + get: + tags: + - Operations on submitted CAS2 Bail applications (Assessors) + summary: Gets a single submitted CAS2 Bail application by its ID + parameters: + - name: applicationId + in: path + description: ID of the application + required: true + schema: + type: string + format: uuid + responses: + 200: + description: successful operation + content: + 'application/json': + schema: + $ref: '_shared.yml#/components/schemas/Cas2SubmittedApplication' + 401: + $ref: '_shared.yml#/components/responses/401Response' + 403: + $ref: '_shared.yml#/components/responses/403Response' + 500: + $ref: '_shared.yml#/components/responses/500Response' + /people/search: + get: + tags: + - People operations + summary: Searches for a Person by their Prison Number (NOMIS ID) + parameters: + - name: nomsNumber + in: query + description: Prison Number to search for + required: true + schema: + type: string + responses: + 200: + description: successful operation + content: + 'application/json': + schema: + $ref: '_shared.yml#/components/schemas/Person' + 400: + description: invalid params + content: + 'application/problem+json': + schema: + $ref: '_shared.yml#/components/schemas/ValidationError' + 404: + description: invalid CRN + content: + 'application/json': + schema: + $ref: '_shared.yml#/components/schemas/Problem' + 401: + $ref: '_shared.yml#/components/responses/401Response' + 403: + $ref: '_shared.yml#/components/responses/403Response' + 500: + $ref: '_shared.yml#/components/responses/500Response' + /people/{crn}/oasys/risk-to-self: + get: + tags: + - People operations + summary: Returns the Risk To Individual (known as Risk to Self on frontend) section of an OASys. + parameters: + - name: crn + in: path + description: CRN of the Person to fetch latest OASys + required: true + schema: + type: string + responses: + 200: + description: successful operation + content: + 'application/json': + schema: + $ref: '_shared.yml#/components/schemas/OASysRiskToSelf' + 401: + $ref: '_shared.yml#/components/responses/401Response' + 403: + $ref: '_shared.yml#/components/responses/403Response' + 404: + description: invalid CRN + content: + 'application/json': + schema: + $ref: '_shared.yml#/components/schemas/Problem' + 500: + $ref: '_shared.yml#/components/responses/500Response' + /people/{crn}/oasys/rosh: + get: + tags: + - People operations + summary: Returns the Risk of Serious Harm section of an OASys. + parameters: + - name: crn + in: path + description: CRN of the Person to fetch latest OASys + required: true + schema: + type: string + responses: + 200: + description: successful operation + content: + 'application/json': + schema: + $ref: '_shared.yml#/components/schemas/OASysRiskOfSeriousHarm' + 401: + $ref: '_shared.yml#/components/responses/401Response' + 403: + $ref: '_shared.yml#/components/responses/403Response' + 404: + description: invalid CRN + content: + 'application/json': + schema: + $ref: '_shared.yml#/components/schemas/Problem' + 500: + $ref: '_shared.yml#/components/responses/500Response' + /people/{crn}/risks: + get: + tags: + - People operations + summary: Returns the risks for a Person + parameters: + - name: crn + in: path + description: CRN of the Person to fetch risks for + required: true + schema: + type: string + responses: + 200: + description: successful operation + content: + 'application/json': + schema: + $ref: '_shared.yml#/components/schemas/PersonRisks' + 400: + description: invalid params + content: + 'application/problem+json': + schema: + $ref: '_shared.yml#/components/schemas/ValidationError' + 401: + $ref: '_shared.yml#/components/responses/401Response' + 403: + $ref: '_shared.yml#/components/responses/403Response' + 404: + description: invalid CRN + content: + 'application/json': + schema: + $ref: '_shared.yml#/components/schemas/Problem' + 500: + $ref: '_shared.yml#/components/responses/500Response' + /reference-data/application-status: + get: + tags: + - Reference Data + summary: Lists all application status update choices + responses: + 200: + description: successful operation + content: + 'application/json': + schema: + type: array + items: + $ref: '_shared.yml#/components/schemas/Cas2ApplicationStatus' + 401: + $ref: '_shared.yml#/components/responses/401Response' + 403: + $ref: '_shared.yml#/components/responses/403Response' + 500: + $ref: '_shared.yml#/components/responses/500Response' + /reports/{reportName}: + get: + tags: + - Reports + summary: Returns a 'report' spreadsheet of metrics + parameters: + - name: reportName + in: path + description: name of the report to download + required: true + schema: + $ref: '_shared.yml#/components/schemas/Cas2ReportName' + responses: + 200: + description: successful operation + content: + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': + schema: + type: string + format: binary diff --git a/src/main/resources/static/cas2bail-schemas.yml b/src/main/resources/static/cas2bail-schemas.yml new file mode 100644 index 0000000000..636066bccf --- /dev/null +++ b/src/main/resources/static/cas2bail-schemas.yml @@ -0,0 +1,4 @@ + +# GARETH above here + +# TOBY below here \ No newline at end of file diff --git a/src/main/resources/static/codegen/built-cas2bail-api-spec.yml b/src/main/resources/static/codegen/built-cas2bail-api-spec.yml new file mode 100644 index 0000000000..0629bbdee4 --- /dev/null +++ b/src/main/resources/static/codegen/built-cas2bail-api-spec.yml @@ -0,0 +1,5779 @@ +# DO NOT EDIT. +# This is a build artefact for use in code generation. +openapi: 3.0.1 +info: + title: 'Community Accommodation Services: Tier 2 (CAS2 Bail)' + version: 1.0.0 +servers: + - url: /cas2bail +paths: + /applications: + post: + tags: + - Operations on CAS2 Bail applications + summary: Creates a CAS2 Bail application + requestBody: + description: Information to create a blank application with + content: + 'application/json': + schema: + $ref: '#/components/schemas/NewApplication' + required: true + responses: + 201: + description: successful operation + content: + 'application/json': + schema: + $ref: '#/components/schemas/Application' + 400: + description: invalid params + content: + 'application/problem+json': + schema: + $ref: '#/components/schemas/ValidationError' + 401: + $ref: '#/components/responses/401Response' + 403: + $ref: '#/components/responses/403Response' + 404: + description: invalid CRN + content: + 'application/json': + schema: + $ref: '#/components/schemas/Problem' + 500: + $ref: '#/components/responses/500Response' + x-codegen-request-body-name: body + get: + tags: + - Operations on CAS2 Bail applications + summary: List summaries of all CAS2 Bail applications authorised for the logged in user + parameters: + - name: isSubmitted + in: query + description: Returns submitted applications if true, un submitted applications if false, and all applications if absent + schema: + type: boolean + - name: page + in: query + description: Page number of results to return. If blank, returns all results + schema: + type: integer + - name: prisonCode + in: query + description: Prison code of applications to return. If blank, returns all results. + schema: + type: string + responses: + 200: + description: successful operation + content: + 'application/json': + schema: + type: array + items: + $ref: '#/components/schemas/Cas2ApplicationSummary' + headers: + X-Pagination-CurrentPage: + $ref: '#/components/headers/X-Pagination-CurrentPage' + X-Pagination-TotalPages: + $ref: '#/components/headers/X-Pagination-TotalPages' + X-Pagination-TotalResults: + $ref: '#/components/headers/X-Pagination-TotalResults' + X-Pagination-PageSize: + $ref: '#/components/headers/X-Pagination-TotalResults' + 401: + $ref: '#/components/responses/401Response' + 403: + $ref: '#/components/responses/403Response' + 500: + $ref: '#/components/responses/500Response' + + /applications/{applicationId}: + put: + tags: + - Operations on CAS2 Bail applications + summary: Updates a CAS2 Bail application + parameters: + - name: applicationId + in: path + description: ID of the application + required: true + schema: + type: string + format: uuid + requestBody: + description: Information to update the application with + content: + 'application/json': + schema: + $ref: '#/components/schemas/UpdateApplication' + required: true + responses: + 200: + description: successful operation + content: + 'application/json': + schema: + $ref: '#/components/schemas/Application' + 400: + description: invalid params + content: + 'application/problem+json': + schema: + $ref: '#/components/schemas/ValidationError' + 401: + $ref: '#/components/responses/401Response' + 403: + $ref: '#/components/responses/403Response' + 500: + $ref: '#/components/responses/500Response' + x-codegen-request-body-name: body + get: + tags: + - Operations on CAS2 Bail applications + summary: Gets a single CAS2 Bail application by its ID + parameters: + - name: applicationId + in: path + description: ID of the application + required: true + schema: + type: string + format: uuid + responses: + 200: + description: successful operation + content: + 'application/json': + schema: + $ref: '#/components/schemas/Application' + 401: + $ref: '#/components/responses/401Response' + 403: + $ref: '#/components/responses/403Response' + 500: + $ref: '#/components/responses/500Response' + /applications/{applicationId}/abandon: + put: + tags: + - Operations on CAS2 Bail applications + summary: Abandons an in progress CAS2 Bail application + parameters: + - name: applicationId + in: path + description: ID of the application + required: true + schema: + type: string + format: uuid + responses: + 200: + description: successful operation + 401: + $ref: '#/components/responses/401Response' + 409: + description: The application has been submitted + content: + 'application/json': + schema: + $ref: '#/components/schemas/Problem' + 500: + $ref: '#/components/responses/500Response' + /assessments/{assessmentId}: + put: + tags: + - Operations on submitted CAS2 Bail applications (Assessors) + summary: Updates a single CAS2 Bail assessment by its ID + parameters: + - name: assessmentId + in: path + description: ID of the assessment + required: true + schema: + type: string + format: uuid + requestBody: + description: Information to update the assessment with + content: + 'application/json': + schema: + $ref: '#/components/schemas/UpdateCas2Assessment' + required: true + responses: + 200: + description: successful operation + content: + 'application/json': + schema: + $ref: '#/components/schemas/Cas2Assessment' + 401: + $ref: '#/components/responses/401Response' + 403: + $ref: '#/components/responses/403Response' + 404: + description: invalid assessmentId + content: + 'application/json': + schema: + $ref: '#/components/schemas/Problem' + 500: + $ref: '#/components/responses/500Response' + get: + tags: + - Operations on submitted CAS2 Bail applications (Assessors) + summary: Gets a single CAS2 Bail assessment by its ID + parameters: + - name: assessmentId + in: path + description: ID of the assessment + required: true + schema: + type: string + format: uuid + responses: + 200: + description: successful operation + content: + 'application/json': + schema: + $ref: '#/components/schemas/Cas2Assessment' + 401: + $ref: '#/components/responses/401Response' + 403: + $ref: '#/components/responses/403Response' + 404: + description: invalid assessmentId + content: + 'application/json': + schema: + $ref: '#/components/schemas/Problem' + 500: + $ref: '#/components/responses/500Response' + /assessments/{assessmentId}/status-updates: + post: + tags: + - Operations on submitted CAS2 Bail applications (Assessors) + summary: Creates a status update on an assessment + parameters: + - name: assessmentId + in: path + description: ID of the assessment whose status is to be updated + required: true + schema: + type: string + format: uuid + requestBody: + description: Information on the new status to be applied + content: + 'application/json': + schema: + $ref: '#/components/schemas/Cas2AssessmentStatusUpdate' + required: true + responses: + 200: + description: successfully created the status update + 400: + description: status update has already been submitted + content: + 'application/problem+json': + schema: + $ref: '#/components/schemas/ValidationError' + 401: + $ref: '#/components/responses/401Response' + 403: + $ref: '#/components/responses/403Response' + 500: + $ref: '#/components/responses/500Response' + /assessments/{assessmentId}/notes: + post: + tags: + - Operations on CAS2 Bail assessments + summary: Add a note to an assessment + parameters: + - name: assessmentId + in: path + description: ID of the assessment + required: true + schema: + type: string + format: uuid + requestBody: + description: the note to add + content: + 'application/json': + schema: + $ref: '#/components/schemas/NewCas2ApplicationNote' + required: true + responses: + 200: + description: successful operation + content: + 'application/json': + schema: + $ref: '#/components/schemas/Cas2ApplicationNote' + 401: + $ref: '#/components/responses/401Response' + 403: + $ref: '#/components/responses/403Response' + 404: + description: invalid assessmentId + content: + 'application/json': + schema: + $ref: '#/components/schemas/Problem' + 500: + $ref: '#/components/responses/500Response' + x-codegen-request-body-name: body + /submissions: + get: + tags: + - Operations on submitted CAS2 Bail applications (Assessors) + summary: List summaries of all submitted CAS2 Bail applications + parameters: + - name: page + in: query + description: Page number of results to return. If blank, returns all results + schema: + type: integer + responses: + 200: + description: successful operation + content: + 'application/json': + schema: + type: array + items: + $ref: '#/components/schemas/Cas2SubmittedApplicationSummary' + headers: + X-Pagination-CurrentPage: + $ref: '#/components/headers/X-Pagination-CurrentPage' + X-Pagination-TotalPages: + $ref: '#/components/headers/X-Pagination-TotalPages' + X-Pagination-TotalResults: + $ref: '#/components/headers/X-Pagination-TotalResults' + X-Pagination-PageSize: + $ref: '#/components/headers/X-Pagination-TotalResults' + 401: + $ref: '#/components/responses/401Response' + 403: + $ref: '#/components/responses/403Response' + 500: + $ref: '#/components/responses/500Response' + post: + tags: + - Operations on CAS2 Bail applications + summary: Submits a CAS2 Bail Application (creates a SubmittedApplication) + requestBody: + description: Information needed to submit an application + content: + 'application/json': + schema: + $ref: '#/components/schemas/SubmitCas2Application' + required: true + responses: + 200: + description: successfully submitted the application + 400: + description: application has already been submitted + content: + 'application/problem+json': + schema: + $ref: '#/components/schemas/ValidationError' + 401: + $ref: '#/components/responses/401Response' + 403: + $ref: '#/components/responses/403Response' + 500: + $ref: '#/components/responses/500Response' + /submissions/{applicationId}: + get: + tags: + - Operations on submitted CAS2 Bail applications (Assessors) + summary: Gets a single submitted CAS2 Bail application by its ID + parameters: + - name: applicationId + in: path + description: ID of the application + required: true + schema: + type: string + format: uuid + responses: + 200: + description: successful operation + content: + 'application/json': + schema: + $ref: '#/components/schemas/Cas2SubmittedApplication' + 401: + $ref: '#/components/responses/401Response' + 403: + $ref: '#/components/responses/403Response' + 500: + $ref: '#/components/responses/500Response' + /people/search: + get: + tags: + - People operations + summary: Searches for a Person by their Prison Number (NOMIS ID) + parameters: + - name: nomsNumber + in: query + description: Prison Number to search for + required: true + schema: + type: string + responses: + 200: + description: successful operation + content: + 'application/json': + schema: + $ref: '#/components/schemas/Person' + 400: + description: invalid params + content: + 'application/problem+json': + schema: + $ref: '#/components/schemas/ValidationError' + 404: + description: invalid CRN + content: + 'application/json': + schema: + $ref: '#/components/schemas/Problem' + 401: + $ref: '#/components/responses/401Response' + 403: + $ref: '#/components/responses/403Response' + 500: + $ref: '#/components/responses/500Response' + /people/{crn}/oasys/risk-to-self: + get: + tags: + - People operations + summary: Returns the Risk To Individual (known as Risk to Self on frontend) section of an OASys. + parameters: + - name: crn + in: path + description: CRN of the Person to fetch latest OASys + required: true + schema: + type: string + responses: + 200: + description: successful operation + content: + 'application/json': + schema: + $ref: '#/components/schemas/OASysRiskToSelf' + 401: + $ref: '#/components/responses/401Response' + 403: + $ref: '#/components/responses/403Response' + 404: + description: invalid CRN + content: + 'application/json': + schema: + $ref: '#/components/schemas/Problem' + 500: + $ref: '#/components/responses/500Response' + /people/{crn}/oasys/rosh: + get: + tags: + - People operations + summary: Returns the Risk of Serious Harm section of an OASys. + parameters: + - name: crn + in: path + description: CRN of the Person to fetch latest OASys + required: true + schema: + type: string + responses: + 200: + description: successful operation + content: + 'application/json': + schema: + $ref: '#/components/schemas/OASysRiskOfSeriousHarm' + 401: + $ref: '#/components/responses/401Response' + 403: + $ref: '#/components/responses/403Response' + 404: + description: invalid CRN + content: + 'application/json': + schema: + $ref: '#/components/schemas/Problem' + 500: + $ref: '#/components/responses/500Response' + /people/{crn}/risks: + get: + tags: + - People operations + summary: Returns the risks for a Person + parameters: + - name: crn + in: path + description: CRN of the Person to fetch risks for + required: true + schema: + type: string + responses: + 200: + description: successful operation + content: + 'application/json': + schema: + $ref: '#/components/schemas/PersonRisks' + 400: + description: invalid params + content: + 'application/problem+json': + schema: + $ref: '#/components/schemas/ValidationError' + 401: + $ref: '#/components/responses/401Response' + 403: + $ref: '#/components/responses/403Response' + 404: + description: invalid CRN + content: + 'application/json': + schema: + $ref: '#/components/schemas/Problem' + 500: + $ref: '#/components/responses/500Response' + /reference-data/application-status: + get: + tags: + - Reference Data + summary: Lists all application status update choices + responses: + 200: + description: successful operation + content: + 'application/json': + schema: + type: array + items: + $ref: '#/components/schemas/Cas2ApplicationStatus' + 401: + $ref: '#/components/responses/401Response' + 403: + $ref: '#/components/responses/403Response' + 500: + $ref: '#/components/responses/500Response' + /reports/{reportName}: + get: + tags: + - Reports + summary: Returns a 'report' spreadsheet of metrics + parameters: + - name: reportName + in: path + description: name of the report to download + required: true + schema: + $ref: '#/components/schemas/Cas2ReportName' + responses: + 200: + description: successful operation + content: + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': + schema: + type: string + format: binary +components: + responses: + 401Response: + description: not authenticated + content: + 'application/json': + schema: + $ref: '#/components/schemas/Problem' + 403Response: + description: unauthorised + content: + 'application/json': + schema: + $ref: '#/components/schemas/Problem' + 500Response: + description: unexpected error + content: + 'application/json': + schema: + $ref: '#/components/schemas/Problem' + headers: + X-Pagination-CurrentPage: + schema: + type: integer + description: The current page number + X-Pagination-TotalPages: + schema: + type: integer + description: The total number of pages + X-Pagination-TotalResults: + schema: + type: integer + description: The total number of results + X-Pagination-PageSize: + schema: + type: integer + description: The size of each page + schemas: + Premises: + type: object + properties: + service: + type: string + id: + type: string + format: uuid + name: + type: string + example: Hope House + addressLine1: + type: string + example: one something street + addressLine2: + type: string + example: Blackmore End + town: + type: string + example: Braintree + postcode: + type: string + example: LS1 3AD + bedCount: + type: integer + example: 22 + availableBedsForToday: + type: integer + example: 20 + notes: + type: string + example: some notes about this property + probationRegion: + $ref: '#/components/schemas/ProbationRegion' + apArea: + $ref: '#/components/schemas/ApArea' + localAuthorityArea: + $ref: '#/components/schemas/LocalAuthorityArea' + characteristics: + type: array + items: + $ref: '#/components/schemas/Characteristic' + status: + $ref: '#/components/schemas/PropertyStatus' + discriminator: + propertyName: service + mapping: + CAS1: '#/components/schemas/ApprovedPremises' + CAS3: '#/components/schemas/TemporaryAccommodationPremises' + required: + - service + - id + - name + - addressLine1 + - postcode + - bedCount + - availableBedsForToday + - probationRegion + - apArea + - status + PremisesSummary: + type: object + properties: + service: + type: string + id: + type: string + format: uuid + name: + type: string + example: Hope House + addressLine1: + type: string + example: one something street + addressLine2: + type: string + example: Blackmore End + postcode: + type: string + example: LS1 3AD + bedCount: + type: integer + example: 22 + status: + $ref: '#/components/schemas/PropertyStatus' + discriminator: + propertyName: service + mapping: + CAS1: '#/components/schemas/ApprovedPremisesSummary' + CAS3: '#/components/schemas/TemporaryAccommodationPremisesSummary' + required: + - service + - id + - name + - addressLine1 + - postcode + - bedCount + - status + ApprovedPremises: + allOf: + - $ref: '#/components/schemas/Premises' + - type: object + properties: + apCode: + type: string + example: NEHOPE1 + required: + - apCode + - localAuthorityArea + TemporaryAccommodationPremises: + allOf: + - $ref: '#/components/schemas/Premises' + - type: object + properties: + pdu: + type: string + probationDeliveryUnit: + $ref: '#/components/schemas/ProbationDeliveryUnit' + turnaroundWorkingDayCount: + type: integer + example: 2 + required: + - pdu + ApprovedPremisesSummary: + allOf: + - $ref: '#/components/schemas/PremisesSummary' + - type: object + properties: + apCode: + type: string + example: NEHOPE1 + probationRegion: + type: string + apArea: + type: string + required: + - apCode + - probationRegion + - apArea + TemporaryAccommodationPremisesSummary: + allOf: + - $ref: '#/components/schemas/PremisesSummary' + - type: object + properties: + pdu: + type: string + localAuthorityAreaName: + type: string + required: + - pdu + NewPremises: + type: object + properties: + name: + type: string + addressLine1: + type: string + addressLine2: + type: string + town: + type: string + postcode: + type: string + notes: + type: string + example: some notes about this property + localAuthorityAreaId: + type: string + format: uuid + probationRegionId: + type: string + format: uuid + characteristicIds: + type: array + items: + type: string + format: uuid + status: + $ref: '#/components/schemas/PropertyStatus' + pdu: + type: string + probationDeliveryUnitId: + type: string + format: uuid + turnaroundWorkingDayCount: + type: integer + required: + - name + - addressLine1 + - postcode + - probationRegionId + - characteristicIds + - status + TaskWrapper: + type: object + properties: + task: + $ref: '#/components/schemas/Task' + users: + type: array + description: Users to whom this task can be allocated + items: + $ref: '#/components/schemas/UserWithWorkload' + required: + - task + - users + Task: + type: object + properties: + taskType: + $ref: '#/components/schemas/TaskType' + id: + type: string + format: uuid + example: 6abb5fa3-e93f-4445-887b-30d081688f44 + applicationId: + type: string + format: uuid + example: 6abb5fa3-e93f-4445-887b-30d081688f44 + personSummary: + $ref: '#/components/schemas/PersonSummary' + personName: + type: string + deprecated: true + description: Superseded by personSummary which provides 'name' as well as 'personType' and 'crn'. + crn: + type: string + dueDate: + type: string + format: date + deprecated: true + description: The Due date of the task - this is deprecated in favour of the `dueAt` field + dueAt: + type: string + format: date-time + allocatedToStaffMember: + $ref: '#/components/schemas/ApprovedPremisesUser' + status: + $ref: '#/components/schemas/TaskStatus' + apArea: + $ref: '#/components/schemas/ApArea' + probationDeliveryUnit: + $ref: '#/components/schemas/ProbationDeliveryUnit' + outcomeRecordedAt: + type: string + format: date-time + discriminator: + propertyName: taskType + mapping: + Assessment: '#/components/schemas/AssessmentTask' + PlacementRequest: '#/components/schemas/PlacementRequestTask' + PlacementApplication: '#/components/schemas/PlacementApplicationTask' + BookingAppeal: '#/components/schemas/BookingAppealTask' + required: + - id + - taskType + - applicationId + - personName + - personSummary + - dueDate + - status + - crn + - dueAt + TaskStatus: + type: string + enum: + - not_started + - in_progress + - complete + - info_requested + TaskSortField: + type: string + enum: + - createdAt + - dueAt + - person + - allocatedTo + - completedAt + - taskType + - decision + AssessmentTask: + allOf: + - $ref: '#/components/schemas/Task' + - type: object + properties: + createdFromAppeal: + type: boolean + outcome: + $ref: '#/components/schemas/AssessmentDecision' + required: + - createdFromAppeal + PlacementRequestTask: + allOf: + - $ref: '#/components/schemas/Task' + - $ref: '#/components/schemas/PlacementDates' + - type: object + properties: + tier: + $ref: '#/components/schemas/RiskTierEnvelope' + releaseType: + $ref: '#/components/schemas/ReleaseTypeOption' + placementRequestStatus: + $ref: '#/components/schemas/PlacementRequestStatus' + outcome: + $ref: '#/components/schemas/PlacementRequestTaskOutcome' + required: + - tier + - releaseType + - placementRequestStatus + PlacementApplicationTask: + allOf: + - $ref: '#/components/schemas/Task' + - type: object + properties: + tier: + $ref: '#/components/schemas/RiskTierEnvelope' + releaseType: + $ref: '#/components/schemas/ReleaseTypeOption' + placementType: + $ref: '#/components/schemas/PlacementType' + placementDates: + type: array + items: + $ref: '#/components/schemas/PlacementDates' + outcome: + $ref: '#/components/schemas/PlacementApplicationDecision' + required: + - id + - tier + - releaseType + - placementType + PlacementRequestTaskOutcome: + type: string + enum: + - matched + - unable_to_match + BookingAppealTask: + allOf: + - $ref: '#/components/schemas/Task' + TaskType: + type: string + enum: + - Assessment + - PlacementRequest + - PlacementApplication + - BookingAppeal + LocalAuthorityArea: + type: object + properties: + id: + type: string + format: uuid + example: 6abb5fa3-e93f-4445-887b-30d081688f44 + identifier: + type: string + example: LEEDS + name: + type: string + example: Leeds City Council + required: + - id + - identifier + - name + ApArea: + type: object + properties: + id: + type: string + format: uuid + example: cd1c2d43-0b0b-4438-b0e3-d4424e61fb6a + identifier: + type: string + example: LON + name: + type: string + example: Yorkshire & The Humber + required: + - id + - identifier + - name + Cas1CruManagementArea: + type: object + properties: + id: + type: string + format: uuid + name: + type: string + required: + - id + - name + ProbationRegion: + type: object + properties: + id: + type: string + format: uuid + example: 952790c0-21d7-4fd6-a7e1-9018f08d8bb0 + name: + type: string + example: NPS North East Central Referrals + required: + - id + - name + Characteristic: + type: object + properties: + id: + type: string + format: uuid + example: 952790c0-21d7-4fd6-a7e1-9018f08d8bb0 + name: + type: string + example: Is this premises catered (rather than self-catered)? + propertyName: + type: string + example: isCatered + serviceScope: + type: string + enum: + - approved-premises + - temporary-accommodation + - "*" + modelScope: + type: string + enum: + - premises + - room + - "*" + required: + - id + - name + - serviceScope + - modelScope + BookingBody: + type: object + properties: + id: + type: string + format: uuid + person: + $ref: '#/components/schemas/Person' + arrivalDate: + type: string + format: date + originalArrivalDate: + type: string + format: date + departureDate: + type: string + format: date + originalDepartureDate: + type: string + format: date + createdAt: + type: string + format: date-time + keyWorker: + deprecated: true + description: KeyWorker is a legacy field only used by CAS1. It is not longer being captured or populated + allOf: + - $ref: '#/components/schemas/StaffMember' + serviceName: + $ref: '#/components/schemas/ServiceName' + bed: + $ref: '#/components/schemas/Bed' + required: + - id + - person + - arrivalDate + - originalArrivalDate + - departureDate + - originalDepartureDate + - createdAt + - serviceName + NewBooking: + type: object + properties: + crn: + type: string + example: A123456 + arrivalDate: + type: string + format: date + example: 2022-07-28 + departureDate: + type: string + format: date + example: 2022-09-30 + bedId: + type: string + format: uuid + serviceName: + $ref: '#/components/schemas/ServiceName' + enableTurnarounds: + type: boolean + assessmentId: + type: string + format: uuid + eventNumber: + type: string + required: + - crn + - arrivalDate + - departureDate + - serviceName + NewPlacementRequestBooking: + type: object + properties: + arrivalDate: + type: string + format: date + example: 2022-07-28 + departureDate: + type: string + format: date + example: 2022-09-30 + bedId: + type: string + format: uuid + premisesId: + type: string + format: uuid + required: + - arrivalDate + - departureDate + WithdrawPlacementRequest: + type: object + properties: + reason: + $ref: '#/components/schemas/WithdrawPlacementRequestReason' + required: + - reason + WithdrawPlacementRequestReason: + type: string + enum: + - DuplicatePlacementRequest + - AlternativeProvisionIdentified + - ChangeInCircumstances + - ChangeInReleaseDecision + - NoCapacityDueToLostBed + - NoCapacityDueToPlacementPrioritisation + - NoCapacity + - ErrorInPlacementRequest + - WithdrawnByPP + - RelatedApplicationWithdrawn + - RelatedPlacementRequestWithdrawn + - RelatedPlacementApplicationWithdrawn + PlacementApplicationType: + type: string + description: | + 'Initial' means that the request for placement was created for the arrival date included on the original application. + 'Additional' means the request for placement was created after the application had been assessed as suitable. + A given application should only have, at most, one request for placement of type 'Initial'. + enum: + - Initial + - Additional + Booking: + allOf: + - $ref: '#/components/schemas/BookingBody' + - type: object + properties: + status: + $ref: '#/components/schemas/BookingStatus' + extensions: + type: array + items: + $ref: '#/components/schemas/Extension' + arrival: + nullable: true + allOf: + - $ref: '#/components/schemas/Arrival' + departure: + description: The latest version of the departure, if it exists + nullable: true + allOf: + - $ref: '#/components/schemas/Departure' + departures: + description: The full history of the departure + type: array + items: + $ref: '#/components/schemas/Departure' + nonArrival: + nullable: true + allOf: + - $ref: '#/components/schemas/Nonarrival' + cancellation: + description: The latest version of the cancellation, if it exists + nullable: true + allOf: + - $ref: '#/components/schemas/Cancellation' + cancellations: + description: The full history of the cancellation + type: array + items: + $ref: '#/components/schemas/Cancellation' + confirmation: + nullable: true + allOf: + - $ref: '#/components/schemas/Confirmation' + turnaround: + description: The latest version of the turnaround, if it exists + nullable: true + allOf: + - $ref: '#/components/schemas/Turnaround' + turnarounds: + description: The full history of turnarounds + type: array + items: + $ref: '#/components/schemas/Turnaround' + turnaroundStartDate: + type: string + format: date + effectiveEndDate: + type: string + format: date + applicationId: + type: string + format: uuid + assessmentId: + type: string + format: uuid + premises: + $ref: '#/components/schemas/BookingPremisesSummary' + required: + - status + - extensions + - departures + - cancellations + - premises + BookingPremisesSummary: + type: object + properties: + id: + type: string + format: uuid + name: + type: string + required: + - id + - name + ExtendedPremisesSummary: + type: object + properties: + id: + type: string + format: uuid + name: + type: string + apCode: + type: string + postcode: + type: string + bedCount: + type: integer + availableBedsForToday: + type: integer + bookings: + type: array + items: + $ref: '#/components/schemas/PremisesBooking' + dateCapacities: + type: array + items: + $ref: '#/components/schemas/DateCapacity' + PremisesBooking: + type: object + properties: + id: + type: string + format: uuid + arrivalDate: + type: string + format: date + departureDate: + type: string + format: date + person: + $ref: '#/components/schemas/Person' + bed: + $ref: '#/components/schemas/Bed' + status: + $ref: '#/components/schemas/BookingStatus' + BookingSummary: + type: object + properties: + id: + type: string + format: uuid + premisesId: + type: string + format: uuid + premisesName: + type: string + arrivalDate: + type: string + format: date + departureDate: + type: string + format: date + createdAt: + type: string + format: date-time + type: + type: string + enum: + - space + - legacy + required: + - id + - premisesId + - premisesName + - arrivalDate + - departureDate + - createdAt + - type + Person: + type: object + properties: + crn: + type: string + type: + $ref: '#/components/schemas/PersonType' + discriminator: + propertyName: type + mapping: + FullPerson: '#/components/schemas/FullPerson' + RestrictedPerson: '#/components/schemas/RestrictedPerson' + UnknownPerson: '#/components/schemas/UnknownPerson' + required: + - crn + - type + UnknownPerson: + allOf: + - $ref: '#/components/schemas/Person' + RestrictedPerson: + allOf: + - $ref: '#/components/schemas/Person' + FullPerson: + allOf: + - $ref: '#/components/schemas/Person' + - type: object + properties: + name: + type: string + dateOfBirth: + type: string + format: date + nomsNumber: + type: string + pncNumber: + type: string + ethnicity: + type: string + nationality: + type: string + religionOrBelief: + type: string + sex: + type: string + genderIdentity: + type: string + status: + $ref: '#/components/schemas/PersonStatus' + prisonName: + type: string + isRestricted: + type: boolean + required: + - name + - dateOfBirth + - sex + - status + PersonSummary: + type: object + properties: + crn: + type: string + personType: + $ref: '#/components/schemas/PersonSummaryDiscriminator' + discriminator: + propertyName: personType + mapping: + FullPersonSummary: '#/components/schemas/FullPersonSummary' + RestrictedPersonSummary: '#/components/schemas/RestrictedPersonSummary' + UnknownPersonSummary: '#/components/schemas/UnknownPersonSummary' + required: + - crn + - type + - personType + PersonStatus: + type: string + enum: + - InCustody + - InCommunity + - Unknown + NewArrival: + type: object + properties: + type: + type: string + expectedDepartureDate: + type: string + format: date + notes: + type: string + keyWorkerStaffCode: + type: string + required: + - type + - expectedDepartureDate + discriminator: + propertyName: type + mapping: + CAS2: '#/components/schemas/NewCas2Arrival' + CAS3: '#/components/schemas/NewCas3Arrival' + NewCas2Arrival: + allOf: + - $ref: '#/components/schemas/NewArrival' + - type: object + properties: + arrivalDate: + type: string + format: date + required: + - arrivalDate + NewCas3Arrival: + allOf: + - $ref: '#/components/schemas/NewArrival' + - type: object + properties: + arrivalDate: + type: string + format: date + required: + - arrivalDate + Arrival: + type: object + properties: + expectedDepartureDate: + type: string + format: date + arrivalDate: + type: string + format: date + arrivalTime: + type: string + format: time + notes: + type: string + keyWorkerStaffCode: + type: string + bookingId: + type: string + format: uuid + createdAt: + type: string + format: date-time + required: + - type + - bookingId + - expectedDepartureDate + - createdAt + - arrivalDate + - arrivalTime + Nonarrival: + type: object + properties: + id: + type: string + format: uuid + bookingId: + type: string + format: uuid + date: + type: string + format: date + reason: + $ref: '#/components/schemas/NonArrivalReason' + notes: + type: string + createdAt: + type: string + format: date-time + required: + - id + - bookingId + - date + - reason + - createdAt + NewCancellation: + type: object + properties: + date: + type: string + format: date + reason: + type: string + format: uuid + notes: + type: string + otherReason: + type: string + required: + - bookingId + - date + - reason + NewWithdrawal: + type: object + properties: + reason: + $ref: '#/components/schemas/WithdrawalReason' + otherReason: + type: string + required: + - reason + Cancellation: + type: object + properties: + id: + type: string + format: uuid + bookingId: + type: string + format: uuid + date: + type: string + format: date + reason: + $ref: '#/components/schemas/CancellationReason' + notes: + type: string + createdAt: + type: string + format: date-time + premisesName: + type: string + otherReason: + type: string + required: + - bookingId + - date + - reason + - createdAt + - premisesName + Extension: + type: object + properties: + id: + type: string + format: uuid + bookingId: + type: string + format: uuid + previousDepartureDate: + type: string + format: date + newDepartureDate: + type: string + format: date + notes: + type: string + createdAt: + type: string + format: date-time + required: + - id + - bookingId + - previousDepartureDate + - newDepartureDate + - createdAt + NewExtension: + type: object + properties: + newDepartureDate: + type: string + format: date + notes: + type: string + required: + - newDepartureDate + DateChange: + type: object + properties: + id: + type: string + format: uuid + bookingId: + type: string + format: uuid + previousArrivalDate: + type: string + format: date + newArrivalDate: + type: string + format: date + previousDepartureDate: + type: string + format: date + newDepartureDate: + type: string + format: date + createdAt: + type: string + format: date-time + required: + - id + - bookingId + - previousArrivalDate + - newArrivalDate + - previousDepartureDate + - newDepartureDate + - createdAt + NewDateChange: + type: object + properties: + newArrivalDate: + type: string + format: date + newDepartureDate: + type: string + format: date + NewDeparture: + type: object + properties: + dateTime: + type: string + format: date-time + reasonId: + type: string + format: uuid + notes: + type: string + moveOnCategoryId: + type: string + format: uuid + destinationProviderId: + type: string + format: uuid + required: + - dateTime + - reasonId + - moveOnCategoryId + Departure: + type: object + properties: + id: + type: string + format: uuid + bookingId: + type: string + format: uuid + dateTime: + type: string + format: date-time + reason: + $ref: '#/components/schemas/DepartureReason' + notes: + type: string + moveOnCategory: + $ref: '#/components/schemas/MoveOnCategory' + destinationProvider: + $ref: '#/components/schemas/DestinationProvider' + createdAt: + type: string + format: date-time + required: + - id + - bookingId + - dateTime + - reason + - moveOnCategory + - createdAt + Confirmation: + type: object + properties: + id: + type: string + format: uuid + bookingId: + type: string + format: uuid + dateTime: + type: string + format: date-time + notes: + type: string + createdAt: + type: string + format: date-time + required: + - id + - bookingId + - dateTime + - createdAt + NewConfirmation: + type: object + properties: + notes: + type: string + Turnaround: + type: object + properties: + id: + type: string + format: uuid + bookingId: + type: string + format: uuid + workingDays: + type: integer + createdAt: + type: string + format: date-time + required: + - id + - bookingId + - workingDays + - createdAt + NewTurnaround: + type: object + properties: + workingDays: + type: integer + required: + - workingDays + Problem: + type: object + properties: + type: + type: string + example: https://example.net/validation-error + title: + type: string + example: Invalid request parameters + status: + type: integer + example: 400 + detail: + type: string + example: You provided invalid request parameters + instance: + type: string + example: f7493e12-546d-42c3-b838-06c12671ab5b + ValidationError: + allOf: + - $ref: '#/components/schemas/Problem' + - type: object + properties: + invalid-params: + type: array + items: + $ref: '#/components/schemas/InvalidParam' + InvalidParam: + type: object + properties: + propertyName: + type: string + example: arrivalDate + errorType: + type: string + LostBed: + type: object + properties: + id: + type: string + format: uuid + startDate: + type: string + format: date + endDate: + type: string + format: date + bedId: + type: string + format: uuid + bedName: + type: string + roomName: + type: string + reason: + $ref: '#/components/schemas/LostBedReason' + referenceNumber: + type: string + notes: + type: string + status: + $ref: '#/components/schemas/LostBedStatus' + cancellation: + nullable: true + allOf: + - $ref: '#/components/schemas/LostBedCancellation' + required: + - id + - startDate + - endDate + - bedId + - bedName + - roomName + - reason + - status + NewLostBed: + type: object + properties: + startDate: + type: string + format: date + endDate: + type: string + format: date + reason: + type: string + format: uuid + referenceNumber: + type: string + notes: + type: string + bedId: + type: string + format: uuid + required: + - startDate + - endDate + - reason + - bedId + UpdateLostBed: + type: object + properties: + startDate: + type: string + format: date + endDate: + type: string + format: date + reason: + type: string + format: uuid + referenceNumber: + type: string + notes: + type: string + required: + - startDate + - endDate + - reason + LostBedCancellation: + type: object + properties: + id: + type: string + format: uuid + createdAt: + type: string + format: date-time + notes: + type: string + required: + - id + - createdAt + NewLostBedCancellation: + type: object + properties: + notes: + type: string + LostBedStatus: + type: string + enum: + - active + - cancelled + DepartureReason: + type: object + properties: + id: + type: string + format: uuid + name: + type: string + example: Admitted to Hospital + serviceScope: + type: string + parentReasonId: + type: string + format: uuid + isActive: + type: boolean + required: + - id + - name + - serviceScope + - isActive + MoveOnCategory: + type: object + properties: + id: + type: string + format: uuid + name: + type: string + example: Housing Association - Rented + serviceScope: + type: string + isActive: + type: boolean + required: + - id + - name + - serviceScope + - isActive + DestinationProvider: + type: object + properties: + id: + type: string + format: uuid + name: + type: string + example: Ext - North East Region + isActive: + type: boolean + required: + - id + - name + - isActive + SupervisingProvider: + type: object + properties: + id: + type: string + format: uuid + name: + type: string + example: North East Region + isActive: + type: boolean + required: + - id + - name + - isActive + SupervisingTeam: + type: object + properties: + id: + type: string + format: uuid + name: + type: string + example: CP - Sheffield + isActive: + type: boolean + required: + - id + - name + - isActive + SupervisingOfficer: + type: object + properties: + id: + type: string + format: uuid + name: + type: string + example: Smith, John (PS - PO) + isActive: + type: boolean + required: + - id + - name + - isActive + StaffMember: + type: object + properties: + code: + type: string + keyWorker: + type: boolean + name: + type: string + example: Brown, James (PS - PSO) + required: + - code + - keyWorker + - name + LostBedReason: + type: object + properties: + id: + type: string + format: uuid + name: + type: string + example: Double Room with Single Occupancy - Other (Non-FM) + isActive: + type: boolean + serviceScope: + type: string + required: + - id + - name + - isActive + - serviceScope + CancellationReason: + type: object + properties: + id: + type: string + format: uuid + name: + type: string + example: Recall + isActive: + type: boolean + serviceScope: + type: string + required: + - id + - name + - isActive + - serviceScope + NonArrivalReason: + type: object + properties: + id: + type: string + format: uuid + name: + type: string + example: Recall + isActive: + type: boolean + required: + - id + - name + - isActive + DateCapacity: + type: object + properties: + date: + type: string + format: date + availableBeds: + type: integer + example: 10 + required: + - date + - availableBeds + PersonRisks: + type: object + properties: + crn: + type: string + roshRisks: + $ref: '#/components/schemas/RoshRisksEnvelope' + mappa: + $ref: '#/components/schemas/MappaEnvelope' + tier: + $ref: '#/components/schemas/RiskTierEnvelope' + flags: + $ref: '#/components/schemas/FlagsEnvelope' + required: + - crn + - roshRisks + - tier + - flags + RoshRisksEnvelope: + type: object + properties: + status: + $ref: '#/components/schemas/RiskEnvelopeStatus' + value: + $ref: '#/components/schemas/RoshRisks' + required: + - status + RoshRisks: + type: object + properties: + overallRisk: + type: string + riskToChildren: + type: string + riskToPublic: + type: string + riskToKnownAdult: + type: string + riskToStaff: + type: string + lastUpdated: + type: string + format: date + required: + - overallRisk + - riskToChildren + - riskToPublic + - riskToKnownAdult + - riskToStaff + MappaEnvelope: + type: object + properties: + status: + $ref: '#/components/schemas/RiskEnvelopeStatus' + value: + $ref: '#/components/schemas/Mappa' + required: + - status + Mappa: + type: object + properties: + level: + type: string + lastUpdated: + type: string + format: date + required: + - level + - lastUpdated + RiskTierEnvelope: + type: object + properties: + status: + $ref: '#/components/schemas/RiskEnvelopeStatus' + value: + $ref: '#/components/schemas/RiskTier' + required: + - status + RiskTier: + type: object + properties: + level: + type: string + lastUpdated: + type: string + format: date + required: + - level + - lastUpdated + FlagsEnvelope: + type: object + properties: + status: + $ref: '#/components/schemas/RiskEnvelopeStatus' + value: + type: array + items: + type: string + required: + - status + RiskEnvelopeStatus: + type: string + enum: + - retrieved + - not_found + - error + PersonAcctAlert: + type: object + properties: + alertId: + type: integer + format: int64 + comment: + type: string + dateCreated: + type: string + format: date + dateExpires: + type: string + format: date + expired: + type: boolean + active: + type: boolean + required: + - alertId + - dateCreated + - expired + - active + Application: + type: object + properties: + type: + type: string + id: + type: string + format: uuid + person: + $ref: '#/components/schemas/Person' + createdAt: + type: string + format: date-time + discriminator: + propertyName: type + mapping: + Offline: '#/components/schemas/OfflineApplication' + CAS1: '#/components/schemas/ApprovedPremisesApplication' + CAS2: '#/components/schemas/Cas2Application' + CAS3: '#/components/schemas/TemporaryAccommodationApplication' + required: + - type + - id + - person + - createdAt + OfflineApplication: + allOf: + - $ref: '#/components/schemas/Application' + ApprovedPremisesApplication: + allOf: + - $ref: '#/components/schemas/Application' + - type: object + properties: + isWomensApplication: + type: boolean + isPipeApplication: + deprecated: true + description: Use apType + type: boolean + isEmergencyApplication: + type: boolean + isEsapApplication: + deprecated: true + description: Use apType + type: boolean + apType: + $ref: '#/components/schemas/ApType' + arrivalDate: + type: string + format: date-time + risks: + $ref: '#/components/schemas/PersonRisks' + createdByUserId: + type: string + format: uuid + schemaVersion: + type: string + format: uuid + outdatedSchema: + type: boolean + data: + $ref: '#/components/schemas/AnyValue' + document: + $ref: '#/components/schemas/AnyValue' + status: + $ref: '#/components/schemas/ApprovedPremisesApplicationStatus' + assessmentId: + type: string + format: uuid + assessmentDecision: + $ref: '#/components/schemas/AssessmentDecision' + assessmentDecisionDate: + type: string + format: date + submittedAt: + type: string + format: date-time + personStatusOnSubmission: + $ref: '#/components/schemas/PersonStatus' + apArea: + $ref: '#/components/schemas/ApArea' + cruManagementArea: + $ref: '#/components/schemas/Cas1CruManagementArea' + applicantUserDetails: + $ref: '#/components/schemas/Cas1ApplicationUserDetails' + caseManagerIsNotApplicant: + description: "If true, caseManagerUserDetails will provide case manager details. Otherwise, applicantUserDetails can be used for case manager details" + type: boolean + caseManagerUserDetails: + $ref: '#/components/schemas/Cas1ApplicationUserDetails' + genderForAp: + $ref: '#/components/schemas/GenderForAp' + required: + - createdByUserId + - schemaVersion + - outdatedSchema + - status + Cas2Application: + allOf: + - $ref: '#/components/schemas/Application' + - type: object + properties: + createdBy: + $ref: '#/components/schemas/NomisUser' + schemaVersion: + type: string + format: uuid + outdatedSchema: + type: boolean + data: + $ref: '#/components/schemas/AnyValue' + document: + $ref: '#/components/schemas/AnyValue' + status: + $ref: '#/components/schemas/ApplicationStatus' + submittedAt: + type: string + format: date-time + telephoneNumber: + type: string + assessment: + $ref: '#/components/schemas/Cas2Assessment' + timelineEvents: + type: array + items: + $ref: '#/components/schemas/Cas2TimelineEvent' + required: + - createdBy + - schemaVersion + - outdatedSchema + - status + Cas2SubmittedApplication: + type: object + properties: + id: + type: string + format: uuid + person: + $ref: '#/components/schemas/Person' + createdAt: + type: string + format: date-time + submittedBy: + $ref: '#/components/schemas/NomisUser' + schemaVersion: + type: string + format: uuid + outdatedSchema: + type: boolean + document: + $ref: '#/components/schemas/AnyValue' + submittedAt: + type: string + format: date-time + telephoneNumber: + type: string + timelineEvents: + type: array + items: + $ref: '#/components/schemas/Cas2TimelineEvent' + assessment: + type: object + $ref: '#/components/schemas/Cas2Assessment' + required: + - id + - person + - createdAt + - createdBy + - schemaVersion + - outdatedSchema + - status + - timelineEvents + - assessment + TemporaryAccommodationApplication: + allOf: + - $ref: '#/components/schemas/Application' + - type: object + properties: + createdByUserId: + type: string + format: uuid + schemaVersion: + type: string + format: uuid + outdatedSchema: + type: boolean + data: + $ref: '#/components/schemas/AnyValue' + document: + $ref: '#/components/schemas/AnyValue' + status: + $ref: '#/components/schemas/ApplicationStatus' + risks: + $ref: '#/components/schemas/PersonRisks' + submittedAt: + type: string + format: date-time + arrivalDate: + type: string + format: date-time + offenceId: + type: string + required: + - createdByUserId + - schemaVersion + - outdatedSchema + - status + - offenceId + ApplicationSummary: + type: object + properties: + type: + type: string + id: + type: string + format: uuid + person: + $ref: '#/components/schemas/Person' + createdAt: + type: string + format: date-time + submittedAt: + type: string + format: date-time + discriminator: + propertyName: type + mapping: + Offline: '#/components/schemas/OfflineApplicationSummary' + CAS1: '#/components/schemas/ApprovedPremisesApplicationSummary' + CAS2: '#/components/schemas/Cas2ApplicationSummary' + CAS3: '#/components/schemas/TemporaryAccommodationApplicationSummary' + required: + - type + - id + - person + - createdAt + OfflineApplicationSummary: + allOf: + - $ref: '#/components/schemas/ApplicationSummary' + ApprovedPremisesApplicationSummary: + allOf: + - $ref: '#/components/schemas/ApplicationSummary' + - type: object + properties: + isWomensApplication: + type: boolean + isPipeApplication: + type: boolean + isEmergencyApplication: + type: boolean + isEsapApplication: + type: boolean + arrivalDate: + type: string + format: date-time + risks: + $ref: '#/components/schemas/PersonRisks' + createdByUserId: + type: string + format: uuid + status: + $ref: '#/components/schemas/ApprovedPremisesApplicationStatus' + tier: + type: string + isWithdrawn: + type: boolean + releaseType: + $ref: '#/components/schemas/ReleaseTypeOption' + hasRequestsForPlacement: + type: boolean + required: + - createdByUserId + - status + - isWithdrawn + - hasRequestsForPlacement + TemporaryAccommodationApplicationSummary: + allOf: + - $ref: '#/components/schemas/ApplicationSummary' + - type: object + properties: + createdByUserId: + type: string + format: uuid + status: + $ref: '#/components/schemas/ApplicationStatus' + risks: + $ref: '#/components/schemas/PersonRisks' + required: + - createdByUserId + - status + Cas2ApplicationSummary: + type: object + properties: + type: + type: string + id: + type: string + format: uuid + createdAt: + type: string + format: date-time + submittedAt: + type: string + format: date-time + createdByUserId: + type: string + format: uuid + createdByUserName: + type: string + status: + $ref: '#/components/schemas/ApplicationStatus' + latestStatusUpdate: + $ref: '#/components/schemas/LatestCas2StatusUpdate' + risks: + $ref: '#/components/schemas/PersonRisks' + hdcEligibilityDate: + type: string + format: date + personName: + type: string + crn: + type: string + nomsNumber: + type: string + required: + - type + - id + - createdAt + - createdByUserId + - status + - personName + - crn + - nomsNumber + NewCas2ApplicationNote: + type: object + properties: + note: + type: string + required: + - note + description: A note to add to an application + Cas2ApplicationNote: + type: object + properties: + id: + type: string + format: uuid + email: + type: string + example: 'roger@example.com' + name: + type: string + example: 'Roger Smith' + body: + type: string + createdAt: + type: string + format: date-time + required: + - username + - email + - name + - body + - createdAt + description: Notes added to an application + Cas2ApplicationStatus: + type: object + properties: + id: + type: string + format: uuid + name: + type: string + example: 'moreInfoRequested' + label: + type: string + example: 'More information requested' + description: + type: string + example: 'More information about the application has been requested from the POM (Prison Offender Manager).' + statusDetails: + type: array + items: + $ref: '#/components/schemas/Cas2ApplicationStatusDetail' + required: + - id + - name + - label + - description + - statusDetails + Cas2ApplicationStatusDetail: + type: object + properties: + id: + type: string + format: uuid + name: + type: string + example: 'changeOfCircumstances' + label: + type: string + example: 'Change of Circumstances' + required: + - id + - name + - label + Cas2AssessmentStatusUpdate: + type: object + properties: + newStatus: + type: string + example: 'moreInfoRequired' + description: 'The "name" of the new status to be applied' + newStatusDetails: + type: array + items: + type: string + example: 'changeOfCircumstances' + description: 'The "name" of the new detail belonging to the new status' + required: + - newStatus + Cas2SubmittedApplicationSummary: + type: object + properties: + id: + type: string + format: uuid + createdByUserId: + type: string + format: uuid + crn: + type: string + nomsNumber: + type: string + personName: + type: string + createdAt: + type: string + format: date-time + submittedAt: + type: string + format: date-time + required: + - createdByUserId + - status + - id + - person + - createdAt + - personName + - crn + - nomsNumber + Cas2StatusUpdate: + type: object + properties: + id: + type: string + format: uuid + name: + type: string + example: 'moreInfoRequested' + label: + type: string + example: 'More information requested' + description: + type: string + example: 'More information about the application has been requested from the POM (Prison Offender Manager).' + updatedBy: + $ref: '#/components/schemas/ExternalUser' + updatedAt: + type: string + format: date-time + statusUpdateDetails: + type: array + items: + $ref: '#/components/schemas/Cas2StatusUpdateDetail' + required: + - id + - name + - label + - description + Cas2StatusUpdateDetail: + type: object + properties: + id: + type: string + format: uuid + name: + type: string + example: 'moreInfoRequested' + label: + type: string + example: 'More information requested' + required: + - id + - name + - label + LatestCas2StatusUpdate: + type: object + properties: + statusId: + type: string + format: uuid + label: + type: string + example: 'More information requested' + required: + - statusId + - label + ApplicationStatus: + type: string + enum: + - inProgress + - submitted + - requestedFurtherInformation + - pending + - rejected + - awaitingPlacement + - placed + - inapplicable + - withdrawn + ApprovedPremisesApplicationStatus: + type: string + enum: + - started + - submitted + - rejected + - awaitingAssesment + - unallocatedAssesment + - assesmentInProgress + - awaitingPlacement + - placementAllocated + - inapplicable + - withdrawn + - requestedFurtherInformation + - pendingPlacementRequest + - expired + AnyValue: + description: Any object that conforms to the current JSON schema for an application + type: object + ApplicationTimelineNote: + type: object + properties: + id: + type: string + format: uuid + createdByUser: + $ref: '#/components/schemas/User' + note: + type: string + createdAt: + type: string + format: date-time + required: + - createdByUserId + - note + description: Notes added to an application + NewApplicationTimelineNote: + type: object + properties: + note: + type: string + required: + - note + description: A note to add to an application + NewApplication: + type: object + properties: + crn: + type: string + convictionId: + type: integer + format: int64 + example: 1502724704 + deliusEventNumber: + type: string + example: "7" + offenceId: + type: string + example: "M1502750438" + required: + - crn + UpdateApplicationType: + type: string + enum: + - CAS1 + - CAS2 + - CAS3 + x-enum-varnames: + - CAS1 + - CAS2 + - CAS3 + UpdateApplication: + type: object + properties: + type: + $ref: '#/components/schemas/UpdateApplicationType' + data: + type: object + additionalProperties: + $ref: '#/components/schemas/AnyValue' + discriminator: + propertyName: type + mapping: + CAS1: '#/components/schemas/UpdateApprovedPremisesApplication' + CAS2: '#/components/schemas/UpdateCas2Application' + CAS3: '#/components/schemas/UpdateTemporaryAccommodationApplication' + required: + - type + - data + UpdateCas2Application: + allOf: + - $ref: '#/components/schemas/UpdateApplication' + UpdateApprovedPremisesApplication: + allOf: + - $ref: '#/components/schemas/UpdateApplication' + - type: object + properties: + isInapplicable: + type: boolean + isWomensApplication: + type: boolean + isPipeApplication: + deprecated: true + description: Use apType + type: boolean + isEmergencyApplication: + deprecated: true + description: noticeType should be used to indicate if an emergency application + type: boolean + isEsapApplication: + deprecated: true + description: Use apType + type: boolean + apType: + $ref: '#/components/schemas/ApType' + targetLocation: + type: string + releaseType: + $ref: '#/components/schemas/ReleaseTypeOption' + arrivalDate: + type: string + format: date + noticeType: + $ref: '#/components/schemas/Cas1ApplicationTimelinessCategory' + UpdateTemporaryAccommodationApplication: + allOf: + - $ref: '#/components/schemas/UpdateApplication' + SubmitApplication: + type: object + properties: + type: + type: string + translatedDocument: + $ref: '#/components/schemas/AnyValue' + discriminator: + propertyName: type + mapping: + CAS1: '#/components/schemas/SubmitApprovedPremisesApplication' + CAS3: '#/components/schemas/SubmitTemporaryAccommodationApplication' + CAS2: '#/components/schemas/SubmitCas2Application' + # Ideally translatedDocument would be marked required here, but there is a bug + # in open api generator that leads to the subclass marking this as kotlin.Any? + # whilst in the superclass it has the type kotlin.Any. This leads to a compilation + # error as the override has the incorrect type. + required: + - type + SubmitApprovedPremisesApplication: + allOf: + - $ref: '#/components/schemas/SubmitApplication' + - type: object + properties: + isPipeApplication: + deprecated: true + description: Use apType + type: boolean + isWomensApplication: + type: boolean + isEmergencyApplication: + deprecated: true + description: noticeType should be used to indicate if this an emergency application + type: boolean + isEsapApplication: + deprecated: true + description: Use apType + type: boolean + apType: + $ref: '#/components/schemas/ApType' + targetLocation: + type: string + releaseType: + $ref: '#/components/schemas/ReleaseTypeOption' + sentenceType: + $ref: '#/components/schemas/SentenceTypeOption' + situation: + $ref: '#/components/schemas/SituationOption' + arrivalDate: + type: string + format: date + apAreaId: + description: If the user's ap area id is incorrect, they can optionally override it for the application + type: string + format: uuid + applicantUserDetails: + $ref: '#/components/schemas/Cas1ApplicationUserDetails' + caseManagerIsNotApplicant: + type: boolean + caseManagerUserDetails: + $ref: '#/components/schemas/Cas1ApplicationUserDetails' + noticeType: + $ref: '#/components/schemas/Cas1ApplicationTimelinessCategory' + reasonForShortNotice: + type: string + reasonForShortNoticeOther: + type: string + required: + - targetLocation + - releaseType + - sentenceType + SubmitCas2Application: + type: object + properties: + translatedDocument: + $ref: '#/components/schemas/AnyValue' + applicationId: + type: string + format: uuid + description: Id of the application being submitted + preferredAreas: + type: string + description: First and second preferences for where the accommodation should be located, pipe-separated + example: 'Leeds | Bradford' + hdcEligibilityDate: + type: string + example: '2023-03-30' + format: date + conditionalReleaseDate: + type: string + example: '2023-04-30' + format: date + telephoneNumber: + type: string + required: + - translatedDocument + - applicationId + - telephoneNumber + SubmitTemporaryAccommodationApplication: + allOf: + - $ref: '#/components/schemas/SubmitApplication' + - type: object + properties: + arrivalDate: + type: string + format: date + isRegisteredSexOffender: + type: boolean + needsAccessibleProperty: + type: boolean + hasHistoryOfArson: + type: boolean + isDutyToReferSubmitted: + type: boolean + dutyToReferSubmissionDate: + type: string + format: date + dutyToReferOutcome: + type: string + example: 'Pending' + isApplicationEligible: + type: boolean + eligibilityReason: + type: string + dutyToReferLocalAuthorityAreaName: + type: string + personReleaseDate: + type: string + format: date + example: '2024-02-21' + pdu: + type: string + probationDeliveryUnitId: + type: string + format: uuid + isHistoryOfSexualOffence: + type: boolean + isConcerningSexualBehaviour: + type: boolean + isConcerningArsonBehaviour: + type: boolean + prisonReleaseTypes: + type: array + items: + type: string + example: 'PSS' + summaryData: + $ref: '#/components/schemas/AnyValue' + required: + - arrivalDate + - summaryData + ReleaseTypeOption: + type: string + enum: + - licence + - rotl + - hdc + - pss + - in_community + - not_applicable + - extendedDeterminateLicence + - paroleDirectedLicence + - reReleasedPostRecall + SentenceTypeOption: + type: string + enum: + - standardDeterminate + - life + - ipp + - extendedDeterminate + - communityOrder + - bailPlacement + - nonStatutory + SituationOption: + type: string + enum: + - riskManagement + - residencyManagement + - bailAssessment + - bailSentence + - awaitingSentence + Cas1ApplicationTimelinessCategory: + type: string + enum: + - standard + - emergency + - shortNotice + PrisonCaseNote: + type: object + properties: + id: + type: string + sensitive: + type: boolean + createdAt: + type: string + format: date-time + occurredAt: + type: string + format: date-time + authorName: + type: string + type: + type: string + subType: + type: string + note: + type: string + required: + - id + - sensitive + - createdAt + - occurredAt + - authorName + - type + - subType + - note + Adjudication: + type: object + properties: + id: + type: integer + format: int64 + reportedAt: + type: string + format: date-time + establishment: + type: string + offenceDescription: + type: string + example: "Wounding or inflicting grievous bodily harm (inflicting bodily injury with or without weapon) (S20) - 00801" + hearingHeld: + type: boolean + finding: + type: string + required: + - id + - reportedAt + - establishment + - offenceDescription + - hearingHeld + Assessment: + type: object + properties: + service: + type: string + id: + type: string + format: uuid + schemaVersion: + type: string + format: uuid + outdatedSchema: + type: boolean + createdAt: + type: string + format: date-time + allocatedAt: + type: string + format: date-time + submittedAt: + type: string + format: date-time + decision: + $ref: '#/components/schemas/AssessmentDecision' + rejectionRationale: + type: string + data: + $ref: '#/components/schemas/AnyValue' + clarificationNotes: + type: array + items: + $ref: '#/components/schemas/ClarificationNote' + referralHistoryNotes: + type: array + items: + $ref: '#/components/schemas/ReferralHistoryNote' + discriminator: + propertyName: service + mapping: + CAS1: '#/components/schemas/ApprovedPremisesAssessment' + CAS3: '#/components/schemas/TemporaryAccommodationAssessment' + required: + - service + - id + - allocatedToUser + - schemaVersion + - outdatedSchema + - createdAt + - clarificationNotes + AssessmentStatus: + type: string + enum: + - awaiting_response + - completed + - reallocated + - in_progress + - not_started + - unallocated + - in_review + - ready_to_place + - closed + - rejected + x-enum-varnames: + - cas1AwaitingResponse + - cas1Completed + - cas1Reallocated + - cas1InProgress + - cas1NotStarted + - cas3Unallocated + - cas3InReview + - cas3ReadyToPlace + - cas3Closed + - cas3Rejected + ApprovedPremisesAssessmentStatus: + type: string + enum: + - awaiting_response + - completed + - reallocated + - in_progress + - not_started + TemporaryAccommodationAssessmentStatus: + type: string + enum: + - unallocated + - in_review + - ready_to_place + - closed + - rejected + ApprovedPremisesAssessment: + allOf: + - $ref: '#/components/schemas/Assessment' + - type: object + properties: + application: + $ref: '#/components/schemas/ApprovedPremisesApplication' + allocatedToStaffMember: + $ref: '#/components/schemas/ApprovedPremisesUser' + status: + $ref: '#/components/schemas/ApprovedPremisesAssessmentStatus' + createdFromAppeal: + type: boolean + required: + - application + - createdFromAppeal + TemporaryAccommodationAssessment: + allOf: + - $ref: '#/components/schemas/Assessment' + - type: object + properties: + application: + $ref: '#/components/schemas/TemporaryAccommodationApplication' + allocatedToStaffMember: + $ref: '#/components/schemas/TemporaryAccommodationUser' + status: + $ref: '#/components/schemas/TemporaryAccommodationAssessmentStatus' + summaryData: + $ref: '#/components/schemas/AnyValue' + releaseDate: + type: string + format: date + accommodationRequiredFromDate: + type: string + format: date + + required: + - application + - summaryData + Cas2Assessment: + type: object + properties: + id: + type: string + format: uuid + nacroReferralId: + type: string + assessorName: + type: string + statusUpdates: + type: array + items: + $ref: '#/components/schemas/Cas2StatusUpdate' + required: + - id + UpdateCas2Assessment: + type: object + properties: + nacroReferralId: + type: string + assessorName: + type: string + AssessmentSummary: + type: object + properties: + type: + type: string + id: + type: string + format: uuid + applicationId: + type: string + format: uuid + arrivalDate: + type: string + format: date-time + createdAt: + type: string + format: date-time + dateOfInfoRequest: + type: string + format: date-time + decision: + $ref: '#/components/schemas/AssessmentDecision' + risks: + $ref: '#/components/schemas/PersonRisks' + person: + $ref: '#/components/schemas/Person' + required: + - type + - id + - applicationId + - createdAt + - person + discriminator: + propertyName: type + mapping: + CAS1: '#/components/schemas/ApprovedPremisesAssessmentSummary' + CAS3: '#/components/schemas/TemporaryAccommodationAssessmentSummary' + ApprovedPremisesAssessmentSummary: + allOf: + - $ref: '#/components/schemas/AssessmentSummary' + - type: object + properties: + status: + $ref: '#/components/schemas/ApprovedPremisesAssessmentStatus' + dueAt: + type: string + format: date-time + required: + - status + - dueAt + TemporaryAccommodationAssessmentSummary: + allOf: + - $ref: '#/components/schemas/AssessmentSummary' + - type: object + properties: + status: + $ref: '#/components/schemas/TemporaryAccommodationAssessmentStatus' + probationDeliveryUnitName: + type: string + required: + - status + AssessmentSortField: + type: string + enum: + - name + - crn + - arrivalDate + - status + - createdAt + - dueAt + - probationDeliveryUnitName + x-enum-varnames: + - personName + - personCrn + - assessmentArrivalDate + - assessmentStatus + - assessmentCreatedAt + - assessmentDueAt + - applicationProbationDeliveryUnitName + AssessmentDecision: + type: string + enum: + - accepted + - rejected + UpdatePremises: + type: object + properties: + addressLine1: + type: string + addressLine2: + type: string + town: + type: string + postcode: + type: string + notes: + type: string + localAuthorityAreaId: + type: string + format: uuid + probationRegionId: + type: string + format: uuid + characteristicIds: + type: array + items: + type: string + format: uuid + status: + $ref: '#/components/schemas/PropertyStatus' + pdu: + type: string + probationDeliveryUnitId: + type: string + format: uuid + turnaroundWorkingDayCount: + type: integer + name: + type: string + required: + - addressLine1 + - postcode + - probationRegionId + - characteristicIds + - status + UpdateAssessment: + type: object + properties: + data: + type: object + additionalProperties: + $ref: '#/components/schemas/AnyValue' + releaseDate: + type: string + format: date + accommodationRequiredFromDate: + type: string + format: date + required: + - data + AssessmentAcceptance: + type: object + properties: + document: + $ref: '#/components/schemas/AnyValue' + requirements: + $ref: '#/components/schemas/PlacementRequirements' + placementDates: + $ref: '#/components/schemas/PlacementDates' + apType: + $ref: '#/components/schemas/ApType' + notes: + type: string + required: + - document + AssessmentRejection: + type: object + properties: + document: + $ref: '#/components/schemas/AnyValue' + rejectionRationale: + type: string + referralRejectionReasonId: + type: string + format: uuid + referralRejectionReasonDetail: + type: string + isWithdrawn: + type: boolean + required: + - document + - rejectionRationale + ClarificationNote: + type: object + properties: + id: + type: string + format: uuid + createdAt: + type: string + format: date-time + responseReceivedOn: + type: string + format: date + createdByStaffMemberId: + type: string + format: uuid + query: + type: string + response: + type: string + required: + - id + - createdAt + - createdByStaffMemberId + - query + UpdatedClarificationNote: + type: object + properties: + response: + type: string + responseReceivedOn: + type: string + format: date + example: 2022-07-28 + required: + - response + - responseReceivedOn + NewClarificationNote: + type: object + properties: + query: + type: string + required: + - query + ReferralHistoryNote: + type: object + properties: + id: + type: string + format: uuid + createdAt: + type: string + format: date-time + message: + type: string + messageDetails: + $ref: '#/components/schemas/ReferralHistoryNoteMessageDetails' + createdByUserName: + type: string + type: + type: string + required: + - id + - createdAt + - createdByUserName + - type + discriminator: + propertyName: type + mapping: + user: '#/components/schemas/ReferralHistoryUserNote' + system: '#/components/schemas/ReferralHistorySystemNote' + domainEvent: '#/components/schemas/ReferralHistoryDomainEventNote' + ReferralHistoryNoteMessageDetails: + type: object + properties: + rejectionReason: + type: string + rejectionReasonDetails: + type: string + isWithdrawn: + type: boolean + domainEvent: + $ref: '#/components/schemas/AnyValue' + ReferralHistoryDomainEventNote: + type: object + allOf: + - $ref: '#/components/schemas/ReferralHistoryNote' + ReferralHistoryUserNote: + allOf: + - $ref: '#/components/schemas/ReferralHistoryNote' + ReferralHistorySystemNote: + allOf: + - $ref: '#/components/schemas/ReferralHistoryNote' + - type: object + properties: + category: + type: string + enum: + - submitted + - unallocated + - in_review + - ready_to_place + - rejected + - completed + required: + - category + NewReferralHistoryUserNote: + type: object + properties: + message: + type: string + required: + - message + NewReallocation: + type: object + properties: + userId: + type: string + format: uuid + Reallocation: + type: object + properties: + user: + $ref: '#/components/schemas/ApprovedPremisesUser' + taskType: + $ref: '#/components/schemas/TaskType' + required: + - user + - taskType + ExternalUser: + type: object + properties: + id: + type: string + format: uuid + username: + type: string + example: 'CAS2_ASSESSOR_USER' + name: + type: string + example: 'Roger Smith' + email: + type: string + example: 'roger@external.example.com' + origin: + type: string + example: 'NACRO' + required: + - id + - username + - name + - email + NomisUser: + type: object + properties: + id: + type: string + format: uuid + name: + type: string + example: 'Roger Smith' + nomisUsername: + type: string + example: 'SMITHR_GEN' + email: + type: string + example: 'Roger.Smith@justice.gov.uk' + isActive: + type: boolean + example: true + required: + - id + - name + - nomisUsername + - isActive + User: + type: object + properties: + service: + type: string + id: + type: string + format: uuid + name: + type: string + deliusUsername: + type: string + email: + type: string + telephoneNumber: + type: string + isActive: + type: boolean + region: + $ref: '#/components/schemas/ProbationRegion' + probationDeliveryUnit: + $ref: '#/components/schemas/ProbationDeliveryUnit' + discriminator: + propertyName: service + mapping: + CAS1: '#/components/schemas/ApprovedPremisesUser' + CAS3: '#/components/schemas/TemporaryAccommodationUser' + required: + - service + - id + - name + - deliusUsername + - region + UserSummary: + type: object + properties: + id: + type: string + format: uuid + name: + type: string + required: + - id + - name + UserWithWorkload: + allOf: + - $ref: '#/components/schemas/User' + - type: object + properties: + numTasksPending: + type: integer + numTasksCompleted7Days: + type: integer + numTasksCompleted30Days: + type: integer + qualifications: + type: array + items: + $ref: '#/components/schemas/UserQualification' + roles: + type: array + items: + $ref: '#/components/schemas/ApprovedPremisesUserRole' + apArea: + deprecated: true + description: This is deprecated. Used cruManagementArea instead as this is used to group task management + allOf: + - $ref: '#/components/schemas/ApArea' + cruManagementArea: + allOf: + - $ref: "#/components/schemas/NamedId" + ApprovedPremisesUser: + allOf: + - $ref: '#/components/schemas/User' + - type: object + properties: + qualifications: + type: array + items: + $ref: '#/components/schemas/UserQualification' + roles: + type: array + items: + $ref: '#/components/schemas/ApprovedPremisesUserRole' + permissions: + type: array + items: + $ref: '#/components/schemas/ApprovedPremisesUserPermission' + apArea: + $ref: '#/components/schemas/ApArea' + cruManagementArea: + description: CRU Management Area to use. This will be the same as cruManagementAreaDefault unless cruManagementAreaOverride is defined + allOf: + - $ref: "#/components/schemas/NamedId" + cruManagementAreaDefault: + description: The CRU Management Area used if no override is defined. This is provided to support the user configuration page. + allOf: + - $ref: "#/components/schemas/NamedId" + cruManagementAreaOverride: + description: The CRU Management Area manually set on this user. This is provided to support the user configuration page. + allOf: + - $ref: "#/components/schemas/NamedId" + version: + type: integer + required: + - qualifications + - roles + - apArea + - cruManagementArea + - cruManagementAreaDefault + UserRolesAndQualifications: + type: object + properties: + roles: + type: array + items: + $ref: '#/components/schemas/ApprovedPremisesUserRole' + qualifications: + type: array + items: + $ref: '#/components/schemas/UserQualification' + required: + - roles + - qualifications + ProfileResponse: + type: object + properties: + deliusUsername: + type: string + loadError: + type: string + enum: + - staff_record_not_found + user: + $ref: '#/components/schemas/User' + required: + - deliusUsername + TemporaryAccommodationUser: + allOf: + - $ref: '#/components/schemas/User' + - type: object + properties: + roles: + type: array + items: + $ref: '#/components/schemas/TemporaryAccommodationUserRole' + required: + - roles + ApprovedPremisesUserRole: + type: string + enum: + - assessor + - matcher + - manager + - legacy_manager + - future_manager + - workflow_manager + - cru_member + - cru_member_find_and_book_beta + - applicant + - role_admin + - report_viewer + - excluded_from_assess_allocation + - excluded_from_match_allocation + - excluded_from_placement_application_allocation + - appeals_manager + - janitor + - user_manager + ApprovedPremisesUserPermission: + type: string + enum: + - cas1_adhoc_booking_create + - cas1_application_withdraw_others + - cas1_assess_appealed_application + - cas1_assess_application + - cas1_assess_placement_application + - cas1_assess_placement_request + - cas1_booking_create + - cas1_booking_change_dates + - cas1_booking_withdraw + - cas1_out_of_service_bed_create + - cas1_process_an_appeal + - cas1_user_list + - cas1_user_management + - cas1_view_assigned_assessments + - cas1_view_cru_dashboard + - cas1_view_manage_tasks + - cas1_view_out_of_service_beds + - cas1_request_for_placement_withdraw_others + - cas1_space_booking_create + - cas1_space_booking_list + - cas1_space_booking_record_arrival + - cas1_space_booking_record_departure + - cas1_space_booking_record_non_arrival + - cas1_space_booking_record_keyworker + - cas1_space_booking_view + - cas1_space_booking_withdraw + - cas1_premises_view_capacity + - cas1_premises_view_summary + - cas1_reports_view + TemporaryAccommodationUserRole: + type: string + enum: + - assessor + - referrer + - reporter + UserQualification: + type: string + enum: + - pipe + - lao + - emergency + - esap + - recovery_focused + - mental_health_specialist + ServiceName: + type: string + enum: + - approved-premises + - cas2 + - temporary-accommodation + x-enum-varnames: + - approvedPremises + - cas2 + - temporaryAccommodation + NewRoom: + type: object + properties: + name: + type: string + notes: + type: string + characteristicIds: + type: array + items: + type: string + format: uuid + bedEndDate: + type: string + format: date + example: 2024-03-30 + description: End date of the bed availability, open for availability if not specified. + required: + - name + - characteristicIds + UpdateRoom: + type: object + properties: + notes: + type: string + characteristicIds: + type: array + items: + type: string + format: uuid + name: + type: string + bedEndDate: + type: string + format: date + example: 2024-03-30 + description: End date of the bed availability, open for availability if not specified + required: + - characteristicIds + Room: + type: object + properties: + id: + type: string + format: uuid + name: + type: string + code: + type: string + example: NEABC-4 + notes: + type: string + beds: + type: array + items: + $ref: '#/components/schemas/Bed' + characteristics: + type: array + items: + $ref: '#/components/schemas/Characteristic' + required: + - id + - name + - characteristics + Bed: + type: object + properties: + id: + type: string + format: uuid + name: + type: string + code: + type: string + example: NEABC04 + bedEndDate: + type: string + format: date + example: 2024-03-30 + description: End date of the bed availability, open for availability if not specified + required: + - id + - name + BedSummary: + type: object + properties: + id: + type: string + format: uuid + name: + type: string + roomName: + type: string + status: + $ref: '#/components/schemas/BedStatus' + required: + - id + - name + - roomName + - status + BedDetail: + allOf: + - $ref: '#/components/schemas/BedSummary' + - type: object + properties: + characteristics: + type: array + items: + type: object + $ref: '#/components/schemas/CharacteristicPair' + required: + - characteristics + BedStatus: + type: string + enum: + - occupied + - available + - out_of_service + PropertyStatus: + type: string + enum: + - pending + - active + - archived + OASysAssessmentId: + description: The ID of assessment being used. This should always be the latest Layer 3 assessment, regardless of state. + type: integer + format: int64 + example: 138985987 + OASysSupportingInformationQuestion: + type: object + properties: + label: + type: string + sectionNumber: + type: integer + questionNumber: + type: string + linkedToHarm: + type: boolean + linkedToReOffending: + type: boolean + answer: + type: string + required: + - label + - questionNumber + OASysQuestion: + type: object + properties: + label: + type: string + questionNumber: + type: string + answer: + type: string + required: + - label + - questionNumber + ArrayOfOASysOffenceDetailsQuestions: + type: array + items: + type: object + $ref: '#/components/schemas/OASysQuestion' + example: + - label: "Offence analysis" + questionNumber: "2.1" + answer: "Mr Smith admits he went to Mr Jones's address on 23rd March 2010..." + - label: "Victim - perpetrator relationship" + questionNumber: "2.4.1" + answer: "Mr Smith told me that he did not know the victim, prior to the incident..." + ArrayOfOASysRiskContributorsQuestions: + type: array + items: + type: object + $ref: '#/components/schemas/OASysQuestion' + example: + - label: "Accommodation issues contributing to risks of offending and harm" + questionNumber: "3.9" + answer: "Mr Smith told me that he was renting a room in a shared house, prior to his current remand..." + - label: "Education, training and employability issues contributing to risks of offending and harm" + questionNumber: "4.9" + answer: "Mr Smith told me that his formal school education was regularly interrupted as he and his family travelled a lot whilst he was growing up..." + ArrayOfOASysRiskManagementQuestions: + type: array + items: + type: object + $ref: '#/components/schemas/OASysQuestion' + example: + - label: "Current situation" + questionNumber: "RM28.1" + answer: "Currently on remand at HMP Wandsworth - Management of case under MAPPA - level not yet set." + - label: "Supervision" + questionNumber: "RM30" + answer: "Probation Officer, Education training and employment Officer, Prison Offender Supervisor" + - label: "Monitoring and control" + questionNumber: "RM31" + answer: "State they will have secure accommodation for Mr Smith and partner on release, although ..." + ArrayOfOASysRiskOfSeriousHarmSummaryQuestions: + type: array + items: + type: object + $ref: '#/components/schemas/OASysQuestion' + example: + - label: "Who is at risk?" + questionNumber: "R10.1" + answer: "Males whom Mr Smith believes have wronged him, or strangers with whom he gets...." + - label: "What is the nature of the risk?" + questionNumber: "R10.2" + answer: "Violence, Threats, Physical harm" + - label: "When is the risk likely to be greatest?" + questionNumber: "R10.3" + answer: "Not imminent ? lengthy gap between convictions for violent offences..." + - label: "What circumstances are likely to increase the risk?" + questionNumber: "R10.4" + answer: "As above ? drug use, need to obtain money for drugs at all costs..." + - label: "What factors are likely to reduce the risk?" + questionNumber: "R10.5" + answer: "Mr Smith thinking about consequences of his actions and not acting impulsively..." + ArrayOfOASysRisksToTheIndividualQuestions: + type: array + items: + type: object + $ref: '#/components/schemas/OASysQuestion' + example: + - label: "Current concerns about self-harm or suicide" + questionNumber: "R8.1.1" + answers: "There have been numerous ACCTs opened since 2013 and every subsequent year he has been in custody..." + - label: "Previous concerns about self-harm or suicide" + questionNumber: "R8.1.4" + answer: "During Mr Smith's psr report, he denied having any history of self-harm, however... " + - label: "Current concerns about Coping in Custody or Hostel." + questionNumber: "R8.2.1" + answer: "Has told prison staff that he swallowed batteries on one occasion due to wanting..." + - label: "Previous concerns about Coping in Custody or Hostel." + questionNumber: "R8.2.2" + answer: "Reported in 2021 that he will keep himself to himself because he gets discomfiture..." + ArrayOfOASysRisksToOthersQuestions: + type: array + items: + type: object + $ref: '#/components/schemas/OASysQuestion' + example: + - label: "Current offence details" + questionNumber: "R6.1 FA1" + answer: "Mr Smith was released on licence from custody on 12/7/2018. Between then and 1/8/2018 Mr Smith approached..." + - label: "Current where and when" + questionNumber: "R6.1 FA2" + answer: "Between 12/7/2018 - 31/7/2018, initially victim in the street putting bins out... " + - label: "Previous offence details" + questionNumber: "R6.2 FA8" + answer: "Mr Smith is assessed to have committed a number of offences that are considered to have crossed the Threshold of serious harm..." + - label: "Previous where and when" + questionNumber: "R6.2 FA9" + answer: "Bolton and Bury Districts." + ArrayOfOASysSupportingInformationQuestions: + type: array + items: + type: object + $ref: '#/components/schemas/OASysSupportingInformationQuestion' + example: + - label: "Accommodation issues contributing to risks of offending and harm?" + questionNumber: "3.9" + sectionNumber: 3 + linkedToHarm: false + linkedToReOffending: true + answer: "Mr Smith told me that he was renting a room in a shared house, prior to his current remand. He said that this accomodation..." + - label: "Education, training and employability issues contributing to risks of offending and harm" + questionNumber: "4.9" + sectionNumber: 4 + linkedToHarm: false + linkedToReOffending: true + answer: "He said that he has since learnt to read and write during periods of custody and..." + ArrayOfOASysRiskToSelfQuestions: + type: array + items: + type: object + $ref: '#/components/schemas/OASysQuestion' + example: + - label: "Current concerns about self-harm or suicide" + questionNumber: "R8.1.1" + answer: "There have been numerous ACCTs opened since 2013 and every subsequent year he has been in custody....." + - label: "Current concerns about Coping in Custody or Hostel." + questionNumber: "R8.2.1" + answer: "Has told prison staff that he swallowed batteries on one occasion due to wanting..." + ArrayOfOASysRiskManagementPlanQuestions: + type: array + items: + type: object + $ref: '#/components/schemas/OASysQuestion' + example: + - label: "Key information about current situation" + questionNumber: "RM28.1" + answer: "Currently on remand - Management of case under MAPPA - level not yet set......" + - label: "Further considerations about current situation" + questionNumber: "RM28" + answer: "Mr Manette is currently on remand awaiting sentence" + OASysSections: + type: object + properties: + assessmentId: + $ref: '#/components/schemas/OASysAssessmentId' + assessmentState: + $ref: '#/components/schemas/OASysAssessmentState' + dateStarted: + type: string + format: date-time + dateCompleted: + type: string + format: date-time + offenceDetails: + $ref: '#/components/schemas/ArrayOfOASysOffenceDetailsQuestions' + roshSummary: + $ref: '#/components/schemas/ArrayOfOASysRiskOfSeriousHarmSummaryQuestions' + supportingInformation: + $ref: '#/components/schemas/ArrayOfOASysSupportingInformationQuestions' + riskToSelf: + $ref: '#/components/schemas/ArrayOfOASysRiskToSelfQuestions' + riskManagementPlan: + $ref: '#/components/schemas/ArrayOfOASysRiskManagementPlanQuestions' + required: + - assessmentId + - assessmentState + - dateStarted + - offenceDetails + - roshSummary + - supportingInformation + - riskToSelf + - riskManagementPlan + OASysRiskToSelf: + type: object + properties: + assessmentId: + $ref: '#/components/schemas/OASysAssessmentId' + assessmentState: + $ref: '#/components/schemas/OASysAssessmentState' + dateStarted: + type: string + format: date-time + dateCompleted: + type: string + format: date-time + riskToSelf: + $ref: '#/components/schemas/ArrayOfOASysRiskToSelfQuestions' + required: + - assessmentId + - assessmentState + - dateStarted + - riskToSelf + OASysRiskOfSeriousHarm: + type: object + properties: + assessmentId: + $ref: '#/components/schemas/OASysAssessmentId' + assessmentState: + $ref: '#/components/schemas/OASysAssessmentState' + dateStarted: + type: string + format: date-time + dateCompleted: + type: string + format: date-time + rosh: + $ref: '#/components/schemas/ArrayOfOASysRiskOfSeriousHarmSummaryQuestions' + required: + - assessmentId + - assessmentState + - dateStarted + - rosh + OASysSection: + type: object + properties: + section: + type: integer + example: 10 + name: + type: string + example: Emotional wellbeing + linkedToHarm: + type: boolean + linkedToReOffending: + type: boolean + required: + - section + - name + OASysAssessmentState: + type: string + enum: + - Completed + - Incomplete + ActiveOffence: + type: object + properties: + deliusEventNumber: + type: string + example: "7" + offenceDescription: + type: string + offenceId: + type: string + example: "M1502750438" + convictionId: + type: integer + format: int64 + example: 1502724704 + offenceDate: + type: string + format: date + required: + - deliusEventNumber + - offenceDescription + - offenceId + - convictionId + DocumentLevel: + type: string + description: The level at which a Document is associated - i.e. to the Offender or to a specific Conviction + enum: + - Offender + - Conviction + Document: + type: object + description: Meta Info about a file relating to an Offender + properties: + id: + type: string + level: + $ref: '#/components/schemas/DocumentLevel' + fileName: + type: string + createdAt: + type: string + format: date-time + typeCode: + type: string + typeDescription: + type: string + description: + type: string + required: + - id + - level + - fileName + - createdAt + - typeCode + - typeDescription + PersonalTimeline: + type: object + properties: + person: + $ref: '#/components/schemas/Person' + applications: + type: array + items: + $ref: '#/components/schemas/ApplicationTimeline' + required: + - person + - applications + ApplicationTimeline: + type: object + properties: + id: + type: string + format: uuid + createdAt: + type: string + format: date-time + isOfflineApplication: + type: boolean + status: + $ref: '#/components/schemas/ApprovedPremisesApplicationStatus' + createdBy: + $ref: '#/components/schemas/User' + timelineEvents: + type: array + items: + $ref: '#/components/schemas/TimelineEvent' + required: + - id + - createdAt + - isOfflineApplication + - timelineEvents + TimelineEvent: + type: object + properties: + type: + $ref: '#/components/schemas/TimelineEventType' + id: + type: string + occurredAt: + type: string + format: date-time + content: + type: string + createdBy: + $ref: '#/components/schemas/User' + associatedUrls: + type: array + items: + $ref: '#/components/schemas/TimelineEventAssociatedUrl' + triggerSource: + type: string + $ref: '#/components/schemas/TriggerSourceType' + TimelineEventAssociatedUrl: + type: object + properties: + type: + $ref: '#/components/schemas/TimelineEventUrlType' + url: + type: string + required: + - type + - url + TimelineEventType: + type: string + enum: + - approved_premises_application_submitted + - approved_premises_application_assessed + - approved_premises_booking_made + - approved_premises_person_arrived + - approved_premises_person_not_arrived + - approved_premises_person_departed + - approved_premises_booking_not_made + - approved_premises_booking_cancelled + - approved_premises_booking_changed + - approved_premises_booking_keyworker_assigned + - approved_premises_application_withdrawn + - approved_premises_application_expired + - approved_premises_information_request + - approved_premises_assessment_appealed + - approved_premises_assessment_allocated + - approved_premises_placement_application_withdrawn + - approved_premises_placement_application_allocated + - approved_premises_match_request_withdrawn + - approved_premises_request_for_placement_created + - approved_premises_request_for_placement_assessed + - cas3_person_arrived + - cas3_person_departed + - application_timeline_note + - cas2_application_submitted + - cas2_note + - cas2_status_update + TriggerSourceType: + type: string + enum: + - user + - system + TimelineEventUrlType: + type: string + enum: + - application + - booking + - assessment + - assessmentAppeal + - cas1SpaceBooking + Cas2TimelineEvent: + type: object + properties: + type: + $ref: '#/components/schemas/TimelineEventType' + occurredAt: + type: string + format: date-time + label: + type: string + body: + type: string + createdByName: + type: string + required: + - type + - occurredAt + - label + - createdByName + SeedRequest: + type: object + properties: + seedType: + $ref: '#/components/schemas/SeedFileType' + fileName: + type: string + required: + - seedType + - fileName + SeedFileType: + type: string + enum: + - approved_premises + - approved_premises_rooms + - temporary_accommodation_premises + - temporary_accommodation_bedspace + - user + - nomis_users + - external_users + - cas2_applications + - temporary_accommodation_users + - approved_premises_users + - characteristics + - update_noms_number + - update_users_from_api + - approved_premises_ap_staff_users + - approved_premises_cancel_bookings + - approved_premises_assessment_more_info_bug_fix + - approved_premises_redact_assessment_details + - approved_premises_booking_to_space_booking + - approved_premises_withdraw_placement_request + - approved_premises_replay_domain_events + - approved_premises_duplicate_application + - approved_premises_update_event_number + - approved_premises_link_booking_to_placement_request + - approved_premises_out_of_service_beds + - approved_premises_cru_management_areas + - approved_premises_space_planning_dry_run + - approved_premises_import_delius_booking_management_data + MigrationJobRequest: + type: object + properties: + jobType: + $ref: '#/components/schemas/MigrationJobType' + required: + - jobType + MigrationJobType: + type: string + enum: + - update_all_users_from_community_api + - update_sentence_type_and_situation + - update_booking_status + - update_task_due_dates + - update_users_pdu_by_api + - update_cas2_applications_with_assessments + - update_cas2_status_updates_with_assessments + - update_cas2_notes_with_assessments + - update_cas1_fix_placement_app_links + - update_cas1_notice_types + - update_cas1_backfill_user_ap_area + - update_cas3_application_offender_name + - update_cas3_domain_event_type_for_person_departed_updated + PlacementDates: + type: object + properties: + expectedArrival: + type: string + format: date + duration: + type: integer + required: + - expectedArrival + - duration + PlacementRequirements: + type: object + properties: + gender: + $ref: '#/components/schemas/Gender' + type: + $ref: '#/components/schemas/ApType' + location: + type: string + example: B74 + radius: + type: integer + essentialCriteria: + type: array + items: + $ref: '#/components/schemas/PlacementCriteria' + desirableCriteria: + type: array + items: + $ref: '#/components/schemas/PlacementCriteria' + required: + - gender + - type + - location + - radius + - essentialCriteria + - desirableCriteria + PlacementApplication: + allOf: + - $ref: '#/components/schemas/NewPlacementApplication' + - type: object + properties: + id: + type: string + description: If type is 'Additional', provides the PlacementApplication ID. If type is 'Initial' this field provides a PlacementRequest ID. + format: uuid + createdByUserId: + type: string + format: uuid + schemaVersion: + type: string + format: uuid + outdatedSchema: + type: boolean + createdAt: + type: string + format: date-time + submittedAt: + type: string + format: date-time + assessmentId: + type: string + description: If type is 'Additional', provides the PlacementApplication ID. If type is 'Initial' this field shouldn't be used. + format: uuid + assessmentCompletedAt: + type: string + format: date-time + applicationCompletedAt: + type: string + format: date-time + data: + $ref: '#/components/schemas/AnyValue' + document: + $ref: '#/components/schemas/AnyValue' + canBeWithdrawn: + type: boolean + isWithdrawn: + type: boolean + withdrawalReason: + $ref: '#/components/schemas/WithdrawPlacementRequestReason' + type: + $ref: '#/components/schemas/PlacementApplicationType' + placementDates: + type: array + items: + $ref: '#/components/schemas/PlacementDates' + required: + - id + - createdByUserId + - schemaVersion + - outdatedScheme + - createdAt + - assessmentId + - assessmentCompletedAt + - applicationCompletedAt + - canBeWithdrawn + - isWithdrawn + - type + - placementDates + NewPlacementApplication: + type: object + properties: + applicationId: + type: string + format: uuid + required: + - applicationId + UpdatePlacementApplication: + type: object + properties: + data: + type: object + additionalProperties: + $ref: '#/components/schemas/AnyValue' + required: + - data + SubmitPlacementApplication: + type: object + properties: + translatedDocument: + $ref: '#/components/schemas/AnyValue' + placementType: + $ref: '#/components/schemas/PlacementType' + placementDates: + type: array + items: + $ref: '#/components/schemas/PlacementDates' + required: + - translatedDocument + - placementType + - placementDates + PlacementApplicationDecisionEnvelope: + type: object + properties: + decision: + $ref: '#/components/schemas/PlacementApplicationDecision' + summaryOfChanges: + type: string + decisionSummary: + type: string + required: + - decision + - summaryOfChanges + - decisionSummary + PlacementApplicationDecision: + type: string + enum: + - accepted + - rejected + - withdraw + - withdrawn_by_pp + PlacementType: + type: string + enum: + - rotl + - release_following_decision + - additional_placement + WithdrawPlacementApplication: + type: object + properties: + reason: + $ref: '#/components/schemas/WithdrawPlacementRequestReason' + required: + - reason + RequestForPlacement: + type: object + properties: + id: + type: string + description: | + If `type` is `"manual"`, provides the `PlacementApplication` ID. + If `type` is `"automatic"` this field provides a `PlacementRequest` ID. + format: uuid + createdByUserId: + type: string + format: uuid + createdAt: + type: string + format: date-time + submittedAt: + type: string + format: date-time + requestReviewedAt: + type: string + description: | + If `type` is `"manual"`, provides the value of `PlacementApplication.decisionMadeAt`. + If `type` is `"automatic"` this field provides the value of `PlacementRequest.assessmentCompletedAt`. + format: date-time + document: + $ref: '#/components/schemas/AnyValue' + canBeDirectlyWithdrawn: + description: | + If true, the user making this request can withdraw this request for placement. + If false, it may still be possible to indirectly withdraw this request for placement by withdrawing the application. + type: boolean + isWithdrawn: + type: boolean + withdrawalReason: + $ref: '#/components/schemas/WithdrawPlacementRequestReason' + type: + $ref: '#/components/schemas/RequestForPlacementType' + placementDates: + type: array + items: + $ref: '#/components/schemas/PlacementDates' + status: + $ref: '#/components/schemas/RequestForPlacementStatus' + required: + - id + - createdByUserId + - createdAt + - canBeDirectlyWithdrawn + - isWithdrawn + - type + - placementDates + - status + RequestForPlacementType: + type: string + enum: + - manual + - automatic + RequestForPlacementStatus: + type: string + enum: + - request_unsubmitted + - request_rejected + - request_submitted + - awaiting_match + - request_withdrawn + - placement_booked + - person_arrived + - person_not_arrived + - person_departed + PlacementRequest: + allOf: + - $ref: '#/components/schemas/PlacementRequirements' + - $ref: '#/components/schemas/PlacementDates' + - type: object + properties: + id: + type: string + format: uuid + person: + $ref: '#/components/schemas/Person' + risks: + $ref: '#/components/schemas/PersonRisks' + applicationId: + type: string + format: uuid + assessmentId: + type: string + format: uuid + releaseType: + $ref: '#/components/schemas/ReleaseTypeOption' + status: + $ref: '#/components/schemas/PlacementRequestStatus' + assessmentDecision: + $ref: '#/components/schemas/AssessmentDecision' + assessmentDate: + type: string + format: date-time + applicationDate: + type: string + format: date-time + assessor: + $ref: '#/components/schemas/ApprovedPremisesUser' + isParole: + type: boolean + notes: + type: string + booking: + $ref: '#/components/schemas/BookingSummary' + requestType: + $ref: '#/components/schemas/PlacementRequestRequestType' + isWithdrawn: + type: boolean + withdrawalReason: + $ref: '#/components/schemas/WithdrawPlacementRequestReason' + required: + - person + - risks + - id + - applicationId + - assessmentId + - releaseType + - status + - assessmentDecision + - assessmentDate + - applicationDate + - assessor + - isParole + - isWithdrawn + PlacementRequestRequestType: + type: string + enum: + - parole + - standardRelease + PlacementRequestDetail: + allOf: + - $ref: '#/components/schemas/PlacementRequest' + - type: object + properties: + cancellations: + deprecated: true + description: Not used by UI. Space Booking cancellations to be provided if cancellations are required in future. + type: array + items: + $ref: '#/components/schemas/Cancellation' + application: + $ref: '#/components/schemas/Application' + required: + - cancellations + - application + PlacementRequestStatus: + type: string + enum: + - notMatched + - unableToMatch + - matched + PlacementCriteria: + type: string + enum: + - isPIPE + - isESAP + - isMHAPStJosephs + - isMHAPElliottHouse + - isSemiSpecialistMentalHealth + - isRecoveryFocussed + - hasBrailleSignage + - hasTactileFlooring + - hasHearingLoop + - isStepFreeDesignated + - isArsonDesignated + - isWheelchairDesignated + - isSingle + - isCatered + - isSuitedForSexOffenders + - isSuitableForVulnerable + - acceptsSexOffenders + - acceptsHateCrimeOffenders + - acceptsChildSexOffenders + - acceptsNonSexualChildOffenders + - isArsonSuitable + - isGroundFloor + - hasEnSuite + Gender: + type: string + enum: + - male + - female + ApType: + type: string + enum: + - normal + - pipe + - esap + - rfap + - mhapStJosephs + - mhapElliottHouse + BedSearchParameters: + type: object + properties: + serviceName: + type: string + startDate: + type: string + format: date + description: The date the Bed will need to be free from + durationDays: + type: integer + description: The number of days the Bed will need to be free from the start_date until + required: + - serviceName + - startDate + - durationDays + discriminator: + propertyName: serviceName + mapping: + approved-premises: '#/components/schemas/ApprovedPremisesBedSearchParameters' + temporary-accommodation: '#/components/schemas/TemporaryAccommodationBedSearchParameters' + ApprovedPremisesBedSearchParameters: + allOf: + - $ref: '#/components/schemas/BedSearchParameters' + - type: object + properties: + postcodeDistrict: + type: string + description: The postcode district to search outwards from + maxDistanceMiles: + type: integer + description: Maximum number of miles from the postcode district to search, only required if more than 50 miles which is the default + requiredCharacteristics: + type: array + items: + $ref: '#/components/schemas/PlacementCriteria' + required: + - postcodeDistrict + - maxDistanceMiles + - requiredCharacteristics + TemporaryAccommodationBedSearchParameters: + allOf: + - $ref: '#/components/schemas/BedSearchParameters' + - type: object + properties: + probationDeliveryUnits: + type: array + description: The list of pdus Ids to search within + items: + type: string + format: uuid + attributes: + type: array + description: Bedspace and property attributes to filter on + items: + $ref: "#/components/schemas/BedSearchAttributes" + required: + - probationDeliveryUnits + BedSearchResults: + type: object + properties: + resultsRoomCount: + type: integer + description: How many distinct Rooms the Beds in the results belong to + resultsPremisesCount: + type: integer + description: How many distinct Premises the Beds in the results belong to + resultsBedCount: + type: integer + description: How many Beds are in the results + results: + type: array + items: + $ref: '#/components/schemas/BedSearchResult' + required: + - resultsRoomCount + - resultsPremisesCount + - resultsBedCount + - results + BedSearchResult: + type: object + properties: + serviceName: + $ref: '#/components/schemas/ServiceName' + premises: + $ref: '#/components/schemas/BedSearchResultPremisesSummary' + room: + $ref: '#/components/schemas/BedSearchResultRoomSummary' + bed: + $ref: '#/components/schemas/BedSearchResultBedSummary' + required: + - serviceName + - premises + - room + - bed + discriminator: + propertyName: serviceName + mapping: + approved-premises: '#/components/schemas/ApprovedPremisesBedSearchResult' + temporary-accommodation: '#/components/schemas/TemporaryAccommodationBedSearchResult' + ApprovedPremisesBedSearchResult: + allOf: + - $ref: '#/components/schemas/BedSearchResult' + - type: object + properties: + distanceMiles: + type: number + description: how many miles away from the postcode district the Premises this Bed belongs to is + required: + - distanceMiles + TemporaryAccommodationBedSearchResult: + allOf: + - $ref: '#/components/schemas/BedSearchResult' + - type: object + properties: + overlaps: + type: array + items: + $ref: '#/components/schemas/TemporaryAccommodationBedSearchResultOverlap' + required: + - overlaps + TemporaryAccommodationBedSearchResultOverlap: + type: object + properties: + name: + type: string + crn: + type: string + sex: + type: string + personType: + $ref: '#/components/schemas/PersonType' + days: + type: integer + bookingId: + type: string + format: uuid + roomId: + type: string + format: uuid + assessmentId: + type: string + format: uuid + required: + - name + - crn + - personType + - days + - bookingId + - roomId + BedSearchResultPremisesSummary: + type: object + properties: + id: + type: string + format: uuid + name: + type: string + addressLine1: + type: string + addressLine2: + type: string + town: + type: string + postcode: + type: string + probationDeliveryUnitName: + type: string + notes: + type: string + characteristics: + type: array + items: + $ref: '#/components/schemas/CharacteristicPair' + bedCount: + type: integer + description: the total number of Beds in the Premises + bookedBedCount: + type: integer + description: the total number of booked Beds in the Premises + required: + - id + - name + - addressLine1 + - postcode + - characteristics + - bedCount + BedSearchResultRoomSummary: + type: object + properties: + id: + type: string + format: uuid + name: + type: string + characteristics: + type: array + items: + $ref: '#/components/schemas/CharacteristicPair' + required: + - id + - name + - characteristics + BedSearchResultBedSummary: + type: object + properties: + id: + type: string + format: uuid + name: + type: string + required: + - id + - name + CharacteristicPair: + type: object + properties: + propertyName: + type: string + name: + type: string + required: + - name + SortOrder: + type: string + enum: + - ascending + - descending + BookingStatus: + type: string + enum: + - arrived + - awaiting-arrival + - not-arrived + - departed + - cancelled + - provisional + - confirmed + - closed + BookingSearchSortField: + type: string + enum: + - name + - crn + - startDate + - endDate + - createdAt + x-enum-varnames: + - personName + - personCrn + - bookingStartDate + - bookingEndDate + - bookingCreatedAt + BookingSearchResults: + type: object + properties: + resultsCount: + type: integer + results: + type: array + items: + $ref: "#/components/schemas/BookingSearchResult" + required: + - resultsCount + - results + BookingSearchResult: + type: object + properties: + person: + $ref: "#/components/schemas/BookingSearchResultPersonSummary" + booking: + $ref: "#/components/schemas/BookingSearchResultBookingSummary" + premises: + $ref: "#/components/schemas/BookingSearchResultPremisesSummary" + room: + $ref: "#/components/schemas/BookingSearchResultRoomSummary" + bed: + $ref: "#/components/schemas/BookingSearchResultBedSummary" + required: + - person + - booking + - premises + - room + - bed + BookingSearchResultPersonSummary: + type: object + properties: + name: + type: string + crn: + type: string + required: + - crn + BookingSearchResultBookingSummary: + type: object + properties: + id: + type: string + format: uuid + status: + $ref: "#/components/schemas/BookingStatus" + startDate: + type: string + format: date + endDate: + type: string + format: date + createdAt: + type: string + format: date-time + required: + - id + - status + - startDate + - endDate + - createdAt + BookingSearchResultPremisesSummary: + type: object + properties: + id: + type: string + format: uuid + name: + type: string + addressLine1: + type: string + addressLine2: + type: string + town: + type: string + postcode: + type: string + required: + - id + - name + - addressLine1 + - postcode + BookingSearchResultRoomSummary: + type: object + properties: + id: + type: string + format: uuid + name: + type: string + required: + - id + - name + BookingSearchResultBedSummary: + type: object + properties: + id: + type: string + format: uuid + name: + type: string + required: + - id + - name + NewPlacementRequestBookingConfirmation: + type: object + properties: + premisesName: + type: string + arrivalDate: + type: string + format: date + example: 2022-07-28 + departureDate: + type: string + format: date + example: 2022-09-30 + required: + - premisesName + - arrivalDate + - departureDate + NewBedMove: + type: object + properties: + bedId: + type: string + format: uuid + notes: + type: string + required: + - bedId + NewBookingNotMade: + type: object + properties: + notes: + type: string + BookingNotMade: + type: object + properties: + id: + type: string + format: uuid + placementRequestId: + type: string + format: uuid + createdAt: + type: string + format: date-time + notes: + type: string + required: + - id + - placementRequestId + - createdAt + ProbationDeliveryUnit: + type: object + properties: + id: + type: string + format: uuid + name: + type: string + required: + - id + - name + ReferralRejectionReason: + type: object + properties: + id: + type: string + format: uuid + name: + type: string + example: There was not enough time to place them + serviceScope: + type: string + isActive: + type: boolean + required: + - id + - name + - serviceScope + - isActive + CacheType: + type: string + enum: + - qCodeStaffMembers + - userAccess + - staffDetails + - teamsManagingCase + - ukBankHolidays + - inmateDetails + BedOccupancyRange: + type: object + properties: + bedId: + type: string + format: uuid + bedName: + type: string + schedule: + type: array + items: + $ref: '#/components/schemas/BedOccupancyEntry' + required: + - bedId + - bedName + - schedule + BedOccupancyEntryType: + type: string + enum: + - booking + - lost_bed + - open + BedOccupancyEntry: + type: object + properties: + type: + $ref: '#/components/schemas/BedOccupancyEntryType' + length: + type: integer + startDate: + type: string + format: date + endDate: + type: string + format: date + required: + - type + - length + - startDate + - endDate + discriminator: + propertyName: type + mapping: + booking: '#/components/schemas/BedOccupancyBookingEntry' + lost_bed: '#/components/schemas/BedOccupancyLostBedEntry' + open: '#/components/schemas/BedOccupancyOpenEntry' + BedOccupancyBookingEntry: + allOf: + - $ref: '#/components/schemas/BedOccupancyEntry' + - type: object + properties: + bookingId: + type: string + format: uuid + personName: + type: string + required: + - bookingId + - personName + BedOccupancyLostBedEntry: + allOf: + - $ref: '#/components/schemas/BedOccupancyEntry' + - type: object + properties: + lostBedId: + type: string + format: uuid + required: + - lostBedId + BedOccupancyOpenEntry: + allOf: + - $ref: '#/components/schemas/BedOccupancyEntry' + PersonType: + type: string + enum: + - FullPerson + - RestrictedPerson + - UnknownPerson + FullPersonSummary: + allOf: + - $ref: '#/components/schemas/PersonSummary' + - type : object + properties: + name: + type: string + required: + - name + RestrictedPersonSummary: + allOf: + - $ref: '#/components/schemas/PersonSummary' + UnknownPersonSummary: + allOf: + - $ref: '#/components/schemas/PersonSummary' + PersonSummaryDiscriminator: + type: string + enum: + - FullPersonSummary + - RestrictedPersonSummary + - UnknownPersonSummary + Withdrawables: + type: object + properties: + notes: + type: array + items: + type: string + withdrawables: + type: array + items: + $ref: '#/components/schemas/Withdrawable' + required: + - notes + - withdrawables + Withdrawable: + type: object + properties: + id: + type: string + format: uuid + type: + $ref: '#/components/schemas/WithdrawableType' + dates: + type: array + items: + $ref: '#/components/schemas/DatePeriod' + description: 0, 1 or more dates can be specified depending upon the WithdrawableType + required: + - id + - type + - dates + DatePeriod: + type: object + properties: + startDate: + type: string + format: date + endDate: + type: string + format: date + required: + - startDate + - endDate + WithdrawableType: + type: string + enum: + - application + - booking + - placement_application + - placement_request + - space_booking + WithdrawalReason: + type: string + enum: + - change_in_circumstances_new_application_to_be_submitted + - error_in_application + - duplicate_application + - death + - other_accommodation_identified + - other + PlacementRequestSortField: + type: string + enum: + - duration + - expected_arrival + - created_at + - application_date + - request_type + - person_name + - person_risks_tier + x-enum-varnames: + - duration + - expectedArrival + - createdAt + - applicationSubmittedAt + UserSortField: + type: string + enum: + - name + x-enum-varnames: + - personName + SortDirection: + type: string + enum: + - asc + - desc + AllocatedFilter: + type: string + enum: + - allocated + - unallocated + ApplicationSortField: + type: string + enum: + - tier + - createdAt + - arrivalDate + - releaseType + RiskTierLevel: + type: string + enum: + - D0 + - D1 + - D2 + - D3 + - C0 + - C1 + - C2 + - C3 + - B0 + - B1 + - B2 + - B3 + - A0 + - A1 + - A2 + - A3 + NewAppeal: + type: object + properties: + appealDate: + type: string + format: date + appealDetail: + type: string + decision: + $ref: '#/components/schemas/AppealDecision' + decisionDetail: + type: string + required: + - appealDate + - appealDetail + - decision + - decisionDetail + Appeal: + type: object + properties: + id: + type: string + format: uuid + appealDate: + type: string + format: date + appealDetail: + type: string + decision: + $ref: '#/components/schemas/AppealDecision' + decisionDetail: + type: string + createdAt: + type: string + format: date-time + applicationId: + type: string + format: uuid + assessmentId: + type: string + format: uuid + createdByUser: + $ref: '#/components/schemas/User' + required: + - id + - appealDate + - appealDetail + - decision + - decisionDetail + - createdAt + - applicationId + - createdByUser + AppealDecision: + type: string + enum: + - accepted + - rejected + Cas1ApplicationUserDetails: + type: object + properties: + name: + type: string + email: + type: string + telephoneNumber: + type: string + required: + - name + Cas3ReportType: + type: string + enum: + - referral + - booking + - bedUsage + - bedOccupancy + - futureBookings + - futureBookingsCsv + - bookingGap + Cas2ReportName: + type: string + enum: + - submitted-applications + - application-status-updates + - unsubmitted-applications + Cas1OutOfServiceBed: + type: object + properties: + id: + type: string + format: uuid + createdAt: + type: string + format: date-time + startDate: + type: string + format: date + endDate: + type: string + format: date + bed: + $ref: '#/components/schemas/NamedId' + room: + $ref: '#/components/schemas/NamedId' + premises: + $ref: '#/components/schemas/NamedId' + apArea: + $ref: '#/components/schemas/NamedId' + reason: + $ref: '#/components/schemas/Cas1OutOfServiceBedReason' + referenceNumber: + type: string + notes: + type: string + daysLostCount: + type: integer + temporality: + $ref: '#/components/schemas/Temporality' + status: + $ref: '#/components/schemas/Cas1OutOfServiceBedStatus' + cancellation: + nullable: true + allOf: + - $ref: '#/components/schemas/Cas1OutOfServiceBedCancellation' + revisionHistory: + type: array + items: + $ref: '#/components/schemas/Cas1OutOfServiceBedRevision' + required: + - id + - createdAt + - startDate + - endDate + - bed + - room + - premises + - apArea + - reason + - daysLostCount + - temporality + - status + - revisionHistory + NewCas1OutOfServiceBed: + type: object + properties: + startDate: + type: string + format: date + endDate: + type: string + format: date + reason: + type: string + format: uuid + referenceNumber: + type: string + notes: + type: string + bedId: + type: string + format: uuid + required: + - startDate + - endDate + - reason + - bedId + UpdateCas1OutOfServiceBed: + type: object + properties: + startDate: + type: string + format: date + endDate: + type: string + format: date + reason: + type: string + format: uuid + referenceNumber: + type: string + notes: + type: string + required: + - startDate + - endDate + - reason + Cas1OutOfServiceBedCancellation: + type: object + properties: + id: + type: string + format: uuid + createdAt: + type: string + format: date-time + notes: + type: string + required: + - id + - createdAt + NewCas1OutOfServiceBedCancellation: + type: object + properties: + notes: + type: string + Cas1OutOfServiceBedStatus: + type: string + enum: + - active + - cancelled + Cas1OutOfServiceBedReason: + type: object + properties: + id: + type: string + format: uuid + name: + type: string + example: Double Room with Single Occupancy - Other (Non-FM) + isActive: + type: boolean + required: + - id + - name + - isActive + Cas1OutOfServiceBedSortField: + type: string + enum: + - premisesName + - roomName + - bedName + - startDate + - endDate + - reason + - daysLost + Cas1OutOfServiceBedRevision: + type: object + properties: + id: + type: string + format: uuid + updatedAt: + type: string + format: date-time + updatedBy: + $ref: '#/components/schemas/User' + revisionType: + type: array + items: + $ref: '#/components/schemas/Cas1OutOfServiceBedRevisionType' + startDate: + type: string + format: date + endDate: + type: string + format: date + reason: + $ref: '#/components/schemas/Cas1OutOfServiceBedReason' + referenceNumber: + type: string + notes: + type: string + required: + - id + - updatedAt + - revisionType + Cas1OutOfServiceBedRevisionType: + type: string + enum: + - created + - updatedStartDate + - updatedEndDate + - updatedReferenceNumber + - updatedReason + - updatedNotes + NamedId: + type: object + description: A generic stub for an object with a name and an ID. + properties: + id: + type: string + format: uuid + name: + type: string + required: + - id + - name + Temporality: + type: string + enum: + - past + - current + - future + GenderForAp: + type: string + enum: + - male + - female + BedSearchAttributes: + type: string + enum: + - sharedProperty + - singleOccupancy + - wheelchairAccessible + +# GARETH above here + +# TOBY below here \ No newline at end of file diff --git a/src/main/resources/static/codegen/built-cas3-api-spec.yml b/src/main/resources/static/codegen/built-cas3-api-spec.yml index 754f4dd0f7..a00b8f38b5 100644 --- a/src/main/resources/static/codegen/built-cas3-api-spec.yml +++ b/src/main/resources/static/codegen/built-cas3-api-spec.yml @@ -95,7 +95,8 @@ paths: 'application/problem+json': schema: $ref: '#/components/schemas/ValidationError' - + + components: responses: 401Response: From 45474a5e4a7218a0239ad25b7f7fdb6b02dbf188 Mon Sep 17 00:00:00 2001 From: garethCAS2 Date: Thu, 28 Nov 2024 14:38:06 +0000 Subject: [PATCH 02/37] first round of cas2bail application --- .../cas2bail/Cas2BailApplicationController.kt | 175 ++++++++ .../jpa/entity/JsonSchemaEntity.kt | 10 + .../cas2bail/Cas2BailApplicationEntity.kt | 114 +++++ .../cas2bail/Cas2BailApplicationNoteEntity.kt | 69 ++++ .../Cas2BailApplicationSummaryEntity.kt | 58 +++ .../cas2bail/Cas2BailAssessmentEntity.kt | 44 ++ .../service/cas2/JsonSchemaService.kt | 7 + .../cas2bail/Cas2BailApplicationService.kt | 390 ++++++++++++++++++ .../cas2bail/Cas2BailAssessmentService.kt | 54 +++ .../cas2bail/Cas2BailUserAccessService.kt | 26 ++ .../Cas2BailApplicationsTransformer.kt | 84 ++++ .../Cas2BailAssessmentsTransformer.kt | 21 + .../Cas2BailStatusUpdateTransformer.kt | 51 +++ .../Cas2BailTimelineEventsTransformer.kt | 59 +++ src/main/resources/static/_shared.yml | 43 ++ .../static/codegen/built-api-spec.yml | 43 ++ .../static/codegen/built-cas1-api-spec.yml | 43 ++ .../static/codegen/built-cas2-api-spec.yml | 43 ++ .../codegen/built-cas2bail-api-spec.yml | 43 ++ .../static/codegen/built-cas3-api-spec.yml | 43 ++ 20 files changed, 1420 insertions(+) create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailApplicationController.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationEntity.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationNoteEntity.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationSummaryEntity.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailAssessmentEntity.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationService.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailAssessmentService.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailUserAccessService.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailApplicationsTransformer.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailAssessmentsTransformer.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailStatusUpdateTransformer.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailTimelineEventsTransformer.kt diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailApplicationController.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailApplicationController.kt new file mode 100644 index 0000000000..ae01b8fd91 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailApplicationController.kt @@ -0,0 +1,175 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.controller.cas2bail + +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.cas2bail.ApplicationsCas2bailDelegate +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationSummaryEntity +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.problem.NotFoundProblem +import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.AuthorisableActionResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.ValidatableActionResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.HttpAuthService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.NomisUserService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2.OffenderService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2bail.Cas2BailApplicationService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas2bail.Cas2BailApplicationsTransformer +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.PageCriteria +import java.net.URI +import java.util.* + +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas2ApplicationSummary as ModelCas2ApplicationSummary + +@Service( + "uk.gov.justice.digital.hmpps.approvedpremisesapi.controller.cas2bail" + + ".Cas2BailApplicationsController", +) +class Cas2BailApplicationController( + private val httpAuthService: HttpAuthService, + private val applicationService: Cas2BailApplicationService, + private val applicationsTransformer: Cas2BailApplicationsTransformer, + private val objectMapper: ObjectMapper, + private val offenderService: OffenderService, + private val userService: NomisUserService, +) : ApplicationsCas2bailDelegate { + + override fun applicationsGet( + isSubmitted: Boolean?, + page: Int?, + prisonCode: String?, + ): ResponseEntity> { + val user = userService.getUserForRequest() + + prisonCode?.let { if (prisonCode != user.activeCaseloadId) throw ForbiddenProblem() } + + val pageCriteria = PageCriteria("createdAt", SortDirection.desc, page) + + val (applications, metadata) = applicationService.getCas2BailApplications(prisonCode, isSubmitted, user, pageCriteria) + + return ResponseEntity.ok().headers( + metadata?.toHeaders(), + ).body(getPersonNamesAndTransformToSummaries(applications)) + } + + override fun applicationsApplicationIdGet(applicationId: UUID): ResponseEntity { + val user = userService.getUserForRequest() + + val application = when ( + val applicationResult = applicationService + .getCas2BailApplicationForUser( + applicationId, + user + ) + + ) { + is AuthorisableActionResult.NotFound -> null + is AuthorisableActionResult.Unauthorised -> throw ForbiddenProblem() + is AuthorisableActionResult.Success -> applicationResult.entity + } + + if (application != null) { + return ResponseEntity.ok(getPersonDetailAndTransform(application)) + } + throw NotFoundProblem(applicationId, "Application") + } + + @Transactional + override fun applicationsPost(body: NewApplication): ResponseEntity { + val nomisPrincipal = httpAuthService.getNomisPrincipalOrThrow() + val user = userService.getUserForRequest() + + val personInfo = offenderService.getFullInfoForPersonOrThrow(body.crn) + + val applicationResult = applicationService.createCas2BailApplication( + body.crn, + user, + nomisPrincipal.token.tokenValue, + ) + + 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("/cas2/applications/${application.id}")) + .body(applicationsTransformer.transformJpaToApi(application, personInfo)) + } + + @Transactional + override fun applicationsApplicationIdPut( + applicationId: UUID, + body: UpdateApplication, + ): ResponseEntity { + val user = userService.getUserForRequest() + + val serializedData = objectMapper.writeValueAsString(body.data) + + val applicationResult = applicationService.updateCas2BailApplication( + applicationId = + applicationId, + data = serializedData, + user, + ) + + val validationResult = when (applicationResult) { + is AuthorisableActionResult.NotFound -> throw NotFoundProblem(applicationId, "Application") + is AuthorisableActionResult.Unauthorised -> throw ForbiddenProblem() + is AuthorisableActionResult.Success -> applicationResult.entity + } + + val updatedApplication = when (validationResult) { + 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 + } + + return ResponseEntity.ok(getPersonDetailAndTransform(updatedApplication)) + } + + @Transactional + override fun applicationsApplicationIdAbandonPut(applicationId: UUID): ResponseEntity { + val user = userService.getUserForRequest() + + val validationResult = when (val applicationResult = applicationService.abandonCas2BailApplication(applicationId, user)) { + is AuthorisableActionResult.NotFound -> throw NotFoundProblem(applicationId, "Application") + is AuthorisableActionResult.Unauthorised -> throw ForbiddenProblem() + is AuthorisableActionResult.Success -> applicationResult.entity + } + + when (validationResult) { + 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 + } + + return ResponseEntity.ok(Unit) + } + + private fun getPersonNamesAndTransformToSummaries(applicationSummaries: List): List { + val crns = applicationSummaries.map { it.crn } + + val personNamesMap = offenderService.getMapOfPersonNamesAndCrns(crns) + + return applicationSummaries.map { application -> + applicationsTransformer.transformJpaSummaryToSummary(application, personNamesMap[application.crn]!!) + } + } + + private fun getPersonDetailAndTransform( + application: Cas2BailApplicationEntity, + ): Application { + val personInfo = offenderService.getFullInfoForPersonOrThrow(application.crn) + + return applicationsTransformer.transformJpaToApi(application, personInfo) + } +} \ No newline at end of file diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/JsonSchemaEntity.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/JsonSchemaEntity.kt index dcbffe6948..578bd132cd 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/JsonSchemaEntity.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/JsonSchemaEntity.kt @@ -53,6 +53,16 @@ class Cas2ApplicationJsonSchemaEntity( schema: String, ) : JsonSchemaEntity(id, addedAt, schema) +@Entity +@DiscriminatorValue("CAS_2_BAIL_APPLICATION") +@Table(name = "cas_2_bail_application_json_schemas") +@PrimaryKeyJoinColumn(name = "json_schema_id") +class Cas2BailApplicationJsonSchemaEntity( + id: UUID, + addedAt: OffsetDateTime, + schema: String, +) : JsonSchemaEntity(id, addedAt, schema) + @Entity @DiscriminatorValue("TEMPORARY_ACCOMMODATION_APPLICATION") @Table(name = "temporary_accommodation_application_json_schemas") diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationEntity.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationEntity.kt new file mode 100644 index 0000000000..9b5cab136d --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationEntity.kt @@ -0,0 +1,114 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail + +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.* + + +import io.hypersistence.utils.hibernate.type.json.JsonType +import jakarta.persistence.Entity +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.LockModeType +import jakarta.persistence.ManyToOne +import jakarta.persistence.OneToMany +import jakarta.persistence.OneToOne +import jakarta.persistence.Table +import org.hibernate.annotations.Immutable +import org.hibernate.annotations.OrderBy +import org.hibernate.annotations.Type +import org.springframework.data.domain.Slice +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Lock +import org.springframework.data.jpa.repository.Query +import org.springframework.stereotype.Repository +import java.time.LocalDate +import java.time.OffsetDateTime +import java.util.UUID + +@Suppress("TooManyFunctions") +@Repository +interface Cas2BailApplicationRepository : JpaRepository { + @Query( + "SELECT a FROM Cas2BailApplicationEntity a WHERE a.id = :id AND " + + "a.submittedAt IS NOT NULL", + ) + fun findSubmittedApplicationById(id: UUID): Cas2BailApplicationEntity? + + @Query("SELECT a FROM Cas2BailApplicationEntity a WHERE a.createdByUser.id = :id") + fun findAllByCreatedByUserId(id: UUID): List + + @Query( + "SELECT a FROM Cas2BailApplicationEntity a WHERE a.submittedAt IS NOT NULL " + + "AND a NOT IN (SELECT application FROM Cas2AssessmentEntity)", + ) + fun findAllSubmittedApplicationsWithoutAssessments(): Slice +} + +@Repository +interface Cas2BailLockableApplicationRepository : JpaRepository { + @Query("SELECT a FROM Cas2BailLockableApplicationEntity a WHERE a.id = :id") + @Lock(LockModeType.PESSIMISTIC_WRITE) + fun acquirePessimisticLock(id: UUID): Cas2LockableApplicationEntity? +} + +@Entity +@Table(name = "cas_2_bail_applications") +data class Cas2BailApplicationEntity( + @Id + val id: UUID, + + val crn: String, + + @ManyToOne + @JoinColumn(name = "created_by_user_id") + val createdByUser: NomisUserEntity, + + @Type(JsonType::class) + var data: String?, + + @Type(JsonType::class) + var document: String?, + + @ManyToOne + @JoinColumn(name = "schema_version") + var schemaVersion: JsonSchemaEntity, + val createdAt: OffsetDateTime, + var submittedAt: OffsetDateTime?, + var abandonedAt: OffsetDateTime? = null, + + @OneToMany(mappedBy = "application") + @OrderBy(clause = "createdAt DESC") + var statusUpdates: MutableList? = null, + + @OneToMany(mappedBy = "application") + @OrderBy(clause = "createdAt DESC") + var notes: MutableList? = null, + + @OneToOne(mappedBy = "application") + var assessment: Cas2BailAssessmentEntity? = null, + + @Transient + var schemaUpToDate: Boolean, + + var nomsNumber: String?, + + var referringPrisonCode: String? = null, + var preferredAreas: String? = null, + var hdcEligibilityDate: LocalDate? = null, + var conditionalReleaseDate: LocalDate? = null, + var telephoneNumber: String? = null, +) { + override fun toString() = "Cas2BailApplicationEntity: $id" +} + +/** + * Provides a version of the Cas2BailApplicationEntity with no relationships, allowing + * us to lock the applications table only without JPA/Hibernate attempting to + * lock all eagerly loaded relationships + */ +@Entity +@Table(name = "cas_2_bail_applications") +@Immutable +class Cas2BailLockableApplicationEntity( + @Id + val id: UUID, +) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationNoteEntity.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationNoteEntity.kt new file mode 100644 index 0000000000..828e678850 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationNoteEntity.kt @@ -0,0 +1,69 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail + +import jakarta.persistence.* +import org.springframework.data.domain.PageRequest +import org.springframework.data.domain.Slice +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query +import org.springframework.stereotype.Repository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.* +import java.time.OffsetDateTime +import java.util.* +import kotlin.jvm.Transient + +@Repository +interface Cas2BailApplicationNoteRepository : JpaRepository { + @Query( + "SELECT n FROM Cas2BailApplicationNoteEntity n WHERE n.assessment IS NULL", + ) + fun findAllNotesWithoutAssessment(of: PageRequest): Slice + +} + +@Entity +@Table(name = "cas_2_bail_application_notes") +data class Cas2BailApplicationNoteEntity( + @Id + val id: UUID, + + @Transient + final val createdByUser: Cas2User, + + @ManyToOne + @JoinColumn(name = "application_id") + val application: Cas2BailApplicationEntity, + + val createdAt: OffsetDateTime, + + var body: String, + + @ManyToOne + @JoinColumn(name = "assessment_id") + var assessment: Cas2BailAssessmentEntity?, +) { + + @ManyToOne + @JoinColumn(name = "created_by_nomis_user_id") + var createdByNomisUser: NomisUserEntity? = null + + @ManyToOne + @JoinColumn(name = "created_by_external_user_id") + var createdByExternalUser: ExternalUserEntity? = null + + init { + when (this.createdByUser) { + is NomisUserEntity -> this.createdByNomisUser = this.createdByUser + is ExternalUserEntity -> this.createdByExternalUser = this.createdByUser + } + } + + fun getUser(): Cas2User { + return if (this.createdByNomisUser != null) { + this.createdByNomisUser!! + } else { + this.createdByExternalUser!! + } + } + + override fun toString() = "Cas2BailApplicationNoteEntity: $id" +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationSummaryEntity.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationSummaryEntity.kt new file mode 100644 index 0000000000..991fa69810 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationSummaryEntity.kt @@ -0,0 +1,58 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.Id +import jakarta.persistence.Table +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository +import java.time.LocalDate +import java.time.OffsetDateTime +import java.util.* + +@Repository +interface Cas2BailApplicationSummaryRepository : JpaRepository { + fun findByUserId(userId: String, pageable: Pageable?): Page + + fun findByUserIdAndSubmittedAtIsNotNull(userId: String, pageable: Pageable?): Page + + fun findByUserIdAndSubmittedAtIsNull(userId: String, pageable: Pageable?): Page + + fun findByPrisonCode(prisonCode: String, pageable: Pageable?): Page + + fun findByPrisonCodeAndSubmittedAtIsNotNull(prisonCode: String, pageable: Pageable?): Page + + fun findByPrisonCodeAndSubmittedAtIsNull(prisonCode: String, pageable: Pageable?): Page + + fun findBySubmittedAtIsNotNull(pageable: Pageable?): Page +} + +@Entity +@Table(name = "cas_2_bail_application_live_summary") +data class Cas2BailApplicationSummaryEntity( + @Id + val id: UUID, + val crn: String, + @Column(name = "noms_number") + var nomsNumber: String, + @Column(name = "created_by_user_id") + val userId: String, + @Column(name = "name") + val userName: String, + @Column(name = "created_at") + val createdAt: OffsetDateTime, + @Column(name = "submitted_at") + var submittedAt: OffsetDateTime?, + @Column(name = "abandoned_at") + var abandonedAt: OffsetDateTime? = null, + @Column(name = "hdc_eligibility_date") + var hdcEligibilityDate: LocalDate? = null, + @Column(name = "label") + var latestStatusUpdateLabel: String? = null, + @Column(name = "status_id") + var latestStatusUpdateStatusId: String? = null, + @Column(name = "referring_prison_code") + val prisonCode: String, +) \ No newline at end of file diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailAssessmentEntity.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailAssessmentEntity.kt new file mode 100644 index 0000000000..b6df838320 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailAssessmentEntity.kt @@ -0,0 +1,44 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail + +import jakarta.persistence.Entity +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.OneToMany +import jakarta.persistence.Table +import jakarta.persistence.OrderBy + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2StatusUpdateEntity +import java.time.OffsetDateTime +import java.util.UUID + +@Repository +interface Cas2BailAssessmentRepository : JpaRepository { + fun findFirstByApplicationId(applicationId: UUID): Cas2BailAssessmentEntity? +} + +@Entity +@Table(name = "cas_2_bail_assessments") +data class Cas2BailAssessmentEntity( + @Id + val id: UUID, + + @ManyToOne + @JoinColumn(name = "application_id") + val application: Cas2BailApplicationEntity, + + val createdAt: OffsetDateTime, + + var nacroReferralId: String? = null, + + var assessorName: String? = null, + + @OneToMany(mappedBy = "assessment") + @OrderBy("createdAt DESC") + var statusUpdates: MutableList? = null, +) { + override fun toString() = "Cas2AssessmentEntity: $id" +} \ No newline at end of file diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2/JsonSchemaService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2/JsonSchemaService.kt index 5ada788e08..19f6de5e63 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2/JsonSchemaService.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2/JsonSchemaService.kt @@ -10,6 +10,7 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2Applicati import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationRepository import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.JsonSchemaEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.JsonSchemaRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity import java.util.Collections.synchronizedMap import java.util.UUID @@ -47,5 +48,11 @@ class JsonSchemaService( return application.apply { application.schemaUpToDate = application.schemaVersion.id == newestSchema.id } } + fun checkCas2BailSchemaOutdated(application: Cas2BailApplicationEntity): Cas2BailApplicationEntity { + val newestSchema = getNewestSchema(application.schemaVersion.javaClass) + + return application.apply { application.schemaUpToDate = application.schemaVersion.id == newestSchema.id } + } + fun getNewestSchema(type: Class): JsonSchemaEntity = jsonSchemaRepository.getSchemasForType(type).maxBy { it.addedAt } } diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationService.kt new file mode 100644 index 0000000000..4e1779b193 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationService.kt @@ -0,0 +1,390 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2bail + +import com.fasterxml.jackson.databind.ObjectMapper +import jakarta.transaction.Transactional +import org.springframework.beans.factory.annotation.Value +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.events.cas2.model.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.SubmitCas2Application +import uk.gov.justice.digital.hmpps.approvedpremisesapi.config.NotifyConfig +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.DomainEvent +import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.PaginationMetadata +import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.ValidationErrors +import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.validated +import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.AuthorisableActionResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.ValidatableActionResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.EmailNotificationService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.UpstreamApiException +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.PageCriteria +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.getMetadata +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.getPageableOrAllPages +import java.time.OffsetDateTime +import java.util.* + +@Service("Cas2BailApplicationService") +class Cas2BailApplicationService( + private val cas2BailApplicationRepository: Cas2BailApplicationRepository, + private val lockableApplicationRepository: Cas2BailLockableApplicationRepository, + private val applicationSummaryRepository: Cas2BailApplicationSummaryRepository, + private val jsonSchemaService: JsonSchemaService, + private val offenderService: OffenderService, + private val cas2BailUserAccessService: Cas2BailUserAccessService, + private val domainEventService: DomainEventService, + private val emailNotificationService: EmailNotificationService, + private val assessmentService: Cas2BailAssessmentService, + private val notifyConfig: NotifyConfig, + private val objectMapper: ObjectMapper, + @Value("\${url-templates.frontend.cas2.application}") private val applicationUrlTemplate: String, + @Value("\${url-templates.frontend.cas2.submitted-application-overview}") private val submittedApplicationUrlTemplate: String, +) { + + val repositoryUserFunctionMap = mapOf( + null to applicationSummaryRepository::findByUserId, + true to applicationSummaryRepository::findByUserIdAndSubmittedAtIsNotNull, + false to applicationSummaryRepository::findByUserIdAndSubmittedAtIsNull, + ) + + val repositoryPrisonFunctionMap = mapOf( + null to applicationSummaryRepository::findByPrisonCode, + true to applicationSummaryRepository::findByPrisonCodeAndSubmittedAtIsNotNull, + false to applicationSummaryRepository::findByPrisonCodeAndSubmittedAtIsNull, + ) + + fun getCas2BailApplications( + prisonCode: String?, + isSubmitted: Boolean?, + user: NomisUserEntity, + pageCriteria: PageCriteria, + ): Pair, PaginationMetadata?> { + val response = if (prisonCode == null) { + repositoryUserFunctionMap.get(isSubmitted)!!(user.id.toString(), getPageableOrAllPages(pageCriteria)) + } else { + repositoryPrisonFunctionMap.get(isSubmitted)!!(prisonCode, getPageableOrAllPages(pageCriteria)) + } + val metadata = getMetadata(response, pageCriteria) + return Pair(response.content, metadata) + } + + fun getAllSubmittedCas2BailApplicationsForAssessor(pageCriteria: PageCriteria): Pair, PaginationMetadata?> { + val pageable = getPageableOrAllPages(pageCriteria) + + val response = applicationSummaryRepository.findBySubmittedAtIsNotNull(pageable) + + val metadata = getMetadata(response, pageCriteria) + + return Pair(response.content, metadata) + } + + fun getSubmittedCas2BailApplicationForAssessor(applicationId: UUID): AuthorisableActionResult { + val applicationEntity = cas2BailApplicationRepository.findSubmittedApplicationById(applicationId) + ?: return AuthorisableActionResult.NotFound() + + return AuthorisableActionResult.Success( + jsonSchemaService.checkCas2BailSchemaOutdated(applicationEntity), + ) + } + + fun getCas2BailApplicationForUser(applicationId: UUID, user: NomisUserEntity): AuthorisableActionResult { + val applicationEntity = cas2BailApplicationRepository.findByIdOrNull(applicationId) + ?: return AuthorisableActionResult.NotFound() + + if (applicationEntity.abandonedAt != null) { + return AuthorisableActionResult.NotFound() + } + + val canAccess = cas2BailUserAccessService.userCanViewCas2BailApplication(user, applicationEntity) + + return if (canAccess) { + AuthorisableActionResult.Success( + jsonSchemaService.checkCas2BailSchemaOutdated + (applicationEntity), + ) + } else { + AuthorisableActionResult.Unauthorised() + } + } + + fun createCas2BailApplication(crn: String, user: NomisUserEntity, jwt: String) = + validated { + val offenderDetailsResult = offenderService.getOffenderByCrn(crn) + + val offenderDetails = when (offenderDetailsResult) { + is AuthorisableActionResult.NotFound -> return "$.crn" hasSingleValidationError "doesNotExist" + is AuthorisableActionResult.Unauthorised -> return "$.crn" hasSingleValidationError "userPermission" + is AuthorisableActionResult.Success -> offenderDetailsResult.entity + } + + if (offenderDetails.otherIds.nomsNumber == null) { + throw RuntimeException("Cannot create an Application for an Offender without a NOMS number") + } + + if (validationErrors.any()) { + return fieldValidationError + } + + val createdApplication = cas2BailApplicationRepository.save( + Cas2BailApplicationEntity( + id = UUID.randomUUID(), + crn = crn, + createdByUser = user, + data = null, + document = null, + schemaVersion = jsonSchemaService.getNewestSchema(Cas2ApplicationJsonSchemaEntity::class.java), + createdAt = OffsetDateTime.now(), + submittedAt = null, + schemaUpToDate = true, + nomsNumber = offenderDetails.otherIds.nomsNumber, + telephoneNumber = null, + ), + ) + + return success(createdApplication.apply { schemaUpToDate = true }) + } + + @SuppressWarnings("ReturnCount") + fun updateCas2BailApplication(applicationId: UUID, data: String?, user: NomisUserEntity): AuthorisableActionResult> { + val application = cas2BailApplicationRepository.findByIdOrNull(applicationId)?.let(jsonSchemaService::checkCas2BailSchemaOutdated) + ?: return AuthorisableActionResult.NotFound() + + if (application.createdByUser != user) { + return AuthorisableActionResult.Unauthorised() + } + + if (application.submittedAt != null) { + return AuthorisableActionResult.Success( + ValidatableActionResult.GeneralValidationError("This application has already been submitted"), + ) + } + + if (application.abandonedAt != null) { + return AuthorisableActionResult.Success( + ValidatableActionResult.GeneralValidationError("This application has been abandoned"), + ) + } + + if (!application.schemaUpToDate) { + return AuthorisableActionResult.Success( + ValidatableActionResult.GeneralValidationError("The schema version is outdated"), + ) + } + + application.apply { + this.data = removeXssCharacters(data) + } + + val savedApplication = cas2BailApplicationRepository.save(application) + + return AuthorisableActionResult.Success( + ValidatableActionResult.Success(savedApplication), + ) + } + + @SuppressWarnings("ReturnCount") + fun abandonCas2BailApplication(applicationId: UUID, user: NomisUserEntity): AuthorisableActionResult> { + val application = cas2BailApplicationRepository.findByIdOrNull(applicationId) + ?: return AuthorisableActionResult.NotFound() + + if (application.createdByUser != user) { + return AuthorisableActionResult.Unauthorised() + } + + if (application.submittedAt != null) { + return AuthorisableActionResult.Success( + ValidatableActionResult.ConflictError(applicationId, "This application has already been submitted"), + ) + } + + if (application.abandonedAt != null) { + return AuthorisableActionResult.Success( + ValidatableActionResult.Success(application), + ) + } + + application.apply { + this.abandonedAt = OffsetDateTime.now() + this.data = null + } + + val savedApplication = cas2BailApplicationRepository.save(application) + + return AuthorisableActionResult.Success( + ValidatableActionResult.Success(savedApplication), + ) + } + + @SuppressWarnings("ReturnCount") + @Transactional + fun submitCas2BailApplication( + submitApplication: SubmitCas2Application, + user: NomisUserEntity, + ): AuthorisableActionResult> { + val applicationId = submitApplication.applicationId + + lockableApplicationRepository.acquirePessimisticLock(applicationId) + + var application = cas2BailApplicationRepository.findByIdOrNull(applicationId) + ?.let(jsonSchemaService::checkCas2BailSchemaOutdated) + ?: return AuthorisableActionResult.NotFound() + + val serializedTranslatedDocument = objectMapper.writeValueAsString(submitApplication.translatedDocument) + + if (application.createdByUser != user) { + return AuthorisableActionResult.Unauthorised() + } + + if (application.abandonedAt != null) { + return AuthorisableActionResult.Success( + ValidatableActionResult.GeneralValidationError("This application has already been abandoned"), + ) + } + + if (application.submittedAt != null) { + return AuthorisableActionResult.Success( + ValidatableActionResult.GeneralValidationError("This application has already been submitted"), + ) + } + + if (!application.schemaUpToDate) { + return AuthorisableActionResult.Success( + ValidatableActionResult.GeneralValidationError("The schema version is outdated"), + ) + } + + val validationErrors = ValidationErrors() + val applicationData = application.data + + if (applicationData == null) { + validationErrors["$.data"] = "empty" + } else if (!jsonSchemaService.validate(application.schemaVersion, applicationData)) { + validationErrors["$.data"] = "invalid" + } + + if (validationErrors.any()) { + return AuthorisableActionResult.Success( + ValidatableActionResult.FieldValidationError(validationErrors), + ) + } + + val schema = application.schemaVersion as? Cas2BailApplicationJsonSchemaEntity + ?: throw RuntimeException("Incorrect type of JSON schema referenced by CAS2 Bail Application") + + try { + application.apply { + submittedAt = OffsetDateTime.now() + document = serializedTranslatedDocument + referringPrisonCode = retrievePrisonCode(application) + preferredAreas = submitApplication.preferredAreas + hdcEligibilityDate = submitApplication.hdcEligibilityDate + conditionalReleaseDate = submitApplication.conditionalReleaseDate + telephoneNumber = submitApplication.telephoneNumber + } + } catch (error: UpstreamApiException) { + return AuthorisableActionResult.Success( + ValidatableActionResult.GeneralValidationError(error.message.toString()), + ) + } + + application = cas2BailApplicationRepository.save(application) + + createCas2ApplicationSubmittedEvent(application) + + createAssessment(application) + + sendEmailApplicationSubmitted(user, application) + + return AuthorisableActionResult.Success( + ValidatableActionResult.Success(application), + ) + } + + fun createCas2ApplicationSubmittedEvent(application: Cas2BailApplicationEntity) { + val domainEventId = UUID.randomUUID() + val eventOccurredAt = application.submittedAt ?: OffsetDateTime.now() + + domainEventService.saveCas2ApplicationSubmittedDomainEvent( + DomainEvent( + id = domainEventId, + applicationId = application.id, + crn = application.crn, + nomsNumber = application.nomsNumber, + occurredAt = eventOccurredAt.toInstant(), + data = Cas2ApplicationSubmittedEvent( + id = domainEventId, + timestamp = eventOccurredAt.toInstant(), + eventType = EventType.applicationSubmitted, + eventDetails = Cas2ApplicationSubmittedEventDetails( + applicationId = application.id, + applicationUrl = applicationUrlTemplate + .replace("#id", application.id.toString()), + submittedAt = eventOccurredAt.toInstant(), + personReference = PersonReference( + noms = application.nomsNumber ?: "Unknown NOMS Number", + crn = application.crn, + ), + referringPrisonCode = application.referringPrisonCode, + preferredAreas = application.preferredAreas, + hdcEligibilityDate = application.hdcEligibilityDate, + conditionalReleaseDate = application.conditionalReleaseDate, + submittedBy = Cas2ApplicationSubmittedEventDetailsSubmittedBy( + staffMember = Cas2StaffMember( + staffIdentifier = application.createdByUser.nomisStaffId, + name = application.createdByUser.name, + username = application.createdByUser.nomisUsername, + ), + ), + ), + ), + ), + ) + } + + fun createAssessment(application: Cas2BailApplicationEntity) { + assessmentService.createCas2Assessment(application) + } + + @SuppressWarnings("ThrowsCount") + private fun retrievePrisonCode(application: Cas2BailApplicationEntity): String { + val inmateDetailResult = offenderService.getInmateDetailByNomsNumber( + crn = application.crn, + nomsNumber = application.nomsNumber.toString(), + ) + val inmateDetail = when (inmateDetailResult) { + is AuthorisableActionResult.NotFound -> throw UpstreamApiException("Inmate Detail not found") + is AuthorisableActionResult.Unauthorised -> throw UpstreamApiException("Inmate Detail unauthorised") + is AuthorisableActionResult.Success -> inmateDetailResult.entity + } + + return inmateDetail?.assignedLivingUnit?.agencyId ?: throw UpstreamApiException("No prison code available") + } + + private fun sendEmailApplicationSubmitted(user: NomisUserEntity, application: Cas2BailApplicationEntity) { + emailNotificationService.sendEmail( + recipientEmailAddress = notifyConfig.emailAddresses.cas2Assessors, + templateId = notifyConfig.templates.cas2ApplicationSubmitted, + personalisation = mapOf( + "name" to user.name, + "email" to user.email, + "prisonNumber" to application.nomsNumber, + "telephoneNumber" to application.telephoneNumber, + "applicationUrl" to submittedApplicationUrlTemplate.replace("#applicationId", application.id.toString()), + ), + replyToEmailId = notifyConfig.emailAddresses.cas2ReplyToId, + ) + } + + private fun removeXssCharacters(data: String?): String? { + if (data != null) { + val xssCharacters = setOf('<', '<', '〈', '〈', '>', '>', '〉', '〉') + var sanitisedData = data + xssCharacters.forEach { character -> + sanitisedData = sanitisedData?.replace(character.toString(), "") + } + return sanitisedData + } + return null + } +} \ No newline at end of file diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailAssessmentService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailAssessmentService.kt new file mode 100644 index 0000000000..5633c613f9 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailAssessmentService.kt @@ -0,0 +1,54 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2bail + +import jakarta.transaction.Transactional +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.UpdateCas2Assessment + +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailAssessmentEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailAssessmentRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.AuthorisableActionResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.ValidatableActionResult +import java.time.OffsetDateTime +import java.util.* + + +@Service("Cas2BailAssessmentService") +class Cas2BailAssessmentService ( + private val assessmentRepository: Cas2BailAssessmentRepository, +) { + + @Transactional + fun createCas2Assessment(cas2BailApplicationEntity: Cas2BailApplicationEntity): Cas2BailAssessmentEntity = + assessmentRepository.save( + Cas2BailAssessmentEntity( + id = UUID.randomUUID(), + createdAt = OffsetDateTime.now(), + application = cas2BailApplicationEntity, + ), + ) + + fun updateAssessment(assessmentId: UUID, newAssessment: UpdateCas2Assessment): AuthorisableActionResult> { + val assessmentEntity = assessmentRepository.findByIdOrNull(assessmentId) + ?: return AuthorisableActionResult.NotFound() + + assessmentEntity.apply { + this.nacroReferralId = newAssessment.nacroReferralId + this.assessorName = newAssessment.assessorName + } + + val savedAssessment = assessmentRepository.save(assessmentEntity) + + return AuthorisableActionResult.Success( + ValidatableActionResult.Success(savedAssessment), + ) + } + + fun getAssessment(assessmentId: UUID): AuthorisableActionResult { + val assessmentEntity = assessmentRepository.findByIdOrNull(assessmentId) + ?: return AuthorisableActionResult.NotFound() + + return AuthorisableActionResult.Success(assessmentEntity) + } +} \ No newline at end of file diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailUserAccessService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailUserAccessService.kt new file mode 100644 index 0000000000..caedfa1486 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailUserAccessService.kt @@ -0,0 +1,26 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2bail + +import org.springframework.stereotype.Service +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.NomisUserEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity + +@Service("Cas2BailUserAccessService") +class Cas2BailUserAccessService { + fun userCanViewCas2BailApplication(user: NomisUserEntity, application: Cas2BailApplicationEntity): Boolean { + return if (user.id == application.createdByUser.id) { + true + } else if (application.submittedAt == null) { + false + } else { + offenderIsFromSamePrisonAsUser(application.referringPrisonCode, user.activeCaseloadId) + } + } + + fun offenderIsFromSamePrisonAsUser(referringPrisonCode: String?, activeCaseloadId: String?): Boolean { + return if (referringPrisonCode !== null && activeCaseloadId !== null) { + activeCaseloadId == referringPrisonCode + } else { + false + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailApplicationsTransformer.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailApplicationsTransformer.kt new file mode 100644 index 0000000000..6cb05bd217 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailApplicationsTransformer.kt @@ -0,0 +1,84 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas2bail + +import com.fasterxml.jackson.databind.ObjectMapper +import org.springframework.stereotype.Component +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.ApplicationStatus +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas2Application +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationSummaryEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationSummaryEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.PersonInfoResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.NomisUserTransformer +import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.PersonTransformer +import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas2.AssessmentsTransformer +import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas2.StatusUpdateTransformer +import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas2.TimelineEventsTransformer +import java.util.* + +@Component("Cas2BailApplicationsTransformer") +class Cas2BailApplicationsTransformer( + private val objectMapper: ObjectMapper, + private val personTransformer: PersonTransformer, + private val nomisUserTransformer: NomisUserTransformer, + private val statusUpdateTransformer: Cas2BailStatusUpdateTransformer, + private val timelineEventsTransformer: Cas2BailTimelineEventsTransformer, + private val assessmentsTransformer: Cas2BailAssessmentsTransformer, +) { + + + fun transformJpaToApi(jpa: Cas2BailApplicationEntity, personInfo: PersonInfoResult): Cas2Application { + return Cas2Application( + id = jpa.id, + person = personTransformer.transformModelToPersonApi(personInfo), + createdBy = nomisUserTransformer.transformJpaToApi(jpa.createdByUser), + schemaVersion = jpa.schemaVersion.id, + outdatedSchema = !jpa.schemaUpToDate, + createdAt = jpa.createdAt.toInstant(), + submittedAt = jpa.submittedAt?.toInstant(), + data = if (jpa.data != null) objectMapper.readTree(jpa.data) else null, + document = if (jpa.document != null) objectMapper.readTree(jpa.document) else null, + status = getStatus(jpa), + type = "CAS2", + telephoneNumber = jpa.telephoneNumber, + assessment = if (jpa.assessment != null) assessmentsTransformer.transformJpaToApiRepresentation(jpa.assessment!!) else null, + timelineEvents = timelineEventsTransformer.transformApplicationToTimelineEvents(jpa), + ) + } + + fun transformJpaSummaryToSummary( + jpaSummary: Cas2BailApplicationSummaryEntity, + personName: String, + ): uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas2ApplicationSummary { + return uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model + .Cas2ApplicationSummary( + id = jpaSummary.id, + createdByUserId = UUID.fromString(jpaSummary.userId), + createdByUserName = jpaSummary.userName, + createdAt = jpaSummary.createdAt.toInstant(), + submittedAt = jpaSummary.submittedAt?.toInstant(), + status = getStatusFromSummary(jpaSummary), + latestStatusUpdate = statusUpdateTransformer.transformJpaSummaryToLatestStatusUpdateApi(jpaSummary), + type = "CAS2", + hdcEligibilityDate = jpaSummary.hdcEligibilityDate, + crn = jpaSummary.crn, + nomsNumber = jpaSummary.nomsNumber, + personName = personName, + ) + } + + private fun getStatus(entity: Cas2BailApplicationEntity): ApplicationStatus { + if (entity.submittedAt !== null) { + return ApplicationStatus.submitted + } + + return ApplicationStatus.inProgress + } + + private fun getStatusFromSummary(summary: Cas2BailApplicationSummaryEntity): ApplicationStatus { + return when { + summary.submittedAt != null -> ApplicationStatus.submitted + else -> ApplicationStatus.inProgress + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailAssessmentsTransformer.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailAssessmentsTransformer.kt new file mode 100644 index 0000000000..f4e561f228 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailAssessmentsTransformer.kt @@ -0,0 +1,21 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas2bail + +import org.springframework.stereotype.Component +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas2Assessment +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2AssessmentEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailAssessmentEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas2.StatusUpdateTransformer + +@Component("Cas2BailAssessmentsTransformer") +class Cas2BailAssessmentsTransformer (private val statusUpdateTransformer: StatusUpdateTransformer) { + fun transformJpaToApiRepresentation( + jpaAssessment: Cas2BailAssessmentEntity, + ): Cas2Assessment { + return Cas2Assessment( + jpaAssessment.id, + jpaAssessment.nacroReferralId, + jpaAssessment.assessorName, + jpaAssessment.statusUpdates?.map { update -> statusUpdateTransformer.transformJpaToApi(update) }, + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailStatusUpdateTransformer.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailStatusUpdateTransformer.kt new file mode 100644 index 0000000000..4c4d804e09 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailStatusUpdateTransformer.kt @@ -0,0 +1,51 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas2bail + +import org.springframework.stereotype.Component +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas2StatusUpdate +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas2StatusUpdateDetail +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.LatestCas2StatusUpdate +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationSummaryEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2StatusUpdateDetailEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2StatusUpdateEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationSummaryEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.ExternalUserTransformer +import java.util.* + +@Component("Cas2BailStatusUpdateTransformer") +class Cas2BailStatusUpdateTransformer( + private val externalUserTransformer: ExternalUserTransformer, +) { + + fun transformJpaToApi( + jpa: Cas2StatusUpdateEntity, + ): Cas2StatusUpdate { + return Cas2StatusUpdate( + id = jpa.id, + name = jpa.status().name, + label = jpa.label, + description = jpa.description, + updatedBy = externalUserTransformer.transformJpaToApi(jpa.assessor), + updatedAt = jpa.createdAt?.toInstant(), + statusUpdateDetails = jpa.statusUpdateDetails?.map { detail -> transformStatusUpdateDetailsJpaToApi(detail) }, + ) + } + + fun transformStatusUpdateDetailsJpaToApi(jpa: Cas2StatusUpdateDetailEntity): Cas2StatusUpdateDetail { + return Cas2StatusUpdateDetail( + id = jpa.id, + name = jpa.statusDetail(jpa.statusUpdate.statusId, jpa.statusDetailId).name, + label = jpa.label, + ) + } + + fun transformJpaSummaryToLatestStatusUpdateApi(jpa: Cas2BailApplicationSummaryEntity): LatestCas2StatusUpdate? { + if (jpa.latestStatusUpdateStatusId !== null) { + return LatestCas2StatusUpdate( + statusId = UUID.fromString(jpa.latestStatusUpdateStatusId!!), + label = jpa.latestStatusUpdateLabel!!, + ) + } else { + return null + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailTimelineEventsTransformer.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailTimelineEventsTransformer.kt new file mode 100644 index 0000000000..30bd9da77e --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailTimelineEventsTransformer.kt @@ -0,0 +1,59 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas2bail + +import org.springframework.stereotype.Component +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas2TimelineEvent +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.TimelineEventType +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity + +@Component("Cas2BailTimelineEventsTransformer") +class Cas2BailTimelineEventsTransformer { + + fun transformApplicationToTimelineEvents(jpa: Cas2BailApplicationEntity): List { + val timelineEvents: MutableList = mutableListOf() + + addSubmittedEvent(jpa, timelineEvents) + + addStatusUpdateEvents(jpa, timelineEvents) + + addNoteEvents(jpa, timelineEvents) + + return timelineEvents.sortedByDescending { it.occurredAt } + } + + private fun addNoteEvents(jpa: Cas2BailApplicationEntity, timelineEvents: MutableList) { + jpa.notes?.forEach { + timelineEvents += Cas2TimelineEvent( + type = TimelineEventType.cas2Note, + occurredAt = it.createdAt.toInstant(), + label = "Note", + createdByName = it.getUser().name, + body = it.body, + ) + } + } + + private fun addStatusUpdateEvents(jpa: Cas2BailApplicationEntity, timelineEvents: MutableList) { + jpa.statusUpdates?.forEach { + timelineEvents += Cas2TimelineEvent( + type = TimelineEventType.cas2StatusUpdate, + occurredAt = it.createdAt.toInstant(), + label = it.label, + createdByName = it.assessor.name, + body = it.statusUpdateDetails?.joinToString { detail -> detail.label }, + ) + } + } + + private fun addSubmittedEvent(jpa: Cas2BailApplicationEntity, timelineEvents: MutableList) { + if (jpa.submittedAt !== null) { + val submittedAtEvent = Cas2TimelineEvent( + type = TimelineEventType.cas2ApplicationSubmitted, + occurredAt = jpa.submittedAt?.toInstant()!!, + label = "Application submitted", + createdByName = jpa.createdByUser.name, + ) + timelineEvents += submittedAtEvent + } + } +} \ No newline at end of file diff --git a/src/main/resources/static/_shared.yml b/src/main/resources/static/_shared.yml index 27c2b5358e..add9268d6c 100644 --- a/src/main/resources/static/_shared.yml +++ b/src/main/resources/static/_shared.yml @@ -1943,6 +1943,49 @@ components: - personName - crn - nomsNumber + Cas2BailApplicationSummary: + type: object + properties: + type: + type: string + id: + type: string + format: uuid + createdAt: + type: string + format: date-time + submittedAt: + type: string + format: date-time + createdByUserId: + type: string + format: uuid + createdByUserName: + type: string + status: + $ref: '#/components/schemas/ApplicationStatus' + latestStatusUpdate: + $ref: '#/components/schemas/LatestCas2StatusUpdate' + risks: + $ref: '#/components/schemas/PersonRisks' + hdcEligibilityDate: + type: string + format: date + personName: + type: string + crn: + type: string + nomsNumber: + type: string + required: + - type + - id + - createdAt + - createdByUserId + - status + - personName + - crn + - nomsNumber NewCas2ApplicationNote: type: object properties: diff --git a/src/main/resources/static/codegen/built-api-spec.yml b/src/main/resources/static/codegen/built-api-spec.yml index 41b4ed63e8..2f376c5eb6 100644 --- a/src/main/resources/static/codegen/built-api-spec.yml +++ b/src/main/resources/static/codegen/built-api-spec.yml @@ -6274,6 +6274,49 @@ components: - personName - crn - nomsNumber + Cas2BailApplicationSummary: + type: object + properties: + type: + type: string + id: + type: string + format: uuid + createdAt: + type: string + format: date-time + submittedAt: + type: string + format: date-time + createdByUserId: + type: string + format: uuid + createdByUserName: + type: string + status: + $ref: '#/components/schemas/ApplicationStatus' + latestStatusUpdate: + $ref: '#/components/schemas/LatestCas2StatusUpdate' + risks: + $ref: '#/components/schemas/PersonRisks' + hdcEligibilityDate: + type: string + format: date + personName: + type: string + crn: + type: string + nomsNumber: + type: string + required: + - type + - id + - createdAt + - createdByUserId + - status + - personName + - crn + - nomsNumber NewCas2ApplicationNote: type: object properties: diff --git a/src/main/resources/static/codegen/built-cas1-api-spec.yml b/src/main/resources/static/codegen/built-cas1-api-spec.yml index 163c572551..ba124684e1 100644 --- a/src/main/resources/static/codegen/built-cas1-api-spec.yml +++ b/src/main/resources/static/codegen/built-cas1-api-spec.yml @@ -3055,6 +3055,49 @@ components: - personName - crn - nomsNumber + Cas2BailApplicationSummary: + type: object + properties: + type: + type: string + id: + type: string + format: uuid + createdAt: + type: string + format: date-time + submittedAt: + type: string + format: date-time + createdByUserId: + type: string + format: uuid + createdByUserName: + type: string + status: + $ref: '#/components/schemas/ApplicationStatus' + latestStatusUpdate: + $ref: '#/components/schemas/LatestCas2StatusUpdate' + risks: + $ref: '#/components/schemas/PersonRisks' + hdcEligibilityDate: + type: string + format: date + personName: + type: string + crn: + type: string + nomsNumber: + type: string + required: + - type + - id + - createdAt + - createdByUserId + - status + - personName + - crn + - nomsNumber NewCas2ApplicationNote: type: object properties: diff --git a/src/main/resources/static/codegen/built-cas2-api-spec.yml b/src/main/resources/static/codegen/built-cas2-api-spec.yml index 8e287c46dc..c6427c7ca0 100644 --- a/src/main/resources/static/codegen/built-cas2-api-spec.yml +++ b/src/main/resources/static/codegen/built-cas2-api-spec.yml @@ -2534,6 +2534,49 @@ components: - personName - crn - nomsNumber + Cas2BailApplicationSummary: + type: object + properties: + type: + type: string + id: + type: string + format: uuid + createdAt: + type: string + format: date-time + submittedAt: + type: string + format: date-time + createdByUserId: + type: string + format: uuid + createdByUserName: + type: string + status: + $ref: '#/components/schemas/ApplicationStatus' + latestStatusUpdate: + $ref: '#/components/schemas/LatestCas2StatusUpdate' + risks: + $ref: '#/components/schemas/PersonRisks' + hdcEligibilityDate: + type: string + format: date + personName: + type: string + crn: + type: string + nomsNumber: + type: string + required: + - type + - id + - createdAt + - createdByUserId + - status + - personName + - crn + - nomsNumber NewCas2ApplicationNote: type: object properties: diff --git a/src/main/resources/static/codegen/built-cas2bail-api-spec.yml b/src/main/resources/static/codegen/built-cas2bail-api-spec.yml index 0629bbdee4..b3ff5ea48e 100644 --- a/src/main/resources/static/codegen/built-cas2bail-api-spec.yml +++ b/src/main/resources/static/codegen/built-cas2bail-api-spec.yml @@ -2534,6 +2534,49 @@ components: - personName - crn - nomsNumber + Cas2BailApplicationSummary: + type: object + properties: + type: + type: string + id: + type: string + format: uuid + createdAt: + type: string + format: date-time + submittedAt: + type: string + format: date-time + createdByUserId: + type: string + format: uuid + createdByUserName: + type: string + status: + $ref: '#/components/schemas/ApplicationStatus' + latestStatusUpdate: + $ref: '#/components/schemas/LatestCas2StatusUpdate' + risks: + $ref: '#/components/schemas/PersonRisks' + hdcEligibilityDate: + type: string + format: date + personName: + type: string + crn: + type: string + nomsNumber: + type: string + required: + - type + - id + - createdAt + - createdByUserId + - status + - personName + - crn + - nomsNumber NewCas2ApplicationNote: type: object properties: diff --git a/src/main/resources/static/codegen/built-cas3-api-spec.yml b/src/main/resources/static/codegen/built-cas3-api-spec.yml index a00b8f38b5..af4d2217fe 100644 --- a/src/main/resources/static/codegen/built-cas3-api-spec.yml +++ b/src/main/resources/static/codegen/built-cas3-api-spec.yml @@ -2042,6 +2042,49 @@ components: - personName - crn - nomsNumber + Cas2BailApplicationSummary: + type: object + properties: + type: + type: string + id: + type: string + format: uuid + createdAt: + type: string + format: date-time + submittedAt: + type: string + format: date-time + createdByUserId: + type: string + format: uuid + createdByUserName: + type: string + status: + $ref: '#/components/schemas/ApplicationStatus' + latestStatusUpdate: + $ref: '#/components/schemas/LatestCas2StatusUpdate' + risks: + $ref: '#/components/schemas/PersonRisks' + hdcEligibilityDate: + type: string + format: date + personName: + type: string + crn: + type: string + nomsNumber: + type: string + required: + - type + - id + - createdAt + - createdByUserId + - status + - personName + - crn + - nomsNumber NewCas2ApplicationNote: type: object properties: From 0402efd9f7d88c59607e0609dffcd757914f152b Mon Sep 17 00:00:00 2001 From: Toby Batch Date: Thu, 28 Nov 2024 16:29:32 +0000 Subject: [PATCH 03/37] feat: application controller and linked services in cas2bail --- .../cas2bail/Cas2BailApplicationEntity.kt | 25 +++----- .../cas2bail/Cas2BailAssessmentEntity.kt | 17 ++---- .../Cas2BailStatusUpdateDetailEntry.kt | 40 ++++++++++++ .../cas2bail/Cas2BailStatusUpdateEntity.kt | 61 +++++++++++++++++++ .../Cas2BailAssessmentsTransformer.kt | 4 +- .../Cas2BailStatusUpdateTransformer.kt | 6 +- 6 files changed, 123 insertions(+), 30 deletions(-) create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailStatusUpdateDetailEntry.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailStatusUpdateEntity.kt diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationEntity.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationEntity.kt index 9b5cab136d..77a75f5cbe 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationEntity.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationEntity.kt @@ -4,16 +4,8 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.* import io.hypersistence.utils.hibernate.type.json.JsonType -import jakarta.persistence.Entity -import jakarta.persistence.Id -import jakarta.persistence.JoinColumn -import jakarta.persistence.LockModeType -import jakarta.persistence.ManyToOne -import jakarta.persistence.OneToMany -import jakarta.persistence.OneToOne -import jakarta.persistence.Table +import jakarta.persistence.* import org.hibernate.annotations.Immutable -import org.hibernate.annotations.OrderBy import org.hibernate.annotations.Type import org.springframework.data.domain.Slice import org.springframework.data.jpa.repository.JpaRepository @@ -23,6 +15,7 @@ import org.springframework.stereotype.Repository import java.time.LocalDate import java.time.OffsetDateTime import java.util.UUID +import kotlin.jvm.Transient @Suppress("TooManyFunctions") @Repository @@ -38,7 +31,7 @@ interface Cas2BailApplicationRepository : JpaRepository } @@ -47,7 +40,7 @@ interface Cas2BailApplicationRepository : JpaRepository { @Query("SELECT a FROM Cas2BailLockableApplicationEntity a WHERE a.id = :id") @Lock(LockModeType.PESSIMISTIC_WRITE) - fun acquirePessimisticLock(id: UUID): Cas2LockableApplicationEntity? + fun acquirePessimisticLock(id: UUID): Cas2BailLockableApplicationEntity? } @Entity @@ -76,14 +69,14 @@ data class Cas2BailApplicationEntity( var abandonedAt: OffsetDateTime? = null, @OneToMany(mappedBy = "application") - @OrderBy(clause = "createdAt DESC") - var statusUpdates: MutableList? = null, + @OrderBy("createdAt DESC") + var statusUpdates: MutableList? = null, @OneToMany(mappedBy = "application") - @OrderBy(clause = "createdAt DESC") - var notes: MutableList? = null, + @OrderBy("createdAt DESC") + var notes: MutableList? = null, - @OneToOne(mappedBy = "application") + @OneToOne(fetch = FetchType.LAZY) var assessment: Cas2BailAssessmentEntity? = null, @Transient diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailAssessmentEntity.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailAssessmentEntity.kt index b6df838320..d934cb5e0c 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailAssessmentEntity.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailAssessmentEntity.kt @@ -1,12 +1,7 @@ package uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail -import jakarta.persistence.Entity -import jakarta.persistence.Id -import jakarta.persistence.JoinColumn -import jakarta.persistence.ManyToOne -import jakarta.persistence.OneToMany -import jakarta.persistence.Table -import jakarta.persistence.OrderBy +import jakarta.persistence.* +import org.springframework.context.annotation.Lazy import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository @@ -26,8 +21,8 @@ data class Cas2BailAssessmentEntity( @Id val id: UUID, - @ManyToOne - @JoinColumn(name = "application_id") + @OneToOne + @Lazy val application: Cas2BailApplicationEntity, val createdAt: OffsetDateTime, @@ -38,7 +33,7 @@ data class Cas2BailAssessmentEntity( @OneToMany(mappedBy = "assessment") @OrderBy("createdAt DESC") - var statusUpdates: MutableList? = null, + var statusUpdates: MutableList? = null, ) { - override fun toString() = "Cas2AssessmentEntity: $id" + override fun toString() = "Cas2BailAssessmentEntity: $id" } \ No newline at end of file diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailStatusUpdateDetailEntry.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailStatusUpdateDetailEntry.kt new file mode 100644 index 0000000000..032abe277c --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailStatusUpdateDetailEntry.kt @@ -0,0 +1,40 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail + +import jakarta.persistence.* +import org.hibernate.annotations.CreationTimestamp +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository +import java.time.OffsetDateTime + +import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.reference.Cas2PersistedApplicationStatusDetail +import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.reference.Cas2PersistedApplicationStatusFinder +import java.util.UUID + +@Repository +interface Cas2BailStatusUpdateDetailRepository : JpaRepository { + fun findFirstByStatusUpdateIdOrderByCreatedAtDesc(statusUpdateId: UUID): Cas2BailStatusUpdateDetailEntity? +} + +@Entity +@Table(name = "cas_2_bail_status_update_details") +data class Cas2BailStatusUpdateDetailEntity( + @Id + val id: UUID, + val statusDetailId: UUID, + + val label: String, + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "status_update_id") + val statusUpdate: Cas2BailStatusUpdateEntity, + + @CreationTimestamp + var createdAt: OffsetDateTime? = null, +) { + override fun toString() = "Cas2BailStatusDetailEntity: $id" + + fun statusDetail(statusId: UUID, detailId: UUID): Cas2PersistedApplicationStatusDetail { + return Cas2PersistedApplicationStatusFinder().getById(statusId).statusDetails?.find { detail -> detail.id == detailId } + ?: error("Status detail with id $detailId not found") + } +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailStatusUpdateEntity.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailStatusUpdateEntity.kt new file mode 100644 index 0000000000..41ce37271d --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailStatusUpdateEntity.kt @@ -0,0 +1,61 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail + +import jakarta.persistence.* +import org.hibernate.annotations.CreationTimestamp +import org.springframework.data.domain.Pageable +import org.springframework.data.domain.Slice +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query +import org.springframework.stereotype.Repository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.reference.Cas2PersistedApplicationStatus +import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.reference.Cas2PersistedApplicationStatusFinder +import java.time.OffsetDateTime +import java.util.* + + +@Repository +interface Cas2BailStatusUpdateRepository : JpaRepository { + fun findFirstByApplicationIdOrderByCreatedAtDesc(applicationId: UUID): Cas2BailStatusUpdateEntity? + + @Query( + "SELECT su FROM Cas2BailStatusUpdateEntity su WHERE su.assessment IS NULL", + ) + fun findAllStatusUpdatesWithoutAssessment(pageable: Pageable?): Slice +} + +@Entity +@Table(name = "cas_2_bail_status_updates") +data class Cas2BailStatusUpdateEntity( + + @Id + val id: UUID, + + val statusId: UUID, + val description: String, + val label: String, + + @ManyToOne + @JoinColumn(name = "assessor_id") + val assessor: ExternalUserEntity, + + @ManyToOne + @JoinColumn(name = "application_id") + val application: Cas2BailApplicationEntity, + + @ManyToOne + @JoinColumn(name = "assessment_id") + var assessment: Cas2BailAssessmentEntity? = null, + + @OneToMany(mappedBy = "statusUpdate") + val statusUpdateDetails: List? = null, + + @CreationTimestamp + var createdAt: OffsetDateTime, +) { + override fun toString() = "Cas2BailStatusEntity: $id" + + fun status(): Cas2PersistedApplicationStatus { + return Cas2PersistedApplicationStatusFinder().getById(statusId) + } +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailAssessmentsTransformer.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailAssessmentsTransformer.kt index f4e561f228..dd6efe0b2b 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailAssessmentsTransformer.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailAssessmentsTransformer.kt @@ -7,7 +7,9 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2 import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas2.StatusUpdateTransformer @Component("Cas2BailAssessmentsTransformer") -class Cas2BailAssessmentsTransformer (private val statusUpdateTransformer: StatusUpdateTransformer) { +class Cas2BailAssessmentsTransformer ( + private val statusUpdateTransformer: Cas2BailStatusUpdateTransformer +) { fun transformJpaToApiRepresentation( jpaAssessment: Cas2BailAssessmentEntity, ): Cas2Assessment { diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailStatusUpdateTransformer.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailStatusUpdateTransformer.kt index 4c4d804e09..12e79e36ed 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailStatusUpdateTransformer.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailStatusUpdateTransformer.kt @@ -8,6 +8,8 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2Applicati import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2StatusUpdateDetailEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2StatusUpdateEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationSummaryEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailStatusUpdateDetailEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailStatusUpdateEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.ExternalUserTransformer import java.util.* @@ -17,7 +19,7 @@ class Cas2BailStatusUpdateTransformer( ) { fun transformJpaToApi( - jpa: Cas2StatusUpdateEntity, + jpa: Cas2BailStatusUpdateEntity, ): Cas2StatusUpdate { return Cas2StatusUpdate( id = jpa.id, @@ -30,7 +32,7 @@ class Cas2BailStatusUpdateTransformer( ) } - fun transformStatusUpdateDetailsJpaToApi(jpa: Cas2StatusUpdateDetailEntity): Cas2StatusUpdateDetail { + fun transformStatusUpdateDetailsJpaToApi(jpa: Cas2BailStatusUpdateDetailEntity): Cas2StatusUpdateDetail { return Cas2StatusUpdateDetail( id = jpa.id, name = jpa.statusDetail(jpa.statusUpdate.statusId, jpa.statusDetailId).name, From 4291eef082914612818807b5d9d00bb443ffa73e Mon Sep 17 00:00:00 2001 From: Toby Batch Date: Thu, 28 Nov 2024 16:57:52 +0000 Subject: [PATCH 04/37] feat: added migrations --- ...roller_and_linked_services_in_cas2bail.sql | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 src/main/resources/db/migration/all/20241128163133__application_controller_and_linked_services_in_cas2bail.sql diff --git a/src/main/resources/db/migration/all/20241128163133__application_controller_and_linked_services_in_cas2bail.sql b/src/main/resources/db/migration/all/20241128163133__application_controller_and_linked_services_in_cas2bail.sql new file mode 100644 index 0000000000..75dfb4fb0f --- /dev/null +++ b/src/main/resources/db/migration/all/20241128163133__application_controller_and_linked_services_in_cas2bail.sql @@ -0,0 +1,128 @@ + +CREATE TABLE cas_2_bail_application_json_schemas +( + json_schema_id UUID NOT NULL, + CONSTRAINT pk_cas_2_bail_application_json_schemas PRIMARY KEY (json_schema_id) +); + +CREATE TABLE cas_2_bail_application_live_summary +( + id UUID NOT NULL, + crn VARCHAR(255), + noms_number VARCHAR(255), + created_by_user_id VARCHAR(255), + name VARCHAR(255), + created_at TIMESTAMP WITHOUT TIME ZONE, + submitted_at TIMESTAMP WITHOUT TIME ZONE, + abandoned_at TIMESTAMP WITHOUT TIME ZONE, + hdc_eligibility_date date, + label VARCHAR(255), + status_id VARCHAR(255), + referring_prison_code VARCHAR(255), + CONSTRAINT pk_cas_2_bail_application_live_summary PRIMARY KEY (id) +); + +CREATE TABLE cas_2_bail_application_notes +( + id UUID NOT NULL, + application_id UUID, + created_at TIMESTAMP WITHOUT TIME ZONE, + body VARCHAR(255), + assessment_id UUID, + created_by_nomis_user_id UUID, + created_by_external_user_id UUID, + CONSTRAINT pk_cas_2_bail_application_notes PRIMARY KEY (id) +); + +CREATE TABLE cas_2_bail_applications +( + id UUID NOT NULL, + crn VARCHAR(255), + created_by_user_id UUID, + data VARCHAR(255), + document VARCHAR(255), + schema_version UUID, + created_at TIMESTAMP WITHOUT TIME ZONE, + submitted_at TIMESTAMP WITHOUT TIME ZONE, + abandoned_at TIMESTAMP WITHOUT TIME ZONE, + assessment_id UUID, + noms_number VARCHAR(255), + referring_prison_code VARCHAR(255), + preferred_areas VARCHAR(255), + hdc_eligibility_date date, + conditional_release_date date, + telephone_number VARCHAR(255), + CONSTRAINT pk_cas_2_bail_applications PRIMARY KEY (id) +); + +CREATE TABLE cas_2_bail_assessments +( + id UUID NOT NULL, + application_id UUID, + created_at TIMESTAMP WITHOUT TIME ZONE, + nacro_referral_id VARCHAR(255), + assessor_name VARCHAR(255), + CONSTRAINT pk_cas_2_bail_assessments PRIMARY KEY (id) +); + +CREATE TABLE cas_2_bail_status_update_details +( + id UUID NOT NULL, + status_detail_id UUID, + label VARCHAR(255), + status_update_id UUID, + created_at TIMESTAMP WITHOUT TIME ZONE, + CONSTRAINT pk_cas_2_bail_status_update_details PRIMARY KEY (id) +); + +CREATE TABLE cas_2_bail_status_updates +( + id UUID NOT NULL, + status_id UUID, + description VARCHAR(255), + label VARCHAR(255), + assessor_id UUID, + application_id UUID, + assessment_id UUID, + created_at TIMESTAMP WITHOUT TIME ZONE, + CONSTRAINT pk_cas_2_bail_status_updates PRIMARY KEY (id) +); + +ALTER TABLE cas_2_bail_applications + ADD CONSTRAINT FK_CAS_2_BAIL_APPLICATIONS_ON_ASSESSMENT FOREIGN KEY (assessment_id) REFERENCES cas_2_bail_assessments (id); + +ALTER TABLE cas_2_bail_applications + ADD CONSTRAINT FK_CAS_2_BAIL_APPLICATIONS_ON_CREATED_BY_USER FOREIGN KEY (created_by_user_id) REFERENCES nomis_users (id); + +ALTER TABLE cas_2_bail_applications + ADD CONSTRAINT FK_CAS_2_BAIL_APPLICATIONS_ON_SCHEMA_VERSION FOREIGN KEY (schema_version) REFERENCES json_schemas (id); + +ALTER TABLE cas_2_bail_application_json_schemas + ADD CONSTRAINT FK_CAS_2_BAIL_APPLICATION_JSON_SCHEMAS_ON_JSON_SCHEMA FOREIGN KEY (json_schema_id) REFERENCES json_schemas (id); + +ALTER TABLE cas_2_bail_application_notes + ADD CONSTRAINT FK_CAS_2_BAIL_APPLICATION_NOTES_ON_APPLICATION FOREIGN KEY (application_id) REFERENCES cas_2_bail_applications (id); + +ALTER TABLE cas_2_bail_application_notes + ADD CONSTRAINT FK_CAS_2_BAIL_APPLICATION_NOTES_ON_ASSESSMENT FOREIGN KEY (assessment_id) REFERENCES cas_2_bail_assessments (id); + +ALTER TABLE cas_2_bail_application_notes + ADD CONSTRAINT FK_CAS_2_BAIL_APPLICATION_NOTES_ON_CREATED_BY_EXTERNAL_USER FOREIGN KEY (created_by_external_user_id) REFERENCES external_users (id); + +ALTER TABLE cas_2_bail_application_notes + ADD CONSTRAINT FK_CAS_2_BAIL_APPLICATION_NOTES_ON_CREATED_BY_NOMIS_USER FOREIGN KEY (created_by_nomis_user_id) REFERENCES nomis_users (id); + +ALTER TABLE cas_2_bail_assessments + ADD CONSTRAINT FK_CAS_2_BAIL_ASSESSMENTS_ON_APPLICATION FOREIGN KEY (application_id) REFERENCES cas_2_bail_applications (id); + +ALTER TABLE cas_2_bail_status_updates + ADD CONSTRAINT FK_CAS_2_BAIL_STATUS_UPDATES_ON_APPLICATION FOREIGN KEY (application_id) REFERENCES cas_2_bail_applications (id); + +ALTER TABLE cas_2_bail_status_updates + ADD CONSTRAINT FK_CAS_2_BAIL_STATUS_UPDATES_ON_ASSESSMENT FOREIGN KEY (assessment_id) REFERENCES cas_2_bail_assessments (id); + +ALTER TABLE cas_2_bail_status_updates + ADD CONSTRAINT FK_CAS_2_BAIL_STATUS_UPDATES_ON_ASSESSOR FOREIGN KEY (assessor_id) REFERENCES external_users (id); + +ALTER TABLE cas_2_bail_status_update_details + ADD CONSTRAINT FK_CAS_2_BAIL_STATUS_UPDATE_DETAILS_ON_STATUS_UPDATE FOREIGN KEY (status_update_id) REFERENCES cas_2_bail_status_updates (id); From 387da5630d5208ab24b82248dd4bca63c37d45ad Mon Sep 17 00:00:00 2001 From: garethCAS2 Date: Fri, 29 Nov 2024 13:25:46 +0000 Subject: [PATCH 05/37] started working through the tests --- ...uth2ResourceServerSecurityConfiguration.kt | 11 + ...2BailApplicationJsonSchemaEntityFactory.kt | 47 + .../Cas2BailApplicationEntityFactory.kt | 154 ++ .../Cas2BailStatusUpdateEntityFactory.kt | 81 + .../integration/IntegrationTestBase.kt | 237 +-- .../cas2bail/Cas2BailApplicationTest.kt | 1699 +++++++++++++++++ .../repository/ApplicationTestRepository.kt | 4 + .../Cas2BailStatusUpdateTestRepository.kt | 10 + .../repository/JsonSchemaTestRepository.kt | 10 +- 9 files changed, 2033 insertions(+), 220 deletions(-) create mode 100644 src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/Cas2BailApplicationJsonSchemaEntityFactory.kt create mode 100644 src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/cas2bail/Cas2BailApplicationEntityFactory.kt create mode 100644 src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/cas2bail/Cas2BailStatusUpdateEntityFactory.kt create mode 100644 src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationTest.kt create mode 100644 src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/repository/Cas2BailStatusUpdateTestRepository.kt diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/config/OAuth2ResourceServerSecurityConfiguration.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/config/OAuth2ResourceServerSecurityConfiguration.kt index a32352601f..f8f5facd26 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/config/OAuth2ResourceServerSecurityConfiguration.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/config/OAuth2ResourceServerSecurityConfiguration.kt @@ -74,6 +74,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, "/cas2bail/assessments/**", hasRole("CAS2_ASSESSOR")) + authorize(HttpMethod.GET, "/cas2bail/assessments/**", hasAnyRole("CAS2_ASSESSOR", "CAS2_ADMIN")) + authorize(HttpMethod.POST, "/cas2bail/assessments/*/status-updates", hasRole("CAS2_ASSESSOR")) + authorize(HttpMethod.POST, "/cas2bail/assessments/*/notes", hasAnyRole("LICENCE_CA", "POM", "CAS2_ASSESSOR")) + authorize(HttpMethod.GET, "/cas2bail/submissions/**", hasAnyRole("CAS2_ASSESSOR", "CAS2_ADMIN")) + authorize(HttpMethod.POST, "/cas2bail/submissions/*/status-updates", hasRole("CAS2_ASSESSOR")) + authorize(HttpMethod.GET, "/cas2bail/reference-data/**", hasAnyRole("CAS2_ASSESSOR", "POM")) + authorize(HttpMethod.GET, "/cas2bail/reports/**", hasRole("CAS2_MI")) + authorize("/cas2bail/**", 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")) diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/Cas2BailApplicationJsonSchemaEntityFactory.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/Cas2BailApplicationJsonSchemaEntityFactory.kt new file mode 100644 index 0000000000..fb830f3b12 --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/Cas2BailApplicationJsonSchemaEntityFactory.kt @@ -0,0 +1,47 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.factory + +import io.github.bluegroundltd.kfactory.Factory +import io.github.bluegroundltd.kfactory.Yielded +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2BailApplicationJsonSchemaEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.randomDateTimeBefore +import java.time.OffsetDateTime +import java.util.UUID + +class Cas2BailApplicationJsonSchemaEntityFactory : Factory { + private var id: Yielded = { UUID.randomUUID() } + private var addedAt: Yielded = { OffsetDateTime.now().randomDateTimeBefore(7) } + private var schema: Yielded = { "{}" } + + fun withId(id: UUID) = apply { + this.id = { id } + } + + fun withAddedAt(addedAt: OffsetDateTime) = apply { + this.addedAt = { addedAt } + } + + fun withSchema(schema: String) = apply { + this.schema = { schema } + } + + fun withPermissiveSchema() = apply { + withSchema( + """ + { + "${"\$schema"}": "https://json-schema.org/draft/2020-12/schema", + "${"\$id"}": "https://example.com/product.schema.json", + "title": "Thing", + "description": "A thing", + "type": "object", + "properties": { } + } + """, + ) + } + + override fun produce(): Cas2BailApplicationJsonSchemaEntity = Cas2BailApplicationJsonSchemaEntity( + id = this.id(), + addedAt = this.addedAt(), + schema = this.schema(), + ) +} diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/cas2bail/Cas2BailApplicationEntityFactory.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/cas2bail/Cas2BailApplicationEntityFactory.kt new file mode 100644 index 0000000000..35c6869c03 --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/cas2bail/Cas2BailApplicationEntityFactory.kt @@ -0,0 +1,154 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.cas2bail + +import io.github.bluegroundltd.kfactory.Factory +import io.github.bluegroundltd.kfactory.Yielded +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ApprovedPremisesApplicationJsonSchemaEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationNoteEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2AssessmentEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2StatusUpdateEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.JsonSchemaEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.NomisUserEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationNoteEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailAssessmentEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailStatusUpdateEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.randomDateTimeBefore +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.randomInt +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.randomNumberChars +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.randomStringMultiCaseWithNumbers +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.randomStringUpperCase +import java.time.LocalDate +import java.time.OffsetDateTime +import java.util.UUID + + +class Cas2BailApplicationEntityFactory : Factory { + private var id: Yielded = { UUID.randomUUID() } + private var crn: Yielded = { randomStringMultiCaseWithNumbers(8) } + private var createdByUser: Yielded? = null + private var data: Yielded = { "{}" } + private var document: Yielded = { "{}" } + private var applicationSchema: Yielded = { + ApprovedPremisesApplicationJsonSchemaEntityFactory().produce() + } + private var createdAt: Yielded = { OffsetDateTime.now().randomDateTimeBefore(30) } + private var submittedAt: Yielded = { null } + private var abandonedAt: Yielded = { null } + private var statusUpdates: Yielded> = { mutableListOf() } + private var eventNumber: Yielded = { randomInt(1, 9).toString() } + private var nomsNumber: Yielded = { randomStringUpperCase(6) } + private var telephoneNumber: Yielded = { randomNumberChars(12) } + private var notes: Yielded> = { mutableListOf() } + private var assessment: Yielded = { null } + private var referringPrisonCode: Yielded = { null } + private var preferredAreas: Yielded = { null } + private var hdcEligibilityDate: Yielded = { null } + private var conditionalReleaseDate: Yielded = { null } + + fun withId(id: UUID) = apply { + this.id = { id } + } + + fun withCrn(crn: String) = apply { + this.crn = { crn } + } + + fun withNomsNumber(nomsNumber: String) = apply { + this.nomsNumber = { nomsNumber } + } + + fun withCreatedByUser(createdByUser: NomisUserEntity) = apply { + this.createdByUser = { createdByUser } + } + + fun withYieldedCreatedByUser(createdByUser: Yielded) = apply { + this.createdByUser = createdByUser + } + + fun withData(data: String?) = apply { + this.data = { data } + } + + fun withDocument(document: String?) = apply { + this.document = { document } + } + + fun withApplicationSchema(applicationSchema: JsonSchemaEntity) = apply { + this.applicationSchema = { applicationSchema } + } + + fun withYieldedApplicationSchema(applicationSchema: Yielded) = apply { + this.applicationSchema = applicationSchema + } + + fun withCreatedAt(createdAt: OffsetDateTime) = apply { + this.createdAt = { createdAt } + } + + fun withSubmittedAt(submittedAt: OffsetDateTime?) = apply { + this.submittedAt = { submittedAt } + } + + fun withAbandonedAt(abandonedAt: OffsetDateTime?) = apply { + this.abandonedAt = { abandonedAt } + } + + fun withStatusUpdates(statusUpdates: MutableList) = apply { + this.statusUpdates = { statusUpdates } + } + + fun withNotes(notes: MutableList) = apply { + this.notes = { notes } + } + + fun withEventNumber(eventNumber: String) = apply { + this.eventNumber = { eventNumber } + } + + fun withAssessment(assessmentEntity: Cas2BailAssessmentEntity) = apply { + this.assessment = { assessmentEntity } + } + + fun withReferringPrisonCode(referringPrisonCode: String) = apply { + this.referringPrisonCode = { referringPrisonCode } + } + + fun withPreferredAreas(preferredAreas: String) = apply { + this.preferredAreas = { preferredAreas } + } + + fun withTelephoneNumber(telephoneNumber: String) = apply { + this.telephoneNumber = { telephoneNumber } + } + + fun withHdcEligibilityDate(hdcEligibilityDate: LocalDate) = apply { + this.hdcEligibilityDate = { hdcEligibilityDate } + } + + fun withConditionalReleaseDate(conditionalReleaseDate: LocalDate) = apply { + this.conditionalReleaseDate = { conditionalReleaseDate } + } + + override fun produce(): Cas2BailApplicationEntity = Cas2BailApplicationEntity( + id = this.id(), + crn = this.crn(), + createdByUser = this.createdByUser?.invoke() ?: throw RuntimeException("Must provide a createdByUser"), + data = this.data(), + document = this.document(), + schemaVersion = this.applicationSchema(), + createdAt = this.createdAt(), + submittedAt = this.submittedAt(), + abandonedAt = this.abandonedAt(), + statusUpdates = this.statusUpdates(), + schemaUpToDate = false, + nomsNumber = this.nomsNumber(), + telephoneNumber = this.telephoneNumber(), + notes = this.notes(), + assessment = this.assessment(), + referringPrisonCode = this.referringPrisonCode(), + hdcEligibilityDate = this.hdcEligibilityDate(), + conditionalReleaseDate = this.conditionalReleaseDate(), + preferredAreas = this.preferredAreas(), + ) +} \ No newline at end of file diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/cas2bail/Cas2BailStatusUpdateEntityFactory.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/cas2bail/Cas2BailStatusUpdateEntityFactory.kt new file mode 100644 index 0000000000..c48252a8ce --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/cas2bail/Cas2BailStatusUpdateEntityFactory.kt @@ -0,0 +1,81 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.cas2bail + +import io.github.bluegroundltd.kfactory.Factory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ExternalUserEntityFactory + +import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.reference.Cas2ApplicationStatusSeeding +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.randomDateTimeBefore +import java.time.OffsetDateTime + +import io.github.bluegroundltd.kfactory.Yielded +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2AssessmentEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2StatusUpdateDetailEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2StatusUpdateEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ExternalUserEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailAssessmentEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailStatusUpdateDetailEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailStatusUpdateEntity + +import java.util.UUID + +class Cas2BailStatusUpdateEntityFactory : Factory { + private var id: Yielded = { UUID.randomUUID() } + private var assessor: Yielded = { ExternalUserEntityFactory().produce() } + private var assessment: Yielded = { null } + private var application: Yielded? = null + private var statusId: Yielded = { Cas2ApplicationStatusSeeding.statusList().random().id } + private var createdAt: Yielded = { OffsetDateTime.now().randomDateTimeBefore(30) } + private var label: Yielded = { "More information requested" } + private var description: Yielded = { "More information about the application has been requested" } + private var statusUpdateDetails: Yielded?> = { null } + + fun withId(id: UUID) = apply { + this.id = { id } + } + + fun withAssessor(assessor: ExternalUserEntity) = apply { + this.assessor = { assessor } + } + + fun withAssessment(assessment: Cas2BailAssessmentEntity) = apply { + this.assessment = { assessment } + } + + fun withApplication(application: Cas2BailApplicationEntity) = apply { + this.application = { application } + } + + fun withStatusId(statusId: UUID) = apply { + this.statusId = { statusId } + } + + fun withCreatedAt(createdAt: OffsetDateTime) = apply { + this.createdAt = { createdAt } + } + + fun withLabel(label: String) = apply { + this.label = { label } + } + + fun withDescription(description: String) = apply { + this.description = { description } + } + + fun withStatusUpdateDetails(details: List) = apply { + this.statusUpdateDetails = { details } + } + + override fun produce(): Cas2BailStatusUpdateEntity = Cas2BailStatusUpdateEntity( + id = this.id(), + assessor = this.assessor(), + application = this.application?.invoke() ?: error("Must provide a submitted application"), + assessment = this.assessment(), + statusId = this.statusId(), + createdAt = this.createdAt(), + label = this.label(), + description = this.description(), + statusUpdateDetails = this.statusUpdateDetails(), + ) +} diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/IntegrationTestBase.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/IntegrationTestBase.kt index a9343ce664..e7c8b9fb85 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/IntegrationTestBase.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/IntegrationTestBase.kt @@ -30,77 +30,10 @@ import org.springframework.test.context.ContextConfiguration import org.springframework.test.web.reactive.server.WebTestClient import uk.gov.justice.digital.hmpps.approvedpremisesapi.client.PrisonsApiClient import uk.gov.justice.digital.hmpps.approvedpremisesapi.config.NotifyConfig -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ApAreaEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.AppealEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ApplicationTeamCodeEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ApplicationTimelineNoteEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ApprovedPremisesApplicationEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ApprovedPremisesApplicationJsonSchemaEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ApprovedPremisesAssessmentEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ApprovedPremisesAssessmentJsonSchemaEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ApprovedPremisesEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ApprovedPremisesPlacementApplicationJsonSchemaEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ArrivalEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.AssessmentClarificationNoteEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.AssessmentReferralHistorySystemNoteEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.AssessmentReferralHistoryUserNoteEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.BedEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.BedMoveEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.BookingEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.BookingNotMadeEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.CancellationEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.CancellationReasonEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.Cas1ApplicationUserDetailsEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.Cas1OutOfServiceBedCancellationEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.Cas1OutOfServiceBedEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.Cas1OutOfServiceBedReasonEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.Cas1OutOfServiceBedRevisionEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.Cas1SpaceBookingEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.Cas2ApplicationEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.Cas2ApplicationJsonSchemaEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.Cas2AssessmentEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.Cas2NoteEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.Cas2StatusUpdateDetailEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.Cas2StatusUpdateEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.CharacteristicEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ConfirmationEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.DateChangeEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.DepartureEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.DepartureReasonEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.DestinationProviderEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.DomainEventEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ExtensionEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ExternalUserEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.LocalAuthorityEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.LostBedCancellationEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.LostBedReasonEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.LostBedsEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.MoveOnCategoryEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.NomisUserEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.NonArrivalEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.NonArrivalReasonEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.OfflineApplicationEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.PersistedFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.PlacementApplicationEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.PlacementDateEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.PlacementRequestEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.PlacementRequirementsEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.PostCodeDistrictEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ProbationAreaProbationRegionMappingEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ProbationDeliveryUnitEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ProbationRegionEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ReferralRejectionReasonEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.RoomEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.TemporaryAccommodationApplicationEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.TemporaryAccommodationApplicationJsonSchemaEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.TemporaryAccommodationAssessmentEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.TemporaryAccommodationAssessmentJsonSchemaEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.TemporaryAccommodationPremisesEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.TurnaroundEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.UserEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.UserQualificationAssignmentEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.UserRoleAssignmentEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.* import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.cas1.Cas1CruManagementAreaEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.cas2bail.Cas2BailApplicationEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.cas2bail.Cas2BailStatusUpdateEntityFactory import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.asserter.DomainEventAsserter import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.asserter.EmailNotificationAsserter import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.config.IntegrationTestDbManager @@ -108,156 +41,16 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.config.TestP import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.mocks.MockFeatureFlagService import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.mocks.MutableClockConfiguration import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.mocks.NoOpSentryService -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApAreaEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.AppealEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApplicationTeamCodeEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApplicationTimelineNoteEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApplicationTimelineNoteRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApprovedPremisesApplicationEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApprovedPremisesApplicationJsonSchemaEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApprovedPremisesAssessmentEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApprovedPremisesAssessmentJsonSchemaEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApprovedPremisesEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApprovedPremisesPlacementApplicationJsonSchemaEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ArrivalEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.AssessmentClarificationNoteEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.AssessmentReferralHistorySystemNoteEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.AssessmentReferralHistoryUserNoteEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.BedEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.BedMoveEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.BedMoveRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.BedRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.BookingEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.BookingNotMadeEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.CancellationEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.CancellationReasonEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas1ApplicationUserDetailsEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas1ApplicationUserDetailsRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas1CruManagementAreaEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas1CruManagementAreaRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas1OutOfServiceBedCancellationEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas1OutOfServiceBedEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas1OutOfServiceBedReasonEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas1OutOfServiceBedRevisionEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas1SpaceBookingEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas1SpaceBookingRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationJsonSchemaEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationNoteEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationNoteRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2AssessmentEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2AssessmentRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2StatusUpdateDetailEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2StatusUpdateDetailRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2StatusUpdateEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.CharacteristicEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.CharacteristicRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ConfirmationEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.DateChangeEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.DateChangeRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.DepartureEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.DepartureReasonEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.DestinationProviderEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.DomainEventEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ExtensionEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ExternalUserEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.LocalAuthorityAreaEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.LostBedCancellationEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.LostBedReasonEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.LostBedsEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.MoveOnCategoryEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.NomisUserEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.NonArrivalEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.NonArrivalReasonEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.OfflineApplicationEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.PlacementApplicationEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.PlacementApplicationRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.PlacementDateEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.PlacementDateRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.PlacementRequestEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.PlacementRequestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.PlacementRequirementsEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.PlacementRequirementsRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.PostCodeDistrictEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ProbationAreaProbationRegionMappingEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ProbationDeliveryUnitEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ProbationRegionEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ReferralRejectionReasonEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ReferralRejectionReasonRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.RoomEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.RoomRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.TemporaryAccommodationApplicationEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.TemporaryAccommodationApplicationJsonSchemaEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.TemporaryAccommodationAssessmentEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.TemporaryAccommodationAssessmentJsonSchemaEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.TemporaryAccommodationPremisesEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.TurnaroundEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.UserEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.UserQualificationAssignmentEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.UserRoleAssignmentEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailStatusUpdateEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.community.OffenderDetailSummary import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.community.UserOffenderAccess import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.deliuscontext.StaffMember import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.deliuscontext.StaffMembersPage import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.hmppsauth.GetTokenResponse import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.prisonsapi.InmateDetail -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.ApAreaTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.AppealTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.ApplicationTeamCodeTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.ApprovedPremisesApplicationJsonSchemaTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.ApprovedPremisesApplicationTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.ApprovedPremisesAssessmentJsonSchemaTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.ApprovedPremisesAssessmentTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.ApprovedPremisesPlacementApplicationJsonSchemaTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.ApprovedPremisesTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.ArrivalTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.AssessmentClarificationNoteTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.AssessmentReferralHistorySystemNoteTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.AssessmentReferralHistoryUserNoteTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.AssessmentTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.BookingNotMadeTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.BookingTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.CancellationReasonTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.CancellationTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.Cas1OutOfServiceBedCancellationTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.Cas1OutOfServiceBedDetailsTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.Cas1OutOfServiceBedReasonTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.Cas1OutOfServiceBedTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.Cas2ApplicationJsonSchemaTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.Cas2ApplicationTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.Cas2StatusUpdateTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.ConfirmationTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.DepartureReasonTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.DepartureTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.DestinationProviderTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.DomainEventTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.ExtensionTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.ExternalUserTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.LocalAuthorityAreaTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.LostBedCancellationTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.LostBedReasonTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.LostBedsTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.MoveOnCategoryTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.NomisUserTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.NonArrivalReasonTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.NonArrivalTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.OfflineApplicationTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.PlacementApplicationTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.PlacementRequestTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.PostCodeDistrictTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.ProbationAreaProbationRegionMappingTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.ProbationDeliveryUnitTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.ProbationRegionTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.RoomTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.TemporaryAccommodationApplicationJsonSchemaTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.TemporaryAccommodationApplicationTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.TemporaryAccommodationAssessmentJsonSchemaTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.TemporaryAccommodationAssessmentTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.TemporaryAccommodationPremisesTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.TurnaroundTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.UserQualificationAssignmentTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.UserRoleAssignmentTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.UserTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.* import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.JwtAuthHelper import java.time.Duration import java.util.TimeZone @@ -380,12 +173,18 @@ abstract class IntegrationTestBase { @Autowired lateinit var cas2ApplicationRepository: Cas2ApplicationTestRepository + @Autowired + lateinit var cas2BailApplicationRepository: Cas2BailApplicationTestRepository + @Autowired lateinit var cas2AssessmentRepository: Cas2AssessmentRepository @Autowired lateinit var cas2StatusUpdateRepository: Cas2StatusUpdateTestRepository + @Autowired + lateinit var cas2BailStatusUpdateRepository: Cas2BailStatusUpdateTestRepository + @Autowired lateinit var cas2NoteRepository: Cas2ApplicationNoteRepository @@ -401,6 +200,9 @@ abstract class IntegrationTestBase { @Autowired lateinit var cas2ApplicationJsonSchemaRepository: Cas2ApplicationJsonSchemaTestRepository + @Autowired + lateinit var cas2BailApplicationJsonSchemaRepository: Cas2BailApplicationJsonSchemaTestRepository + @Autowired lateinit var temporaryAccommodationApplicationJsonSchemaRepository: TemporaryAccommodationApplicationJsonSchemaTestRepository @@ -568,14 +370,17 @@ abstract class IntegrationTestBase { lateinit var nonArrivalReasonEntityFactory: PersistedFactory lateinit var approvedPremisesApplicationEntityFactory: PersistedFactory lateinit var cas2ApplicationEntityFactory: PersistedFactory + lateinit var cas2BailApplicationEntityFactory: PersistedFactory lateinit var cas2AssessmentEntityFactory: PersistedFactory lateinit var cas2StatusUpdateEntityFactory: PersistedFactory + lateinit var cas2BailStatusUpdateEntityFactory: PersistedFactory lateinit var cas2StatusUpdateDetailEntityFactory: PersistedFactory lateinit var cas2NoteEntityFactory: PersistedFactory lateinit var temporaryAccommodationApplicationEntityFactory: PersistedFactory lateinit var offlineApplicationEntityFactory: PersistedFactory lateinit var approvedPremisesApplicationJsonSchemaEntityFactory: PersistedFactory lateinit var cas2ApplicationJsonSchemaEntityFactory: PersistedFactory + lateinit var cas2BailApplicationJsonSchemaEntityFactory: PersistedFactory lateinit var temporaryAccommodationApplicationJsonSchemaEntityFactory: PersistedFactory lateinit var approvedPremisesPlacementApplicationJsonSchemaEntityFactory: PersistedFactory lateinit var approvedPremisesAssessmentJsonSchemaEntityFactory: PersistedFactory @@ -679,14 +484,18 @@ abstract class IntegrationTestBase { nonArrivalReasonEntityFactory = PersistedFactory({ NonArrivalReasonEntityFactory() }, nonArrivalReasonRepository) approvedPremisesApplicationEntityFactory = PersistedFactory({ ApprovedPremisesApplicationEntityFactory() }, approvedPremisesApplicationRepository) cas2ApplicationEntityFactory = PersistedFactory({ Cas2ApplicationEntityFactory() }, cas2ApplicationRepository) + + cas2BailApplicationEntityFactory = PersistedFactory({ Cas2BailApplicationEntityFactory() }, cas2BailApplicationRepository) cas2AssessmentEntityFactory = PersistedFactory({ Cas2AssessmentEntityFactory() }, cas2AssessmentRepository) cas2StatusUpdateEntityFactory = PersistedFactory({ Cas2StatusUpdateEntityFactory() }, cas2StatusUpdateRepository) + cas2BailStatusUpdateEntityFactory = PersistedFactory({ Cas2BailStatusUpdateEntityFactory() }, cas2BailStatusUpdateRepository) cas2StatusUpdateDetailEntityFactory = PersistedFactory({ Cas2StatusUpdateDetailEntityFactory() }, cas2StatusUpdateDetailRepository) cas2NoteEntityFactory = PersistedFactory({ Cas2NoteEntityFactory() }, cas2NoteRepository) temporaryAccommodationApplicationEntityFactory = PersistedFactory({ TemporaryAccommodationApplicationEntityFactory() }, temporaryAccommodationApplicationRepository) offlineApplicationEntityFactory = PersistedFactory({ OfflineApplicationEntityFactory() }, offlineApplicationRepository) approvedPremisesApplicationJsonSchemaEntityFactory = PersistedFactory({ ApprovedPremisesApplicationJsonSchemaEntityFactory() }, approvedPremisesApplicationJsonSchemaRepository) cas2ApplicationJsonSchemaEntityFactory = PersistedFactory({ Cas2ApplicationJsonSchemaEntityFactory() }, cas2ApplicationJsonSchemaRepository) + cas2BailApplicationJsonSchemaEntityFactory = PersistedFactory({ Cas2BailApplicationJsonSchemaEntityFactory() }, cas2BailApplicationJsonSchemaRepository) temporaryAccommodationApplicationJsonSchemaEntityFactory = PersistedFactory({ TemporaryAccommodationApplicationJsonSchemaEntityFactory() }, temporaryAccommodationApplicationJsonSchemaRepository) approvedPremisesAssessmentJsonSchemaEntityFactory = PersistedFactory({ ApprovedPremisesAssessmentJsonSchemaEntityFactory() }, approvedPremisesAssessmentJsonSchemaRepository) temporaryAccommodationAssessmentJsonSchemaEntityFactory = PersistedFactory({ TemporaryAccommodationAssessmentJsonSchemaEntityFactory() }, temporaryAccommodationAssessmentJsonSchemaRepository) diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationTest.kt new file mode 100644 index 0000000000..05b7aa1344 --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationTest.kt @@ -0,0 +1,1699 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.cas2bail + + + +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.node.NullNode +import com.ninjasquad.springmockk.SpykBean +import io.mockk.clearMocks +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import org.springframework.test.web.reactive.server.returnResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.IntegrationTestBase +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenACas2Assessor +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenACas2LicenceCaseAdminUser +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenACas2PomUser +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenAnOffender +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.httpmocks.communityAPIMockNotFoundOffenderDetailsCall +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.httpmocks.prisonAPIMockNotFoundInmateDetailsCall +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailStatusUpdateEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.community.OffenderDetailSummary +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.randomDateAfter +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.randomDateBefore +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.randomDateTimeBefore +import java.time.LocalDate +import java.time.OffsetDateTime +import java.util.* +import kotlin.math.sign + +class Cas2BailApplicationTest : IntegrationTestBase() { + + @SpykBean + lateinit var realApplicationRepository: ApplicationRepository + + val schema = """ + { + "${"\$schema"}": "https://json-schema.org/draft/2020-12/schema", + "${"\$id"}": "https://example.com/product.schema.json", + "title": "Thing", + "description": "A thing", + "type": "object", + "properties": { + "thingId": { + "description": "The unique identifier for a thing", + "type": "integer" + } + }, + "required": [ "thingId" ] + } + """ + + val data = """ + { + "thingId": 123 + } + """ + + @AfterEach + fun afterEach() { + // SpringMockK does not correctly clear mocks for @SpyKBeans that are also a @Repository, causing mocked behaviour + // in one test to show up in another (see https://github.com/Ninja-Squad/springmockk/issues/85) + // Manually clearing after each test seems to fix this. + clearMocks(realApplicationRepository) + } + + + @Nested + inner class ControlsOnExternalUsers { + @ParameterizedTest + @ValueSource(strings = ["ROLE_CAS2_ASSESSOR", "ROLE_CAS2_MI"]) + fun `creating an application is forbidden to external users based on role`(role: String) { + val jwt = jwtAuthHelper.createClientCredentialsJwt( + username = "username", + authSource = "nomis", + roles = listOf(role), + ) + + webTestClient.post() + .uri("/cas2bail/applications") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isForbidden + } + + @ParameterizedTest + @ValueSource(strings = ["ROLE_CAS2_ASSESSOR", "ROLE_CAS2_MI"]) + fun `updating an application is forbidden to external users based on role`(role: String) { + val jwt = jwtAuthHelper.createClientCredentialsJwt( + username = "username", + authSource = "nomis", + roles = listOf(role), + ) + + webTestClient.put() + .uri("/cas2bail/applications/66911cf0-75b1-4361-84bd-501b176fd4fd") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isForbidden + } + + @Test + fun `viewing list of applications is forbidden to external users based on role`() { + val jwt = jwtAuthHelper.createClientCredentialsJwt( + username = "username", + authSource = "nomis", + roles = listOf("ROLE_CAS2_ASSESSOR"), + ) + + webTestClient.get() + .uri("/cas2bail/applications") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isForbidden + } + + @Test + fun `viewing an application is forbidden to external users based on role`() { + val jwt = jwtAuthHelper.createClientCredentialsJwt( + username = "username", + authSource = "nomis", + roles = listOf("ROLE_CAS2_ASSESSOR"), + ) + + webTestClient.get() + .uri("/cas2bail/applications/66911cf0-75b1-4361-84bd-501b176fd4") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isForbidden + } + } + + + @Nested + inner class MissingJwt { + @Test + fun `Get all applications without JWT returns 401`() { + webTestClient.get() + .uri("/cas2bail/applications") + .exchange() + .expectStatus() + .isUnauthorized + } + + @Test + fun `Get single application without JWT returns 401`() { + webTestClient.get() + .uri("/cas2bail/applications/9b785e59-b85c-4be0-b271-d9ac287684b6") + .exchange() + .expectStatus() + .isUnauthorized + } + + @Test + fun `Create new application without JWT returns 401`() { + webTestClient.post() + .uri("/cas2bail/applications") + .exchange() + .expectStatus() + .isUnauthorized + } + } + + @Nested + inner class GetToIndex { + + @Test + fun `return unexpired applications when applications GET is requested`() { + val unexpiredSubset = setOf( + Pair("More information requested", UUID.fromString("f5cd423b-08eb-4efb-96ff-5cc6bb073905")), + Pair("Awaiting decision", UUID.fromString("ba4d8432-250b-4ab9-81ec-7eb4b16e5dd1")), + Pair("On waiting list", UUID.fromString("a919097d-b324-471c-9834-756f255e87ea")), + Pair("Place offered", UUID.fromString("176bbda0-0766-4d77-8d56-18ed8f9a4ef2")), + Pair("Offer accepted", UUID.fromString("fe254d88-ce1d-4cd8-8bd6-88de88f39019")), + Pair("Could not be placed", UUID.fromString("758eee61-2a6d-46b9-8bdd-869536d77f1b")), + Pair("Incomplete", UUID.fromString("4ad9bbfa-e5b0-456f-b746-146f7fd511dd")), + Pair("Offer declined or withdrawn", UUID.fromString("9a381bc6-22d3-41d6-804d-4e49f428c1de")), + ) + + val expiredSubset = setOf( + Pair("Referral withdrawn", UUID.fromString("004e2419-9614-4c1e-a207-a8418009f23d")), + Pair("Referral cancelled", UUID.fromString("f13bbdd6-44f1-4362-b9d3-e6f1298b1bf9")), + Pair("Awaiting arrival", UUID.fromString("89458555-3219-44a2-9584-c4f715d6b565")), + ) + + val applicationSchema = cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + withAddedAt(OffsetDateTime.now()) + withId(UUID.randomUUID()) + } + + fun createApplication(userEntity: NomisUserEntity, offenderDetails: OffenderDetailSummary): Cas2BailApplicationEntity { + return cas2BailApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(userEntity) + withCrn(offenderDetails.otherIds.crn) + withCreatedAt(OffsetDateTime.now().minusDays(28)) + withConditionalReleaseDate(LocalDate.now().plusDays(1)) + } + } + + fun createStatusUpdate(status: Pair, application: Cas2BailApplicationEntity): Cas2BailStatusUpdateEntity { + return cas2BailStatusUpdateEntityFactory.produceAndPersist { + withLabel(status.first) + withStatusId(status.second) + withApplication(application) + withAssessor(externalUserEntityFactory.produceAndPersist()) + } + } + + fun unexpiredDateTime() = OffsetDateTime.now().randomDateTimeBefore(32) + fun expiredDateTime() = unexpiredDateTime().minusDays(33) + + val unexpiredApplicationIds = mutableSetOf() + val expiredApplicationIds = mutableSetOf() + + givenACas2PomUser { userEntity, jwt -> + givenAnOffender { offenderDetails, _ -> + + repeat(2) { + unexpiredApplicationIds.add(createApplication(userEntity, offenderDetails).id) + } + + unexpiredSubset.union(expiredSubset).forEach { + val application = createApplication(userEntity, offenderDetails) + val statusUpdate = createStatusUpdate(it, application) + statusUpdate.createdAt = unexpiredDateTime() + cas2BailStatusUpdateRepository.save(statusUpdate) + unexpiredApplicationIds.add(application.id) + } + + unexpiredSubset.forEach { + val application = createApplication(userEntity, offenderDetails) + val statusUpdate = createStatusUpdate(it, application) + statusUpdate.createdAt = unexpiredDateTime() + cas2BailStatusUpdateRepository.save(statusUpdate) + unexpiredApplicationIds.add(application.id) + } + + expiredSubset.forEach { + val application = createApplication(userEntity, offenderDetails) + val statusUpdate = createStatusUpdate(it, application) + statusUpdate.createdAt = expiredDateTime() + cas2BailStatusUpdateRepository.save(statusUpdate) + expiredApplicationIds.add(application.id) + } + + val rawResponseBody = webTestClient.get() + .uri("/cas2bail/applications") + .header("Authorization", "Bearer $jwt") + .header("X-Service-Name", ServiceName.cas2.value) + .exchange() + .expectStatus() + .isOk + .returnResult() + .responseBody + .blockFirst() + + val responseBody = + objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) + + val returnedApplicationIds = responseBody.map { it.id }.toSet() + + Assertions.assertThat(returnedApplicationIds.equals(unexpiredApplicationIds)).isTrue() + } + } + } + + @Test + fun `Get all applications returns 200 with correct body`() { + givenACas2PomUser { userEntity, jwt -> + givenACas2PomUser { otherUser, _ -> + givenAnOffender { offenderDetails, _ -> + cas2ApplicationJsonSchemaRepository.deleteAll() + + val applicationSchema = cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + withAddedAt(OffsetDateTime.now()) + withId(UUID.randomUUID()) + } + + // abandoned application + val abandonedApplicationEntity = cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(userEntity) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + withCreatedAt(OffsetDateTime.parse("2024-01-03T16:10:00+01:00")) + withAbandonedAt(OffsetDateTime.now()) + } + + // unsubmitted application + val firstApplicationEntity = cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(userEntity) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + withCreatedAt(OffsetDateTime.parse("2024-01-03T16:10:00+01:00")) + withHdcEligibilityDate(LocalDate.now().plusMonths(3)) + } + + // submitted application, CRD >= today so should be returned + val secondApplicationEntity = cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(userEntity) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + withCreatedAt(OffsetDateTime.parse("2024-02-29T09:00:00+01:00")) + withSubmittedAt(OffsetDateTime.now()) + withConditionalReleaseDate(LocalDate.now()) + } + + // submitted application, CRD = yesterday, so should not be returned + val thirdApplicationEntity = cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(userEntity) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + withCreatedAt(OffsetDateTime.parse("2024-02-29T09:00:00+01:00")) + withSubmittedAt(OffsetDateTime.now()) + withConditionalReleaseDate(LocalDate.now().minusDays(1)) + } + + val statusUpdate = cas2StatusUpdateEntityFactory.produceAndPersist { + withLabel("More information requested") + withApplication(secondApplicationEntity) + withAssessor(externalUserEntityFactory.produceAndPersist()) + } + + val otherCas2ApplicationEntity = cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(otherUser) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + } + + val rawResponseBody = webTestClient.get() + .uri("/cas2/applications") + .header("Authorization", "Bearer $jwt") + .header("X-Service-Name", ServiceName.cas2.value) + .exchange() + .expectStatus() + .isOk + .returnResult() + .responseBody + .blockFirst() + + val responseBody = + objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) + + // check transformers were able to return all fields + Assertions.assertThat(responseBody).anyMatch { + firstApplicationEntity.id == it.id && + firstApplicationEntity.crn == it.crn && + firstApplicationEntity.nomsNumber == it.nomsNumber && + "${offenderDetails.firstName} ${offenderDetails.surname}" == it.personName && + firstApplicationEntity.createdAt.toInstant() == it.createdAt && + firstApplicationEntity.createdByUser.id == it.createdByUserId && + firstApplicationEntity.submittedAt?.toInstant() == it.submittedAt && + firstApplicationEntity.hdcEligibilityDate == it.hdcEligibilityDate && + firstApplicationEntity.createdByUser.name == it.createdByUserName + } + + Assertions.assertThat(responseBody).noneMatch { + thirdApplicationEntity.id == it.id + } + + Assertions.assertThat(responseBody).noneMatch { + otherCas2ApplicationEntity.id == it.id + } + + Assertions.assertThat(responseBody).noneMatch { + abandonedApplicationEntity.id == it.id + } + + Assertions.assertThat(responseBody[0].createdAt) + .isEqualTo(secondApplicationEntity.createdAt.toInstant()) + + Assertions.assertThat(responseBody[0].latestStatusUpdate!!.label) + .isEqualTo(statusUpdate.label) + + Assertions.assertThat(responseBody[1].createdAt) + .isEqualTo(firstApplicationEntity.createdAt.toInstant()) + } + } + } + } + + @Test + fun `Get all applications with pagination returns 200 with correct body and header`() { + givenACas2PomUser { userEntity, jwt -> + givenACas2PomUser { otherUser, _ -> + givenAnOffender { offenderDetails, _ -> + cas2ApplicationJsonSchemaRepository.deleteAll() + + val applicationSchema = cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + withAddedAt(OffsetDateTime.now()) + withId(UUID.randomUUID()) + } + + repeat(12) { + cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(userEntity) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + withCreatedAt(OffsetDateTime.now().randomDateTimeBefore(14)) + } + } + + val rawResponseBodyPage1 = webTestClient.get() + .uri("/cas2/applications?page=1") + .header("Authorization", "Bearer $jwt") + .header("X-Service-Name", ServiceName.cas2.value) + .exchange() + .expectStatus() + .isOk + .expectHeader().valueEquals("X-Pagination-CurrentPage", 1) + .expectHeader().valueEquals("X-Pagination-TotalPages", 2) + .expectHeader().valueEquals("X-Pagination-TotalResults", 12) + .expectHeader().valueEquals("X-Pagination-PageSize", 10) + .returnResult() + .responseBody + .blockFirst() + + val responseBodyPage1 = + objectMapper.readValue(rawResponseBodyPage1, object : TypeReference>() {}) + + Assertions.assertThat(responseBodyPage1).size().isEqualTo(10) + + Assertions.assertThat(isOrderedByCreatedAtDescending(responseBodyPage1)).isTrue() + + val rawResponseBodyPage2 = webTestClient.get() + .uri("/cas2/applications?page=2") + .header("Authorization", "Bearer $jwt") + .header("X-Service-Name", ServiceName.cas2.value) + .exchange() + .expectStatus() + .isOk + .expectHeader().valueEquals("X-Pagination-CurrentPage", 2) + .expectHeader().valueEquals("X-Pagination-TotalPages", 2) + .expectHeader().valueEquals("X-Pagination-TotalResults", 12) + .expectHeader().valueEquals("X-Pagination-PageSize", 10) + .returnResult() + .responseBody + .blockFirst() + + val responseBodyPage2 = + objectMapper.readValue(rawResponseBodyPage2, object : TypeReference>() {}) + + Assertions.assertThat(responseBodyPage2).size().isEqualTo(2) + } + } + } + } + + @Test + fun `When a person is not found, returns 200 with placeholder text`() { + givenACas2PomUser { userEntity, jwt -> + val crn = "X1234" + + produceAndPersistBasicApplication(crn, userEntity) + communityAPIMockNotFoundOffenderDetailsCall(crn) + + webTestClient.get() + .uri("/cas2/applications") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isOk + .expectBody() + .jsonPath("$[0].personName").isEqualTo("Person Not Found") + } + } + + /** + * Returns true if the list of application summaries is sorted by descending created_at + * or false if not. + * + * Works by calculating the difference in seconds between two dates and using the sign + * of this difference. If two dates are descending then the difference will be positive. + * If two dates are ascending the difference will be negative (which is set to 0). + * + * For a list of dates, the cumulative multiple of these signs will be 1 if all + * dates in the range are descending (= 1 x 1 x 1 etc.). + * + * If any dates are ascending the multiple will be 0 ( = 1 x 1 x 0 etc.). + * + * If all dates are ascending the multiple will also be 0 ( = 1 x 0 x 0 etc.). + */ + private fun isOrderedByCreatedAtDescending(responseBody: List): Boolean { + var allDescending = 1 + for (i in 1..(responseBody.size - 1)) { + val isDescending = (responseBody[i - 1].createdAt.epochSecond - responseBody[i].createdAt.epochSecond).sign + allDescending *= if (isDescending > 0) 1 else 0 + } + return allDescending == 1 + } + + @Nested + inner class WithPrisonCode { + @Test + fun `Get all applications using prisonCode returns 200 with correct body`() { + givenACas2Assessor { assessor, _ -> + givenACas2PomUser { userAPrisonA, jwt -> + givenAnOffender { offenderDetails, _ -> + cas2ApplicationJsonSchemaRepository.deleteAll() + + val applicationSchema = cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + withAddedAt(OffsetDateTime.now()) + withId(UUID.randomUUID()) + } + + val userAPrisonAApplicationIds = mutableListOf() + + repeat(5) { + userAPrisonAApplicationIds.add( + cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(userAPrisonA) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + withCreatedAt(OffsetDateTime.now().randomDateTimeBefore(14)) + withReferringPrisonCode(userAPrisonA.activeCaseloadId!!) + }.id, + ) + } + + val userBPrisonA = nomisUserEntityFactory.produceAndPersist { + withActiveCaseloadId(userAPrisonA.activeCaseloadId!!) + } + + val userBPrisonAApplicationIds = mutableListOf() + + // submitted applications with conditional release dates in the future + repeat(6) { + userBPrisonAApplicationIds.add( + cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(userBPrisonA) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + withCreatedAt(OffsetDateTime.now().minusDays(it.toLong())) + withReferringPrisonCode(userAPrisonA.activeCaseloadId!!) + withSubmittedAt(OffsetDateTime.now().minusDays(it.toLong())) + withConditionalReleaseDate(LocalDate.now().randomDateAfter(14)) + }.id, + ) + } + + // submitted applications with conditional release dates today + repeat(2) { + userBPrisonAApplicationIds.add( + cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(userBPrisonA) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + withCreatedAt(OffsetDateTime.now().minusDays(it.toLong() + 6)) + withReferringPrisonCode(userAPrisonA.activeCaseloadId!!) + withSubmittedAt(OffsetDateTime.now().minusDays(it.toLong() + 6)) + withConditionalReleaseDate(LocalDate.now()) + }.id, + ) + } + + // submitted application with a conditional release date before today + val excludedApplicationId = cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(userBPrisonA) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + withCreatedAt(OffsetDateTime.now().minusDays(14)) + withReferringPrisonCode(userAPrisonA.activeCaseloadId!!) + withSubmittedAt(OffsetDateTime.now()) + withConditionalReleaseDate(LocalDate.now().randomDateBefore(14)) + }.id + + addStatusUpdates(userBPrisonAApplicationIds.first(), assessor) + + val userCPrisonB = nomisUserEntityFactory.produceAndPersist { + withActiveCaseloadId("another prison") + } + + val otherPrisonApplication = cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(userCPrisonB) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + withCreatedAt(OffsetDateTime.now().randomDateTimeBefore(14)) + withReferringPrisonCode(userCPrisonB.activeCaseloadId!!) + } + + val rawResponseBody = webTestClient.get() + .uri("/cas2/applications?prisonCode=${userAPrisonA.activeCaseloadId}") + .header("Authorization", "Bearer $jwt") + .header("X-Service-Name", ServiceName.cas2.value) + .exchange() + .expectStatus() + .isOk + .returnResult() + .responseBody + .blockFirst() + + val responseBody = + objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) + + Assertions.assertThat(responseBody).noneMatch { + excludedApplicationId == it.id + } + + val returnedApplicationIds = responseBody.map { it.id }.toSet() + + Assertions.assertThat(returnedApplicationIds).isEqualTo( + userAPrisonAApplicationIds.toSet().union(userBPrisonAApplicationIds.toSet()), + ) + + Assertions.assertThat(responseBody).noneMatch { + otherPrisonApplication.id == it.id + } + + Assertions.assertThat(responseBody[0].latestStatusUpdate?.label).isEqualTo("Awaiting decision") + Assertions.assertThat(responseBody[0].latestStatusUpdate?.statusId).isEqualTo(UUID.fromString("c74c3e54-52d8-4aa2-86f6-05190985efee")) + } + } + } + } + + @Test + fun `Get all submitted applications using prisonCode returns 200 with correct body`() { + givenACas2Assessor { assessor, _ -> + givenACas2PomUser { userAPrisonA, jwt -> + givenAnOffender { offenderDetails, _ -> + cas2ApplicationJsonSchemaRepository.deleteAll() + + val applicationSchema = cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + withAddedAt(OffsetDateTime.now()) + withId(UUID.randomUUID()) + } + + val userAPrisonAApplicationIds = mutableListOf() + + repeat(5) { + userAPrisonAApplicationIds.add( + cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(userAPrisonA) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + withCreatedAt(OffsetDateTime.now().minusDays(it.toLong())) + withReferringPrisonCode(userAPrisonA.activeCaseloadId!!) + }.id, + ) + } + + val userBPrisonA = nomisUserEntityFactory.produceAndPersist { + withActiveCaseloadId(userAPrisonA.activeCaseloadId!!) + } + + val userBPrisonAApplicationIds = mutableListOf() + + repeat(6) { + userBPrisonAApplicationIds.add( + cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(userBPrisonA) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + withCreatedAt(OffsetDateTime.now().minusDays(it.toLong())) + withSubmittedAt(OffsetDateTime.now().randomDateTimeBefore(14)) + withReferringPrisonCode(userAPrisonA.activeCaseloadId!!) + withConditionalReleaseDate(LocalDate.now().randomDateAfter(14)) + }.id, + ) + } + + // submitted applications with conditional release dates today + repeat(2) { + userBPrisonAApplicationIds.add( + cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(userBPrisonA) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + withCreatedAt(OffsetDateTime.now().minusDays(it.toLong() + 6)) + withReferringPrisonCode(userAPrisonA.activeCaseloadId!!) + withSubmittedAt(OffsetDateTime.now().minusDays(it.toLong() + 6)) + withConditionalReleaseDate(LocalDate.now()) + }.id, + ) + } + + // submitted application with a conditional release date before today + val excludedApplicationId = cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(userBPrisonA) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + withCreatedAt(OffsetDateTime.now().minusDays(14)) + withReferringPrisonCode(userAPrisonA.activeCaseloadId!!) + withSubmittedAt(OffsetDateTime.now()) + withConditionalReleaseDate(LocalDate.now().randomDateBefore(14)) + }.id + + addStatusUpdates(userBPrisonAApplicationIds.first(), assessor) + + val rawResponseBody = webTestClient.get() + .uri("/cas2/applications?isSubmitted=true&prisonCode=${userAPrisonA.activeCaseloadId}") + .header("Authorization", "Bearer $jwt") + .header("X-Service-Name", ServiceName.cas2.value) + .exchange() + .expectStatus() + .isOk + .returnResult() + .responseBody + .blockFirst() + + val responseBody = + objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) + + Assertions.assertThat(responseBody).noneMatch { + excludedApplicationId == it.id + } + + val returnedApplicationIds = responseBody.map { it.id }.toSet() + + Assertions.assertThat(returnedApplicationIds).isEqualTo(userBPrisonAApplicationIds.toSet()) + Assertions.assertThat(responseBody[0].latestStatusUpdate?.label).isEqualTo("Awaiting decision") + Assertions.assertThat(responseBody[0].latestStatusUpdate?.statusId).isEqualTo(UUID.fromString("c74c3e54-52d8-4aa2-86f6-05190985efee")) + } + } + } + } + + @Test + fun `Get all unsubmitted applications using prisonCode returns 200 with correct body`() { + givenACas2PomUser { userAPrisonA, jwt -> + givenAnOffender { offenderDetails, _ -> + cas2ApplicationJsonSchemaRepository.deleteAll() + + val applicationSchema = cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + withAddedAt(OffsetDateTime.now()) + withId(UUID.randomUUID()) + } + + val userAPrisonAApplicationIds = mutableListOf() + + repeat(5) { + userAPrisonAApplicationIds.add( + cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(userAPrisonA) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + withCreatedAt(OffsetDateTime.now().randomDateTimeBefore(14)) + withReferringPrisonCode(userAPrisonA.activeCaseloadId!!) + }.id, + ) + } + + val userBPrisonA = nomisUserEntityFactory.produceAndPersist { + withActiveCaseloadId(userAPrisonA.activeCaseloadId!!) + } + + val userBPrisonAApplicationIds = mutableListOf() + + repeat(6) { + userBPrisonAApplicationIds.add( + cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(userBPrisonA) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + withCreatedAt(OffsetDateTime.now().randomDateTimeBefore(14)) + withSubmittedAt(OffsetDateTime.now().randomDateTimeBefore(14)) + withReferringPrisonCode(userAPrisonA.activeCaseloadId!!) + }.id, + ) + } + + val rawResponseBody = webTestClient.get() + .uri("/cas2/applications?isSubmitted=false&prisonCode=${userAPrisonA.activeCaseloadId}") + .header("Authorization", "Bearer $jwt") + .header("X-Service-Name", ServiceName.cas2.value) + .exchange() + .expectStatus() + .isOk + .returnResult() + .responseBody + .blockFirst() + + val responseBody = + objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) + + val returnedApplicationIds = responseBody.map { it.id }.toSet() + + Assertions.assertThat(returnedApplicationIds).isEqualTo(userAPrisonAApplicationIds.toSet()) + } + } + } + + @Test + fun `Get applications using another prisonCode returns Forbidden 403`() { + givenACas2PomUser { userAPrisonA, jwt -> + val rawResponseBody = webTestClient.get() + .uri("/cas2/applications?prisonCode=${userAPrisonA.activeCaseloadId!!.reversed()}") + .header("Authorization", "Bearer $jwt") + .header("X-Service-Name", ServiceName.cas2.value) + .exchange() + .expectStatus() + .isForbidden() + } + } + } + + @Nested + inner class AsLicenceCaseAdminUser { + @Test + fun `Get all submitted applications using prisonCode returns 200 with correct body`() { + givenACas2Assessor { assessor, _ -> + givenACas2LicenceCaseAdminUser { caseAdminPrisonA, jwt -> + givenAnOffender { offenderDetails, _ -> + cas2ApplicationJsonSchemaRepository.deleteAll() + + val applicationSchema = cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + withAddedAt(OffsetDateTime.now()) + withId(UUID.randomUUID()) + } + + val pomUserPrisonA = nomisUserEntityFactory.produceAndPersist { + withActiveCaseloadId(caseAdminPrisonA.activeCaseloadId!!) + } + + val userBPrisonAApplicationIds = mutableListOf() + + // submitted applications with conditional release dates in the future + repeat(6) { + userBPrisonAApplicationIds.add( + cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(pomUserPrisonA) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + withCreatedAt(OffsetDateTime.now().minusDays(it.toLong())) + withSubmittedAt(OffsetDateTime.now().randomDateTimeBefore(14)) + withReferringPrisonCode(caseAdminPrisonA.activeCaseloadId!!) + withConditionalReleaseDate(LocalDate.now().randomDateAfter(14)) + }.id, + ) + } + + // submitted applications with conditional release date of today + repeat(2) { + userBPrisonAApplicationIds.add( + cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(pomUserPrisonA) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + withCreatedAt(OffsetDateTime.now().minusDays(it.toLong() + 6)) + withSubmittedAt(OffsetDateTime.now().randomDateTimeBefore(14)) + withReferringPrisonCode(caseAdminPrisonA.activeCaseloadId!!) + withConditionalReleaseDate(LocalDate.now()) + }.id, + ) + } + + // submitted application with a conditional release date before today + val excludedApplicationId = cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(pomUserPrisonA) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + withCreatedAt(OffsetDateTime.now().randomDateTimeBefore(14)) + withSubmittedAt(OffsetDateTime.now().randomDateTimeBefore(14)) + withReferringPrisonCode(caseAdminPrisonA.activeCaseloadId!!) + withConditionalReleaseDate(LocalDate.now().randomDateBefore(14)) + }.id + + val pomUserPrisonB = nomisUserEntityFactory.produceAndPersist { + withActiveCaseloadId("other_prison") + } + + val otherPrisonApplication = cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(pomUserPrisonB) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + withCreatedAt(OffsetDateTime.now()) + withSubmittedAt(OffsetDateTime.now().randomDateTimeBefore(14)) + withReferringPrisonCode("other_prison") + } + + val rawResponseBody = webTestClient.get() + .uri("/cas2/applications?isSubmitted=true&prisonCode=${caseAdminPrisonA.activeCaseloadId}") + .header("Authorization", "Bearer $jwt") + .header("X-Service-Name", ServiceName.cas2.value) + .exchange() + .expectStatus() + .isOk + .returnResult() + .responseBody + .blockFirst() + + val responseBody = + objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) + + Assertions.assertThat(responseBody).noneMatch { + excludedApplicationId == it.id + } + val returnedApplicationIds = responseBody.map { it.id }.toSet() + + Assertions.assertThat(returnedApplicationIds).isEqualTo(userBPrisonAApplicationIds.toSet()) + Assertions.assertThat(returnedApplicationIds).noneMatch { + otherPrisonApplication.id == it + } + } + } + } + } + } + } + + private fun addStatusUpdates(applicationId: UUID, assessor: ExternalUserEntity) { + cas2StatusUpdateEntityFactory.produceAndPersist { + withLabel("More information requested") + withApplication(cas2ApplicationRepository.findById(applicationId).get()) + withAssessor(assessor) + } + // this is the one that should be returned as latestStatusUpdate + cas2StatusUpdateEntityFactory.produceAndPersist { + withStatusId(UUID.fromString("c74c3e54-52d8-4aa2-86f6-05190985efee")) + withLabel("Awaiting decision") + withApplication(cas2ApplicationRepository.findById(applicationId).get()) + withAssessor(assessor) + } + } + + @Nested + inner class GetToIndexUsingIsSubmitted { + + var jwtForUser: String? = null + val submittedIds = mutableSetOf() + val unSubmittedIds = mutableSetOf() + lateinit var excludedApplicationId: UUID + + @BeforeEach + fun setup() { + givenACas2Assessor { assessor, _ -> + givenACas2PomUser { userEntity, jwt -> + givenACas2PomUser { otherUser, _ -> + givenAnOffender { offenderDetails, _ -> + cas2ApplicationJsonSchemaRepository.deleteAll() + + val applicationSchema = cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + withAddedAt(OffsetDateTime.now()) + withId(UUID.randomUUID()) + } + + // create 3 x submitted applications for this user + // with most recent first and conditional release dates in the future + repeat(3) { + submittedIds.add( + cas2ApplicationEntityFactory.produceAndPersist { + withCreatedAt(OffsetDateTime.now().minusDays(it.toLong())) + withApplicationSchema(applicationSchema) + withCreatedByUser(userEntity) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + withSubmittedAt(OffsetDateTime.now().minusDays(it.toLong())) + withConditionalReleaseDate(LocalDate.now().randomDateAfter(14)) + }.id, + ) + } + + // create 2 x submitted applications for this user + // with most recent first and conditional release dates of today + repeat(2) { + submittedIds.add( + cas2ApplicationEntityFactory.produceAndPersist { + withCreatedAt(OffsetDateTime.now().minusDays(it.toLong() + 3)) + withApplicationSchema(applicationSchema) + withCreatedByUser(userEntity) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + withSubmittedAt(OffsetDateTime.now().minusDays(it.toLong() + 3)) + withConditionalReleaseDate(LocalDate.now()) + }.id, + ) + } + + // submitted application with a conditional release date before today + excludedApplicationId = cas2ApplicationEntityFactory.produceAndPersist { + withCreatedAt(OffsetDateTime.now().minusDays(14)) + withApplicationSchema(applicationSchema) + withCreatedByUser(userEntity) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + withSubmittedAt(OffsetDateTime.now()) + withConditionalReleaseDate(LocalDate.now().randomDateBefore(14)) + }.id + + addStatusUpdates(submittedIds.first(), assessor) + + // create 4 x un-submitted in-progress applications for this user + repeat(4) { + unSubmittedIds.add( + cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(userEntity) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + }.id, + ) + } + + // create a submitted application by another user which should not be in results + cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(otherUser) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + withSubmittedAt(OffsetDateTime.now()) + } + + // create an unsubmitted application by another user which should not be in results + cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(otherUser) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + } + + jwtForUser = jwt + } + } + } + } + } + + @Test + fun `returns all applications for user when isSubmitted is null`() { + val rawResponseBody = webTestClient.get() + .uri("/cas2/applications") + .header("Authorization", "Bearer $jwtForUser") + .header("X-Service-Name", ServiceName.cas2.value) + .exchange() + .expectStatus() + .isOk + .returnResult() + .responseBody + .blockFirst() + + val responseBody = + objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) + + Assertions.assertThat(responseBody).noneMatch { + excludedApplicationId == it.id + } + + val uuids = responseBody.map { it.id }.toSet() + Assertions.assertThat(uuids).isEqualTo(submittedIds.union(unSubmittedIds)) + } + + @Test + fun `returns submitted applications for user when isSubmitted is true`() { + val rawResponseBody = webTestClient.get() + .uri("/cas2/applications?isSubmitted=true") + .header("Authorization", "Bearer $jwtForUser") + .header("X-Service-Name", ServiceName.cas2.value) + .exchange() + .expectStatus() + .isOk + .returnResult() + .responseBody + .blockFirst() + + val responseBody = + objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) + + Assertions.assertThat(responseBody).noneMatch { + excludedApplicationId == it.id + } + + val uuids = responseBody.map { it.id }.toSet() + Assertions.assertThat(uuids).isEqualTo(submittedIds) + Assertions.assertThat(responseBody[0].latestStatusUpdate?.label).isEqualTo("Awaiting decision") + Assertions.assertThat(responseBody[0].latestStatusUpdate?.statusId).isEqualTo(UUID.fromString("c74c3e54-52d8-4aa2-86f6-05190985efee")) + } + + @Test + fun `returns submitted applications for user when isSubmitted is true and page specified`() { + val rawResponseBody = webTestClient.get() + .uri("/cas2/applications?isSubmitted=true&page=1") + .header("Authorization", "Bearer $jwtForUser") + .header("X-Service-Name", ServiceName.cas2.value) + .exchange() + .expectStatus() + .isOk + .returnResult() + .responseBody + .blockFirst() + + val responseBody = + objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) + + val uuids = responseBody.map { it.id }.toSet() + Assertions.assertThat(uuids).isEqualTo(submittedIds) + Assertions.assertThat(responseBody[0].latestStatusUpdate?.label).isEqualTo("Awaiting decision") + Assertions.assertThat(responseBody[0].latestStatusUpdate?.statusId).isEqualTo(UUID.fromString("c74c3e54-52d8-4aa2-86f6-05190985efee")) + } + + @Test + fun `returns unsubmitted applications for user when isSubmitted is false`() { + val rawResponseBody = webTestClient.get() + .uri("/cas2/applications?isSubmitted=false") + .header("Authorization", "Bearer $jwtForUser") + .header("X-Service-Name", ServiceName.cas2.value) + .exchange() + .expectStatus() + .isOk + .returnResult() + .responseBody + .blockFirst() + + val responseBody = + objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) + + val uuids = responseBody.map { it.id }.toSet() + Assertions.assertThat(uuids).isEqualTo(unSubmittedIds) + } + } + + @Nested + inner class GetToShow { + + @Nested + inner class WhenCreatedBySameUser { + // When the application requested was created by the logged-in user + @Test + fun `Get single in progress application returns 200 with correct body`() { + givenACas2PomUser { userEntity, jwt -> + givenAnOffender { offenderDetails, inmateDetails -> + cas2ApplicationJsonSchemaRepository.deleteAll() + + val newestJsonSchema = cas2ApplicationJsonSchemaEntityFactory + .produceAndPersist { + withAddedAt(OffsetDateTime.parse("2022-09-21T12:45:00+01:00")) + withSchema( + schema, + ) + } + + val applicationEntity = cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(newestJsonSchema) + withCrn(offenderDetails.otherIds.crn) + withCreatedByUser(userEntity) + withData( + data, + ) + } + + val rawResponseBody = webTestClient.get() + .uri("/cas2/applications/${applicationEntity.id}") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isOk + .returnResult() + .responseBody + .blockFirst() + + val responseBody = objectMapper.readValue( + rawResponseBody, + Cas2Application::class.java, + ) + + Assertions.assertThat(responseBody).matches { + applicationEntity.id == it.id && + applicationEntity.crn == it.person.crn && + applicationEntity.createdAt.toInstant() == it.createdAt && + applicationEntity.createdByUser.id == it.createdBy.id && + applicationEntity.submittedAt?.toInstant() == it.submittedAt && + serializableToJsonNode(applicationEntity.data) == serializableToJsonNode(it.data) && + newestJsonSchema.id == it.schemaVersion && !it.outdatedSchema + } + } + } + } + + @Test + fun `Get single application returns successfully when the offender cannot be fetched from the prisons API`() { + givenACas2PomUser { userEntity, jwt -> + val crn = "X1234" + + givenAnOffender( + offenderDetailsConfigBlock = { + withCrn(crn) + withNomsNumber("ABC123") + }, + ) { offenderDetails, _ -> + val application = produceAndPersistBasicApplication(crn, userEntity) + + prisonAPIMockNotFoundInmateDetailsCall(offenderDetails.otherIds.nomsNumber!!) + loadPreemptiveCacheForInmateDetails(offenderDetails.otherIds.nomsNumber!!) + + val rawResponseBody = webTestClient.get() + .uri("/cas2/applications/${application.id}") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isOk + .returnResult() + .responseBody + .blockFirst() + + val responseBody = objectMapper.readValue( + rawResponseBody, + Cas2Application::class.java, + ) + + Assertions.assertThat(responseBody.person is FullPerson).isTrue + + Assertions.assertThat(responseBody).matches { + val person = it.person as FullPerson + + application.id == it.id && + application.crn == person.crn && + person.nomsNumber == null && + person.status == PersonStatus.unknown && + person.prisonName == null + } + } + } + } + + @Test + fun `Get single submitted application returns 200 with timeline events`() { + givenACas2PomUser { userEntity, jwt -> + givenAnOffender { offenderDetails, inmateDetails -> + cas2ApplicationJsonSchemaRepository.deleteAll() + + val newestJsonSchema = cas2ApplicationJsonSchemaEntityFactory + .produceAndPersist { + withAddedAt(OffsetDateTime.parse("2022-09-21T12:45:00+01:00")) + withSchema( + schema, + ) + } + + val applicationEntity = cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(newestJsonSchema) + withCrn(offenderDetails.otherIds.crn) + withCreatedByUser(userEntity) + withSubmittedAt(OffsetDateTime.now().minusDays(1)) + } + + cas2AssessmentEntityFactory.produceAndPersist { + withApplication(applicationEntity) + } + + val rawResponseBody = webTestClient.get() + .uri("/cas2/applications/${applicationEntity.id}") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isOk + .returnResult() + .responseBody + .blockFirst() + + val responseBody = objectMapper.readValue( + rawResponseBody, + Cas2Application::class.java, + ) + + Assertions.assertThat(responseBody.assessment!!.statusUpdates).isEqualTo(emptyList()) + + Assertions.assertThat(responseBody.timelineEvents!!.map { event -> event.label }) + .isEqualTo(listOf("Application submitted")) + } + } + } + } + + @Nested + inner class WhenCreatedByDifferentUser { + + @Nested + inner class WhenDifferentPrison { + @Test + fun `Get single submitted application is forbidden`() { + givenACas2PomUser { userEntity, jwt -> + givenAnOffender { offenderDetails, inmateDetails -> + cas2ApplicationJsonSchemaRepository.deleteAll() + + val otherUser = nomisUserEntityFactory.produceAndPersist { + withActiveCaseloadId("other_caseload") + } + + val newestJsonSchema = cas2ApplicationJsonSchemaEntityFactory + .produceAndPersist { + withAddedAt(OffsetDateTime.parse("2022-09-21T12:45:00+01:00")) + withSchema( + schema, + ) + } + + val applicationEntity = cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(newestJsonSchema) + withCrn(offenderDetails.otherIds.crn) + withSubmittedAt(OffsetDateTime.now()) + withCreatedByUser(otherUser) + withData( + data, + ) + } + + webTestClient.get() + .uri("/cas2/applications/${applicationEntity.id}") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isForbidden + } + } + } + } + + @Nested + inner class WhenSamePrison { + @Test + fun `Get single submitted application returns 200 with timeline events`() { + givenACas2PomUser { userEntity, jwt -> + givenAnOffender { offenderDetails, inmateDetails -> + cas2ApplicationJsonSchemaRepository.deleteAll() + + val newestJsonSchema = cas2ApplicationJsonSchemaEntityFactory + .produceAndPersist { + withAddedAt(OffsetDateTime.parse("2022-09-21T12:45:00+01:00")) + withSchema( + schema, + ) + } + + val otherUser = nomisUserEntityFactory.produceAndPersist { + withActiveCaseloadId(userEntity.activeCaseloadId!!) + } + + val applicationEntity = cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(newestJsonSchema) + withCrn(offenderDetails.otherIds.crn) + withCreatedByUser(otherUser) + withSubmittedAt(OffsetDateTime.now().minusDays(1)) + withReferringPrisonCode(userEntity.activeCaseloadId!!) + } + + cas2AssessmentEntityFactory.produceAndPersist { + withApplication(applicationEntity) + } + + val rawResponseBody = webTestClient.get() + .uri("/cas2/applications/${applicationEntity.id}") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isOk + .returnResult() + .responseBody + .blockFirst() + + val responseBody = objectMapper.readValue( + rawResponseBody, + Cas2Application::class.java, + ) + + Assertions.assertThat(responseBody.assessment!!.statusUpdates).isEqualTo(emptyList()) + + Assertions.assertThat(responseBody.timelineEvents!!.map { event -> event.label }) + .isEqualTo(listOf("Application submitted")) + } + } + } + + @Test + fun `Get single unsubmitted application returns 403`() { + givenACas2PomUser { userEntity, jwt -> + givenAnOffender { offenderDetails, inmateDetails -> + cas2ApplicationJsonSchemaRepository.deleteAll() + + val newestJsonSchema = cas2ApplicationJsonSchemaEntityFactory + .produceAndPersist { + withAddedAt(OffsetDateTime.parse("2022-09-21T12:45:00+01:00")) + withSchema( + schema, + ) + } + + val otherUser = nomisUserEntityFactory.produceAndPersist { + withActiveCaseloadId(userEntity.activeCaseloadId!!) + } + + val applicationEntity = cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(newestJsonSchema) + withCrn(offenderDetails.otherIds.crn) + withCreatedByUser(otherUser) + withReferringPrisonCode(userEntity.activeCaseloadId!!) + } + + webTestClient.get() + .uri("/cas2/applications/${applicationEntity.id}") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isForbidden + } + } + } + } + } + } + + @Nested + inner class PostToCreate { + + @Nested + inner class PomUsers { + @Test + fun `Create new application for CAS-2 returns 201 with correct body and Location header`() { + givenACas2PomUser { userEntity, jwt -> + givenAnOffender { offenderDetails, _ -> + val applicationSchema = + cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + withAddedAt(OffsetDateTime.now()) + withId(UUID.randomUUID()) + } + + val result = webTestClient.post() + .uri("/cas2/applications") + .header("Authorization", "Bearer $jwt") + .header("X-Service-Name", ServiceName.cas2.value) + .bodyValue( + NewApplication( + crn = offenderDetails.otherIds.crn, + ), + ) + .exchange() + .expectStatus() + .isCreated + .returnResult(Cas2Application::class.java) + + Assertions.assertThat(result.responseHeaders["Location"]).anyMatch { + it.matches(Regex("/cas2/applications/.+")) + } + + Assertions.assertThat(result.responseBody.blockFirst()).matches { + it.person.crn == offenderDetails.otherIds.crn && + it.schemaVersion == applicationSchema.id + } + } + } + } + + @Test + fun `Create new application returns 404 when a person cannot be found`() { + givenACas2PomUser { userEntity, jwt -> + val crn = "X1234" + + communityAPIMockNotFoundOffenderDetailsCall(crn) + + cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + withAddedAt(OffsetDateTime.now()) + withId(UUID.randomUUID()) + } + + webTestClient.post() + .uri("/cas2/applications") + .header("Authorization", "Bearer $jwt") + .bodyValue( + NewApplication( + crn = crn, + ), + ) + .exchange() + .expectStatus() + .isNotFound + .expectBody() + .jsonPath("$.detail").isEqualTo("No Offender with an ID of $crn could be found") + } + } + } + + @Nested + inner class LicenceCaseAdminUsers { + @Test + fun `Create new application for CAS-2 returns 201 with correct body and Location header`() { + givenACas2LicenceCaseAdminUser { _, jwt -> + givenAnOffender { offenderDetails, _ -> + val applicationSchema = + cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + withAddedAt(OffsetDateTime.now()) + withId(UUID.randomUUID()) + } + + val result = webTestClient.post() + .uri("/cas2/applications") + .header("Authorization", "Bearer $jwt") + .header("X-Service-Name", ServiceName.cas2.value) + .bodyValue( + NewApplication( + crn = offenderDetails.otherIds.crn, + ), + ) + .exchange() + .expectStatus() + .isCreated + .returnResult(Cas2Application::class.java) + + Assertions.assertThat(result.responseHeaders["Location"]).anyMatch { + it.matches(Regex("/cas2/applications/.+")) + } + + Assertions.assertThat(result.responseBody.blockFirst()).matches { + it.person.crn == offenderDetails.otherIds.crn && + it.schemaVersion == applicationSchema.id + } + } + } + } + + @Test + fun `Create new application returns 404 when a person cannot be found`() { + givenACas2LicenceCaseAdminUser { _, jwt -> + val crn = "X1234" + + communityAPIMockNotFoundOffenderDetailsCall(crn) + + cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + withAddedAt(OffsetDateTime.now()) + withId(UUID.randomUUID()) + } + + webTestClient.post() + .uri("/cas2/applications") + .header("Authorization", "Bearer $jwt") + .bodyValue( + NewApplication( + crn = crn, + ), + ) + .exchange() + .expectStatus() + .isNotFound + .expectBody() + .jsonPath("$.detail").isEqualTo("No Offender with an ID of $crn could be found") + } + } + } + } + + @Nested + inner class PutToUpdate { + + @Nested + inner class PomUsers { + @Test + fun `Update existing CAS2 application returns 200 with correct body`() { + givenACas2PomUser { submittingUser, jwt -> + givenAnOffender { offenderDetails, _ -> + val applicationId = UUID.fromString("22ceda56-98b2-411d-91cc-ace0ab8be872") + + val applicationSchema = + cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + withAddedAt(OffsetDateTime.now()) + withId(UUID.randomUUID()) + withSchema( + schema, + ) + } + + cas2ApplicationEntityFactory.produceAndPersist { + withCrn(offenderDetails.otherIds.crn) + withId(applicationId) + withApplicationSchema(applicationSchema) + withCreatedByUser(submittingUser) + } + + val resultBody = webTestClient.put() + .uri("/cas2/applications/$applicationId") + .header("Authorization", "Bearer $jwt") + .bodyValue( + UpdateCas2Application( + data = mapOf("thingId" to 123), + type = UpdateApplicationType.CAS2, + ), + ) + .exchange() + .expectStatus() + .isOk + .returnResult(String::class.java) + .responseBody + .blockFirst() + + val result = objectMapper.readValue(resultBody, Application::class.java) + + Assertions.assertThat(result.person.crn).isEqualTo(offenderDetails.otherIds.crn) + } + } + } + } + + @Nested + inner class LicenceCaseAdminUsers { + @Test + fun `Update existing CAS2 application returns 200 with correct body`() { + givenACas2LicenceCaseAdminUser { submittingUser, jwt -> + givenAnOffender { offenderDetails, _ -> + val applicationId = UUID.fromString("22ceda56-98b2-411d-91cc-ace0ab8be872") + + val applicationSchema = + cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + withAddedAt(OffsetDateTime.now()) + withId(UUID.randomUUID()) + withSchema( + schema, + ) + } + + cas2ApplicationEntityFactory.produceAndPersist { + withCrn(offenderDetails.otherIds.crn) + withId(applicationId) + withApplicationSchema(applicationSchema) + withCreatedByUser(submittingUser) + } + + val resultBody = webTestClient.put() + .uri("/cas2/applications/$applicationId") + .header("Authorization", "Bearer $jwt") + .bodyValue( + UpdateCas2Application( + data = mapOf("thingId" to 123), + type = UpdateApplicationType.CAS2, + ), + ) + .exchange() + .expectStatus() + .isOk + .returnResult(String::class.java) + .responseBody + .blockFirst() + + val result = objectMapper.readValue(resultBody, Application::class.java) + + Assertions.assertThat(result.person.crn).isEqualTo(offenderDetails.otherIds.crn) + } + } + } + } + } + + private fun serializableToJsonNode(serializable: Any?): JsonNode { + if (serializable == null) return NullNode.instance + if (serializable is String) return objectMapper.readTree(serializable) + + return objectMapper.readTree(objectMapper.writeValueAsString(serializable)) + } + + private fun produceAndPersistBasicApplication( + crn: String, + userEntity: NomisUserEntity, + ): Cas2ApplicationEntity { + val jsonSchema = cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + withAddedAt(OffsetDateTime.parse("2022-09-21T12:45:00+01:00")) + withSchema( + schema, + ) + } + + val application = cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(jsonSchema) + withCrn(crn) + withCreatedByUser(userEntity) + withData( + data, + ) + } + + return application + } +} \ No newline at end of file diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/repository/ApplicationTestRepository.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/repository/ApplicationTestRepository.kt index 1a256d5386..f0a9ce5c1b 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/repository/ApplicationTestRepository.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/repository/ApplicationTestRepository.kt @@ -5,6 +5,7 @@ import org.springframework.stereotype.Repository import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApprovedPremisesApplicationEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.TemporaryAccommodationApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity import java.util.UUID @Repository @@ -13,5 +14,8 @@ interface ApprovedPremisesApplicationTestRepository : JpaRepository +@Repository +interface Cas2BailApplicationTestRepository : JpaRepository + @Repository interface TemporaryAccommodationApplicationTestRepository : JpaRepository diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/repository/Cas2BailStatusUpdateTestRepository.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/repository/Cas2BailStatusUpdateTestRepository.kt new file mode 100644 index 0000000000..3e61982bf7 --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/repository/Cas2BailStatusUpdateTestRepository.kt @@ -0,0 +1,10 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.repository + + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailStatusUpdateEntity +import java.util.UUID + +@Repository +interface Cas2BailStatusUpdateTestRepository : JpaRepository diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/repository/JsonSchemaTestRepository.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/repository/JsonSchemaTestRepository.kt index b345e64d79..a15a899f01 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/repository/JsonSchemaTestRepository.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/repository/JsonSchemaTestRepository.kt @@ -2,12 +2,7 @@ package uk.gov.justice.digital.hmpps.approvedpremisesapi.repository import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApprovedPremisesApplicationJsonSchemaEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApprovedPremisesAssessmentJsonSchemaEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApprovedPremisesPlacementApplicationJsonSchemaEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationJsonSchemaEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.TemporaryAccommodationApplicationJsonSchemaEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.TemporaryAccommodationAssessmentJsonSchemaEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.* import java.util.UUID @Repository @@ -19,6 +14,9 @@ interface TemporaryAccommodationApplicationJsonSchemaTestRepository : JpaReposit @Repository interface Cas2ApplicationJsonSchemaTestRepository : JpaRepository +@Repository +interface Cas2BailApplicationJsonSchemaTestRepository : JpaRepository + @Repository interface ApprovedPremisesAssessmentJsonSchemaTestRepository : JpaRepository From 6166492368fd0b0270afd7489ab9772213ecfbcc Mon Sep 17 00:00:00 2001 From: garethCAS2 Date: Fri, 29 Nov 2024 14:18:59 +0000 Subject: [PATCH 06/37] initial unit tests working - for cas2bail application --- .../cas2bail/Cas2BailApplicationService.kt | 4 +- .../cas2bail/Cas2BailAssessmentService.kt | 3 +- ...2BailApplicationJsonSchemaEntityFactory.kt | 9 +- .../Cas2BailApplicationServiceTest.kt | 1126 +++++++++++++++++ 4 files changed, 1135 insertions(+), 7 deletions(-) create mode 100644 src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailApplicationServiceTest.kt diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationService.kt index 4e1779b193..7e15b54621 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationService.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationService.kt @@ -133,7 +133,7 @@ class Cas2BailApplicationService( createdByUser = user, data = null, document = null, - schemaVersion = jsonSchemaService.getNewestSchema(Cas2ApplicationJsonSchemaEntity::class.java), + schemaVersion = jsonSchemaService.getNewestSchema(Cas2BailApplicationJsonSchemaEntity::class.java), createdAt = OffsetDateTime.now(), submittedAt = null, schemaUpToDate = true, @@ -343,7 +343,7 @@ class Cas2BailApplicationService( } fun createAssessment(application: Cas2BailApplicationEntity) { - assessmentService.createCas2Assessment(application) + assessmentService.createCas2BailAssessment(application) } @SuppressWarnings("ThrowsCount") diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailAssessmentService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailAssessmentService.kt index 5633c613f9..b9b3caadb7 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailAssessmentService.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailAssessmentService.kt @@ -19,8 +19,9 @@ class Cas2BailAssessmentService ( private val assessmentRepository: Cas2BailAssessmentRepository, ) { + @Transactional - fun createCas2Assessment(cas2BailApplicationEntity: Cas2BailApplicationEntity): Cas2BailAssessmentEntity = + fun createCas2BailAssessment(cas2BailApplicationEntity: Cas2BailApplicationEntity): Cas2BailAssessmentEntity = assessmentRepository.save( Cas2BailAssessmentEntity( id = UUID.randomUUID(), diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/Cas2BailApplicationJsonSchemaEntityFactory.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/Cas2BailApplicationJsonSchemaEntityFactory.kt index fb830f3b12..709386c6ca 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/Cas2BailApplicationJsonSchemaEntityFactory.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/Cas2BailApplicationJsonSchemaEntityFactory.kt @@ -40,8 +40,9 @@ class Cas2BailApplicationJsonSchemaEntityFactory : Factory() + private val mockLockableApplicationRepository = mockk() + private val mockApplicationSummaryRepository = mockk() + private val mockJsonSchemaService = mockk() + private val mockOffenderService = mockk() + private val mockUserAccessService = mockk() + private val mockDomainEventService = mockk() + private val mockEmailNotificationService = mockk() + private val mockAssessmentService = mockk() + private val mockObjectMapper = mockk() + private val mockNotifyConfig = mockk() + + + private val applicationService = Cas2BailApplicationService( + mockApplicationRepository, + mockLockableApplicationRepository, + mockApplicationSummaryRepository, + mockJsonSchemaService, + mockOffenderService, + mockUserAccessService, + mockDomainEventService, + mockEmailNotificationService, + mockAssessmentService, + mockNotifyConfig, + mockObjectMapper, + "http://frontend/applications/#id", + "http://frontend/assess/applications/#applicationId/overview", + ) + + @Nested + inner class GetAllSubmittedApplicationsForAssessor { + @Test + fun `returns Success result with entity from db`() { + val applicationSummary = Cas2BailApplicationSummaryEntity( + id = UUID.fromString("2f838a8c-dffc-48a3-9536-f0e95985e809"), + crn = randomStringMultiCaseWithNumbers(6), + nomsNumber = randomStringMultiCaseWithNumbers(6), + userId = "836a9460-b177-433a-a0d9-262509092c9f", + userName = "first last", + createdAt = OffsetDateTime.parse("2023-04-19T13:25:00+01:00"), + submittedAt = OffsetDateTime.parse("2023-04-19T13:25:30+01:00"), + hdcEligibilityDate = LocalDate.parse("2023-04-29"), + latestStatusUpdateLabel = null, + latestStatusUpdateStatusId = null, + prisonCode = "BRI", + ) + + PaginationConfig(defaultPageSize = 10).postInit() + val page = mockk>() + val pageRequest = mockk() + val pageCriteria = PageCriteria(sortBy = "submitted_at", sortDirection = SortDirection.asc, page = 3) + + mockkStatic(PageRequest::class) + + every { PageRequest.of(2, 10, Sort.by("submitted_at").ascending()) } returns pageRequest + every { page.content } returns listOf(applicationSummary) + every { page.totalPages } returns 10 + every { page.totalElements } returns 100 + + every { + mockApplicationSummaryRepository.findBySubmittedAtIsNotNull( + PageRequest.of( + 2, + 10, + Sort.by(Sort.Direction.ASC, "submitted_at"), + ), + ) + } returns page + + val (applicationSummaries, metadata) = applicationService.getAllSubmittedCas2BailApplicationsForAssessor(pageCriteria) + + assertThat(applicationSummaries).isEqualTo(listOf(applicationSummary)) + assertThat(metadata?.currentPage).isEqualTo(3) + assertThat(metadata?.pageSize).isEqualTo(10) + assertThat(metadata?.totalPages).isEqualTo(10) + assertThat(metadata?.totalResults).isEqualTo(100) + } + } + + @Nested + inner class GetApplicationsWithPrisonCode { + val applicationSummary = Cas2BailApplicationSummaryEntity( + id = UUID.fromString("2f838a8c-dffc-48a3-9536-f0e95985e809"), + crn = randomStringMultiCaseWithNumbers(6), + nomsNumber = randomStringMultiCaseWithNumbers(6), + userId = "836a9460-b177-433a-a0d9-262509092c9f", + userName = "first last", + createdAt = OffsetDateTime.parse("2023-04-19T13:25:00+01:00"), + submittedAt = OffsetDateTime.parse("2023-04-19T13:25:30+01:00"), + hdcEligibilityDate = LocalDate.parse("2023-04-29"), + latestStatusUpdateLabel = null, + latestStatusUpdateStatusId = null, + prisonCode = "BRI", + ) + val page = mockk>() + val pageCriteria = PageCriteria(sortBy = "submitted_at", sortDirection = SortDirection.asc, page = 3) + val user = NomisUserEntityFactory().produce() + val prisonCode = "BRI" + + fun testPrisonCodeWithIsSubmitted(isSubmitted: Boolean?) { + every { page.content } returns listOf(applicationSummary) + every { page.totalPages } returns 10 + every { page.totalElements } returns 100 + + val (applicationSummaries, _) = applicationService.getCas2BailApplications(prisonCode, isSubmitted, user, pageCriteria) + + assertThat(applicationSummaries).isEqualTo(listOf(applicationSummary)) + } + + @Test + fun `return all applications when prisonCode is specified and isSubmitted is null`() { + PaginationConfig(defaultPageSize = 10).postInit() + + every { + mockApplicationSummaryRepository.findByPrisonCode( + prisonCode, + getPageableOrAllPages(pageCriteria), + ) + } returns page + + testPrisonCodeWithIsSubmitted(null) + } + + @Test + fun `return submitted prison applications when prisonCode is specified and isSubmitted is true`() { + PaginationConfig(defaultPageSize = 10).postInit() + + every { + mockApplicationSummaryRepository.findByPrisonCodeAndSubmittedAtIsNotNull( + prisonCode, + getPageableOrAllPages(pageCriteria), + ) + } returns page + + testPrisonCodeWithIsSubmitted(true) + } + + @Test + fun `return unsubmitted prison applications when prisonCode is specified and isSubmitted is false`() { + PaginationConfig(defaultPageSize = 10).postInit() + + every { + mockApplicationSummaryRepository.findByPrisonCodeAndSubmittedAtIsNull( + prisonCode, + getPageableOrAllPages(pageCriteria), + ) + } returns page + + testPrisonCodeWithIsSubmitted(false) + } + } + + @Nested + inner class GetApplicationForUser { + @Test + fun `where application does not exist returns NotFound result`() { + val user = NomisUserEntityFactory().produce() + val applicationId = UUID.fromString("c1750938-19fc-48a1-9ae9-f2e119ffc1f4") + + every { mockApplicationRepository.findByIdOrNull(applicationId) } returns null + + assertThat(applicationService.getCas2BailApplicationForUser(applicationId, user) is AuthorisableActionResult.NotFound).isTrue + } + + @Test + fun `where application is abandoned returns NotFound result`() { + val user = NomisUserEntityFactory().produce() + val applicationId = UUID.fromString("c1750938-19fc-48a1-9ae9-f2e119ffc1f4") + + every { mockApplicationRepository.findByIdOrNull(any()) } returns + Cas2BailApplicationEntityFactory() + .withCreatedByUser( + NomisUserEntityFactory() + .produce(), + ) + .withAbandonedAt(OffsetDateTime.now()) + .produce() + + assertThat(applicationService.getCas2BailApplicationForUser(applicationId, user) is AuthorisableActionResult.NotFound).isTrue + } + + @Test + fun `where user cannot access the application returns Unauthorised result`() { + val user = NomisUserEntityFactory() + .produce() + val applicationId = UUID.fromString("c1750938-19fc-48a1-9ae9-f2e119ffc1f4") + + every { mockApplicationRepository.findByIdOrNull(any()) } returns + Cas2BailApplicationEntityFactory() + .withCreatedByUser( + NomisUserEntityFactory() + .produce(), + ) + .produce() + + every { mockUserAccessService.userCanViewCas2BailApplication(any(), any()) } returns false + + + assertThat(applicationService.getCas2BailApplicationForUser(applicationId, user) is AuthorisableActionResult.Unauthorised).isTrue + } + + @Test + fun `where user can access the application returns Success result with entity from db`() { + val distinguishedName = "SOMEPERSON" + val userId = UUID.fromString("239b5e41-f83e-409e-8fc0-8f1e058d417e") + val applicationId = UUID.fromString("c1750938-19fc-48a1-9ae9-f2e119ffc1f4") + + val newestJsonSchema = Cas2BailApplicationJsonSchemaEntityFactory() + .withSchema("{}") + .produce() + + val userEntity = NomisUserEntityFactory() + .withId(userId) + .withNomisUsername(distinguishedName) + .produce() + + val applicationEntity = Cas2BailApplicationEntityFactory() + .withCreatedByUser(userEntity) + .withApplicationSchema(newestJsonSchema) + .produce() + + every { mockJsonSchemaService.checkCas2BailSchemaOutdated(any()) } answers { + it.invocation + .args[0] as Cas2BailApplicationEntity + } + every { mockApplicationRepository.findByIdOrNull(any()) } returns applicationEntity + every { mockUserAccessService.userCanViewCas2BailApplication(any(), any()) } returns true + + val result = applicationService.getCas2BailApplicationForUser(applicationId, userEntity) + + assertThat(result is AuthorisableActionResult.Success).isTrue + result as AuthorisableActionResult.Success + + assertThat(result.entity).isEqualTo(applicationEntity) + } + } + + @Nested + inner class CreateApplication { + @Test + fun `returns FieldValidationError when Offender is not found`() { + val crn = "CRN345" + val username = "SOMEPERSON" + + every { mockOffenderService.getOffenderByCrn(crn) } returns AuthorisableActionResult.NotFound() + + val user = userWithUsername(username) + + val result = applicationService.createCas2BailApplication(crn, user, "jwt") + + assertThat(result is ValidatableActionResult.FieldValidationError).isTrue + result as ValidatableActionResult.FieldValidationError + assertThat(result.validationMessages).containsEntry("$.crn", "doesNotExist") + } + + @Test + fun `returns FieldValidationError when user is not authorised to view CRN`() { + val crn = "CRN345" + val username = "SOMEPERSON" + + every { mockOffenderService.getOffenderByCrn(crn) } returns AuthorisableActionResult.Unauthorised() + + val user = userWithUsername(username) + + val result = applicationService.createCas2BailApplication(crn, user, "jwt") + + assertThat(result is ValidatableActionResult.FieldValidationError).isTrue + result as ValidatableActionResult.FieldValidationError + assertThat(result.validationMessages).containsEntry("$.crn", "userPermission") + } + + @Test + fun `returns Success with created Application`() { + val crn = "CRN345" + val username = "SOMEPERSON" + + val schema = Cas2BailApplicationJsonSchemaEntityFactory().produce() + + val user = userWithUsername(username) + + every { mockOffenderService.getOffenderByCrn(crn) } returns AuthorisableActionResult.Success( + OffenderDetailsSummaryFactory().produce(), + ) + + every { mockJsonSchemaService.getNewestSchema(Cas2BailApplicationJsonSchemaEntity::class.java) } returns schema + every { mockApplicationRepository.save(any()) } answers { + it.invocation.args[0] as + Cas2BailApplicationEntity + } + + val result = applicationService.createCas2BailApplication(crn, user, "jwt") + + assertThat(result is ValidatableActionResult.Success).isTrue + result as ValidatableActionResult.Success + assertThat(result.entity.crn).isEqualTo(crn) + assertThat(result.entity.createdByUser).isEqualTo(user) + } + } + + @Nested + inner class UpdateApplication { + val user = NomisUserEntityFactory().produce() + + @Test + fun `returns NotFound when application doesn't exist`() { + val applicationId = UUID.fromString("fa6e97ce-7b9e-473c-883c-83b1c2af773d") + + every { mockApplicationRepository.findByIdOrNull(applicationId) } returns null + + assertThat( + applicationService.updateCas2BailApplication( + applicationId = applicationId, + data = "{}", + user = user, + ) is AuthorisableActionResult.NotFound, + ).isTrue + } + + @Test + fun `returns Unauthorised when application doesn't belong to request user`() { + val applicationId = UUID.fromString("fa6e97ce-7b9e-473c-883c-83b1c2af773d") + + val application = Cas2BailApplicationEntityFactory() + .withId(applicationId) + .withYieldedCreatedByUser { + NomisUserEntityFactory() + .produce() + } + .produce() + + every { mockApplicationRepository.findByIdOrNull(applicationId) } returns + application + every { mockJsonSchemaService.checkCas2BailSchemaOutdated(application) } returns + application + + assertThat( + applicationService.updateCas2BailApplication( + applicationId = applicationId, + data = "{}", + user = user, + ) is AuthorisableActionResult.Unauthorised, + ).isTrue + } + + @Test + fun `returns GeneralValidationError when application has already been submitted`() { + val applicationId = UUID.fromString("fa6e97ce-7b9e-473c-883c-83b1c2af773d") + + val newestSchema = Cas2BailApplicationJsonSchemaEntityFactory().produce() + + val application = Cas2BailApplicationEntityFactory() + .withApplicationSchema(newestSchema) + .withId(applicationId) + .withCreatedByUser(user) + .withSubmittedAt(OffsetDateTime.now()) + .produce() + .apply { + schemaUpToDate = true + } + + every { mockApplicationRepository.findByIdOrNull(applicationId) } returns + application + every { mockJsonSchemaService.checkCas2BailSchemaOutdated(application) } returns + application + + val result = applicationService.updateCas2BailApplication( + applicationId = applicationId, + data = "{}", + user = user, + ) + + assertThat(result is AuthorisableActionResult.Success).isTrue + result as AuthorisableActionResult.Success + + assertThat(result.entity is ValidatableActionResult.GeneralValidationError).isTrue + val validatableActionResult = result.entity as ValidatableActionResult.GeneralValidationError + + assertThat(validatableActionResult.message).isEqualTo("This application has already been submitted") + } + + @Test + fun `returns GeneralValidationError when application has been abandoned`() { + val applicationId = UUID.fromString("fa6e97ce-7b9e-473c-883c-83b1c2af773d") + + val newestSchema = Cas2BailApplicationJsonSchemaEntityFactory().produce() + + val application = Cas2BailApplicationEntityFactory() + .withApplicationSchema(newestSchema) + .withId(applicationId) + .withCreatedByUser(user) + .withAbandonedAt(OffsetDateTime.now()) + .produce() + .apply { + schemaUpToDate = true + } + + every { mockApplicationRepository.findByIdOrNull(applicationId) } returns application + every { mockJsonSchemaService.checkCas2BailSchemaOutdated(application) } returns application + + val result = applicationService.updateCas2BailApplication( + applicationId = applicationId, + data = "{}", + user = user, + ) + + assertThat(result is AuthorisableActionResult.Success).isTrue + result as AuthorisableActionResult.Success + + assertThat(result.entity is ValidatableActionResult.GeneralValidationError).isTrue + val validatableActionResult = result.entity as ValidatableActionResult.GeneralValidationError + + assertThat(validatableActionResult.message).isEqualTo("This application has been abandoned") + } + + @Test + fun `returns GeneralValidationError when application schema is outdated`() { + val applicationId = UUID.fromString("fa6e97ce-7b9e-473c-883c-83b1c2af773d") + + val application = Cas2BailApplicationEntityFactory() + .withId(applicationId) + .withCreatedByUser(user) + .withSubmittedAt(null) + .produce() + .apply { + schemaUpToDate = false + } + + every { mockApplicationRepository.findByIdOrNull(applicationId) } returns application + every { mockJsonSchemaService.checkCas2BailSchemaOutdated(application) } returns application + + val result = applicationService.updateCas2BailApplication( + applicationId = applicationId, + data = "{}", + user = user, + ) + + assertThat(result is AuthorisableActionResult.Success).isTrue + result as AuthorisableActionResult.Success + + assertThat(result.entity is ValidatableActionResult.GeneralValidationError).isTrue + val validatableActionResult = result.entity as ValidatableActionResult.GeneralValidationError + + assertThat(validatableActionResult.message).isEqualTo("The schema version is outdated") + } + + @ParameterizedTest + @ValueSource(strings = ["<", "<", "〈", "〈", ">", ">", "〉", "〉", "<<〈〈>>〉〉"]) + fun `returns Success when an application, that contains removed malicious characters, is updated`(str: String) { + val applicationId = UUID.fromString("dced02b1-8e3b-4ea5-bf99-1fba0ca1b87c") + + val newestSchema = Cas2BailApplicationJsonSchemaEntityFactory().produce() + val updatedData = """ + { + "aProperty": "val${str}ue" + } + """ + + val application = Cas2BailApplicationEntityFactory() + .withApplicationSchema(newestSchema) + .withId(applicationId) + .withCreatedByUser(user) + .produce() + .apply { + schemaUpToDate = true + } + + every { mockApplicationRepository.findByIdOrNull(applicationId) } returns + application + every { mockJsonSchemaService.checkCas2BailSchemaOutdated(application) } returns + application + every { + mockJsonSchemaService.getNewestSchema( + Cas2BailApplicationJsonSchemaEntity::class + .java, + ) + } returns newestSchema + every { mockJsonSchemaService.validate(newestSchema, updatedData) } returns true + every { mockApplicationRepository.save(any()) } answers { + it.invocation.args[0] + as Cas2BailApplicationEntity + } + + val result = applicationService.updateCas2BailApplication( + applicationId = applicationId, + data = updatedData, + user = user, + ) + + assertThat(result is AuthorisableActionResult.Success).isTrue + result as AuthorisableActionResult.Success + + assertThat(result.entity is ValidatableActionResult.Success).isTrue + val validatableActionResult = result.entity as ValidatableActionResult.Success + + val cas2BailApplication = validatableActionResult.entity + + assertThat(cas2BailApplication.data).isEqualTo( + """ + { + "aProperty": "value" + } + """, + ) + } + + @Test + fun `returns Success with updated Application`() { + val applicationId = UUID.fromString("fa6e97ce-7b9e-473c-883c-83b1c2af773d") + + val newestSchema = Cas2BailApplicationJsonSchemaEntityFactory().produce() + val updatedData = """ + { + "aProperty": "value" + } + """ + + val application = Cas2BailApplicationEntityFactory() + .withApplicationSchema(newestSchema) + .withId(applicationId) + .withCreatedByUser(user) + .produce() + .apply { + schemaUpToDate = true + } + + every { mockApplicationRepository.findByIdOrNull(applicationId) } returns + application + every { mockJsonSchemaService.checkCas2BailSchemaOutdated(application) } returns + application + every { + mockJsonSchemaService.getNewestSchema( + Cas2BailApplicationJsonSchemaEntity::class + .java, + ) + } returns newestSchema + every { mockJsonSchemaService.validate(newestSchema, updatedData) } returns true + every { mockApplicationRepository.save(any()) } answers { + it.invocation.args[0] + as Cas2BailApplicationEntity + } + + val result = applicationService.updateCas2BailApplication( + applicationId = applicationId, + data = updatedData, + user = user, + ) + + assertThat(result is AuthorisableActionResult.Success).isTrue + result as AuthorisableActionResult.Success + + assertThat(result.entity is ValidatableActionResult.Success).isTrue + val validatableActionResult = result.entity as ValidatableActionResult.Success + + val cas2BailApplication = validatableActionResult.entity + + assertThat(cas2BailApplication.data).isEqualTo(updatedData) + } + } + + @Nested + inner class AbandonApplication { + val user = NomisUserEntityFactory().produce() + + @Test + fun `returns NotFound when application doesn't exist`() { + val applicationId = UUID.fromString("fa6e97ce-7b9e-473c-883c-83b1c2af773d") + + every { mockApplicationRepository.findByIdOrNull(applicationId) } returns null + + assertThat( + applicationService.abandonCas2BailApplication( + applicationId = applicationId, + user = user, + ) is AuthorisableActionResult.NotFound, + ).isTrue + } + + @Test + fun `returns Unauthorised when application doesn't belong to request user`() { + val applicationId = UUID.fromString("fa6e97ce-7b9e-473c-883c-83b1c2af773d") + + val application = Cas2BailApplicationEntityFactory() + .withId(applicationId) + .withYieldedCreatedByUser { + NomisUserEntityFactory() + .produce() + } + .produce() + + every { mockApplicationRepository.findByIdOrNull(applicationId) } returns + application + + assertThat( + applicationService.abandonCas2BailApplication( + applicationId = applicationId, + user = user, + ) is AuthorisableActionResult.Unauthorised, + ).isTrue + } + + @Test + fun `returns Conflict Error when application has already been submitted`() { + val applicationId = UUID.fromString("fa6e97ce-7b9e-473c-883c-83b1c2af773d") + + val newestSchema = Cas2BailApplicationJsonSchemaEntityFactory().produce() + + val application = Cas2BailApplicationEntityFactory() + .withApplicationSchema(newestSchema) + .withId(applicationId) + .withCreatedByUser(user) + .withSubmittedAt(OffsetDateTime.now()) + .produce() + .apply { + schemaUpToDate = true + } + + every { mockApplicationRepository.findByIdOrNull(applicationId) } returns + application + + val result = applicationService.abandonCas2BailApplication( + applicationId = applicationId, + user = user, + ) + + assertThat(result is AuthorisableActionResult.Success).isTrue + result as AuthorisableActionResult.Success + + assertThat(result.entity is ValidatableActionResult.ConflictError).isTrue + val validatableActionResult = result.entity as ValidatableActionResult.ConflictError + + assertThat(validatableActionResult.message).isEqualTo("This application has already been submitted") + } + + @Test + fun `returns Success when application has already been abandoned`() { + val applicationId = UUID.fromString("fa6e97ce-7b9e-473c-883c-83b1c2af773d") + + val newestSchema = Cas2BailApplicationJsonSchemaEntityFactory().produce() + + val application = Cas2BailApplicationEntityFactory() + .withApplicationSchema(newestSchema) + .withId(applicationId) + .withCreatedByUser(user) + .withAbandonedAt(OffsetDateTime.now()) + .produce() + .apply { + schemaUpToDate = true + } + + every { mockApplicationRepository.findByIdOrNull(applicationId) } returns + application + + val result = applicationService.abandonCas2BailApplication( + applicationId = applicationId, + user = user, + ) + + assertThat(result is AuthorisableActionResult.Success).isTrue + result as AuthorisableActionResult.Success + + assertThat(result.entity is ValidatableActionResult.Success).isTrue + } + + @Test + fun `returns Success and deletes the application data`() { + val applicationId = UUID.fromString("dced02b1-8e3b-4ea5-bf99-1fba0ca1b87c") + + val newestSchema = Cas2BailApplicationJsonSchemaEntityFactory().produce() + val data = """ + { + "aProperty": "value" + } + """ + + val application = Cas2BailApplicationEntityFactory() + .withApplicationSchema(newestSchema) + .withId(applicationId) + .withCreatedByUser(user) + .withData(data) + .produce() + .apply { + schemaUpToDate = true + } + + every { mockApplicationRepository.findByIdOrNull(applicationId) } returns + application + + every { mockApplicationRepository.save(any()) } answers { + it.invocation.args[0] as Cas2BailApplicationEntity + } + + val result = applicationService.abandonCas2BailApplication( + applicationId = applicationId, + user = user, + ) + + assertThat(result is AuthorisableActionResult.Success).isTrue + result as AuthorisableActionResult.Success + + assertThat(result.entity is ValidatableActionResult.Success).isTrue + val validatableActionResult = result.entity as ValidatableActionResult.Success + + val cas2BailApplication = validatableActionResult.entity + + assertThat(cas2BailApplication.data).isNull() + } + } + + @Nested + inner class SubmitApplication { + val applicationId: UUID = UUID.fromString("fa6e97ce-7b9e-473c-883c-83b1c2af773d") + val username = "SOMEPERSON" + val user = NomisUserEntityFactory() + .withNomisUsername(this.username) + .produce() + val hdcEligibilityDate = LocalDate.parse("2023-03-30") + val conditionalReleaseDate = LocalDate.parse("2023-04-29") + + private val submitCas2Application = SubmitCas2Application( + translatedDocument = {}, + applicationId = applicationId, + preferredAreas = "Leeds | Bradford", + hdcEligibilityDate = hdcEligibilityDate, + conditionalReleaseDate = conditionalReleaseDate, + telephoneNumber = "123", + ) + + @BeforeEach + fun setup() { + every { mockLockableApplicationRepository.acquirePessimisticLock(any()) } returns Cas2BailLockableApplicationEntity(UUID.randomUUID()) + every { mockObjectMapper.writeValueAsString(submitCas2Application.translatedDocument) } returns "{}" + every { mockDomainEventService.saveCas2ApplicationSubmittedDomainEvent(any()) } just Runs + } + + @Test + fun `returns NotFound when application doesn't exist`() { + val applicationId = UUID.fromString("fa6e97ce-7b9e-473c-883c-83b1c2af773d") + + every { mockApplicationRepository.findByIdOrNull(applicationId) } returns null + + assertThat(applicationService.submitCas2BailApplication(submitCas2Application, user) is AuthorisableActionResult.NotFound).isTrue + + assertEmailAndAssessmentsWereNotCreated() + } + + @Test + fun `returns Unauthorised when application doesn't belong to request user`() { + val differentUser = NomisUserEntityFactory() + .produce() + + val application = Cas2BailApplicationEntityFactory() + .withId(applicationId) + .withCreatedByUser(differentUser) + .produce() + + every { mockApplicationRepository.findByIdOrNull(applicationId) } returns application + every { mockJsonSchemaService.checkCas2BailSchemaOutdated(application) } returns + application + + assertThat(applicationService.submitCas2BailApplication(submitCas2Application, user) is AuthorisableActionResult.Unauthorised).isTrue + + assertEmailAndAssessmentsWereNotCreated() + } + + @Test + fun `returns GeneralValidationError when application schema is outdated`() { + val application = Cas2BailApplicationEntityFactory() + .withId(applicationId) + .withCreatedByUser(user) + .withSubmittedAt(null) + .produce() + .apply { + schemaUpToDate = false + } + + every { + mockApplicationRepository.findByIdOrNull(applicationId) + } returns application + every { mockJsonSchemaService.checkCas2BailSchemaOutdated(application) } returns application + + val result = applicationService.submitCas2BailApplication(submitCas2Application, user) + + assertThat(result is AuthorisableActionResult.Success).isTrue + result as AuthorisableActionResult.Success + + assertThat(result.entity is ValidatableActionResult.GeneralValidationError).isTrue + val validatableActionResult = result.entity as ValidatableActionResult.GeneralValidationError + + assertThat(validatableActionResult.message).isEqualTo("The schema version is outdated") + + assertEmailAndAssessmentsWereNotCreated() + } + + @Test + fun `returns GeneralValidationError when application has already been submitted`() { + val newestSchema = Cas2BailApplicationJsonSchemaEntityFactory().produce() + + val application = Cas2BailApplicationEntityFactory() + .withApplicationSchema(newestSchema) + .withId(applicationId) + .withCreatedByUser(user) + .withSubmittedAt(OffsetDateTime.now()) + .produce() + .apply { + schemaUpToDate = true + } + + every { + mockApplicationRepository.findByIdOrNull(applicationId) + } returns application + every { mockJsonSchemaService.checkCas2BailSchemaOutdated(application) } returns application + + val result = applicationService.submitCas2BailApplication(submitCas2Application, user) + + assertThat(result is AuthorisableActionResult.Success).isTrue + result as AuthorisableActionResult.Success + + assertThat(result.entity is ValidatableActionResult.GeneralValidationError).isTrue + val validatableActionResult = result.entity as ValidatableActionResult.GeneralValidationError + + assertThat(validatableActionResult.message).isEqualTo("This application has already been submitted") + + assertEmailAndAssessmentsWereNotCreated() + } + + @Test + fun `returns GeneralValidationError when application has already been abandoned`() { + val newestSchema = Cas2BailApplicationJsonSchemaEntityFactory().produce() + + val application = Cas2BailApplicationEntityFactory() + .withApplicationSchema(newestSchema) + .withId(applicationId) + .withCreatedByUser(user) + .withAbandonedAt(OffsetDateTime.now()) + .produce() + + every { + mockApplicationRepository.findByIdOrNull(applicationId) + } returns application + every { mockJsonSchemaService.checkCas2BailSchemaOutdated(application) } returns application + + val result = applicationService.submitCas2BailApplication(submitCas2Application, user) + + assertThat(result is AuthorisableActionResult.Success).isTrue + result as AuthorisableActionResult.Success + + assertThat(result.entity is ValidatableActionResult.GeneralValidationError).isTrue + val validatableActionResult = result.entity as ValidatableActionResult.GeneralValidationError + + assertThat(validatableActionResult.message).isEqualTo("This application has already been abandoned") + + assertEmailAndAssessmentsWereNotCreated() + } + + @Test + fun `throws a validation error if InmateDetails (for prison code) are not available`() { + val newestSchema = Cas2BailApplicationJsonSchemaEntityFactory().produce() + + val application = Cas2BailApplicationEntityFactory() + .withApplicationSchema(newestSchema) + .withId(applicationId) + .withCreatedByUser(user) + .withSubmittedAt(null) + .produce() + .apply { + schemaUpToDate = true + } + + every { + mockApplicationRepository.findByIdOrNull(any()) + } returns application + every { mockJsonSchemaService.checkCas2BailSchemaOutdated(any()) } returns + application + every { mockJsonSchemaService.validate(any(), any()) } returns true + + every { mockApplicationRepository.save(any()) } answers { + it.invocation.args[0] + as Cas2BailApplicationEntity + } + + val offenderDetails = OffenderDetailsSummaryFactory() + .withCrn(application.crn) + .produce() + + every { mockOffenderService.getOffenderByCrn(any()) } returns AuthorisableActionResult.Success( + offenderDetails, + ) + + // this call to the Prison API to find the referringPrisonCode when saving + // the application.submitted domain event *should* never 404 or otherwise fail, + // as when creating the application initially a similar call was made. + // If there is a problem with accessing the Prison API, we fail hard and + // abort our attempt to submit the application. + every { + mockOffenderService.getInmateDetailByNomsNumber(any(), any()) + } returns AuthorisableActionResult.NotFound() + + assertGeneralValidationError("Inmate Detail not found") + + assertEmailAndAssessmentsWereNotCreated() + } + + @Test + fun `throws an UpstreamApiException if prison code is null`() { + val newestSchema = Cas2BailApplicationJsonSchemaEntityFactory().produce() + + val application = Cas2BailApplicationEntityFactory() + .withApplicationSchema(newestSchema) + .withId(applicationId) + .withCreatedByUser(user) + .withSubmittedAt(null) + .produce() + .apply { + schemaUpToDate = true + } + + every { + mockApplicationRepository.findByIdOrNull(any()) + } returns application + every { mockJsonSchemaService.checkCas2BailSchemaOutdated(any()) } returns + application + every { mockJsonSchemaService.validate(any(), any()) } returns true + + every { mockApplicationRepository.save(any()) } answers { + it.invocation.args[0] + as Cas2BailApplicationEntity + } + + val offenderDetails = OffenderDetailsSummaryFactory() + .withCrn(application.crn) + .produce() + + every { mockOffenderService.getOffenderByCrn(any()) } returns AuthorisableActionResult.Success( + offenderDetails, + ) + + // this call to the Prison API to find the referringPrisonCode when saving + // the application.submitted domain event *should* always have a prison code, + // but we need to account for possibility it may be missing. + // If there is a problem with accessing the Prison API, we fail hard and + // abort our attempt to submit the application and return a validation message. + every { + mockOffenderService.getInmateDetailByNomsNumber(any(), any()) + } returns AuthorisableActionResult.Success(InmateDetailFactory().produce()) + + assertGeneralValidationError("No prison code available") + + assertEmailAndAssessmentsWereNotCreated() + } + + private fun assertGeneralValidationError(message: String) { + val result = applicationService.submitCas2BailApplication(submitCas2Application, user) + assertThat(result is AuthorisableActionResult.Success).isTrue + result as AuthorisableActionResult.Success + + assertThat(result.entity is ValidatableActionResult.GeneralValidationError).isTrue + val error = result.entity as ValidatableActionResult.GeneralValidationError + + assertThat(error.message).isEqualTo(message) + } + + private fun assertEmailAndAssessmentsWereNotCreated() { + verify(exactly = 0) { mockEmailNotificationService.sendEmail(any(), any(), any()) } + verify(exactly = 0) { mockAssessmentService.createCas2BailAssessment(any()) } + } + + @Test + fun `returns Success and stores event`() { + val newestSchema = Cas2BailApplicationJsonSchemaEntityFactory().produce() + + val application = Cas2BailApplicationEntityFactory() + .withApplicationSchema(newestSchema) + .withId(applicationId) + .withCreatedByUser(user) + .withSubmittedAt(null) + .produce() + .apply { + schemaUpToDate = true + } + + every { + mockApplicationRepository.findByIdOrNull(applicationId) + } returns application + every { mockJsonSchemaService.checkCas2BailSchemaOutdated(application) } returns + application + every { mockJsonSchemaService.validate(newestSchema, application.data!!) } returns true + + val inmateDetail = InmateDetailFactory() + .withAssignedLivingUnit( + AssignedLivingUnit( + agencyId = "BRI", + locationId = 1234, + description = "description", + agencyName = "HMP Bristol", + ), + ) + .produce() + + every { + mockOffenderService.getInmateDetailByNomsNumber( + application.crn, + application.nomsNumber.toString(), + ) + } returns AuthorisableActionResult.Success(inmateDetail) + + every { mockNotifyConfig.templates.cas2ApplicationSubmitted } returns "abc123" + every { mockNotifyConfig.emailAddresses.cas2Assessors } returns "exampleAssessorInbox@example.com" + every { mockNotifyConfig.emailAddresses.cas2ReplyToId } returns "def456" + every { mockEmailNotificationService.sendEmail(any(), any(), any(), any()) } just Runs + + every { mockApplicationRepository.save(any()) } answers { + it.invocation.args[0] + as Cas2BailApplicationEntity + } + + val offenderDetails = OffenderDetailsSummaryFactory() + .withGender("male") + .withCrn(application.crn) + .produce() + + every { mockOffenderService.getOffenderByCrn(application.crn) } returns AuthorisableActionResult.Success( + offenderDetails, + ) + + every { mockAssessmentService.createCas2BailAssessment(any()) } returns any() + + val result = applicationService.submitCas2BailApplication(submitCas2Application, user) + + assertThat(result is AuthorisableActionResult.Success).isTrue + result as AuthorisableActionResult.Success + + assertThat(result.entity is ValidatableActionResult.Success).isTrue + val validatableActionResult = result.entity as ValidatableActionResult.Success + val persistedApplication = validatableActionResult.entity + + assertThat(persistedApplication.crn).isEqualTo(application.crn) + assertThat(persistedApplication.preferredAreas).isEqualTo("Leeds | Bradford") + assertThat(persistedApplication.hdcEligibilityDate).isEqualTo(hdcEligibilityDate) + assertThat(persistedApplication.conditionalReleaseDate).isEqualTo(conditionalReleaseDate) + + verify { mockApplicationRepository.save(any()) } + + verify(exactly = 1) { + mockDomainEventService.saveCas2ApplicationSubmittedDomainEvent( + match { + val data = it.data.eventDetails + + it.applicationId == application.id && + data.personReference.noms == application.nomsNumber && + data.personReference.crn == application.crn && + data.applicationUrl == "http://frontend/applications/${application.id}" && + data.submittedBy.staffMember.username == username && + data.referringPrisonCode == "BRI" && + data.preferredAreas == "Leeds | Bradford" && + data.hdcEligibilityDate == hdcEligibilityDate && + data.conditionalReleaseDate == conditionalReleaseDate + }, + ) + } + + verify(exactly = 1) { + mockEmailNotificationService.sendEmail( + "exampleAssessorInbox@example.com", + "abc123", + match { + it["name"] == user.name && + it["email"] == user.email && + it["prisonNumber"] == application.nomsNumber && + it["telephoneNumber"] == application.telephoneNumber && + it["applicationUrl"] == "http://frontend/assess/applications/$applicationId/overview" + }, + "def456", + ) + } + + verify(exactly = 1) { mockAssessmentService.createCas2BailAssessment(persistedApplication) } + } + } + + + private fun userWithUsername(username: String) = NomisUserEntityFactory() + .withNomisUsername(username) + .produce() + +} \ No newline at end of file From 0b40d880cbf0978f212224869ca95ba45c4d1483 Mon Sep 17 00:00:00 2001 From: garethCAS2 Date: Fri, 29 Nov 2024 14:55:16 +0000 Subject: [PATCH 07/37] unit tests for Cas2BailAssessments and Cas2UserAccessService --- .../Cas2BailApplicationServiceTest.kt | 300 +++++++++--------- .../cas2bail/Cas2BailAssessmentServiceTest.kt | 138 ++++++++ .../cas2bail/Cas2BailUserAccessServiceTest.kt | 140 ++++++++ 3 files changed, 427 insertions(+), 151 deletions(-) create mode 100644 src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailAssessmentServiceTest.kt create mode 100644 src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailUserAccessServiceTest.kt diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailApplicationServiceTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailApplicationServiceTest.kt index 0f5fb00635..bb64014eba 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailApplicationServiceTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailApplicationServiceTest.kt @@ -18,9 +18,7 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.SubmitCas2Appl import uk.gov.justice.digital.hmpps.approvedpremisesapi.config.NotifyConfig import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.* import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.cas2bail.Cas2BailApplicationEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2BailApplicationJsonSchemaEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2LockableApplicationEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.* import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.prisonsapi.AssignedLivingUnit import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.AuthorisableActionResult @@ -39,29 +37,29 @@ import java.time.OffsetDateTime import java.util.* class Cas2BailApplicationServiceTest { - private val mockApplicationRepository = mockk() - private val mockLockableApplicationRepository = mockk() - private val mockApplicationSummaryRepository = mockk() + private val mockCas2BailApplicationRepository = mockk() + private val mockCas2BailLockableApplicationRepository = mockk() + private val mockCas2BailApplicationSummaryRepository = mockk() private val mockJsonSchemaService = mockk() private val mockOffenderService = mockk() - private val mockUserAccessService = mockk() + private val mockCas2BailUserAccessService = mockk() private val mockDomainEventService = mockk() private val mockEmailNotificationService = mockk() - private val mockAssessmentService = mockk() + private val mockCas2BailAssessmentService = mockk() private val mockObjectMapper = mockk() private val mockNotifyConfig = mockk() - private val applicationService = Cas2BailApplicationService( - mockApplicationRepository, - mockLockableApplicationRepository, - mockApplicationSummaryRepository, + private val cas2BailApplicationService = Cas2BailApplicationService( + mockCas2BailApplicationRepository, + mockCas2BailLockableApplicationRepository, + mockCas2BailApplicationSummaryRepository, mockJsonSchemaService, mockOffenderService, - mockUserAccessService, + mockCas2BailUserAccessService, mockDomainEventService, mockEmailNotificationService, - mockAssessmentService, + mockCas2BailAssessmentService, mockNotifyConfig, mockObjectMapper, "http://frontend/applications/#id", @@ -69,10 +67,10 @@ class Cas2BailApplicationServiceTest { ) @Nested - inner class GetAllSubmittedApplicationsForAssessor { + inner class GetAllSubmittedCas2BailApplicationsForAssessor { @Test fun `returns Success result with entity from db`() { - val applicationSummary = Cas2BailApplicationSummaryEntity( + val cas2BailApplicationSummary = Cas2BailApplicationSummaryEntity( id = UUID.fromString("2f838a8c-dffc-48a3-9536-f0e95985e809"), crn = randomStringMultiCaseWithNumbers(6), nomsNumber = randomStringMultiCaseWithNumbers(6), @@ -94,12 +92,12 @@ class Cas2BailApplicationServiceTest { mockkStatic(PageRequest::class) every { PageRequest.of(2, 10, Sort.by("submitted_at").ascending()) } returns pageRequest - every { page.content } returns listOf(applicationSummary) + every { page.content } returns listOf(cas2BailApplicationSummary) every { page.totalPages } returns 10 every { page.totalElements } returns 100 every { - mockApplicationSummaryRepository.findBySubmittedAtIsNotNull( + mockCas2BailApplicationSummaryRepository.findBySubmittedAtIsNotNull( PageRequest.of( 2, 10, @@ -108,9 +106,9 @@ class Cas2BailApplicationServiceTest { ) } returns page - val (applicationSummaries, metadata) = applicationService.getAllSubmittedCas2BailApplicationsForAssessor(pageCriteria) + val (applicationSummaries, metadata) = cas2BailApplicationService.getAllSubmittedCas2BailApplicationsForAssessor(pageCriteria) - assertThat(applicationSummaries).isEqualTo(listOf(applicationSummary)) + assertThat(applicationSummaries).isEqualTo(listOf(cas2BailApplicationSummary)) assertThat(metadata?.currentPage).isEqualTo(3) assertThat(metadata?.pageSize).isEqualTo(10) assertThat(metadata?.totalPages).isEqualTo(10) @@ -119,8 +117,8 @@ class Cas2BailApplicationServiceTest { } @Nested - inner class GetApplicationsWithPrisonCode { - val applicationSummary = Cas2BailApplicationSummaryEntity( + inner class GetCas2BailApplicationsWithPrisonCode { + val cas2BailApplicationSummary = Cas2BailApplicationSummaryEntity( id = UUID.fromString("2f838a8c-dffc-48a3-9536-f0e95985e809"), crn = randomStringMultiCaseWithNumbers(6), nomsNumber = randomStringMultiCaseWithNumbers(6), @@ -138,14 +136,14 @@ class Cas2BailApplicationServiceTest { val user = NomisUserEntityFactory().produce() val prisonCode = "BRI" - fun testPrisonCodeWithIsSubmitted(isSubmitted: Boolean?) { - every { page.content } returns listOf(applicationSummary) + private fun testPrisonCodeWithIsSubmitted(isSubmitted: Boolean?) { + every { page.content } returns listOf(cas2BailApplicationSummary) every { page.totalPages } returns 10 every { page.totalElements } returns 100 - val (applicationSummaries, _) = applicationService.getCas2BailApplications(prisonCode, isSubmitted, user, pageCriteria) + val (applicationSummaries, _) = cas2BailApplicationService.getCas2BailApplications(prisonCode, isSubmitted, user, pageCriteria) - assertThat(applicationSummaries).isEqualTo(listOf(applicationSummary)) + assertThat(applicationSummaries).isEqualTo(listOf(cas2BailApplicationSummary)) } @Test @@ -153,7 +151,7 @@ class Cas2BailApplicationServiceTest { PaginationConfig(defaultPageSize = 10).postInit() every { - mockApplicationSummaryRepository.findByPrisonCode( + mockCas2BailApplicationSummaryRepository.findByPrisonCode( prisonCode, getPageableOrAllPages(pageCriteria), ) @@ -167,7 +165,7 @@ class Cas2BailApplicationServiceTest { PaginationConfig(defaultPageSize = 10).postInit() every { - mockApplicationSummaryRepository.findByPrisonCodeAndSubmittedAtIsNotNull( + mockCas2BailApplicationSummaryRepository.findByPrisonCodeAndSubmittedAtIsNotNull( prisonCode, getPageableOrAllPages(pageCriteria), ) @@ -181,7 +179,7 @@ class Cas2BailApplicationServiceTest { PaginationConfig(defaultPageSize = 10).postInit() every { - mockApplicationSummaryRepository.findByPrisonCodeAndSubmittedAtIsNull( + mockCas2BailApplicationSummaryRepository.findByPrisonCodeAndSubmittedAtIsNull( prisonCode, getPageableOrAllPages(pageCriteria), ) @@ -192,23 +190,23 @@ class Cas2BailApplicationServiceTest { } @Nested - inner class GetApplicationForUser { + inner class GetCas2BailApplicationForUser { @Test - fun `where application does not exist returns NotFound result`() { + fun `where cas2bail application does not exist returns NotFound result`() { val user = NomisUserEntityFactory().produce() val applicationId = UUID.fromString("c1750938-19fc-48a1-9ae9-f2e119ffc1f4") - every { mockApplicationRepository.findByIdOrNull(applicationId) } returns null + every { mockCas2BailApplicationRepository.findByIdOrNull(applicationId) } returns null - assertThat(applicationService.getCas2BailApplicationForUser(applicationId, user) is AuthorisableActionResult.NotFound).isTrue + assertThat(cas2BailApplicationService.getCas2BailApplicationForUser(applicationId, user) is AuthorisableActionResult.NotFound).isTrue } @Test - fun `where application is abandoned returns NotFound result`() { + fun `where cas2bail application is abandoned returns NotFound result`() { val user = NomisUserEntityFactory().produce() val applicationId = UUID.fromString("c1750938-19fc-48a1-9ae9-f2e119ffc1f4") - every { mockApplicationRepository.findByIdOrNull(any()) } returns + every { mockCas2BailApplicationRepository.findByIdOrNull(any()) } returns Cas2BailApplicationEntityFactory() .withCreatedByUser( NomisUserEntityFactory() @@ -217,16 +215,16 @@ class Cas2BailApplicationServiceTest { .withAbandonedAt(OffsetDateTime.now()) .produce() - assertThat(applicationService.getCas2BailApplicationForUser(applicationId, user) is AuthorisableActionResult.NotFound).isTrue + assertThat(cas2BailApplicationService.getCas2BailApplicationForUser(applicationId, user) is AuthorisableActionResult.NotFound).isTrue } @Test - fun `where user cannot access the application returns Unauthorised result`() { + fun `where user cannot access the cas2bail application returns Unauthorised result`() { val user = NomisUserEntityFactory() .produce() val applicationId = UUID.fromString("c1750938-19fc-48a1-9ae9-f2e119ffc1f4") - every { mockApplicationRepository.findByIdOrNull(any()) } returns + every { mockCas2BailApplicationRepository.findByIdOrNull(any()) } returns Cas2BailApplicationEntityFactory() .withCreatedByUser( NomisUserEntityFactory() @@ -234,14 +232,14 @@ class Cas2BailApplicationServiceTest { ) .produce() - every { mockUserAccessService.userCanViewCas2BailApplication(any(), any()) } returns false + every { mockCas2BailUserAccessService.userCanViewCas2BailApplication(any(), any()) } returns false - assertThat(applicationService.getCas2BailApplicationForUser(applicationId, user) is AuthorisableActionResult.Unauthorised).isTrue + assertThat(cas2BailApplicationService.getCas2BailApplicationForUser(applicationId, user) is AuthorisableActionResult.Unauthorised).isTrue } @Test - fun `where user can access the application returns Success result with entity from db`() { + fun `where user can access the cas2bail application returns Success result with entity from db`() { val distinguishedName = "SOMEPERSON" val userId = UUID.fromString("239b5e41-f83e-409e-8fc0-8f1e058d417e") val applicationId = UUID.fromString("c1750938-19fc-48a1-9ae9-f2e119ffc1f4") @@ -255,7 +253,7 @@ class Cas2BailApplicationServiceTest { .withNomisUsername(distinguishedName) .produce() - val applicationEntity = Cas2BailApplicationEntityFactory() + val cas2BailApplicationEntity = Cas2BailApplicationEntityFactory() .withCreatedByUser(userEntity) .withApplicationSchema(newestJsonSchema) .produce() @@ -264,15 +262,15 @@ class Cas2BailApplicationServiceTest { it.invocation .args[0] as Cas2BailApplicationEntity } - every { mockApplicationRepository.findByIdOrNull(any()) } returns applicationEntity - every { mockUserAccessService.userCanViewCas2BailApplication(any(), any()) } returns true + every { mockCas2BailApplicationRepository.findByIdOrNull(any()) } returns cas2BailApplicationEntity + every { mockCas2BailUserAccessService.userCanViewCas2BailApplication(any(), any()) } returns true - val result = applicationService.getCas2BailApplicationForUser(applicationId, userEntity) + val result = cas2BailApplicationService.getCas2BailApplicationForUser(applicationId, userEntity) assertThat(result is AuthorisableActionResult.Success).isTrue result as AuthorisableActionResult.Success - assertThat(result.entity).isEqualTo(applicationEntity) + assertThat(result.entity).isEqualTo(cas2BailApplicationEntity) } } @@ -287,7 +285,7 @@ class Cas2BailApplicationServiceTest { val user = userWithUsername(username) - val result = applicationService.createCas2BailApplication(crn, user, "jwt") + val result = cas2BailApplicationService.createCas2BailApplication(crn, user, "jwt") assertThat(result is ValidatableActionResult.FieldValidationError).isTrue result as ValidatableActionResult.FieldValidationError @@ -303,7 +301,7 @@ class Cas2BailApplicationServiceTest { val user = userWithUsername(username) - val result = applicationService.createCas2BailApplication(crn, user, "jwt") + val result = cas2BailApplicationService.createCas2BailApplication(crn, user, "jwt") assertThat(result is ValidatableActionResult.FieldValidationError).isTrue result as ValidatableActionResult.FieldValidationError @@ -315,7 +313,7 @@ class Cas2BailApplicationServiceTest { val crn = "CRN345" val username = "SOMEPERSON" - val schema = Cas2BailApplicationJsonSchemaEntityFactory().produce() + val cas2BailApplicationSchema = Cas2BailApplicationJsonSchemaEntityFactory().produce() val user = userWithUsername(username) @@ -323,13 +321,13 @@ class Cas2BailApplicationServiceTest { OffenderDetailsSummaryFactory().produce(), ) - every { mockJsonSchemaService.getNewestSchema(Cas2BailApplicationJsonSchemaEntity::class.java) } returns schema - every { mockApplicationRepository.save(any()) } answers { + every { mockJsonSchemaService.getNewestSchema(Cas2BailApplicationJsonSchemaEntity::class.java) } returns cas2BailApplicationSchema + every { mockCas2BailApplicationRepository.save(any()) } answers { it.invocation.args[0] as Cas2BailApplicationEntity } - val result = applicationService.createCas2BailApplication(crn, user, "jwt") + val result = cas2BailApplicationService.createCas2BailApplication(crn, user, "jwt") assertThat(result is ValidatableActionResult.Success).isTrue result as ValidatableActionResult.Success @@ -343,13 +341,13 @@ class Cas2BailApplicationServiceTest { val user = NomisUserEntityFactory().produce() @Test - fun `returns NotFound when application doesn't exist`() { + fun `returns NotFound when cas2bail application doesn't exist`() { val applicationId = UUID.fromString("fa6e97ce-7b9e-473c-883c-83b1c2af773d") - every { mockApplicationRepository.findByIdOrNull(applicationId) } returns null + every { mockCas2BailApplicationRepository.findByIdOrNull(applicationId) } returns null assertThat( - applicationService.updateCas2BailApplication( + cas2BailApplicationService.updateCas2BailApplication( applicationId = applicationId, data = "{}", user = user, @@ -361,7 +359,7 @@ class Cas2BailApplicationServiceTest { fun `returns Unauthorised when application doesn't belong to request user`() { val applicationId = UUID.fromString("fa6e97ce-7b9e-473c-883c-83b1c2af773d") - val application = Cas2BailApplicationEntityFactory() + val cas2BailApplication = Cas2BailApplicationEntityFactory() .withId(applicationId) .withYieldedCreatedByUser { NomisUserEntityFactory() @@ -369,13 +367,13 @@ class Cas2BailApplicationServiceTest { } .produce() - every { mockApplicationRepository.findByIdOrNull(applicationId) } returns - application - every { mockJsonSchemaService.checkCas2BailSchemaOutdated(application) } returns - application + every { mockCas2BailApplicationRepository.findByIdOrNull(applicationId) } returns + cas2BailApplication + every { mockJsonSchemaService.checkCas2BailSchemaOutdated(cas2BailApplication) } returns + cas2BailApplication assertThat( - applicationService.updateCas2BailApplication( + cas2BailApplicationService.updateCas2BailApplication( applicationId = applicationId, data = "{}", user = user, @@ -384,12 +382,12 @@ class Cas2BailApplicationServiceTest { } @Test - fun `returns GeneralValidationError when application has already been submitted`() { + fun `returns GeneralValidationError when cas2bail application has already been submitted`() { val applicationId = UUID.fromString("fa6e97ce-7b9e-473c-883c-83b1c2af773d") val newestSchema = Cas2BailApplicationJsonSchemaEntityFactory().produce() - val application = Cas2BailApplicationEntityFactory() + val cas2BailApplication = Cas2BailApplicationEntityFactory() .withApplicationSchema(newestSchema) .withId(applicationId) .withCreatedByUser(user) @@ -399,12 +397,12 @@ class Cas2BailApplicationServiceTest { schemaUpToDate = true } - every { mockApplicationRepository.findByIdOrNull(applicationId) } returns - application - every { mockJsonSchemaService.checkCas2BailSchemaOutdated(application) } returns - application + every { mockCas2BailApplicationRepository.findByIdOrNull(applicationId) } returns + cas2BailApplication + every { mockJsonSchemaService.checkCas2BailSchemaOutdated(cas2BailApplication) } returns + cas2BailApplication - val result = applicationService.updateCas2BailApplication( + val result = cas2BailApplicationService.updateCas2BailApplication( applicationId = applicationId, data = "{}", user = user, @@ -425,7 +423,7 @@ class Cas2BailApplicationServiceTest { val newestSchema = Cas2BailApplicationJsonSchemaEntityFactory().produce() - val application = Cas2BailApplicationEntityFactory() + val cas2BailApplication = Cas2BailApplicationEntityFactory() .withApplicationSchema(newestSchema) .withId(applicationId) .withCreatedByUser(user) @@ -435,10 +433,10 @@ class Cas2BailApplicationServiceTest { schemaUpToDate = true } - every { mockApplicationRepository.findByIdOrNull(applicationId) } returns application - every { mockJsonSchemaService.checkCas2BailSchemaOutdated(application) } returns application + every { mockCas2BailApplicationRepository.findByIdOrNull(applicationId) } returns cas2BailApplication + every { mockJsonSchemaService.checkCas2BailSchemaOutdated(cas2BailApplication) } returns cas2BailApplication - val result = applicationService.updateCas2BailApplication( + val result = cas2BailApplicationService.updateCas2BailApplication( applicationId = applicationId, data = "{}", user = user, @@ -457,7 +455,7 @@ class Cas2BailApplicationServiceTest { fun `returns GeneralValidationError when application schema is outdated`() { val applicationId = UUID.fromString("fa6e97ce-7b9e-473c-883c-83b1c2af773d") - val application = Cas2BailApplicationEntityFactory() + val cas2BailApplication = Cas2BailApplicationEntityFactory() .withId(applicationId) .withCreatedByUser(user) .withSubmittedAt(null) @@ -466,10 +464,10 @@ class Cas2BailApplicationServiceTest { schemaUpToDate = false } - every { mockApplicationRepository.findByIdOrNull(applicationId) } returns application - every { mockJsonSchemaService.checkCas2BailSchemaOutdated(application) } returns application + every { mockCas2BailApplicationRepository.findByIdOrNull(applicationId) } returns cas2BailApplication + every { mockJsonSchemaService.checkCas2BailSchemaOutdated(cas2BailApplication) } returns cas2BailApplication - val result = applicationService.updateCas2BailApplication( + val result = cas2BailApplicationService.updateCas2BailApplication( applicationId = applicationId, data = "{}", user = user, @@ -505,7 +503,7 @@ class Cas2BailApplicationServiceTest { schemaUpToDate = true } - every { mockApplicationRepository.findByIdOrNull(applicationId) } returns + every { mockCas2BailApplicationRepository.findByIdOrNull(applicationId) } returns application every { mockJsonSchemaService.checkCas2BailSchemaOutdated(application) } returns application @@ -516,12 +514,12 @@ class Cas2BailApplicationServiceTest { ) } returns newestSchema every { mockJsonSchemaService.validate(newestSchema, updatedData) } returns true - every { mockApplicationRepository.save(any()) } answers { + every { mockCas2BailApplicationRepository.save(any()) } answers { it.invocation.args[0] as Cas2BailApplicationEntity } - val result = applicationService.updateCas2BailApplication( + val result = cas2BailApplicationService.updateCas2BailApplication( applicationId = applicationId, data = updatedData, user = user, @@ -564,7 +562,7 @@ class Cas2BailApplicationServiceTest { schemaUpToDate = true } - every { mockApplicationRepository.findByIdOrNull(applicationId) } returns + every { mockCas2BailApplicationRepository.findByIdOrNull(applicationId) } returns application every { mockJsonSchemaService.checkCas2BailSchemaOutdated(application) } returns application @@ -575,12 +573,12 @@ class Cas2BailApplicationServiceTest { ) } returns newestSchema every { mockJsonSchemaService.validate(newestSchema, updatedData) } returns true - every { mockApplicationRepository.save(any()) } answers { + every { mockCas2BailApplicationRepository.save(any()) } answers { it.invocation.args[0] as Cas2BailApplicationEntity } - val result = applicationService.updateCas2BailApplication( + val result = cas2BailApplicationService.updateCas2BailApplication( applicationId = applicationId, data = updatedData, user = user, @@ -606,10 +604,10 @@ class Cas2BailApplicationServiceTest { fun `returns NotFound when application doesn't exist`() { val applicationId = UUID.fromString("fa6e97ce-7b9e-473c-883c-83b1c2af773d") - every { mockApplicationRepository.findByIdOrNull(applicationId) } returns null + every { mockCas2BailApplicationRepository.findByIdOrNull(applicationId) } returns null assertThat( - applicationService.abandonCas2BailApplication( + cas2BailApplicationService.abandonCas2BailApplication( applicationId = applicationId, user = user, ) is AuthorisableActionResult.NotFound, @@ -628,11 +626,11 @@ class Cas2BailApplicationServiceTest { } .produce() - every { mockApplicationRepository.findByIdOrNull(applicationId) } returns + every { mockCas2BailApplicationRepository.findByIdOrNull(applicationId) } returns application assertThat( - applicationService.abandonCas2BailApplication( + cas2BailApplicationService.abandonCas2BailApplication( applicationId = applicationId, user = user, ) is AuthorisableActionResult.Unauthorised, @@ -655,10 +653,10 @@ class Cas2BailApplicationServiceTest { schemaUpToDate = true } - every { mockApplicationRepository.findByIdOrNull(applicationId) } returns + every { mockCas2BailApplicationRepository.findByIdOrNull(applicationId) } returns application - val result = applicationService.abandonCas2BailApplication( + val result = cas2BailApplicationService.abandonCas2BailApplication( applicationId = applicationId, user = user, ) @@ -688,10 +686,10 @@ class Cas2BailApplicationServiceTest { schemaUpToDate = true } - every { mockApplicationRepository.findByIdOrNull(applicationId) } returns + every { mockCas2BailApplicationRepository.findByIdOrNull(applicationId) } returns application - val result = applicationService.abandonCas2BailApplication( + val result = cas2BailApplicationService.abandonCas2BailApplication( applicationId = applicationId, user = user, ) @@ -723,14 +721,14 @@ class Cas2BailApplicationServiceTest { schemaUpToDate = true } - every { mockApplicationRepository.findByIdOrNull(applicationId) } returns + every { mockCas2BailApplicationRepository.findByIdOrNull(applicationId) } returns application - every { mockApplicationRepository.save(any()) } answers { + every { mockCas2BailApplicationRepository.save(any()) } answers { it.invocation.args[0] as Cas2BailApplicationEntity } - val result = applicationService.abandonCas2BailApplication( + val result = cas2BailApplicationService.abandonCas2BailApplication( applicationId = applicationId, user = user, ) @@ -768,7 +766,7 @@ class Cas2BailApplicationServiceTest { @BeforeEach fun setup() { - every { mockLockableApplicationRepository.acquirePessimisticLock(any()) } returns Cas2BailLockableApplicationEntity(UUID.randomUUID()) + every { mockCas2BailLockableApplicationRepository.acquirePessimisticLock(any()) } returns Cas2BailLockableApplicationEntity(UUID.randomUUID()) every { mockObjectMapper.writeValueAsString(submitCas2Application.translatedDocument) } returns "{}" every { mockDomainEventService.saveCas2ApplicationSubmittedDomainEvent(any()) } just Runs } @@ -777,9 +775,9 @@ class Cas2BailApplicationServiceTest { fun `returns NotFound when application doesn't exist`() { val applicationId = UUID.fromString("fa6e97ce-7b9e-473c-883c-83b1c2af773d") - every { mockApplicationRepository.findByIdOrNull(applicationId) } returns null + every { mockCas2BailApplicationRepository.findByIdOrNull(applicationId) } returns null - assertThat(applicationService.submitCas2BailApplication(submitCas2Application, user) is AuthorisableActionResult.NotFound).isTrue + assertThat(cas2BailApplicationService.submitCas2BailApplication(submitCas2Application, user) is AuthorisableActionResult.NotFound).isTrue assertEmailAndAssessmentsWereNotCreated() } @@ -789,23 +787,23 @@ class Cas2BailApplicationServiceTest { val differentUser = NomisUserEntityFactory() .produce() - val application = Cas2BailApplicationEntityFactory() + val cas2BailApplication = Cas2BailApplicationEntityFactory() .withId(applicationId) .withCreatedByUser(differentUser) .produce() - every { mockApplicationRepository.findByIdOrNull(applicationId) } returns application - every { mockJsonSchemaService.checkCas2BailSchemaOutdated(application) } returns - application + every { mockCas2BailApplicationRepository.findByIdOrNull(applicationId) } returns cas2BailApplication + every { mockJsonSchemaService.checkCas2BailSchemaOutdated(cas2BailApplication) } returns + cas2BailApplication - assertThat(applicationService.submitCas2BailApplication(submitCas2Application, user) is AuthorisableActionResult.Unauthorised).isTrue + assertThat(cas2BailApplicationService.submitCas2BailApplication(submitCas2Application, user) is AuthorisableActionResult.Unauthorised).isTrue assertEmailAndAssessmentsWereNotCreated() } @Test fun `returns GeneralValidationError when application schema is outdated`() { - val application = Cas2BailApplicationEntityFactory() + val cas2BailApplication = Cas2BailApplicationEntityFactory() .withId(applicationId) .withCreatedByUser(user) .withSubmittedAt(null) @@ -815,11 +813,11 @@ class Cas2BailApplicationServiceTest { } every { - mockApplicationRepository.findByIdOrNull(applicationId) - } returns application - every { mockJsonSchemaService.checkCas2BailSchemaOutdated(application) } returns application + mockCas2BailApplicationRepository.findByIdOrNull(applicationId) + } returns cas2BailApplication + every { mockJsonSchemaService.checkCas2BailSchemaOutdated(cas2BailApplication) } returns cas2BailApplication - val result = applicationService.submitCas2BailApplication(submitCas2Application, user) + val result = cas2BailApplicationService.submitCas2BailApplication(submitCas2Application, user) assertThat(result is AuthorisableActionResult.Success).isTrue result as AuthorisableActionResult.Success @@ -836,7 +834,7 @@ class Cas2BailApplicationServiceTest { fun `returns GeneralValidationError when application has already been submitted`() { val newestSchema = Cas2BailApplicationJsonSchemaEntityFactory().produce() - val application = Cas2BailApplicationEntityFactory() + val cas2BailApplication = Cas2BailApplicationEntityFactory() .withApplicationSchema(newestSchema) .withId(applicationId) .withCreatedByUser(user) @@ -847,11 +845,11 @@ class Cas2BailApplicationServiceTest { } every { - mockApplicationRepository.findByIdOrNull(applicationId) - } returns application - every { mockJsonSchemaService.checkCas2BailSchemaOutdated(application) } returns application + mockCas2BailApplicationRepository.findByIdOrNull(applicationId) + } returns cas2BailApplication + every { mockJsonSchemaService.checkCas2BailSchemaOutdated(cas2BailApplication) } returns cas2BailApplication - val result = applicationService.submitCas2BailApplication(submitCas2Application, user) + val result = cas2BailApplicationService.submitCas2BailApplication(submitCas2Application, user) assertThat(result is AuthorisableActionResult.Success).isTrue result as AuthorisableActionResult.Success @@ -868,7 +866,7 @@ class Cas2BailApplicationServiceTest { fun `returns GeneralValidationError when application has already been abandoned`() { val newestSchema = Cas2BailApplicationJsonSchemaEntityFactory().produce() - val application = Cas2BailApplicationEntityFactory() + val cas2BailApplication = Cas2BailApplicationEntityFactory() .withApplicationSchema(newestSchema) .withId(applicationId) .withCreatedByUser(user) @@ -876,11 +874,11 @@ class Cas2BailApplicationServiceTest { .produce() every { - mockApplicationRepository.findByIdOrNull(applicationId) - } returns application - every { mockJsonSchemaService.checkCas2BailSchemaOutdated(application) } returns application + mockCas2BailApplicationRepository.findByIdOrNull(applicationId) + } returns cas2BailApplication + every { mockJsonSchemaService.checkCas2BailSchemaOutdated(cas2BailApplication) } returns cas2BailApplication - val result = applicationService.submitCas2BailApplication(submitCas2Application, user) + val result = cas2BailApplicationService.submitCas2BailApplication(submitCas2Application, user) assertThat(result is AuthorisableActionResult.Success).isTrue result as AuthorisableActionResult.Success @@ -897,7 +895,7 @@ class Cas2BailApplicationServiceTest { fun `throws a validation error if InmateDetails (for prison code) are not available`() { val newestSchema = Cas2BailApplicationJsonSchemaEntityFactory().produce() - val application = Cas2BailApplicationEntityFactory() + val cas2BailApplication = Cas2BailApplicationEntityFactory() .withApplicationSchema(newestSchema) .withId(applicationId) .withCreatedByUser(user) @@ -908,19 +906,19 @@ class Cas2BailApplicationServiceTest { } every { - mockApplicationRepository.findByIdOrNull(any()) - } returns application + mockCas2BailApplicationRepository.findByIdOrNull(any()) + } returns cas2BailApplication every { mockJsonSchemaService.checkCas2BailSchemaOutdated(any()) } returns - application + cas2BailApplication every { mockJsonSchemaService.validate(any(), any()) } returns true - every { mockApplicationRepository.save(any()) } answers { + every { mockCas2BailApplicationRepository.save(any()) } answers { it.invocation.args[0] as Cas2BailApplicationEntity } val offenderDetails = OffenderDetailsSummaryFactory() - .withCrn(application.crn) + .withCrn(cas2BailApplication.crn) .produce() every { mockOffenderService.getOffenderByCrn(any()) } returns AuthorisableActionResult.Success( @@ -945,7 +943,7 @@ class Cas2BailApplicationServiceTest { fun `throws an UpstreamApiException if prison code is null`() { val newestSchema = Cas2BailApplicationJsonSchemaEntityFactory().produce() - val application = Cas2BailApplicationEntityFactory() + val cas2BailApplication = Cas2BailApplicationEntityFactory() .withApplicationSchema(newestSchema) .withId(applicationId) .withCreatedByUser(user) @@ -956,19 +954,19 @@ class Cas2BailApplicationServiceTest { } every { - mockApplicationRepository.findByIdOrNull(any()) - } returns application + mockCas2BailApplicationRepository.findByIdOrNull(any()) + } returns cas2BailApplication every { mockJsonSchemaService.checkCas2BailSchemaOutdated(any()) } returns - application + cas2BailApplication every { mockJsonSchemaService.validate(any(), any()) } returns true - every { mockApplicationRepository.save(any()) } answers { + every { mockCas2BailApplicationRepository.save(any()) } answers { it.invocation.args[0] as Cas2BailApplicationEntity } val offenderDetails = OffenderDetailsSummaryFactory() - .withCrn(application.crn) + .withCrn(cas2BailApplication.crn) .produce() every { mockOffenderService.getOffenderByCrn(any()) } returns AuthorisableActionResult.Success( @@ -990,7 +988,7 @@ class Cas2BailApplicationServiceTest { } private fun assertGeneralValidationError(message: String) { - val result = applicationService.submitCas2BailApplication(submitCas2Application, user) + val result = cas2BailApplicationService.submitCas2BailApplication(submitCas2Application, user) assertThat(result is AuthorisableActionResult.Success).isTrue result as AuthorisableActionResult.Success @@ -1002,14 +1000,14 @@ class Cas2BailApplicationServiceTest { private fun assertEmailAndAssessmentsWereNotCreated() { verify(exactly = 0) { mockEmailNotificationService.sendEmail(any(), any(), any()) } - verify(exactly = 0) { mockAssessmentService.createCas2BailAssessment(any()) } + verify(exactly = 0) { mockCas2BailAssessmentService.createCas2BailAssessment(any()) } } @Test fun `returns Success and stores event`() { val newestSchema = Cas2BailApplicationJsonSchemaEntityFactory().produce() - val application = Cas2BailApplicationEntityFactory() + val cas2BailApplication = Cas2BailApplicationEntityFactory() .withApplicationSchema(newestSchema) .withId(applicationId) .withCreatedByUser(user) @@ -1020,11 +1018,11 @@ class Cas2BailApplicationServiceTest { } every { - mockApplicationRepository.findByIdOrNull(applicationId) - } returns application - every { mockJsonSchemaService.checkCas2BailSchemaOutdated(application) } returns - application - every { mockJsonSchemaService.validate(newestSchema, application.data!!) } returns true + mockCas2BailApplicationRepository.findByIdOrNull(applicationId) + } returns cas2BailApplication + every { mockJsonSchemaService.checkCas2BailSchemaOutdated(cas2BailApplication) } returns + cas2BailApplication + every { mockJsonSchemaService.validate(newestSchema, cas2BailApplication.data!!) } returns true val inmateDetail = InmateDetailFactory() .withAssignedLivingUnit( @@ -1039,8 +1037,8 @@ class Cas2BailApplicationServiceTest { every { mockOffenderService.getInmateDetailByNomsNumber( - application.crn, - application.nomsNumber.toString(), + cas2BailApplication.crn, + cas2BailApplication.nomsNumber.toString(), ) } returns AuthorisableActionResult.Success(inmateDetail) @@ -1049,23 +1047,23 @@ class Cas2BailApplicationServiceTest { every { mockNotifyConfig.emailAddresses.cas2ReplyToId } returns "def456" every { mockEmailNotificationService.sendEmail(any(), any(), any(), any()) } just Runs - every { mockApplicationRepository.save(any()) } answers { + every { mockCas2BailApplicationRepository.save(any()) } answers { it.invocation.args[0] as Cas2BailApplicationEntity } val offenderDetails = OffenderDetailsSummaryFactory() .withGender("male") - .withCrn(application.crn) + .withCrn(cas2BailApplication.crn) .produce() - every { mockOffenderService.getOffenderByCrn(application.crn) } returns AuthorisableActionResult.Success( + every { mockOffenderService.getOffenderByCrn(cas2BailApplication.crn) } returns AuthorisableActionResult.Success( offenderDetails, ) - every { mockAssessmentService.createCas2BailAssessment(any()) } returns any() + every { mockCas2BailAssessmentService.createCas2BailAssessment(any()) } returns any() - val result = applicationService.submitCas2BailApplication(submitCas2Application, user) + val result = cas2BailApplicationService.submitCas2BailApplication(submitCas2Application, user) assertThat(result is AuthorisableActionResult.Success).isTrue result as AuthorisableActionResult.Success @@ -1074,22 +1072,22 @@ class Cas2BailApplicationServiceTest { val validatableActionResult = result.entity as ValidatableActionResult.Success val persistedApplication = validatableActionResult.entity - assertThat(persistedApplication.crn).isEqualTo(application.crn) + assertThat(persistedApplication.crn).isEqualTo(cas2BailApplication.crn) assertThat(persistedApplication.preferredAreas).isEqualTo("Leeds | Bradford") assertThat(persistedApplication.hdcEligibilityDate).isEqualTo(hdcEligibilityDate) assertThat(persistedApplication.conditionalReleaseDate).isEqualTo(conditionalReleaseDate) - verify { mockApplicationRepository.save(any()) } + verify { mockCas2BailApplicationRepository.save(any()) } verify(exactly = 1) { mockDomainEventService.saveCas2ApplicationSubmittedDomainEvent( match { val data = it.data.eventDetails - it.applicationId == application.id && - data.personReference.noms == application.nomsNumber && - data.personReference.crn == application.crn && - data.applicationUrl == "http://frontend/applications/${application.id}" && + it.applicationId == cas2BailApplication.id && + data.personReference.noms == cas2BailApplication.nomsNumber && + data.personReference.crn == cas2BailApplication.crn && + data.applicationUrl == "http://frontend/applications/${cas2BailApplication.id}" && data.submittedBy.staffMember.username == username && data.referringPrisonCode == "BRI" && data.preferredAreas == "Leeds | Bradford" && @@ -1106,15 +1104,15 @@ class Cas2BailApplicationServiceTest { match { it["name"] == user.name && it["email"] == user.email && - it["prisonNumber"] == application.nomsNumber && - it["telephoneNumber"] == application.telephoneNumber && + it["prisonNumber"] == cas2BailApplication.nomsNumber && + it["telephoneNumber"] == cas2BailApplication.telephoneNumber && it["applicationUrl"] == "http://frontend/assess/applications/$applicationId/overview" }, "def456", ) } - verify(exactly = 1) { mockAssessmentService.createCas2BailAssessment(persistedApplication) } + verify(exactly = 1) { mockCas2BailAssessmentService.createCas2BailAssessment(persistedApplication) } } } diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailAssessmentServiceTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailAssessmentServiceTest.kt new file mode 100644 index 0000000000..d56321be64 --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailAssessmentServiceTest.kt @@ -0,0 +1,138 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.unit.service.cas2bail + +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.springframework.data.repository.findByIdOrNull +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.UpdateCas2Assessment +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.NomisUserEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.cas2bail.Cas2BailApplicationEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailAssessmentEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailAssessmentRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.AuthorisableActionResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.ValidatableActionResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2bail.Cas2BailAssessmentService +import java.time.OffsetDateTime +import java.util.* + +class Cas2BailAssessmentServiceTest { + + private val mockCas2BailAssessmentRepository = mockk() + + private val cas2BailAssessmentService = Cas2BailAssessmentService( + mockCas2BailAssessmentRepository, + ) + + @Nested + inner class CreateAssessment { + + @Test + fun `saves and returns entity from db`() { + val cas2BailApplication = Cas2BailApplicationEntityFactory() + .withCreatedByUser( + NomisUserEntityFactory() + .produce(), + ).produce() + val assessEntity = Cas2BailAssessmentEntity( + id = UUID.randomUUID(), + application = cas2BailApplication, + createdAt = OffsetDateTime.now(), + ) + + every { mockCas2BailAssessmentRepository.save(any()) } answers + { + assessEntity + } + + val result = cas2BailAssessmentService.createCas2BailAssessment( + cas2BailApplication, + ) + Assertions.assertThat(result).isEqualTo(assessEntity) + + verify(exactly = 1) { + mockCas2BailAssessmentRepository.save( + match { it.application == cas2BailApplication }, + ) + } + } + } + + @Nested + inner class UpdateAssessment { + + @Test + fun `saves and returns entity from db`() { + val assessmentId = UUID.randomUUID() + val cas2BailApplication = Cas2BailApplicationEntityFactory() + .withCreatedByUser( + NomisUserEntityFactory() + .produce(), + ).produce() + val assessEntity = Cas2BailAssessmentEntity( + id = assessmentId, + application = cas2BailApplication, + createdAt = OffsetDateTime.now(), + ) + + val newAssessmentData = UpdateCas2Assessment( + nacroReferralId = "1234OH", + assessorName = "Anne Assessor", + ) + + every { mockCas2BailAssessmentRepository.save(any()) } answers + { + assessEntity + } + + every { mockCas2BailAssessmentRepository.findByIdOrNull(assessmentId) } answers + { + assessEntity + } + + val result = cas2BailAssessmentService.updateAssessment( + assessmentId = assessmentId, + newAssessment = newAssessmentData, + ) + Assertions.assertThat(result).isEqualTo( + AuthorisableActionResult.Success( + ValidatableActionResult.Success(assessEntity), + ), + ) + + verify(exactly = 1) { + mockCas2BailAssessmentRepository.save( + match { + it.id == assessEntity.id && + it.nacroReferralId == newAssessmentData.nacroReferralId && + it.assessorName == newAssessmentData.assessorName + }, + ) + } + } + + @Test + fun `returns NotFound if entity is not found`() { + val assessmentId = UUID.randomUUID() + val newAssessmentData = UpdateCas2Assessment( + nacroReferralId = "1234OH", + assessorName = "Anne Assessor", + ) + + every { mockCas2BailAssessmentRepository.findByIdOrNull(assessmentId) } answers + { + null + } + + val result = cas2BailAssessmentService.updateAssessment( + assessmentId = assessmentId, + newAssessment = newAssessmentData, + ) + + Assertions.assertThat(result is AuthorisableActionResult.NotFound).isTrue + } + } + +} \ No newline at end of file diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailUserAccessServiceTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailUserAccessServiceTest.kt new file mode 100644 index 0000000000..0ab22ca243 --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailUserAccessServiceTest.kt @@ -0,0 +1,140 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.unit.service.cas2bail + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.Cas2ApplicationJsonSchemaEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.Cas2BailApplicationJsonSchemaEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.NomisUserEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.cas2bail.Cas2BailApplicationEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2bail.Cas2BailUserAccessService +import java.time.OffsetDateTime + +class Cas2BailUserAccessServiceTest { + + @Nested + inner class UserCanViewApplication { + + private val cas2BailUserAccessService = Cas2BailUserAccessService() + val newestJsonSchema = Cas2BailApplicationJsonSchemaEntityFactory() + .withSchema("{}") + .produce() + + @Nested + inner class WhenApplicationCreatedByUser { + private val user = NomisUserEntityFactory() + .produce() + + @Test + fun `returns true`() { + val application = Cas2BailApplicationEntityFactory() + .withApplicationSchema(newestJsonSchema) + .withCreatedByUser(user) + .produce() + + assertThat(cas2BailUserAccessService.userCanViewCas2BailApplication(user, application)).isTrue + } + } + + @Nested + inner class WhenApplicationNotCreatedByUser { + + @Nested + inner class WhenApplicationNotSubmitted { + private val user = NomisUserEntityFactory() + .produce() + private val anotherUser = NomisUserEntityFactory() + .produce() + + @Test + fun `returns false`() { + val cas2BailApplication = Cas2BailApplicationEntityFactory() + .withApplicationSchema(newestJsonSchema) + .withCreatedByUser(anotherUser) + .produce() + + assertThat(cas2BailUserAccessService.userCanViewCas2BailApplication(user, cas2BailApplication)).isFalse + } + } + + @Nested + inner class WhenApplicationMadeForDifferentPrison { + private val user = NomisUserEntityFactory() + .withActiveCaseloadId("my-prison") + .produce() + private val anotherUser = NomisUserEntityFactory() + .withActiveCaseloadId("different-prison").produce() + + @Test + fun `returns false`() { + val cas2BailApplication = Cas2BailApplicationEntityFactory() + .withApplicationSchema(newestJsonSchema) + .withCreatedByUser(anotherUser) + .withSubmittedAt(OffsetDateTime.now()) + .withReferringPrisonCode("different-prison") + .produce() + + assertThat(cas2BailUserAccessService.userCanViewCas2BailApplication(user, cas2BailApplication)).isFalse + } + + @Nested + inner class WhenNoPrisonData { + private val userWithNoPrison = NomisUserEntityFactory() + .withActiveCaseloadId("my-prison") + .produce() + private val anotherUserWithNoPrison = NomisUserEntityFactory() + .withActiveCaseloadId("different-prison").produce() + + @Test + fun `returns false`() { + val cas2BailApplication = Cas2BailApplicationEntityFactory() + .withApplicationSchema(newestJsonSchema) + .withCreatedByUser(anotherUserWithNoPrison) + .withSubmittedAt(OffsetDateTime.now()) + .produce() + + assertThat(cas2BailUserAccessService.userCanViewCas2BailApplication(userWithNoPrison, cas2BailApplication)).isFalse + } + } + } + + @Nested + inner class WhenCas2BailApplicationMadeForSamePrison { + private val user = NomisUserEntityFactory() + .withActiveCaseloadId("my-prison") + .produce() + private val anotherUser = NomisUserEntityFactory() + .withActiveCaseloadId("my-prison") + .produce() + + @Test + fun `returns true if the user created the application`() { + val cas2BailApplication = Cas2BailApplicationEntityFactory() + .withApplicationSchema(newestJsonSchema) + .withCreatedByUser(user) + .withSubmittedAt(OffsetDateTime.now()) + .withReferringPrisonCode("my-prison") + .produce() + + assertThat(cas2BailUserAccessService.userCanViewCas2BailApplication(user, cas2BailApplication)).isTrue + } + + @Test + fun `returns true when user NOT creator`() { + val newestJsonSchema = Cas2ApplicationJsonSchemaEntityFactory() + .withSchema("{}") + .produce() + + val cas2BailApplication = Cas2BailApplicationEntityFactory() + .withApplicationSchema(newestJsonSchema) + .withCreatedByUser(anotherUser) + .withSubmittedAt(OffsetDateTime.now()) + .withReferringPrisonCode("my-prison") + .produce() + + assertThat(cas2BailUserAccessService.userCanViewCas2BailApplication(user, cas2BailApplication)).isTrue + } + } + } + } +} \ No newline at end of file From f16a186d568df4cfdd3a9f6a3dfc990c3871fbc1 Mon Sep 17 00:00:00 2001 From: garethCAS2 Date: Mon, 2 Dec 2024 12:08:01 +0000 Subject: [PATCH 08/37] wip tests and CasResult --- .../cas2bail/Cas2BailAssessmentsController.kt | 116 +++++++++++ .../Cas2BailApplicationNoteService.kt | 171 ++++++++++++++++ .../cas2bail/Cas2BailApplicationService.kt | 36 ++-- .../cas2bail/Cas2BailAssessmentService.kt | 15 +- .../cas2bail/Cas2BailStatusUpdateService.kt | 182 ++++++++++++++++++ .../cas2/AssessmentsTransformer.kt | 1 + .../Cas2BailApplicationServiceTest.kt | 51 +++-- .../cas2bail/Cas2BailAssessmentServiceTest.kt | 9 +- 8 files changed, 521 insertions(+), 60 deletions(-) create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailAssessmentsController.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationNoteService.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailStatusUpdateService.kt diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailAssessmentsController.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailAssessmentsController.kt new file mode 100644 index 0000000000..54b645aafe --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailAssessmentsController.kt @@ -0,0 +1,116 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.controller.cas2bail + +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.stereotype.Service +import org.zalando.problem.AbstractThrowableProblem +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.cas2bail.AssessmentsCas2bailDelegate +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationNoteEntity +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.problem.NotFoundProblem +import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.CasResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.ExternalUserService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2bail.Cas2BailApplicationNoteService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2bail.Cas2BailAssessmentService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2bail.Cas2BailStatusUpdateService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas2.ApplicationNotesTransformer +import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas2bail.Cas2BailAssessmentsTransformer +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.extractEntityFromCasResult +import java.net.URI +import java.util.* + +@Service("Cas2BailAssessmentsController") +class Cas2BailAssessmentsController ( + private val cas2BailAssessmentService: Cas2BailAssessmentService, + private val cas2BailApplicationNoteService: Cas2BailApplicationNoteService, + private val cas2BailAssessmentsTransformer: Cas2BailAssessmentsTransformer, + private val applicationNotesTransformer: ApplicationNotesTransformer, + private val cas2BailStatusUpdateService: Cas2BailStatusUpdateService, + private val externalUserService: ExternalUserService, +) : AssessmentsCas2bailDelegate { + + override fun assessmentsAssessmentIdGet(assessmentId: UUID): ResponseEntity { + val assessment = when ( + val assessmentResult = cas2BailAssessmentService.getAssessment(assessmentId) + ) { + is CasResult.NotFound -> throw NotFoundProblem(assessmentId, "Cas2BailAssessment") + is CasResult.Unauthorised -> throw ForbiddenProblem() + is CasResult.Success -> assessmentResult + is CasResult.ConflictError<*> -> throw ConflictProblem(assessmentId, "Cas2BailAssessment conflict by assessmentId") + is CasResult.FieldValidationError<*> -> CasResult.FieldValidationError(mapOf("$.reason" to "doesNotExist")) + is CasResult.GeneralValidationError<*> -> CasResult.GeneralValidationError("General Validation Error") + } + + val cas2BailAssessmentEntity = extractEntityFromCasResult(assessment) + return ResponseEntity.ok(cas2BailAssessmentsTransformer.transformJpaToApiRepresentation(cas2BailAssessmentEntity)) + } + + override fun assessmentsAssessmentIdPut( + assessmentId: UUID, + updateCas2Assessment: UpdateCas2Assessment, + ): ResponseEntity { + val assessmentResult = cas2BailAssessmentService.updateAssessment(assessmentId, updateCas2Assessment) + when (assessmentResult) { + is CasResult.NotFound -> throw NotFoundProblem(assessmentId, "Assessment") + is CasResult.Unauthorised -> throw ForbiddenProblem() + is CasResult.Success -> assessmentResult + is CasResult.ConflictError<*> -> throw ConflictProblem(assessmentId, "Cas2BailAssessment conflict by assessmentId") + is CasResult.FieldValidationError<*> -> CasResult.FieldValidationError(mapOf("$.reason" to "doesNotExist")) + is CasResult.GeneralValidationError<*> -> CasResult.GeneralValidationError("General Validation Error") + } + + val cas2BailAssessmentEntity = extractEntityFromCasResult(assessmentResult) + return ResponseEntity.ok( + cas2BailAssessmentsTransformer.transformJpaToApiRepresentation(cas2BailAssessmentEntity), + ) + } + + override fun assessmentsAssessmentIdStatusUpdatesPost( + assessmentId: UUID, + cas2AssessmentStatusUpdate: Cas2AssessmentStatusUpdate, + ): ResponseEntity { + val result = cas2BailStatusUpdateService.createForAssessment( + assessmentId = assessmentId, + statusUpdate = cas2AssessmentStatusUpdate, + assessor = externalUserService.getUserForRequest(), + ) + + processAuthorisationFor(result) + .run { processValidation(result) } + + return ResponseEntity(HttpStatus.CREATED) + } + + override fun assessmentsAssessmentIdNotesPost( + assessmentId: UUID, + body: NewCas2ApplicationNote, + ): ResponseEntity { + val noteResult = cas2BailApplicationNoteService.createAssessmentNote(assessmentId, body) + + val validationResult = processAuthorisationFor( noteResult) as CasResult + + val note = processValidation(validationResult) as Cas2ApplicationNoteEntity + + return ResponseEntity + .created(URI.create("/cas2/assessments/$assessmentId/notes/${note.id}")) + .body( + applicationNotesTransformer.transformJpaToApi(note), + ) + } + + private fun processAuthorisationFor( + result: CasResult + ): Any? { + + return extractEntityFromCasResult(result) + + } + + + private fun processValidation(casResult: CasResult): Any { + return extractEntityFromCasResult(casResult) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationNoteService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationNoteService.kt new file mode 100644 index 0000000000..b81a62225f --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationNoteService.kt @@ -0,0 +1,171 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2bail + +import io.sentry.Sentry +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Value +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.NewCas2ApplicationNote +import uk.gov.justice.digital.hmpps.approvedpremisesapi.config.NotifyConfig +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.AuthorisableActionResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.CasResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.ValidatableActionResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.EmailNotificationService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.ExternalUserService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.HttpAuthService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.NomisUserService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2.UserAccessService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.toCas2UiFormat +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.toCas2UiFormattedHourOfDay +import java.time.OffsetDateTime +import java.util.* + +@Service("Cas2BailApplicationNoteService") +class Cas2BailApplicationNoteService( + private val cas2BailApplicationRepository: Cas2BailApplicationRepository, + private val cas2BailAssessmentRepository: Cas2BailAssessmentRepository, + private val cas2BailApplicationNoteRepository: Cas2BailApplicationNoteRepository, + private val userService: NomisUserService, + private val externalUserService: ExternalUserService, + private val httpAuthService: HttpAuthService, + private val emailNotificationService: EmailNotificationService, + private val userAccessService: UserAccessService, + private val notifyConfig: NotifyConfig, + @Value("\${url-templates.frontend.cas2.application-overview}") private val applicationUrlTemplate: String, + @Value("\${url-templates.frontend.cas2.submitted-application-overview}") private val assessmentUrlTemplate: String, +) { + + private val log = LoggerFactory.getLogger(this::class.java) + + @Suppress("ReturnCount") + fun createAssessmentNote(assessmentId: UUID, note: NewCas2ApplicationNote): CasResult { + val assessment = cas2BailAssessmentRepository.findByIdOrNull(assessmentId) + ?: return CasResult.NotFound() + + val application = cas2BailApplicationRepository.findByIdOrNull(assessment.application.id) + ?: return CasResult.NotFound() + + if (application.submittedAt == null) { + return CasResult.GeneralValidationError("This application has not been submitted") + } + + val isExternalUser = httpAuthService.getCas2AuthenticatedPrincipalOrThrow().isExternalUser() + val user = getCas2User(isExternalUser) + + if (!isExternalUser && !nomisUserCanAddNote(application, user as NomisUserEntity)) { + return CasResult.Unauthorised() + } + + val savedNote = saveNote(application, assessment, note.note, user) + + sendEmail(isExternalUser, application, savedNote) + + return CasResult.Success( savedNote ) + } + + private fun sendEmail( + isExternalUser: Boolean, + application: Cas2BailApplicationEntity, + savedNote: Cas2BailApplicationNoteEntity, + ) { + if (isExternalUser) { + sendEmailToReferrer(application, savedNote) + } else { + sendEmailToAssessors(application, savedNote) + } + } + + private fun sendEmailToReferrer( + application: Cas2BailApplicationEntity, + savedNote: Cas2BailApplicationNoteEntity) { + if (application.createdByUser.email != null) { + emailNotificationService.sendCas2Email( + recipientEmailAddress = application.createdByUser.email!!, + templateId = notifyConfig.templates.cas2NoteAddedForReferrer, + personalisation = mapOf( + "dateNoteAdded" to savedNote.createdAt.toLocalDate().toCas2UiFormat(), + "timeNoteAdded" to savedNote.createdAt.toCas2UiFormattedHourOfDay(), + "nomsNumber" to application.nomsNumber, + "applicationType" to "Home Detention Curfew (HDC)", + "applicationUrl" to applicationUrlTemplate.replace("#id", application.id.toString()), + ), + ) + } else { + log.error("Email not found for User ${application.createdByUser.id}. Unable to send email for Note ${savedNote.id} on Application ${application.id}") + Sentry.captureMessage("Email not found for User ${application.createdByUser.id}. Unable to send email for Note ${savedNote.id} on Application ${application.id}") + } + } + + private fun sendEmailToAssessors( + application: Cas2BailApplicationEntity, + savedNote: Cas2BailApplicationNoteEntity, + ) { + emailNotificationService.sendCas2Email( + recipientEmailAddress = notifyConfig.emailAddresses.cas2Assessors, + templateId = notifyConfig.templates.cas2NoteAddedForAssessor, + personalisation = mapOf( + "nacroReferenceId" to getNacroReferenceIdOrPlaceholder(application.assessment!!), + "nacroReferenceIdInSubject" to getSubjectLineReferenceIdOrPlaceholder(application.assessment!!), + "dateNoteAdded" to savedNote.createdAt.toLocalDate().toCas2UiFormat(), + "timeNoteAdded" to savedNote.createdAt.toCas2UiFormattedHourOfDay(), + "assessorName" to getAssessorNameOrPlaceholder(application.assessment!!), + "applicationType" to "Home Detention Curfew (HDC)", + "applicationUrl" to assessmentUrlTemplate.replace("#applicationId", application.id.toString()), + ), + ) + } + + private fun getSubjectLineReferenceIdOrPlaceholder(assessment: Cas2BailAssessmentEntity): String { + if (assessment.nacroReferralId != null) { + return "(${assessment.nacroReferralId!!})" + } + return "" + } + + private fun getNacroReferenceIdOrPlaceholder(assessment: Cas2BailAssessmentEntity): String { + if (assessment.nacroReferralId != null) { + return assessment.nacroReferralId!! + } + return "Unknown. " + + "The Nacro CAS-2 reference number has not been added to the application yet." + } + + private fun getAssessorNameOrPlaceholder(assessment: Cas2BailAssessmentEntity): String { + if (assessment.assessorName != null) { + return assessment.assessorName!! + } + return "Unknown. " + + "The assessor has not added their name to the application yet." + } + + private fun getCas2User(isExternalUser: Boolean): Cas2User { + return if (isExternalUser) { + externalUserService.getUserForRequest() + } else { + userService.getUserForRequest() + } + } + + private fun nomisUserCanAddNote(application: Cas2BailApplicationEntity, user: NomisUserEntity): Boolean { + return if (user.id == application.createdByUser.id) { + true + } else { + userAccessService.offenderIsFromSamePrisonAsUser(application.referringPrisonCode, user.activeCaseloadId) + } + } + + private fun saveNote(application: Cas2BailApplicationEntity, assessment: Cas2BailAssessmentEntity, body: String, user: Cas2User): Cas2BailApplicationNoteEntity { + val newNote = Cas2BailApplicationNoteEntity( + id = UUID.randomUUID(), + application = application, + body = body, + createdAt = OffsetDateTime.now(), + createdByUser = user, + assessment = assessment, + ) + + return cas2BailApplicationNoteRepository.save(newNote) + } +} \ No newline at end of file diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationService.kt index 7e15b54621..f04b4b898f 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationService.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationService.kt @@ -15,6 +15,7 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.PaginationMetadata import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.ValidationErrors import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.validated import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.AuthorisableActionResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.CasResult import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.ValidatableActionResult import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.EmailNotificationService import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.UpstreamApiException @@ -221,37 +222,34 @@ class Cas2BailApplicationService( fun submitCas2BailApplication( submitApplication: SubmitCas2Application, user: NomisUserEntity, - ): AuthorisableActionResult> { + ): CasResult { val applicationId = submitApplication.applicationId lockableApplicationRepository.acquirePessimisticLock(applicationId) var application = cas2BailApplicationRepository.findByIdOrNull(applicationId) ?.let(jsonSchemaService::checkCas2BailSchemaOutdated) - ?: return AuthorisableActionResult.NotFound() + ?: return CasResult.NotFound() val serializedTranslatedDocument = objectMapper.writeValueAsString(submitApplication.translatedDocument) if (application.createdByUser != user) { - return AuthorisableActionResult.Unauthorised() + return CasResult.Unauthorised() } if (application.abandonedAt != null) { - return AuthorisableActionResult.Success( - ValidatableActionResult.GeneralValidationError("This application has already been abandoned"), - ) + return CasResult.GeneralValidationError("This application has already been abandoned") + } if (application.submittedAt != null) { - return AuthorisableActionResult.Success( - ValidatableActionResult.GeneralValidationError("This application has already been submitted"), - ) + return CasResult.GeneralValidationError("This application has already been submitted") + } if (!application.schemaUpToDate) { - return AuthorisableActionResult.Success( - ValidatableActionResult.GeneralValidationError("The schema version is outdated"), - ) + return CasResult.GeneralValidationError("The schema version is outdated") + } val validationErrors = ValidationErrors() @@ -264,9 +262,8 @@ class Cas2BailApplicationService( } if (validationErrors.any()) { - return AuthorisableActionResult.Success( - ValidatableActionResult.FieldValidationError(validationErrors), - ) + return CasResult.FieldValidationError(validationErrors) + } val schema = application.schemaVersion as? Cas2BailApplicationJsonSchemaEntity @@ -283,9 +280,7 @@ class Cas2BailApplicationService( telephoneNumber = submitApplication.telephoneNumber } } catch (error: UpstreamApiException) { - return AuthorisableActionResult.Success( - ValidatableActionResult.GeneralValidationError(error.message.toString()), - ) + return CasResult.GeneralValidationError(error.message.toString()) } application = cas2BailApplicationRepository.save(application) @@ -296,9 +291,7 @@ class Cas2BailApplicationService( sendEmailApplicationSubmitted(user, application) - return AuthorisableActionResult.Success( - ValidatableActionResult.Success(application), - ) + return CasResult.Success(application) } fun createCas2ApplicationSubmittedEvent(application: Cas2BailApplicationEntity) { @@ -352,6 +345,7 @@ class Cas2BailApplicationService( crn = application.crn, nomsNumber = application.nomsNumber.toString(), ) + //AuthorisableActionResult is deprecated but we don;t want to be touching offenderService while doing Cas2Bail work. val inmateDetail = when (inmateDetailResult) { is AuthorisableActionResult.NotFound -> throw UpstreamApiException("Inmate Detail not found") is AuthorisableActionResult.Unauthorised -> throw UpstreamApiException("Inmate Detail unauthorised") diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailAssessmentService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailAssessmentService.kt index b9b3caadb7..da6aef71eb 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailAssessmentService.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailAssessmentService.kt @@ -9,6 +9,7 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2 import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailAssessmentEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailAssessmentRepository import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.AuthorisableActionResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.CasResult import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.ValidatableActionResult import java.time.OffsetDateTime import java.util.* @@ -30,9 +31,9 @@ class Cas2BailAssessmentService ( ), ) - fun updateAssessment(assessmentId: UUID, newAssessment: UpdateCas2Assessment): AuthorisableActionResult> { + fun updateAssessment(assessmentId: UUID, newAssessment: UpdateCas2Assessment): CasResult { val assessmentEntity = assessmentRepository.findByIdOrNull(assessmentId) - ?: return AuthorisableActionResult.NotFound() + ?: return CasResult.NotFound() assessmentEntity.apply { this.nacroReferralId = newAssessment.nacroReferralId @@ -41,15 +42,13 @@ class Cas2BailAssessmentService ( val savedAssessment = assessmentRepository.save(assessmentEntity) - return AuthorisableActionResult.Success( - ValidatableActionResult.Success(savedAssessment), - ) + return CasResult.Success(savedAssessment) } - fun getAssessment(assessmentId: UUID): AuthorisableActionResult { + fun getAssessment(assessmentId: UUID): CasResult { val assessmentEntity = assessmentRepository.findByIdOrNull(assessmentId) - ?: return AuthorisableActionResult.NotFound() + ?: return CasResult.NotFound() - return AuthorisableActionResult.Success(assessmentEntity) + return CasResult.Success(assessmentEntity) } } \ No newline at end of file diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailStatusUpdateService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailStatusUpdateService.kt new file mode 100644 index 0000000000..c1ae352f63 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailStatusUpdateService.kt @@ -0,0 +1,182 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2bail + +import io.sentry.Sentry +import jakarta.transaction.Transactional +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Value +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.events.cas2.model.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas2AssessmentStatusUpdate +import uk.gov.justice.digital.hmpps.approvedpremisesapi.config.NotifyConfig +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.DomainEvent +import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.ValidationErrors +import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.reference.Cas2PersistedApplicationStatus +import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.reference.Cas2PersistedApplicationStatusDetail +import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.reference.Cas2PersistedApplicationStatusFinder +import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.AuthorisableActionResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.CasResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.ValidatableActionResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.EmailNotificationService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2.Constants.HDC_APPLICATION_TYPE +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2.DomainEventService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas2.ApplicationStatusTransformer +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.extractEntityFromCasResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.toCas2UiFormat +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.toCas2UiFormattedHourOfDay +import java.time.OffsetDateTime +import java.util.* + +@Service("Cas2BailStatusUpdateService") +class Cas2BailStatusUpdateService ( + private val cas2BailAssessmentRepository: Cas2BailAssessmentRepository, + private val cas2BailStatusUpdateRepository: Cas2BailStatusUpdateRepository, + private val cas2BailStatusUpdateDetailRepository: Cas2BailStatusUpdateDetailRepository, + private val domainEventService: DomainEventService, + private val emailNotificationService: EmailNotificationService, + private val notifyConfig: NotifyConfig, + private val statusFinder: Cas2PersistedApplicationStatusFinder, + private val statusTransformer: ApplicationStatusTransformer, + @Value("\${url-templates.frontend.cas2.application}") private val applicationUrlTemplate: String, + @Value("\${url-templates.frontend.cas2.application-overview}") private val applicationOverviewUrlTemplate: String, +) { + + private val log = LoggerFactory.getLogger(this::class.java) + + fun isValidStatus(statusUpdate: Cas2AssessmentStatusUpdate): Boolean { + return findActiveStatusByName(statusUpdate.newStatus) != null + } + + @Transactional + @SuppressWarnings("ReturnCount") + fun createForAssessment( + assessmentId: UUID, + statusUpdate: Cas2AssessmentStatusUpdate, + assessor: ExternalUserEntity, + ): CasResult { + val assessment = cas2BailAssessmentRepository.findByIdOrNull(assessmentId) + ?: return CasResult.NotFound() + + val status = findActiveStatusByName(statusUpdate.newStatus) + ?: return CasResult.GeneralValidationError("The status ${statusUpdate.newStatus} is not valid") + + + val newDetails = statusUpdate.newStatusDetails.isNullOrEmpty() + val statusDetails = if (newDetails) { + emptyList() + } else { + statusUpdate.newStatusDetails?.map { detail -> + status.findStatusDetailOnStatus(detail) + ?: return CasResult.GeneralValidationError("The status detail $detail is not valid") + } + } + + if (ValidationErrors().any()) { + return CasResult.FieldValidationError(ValidationErrors()) + } + + val createdStatusUpdate = cas2BailStatusUpdateRepository.save( + Cas2BailStatusUpdateEntity( + id = UUID.randomUUID(), + assessment = assessment, + application = assessment.application, + assessor = assessor, + statusId = status.id, + description = status.description, + label = status.label, + createdAt = OffsetDateTime.now(), + ), + ) + + statusDetails?.forEach { detail -> + cas2BailStatusUpdateDetailRepository.save( + Cas2BailStatusUpdateDetailEntity( + id = UUID.randomUUID(), + statusDetailId = detail.id, + statusUpdate = createdStatusUpdate, + label = detail.label, + ), + ) + } + + sendEmailStatusUpdated(assessment.application.createdByUser, assessment.application, createdStatusUpdate) + + createStatusUpdatedDomainEvent(createdStatusUpdate, statusDetails) + + + return CasResult.Success(createdStatusUpdate) + } + + private fun findActiveStatusByName(statusName: String): Cas2PersistedApplicationStatus? { + return statusFinder.active() + .find { status -> status.name == statusName } + } + + fun createStatusUpdatedDomainEvent( + statusUpdate: Cas2BailStatusUpdateEntity, + statusDetails: List? = emptyList()) { + val domainEventId = UUID.randomUUID() + val eventOccurredAt = statusUpdate.createdAt + val application = statusUpdate.application + val newStatus = statusUpdate.status() + val assessor = statusUpdate.assessor + + domainEventService.saveCas2ApplicationStatusUpdatedDomainEvent( + DomainEvent( + id = domainEventId, + applicationId = application.id, + crn = application.crn, + nomsNumber = application.nomsNumber, + occurredAt = eventOccurredAt.toInstant(), + data = Cas2ApplicationStatusUpdatedEvent( + id = domainEventId, + timestamp = eventOccurredAt.toInstant(), + eventType = EventType.applicationStatusUpdated, + eventDetails = Cas2ApplicationStatusUpdatedEventDetails( + applicationId = application.id, + applicationUrl = applicationUrlTemplate.replace("#id", application.id.toString()), + personReference = PersonReference( + crn = application.crn, + noms = application.nomsNumber.toString(), + ), + newStatus = Cas2Status( + name = newStatus.name, + description = newStatus.description, + label = newStatus.label, + statusDetails = statusDetails?.let { statusTransformer.transformStatusDetailListToDetailItemList(it) }, + ), + updatedBy = ExternalUser( + username = assessor.username, + name = assessor.name, + email = assessor.email, + origin = assessor.origin, + ), + updatedAt = eventOccurredAt.toInstant(), + ), + ), + ), + ) + } + + private fun sendEmailStatusUpdated(user: NomisUserEntity, application: Cas2BailApplicationEntity, status: Cas2BailStatusUpdateEntity) { + if (application.createdByUser.email != null) { + emailNotificationService.sendCas2Email( + recipientEmailAddress = user.email!!, + templateId = notifyConfig.templates.cas2ApplicationStatusUpdated, + personalisation = mapOf( + "applicationStatus" to status.label, + "dateStatusChanged" to status.createdAt.toLocalDate().toCas2UiFormat(), + "timeStatusChanged" to status.createdAt.toCas2UiFormattedHourOfDay(), + "applicationType" to HDC_APPLICATION_TYPE, + "nomsNumber" to application.nomsNumber, + "applicationUrl" to applicationOverviewUrlTemplate.replace("#id", application.id.toString()), + ), + ) + } else { + log.error("Email not found for User ${application.createdByUser.id}. Unable to send email when updating status of Application ${application.id}") + Sentry.captureMessage("Email not found for User ${application.createdByUser.id}. Unable to send email when updating status of Application ${application.id}") + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2/AssessmentsTransformer.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2/AssessmentsTransformer.kt index df053b2248..1262cf390f 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2/AssessmentsTransformer.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2/AssessmentsTransformer.kt @@ -3,6 +3,7 @@ package uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas2 import org.springframework.stereotype.Component import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas2Assessment import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2AssessmentEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailAssessmentEntity @Component("Cas2AssessmentsTransformer") class AssessmentsTransformer(private val statusUpdateTransformer: StatusUpdateTransformer) { diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailApplicationServiceTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailApplicationServiceTest.kt index bb64014eba..bbab845308 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailApplicationServiceTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailApplicationServiceTest.kt @@ -22,16 +22,14 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2BailAppli import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.* import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.prisonsapi.AssignedLivingUnit import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.AuthorisableActionResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.CasResult import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.ValidatableActionResult import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.EmailNotificationService import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2.* import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2bail.Cas2BailApplicationService import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2bail.Cas2BailAssessmentService import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2bail.Cas2BailUserAccessService -import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.PageCriteria -import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.PaginationConfig -import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.getPageableOrAllPages -import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.randomStringMultiCaseWithNumbers +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.* import java.time.LocalDate import java.time.OffsetDateTime import java.util.* @@ -777,7 +775,7 @@ class Cas2BailApplicationServiceTest { every { mockCas2BailApplicationRepository.findByIdOrNull(applicationId) } returns null - assertThat(cas2BailApplicationService.submitCas2BailApplication(submitCas2Application, user) is AuthorisableActionResult.NotFound).isTrue + assertThat(cas2BailApplicationService.submitCas2BailApplication(submitCas2Application, user) is CasResult.NotFound).isTrue assertEmailAndAssessmentsWereNotCreated() } @@ -796,7 +794,7 @@ class Cas2BailApplicationServiceTest { every { mockJsonSchemaService.checkCas2BailSchemaOutdated(cas2BailApplication) } returns cas2BailApplication - assertThat(cas2BailApplicationService.submitCas2BailApplication(submitCas2Application, user) is AuthorisableActionResult.Unauthorised).isTrue + assertThat(cas2BailApplicationService.submitCas2BailApplication(submitCas2Application, user) is CasResult.Unauthorised).isTrue assertEmailAndAssessmentsWereNotCreated() } @@ -819,11 +817,11 @@ class Cas2BailApplicationServiceTest { val result = cas2BailApplicationService.submitCas2BailApplication(submitCas2Application, user) - assertThat(result is AuthorisableActionResult.Success).isTrue - result as AuthorisableActionResult.Success + assertThat(result is CasResult.Success).isTrue + result as CasResult.Success - assertThat(result.entity is ValidatableActionResult.GeneralValidationError).isTrue - val validatableActionResult = result.entity as ValidatableActionResult.GeneralValidationError + assertThat(result is CasResult.GeneralValidationError).isTrue + val validatableActionResult = result as CasResult.GeneralValidationError assertThat(validatableActionResult.message).isEqualTo("The schema version is outdated") @@ -851,11 +849,11 @@ class Cas2BailApplicationServiceTest { val result = cas2BailApplicationService.submitCas2BailApplication(submitCas2Application, user) - assertThat(result is AuthorisableActionResult.Success).isTrue - result as AuthorisableActionResult.Success + assertThat(result is CasResult.Success).isTrue + result as CasResult.Success - assertThat(result.entity is ValidatableActionResult.GeneralValidationError).isTrue - val validatableActionResult = result.entity as ValidatableActionResult.GeneralValidationError + assertThat(result is CasResult.GeneralValidationError).isTrue + val validatableActionResult = result as CasResult.GeneralValidationError assertThat(validatableActionResult.message).isEqualTo("This application has already been submitted") @@ -880,11 +878,11 @@ class Cas2BailApplicationServiceTest { val result = cas2BailApplicationService.submitCas2BailApplication(submitCas2Application, user) - assertThat(result is AuthorisableActionResult.Success).isTrue - result as AuthorisableActionResult.Success + assertThat(result is CasResult.Success).isTrue + result as CasResult.Success - assertThat(result.entity is ValidatableActionResult.GeneralValidationError).isTrue - val validatableActionResult = result.entity as ValidatableActionResult.GeneralValidationError + assertThat(result is CasResult.GeneralValidationError).isTrue + val validatableActionResult = result as CasResult.GeneralValidationError assertThat(validatableActionResult.message).isEqualTo("This application has already been abandoned") @@ -989,11 +987,11 @@ class Cas2BailApplicationServiceTest { private fun assertGeneralValidationError(message: String) { val result = cas2BailApplicationService.submitCas2BailApplication(submitCas2Application, user) - assertThat(result is AuthorisableActionResult.Success).isTrue - result as AuthorisableActionResult.Success + assertThat(result is CasResult.Success).isTrue + result as CasResult.Success - assertThat(result.entity is ValidatableActionResult.GeneralValidationError).isTrue - val error = result.entity as ValidatableActionResult.GeneralValidationError + assertThat(result is CasResult.GeneralValidationError).isTrue + val error = result as CasResult.GeneralValidationError assertThat(error.message).isEqualTo(message) } @@ -1065,12 +1063,11 @@ class Cas2BailApplicationServiceTest { val result = cas2BailApplicationService.submitCas2BailApplication(submitCas2Application, user) - assertThat(result is AuthorisableActionResult.Success).isTrue - result as AuthorisableActionResult.Success + assertThat(result is CasResult.Success).isTrue + result as CasResult.Success - assertThat(result.entity is ValidatableActionResult.Success).isTrue - val validatableActionResult = result.entity as ValidatableActionResult.Success - val persistedApplication = validatableActionResult.entity + assertThat(true).isTrue + val persistedApplication = extractEntityFromCasResult(result) assertThat(persistedApplication.crn).isEqualTo(cas2BailApplication.crn) assertThat(persistedApplication.preferredAreas).isEqualTo("Leeds | Bradford") diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailAssessmentServiceTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailAssessmentServiceTest.kt index d56321be64..2a46d5b263 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailAssessmentServiceTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailAssessmentServiceTest.kt @@ -13,6 +13,7 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.cas2bail.Cas2Bai import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailAssessmentEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailAssessmentRepository import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.AuthorisableActionResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.CasResult import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.ValidatableActionResult import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2bail.Cas2BailAssessmentService import java.time.OffsetDateTime @@ -97,9 +98,9 @@ class Cas2BailAssessmentServiceTest { newAssessment = newAssessmentData, ) Assertions.assertThat(result).isEqualTo( - AuthorisableActionResult.Success( - ValidatableActionResult.Success(assessEntity), - ), + + CasResult.Success(assessEntity), + ) verify(exactly = 1) { @@ -131,7 +132,7 @@ class Cas2BailAssessmentServiceTest { newAssessment = newAssessmentData, ) - Assertions.assertThat(result is AuthorisableActionResult.NotFound).isTrue + Assertions.assertThat(result is CasResult.NotFound).isTrue } } From 162c90df7dc52cb0c39c4bab222526143efb22f5 Mon Sep 17 00:00:00 2001 From: garethCAS2 Date: Mon, 2 Dec 2024 14:59:08 +0000 Subject: [PATCH 09/37] More tests --- src/main/resources/application-local.yml | 7 ++++++- .../integration/cas2bail/Cas2BailApplicationTest.kt | 2 +- .../cas2bail/Cas2BailApplicationServiceTest.kt | 12 ------------ 3 files changed, 7 insertions(+), 14 deletions(-) diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index ed28109c30..f2ed06a6c1 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -8,6 +8,10 @@ spring: locations: classpath:db/migration/all,classpath:db/migration/local+dev+test,classpath:db/migration/local,classpath:db/migration/all-except-integration jpa: database: postgresql + properties: + hibernate: + show_sql: true + format_sql: true data: redis: host: localhost @@ -67,7 +71,8 @@ logging: org.hibernate.SQL: DEBUG # Uncomment the two entries below to see SQL binding #org.hibernate.orm.jdbc.bind: TRACE - #org.hibernate.type.descriptor.sql.BasicBinder: TRACE + org.hibernate.type.descriptor.sql.BasicBinder: TRACE + org.hibernate.type.descriptor.sql: tract # allows us to see the JWT token to simplify local API invocation uk.gov.justice.digital.hmpps.approvedpremisesapi.config.RequestResponseLoggingFilter: TRACE # allows us to see the request URL and method for upstream requests diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationTest.kt index 05b7aa1344..6c18faa868 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationTest.kt @@ -194,7 +194,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { Pair("Awaiting arrival", UUID.fromString("89458555-3219-44a2-9584-c4f715d6b565")), ) - val applicationSchema = cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + val applicationSchema = cas2BailApplicationJsonSchemaEntityFactory.produceAndPersist { withAddedAt(OffsetDateTime.now()) withId(UUID.randomUUID()) } diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailApplicationServiceTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailApplicationServiceTest.kt index bbab845308..68749fa937 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailApplicationServiceTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailApplicationServiceTest.kt @@ -817,9 +817,6 @@ class Cas2BailApplicationServiceTest { val result = cas2BailApplicationService.submitCas2BailApplication(submitCas2Application, user) - assertThat(result is CasResult.Success).isTrue - result as CasResult.Success - assertThat(result is CasResult.GeneralValidationError).isTrue val validatableActionResult = result as CasResult.GeneralValidationError @@ -849,9 +846,6 @@ class Cas2BailApplicationServiceTest { val result = cas2BailApplicationService.submitCas2BailApplication(submitCas2Application, user) - assertThat(result is CasResult.Success).isTrue - result as CasResult.Success - assertThat(result is CasResult.GeneralValidationError).isTrue val validatableActionResult = result as CasResult.GeneralValidationError @@ -878,9 +872,6 @@ class Cas2BailApplicationServiceTest { val result = cas2BailApplicationService.submitCas2BailApplication(submitCas2Application, user) - assertThat(result is CasResult.Success).isTrue - result as CasResult.Success - assertThat(result is CasResult.GeneralValidationError).isTrue val validatableActionResult = result as CasResult.GeneralValidationError @@ -987,9 +978,6 @@ class Cas2BailApplicationServiceTest { private fun assertGeneralValidationError(message: String) { val result = cas2BailApplicationService.submitCas2BailApplication(submitCas2Application, user) - assertThat(result is CasResult.Success).isTrue - result as CasResult.Success - assertThat(result is CasResult.GeneralValidationError).isTrue val error = result as CasResult.GeneralValidationError From 3a2baaaa38825c5cd3980c4d7f69327bc693d02c Mon Sep 17 00:00:00 2001 From: garethCAS2 Date: Tue, 3 Dec 2024 11:28:55 +0000 Subject: [PATCH 10/37] WIP around tests --- .../cas2bail/Cas2BailApplicationController.kt | 20 ++++---- .../cas2bail/Cas2BailApplicationService.kt | 50 ++++++++++--------- .../Cas2BailApplicationEntityFactory.kt | 47 +++++++++-------- 3 files changed, 63 insertions(+), 54 deletions(-) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailApplicationController.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailApplicationController.kt index ae01b8fd91..de7adce706 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailApplicationController.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailApplicationController.kt @@ -31,8 +31,8 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas2Applicatio ) class Cas2BailApplicationController( private val httpAuthService: HttpAuthService, - private val applicationService: Cas2BailApplicationService, - private val applicationsTransformer: Cas2BailApplicationsTransformer, + private val cas2BailApplicationService: Cas2BailApplicationService, + private val cas2BailApplicationsTransformer: Cas2BailApplicationsTransformer, private val objectMapper: ObjectMapper, private val offenderService: OffenderService, private val userService: NomisUserService, @@ -49,7 +49,7 @@ class Cas2BailApplicationController( val pageCriteria = PageCriteria("createdAt", SortDirection.desc, page) - val (applications, metadata) = applicationService.getCas2BailApplications(prisonCode, isSubmitted, user, pageCriteria) + val (applications, metadata) = cas2BailApplicationService.getCas2BailApplications(prisonCode, isSubmitted, user, pageCriteria) return ResponseEntity.ok().headers( metadata?.toHeaders(), @@ -60,7 +60,7 @@ class Cas2BailApplicationController( val user = userService.getUserForRequest() val application = when ( - val applicationResult = applicationService + val applicationResult = cas2BailApplicationService .getCas2BailApplicationForUser( applicationId, user @@ -85,7 +85,7 @@ class Cas2BailApplicationController( val personInfo = offenderService.getFullInfoForPersonOrThrow(body.crn) - val applicationResult = applicationService.createCas2BailApplication( + val applicationResult = cas2BailApplicationService.createCas2BailApplication( body.crn, user, nomisPrincipal.token.tokenValue, @@ -100,7 +100,7 @@ class Cas2BailApplicationController( return ResponseEntity .created(URI.create("/cas2/applications/${application.id}")) - .body(applicationsTransformer.transformJpaToApi(application, personInfo)) + .body(cas2BailApplicationsTransformer.transformJpaToApi(application, personInfo)) } @Transactional @@ -112,7 +112,7 @@ class Cas2BailApplicationController( val serializedData = objectMapper.writeValueAsString(body.data) - val applicationResult = applicationService.updateCas2BailApplication( + val applicationResult = cas2BailApplicationService.updateCas2BailApplication( applicationId = applicationId, data = serializedData, @@ -139,7 +139,7 @@ class Cas2BailApplicationController( override fun applicationsApplicationIdAbandonPut(applicationId: UUID): ResponseEntity { val user = userService.getUserForRequest() - val validationResult = when (val applicationResult = applicationService.abandonCas2BailApplication(applicationId, user)) { + val validationResult = when (val applicationResult = cas2BailApplicationService.abandonCas2BailApplication(applicationId, user)) { is AuthorisableActionResult.NotFound -> throw NotFoundProblem(applicationId, "Application") is AuthorisableActionResult.Unauthorised -> throw ForbiddenProblem() is AuthorisableActionResult.Success -> applicationResult.entity @@ -161,7 +161,7 @@ class Cas2BailApplicationController( val personNamesMap = offenderService.getMapOfPersonNamesAndCrns(crns) return applicationSummaries.map { application -> - applicationsTransformer.transformJpaSummaryToSummary(application, personNamesMap[application.crn]!!) + cas2BailApplicationsTransformer.transformJpaSummaryToSummary(application, personNamesMap[application.crn]!!) } } @@ -170,6 +170,6 @@ class Cas2BailApplicationController( ): Application { val personInfo = offenderService.getFullInfoForPersonOrThrow(application.crn) - return applicationsTransformer.transformJpaToApi(application, personInfo) + return cas2BailApplicationsTransformer.transformJpaToApi(application, personInfo) } } \ No newline at end of file diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationService.kt index f04b4b898f..31100e39aa 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationService.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationService.kt @@ -29,8 +29,8 @@ import java.util.* @Service("Cas2BailApplicationService") class Cas2BailApplicationService( private val cas2BailApplicationRepository: Cas2BailApplicationRepository, - private val lockableApplicationRepository: Cas2BailLockableApplicationRepository, - private val applicationSummaryRepository: Cas2BailApplicationSummaryRepository, + private val cas2BailLockableApplicationRepository: Cas2BailLockableApplicationRepository, + private val cas2BailApplicationSummaryRepository: Cas2BailApplicationSummaryRepository, private val jsonSchemaService: JsonSchemaService, private val offenderService: OffenderService, private val cas2BailUserAccessService: Cas2BailUserAccessService, @@ -44,15 +44,15 @@ class Cas2BailApplicationService( ) { val repositoryUserFunctionMap = mapOf( - null to applicationSummaryRepository::findByUserId, - true to applicationSummaryRepository::findByUserIdAndSubmittedAtIsNotNull, - false to applicationSummaryRepository::findByUserIdAndSubmittedAtIsNull, + null to cas2BailApplicationSummaryRepository::findByUserId, + true to cas2BailApplicationSummaryRepository::findByUserIdAndSubmittedAtIsNotNull, + false to cas2BailApplicationSummaryRepository::findByUserIdAndSubmittedAtIsNull, ) val repositoryPrisonFunctionMap = mapOf( - null to applicationSummaryRepository::findByPrisonCode, - true to applicationSummaryRepository::findByPrisonCodeAndSubmittedAtIsNotNull, - false to applicationSummaryRepository::findByPrisonCodeAndSubmittedAtIsNull, + null to cas2BailApplicationSummaryRepository::findByPrisonCode, + true to cas2BailApplicationSummaryRepository::findByPrisonCodeAndSubmittedAtIsNotNull, + false to cas2BailApplicationSummaryRepository::findByPrisonCodeAndSubmittedAtIsNull, ) fun getCas2BailApplications( @@ -73,7 +73,7 @@ class Cas2BailApplicationService( fun getAllSubmittedCas2BailApplicationsForAssessor(pageCriteria: PageCriteria): Pair, PaginationMetadata?> { val pageable = getPageableOrAllPages(pageCriteria) - val response = applicationSummaryRepository.findBySubmittedAtIsNotNull(pageable) + val response = cas2BailApplicationSummaryRepository.findBySubmittedAtIsNotNull(pageable) val metadata = getMetadata(response, pageCriteria) @@ -127,20 +127,24 @@ class Cas2BailApplicationService( return fieldValidationError } + val id = UUID.randomUUID() + + val entityToSave = Cas2BailApplicationEntity( + id = id, + crn = crn, + createdByUser = user, + data = null, + document = null, + schemaVersion = jsonSchemaService.getNewestSchema(Cas2BailApplicationJsonSchemaEntity::class.java), + createdAt = OffsetDateTime.now(), + submittedAt = null, + schemaUpToDate = true, + nomsNumber = offenderDetails.otherIds.nomsNumber, + telephoneNumber = null, + ) + val createdApplication = cas2BailApplicationRepository.save( - Cas2BailApplicationEntity( - id = UUID.randomUUID(), - crn = crn, - createdByUser = user, - data = null, - document = null, - schemaVersion = jsonSchemaService.getNewestSchema(Cas2BailApplicationJsonSchemaEntity::class.java), - createdAt = OffsetDateTime.now(), - submittedAt = null, - schemaUpToDate = true, - nomsNumber = offenderDetails.otherIds.nomsNumber, - telephoneNumber = null, - ), + entityToSave, ) return success(createdApplication.apply { schemaUpToDate = true }) @@ -225,7 +229,7 @@ class Cas2BailApplicationService( ): CasResult { val applicationId = submitApplication.applicationId - lockableApplicationRepository.acquirePessimisticLock(applicationId) + cas2BailLockableApplicationRepository.acquirePessimisticLock(applicationId) var application = cas2BailApplicationRepository.findByIdOrNull(applicationId) ?.let(jsonSchemaService::checkCas2BailSchemaOutdated) diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/cas2bail/Cas2BailApplicationEntityFactory.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/cas2bail/Cas2BailApplicationEntityFactory.kt index 35c6869c03..e2d95068e5 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/cas2bail/Cas2BailApplicationEntityFactory.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/cas2bail/Cas2BailApplicationEntityFactory.kt @@ -130,25 +130,30 @@ class Cas2BailApplicationEntityFactory : Factory { this.conditionalReleaseDate = { conditionalReleaseDate } } - override fun produce(): Cas2BailApplicationEntity = Cas2BailApplicationEntity( - id = this.id(), - crn = this.crn(), - createdByUser = this.createdByUser?.invoke() ?: throw RuntimeException("Must provide a createdByUser"), - data = this.data(), - document = this.document(), - schemaVersion = this.applicationSchema(), - createdAt = this.createdAt(), - submittedAt = this.submittedAt(), - abandonedAt = this.abandonedAt(), - statusUpdates = this.statusUpdates(), - schemaUpToDate = false, - nomsNumber = this.nomsNumber(), - telephoneNumber = this.telephoneNumber(), - notes = this.notes(), - assessment = this.assessment(), - referringPrisonCode = this.referringPrisonCode(), - hdcEligibilityDate = this.hdcEligibilityDate(), - conditionalReleaseDate = this.conditionalReleaseDate(), - preferredAreas = this.preferredAreas(), - ) + override fun produce(): Cas2BailApplicationEntity { + val entity = Cas2BailApplicationEntity( + id = this.id(), + crn = this.crn(), + createdByUser = this.createdByUser?.invoke() ?: throw RuntimeException("Must provide a createdByUser"), + data = this.data(), + document = this.document(), + schemaVersion = this.applicationSchema(), + createdAt = this.createdAt(), + submittedAt = this.submittedAt(), + abandonedAt = this.abandonedAt(), + statusUpdates = this.statusUpdates(), + schemaUpToDate = false, + nomsNumber = this.nomsNumber(), + telephoneNumber = this.telephoneNumber(), + notes = this.notes(), + assessment = this.assessment(), + referringPrisonCode = this.referringPrisonCode(), + hdcEligibilityDate = this.hdcEligibilityDate(), + conditionalReleaseDate = this.conditionalReleaseDate(), + preferredAreas = this.preferredAreas(), + ) + + return entity + + } } \ No newline at end of file From 3ddd76ba7a767d948b72e73eb6fe6e385062154e Mon Sep 17 00:00:00 2001 From: Toby Batch Date: Tue, 3 Dec 2024 14:53:26 +0000 Subject: [PATCH 11/37] feat: adds the view for live summary --- ...02180207__make_cas2_bail_summary_views.sql | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/main/resources/db/migration/all/20241202180207__make_cas2_bail_summary_views.sql diff --git a/src/main/resources/db/migration/all/20241202180207__make_cas2_bail_summary_views.sql b/src/main/resources/db/migration/all/20241202180207__make_cas2_bail_summary_views.sql new file mode 100644 index 0000000000..dc83f6f1c4 --- /dev/null +++ b/src/main/resources/db/migration/all/20241202180207__make_cas2_bail_summary_views.sql @@ -0,0 +1,49 @@ +-- remove id cast to TEXT +DROP VIEW IF EXISTS cas_2_bail_application_summary CASCADE; +CREATE OR REPLACE VIEW cas_2_bail_application_summary AS SELECT + a.id, + a.crn, + a.noms_number, + CAST(a.created_by_user_id AS TEXT), + nu.name, + a.created_at, + a.submitted_at, + a.hdc_eligibility_date, + asu.label, + CAST(asu.status_id AS TEXT), + a.referring_prison_code, + a.conditional_release_date, + asu.created_at AS status_created_at, + a.abandoned_at +FROM cas_2_applications a +LEFT JOIN (SELECT DISTINCT ON (application_id) su.application_id, su.label, su.status_id, su.created_at + FROM cas_2_bail_status_updates su + ORDER BY su.application_id, su.created_at DESC) as asu + ON a.id = asu.application_id +JOIN nomis_users nu ON nu.id = a.created_by_user_id; + +DROP TABLE IF EXISTS cas_2_bail_application_live_summary CASCADE; +DROP VIEW IF EXISTS cas_2_bail_application_live_summary CASCADE; +CREATE OR REPLACE VIEW cas_2_bail_application_live_summary AS SELECT + a.id, + a.crn, + a.noms_number, + a.created_by_user_id, + a.name, + a.created_at, + a.submitted_at, + a.hdc_eligibility_date, + a.label, + a.status_id, + a.referring_prison_code, + a.abandoned_at +FROM cas_2_bail_application_summary a +WHERE (a.conditional_release_date IS NULL OR a.conditional_release_date >= current_date) +AND a.abandoned_at IS NULL +AND a.status_id IS NULL + OR (a.status_id = '004e2419-9614-4c1e-a207-a8418009f23d' AND a.status_created_at > (current_date - INTERVAL '32 DAY')) -- Referral withdrawn + OR (a.status_id = 'f13bbdd6-44f1-4362-b9d3-e6f1298b1bf9' AND a.status_created_at > (current_date - INTERVAL '32 DAY')) -- Referral cancelled + OR (a.status_id = '89458555-3219-44a2-9584-c4f715d6b565' AND a.status_created_at > (current_date - INTERVAL '32 DAY')) -- Awaiting arrival + OR (a.status_id NOT IN ('004e2419-9614-4c1e-a207-a8418009f23d', + 'f13bbdd6-44f1-4362-b9d3-e6f1298b1bf9', + '89458555-3219-44a2-9584-c4f715d6b565')); \ No newline at end of file From a6964b11919de157378ab08609258abe0fd33f5c Mon Sep 17 00:00:00 2001 From: Toby Batch Date: Tue, 3 Dec 2024 15:17:48 +0000 Subject: [PATCH 12/37] fix: merged new bail migrations --- ...roller_and_linked_services_in_cas2bail.sql | 63 ++++++++++++++----- ...02180207__make_cas2_bail_summary_views.sql | 49 --------------- 2 files changed, 46 insertions(+), 66 deletions(-) delete mode 100644 src/main/resources/db/migration/all/20241202180207__make_cas2_bail_summary_views.sql diff --git a/src/main/resources/db/migration/all/20241128163133__application_controller_and_linked_services_in_cas2bail.sql b/src/main/resources/db/migration/all/20241128163133__application_controller_and_linked_services_in_cas2bail.sql index 75dfb4fb0f..5c869be832 100644 --- a/src/main/resources/db/migration/all/20241128163133__application_controller_and_linked_services_in_cas2bail.sql +++ b/src/main/resources/db/migration/all/20241128163133__application_controller_and_linked_services_in_cas2bail.sql @@ -5,23 +5,6 @@ CREATE TABLE cas_2_bail_application_json_schemas CONSTRAINT pk_cas_2_bail_application_json_schemas PRIMARY KEY (json_schema_id) ); -CREATE TABLE cas_2_bail_application_live_summary -( - id UUID NOT NULL, - crn VARCHAR(255), - noms_number VARCHAR(255), - created_by_user_id VARCHAR(255), - name VARCHAR(255), - created_at TIMESTAMP WITHOUT TIME ZONE, - submitted_at TIMESTAMP WITHOUT TIME ZONE, - abandoned_at TIMESTAMP WITHOUT TIME ZONE, - hdc_eligibility_date date, - label VARCHAR(255), - status_id VARCHAR(255), - referring_prison_code VARCHAR(255), - CONSTRAINT pk_cas_2_bail_application_live_summary PRIMARY KEY (id) -); - CREATE TABLE cas_2_bail_application_notes ( id UUID NOT NULL, @@ -126,3 +109,49 @@ ALTER TABLE cas_2_bail_status_updates ALTER TABLE cas_2_bail_status_update_details ADD CONSTRAINT FK_CAS_2_BAIL_STATUS_UPDATE_DETAILS_ON_STATUS_UPDATE FOREIGN KEY (status_update_id) REFERENCES cas_2_bail_status_updates (id); + +CREATE OR REPLACE VIEW cas_2_bail_application_summary AS SELECT + a.id, + a.crn, + a.noms_number, + CAST(a.created_by_user_id AS TEXT), + nu.name, + a.created_at, + a.submitted_at, + a.hdc_eligibility_date, + asu.label, + CAST(asu.status_id AS TEXT), + a.referring_prison_code, + a.conditional_release_date, + asu.created_at AS status_created_at, + a.abandoned_at +FROM cas_2_applications a +LEFT JOIN (SELECT DISTINCT ON (application_id) su.application_id, su.label, su.status_id, su.created_at + FROM cas_2_bail_status_updates su + ORDER BY su.application_id, su.created_at DESC) as asu + ON a.id = asu.application_id +JOIN nomis_users nu ON nu.id = a.created_by_user_id; + +CREATE OR REPLACE VIEW cas_2_bail_application_live_summary AS SELECT + a.id, + a.crn, + a.noms_number, + a.created_by_user_id, + a.name, + a.created_at, + a.submitted_at, + a.hdc_eligibility_date, + a.label, + a.status_id, + a.referring_prison_code, + a.abandoned_at +FROM cas_2_bail_application_summary a +WHERE (a.conditional_release_date IS NULL OR a.conditional_release_date >= current_date) +AND a.abandoned_at IS NULL +AND a.status_id IS NULL + OR (a.status_id = '004e2419-9614-4c1e-a207-a8418009f23d' AND a.status_created_at > (current_date - INTERVAL '32 DAY')) -- Referral withdrawn + OR (a.status_id = 'f13bbdd6-44f1-4362-b9d3-e6f1298b1bf9' AND a.status_created_at > (current_date - INTERVAL '32 DAY')) -- Referral cancelled + OR (a.status_id = '89458555-3219-44a2-9584-c4f715d6b565' AND a.status_created_at > (current_date - INTERVAL '32 DAY')) -- Awaiting arrival + OR (a.status_id NOT IN ('004e2419-9614-4c1e-a207-a8418009f23d', + 'f13bbdd6-44f1-4362-b9d3-e6f1298b1bf9', + '89458555-3219-44a2-9584-c4f715d6b565')); \ No newline at end of file diff --git a/src/main/resources/db/migration/all/20241202180207__make_cas2_bail_summary_views.sql b/src/main/resources/db/migration/all/20241202180207__make_cas2_bail_summary_views.sql deleted file mode 100644 index dc83f6f1c4..0000000000 --- a/src/main/resources/db/migration/all/20241202180207__make_cas2_bail_summary_views.sql +++ /dev/null @@ -1,49 +0,0 @@ --- remove id cast to TEXT -DROP VIEW IF EXISTS cas_2_bail_application_summary CASCADE; -CREATE OR REPLACE VIEW cas_2_bail_application_summary AS SELECT - a.id, - a.crn, - a.noms_number, - CAST(a.created_by_user_id AS TEXT), - nu.name, - a.created_at, - a.submitted_at, - a.hdc_eligibility_date, - asu.label, - CAST(asu.status_id AS TEXT), - a.referring_prison_code, - a.conditional_release_date, - asu.created_at AS status_created_at, - a.abandoned_at -FROM cas_2_applications a -LEFT JOIN (SELECT DISTINCT ON (application_id) su.application_id, su.label, su.status_id, su.created_at - FROM cas_2_bail_status_updates su - ORDER BY su.application_id, su.created_at DESC) as asu - ON a.id = asu.application_id -JOIN nomis_users nu ON nu.id = a.created_by_user_id; - -DROP TABLE IF EXISTS cas_2_bail_application_live_summary CASCADE; -DROP VIEW IF EXISTS cas_2_bail_application_live_summary CASCADE; -CREATE OR REPLACE VIEW cas_2_bail_application_live_summary AS SELECT - a.id, - a.crn, - a.noms_number, - a.created_by_user_id, - a.name, - a.created_at, - a.submitted_at, - a.hdc_eligibility_date, - a.label, - a.status_id, - a.referring_prison_code, - a.abandoned_at -FROM cas_2_bail_application_summary a -WHERE (a.conditional_release_date IS NULL OR a.conditional_release_date >= current_date) -AND a.abandoned_at IS NULL -AND a.status_id IS NULL - OR (a.status_id = '004e2419-9614-4c1e-a207-a8418009f23d' AND a.status_created_at > (current_date - INTERVAL '32 DAY')) -- Referral withdrawn - OR (a.status_id = 'f13bbdd6-44f1-4362-b9d3-e6f1298b1bf9' AND a.status_created_at > (current_date - INTERVAL '32 DAY')) -- Referral cancelled - OR (a.status_id = '89458555-3219-44a2-9584-c4f715d6b565' AND a.status_created_at > (current_date - INTERVAL '32 DAY')) -- Awaiting arrival - OR (a.status_id NOT IN ('004e2419-9614-4c1e-a207-a8418009f23d', - 'f13bbdd6-44f1-4362-b9d3-e6f1298b1bf9', - '89458555-3219-44a2-9584-c4f715d6b565')); \ No newline at end of file From c8d6247af368b0180618fd18ccbb057a7c2d9a82 Mon Sep 17 00:00:00 2001 From: Toby Batch Date: Tue, 3 Dec 2024 15:34:01 +0000 Subject: [PATCH 13/37] chore: more test debug --- .../approvedpremisesapi/integration/IntegrationTestBase.kt | 4 ++++ .../integration/cas2/Cas2ApplicationTest.kt | 3 +++ .../integration/cas2bail/Cas2BailApplicationTest.kt | 7 +++++++ .../repository/ApplicationTestRepository.kt | 4 ++++ 4 files changed, 18 insertions(+) diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/IntegrationTestBase.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/IntegrationTestBase.kt index e7c8b9fb85..8a667cfac2 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/IntegrationTestBase.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/IntegrationTestBase.kt @@ -176,6 +176,9 @@ abstract class IntegrationTestBase { @Autowired lateinit var cas2BailApplicationRepository: Cas2BailApplicationTestRepository + @Autowired + lateinit var cas2BailAssessmentRepository: Cas2BailAssessmentTestRepository + @Autowired lateinit var cas2AssessmentRepository: Cas2AssessmentRepository @@ -486,6 +489,7 @@ abstract class IntegrationTestBase { cas2ApplicationEntityFactory = PersistedFactory({ Cas2ApplicationEntityFactory() }, cas2ApplicationRepository) cas2BailApplicationEntityFactory = PersistedFactory({ Cas2BailApplicationEntityFactory() }, cas2BailApplicationRepository) + cas2BailApplicationEntityFactory = PersistedFactory({ Cas2BailAssementEntityFactory() }, cas2BailAssessmentRepository) cas2AssessmentEntityFactory = PersistedFactory({ Cas2AssessmentEntityFactory() }, cas2AssessmentRepository) cas2StatusUpdateEntityFactory = PersistedFactory({ Cas2StatusUpdateEntityFactory() }, cas2StatusUpdateRepository) cas2BailStatusUpdateEntityFactory = PersistedFactory({ Cas2BailStatusUpdateEntityFactory() }, cas2BailStatusUpdateRepository) diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2/Cas2ApplicationTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2/Cas2ApplicationTest.kt index 3e858381bf..19ec2104e0 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2/Cas2ApplicationTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2/Cas2ApplicationTest.kt @@ -261,6 +261,9 @@ class Cas2ApplicationTest : IntegrationTestBase() { expiredApplicationIds.add(application.id) } + val xxx = cas2ApplicationRepository.findAll() + val yyy = cas2AssessmentRepository.findAll() + val rawResponseBody = webTestClient.get() .uri("/cas2/applications") .header("Authorization", "Bearer $jwt") diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationTest.kt index 6c18faa868..2a049ee35f 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationTest.kt @@ -255,6 +255,13 @@ class Cas2BailApplicationTest : IntegrationTestBase() { expiredApplicationIds.add(application.id) } + val aaa = cas2BailApplicationRepository.findAll() + val bbb = cas2BailAssessmentRepository.findAll() + + val xxx = cas2ApplicationRepository.findAll() + val yyy = cas2AssessmentRepository.findAll() + + val rawResponseBody = webTestClient.get() .uri("/cas2bail/applications") .header("Authorization", "Bearer $jwt") diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/repository/ApplicationTestRepository.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/repository/ApplicationTestRepository.kt index f0a9ce5c1b..b3e0e450e1 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/repository/ApplicationTestRepository.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/repository/ApplicationTestRepository.kt @@ -6,6 +6,7 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApprovedPremi import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.TemporaryAccommodationApplicationEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailAssessmentEntity import java.util.UUID @Repository @@ -17,5 +18,8 @@ interface Cas2ApplicationTestRepository : JpaRepository +@Repository +interface Cas2BailAssessmentTestRepository : JpaRepository + @Repository interface TemporaryAccommodationApplicationTestRepository : JpaRepository From e7c5f015f2c1c6c766e3ac246eecb8c494c798a1 Mon Sep 17 00:00:00 2001 From: garethCAS2 Date: Tue, 3 Dec 2024 15:40:50 +0000 Subject: [PATCH 14/37] chore: added Cas2BailAssessmentEntityFactory --- .../Cas2BailAssessmentEntityFactory.kt | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/cas2bail/Cas2BailAssessmentEntityFactory.kt diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/cas2bail/Cas2BailAssessmentEntityFactory.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/cas2bail/Cas2BailAssessmentEntityFactory.kt new file mode 100644 index 0000000000..972ee88054 --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/cas2bail/Cas2BailAssessmentEntityFactory.kt @@ -0,0 +1,56 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.cas2bail + +import io.github.bluegroundltd.kfactory.Factory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.NomisUserEntityFactory +import java.time.OffsetDateTime +import io.github.bluegroundltd.kfactory.Yielded +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailAssessmentEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailStatusUpdateEntity +import java.util.UUID + +class Cas2BailAssessmentEntityFactory: Factory { + private var id: Yielded = { UUID.randomUUID() } + private var createdAt: Yielded = { OffsetDateTime.now() } + private var application: Yielded = { + Cas2BailApplicationEntityFactory() + .withCreatedByUser(NomisUserEntityFactory().produce()) + .produce() + } + private var nacroReferralId: String? = null + private var assessorName: String? = null + private var statusUpdates: MutableList = mutableListOf() + + fun withId(id: UUID) = apply { + this.id = { id } + } + + fun withApplication(application: Cas2BailApplicationEntity) = apply { + this.application = { application } + } + + fun withStatusUpdates(statusUpdates: MutableList) = apply { + this.statusUpdates = statusUpdates + } + + fun withNacroReferralId(id: String) = apply { + this.nacroReferralId = id + } + + fun withAssessorName(name: String) = apply { + this.assessorName = name + } + + fun withCreatedAt(createdAt: OffsetDateTime) = apply { + this.createdAt = { createdAt } + } + + override fun produce(): Cas2BailAssessmentEntity = Cas2BailAssessmentEntity( + id = this.id(), + createdAt = this.createdAt(), + application = this.application(), + nacroReferralId = this.nacroReferralId, + assessorName = this.assessorName, + statusUpdates = this.statusUpdates, + ) +} \ No newline at end of file From dc1abef3c061bfb985bf0e8cfc0a38ca0a97944a Mon Sep 17 00:00:00 2001 From: garethCAS2 Date: Tue, 3 Dec 2024 16:35:10 +0000 Subject: [PATCH 15/37] chore: more test changes and some renaming --- .../cas2bail/Cas2BailApplicationEntity.kt | 12 ++++++++++-- .../cas2bail/Cas2BailAssessmentService.kt | 17 +++++++++-------- .../integration/IntegrationTestBase.kt | 9 +++++++-- .../integration/cas2/Cas2ApplicationTest.kt | 3 --- .../cas2bail/Cas2BailApplicationTest.kt | 2 -- 5 files changed, 26 insertions(+), 17 deletions(-) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationEntity.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationEntity.kt index 77a75f5cbe..c04ee0c27d 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationEntity.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationEntity.kt @@ -4,8 +4,16 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.* import io.hypersistence.utils.hibernate.type.json.JsonType -import jakarta.persistence.* +import jakarta.persistence.Entity +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.LockModeType +import jakarta.persistence.ManyToOne +import jakarta.persistence.OneToMany +import jakarta.persistence.OneToOne +import jakarta.persistence.Table import org.hibernate.annotations.Immutable +import org.hibernate.annotations.OrderBy import org.hibernate.annotations.Type import org.springframework.data.domain.Slice import org.springframework.data.jpa.repository.JpaRepository @@ -76,7 +84,7 @@ data class Cas2BailApplicationEntity( @OrderBy("createdAt DESC") var notes: MutableList? = null, - @OneToOne(fetch = FetchType.LAZY) + @OneToOne(mappedBy = "application") var assessment: Cas2BailAssessmentEntity? = null, @Transient diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailAssessmentService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailAssessmentService.kt index da6aef71eb..3d4fc340cf 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailAssessmentService.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailAssessmentService.kt @@ -8,22 +8,20 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.UpdateCas2Asse import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailAssessmentEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailAssessmentRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.AuthorisableActionResult import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.CasResult -import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.ValidatableActionResult import java.time.OffsetDateTime import java.util.* @Service("Cas2BailAssessmentService") class Cas2BailAssessmentService ( - private val assessmentRepository: Cas2BailAssessmentRepository, + private val cas2AssessmentRepository: Cas2BailAssessmentRepository, ) { @Transactional fun createCas2BailAssessment(cas2BailApplicationEntity: Cas2BailApplicationEntity): Cas2BailAssessmentEntity = - assessmentRepository.save( + cas2AssessmentRepository.save( Cas2BailAssessmentEntity( id = UUID.randomUUID(), createdAt = OffsetDateTime.now(), @@ -31,8 +29,11 @@ class Cas2BailAssessmentService ( ), ) - fun updateAssessment(assessmentId: UUID, newAssessment: UpdateCas2Assessment): CasResult { - val assessmentEntity = assessmentRepository.findByIdOrNull(assessmentId) + fun updateAssessment( + assessmentId: UUID, + newAssessment: UpdateCas2Assessment) + : CasResult { + val assessmentEntity = cas2AssessmentRepository.findByIdOrNull(assessmentId) ?: return CasResult.NotFound() assessmentEntity.apply { @@ -40,13 +41,13 @@ class Cas2BailAssessmentService ( this.assessorName = newAssessment.assessorName } - val savedAssessment = assessmentRepository.save(assessmentEntity) + val savedAssessment = cas2AssessmentRepository.save(assessmentEntity) return CasResult.Success(savedAssessment) } fun getAssessment(assessmentId: UUID): CasResult { - val assessmentEntity = assessmentRepository.findByIdOrNull(assessmentId) + val assessmentEntity = cas2AssessmentRepository.findByIdOrNull(assessmentId) ?: return CasResult.NotFound() return CasResult.Success(assessmentEntity) diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/IntegrationTestBase.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/IntegrationTestBase.kt index 8a667cfac2..1e18204d4c 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/IntegrationTestBase.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/IntegrationTestBase.kt @@ -33,6 +33,7 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.config.NotifyConfig import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.* import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.cas1.Cas1CruManagementAreaEntityFactory import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.cas2bail.Cas2BailApplicationEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.cas2bail.Cas2BailAssessmentEntityFactory import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.cas2bail.Cas2BailStatusUpdateEntityFactory import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.asserter.DomainEventAsserter import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.asserter.EmailNotificationAsserter @@ -43,6 +44,8 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.mocks.Mutabl import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.mocks.NoOpSentryService import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.* import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailAssessmentEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailAssessmentRepository import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailStatusUpdateEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.community.OffenderDetailSummary import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.community.UserOffenderAccess @@ -177,7 +180,7 @@ abstract class IntegrationTestBase { lateinit var cas2BailApplicationRepository: Cas2BailApplicationTestRepository @Autowired - lateinit var cas2BailAssessmentRepository: Cas2BailAssessmentTestRepository + lateinit var cas2BailAssessmentRepository: Cas2BailAssessmentRepository @Autowired lateinit var cas2AssessmentRepository: Cas2AssessmentRepository @@ -374,7 +377,9 @@ abstract class IntegrationTestBase { lateinit var approvedPremisesApplicationEntityFactory: PersistedFactory lateinit var cas2ApplicationEntityFactory: PersistedFactory lateinit var cas2BailApplicationEntityFactory: PersistedFactory + lateinit var cas2BailAssementEntityFactory: PersistedFactory lateinit var cas2AssessmentEntityFactory: PersistedFactory + lateinit var cas2StatusUpdateEntityFactory: PersistedFactory lateinit var cas2BailStatusUpdateEntityFactory: PersistedFactory lateinit var cas2StatusUpdateDetailEntityFactory: PersistedFactory @@ -489,7 +494,7 @@ abstract class IntegrationTestBase { cas2ApplicationEntityFactory = PersistedFactory({ Cas2ApplicationEntityFactory() }, cas2ApplicationRepository) cas2BailApplicationEntityFactory = PersistedFactory({ Cas2BailApplicationEntityFactory() }, cas2BailApplicationRepository) - cas2BailApplicationEntityFactory = PersistedFactory({ Cas2BailAssementEntityFactory() }, cas2BailAssessmentRepository) + cas2BailAssementEntityFactory = PersistedFactory({ Cas2BailAssessmentEntityFactory() }, cas2BailAssessmentRepository) cas2AssessmentEntityFactory = PersistedFactory({ Cas2AssessmentEntityFactory() }, cas2AssessmentRepository) cas2StatusUpdateEntityFactory = PersistedFactory({ Cas2StatusUpdateEntityFactory() }, cas2StatusUpdateRepository) cas2BailStatusUpdateEntityFactory = PersistedFactory({ Cas2BailStatusUpdateEntityFactory() }, cas2BailStatusUpdateRepository) diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2/Cas2ApplicationTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2/Cas2ApplicationTest.kt index 19ec2104e0..3e858381bf 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2/Cas2ApplicationTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2/Cas2ApplicationTest.kt @@ -261,9 +261,6 @@ class Cas2ApplicationTest : IntegrationTestBase() { expiredApplicationIds.add(application.id) } - val xxx = cas2ApplicationRepository.findAll() - val yyy = cas2AssessmentRepository.findAll() - val rawResponseBody = webTestClient.get() .uri("/cas2/applications") .header("Authorization", "Bearer $jwt") diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationTest.kt index 2a049ee35f..aae0fc2885 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationTest.kt @@ -258,8 +258,6 @@ class Cas2BailApplicationTest : IntegrationTestBase() { val aaa = cas2BailApplicationRepository.findAll() val bbb = cas2BailAssessmentRepository.findAll() - val xxx = cas2ApplicationRepository.findAll() - val yyy = cas2AssessmentRepository.findAll() val rawResponseBody = webTestClient.get() From 86ab9634c6c88db12d0751dd32a8a7d89ae7f5c1 Mon Sep 17 00:00:00 2001 From: Toby Batch Date: Wed, 4 Dec 2024 09:58:49 +0000 Subject: [PATCH 16/37] chore: debug code for why the JPA isn't writing to some objects --- .../cas2bail/Cas2BailAssessmentEntity.kt | 1 - .../Cas2BailStatusUpdateDetailEntry.kt | 2 +- .../service/cas2/ApplicationService.kt | 2 + .../cas2bail/Cas2BailApplicationService.kt | 3 + .../cas2bail/Cas2BailApplicationTest.kt | 64 +++++++++++++++---- 5 files changed, 57 insertions(+), 15 deletions(-) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailAssessmentEntity.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailAssessmentEntity.kt index d934cb5e0c..dc062b7b76 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailAssessmentEntity.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailAssessmentEntity.kt @@ -22,7 +22,6 @@ data class Cas2BailAssessmentEntity( val id: UUID, @OneToOne - @Lazy val application: Cas2BailApplicationEntity, val createdAt: OffsetDateTime, diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailStatusUpdateDetailEntry.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailStatusUpdateDetailEntry.kt index 032abe277c..be50954750 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailStatusUpdateDetailEntry.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailStatusUpdateDetailEntry.kt @@ -24,7 +24,7 @@ data class Cas2BailStatusUpdateDetailEntity( val label: String, - @ManyToOne(fetch = FetchType.LAZY) + @ManyToOne() @JoinColumn(name = "status_update_id") val statusUpdate: Cas2BailStatusUpdateEntity, diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2/ApplicationService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2/ApplicationService.kt index 2cc434eba6..52a516d6d0 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2/ApplicationService.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2/ApplicationService.kt @@ -70,6 +70,8 @@ class ApplicationService( pageCriteria: PageCriteria, ): Pair, PaginationMetadata?> { val response = if (prisonCode == null) { + val uid = user.id.toString() + val records = applicationSummaryRepository.findByUserId(uid, null) repositoryUserFunctionMap.get(isSubmitted)!!(user.id.toString(), getPageableOrAllPages(pageCriteria)) } else { repositoryPrisonFunctionMap.get(isSubmitted)!!(prisonCode, getPageableOrAllPages(pageCriteria)) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationService.kt index 31100e39aa..7984beec36 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationService.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationService.kt @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import jakarta.transaction.Transactional import org.springframework.beans.factory.annotation.Value import org.springframework.data.repository.findByIdOrNull +import org.springframework.jdbc.core.JdbcTemplate import org.springframework.stereotype.Service import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.events.cas2.model.* import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.SubmitCas2Application @@ -61,6 +62,8 @@ class Cas2BailApplicationService( user: NomisUserEntity, pageCriteria: PageCriteria, ): Pair, PaginationMetadata?> { + val uid = user.id.toString() + val records = cas2BailApplicationSummaryRepository.findByUserId(uid, null) val response = if (prisonCode == null) { repositoryUserFunctionMap.get(isSubmitted)!!(user.id.toString(), getPageableOrAllPages(pageCriteria)) } else { diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationTest.kt index aae0fc2885..da0668291f 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationTest.kt @@ -7,6 +7,7 @@ import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.node.NullNode import com.ninjasquad.springmockk.SpykBean import io.mockk.clearMocks +import jakarta.persistence.EntityManager import org.assertj.core.api.Assertions import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach @@ -14,6 +15,11 @@ import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ValueSource +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query +import org.springframework.data.repository.query.Param +import org.springframework.stereotype.Repository import org.springframework.test.web.reactive.server.returnResult import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.* import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.IntegrationTestBase @@ -25,6 +31,7 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.httpmocks.co import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.httpmocks.prisonAPIMockNotFoundInmateDetailsCall import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.* import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationSummaryEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailStatusUpdateEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.community.OffenderDetailSummary import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.randomDateAfter @@ -37,6 +44,10 @@ import kotlin.math.sign class Cas2BailApplicationTest : IntegrationTestBase() { + /* START HERE - TOBY DEBUG */ + @Autowired + private lateinit var entityManager: EntityManager + @SpykBean lateinit var realApplicationRepository: ApplicationRepository @@ -175,6 +186,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { @Nested inner class GetToIndex { + @Test fun `return unexpired applications when applications GET is requested`() { val unexpiredSubset = setOf( @@ -258,8 +270,6 @@ class Cas2BailApplicationTest : IntegrationTestBase() { val aaa = cas2BailApplicationRepository.findAll() val bbb = cas2BailAssessmentRepository.findAll() - - val rawResponseBody = webTestClient.get() .uri("/cas2bail/applications") .header("Authorization", "Bearer $jwt") @@ -286,15 +296,15 @@ class Cas2BailApplicationTest : IntegrationTestBase() { givenACas2PomUser { userEntity, jwt -> givenACas2PomUser { otherUser, _ -> givenAnOffender { offenderDetails, _ -> - cas2ApplicationJsonSchemaRepository.deleteAll() + cas2BailApplicationJsonSchemaRepository.deleteAll() - val applicationSchema = cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + val applicationSchema = cas2BailApplicationJsonSchemaEntityFactory.produceAndPersist { withAddedAt(OffsetDateTime.now()) withId(UUID.randomUUID()) } // abandoned application - val abandonedApplicationEntity = cas2ApplicationEntityFactory.produceAndPersist { + val abandonedApplicationEntity = cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(applicationSchema) withCreatedByUser(userEntity) withCrn(offenderDetails.otherIds.crn) @@ -304,7 +314,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } // unsubmitted application - val firstApplicationEntity = cas2ApplicationEntityFactory.produceAndPersist { + val firstApplicationEntity = cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(applicationSchema) withCreatedByUser(userEntity) withCrn(offenderDetails.otherIds.crn) @@ -314,7 +324,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } // submitted application, CRD >= today so should be returned - val secondApplicationEntity = cas2ApplicationEntityFactory.produceAndPersist { + val secondApplicationEntity = cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(applicationSchema) withCreatedByUser(userEntity) withCrn(offenderDetails.otherIds.crn) @@ -325,7 +335,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } // submitted application, CRD = yesterday, so should not be returned - val thirdApplicationEntity = cas2ApplicationEntityFactory.produceAndPersist { + val thirdApplicationEntity = cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(applicationSchema) withCreatedByUser(userEntity) withCrn(offenderDetails.otherIds.crn) @@ -335,13 +345,13 @@ class Cas2BailApplicationTest : IntegrationTestBase() { withConditionalReleaseDate(LocalDate.now().minusDays(1)) } - val statusUpdate = cas2StatusUpdateEntityFactory.produceAndPersist { + val statusUpdate = cas2BailStatusUpdateEntityFactory.produceAndPersist { withLabel("More information requested") withApplication(secondApplicationEntity) withAssessor(externalUserEntityFactory.produceAndPersist()) } - val otherCas2ApplicationEntity = cas2ApplicationEntityFactory.produceAndPersist { + val othercas2BailApplicationEntity = cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(applicationSchema) withCreatedByUser(otherUser) withCrn(offenderDetails.otherIds.crn) @@ -349,7 +359,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } val rawResponseBody = webTestClient.get() - .uri("/cas2/applications") + .uri("/cas2bail/applications") .header("Authorization", "Bearer $jwt") .header("X-Service-Name", ServiceName.cas2.value) .exchange() @@ -359,8 +369,36 @@ class Cas2BailApplicationTest : IntegrationTestBase() { .responseBody .blockFirst() + /* START HERE - TOBY DEBUG */ + val r1 = entityManager.createNativeQuery("SELECT * FROM cas_2_bail_applications").resultList + val r2 = entityManager.createNativeQuery("SELECT * FROM cas_2_bail_application_live_summary").resultList + val r3 = entityManager.createNativeQuery("SELECT * FROM cas_2_bail_status_updates").resultList + val r4 = entityManager.createNativeQuery("SELECT * FROM cas_2_bail_assessments").resultList + val r5 = entityManager.createNativeQuery("SELECT\n" + + " a.id,\n" + + " a.crn,\n" + + " a.noms_number,\n" + + " CAST(a.created_by_user_id AS TEXT),\n" + + " nu.name,\n" + + " a.created_at,\n" + + " a.submitted_at,\n" + + " a.hdc_eligibility_date,\n" + + " asu.label,\n" + + " CAST(asu.status_id AS TEXT),\n" + + " a.referring_prison_code,\n" + + " a.conditional_release_date,\n" + + " asu.created_at AS status_created_at,\n" + + " a.abandoned_at\n" + + "FROM cas_2_applications a\n" + + "LEFT JOIN (SELECT DISTINCT ON (application_id) su.application_id, su.label, su.status_id, su.created_at\n" + + " FROM cas_2_bail_status_updates su\n" + + " ORDER BY su.application_id, su.created_at DESC) as asu\n" + + " ON a.id = asu.application_id\n" + + "JOIN nomis_users nu ON nu.id = a.created_by_user_id").resultList + /* END HERE - TOBY DEBUG */ + val responseBody = - objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) + objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) // check transformers were able to return all fields Assertions.assertThat(responseBody).anyMatch { @@ -380,7 +418,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } Assertions.assertThat(responseBody).noneMatch { - otherCas2ApplicationEntity.id == it.id + othercas2BailApplicationEntity.id == it.id } Assertions.assertThat(responseBody).noneMatch { From 4c784bead72e691c9976a7e38c6de7cebc606aa7 Mon Sep 17 00:00:00 2001 From: garethCAS2 Date: Wed, 4 Dec 2024 11:08:47 +0000 Subject: [PATCH 17/37] feat: Cas2BailApplication tests running --- ...roller_and_linked_services_in_cas2bail.sql | 2 +- .../integration/IntegrationTestBase.kt | 4 +- .../cas2bail/Cas2BailApplicationTest.kt | 297 ++++++++---------- 3 files changed, 133 insertions(+), 170 deletions(-) diff --git a/src/main/resources/db/migration/all/20241128163133__application_controller_and_linked_services_in_cas2bail.sql b/src/main/resources/db/migration/all/20241128163133__application_controller_and_linked_services_in_cas2bail.sql index 5c869be832..a3e0a7f1a5 100644 --- a/src/main/resources/db/migration/all/20241128163133__application_controller_and_linked_services_in_cas2bail.sql +++ b/src/main/resources/db/migration/all/20241128163133__application_controller_and_linked_services_in_cas2bail.sql @@ -125,7 +125,7 @@ CREATE OR REPLACE VIEW cas_2_bail_application_summary AS SELECT a.conditional_release_date, asu.created_at AS status_created_at, a.abandoned_at -FROM cas_2_applications a +FROM cas_2_bail_applications a LEFT JOIN (SELECT DISTINCT ON (application_id) su.application_id, su.label, su.status_id, su.created_at FROM cas_2_bail_status_updates su ORDER BY su.application_id, su.created_at DESC) as asu diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/IntegrationTestBase.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/IntegrationTestBase.kt index 1e18204d4c..57aa23de91 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/IntegrationTestBase.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/IntegrationTestBase.kt @@ -377,7 +377,7 @@ abstract class IntegrationTestBase { lateinit var approvedPremisesApplicationEntityFactory: PersistedFactory lateinit var cas2ApplicationEntityFactory: PersistedFactory lateinit var cas2BailApplicationEntityFactory: PersistedFactory - lateinit var cas2BailAssementEntityFactory: PersistedFactory + lateinit var cas2BailAssessmentEntityFactory: PersistedFactory lateinit var cas2AssessmentEntityFactory: PersistedFactory lateinit var cas2StatusUpdateEntityFactory: PersistedFactory @@ -494,7 +494,7 @@ abstract class IntegrationTestBase { cas2ApplicationEntityFactory = PersistedFactory({ Cas2ApplicationEntityFactory() }, cas2ApplicationRepository) cas2BailApplicationEntityFactory = PersistedFactory({ Cas2BailApplicationEntityFactory() }, cas2BailApplicationRepository) - cas2BailAssementEntityFactory = PersistedFactory({ Cas2BailAssessmentEntityFactory() }, cas2BailAssessmentRepository) + cas2BailAssessmentEntityFactory = PersistedFactory({ Cas2BailAssessmentEntityFactory() }, cas2BailAssessmentRepository) cas2AssessmentEntityFactory = PersistedFactory({ Cas2AssessmentEntityFactory() }, cas2AssessmentRepository) cas2StatusUpdateEntityFactory = PersistedFactory({ Cas2StatusUpdateEntityFactory() }, cas2StatusUpdateRepository) cas2BailStatusUpdateEntityFactory = PersistedFactory({ Cas2BailStatusUpdateEntityFactory() }, cas2BailStatusUpdateRepository) diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationTest.kt index da0668291f..b79cf1126e 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationTest.kt @@ -16,10 +16,6 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ValueSource import org.springframework.beans.factory.annotation.Autowired -import org.springframework.data.jpa.repository.JpaRepository -import org.springframework.data.jpa.repository.Query -import org.springframework.data.repository.query.Param -import org.springframework.stereotype.Repository import org.springframework.test.web.reactive.server.returnResult import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.* import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.IntegrationTestBase @@ -31,7 +27,6 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.httpmocks.co import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.httpmocks.prisonAPIMockNotFoundInmateDetailsCall import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.* import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationSummaryEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailStatusUpdateEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.community.OffenderDetailSummary import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.randomDateAfter @@ -44,10 +39,6 @@ import kotlin.math.sign class Cas2BailApplicationTest : IntegrationTestBase() { - /* START HERE - TOBY DEBUG */ - @Autowired - private lateinit var entityManager: EntityManager - @SpykBean lateinit var realApplicationRepository: ApplicationRepository @@ -87,7 +78,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { inner class ControlsOnExternalUsers { @ParameterizedTest @ValueSource(strings = ["ROLE_CAS2_ASSESSOR", "ROLE_CAS2_MI"]) - fun `creating an application is forbidden to external users based on role`(role: String) { + fun `creating a cas2bail application is forbidden to external users based on role`(role: String) { val jwt = jwtAuthHelper.createClientCredentialsJwt( username = "username", authSource = "nomis", @@ -104,7 +95,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { @ParameterizedTest @ValueSource(strings = ["ROLE_CAS2_ASSESSOR", "ROLE_CAS2_MI"]) - fun `updating an application is forbidden to external users based on role`(role: String) { + fun `updating a cas2bail application is forbidden to external users based on role`(role: String) { val jwt = jwtAuthHelper.createClientCredentialsJwt( username = "username", authSource = "nomis", @@ -120,7 +111,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } @Test - fun `viewing list of applications is forbidden to external users based on role`() { + fun `viewing list of cas2bail applications is forbidden to external users based on role`() { val jwt = jwtAuthHelper.createClientCredentialsJwt( username = "username", authSource = "nomis", @@ -136,7 +127,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } @Test - fun `viewing an application is forbidden to external users based on role`() { + fun `viewing a cas2bail application is forbidden to external users based on role`() { val jwt = jwtAuthHelper.createClientCredentialsJwt( username = "username", authSource = "nomis", @@ -156,7 +147,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { @Nested inner class MissingJwt { @Test - fun `Get all applications without JWT returns 401`() { + fun `Get all cas2bail applications without JWT returns 401`() { webTestClient.get() .uri("/cas2bail/applications") .exchange() @@ -165,7 +156,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } @Test - fun `Get single application without JWT returns 401`() { + fun `Get single cas2bail application without JWT returns 401`() { webTestClient.get() .uri("/cas2bail/applications/9b785e59-b85c-4be0-b271-d9ac287684b6") .exchange() @@ -174,7 +165,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } @Test - fun `Create new application without JWT returns 401`() { + fun `Create new cas2bail application without JWT returns 401`() { webTestClient.post() .uri("/cas2bail/applications") .exchange() @@ -188,7 +179,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { @Test - fun `return unexpired applications when applications GET is requested`() { + fun `return unexpired cas2bail applications when applications GET is requested`() { val unexpiredSubset = setOf( Pair("More information requested", UUID.fromString("f5cd423b-08eb-4efb-96ff-5cc6bb073905")), Pair("Awaiting decision", UUID.fromString("ba4d8432-250b-4ab9-81ec-7eb4b16e5dd1")), @@ -292,7 +283,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } @Test - fun `Get all applications returns 200 with correct body`() { + fun `Get all cas2bail applications returns 200 with correct body`() { givenACas2PomUser { userEntity, jwt -> givenACas2PomUser { otherUser, _ -> givenAnOffender { offenderDetails, _ -> @@ -369,34 +360,6 @@ class Cas2BailApplicationTest : IntegrationTestBase() { .responseBody .blockFirst() - /* START HERE - TOBY DEBUG */ - val r1 = entityManager.createNativeQuery("SELECT * FROM cas_2_bail_applications").resultList - val r2 = entityManager.createNativeQuery("SELECT * FROM cas_2_bail_application_live_summary").resultList - val r3 = entityManager.createNativeQuery("SELECT * FROM cas_2_bail_status_updates").resultList - val r4 = entityManager.createNativeQuery("SELECT * FROM cas_2_bail_assessments").resultList - val r5 = entityManager.createNativeQuery("SELECT\n" + - " a.id,\n" + - " a.crn,\n" + - " a.noms_number,\n" + - " CAST(a.created_by_user_id AS TEXT),\n" + - " nu.name,\n" + - " a.created_at,\n" + - " a.submitted_at,\n" + - " a.hdc_eligibility_date,\n" + - " asu.label,\n" + - " CAST(asu.status_id AS TEXT),\n" + - " a.referring_prison_code,\n" + - " a.conditional_release_date,\n" + - " asu.created_at AS status_created_at,\n" + - " a.abandoned_at\n" + - "FROM cas_2_applications a\n" + - "LEFT JOIN (SELECT DISTINCT ON (application_id) su.application_id, su.label, su.status_id, su.created_at\n" + - " FROM cas_2_bail_status_updates su\n" + - " ORDER BY su.application_id, su.created_at DESC) as asu\n" + - " ON a.id = asu.application_id\n" + - "JOIN nomis_users nu ON nu.id = a.created_by_user_id").resultList - /* END HERE - TOBY DEBUG */ - val responseBody = objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) @@ -439,19 +402,19 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } @Test - fun `Get all applications with pagination returns 200 with correct body and header`() { + fun `Get all cas2bail applications with pagination returns 200 with correct body and header`() { givenACas2PomUser { userEntity, jwt -> givenACas2PomUser { otherUser, _ -> givenAnOffender { offenderDetails, _ -> - cas2ApplicationJsonSchemaRepository.deleteAll() + cas2BailApplicationJsonSchemaRepository.deleteAll() - val applicationSchema = cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + val applicationSchema = cas2BailApplicationJsonSchemaEntityFactory.produceAndPersist { withAddedAt(OffsetDateTime.now()) withId(UUID.randomUUID()) } repeat(12) { - cas2ApplicationEntityFactory.produceAndPersist { + cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(applicationSchema) withCreatedByUser(userEntity) withCrn(offenderDetails.otherIds.crn) @@ -461,7 +424,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } val rawResponseBodyPage1 = webTestClient.get() - .uri("/cas2/applications?page=1") + .uri("/cas2bail/applications?page=1") .header("Authorization", "Bearer $jwt") .header("X-Service-Name", ServiceName.cas2.value) .exchange() @@ -476,14 +439,14 @@ class Cas2BailApplicationTest : IntegrationTestBase() { .blockFirst() val responseBodyPage1 = - objectMapper.readValue(rawResponseBodyPage1, object : TypeReference>() {}) + objectMapper.readValue(rawResponseBodyPage1, object : TypeReference>() {}) Assertions.assertThat(responseBodyPage1).size().isEqualTo(10) Assertions.assertThat(isOrderedByCreatedAtDescending(responseBodyPage1)).isTrue() val rawResponseBodyPage2 = webTestClient.get() - .uri("/cas2/applications?page=2") + .uri("/cas2bail/applications?page=2") .header("Authorization", "Bearer $jwt") .header("X-Service-Name", ServiceName.cas2.value) .exchange() @@ -498,7 +461,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { .blockFirst() val responseBodyPage2 = - objectMapper.readValue(rawResponseBodyPage2, object : TypeReference>() {}) + objectMapper.readValue(rawResponseBodyPage2, object : TypeReference>() {}) Assertions.assertThat(responseBodyPage2).size().isEqualTo(2) } @@ -507,7 +470,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } @Test - fun `When a person is not found, returns 200 with placeholder text`() { + fun `When a person is not found in cas2bail, returns 200 with placeholder text`() { givenACas2PomUser { userEntity, jwt -> val crn = "X1234" @@ -515,7 +478,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { communityAPIMockNotFoundOffenderDetailsCall(crn) webTestClient.get() - .uri("/cas2/applications") + .uri("/cas2bail/applications") .header("Authorization", "Bearer $jwt") .exchange() .expectStatus() @@ -540,7 +503,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { * * If all dates are ascending the multiple will also be 0 ( = 1 x 0 x 0 etc.). */ - private fun isOrderedByCreatedAtDescending(responseBody: List): Boolean { + private fun isOrderedByCreatedAtDescending(responseBody: List): Boolean { var allDescending = 1 for (i in 1..(responseBody.size - 1)) { val isDescending = (responseBody[i - 1].createdAt.epochSecond - responseBody[i].createdAt.epochSecond).sign @@ -556,9 +519,9 @@ class Cas2BailApplicationTest : IntegrationTestBase() { givenACas2Assessor { assessor, _ -> givenACas2PomUser { userAPrisonA, jwt -> givenAnOffender { offenderDetails, _ -> - cas2ApplicationJsonSchemaRepository.deleteAll() + cas2BailApplicationJsonSchemaRepository.deleteAll() - val applicationSchema = cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + val applicationSchema = cas2BailApplicationJsonSchemaEntityFactory.produceAndPersist { withAddedAt(OffsetDateTime.now()) withId(UUID.randomUUID()) } @@ -567,7 +530,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { repeat(5) { userAPrisonAApplicationIds.add( - cas2ApplicationEntityFactory.produceAndPersist { + cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(applicationSchema) withCreatedByUser(userAPrisonA) withCrn(offenderDetails.otherIds.crn) @@ -587,7 +550,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { // submitted applications with conditional release dates in the future repeat(6) { userBPrisonAApplicationIds.add( - cas2ApplicationEntityFactory.produceAndPersist { + cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(applicationSchema) withCreatedByUser(userBPrisonA) withCrn(offenderDetails.otherIds.crn) @@ -603,7 +566,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { // submitted applications with conditional release dates today repeat(2) { userBPrisonAApplicationIds.add( - cas2ApplicationEntityFactory.produceAndPersist { + cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(applicationSchema) withCreatedByUser(userBPrisonA) withCrn(offenderDetails.otherIds.crn) @@ -617,7 +580,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } // submitted application with a conditional release date before today - val excludedApplicationId = cas2ApplicationEntityFactory.produceAndPersist { + val excludedApplicationId = cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(applicationSchema) withCreatedByUser(userBPrisonA) withCrn(offenderDetails.otherIds.crn) @@ -634,7 +597,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { withActiveCaseloadId("another prison") } - val otherPrisonApplication = cas2ApplicationEntityFactory.produceAndPersist { + val otherPrisonApplication = cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(applicationSchema) withCreatedByUser(userCPrisonB) withCrn(offenderDetails.otherIds.crn) @@ -644,7 +607,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } val rawResponseBody = webTestClient.get() - .uri("/cas2/applications?prisonCode=${userAPrisonA.activeCaseloadId}") + .uri("/cas2bail/applications?prisonCode=${userAPrisonA.activeCaseloadId}") .header("Authorization", "Bearer $jwt") .header("X-Service-Name", ServiceName.cas2.value) .exchange() @@ -655,7 +618,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { .blockFirst() val responseBody = - objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) + objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) Assertions.assertThat(responseBody).noneMatch { excludedApplicationId == it.id @@ -683,9 +646,9 @@ class Cas2BailApplicationTest : IntegrationTestBase() { givenACas2Assessor { assessor, _ -> givenACas2PomUser { userAPrisonA, jwt -> givenAnOffender { offenderDetails, _ -> - cas2ApplicationJsonSchemaRepository.deleteAll() + cas2BailApplicationJsonSchemaRepository.deleteAll() - val applicationSchema = cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + val applicationSchema = cas2BailApplicationJsonSchemaEntityFactory.produceAndPersist { withAddedAt(OffsetDateTime.now()) withId(UUID.randomUUID()) } @@ -694,7 +657,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { repeat(5) { userAPrisonAApplicationIds.add( - cas2ApplicationEntityFactory.produceAndPersist { + cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(applicationSchema) withCreatedByUser(userAPrisonA) withCrn(offenderDetails.otherIds.crn) @@ -713,7 +676,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { repeat(6) { userBPrisonAApplicationIds.add( - cas2ApplicationEntityFactory.produceAndPersist { + cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(applicationSchema) withCreatedByUser(userBPrisonA) withCrn(offenderDetails.otherIds.crn) @@ -729,7 +692,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { // submitted applications with conditional release dates today repeat(2) { userBPrisonAApplicationIds.add( - cas2ApplicationEntityFactory.produceAndPersist { + cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(applicationSchema) withCreatedByUser(userBPrisonA) withCrn(offenderDetails.otherIds.crn) @@ -743,7 +706,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } // submitted application with a conditional release date before today - val excludedApplicationId = cas2ApplicationEntityFactory.produceAndPersist { + val excludedApplicationId = cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(applicationSchema) withCreatedByUser(userBPrisonA) withCrn(offenderDetails.otherIds.crn) @@ -757,7 +720,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { addStatusUpdates(userBPrisonAApplicationIds.first(), assessor) val rawResponseBody = webTestClient.get() - .uri("/cas2/applications?isSubmitted=true&prisonCode=${userAPrisonA.activeCaseloadId}") + .uri("/cas2bail/applications?isSubmitted=true&prisonCode=${userAPrisonA.activeCaseloadId}") .header("Authorization", "Bearer $jwt") .header("X-Service-Name", ServiceName.cas2.value) .exchange() @@ -768,7 +731,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { .blockFirst() val responseBody = - objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) + objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) Assertions.assertThat(responseBody).noneMatch { excludedApplicationId == it.id @@ -788,9 +751,9 @@ class Cas2BailApplicationTest : IntegrationTestBase() { fun `Get all unsubmitted applications using prisonCode returns 200 with correct body`() { givenACas2PomUser { userAPrisonA, jwt -> givenAnOffender { offenderDetails, _ -> - cas2ApplicationJsonSchemaRepository.deleteAll() + cas2BailApplicationJsonSchemaRepository.deleteAll() - val applicationSchema = cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + val applicationSchema = cas2BailApplicationJsonSchemaEntityFactory.produceAndPersist { withAddedAt(OffsetDateTime.now()) withId(UUID.randomUUID()) } @@ -799,7 +762,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { repeat(5) { userAPrisonAApplicationIds.add( - cas2ApplicationEntityFactory.produceAndPersist { + cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(applicationSchema) withCreatedByUser(userAPrisonA) withCrn(offenderDetails.otherIds.crn) @@ -818,7 +781,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { repeat(6) { userBPrisonAApplicationIds.add( - cas2ApplicationEntityFactory.produceAndPersist { + cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(applicationSchema) withCreatedByUser(userBPrisonA) withCrn(offenderDetails.otherIds.crn) @@ -831,7 +794,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } val rawResponseBody = webTestClient.get() - .uri("/cas2/applications?isSubmitted=false&prisonCode=${userAPrisonA.activeCaseloadId}") + .uri("/cas2bail/applications?isSubmitted=false&prisonCode=${userAPrisonA.activeCaseloadId}") .header("Authorization", "Bearer $jwt") .header("X-Service-Name", ServiceName.cas2.value) .exchange() @@ -842,7 +805,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { .blockFirst() val responseBody = - objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) + objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) val returnedApplicationIds = responseBody.map { it.id }.toSet() @@ -855,7 +818,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { fun `Get applications using another prisonCode returns Forbidden 403`() { givenACas2PomUser { userAPrisonA, jwt -> val rawResponseBody = webTestClient.get() - .uri("/cas2/applications?prisonCode=${userAPrisonA.activeCaseloadId!!.reversed()}") + .uri("/cas2bail/applications?prisonCode=${userAPrisonA.activeCaseloadId!!.reversed()}") .header("Authorization", "Bearer $jwt") .header("X-Service-Name", ServiceName.cas2.value) .exchange() @@ -868,13 +831,13 @@ class Cas2BailApplicationTest : IntegrationTestBase() { @Nested inner class AsLicenceCaseAdminUser { @Test - fun `Get all submitted applications using prisonCode returns 200 with correct body`() { + fun `Get all submitted cas2bail applications using prisonCode returns 200 with correct body`() { givenACas2Assessor { assessor, _ -> givenACas2LicenceCaseAdminUser { caseAdminPrisonA, jwt -> givenAnOffender { offenderDetails, _ -> - cas2ApplicationJsonSchemaRepository.deleteAll() + cas2BailApplicationJsonSchemaRepository.deleteAll() - val applicationSchema = cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + val applicationSchema = cas2BailApplicationJsonSchemaEntityFactory.produceAndPersist { withAddedAt(OffsetDateTime.now()) withId(UUID.randomUUID()) } @@ -888,7 +851,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { // submitted applications with conditional release dates in the future repeat(6) { userBPrisonAApplicationIds.add( - cas2ApplicationEntityFactory.produceAndPersist { + cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(applicationSchema) withCreatedByUser(pomUserPrisonA) withCrn(offenderDetails.otherIds.crn) @@ -904,7 +867,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { // submitted applications with conditional release date of today repeat(2) { userBPrisonAApplicationIds.add( - cas2ApplicationEntityFactory.produceAndPersist { + cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(applicationSchema) withCreatedByUser(pomUserPrisonA) withCrn(offenderDetails.otherIds.crn) @@ -918,7 +881,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } // submitted application with a conditional release date before today - val excludedApplicationId = cas2ApplicationEntityFactory.produceAndPersist { + val excludedApplicationId = cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(applicationSchema) withCreatedByUser(pomUserPrisonA) withCrn(offenderDetails.otherIds.crn) @@ -933,7 +896,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { withActiveCaseloadId("other_prison") } - val otherPrisonApplication = cas2ApplicationEntityFactory.produceAndPersist { + val otherPrisonApplication = cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(applicationSchema) withCreatedByUser(pomUserPrisonB) withCrn(offenderDetails.otherIds.crn) @@ -944,7 +907,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } val rawResponseBody = webTestClient.get() - .uri("/cas2/applications?isSubmitted=true&prisonCode=${caseAdminPrisonA.activeCaseloadId}") + .uri("/cas2bail/applications?isSubmitted=true&prisonCode=${caseAdminPrisonA.activeCaseloadId}") .header("Authorization", "Bearer $jwt") .header("X-Service-Name", ServiceName.cas2.value) .exchange() @@ -955,7 +918,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { .blockFirst() val responseBody = - objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) + objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) Assertions.assertThat(responseBody).noneMatch { excludedApplicationId == it.id @@ -974,16 +937,16 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } private fun addStatusUpdates(applicationId: UUID, assessor: ExternalUserEntity) { - cas2StatusUpdateEntityFactory.produceAndPersist { + cas2BailStatusUpdateEntityFactory.produceAndPersist { withLabel("More information requested") - withApplication(cas2ApplicationRepository.findById(applicationId).get()) + withApplication(cas2BailApplicationRepository.findById(applicationId).get()) withAssessor(assessor) } // this is the one that should be returned as latestStatusUpdate - cas2StatusUpdateEntityFactory.produceAndPersist { + cas2BailStatusUpdateEntityFactory.produceAndPersist { withStatusId(UUID.fromString("c74c3e54-52d8-4aa2-86f6-05190985efee")) withLabel("Awaiting decision") - withApplication(cas2ApplicationRepository.findById(applicationId).get()) + withApplication(cas2BailApplicationRepository.findById(applicationId).get()) withAssessor(assessor) } } @@ -1002,9 +965,9 @@ class Cas2BailApplicationTest : IntegrationTestBase() { givenACas2PomUser { userEntity, jwt -> givenACas2PomUser { otherUser, _ -> givenAnOffender { offenderDetails, _ -> - cas2ApplicationJsonSchemaRepository.deleteAll() + cas2BailApplicationJsonSchemaRepository.deleteAll() - val applicationSchema = cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + val applicationSchema = cas2BailApplicationJsonSchemaEntityFactory.produceAndPersist { withAddedAt(OffsetDateTime.now()) withId(UUID.randomUUID()) } @@ -1013,7 +976,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { // with most recent first and conditional release dates in the future repeat(3) { submittedIds.add( - cas2ApplicationEntityFactory.produceAndPersist { + cas2BailApplicationEntityFactory.produceAndPersist { withCreatedAt(OffsetDateTime.now().minusDays(it.toLong())) withApplicationSchema(applicationSchema) withCreatedByUser(userEntity) @@ -1029,7 +992,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { // with most recent first and conditional release dates of today repeat(2) { submittedIds.add( - cas2ApplicationEntityFactory.produceAndPersist { + cas2BailApplicationEntityFactory.produceAndPersist { withCreatedAt(OffsetDateTime.now().minusDays(it.toLong() + 3)) withApplicationSchema(applicationSchema) withCreatedByUser(userEntity) @@ -1042,7 +1005,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } // submitted application with a conditional release date before today - excludedApplicationId = cas2ApplicationEntityFactory.produceAndPersist { + excludedApplicationId = cas2BailApplicationEntityFactory.produceAndPersist { withCreatedAt(OffsetDateTime.now().minusDays(14)) withApplicationSchema(applicationSchema) withCreatedByUser(userEntity) @@ -1057,7 +1020,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { // create 4 x un-submitted in-progress applications for this user repeat(4) { unSubmittedIds.add( - cas2ApplicationEntityFactory.produceAndPersist { + cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(applicationSchema) withCreatedByUser(userEntity) withCrn(offenderDetails.otherIds.crn) @@ -1067,7 +1030,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } // create a submitted application by another user which should not be in results - cas2ApplicationEntityFactory.produceAndPersist { + cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(applicationSchema) withCreatedByUser(otherUser) withCrn(offenderDetails.otherIds.crn) @@ -1076,7 +1039,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } // create an unsubmitted application by another user which should not be in results - cas2ApplicationEntityFactory.produceAndPersist { + cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(applicationSchema) withCreatedByUser(otherUser) withCrn(offenderDetails.otherIds.crn) @@ -1091,9 +1054,9 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } @Test - fun `returns all applications for user when isSubmitted is null`() { + fun `returns all cas2bail applications for user when isSubmitted is null`() { val rawResponseBody = webTestClient.get() - .uri("/cas2/applications") + .uri("/cas2bail/applications") .header("Authorization", "Bearer $jwtForUser") .header("X-Service-Name", ServiceName.cas2.value) .exchange() @@ -1104,7 +1067,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { .blockFirst() val responseBody = - objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) + objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) Assertions.assertThat(responseBody).noneMatch { excludedApplicationId == it.id @@ -1115,9 +1078,9 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } @Test - fun `returns submitted applications for user when isSubmitted is true`() { + fun `returns submitted cas2bail applications for user when isSubmitted is true`() { val rawResponseBody = webTestClient.get() - .uri("/cas2/applications?isSubmitted=true") + .uri("/cas2bail/applications?isSubmitted=true") .header("Authorization", "Bearer $jwtForUser") .header("X-Service-Name", ServiceName.cas2.value) .exchange() @@ -1128,7 +1091,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { .blockFirst() val responseBody = - objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) + objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) Assertions.assertThat(responseBody).noneMatch { excludedApplicationId == it.id @@ -1141,9 +1104,9 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } @Test - fun `returns submitted applications for user when isSubmitted is true and page specified`() { + fun `returns submitted cas2bail applications for user when isSubmitted is true and page specified`() { val rawResponseBody = webTestClient.get() - .uri("/cas2/applications?isSubmitted=true&page=1") + .uri("/cas2bail/applications?isSubmitted=true&page=1") .header("Authorization", "Bearer $jwtForUser") .header("X-Service-Name", ServiceName.cas2.value) .exchange() @@ -1154,7 +1117,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { .blockFirst() val responseBody = - objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) + objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) val uuids = responseBody.map { it.id }.toSet() Assertions.assertThat(uuids).isEqualTo(submittedIds) @@ -1163,9 +1126,9 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } @Test - fun `returns unsubmitted applications for user when isSubmitted is false`() { + fun `returns unsubmitted cas2bail applications for user when isSubmitted is false`() { val rawResponseBody = webTestClient.get() - .uri("/cas2/applications?isSubmitted=false") + .uri("/cas2bail/applications?isSubmitted=false") .header("Authorization", "Bearer $jwtForUser") .header("X-Service-Name", ServiceName.cas2.value) .exchange() @@ -1176,7 +1139,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { .blockFirst() val responseBody = - objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) + objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) val uuids = responseBody.map { it.id }.toSet() Assertions.assertThat(uuids).isEqualTo(unSubmittedIds) @@ -1190,12 +1153,12 @@ class Cas2BailApplicationTest : IntegrationTestBase() { inner class WhenCreatedBySameUser { // When the application requested was created by the logged-in user @Test - fun `Get single in progress application returns 200 with correct body`() { + fun `Get single in progress cas2bail application returns 200 with correct body`() { givenACas2PomUser { userEntity, jwt -> givenAnOffender { offenderDetails, inmateDetails -> - cas2ApplicationJsonSchemaRepository.deleteAll() + cas2BailApplicationJsonSchemaRepository.deleteAll() - val newestJsonSchema = cas2ApplicationJsonSchemaEntityFactory + val newestJsonSchema = cas2BailApplicationJsonSchemaEntityFactory .produceAndPersist { withAddedAt(OffsetDateTime.parse("2022-09-21T12:45:00+01:00")) withSchema( @@ -1203,7 +1166,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { ) } - val applicationEntity = cas2ApplicationEntityFactory.produceAndPersist { + val applicationEntity = cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(newestJsonSchema) withCrn(offenderDetails.otherIds.crn) withCreatedByUser(userEntity) @@ -1213,7 +1176,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } val rawResponseBody = webTestClient.get() - .uri("/cas2/applications/${applicationEntity.id}") + .uri("/cas2bail/applications/${applicationEntity.id}") .header("Authorization", "Bearer $jwt") .exchange() .expectStatus() @@ -1241,7 +1204,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } @Test - fun `Get single application returns successfully when the offender cannot be fetched from the prisons API`() { + fun `Get single cas2bail application returns successfully when the offender cannot be fetched from the prisons API`() { givenACas2PomUser { userEntity, jwt -> val crn = "X1234" @@ -1257,7 +1220,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { loadPreemptiveCacheForInmateDetails(offenderDetails.otherIds.nomsNumber!!) val rawResponseBody = webTestClient.get() - .uri("/cas2/applications/${application.id}") + .uri("/cas2bail/applications/${application.id}") .header("Authorization", "Bearer $jwt") .exchange() .expectStatus() @@ -1287,12 +1250,12 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } @Test - fun `Get single submitted application returns 200 with timeline events`() { + fun `Get single submitted cas2bail application returns 200 with timeline events`() { givenACas2PomUser { userEntity, jwt -> givenAnOffender { offenderDetails, inmateDetails -> - cas2ApplicationJsonSchemaRepository.deleteAll() + cas2BailApplicationJsonSchemaRepository.deleteAll() - val newestJsonSchema = cas2ApplicationJsonSchemaEntityFactory + val newestJsonSchema = cas2BailApplicationJsonSchemaEntityFactory .produceAndPersist { withAddedAt(OffsetDateTime.parse("2022-09-21T12:45:00+01:00")) withSchema( @@ -1300,14 +1263,14 @@ class Cas2BailApplicationTest : IntegrationTestBase() { ) } - val applicationEntity = cas2ApplicationEntityFactory.produceAndPersist { + val applicationEntity = cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(newestJsonSchema) withCrn(offenderDetails.otherIds.crn) withCreatedByUser(userEntity) withSubmittedAt(OffsetDateTime.now().minusDays(1)) } - cas2AssessmentEntityFactory.produceAndPersist { + cas2BailAssessmentEntityFactory.produceAndPersist { withApplication(applicationEntity) } @@ -1341,16 +1304,16 @@ class Cas2BailApplicationTest : IntegrationTestBase() { @Nested inner class WhenDifferentPrison { @Test - fun `Get single submitted application is forbidden`() { + fun `Get single submitted cas2bail application is forbidden`() { givenACas2PomUser { userEntity, jwt -> givenAnOffender { offenderDetails, inmateDetails -> - cas2ApplicationJsonSchemaRepository.deleteAll() + cas2BailApplicationJsonSchemaRepository.deleteAll() val otherUser = nomisUserEntityFactory.produceAndPersist { withActiveCaseloadId("other_caseload") } - val newestJsonSchema = cas2ApplicationJsonSchemaEntityFactory + val newestJsonSchema = cas2BailApplicationJsonSchemaEntityFactory .produceAndPersist { withAddedAt(OffsetDateTime.parse("2022-09-21T12:45:00+01:00")) withSchema( @@ -1358,7 +1321,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { ) } - val applicationEntity = cas2ApplicationEntityFactory.produceAndPersist { + val applicationEntity = cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(newestJsonSchema) withCrn(offenderDetails.otherIds.crn) withSubmittedAt(OffsetDateTime.now()) @@ -1369,7 +1332,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } webTestClient.get() - .uri("/cas2/applications/${applicationEntity.id}") + .uri("/cas2bail/applications/${applicationEntity.id}") .header("Authorization", "Bearer $jwt") .exchange() .expectStatus() @@ -1382,12 +1345,12 @@ class Cas2BailApplicationTest : IntegrationTestBase() { @Nested inner class WhenSamePrison { @Test - fun `Get single submitted application returns 200 with timeline events`() { + fun `Get single submitted cas2bail application returns 200 with timeline events`() { givenACas2PomUser { userEntity, jwt -> givenAnOffender { offenderDetails, inmateDetails -> - cas2ApplicationJsonSchemaRepository.deleteAll() + cas2BailApplicationJsonSchemaRepository.deleteAll() - val newestJsonSchema = cas2ApplicationJsonSchemaEntityFactory + val newestJsonSchema = cas2BailApplicationJsonSchemaEntityFactory .produceAndPersist { withAddedAt(OffsetDateTime.parse("2022-09-21T12:45:00+01:00")) withSchema( @@ -1399,7 +1362,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { withActiveCaseloadId(userEntity.activeCaseloadId!!) } - val applicationEntity = cas2ApplicationEntityFactory.produceAndPersist { + val applicationEntity = cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(newestJsonSchema) withCrn(offenderDetails.otherIds.crn) withCreatedByUser(otherUser) @@ -1407,12 +1370,12 @@ class Cas2BailApplicationTest : IntegrationTestBase() { withReferringPrisonCode(userEntity.activeCaseloadId!!) } - cas2AssessmentEntityFactory.produceAndPersist { + cas2BailAssessmentEntityFactory.produceAndPersist { withApplication(applicationEntity) } val rawResponseBody = webTestClient.get() - .uri("/cas2/applications/${applicationEntity.id}") + .uri("/cas2bail/applications/${applicationEntity.id}") .header("Authorization", "Bearer $jwt") .exchange() .expectStatus() @@ -1435,12 +1398,12 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } @Test - fun `Get single unsubmitted application returns 403`() { + fun `Get single unsubmitted cas2bail application returns 403`() { givenACas2PomUser { userEntity, jwt -> givenAnOffender { offenderDetails, inmateDetails -> - cas2ApplicationJsonSchemaRepository.deleteAll() + cas2BailApplicationJsonSchemaRepository.deleteAll() - val newestJsonSchema = cas2ApplicationJsonSchemaEntityFactory + val newestJsonSchema = cas2BailApplicationJsonSchemaEntityFactory .produceAndPersist { withAddedAt(OffsetDateTime.parse("2022-09-21T12:45:00+01:00")) withSchema( @@ -1452,7 +1415,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { withActiveCaseloadId(userEntity.activeCaseloadId!!) } - val applicationEntity = cas2ApplicationEntityFactory.produceAndPersist { + val applicationEntity = cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(newestJsonSchema) withCrn(offenderDetails.otherIds.crn) withCreatedByUser(otherUser) @@ -1460,7 +1423,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } webTestClient.get() - .uri("/cas2/applications/${applicationEntity.id}") + .uri("/cas2bail/applications/${applicationEntity.id}") .header("Authorization", "Bearer $jwt") .exchange() .expectStatus() @@ -1478,17 +1441,17 @@ class Cas2BailApplicationTest : IntegrationTestBase() { @Nested inner class PomUsers { @Test - fun `Create new application for CAS-2 returns 201 with correct body and Location header`() { + fun `Create new application for cas2bail returns 201 with correct body and Location header`() { givenACas2PomUser { userEntity, jwt -> givenAnOffender { offenderDetails, _ -> val applicationSchema = - cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + cas2BailApplicationJsonSchemaEntityFactory.produceAndPersist { withAddedAt(OffsetDateTime.now()) withId(UUID.randomUUID()) } val result = webTestClient.post() - .uri("/cas2/applications") + .uri("/cas2bail/applications") .header("Authorization", "Bearer $jwt") .header("X-Service-Name", ServiceName.cas2.value) .bodyValue( @@ -1502,7 +1465,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { .returnResult(Cas2Application::class.java) Assertions.assertThat(result.responseHeaders["Location"]).anyMatch { - it.matches(Regex("/cas2/applications/.+")) + it.matches(Regex("/cas2bail/applications/.+")) } Assertions.assertThat(result.responseBody.blockFirst()).matches { @@ -1514,19 +1477,19 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } @Test - fun `Create new application returns 404 when a person cannot be found`() { + fun `Create new cas2bail application returns 404 when a person cannot be found`() { givenACas2PomUser { userEntity, jwt -> val crn = "X1234" communityAPIMockNotFoundOffenderDetailsCall(crn) - cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + cas2BailApplicationJsonSchemaEntityFactory.produceAndPersist { withAddedAt(OffsetDateTime.now()) withId(UUID.randomUUID()) } webTestClient.post() - .uri("/cas2/applications") + .uri("/cas2bail/applications") .header("Authorization", "Bearer $jwt") .bodyValue( NewApplication( @@ -1545,17 +1508,17 @@ class Cas2BailApplicationTest : IntegrationTestBase() { @Nested inner class LicenceCaseAdminUsers { @Test - fun `Create new application for CAS-2 returns 201 with correct body and Location header`() { + fun `Create new cas2bail application for CAS-2 returns 201 with correct body and Location header`() { givenACas2LicenceCaseAdminUser { _, jwt -> givenAnOffender { offenderDetails, _ -> val applicationSchema = - cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + cas2BailApplicationJsonSchemaEntityFactory.produceAndPersist { withAddedAt(OffsetDateTime.now()) withId(UUID.randomUUID()) } val result = webTestClient.post() - .uri("/cas2/applications") + .uri("/cas2bail/applications") .header("Authorization", "Bearer $jwt") .header("X-Service-Name", ServiceName.cas2.value) .bodyValue( @@ -1581,19 +1544,19 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } @Test - fun `Create new application returns 404 when a person cannot be found`() { + fun `Create new cas2bail application returns 404 when a person cannot be found`() { givenACas2LicenceCaseAdminUser { _, jwt -> val crn = "X1234" communityAPIMockNotFoundOffenderDetailsCall(crn) - cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + cas2BailApplicationJsonSchemaEntityFactory.produceAndPersist { withAddedAt(OffsetDateTime.now()) withId(UUID.randomUUID()) } webTestClient.post() - .uri("/cas2/applications") + .uri("/cas2bail/applications") .header("Authorization", "Bearer $jwt") .bodyValue( NewApplication( @@ -1616,13 +1579,13 @@ class Cas2BailApplicationTest : IntegrationTestBase() { @Nested inner class PomUsers { @Test - fun `Update existing CAS2 application returns 200 with correct body`() { + fun `Update existing cas2bail application returns 200 with correct body`() { givenACas2PomUser { submittingUser, jwt -> givenAnOffender { offenderDetails, _ -> val applicationId = UUID.fromString("22ceda56-98b2-411d-91cc-ace0ab8be872") val applicationSchema = - cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + cas2BailApplicationJsonSchemaEntityFactory.produceAndPersist { withAddedAt(OffsetDateTime.now()) withId(UUID.randomUUID()) withSchema( @@ -1630,15 +1593,15 @@ class Cas2BailApplicationTest : IntegrationTestBase() { ) } - cas2ApplicationEntityFactory.produceAndPersist { + cas2BailApplicationEntityFactory.produceAndPersist { withCrn(offenderDetails.otherIds.crn) withId(applicationId) withApplicationSchema(applicationSchema) withCreatedByUser(submittingUser) } - +//TODO what is a UpdateCas2Application? val resultBody = webTestClient.put() - .uri("/cas2/applications/$applicationId") + .uri("/cas2bail/applications/$applicationId") .header("Authorization", "Bearer $jwt") .bodyValue( UpdateCas2Application( @@ -1664,13 +1627,13 @@ class Cas2BailApplicationTest : IntegrationTestBase() { @Nested inner class LicenceCaseAdminUsers { @Test - fun `Update existing CAS2 application returns 200 with correct body`() { + fun `Update existing cas2bail application returns 200 with correct body`() { givenACas2LicenceCaseAdminUser { submittingUser, jwt -> givenAnOffender { offenderDetails, _ -> val applicationId = UUID.fromString("22ceda56-98b2-411d-91cc-ace0ab8be872") val applicationSchema = - cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + cas2BailApplicationJsonSchemaEntityFactory.produceAndPersist { withAddedAt(OffsetDateTime.now()) withId(UUID.randomUUID()) withSchema( @@ -1678,7 +1641,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { ) } - cas2ApplicationEntityFactory.produceAndPersist { + cas2BailApplicationEntityFactory.produceAndPersist { withCrn(offenderDetails.otherIds.crn) withId(applicationId) withApplicationSchema(applicationSchema) @@ -1686,7 +1649,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } val resultBody = webTestClient.put() - .uri("/cas2/applications/$applicationId") + .uri("/cas2bail/applications/$applicationId") .header("Authorization", "Bearer $jwt") .bodyValue( UpdateCas2Application( @@ -1720,15 +1683,15 @@ class Cas2BailApplicationTest : IntegrationTestBase() { private fun produceAndPersistBasicApplication( crn: String, userEntity: NomisUserEntity, - ): Cas2ApplicationEntity { - val jsonSchema = cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + ): Cas2BailApplicationEntity { + val jsonSchema = cas2BailApplicationJsonSchemaEntityFactory.produceAndPersist { withAddedAt(OffsetDateTime.parse("2022-09-21T12:45:00+01:00")) withSchema( schema, ) } - val application = cas2ApplicationEntityFactory.produceAndPersist { + val application = cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(jsonSchema) withCrn(crn) withCreatedByUser(userEntity) From adacbf88ab21329aa848260c283d060ed18a21ce Mon Sep 17 00:00:00 2001 From: garethCAS2 Date: Wed, 4 Dec 2024 11:21:30 +0000 Subject: [PATCH 18/37] feat: Cas2BailApplicationAbandonTest added --- .../Cas2BailApplicationAbandonTest.kt | 159 ++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationAbandonTest.kt diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationAbandonTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationAbandonTest.kt new file mode 100644 index 0000000000..14bda597a1 --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationAbandonTest.kt @@ -0,0 +1,159 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.cas2bail + + +import com.ninjasquad.springmockk.SpykBean +import io.mockk.clearMocks +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.IntegrationTestBase +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenACas2LicenceCaseAdminUser +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenACas2PomUser +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenAnOffender +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.NomisUserEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationRepository +import java.time.OffsetDateTime + +class Cas2BailApplicationAbandonTest : IntegrationTestBase() { + @SpykBean + lateinit var realApplicationRepository: Cas2BailApplicationRepository + + val schema = """ + { + "${"\$schema"}": "https://json-schema.org/draft/2020-12/schema", + "${"\$id"}": "https://example.com/product.schema.json", + "title": "Thing", + "description": "A thing", + "type": "object", + "properties": { + "thingId": { + "description": "The unique identifier for a thing", + "type": "integer" + } + }, + "required": [ "thingId" ] + } + """ + + val data = """ + { + "thingId": 123 + } + """ + + @AfterEach + fun afterEach() { + // SpringMockK does not correctly clear mocks for @SpyKBeans that are also a @Repository, causing mocked behaviour + // in one test to show up in another (see https://github.com/Ninja-Squad/springmockk/issues/85) + // Manually clearing after each test seems to fix this. + clearMocks(realApplicationRepository) + } + + @Nested + inner class ControlsOnExternalUsers { + + @ParameterizedTest + @ValueSource(strings = ["ROLE_CAS2_ASSESSOR", "ROLE_CAS2_MI"]) + fun `abandoning a cas2bail application is forbidden to external users based on role`(role: String) { + val jwt = jwtAuthHelper.createClientCredentialsJwt( + username = "username", + authSource = "nomis", + roles = listOf(role), + ) + + webTestClient.put() + .uri("/cas2bail/applications/66911cf0-75b1-4361-84bd-501b176fd4fd/abandon") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isForbidden + } + } + + @Nested + inner class MissingJwt { + @Test + fun `Abandon a cas2bail application without JWT returns 401`() { + webTestClient.put() + .uri("/cas2bail/applications/9b785e59-b85c-4be0-b271-d9ac287684b6/abandon") + .exchange() + .expectStatus() + .isUnauthorized + } + } + + @Nested + inner class PutToAbandon { + + @Nested + inner class PomUsers { + @Test + fun `Abandon existing cas2bail application returns 200 with correct body`() { + givenACas2PomUser { submittingUser, jwt -> + givenAnOffender { offenderDetails, _ -> + val application = produceAndPersistBasicApplication(offenderDetails.otherIds.crn, submittingUser) + + webTestClient.put() + .uri("/cas2bail/applications/${application.id}/abandon") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isOk + + Assertions.assertNotNull(realApplicationRepository.findById(application.id).get().abandonedAt) + } + } + } + } + + @Nested + inner class LicenceCaseAdminUsers { + @Test + fun `Abandon existing cas2bail application returns 200 with correct body`() { + givenACas2LicenceCaseAdminUser { submittingUser, jwt -> + givenAnOffender { offenderDetails, _ -> + val application = produceAndPersistBasicApplication(offenderDetails.otherIds.crn, submittingUser) + + webTestClient.put() + .uri("/cas2bail/applications/${application.id}/abandon") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isOk + + Assertions.assertNotNull(realApplicationRepository.findById(application.id).get().abandonedAt) + } + } + } + } + } + + private fun produceAndPersistBasicApplication( + crn: String, + userEntity: NomisUserEntity, + ): Cas2BailApplicationEntity { + val jsonSchema = cas2BailApplicationJsonSchemaEntityFactory.produceAndPersist { + withAddedAt(OffsetDateTime.parse("2022-09-21T12:45:00+01:00")) + withSchema( + schema, + ) + } + + val application = cas2BailApplicationEntityFactory.produceAndPersist { + withApplicationSchema(jsonSchema) + withCrn(crn) + withCreatedByUser(userEntity) + withData( + data, + ) + } + + return application + } +} From 790d54140f2a1d0f23c7f5c6b93adc85a7db010f Mon Sep 17 00:00:00 2001 From: garethCAS2 Date: Wed, 4 Dec 2024 13:31:16 +0000 Subject: [PATCH 19/37] feat: Cas2BailAssessmentTest --- .../cas2bail/Cas2BailAssessmentTest.kt | 283 ++++++++++++++++++ 1 file changed, 283 insertions(+) create mode 100644 src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailAssessmentTest.kt diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailAssessmentTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailAssessmentTest.kt new file mode 100644 index 0000000000..5c312e3812 --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailAssessmentTest.kt @@ -0,0 +1,283 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.cas2bail + + +import com.fasterxml.jackson.core.type.TypeReference +import com.ninjasquad.springmockk.SpykBean +import io.mockk.clearMocks +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.springframework.test.web.reactive.server.returnResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas2Assessment +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.ServiceName +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.UpdateCas2Assessment +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.IntegrationTestBase +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenACas2Admin +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenACas2Assessor +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenACas2PomUser +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2AssessmentRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.NomisUserEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailAssessmentRepository +import java.time.OffsetDateTime +import java.util.UUID + +class Cas2BailAssessmentTest : IntegrationTestBase() { + + @SpykBean + lateinit var realAssessmentRepository: Cas2BailAssessmentRepository + + @AfterEach + fun afterEach() { + // SpringMockK does not correctly clear mocks for @SpyKBeans that are also a @Repository, causing mocked behaviour + // in one test to show up in another (see https://github.com/Ninja-Squad/springmockk/issues/85) + // Manually clearing after each test seems to fix this. + clearMocks(realAssessmentRepository) + } + + @Nested + inner class PutToUpdate { + @Nested + inner class MissingJwt { + @Test + fun `updating an assessment without JWT returns 401`() { + webTestClient.put() + .uri("/cas2bail/assessments/de6512fc-a225-4109-bdcd-86c6307a5237") + .exchange() + .expectStatus() + .isUnauthorized + } + } + + @Nested + inner class ControlsOnExternalUsers { + @Test + fun `updating an assessment is forbidden to external users who are not Assessors`() { + val jwt = jwtAuthHelper.createClientCredentialsJwt( + username = "username", + authSource = "auth", + roles = listOf("ROLE_CAS2_ADMIN"), + ) + + webTestClient.put() + .uri("/cas2bail/assessments/de6512fc-a225-4109-bdcd-86c6307a5237") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isForbidden + } + } + + @Nested + inner class ControlsOnInternalUsers { + @Test + fun `updating an assessment is forbidden to nomis users`() { + val jwt = jwtAuthHelper.createClientCredentialsJwt( + username = "username", + authSource = "nomis", + roles = listOf("ROLE_POM"), + ) + + webTestClient.put() + .uri("/cas2bail/assessments/de6512fc-a225-4109-bdcd-86c6307a5237") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isForbidden + } + } + + @Test + fun `assessors create note returns 201`() { + val applicationId = UUID.fromString("22ceda56-98b2-411d-91cc-ace0ab8be872") + + givenACas2PomUser { referrer, _ -> + givenACas2Assessor { assessor, jwt -> + val submittedApplication = createSubmittedApplication(applicationId, referrer) + + // with an assessment + val assessment = cas2BailAssessmentEntityFactory.produceAndPersist { + withApplication(submittedApplication) + withNacroReferralId("someID") + withAssessorName("some name") + } + + val updatedNacroReferralId = "123N" + val updatedAssessorName = "Anne Assessor" + + val rawResponseBody = webTestClient.put() + .uri("/cas2bail/assessments/${assessment.id}") + .header("Authorization", "Bearer $jwt") + .header("X-Service-Name", ServiceName.cas2.value) + .bodyValue( + UpdateCas2Assessment( + nacroReferralId = updatedNacroReferralId, + assessorName = updatedAssessorName, + ), + ) + .exchange() + .expectStatus() + .isOk + .returnResult() + .responseBody + .blockFirst() + + val responseBody = + objectMapper.readValue(rawResponseBody, object : TypeReference() {}) + + Assertions.assertThat(responseBody.nacroReferralId).isEqualTo(updatedNacroReferralId) + Assertions.assertThat(responseBody.assessorName).isEqualTo(updatedAssessorName) + } + } + } + + private fun createSubmittedApplication(applicationId: UUID, referrer: NomisUserEntity): Cas2BailApplicationEntity { + val applicationSchema = + cas2BailApplicationJsonSchemaEntityFactory.produceAndPersist() + + return cas2BailApplicationEntityFactory.produceAndPersist { + withId(applicationId) + withCreatedByUser(referrer) + withApplicationSchema(applicationSchema) + withSubmittedAt(OffsetDateTime.now()) + } + } + } + + @Nested + inner class GetToShow { + @Nested + inner class MissingJwt { + @Test + fun `getting an assessment without JWT returns 401`() { + webTestClient.get() + .uri("/cas2bail/assessments/de6512fc-a225-4109-bdcd-86c6307a5237") + .exchange() + .expectStatus() + .isUnauthorized + } + } + + @Nested + inner class ControlsOnExternalUsers { + @Test + fun `getting an assessment is forbidden to external users who are not Assessors or Admins`() { + val jwt = jwtAuthHelper.createClientCredentialsJwt( + username = "username", + authSource = "auth", + roles = listOf("ROLE_CAS2_EXAMPLE"), + ) + + webTestClient.get() + .uri("/cas2bail/assessments/de6512fc-a225-4109-bdcd-86c6307a5237") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isForbidden + } + } + + @Nested + inner class ControlsOnInternalUsers { + @Test + fun `getting an assessment is forbidden to nomis users`() { + val jwt = jwtAuthHelper.createClientCredentialsJwt( + username = "username", + authSource = "nomis", + roles = listOf("ROLE_POM"), + ) + + webTestClient.get() + .uri("/cas2bail/assessments/de6512fc-a225-4109-bdcd-86c6307a5237") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isForbidden + } + } + + @Test + fun `assessors update assessment returns 200`() { + val applicationId = UUID.fromString("22ceda56-98b2-411d-91cc-ace0ab8be872") + + givenACas2PomUser { referrer, _ -> + givenACas2Assessor { assessor, jwt -> + val submittedApplication = createSubmittedApplication(applicationId, referrer) + + // with an assessment + val assessment = cas2BailAssessmentEntityFactory.produceAndPersist { + withApplication(submittedApplication) + withNacroReferralId("someID") + withAssessorName("some name") + } + + val rawResponseBody = webTestClient.get() + .uri("/cas2bail/assessments/${assessment.id}") + .header("Authorization", "Bearer $jwt") + .header("X-Service-Name", ServiceName.cas2.value) + .exchange() + .expectStatus() + .isOk + .returnResult() + .responseBody + .blockFirst() + + val responseBody = + objectMapper.readValue(rawResponseBody, object : TypeReference() {}) + + Assertions.assertThat(responseBody.nacroReferralId).isEqualTo(assessment.nacroReferralId) + Assertions.assertThat(responseBody.assessorName).isEqualTo(assessment.assessorName) + } + } + } + + @Test + fun `admins get assessment returns 200`() { + val applicationId = UUID.fromString("22ceda56-98b2-411d-91cc-ace0ab8be872") + + givenACas2PomUser { referrer, _ -> + givenACas2Admin { admin, jwt -> + val submittedApplication = createSubmittedApplication(applicationId, referrer) + + // with an assessment + val assessment = cas2BailAssessmentEntityFactory.produceAndPersist { + withApplication(submittedApplication) + withNacroReferralId("someID") + withAssessorName("some name") + } + + val rawResponseBody = webTestClient.get() + .uri("/cas2bail/assessments/${assessment.id}") + .header("Authorization", "Bearer $jwt") + .header("X-Service-Name", ServiceName.cas2.value) + .exchange() + .expectStatus() + .isOk + .returnResult() + .responseBody + .blockFirst() + + val responseBody = + objectMapper.readValue(rawResponseBody, object : TypeReference() {}) + + Assertions.assertThat(responseBody.nacroReferralId).isEqualTo(assessment.nacroReferralId) + Assertions.assertThat(responseBody.assessorName).isEqualTo(assessment.assessorName) + } + } + } + + private fun createSubmittedApplication(applicationId: UUID, referrer: NomisUserEntity): Cas2BailApplicationEntity { + val applicationSchema = + cas2BailApplicationJsonSchemaEntityFactory.produceAndPersist() + + return cas2BailApplicationEntityFactory.produceAndPersist { + withId(applicationId) + withCreatedByUser(referrer) + withApplicationSchema(applicationSchema) + withSubmittedAt(OffsetDateTime.now()) + } + } + } +} \ No newline at end of file From c11e56149e41410a0dcee7cef446f466db5dc18e Mon Sep 17 00:00:00 2001 From: garethCAS2 Date: Wed, 4 Dec 2024 13:37:29 +0000 Subject: [PATCH 20/37] feat: Cas2BailAssessmentTest renamed the tests --- .../cas2bail/Cas2BailAssessmentTest.kt | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailAssessmentTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailAssessmentTest.kt index 5c312e3812..7385aaf28e 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailAssessmentTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailAssessmentTest.kt @@ -42,7 +42,7 @@ class Cas2BailAssessmentTest : IntegrationTestBase() { @Nested inner class MissingJwt { @Test - fun `updating an assessment without JWT returns 401`() { + fun `updating a cas2bail assessment without JWT returns 401`() { webTestClient.put() .uri("/cas2bail/assessments/de6512fc-a225-4109-bdcd-86c6307a5237") .exchange() @@ -54,7 +54,7 @@ class Cas2BailAssessmentTest : IntegrationTestBase() { @Nested inner class ControlsOnExternalUsers { @Test - fun `updating an assessment is forbidden to external users who are not Assessors`() { + fun `updating a cas2bail assessment is forbidden to external users who are not Assessors`() { val jwt = jwtAuthHelper.createClientCredentialsJwt( username = "username", authSource = "auth", @@ -73,7 +73,7 @@ class Cas2BailAssessmentTest : IntegrationTestBase() { @Nested inner class ControlsOnInternalUsers { @Test - fun `updating an assessment is forbidden to nomis users`() { + fun `updating a cas2bail assessment is forbidden to nomis users`() { val jwt = jwtAuthHelper.createClientCredentialsJwt( username = "username", authSource = "nomis", @@ -90,7 +90,7 @@ class Cas2BailAssessmentTest : IntegrationTestBase() { } @Test - fun `assessors create note returns 201`() { + fun `assessors create cas2bail note returns 201`() { val applicationId = UUID.fromString("22ceda56-98b2-411d-91cc-ace0ab8be872") givenACas2PomUser { referrer, _ -> @@ -151,7 +151,7 @@ class Cas2BailAssessmentTest : IntegrationTestBase() { @Nested inner class MissingJwt { @Test - fun `getting an assessment without JWT returns 401`() { + fun `getting a cas2bail assessment without JWT returns 401`() { webTestClient.get() .uri("/cas2bail/assessments/de6512fc-a225-4109-bdcd-86c6307a5237") .exchange() @@ -163,7 +163,7 @@ class Cas2BailAssessmentTest : IntegrationTestBase() { @Nested inner class ControlsOnExternalUsers { @Test - fun `getting an assessment is forbidden to external users who are not Assessors or Admins`() { + fun `getting a cas2bail assessment is forbidden to external users who are not Assessors or Admins`() { val jwt = jwtAuthHelper.createClientCredentialsJwt( username = "username", authSource = "auth", @@ -182,7 +182,7 @@ class Cas2BailAssessmentTest : IntegrationTestBase() { @Nested inner class ControlsOnInternalUsers { @Test - fun `getting an assessment is forbidden to nomis users`() { + fun `getting a cas2bail assessment is forbidden to nomis users`() { val jwt = jwtAuthHelper.createClientCredentialsJwt( username = "username", authSource = "nomis", @@ -199,7 +199,7 @@ class Cas2BailAssessmentTest : IntegrationTestBase() { } @Test - fun `assessors update assessment returns 200`() { + fun `assessors update cas2bail assessment returns 200`() { val applicationId = UUID.fromString("22ceda56-98b2-411d-91cc-ace0ab8be872") givenACas2PomUser { referrer, _ -> @@ -234,7 +234,7 @@ class Cas2BailAssessmentTest : IntegrationTestBase() { } @Test - fun `admins get assessment returns 200`() { + fun `admins get cas2bail assessment returns 200`() { val applicationId = UUID.fromString("22ceda56-98b2-411d-91cc-ace0ab8be872") givenACas2PomUser { referrer, _ -> From c1afc7b010f33df68b78c0042f5e0ea951d2ed38 Mon Sep 17 00:00:00 2001 From: garethCAS2 Date: Wed, 4 Dec 2024 15:10:17 +0000 Subject: [PATCH 21/37] feat: Cas2BailPeopleController and tests --- .../cas2bail/Cas2BailPeopleController.kt | 123 ++++++++++ .../Cas2BailPersonOASysRiskToSelfTest.kt | 122 ++++++++++ .../cas2bail/Cas2BailPersonOASysRoshTest.kt | 122 ++++++++++ .../cas2bail/Cas2BailPersonRisksTest.kt | 135 +++++++++++ .../cas2bail/Cas2BailPersonSearchTest.kt | 212 ++++++++++++++++++ 5 files changed, 714 insertions(+) create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailPeopleController.kt create mode 100644 src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailPersonOASysRiskToSelfTest.kt create mode 100644 src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailPersonOASysRoshTest.kt create mode 100644 src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailPersonRisksTest.kt create mode 100644 src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailPersonSearchTest.kt diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailPeopleController.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailPeopleController.kt new file mode 100644 index 0000000000..cf025ebc14 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailPeopleController.kt @@ -0,0 +1,123 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.controller.cas2bail + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.runBlocking +import org.slf4j.LoggerFactory +import org.springframework.http.ResponseEntity +import org.springframework.stereotype.Service +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.cas2.PeopleCas2Delegate +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.cas2bail.PeopleCas2bailDelegate +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.OASysRiskOfSeriousHarm +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.OASysRiskToSelf +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Person +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.PersonRisks +import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.ProbationOffenderSearchResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.community.OffenderDetailSummary +import uk.gov.justice.digital.hmpps.approvedpremisesapi.problem.ForbiddenProblem +import uk.gov.justice.digital.hmpps.approvedpremisesapi.problem.NotFoundProblem +import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.AuthorisableActionResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.NomisUserService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2.OffenderService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.OASysSectionsTransformer +import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.PersonTransformer +import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.RisksTransformer +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.OffenderService as OASysOffenderService + +@Service("Cas2BailPeopleController") +class Cas2BailPeopleController( + private val offenderService: OffenderService, + private val oaSysOffenderService: OASysOffenderService, + private val oaSysSectionsTransformer: OASysSectionsTransformer, + private val personTransformer: PersonTransformer, + private val risksTransformer: RisksTransformer, + private val nomisUserService: NomisUserService, +) : PeopleCas2bailDelegate { + private val log = LoggerFactory.getLogger(this::class.java) + + override fun peopleSearchGet(nomsNumber: String): ResponseEntity { + val currentUser = nomisUserService.getUserForRequest() + + val probationOffenderResult = offenderService.getPersonByNomsNumber(nomsNumber, currentUser) + + when (probationOffenderResult) { + is ProbationOffenderSearchResult.NotFound -> throw NotFoundProblem(nomsNumber, "Offender") + is ProbationOffenderSearchResult.Forbidden -> throw ForbiddenProblem() + is ProbationOffenderSearchResult.Unknown -> + throw probationOffenderResult.throwable + ?: RuntimeException("Could not retrieve person info for Prison Number: $nomsNumber") + + is ProbationOffenderSearchResult.Success.Full -> return ResponseEntity.ok( + personTransformer.transformProbationOffenderToPersonApi(probationOffenderResult, nomsNumber), + ) + } + } + + override fun peopleCrnOasysRiskToSelfGet(crn: String): ResponseEntity { + getOffenderDetails(crn) + + return runBlocking(context = Dispatchers.IO) { + val offenceDetailsResult = async { + oaSysOffenderService.getOASysOffenceDetails(crn) + } + + val riskToTheIndividualResult = async { + oaSysOffenderService.getOASysRiskToTheIndividual(crn) + } + + val offenceDetails = getSuccessEntityOrThrow(crn, offenceDetailsResult.await()) + val riskToTheIndividual = getSuccessEntityOrThrow(crn, riskToTheIndividualResult.await()) + + ResponseEntity.ok( + oaSysSectionsTransformer.transformRiskToIndividual(offenceDetails, riskToTheIndividual), + ) + } + } + + override fun peopleCrnOasysRoshGet(crn: String): ResponseEntity { + getOffenderDetails(crn) + + return runBlocking(context = Dispatchers.IO) { + val offenceDetailsResult = async { + oaSysOffenderService.getOASysOffenceDetails(crn) + } + + val roshResult = async { + oaSysOffenderService.getOASysRoshSummary(crn) + } + + val offenceDetails = getSuccessEntityOrThrow(crn, offenceDetailsResult.await()) + val rosh = getSuccessEntityOrThrow(crn, roshResult.await()) + + ResponseEntity.ok( + oaSysSectionsTransformer.transformRiskOfSeriousHarm(offenceDetails, rosh), + ) + } + } + + override fun peopleCrnRisksGet(crn: String): ResponseEntity { + val risks = when (val risksResult = offenderService.getRiskByCrn(crn)) { + is AuthorisableActionResult.Unauthorised -> throw ForbiddenProblem() + is AuthorisableActionResult.NotFound -> throw NotFoundProblem(crn, "Person") + is AuthorisableActionResult.Success -> risksResult.entity + } + + return ResponseEntity.ok(risksTransformer.transformDomainToApi(risks, crn)) + } + + private fun getOffenderDetails(crn: String): OffenderDetailSummary { + val offenderDetails = when (val offenderDetailsResult = offenderService.getOffenderByCrn(crn)) { + is AuthorisableActionResult.NotFound -> throw NotFoundProblem(crn, "Person") + is AuthorisableActionResult.Unauthorised -> throw ForbiddenProblem() + is AuthorisableActionResult.Success -> offenderDetailsResult.entity + } + + return offenderDetails + } + + private fun getSuccessEntityOrThrow(crn: String, authorisableActionResult: AuthorisableActionResult): T = when (authorisableActionResult) { + is AuthorisableActionResult.NotFound -> throw NotFoundProblem(crn, "Person") + is AuthorisableActionResult.Unauthorised -> throw ForbiddenProblem() + is AuthorisableActionResult.Success -> authorisableActionResult.entity + } +} \ No newline at end of file diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailPersonOASysRiskToSelfTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailPersonOASysRiskToSelfTest.kt new file mode 100644 index 0000000000..78abbb10d3 --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailPersonOASysRiskToSelfTest.kt @@ -0,0 +1,122 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.cas2bail + + +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.OffenceDetailsFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.RiskToTheIndividualFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.IntegrationTestBase +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenACas2PomUser +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenAnOffender +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.httpmocks.apOASysContextMockSuccessfulOffenceDetailsCall +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.httpmocks.apOASysContextMockSuccessfulRiskToTheIndividualCall +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.httpmocks.apOASysContextMockUnsuccessfulRisksToTheIndividualCallWithDelay +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.httpmocks.communityAPIMockNotFoundOffenderDetailsCall +import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.OASysSectionsTransformer + +class Cas2BailPersonOASysRiskToSelfTest : IntegrationTestBase() { + @Autowired + lateinit var oaSysSectionsTransformer: OASysSectionsTransformer + + @Test + fun `Getting cas2bail Risk to Self by CRN without a JWT returns 401`() { + webTestClient.get() + .uri("/cas2bail/people/CRN/oasys/risk-to-self") + .exchange() + .expectStatus() + .isUnauthorized + } + + @Test + fun `Getting cas2bail Risk to Self for a CRN with an invalid auth-source JWT returns 403`() { + val jwt = jwtAuthHelper.createClientCredentialsJwt( + username = "username", + authSource = "bananas", + ) + + webTestClient.get() + .uri("/cas2bail/people/CRN/oasys/risk-to-self") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isForbidden + } + + @Test + fun `Getting cas2bail oasys sections for a CRN without ROLE_PROBATION or ROLE_POM returns 403`() { + val jwt = jwtAuthHelper.createAuthorizationCodeJwt( + subject = "username", + authSource = "delius", + roles = listOf("ROLE_OTHER"), + ) + + webTestClient.get() + .uri("/cas2bail/people/CRN/oasys/risk-to-self") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isForbidden + } + + @Test + fun `Getting cas2bail Risk To Self for a CRN that does not exist returns 404`() { + givenACas2PomUser { userEntity, jwt -> + val crn = "CRN123" + + communityAPIMockNotFoundOffenderDetailsCall(crn) + + webTestClient.get() + .uri("/cas2bail/people/$crn/oasys/risk-to-self") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isNotFound + } + } + + @Test + fun `Getting cas2bail Risk to Self for a CRN returns OK with correct body`() { + givenACas2PomUser { userEntity, jwt -> + givenAnOffender { offenderDetails, inmateDetails -> + val offenceDetails = OffenceDetailsFactory().produce() + apOASysContextMockSuccessfulOffenceDetailsCall(offenderDetails.otherIds.crn, offenceDetails) + + val risksToTheIndividual = RiskToTheIndividualFactory().produce() + apOASysContextMockSuccessfulRiskToTheIndividualCall(offenderDetails.otherIds.crn, risksToTheIndividual) + + webTestClient.get() + .uri("/cas2bail/people/${offenderDetails.otherIds.crn}/oasys/risk-to-self") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isOk + .expectBody() + .json( + objectMapper.writeValueAsString( + oaSysSectionsTransformer.transformRiskToIndividual( + offenceDetails, + risksToTheIndividual, + ), + ), + ) + } + } + } + + @Test + fun `Getting cas2bail Risk to Self when upstream times out returns 404`() { + givenACas2PomUser { userEntity, jwt -> + givenAnOffender { offenderDetails, inmateDetails -> + val risksToTheIndividual = RiskToTheIndividualFactory().produce() + apOASysContextMockUnsuccessfulRisksToTheIndividualCallWithDelay(offenderDetails.otherIds.crn, risksToTheIndividual, 2500) + + webTestClient.get() + .uri("/cas2bail/people/${offenderDetails.otherIds.crn}/oasys/risk-to-self") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isNotFound + } + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailPersonOASysRoshTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailPersonOASysRoshTest.kt new file mode 100644 index 0000000000..ae5aa65233 --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailPersonOASysRoshTest.kt @@ -0,0 +1,122 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.cas2bail + + +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.OffenceDetailsFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.RoshSummaryFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.IntegrationTestBase +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenACas2PomUser +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenAnOffender +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.httpmocks.apOASysContextMockSuccessfulOffenceDetailsCall +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.httpmocks.apOASysContextMockSuccessfulRoSHSummaryCall +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.httpmocks.apOASysContextMockUnsuccessfulRoshCallWithDelay +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.httpmocks.communityAPIMockNotFoundOffenderDetailsCall +import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.OASysSectionsTransformer + +class Cas2BailPersonOASysRoshTest : IntegrationTestBase() { + @Autowired + lateinit var oaSysSectionsTransformer: OASysSectionsTransformer + + @Test + fun `Getting cas2bail RoSH by CRN without a JWT returns 401`() { + webTestClient.get() + .uri("/cas2bail/people/CRN/oasys/rosh") + .exchange() + .expectStatus() + .isUnauthorized + } + + @Test + fun `Gettingcas2bail RoSH for a CRN with an invalid auth-source JWT returns 403`() { + val jwt = jwtAuthHelper.createClientCredentialsJwt( + username = "username", + authSource = "bananas", + ) + + webTestClient.get() + .uri("/cas2bail/people/CRN/oasys/rosh") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isForbidden + } + + @Test + fun `Getting cas2bail RoSH for a CRN without ROLE_PROBATION or ROLE_POM returns 403`() { + val jwt = jwtAuthHelper.createAuthorizationCodeJwt( + subject = "username", + authSource = "nomis", + roles = listOf("ROLE_OTHER"), + ) + + webTestClient.get() + .uri("/cas2bail/people/CRN/oasys/rosh") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isForbidden + } + + @Test + fun `Getting cas2bail Rosh for a CRN that does not exist returns 404`() { + givenACas2PomUser { userEntity, jwt -> + val crn = "CRN123" + + communityAPIMockNotFoundOffenderDetailsCall(crn) + + webTestClient.get() + .uri("/cas2bail/people/$crn/oasys/rosh") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isNotFound + } + } + + @Test + fun `Getting cas2bail RoSH for a CRN returns OK with correct body`() { + givenACas2PomUser { userEntity, jwt -> + givenAnOffender { offenderDetails, inmateDetails -> + val offenceDetails = OffenceDetailsFactory().produce() + apOASysContextMockSuccessfulOffenceDetailsCall(offenderDetails.otherIds.crn, offenceDetails) + + val rosh = RoshSummaryFactory().produce() + apOASysContextMockSuccessfulRoSHSummaryCall(offenderDetails.otherIds.crn, rosh) + + webTestClient.get() + .uri("/cas2bail/people/${offenderDetails.otherIds.crn}/oasys/rosh") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isOk + .expectBody() + .json( + objectMapper.writeValueAsString( + oaSysSectionsTransformer.transformRiskOfSeriousHarm( + offenceDetails, + rosh, + ), + ), + ) + } + } + } + + @Test + fun `Getting cas2bail RoSH when upstream times out returns 404`() { + givenACas2PomUser { userEntity, jwt -> + givenAnOffender { offenderDetails, inmateDetails -> + val rosh = RoshSummaryFactory().produce() + apOASysContextMockUnsuccessfulRoshCallWithDelay(offenderDetails.otherIds.crn, rosh, 2500) + + webTestClient.get() + .uri("/cas2bail/people/${offenderDetails.otherIds.crn}/oasys/rosh") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isNotFound + } + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailPersonRisksTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailPersonRisksTest.kt new file mode 100644 index 0000000000..8c7cfba428 --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailPersonRisksTest.kt @@ -0,0 +1,135 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.cas2bail + + +import org.junit.jupiter.api.Test +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.FlagsEnvelope +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.MappaEnvelope +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.PersonRisks +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.RiskEnvelopeStatus +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.RiskTierEnvelope +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.RoshRisks +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.RoshRisksEnvelope +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.RoshRatingsFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.IntegrationTestBase +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenACas2PomUser +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenAnOffender +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.httpmocks.apOASysContextMockSuccessfulRoshRatingsCall +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.httpmocks.communityAPIMockNotFoundOffenderDetailsCall +import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.oasyscontext.RiskLevel +import java.time.LocalDate +import java.time.OffsetDateTime + +class Cas2BailPersonRisksTest : IntegrationTestBase() { + @Test + fun `Getting cas2bail risks by CRN without a JWT returns 401`() { + webTestClient.get() + .uri("/cas2bail/people/CRN/risks") + .exchange() + .expectStatus() + .isUnauthorized + } + + @Test + fun `Getting cas2bail risks for a CRN with a non-Delius JWT returns 403`() { + val jwt = jwtAuthHelper.createClientCredentialsJwt( + username = "username", + authSource = "nomis", + ) + + webTestClient.get() + .uri("/cas2bail/people/CRN/risks") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isForbidden + } + + @Test + fun `Getting cas2bail risks for a CRN without ROLE_POM returns 403`() { + val jwt = jwtAuthHelper.createAuthorizationCodeJwt( + subject = "username", + authSource = "nomis", + roles = listOf("ROLE_OTHER"), + ) + + webTestClient.get() + .uri("/cas2bail/people/CRN/risks") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isForbidden + } + + @Test + fun `Getting cas2bail risks for a CRN that does not exist returns 404`() { + givenACas2PomUser { userEntity, jwt -> + val crn = "CRN123" + + communityAPIMockNotFoundOffenderDetailsCall(crn) + + webTestClient.get() + .uri("/cas2bail/people/$crn/risks") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isNotFound + } + } + + @Test + fun `Getting cas2bail risks for a CRN returns OK with correct body`() { + givenACas2PomUser { userEntity, jwt -> + givenAnOffender { offenderDetails, inmateDetails -> + apOASysContextMockSuccessfulRoshRatingsCall( + offenderDetails.otherIds.crn, + RoshRatingsFactory().apply { + withDateCompleted(OffsetDateTime.parse("2022-09-06T15:15:15Z")) + withAssessmentId(34853487) + withRiskChildrenCommunity(RiskLevel.LOW) + withRiskPublicCommunity(RiskLevel.MEDIUM) + withRiskKnownAdultCommunity(RiskLevel.HIGH) + withRiskStaffCommunity(RiskLevel.VERY_HIGH) + }.produce(), + ) + + webTestClient.get() + .uri("/cas2bail/people/${offenderDetails.otherIds.crn}/risks") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isOk + .expectBody() + .json( + objectMapper.writeValueAsString( + PersonRisks( + crn = offenderDetails.otherIds.crn, + roshRisks = RoshRisksEnvelope( + status = RiskEnvelopeStatus.retrieved, + value = RoshRisks( + overallRisk = "Very High", + riskToChildren = "Low", + riskToPublic = "Medium", + riskToKnownAdult = "High", + riskToStaff = "Very High", + lastUpdated = LocalDate.parse("2022-09-06"), + ), + ), + tier = RiskTierEnvelope( + status = RiskEnvelopeStatus.notFound, + value = null, + ), + flags = FlagsEnvelope( + status = RiskEnvelopeStatus.notFound, + value = null, + ), + mappa = MappaEnvelope( + status = RiskEnvelopeStatus.notFound, + value = null, + ), + ), + ), + ) + } + } + } +} diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailPersonSearchTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailPersonSearchTest.kt new file mode 100644 index 0000000000..8798ee58f9 --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailPersonSearchTest.kt @@ -0,0 +1,212 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.cas2bail + + +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.FullPerson +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.PersonStatus +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.PersonType +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.InmateDetailFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ProbationOffenderDetailFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.IntegrationTestBase +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenACas2PomUser +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.httpmocks.prisonAPIMockSuccessfulInmateDetailsCall +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.httpmocks.probationOffenderSearchAPIMockForbiddenOffenderSearchCall +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.httpmocks.probationOffenderSearchAPIMockNotFoundSearchCall +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.httpmocks.probationOffenderSearchAPIMockServerErrorSearchCall +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.httpmocks.probationOffenderSearchAPIMockSuccessfulOffenderSearchCall +import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.prisonsapi.AssignedLivingUnit +import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.prisonsapi.InmateStatus +import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.probationoffendersearchapi.IDs +import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.probationoffendersearchapi.OffenderProfile +import java.time.LocalDate + +class Cas2BailPersonSearchTest : IntegrationTestBase() { + @Nested + inner class Cas2BailPeopleSearchGet { + + @Nested + inner class WhenThereIsAnError { + @Test + fun `Searching cas2bail by NOMIS ID without a JWT returns 401`() { + webTestClient.get() + .uri("/cas2bail/people/search?nomsNumber=nomsNumber").exchange() + .expectStatus() + .isUnauthorized + } + + @Test + fun `Searching cas2bail for a NOMIS ID with a non-Delius or NOMIS JWT returns 403`() { + val jwt = jwtAuthHelper.createClientCredentialsJwt( + username = "username", + authSource = "other source", + ) + + webTestClient.get() + .uri("/cas2bail/people/search?nomsNumber=nomsNumber") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isForbidden + } + + @Test + fun `Searching cas2bail for a NOMIS ID without ROLE_POM returns 403`() { + val jwt = jwtAuthHelper.createAuthorizationCodeJwt( + subject = "username", + authSource = "nomis", + roles = listOf("ROLE_OTHER"), + ) + + webTestClient.get() + .uri("/cas2bail/people/search?nomsNumber=nomsNumber") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isForbidden + } + + @Test + fun `Searching cas2bail for a NOMIS ID returns Unauthorised error when it is unauthorized by the API`() { + givenACas2PomUser { userEntity, jwt -> + probationOffenderSearchAPIMockForbiddenOffenderSearchCall() + + webTestClient.get() + .uri("/cas2bail/people/search?nomsNumber=NOMS321") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isForbidden + } + } + + @Test + fun `Searching cas2bail for a NOMIS ID returns unauthorised error when offender is in a different prison`() { + givenACas2PomUser { userEntity, jwt -> + + val offender = ProbationOffenderDetailFactory() + .withOtherIds(IDs(crn = "CRN", nomsNumber = "NOMS456", pncNumber = "PNC456")) + .withFirstName("Jo") + .withSurname("AnotherPrison") + .withDateOfBirth( + LocalDate + .parse("1985-05-05"), + ) + .withGender("Male") + .withOffenderProfile(OffenderProfile(nationality = "English")) + .produce() + + val inmateDetail = InmateDetailFactory().withOffenderNo("NOMS456") + .withCustodyStatus(InmateStatus.IN) + .withAssignedLivingUnit( + AssignedLivingUnit( + agencyId = "ANOTHER_PRISON", + locationId = 5, + description = "B-2F-004", + agencyName = "HMP Example", + ), + ) + .produce() + + probationOffenderSearchAPIMockSuccessfulOffenderSearchCall("NOMS456", listOf(offender)) + prisonAPIMockSuccessfulInmateDetailsCall(inmateDetail = inmateDetail) + + webTestClient.get() + .uri("/cas2bail/people/search?nomsNumber=NOMS456") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isForbidden + } + } + + @Test + fun `Searching cas2bail for a NOMIS ID returns 404 error when it is not found`() { + givenACas2PomUser { userEntity, jwt -> + probationOffenderSearchAPIMockNotFoundSearchCall() + + webTestClient.get() + .uri("/cas2bail/people/search?nomsNumber=NOMS321") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isNotFound + } + } + + @Test + fun `Searching cas2bail for a NOMIS ID returns server error when there is a server error`() { + givenACas2PomUser { userEntity, jwt -> + probationOffenderSearchAPIMockServerErrorSearchCall() + + webTestClient.get() + .uri("/cas2bail/people/search?nomsNumber=NOMS321") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .is5xxServerError + } + } + } + + @Nested + inner class WhenSuccessful { + @Test + fun `Searching cas2bail for a NOMIS ID returns OK with correct body`() { + givenACas2PomUser(nomisUserDetailsConfigBlock = { withActiveCaseloadId("BRI") }) { userEntity, jwt -> + val offender = ProbationOffenderDetailFactory() + .withOtherIds(IDs(crn = "CRN", nomsNumber = "NOMS321", pncNumber = "PNC123")) + .withFirstName("James") + .withSurname("Someone") + .withDateOfBirth( + LocalDate + .parse("1985-05-05"), + ) + .withGender("Male") + .withOffenderProfile(OffenderProfile(nationality = "English")) + .produce() + + val inmateDetail = InmateDetailFactory().withOffenderNo("NOMS321") + .withCustodyStatus(InmateStatus.IN) + .withAssignedLivingUnit( + AssignedLivingUnit( + agencyId = "BRI", + locationId = 5, + description = "B-2F-004", + agencyName = "HMP Bristol", + ), + ) + .produce() + + probationOffenderSearchAPIMockSuccessfulOffenderSearchCall("NOMS321", listOf(offender)) + prisonAPIMockSuccessfulInmateDetailsCall(inmateDetail = inmateDetail) + + webTestClient.get() + .uri("/cas2bail/people/search?nomsNumber=NOMS321") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isOk + .expectBody() + .json( + objectMapper.writeValueAsString( + FullPerson( + type = PersonType.fullPerson, + crn = "CRN", + name = "James Someone", + dateOfBirth = LocalDate.parse("1985-05-05"), + sex = "Male", + status = PersonStatus.inCustody, + nomsNumber = "NOMS321", + pncNumber = "PNC123", + nationality = "English", + isRestricted = false, + prisonName = "HMP Bristol", + ), + ), + ) + } + } + } + } +} From 2048331c3f2957e6af65e344867766368f6fd1cf Mon Sep 17 00:00:00 2001 From: Toby Batch Date: Wed, 4 Dec 2024 15:13:33 +0000 Subject: [PATCH 22/37] chore: wip --- .../cas2bail/Cas2BailSubmissionsController.kt | 133 +++ .../service/HttpAuthService.kt | 9 + .../cas2bail/Cas2SubmissionTest.kt | 904 ++++++++++++++++++ 3 files changed, 1046 insertions(+) create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailSubmissionsController.kt create mode 100644 src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2SubmissionTest.kt diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailSubmissionsController.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailSubmissionsController.kt new file mode 100644 index 0000000000..9dbfcfb066 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailSubmissionsController.kt @@ -0,0 +1,133 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.controller.cas2bail + +import jakarta.transaction.Transactional +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.stereotype.Service +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.cas2.SubmissionsCas2Delegate +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.cas2bail.SubmissionsCas2bail +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.cas2bail.SubmissionsCas2bailDelegate +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas2SubmittedApplication +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas2SubmittedApplicationSummary +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.SortDirection +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.SubmitCas2Application +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationSummaryEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationSummaryEntity +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.problem.NotFoundProblem +import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.AuthorisableActionResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.ValidatableActionResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.ExternalUserService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.HttpAuthService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.NomisUserService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2.ApplicationService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2.OffenderService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas2.SubmissionsTransformer +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.PageCriteria +import java.util.UUID + +@Service("Cas2BailSubmissionsController") +class Cas2BailSubmissionsController( + private val httpAuthService: HttpAuthService, + private val applicationService: ApplicationService, + private val submissionsTransformer: SubmissionsTransformer, + private val offenderService: OffenderService, + private val externalUserService: ExternalUserService, + private val nomisUserService: NomisUserService, +) : SubmissionsCas2bailDelegate { + + override fun submissionsGet(page: Int?): ResponseEntity> { + val principal = httpAuthService.getCas2BailAuthenticatedPrincipalOrThrow() + if (principal.isExternalUser()) { + ensureExternalUserPersisted() + } else { + ensureNomisUserPersisted() + } + + val sortDirection = SortDirection.asc + val sortBy = "submittedAt" + + val (applications, metadata) = applicationService.getAllSubmittedApplicationsForAssessor(PageCriteria(sortBy, sortDirection, page)) + + return ResponseEntity.ok().headers( + metadata?.toHeaders(), + ).body(getPersonNamesAndTransformToSummaries(applications)) + } + + override fun submissionsApplicationIdGet(applicationId: UUID): ResponseEntity { + val principal = httpAuthService.getCas2BailAuthenticatedPrincipalOrThrow() + if (principal.isExternalUser()) { + ensureExternalUserPersisted() + } else { + ensureNomisUserPersisted() + } + + val application = when ( + val applicationResult = applicationService.getSubmittedApplicationForAssessor(applicationId) + ) { + is AuthorisableActionResult.NotFound -> null + is AuthorisableActionResult.Unauthorised -> throw ForbiddenProblem() + is AuthorisableActionResult.Success -> applicationResult.entity + } + + if (application != null) { + return ResponseEntity.ok(getPersonDetailAndTransform(application)) + } + throw NotFoundProblem(applicationId, "Application") + } + + @Transactional + override fun submissionsPost( + submitCas2BailApplication: SubmitCas2Application, + ): ResponseEntity { + val user = nomisUserService.getUserForRequest() + val submitResult = applicationService.submitApplication(submitCas2BailApplication, user) + + val validationResult = when (submitResult) { + is AuthorisableActionResult.NotFound -> throw NotFoundProblem(submitCas2BailApplication.applicationId, "Application") + is AuthorisableActionResult.Unauthorised -> throw ForbiddenProblem() + is AuthorisableActionResult.Success -> submitResult.entity + } + + when (validationResult) { + 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 -> Unit + } + + return ResponseEntity(HttpStatus.OK) + } + + private fun ensureExternalUserPersisted() { + externalUserService.getUserForRequest() + } + + private fun ensureNomisUserPersisted() { + nomisUserService.getUserForRequest() + } + + private fun getPersonNamesAndTransformToSummaries(applicationSummaries: List): List { + val crns = applicationSummaries.map { it.crn } + + val personNamesMap = offenderService.getMapOfPersonNamesAndCrns(crns) + + return applicationSummaries.map { application -> + submissionsTransformer.transformJpaSummaryToApiRepresentation(application, personNamesMap[application.crn]!!) + } + } + + private fun getPersonDetailAndTransform( + application: Cas2ApplicationEntity, + ): Cas2SubmittedApplication { + val personInfo = offenderService.getFullInfoForPersonOrThrow(application.crn) + + return submissionsTransformer.transformJpaToApiRepresentation( + application, + personInfo, + ) + } +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/HttpAuthService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/HttpAuthService.kt index 87b8e4125b..0689185efe 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/HttpAuthService.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/HttpAuthService.kt @@ -26,6 +26,15 @@ class HttpAuthService { return principal } + fun getCas2BailAuthenticatedPrincipalOrThrow(): AuthAwareAuthenticationToken { + val principal = SecurityContextHolder.getContext().authentication as AuthAwareAuthenticationToken + if (!listOf("nomis", "auth").contains(principal.token.claims["auth_source"])) { + throw ForbiddenProblem() + } + + return principal + } + fun getDeliusPrincipalOrThrow(): AuthAwareAuthenticationToken { val principal = SecurityContextHolder.getContext().authentication as AuthAwareAuthenticationToken diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2SubmissionTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2SubmissionTest.kt new file mode 100644 index 0000000000..5105b92063 --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2SubmissionTest.kt @@ -0,0 +1,904 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.cas2bail + +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.node.NullNode +import com.ninjasquad.springmockk.SpykBean +import io.mockk.clearMocks +import io.mockk.every +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Value +import org.springframework.http.HttpStatus +import org.springframework.test.web.reactive.server.returnResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.events.cas2.model.Cas2ApplicationSubmittedEvent +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas2SubmittedApplication +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas2SubmittedApplicationSummary +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.ServiceName +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.SubmitCas2Application +import uk.gov.justice.digital.hmpps.approvedpremisesapi.client.toHttpStatus +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ExternalUserDetailsFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.IntegrationTestBase +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenACas2Admin +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenACas2Assessor +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenACas2PomUser +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenAnOffender +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.httpmocks.manageUsersMockSuccessfulExternalUsersCall +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.community.OffenderDetailSummary +import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.prisonsapi.AssignedLivingUnit +import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.NomisUserTransformer +import java.time.LocalDate +import java.time.OffsetDateTime +import java.util.UUID + +class Cas2BailSubmissionTest( + @Value("\${url-templates.frontend.cas2.application}") private val applicationUrlTemplate: String, + @Value("\${url-templates.frontend.cas2.submitted-application-overview}") private val submittedApplicationUrlTemplate: String, +) : IntegrationTestBase() { + @SpykBean + lateinit var cas2BailRealApplicationRepository: Cas2BailApplicationRepository + + @SpykBean + lateinit var cas2BailRealAssessmentRepository: Cas2BailAssessmentRepository + + @SpykBean + lateinit var cas2BailRealStatusUpdateRepository: Cas2BailStatusUpdateRepository + + @SpykBean + lateinit var cas2BailRealStatusUpdateDetailRepository: Cas2BailStatusUpdateDetailRepository + + @Autowired + lateinit var nomisUserTransformer: NomisUserTransformer + + val schema = """ + { + "${"\$schema"}": "https://json-schema.org/draft/2020-12/schema", + "${"\$id"}": "https://example.com/product.schema.json", + "title": "Thing", + "description": "A thing", + "type": "object", + "properties": { + "thingId": { + "description": "The unique identifier for a thing", + "type": "integer" + } + }, + "required": [ "thingId" ] + } + """ + + @AfterEach + fun afterEach() { + // SpringMockK does not correctly clear mocks for @SpyKBeans that are also a @Repository, causing mocked behaviour + // in one test to show up in another (see https://github.com/Ninja-Squad/springmockk/issues/85) + // Manually clearing after each test seems to fix this. + clearMocks(cas2BailRealApplicationRepository) + clearMocks(cas2BailRealAssessmentRepository) + clearMocks(cas2BailRealStatusUpdateRepository) + clearMocks(cas2BailRealStatusUpdateDetailRepository) + } + + @Nested + inner class ControlsOnExternalUsers { + @Test + fun `submitting an application is forbidden to external users based on role`() { + val jwt = jwtAuthHelper.createClientCredentialsJwt( + username = "username", + authSource = "auth", + roles = listOf("ROLE_CAS2_ASSESSOR"), + ) + + webTestClient.post() + .uri("/cas2bail/submissions?applicationId=de6512fc-a225-4109-bdcd-86c6307a5237") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isForbidden + } + + @Test + fun `viewing submitted applications is forbidden to internal users based on role`() { + val jwt = jwtAuthHelper.createClientCredentialsJwt( + username = "username", + authSource = "nomis", + roles = listOf("ROLE_POM"), + ) + + webTestClient.get() + .uri("/cas2bail/submissions") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isForbidden + } + + @Test + fun `viewing a single submitted application is forbidden to internal users based on role`() { + val jwt = jwtAuthHelper.createClientCredentialsJwt( + username = "username", + authSource = "nomis", + roles = listOf("ROLE_POM"), + ) + + webTestClient.get() + .uri("/cas2bail/submissions/66911cf0-75b1-4361-84bd-501b176fd4fd") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isForbidden + } + } + + @Nested + inner class MissingJwt { + @Test + fun `Get all submitted applications without JWT returns 401`() { + webTestClient.get() + .uri("/cas2bail/submissions") + .exchange() + .expectStatus() + .isUnauthorized + } + + @Test + fun `Get single submitted application without JWT returns 401`() { + webTestClient.get() + .uri("/cas2bail/submissions/9b785e59-b85c-4be0-b271-d9ac287684b6") + .exchange() + .expectStatus() + .isUnauthorized + } + } + + @Nested + inner class GetToIndex { + @Test + fun `Previously unknown Assessor has an ExternalUser record created from details retrieved from Manage-Users API `() { + externalUserRepository.deleteAll() + + val username = "PREVIOUSLY_UNKNOWN_ASSESSOR" + val externalUserDetails = ExternalUserDetailsFactory() + .withUsername(username) + .produce() + + manageUsersMockSuccessfulExternalUsersCall( + username = username, + externalUserDetails = externalUserDetails, + ) + + val jwt = jwtAuthHelper.createValidExternalAuthorisationCodeJwt(username) + + webTestClient.get() + .uri("/cas2bail/submissions") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isOk + + Assertions.assertThat( + externalUserRepository.findByUsername(username), + ).isNotNull + } + + @Test + fun `Assessor can view ALL submitted cas2bail applications`() { + givenACas2Assessor { _externalUserEntity, jwt -> + givenACas2PomUser { user, _ -> + givenAnOffender { offenderDetails, _ -> + cas2BailApplicationJsonSchemaRepository.deleteAll() + + val cas2bailApplicationSchema = + cas2BailApplicationJsonSchemaEntityFactory.produceAndPersist { + withAddedAt(OffsetDateTime.now()) + withId(UUID.randomUUID()) + } + + val submittedcas2bailApplicationentitySecond = cas2BailApplicationEntityFactory + .produceAndPersist { + withApplicationSchema(cas2bailApplicationSchema) + withCreatedByUser(user) + withCrn(offenderDetails.otherIds.crn) + withNomsNumber(offenderDetails.otherIds.nomsNumber!!) + withSubmittedAt(OffsetDateTime.parse("2023-01-02T09:00:00+01:00")) + withData("{}") + } + + val submittedcas2bailApplicationentityFirst = cas2BailApplicationEntityFactory + .produceAndPersist { + withApplicationSchema(cas2bailApplicationSchema) + withCreatedByUser(user) + withCrn(offenderDetails.otherIds.crn) + withNomsNumber(offenderDetails.otherIds.nomsNumber!!) + withSubmittedAt(OffsetDateTime.parse("2023-01-01T09:00:00+01:00")) + withData("{}") + } + + val submittedcas2bailApplicationentityThird = cas2BailApplicationEntityFactory + .produceAndPersist { + withApplicationSchema(cas2bailApplicationSchema) + withCreatedByUser(user) + withCrn(offenderDetails.otherIds.crn) + withNomsNumber(offenderDetails.otherIds.nomsNumber!!) + withSubmittedAt(OffsetDateTime.parse("2023-01-03T09:00:00+01:00")) + withData("{}") + } + + val inProgressCas2bailApplicationEntity = cas2BailApplicationEntityFactory + .produceAndPersist { + withApplicationSchema(cas2bailApplicationSchema) + withCreatedByUser(user) + withCrn(offenderDetails.otherIds.crn) + withNomsNumber(offenderDetails.otherIds.nomsNumber!!) + withSubmittedAt(null) + withData("{}") + } + + val rawResponseBody = webTestClient.get() + .uri("/cas2bail/submissions?page=1") + .header("Authorization", "Bearer $jwt") + .header("X-Service-Name", ServiceName.cas2.value) + .exchange() + .expectStatus() + .isOk + .expectHeader().valueEquals("X-Pagination-CurrentPage", 1) + .expectHeader().valueEquals("X-Pagination-TotalPages", 1) + .expectHeader().valueEquals("X-Pagination-TotalResults", 3) + .expectHeader().valueEquals("X-Pagination-PageSize", 10) + .returnResult() + .responseBody + .blockFirst() + + val responseBody = + objectMapper.readValue( + rawResponseBody, + object : TypeReference>() {}, + ) + + assertApplicationResponseMatchesExpected( + responseBody[0], + submittedcas2bailApplicationentityFirst, + offenderDetails, + ) + + assertApplicationResponseMatchesExpected( + responseBody[1], + submittedcas2bailApplicationentitySecond, + offenderDetails, + ) + + assertApplicationResponseMatchesExpected( + responseBody[2], + submittedcas2bailApplicationentityThird, + offenderDetails, + ) + + Assertions.assertThat(responseBody).noneMatch { + inProgressCas2bailApplicationEntity.id == it.id + } + } + } + } + } + + private fun assertApplicationResponseMatchesExpected( + response: Cas2SubmittedApplicationSummary, + expectedSubmittedApplication: Cas2BailApplicationEntity, + offenderDetails: OffenderDetailSummary, + ) { + Assertions.assertThat(response).matches { + expectedSubmittedApplication.id == it.id && + expectedSubmittedApplication.crn == it.crn && + expectedSubmittedApplication.nomsNumber == it.nomsNumber && + expectedSubmittedApplication.createdAt.toInstant() == it.createdAt && + expectedSubmittedApplication.createdByUser.id == it.createdByUserId && + expectedSubmittedApplication.submittedAt?.toInstant() == it.submittedAt + } + + Assertions.assertThat(response.personName) + .isEqualTo("${offenderDetails.firstName} ${offenderDetails.surname}") + } + } + + @Nested + inner class GetToShow { + + private fun createInProgressApplication( + newestJsonSchema: Cas2BailApplicationJsonSchemaEntity, + crn: String, + user: NomisUserEntity, + ): Cas2BailApplicationEntity { + val applicationEntity = cas2BailApplicationEntityFactory.produceAndPersist { + withApplicationSchema(newestJsonSchema) + withCrn(crn) + withCreatedByUser(user) + withSubmittedAt(null) + withData( + """ + { + "thingId": 123 + } + """, + ) + } + + return applicationEntity + } + + @Test + fun `Previously unknown Assessor has an ExternalUser record created from details retrieved from Manage-Users API`() { + externalUserRepository.deleteAll() + + val username = "PREVIOUSLY_UNKNOWN_ASSESSOR" + val externalUserDetails = ExternalUserDetailsFactory() + .withUsername(username) + .produce() + + manageUsersMockSuccessfulExternalUsersCall( + username = username, + externalUserDetails = externalUserDetails, + ) + + val jwt = jwtAuthHelper.createValidExternalAuthorisationCodeJwt(username) + + webTestClient.get() + .uri("/cas2/submissions/fea7986d-cae6-4a7a-8420-5b31376ce787") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isNotFound + + Assertions.assertThat( + externalUserRepository.findByUsername("PREVIOUSLY_UNKNOWN_ASSESSOR"), + ).isNotNull + } + + @Test + fun `Assessor can view single submitted application`() { + givenACas2Assessor { assessor, jwt -> + givenACas2PomUser { user, _ -> + givenAnOffender { offenderDetails, _ -> + cas2BailApplicationJsonSchemaRepository.deleteAll() + + val newestJsonSchema = cas2BailApplicationJsonSchemaEntityFactory + .produceAndPersist { + withAddedAt(OffsetDateTime.parse("2022-09-21T12:45:00+01:00")) + withSchema( + schema, + ) + } + + val applicationEntity = cas2BailApplicationEntityFactory.produceAndPersist { + withApplicationSchema(newestJsonSchema) + withCrn(offenderDetails.otherIds.crn) + withCreatedByUser(user) + withSubmittedAt(OffsetDateTime.parse("2022-09-21T12:45:00+01:00")) + withData( + """ + { + "thingId": 123 + } + """, + ) + } + + val assessmentEntity = cas2BailAssessmentEntityFactory.produceAndPersist { + withApplication(applicationEntity) + withNacroReferralId("OH123") + withAssessorName("Assessor name") + } + + val update1 = cas2BailStatusUpdateEntityFactory.produceAndPersist { + withApplication(applicationEntity) + withAssessment(assessmentEntity) + withAssessor(assessor) + withLabel("1st update") + } + + val update2 = cas2BailStatusUpdateEntityFactory.produceAndPersist { + withApplication(applicationEntity) + withAssessment(assessmentEntity) + withAssessor(assessor) + withLabel("2nd update") + } + + val update3 = cas2BailStatusUpdateEntityFactory.produceAndPersist { + withApplication(applicationEntity) + withAssessment(assessmentEntity) + withAssessor(assessor) + withStatusId(UUID.fromString("9a381bc6-22d3-41d6-804d-4e49f428c1de")) + withLabel("3rd update") + } + + val statusUpdateDetail = Cas2BailStatusUpdateDetailEntity( + id = UUID.fromString("5f89ec4d-1a3e-4ec3-a48b-52959d6fcc6a"), + statusUpdate = update3, + statusDetailId = UUID.fromString("62645779-242d-4601-a8f8-d2cbf1d41dfa"), + label = "Detail on 3rd update", + ) + + update1.apply { this.createdAt = OffsetDateTime.now().minusDays(20) } + cas2BailRealStatusUpdateRepository.save(update1) + + update2.apply { this.createdAt = OffsetDateTime.now().minusDays(15) } + cas2BailRealStatusUpdateRepository.save(update2) + + update3.apply { this.createdAt = OffsetDateTime.now().minusDays(1) } + cas2BailRealStatusUpdateRepository.save(update3) + + statusUpdateDetail.apply { this.createdAt = OffsetDateTime.now().minusDays(1) } + cas2BailRealStatusUpdateDetailRepository.save(statusUpdateDetail) + + val rawResponseBody = webTestClient.get() + .uri("/cas2/submissions/${applicationEntity.id}") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isOk + .returnResult() + .responseBody + .blockFirst() + + val responseBody = objectMapper.readValue( + rawResponseBody, + Cas2SubmittedApplication::class.java, + ) + + val applicant = nomisUserTransformer.transformJpaToApi( + applicationEntity + .createdByUser, + ) + + Assertions.assertThat(responseBody).matches { + applicationEntity.id == it.id && + applicationEntity.crn == it.person.crn && + applicationEntity.createdAt.toInstant() == it.createdAt && + applicant == it.submittedBy && + applicationEntity.submittedAt?.toInstant() == it.submittedAt && + serializableToJsonNode(applicationEntity.document) == serializableToJsonNode(it.document) && + newestJsonSchema.id == it.schemaVersion && !it.outdatedSchema && + assessmentEntity.assessorName == it.assessment.assessorName && + assessmentEntity.nacroReferralId == it.assessment.nacroReferralId + } + + Assertions.assertThat(responseBody.assessment.statusUpdates!!.map { update -> update.label }) + .isEqualTo(listOf("3rd update", "2nd update", "1st update")) + + Assertions.assertThat(responseBody.assessment.statusUpdates!!.map { update -> update.label }) + .isEqualTo(listOf("3rd update", "2nd update", "1st update")) + + Assertions.assertThat( + responseBody.assessment.statusUpdates!!.first().statusUpdateDetails!! + .map { detail -> detail.label }, + ) + .isEqualTo(listOf("Detail on 3rd update")) + + Assertions.assertThat(responseBody.timelineEvents.map { event -> event.label }) + .isEqualTo(listOf("3rd update", "2nd update", "1st update", "Application submitted")) + } + } + } + } + + @Test + fun `Assessor can NOT view single in-progress application`() { + givenACas2Assessor { _, jwt -> + givenACas2PomUser { user, _ -> + givenAnOffender { offenderDetails, _ -> + cas2BailApplicationJsonSchemaRepository.deleteAll() + + val newestJsonSchema = cas2BailApplicationJsonSchemaEntityFactory + .produceAndPersist { + withAddedAt(OffsetDateTime.parse("2022-09-21T12:45:00+01:00")) + withSchema( + schema, + ) + } + + val applicationEntity = createInProgressApplication( + newestJsonSchema, + offenderDetails.otherIds.crn, + user, + ) + + val rawResponseBody = webTestClient.get() + .uri("/cas2/submissions/${applicationEntity.id}") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isNotFound + } + } + } + } + + @Nested + inner class ControlsOnCas2Admin { + @Test + fun `Admin can view single submitted application`() { + givenACas2Assessor { assessor, _ -> + givenACas2Admin { admin, jwt -> + givenACas2PomUser { user, _ -> + givenAnOffender { offenderDetails, _ -> + cas2BailApplicationJsonSchemaRepository.deleteAll() + + val newestJsonSchema = cas2BailApplicationJsonSchemaEntityFactory + .produceAndPersist { + withAddedAt(OffsetDateTime.parse("2022-09-21T12:45:00+01:00")) + withSchema( + schema, + ) + } + + val applicationEntity = cas2BailApplicationEntityFactory.produceAndPersist { + withApplicationSchema(newestJsonSchema) + withCrn(offenderDetails.otherIds.crn) + withCreatedByUser(user) + withSubmittedAt(OffsetDateTime.parse("2022-09-21T12:45:00+01:00")) + withData( + """ + { + "thingId": 123 + } + """, + ) + } + + val assessmentEntity = cas2BailAssessmentEntityFactory.produceAndPersist { + withApplication(applicationEntity) + withNacroReferralId("OH123") + withAssessorName("Assessor name") + } + + val update1 = cas2BailStatusUpdateEntityFactory.produceAndPersist { + withApplication(applicationEntity) + withAssessor(assessor) + withAssessment(assessmentEntity) + withLabel("1st update") + } + + val update2 = cas2BailStatusUpdateEntityFactory.produceAndPersist { + withApplication(applicationEntity) + withAssessment(assessmentEntity) + withAssessor(assessor) + withLabel("2nd update") + } + + val update3 = cas2BailStatusUpdateEntityFactory.produceAndPersist { + withApplication(applicationEntity) + withAssessment(assessmentEntity) + withAssessor(assessor) + withLabel("3rd update") + } + + update1.apply { this.createdAt = OffsetDateTime.now().minusDays(20) } + cas2BailRealStatusUpdateRepository.save(update1) + + update2.apply { this.createdAt = OffsetDateTime.now().minusDays(15) } + cas2BailRealStatusUpdateRepository.save(update2) + + update3.apply { this.createdAt = OffsetDateTime.now().minusDays(1) } + cas2BailRealStatusUpdateRepository.save(update3) + + val rawResponseBody = webTestClient.get() + .uri("/cas2/submissions/${applicationEntity.id}") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isOk + .returnResult() + .responseBody + .blockFirst() + + val responseBody = objectMapper.readValue( + rawResponseBody, + Cas2SubmittedApplication::class.java, + ) + + val applicant = nomisUserTransformer.transformJpaToApi( + applicationEntity + .createdByUser, + ) + + Assertions.assertThat(responseBody).matches { + applicationEntity.id == it.id && + applicationEntity.crn == it.person.crn && + applicationEntity.createdAt.toInstant() == it.createdAt && + applicant == it.submittedBy && + applicationEntity.submittedAt?.toInstant() == it.submittedAt && + serializableToJsonNode(applicationEntity.document) == serializableToJsonNode(it.document) && + newestJsonSchema.id == it.schemaVersion && !it.outdatedSchema + } + + Assertions.assertThat(responseBody.assessment.statusUpdates!!.map { update -> update.label }) + .isEqualTo(listOf("3rd update", "2nd update", "1st update")) + + Assertions.assertThat(responseBody.timelineEvents.map { event -> event.label }) + .isEqualTo(listOf("3rd update", "2nd update", "1st update", "Application submitted")) + } + } + } + } + } + + @Test + fun `Admin can NOT view single in-progress application`() { + givenACas2Admin { _, jwt -> + givenACas2PomUser { user, _ -> + givenAnOffender { offenderDetails, _ -> + cas2BailApplicationJsonSchemaRepository.deleteAll() + + val newestJsonSchema = cas2BailApplicationJsonSchemaEntityFactory + .produceAndPersist { + withAddedAt(OffsetDateTime.parse("2022-09-21T12:45:00+01:00")) + withSchema( + schema, + ) + } + + val applicationEntity = createInProgressApplication( + newestJsonSchema, + offenderDetails.otherIds.crn, + user, + ) + + val rawResponseBody = webTestClient.get() + .uri("/cas2/submissions/${applicationEntity.id}") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isNotFound + } + } + } + } + } + } + + @Nested + inner class PostToSubmit { + + @Test + fun `Submit Cas2 application returns 200`() { + val applicationId = UUID.fromString("22ceda56-98b2-411d-91cc-ace0ab8be872") + val telephoneNumber = "123 456 7891" + + givenACas2PomUser { submittingUser, jwt -> + givenAnOffender( + inmateDetailsConfigBlock = { + withAssignedLivingUnit( + AssignedLivingUnit( + agencyId = "agency_id", + locationId = 123.toLong(), + agencyName = "agency_name", + description = null, + ), + ) + }, + ) { offenderDetails, _ -> + + val applicationSchema = + cas2BailApplicationJsonSchemaEntityFactory.produceAndPersist { + withAddedAt(OffsetDateTime.now()) + withId(UUID.randomUUID()) + withSchema( + schema, + ) + } + + cas2BailApplicationEntityFactory.produceAndPersist { + withCrn(offenderDetails.otherIds.crn) + withNomsNumber(offenderDetails.otherIds.nomsNumber.toString()) + withId(applicationId) + withApplicationSchema(applicationSchema) + withCreatedByUser(submittingUser) + withData( + """ + { + "thingId": 123 + } + """, + ) + } + + Assertions.assertThat(cas2BailRealAssessmentRepository.count()).isEqualTo(0) + + webTestClient.post() + .uri("/cas2/submissions") + .header("Authorization", "Bearer $jwt") + .header("X-Service-Name", ServiceName.cas2.value) + .bodyValue( + SubmitCas2Application( + applicationId = applicationId, + translatedDocument = {}, + preferredAreas = "Leeds | Bradford", + hdcEligibilityDate = LocalDate.parse("2023-03-30"), + conditionalReleaseDate = LocalDate.parse("2023-04-29"), + telephoneNumber = telephoneNumber, + ), + ) + .exchange() + .expectStatus() + .isOk + } + + // verify that generated 'application.submitted' domain event links to the CAS2 domain + val expectedFrontEndUrl = applicationUrlTemplate.replace("#id", applicationId.toString()) + val persistedDomainEvent = domainEventRepository.findFirstByOrderByCreatedAtDesc() + val domainEventFromJson = objectMapper.readValue( + persistedDomainEvent!!.data, + Cas2ApplicationSubmittedEvent::class.java, + ) + Assertions.assertThat(domainEventFromJson.eventDetails.applicationUrl) + .isEqualTo(expectedFrontEndUrl) + + val persistedAssessment = cas2BailRealAssessmentRepository.findAll().first() + Assertions.assertThat(persistedAssessment!!.application.id).isEqualTo(applicationId) + + val expectedEmailUrl = submittedApplicationUrlTemplate.replace("#applicationId", applicationId.toString()) + emailAsserter.assertEmailsRequestedCount(1) + emailAsserter.assertEmailRequested( + notifyConfig.emailAddresses.cas2Assessors, + notifyConfig.templates.cas2ApplicationSubmitted, + personalisation = mapOf( + "name" to submittingUser.name, + "email" to submittingUser.email!!, + "prisonNumber" to persistedAssessment.application.nomsNumber!!, + "telephoneNumber" to telephoneNumber, + "applicationUrl" to expectedEmailUrl, + ), + replyToEmailId = notifyConfig.emailAddresses.cas2ReplyToId, + ) + } + } + + @Test + fun `When several concurrent submit application requests occur, only one is successful, all others return 400`() { + givenACas2PomUser { submittingUser, jwt -> + givenAnOffender( + inmateDetailsConfigBlock = { + withAssignedLivingUnit( + AssignedLivingUnit( + agencyId = "agency_id", + locationId = 123.toLong(), + agencyName = "agency_name", + description = null, + ), + ) + }, + ) { offenderDetails, _ -> + val applicationId = UUID.fromString("22ceda56-98b2-411d-91cc-ace0ab8be872") + + val applicationSchema = cas2BailApplicationJsonSchemaEntityFactory + .produceAndPersist { + withAddedAt(OffsetDateTime.now()) + withId(UUID.randomUUID()) + withSchema( + schema, + ) + } + + cas2BailApplicationEntityFactory.produceAndPersist { + withCrn(offenderDetails.otherIds.crn) + withNomsNumber(offenderDetails.otherIds.nomsNumber.toString()) + withId(applicationId) + withApplicationSchema(applicationSchema) + withCreatedByUser(submittingUser) + withData( + """ + { + "thingId": 123 + } + """, + ) + } + + every { cas2BailRealApplicationRepository.save(any()) } answers { + Thread.sleep(1000) + it.invocation.args[0] as Cas2BailApplicationEntity + } + + val responseStatuses = mutableListOf() + + (1..10).map { + val thread = Thread { + webTestClient.post() + .uri("/cas2/submissions") + .header("Authorization", "Bearer $jwt") + .bodyValue( + SubmitCas2Application( + applicationId = applicationId, + translatedDocument = {}, + telephoneNumber = "123 456 7891", + ), + ) + .exchange() + .returnResult() + .consumeWith { + synchronized(responseStatuses) { + responseStatuses += it.status.toHttpStatus() + } + } + } + + thread.start() + + thread + }.forEach(Thread::join) + + Assertions.assertThat(responseStatuses.count { it.value() == 200 }).isEqualTo(1) + Assertions.assertThat(responseStatuses.count { it.value() == 400 }).isEqualTo(9) + } + } + } + + @Test + fun `When there's an error fetching the referred person's prison code, the application is not saved`() { + givenACas2PomUser { submittingUser, jwt -> + givenAnOffender(mockNotFoundErrorForPrisonApi = true) { offenderDetails, _ -> + val applicationId = UUID.fromString("22ceda56-98b2-411d-91cc-ace0ab8be872") + + val applicationSchema = cas2BailApplicationJsonSchemaEntityFactory + .produceAndPersist { + withAddedAt(OffsetDateTime.now()) + withId(UUID.randomUUID()) + withSchema( + schema, + ) + } + + cas2BailApplicationEntityFactory.produceAndPersist { + withCrn(offenderDetails.otherIds.crn) + withNomsNumber(offenderDetails.otherIds.nomsNumber!!) + withId(applicationId) + withApplicationSchema(applicationSchema) + withCreatedByUser(submittingUser) + withData( + """ + { + "thingId": 123 + } + """, + ) + } + + Assertions.assertThat(domainEventRepository.count()).isEqualTo(0) + Assertions.assertThat(cas2BailRealAssessmentRepository.count()).isEqualTo(0) + + webTestClient.post() + .uri("/cas2/submissions") + .header("Authorization", "Bearer $jwt") + .header("X-Service-Name", ServiceName.cas2.value) + .bodyValue( + SubmitCas2Application( + applicationId = applicationId, + translatedDocument = {}, + preferredAreas = "Leeds | Bradford", + hdcEligibilityDate = LocalDate.parse("2023-03-30"), + conditionalReleaseDate = LocalDate.parse("2023-04-29"), + telephoneNumber = "123 456 789", + ), + ) + .exchange() + .expectStatus() + .isBadRequest + + Assertions.assertThat(domainEventRepository.count()).isEqualTo(0) + Assertions.assertThat(cas2BailRealAssessmentRepository.count()).isEqualTo(0) + Assertions.assertThat(cas2BailRealApplicationRepository.findById(applicationId).get().submittedAt).isNull() + } + } + } + } + + private fun serializableToJsonNode(serializable: Any?): JsonNode { + if (serializable == null) return NullNode.instance + if (serializable is String) return objectMapper.readTree(serializable) + + return objectMapper.readTree(objectMapper.writeValueAsString(serializable)) + } +} From 134b3a03a36059f8fef553f7a6ae139c0cd9037b Mon Sep 17 00:00:00 2001 From: garethCAS2 Date: Thu, 28 Nov 2024 14:38:06 +0000 Subject: [PATCH 23/37] first round of cas2bail application --- .../cas2bail/Cas2BailApplicationController.kt | 175 ++++++++ .../jpa/entity/JsonSchemaEntity.kt | 10 + .../cas2bail/Cas2BailApplicationEntity.kt | 107 +++++ .../cas2bail/Cas2BailApplicationNoteEntity.kt | 69 ++++ .../Cas2BailApplicationSummaryEntity.kt | 58 +++ .../cas2bail/Cas2BailAssessmentEntity.kt | 39 ++ .../Cas2BailStatusUpdateDetailEntry.kt | 40 ++ .../cas2bail/Cas2BailStatusUpdateEntity.kt | 61 +++ .../service/cas2/JsonSchemaService.kt | 7 + .../cas2bail/Cas2BailApplicationService.kt | 390 ++++++++++++++++++ .../cas2bail/Cas2BailAssessmentService.kt | 54 +++ .../cas2bail/Cas2BailUserAccessService.kt | 26 ++ .../Cas2BailApplicationsTransformer.kt | 84 ++++ .../Cas2BailAssessmentsTransformer.kt | 23 ++ .../Cas2BailStatusUpdateTransformer.kt | 53 +++ .../Cas2BailTimelineEventsTransformer.kt | 59 +++ src/main/resources/static/_shared.yml | 43 ++ .../static/codegen/built-api-spec.yml | 43 ++ .../static/codegen/built-cas1-api-spec.yml | 43 ++ .../static/codegen/built-cas2-api-spec.yml | 43 ++ .../codegen/built-cas2bail-api-spec.yml | 43 ++ .../static/codegen/built-cas3-api-spec.yml | 43 ++ 22 files changed, 1513 insertions(+) create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailApplicationController.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationEntity.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationNoteEntity.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationSummaryEntity.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailAssessmentEntity.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailStatusUpdateDetailEntry.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailStatusUpdateEntity.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationService.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailAssessmentService.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailUserAccessService.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailApplicationsTransformer.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailAssessmentsTransformer.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailStatusUpdateTransformer.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailTimelineEventsTransformer.kt diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailApplicationController.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailApplicationController.kt new file mode 100644 index 0000000000..ae01b8fd91 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailApplicationController.kt @@ -0,0 +1,175 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.controller.cas2bail + +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.cas2bail.ApplicationsCas2bailDelegate +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationSummaryEntity +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.problem.NotFoundProblem +import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.AuthorisableActionResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.ValidatableActionResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.HttpAuthService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.NomisUserService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2.OffenderService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2bail.Cas2BailApplicationService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas2bail.Cas2BailApplicationsTransformer +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.PageCriteria +import java.net.URI +import java.util.* + +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas2ApplicationSummary as ModelCas2ApplicationSummary + +@Service( + "uk.gov.justice.digital.hmpps.approvedpremisesapi.controller.cas2bail" + + ".Cas2BailApplicationsController", +) +class Cas2BailApplicationController( + private val httpAuthService: HttpAuthService, + private val applicationService: Cas2BailApplicationService, + private val applicationsTransformer: Cas2BailApplicationsTransformer, + private val objectMapper: ObjectMapper, + private val offenderService: OffenderService, + private val userService: NomisUserService, +) : ApplicationsCas2bailDelegate { + + override fun applicationsGet( + isSubmitted: Boolean?, + page: Int?, + prisonCode: String?, + ): ResponseEntity> { + val user = userService.getUserForRequest() + + prisonCode?.let { if (prisonCode != user.activeCaseloadId) throw ForbiddenProblem() } + + val pageCriteria = PageCriteria("createdAt", SortDirection.desc, page) + + val (applications, metadata) = applicationService.getCas2BailApplications(prisonCode, isSubmitted, user, pageCriteria) + + return ResponseEntity.ok().headers( + metadata?.toHeaders(), + ).body(getPersonNamesAndTransformToSummaries(applications)) + } + + override fun applicationsApplicationIdGet(applicationId: UUID): ResponseEntity { + val user = userService.getUserForRequest() + + val application = when ( + val applicationResult = applicationService + .getCas2BailApplicationForUser( + applicationId, + user + ) + + ) { + is AuthorisableActionResult.NotFound -> null + is AuthorisableActionResult.Unauthorised -> throw ForbiddenProblem() + is AuthorisableActionResult.Success -> applicationResult.entity + } + + if (application != null) { + return ResponseEntity.ok(getPersonDetailAndTransform(application)) + } + throw NotFoundProblem(applicationId, "Application") + } + + @Transactional + override fun applicationsPost(body: NewApplication): ResponseEntity { + val nomisPrincipal = httpAuthService.getNomisPrincipalOrThrow() + val user = userService.getUserForRequest() + + val personInfo = offenderService.getFullInfoForPersonOrThrow(body.crn) + + val applicationResult = applicationService.createCas2BailApplication( + body.crn, + user, + nomisPrincipal.token.tokenValue, + ) + + 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("/cas2/applications/${application.id}")) + .body(applicationsTransformer.transformJpaToApi(application, personInfo)) + } + + @Transactional + override fun applicationsApplicationIdPut( + applicationId: UUID, + body: UpdateApplication, + ): ResponseEntity { + val user = userService.getUserForRequest() + + val serializedData = objectMapper.writeValueAsString(body.data) + + val applicationResult = applicationService.updateCas2BailApplication( + applicationId = + applicationId, + data = serializedData, + user, + ) + + val validationResult = when (applicationResult) { + is AuthorisableActionResult.NotFound -> throw NotFoundProblem(applicationId, "Application") + is AuthorisableActionResult.Unauthorised -> throw ForbiddenProblem() + is AuthorisableActionResult.Success -> applicationResult.entity + } + + val updatedApplication = when (validationResult) { + 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 + } + + return ResponseEntity.ok(getPersonDetailAndTransform(updatedApplication)) + } + + @Transactional + override fun applicationsApplicationIdAbandonPut(applicationId: UUID): ResponseEntity { + val user = userService.getUserForRequest() + + val validationResult = when (val applicationResult = applicationService.abandonCas2BailApplication(applicationId, user)) { + is AuthorisableActionResult.NotFound -> throw NotFoundProblem(applicationId, "Application") + is AuthorisableActionResult.Unauthorised -> throw ForbiddenProblem() + is AuthorisableActionResult.Success -> applicationResult.entity + } + + when (validationResult) { + 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 + } + + return ResponseEntity.ok(Unit) + } + + private fun getPersonNamesAndTransformToSummaries(applicationSummaries: List): List { + val crns = applicationSummaries.map { it.crn } + + val personNamesMap = offenderService.getMapOfPersonNamesAndCrns(crns) + + return applicationSummaries.map { application -> + applicationsTransformer.transformJpaSummaryToSummary(application, personNamesMap[application.crn]!!) + } + } + + private fun getPersonDetailAndTransform( + application: Cas2BailApplicationEntity, + ): Application { + val personInfo = offenderService.getFullInfoForPersonOrThrow(application.crn) + + return applicationsTransformer.transformJpaToApi(application, personInfo) + } +} \ No newline at end of file diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/JsonSchemaEntity.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/JsonSchemaEntity.kt index dcbffe6948..578bd132cd 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/JsonSchemaEntity.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/JsonSchemaEntity.kt @@ -53,6 +53,16 @@ class Cas2ApplicationJsonSchemaEntity( schema: String, ) : JsonSchemaEntity(id, addedAt, schema) +@Entity +@DiscriminatorValue("CAS_2_BAIL_APPLICATION") +@Table(name = "cas_2_bail_application_json_schemas") +@PrimaryKeyJoinColumn(name = "json_schema_id") +class Cas2BailApplicationJsonSchemaEntity( + id: UUID, + addedAt: OffsetDateTime, + schema: String, +) : JsonSchemaEntity(id, addedAt, schema) + @Entity @DiscriminatorValue("TEMPORARY_ACCOMMODATION_APPLICATION") @Table(name = "temporary_accommodation_application_json_schemas") diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationEntity.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationEntity.kt new file mode 100644 index 0000000000..77a75f5cbe --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationEntity.kt @@ -0,0 +1,107 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail + +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.* + + +import io.hypersistence.utils.hibernate.type.json.JsonType +import jakarta.persistence.* +import org.hibernate.annotations.Immutable +import org.hibernate.annotations.Type +import org.springframework.data.domain.Slice +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Lock +import org.springframework.data.jpa.repository.Query +import org.springframework.stereotype.Repository +import java.time.LocalDate +import java.time.OffsetDateTime +import java.util.UUID +import kotlin.jvm.Transient + +@Suppress("TooManyFunctions") +@Repository +interface Cas2BailApplicationRepository : JpaRepository { + @Query( + "SELECT a FROM Cas2BailApplicationEntity a WHERE a.id = :id AND " + + "a.submittedAt IS NOT NULL", + ) + fun findSubmittedApplicationById(id: UUID): Cas2BailApplicationEntity? + + @Query("SELECT a FROM Cas2BailApplicationEntity a WHERE a.createdByUser.id = :id") + fun findAllByCreatedByUserId(id: UUID): List + + @Query( + "SELECT a FROM Cas2BailApplicationEntity a WHERE a.submittedAt IS NOT NULL " + + "AND a NOT IN (SELECT application FROM Cas2BailAssessmentEntity)", + ) + fun findAllSubmittedApplicationsWithoutAssessments(): Slice +} + +@Repository +interface Cas2BailLockableApplicationRepository : JpaRepository { + @Query("SELECT a FROM Cas2BailLockableApplicationEntity a WHERE a.id = :id") + @Lock(LockModeType.PESSIMISTIC_WRITE) + fun acquirePessimisticLock(id: UUID): Cas2BailLockableApplicationEntity? +} + +@Entity +@Table(name = "cas_2_bail_applications") +data class Cas2BailApplicationEntity( + @Id + val id: UUID, + + val crn: String, + + @ManyToOne + @JoinColumn(name = "created_by_user_id") + val createdByUser: NomisUserEntity, + + @Type(JsonType::class) + var data: String?, + + @Type(JsonType::class) + var document: String?, + + @ManyToOne + @JoinColumn(name = "schema_version") + var schemaVersion: JsonSchemaEntity, + val createdAt: OffsetDateTime, + var submittedAt: OffsetDateTime?, + var abandonedAt: OffsetDateTime? = null, + + @OneToMany(mappedBy = "application") + @OrderBy("createdAt DESC") + var statusUpdates: MutableList? = null, + + @OneToMany(mappedBy = "application") + @OrderBy("createdAt DESC") + var notes: MutableList? = null, + + @OneToOne(fetch = FetchType.LAZY) + var assessment: Cas2BailAssessmentEntity? = null, + + @Transient + var schemaUpToDate: Boolean, + + var nomsNumber: String?, + + var referringPrisonCode: String? = null, + var preferredAreas: String? = null, + var hdcEligibilityDate: LocalDate? = null, + var conditionalReleaseDate: LocalDate? = null, + var telephoneNumber: String? = null, +) { + override fun toString() = "Cas2BailApplicationEntity: $id" +} + +/** + * Provides a version of the Cas2BailApplicationEntity with no relationships, allowing + * us to lock the applications table only without JPA/Hibernate attempting to + * lock all eagerly loaded relationships + */ +@Entity +@Table(name = "cas_2_bail_applications") +@Immutable +class Cas2BailLockableApplicationEntity( + @Id + val id: UUID, +) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationNoteEntity.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationNoteEntity.kt new file mode 100644 index 0000000000..828e678850 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationNoteEntity.kt @@ -0,0 +1,69 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail + +import jakarta.persistence.* +import org.springframework.data.domain.PageRequest +import org.springframework.data.domain.Slice +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query +import org.springframework.stereotype.Repository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.* +import java.time.OffsetDateTime +import java.util.* +import kotlin.jvm.Transient + +@Repository +interface Cas2BailApplicationNoteRepository : JpaRepository { + @Query( + "SELECT n FROM Cas2BailApplicationNoteEntity n WHERE n.assessment IS NULL", + ) + fun findAllNotesWithoutAssessment(of: PageRequest): Slice + +} + +@Entity +@Table(name = "cas_2_bail_application_notes") +data class Cas2BailApplicationNoteEntity( + @Id + val id: UUID, + + @Transient + final val createdByUser: Cas2User, + + @ManyToOne + @JoinColumn(name = "application_id") + val application: Cas2BailApplicationEntity, + + val createdAt: OffsetDateTime, + + var body: String, + + @ManyToOne + @JoinColumn(name = "assessment_id") + var assessment: Cas2BailAssessmentEntity?, +) { + + @ManyToOne + @JoinColumn(name = "created_by_nomis_user_id") + var createdByNomisUser: NomisUserEntity? = null + + @ManyToOne + @JoinColumn(name = "created_by_external_user_id") + var createdByExternalUser: ExternalUserEntity? = null + + init { + when (this.createdByUser) { + is NomisUserEntity -> this.createdByNomisUser = this.createdByUser + is ExternalUserEntity -> this.createdByExternalUser = this.createdByUser + } + } + + fun getUser(): Cas2User { + return if (this.createdByNomisUser != null) { + this.createdByNomisUser!! + } else { + this.createdByExternalUser!! + } + } + + override fun toString() = "Cas2BailApplicationNoteEntity: $id" +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationSummaryEntity.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationSummaryEntity.kt new file mode 100644 index 0000000000..991fa69810 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationSummaryEntity.kt @@ -0,0 +1,58 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.Id +import jakarta.persistence.Table +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository +import java.time.LocalDate +import java.time.OffsetDateTime +import java.util.* + +@Repository +interface Cas2BailApplicationSummaryRepository : JpaRepository { + fun findByUserId(userId: String, pageable: Pageable?): Page + + fun findByUserIdAndSubmittedAtIsNotNull(userId: String, pageable: Pageable?): Page + + fun findByUserIdAndSubmittedAtIsNull(userId: String, pageable: Pageable?): Page + + fun findByPrisonCode(prisonCode: String, pageable: Pageable?): Page + + fun findByPrisonCodeAndSubmittedAtIsNotNull(prisonCode: String, pageable: Pageable?): Page + + fun findByPrisonCodeAndSubmittedAtIsNull(prisonCode: String, pageable: Pageable?): Page + + fun findBySubmittedAtIsNotNull(pageable: Pageable?): Page +} + +@Entity +@Table(name = "cas_2_bail_application_live_summary") +data class Cas2BailApplicationSummaryEntity( + @Id + val id: UUID, + val crn: String, + @Column(name = "noms_number") + var nomsNumber: String, + @Column(name = "created_by_user_id") + val userId: String, + @Column(name = "name") + val userName: String, + @Column(name = "created_at") + val createdAt: OffsetDateTime, + @Column(name = "submitted_at") + var submittedAt: OffsetDateTime?, + @Column(name = "abandoned_at") + var abandonedAt: OffsetDateTime? = null, + @Column(name = "hdc_eligibility_date") + var hdcEligibilityDate: LocalDate? = null, + @Column(name = "label") + var latestStatusUpdateLabel: String? = null, + @Column(name = "status_id") + var latestStatusUpdateStatusId: String? = null, + @Column(name = "referring_prison_code") + val prisonCode: String, +) \ No newline at end of file diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailAssessmentEntity.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailAssessmentEntity.kt new file mode 100644 index 0000000000..d934cb5e0c --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailAssessmentEntity.kt @@ -0,0 +1,39 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail + +import jakarta.persistence.* +import org.springframework.context.annotation.Lazy + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2StatusUpdateEntity +import java.time.OffsetDateTime +import java.util.UUID + +@Repository +interface Cas2BailAssessmentRepository : JpaRepository { + fun findFirstByApplicationId(applicationId: UUID): Cas2BailAssessmentEntity? +} + +@Entity +@Table(name = "cas_2_bail_assessments") +data class Cas2BailAssessmentEntity( + @Id + val id: UUID, + + @OneToOne + @Lazy + val application: Cas2BailApplicationEntity, + + val createdAt: OffsetDateTime, + + var nacroReferralId: String? = null, + + var assessorName: String? = null, + + @OneToMany(mappedBy = "assessment") + @OrderBy("createdAt DESC") + var statusUpdates: MutableList? = null, +) { + override fun toString() = "Cas2BailAssessmentEntity: $id" +} \ No newline at end of file diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailStatusUpdateDetailEntry.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailStatusUpdateDetailEntry.kt new file mode 100644 index 0000000000..032abe277c --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailStatusUpdateDetailEntry.kt @@ -0,0 +1,40 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail + +import jakarta.persistence.* +import org.hibernate.annotations.CreationTimestamp +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository +import java.time.OffsetDateTime + +import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.reference.Cas2PersistedApplicationStatusDetail +import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.reference.Cas2PersistedApplicationStatusFinder +import java.util.UUID + +@Repository +interface Cas2BailStatusUpdateDetailRepository : JpaRepository { + fun findFirstByStatusUpdateIdOrderByCreatedAtDesc(statusUpdateId: UUID): Cas2BailStatusUpdateDetailEntity? +} + +@Entity +@Table(name = "cas_2_bail_status_update_details") +data class Cas2BailStatusUpdateDetailEntity( + @Id + val id: UUID, + val statusDetailId: UUID, + + val label: String, + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "status_update_id") + val statusUpdate: Cas2BailStatusUpdateEntity, + + @CreationTimestamp + var createdAt: OffsetDateTime? = null, +) { + override fun toString() = "Cas2BailStatusDetailEntity: $id" + + fun statusDetail(statusId: UUID, detailId: UUID): Cas2PersistedApplicationStatusDetail { + return Cas2PersistedApplicationStatusFinder().getById(statusId).statusDetails?.find { detail -> detail.id == detailId } + ?: error("Status detail with id $detailId not found") + } +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailStatusUpdateEntity.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailStatusUpdateEntity.kt new file mode 100644 index 0000000000..41ce37271d --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailStatusUpdateEntity.kt @@ -0,0 +1,61 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail + +import jakarta.persistence.* +import org.hibernate.annotations.CreationTimestamp +import org.springframework.data.domain.Pageable +import org.springframework.data.domain.Slice +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query +import org.springframework.stereotype.Repository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.reference.Cas2PersistedApplicationStatus +import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.reference.Cas2PersistedApplicationStatusFinder +import java.time.OffsetDateTime +import java.util.* + + +@Repository +interface Cas2BailStatusUpdateRepository : JpaRepository { + fun findFirstByApplicationIdOrderByCreatedAtDesc(applicationId: UUID): Cas2BailStatusUpdateEntity? + + @Query( + "SELECT su FROM Cas2BailStatusUpdateEntity su WHERE su.assessment IS NULL", + ) + fun findAllStatusUpdatesWithoutAssessment(pageable: Pageable?): Slice +} + +@Entity +@Table(name = "cas_2_bail_status_updates") +data class Cas2BailStatusUpdateEntity( + + @Id + val id: UUID, + + val statusId: UUID, + val description: String, + val label: String, + + @ManyToOne + @JoinColumn(name = "assessor_id") + val assessor: ExternalUserEntity, + + @ManyToOne + @JoinColumn(name = "application_id") + val application: Cas2BailApplicationEntity, + + @ManyToOne + @JoinColumn(name = "assessment_id") + var assessment: Cas2BailAssessmentEntity? = null, + + @OneToMany(mappedBy = "statusUpdate") + val statusUpdateDetails: List? = null, + + @CreationTimestamp + var createdAt: OffsetDateTime, +) { + override fun toString() = "Cas2BailStatusEntity: $id" + + fun status(): Cas2PersistedApplicationStatus { + return Cas2PersistedApplicationStatusFinder().getById(statusId) + } +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2/JsonSchemaService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2/JsonSchemaService.kt index 5ada788e08..19f6de5e63 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2/JsonSchemaService.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2/JsonSchemaService.kt @@ -10,6 +10,7 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2Applicati import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationRepository import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.JsonSchemaEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.JsonSchemaRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity import java.util.Collections.synchronizedMap import java.util.UUID @@ -47,5 +48,11 @@ class JsonSchemaService( return application.apply { application.schemaUpToDate = application.schemaVersion.id == newestSchema.id } } + fun checkCas2BailSchemaOutdated(application: Cas2BailApplicationEntity): Cas2BailApplicationEntity { + val newestSchema = getNewestSchema(application.schemaVersion.javaClass) + + return application.apply { application.schemaUpToDate = application.schemaVersion.id == newestSchema.id } + } + fun getNewestSchema(type: Class): JsonSchemaEntity = jsonSchemaRepository.getSchemasForType(type).maxBy { it.addedAt } } diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationService.kt new file mode 100644 index 0000000000..4e1779b193 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationService.kt @@ -0,0 +1,390 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2bail + +import com.fasterxml.jackson.databind.ObjectMapper +import jakarta.transaction.Transactional +import org.springframework.beans.factory.annotation.Value +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.events.cas2.model.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.SubmitCas2Application +import uk.gov.justice.digital.hmpps.approvedpremisesapi.config.NotifyConfig +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.DomainEvent +import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.PaginationMetadata +import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.ValidationErrors +import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.validated +import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.AuthorisableActionResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.ValidatableActionResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.EmailNotificationService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.UpstreamApiException +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.PageCriteria +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.getMetadata +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.getPageableOrAllPages +import java.time.OffsetDateTime +import java.util.* + +@Service("Cas2BailApplicationService") +class Cas2BailApplicationService( + private val cas2BailApplicationRepository: Cas2BailApplicationRepository, + private val lockableApplicationRepository: Cas2BailLockableApplicationRepository, + private val applicationSummaryRepository: Cas2BailApplicationSummaryRepository, + private val jsonSchemaService: JsonSchemaService, + private val offenderService: OffenderService, + private val cas2BailUserAccessService: Cas2BailUserAccessService, + private val domainEventService: DomainEventService, + private val emailNotificationService: EmailNotificationService, + private val assessmentService: Cas2BailAssessmentService, + private val notifyConfig: NotifyConfig, + private val objectMapper: ObjectMapper, + @Value("\${url-templates.frontend.cas2.application}") private val applicationUrlTemplate: String, + @Value("\${url-templates.frontend.cas2.submitted-application-overview}") private val submittedApplicationUrlTemplate: String, +) { + + val repositoryUserFunctionMap = mapOf( + null to applicationSummaryRepository::findByUserId, + true to applicationSummaryRepository::findByUserIdAndSubmittedAtIsNotNull, + false to applicationSummaryRepository::findByUserIdAndSubmittedAtIsNull, + ) + + val repositoryPrisonFunctionMap = mapOf( + null to applicationSummaryRepository::findByPrisonCode, + true to applicationSummaryRepository::findByPrisonCodeAndSubmittedAtIsNotNull, + false to applicationSummaryRepository::findByPrisonCodeAndSubmittedAtIsNull, + ) + + fun getCas2BailApplications( + prisonCode: String?, + isSubmitted: Boolean?, + user: NomisUserEntity, + pageCriteria: PageCriteria, + ): Pair, PaginationMetadata?> { + val response = if (prisonCode == null) { + repositoryUserFunctionMap.get(isSubmitted)!!(user.id.toString(), getPageableOrAllPages(pageCriteria)) + } else { + repositoryPrisonFunctionMap.get(isSubmitted)!!(prisonCode, getPageableOrAllPages(pageCriteria)) + } + val metadata = getMetadata(response, pageCriteria) + return Pair(response.content, metadata) + } + + fun getAllSubmittedCas2BailApplicationsForAssessor(pageCriteria: PageCriteria): Pair, PaginationMetadata?> { + val pageable = getPageableOrAllPages(pageCriteria) + + val response = applicationSummaryRepository.findBySubmittedAtIsNotNull(pageable) + + val metadata = getMetadata(response, pageCriteria) + + return Pair(response.content, metadata) + } + + fun getSubmittedCas2BailApplicationForAssessor(applicationId: UUID): AuthorisableActionResult { + val applicationEntity = cas2BailApplicationRepository.findSubmittedApplicationById(applicationId) + ?: return AuthorisableActionResult.NotFound() + + return AuthorisableActionResult.Success( + jsonSchemaService.checkCas2BailSchemaOutdated(applicationEntity), + ) + } + + fun getCas2BailApplicationForUser(applicationId: UUID, user: NomisUserEntity): AuthorisableActionResult { + val applicationEntity = cas2BailApplicationRepository.findByIdOrNull(applicationId) + ?: return AuthorisableActionResult.NotFound() + + if (applicationEntity.abandonedAt != null) { + return AuthorisableActionResult.NotFound() + } + + val canAccess = cas2BailUserAccessService.userCanViewCas2BailApplication(user, applicationEntity) + + return if (canAccess) { + AuthorisableActionResult.Success( + jsonSchemaService.checkCas2BailSchemaOutdated + (applicationEntity), + ) + } else { + AuthorisableActionResult.Unauthorised() + } + } + + fun createCas2BailApplication(crn: String, user: NomisUserEntity, jwt: String) = + validated { + val offenderDetailsResult = offenderService.getOffenderByCrn(crn) + + val offenderDetails = when (offenderDetailsResult) { + is AuthorisableActionResult.NotFound -> return "$.crn" hasSingleValidationError "doesNotExist" + is AuthorisableActionResult.Unauthorised -> return "$.crn" hasSingleValidationError "userPermission" + is AuthorisableActionResult.Success -> offenderDetailsResult.entity + } + + if (offenderDetails.otherIds.nomsNumber == null) { + throw RuntimeException("Cannot create an Application for an Offender without a NOMS number") + } + + if (validationErrors.any()) { + return fieldValidationError + } + + val createdApplication = cas2BailApplicationRepository.save( + Cas2BailApplicationEntity( + id = UUID.randomUUID(), + crn = crn, + createdByUser = user, + data = null, + document = null, + schemaVersion = jsonSchemaService.getNewestSchema(Cas2ApplicationJsonSchemaEntity::class.java), + createdAt = OffsetDateTime.now(), + submittedAt = null, + schemaUpToDate = true, + nomsNumber = offenderDetails.otherIds.nomsNumber, + telephoneNumber = null, + ), + ) + + return success(createdApplication.apply { schemaUpToDate = true }) + } + + @SuppressWarnings("ReturnCount") + fun updateCas2BailApplication(applicationId: UUID, data: String?, user: NomisUserEntity): AuthorisableActionResult> { + val application = cas2BailApplicationRepository.findByIdOrNull(applicationId)?.let(jsonSchemaService::checkCas2BailSchemaOutdated) + ?: return AuthorisableActionResult.NotFound() + + if (application.createdByUser != user) { + return AuthorisableActionResult.Unauthorised() + } + + if (application.submittedAt != null) { + return AuthorisableActionResult.Success( + ValidatableActionResult.GeneralValidationError("This application has already been submitted"), + ) + } + + if (application.abandonedAt != null) { + return AuthorisableActionResult.Success( + ValidatableActionResult.GeneralValidationError("This application has been abandoned"), + ) + } + + if (!application.schemaUpToDate) { + return AuthorisableActionResult.Success( + ValidatableActionResult.GeneralValidationError("The schema version is outdated"), + ) + } + + application.apply { + this.data = removeXssCharacters(data) + } + + val savedApplication = cas2BailApplicationRepository.save(application) + + return AuthorisableActionResult.Success( + ValidatableActionResult.Success(savedApplication), + ) + } + + @SuppressWarnings("ReturnCount") + fun abandonCas2BailApplication(applicationId: UUID, user: NomisUserEntity): AuthorisableActionResult> { + val application = cas2BailApplicationRepository.findByIdOrNull(applicationId) + ?: return AuthorisableActionResult.NotFound() + + if (application.createdByUser != user) { + return AuthorisableActionResult.Unauthorised() + } + + if (application.submittedAt != null) { + return AuthorisableActionResult.Success( + ValidatableActionResult.ConflictError(applicationId, "This application has already been submitted"), + ) + } + + if (application.abandonedAt != null) { + return AuthorisableActionResult.Success( + ValidatableActionResult.Success(application), + ) + } + + application.apply { + this.abandonedAt = OffsetDateTime.now() + this.data = null + } + + val savedApplication = cas2BailApplicationRepository.save(application) + + return AuthorisableActionResult.Success( + ValidatableActionResult.Success(savedApplication), + ) + } + + @SuppressWarnings("ReturnCount") + @Transactional + fun submitCas2BailApplication( + submitApplication: SubmitCas2Application, + user: NomisUserEntity, + ): AuthorisableActionResult> { + val applicationId = submitApplication.applicationId + + lockableApplicationRepository.acquirePessimisticLock(applicationId) + + var application = cas2BailApplicationRepository.findByIdOrNull(applicationId) + ?.let(jsonSchemaService::checkCas2BailSchemaOutdated) + ?: return AuthorisableActionResult.NotFound() + + val serializedTranslatedDocument = objectMapper.writeValueAsString(submitApplication.translatedDocument) + + if (application.createdByUser != user) { + return AuthorisableActionResult.Unauthorised() + } + + if (application.abandonedAt != null) { + return AuthorisableActionResult.Success( + ValidatableActionResult.GeneralValidationError("This application has already been abandoned"), + ) + } + + if (application.submittedAt != null) { + return AuthorisableActionResult.Success( + ValidatableActionResult.GeneralValidationError("This application has already been submitted"), + ) + } + + if (!application.schemaUpToDate) { + return AuthorisableActionResult.Success( + ValidatableActionResult.GeneralValidationError("The schema version is outdated"), + ) + } + + val validationErrors = ValidationErrors() + val applicationData = application.data + + if (applicationData == null) { + validationErrors["$.data"] = "empty" + } else if (!jsonSchemaService.validate(application.schemaVersion, applicationData)) { + validationErrors["$.data"] = "invalid" + } + + if (validationErrors.any()) { + return AuthorisableActionResult.Success( + ValidatableActionResult.FieldValidationError(validationErrors), + ) + } + + val schema = application.schemaVersion as? Cas2BailApplicationJsonSchemaEntity + ?: throw RuntimeException("Incorrect type of JSON schema referenced by CAS2 Bail Application") + + try { + application.apply { + submittedAt = OffsetDateTime.now() + document = serializedTranslatedDocument + referringPrisonCode = retrievePrisonCode(application) + preferredAreas = submitApplication.preferredAreas + hdcEligibilityDate = submitApplication.hdcEligibilityDate + conditionalReleaseDate = submitApplication.conditionalReleaseDate + telephoneNumber = submitApplication.telephoneNumber + } + } catch (error: UpstreamApiException) { + return AuthorisableActionResult.Success( + ValidatableActionResult.GeneralValidationError(error.message.toString()), + ) + } + + application = cas2BailApplicationRepository.save(application) + + createCas2ApplicationSubmittedEvent(application) + + createAssessment(application) + + sendEmailApplicationSubmitted(user, application) + + return AuthorisableActionResult.Success( + ValidatableActionResult.Success(application), + ) + } + + fun createCas2ApplicationSubmittedEvent(application: Cas2BailApplicationEntity) { + val domainEventId = UUID.randomUUID() + val eventOccurredAt = application.submittedAt ?: OffsetDateTime.now() + + domainEventService.saveCas2ApplicationSubmittedDomainEvent( + DomainEvent( + id = domainEventId, + applicationId = application.id, + crn = application.crn, + nomsNumber = application.nomsNumber, + occurredAt = eventOccurredAt.toInstant(), + data = Cas2ApplicationSubmittedEvent( + id = domainEventId, + timestamp = eventOccurredAt.toInstant(), + eventType = EventType.applicationSubmitted, + eventDetails = Cas2ApplicationSubmittedEventDetails( + applicationId = application.id, + applicationUrl = applicationUrlTemplate + .replace("#id", application.id.toString()), + submittedAt = eventOccurredAt.toInstant(), + personReference = PersonReference( + noms = application.nomsNumber ?: "Unknown NOMS Number", + crn = application.crn, + ), + referringPrisonCode = application.referringPrisonCode, + preferredAreas = application.preferredAreas, + hdcEligibilityDate = application.hdcEligibilityDate, + conditionalReleaseDate = application.conditionalReleaseDate, + submittedBy = Cas2ApplicationSubmittedEventDetailsSubmittedBy( + staffMember = Cas2StaffMember( + staffIdentifier = application.createdByUser.nomisStaffId, + name = application.createdByUser.name, + username = application.createdByUser.nomisUsername, + ), + ), + ), + ), + ), + ) + } + + fun createAssessment(application: Cas2BailApplicationEntity) { + assessmentService.createCas2Assessment(application) + } + + @SuppressWarnings("ThrowsCount") + private fun retrievePrisonCode(application: Cas2BailApplicationEntity): String { + val inmateDetailResult = offenderService.getInmateDetailByNomsNumber( + crn = application.crn, + nomsNumber = application.nomsNumber.toString(), + ) + val inmateDetail = when (inmateDetailResult) { + is AuthorisableActionResult.NotFound -> throw UpstreamApiException("Inmate Detail not found") + is AuthorisableActionResult.Unauthorised -> throw UpstreamApiException("Inmate Detail unauthorised") + is AuthorisableActionResult.Success -> inmateDetailResult.entity + } + + return inmateDetail?.assignedLivingUnit?.agencyId ?: throw UpstreamApiException("No prison code available") + } + + private fun sendEmailApplicationSubmitted(user: NomisUserEntity, application: Cas2BailApplicationEntity) { + emailNotificationService.sendEmail( + recipientEmailAddress = notifyConfig.emailAddresses.cas2Assessors, + templateId = notifyConfig.templates.cas2ApplicationSubmitted, + personalisation = mapOf( + "name" to user.name, + "email" to user.email, + "prisonNumber" to application.nomsNumber, + "telephoneNumber" to application.telephoneNumber, + "applicationUrl" to submittedApplicationUrlTemplate.replace("#applicationId", application.id.toString()), + ), + replyToEmailId = notifyConfig.emailAddresses.cas2ReplyToId, + ) + } + + private fun removeXssCharacters(data: String?): String? { + if (data != null) { + val xssCharacters = setOf('<', '<', '〈', '〈', '>', '>', '〉', '〉') + var sanitisedData = data + xssCharacters.forEach { character -> + sanitisedData = sanitisedData?.replace(character.toString(), "") + } + return sanitisedData + } + return null + } +} \ No newline at end of file diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailAssessmentService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailAssessmentService.kt new file mode 100644 index 0000000000..5633c613f9 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailAssessmentService.kt @@ -0,0 +1,54 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2bail + +import jakarta.transaction.Transactional +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.UpdateCas2Assessment + +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailAssessmentEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailAssessmentRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.AuthorisableActionResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.ValidatableActionResult +import java.time.OffsetDateTime +import java.util.* + + +@Service("Cas2BailAssessmentService") +class Cas2BailAssessmentService ( + private val assessmentRepository: Cas2BailAssessmentRepository, +) { + + @Transactional + fun createCas2Assessment(cas2BailApplicationEntity: Cas2BailApplicationEntity): Cas2BailAssessmentEntity = + assessmentRepository.save( + Cas2BailAssessmentEntity( + id = UUID.randomUUID(), + createdAt = OffsetDateTime.now(), + application = cas2BailApplicationEntity, + ), + ) + + fun updateAssessment(assessmentId: UUID, newAssessment: UpdateCas2Assessment): AuthorisableActionResult> { + val assessmentEntity = assessmentRepository.findByIdOrNull(assessmentId) + ?: return AuthorisableActionResult.NotFound() + + assessmentEntity.apply { + this.nacroReferralId = newAssessment.nacroReferralId + this.assessorName = newAssessment.assessorName + } + + val savedAssessment = assessmentRepository.save(assessmentEntity) + + return AuthorisableActionResult.Success( + ValidatableActionResult.Success(savedAssessment), + ) + } + + fun getAssessment(assessmentId: UUID): AuthorisableActionResult { + val assessmentEntity = assessmentRepository.findByIdOrNull(assessmentId) + ?: return AuthorisableActionResult.NotFound() + + return AuthorisableActionResult.Success(assessmentEntity) + } +} \ No newline at end of file diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailUserAccessService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailUserAccessService.kt new file mode 100644 index 0000000000..caedfa1486 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailUserAccessService.kt @@ -0,0 +1,26 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2bail + +import org.springframework.stereotype.Service +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.NomisUserEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity + +@Service("Cas2BailUserAccessService") +class Cas2BailUserAccessService { + fun userCanViewCas2BailApplication(user: NomisUserEntity, application: Cas2BailApplicationEntity): Boolean { + return if (user.id == application.createdByUser.id) { + true + } else if (application.submittedAt == null) { + false + } else { + offenderIsFromSamePrisonAsUser(application.referringPrisonCode, user.activeCaseloadId) + } + } + + fun offenderIsFromSamePrisonAsUser(referringPrisonCode: String?, activeCaseloadId: String?): Boolean { + return if (referringPrisonCode !== null && activeCaseloadId !== null) { + activeCaseloadId == referringPrisonCode + } else { + false + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailApplicationsTransformer.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailApplicationsTransformer.kt new file mode 100644 index 0000000000..6cb05bd217 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailApplicationsTransformer.kt @@ -0,0 +1,84 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas2bail + +import com.fasterxml.jackson.databind.ObjectMapper +import org.springframework.stereotype.Component +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.ApplicationStatus +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas2Application +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationSummaryEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationSummaryEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.PersonInfoResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.NomisUserTransformer +import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.PersonTransformer +import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas2.AssessmentsTransformer +import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas2.StatusUpdateTransformer +import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas2.TimelineEventsTransformer +import java.util.* + +@Component("Cas2BailApplicationsTransformer") +class Cas2BailApplicationsTransformer( + private val objectMapper: ObjectMapper, + private val personTransformer: PersonTransformer, + private val nomisUserTransformer: NomisUserTransformer, + private val statusUpdateTransformer: Cas2BailStatusUpdateTransformer, + private val timelineEventsTransformer: Cas2BailTimelineEventsTransformer, + private val assessmentsTransformer: Cas2BailAssessmentsTransformer, +) { + + + fun transformJpaToApi(jpa: Cas2BailApplicationEntity, personInfo: PersonInfoResult): Cas2Application { + return Cas2Application( + id = jpa.id, + person = personTransformer.transformModelToPersonApi(personInfo), + createdBy = nomisUserTransformer.transformJpaToApi(jpa.createdByUser), + schemaVersion = jpa.schemaVersion.id, + outdatedSchema = !jpa.schemaUpToDate, + createdAt = jpa.createdAt.toInstant(), + submittedAt = jpa.submittedAt?.toInstant(), + data = if (jpa.data != null) objectMapper.readTree(jpa.data) else null, + document = if (jpa.document != null) objectMapper.readTree(jpa.document) else null, + status = getStatus(jpa), + type = "CAS2", + telephoneNumber = jpa.telephoneNumber, + assessment = if (jpa.assessment != null) assessmentsTransformer.transformJpaToApiRepresentation(jpa.assessment!!) else null, + timelineEvents = timelineEventsTransformer.transformApplicationToTimelineEvents(jpa), + ) + } + + fun transformJpaSummaryToSummary( + jpaSummary: Cas2BailApplicationSummaryEntity, + personName: String, + ): uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas2ApplicationSummary { + return uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model + .Cas2ApplicationSummary( + id = jpaSummary.id, + createdByUserId = UUID.fromString(jpaSummary.userId), + createdByUserName = jpaSummary.userName, + createdAt = jpaSummary.createdAt.toInstant(), + submittedAt = jpaSummary.submittedAt?.toInstant(), + status = getStatusFromSummary(jpaSummary), + latestStatusUpdate = statusUpdateTransformer.transformJpaSummaryToLatestStatusUpdateApi(jpaSummary), + type = "CAS2", + hdcEligibilityDate = jpaSummary.hdcEligibilityDate, + crn = jpaSummary.crn, + nomsNumber = jpaSummary.nomsNumber, + personName = personName, + ) + } + + private fun getStatus(entity: Cas2BailApplicationEntity): ApplicationStatus { + if (entity.submittedAt !== null) { + return ApplicationStatus.submitted + } + + return ApplicationStatus.inProgress + } + + private fun getStatusFromSummary(summary: Cas2BailApplicationSummaryEntity): ApplicationStatus { + return when { + summary.submittedAt != null -> ApplicationStatus.submitted + else -> ApplicationStatus.inProgress + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailAssessmentsTransformer.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailAssessmentsTransformer.kt new file mode 100644 index 0000000000..dd6efe0b2b --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailAssessmentsTransformer.kt @@ -0,0 +1,23 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas2bail + +import org.springframework.stereotype.Component +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas2Assessment +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2AssessmentEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailAssessmentEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas2.StatusUpdateTransformer + +@Component("Cas2BailAssessmentsTransformer") +class Cas2BailAssessmentsTransformer ( + private val statusUpdateTransformer: Cas2BailStatusUpdateTransformer +) { + fun transformJpaToApiRepresentation( + jpaAssessment: Cas2BailAssessmentEntity, + ): Cas2Assessment { + return Cas2Assessment( + jpaAssessment.id, + jpaAssessment.nacroReferralId, + jpaAssessment.assessorName, + jpaAssessment.statusUpdates?.map { update -> statusUpdateTransformer.transformJpaToApi(update) }, + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailStatusUpdateTransformer.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailStatusUpdateTransformer.kt new file mode 100644 index 0000000000..12e79e36ed --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailStatusUpdateTransformer.kt @@ -0,0 +1,53 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas2bail + +import org.springframework.stereotype.Component +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas2StatusUpdate +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas2StatusUpdateDetail +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.LatestCas2StatusUpdate +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationSummaryEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2StatusUpdateDetailEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2StatusUpdateEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationSummaryEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailStatusUpdateDetailEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailStatusUpdateEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.ExternalUserTransformer +import java.util.* + +@Component("Cas2BailStatusUpdateTransformer") +class Cas2BailStatusUpdateTransformer( + private val externalUserTransformer: ExternalUserTransformer, +) { + + fun transformJpaToApi( + jpa: Cas2BailStatusUpdateEntity, + ): Cas2StatusUpdate { + return Cas2StatusUpdate( + id = jpa.id, + name = jpa.status().name, + label = jpa.label, + description = jpa.description, + updatedBy = externalUserTransformer.transformJpaToApi(jpa.assessor), + updatedAt = jpa.createdAt?.toInstant(), + statusUpdateDetails = jpa.statusUpdateDetails?.map { detail -> transformStatusUpdateDetailsJpaToApi(detail) }, + ) + } + + fun transformStatusUpdateDetailsJpaToApi(jpa: Cas2BailStatusUpdateDetailEntity): Cas2StatusUpdateDetail { + return Cas2StatusUpdateDetail( + id = jpa.id, + name = jpa.statusDetail(jpa.statusUpdate.statusId, jpa.statusDetailId).name, + label = jpa.label, + ) + } + + fun transformJpaSummaryToLatestStatusUpdateApi(jpa: Cas2BailApplicationSummaryEntity): LatestCas2StatusUpdate? { + if (jpa.latestStatusUpdateStatusId !== null) { + return LatestCas2StatusUpdate( + statusId = UUID.fromString(jpa.latestStatusUpdateStatusId!!), + label = jpa.latestStatusUpdateLabel!!, + ) + } else { + return null + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailTimelineEventsTransformer.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailTimelineEventsTransformer.kt new file mode 100644 index 0000000000..30bd9da77e --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailTimelineEventsTransformer.kt @@ -0,0 +1,59 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas2bail + +import org.springframework.stereotype.Component +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas2TimelineEvent +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.TimelineEventType +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity + +@Component("Cas2BailTimelineEventsTransformer") +class Cas2BailTimelineEventsTransformer { + + fun transformApplicationToTimelineEvents(jpa: Cas2BailApplicationEntity): List { + val timelineEvents: MutableList = mutableListOf() + + addSubmittedEvent(jpa, timelineEvents) + + addStatusUpdateEvents(jpa, timelineEvents) + + addNoteEvents(jpa, timelineEvents) + + return timelineEvents.sortedByDescending { it.occurredAt } + } + + private fun addNoteEvents(jpa: Cas2BailApplicationEntity, timelineEvents: MutableList) { + jpa.notes?.forEach { + timelineEvents += Cas2TimelineEvent( + type = TimelineEventType.cas2Note, + occurredAt = it.createdAt.toInstant(), + label = "Note", + createdByName = it.getUser().name, + body = it.body, + ) + } + } + + private fun addStatusUpdateEvents(jpa: Cas2BailApplicationEntity, timelineEvents: MutableList) { + jpa.statusUpdates?.forEach { + timelineEvents += Cas2TimelineEvent( + type = TimelineEventType.cas2StatusUpdate, + occurredAt = it.createdAt.toInstant(), + label = it.label, + createdByName = it.assessor.name, + body = it.statusUpdateDetails?.joinToString { detail -> detail.label }, + ) + } + } + + private fun addSubmittedEvent(jpa: Cas2BailApplicationEntity, timelineEvents: MutableList) { + if (jpa.submittedAt !== null) { + val submittedAtEvent = Cas2TimelineEvent( + type = TimelineEventType.cas2ApplicationSubmitted, + occurredAt = jpa.submittedAt?.toInstant()!!, + label = "Application submitted", + createdByName = jpa.createdByUser.name, + ) + timelineEvents += submittedAtEvent + } + } +} \ No newline at end of file diff --git a/src/main/resources/static/_shared.yml b/src/main/resources/static/_shared.yml index 27c2b5358e..add9268d6c 100644 --- a/src/main/resources/static/_shared.yml +++ b/src/main/resources/static/_shared.yml @@ -1943,6 +1943,49 @@ components: - personName - crn - nomsNumber + Cas2BailApplicationSummary: + type: object + properties: + type: + type: string + id: + type: string + format: uuid + createdAt: + type: string + format: date-time + submittedAt: + type: string + format: date-time + createdByUserId: + type: string + format: uuid + createdByUserName: + type: string + status: + $ref: '#/components/schemas/ApplicationStatus' + latestStatusUpdate: + $ref: '#/components/schemas/LatestCas2StatusUpdate' + risks: + $ref: '#/components/schemas/PersonRisks' + hdcEligibilityDate: + type: string + format: date + personName: + type: string + crn: + type: string + nomsNumber: + type: string + required: + - type + - id + - createdAt + - createdByUserId + - status + - personName + - crn + - nomsNumber NewCas2ApplicationNote: type: object properties: diff --git a/src/main/resources/static/codegen/built-api-spec.yml b/src/main/resources/static/codegen/built-api-spec.yml index 41b4ed63e8..2f376c5eb6 100644 --- a/src/main/resources/static/codegen/built-api-spec.yml +++ b/src/main/resources/static/codegen/built-api-spec.yml @@ -6274,6 +6274,49 @@ components: - personName - crn - nomsNumber + Cas2BailApplicationSummary: + type: object + properties: + type: + type: string + id: + type: string + format: uuid + createdAt: + type: string + format: date-time + submittedAt: + type: string + format: date-time + createdByUserId: + type: string + format: uuid + createdByUserName: + type: string + status: + $ref: '#/components/schemas/ApplicationStatus' + latestStatusUpdate: + $ref: '#/components/schemas/LatestCas2StatusUpdate' + risks: + $ref: '#/components/schemas/PersonRisks' + hdcEligibilityDate: + type: string + format: date + personName: + type: string + crn: + type: string + nomsNumber: + type: string + required: + - type + - id + - createdAt + - createdByUserId + - status + - personName + - crn + - nomsNumber NewCas2ApplicationNote: type: object properties: diff --git a/src/main/resources/static/codegen/built-cas1-api-spec.yml b/src/main/resources/static/codegen/built-cas1-api-spec.yml index 163c572551..ba124684e1 100644 --- a/src/main/resources/static/codegen/built-cas1-api-spec.yml +++ b/src/main/resources/static/codegen/built-cas1-api-spec.yml @@ -3055,6 +3055,49 @@ components: - personName - crn - nomsNumber + Cas2BailApplicationSummary: + type: object + properties: + type: + type: string + id: + type: string + format: uuid + createdAt: + type: string + format: date-time + submittedAt: + type: string + format: date-time + createdByUserId: + type: string + format: uuid + createdByUserName: + type: string + status: + $ref: '#/components/schemas/ApplicationStatus' + latestStatusUpdate: + $ref: '#/components/schemas/LatestCas2StatusUpdate' + risks: + $ref: '#/components/schemas/PersonRisks' + hdcEligibilityDate: + type: string + format: date + personName: + type: string + crn: + type: string + nomsNumber: + type: string + required: + - type + - id + - createdAt + - createdByUserId + - status + - personName + - crn + - nomsNumber NewCas2ApplicationNote: type: object properties: diff --git a/src/main/resources/static/codegen/built-cas2-api-spec.yml b/src/main/resources/static/codegen/built-cas2-api-spec.yml index 8e287c46dc..c6427c7ca0 100644 --- a/src/main/resources/static/codegen/built-cas2-api-spec.yml +++ b/src/main/resources/static/codegen/built-cas2-api-spec.yml @@ -2534,6 +2534,49 @@ components: - personName - crn - nomsNumber + Cas2BailApplicationSummary: + type: object + properties: + type: + type: string + id: + type: string + format: uuid + createdAt: + type: string + format: date-time + submittedAt: + type: string + format: date-time + createdByUserId: + type: string + format: uuid + createdByUserName: + type: string + status: + $ref: '#/components/schemas/ApplicationStatus' + latestStatusUpdate: + $ref: '#/components/schemas/LatestCas2StatusUpdate' + risks: + $ref: '#/components/schemas/PersonRisks' + hdcEligibilityDate: + type: string + format: date + personName: + type: string + crn: + type: string + nomsNumber: + type: string + required: + - type + - id + - createdAt + - createdByUserId + - status + - personName + - crn + - nomsNumber NewCas2ApplicationNote: type: object properties: diff --git a/src/main/resources/static/codegen/built-cas2bail-api-spec.yml b/src/main/resources/static/codegen/built-cas2bail-api-spec.yml index 0629bbdee4..b3ff5ea48e 100644 --- a/src/main/resources/static/codegen/built-cas2bail-api-spec.yml +++ b/src/main/resources/static/codegen/built-cas2bail-api-spec.yml @@ -2534,6 +2534,49 @@ components: - personName - crn - nomsNumber + Cas2BailApplicationSummary: + type: object + properties: + type: + type: string + id: + type: string + format: uuid + createdAt: + type: string + format: date-time + submittedAt: + type: string + format: date-time + createdByUserId: + type: string + format: uuid + createdByUserName: + type: string + status: + $ref: '#/components/schemas/ApplicationStatus' + latestStatusUpdate: + $ref: '#/components/schemas/LatestCas2StatusUpdate' + risks: + $ref: '#/components/schemas/PersonRisks' + hdcEligibilityDate: + type: string + format: date + personName: + type: string + crn: + type: string + nomsNumber: + type: string + required: + - type + - id + - createdAt + - createdByUserId + - status + - personName + - crn + - nomsNumber NewCas2ApplicationNote: type: object properties: diff --git a/src/main/resources/static/codegen/built-cas3-api-spec.yml b/src/main/resources/static/codegen/built-cas3-api-spec.yml index a00b8f38b5..af4d2217fe 100644 --- a/src/main/resources/static/codegen/built-cas3-api-spec.yml +++ b/src/main/resources/static/codegen/built-cas3-api-spec.yml @@ -2042,6 +2042,49 @@ components: - personName - crn - nomsNumber + Cas2BailApplicationSummary: + type: object + properties: + type: + type: string + id: + type: string + format: uuid + createdAt: + type: string + format: date-time + submittedAt: + type: string + format: date-time + createdByUserId: + type: string + format: uuid + createdByUserName: + type: string + status: + $ref: '#/components/schemas/ApplicationStatus' + latestStatusUpdate: + $ref: '#/components/schemas/LatestCas2StatusUpdate' + risks: + $ref: '#/components/schemas/PersonRisks' + hdcEligibilityDate: + type: string + format: date + personName: + type: string + crn: + type: string + nomsNumber: + type: string + required: + - type + - id + - createdAt + - createdByUserId + - status + - personName + - crn + - nomsNumber NewCas2ApplicationNote: type: object properties: From 5f74c574cf3c30093b6eadcd213fd5f2ea0030cf Mon Sep 17 00:00:00 2001 From: Toby Batch Date: Thu, 28 Nov 2024 16:57:52 +0000 Subject: [PATCH 24/37] feat: added migrations --- ...roller_and_linked_services_in_cas2bail.sql | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 src/main/resources/db/migration/all/20241128163133__application_controller_and_linked_services_in_cas2bail.sql diff --git a/src/main/resources/db/migration/all/20241128163133__application_controller_and_linked_services_in_cas2bail.sql b/src/main/resources/db/migration/all/20241128163133__application_controller_and_linked_services_in_cas2bail.sql new file mode 100644 index 0000000000..75dfb4fb0f --- /dev/null +++ b/src/main/resources/db/migration/all/20241128163133__application_controller_and_linked_services_in_cas2bail.sql @@ -0,0 +1,128 @@ + +CREATE TABLE cas_2_bail_application_json_schemas +( + json_schema_id UUID NOT NULL, + CONSTRAINT pk_cas_2_bail_application_json_schemas PRIMARY KEY (json_schema_id) +); + +CREATE TABLE cas_2_bail_application_live_summary +( + id UUID NOT NULL, + crn VARCHAR(255), + noms_number VARCHAR(255), + created_by_user_id VARCHAR(255), + name VARCHAR(255), + created_at TIMESTAMP WITHOUT TIME ZONE, + submitted_at TIMESTAMP WITHOUT TIME ZONE, + abandoned_at TIMESTAMP WITHOUT TIME ZONE, + hdc_eligibility_date date, + label VARCHAR(255), + status_id VARCHAR(255), + referring_prison_code VARCHAR(255), + CONSTRAINT pk_cas_2_bail_application_live_summary PRIMARY KEY (id) +); + +CREATE TABLE cas_2_bail_application_notes +( + id UUID NOT NULL, + application_id UUID, + created_at TIMESTAMP WITHOUT TIME ZONE, + body VARCHAR(255), + assessment_id UUID, + created_by_nomis_user_id UUID, + created_by_external_user_id UUID, + CONSTRAINT pk_cas_2_bail_application_notes PRIMARY KEY (id) +); + +CREATE TABLE cas_2_bail_applications +( + id UUID NOT NULL, + crn VARCHAR(255), + created_by_user_id UUID, + data VARCHAR(255), + document VARCHAR(255), + schema_version UUID, + created_at TIMESTAMP WITHOUT TIME ZONE, + submitted_at TIMESTAMP WITHOUT TIME ZONE, + abandoned_at TIMESTAMP WITHOUT TIME ZONE, + assessment_id UUID, + noms_number VARCHAR(255), + referring_prison_code VARCHAR(255), + preferred_areas VARCHAR(255), + hdc_eligibility_date date, + conditional_release_date date, + telephone_number VARCHAR(255), + CONSTRAINT pk_cas_2_bail_applications PRIMARY KEY (id) +); + +CREATE TABLE cas_2_bail_assessments +( + id UUID NOT NULL, + application_id UUID, + created_at TIMESTAMP WITHOUT TIME ZONE, + nacro_referral_id VARCHAR(255), + assessor_name VARCHAR(255), + CONSTRAINT pk_cas_2_bail_assessments PRIMARY KEY (id) +); + +CREATE TABLE cas_2_bail_status_update_details +( + id UUID NOT NULL, + status_detail_id UUID, + label VARCHAR(255), + status_update_id UUID, + created_at TIMESTAMP WITHOUT TIME ZONE, + CONSTRAINT pk_cas_2_bail_status_update_details PRIMARY KEY (id) +); + +CREATE TABLE cas_2_bail_status_updates +( + id UUID NOT NULL, + status_id UUID, + description VARCHAR(255), + label VARCHAR(255), + assessor_id UUID, + application_id UUID, + assessment_id UUID, + created_at TIMESTAMP WITHOUT TIME ZONE, + CONSTRAINT pk_cas_2_bail_status_updates PRIMARY KEY (id) +); + +ALTER TABLE cas_2_bail_applications + ADD CONSTRAINT FK_CAS_2_BAIL_APPLICATIONS_ON_ASSESSMENT FOREIGN KEY (assessment_id) REFERENCES cas_2_bail_assessments (id); + +ALTER TABLE cas_2_bail_applications + ADD CONSTRAINT FK_CAS_2_BAIL_APPLICATIONS_ON_CREATED_BY_USER FOREIGN KEY (created_by_user_id) REFERENCES nomis_users (id); + +ALTER TABLE cas_2_bail_applications + ADD CONSTRAINT FK_CAS_2_BAIL_APPLICATIONS_ON_SCHEMA_VERSION FOREIGN KEY (schema_version) REFERENCES json_schemas (id); + +ALTER TABLE cas_2_bail_application_json_schemas + ADD CONSTRAINT FK_CAS_2_BAIL_APPLICATION_JSON_SCHEMAS_ON_JSON_SCHEMA FOREIGN KEY (json_schema_id) REFERENCES json_schemas (id); + +ALTER TABLE cas_2_bail_application_notes + ADD CONSTRAINT FK_CAS_2_BAIL_APPLICATION_NOTES_ON_APPLICATION FOREIGN KEY (application_id) REFERENCES cas_2_bail_applications (id); + +ALTER TABLE cas_2_bail_application_notes + ADD CONSTRAINT FK_CAS_2_BAIL_APPLICATION_NOTES_ON_ASSESSMENT FOREIGN KEY (assessment_id) REFERENCES cas_2_bail_assessments (id); + +ALTER TABLE cas_2_bail_application_notes + ADD CONSTRAINT FK_CAS_2_BAIL_APPLICATION_NOTES_ON_CREATED_BY_EXTERNAL_USER FOREIGN KEY (created_by_external_user_id) REFERENCES external_users (id); + +ALTER TABLE cas_2_bail_application_notes + ADD CONSTRAINT FK_CAS_2_BAIL_APPLICATION_NOTES_ON_CREATED_BY_NOMIS_USER FOREIGN KEY (created_by_nomis_user_id) REFERENCES nomis_users (id); + +ALTER TABLE cas_2_bail_assessments + ADD CONSTRAINT FK_CAS_2_BAIL_ASSESSMENTS_ON_APPLICATION FOREIGN KEY (application_id) REFERENCES cas_2_bail_applications (id); + +ALTER TABLE cas_2_bail_status_updates + ADD CONSTRAINT FK_CAS_2_BAIL_STATUS_UPDATES_ON_APPLICATION FOREIGN KEY (application_id) REFERENCES cas_2_bail_applications (id); + +ALTER TABLE cas_2_bail_status_updates + ADD CONSTRAINT FK_CAS_2_BAIL_STATUS_UPDATES_ON_ASSESSMENT FOREIGN KEY (assessment_id) REFERENCES cas_2_bail_assessments (id); + +ALTER TABLE cas_2_bail_status_updates + ADD CONSTRAINT FK_CAS_2_BAIL_STATUS_UPDATES_ON_ASSESSOR FOREIGN KEY (assessor_id) REFERENCES external_users (id); + +ALTER TABLE cas_2_bail_status_update_details + ADD CONSTRAINT FK_CAS_2_BAIL_STATUS_UPDATE_DETAILS_ON_STATUS_UPDATE FOREIGN KEY (status_update_id) REFERENCES cas_2_bail_status_updates (id); From 947158b5eb7b8dd0b6262fe244fd965dac49559c Mon Sep 17 00:00:00 2001 From: garethCAS2 Date: Fri, 29 Nov 2024 13:25:46 +0000 Subject: [PATCH 25/37] started working through the tests --- ...uth2ResourceServerSecurityConfiguration.kt | 11 + .../cas2bail/Cas2BailApplicationController.kt | 20 +- .../cas2bail/Cas2BailAssessmentsController.kt | 116 ++ .../Cas2BailApplicationNoteService.kt | 171 ++ .../cas2bail/Cas2BailApplicationService.kt | 88 +- .../cas2bail/Cas2BailAssessmentService.kt | 18 +- .../cas2bail/Cas2BailStatusUpdateService.kt | 182 ++ .../cas2/AssessmentsTransformer.kt | 1 + src/main/resources/application-local.yml | 7 +- ...2BailApplicationJsonSchemaEntityFactory.kt | 48 + .../Cas2BailApplicationEntityFactory.kt | 159 ++ .../Cas2BailStatusUpdateEntityFactory.kt | 81 + .../integration/IntegrationTestBase.kt | 237 +-- .../cas2bail/Cas2BailApplicationTest.kt | 1699 +++++++++++++++++ .../repository/ApplicationTestRepository.kt | 4 + .../Cas2BailStatusUpdateTestRepository.kt | 10 + .../repository/JsonSchemaTestRepository.kt | 10 +- .../Cas2BailApplicationServiceTest.kt | 1109 +++++++++++ .../cas2bail/Cas2BailAssessmentServiceTest.kt | 139 ++ .../cas2bail/Cas2BailUserAccessServiceTest.kt | 140 ++ 20 files changed, 3965 insertions(+), 285 deletions(-) create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailAssessmentsController.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationNoteService.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailStatusUpdateService.kt create mode 100644 src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/Cas2BailApplicationJsonSchemaEntityFactory.kt create mode 100644 src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/cas2bail/Cas2BailApplicationEntityFactory.kt create mode 100644 src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/cas2bail/Cas2BailStatusUpdateEntityFactory.kt create mode 100644 src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationTest.kt create mode 100644 src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/repository/Cas2BailStatusUpdateTestRepository.kt create mode 100644 src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailApplicationServiceTest.kt create mode 100644 src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailAssessmentServiceTest.kt create mode 100644 src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailUserAccessServiceTest.kt diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/config/OAuth2ResourceServerSecurityConfiguration.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/config/OAuth2ResourceServerSecurityConfiguration.kt index a32352601f..f8f5facd26 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/config/OAuth2ResourceServerSecurityConfiguration.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/config/OAuth2ResourceServerSecurityConfiguration.kt @@ -74,6 +74,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, "/cas2bail/assessments/**", hasRole("CAS2_ASSESSOR")) + authorize(HttpMethod.GET, "/cas2bail/assessments/**", hasAnyRole("CAS2_ASSESSOR", "CAS2_ADMIN")) + authorize(HttpMethod.POST, "/cas2bail/assessments/*/status-updates", hasRole("CAS2_ASSESSOR")) + authorize(HttpMethod.POST, "/cas2bail/assessments/*/notes", hasAnyRole("LICENCE_CA", "POM", "CAS2_ASSESSOR")) + authorize(HttpMethod.GET, "/cas2bail/submissions/**", hasAnyRole("CAS2_ASSESSOR", "CAS2_ADMIN")) + authorize(HttpMethod.POST, "/cas2bail/submissions/*/status-updates", hasRole("CAS2_ASSESSOR")) + authorize(HttpMethod.GET, "/cas2bail/reference-data/**", hasAnyRole("CAS2_ASSESSOR", "POM")) + authorize(HttpMethod.GET, "/cas2bail/reports/**", hasRole("CAS2_MI")) + authorize("/cas2bail/**", 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")) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailApplicationController.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailApplicationController.kt index ae01b8fd91..de7adce706 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailApplicationController.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailApplicationController.kt @@ -31,8 +31,8 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas2Applicatio ) class Cas2BailApplicationController( private val httpAuthService: HttpAuthService, - private val applicationService: Cas2BailApplicationService, - private val applicationsTransformer: Cas2BailApplicationsTransformer, + private val cas2BailApplicationService: Cas2BailApplicationService, + private val cas2BailApplicationsTransformer: Cas2BailApplicationsTransformer, private val objectMapper: ObjectMapper, private val offenderService: OffenderService, private val userService: NomisUserService, @@ -49,7 +49,7 @@ class Cas2BailApplicationController( val pageCriteria = PageCriteria("createdAt", SortDirection.desc, page) - val (applications, metadata) = applicationService.getCas2BailApplications(prisonCode, isSubmitted, user, pageCriteria) + val (applications, metadata) = cas2BailApplicationService.getCas2BailApplications(prisonCode, isSubmitted, user, pageCriteria) return ResponseEntity.ok().headers( metadata?.toHeaders(), @@ -60,7 +60,7 @@ class Cas2BailApplicationController( val user = userService.getUserForRequest() val application = when ( - val applicationResult = applicationService + val applicationResult = cas2BailApplicationService .getCas2BailApplicationForUser( applicationId, user @@ -85,7 +85,7 @@ class Cas2BailApplicationController( val personInfo = offenderService.getFullInfoForPersonOrThrow(body.crn) - val applicationResult = applicationService.createCas2BailApplication( + val applicationResult = cas2BailApplicationService.createCas2BailApplication( body.crn, user, nomisPrincipal.token.tokenValue, @@ -100,7 +100,7 @@ class Cas2BailApplicationController( return ResponseEntity .created(URI.create("/cas2/applications/${application.id}")) - .body(applicationsTransformer.transformJpaToApi(application, personInfo)) + .body(cas2BailApplicationsTransformer.transformJpaToApi(application, personInfo)) } @Transactional @@ -112,7 +112,7 @@ class Cas2BailApplicationController( val serializedData = objectMapper.writeValueAsString(body.data) - val applicationResult = applicationService.updateCas2BailApplication( + val applicationResult = cas2BailApplicationService.updateCas2BailApplication( applicationId = applicationId, data = serializedData, @@ -139,7 +139,7 @@ class Cas2BailApplicationController( override fun applicationsApplicationIdAbandonPut(applicationId: UUID): ResponseEntity { val user = userService.getUserForRequest() - val validationResult = when (val applicationResult = applicationService.abandonCas2BailApplication(applicationId, user)) { + val validationResult = when (val applicationResult = cas2BailApplicationService.abandonCas2BailApplication(applicationId, user)) { is AuthorisableActionResult.NotFound -> throw NotFoundProblem(applicationId, "Application") is AuthorisableActionResult.Unauthorised -> throw ForbiddenProblem() is AuthorisableActionResult.Success -> applicationResult.entity @@ -161,7 +161,7 @@ class Cas2BailApplicationController( val personNamesMap = offenderService.getMapOfPersonNamesAndCrns(crns) return applicationSummaries.map { application -> - applicationsTransformer.transformJpaSummaryToSummary(application, personNamesMap[application.crn]!!) + cas2BailApplicationsTransformer.transformJpaSummaryToSummary(application, personNamesMap[application.crn]!!) } } @@ -170,6 +170,6 @@ class Cas2BailApplicationController( ): Application { val personInfo = offenderService.getFullInfoForPersonOrThrow(application.crn) - return applicationsTransformer.transformJpaToApi(application, personInfo) + return cas2BailApplicationsTransformer.transformJpaToApi(application, personInfo) } } \ No newline at end of file diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailAssessmentsController.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailAssessmentsController.kt new file mode 100644 index 0000000000..54b645aafe --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailAssessmentsController.kt @@ -0,0 +1,116 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.controller.cas2bail + +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.stereotype.Service +import org.zalando.problem.AbstractThrowableProblem +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.cas2bail.AssessmentsCas2bailDelegate +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationNoteEntity +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.problem.NotFoundProblem +import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.CasResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.ExternalUserService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2bail.Cas2BailApplicationNoteService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2bail.Cas2BailAssessmentService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2bail.Cas2BailStatusUpdateService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas2.ApplicationNotesTransformer +import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas2bail.Cas2BailAssessmentsTransformer +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.extractEntityFromCasResult +import java.net.URI +import java.util.* + +@Service("Cas2BailAssessmentsController") +class Cas2BailAssessmentsController ( + private val cas2BailAssessmentService: Cas2BailAssessmentService, + private val cas2BailApplicationNoteService: Cas2BailApplicationNoteService, + private val cas2BailAssessmentsTransformer: Cas2BailAssessmentsTransformer, + private val applicationNotesTransformer: ApplicationNotesTransformer, + private val cas2BailStatusUpdateService: Cas2BailStatusUpdateService, + private val externalUserService: ExternalUserService, +) : AssessmentsCas2bailDelegate { + + override fun assessmentsAssessmentIdGet(assessmentId: UUID): ResponseEntity { + val assessment = when ( + val assessmentResult = cas2BailAssessmentService.getAssessment(assessmentId) + ) { + is CasResult.NotFound -> throw NotFoundProblem(assessmentId, "Cas2BailAssessment") + is CasResult.Unauthorised -> throw ForbiddenProblem() + is CasResult.Success -> assessmentResult + is CasResult.ConflictError<*> -> throw ConflictProblem(assessmentId, "Cas2BailAssessment conflict by assessmentId") + is CasResult.FieldValidationError<*> -> CasResult.FieldValidationError(mapOf("$.reason" to "doesNotExist")) + is CasResult.GeneralValidationError<*> -> CasResult.GeneralValidationError("General Validation Error") + } + + val cas2BailAssessmentEntity = extractEntityFromCasResult(assessment) + return ResponseEntity.ok(cas2BailAssessmentsTransformer.transformJpaToApiRepresentation(cas2BailAssessmentEntity)) + } + + override fun assessmentsAssessmentIdPut( + assessmentId: UUID, + updateCas2Assessment: UpdateCas2Assessment, + ): ResponseEntity { + val assessmentResult = cas2BailAssessmentService.updateAssessment(assessmentId, updateCas2Assessment) + when (assessmentResult) { + is CasResult.NotFound -> throw NotFoundProblem(assessmentId, "Assessment") + is CasResult.Unauthorised -> throw ForbiddenProblem() + is CasResult.Success -> assessmentResult + is CasResult.ConflictError<*> -> throw ConflictProblem(assessmentId, "Cas2BailAssessment conflict by assessmentId") + is CasResult.FieldValidationError<*> -> CasResult.FieldValidationError(mapOf("$.reason" to "doesNotExist")) + is CasResult.GeneralValidationError<*> -> CasResult.GeneralValidationError("General Validation Error") + } + + val cas2BailAssessmentEntity = extractEntityFromCasResult(assessmentResult) + return ResponseEntity.ok( + cas2BailAssessmentsTransformer.transformJpaToApiRepresentation(cas2BailAssessmentEntity), + ) + } + + override fun assessmentsAssessmentIdStatusUpdatesPost( + assessmentId: UUID, + cas2AssessmentStatusUpdate: Cas2AssessmentStatusUpdate, + ): ResponseEntity { + val result = cas2BailStatusUpdateService.createForAssessment( + assessmentId = assessmentId, + statusUpdate = cas2AssessmentStatusUpdate, + assessor = externalUserService.getUserForRequest(), + ) + + processAuthorisationFor(result) + .run { processValidation(result) } + + return ResponseEntity(HttpStatus.CREATED) + } + + override fun assessmentsAssessmentIdNotesPost( + assessmentId: UUID, + body: NewCas2ApplicationNote, + ): ResponseEntity { + val noteResult = cas2BailApplicationNoteService.createAssessmentNote(assessmentId, body) + + val validationResult = processAuthorisationFor( noteResult) as CasResult + + val note = processValidation(validationResult) as Cas2ApplicationNoteEntity + + return ResponseEntity + .created(URI.create("/cas2/assessments/$assessmentId/notes/${note.id}")) + .body( + applicationNotesTransformer.transformJpaToApi(note), + ) + } + + private fun processAuthorisationFor( + result: CasResult + ): Any? { + + return extractEntityFromCasResult(result) + + } + + + private fun processValidation(casResult: CasResult): Any { + return extractEntityFromCasResult(casResult) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationNoteService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationNoteService.kt new file mode 100644 index 0000000000..b81a62225f --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationNoteService.kt @@ -0,0 +1,171 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2bail + +import io.sentry.Sentry +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Value +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.NewCas2ApplicationNote +import uk.gov.justice.digital.hmpps.approvedpremisesapi.config.NotifyConfig +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.AuthorisableActionResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.CasResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.ValidatableActionResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.EmailNotificationService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.ExternalUserService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.HttpAuthService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.NomisUserService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2.UserAccessService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.toCas2UiFormat +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.toCas2UiFormattedHourOfDay +import java.time.OffsetDateTime +import java.util.* + +@Service("Cas2BailApplicationNoteService") +class Cas2BailApplicationNoteService( + private val cas2BailApplicationRepository: Cas2BailApplicationRepository, + private val cas2BailAssessmentRepository: Cas2BailAssessmentRepository, + private val cas2BailApplicationNoteRepository: Cas2BailApplicationNoteRepository, + private val userService: NomisUserService, + private val externalUserService: ExternalUserService, + private val httpAuthService: HttpAuthService, + private val emailNotificationService: EmailNotificationService, + private val userAccessService: UserAccessService, + private val notifyConfig: NotifyConfig, + @Value("\${url-templates.frontend.cas2.application-overview}") private val applicationUrlTemplate: String, + @Value("\${url-templates.frontend.cas2.submitted-application-overview}") private val assessmentUrlTemplate: String, +) { + + private val log = LoggerFactory.getLogger(this::class.java) + + @Suppress("ReturnCount") + fun createAssessmentNote(assessmentId: UUID, note: NewCas2ApplicationNote): CasResult { + val assessment = cas2BailAssessmentRepository.findByIdOrNull(assessmentId) + ?: return CasResult.NotFound() + + val application = cas2BailApplicationRepository.findByIdOrNull(assessment.application.id) + ?: return CasResult.NotFound() + + if (application.submittedAt == null) { + return CasResult.GeneralValidationError("This application has not been submitted") + } + + val isExternalUser = httpAuthService.getCas2AuthenticatedPrincipalOrThrow().isExternalUser() + val user = getCas2User(isExternalUser) + + if (!isExternalUser && !nomisUserCanAddNote(application, user as NomisUserEntity)) { + return CasResult.Unauthorised() + } + + val savedNote = saveNote(application, assessment, note.note, user) + + sendEmail(isExternalUser, application, savedNote) + + return CasResult.Success( savedNote ) + } + + private fun sendEmail( + isExternalUser: Boolean, + application: Cas2BailApplicationEntity, + savedNote: Cas2BailApplicationNoteEntity, + ) { + if (isExternalUser) { + sendEmailToReferrer(application, savedNote) + } else { + sendEmailToAssessors(application, savedNote) + } + } + + private fun sendEmailToReferrer( + application: Cas2BailApplicationEntity, + savedNote: Cas2BailApplicationNoteEntity) { + if (application.createdByUser.email != null) { + emailNotificationService.sendCas2Email( + recipientEmailAddress = application.createdByUser.email!!, + templateId = notifyConfig.templates.cas2NoteAddedForReferrer, + personalisation = mapOf( + "dateNoteAdded" to savedNote.createdAt.toLocalDate().toCas2UiFormat(), + "timeNoteAdded" to savedNote.createdAt.toCas2UiFormattedHourOfDay(), + "nomsNumber" to application.nomsNumber, + "applicationType" to "Home Detention Curfew (HDC)", + "applicationUrl" to applicationUrlTemplate.replace("#id", application.id.toString()), + ), + ) + } else { + log.error("Email not found for User ${application.createdByUser.id}. Unable to send email for Note ${savedNote.id} on Application ${application.id}") + Sentry.captureMessage("Email not found for User ${application.createdByUser.id}. Unable to send email for Note ${savedNote.id} on Application ${application.id}") + } + } + + private fun sendEmailToAssessors( + application: Cas2BailApplicationEntity, + savedNote: Cas2BailApplicationNoteEntity, + ) { + emailNotificationService.sendCas2Email( + recipientEmailAddress = notifyConfig.emailAddresses.cas2Assessors, + templateId = notifyConfig.templates.cas2NoteAddedForAssessor, + personalisation = mapOf( + "nacroReferenceId" to getNacroReferenceIdOrPlaceholder(application.assessment!!), + "nacroReferenceIdInSubject" to getSubjectLineReferenceIdOrPlaceholder(application.assessment!!), + "dateNoteAdded" to savedNote.createdAt.toLocalDate().toCas2UiFormat(), + "timeNoteAdded" to savedNote.createdAt.toCas2UiFormattedHourOfDay(), + "assessorName" to getAssessorNameOrPlaceholder(application.assessment!!), + "applicationType" to "Home Detention Curfew (HDC)", + "applicationUrl" to assessmentUrlTemplate.replace("#applicationId", application.id.toString()), + ), + ) + } + + private fun getSubjectLineReferenceIdOrPlaceholder(assessment: Cas2BailAssessmentEntity): String { + if (assessment.nacroReferralId != null) { + return "(${assessment.nacroReferralId!!})" + } + return "" + } + + private fun getNacroReferenceIdOrPlaceholder(assessment: Cas2BailAssessmentEntity): String { + if (assessment.nacroReferralId != null) { + return assessment.nacroReferralId!! + } + return "Unknown. " + + "The Nacro CAS-2 reference number has not been added to the application yet." + } + + private fun getAssessorNameOrPlaceholder(assessment: Cas2BailAssessmentEntity): String { + if (assessment.assessorName != null) { + return assessment.assessorName!! + } + return "Unknown. " + + "The assessor has not added their name to the application yet." + } + + private fun getCas2User(isExternalUser: Boolean): Cas2User { + return if (isExternalUser) { + externalUserService.getUserForRequest() + } else { + userService.getUserForRequest() + } + } + + private fun nomisUserCanAddNote(application: Cas2BailApplicationEntity, user: NomisUserEntity): Boolean { + return if (user.id == application.createdByUser.id) { + true + } else { + userAccessService.offenderIsFromSamePrisonAsUser(application.referringPrisonCode, user.activeCaseloadId) + } + } + + private fun saveNote(application: Cas2BailApplicationEntity, assessment: Cas2BailAssessmentEntity, body: String, user: Cas2User): Cas2BailApplicationNoteEntity { + val newNote = Cas2BailApplicationNoteEntity( + id = UUID.randomUUID(), + application = application, + body = body, + createdAt = OffsetDateTime.now(), + createdByUser = user, + assessment = assessment, + ) + + return cas2BailApplicationNoteRepository.save(newNote) + } +} \ No newline at end of file diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationService.kt index 4e1779b193..31100e39aa 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationService.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationService.kt @@ -15,6 +15,7 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.PaginationMetadata import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.ValidationErrors import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.validated import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.AuthorisableActionResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.CasResult import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.ValidatableActionResult import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.EmailNotificationService import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.UpstreamApiException @@ -28,8 +29,8 @@ import java.util.* @Service("Cas2BailApplicationService") class Cas2BailApplicationService( private val cas2BailApplicationRepository: Cas2BailApplicationRepository, - private val lockableApplicationRepository: Cas2BailLockableApplicationRepository, - private val applicationSummaryRepository: Cas2BailApplicationSummaryRepository, + private val cas2BailLockableApplicationRepository: Cas2BailLockableApplicationRepository, + private val cas2BailApplicationSummaryRepository: Cas2BailApplicationSummaryRepository, private val jsonSchemaService: JsonSchemaService, private val offenderService: OffenderService, private val cas2BailUserAccessService: Cas2BailUserAccessService, @@ -43,15 +44,15 @@ class Cas2BailApplicationService( ) { val repositoryUserFunctionMap = mapOf( - null to applicationSummaryRepository::findByUserId, - true to applicationSummaryRepository::findByUserIdAndSubmittedAtIsNotNull, - false to applicationSummaryRepository::findByUserIdAndSubmittedAtIsNull, + null to cas2BailApplicationSummaryRepository::findByUserId, + true to cas2BailApplicationSummaryRepository::findByUserIdAndSubmittedAtIsNotNull, + false to cas2BailApplicationSummaryRepository::findByUserIdAndSubmittedAtIsNull, ) val repositoryPrisonFunctionMap = mapOf( - null to applicationSummaryRepository::findByPrisonCode, - true to applicationSummaryRepository::findByPrisonCodeAndSubmittedAtIsNotNull, - false to applicationSummaryRepository::findByPrisonCodeAndSubmittedAtIsNull, + null to cas2BailApplicationSummaryRepository::findByPrisonCode, + true to cas2BailApplicationSummaryRepository::findByPrisonCodeAndSubmittedAtIsNotNull, + false to cas2BailApplicationSummaryRepository::findByPrisonCodeAndSubmittedAtIsNull, ) fun getCas2BailApplications( @@ -72,7 +73,7 @@ class Cas2BailApplicationService( fun getAllSubmittedCas2BailApplicationsForAssessor(pageCriteria: PageCriteria): Pair, PaginationMetadata?> { val pageable = getPageableOrAllPages(pageCriteria) - val response = applicationSummaryRepository.findBySubmittedAtIsNotNull(pageable) + val response = cas2BailApplicationSummaryRepository.findBySubmittedAtIsNotNull(pageable) val metadata = getMetadata(response, pageCriteria) @@ -126,20 +127,24 @@ class Cas2BailApplicationService( return fieldValidationError } + val id = UUID.randomUUID() + + val entityToSave = Cas2BailApplicationEntity( + id = id, + crn = crn, + createdByUser = user, + data = null, + document = null, + schemaVersion = jsonSchemaService.getNewestSchema(Cas2BailApplicationJsonSchemaEntity::class.java), + createdAt = OffsetDateTime.now(), + submittedAt = null, + schemaUpToDate = true, + nomsNumber = offenderDetails.otherIds.nomsNumber, + telephoneNumber = null, + ) + val createdApplication = cas2BailApplicationRepository.save( - Cas2BailApplicationEntity( - id = UUID.randomUUID(), - crn = crn, - createdByUser = user, - data = null, - document = null, - schemaVersion = jsonSchemaService.getNewestSchema(Cas2ApplicationJsonSchemaEntity::class.java), - createdAt = OffsetDateTime.now(), - submittedAt = null, - schemaUpToDate = true, - nomsNumber = offenderDetails.otherIds.nomsNumber, - telephoneNumber = null, - ), + entityToSave, ) return success(createdApplication.apply { schemaUpToDate = true }) @@ -221,37 +226,34 @@ class Cas2BailApplicationService( fun submitCas2BailApplication( submitApplication: SubmitCas2Application, user: NomisUserEntity, - ): AuthorisableActionResult> { + ): CasResult { val applicationId = submitApplication.applicationId - lockableApplicationRepository.acquirePessimisticLock(applicationId) + cas2BailLockableApplicationRepository.acquirePessimisticLock(applicationId) var application = cas2BailApplicationRepository.findByIdOrNull(applicationId) ?.let(jsonSchemaService::checkCas2BailSchemaOutdated) - ?: return AuthorisableActionResult.NotFound() + ?: return CasResult.NotFound() val serializedTranslatedDocument = objectMapper.writeValueAsString(submitApplication.translatedDocument) if (application.createdByUser != user) { - return AuthorisableActionResult.Unauthorised() + return CasResult.Unauthorised() } if (application.abandonedAt != null) { - return AuthorisableActionResult.Success( - ValidatableActionResult.GeneralValidationError("This application has already been abandoned"), - ) + return CasResult.GeneralValidationError("This application has already been abandoned") + } if (application.submittedAt != null) { - return AuthorisableActionResult.Success( - ValidatableActionResult.GeneralValidationError("This application has already been submitted"), - ) + return CasResult.GeneralValidationError("This application has already been submitted") + } if (!application.schemaUpToDate) { - return AuthorisableActionResult.Success( - ValidatableActionResult.GeneralValidationError("The schema version is outdated"), - ) + return CasResult.GeneralValidationError("The schema version is outdated") + } val validationErrors = ValidationErrors() @@ -264,9 +266,8 @@ class Cas2BailApplicationService( } if (validationErrors.any()) { - return AuthorisableActionResult.Success( - ValidatableActionResult.FieldValidationError(validationErrors), - ) + return CasResult.FieldValidationError(validationErrors) + } val schema = application.schemaVersion as? Cas2BailApplicationJsonSchemaEntity @@ -283,9 +284,7 @@ class Cas2BailApplicationService( telephoneNumber = submitApplication.telephoneNumber } } catch (error: UpstreamApiException) { - return AuthorisableActionResult.Success( - ValidatableActionResult.GeneralValidationError(error.message.toString()), - ) + return CasResult.GeneralValidationError(error.message.toString()) } application = cas2BailApplicationRepository.save(application) @@ -296,9 +295,7 @@ class Cas2BailApplicationService( sendEmailApplicationSubmitted(user, application) - return AuthorisableActionResult.Success( - ValidatableActionResult.Success(application), - ) + return CasResult.Success(application) } fun createCas2ApplicationSubmittedEvent(application: Cas2BailApplicationEntity) { @@ -343,7 +340,7 @@ class Cas2BailApplicationService( } fun createAssessment(application: Cas2BailApplicationEntity) { - assessmentService.createCas2Assessment(application) + assessmentService.createCas2BailAssessment(application) } @SuppressWarnings("ThrowsCount") @@ -352,6 +349,7 @@ class Cas2BailApplicationService( crn = application.crn, nomsNumber = application.nomsNumber.toString(), ) + //AuthorisableActionResult is deprecated but we don;t want to be touching offenderService while doing Cas2Bail work. val inmateDetail = when (inmateDetailResult) { is AuthorisableActionResult.NotFound -> throw UpstreamApiException("Inmate Detail not found") is AuthorisableActionResult.Unauthorised -> throw UpstreamApiException("Inmate Detail unauthorised") diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailAssessmentService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailAssessmentService.kt index 5633c613f9..da6aef71eb 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailAssessmentService.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailAssessmentService.kt @@ -9,6 +9,7 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2 import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailAssessmentEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailAssessmentRepository import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.AuthorisableActionResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.CasResult import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.ValidatableActionResult import java.time.OffsetDateTime import java.util.* @@ -19,8 +20,9 @@ class Cas2BailAssessmentService ( private val assessmentRepository: Cas2BailAssessmentRepository, ) { + @Transactional - fun createCas2Assessment(cas2BailApplicationEntity: Cas2BailApplicationEntity): Cas2BailAssessmentEntity = + fun createCas2BailAssessment(cas2BailApplicationEntity: Cas2BailApplicationEntity): Cas2BailAssessmentEntity = assessmentRepository.save( Cas2BailAssessmentEntity( id = UUID.randomUUID(), @@ -29,9 +31,9 @@ class Cas2BailAssessmentService ( ), ) - fun updateAssessment(assessmentId: UUID, newAssessment: UpdateCas2Assessment): AuthorisableActionResult> { + fun updateAssessment(assessmentId: UUID, newAssessment: UpdateCas2Assessment): CasResult { val assessmentEntity = assessmentRepository.findByIdOrNull(assessmentId) - ?: return AuthorisableActionResult.NotFound() + ?: return CasResult.NotFound() assessmentEntity.apply { this.nacroReferralId = newAssessment.nacroReferralId @@ -40,15 +42,13 @@ class Cas2BailAssessmentService ( val savedAssessment = assessmentRepository.save(assessmentEntity) - return AuthorisableActionResult.Success( - ValidatableActionResult.Success(savedAssessment), - ) + return CasResult.Success(savedAssessment) } - fun getAssessment(assessmentId: UUID): AuthorisableActionResult { + fun getAssessment(assessmentId: UUID): CasResult { val assessmentEntity = assessmentRepository.findByIdOrNull(assessmentId) - ?: return AuthorisableActionResult.NotFound() + ?: return CasResult.NotFound() - return AuthorisableActionResult.Success(assessmentEntity) + return CasResult.Success(assessmentEntity) } } \ No newline at end of file diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailStatusUpdateService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailStatusUpdateService.kt new file mode 100644 index 0000000000..c1ae352f63 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailStatusUpdateService.kt @@ -0,0 +1,182 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2bail + +import io.sentry.Sentry +import jakarta.transaction.Transactional +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Value +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.events.cas2.model.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas2AssessmentStatusUpdate +import uk.gov.justice.digital.hmpps.approvedpremisesapi.config.NotifyConfig +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.DomainEvent +import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.ValidationErrors +import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.reference.Cas2PersistedApplicationStatus +import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.reference.Cas2PersistedApplicationStatusDetail +import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.reference.Cas2PersistedApplicationStatusFinder +import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.AuthorisableActionResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.CasResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.ValidatableActionResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.EmailNotificationService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2.Constants.HDC_APPLICATION_TYPE +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2.DomainEventService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas2.ApplicationStatusTransformer +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.extractEntityFromCasResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.toCas2UiFormat +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.toCas2UiFormattedHourOfDay +import java.time.OffsetDateTime +import java.util.* + +@Service("Cas2BailStatusUpdateService") +class Cas2BailStatusUpdateService ( + private val cas2BailAssessmentRepository: Cas2BailAssessmentRepository, + private val cas2BailStatusUpdateRepository: Cas2BailStatusUpdateRepository, + private val cas2BailStatusUpdateDetailRepository: Cas2BailStatusUpdateDetailRepository, + private val domainEventService: DomainEventService, + private val emailNotificationService: EmailNotificationService, + private val notifyConfig: NotifyConfig, + private val statusFinder: Cas2PersistedApplicationStatusFinder, + private val statusTransformer: ApplicationStatusTransformer, + @Value("\${url-templates.frontend.cas2.application}") private val applicationUrlTemplate: String, + @Value("\${url-templates.frontend.cas2.application-overview}") private val applicationOverviewUrlTemplate: String, +) { + + private val log = LoggerFactory.getLogger(this::class.java) + + fun isValidStatus(statusUpdate: Cas2AssessmentStatusUpdate): Boolean { + return findActiveStatusByName(statusUpdate.newStatus) != null + } + + @Transactional + @SuppressWarnings("ReturnCount") + fun createForAssessment( + assessmentId: UUID, + statusUpdate: Cas2AssessmentStatusUpdate, + assessor: ExternalUserEntity, + ): CasResult { + val assessment = cas2BailAssessmentRepository.findByIdOrNull(assessmentId) + ?: return CasResult.NotFound() + + val status = findActiveStatusByName(statusUpdate.newStatus) + ?: return CasResult.GeneralValidationError("The status ${statusUpdate.newStatus} is not valid") + + + val newDetails = statusUpdate.newStatusDetails.isNullOrEmpty() + val statusDetails = if (newDetails) { + emptyList() + } else { + statusUpdate.newStatusDetails?.map { detail -> + status.findStatusDetailOnStatus(detail) + ?: return CasResult.GeneralValidationError("The status detail $detail is not valid") + } + } + + if (ValidationErrors().any()) { + return CasResult.FieldValidationError(ValidationErrors()) + } + + val createdStatusUpdate = cas2BailStatusUpdateRepository.save( + Cas2BailStatusUpdateEntity( + id = UUID.randomUUID(), + assessment = assessment, + application = assessment.application, + assessor = assessor, + statusId = status.id, + description = status.description, + label = status.label, + createdAt = OffsetDateTime.now(), + ), + ) + + statusDetails?.forEach { detail -> + cas2BailStatusUpdateDetailRepository.save( + Cas2BailStatusUpdateDetailEntity( + id = UUID.randomUUID(), + statusDetailId = detail.id, + statusUpdate = createdStatusUpdate, + label = detail.label, + ), + ) + } + + sendEmailStatusUpdated(assessment.application.createdByUser, assessment.application, createdStatusUpdate) + + createStatusUpdatedDomainEvent(createdStatusUpdate, statusDetails) + + + return CasResult.Success(createdStatusUpdate) + } + + private fun findActiveStatusByName(statusName: String): Cas2PersistedApplicationStatus? { + return statusFinder.active() + .find { status -> status.name == statusName } + } + + fun createStatusUpdatedDomainEvent( + statusUpdate: Cas2BailStatusUpdateEntity, + statusDetails: List? = emptyList()) { + val domainEventId = UUID.randomUUID() + val eventOccurredAt = statusUpdate.createdAt + val application = statusUpdate.application + val newStatus = statusUpdate.status() + val assessor = statusUpdate.assessor + + domainEventService.saveCas2ApplicationStatusUpdatedDomainEvent( + DomainEvent( + id = domainEventId, + applicationId = application.id, + crn = application.crn, + nomsNumber = application.nomsNumber, + occurredAt = eventOccurredAt.toInstant(), + data = Cas2ApplicationStatusUpdatedEvent( + id = domainEventId, + timestamp = eventOccurredAt.toInstant(), + eventType = EventType.applicationStatusUpdated, + eventDetails = Cas2ApplicationStatusUpdatedEventDetails( + applicationId = application.id, + applicationUrl = applicationUrlTemplate.replace("#id", application.id.toString()), + personReference = PersonReference( + crn = application.crn, + noms = application.nomsNumber.toString(), + ), + newStatus = Cas2Status( + name = newStatus.name, + description = newStatus.description, + label = newStatus.label, + statusDetails = statusDetails?.let { statusTransformer.transformStatusDetailListToDetailItemList(it) }, + ), + updatedBy = ExternalUser( + username = assessor.username, + name = assessor.name, + email = assessor.email, + origin = assessor.origin, + ), + updatedAt = eventOccurredAt.toInstant(), + ), + ), + ), + ) + } + + private fun sendEmailStatusUpdated(user: NomisUserEntity, application: Cas2BailApplicationEntity, status: Cas2BailStatusUpdateEntity) { + if (application.createdByUser.email != null) { + emailNotificationService.sendCas2Email( + recipientEmailAddress = user.email!!, + templateId = notifyConfig.templates.cas2ApplicationStatusUpdated, + personalisation = mapOf( + "applicationStatus" to status.label, + "dateStatusChanged" to status.createdAt.toLocalDate().toCas2UiFormat(), + "timeStatusChanged" to status.createdAt.toCas2UiFormattedHourOfDay(), + "applicationType" to HDC_APPLICATION_TYPE, + "nomsNumber" to application.nomsNumber, + "applicationUrl" to applicationOverviewUrlTemplate.replace("#id", application.id.toString()), + ), + ) + } else { + log.error("Email not found for User ${application.createdByUser.id}. Unable to send email when updating status of Application ${application.id}") + Sentry.captureMessage("Email not found for User ${application.createdByUser.id}. Unable to send email when updating status of Application ${application.id}") + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2/AssessmentsTransformer.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2/AssessmentsTransformer.kt index df053b2248..1262cf390f 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2/AssessmentsTransformer.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2/AssessmentsTransformer.kt @@ -3,6 +3,7 @@ package uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas2 import org.springframework.stereotype.Component import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas2Assessment import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2AssessmentEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailAssessmentEntity @Component("Cas2AssessmentsTransformer") class AssessmentsTransformer(private val statusUpdateTransformer: StatusUpdateTransformer) { diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index ed28109c30..f2ed06a6c1 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -8,6 +8,10 @@ spring: locations: classpath:db/migration/all,classpath:db/migration/local+dev+test,classpath:db/migration/local,classpath:db/migration/all-except-integration jpa: database: postgresql + properties: + hibernate: + show_sql: true + format_sql: true data: redis: host: localhost @@ -67,7 +71,8 @@ logging: org.hibernate.SQL: DEBUG # Uncomment the two entries below to see SQL binding #org.hibernate.orm.jdbc.bind: TRACE - #org.hibernate.type.descriptor.sql.BasicBinder: TRACE + org.hibernate.type.descriptor.sql.BasicBinder: TRACE + org.hibernate.type.descriptor.sql: tract # allows us to see the JWT token to simplify local API invocation uk.gov.justice.digital.hmpps.approvedpremisesapi.config.RequestResponseLoggingFilter: TRACE # allows us to see the request URL and method for upstream requests diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/Cas2BailApplicationJsonSchemaEntityFactory.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/Cas2BailApplicationJsonSchemaEntityFactory.kt new file mode 100644 index 0000000000..709386c6ca --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/Cas2BailApplicationJsonSchemaEntityFactory.kt @@ -0,0 +1,48 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.factory + +import io.github.bluegroundltd.kfactory.Factory +import io.github.bluegroundltd.kfactory.Yielded +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2BailApplicationJsonSchemaEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.randomDateTimeBefore +import java.time.OffsetDateTime +import java.util.UUID + +class Cas2BailApplicationJsonSchemaEntityFactory : Factory { + private var id: Yielded = { UUID.randomUUID() } + private var addedAt: Yielded = { OffsetDateTime.now().randomDateTimeBefore(7) } + private var schema: Yielded = { "{}" } + + fun withId(id: UUID) = apply { + this.id = { id } + } + + fun withAddedAt(addedAt: OffsetDateTime) = apply { + this.addedAt = { addedAt } + } + + fun withSchema(schema: String) = apply { + this.schema = { schema } + } + + fun withPermissiveSchema() = apply { + withSchema( + """ + { + "${"\$schema"}": "https://json-schema.org/draft/2020-12/schema", + "${"\$id"}": "https://example.com/product.schema.json", + "title": "Thing", + "description": "A thing", + "type": "object", + "properties": { } + } + """, + ) + } + + override fun produce(): Cas2BailApplicationJsonSchemaEntity = Cas2BailApplicationJsonSchemaEntity( + id = this.id(), + addedAt = this.addedAt(), + schema = this.schema(), + ) + +} diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/cas2bail/Cas2BailApplicationEntityFactory.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/cas2bail/Cas2BailApplicationEntityFactory.kt new file mode 100644 index 0000000000..e2d95068e5 --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/cas2bail/Cas2BailApplicationEntityFactory.kt @@ -0,0 +1,159 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.cas2bail + +import io.github.bluegroundltd.kfactory.Factory +import io.github.bluegroundltd.kfactory.Yielded +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ApprovedPremisesApplicationJsonSchemaEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationNoteEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2AssessmentEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2StatusUpdateEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.JsonSchemaEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.NomisUserEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationNoteEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailAssessmentEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailStatusUpdateEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.randomDateTimeBefore +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.randomInt +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.randomNumberChars +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.randomStringMultiCaseWithNumbers +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.randomStringUpperCase +import java.time.LocalDate +import java.time.OffsetDateTime +import java.util.UUID + + +class Cas2BailApplicationEntityFactory : Factory { + private var id: Yielded = { UUID.randomUUID() } + private var crn: Yielded = { randomStringMultiCaseWithNumbers(8) } + private var createdByUser: Yielded? = null + private var data: Yielded = { "{}" } + private var document: Yielded = { "{}" } + private var applicationSchema: Yielded = { + ApprovedPremisesApplicationJsonSchemaEntityFactory().produce() + } + private var createdAt: Yielded = { OffsetDateTime.now().randomDateTimeBefore(30) } + private var submittedAt: Yielded = { null } + private var abandonedAt: Yielded = { null } + private var statusUpdates: Yielded> = { mutableListOf() } + private var eventNumber: Yielded = { randomInt(1, 9).toString() } + private var nomsNumber: Yielded = { randomStringUpperCase(6) } + private var telephoneNumber: Yielded = { randomNumberChars(12) } + private var notes: Yielded> = { mutableListOf() } + private var assessment: Yielded = { null } + private var referringPrisonCode: Yielded = { null } + private var preferredAreas: Yielded = { null } + private var hdcEligibilityDate: Yielded = { null } + private var conditionalReleaseDate: Yielded = { null } + + fun withId(id: UUID) = apply { + this.id = { id } + } + + fun withCrn(crn: String) = apply { + this.crn = { crn } + } + + fun withNomsNumber(nomsNumber: String) = apply { + this.nomsNumber = { nomsNumber } + } + + fun withCreatedByUser(createdByUser: NomisUserEntity) = apply { + this.createdByUser = { createdByUser } + } + + fun withYieldedCreatedByUser(createdByUser: Yielded) = apply { + this.createdByUser = createdByUser + } + + fun withData(data: String?) = apply { + this.data = { data } + } + + fun withDocument(document: String?) = apply { + this.document = { document } + } + + fun withApplicationSchema(applicationSchema: JsonSchemaEntity) = apply { + this.applicationSchema = { applicationSchema } + } + + fun withYieldedApplicationSchema(applicationSchema: Yielded) = apply { + this.applicationSchema = applicationSchema + } + + fun withCreatedAt(createdAt: OffsetDateTime) = apply { + this.createdAt = { createdAt } + } + + fun withSubmittedAt(submittedAt: OffsetDateTime?) = apply { + this.submittedAt = { submittedAt } + } + + fun withAbandonedAt(abandonedAt: OffsetDateTime?) = apply { + this.abandonedAt = { abandonedAt } + } + + fun withStatusUpdates(statusUpdates: MutableList) = apply { + this.statusUpdates = { statusUpdates } + } + + fun withNotes(notes: MutableList) = apply { + this.notes = { notes } + } + + fun withEventNumber(eventNumber: String) = apply { + this.eventNumber = { eventNumber } + } + + fun withAssessment(assessmentEntity: Cas2BailAssessmentEntity) = apply { + this.assessment = { assessmentEntity } + } + + fun withReferringPrisonCode(referringPrisonCode: String) = apply { + this.referringPrisonCode = { referringPrisonCode } + } + + fun withPreferredAreas(preferredAreas: String) = apply { + this.preferredAreas = { preferredAreas } + } + + fun withTelephoneNumber(telephoneNumber: String) = apply { + this.telephoneNumber = { telephoneNumber } + } + + fun withHdcEligibilityDate(hdcEligibilityDate: LocalDate) = apply { + this.hdcEligibilityDate = { hdcEligibilityDate } + } + + fun withConditionalReleaseDate(conditionalReleaseDate: LocalDate) = apply { + this.conditionalReleaseDate = { conditionalReleaseDate } + } + + override fun produce(): Cas2BailApplicationEntity { + val entity = Cas2BailApplicationEntity( + id = this.id(), + crn = this.crn(), + createdByUser = this.createdByUser?.invoke() ?: throw RuntimeException("Must provide a createdByUser"), + data = this.data(), + document = this.document(), + schemaVersion = this.applicationSchema(), + createdAt = this.createdAt(), + submittedAt = this.submittedAt(), + abandonedAt = this.abandonedAt(), + statusUpdates = this.statusUpdates(), + schemaUpToDate = false, + nomsNumber = this.nomsNumber(), + telephoneNumber = this.telephoneNumber(), + notes = this.notes(), + assessment = this.assessment(), + referringPrisonCode = this.referringPrisonCode(), + hdcEligibilityDate = this.hdcEligibilityDate(), + conditionalReleaseDate = this.conditionalReleaseDate(), + preferredAreas = this.preferredAreas(), + ) + + return entity + + } +} \ No newline at end of file diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/cas2bail/Cas2BailStatusUpdateEntityFactory.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/cas2bail/Cas2BailStatusUpdateEntityFactory.kt new file mode 100644 index 0000000000..c48252a8ce --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/cas2bail/Cas2BailStatusUpdateEntityFactory.kt @@ -0,0 +1,81 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.cas2bail + +import io.github.bluegroundltd.kfactory.Factory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ExternalUserEntityFactory + +import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.reference.Cas2ApplicationStatusSeeding +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.randomDateTimeBefore +import java.time.OffsetDateTime + +import io.github.bluegroundltd.kfactory.Yielded +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2AssessmentEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2StatusUpdateDetailEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2StatusUpdateEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ExternalUserEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailAssessmentEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailStatusUpdateDetailEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailStatusUpdateEntity + +import java.util.UUID + +class Cas2BailStatusUpdateEntityFactory : Factory { + private var id: Yielded = { UUID.randomUUID() } + private var assessor: Yielded = { ExternalUserEntityFactory().produce() } + private var assessment: Yielded = { null } + private var application: Yielded? = null + private var statusId: Yielded = { Cas2ApplicationStatusSeeding.statusList().random().id } + private var createdAt: Yielded = { OffsetDateTime.now().randomDateTimeBefore(30) } + private var label: Yielded = { "More information requested" } + private var description: Yielded = { "More information about the application has been requested" } + private var statusUpdateDetails: Yielded?> = { null } + + fun withId(id: UUID) = apply { + this.id = { id } + } + + fun withAssessor(assessor: ExternalUserEntity) = apply { + this.assessor = { assessor } + } + + fun withAssessment(assessment: Cas2BailAssessmentEntity) = apply { + this.assessment = { assessment } + } + + fun withApplication(application: Cas2BailApplicationEntity) = apply { + this.application = { application } + } + + fun withStatusId(statusId: UUID) = apply { + this.statusId = { statusId } + } + + fun withCreatedAt(createdAt: OffsetDateTime) = apply { + this.createdAt = { createdAt } + } + + fun withLabel(label: String) = apply { + this.label = { label } + } + + fun withDescription(description: String) = apply { + this.description = { description } + } + + fun withStatusUpdateDetails(details: List) = apply { + this.statusUpdateDetails = { details } + } + + override fun produce(): Cas2BailStatusUpdateEntity = Cas2BailStatusUpdateEntity( + id = this.id(), + assessor = this.assessor(), + application = this.application?.invoke() ?: error("Must provide a submitted application"), + assessment = this.assessment(), + statusId = this.statusId(), + createdAt = this.createdAt(), + label = this.label(), + description = this.description(), + statusUpdateDetails = this.statusUpdateDetails(), + ) +} diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/IntegrationTestBase.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/IntegrationTestBase.kt index a9343ce664..e7c8b9fb85 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/IntegrationTestBase.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/IntegrationTestBase.kt @@ -30,77 +30,10 @@ import org.springframework.test.context.ContextConfiguration import org.springframework.test.web.reactive.server.WebTestClient import uk.gov.justice.digital.hmpps.approvedpremisesapi.client.PrisonsApiClient import uk.gov.justice.digital.hmpps.approvedpremisesapi.config.NotifyConfig -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ApAreaEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.AppealEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ApplicationTeamCodeEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ApplicationTimelineNoteEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ApprovedPremisesApplicationEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ApprovedPremisesApplicationJsonSchemaEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ApprovedPremisesAssessmentEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ApprovedPremisesAssessmentJsonSchemaEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ApprovedPremisesEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ApprovedPremisesPlacementApplicationJsonSchemaEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ArrivalEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.AssessmentClarificationNoteEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.AssessmentReferralHistorySystemNoteEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.AssessmentReferralHistoryUserNoteEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.BedEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.BedMoveEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.BookingEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.BookingNotMadeEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.CancellationEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.CancellationReasonEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.Cas1ApplicationUserDetailsEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.Cas1OutOfServiceBedCancellationEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.Cas1OutOfServiceBedEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.Cas1OutOfServiceBedReasonEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.Cas1OutOfServiceBedRevisionEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.Cas1SpaceBookingEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.Cas2ApplicationEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.Cas2ApplicationJsonSchemaEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.Cas2AssessmentEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.Cas2NoteEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.Cas2StatusUpdateDetailEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.Cas2StatusUpdateEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.CharacteristicEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ConfirmationEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.DateChangeEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.DepartureEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.DepartureReasonEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.DestinationProviderEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.DomainEventEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ExtensionEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ExternalUserEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.LocalAuthorityEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.LostBedCancellationEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.LostBedReasonEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.LostBedsEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.MoveOnCategoryEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.NomisUserEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.NonArrivalEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.NonArrivalReasonEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.OfflineApplicationEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.PersistedFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.PlacementApplicationEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.PlacementDateEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.PlacementRequestEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.PlacementRequirementsEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.PostCodeDistrictEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ProbationAreaProbationRegionMappingEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ProbationDeliveryUnitEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ProbationRegionEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ReferralRejectionReasonEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.RoomEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.TemporaryAccommodationApplicationEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.TemporaryAccommodationApplicationJsonSchemaEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.TemporaryAccommodationAssessmentEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.TemporaryAccommodationAssessmentJsonSchemaEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.TemporaryAccommodationPremisesEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.TurnaroundEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.UserEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.UserQualificationAssignmentEntityFactory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.UserRoleAssignmentEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.* import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.cas1.Cas1CruManagementAreaEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.cas2bail.Cas2BailApplicationEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.cas2bail.Cas2BailStatusUpdateEntityFactory import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.asserter.DomainEventAsserter import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.asserter.EmailNotificationAsserter import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.config.IntegrationTestDbManager @@ -108,156 +41,16 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.config.TestP import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.mocks.MockFeatureFlagService import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.mocks.MutableClockConfiguration import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.mocks.NoOpSentryService -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApAreaEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.AppealEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApplicationTeamCodeEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApplicationTimelineNoteEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApplicationTimelineNoteRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApprovedPremisesApplicationEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApprovedPremisesApplicationJsonSchemaEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApprovedPremisesAssessmentEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApprovedPremisesAssessmentJsonSchemaEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApprovedPremisesEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApprovedPremisesPlacementApplicationJsonSchemaEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ArrivalEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.AssessmentClarificationNoteEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.AssessmentReferralHistorySystemNoteEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.AssessmentReferralHistoryUserNoteEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.BedEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.BedMoveEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.BedMoveRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.BedRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.BookingEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.BookingNotMadeEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.CancellationEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.CancellationReasonEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas1ApplicationUserDetailsEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas1ApplicationUserDetailsRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas1CruManagementAreaEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas1CruManagementAreaRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas1OutOfServiceBedCancellationEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas1OutOfServiceBedEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas1OutOfServiceBedReasonEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas1OutOfServiceBedRevisionEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas1SpaceBookingEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas1SpaceBookingRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationJsonSchemaEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationNoteEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationNoteRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2AssessmentEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2AssessmentRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2StatusUpdateDetailEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2StatusUpdateDetailRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2StatusUpdateEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.CharacteristicEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.CharacteristicRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ConfirmationEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.DateChangeEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.DateChangeRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.DepartureEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.DepartureReasonEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.DestinationProviderEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.DomainEventEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ExtensionEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ExternalUserEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.LocalAuthorityAreaEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.LostBedCancellationEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.LostBedReasonEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.LostBedsEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.MoveOnCategoryEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.NomisUserEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.NonArrivalEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.NonArrivalReasonEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.OfflineApplicationEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.PlacementApplicationEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.PlacementApplicationRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.PlacementDateEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.PlacementDateRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.PlacementRequestEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.PlacementRequestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.PlacementRequirementsEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.PlacementRequirementsRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.PostCodeDistrictEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ProbationAreaProbationRegionMappingEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ProbationDeliveryUnitEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ProbationRegionEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ReferralRejectionReasonEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ReferralRejectionReasonRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.RoomEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.RoomRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.TemporaryAccommodationApplicationEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.TemporaryAccommodationApplicationJsonSchemaEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.TemporaryAccommodationAssessmentEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.TemporaryAccommodationAssessmentJsonSchemaEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.TemporaryAccommodationPremisesEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.TurnaroundEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.UserEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.UserQualificationAssignmentEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.UserRoleAssignmentEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailStatusUpdateEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.community.OffenderDetailSummary import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.community.UserOffenderAccess import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.deliuscontext.StaffMember import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.deliuscontext.StaffMembersPage import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.hmppsauth.GetTokenResponse import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.prisonsapi.InmateDetail -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.ApAreaTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.AppealTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.ApplicationTeamCodeTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.ApprovedPremisesApplicationJsonSchemaTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.ApprovedPremisesApplicationTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.ApprovedPremisesAssessmentJsonSchemaTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.ApprovedPremisesAssessmentTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.ApprovedPremisesPlacementApplicationJsonSchemaTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.ApprovedPremisesTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.ArrivalTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.AssessmentClarificationNoteTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.AssessmentReferralHistorySystemNoteTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.AssessmentReferralHistoryUserNoteTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.AssessmentTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.BookingNotMadeTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.BookingTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.CancellationReasonTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.CancellationTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.Cas1OutOfServiceBedCancellationTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.Cas1OutOfServiceBedDetailsTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.Cas1OutOfServiceBedReasonTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.Cas1OutOfServiceBedTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.Cas2ApplicationJsonSchemaTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.Cas2ApplicationTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.Cas2StatusUpdateTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.ConfirmationTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.DepartureReasonTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.DepartureTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.DestinationProviderTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.DomainEventTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.ExtensionTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.ExternalUserTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.LocalAuthorityAreaTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.LostBedCancellationTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.LostBedReasonTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.LostBedsTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.MoveOnCategoryTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.NomisUserTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.NonArrivalReasonTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.NonArrivalTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.OfflineApplicationTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.PlacementApplicationTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.PlacementRequestTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.PostCodeDistrictTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.ProbationAreaProbationRegionMappingTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.ProbationDeliveryUnitTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.ProbationRegionTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.RoomTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.TemporaryAccommodationApplicationJsonSchemaTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.TemporaryAccommodationApplicationTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.TemporaryAccommodationAssessmentJsonSchemaTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.TemporaryAccommodationAssessmentTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.TemporaryAccommodationPremisesTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.TurnaroundTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.UserQualificationAssignmentTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.UserRoleAssignmentTestRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.UserTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.* import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.JwtAuthHelper import java.time.Duration import java.util.TimeZone @@ -380,12 +173,18 @@ abstract class IntegrationTestBase { @Autowired lateinit var cas2ApplicationRepository: Cas2ApplicationTestRepository + @Autowired + lateinit var cas2BailApplicationRepository: Cas2BailApplicationTestRepository + @Autowired lateinit var cas2AssessmentRepository: Cas2AssessmentRepository @Autowired lateinit var cas2StatusUpdateRepository: Cas2StatusUpdateTestRepository + @Autowired + lateinit var cas2BailStatusUpdateRepository: Cas2BailStatusUpdateTestRepository + @Autowired lateinit var cas2NoteRepository: Cas2ApplicationNoteRepository @@ -401,6 +200,9 @@ abstract class IntegrationTestBase { @Autowired lateinit var cas2ApplicationJsonSchemaRepository: Cas2ApplicationJsonSchemaTestRepository + @Autowired + lateinit var cas2BailApplicationJsonSchemaRepository: Cas2BailApplicationJsonSchemaTestRepository + @Autowired lateinit var temporaryAccommodationApplicationJsonSchemaRepository: TemporaryAccommodationApplicationJsonSchemaTestRepository @@ -568,14 +370,17 @@ abstract class IntegrationTestBase { lateinit var nonArrivalReasonEntityFactory: PersistedFactory lateinit var approvedPremisesApplicationEntityFactory: PersistedFactory lateinit var cas2ApplicationEntityFactory: PersistedFactory + lateinit var cas2BailApplicationEntityFactory: PersistedFactory lateinit var cas2AssessmentEntityFactory: PersistedFactory lateinit var cas2StatusUpdateEntityFactory: PersistedFactory + lateinit var cas2BailStatusUpdateEntityFactory: PersistedFactory lateinit var cas2StatusUpdateDetailEntityFactory: PersistedFactory lateinit var cas2NoteEntityFactory: PersistedFactory lateinit var temporaryAccommodationApplicationEntityFactory: PersistedFactory lateinit var offlineApplicationEntityFactory: PersistedFactory lateinit var approvedPremisesApplicationJsonSchemaEntityFactory: PersistedFactory lateinit var cas2ApplicationJsonSchemaEntityFactory: PersistedFactory + lateinit var cas2BailApplicationJsonSchemaEntityFactory: PersistedFactory lateinit var temporaryAccommodationApplicationJsonSchemaEntityFactory: PersistedFactory lateinit var approvedPremisesPlacementApplicationJsonSchemaEntityFactory: PersistedFactory lateinit var approvedPremisesAssessmentJsonSchemaEntityFactory: PersistedFactory @@ -679,14 +484,18 @@ abstract class IntegrationTestBase { nonArrivalReasonEntityFactory = PersistedFactory({ NonArrivalReasonEntityFactory() }, nonArrivalReasonRepository) approvedPremisesApplicationEntityFactory = PersistedFactory({ ApprovedPremisesApplicationEntityFactory() }, approvedPremisesApplicationRepository) cas2ApplicationEntityFactory = PersistedFactory({ Cas2ApplicationEntityFactory() }, cas2ApplicationRepository) + + cas2BailApplicationEntityFactory = PersistedFactory({ Cas2BailApplicationEntityFactory() }, cas2BailApplicationRepository) cas2AssessmentEntityFactory = PersistedFactory({ Cas2AssessmentEntityFactory() }, cas2AssessmentRepository) cas2StatusUpdateEntityFactory = PersistedFactory({ Cas2StatusUpdateEntityFactory() }, cas2StatusUpdateRepository) + cas2BailStatusUpdateEntityFactory = PersistedFactory({ Cas2BailStatusUpdateEntityFactory() }, cas2BailStatusUpdateRepository) cas2StatusUpdateDetailEntityFactory = PersistedFactory({ Cas2StatusUpdateDetailEntityFactory() }, cas2StatusUpdateDetailRepository) cas2NoteEntityFactory = PersistedFactory({ Cas2NoteEntityFactory() }, cas2NoteRepository) temporaryAccommodationApplicationEntityFactory = PersistedFactory({ TemporaryAccommodationApplicationEntityFactory() }, temporaryAccommodationApplicationRepository) offlineApplicationEntityFactory = PersistedFactory({ OfflineApplicationEntityFactory() }, offlineApplicationRepository) approvedPremisesApplicationJsonSchemaEntityFactory = PersistedFactory({ ApprovedPremisesApplicationJsonSchemaEntityFactory() }, approvedPremisesApplicationJsonSchemaRepository) cas2ApplicationJsonSchemaEntityFactory = PersistedFactory({ Cas2ApplicationJsonSchemaEntityFactory() }, cas2ApplicationJsonSchemaRepository) + cas2BailApplicationJsonSchemaEntityFactory = PersistedFactory({ Cas2BailApplicationJsonSchemaEntityFactory() }, cas2BailApplicationJsonSchemaRepository) temporaryAccommodationApplicationJsonSchemaEntityFactory = PersistedFactory({ TemporaryAccommodationApplicationJsonSchemaEntityFactory() }, temporaryAccommodationApplicationJsonSchemaRepository) approvedPremisesAssessmentJsonSchemaEntityFactory = PersistedFactory({ ApprovedPremisesAssessmentJsonSchemaEntityFactory() }, approvedPremisesAssessmentJsonSchemaRepository) temporaryAccommodationAssessmentJsonSchemaEntityFactory = PersistedFactory({ TemporaryAccommodationAssessmentJsonSchemaEntityFactory() }, temporaryAccommodationAssessmentJsonSchemaRepository) diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationTest.kt new file mode 100644 index 0000000000..6c18faa868 --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationTest.kt @@ -0,0 +1,1699 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.cas2bail + + + +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.node.NullNode +import com.ninjasquad.springmockk.SpykBean +import io.mockk.clearMocks +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import org.springframework.test.web.reactive.server.returnResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.IntegrationTestBase +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenACas2Assessor +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenACas2LicenceCaseAdminUser +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenACas2PomUser +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenAnOffender +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.httpmocks.communityAPIMockNotFoundOffenderDetailsCall +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.httpmocks.prisonAPIMockNotFoundInmateDetailsCall +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailStatusUpdateEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.community.OffenderDetailSummary +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.randomDateAfter +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.randomDateBefore +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.randomDateTimeBefore +import java.time.LocalDate +import java.time.OffsetDateTime +import java.util.* +import kotlin.math.sign + +class Cas2BailApplicationTest : IntegrationTestBase() { + + @SpykBean + lateinit var realApplicationRepository: ApplicationRepository + + val schema = """ + { + "${"\$schema"}": "https://json-schema.org/draft/2020-12/schema", + "${"\$id"}": "https://example.com/product.schema.json", + "title": "Thing", + "description": "A thing", + "type": "object", + "properties": { + "thingId": { + "description": "The unique identifier for a thing", + "type": "integer" + } + }, + "required": [ "thingId" ] + } + """ + + val data = """ + { + "thingId": 123 + } + """ + + @AfterEach + fun afterEach() { + // SpringMockK does not correctly clear mocks for @SpyKBeans that are also a @Repository, causing mocked behaviour + // in one test to show up in another (see https://github.com/Ninja-Squad/springmockk/issues/85) + // Manually clearing after each test seems to fix this. + clearMocks(realApplicationRepository) + } + + + @Nested + inner class ControlsOnExternalUsers { + @ParameterizedTest + @ValueSource(strings = ["ROLE_CAS2_ASSESSOR", "ROLE_CAS2_MI"]) + fun `creating an application is forbidden to external users based on role`(role: String) { + val jwt = jwtAuthHelper.createClientCredentialsJwt( + username = "username", + authSource = "nomis", + roles = listOf(role), + ) + + webTestClient.post() + .uri("/cas2bail/applications") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isForbidden + } + + @ParameterizedTest + @ValueSource(strings = ["ROLE_CAS2_ASSESSOR", "ROLE_CAS2_MI"]) + fun `updating an application is forbidden to external users based on role`(role: String) { + val jwt = jwtAuthHelper.createClientCredentialsJwt( + username = "username", + authSource = "nomis", + roles = listOf(role), + ) + + webTestClient.put() + .uri("/cas2bail/applications/66911cf0-75b1-4361-84bd-501b176fd4fd") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isForbidden + } + + @Test + fun `viewing list of applications is forbidden to external users based on role`() { + val jwt = jwtAuthHelper.createClientCredentialsJwt( + username = "username", + authSource = "nomis", + roles = listOf("ROLE_CAS2_ASSESSOR"), + ) + + webTestClient.get() + .uri("/cas2bail/applications") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isForbidden + } + + @Test + fun `viewing an application is forbidden to external users based on role`() { + val jwt = jwtAuthHelper.createClientCredentialsJwt( + username = "username", + authSource = "nomis", + roles = listOf("ROLE_CAS2_ASSESSOR"), + ) + + webTestClient.get() + .uri("/cas2bail/applications/66911cf0-75b1-4361-84bd-501b176fd4") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isForbidden + } + } + + + @Nested + inner class MissingJwt { + @Test + fun `Get all applications without JWT returns 401`() { + webTestClient.get() + .uri("/cas2bail/applications") + .exchange() + .expectStatus() + .isUnauthorized + } + + @Test + fun `Get single application without JWT returns 401`() { + webTestClient.get() + .uri("/cas2bail/applications/9b785e59-b85c-4be0-b271-d9ac287684b6") + .exchange() + .expectStatus() + .isUnauthorized + } + + @Test + fun `Create new application without JWT returns 401`() { + webTestClient.post() + .uri("/cas2bail/applications") + .exchange() + .expectStatus() + .isUnauthorized + } + } + + @Nested + inner class GetToIndex { + + @Test + fun `return unexpired applications when applications GET is requested`() { + val unexpiredSubset = setOf( + Pair("More information requested", UUID.fromString("f5cd423b-08eb-4efb-96ff-5cc6bb073905")), + Pair("Awaiting decision", UUID.fromString("ba4d8432-250b-4ab9-81ec-7eb4b16e5dd1")), + Pair("On waiting list", UUID.fromString("a919097d-b324-471c-9834-756f255e87ea")), + Pair("Place offered", UUID.fromString("176bbda0-0766-4d77-8d56-18ed8f9a4ef2")), + Pair("Offer accepted", UUID.fromString("fe254d88-ce1d-4cd8-8bd6-88de88f39019")), + Pair("Could not be placed", UUID.fromString("758eee61-2a6d-46b9-8bdd-869536d77f1b")), + Pair("Incomplete", UUID.fromString("4ad9bbfa-e5b0-456f-b746-146f7fd511dd")), + Pair("Offer declined or withdrawn", UUID.fromString("9a381bc6-22d3-41d6-804d-4e49f428c1de")), + ) + + val expiredSubset = setOf( + Pair("Referral withdrawn", UUID.fromString("004e2419-9614-4c1e-a207-a8418009f23d")), + Pair("Referral cancelled", UUID.fromString("f13bbdd6-44f1-4362-b9d3-e6f1298b1bf9")), + Pair("Awaiting arrival", UUID.fromString("89458555-3219-44a2-9584-c4f715d6b565")), + ) + + val applicationSchema = cas2BailApplicationJsonSchemaEntityFactory.produceAndPersist { + withAddedAt(OffsetDateTime.now()) + withId(UUID.randomUUID()) + } + + fun createApplication(userEntity: NomisUserEntity, offenderDetails: OffenderDetailSummary): Cas2BailApplicationEntity { + return cas2BailApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(userEntity) + withCrn(offenderDetails.otherIds.crn) + withCreatedAt(OffsetDateTime.now().minusDays(28)) + withConditionalReleaseDate(LocalDate.now().plusDays(1)) + } + } + + fun createStatusUpdate(status: Pair, application: Cas2BailApplicationEntity): Cas2BailStatusUpdateEntity { + return cas2BailStatusUpdateEntityFactory.produceAndPersist { + withLabel(status.first) + withStatusId(status.second) + withApplication(application) + withAssessor(externalUserEntityFactory.produceAndPersist()) + } + } + + fun unexpiredDateTime() = OffsetDateTime.now().randomDateTimeBefore(32) + fun expiredDateTime() = unexpiredDateTime().minusDays(33) + + val unexpiredApplicationIds = mutableSetOf() + val expiredApplicationIds = mutableSetOf() + + givenACas2PomUser { userEntity, jwt -> + givenAnOffender { offenderDetails, _ -> + + repeat(2) { + unexpiredApplicationIds.add(createApplication(userEntity, offenderDetails).id) + } + + unexpiredSubset.union(expiredSubset).forEach { + val application = createApplication(userEntity, offenderDetails) + val statusUpdate = createStatusUpdate(it, application) + statusUpdate.createdAt = unexpiredDateTime() + cas2BailStatusUpdateRepository.save(statusUpdate) + unexpiredApplicationIds.add(application.id) + } + + unexpiredSubset.forEach { + val application = createApplication(userEntity, offenderDetails) + val statusUpdate = createStatusUpdate(it, application) + statusUpdate.createdAt = unexpiredDateTime() + cas2BailStatusUpdateRepository.save(statusUpdate) + unexpiredApplicationIds.add(application.id) + } + + expiredSubset.forEach { + val application = createApplication(userEntity, offenderDetails) + val statusUpdate = createStatusUpdate(it, application) + statusUpdate.createdAt = expiredDateTime() + cas2BailStatusUpdateRepository.save(statusUpdate) + expiredApplicationIds.add(application.id) + } + + val rawResponseBody = webTestClient.get() + .uri("/cas2bail/applications") + .header("Authorization", "Bearer $jwt") + .header("X-Service-Name", ServiceName.cas2.value) + .exchange() + .expectStatus() + .isOk + .returnResult() + .responseBody + .blockFirst() + + val responseBody = + objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) + + val returnedApplicationIds = responseBody.map { it.id }.toSet() + + Assertions.assertThat(returnedApplicationIds.equals(unexpiredApplicationIds)).isTrue() + } + } + } + + @Test + fun `Get all applications returns 200 with correct body`() { + givenACas2PomUser { userEntity, jwt -> + givenACas2PomUser { otherUser, _ -> + givenAnOffender { offenderDetails, _ -> + cas2ApplicationJsonSchemaRepository.deleteAll() + + val applicationSchema = cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + withAddedAt(OffsetDateTime.now()) + withId(UUID.randomUUID()) + } + + // abandoned application + val abandonedApplicationEntity = cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(userEntity) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + withCreatedAt(OffsetDateTime.parse("2024-01-03T16:10:00+01:00")) + withAbandonedAt(OffsetDateTime.now()) + } + + // unsubmitted application + val firstApplicationEntity = cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(userEntity) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + withCreatedAt(OffsetDateTime.parse("2024-01-03T16:10:00+01:00")) + withHdcEligibilityDate(LocalDate.now().plusMonths(3)) + } + + // submitted application, CRD >= today so should be returned + val secondApplicationEntity = cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(userEntity) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + withCreatedAt(OffsetDateTime.parse("2024-02-29T09:00:00+01:00")) + withSubmittedAt(OffsetDateTime.now()) + withConditionalReleaseDate(LocalDate.now()) + } + + // submitted application, CRD = yesterday, so should not be returned + val thirdApplicationEntity = cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(userEntity) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + withCreatedAt(OffsetDateTime.parse("2024-02-29T09:00:00+01:00")) + withSubmittedAt(OffsetDateTime.now()) + withConditionalReleaseDate(LocalDate.now().minusDays(1)) + } + + val statusUpdate = cas2StatusUpdateEntityFactory.produceAndPersist { + withLabel("More information requested") + withApplication(secondApplicationEntity) + withAssessor(externalUserEntityFactory.produceAndPersist()) + } + + val otherCas2ApplicationEntity = cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(otherUser) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + } + + val rawResponseBody = webTestClient.get() + .uri("/cas2/applications") + .header("Authorization", "Bearer $jwt") + .header("X-Service-Name", ServiceName.cas2.value) + .exchange() + .expectStatus() + .isOk + .returnResult() + .responseBody + .blockFirst() + + val responseBody = + objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) + + // check transformers were able to return all fields + Assertions.assertThat(responseBody).anyMatch { + firstApplicationEntity.id == it.id && + firstApplicationEntity.crn == it.crn && + firstApplicationEntity.nomsNumber == it.nomsNumber && + "${offenderDetails.firstName} ${offenderDetails.surname}" == it.personName && + firstApplicationEntity.createdAt.toInstant() == it.createdAt && + firstApplicationEntity.createdByUser.id == it.createdByUserId && + firstApplicationEntity.submittedAt?.toInstant() == it.submittedAt && + firstApplicationEntity.hdcEligibilityDate == it.hdcEligibilityDate && + firstApplicationEntity.createdByUser.name == it.createdByUserName + } + + Assertions.assertThat(responseBody).noneMatch { + thirdApplicationEntity.id == it.id + } + + Assertions.assertThat(responseBody).noneMatch { + otherCas2ApplicationEntity.id == it.id + } + + Assertions.assertThat(responseBody).noneMatch { + abandonedApplicationEntity.id == it.id + } + + Assertions.assertThat(responseBody[0].createdAt) + .isEqualTo(secondApplicationEntity.createdAt.toInstant()) + + Assertions.assertThat(responseBody[0].latestStatusUpdate!!.label) + .isEqualTo(statusUpdate.label) + + Assertions.assertThat(responseBody[1].createdAt) + .isEqualTo(firstApplicationEntity.createdAt.toInstant()) + } + } + } + } + + @Test + fun `Get all applications with pagination returns 200 with correct body and header`() { + givenACas2PomUser { userEntity, jwt -> + givenACas2PomUser { otherUser, _ -> + givenAnOffender { offenderDetails, _ -> + cas2ApplicationJsonSchemaRepository.deleteAll() + + val applicationSchema = cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + withAddedAt(OffsetDateTime.now()) + withId(UUID.randomUUID()) + } + + repeat(12) { + cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(userEntity) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + withCreatedAt(OffsetDateTime.now().randomDateTimeBefore(14)) + } + } + + val rawResponseBodyPage1 = webTestClient.get() + .uri("/cas2/applications?page=1") + .header("Authorization", "Bearer $jwt") + .header("X-Service-Name", ServiceName.cas2.value) + .exchange() + .expectStatus() + .isOk + .expectHeader().valueEquals("X-Pagination-CurrentPage", 1) + .expectHeader().valueEquals("X-Pagination-TotalPages", 2) + .expectHeader().valueEquals("X-Pagination-TotalResults", 12) + .expectHeader().valueEquals("X-Pagination-PageSize", 10) + .returnResult() + .responseBody + .blockFirst() + + val responseBodyPage1 = + objectMapper.readValue(rawResponseBodyPage1, object : TypeReference>() {}) + + Assertions.assertThat(responseBodyPage1).size().isEqualTo(10) + + Assertions.assertThat(isOrderedByCreatedAtDescending(responseBodyPage1)).isTrue() + + val rawResponseBodyPage2 = webTestClient.get() + .uri("/cas2/applications?page=2") + .header("Authorization", "Bearer $jwt") + .header("X-Service-Name", ServiceName.cas2.value) + .exchange() + .expectStatus() + .isOk + .expectHeader().valueEquals("X-Pagination-CurrentPage", 2) + .expectHeader().valueEquals("X-Pagination-TotalPages", 2) + .expectHeader().valueEquals("X-Pagination-TotalResults", 12) + .expectHeader().valueEquals("X-Pagination-PageSize", 10) + .returnResult() + .responseBody + .blockFirst() + + val responseBodyPage2 = + objectMapper.readValue(rawResponseBodyPage2, object : TypeReference>() {}) + + Assertions.assertThat(responseBodyPage2).size().isEqualTo(2) + } + } + } + } + + @Test + fun `When a person is not found, returns 200 with placeholder text`() { + givenACas2PomUser { userEntity, jwt -> + val crn = "X1234" + + produceAndPersistBasicApplication(crn, userEntity) + communityAPIMockNotFoundOffenderDetailsCall(crn) + + webTestClient.get() + .uri("/cas2/applications") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isOk + .expectBody() + .jsonPath("$[0].personName").isEqualTo("Person Not Found") + } + } + + /** + * Returns true if the list of application summaries is sorted by descending created_at + * or false if not. + * + * Works by calculating the difference in seconds between two dates and using the sign + * of this difference. If two dates are descending then the difference will be positive. + * If two dates are ascending the difference will be negative (which is set to 0). + * + * For a list of dates, the cumulative multiple of these signs will be 1 if all + * dates in the range are descending (= 1 x 1 x 1 etc.). + * + * If any dates are ascending the multiple will be 0 ( = 1 x 1 x 0 etc.). + * + * If all dates are ascending the multiple will also be 0 ( = 1 x 0 x 0 etc.). + */ + private fun isOrderedByCreatedAtDescending(responseBody: List): Boolean { + var allDescending = 1 + for (i in 1..(responseBody.size - 1)) { + val isDescending = (responseBody[i - 1].createdAt.epochSecond - responseBody[i].createdAt.epochSecond).sign + allDescending *= if (isDescending > 0) 1 else 0 + } + return allDescending == 1 + } + + @Nested + inner class WithPrisonCode { + @Test + fun `Get all applications using prisonCode returns 200 with correct body`() { + givenACas2Assessor { assessor, _ -> + givenACas2PomUser { userAPrisonA, jwt -> + givenAnOffender { offenderDetails, _ -> + cas2ApplicationJsonSchemaRepository.deleteAll() + + val applicationSchema = cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + withAddedAt(OffsetDateTime.now()) + withId(UUID.randomUUID()) + } + + val userAPrisonAApplicationIds = mutableListOf() + + repeat(5) { + userAPrisonAApplicationIds.add( + cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(userAPrisonA) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + withCreatedAt(OffsetDateTime.now().randomDateTimeBefore(14)) + withReferringPrisonCode(userAPrisonA.activeCaseloadId!!) + }.id, + ) + } + + val userBPrisonA = nomisUserEntityFactory.produceAndPersist { + withActiveCaseloadId(userAPrisonA.activeCaseloadId!!) + } + + val userBPrisonAApplicationIds = mutableListOf() + + // submitted applications with conditional release dates in the future + repeat(6) { + userBPrisonAApplicationIds.add( + cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(userBPrisonA) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + withCreatedAt(OffsetDateTime.now().minusDays(it.toLong())) + withReferringPrisonCode(userAPrisonA.activeCaseloadId!!) + withSubmittedAt(OffsetDateTime.now().minusDays(it.toLong())) + withConditionalReleaseDate(LocalDate.now().randomDateAfter(14)) + }.id, + ) + } + + // submitted applications with conditional release dates today + repeat(2) { + userBPrisonAApplicationIds.add( + cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(userBPrisonA) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + withCreatedAt(OffsetDateTime.now().minusDays(it.toLong() + 6)) + withReferringPrisonCode(userAPrisonA.activeCaseloadId!!) + withSubmittedAt(OffsetDateTime.now().minusDays(it.toLong() + 6)) + withConditionalReleaseDate(LocalDate.now()) + }.id, + ) + } + + // submitted application with a conditional release date before today + val excludedApplicationId = cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(userBPrisonA) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + withCreatedAt(OffsetDateTime.now().minusDays(14)) + withReferringPrisonCode(userAPrisonA.activeCaseloadId!!) + withSubmittedAt(OffsetDateTime.now()) + withConditionalReleaseDate(LocalDate.now().randomDateBefore(14)) + }.id + + addStatusUpdates(userBPrisonAApplicationIds.first(), assessor) + + val userCPrisonB = nomisUserEntityFactory.produceAndPersist { + withActiveCaseloadId("another prison") + } + + val otherPrisonApplication = cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(userCPrisonB) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + withCreatedAt(OffsetDateTime.now().randomDateTimeBefore(14)) + withReferringPrisonCode(userCPrisonB.activeCaseloadId!!) + } + + val rawResponseBody = webTestClient.get() + .uri("/cas2/applications?prisonCode=${userAPrisonA.activeCaseloadId}") + .header("Authorization", "Bearer $jwt") + .header("X-Service-Name", ServiceName.cas2.value) + .exchange() + .expectStatus() + .isOk + .returnResult() + .responseBody + .blockFirst() + + val responseBody = + objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) + + Assertions.assertThat(responseBody).noneMatch { + excludedApplicationId == it.id + } + + val returnedApplicationIds = responseBody.map { it.id }.toSet() + + Assertions.assertThat(returnedApplicationIds).isEqualTo( + userAPrisonAApplicationIds.toSet().union(userBPrisonAApplicationIds.toSet()), + ) + + Assertions.assertThat(responseBody).noneMatch { + otherPrisonApplication.id == it.id + } + + Assertions.assertThat(responseBody[0].latestStatusUpdate?.label).isEqualTo("Awaiting decision") + Assertions.assertThat(responseBody[0].latestStatusUpdate?.statusId).isEqualTo(UUID.fromString("c74c3e54-52d8-4aa2-86f6-05190985efee")) + } + } + } + } + + @Test + fun `Get all submitted applications using prisonCode returns 200 with correct body`() { + givenACas2Assessor { assessor, _ -> + givenACas2PomUser { userAPrisonA, jwt -> + givenAnOffender { offenderDetails, _ -> + cas2ApplicationJsonSchemaRepository.deleteAll() + + val applicationSchema = cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + withAddedAt(OffsetDateTime.now()) + withId(UUID.randomUUID()) + } + + val userAPrisonAApplicationIds = mutableListOf() + + repeat(5) { + userAPrisonAApplicationIds.add( + cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(userAPrisonA) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + withCreatedAt(OffsetDateTime.now().minusDays(it.toLong())) + withReferringPrisonCode(userAPrisonA.activeCaseloadId!!) + }.id, + ) + } + + val userBPrisonA = nomisUserEntityFactory.produceAndPersist { + withActiveCaseloadId(userAPrisonA.activeCaseloadId!!) + } + + val userBPrisonAApplicationIds = mutableListOf() + + repeat(6) { + userBPrisonAApplicationIds.add( + cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(userBPrisonA) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + withCreatedAt(OffsetDateTime.now().minusDays(it.toLong())) + withSubmittedAt(OffsetDateTime.now().randomDateTimeBefore(14)) + withReferringPrisonCode(userAPrisonA.activeCaseloadId!!) + withConditionalReleaseDate(LocalDate.now().randomDateAfter(14)) + }.id, + ) + } + + // submitted applications with conditional release dates today + repeat(2) { + userBPrisonAApplicationIds.add( + cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(userBPrisonA) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + withCreatedAt(OffsetDateTime.now().minusDays(it.toLong() + 6)) + withReferringPrisonCode(userAPrisonA.activeCaseloadId!!) + withSubmittedAt(OffsetDateTime.now().minusDays(it.toLong() + 6)) + withConditionalReleaseDate(LocalDate.now()) + }.id, + ) + } + + // submitted application with a conditional release date before today + val excludedApplicationId = cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(userBPrisonA) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + withCreatedAt(OffsetDateTime.now().minusDays(14)) + withReferringPrisonCode(userAPrisonA.activeCaseloadId!!) + withSubmittedAt(OffsetDateTime.now()) + withConditionalReleaseDate(LocalDate.now().randomDateBefore(14)) + }.id + + addStatusUpdates(userBPrisonAApplicationIds.first(), assessor) + + val rawResponseBody = webTestClient.get() + .uri("/cas2/applications?isSubmitted=true&prisonCode=${userAPrisonA.activeCaseloadId}") + .header("Authorization", "Bearer $jwt") + .header("X-Service-Name", ServiceName.cas2.value) + .exchange() + .expectStatus() + .isOk + .returnResult() + .responseBody + .blockFirst() + + val responseBody = + objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) + + Assertions.assertThat(responseBody).noneMatch { + excludedApplicationId == it.id + } + + val returnedApplicationIds = responseBody.map { it.id }.toSet() + + Assertions.assertThat(returnedApplicationIds).isEqualTo(userBPrisonAApplicationIds.toSet()) + Assertions.assertThat(responseBody[0].latestStatusUpdate?.label).isEqualTo("Awaiting decision") + Assertions.assertThat(responseBody[0].latestStatusUpdate?.statusId).isEqualTo(UUID.fromString("c74c3e54-52d8-4aa2-86f6-05190985efee")) + } + } + } + } + + @Test + fun `Get all unsubmitted applications using prisonCode returns 200 with correct body`() { + givenACas2PomUser { userAPrisonA, jwt -> + givenAnOffender { offenderDetails, _ -> + cas2ApplicationJsonSchemaRepository.deleteAll() + + val applicationSchema = cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + withAddedAt(OffsetDateTime.now()) + withId(UUID.randomUUID()) + } + + val userAPrisonAApplicationIds = mutableListOf() + + repeat(5) { + userAPrisonAApplicationIds.add( + cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(userAPrisonA) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + withCreatedAt(OffsetDateTime.now().randomDateTimeBefore(14)) + withReferringPrisonCode(userAPrisonA.activeCaseloadId!!) + }.id, + ) + } + + val userBPrisonA = nomisUserEntityFactory.produceAndPersist { + withActiveCaseloadId(userAPrisonA.activeCaseloadId!!) + } + + val userBPrisonAApplicationIds = mutableListOf() + + repeat(6) { + userBPrisonAApplicationIds.add( + cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(userBPrisonA) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + withCreatedAt(OffsetDateTime.now().randomDateTimeBefore(14)) + withSubmittedAt(OffsetDateTime.now().randomDateTimeBefore(14)) + withReferringPrisonCode(userAPrisonA.activeCaseloadId!!) + }.id, + ) + } + + val rawResponseBody = webTestClient.get() + .uri("/cas2/applications?isSubmitted=false&prisonCode=${userAPrisonA.activeCaseloadId}") + .header("Authorization", "Bearer $jwt") + .header("X-Service-Name", ServiceName.cas2.value) + .exchange() + .expectStatus() + .isOk + .returnResult() + .responseBody + .blockFirst() + + val responseBody = + objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) + + val returnedApplicationIds = responseBody.map { it.id }.toSet() + + Assertions.assertThat(returnedApplicationIds).isEqualTo(userAPrisonAApplicationIds.toSet()) + } + } + } + + @Test + fun `Get applications using another prisonCode returns Forbidden 403`() { + givenACas2PomUser { userAPrisonA, jwt -> + val rawResponseBody = webTestClient.get() + .uri("/cas2/applications?prisonCode=${userAPrisonA.activeCaseloadId!!.reversed()}") + .header("Authorization", "Bearer $jwt") + .header("X-Service-Name", ServiceName.cas2.value) + .exchange() + .expectStatus() + .isForbidden() + } + } + } + + @Nested + inner class AsLicenceCaseAdminUser { + @Test + fun `Get all submitted applications using prisonCode returns 200 with correct body`() { + givenACas2Assessor { assessor, _ -> + givenACas2LicenceCaseAdminUser { caseAdminPrisonA, jwt -> + givenAnOffender { offenderDetails, _ -> + cas2ApplicationJsonSchemaRepository.deleteAll() + + val applicationSchema = cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + withAddedAt(OffsetDateTime.now()) + withId(UUID.randomUUID()) + } + + val pomUserPrisonA = nomisUserEntityFactory.produceAndPersist { + withActiveCaseloadId(caseAdminPrisonA.activeCaseloadId!!) + } + + val userBPrisonAApplicationIds = mutableListOf() + + // submitted applications with conditional release dates in the future + repeat(6) { + userBPrisonAApplicationIds.add( + cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(pomUserPrisonA) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + withCreatedAt(OffsetDateTime.now().minusDays(it.toLong())) + withSubmittedAt(OffsetDateTime.now().randomDateTimeBefore(14)) + withReferringPrisonCode(caseAdminPrisonA.activeCaseloadId!!) + withConditionalReleaseDate(LocalDate.now().randomDateAfter(14)) + }.id, + ) + } + + // submitted applications with conditional release date of today + repeat(2) { + userBPrisonAApplicationIds.add( + cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(pomUserPrisonA) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + withCreatedAt(OffsetDateTime.now().minusDays(it.toLong() + 6)) + withSubmittedAt(OffsetDateTime.now().randomDateTimeBefore(14)) + withReferringPrisonCode(caseAdminPrisonA.activeCaseloadId!!) + withConditionalReleaseDate(LocalDate.now()) + }.id, + ) + } + + // submitted application with a conditional release date before today + val excludedApplicationId = cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(pomUserPrisonA) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + withCreatedAt(OffsetDateTime.now().randomDateTimeBefore(14)) + withSubmittedAt(OffsetDateTime.now().randomDateTimeBefore(14)) + withReferringPrisonCode(caseAdminPrisonA.activeCaseloadId!!) + withConditionalReleaseDate(LocalDate.now().randomDateBefore(14)) + }.id + + val pomUserPrisonB = nomisUserEntityFactory.produceAndPersist { + withActiveCaseloadId("other_prison") + } + + val otherPrisonApplication = cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(pomUserPrisonB) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + withCreatedAt(OffsetDateTime.now()) + withSubmittedAt(OffsetDateTime.now().randomDateTimeBefore(14)) + withReferringPrisonCode("other_prison") + } + + val rawResponseBody = webTestClient.get() + .uri("/cas2/applications?isSubmitted=true&prisonCode=${caseAdminPrisonA.activeCaseloadId}") + .header("Authorization", "Bearer $jwt") + .header("X-Service-Name", ServiceName.cas2.value) + .exchange() + .expectStatus() + .isOk + .returnResult() + .responseBody + .blockFirst() + + val responseBody = + objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) + + Assertions.assertThat(responseBody).noneMatch { + excludedApplicationId == it.id + } + val returnedApplicationIds = responseBody.map { it.id }.toSet() + + Assertions.assertThat(returnedApplicationIds).isEqualTo(userBPrisonAApplicationIds.toSet()) + Assertions.assertThat(returnedApplicationIds).noneMatch { + otherPrisonApplication.id == it + } + } + } + } + } + } + } + + private fun addStatusUpdates(applicationId: UUID, assessor: ExternalUserEntity) { + cas2StatusUpdateEntityFactory.produceAndPersist { + withLabel("More information requested") + withApplication(cas2ApplicationRepository.findById(applicationId).get()) + withAssessor(assessor) + } + // this is the one that should be returned as latestStatusUpdate + cas2StatusUpdateEntityFactory.produceAndPersist { + withStatusId(UUID.fromString("c74c3e54-52d8-4aa2-86f6-05190985efee")) + withLabel("Awaiting decision") + withApplication(cas2ApplicationRepository.findById(applicationId).get()) + withAssessor(assessor) + } + } + + @Nested + inner class GetToIndexUsingIsSubmitted { + + var jwtForUser: String? = null + val submittedIds = mutableSetOf() + val unSubmittedIds = mutableSetOf() + lateinit var excludedApplicationId: UUID + + @BeforeEach + fun setup() { + givenACas2Assessor { assessor, _ -> + givenACas2PomUser { userEntity, jwt -> + givenACas2PomUser { otherUser, _ -> + givenAnOffender { offenderDetails, _ -> + cas2ApplicationJsonSchemaRepository.deleteAll() + + val applicationSchema = cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + withAddedAt(OffsetDateTime.now()) + withId(UUID.randomUUID()) + } + + // create 3 x submitted applications for this user + // with most recent first and conditional release dates in the future + repeat(3) { + submittedIds.add( + cas2ApplicationEntityFactory.produceAndPersist { + withCreatedAt(OffsetDateTime.now().minusDays(it.toLong())) + withApplicationSchema(applicationSchema) + withCreatedByUser(userEntity) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + withSubmittedAt(OffsetDateTime.now().minusDays(it.toLong())) + withConditionalReleaseDate(LocalDate.now().randomDateAfter(14)) + }.id, + ) + } + + // create 2 x submitted applications for this user + // with most recent first and conditional release dates of today + repeat(2) { + submittedIds.add( + cas2ApplicationEntityFactory.produceAndPersist { + withCreatedAt(OffsetDateTime.now().minusDays(it.toLong() + 3)) + withApplicationSchema(applicationSchema) + withCreatedByUser(userEntity) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + withSubmittedAt(OffsetDateTime.now().minusDays(it.toLong() + 3)) + withConditionalReleaseDate(LocalDate.now()) + }.id, + ) + } + + // submitted application with a conditional release date before today + excludedApplicationId = cas2ApplicationEntityFactory.produceAndPersist { + withCreatedAt(OffsetDateTime.now().minusDays(14)) + withApplicationSchema(applicationSchema) + withCreatedByUser(userEntity) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + withSubmittedAt(OffsetDateTime.now()) + withConditionalReleaseDate(LocalDate.now().randomDateBefore(14)) + }.id + + addStatusUpdates(submittedIds.first(), assessor) + + // create 4 x un-submitted in-progress applications for this user + repeat(4) { + unSubmittedIds.add( + cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(userEntity) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + }.id, + ) + } + + // create a submitted application by another user which should not be in results + cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(otherUser) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + withSubmittedAt(OffsetDateTime.now()) + } + + // create an unsubmitted application by another user which should not be in results + cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(otherUser) + withCrn(offenderDetails.otherIds.crn) + withData("{}") + } + + jwtForUser = jwt + } + } + } + } + } + + @Test + fun `returns all applications for user when isSubmitted is null`() { + val rawResponseBody = webTestClient.get() + .uri("/cas2/applications") + .header("Authorization", "Bearer $jwtForUser") + .header("X-Service-Name", ServiceName.cas2.value) + .exchange() + .expectStatus() + .isOk + .returnResult() + .responseBody + .blockFirst() + + val responseBody = + objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) + + Assertions.assertThat(responseBody).noneMatch { + excludedApplicationId == it.id + } + + val uuids = responseBody.map { it.id }.toSet() + Assertions.assertThat(uuids).isEqualTo(submittedIds.union(unSubmittedIds)) + } + + @Test + fun `returns submitted applications for user when isSubmitted is true`() { + val rawResponseBody = webTestClient.get() + .uri("/cas2/applications?isSubmitted=true") + .header("Authorization", "Bearer $jwtForUser") + .header("X-Service-Name", ServiceName.cas2.value) + .exchange() + .expectStatus() + .isOk + .returnResult() + .responseBody + .blockFirst() + + val responseBody = + objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) + + Assertions.assertThat(responseBody).noneMatch { + excludedApplicationId == it.id + } + + val uuids = responseBody.map { it.id }.toSet() + Assertions.assertThat(uuids).isEqualTo(submittedIds) + Assertions.assertThat(responseBody[0].latestStatusUpdate?.label).isEqualTo("Awaiting decision") + Assertions.assertThat(responseBody[0].latestStatusUpdate?.statusId).isEqualTo(UUID.fromString("c74c3e54-52d8-4aa2-86f6-05190985efee")) + } + + @Test + fun `returns submitted applications for user when isSubmitted is true and page specified`() { + val rawResponseBody = webTestClient.get() + .uri("/cas2/applications?isSubmitted=true&page=1") + .header("Authorization", "Bearer $jwtForUser") + .header("X-Service-Name", ServiceName.cas2.value) + .exchange() + .expectStatus() + .isOk + .returnResult() + .responseBody + .blockFirst() + + val responseBody = + objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) + + val uuids = responseBody.map { it.id }.toSet() + Assertions.assertThat(uuids).isEqualTo(submittedIds) + Assertions.assertThat(responseBody[0].latestStatusUpdate?.label).isEqualTo("Awaiting decision") + Assertions.assertThat(responseBody[0].latestStatusUpdate?.statusId).isEqualTo(UUID.fromString("c74c3e54-52d8-4aa2-86f6-05190985efee")) + } + + @Test + fun `returns unsubmitted applications for user when isSubmitted is false`() { + val rawResponseBody = webTestClient.get() + .uri("/cas2/applications?isSubmitted=false") + .header("Authorization", "Bearer $jwtForUser") + .header("X-Service-Name", ServiceName.cas2.value) + .exchange() + .expectStatus() + .isOk + .returnResult() + .responseBody + .blockFirst() + + val responseBody = + objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) + + val uuids = responseBody.map { it.id }.toSet() + Assertions.assertThat(uuids).isEqualTo(unSubmittedIds) + } + } + + @Nested + inner class GetToShow { + + @Nested + inner class WhenCreatedBySameUser { + // When the application requested was created by the logged-in user + @Test + fun `Get single in progress application returns 200 with correct body`() { + givenACas2PomUser { userEntity, jwt -> + givenAnOffender { offenderDetails, inmateDetails -> + cas2ApplicationJsonSchemaRepository.deleteAll() + + val newestJsonSchema = cas2ApplicationJsonSchemaEntityFactory + .produceAndPersist { + withAddedAt(OffsetDateTime.parse("2022-09-21T12:45:00+01:00")) + withSchema( + schema, + ) + } + + val applicationEntity = cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(newestJsonSchema) + withCrn(offenderDetails.otherIds.crn) + withCreatedByUser(userEntity) + withData( + data, + ) + } + + val rawResponseBody = webTestClient.get() + .uri("/cas2/applications/${applicationEntity.id}") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isOk + .returnResult() + .responseBody + .blockFirst() + + val responseBody = objectMapper.readValue( + rawResponseBody, + Cas2Application::class.java, + ) + + Assertions.assertThat(responseBody).matches { + applicationEntity.id == it.id && + applicationEntity.crn == it.person.crn && + applicationEntity.createdAt.toInstant() == it.createdAt && + applicationEntity.createdByUser.id == it.createdBy.id && + applicationEntity.submittedAt?.toInstant() == it.submittedAt && + serializableToJsonNode(applicationEntity.data) == serializableToJsonNode(it.data) && + newestJsonSchema.id == it.schemaVersion && !it.outdatedSchema + } + } + } + } + + @Test + fun `Get single application returns successfully when the offender cannot be fetched from the prisons API`() { + givenACas2PomUser { userEntity, jwt -> + val crn = "X1234" + + givenAnOffender( + offenderDetailsConfigBlock = { + withCrn(crn) + withNomsNumber("ABC123") + }, + ) { offenderDetails, _ -> + val application = produceAndPersistBasicApplication(crn, userEntity) + + prisonAPIMockNotFoundInmateDetailsCall(offenderDetails.otherIds.nomsNumber!!) + loadPreemptiveCacheForInmateDetails(offenderDetails.otherIds.nomsNumber!!) + + val rawResponseBody = webTestClient.get() + .uri("/cas2/applications/${application.id}") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isOk + .returnResult() + .responseBody + .blockFirst() + + val responseBody = objectMapper.readValue( + rawResponseBody, + Cas2Application::class.java, + ) + + Assertions.assertThat(responseBody.person is FullPerson).isTrue + + Assertions.assertThat(responseBody).matches { + val person = it.person as FullPerson + + application.id == it.id && + application.crn == person.crn && + person.nomsNumber == null && + person.status == PersonStatus.unknown && + person.prisonName == null + } + } + } + } + + @Test + fun `Get single submitted application returns 200 with timeline events`() { + givenACas2PomUser { userEntity, jwt -> + givenAnOffender { offenderDetails, inmateDetails -> + cas2ApplicationJsonSchemaRepository.deleteAll() + + val newestJsonSchema = cas2ApplicationJsonSchemaEntityFactory + .produceAndPersist { + withAddedAt(OffsetDateTime.parse("2022-09-21T12:45:00+01:00")) + withSchema( + schema, + ) + } + + val applicationEntity = cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(newestJsonSchema) + withCrn(offenderDetails.otherIds.crn) + withCreatedByUser(userEntity) + withSubmittedAt(OffsetDateTime.now().minusDays(1)) + } + + cas2AssessmentEntityFactory.produceAndPersist { + withApplication(applicationEntity) + } + + val rawResponseBody = webTestClient.get() + .uri("/cas2/applications/${applicationEntity.id}") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isOk + .returnResult() + .responseBody + .blockFirst() + + val responseBody = objectMapper.readValue( + rawResponseBody, + Cas2Application::class.java, + ) + + Assertions.assertThat(responseBody.assessment!!.statusUpdates).isEqualTo(emptyList()) + + Assertions.assertThat(responseBody.timelineEvents!!.map { event -> event.label }) + .isEqualTo(listOf("Application submitted")) + } + } + } + } + + @Nested + inner class WhenCreatedByDifferentUser { + + @Nested + inner class WhenDifferentPrison { + @Test + fun `Get single submitted application is forbidden`() { + givenACas2PomUser { userEntity, jwt -> + givenAnOffender { offenderDetails, inmateDetails -> + cas2ApplicationJsonSchemaRepository.deleteAll() + + val otherUser = nomisUserEntityFactory.produceAndPersist { + withActiveCaseloadId("other_caseload") + } + + val newestJsonSchema = cas2ApplicationJsonSchemaEntityFactory + .produceAndPersist { + withAddedAt(OffsetDateTime.parse("2022-09-21T12:45:00+01:00")) + withSchema( + schema, + ) + } + + val applicationEntity = cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(newestJsonSchema) + withCrn(offenderDetails.otherIds.crn) + withSubmittedAt(OffsetDateTime.now()) + withCreatedByUser(otherUser) + withData( + data, + ) + } + + webTestClient.get() + .uri("/cas2/applications/${applicationEntity.id}") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isForbidden + } + } + } + } + + @Nested + inner class WhenSamePrison { + @Test + fun `Get single submitted application returns 200 with timeline events`() { + givenACas2PomUser { userEntity, jwt -> + givenAnOffender { offenderDetails, inmateDetails -> + cas2ApplicationJsonSchemaRepository.deleteAll() + + val newestJsonSchema = cas2ApplicationJsonSchemaEntityFactory + .produceAndPersist { + withAddedAt(OffsetDateTime.parse("2022-09-21T12:45:00+01:00")) + withSchema( + schema, + ) + } + + val otherUser = nomisUserEntityFactory.produceAndPersist { + withActiveCaseloadId(userEntity.activeCaseloadId!!) + } + + val applicationEntity = cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(newestJsonSchema) + withCrn(offenderDetails.otherIds.crn) + withCreatedByUser(otherUser) + withSubmittedAt(OffsetDateTime.now().minusDays(1)) + withReferringPrisonCode(userEntity.activeCaseloadId!!) + } + + cas2AssessmentEntityFactory.produceAndPersist { + withApplication(applicationEntity) + } + + val rawResponseBody = webTestClient.get() + .uri("/cas2/applications/${applicationEntity.id}") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isOk + .returnResult() + .responseBody + .blockFirst() + + val responseBody = objectMapper.readValue( + rawResponseBody, + Cas2Application::class.java, + ) + + Assertions.assertThat(responseBody.assessment!!.statusUpdates).isEqualTo(emptyList()) + + Assertions.assertThat(responseBody.timelineEvents!!.map { event -> event.label }) + .isEqualTo(listOf("Application submitted")) + } + } + } + + @Test + fun `Get single unsubmitted application returns 403`() { + givenACas2PomUser { userEntity, jwt -> + givenAnOffender { offenderDetails, inmateDetails -> + cas2ApplicationJsonSchemaRepository.deleteAll() + + val newestJsonSchema = cas2ApplicationJsonSchemaEntityFactory + .produceAndPersist { + withAddedAt(OffsetDateTime.parse("2022-09-21T12:45:00+01:00")) + withSchema( + schema, + ) + } + + val otherUser = nomisUserEntityFactory.produceAndPersist { + withActiveCaseloadId(userEntity.activeCaseloadId!!) + } + + val applicationEntity = cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(newestJsonSchema) + withCrn(offenderDetails.otherIds.crn) + withCreatedByUser(otherUser) + withReferringPrisonCode(userEntity.activeCaseloadId!!) + } + + webTestClient.get() + .uri("/cas2/applications/${applicationEntity.id}") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isForbidden + } + } + } + } + } + } + + @Nested + inner class PostToCreate { + + @Nested + inner class PomUsers { + @Test + fun `Create new application for CAS-2 returns 201 with correct body and Location header`() { + givenACas2PomUser { userEntity, jwt -> + givenAnOffender { offenderDetails, _ -> + val applicationSchema = + cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + withAddedAt(OffsetDateTime.now()) + withId(UUID.randomUUID()) + } + + val result = webTestClient.post() + .uri("/cas2/applications") + .header("Authorization", "Bearer $jwt") + .header("X-Service-Name", ServiceName.cas2.value) + .bodyValue( + NewApplication( + crn = offenderDetails.otherIds.crn, + ), + ) + .exchange() + .expectStatus() + .isCreated + .returnResult(Cas2Application::class.java) + + Assertions.assertThat(result.responseHeaders["Location"]).anyMatch { + it.matches(Regex("/cas2/applications/.+")) + } + + Assertions.assertThat(result.responseBody.blockFirst()).matches { + it.person.crn == offenderDetails.otherIds.crn && + it.schemaVersion == applicationSchema.id + } + } + } + } + + @Test + fun `Create new application returns 404 when a person cannot be found`() { + givenACas2PomUser { userEntity, jwt -> + val crn = "X1234" + + communityAPIMockNotFoundOffenderDetailsCall(crn) + + cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + withAddedAt(OffsetDateTime.now()) + withId(UUID.randomUUID()) + } + + webTestClient.post() + .uri("/cas2/applications") + .header("Authorization", "Bearer $jwt") + .bodyValue( + NewApplication( + crn = crn, + ), + ) + .exchange() + .expectStatus() + .isNotFound + .expectBody() + .jsonPath("$.detail").isEqualTo("No Offender with an ID of $crn could be found") + } + } + } + + @Nested + inner class LicenceCaseAdminUsers { + @Test + fun `Create new application for CAS-2 returns 201 with correct body and Location header`() { + givenACas2LicenceCaseAdminUser { _, jwt -> + givenAnOffender { offenderDetails, _ -> + val applicationSchema = + cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + withAddedAt(OffsetDateTime.now()) + withId(UUID.randomUUID()) + } + + val result = webTestClient.post() + .uri("/cas2/applications") + .header("Authorization", "Bearer $jwt") + .header("X-Service-Name", ServiceName.cas2.value) + .bodyValue( + NewApplication( + crn = offenderDetails.otherIds.crn, + ), + ) + .exchange() + .expectStatus() + .isCreated + .returnResult(Cas2Application::class.java) + + Assertions.assertThat(result.responseHeaders["Location"]).anyMatch { + it.matches(Regex("/cas2/applications/.+")) + } + + Assertions.assertThat(result.responseBody.blockFirst()).matches { + it.person.crn == offenderDetails.otherIds.crn && + it.schemaVersion == applicationSchema.id + } + } + } + } + + @Test + fun `Create new application returns 404 when a person cannot be found`() { + givenACas2LicenceCaseAdminUser { _, jwt -> + val crn = "X1234" + + communityAPIMockNotFoundOffenderDetailsCall(crn) + + cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + withAddedAt(OffsetDateTime.now()) + withId(UUID.randomUUID()) + } + + webTestClient.post() + .uri("/cas2/applications") + .header("Authorization", "Bearer $jwt") + .bodyValue( + NewApplication( + crn = crn, + ), + ) + .exchange() + .expectStatus() + .isNotFound + .expectBody() + .jsonPath("$.detail").isEqualTo("No Offender with an ID of $crn could be found") + } + } + } + } + + @Nested + inner class PutToUpdate { + + @Nested + inner class PomUsers { + @Test + fun `Update existing CAS2 application returns 200 with correct body`() { + givenACas2PomUser { submittingUser, jwt -> + givenAnOffender { offenderDetails, _ -> + val applicationId = UUID.fromString("22ceda56-98b2-411d-91cc-ace0ab8be872") + + val applicationSchema = + cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + withAddedAt(OffsetDateTime.now()) + withId(UUID.randomUUID()) + withSchema( + schema, + ) + } + + cas2ApplicationEntityFactory.produceAndPersist { + withCrn(offenderDetails.otherIds.crn) + withId(applicationId) + withApplicationSchema(applicationSchema) + withCreatedByUser(submittingUser) + } + + val resultBody = webTestClient.put() + .uri("/cas2/applications/$applicationId") + .header("Authorization", "Bearer $jwt") + .bodyValue( + UpdateCas2Application( + data = mapOf("thingId" to 123), + type = UpdateApplicationType.CAS2, + ), + ) + .exchange() + .expectStatus() + .isOk + .returnResult(String::class.java) + .responseBody + .blockFirst() + + val result = objectMapper.readValue(resultBody, Application::class.java) + + Assertions.assertThat(result.person.crn).isEqualTo(offenderDetails.otherIds.crn) + } + } + } + } + + @Nested + inner class LicenceCaseAdminUsers { + @Test + fun `Update existing CAS2 application returns 200 with correct body`() { + givenACas2LicenceCaseAdminUser { submittingUser, jwt -> + givenAnOffender { offenderDetails, _ -> + val applicationId = UUID.fromString("22ceda56-98b2-411d-91cc-ace0ab8be872") + + val applicationSchema = + cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + withAddedAt(OffsetDateTime.now()) + withId(UUID.randomUUID()) + withSchema( + schema, + ) + } + + cas2ApplicationEntityFactory.produceAndPersist { + withCrn(offenderDetails.otherIds.crn) + withId(applicationId) + withApplicationSchema(applicationSchema) + withCreatedByUser(submittingUser) + } + + val resultBody = webTestClient.put() + .uri("/cas2/applications/$applicationId") + .header("Authorization", "Bearer $jwt") + .bodyValue( + UpdateCas2Application( + data = mapOf("thingId" to 123), + type = UpdateApplicationType.CAS2, + ), + ) + .exchange() + .expectStatus() + .isOk + .returnResult(String::class.java) + .responseBody + .blockFirst() + + val result = objectMapper.readValue(resultBody, Application::class.java) + + Assertions.assertThat(result.person.crn).isEqualTo(offenderDetails.otherIds.crn) + } + } + } + } + } + + private fun serializableToJsonNode(serializable: Any?): JsonNode { + if (serializable == null) return NullNode.instance + if (serializable is String) return objectMapper.readTree(serializable) + + return objectMapper.readTree(objectMapper.writeValueAsString(serializable)) + } + + private fun produceAndPersistBasicApplication( + crn: String, + userEntity: NomisUserEntity, + ): Cas2ApplicationEntity { + val jsonSchema = cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + withAddedAt(OffsetDateTime.parse("2022-09-21T12:45:00+01:00")) + withSchema( + schema, + ) + } + + val application = cas2ApplicationEntityFactory.produceAndPersist { + withApplicationSchema(jsonSchema) + withCrn(crn) + withCreatedByUser(userEntity) + withData( + data, + ) + } + + return application + } +} \ No newline at end of file diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/repository/ApplicationTestRepository.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/repository/ApplicationTestRepository.kt index 1a256d5386..f0a9ce5c1b 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/repository/ApplicationTestRepository.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/repository/ApplicationTestRepository.kt @@ -5,6 +5,7 @@ import org.springframework.stereotype.Repository import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApprovedPremisesApplicationEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.TemporaryAccommodationApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity import java.util.UUID @Repository @@ -13,5 +14,8 @@ interface ApprovedPremisesApplicationTestRepository : JpaRepository +@Repository +interface Cas2BailApplicationTestRepository : JpaRepository + @Repository interface TemporaryAccommodationApplicationTestRepository : JpaRepository diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/repository/Cas2BailStatusUpdateTestRepository.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/repository/Cas2BailStatusUpdateTestRepository.kt new file mode 100644 index 0000000000..3e61982bf7 --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/repository/Cas2BailStatusUpdateTestRepository.kt @@ -0,0 +1,10 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.repository + + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailStatusUpdateEntity +import java.util.UUID + +@Repository +interface Cas2BailStatusUpdateTestRepository : JpaRepository diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/repository/JsonSchemaTestRepository.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/repository/JsonSchemaTestRepository.kt index b345e64d79..a15a899f01 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/repository/JsonSchemaTestRepository.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/repository/JsonSchemaTestRepository.kt @@ -2,12 +2,7 @@ package uk.gov.justice.digital.hmpps.approvedpremisesapi.repository import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApprovedPremisesApplicationJsonSchemaEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApprovedPremisesAssessmentJsonSchemaEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApprovedPremisesPlacementApplicationJsonSchemaEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationJsonSchemaEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.TemporaryAccommodationApplicationJsonSchemaEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.TemporaryAccommodationAssessmentJsonSchemaEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.* import java.util.UUID @Repository @@ -19,6 +14,9 @@ interface TemporaryAccommodationApplicationJsonSchemaTestRepository : JpaReposit @Repository interface Cas2ApplicationJsonSchemaTestRepository : JpaRepository +@Repository +interface Cas2BailApplicationJsonSchemaTestRepository : JpaRepository + @Repository interface ApprovedPremisesAssessmentJsonSchemaTestRepository : JpaRepository diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailApplicationServiceTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailApplicationServiceTest.kt new file mode 100644 index 0000000000..68749fa937 --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailApplicationServiceTest.kt @@ -0,0 +1,1109 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.unit.service.cas2bail + +import com.fasterxml.jackson.databind.ObjectMapper +import io.mockk.* +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import org.mockito.kotlin.any +import org.springframework.data.domain.Page +import org.springframework.data.domain.PageRequest +import org.springframework.data.domain.Sort +import org.springframework.data.repository.findByIdOrNull +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.SortDirection +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.SubmitCas2Application +import uk.gov.justice.digital.hmpps.approvedpremisesapi.config.NotifyConfig +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.cas2bail.Cas2BailApplicationEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2BailApplicationJsonSchemaEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.prisonsapi.AssignedLivingUnit +import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.AuthorisableActionResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.CasResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.ValidatableActionResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.EmailNotificationService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2bail.Cas2BailApplicationService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2bail.Cas2BailAssessmentService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2bail.Cas2BailUserAccessService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.* +import java.time.LocalDate +import java.time.OffsetDateTime +import java.util.* + +class Cas2BailApplicationServiceTest { + private val mockCas2BailApplicationRepository = mockk() + private val mockCas2BailLockableApplicationRepository = mockk() + private val mockCas2BailApplicationSummaryRepository = mockk() + private val mockJsonSchemaService = mockk() + private val mockOffenderService = mockk() + private val mockCas2BailUserAccessService = mockk() + private val mockDomainEventService = mockk() + private val mockEmailNotificationService = mockk() + private val mockCas2BailAssessmentService = mockk() + private val mockObjectMapper = mockk() + private val mockNotifyConfig = mockk() + + + private val cas2BailApplicationService = Cas2BailApplicationService( + mockCas2BailApplicationRepository, + mockCas2BailLockableApplicationRepository, + mockCas2BailApplicationSummaryRepository, + mockJsonSchemaService, + mockOffenderService, + mockCas2BailUserAccessService, + mockDomainEventService, + mockEmailNotificationService, + mockCas2BailAssessmentService, + mockNotifyConfig, + mockObjectMapper, + "http://frontend/applications/#id", + "http://frontend/assess/applications/#applicationId/overview", + ) + + @Nested + inner class GetAllSubmittedCas2BailApplicationsForAssessor { + @Test + fun `returns Success result with entity from db`() { + val cas2BailApplicationSummary = Cas2BailApplicationSummaryEntity( + id = UUID.fromString("2f838a8c-dffc-48a3-9536-f0e95985e809"), + crn = randomStringMultiCaseWithNumbers(6), + nomsNumber = randomStringMultiCaseWithNumbers(6), + userId = "836a9460-b177-433a-a0d9-262509092c9f", + userName = "first last", + createdAt = OffsetDateTime.parse("2023-04-19T13:25:00+01:00"), + submittedAt = OffsetDateTime.parse("2023-04-19T13:25:30+01:00"), + hdcEligibilityDate = LocalDate.parse("2023-04-29"), + latestStatusUpdateLabel = null, + latestStatusUpdateStatusId = null, + prisonCode = "BRI", + ) + + PaginationConfig(defaultPageSize = 10).postInit() + val page = mockk>() + val pageRequest = mockk() + val pageCriteria = PageCriteria(sortBy = "submitted_at", sortDirection = SortDirection.asc, page = 3) + + mockkStatic(PageRequest::class) + + every { PageRequest.of(2, 10, Sort.by("submitted_at").ascending()) } returns pageRequest + every { page.content } returns listOf(cas2BailApplicationSummary) + every { page.totalPages } returns 10 + every { page.totalElements } returns 100 + + every { + mockCas2BailApplicationSummaryRepository.findBySubmittedAtIsNotNull( + PageRequest.of( + 2, + 10, + Sort.by(Sort.Direction.ASC, "submitted_at"), + ), + ) + } returns page + + val (applicationSummaries, metadata) = cas2BailApplicationService.getAllSubmittedCas2BailApplicationsForAssessor(pageCriteria) + + assertThat(applicationSummaries).isEqualTo(listOf(cas2BailApplicationSummary)) + assertThat(metadata?.currentPage).isEqualTo(3) + assertThat(metadata?.pageSize).isEqualTo(10) + assertThat(metadata?.totalPages).isEqualTo(10) + assertThat(metadata?.totalResults).isEqualTo(100) + } + } + + @Nested + inner class GetCas2BailApplicationsWithPrisonCode { + val cas2BailApplicationSummary = Cas2BailApplicationSummaryEntity( + id = UUID.fromString("2f838a8c-dffc-48a3-9536-f0e95985e809"), + crn = randomStringMultiCaseWithNumbers(6), + nomsNumber = randomStringMultiCaseWithNumbers(6), + userId = "836a9460-b177-433a-a0d9-262509092c9f", + userName = "first last", + createdAt = OffsetDateTime.parse("2023-04-19T13:25:00+01:00"), + submittedAt = OffsetDateTime.parse("2023-04-19T13:25:30+01:00"), + hdcEligibilityDate = LocalDate.parse("2023-04-29"), + latestStatusUpdateLabel = null, + latestStatusUpdateStatusId = null, + prisonCode = "BRI", + ) + val page = mockk>() + val pageCriteria = PageCriteria(sortBy = "submitted_at", sortDirection = SortDirection.asc, page = 3) + val user = NomisUserEntityFactory().produce() + val prisonCode = "BRI" + + private fun testPrisonCodeWithIsSubmitted(isSubmitted: Boolean?) { + every { page.content } returns listOf(cas2BailApplicationSummary) + every { page.totalPages } returns 10 + every { page.totalElements } returns 100 + + val (applicationSummaries, _) = cas2BailApplicationService.getCas2BailApplications(prisonCode, isSubmitted, user, pageCriteria) + + assertThat(applicationSummaries).isEqualTo(listOf(cas2BailApplicationSummary)) + } + + @Test + fun `return all applications when prisonCode is specified and isSubmitted is null`() { + PaginationConfig(defaultPageSize = 10).postInit() + + every { + mockCas2BailApplicationSummaryRepository.findByPrisonCode( + prisonCode, + getPageableOrAllPages(pageCriteria), + ) + } returns page + + testPrisonCodeWithIsSubmitted(null) + } + + @Test + fun `return submitted prison applications when prisonCode is specified and isSubmitted is true`() { + PaginationConfig(defaultPageSize = 10).postInit() + + every { + mockCas2BailApplicationSummaryRepository.findByPrisonCodeAndSubmittedAtIsNotNull( + prisonCode, + getPageableOrAllPages(pageCriteria), + ) + } returns page + + testPrisonCodeWithIsSubmitted(true) + } + + @Test + fun `return unsubmitted prison applications when prisonCode is specified and isSubmitted is false`() { + PaginationConfig(defaultPageSize = 10).postInit() + + every { + mockCas2BailApplicationSummaryRepository.findByPrisonCodeAndSubmittedAtIsNull( + prisonCode, + getPageableOrAllPages(pageCriteria), + ) + } returns page + + testPrisonCodeWithIsSubmitted(false) + } + } + + @Nested + inner class GetCas2BailApplicationForUser { + @Test + fun `where cas2bail application does not exist returns NotFound result`() { + val user = NomisUserEntityFactory().produce() + val applicationId = UUID.fromString("c1750938-19fc-48a1-9ae9-f2e119ffc1f4") + + every { mockCas2BailApplicationRepository.findByIdOrNull(applicationId) } returns null + + assertThat(cas2BailApplicationService.getCas2BailApplicationForUser(applicationId, user) is AuthorisableActionResult.NotFound).isTrue + } + + @Test + fun `where cas2bail application is abandoned returns NotFound result`() { + val user = NomisUserEntityFactory().produce() + val applicationId = UUID.fromString("c1750938-19fc-48a1-9ae9-f2e119ffc1f4") + + every { mockCas2BailApplicationRepository.findByIdOrNull(any()) } returns + Cas2BailApplicationEntityFactory() + .withCreatedByUser( + NomisUserEntityFactory() + .produce(), + ) + .withAbandonedAt(OffsetDateTime.now()) + .produce() + + assertThat(cas2BailApplicationService.getCas2BailApplicationForUser(applicationId, user) is AuthorisableActionResult.NotFound).isTrue + } + + @Test + fun `where user cannot access the cas2bail application returns Unauthorised result`() { + val user = NomisUserEntityFactory() + .produce() + val applicationId = UUID.fromString("c1750938-19fc-48a1-9ae9-f2e119ffc1f4") + + every { mockCas2BailApplicationRepository.findByIdOrNull(any()) } returns + Cas2BailApplicationEntityFactory() + .withCreatedByUser( + NomisUserEntityFactory() + .produce(), + ) + .produce() + + every { mockCas2BailUserAccessService.userCanViewCas2BailApplication(any(), any()) } returns false + + + assertThat(cas2BailApplicationService.getCas2BailApplicationForUser(applicationId, user) is AuthorisableActionResult.Unauthorised).isTrue + } + + @Test + fun `where user can access the cas2bail application returns Success result with entity from db`() { + val distinguishedName = "SOMEPERSON" + val userId = UUID.fromString("239b5e41-f83e-409e-8fc0-8f1e058d417e") + val applicationId = UUID.fromString("c1750938-19fc-48a1-9ae9-f2e119ffc1f4") + + val newestJsonSchema = Cas2BailApplicationJsonSchemaEntityFactory() + .withSchema("{}") + .produce() + + val userEntity = NomisUserEntityFactory() + .withId(userId) + .withNomisUsername(distinguishedName) + .produce() + + val cas2BailApplicationEntity = Cas2BailApplicationEntityFactory() + .withCreatedByUser(userEntity) + .withApplicationSchema(newestJsonSchema) + .produce() + + every { mockJsonSchemaService.checkCas2BailSchemaOutdated(any()) } answers { + it.invocation + .args[0] as Cas2BailApplicationEntity + } + every { mockCas2BailApplicationRepository.findByIdOrNull(any()) } returns cas2BailApplicationEntity + every { mockCas2BailUserAccessService.userCanViewCas2BailApplication(any(), any()) } returns true + + val result = cas2BailApplicationService.getCas2BailApplicationForUser(applicationId, userEntity) + + assertThat(result is AuthorisableActionResult.Success).isTrue + result as AuthorisableActionResult.Success + + assertThat(result.entity).isEqualTo(cas2BailApplicationEntity) + } + } + + @Nested + inner class CreateApplication { + @Test + fun `returns FieldValidationError when Offender is not found`() { + val crn = "CRN345" + val username = "SOMEPERSON" + + every { mockOffenderService.getOffenderByCrn(crn) } returns AuthorisableActionResult.NotFound() + + val user = userWithUsername(username) + + val result = cas2BailApplicationService.createCas2BailApplication(crn, user, "jwt") + + assertThat(result is ValidatableActionResult.FieldValidationError).isTrue + result as ValidatableActionResult.FieldValidationError + assertThat(result.validationMessages).containsEntry("$.crn", "doesNotExist") + } + + @Test + fun `returns FieldValidationError when user is not authorised to view CRN`() { + val crn = "CRN345" + val username = "SOMEPERSON" + + every { mockOffenderService.getOffenderByCrn(crn) } returns AuthorisableActionResult.Unauthorised() + + val user = userWithUsername(username) + + val result = cas2BailApplicationService.createCas2BailApplication(crn, user, "jwt") + + assertThat(result is ValidatableActionResult.FieldValidationError).isTrue + result as ValidatableActionResult.FieldValidationError + assertThat(result.validationMessages).containsEntry("$.crn", "userPermission") + } + + @Test + fun `returns Success with created Application`() { + val crn = "CRN345" + val username = "SOMEPERSON" + + val cas2BailApplicationSchema = Cas2BailApplicationJsonSchemaEntityFactory().produce() + + val user = userWithUsername(username) + + every { mockOffenderService.getOffenderByCrn(crn) } returns AuthorisableActionResult.Success( + OffenderDetailsSummaryFactory().produce(), + ) + + every { mockJsonSchemaService.getNewestSchema(Cas2BailApplicationJsonSchemaEntity::class.java) } returns cas2BailApplicationSchema + every { mockCas2BailApplicationRepository.save(any()) } answers { + it.invocation.args[0] as + Cas2BailApplicationEntity + } + + val result = cas2BailApplicationService.createCas2BailApplication(crn, user, "jwt") + + assertThat(result is ValidatableActionResult.Success).isTrue + result as ValidatableActionResult.Success + assertThat(result.entity.crn).isEqualTo(crn) + assertThat(result.entity.createdByUser).isEqualTo(user) + } + } + + @Nested + inner class UpdateApplication { + val user = NomisUserEntityFactory().produce() + + @Test + fun `returns NotFound when cas2bail application doesn't exist`() { + val applicationId = UUID.fromString("fa6e97ce-7b9e-473c-883c-83b1c2af773d") + + every { mockCas2BailApplicationRepository.findByIdOrNull(applicationId) } returns null + + assertThat( + cas2BailApplicationService.updateCas2BailApplication( + applicationId = applicationId, + data = "{}", + user = user, + ) is AuthorisableActionResult.NotFound, + ).isTrue + } + + @Test + fun `returns Unauthorised when application doesn't belong to request user`() { + val applicationId = UUID.fromString("fa6e97ce-7b9e-473c-883c-83b1c2af773d") + + val cas2BailApplication = Cas2BailApplicationEntityFactory() + .withId(applicationId) + .withYieldedCreatedByUser { + NomisUserEntityFactory() + .produce() + } + .produce() + + every { mockCas2BailApplicationRepository.findByIdOrNull(applicationId) } returns + cas2BailApplication + every { mockJsonSchemaService.checkCas2BailSchemaOutdated(cas2BailApplication) } returns + cas2BailApplication + + assertThat( + cas2BailApplicationService.updateCas2BailApplication( + applicationId = applicationId, + data = "{}", + user = user, + ) is AuthorisableActionResult.Unauthorised, + ).isTrue + } + + @Test + fun `returns GeneralValidationError when cas2bail application has already been submitted`() { + val applicationId = UUID.fromString("fa6e97ce-7b9e-473c-883c-83b1c2af773d") + + val newestSchema = Cas2BailApplicationJsonSchemaEntityFactory().produce() + + val cas2BailApplication = Cas2BailApplicationEntityFactory() + .withApplicationSchema(newestSchema) + .withId(applicationId) + .withCreatedByUser(user) + .withSubmittedAt(OffsetDateTime.now()) + .produce() + .apply { + schemaUpToDate = true + } + + every { mockCas2BailApplicationRepository.findByIdOrNull(applicationId) } returns + cas2BailApplication + every { mockJsonSchemaService.checkCas2BailSchemaOutdated(cas2BailApplication) } returns + cas2BailApplication + + val result = cas2BailApplicationService.updateCas2BailApplication( + applicationId = applicationId, + data = "{}", + user = user, + ) + + assertThat(result is AuthorisableActionResult.Success).isTrue + result as AuthorisableActionResult.Success + + assertThat(result.entity is ValidatableActionResult.GeneralValidationError).isTrue + val validatableActionResult = result.entity as ValidatableActionResult.GeneralValidationError + + assertThat(validatableActionResult.message).isEqualTo("This application has already been submitted") + } + + @Test + fun `returns GeneralValidationError when application has been abandoned`() { + val applicationId = UUID.fromString("fa6e97ce-7b9e-473c-883c-83b1c2af773d") + + val newestSchema = Cas2BailApplicationJsonSchemaEntityFactory().produce() + + val cas2BailApplication = Cas2BailApplicationEntityFactory() + .withApplicationSchema(newestSchema) + .withId(applicationId) + .withCreatedByUser(user) + .withAbandonedAt(OffsetDateTime.now()) + .produce() + .apply { + schemaUpToDate = true + } + + every { mockCas2BailApplicationRepository.findByIdOrNull(applicationId) } returns cas2BailApplication + every { mockJsonSchemaService.checkCas2BailSchemaOutdated(cas2BailApplication) } returns cas2BailApplication + + val result = cas2BailApplicationService.updateCas2BailApplication( + applicationId = applicationId, + data = "{}", + user = user, + ) + + assertThat(result is AuthorisableActionResult.Success).isTrue + result as AuthorisableActionResult.Success + + assertThat(result.entity is ValidatableActionResult.GeneralValidationError).isTrue + val validatableActionResult = result.entity as ValidatableActionResult.GeneralValidationError + + assertThat(validatableActionResult.message).isEqualTo("This application has been abandoned") + } + + @Test + fun `returns GeneralValidationError when application schema is outdated`() { + val applicationId = UUID.fromString("fa6e97ce-7b9e-473c-883c-83b1c2af773d") + + val cas2BailApplication = Cas2BailApplicationEntityFactory() + .withId(applicationId) + .withCreatedByUser(user) + .withSubmittedAt(null) + .produce() + .apply { + schemaUpToDate = false + } + + every { mockCas2BailApplicationRepository.findByIdOrNull(applicationId) } returns cas2BailApplication + every { mockJsonSchemaService.checkCas2BailSchemaOutdated(cas2BailApplication) } returns cas2BailApplication + + val result = cas2BailApplicationService.updateCas2BailApplication( + applicationId = applicationId, + data = "{}", + user = user, + ) + + assertThat(result is AuthorisableActionResult.Success).isTrue + result as AuthorisableActionResult.Success + + assertThat(result.entity is ValidatableActionResult.GeneralValidationError).isTrue + val validatableActionResult = result.entity as ValidatableActionResult.GeneralValidationError + + assertThat(validatableActionResult.message).isEqualTo("The schema version is outdated") + } + + @ParameterizedTest + @ValueSource(strings = ["<", "<", "〈", "〈", ">", ">", "〉", "〉", "<<〈〈>>〉〉"]) + fun `returns Success when an application, that contains removed malicious characters, is updated`(str: String) { + val applicationId = UUID.fromString("dced02b1-8e3b-4ea5-bf99-1fba0ca1b87c") + + val newestSchema = Cas2BailApplicationJsonSchemaEntityFactory().produce() + val updatedData = """ + { + "aProperty": "val${str}ue" + } + """ + + val application = Cas2BailApplicationEntityFactory() + .withApplicationSchema(newestSchema) + .withId(applicationId) + .withCreatedByUser(user) + .produce() + .apply { + schemaUpToDate = true + } + + every { mockCas2BailApplicationRepository.findByIdOrNull(applicationId) } returns + application + every { mockJsonSchemaService.checkCas2BailSchemaOutdated(application) } returns + application + every { + mockJsonSchemaService.getNewestSchema( + Cas2BailApplicationJsonSchemaEntity::class + .java, + ) + } returns newestSchema + every { mockJsonSchemaService.validate(newestSchema, updatedData) } returns true + every { mockCas2BailApplicationRepository.save(any()) } answers { + it.invocation.args[0] + as Cas2BailApplicationEntity + } + + val result = cas2BailApplicationService.updateCas2BailApplication( + applicationId = applicationId, + data = updatedData, + user = user, + ) + + assertThat(result is AuthorisableActionResult.Success).isTrue + result as AuthorisableActionResult.Success + + assertThat(result.entity is ValidatableActionResult.Success).isTrue + val validatableActionResult = result.entity as ValidatableActionResult.Success + + val cas2BailApplication = validatableActionResult.entity + + assertThat(cas2BailApplication.data).isEqualTo( + """ + { + "aProperty": "value" + } + """, + ) + } + + @Test + fun `returns Success with updated Application`() { + val applicationId = UUID.fromString("fa6e97ce-7b9e-473c-883c-83b1c2af773d") + + val newestSchema = Cas2BailApplicationJsonSchemaEntityFactory().produce() + val updatedData = """ + { + "aProperty": "value" + } + """ + + val application = Cas2BailApplicationEntityFactory() + .withApplicationSchema(newestSchema) + .withId(applicationId) + .withCreatedByUser(user) + .produce() + .apply { + schemaUpToDate = true + } + + every { mockCas2BailApplicationRepository.findByIdOrNull(applicationId) } returns + application + every { mockJsonSchemaService.checkCas2BailSchemaOutdated(application) } returns + application + every { + mockJsonSchemaService.getNewestSchema( + Cas2BailApplicationJsonSchemaEntity::class + .java, + ) + } returns newestSchema + every { mockJsonSchemaService.validate(newestSchema, updatedData) } returns true + every { mockCas2BailApplicationRepository.save(any()) } answers { + it.invocation.args[0] + as Cas2BailApplicationEntity + } + + val result = cas2BailApplicationService.updateCas2BailApplication( + applicationId = applicationId, + data = updatedData, + user = user, + ) + + assertThat(result is AuthorisableActionResult.Success).isTrue + result as AuthorisableActionResult.Success + + assertThat(result.entity is ValidatableActionResult.Success).isTrue + val validatableActionResult = result.entity as ValidatableActionResult.Success + + val cas2BailApplication = validatableActionResult.entity + + assertThat(cas2BailApplication.data).isEqualTo(updatedData) + } + } + + @Nested + inner class AbandonApplication { + val user = NomisUserEntityFactory().produce() + + @Test + fun `returns NotFound when application doesn't exist`() { + val applicationId = UUID.fromString("fa6e97ce-7b9e-473c-883c-83b1c2af773d") + + every { mockCas2BailApplicationRepository.findByIdOrNull(applicationId) } returns null + + assertThat( + cas2BailApplicationService.abandonCas2BailApplication( + applicationId = applicationId, + user = user, + ) is AuthorisableActionResult.NotFound, + ).isTrue + } + + @Test + fun `returns Unauthorised when application doesn't belong to request user`() { + val applicationId = UUID.fromString("fa6e97ce-7b9e-473c-883c-83b1c2af773d") + + val application = Cas2BailApplicationEntityFactory() + .withId(applicationId) + .withYieldedCreatedByUser { + NomisUserEntityFactory() + .produce() + } + .produce() + + every { mockCas2BailApplicationRepository.findByIdOrNull(applicationId) } returns + application + + assertThat( + cas2BailApplicationService.abandonCas2BailApplication( + applicationId = applicationId, + user = user, + ) is AuthorisableActionResult.Unauthorised, + ).isTrue + } + + @Test + fun `returns Conflict Error when application has already been submitted`() { + val applicationId = UUID.fromString("fa6e97ce-7b9e-473c-883c-83b1c2af773d") + + val newestSchema = Cas2BailApplicationJsonSchemaEntityFactory().produce() + + val application = Cas2BailApplicationEntityFactory() + .withApplicationSchema(newestSchema) + .withId(applicationId) + .withCreatedByUser(user) + .withSubmittedAt(OffsetDateTime.now()) + .produce() + .apply { + schemaUpToDate = true + } + + every { mockCas2BailApplicationRepository.findByIdOrNull(applicationId) } returns + application + + val result = cas2BailApplicationService.abandonCas2BailApplication( + applicationId = applicationId, + user = user, + ) + + assertThat(result is AuthorisableActionResult.Success).isTrue + result as AuthorisableActionResult.Success + + assertThat(result.entity is ValidatableActionResult.ConflictError).isTrue + val validatableActionResult = result.entity as ValidatableActionResult.ConflictError + + assertThat(validatableActionResult.message).isEqualTo("This application has already been submitted") + } + + @Test + fun `returns Success when application has already been abandoned`() { + val applicationId = UUID.fromString("fa6e97ce-7b9e-473c-883c-83b1c2af773d") + + val newestSchema = Cas2BailApplicationJsonSchemaEntityFactory().produce() + + val application = Cas2BailApplicationEntityFactory() + .withApplicationSchema(newestSchema) + .withId(applicationId) + .withCreatedByUser(user) + .withAbandonedAt(OffsetDateTime.now()) + .produce() + .apply { + schemaUpToDate = true + } + + every { mockCas2BailApplicationRepository.findByIdOrNull(applicationId) } returns + application + + val result = cas2BailApplicationService.abandonCas2BailApplication( + applicationId = applicationId, + user = user, + ) + + assertThat(result is AuthorisableActionResult.Success).isTrue + result as AuthorisableActionResult.Success + + assertThat(result.entity is ValidatableActionResult.Success).isTrue + } + + @Test + fun `returns Success and deletes the application data`() { + val applicationId = UUID.fromString("dced02b1-8e3b-4ea5-bf99-1fba0ca1b87c") + + val newestSchema = Cas2BailApplicationJsonSchemaEntityFactory().produce() + val data = """ + { + "aProperty": "value" + } + """ + + val application = Cas2BailApplicationEntityFactory() + .withApplicationSchema(newestSchema) + .withId(applicationId) + .withCreatedByUser(user) + .withData(data) + .produce() + .apply { + schemaUpToDate = true + } + + every { mockCas2BailApplicationRepository.findByIdOrNull(applicationId) } returns + application + + every { mockCas2BailApplicationRepository.save(any()) } answers { + it.invocation.args[0] as Cas2BailApplicationEntity + } + + val result = cas2BailApplicationService.abandonCas2BailApplication( + applicationId = applicationId, + user = user, + ) + + assertThat(result is AuthorisableActionResult.Success).isTrue + result as AuthorisableActionResult.Success + + assertThat(result.entity is ValidatableActionResult.Success).isTrue + val validatableActionResult = result.entity as ValidatableActionResult.Success + + val cas2BailApplication = validatableActionResult.entity + + assertThat(cas2BailApplication.data).isNull() + } + } + + @Nested + inner class SubmitApplication { + val applicationId: UUID = UUID.fromString("fa6e97ce-7b9e-473c-883c-83b1c2af773d") + val username = "SOMEPERSON" + val user = NomisUserEntityFactory() + .withNomisUsername(this.username) + .produce() + val hdcEligibilityDate = LocalDate.parse("2023-03-30") + val conditionalReleaseDate = LocalDate.parse("2023-04-29") + + private val submitCas2Application = SubmitCas2Application( + translatedDocument = {}, + applicationId = applicationId, + preferredAreas = "Leeds | Bradford", + hdcEligibilityDate = hdcEligibilityDate, + conditionalReleaseDate = conditionalReleaseDate, + telephoneNumber = "123", + ) + + @BeforeEach + fun setup() { + every { mockCas2BailLockableApplicationRepository.acquirePessimisticLock(any()) } returns Cas2BailLockableApplicationEntity(UUID.randomUUID()) + every { mockObjectMapper.writeValueAsString(submitCas2Application.translatedDocument) } returns "{}" + every { mockDomainEventService.saveCas2ApplicationSubmittedDomainEvent(any()) } just Runs + } + + @Test + fun `returns NotFound when application doesn't exist`() { + val applicationId = UUID.fromString("fa6e97ce-7b9e-473c-883c-83b1c2af773d") + + every { mockCas2BailApplicationRepository.findByIdOrNull(applicationId) } returns null + + assertThat(cas2BailApplicationService.submitCas2BailApplication(submitCas2Application, user) is CasResult.NotFound).isTrue + + assertEmailAndAssessmentsWereNotCreated() + } + + @Test + fun `returns Unauthorised when application doesn't belong to request user`() { + val differentUser = NomisUserEntityFactory() + .produce() + + val cas2BailApplication = Cas2BailApplicationEntityFactory() + .withId(applicationId) + .withCreatedByUser(differentUser) + .produce() + + every { mockCas2BailApplicationRepository.findByIdOrNull(applicationId) } returns cas2BailApplication + every { mockJsonSchemaService.checkCas2BailSchemaOutdated(cas2BailApplication) } returns + cas2BailApplication + + assertThat(cas2BailApplicationService.submitCas2BailApplication(submitCas2Application, user) is CasResult.Unauthorised).isTrue + + assertEmailAndAssessmentsWereNotCreated() + } + + @Test + fun `returns GeneralValidationError when application schema is outdated`() { + val cas2BailApplication = Cas2BailApplicationEntityFactory() + .withId(applicationId) + .withCreatedByUser(user) + .withSubmittedAt(null) + .produce() + .apply { + schemaUpToDate = false + } + + every { + mockCas2BailApplicationRepository.findByIdOrNull(applicationId) + } returns cas2BailApplication + every { mockJsonSchemaService.checkCas2BailSchemaOutdated(cas2BailApplication) } returns cas2BailApplication + + val result = cas2BailApplicationService.submitCas2BailApplication(submitCas2Application, user) + + assertThat(result is CasResult.GeneralValidationError).isTrue + val validatableActionResult = result as CasResult.GeneralValidationError + + assertThat(validatableActionResult.message).isEqualTo("The schema version is outdated") + + assertEmailAndAssessmentsWereNotCreated() + } + + @Test + fun `returns GeneralValidationError when application has already been submitted`() { + val newestSchema = Cas2BailApplicationJsonSchemaEntityFactory().produce() + + val cas2BailApplication = Cas2BailApplicationEntityFactory() + .withApplicationSchema(newestSchema) + .withId(applicationId) + .withCreatedByUser(user) + .withSubmittedAt(OffsetDateTime.now()) + .produce() + .apply { + schemaUpToDate = true + } + + every { + mockCas2BailApplicationRepository.findByIdOrNull(applicationId) + } returns cas2BailApplication + every { mockJsonSchemaService.checkCas2BailSchemaOutdated(cas2BailApplication) } returns cas2BailApplication + + val result = cas2BailApplicationService.submitCas2BailApplication(submitCas2Application, user) + + assertThat(result is CasResult.GeneralValidationError).isTrue + val validatableActionResult = result as CasResult.GeneralValidationError + + assertThat(validatableActionResult.message).isEqualTo("This application has already been submitted") + + assertEmailAndAssessmentsWereNotCreated() + } + + @Test + fun `returns GeneralValidationError when application has already been abandoned`() { + val newestSchema = Cas2BailApplicationJsonSchemaEntityFactory().produce() + + val cas2BailApplication = Cas2BailApplicationEntityFactory() + .withApplicationSchema(newestSchema) + .withId(applicationId) + .withCreatedByUser(user) + .withAbandonedAt(OffsetDateTime.now()) + .produce() + + every { + mockCas2BailApplicationRepository.findByIdOrNull(applicationId) + } returns cas2BailApplication + every { mockJsonSchemaService.checkCas2BailSchemaOutdated(cas2BailApplication) } returns cas2BailApplication + + val result = cas2BailApplicationService.submitCas2BailApplication(submitCas2Application, user) + + assertThat(result is CasResult.GeneralValidationError).isTrue + val validatableActionResult = result as CasResult.GeneralValidationError + + assertThat(validatableActionResult.message).isEqualTo("This application has already been abandoned") + + assertEmailAndAssessmentsWereNotCreated() + } + + @Test + fun `throws a validation error if InmateDetails (for prison code) are not available`() { + val newestSchema = Cas2BailApplicationJsonSchemaEntityFactory().produce() + + val cas2BailApplication = Cas2BailApplicationEntityFactory() + .withApplicationSchema(newestSchema) + .withId(applicationId) + .withCreatedByUser(user) + .withSubmittedAt(null) + .produce() + .apply { + schemaUpToDate = true + } + + every { + mockCas2BailApplicationRepository.findByIdOrNull(any()) + } returns cas2BailApplication + every { mockJsonSchemaService.checkCas2BailSchemaOutdated(any()) } returns + cas2BailApplication + every { mockJsonSchemaService.validate(any(), any()) } returns true + + every { mockCas2BailApplicationRepository.save(any()) } answers { + it.invocation.args[0] + as Cas2BailApplicationEntity + } + + val offenderDetails = OffenderDetailsSummaryFactory() + .withCrn(cas2BailApplication.crn) + .produce() + + every { mockOffenderService.getOffenderByCrn(any()) } returns AuthorisableActionResult.Success( + offenderDetails, + ) + + // this call to the Prison API to find the referringPrisonCode when saving + // the application.submitted domain event *should* never 404 or otherwise fail, + // as when creating the application initially a similar call was made. + // If there is a problem with accessing the Prison API, we fail hard and + // abort our attempt to submit the application. + every { + mockOffenderService.getInmateDetailByNomsNumber(any(), any()) + } returns AuthorisableActionResult.NotFound() + + assertGeneralValidationError("Inmate Detail not found") + + assertEmailAndAssessmentsWereNotCreated() + } + + @Test + fun `throws an UpstreamApiException if prison code is null`() { + val newestSchema = Cas2BailApplicationJsonSchemaEntityFactory().produce() + + val cas2BailApplication = Cas2BailApplicationEntityFactory() + .withApplicationSchema(newestSchema) + .withId(applicationId) + .withCreatedByUser(user) + .withSubmittedAt(null) + .produce() + .apply { + schemaUpToDate = true + } + + every { + mockCas2BailApplicationRepository.findByIdOrNull(any()) + } returns cas2BailApplication + every { mockJsonSchemaService.checkCas2BailSchemaOutdated(any()) } returns + cas2BailApplication + every { mockJsonSchemaService.validate(any(), any()) } returns true + + every { mockCas2BailApplicationRepository.save(any()) } answers { + it.invocation.args[0] + as Cas2BailApplicationEntity + } + + val offenderDetails = OffenderDetailsSummaryFactory() + .withCrn(cas2BailApplication.crn) + .produce() + + every { mockOffenderService.getOffenderByCrn(any()) } returns AuthorisableActionResult.Success( + offenderDetails, + ) + + // this call to the Prison API to find the referringPrisonCode when saving + // the application.submitted domain event *should* always have a prison code, + // but we need to account for possibility it may be missing. + // If there is a problem with accessing the Prison API, we fail hard and + // abort our attempt to submit the application and return a validation message. + every { + mockOffenderService.getInmateDetailByNomsNumber(any(), any()) + } returns AuthorisableActionResult.Success(InmateDetailFactory().produce()) + + assertGeneralValidationError("No prison code available") + + assertEmailAndAssessmentsWereNotCreated() + } + + private fun assertGeneralValidationError(message: String) { + val result = cas2BailApplicationService.submitCas2BailApplication(submitCas2Application, user) + assertThat(result is CasResult.GeneralValidationError).isTrue + val error = result as CasResult.GeneralValidationError + + assertThat(error.message).isEqualTo(message) + } + + private fun assertEmailAndAssessmentsWereNotCreated() { + verify(exactly = 0) { mockEmailNotificationService.sendEmail(any(), any(), any()) } + verify(exactly = 0) { mockCas2BailAssessmentService.createCas2BailAssessment(any()) } + } + + @Test + fun `returns Success and stores event`() { + val newestSchema = Cas2BailApplicationJsonSchemaEntityFactory().produce() + + val cas2BailApplication = Cas2BailApplicationEntityFactory() + .withApplicationSchema(newestSchema) + .withId(applicationId) + .withCreatedByUser(user) + .withSubmittedAt(null) + .produce() + .apply { + schemaUpToDate = true + } + + every { + mockCas2BailApplicationRepository.findByIdOrNull(applicationId) + } returns cas2BailApplication + every { mockJsonSchemaService.checkCas2BailSchemaOutdated(cas2BailApplication) } returns + cas2BailApplication + every { mockJsonSchemaService.validate(newestSchema, cas2BailApplication.data!!) } returns true + + val inmateDetail = InmateDetailFactory() + .withAssignedLivingUnit( + AssignedLivingUnit( + agencyId = "BRI", + locationId = 1234, + description = "description", + agencyName = "HMP Bristol", + ), + ) + .produce() + + every { + mockOffenderService.getInmateDetailByNomsNumber( + cas2BailApplication.crn, + cas2BailApplication.nomsNumber.toString(), + ) + } returns AuthorisableActionResult.Success(inmateDetail) + + every { mockNotifyConfig.templates.cas2ApplicationSubmitted } returns "abc123" + every { mockNotifyConfig.emailAddresses.cas2Assessors } returns "exampleAssessorInbox@example.com" + every { mockNotifyConfig.emailAddresses.cas2ReplyToId } returns "def456" + every { mockEmailNotificationService.sendEmail(any(), any(), any(), any()) } just Runs + + every { mockCas2BailApplicationRepository.save(any()) } answers { + it.invocation.args[0] + as Cas2BailApplicationEntity + } + + val offenderDetails = OffenderDetailsSummaryFactory() + .withGender("male") + .withCrn(cas2BailApplication.crn) + .produce() + + every { mockOffenderService.getOffenderByCrn(cas2BailApplication.crn) } returns AuthorisableActionResult.Success( + offenderDetails, + ) + + every { mockCas2BailAssessmentService.createCas2BailAssessment(any()) } returns any() + + val result = cas2BailApplicationService.submitCas2BailApplication(submitCas2Application, user) + + assertThat(result is CasResult.Success).isTrue + result as CasResult.Success + + assertThat(true).isTrue + val persistedApplication = extractEntityFromCasResult(result) + + assertThat(persistedApplication.crn).isEqualTo(cas2BailApplication.crn) + assertThat(persistedApplication.preferredAreas).isEqualTo("Leeds | Bradford") + assertThat(persistedApplication.hdcEligibilityDate).isEqualTo(hdcEligibilityDate) + assertThat(persistedApplication.conditionalReleaseDate).isEqualTo(conditionalReleaseDate) + + verify { mockCas2BailApplicationRepository.save(any()) } + + verify(exactly = 1) { + mockDomainEventService.saveCas2ApplicationSubmittedDomainEvent( + match { + val data = it.data.eventDetails + + it.applicationId == cas2BailApplication.id && + data.personReference.noms == cas2BailApplication.nomsNumber && + data.personReference.crn == cas2BailApplication.crn && + data.applicationUrl == "http://frontend/applications/${cas2BailApplication.id}" && + data.submittedBy.staffMember.username == username && + data.referringPrisonCode == "BRI" && + data.preferredAreas == "Leeds | Bradford" && + data.hdcEligibilityDate == hdcEligibilityDate && + data.conditionalReleaseDate == conditionalReleaseDate + }, + ) + } + + verify(exactly = 1) { + mockEmailNotificationService.sendEmail( + "exampleAssessorInbox@example.com", + "abc123", + match { + it["name"] == user.name && + it["email"] == user.email && + it["prisonNumber"] == cas2BailApplication.nomsNumber && + it["telephoneNumber"] == cas2BailApplication.telephoneNumber && + it["applicationUrl"] == "http://frontend/assess/applications/$applicationId/overview" + }, + "def456", + ) + } + + verify(exactly = 1) { mockCas2BailAssessmentService.createCas2BailAssessment(persistedApplication) } + } + } + + + private fun userWithUsername(username: String) = NomisUserEntityFactory() + .withNomisUsername(username) + .produce() + +} \ No newline at end of file diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailAssessmentServiceTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailAssessmentServiceTest.kt new file mode 100644 index 0000000000..2a46d5b263 --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailAssessmentServiceTest.kt @@ -0,0 +1,139 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.unit.service.cas2bail + +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.springframework.data.repository.findByIdOrNull +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.UpdateCas2Assessment +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.NomisUserEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.cas2bail.Cas2BailApplicationEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailAssessmentEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailAssessmentRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.AuthorisableActionResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.CasResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.ValidatableActionResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2bail.Cas2BailAssessmentService +import java.time.OffsetDateTime +import java.util.* + +class Cas2BailAssessmentServiceTest { + + private val mockCas2BailAssessmentRepository = mockk() + + private val cas2BailAssessmentService = Cas2BailAssessmentService( + mockCas2BailAssessmentRepository, + ) + + @Nested + inner class CreateAssessment { + + @Test + fun `saves and returns entity from db`() { + val cas2BailApplication = Cas2BailApplicationEntityFactory() + .withCreatedByUser( + NomisUserEntityFactory() + .produce(), + ).produce() + val assessEntity = Cas2BailAssessmentEntity( + id = UUID.randomUUID(), + application = cas2BailApplication, + createdAt = OffsetDateTime.now(), + ) + + every { mockCas2BailAssessmentRepository.save(any()) } answers + { + assessEntity + } + + val result = cas2BailAssessmentService.createCas2BailAssessment( + cas2BailApplication, + ) + Assertions.assertThat(result).isEqualTo(assessEntity) + + verify(exactly = 1) { + mockCas2BailAssessmentRepository.save( + match { it.application == cas2BailApplication }, + ) + } + } + } + + @Nested + inner class UpdateAssessment { + + @Test + fun `saves and returns entity from db`() { + val assessmentId = UUID.randomUUID() + val cas2BailApplication = Cas2BailApplicationEntityFactory() + .withCreatedByUser( + NomisUserEntityFactory() + .produce(), + ).produce() + val assessEntity = Cas2BailAssessmentEntity( + id = assessmentId, + application = cas2BailApplication, + createdAt = OffsetDateTime.now(), + ) + + val newAssessmentData = UpdateCas2Assessment( + nacroReferralId = "1234OH", + assessorName = "Anne Assessor", + ) + + every { mockCas2BailAssessmentRepository.save(any()) } answers + { + assessEntity + } + + every { mockCas2BailAssessmentRepository.findByIdOrNull(assessmentId) } answers + { + assessEntity + } + + val result = cas2BailAssessmentService.updateAssessment( + assessmentId = assessmentId, + newAssessment = newAssessmentData, + ) + Assertions.assertThat(result).isEqualTo( + + CasResult.Success(assessEntity), + + ) + + verify(exactly = 1) { + mockCas2BailAssessmentRepository.save( + match { + it.id == assessEntity.id && + it.nacroReferralId == newAssessmentData.nacroReferralId && + it.assessorName == newAssessmentData.assessorName + }, + ) + } + } + + @Test + fun `returns NotFound if entity is not found`() { + val assessmentId = UUID.randomUUID() + val newAssessmentData = UpdateCas2Assessment( + nacroReferralId = "1234OH", + assessorName = "Anne Assessor", + ) + + every { mockCas2BailAssessmentRepository.findByIdOrNull(assessmentId) } answers + { + null + } + + val result = cas2BailAssessmentService.updateAssessment( + assessmentId = assessmentId, + newAssessment = newAssessmentData, + ) + + Assertions.assertThat(result is CasResult.NotFound).isTrue + } + } + +} \ No newline at end of file diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailUserAccessServiceTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailUserAccessServiceTest.kt new file mode 100644 index 0000000000..0ab22ca243 --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailUserAccessServiceTest.kt @@ -0,0 +1,140 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.unit.service.cas2bail + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.Cas2ApplicationJsonSchemaEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.Cas2BailApplicationJsonSchemaEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.NomisUserEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.cas2bail.Cas2BailApplicationEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2bail.Cas2BailUserAccessService +import java.time.OffsetDateTime + +class Cas2BailUserAccessServiceTest { + + @Nested + inner class UserCanViewApplication { + + private val cas2BailUserAccessService = Cas2BailUserAccessService() + val newestJsonSchema = Cas2BailApplicationJsonSchemaEntityFactory() + .withSchema("{}") + .produce() + + @Nested + inner class WhenApplicationCreatedByUser { + private val user = NomisUserEntityFactory() + .produce() + + @Test + fun `returns true`() { + val application = Cas2BailApplicationEntityFactory() + .withApplicationSchema(newestJsonSchema) + .withCreatedByUser(user) + .produce() + + assertThat(cas2BailUserAccessService.userCanViewCas2BailApplication(user, application)).isTrue + } + } + + @Nested + inner class WhenApplicationNotCreatedByUser { + + @Nested + inner class WhenApplicationNotSubmitted { + private val user = NomisUserEntityFactory() + .produce() + private val anotherUser = NomisUserEntityFactory() + .produce() + + @Test + fun `returns false`() { + val cas2BailApplication = Cas2BailApplicationEntityFactory() + .withApplicationSchema(newestJsonSchema) + .withCreatedByUser(anotherUser) + .produce() + + assertThat(cas2BailUserAccessService.userCanViewCas2BailApplication(user, cas2BailApplication)).isFalse + } + } + + @Nested + inner class WhenApplicationMadeForDifferentPrison { + private val user = NomisUserEntityFactory() + .withActiveCaseloadId("my-prison") + .produce() + private val anotherUser = NomisUserEntityFactory() + .withActiveCaseloadId("different-prison").produce() + + @Test + fun `returns false`() { + val cas2BailApplication = Cas2BailApplicationEntityFactory() + .withApplicationSchema(newestJsonSchema) + .withCreatedByUser(anotherUser) + .withSubmittedAt(OffsetDateTime.now()) + .withReferringPrisonCode("different-prison") + .produce() + + assertThat(cas2BailUserAccessService.userCanViewCas2BailApplication(user, cas2BailApplication)).isFalse + } + + @Nested + inner class WhenNoPrisonData { + private val userWithNoPrison = NomisUserEntityFactory() + .withActiveCaseloadId("my-prison") + .produce() + private val anotherUserWithNoPrison = NomisUserEntityFactory() + .withActiveCaseloadId("different-prison").produce() + + @Test + fun `returns false`() { + val cas2BailApplication = Cas2BailApplicationEntityFactory() + .withApplicationSchema(newestJsonSchema) + .withCreatedByUser(anotherUserWithNoPrison) + .withSubmittedAt(OffsetDateTime.now()) + .produce() + + assertThat(cas2BailUserAccessService.userCanViewCas2BailApplication(userWithNoPrison, cas2BailApplication)).isFalse + } + } + } + + @Nested + inner class WhenCas2BailApplicationMadeForSamePrison { + private val user = NomisUserEntityFactory() + .withActiveCaseloadId("my-prison") + .produce() + private val anotherUser = NomisUserEntityFactory() + .withActiveCaseloadId("my-prison") + .produce() + + @Test + fun `returns true if the user created the application`() { + val cas2BailApplication = Cas2BailApplicationEntityFactory() + .withApplicationSchema(newestJsonSchema) + .withCreatedByUser(user) + .withSubmittedAt(OffsetDateTime.now()) + .withReferringPrisonCode("my-prison") + .produce() + + assertThat(cas2BailUserAccessService.userCanViewCas2BailApplication(user, cas2BailApplication)).isTrue + } + + @Test + fun `returns true when user NOT creator`() { + val newestJsonSchema = Cas2ApplicationJsonSchemaEntityFactory() + .withSchema("{}") + .produce() + + val cas2BailApplication = Cas2BailApplicationEntityFactory() + .withApplicationSchema(newestJsonSchema) + .withCreatedByUser(anotherUser) + .withSubmittedAt(OffsetDateTime.now()) + .withReferringPrisonCode("my-prison") + .produce() + + assertThat(cas2BailUserAccessService.userCanViewCas2BailApplication(user, cas2BailApplication)).isTrue + } + } + } + } +} \ No newline at end of file From b1001e23caab58b0993ad2b31ddd8a8415222b05 Mon Sep 17 00:00:00 2001 From: Toby Batch Date: Tue, 3 Dec 2024 14:53:26 +0000 Subject: [PATCH 26/37] feat: adds the view for live summary --- ...roller_and_linked_services_in_cas2bail.sql | 63 ++++++++++++++----- 1 file changed, 46 insertions(+), 17 deletions(-) diff --git a/src/main/resources/db/migration/all/20241128163133__application_controller_and_linked_services_in_cas2bail.sql b/src/main/resources/db/migration/all/20241128163133__application_controller_and_linked_services_in_cas2bail.sql index 75dfb4fb0f..5c869be832 100644 --- a/src/main/resources/db/migration/all/20241128163133__application_controller_and_linked_services_in_cas2bail.sql +++ b/src/main/resources/db/migration/all/20241128163133__application_controller_and_linked_services_in_cas2bail.sql @@ -5,23 +5,6 @@ CREATE TABLE cas_2_bail_application_json_schemas CONSTRAINT pk_cas_2_bail_application_json_schemas PRIMARY KEY (json_schema_id) ); -CREATE TABLE cas_2_bail_application_live_summary -( - id UUID NOT NULL, - crn VARCHAR(255), - noms_number VARCHAR(255), - created_by_user_id VARCHAR(255), - name VARCHAR(255), - created_at TIMESTAMP WITHOUT TIME ZONE, - submitted_at TIMESTAMP WITHOUT TIME ZONE, - abandoned_at TIMESTAMP WITHOUT TIME ZONE, - hdc_eligibility_date date, - label VARCHAR(255), - status_id VARCHAR(255), - referring_prison_code VARCHAR(255), - CONSTRAINT pk_cas_2_bail_application_live_summary PRIMARY KEY (id) -); - CREATE TABLE cas_2_bail_application_notes ( id UUID NOT NULL, @@ -126,3 +109,49 @@ ALTER TABLE cas_2_bail_status_updates ALTER TABLE cas_2_bail_status_update_details ADD CONSTRAINT FK_CAS_2_BAIL_STATUS_UPDATE_DETAILS_ON_STATUS_UPDATE FOREIGN KEY (status_update_id) REFERENCES cas_2_bail_status_updates (id); + +CREATE OR REPLACE VIEW cas_2_bail_application_summary AS SELECT + a.id, + a.crn, + a.noms_number, + CAST(a.created_by_user_id AS TEXT), + nu.name, + a.created_at, + a.submitted_at, + a.hdc_eligibility_date, + asu.label, + CAST(asu.status_id AS TEXT), + a.referring_prison_code, + a.conditional_release_date, + asu.created_at AS status_created_at, + a.abandoned_at +FROM cas_2_applications a +LEFT JOIN (SELECT DISTINCT ON (application_id) su.application_id, su.label, su.status_id, su.created_at + FROM cas_2_bail_status_updates su + ORDER BY su.application_id, su.created_at DESC) as asu + ON a.id = asu.application_id +JOIN nomis_users nu ON nu.id = a.created_by_user_id; + +CREATE OR REPLACE VIEW cas_2_bail_application_live_summary AS SELECT + a.id, + a.crn, + a.noms_number, + a.created_by_user_id, + a.name, + a.created_at, + a.submitted_at, + a.hdc_eligibility_date, + a.label, + a.status_id, + a.referring_prison_code, + a.abandoned_at +FROM cas_2_bail_application_summary a +WHERE (a.conditional_release_date IS NULL OR a.conditional_release_date >= current_date) +AND a.abandoned_at IS NULL +AND a.status_id IS NULL + OR (a.status_id = '004e2419-9614-4c1e-a207-a8418009f23d' AND a.status_created_at > (current_date - INTERVAL '32 DAY')) -- Referral withdrawn + OR (a.status_id = 'f13bbdd6-44f1-4362-b9d3-e6f1298b1bf9' AND a.status_created_at > (current_date - INTERVAL '32 DAY')) -- Referral cancelled + OR (a.status_id = '89458555-3219-44a2-9584-c4f715d6b565' AND a.status_created_at > (current_date - INTERVAL '32 DAY')) -- Awaiting arrival + OR (a.status_id NOT IN ('004e2419-9614-4c1e-a207-a8418009f23d', + 'f13bbdd6-44f1-4362-b9d3-e6f1298b1bf9', + '89458555-3219-44a2-9584-c4f715d6b565')); \ No newline at end of file From 12d980c3144f0ee568fe684caa4181724aa5c3d6 Mon Sep 17 00:00:00 2001 From: Toby Batch Date: Tue, 3 Dec 2024 15:34:01 +0000 Subject: [PATCH 27/37] chore: more test debug --- .../cas2bail/Cas2BailApplicationEntity.kt | 12 +++- .../cas2bail/Cas2BailAssessmentEntity.kt | 1 - .../Cas2BailStatusUpdateDetailEntry.kt | 2 +- .../service/cas2/ApplicationService.kt | 2 + .../cas2bail/Cas2BailApplicationService.kt | 3 + .../cas2bail/Cas2BailAssessmentService.kt | 17 ++--- .../Cas2BailAssessmentEntityFactory.kt | 56 ++++++++++++++++ .../integration/IntegrationTestBase.kt | 9 +++ .../cas2bail/Cas2BailApplicationTest.kt | 65 +++++++++++++++---- .../repository/ApplicationTestRepository.kt | 4 ++ 10 files changed, 148 insertions(+), 23 deletions(-) create mode 100644 src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/cas2bail/Cas2BailAssessmentEntityFactory.kt diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationEntity.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationEntity.kt index 77a75f5cbe..c04ee0c27d 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationEntity.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationEntity.kt @@ -4,8 +4,16 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.* import io.hypersistence.utils.hibernate.type.json.JsonType -import jakarta.persistence.* +import jakarta.persistence.Entity +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.LockModeType +import jakarta.persistence.ManyToOne +import jakarta.persistence.OneToMany +import jakarta.persistence.OneToOne +import jakarta.persistence.Table import org.hibernate.annotations.Immutable +import org.hibernate.annotations.OrderBy import org.hibernate.annotations.Type import org.springframework.data.domain.Slice import org.springframework.data.jpa.repository.JpaRepository @@ -76,7 +84,7 @@ data class Cas2BailApplicationEntity( @OrderBy("createdAt DESC") var notes: MutableList? = null, - @OneToOne(fetch = FetchType.LAZY) + @OneToOne(mappedBy = "application") var assessment: Cas2BailAssessmentEntity? = null, @Transient diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailAssessmentEntity.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailAssessmentEntity.kt index d934cb5e0c..dc062b7b76 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailAssessmentEntity.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailAssessmentEntity.kt @@ -22,7 +22,6 @@ data class Cas2BailAssessmentEntity( val id: UUID, @OneToOne - @Lazy val application: Cas2BailApplicationEntity, val createdAt: OffsetDateTime, diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailStatusUpdateDetailEntry.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailStatusUpdateDetailEntry.kt index 032abe277c..be50954750 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailStatusUpdateDetailEntry.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailStatusUpdateDetailEntry.kt @@ -24,7 +24,7 @@ data class Cas2BailStatusUpdateDetailEntity( val label: String, - @ManyToOne(fetch = FetchType.LAZY) + @ManyToOne() @JoinColumn(name = "status_update_id") val statusUpdate: Cas2BailStatusUpdateEntity, diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2/ApplicationService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2/ApplicationService.kt index 2cc434eba6..52a516d6d0 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2/ApplicationService.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2/ApplicationService.kt @@ -70,6 +70,8 @@ class ApplicationService( pageCriteria: PageCriteria, ): Pair, PaginationMetadata?> { val response = if (prisonCode == null) { + val uid = user.id.toString() + val records = applicationSummaryRepository.findByUserId(uid, null) repositoryUserFunctionMap.get(isSubmitted)!!(user.id.toString(), getPageableOrAllPages(pageCriteria)) } else { repositoryPrisonFunctionMap.get(isSubmitted)!!(prisonCode, getPageableOrAllPages(pageCriteria)) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationService.kt index 31100e39aa..7984beec36 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationService.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationService.kt @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import jakarta.transaction.Transactional import org.springframework.beans.factory.annotation.Value import org.springframework.data.repository.findByIdOrNull +import org.springframework.jdbc.core.JdbcTemplate import org.springframework.stereotype.Service import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.events.cas2.model.* import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.SubmitCas2Application @@ -61,6 +62,8 @@ class Cas2BailApplicationService( user: NomisUserEntity, pageCriteria: PageCriteria, ): Pair, PaginationMetadata?> { + val uid = user.id.toString() + val records = cas2BailApplicationSummaryRepository.findByUserId(uid, null) val response = if (prisonCode == null) { repositoryUserFunctionMap.get(isSubmitted)!!(user.id.toString(), getPageableOrAllPages(pageCriteria)) } else { diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailAssessmentService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailAssessmentService.kt index da6aef71eb..3d4fc340cf 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailAssessmentService.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailAssessmentService.kt @@ -8,22 +8,20 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.UpdateCas2Asse import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailAssessmentEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailAssessmentRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.AuthorisableActionResult import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.CasResult -import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.ValidatableActionResult import java.time.OffsetDateTime import java.util.* @Service("Cas2BailAssessmentService") class Cas2BailAssessmentService ( - private val assessmentRepository: Cas2BailAssessmentRepository, + private val cas2AssessmentRepository: Cas2BailAssessmentRepository, ) { @Transactional fun createCas2BailAssessment(cas2BailApplicationEntity: Cas2BailApplicationEntity): Cas2BailAssessmentEntity = - assessmentRepository.save( + cas2AssessmentRepository.save( Cas2BailAssessmentEntity( id = UUID.randomUUID(), createdAt = OffsetDateTime.now(), @@ -31,8 +29,11 @@ class Cas2BailAssessmentService ( ), ) - fun updateAssessment(assessmentId: UUID, newAssessment: UpdateCas2Assessment): CasResult { - val assessmentEntity = assessmentRepository.findByIdOrNull(assessmentId) + fun updateAssessment( + assessmentId: UUID, + newAssessment: UpdateCas2Assessment) + : CasResult { + val assessmentEntity = cas2AssessmentRepository.findByIdOrNull(assessmentId) ?: return CasResult.NotFound() assessmentEntity.apply { @@ -40,13 +41,13 @@ class Cas2BailAssessmentService ( this.assessorName = newAssessment.assessorName } - val savedAssessment = assessmentRepository.save(assessmentEntity) + val savedAssessment = cas2AssessmentRepository.save(assessmentEntity) return CasResult.Success(savedAssessment) } fun getAssessment(assessmentId: UUID): CasResult { - val assessmentEntity = assessmentRepository.findByIdOrNull(assessmentId) + val assessmentEntity = cas2AssessmentRepository.findByIdOrNull(assessmentId) ?: return CasResult.NotFound() return CasResult.Success(assessmentEntity) diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/cas2bail/Cas2BailAssessmentEntityFactory.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/cas2bail/Cas2BailAssessmentEntityFactory.kt new file mode 100644 index 0000000000..972ee88054 --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/cas2bail/Cas2BailAssessmentEntityFactory.kt @@ -0,0 +1,56 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.cas2bail + +import io.github.bluegroundltd.kfactory.Factory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.NomisUserEntityFactory +import java.time.OffsetDateTime +import io.github.bluegroundltd.kfactory.Yielded +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailAssessmentEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailStatusUpdateEntity +import java.util.UUID + +class Cas2BailAssessmentEntityFactory: Factory { + private var id: Yielded = { UUID.randomUUID() } + private var createdAt: Yielded = { OffsetDateTime.now() } + private var application: Yielded = { + Cas2BailApplicationEntityFactory() + .withCreatedByUser(NomisUserEntityFactory().produce()) + .produce() + } + private var nacroReferralId: String? = null + private var assessorName: String? = null + private var statusUpdates: MutableList = mutableListOf() + + fun withId(id: UUID) = apply { + this.id = { id } + } + + fun withApplication(application: Cas2BailApplicationEntity) = apply { + this.application = { application } + } + + fun withStatusUpdates(statusUpdates: MutableList) = apply { + this.statusUpdates = statusUpdates + } + + fun withNacroReferralId(id: String) = apply { + this.nacroReferralId = id + } + + fun withAssessorName(name: String) = apply { + this.assessorName = name + } + + fun withCreatedAt(createdAt: OffsetDateTime) = apply { + this.createdAt = { createdAt } + } + + override fun produce(): Cas2BailAssessmentEntity = Cas2BailAssessmentEntity( + id = this.id(), + createdAt = this.createdAt(), + application = this.application(), + nacroReferralId = this.nacroReferralId, + assessorName = this.assessorName, + statusUpdates = this.statusUpdates, + ) +} \ No newline at end of file diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/IntegrationTestBase.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/IntegrationTestBase.kt index e7c8b9fb85..1e18204d4c 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/IntegrationTestBase.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/IntegrationTestBase.kt @@ -33,6 +33,7 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.config.NotifyConfig import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.* import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.cas1.Cas1CruManagementAreaEntityFactory import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.cas2bail.Cas2BailApplicationEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.cas2bail.Cas2BailAssessmentEntityFactory import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.cas2bail.Cas2BailStatusUpdateEntityFactory import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.asserter.DomainEventAsserter import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.asserter.EmailNotificationAsserter @@ -43,6 +44,8 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.mocks.Mutabl import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.mocks.NoOpSentryService import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.* import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailAssessmentEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailAssessmentRepository import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailStatusUpdateEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.community.OffenderDetailSummary import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.community.UserOffenderAccess @@ -176,6 +179,9 @@ abstract class IntegrationTestBase { @Autowired lateinit var cas2BailApplicationRepository: Cas2BailApplicationTestRepository + @Autowired + lateinit var cas2BailAssessmentRepository: Cas2BailAssessmentRepository + @Autowired lateinit var cas2AssessmentRepository: Cas2AssessmentRepository @@ -371,7 +377,9 @@ abstract class IntegrationTestBase { lateinit var approvedPremisesApplicationEntityFactory: PersistedFactory lateinit var cas2ApplicationEntityFactory: PersistedFactory lateinit var cas2BailApplicationEntityFactory: PersistedFactory + lateinit var cas2BailAssementEntityFactory: PersistedFactory lateinit var cas2AssessmentEntityFactory: PersistedFactory + lateinit var cas2StatusUpdateEntityFactory: PersistedFactory lateinit var cas2BailStatusUpdateEntityFactory: PersistedFactory lateinit var cas2StatusUpdateDetailEntityFactory: PersistedFactory @@ -486,6 +494,7 @@ abstract class IntegrationTestBase { cas2ApplicationEntityFactory = PersistedFactory({ Cas2ApplicationEntityFactory() }, cas2ApplicationRepository) cas2BailApplicationEntityFactory = PersistedFactory({ Cas2BailApplicationEntityFactory() }, cas2BailApplicationRepository) + cas2BailAssementEntityFactory = PersistedFactory({ Cas2BailAssessmentEntityFactory() }, cas2BailAssessmentRepository) cas2AssessmentEntityFactory = PersistedFactory({ Cas2AssessmentEntityFactory() }, cas2AssessmentRepository) cas2StatusUpdateEntityFactory = PersistedFactory({ Cas2StatusUpdateEntityFactory() }, cas2StatusUpdateRepository) cas2BailStatusUpdateEntityFactory = PersistedFactory({ Cas2BailStatusUpdateEntityFactory() }, cas2BailStatusUpdateRepository) diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationTest.kt index 6c18faa868..da0668291f 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationTest.kt @@ -7,6 +7,7 @@ import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.node.NullNode import com.ninjasquad.springmockk.SpykBean import io.mockk.clearMocks +import jakarta.persistence.EntityManager import org.assertj.core.api.Assertions import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach @@ -14,6 +15,11 @@ import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ValueSource +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query +import org.springframework.data.repository.query.Param +import org.springframework.stereotype.Repository import org.springframework.test.web.reactive.server.returnResult import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.* import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.IntegrationTestBase @@ -25,6 +31,7 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.httpmocks.co import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.httpmocks.prisonAPIMockNotFoundInmateDetailsCall import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.* import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationSummaryEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailStatusUpdateEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.community.OffenderDetailSummary import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.randomDateAfter @@ -37,6 +44,10 @@ import kotlin.math.sign class Cas2BailApplicationTest : IntegrationTestBase() { + /* START HERE - TOBY DEBUG */ + @Autowired + private lateinit var entityManager: EntityManager + @SpykBean lateinit var realApplicationRepository: ApplicationRepository @@ -175,6 +186,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { @Nested inner class GetToIndex { + @Test fun `return unexpired applications when applications GET is requested`() { val unexpiredSubset = setOf( @@ -255,6 +267,9 @@ class Cas2BailApplicationTest : IntegrationTestBase() { expiredApplicationIds.add(application.id) } + val aaa = cas2BailApplicationRepository.findAll() + val bbb = cas2BailAssessmentRepository.findAll() + val rawResponseBody = webTestClient.get() .uri("/cas2bail/applications") .header("Authorization", "Bearer $jwt") @@ -281,15 +296,15 @@ class Cas2BailApplicationTest : IntegrationTestBase() { givenACas2PomUser { userEntity, jwt -> givenACas2PomUser { otherUser, _ -> givenAnOffender { offenderDetails, _ -> - cas2ApplicationJsonSchemaRepository.deleteAll() + cas2BailApplicationJsonSchemaRepository.deleteAll() - val applicationSchema = cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + val applicationSchema = cas2BailApplicationJsonSchemaEntityFactory.produceAndPersist { withAddedAt(OffsetDateTime.now()) withId(UUID.randomUUID()) } // abandoned application - val abandonedApplicationEntity = cas2ApplicationEntityFactory.produceAndPersist { + val abandonedApplicationEntity = cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(applicationSchema) withCreatedByUser(userEntity) withCrn(offenderDetails.otherIds.crn) @@ -299,7 +314,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } // unsubmitted application - val firstApplicationEntity = cas2ApplicationEntityFactory.produceAndPersist { + val firstApplicationEntity = cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(applicationSchema) withCreatedByUser(userEntity) withCrn(offenderDetails.otherIds.crn) @@ -309,7 +324,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } // submitted application, CRD >= today so should be returned - val secondApplicationEntity = cas2ApplicationEntityFactory.produceAndPersist { + val secondApplicationEntity = cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(applicationSchema) withCreatedByUser(userEntity) withCrn(offenderDetails.otherIds.crn) @@ -320,7 +335,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } // submitted application, CRD = yesterday, so should not be returned - val thirdApplicationEntity = cas2ApplicationEntityFactory.produceAndPersist { + val thirdApplicationEntity = cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(applicationSchema) withCreatedByUser(userEntity) withCrn(offenderDetails.otherIds.crn) @@ -330,13 +345,13 @@ class Cas2BailApplicationTest : IntegrationTestBase() { withConditionalReleaseDate(LocalDate.now().minusDays(1)) } - val statusUpdate = cas2StatusUpdateEntityFactory.produceAndPersist { + val statusUpdate = cas2BailStatusUpdateEntityFactory.produceAndPersist { withLabel("More information requested") withApplication(secondApplicationEntity) withAssessor(externalUserEntityFactory.produceAndPersist()) } - val otherCas2ApplicationEntity = cas2ApplicationEntityFactory.produceAndPersist { + val othercas2BailApplicationEntity = cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(applicationSchema) withCreatedByUser(otherUser) withCrn(offenderDetails.otherIds.crn) @@ -344,7 +359,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } val rawResponseBody = webTestClient.get() - .uri("/cas2/applications") + .uri("/cas2bail/applications") .header("Authorization", "Bearer $jwt") .header("X-Service-Name", ServiceName.cas2.value) .exchange() @@ -354,8 +369,36 @@ class Cas2BailApplicationTest : IntegrationTestBase() { .responseBody .blockFirst() + /* START HERE - TOBY DEBUG */ + val r1 = entityManager.createNativeQuery("SELECT * FROM cas_2_bail_applications").resultList + val r2 = entityManager.createNativeQuery("SELECT * FROM cas_2_bail_application_live_summary").resultList + val r3 = entityManager.createNativeQuery("SELECT * FROM cas_2_bail_status_updates").resultList + val r4 = entityManager.createNativeQuery("SELECT * FROM cas_2_bail_assessments").resultList + val r5 = entityManager.createNativeQuery("SELECT\n" + + " a.id,\n" + + " a.crn,\n" + + " a.noms_number,\n" + + " CAST(a.created_by_user_id AS TEXT),\n" + + " nu.name,\n" + + " a.created_at,\n" + + " a.submitted_at,\n" + + " a.hdc_eligibility_date,\n" + + " asu.label,\n" + + " CAST(asu.status_id AS TEXT),\n" + + " a.referring_prison_code,\n" + + " a.conditional_release_date,\n" + + " asu.created_at AS status_created_at,\n" + + " a.abandoned_at\n" + + "FROM cas_2_applications a\n" + + "LEFT JOIN (SELECT DISTINCT ON (application_id) su.application_id, su.label, su.status_id, su.created_at\n" + + " FROM cas_2_bail_status_updates su\n" + + " ORDER BY su.application_id, su.created_at DESC) as asu\n" + + " ON a.id = asu.application_id\n" + + "JOIN nomis_users nu ON nu.id = a.created_by_user_id").resultList + /* END HERE - TOBY DEBUG */ + val responseBody = - objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) + objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) // check transformers were able to return all fields Assertions.assertThat(responseBody).anyMatch { @@ -375,7 +418,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } Assertions.assertThat(responseBody).noneMatch { - otherCas2ApplicationEntity.id == it.id + othercas2BailApplicationEntity.id == it.id } Assertions.assertThat(responseBody).noneMatch { diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/repository/ApplicationTestRepository.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/repository/ApplicationTestRepository.kt index f0a9ce5c1b..b3e0e450e1 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/repository/ApplicationTestRepository.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/repository/ApplicationTestRepository.kt @@ -6,6 +6,7 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApprovedPremi import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.TemporaryAccommodationApplicationEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailAssessmentEntity import java.util.UUID @Repository @@ -17,5 +18,8 @@ interface Cas2ApplicationTestRepository : JpaRepository +@Repository +interface Cas2BailAssessmentTestRepository : JpaRepository + @Repository interface TemporaryAccommodationApplicationTestRepository : JpaRepository From 06d14297b4835848ba014ba03e7b32931732f4b8 Mon Sep 17 00:00:00 2001 From: garethCAS2 Date: Wed, 4 Dec 2024 11:08:47 +0000 Subject: [PATCH 28/37] feat: Cas2BailApplication tests running --- ...roller_and_linked_services_in_cas2bail.sql | 2 +- .../integration/IntegrationTestBase.kt | 4 +- .../cas2bail/Cas2BailApplicationTest.kt | 297 ++++++++---------- 3 files changed, 133 insertions(+), 170 deletions(-) diff --git a/src/main/resources/db/migration/all/20241128163133__application_controller_and_linked_services_in_cas2bail.sql b/src/main/resources/db/migration/all/20241128163133__application_controller_and_linked_services_in_cas2bail.sql index 5c869be832..a3e0a7f1a5 100644 --- a/src/main/resources/db/migration/all/20241128163133__application_controller_and_linked_services_in_cas2bail.sql +++ b/src/main/resources/db/migration/all/20241128163133__application_controller_and_linked_services_in_cas2bail.sql @@ -125,7 +125,7 @@ CREATE OR REPLACE VIEW cas_2_bail_application_summary AS SELECT a.conditional_release_date, asu.created_at AS status_created_at, a.abandoned_at -FROM cas_2_applications a +FROM cas_2_bail_applications a LEFT JOIN (SELECT DISTINCT ON (application_id) su.application_id, su.label, su.status_id, su.created_at FROM cas_2_bail_status_updates su ORDER BY su.application_id, su.created_at DESC) as asu diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/IntegrationTestBase.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/IntegrationTestBase.kt index 1e18204d4c..57aa23de91 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/IntegrationTestBase.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/IntegrationTestBase.kt @@ -377,7 +377,7 @@ abstract class IntegrationTestBase { lateinit var approvedPremisesApplicationEntityFactory: PersistedFactory lateinit var cas2ApplicationEntityFactory: PersistedFactory lateinit var cas2BailApplicationEntityFactory: PersistedFactory - lateinit var cas2BailAssementEntityFactory: PersistedFactory + lateinit var cas2BailAssessmentEntityFactory: PersistedFactory lateinit var cas2AssessmentEntityFactory: PersistedFactory lateinit var cas2StatusUpdateEntityFactory: PersistedFactory @@ -494,7 +494,7 @@ abstract class IntegrationTestBase { cas2ApplicationEntityFactory = PersistedFactory({ Cas2ApplicationEntityFactory() }, cas2ApplicationRepository) cas2BailApplicationEntityFactory = PersistedFactory({ Cas2BailApplicationEntityFactory() }, cas2BailApplicationRepository) - cas2BailAssementEntityFactory = PersistedFactory({ Cas2BailAssessmentEntityFactory() }, cas2BailAssessmentRepository) + cas2BailAssessmentEntityFactory = PersistedFactory({ Cas2BailAssessmentEntityFactory() }, cas2BailAssessmentRepository) cas2AssessmentEntityFactory = PersistedFactory({ Cas2AssessmentEntityFactory() }, cas2AssessmentRepository) cas2StatusUpdateEntityFactory = PersistedFactory({ Cas2StatusUpdateEntityFactory() }, cas2StatusUpdateRepository) cas2BailStatusUpdateEntityFactory = PersistedFactory({ Cas2BailStatusUpdateEntityFactory() }, cas2BailStatusUpdateRepository) diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationTest.kt index da0668291f..b79cf1126e 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationTest.kt @@ -16,10 +16,6 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ValueSource import org.springframework.beans.factory.annotation.Autowired -import org.springframework.data.jpa.repository.JpaRepository -import org.springframework.data.jpa.repository.Query -import org.springframework.data.repository.query.Param -import org.springframework.stereotype.Repository import org.springframework.test.web.reactive.server.returnResult import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.* import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.IntegrationTestBase @@ -31,7 +27,6 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.httpmocks.co import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.httpmocks.prisonAPIMockNotFoundInmateDetailsCall import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.* import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationSummaryEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailStatusUpdateEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.community.OffenderDetailSummary import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.randomDateAfter @@ -44,10 +39,6 @@ import kotlin.math.sign class Cas2BailApplicationTest : IntegrationTestBase() { - /* START HERE - TOBY DEBUG */ - @Autowired - private lateinit var entityManager: EntityManager - @SpykBean lateinit var realApplicationRepository: ApplicationRepository @@ -87,7 +78,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { inner class ControlsOnExternalUsers { @ParameterizedTest @ValueSource(strings = ["ROLE_CAS2_ASSESSOR", "ROLE_CAS2_MI"]) - fun `creating an application is forbidden to external users based on role`(role: String) { + fun `creating a cas2bail application is forbidden to external users based on role`(role: String) { val jwt = jwtAuthHelper.createClientCredentialsJwt( username = "username", authSource = "nomis", @@ -104,7 +95,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { @ParameterizedTest @ValueSource(strings = ["ROLE_CAS2_ASSESSOR", "ROLE_CAS2_MI"]) - fun `updating an application is forbidden to external users based on role`(role: String) { + fun `updating a cas2bail application is forbidden to external users based on role`(role: String) { val jwt = jwtAuthHelper.createClientCredentialsJwt( username = "username", authSource = "nomis", @@ -120,7 +111,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } @Test - fun `viewing list of applications is forbidden to external users based on role`() { + fun `viewing list of cas2bail applications is forbidden to external users based on role`() { val jwt = jwtAuthHelper.createClientCredentialsJwt( username = "username", authSource = "nomis", @@ -136,7 +127,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } @Test - fun `viewing an application is forbidden to external users based on role`() { + fun `viewing a cas2bail application is forbidden to external users based on role`() { val jwt = jwtAuthHelper.createClientCredentialsJwt( username = "username", authSource = "nomis", @@ -156,7 +147,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { @Nested inner class MissingJwt { @Test - fun `Get all applications without JWT returns 401`() { + fun `Get all cas2bail applications without JWT returns 401`() { webTestClient.get() .uri("/cas2bail/applications") .exchange() @@ -165,7 +156,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } @Test - fun `Get single application without JWT returns 401`() { + fun `Get single cas2bail application without JWT returns 401`() { webTestClient.get() .uri("/cas2bail/applications/9b785e59-b85c-4be0-b271-d9ac287684b6") .exchange() @@ -174,7 +165,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } @Test - fun `Create new application without JWT returns 401`() { + fun `Create new cas2bail application without JWT returns 401`() { webTestClient.post() .uri("/cas2bail/applications") .exchange() @@ -188,7 +179,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { @Test - fun `return unexpired applications when applications GET is requested`() { + fun `return unexpired cas2bail applications when applications GET is requested`() { val unexpiredSubset = setOf( Pair("More information requested", UUID.fromString("f5cd423b-08eb-4efb-96ff-5cc6bb073905")), Pair("Awaiting decision", UUID.fromString("ba4d8432-250b-4ab9-81ec-7eb4b16e5dd1")), @@ -292,7 +283,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } @Test - fun `Get all applications returns 200 with correct body`() { + fun `Get all cas2bail applications returns 200 with correct body`() { givenACas2PomUser { userEntity, jwt -> givenACas2PomUser { otherUser, _ -> givenAnOffender { offenderDetails, _ -> @@ -369,34 +360,6 @@ class Cas2BailApplicationTest : IntegrationTestBase() { .responseBody .blockFirst() - /* START HERE - TOBY DEBUG */ - val r1 = entityManager.createNativeQuery("SELECT * FROM cas_2_bail_applications").resultList - val r2 = entityManager.createNativeQuery("SELECT * FROM cas_2_bail_application_live_summary").resultList - val r3 = entityManager.createNativeQuery("SELECT * FROM cas_2_bail_status_updates").resultList - val r4 = entityManager.createNativeQuery("SELECT * FROM cas_2_bail_assessments").resultList - val r5 = entityManager.createNativeQuery("SELECT\n" + - " a.id,\n" + - " a.crn,\n" + - " a.noms_number,\n" + - " CAST(a.created_by_user_id AS TEXT),\n" + - " nu.name,\n" + - " a.created_at,\n" + - " a.submitted_at,\n" + - " a.hdc_eligibility_date,\n" + - " asu.label,\n" + - " CAST(asu.status_id AS TEXT),\n" + - " a.referring_prison_code,\n" + - " a.conditional_release_date,\n" + - " asu.created_at AS status_created_at,\n" + - " a.abandoned_at\n" + - "FROM cas_2_applications a\n" + - "LEFT JOIN (SELECT DISTINCT ON (application_id) su.application_id, su.label, su.status_id, su.created_at\n" + - " FROM cas_2_bail_status_updates su\n" + - " ORDER BY su.application_id, su.created_at DESC) as asu\n" + - " ON a.id = asu.application_id\n" + - "JOIN nomis_users nu ON nu.id = a.created_by_user_id").resultList - /* END HERE - TOBY DEBUG */ - val responseBody = objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) @@ -439,19 +402,19 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } @Test - fun `Get all applications with pagination returns 200 with correct body and header`() { + fun `Get all cas2bail applications with pagination returns 200 with correct body and header`() { givenACas2PomUser { userEntity, jwt -> givenACas2PomUser { otherUser, _ -> givenAnOffender { offenderDetails, _ -> - cas2ApplicationJsonSchemaRepository.deleteAll() + cas2BailApplicationJsonSchemaRepository.deleteAll() - val applicationSchema = cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + val applicationSchema = cas2BailApplicationJsonSchemaEntityFactory.produceAndPersist { withAddedAt(OffsetDateTime.now()) withId(UUID.randomUUID()) } repeat(12) { - cas2ApplicationEntityFactory.produceAndPersist { + cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(applicationSchema) withCreatedByUser(userEntity) withCrn(offenderDetails.otherIds.crn) @@ -461,7 +424,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } val rawResponseBodyPage1 = webTestClient.get() - .uri("/cas2/applications?page=1") + .uri("/cas2bail/applications?page=1") .header("Authorization", "Bearer $jwt") .header("X-Service-Name", ServiceName.cas2.value) .exchange() @@ -476,14 +439,14 @@ class Cas2BailApplicationTest : IntegrationTestBase() { .blockFirst() val responseBodyPage1 = - objectMapper.readValue(rawResponseBodyPage1, object : TypeReference>() {}) + objectMapper.readValue(rawResponseBodyPage1, object : TypeReference>() {}) Assertions.assertThat(responseBodyPage1).size().isEqualTo(10) Assertions.assertThat(isOrderedByCreatedAtDescending(responseBodyPage1)).isTrue() val rawResponseBodyPage2 = webTestClient.get() - .uri("/cas2/applications?page=2") + .uri("/cas2bail/applications?page=2") .header("Authorization", "Bearer $jwt") .header("X-Service-Name", ServiceName.cas2.value) .exchange() @@ -498,7 +461,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { .blockFirst() val responseBodyPage2 = - objectMapper.readValue(rawResponseBodyPage2, object : TypeReference>() {}) + objectMapper.readValue(rawResponseBodyPage2, object : TypeReference>() {}) Assertions.assertThat(responseBodyPage2).size().isEqualTo(2) } @@ -507,7 +470,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } @Test - fun `When a person is not found, returns 200 with placeholder text`() { + fun `When a person is not found in cas2bail, returns 200 with placeholder text`() { givenACas2PomUser { userEntity, jwt -> val crn = "X1234" @@ -515,7 +478,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { communityAPIMockNotFoundOffenderDetailsCall(crn) webTestClient.get() - .uri("/cas2/applications") + .uri("/cas2bail/applications") .header("Authorization", "Bearer $jwt") .exchange() .expectStatus() @@ -540,7 +503,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { * * If all dates are ascending the multiple will also be 0 ( = 1 x 0 x 0 etc.). */ - private fun isOrderedByCreatedAtDescending(responseBody: List): Boolean { + private fun isOrderedByCreatedAtDescending(responseBody: List): Boolean { var allDescending = 1 for (i in 1..(responseBody.size - 1)) { val isDescending = (responseBody[i - 1].createdAt.epochSecond - responseBody[i].createdAt.epochSecond).sign @@ -556,9 +519,9 @@ class Cas2BailApplicationTest : IntegrationTestBase() { givenACas2Assessor { assessor, _ -> givenACas2PomUser { userAPrisonA, jwt -> givenAnOffender { offenderDetails, _ -> - cas2ApplicationJsonSchemaRepository.deleteAll() + cas2BailApplicationJsonSchemaRepository.deleteAll() - val applicationSchema = cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + val applicationSchema = cas2BailApplicationJsonSchemaEntityFactory.produceAndPersist { withAddedAt(OffsetDateTime.now()) withId(UUID.randomUUID()) } @@ -567,7 +530,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { repeat(5) { userAPrisonAApplicationIds.add( - cas2ApplicationEntityFactory.produceAndPersist { + cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(applicationSchema) withCreatedByUser(userAPrisonA) withCrn(offenderDetails.otherIds.crn) @@ -587,7 +550,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { // submitted applications with conditional release dates in the future repeat(6) { userBPrisonAApplicationIds.add( - cas2ApplicationEntityFactory.produceAndPersist { + cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(applicationSchema) withCreatedByUser(userBPrisonA) withCrn(offenderDetails.otherIds.crn) @@ -603,7 +566,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { // submitted applications with conditional release dates today repeat(2) { userBPrisonAApplicationIds.add( - cas2ApplicationEntityFactory.produceAndPersist { + cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(applicationSchema) withCreatedByUser(userBPrisonA) withCrn(offenderDetails.otherIds.crn) @@ -617,7 +580,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } // submitted application with a conditional release date before today - val excludedApplicationId = cas2ApplicationEntityFactory.produceAndPersist { + val excludedApplicationId = cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(applicationSchema) withCreatedByUser(userBPrisonA) withCrn(offenderDetails.otherIds.crn) @@ -634,7 +597,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { withActiveCaseloadId("another prison") } - val otherPrisonApplication = cas2ApplicationEntityFactory.produceAndPersist { + val otherPrisonApplication = cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(applicationSchema) withCreatedByUser(userCPrisonB) withCrn(offenderDetails.otherIds.crn) @@ -644,7 +607,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } val rawResponseBody = webTestClient.get() - .uri("/cas2/applications?prisonCode=${userAPrisonA.activeCaseloadId}") + .uri("/cas2bail/applications?prisonCode=${userAPrisonA.activeCaseloadId}") .header("Authorization", "Bearer $jwt") .header("X-Service-Name", ServiceName.cas2.value) .exchange() @@ -655,7 +618,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { .blockFirst() val responseBody = - objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) + objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) Assertions.assertThat(responseBody).noneMatch { excludedApplicationId == it.id @@ -683,9 +646,9 @@ class Cas2BailApplicationTest : IntegrationTestBase() { givenACas2Assessor { assessor, _ -> givenACas2PomUser { userAPrisonA, jwt -> givenAnOffender { offenderDetails, _ -> - cas2ApplicationJsonSchemaRepository.deleteAll() + cas2BailApplicationJsonSchemaRepository.deleteAll() - val applicationSchema = cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + val applicationSchema = cas2BailApplicationJsonSchemaEntityFactory.produceAndPersist { withAddedAt(OffsetDateTime.now()) withId(UUID.randomUUID()) } @@ -694,7 +657,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { repeat(5) { userAPrisonAApplicationIds.add( - cas2ApplicationEntityFactory.produceAndPersist { + cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(applicationSchema) withCreatedByUser(userAPrisonA) withCrn(offenderDetails.otherIds.crn) @@ -713,7 +676,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { repeat(6) { userBPrisonAApplicationIds.add( - cas2ApplicationEntityFactory.produceAndPersist { + cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(applicationSchema) withCreatedByUser(userBPrisonA) withCrn(offenderDetails.otherIds.crn) @@ -729,7 +692,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { // submitted applications with conditional release dates today repeat(2) { userBPrisonAApplicationIds.add( - cas2ApplicationEntityFactory.produceAndPersist { + cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(applicationSchema) withCreatedByUser(userBPrisonA) withCrn(offenderDetails.otherIds.crn) @@ -743,7 +706,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } // submitted application with a conditional release date before today - val excludedApplicationId = cas2ApplicationEntityFactory.produceAndPersist { + val excludedApplicationId = cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(applicationSchema) withCreatedByUser(userBPrisonA) withCrn(offenderDetails.otherIds.crn) @@ -757,7 +720,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { addStatusUpdates(userBPrisonAApplicationIds.first(), assessor) val rawResponseBody = webTestClient.get() - .uri("/cas2/applications?isSubmitted=true&prisonCode=${userAPrisonA.activeCaseloadId}") + .uri("/cas2bail/applications?isSubmitted=true&prisonCode=${userAPrisonA.activeCaseloadId}") .header("Authorization", "Bearer $jwt") .header("X-Service-Name", ServiceName.cas2.value) .exchange() @@ -768,7 +731,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { .blockFirst() val responseBody = - objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) + objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) Assertions.assertThat(responseBody).noneMatch { excludedApplicationId == it.id @@ -788,9 +751,9 @@ class Cas2BailApplicationTest : IntegrationTestBase() { fun `Get all unsubmitted applications using prisonCode returns 200 with correct body`() { givenACas2PomUser { userAPrisonA, jwt -> givenAnOffender { offenderDetails, _ -> - cas2ApplicationJsonSchemaRepository.deleteAll() + cas2BailApplicationJsonSchemaRepository.deleteAll() - val applicationSchema = cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + val applicationSchema = cas2BailApplicationJsonSchemaEntityFactory.produceAndPersist { withAddedAt(OffsetDateTime.now()) withId(UUID.randomUUID()) } @@ -799,7 +762,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { repeat(5) { userAPrisonAApplicationIds.add( - cas2ApplicationEntityFactory.produceAndPersist { + cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(applicationSchema) withCreatedByUser(userAPrisonA) withCrn(offenderDetails.otherIds.crn) @@ -818,7 +781,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { repeat(6) { userBPrisonAApplicationIds.add( - cas2ApplicationEntityFactory.produceAndPersist { + cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(applicationSchema) withCreatedByUser(userBPrisonA) withCrn(offenderDetails.otherIds.crn) @@ -831,7 +794,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } val rawResponseBody = webTestClient.get() - .uri("/cas2/applications?isSubmitted=false&prisonCode=${userAPrisonA.activeCaseloadId}") + .uri("/cas2bail/applications?isSubmitted=false&prisonCode=${userAPrisonA.activeCaseloadId}") .header("Authorization", "Bearer $jwt") .header("X-Service-Name", ServiceName.cas2.value) .exchange() @@ -842,7 +805,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { .blockFirst() val responseBody = - objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) + objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) val returnedApplicationIds = responseBody.map { it.id }.toSet() @@ -855,7 +818,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { fun `Get applications using another prisonCode returns Forbidden 403`() { givenACas2PomUser { userAPrisonA, jwt -> val rawResponseBody = webTestClient.get() - .uri("/cas2/applications?prisonCode=${userAPrisonA.activeCaseloadId!!.reversed()}") + .uri("/cas2bail/applications?prisonCode=${userAPrisonA.activeCaseloadId!!.reversed()}") .header("Authorization", "Bearer $jwt") .header("X-Service-Name", ServiceName.cas2.value) .exchange() @@ -868,13 +831,13 @@ class Cas2BailApplicationTest : IntegrationTestBase() { @Nested inner class AsLicenceCaseAdminUser { @Test - fun `Get all submitted applications using prisonCode returns 200 with correct body`() { + fun `Get all submitted cas2bail applications using prisonCode returns 200 with correct body`() { givenACas2Assessor { assessor, _ -> givenACas2LicenceCaseAdminUser { caseAdminPrisonA, jwt -> givenAnOffender { offenderDetails, _ -> - cas2ApplicationJsonSchemaRepository.deleteAll() + cas2BailApplicationJsonSchemaRepository.deleteAll() - val applicationSchema = cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + val applicationSchema = cas2BailApplicationJsonSchemaEntityFactory.produceAndPersist { withAddedAt(OffsetDateTime.now()) withId(UUID.randomUUID()) } @@ -888,7 +851,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { // submitted applications with conditional release dates in the future repeat(6) { userBPrisonAApplicationIds.add( - cas2ApplicationEntityFactory.produceAndPersist { + cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(applicationSchema) withCreatedByUser(pomUserPrisonA) withCrn(offenderDetails.otherIds.crn) @@ -904,7 +867,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { // submitted applications with conditional release date of today repeat(2) { userBPrisonAApplicationIds.add( - cas2ApplicationEntityFactory.produceAndPersist { + cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(applicationSchema) withCreatedByUser(pomUserPrisonA) withCrn(offenderDetails.otherIds.crn) @@ -918,7 +881,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } // submitted application with a conditional release date before today - val excludedApplicationId = cas2ApplicationEntityFactory.produceAndPersist { + val excludedApplicationId = cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(applicationSchema) withCreatedByUser(pomUserPrisonA) withCrn(offenderDetails.otherIds.crn) @@ -933,7 +896,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { withActiveCaseloadId("other_prison") } - val otherPrisonApplication = cas2ApplicationEntityFactory.produceAndPersist { + val otherPrisonApplication = cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(applicationSchema) withCreatedByUser(pomUserPrisonB) withCrn(offenderDetails.otherIds.crn) @@ -944,7 +907,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } val rawResponseBody = webTestClient.get() - .uri("/cas2/applications?isSubmitted=true&prisonCode=${caseAdminPrisonA.activeCaseloadId}") + .uri("/cas2bail/applications?isSubmitted=true&prisonCode=${caseAdminPrisonA.activeCaseloadId}") .header("Authorization", "Bearer $jwt") .header("X-Service-Name", ServiceName.cas2.value) .exchange() @@ -955,7 +918,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { .blockFirst() val responseBody = - objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) + objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) Assertions.assertThat(responseBody).noneMatch { excludedApplicationId == it.id @@ -974,16 +937,16 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } private fun addStatusUpdates(applicationId: UUID, assessor: ExternalUserEntity) { - cas2StatusUpdateEntityFactory.produceAndPersist { + cas2BailStatusUpdateEntityFactory.produceAndPersist { withLabel("More information requested") - withApplication(cas2ApplicationRepository.findById(applicationId).get()) + withApplication(cas2BailApplicationRepository.findById(applicationId).get()) withAssessor(assessor) } // this is the one that should be returned as latestStatusUpdate - cas2StatusUpdateEntityFactory.produceAndPersist { + cas2BailStatusUpdateEntityFactory.produceAndPersist { withStatusId(UUID.fromString("c74c3e54-52d8-4aa2-86f6-05190985efee")) withLabel("Awaiting decision") - withApplication(cas2ApplicationRepository.findById(applicationId).get()) + withApplication(cas2BailApplicationRepository.findById(applicationId).get()) withAssessor(assessor) } } @@ -1002,9 +965,9 @@ class Cas2BailApplicationTest : IntegrationTestBase() { givenACas2PomUser { userEntity, jwt -> givenACas2PomUser { otherUser, _ -> givenAnOffender { offenderDetails, _ -> - cas2ApplicationJsonSchemaRepository.deleteAll() + cas2BailApplicationJsonSchemaRepository.deleteAll() - val applicationSchema = cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + val applicationSchema = cas2BailApplicationJsonSchemaEntityFactory.produceAndPersist { withAddedAt(OffsetDateTime.now()) withId(UUID.randomUUID()) } @@ -1013,7 +976,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { // with most recent first and conditional release dates in the future repeat(3) { submittedIds.add( - cas2ApplicationEntityFactory.produceAndPersist { + cas2BailApplicationEntityFactory.produceAndPersist { withCreatedAt(OffsetDateTime.now().minusDays(it.toLong())) withApplicationSchema(applicationSchema) withCreatedByUser(userEntity) @@ -1029,7 +992,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { // with most recent first and conditional release dates of today repeat(2) { submittedIds.add( - cas2ApplicationEntityFactory.produceAndPersist { + cas2BailApplicationEntityFactory.produceAndPersist { withCreatedAt(OffsetDateTime.now().minusDays(it.toLong() + 3)) withApplicationSchema(applicationSchema) withCreatedByUser(userEntity) @@ -1042,7 +1005,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } // submitted application with a conditional release date before today - excludedApplicationId = cas2ApplicationEntityFactory.produceAndPersist { + excludedApplicationId = cas2BailApplicationEntityFactory.produceAndPersist { withCreatedAt(OffsetDateTime.now().minusDays(14)) withApplicationSchema(applicationSchema) withCreatedByUser(userEntity) @@ -1057,7 +1020,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { // create 4 x un-submitted in-progress applications for this user repeat(4) { unSubmittedIds.add( - cas2ApplicationEntityFactory.produceAndPersist { + cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(applicationSchema) withCreatedByUser(userEntity) withCrn(offenderDetails.otherIds.crn) @@ -1067,7 +1030,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } // create a submitted application by another user which should not be in results - cas2ApplicationEntityFactory.produceAndPersist { + cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(applicationSchema) withCreatedByUser(otherUser) withCrn(offenderDetails.otherIds.crn) @@ -1076,7 +1039,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } // create an unsubmitted application by another user which should not be in results - cas2ApplicationEntityFactory.produceAndPersist { + cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(applicationSchema) withCreatedByUser(otherUser) withCrn(offenderDetails.otherIds.crn) @@ -1091,9 +1054,9 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } @Test - fun `returns all applications for user when isSubmitted is null`() { + fun `returns all cas2bail applications for user when isSubmitted is null`() { val rawResponseBody = webTestClient.get() - .uri("/cas2/applications") + .uri("/cas2bail/applications") .header("Authorization", "Bearer $jwtForUser") .header("X-Service-Name", ServiceName.cas2.value) .exchange() @@ -1104,7 +1067,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { .blockFirst() val responseBody = - objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) + objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) Assertions.assertThat(responseBody).noneMatch { excludedApplicationId == it.id @@ -1115,9 +1078,9 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } @Test - fun `returns submitted applications for user when isSubmitted is true`() { + fun `returns submitted cas2bail applications for user when isSubmitted is true`() { val rawResponseBody = webTestClient.get() - .uri("/cas2/applications?isSubmitted=true") + .uri("/cas2bail/applications?isSubmitted=true") .header("Authorization", "Bearer $jwtForUser") .header("X-Service-Name", ServiceName.cas2.value) .exchange() @@ -1128,7 +1091,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { .blockFirst() val responseBody = - objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) + objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) Assertions.assertThat(responseBody).noneMatch { excludedApplicationId == it.id @@ -1141,9 +1104,9 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } @Test - fun `returns submitted applications for user when isSubmitted is true and page specified`() { + fun `returns submitted cas2bail applications for user when isSubmitted is true and page specified`() { val rawResponseBody = webTestClient.get() - .uri("/cas2/applications?isSubmitted=true&page=1") + .uri("/cas2bail/applications?isSubmitted=true&page=1") .header("Authorization", "Bearer $jwtForUser") .header("X-Service-Name", ServiceName.cas2.value) .exchange() @@ -1154,7 +1117,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { .blockFirst() val responseBody = - objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) + objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) val uuids = responseBody.map { it.id }.toSet() Assertions.assertThat(uuids).isEqualTo(submittedIds) @@ -1163,9 +1126,9 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } @Test - fun `returns unsubmitted applications for user when isSubmitted is false`() { + fun `returns unsubmitted cas2bail applications for user when isSubmitted is false`() { val rawResponseBody = webTestClient.get() - .uri("/cas2/applications?isSubmitted=false") + .uri("/cas2bail/applications?isSubmitted=false") .header("Authorization", "Bearer $jwtForUser") .header("X-Service-Name", ServiceName.cas2.value) .exchange() @@ -1176,7 +1139,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { .blockFirst() val responseBody = - objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) + objectMapper.readValue(rawResponseBody, object : TypeReference>() {}) val uuids = responseBody.map { it.id }.toSet() Assertions.assertThat(uuids).isEqualTo(unSubmittedIds) @@ -1190,12 +1153,12 @@ class Cas2BailApplicationTest : IntegrationTestBase() { inner class WhenCreatedBySameUser { // When the application requested was created by the logged-in user @Test - fun `Get single in progress application returns 200 with correct body`() { + fun `Get single in progress cas2bail application returns 200 with correct body`() { givenACas2PomUser { userEntity, jwt -> givenAnOffender { offenderDetails, inmateDetails -> - cas2ApplicationJsonSchemaRepository.deleteAll() + cas2BailApplicationJsonSchemaRepository.deleteAll() - val newestJsonSchema = cas2ApplicationJsonSchemaEntityFactory + val newestJsonSchema = cas2BailApplicationJsonSchemaEntityFactory .produceAndPersist { withAddedAt(OffsetDateTime.parse("2022-09-21T12:45:00+01:00")) withSchema( @@ -1203,7 +1166,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { ) } - val applicationEntity = cas2ApplicationEntityFactory.produceAndPersist { + val applicationEntity = cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(newestJsonSchema) withCrn(offenderDetails.otherIds.crn) withCreatedByUser(userEntity) @@ -1213,7 +1176,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } val rawResponseBody = webTestClient.get() - .uri("/cas2/applications/${applicationEntity.id}") + .uri("/cas2bail/applications/${applicationEntity.id}") .header("Authorization", "Bearer $jwt") .exchange() .expectStatus() @@ -1241,7 +1204,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } @Test - fun `Get single application returns successfully when the offender cannot be fetched from the prisons API`() { + fun `Get single cas2bail application returns successfully when the offender cannot be fetched from the prisons API`() { givenACas2PomUser { userEntity, jwt -> val crn = "X1234" @@ -1257,7 +1220,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { loadPreemptiveCacheForInmateDetails(offenderDetails.otherIds.nomsNumber!!) val rawResponseBody = webTestClient.get() - .uri("/cas2/applications/${application.id}") + .uri("/cas2bail/applications/${application.id}") .header("Authorization", "Bearer $jwt") .exchange() .expectStatus() @@ -1287,12 +1250,12 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } @Test - fun `Get single submitted application returns 200 with timeline events`() { + fun `Get single submitted cas2bail application returns 200 with timeline events`() { givenACas2PomUser { userEntity, jwt -> givenAnOffender { offenderDetails, inmateDetails -> - cas2ApplicationJsonSchemaRepository.deleteAll() + cas2BailApplicationJsonSchemaRepository.deleteAll() - val newestJsonSchema = cas2ApplicationJsonSchemaEntityFactory + val newestJsonSchema = cas2BailApplicationJsonSchemaEntityFactory .produceAndPersist { withAddedAt(OffsetDateTime.parse("2022-09-21T12:45:00+01:00")) withSchema( @@ -1300,14 +1263,14 @@ class Cas2BailApplicationTest : IntegrationTestBase() { ) } - val applicationEntity = cas2ApplicationEntityFactory.produceAndPersist { + val applicationEntity = cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(newestJsonSchema) withCrn(offenderDetails.otherIds.crn) withCreatedByUser(userEntity) withSubmittedAt(OffsetDateTime.now().minusDays(1)) } - cas2AssessmentEntityFactory.produceAndPersist { + cas2BailAssessmentEntityFactory.produceAndPersist { withApplication(applicationEntity) } @@ -1341,16 +1304,16 @@ class Cas2BailApplicationTest : IntegrationTestBase() { @Nested inner class WhenDifferentPrison { @Test - fun `Get single submitted application is forbidden`() { + fun `Get single submitted cas2bail application is forbidden`() { givenACas2PomUser { userEntity, jwt -> givenAnOffender { offenderDetails, inmateDetails -> - cas2ApplicationJsonSchemaRepository.deleteAll() + cas2BailApplicationJsonSchemaRepository.deleteAll() val otherUser = nomisUserEntityFactory.produceAndPersist { withActiveCaseloadId("other_caseload") } - val newestJsonSchema = cas2ApplicationJsonSchemaEntityFactory + val newestJsonSchema = cas2BailApplicationJsonSchemaEntityFactory .produceAndPersist { withAddedAt(OffsetDateTime.parse("2022-09-21T12:45:00+01:00")) withSchema( @@ -1358,7 +1321,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { ) } - val applicationEntity = cas2ApplicationEntityFactory.produceAndPersist { + val applicationEntity = cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(newestJsonSchema) withCrn(offenderDetails.otherIds.crn) withSubmittedAt(OffsetDateTime.now()) @@ -1369,7 +1332,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } webTestClient.get() - .uri("/cas2/applications/${applicationEntity.id}") + .uri("/cas2bail/applications/${applicationEntity.id}") .header("Authorization", "Bearer $jwt") .exchange() .expectStatus() @@ -1382,12 +1345,12 @@ class Cas2BailApplicationTest : IntegrationTestBase() { @Nested inner class WhenSamePrison { @Test - fun `Get single submitted application returns 200 with timeline events`() { + fun `Get single submitted cas2bail application returns 200 with timeline events`() { givenACas2PomUser { userEntity, jwt -> givenAnOffender { offenderDetails, inmateDetails -> - cas2ApplicationJsonSchemaRepository.deleteAll() + cas2BailApplicationJsonSchemaRepository.deleteAll() - val newestJsonSchema = cas2ApplicationJsonSchemaEntityFactory + val newestJsonSchema = cas2BailApplicationJsonSchemaEntityFactory .produceAndPersist { withAddedAt(OffsetDateTime.parse("2022-09-21T12:45:00+01:00")) withSchema( @@ -1399,7 +1362,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { withActiveCaseloadId(userEntity.activeCaseloadId!!) } - val applicationEntity = cas2ApplicationEntityFactory.produceAndPersist { + val applicationEntity = cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(newestJsonSchema) withCrn(offenderDetails.otherIds.crn) withCreatedByUser(otherUser) @@ -1407,12 +1370,12 @@ class Cas2BailApplicationTest : IntegrationTestBase() { withReferringPrisonCode(userEntity.activeCaseloadId!!) } - cas2AssessmentEntityFactory.produceAndPersist { + cas2BailAssessmentEntityFactory.produceAndPersist { withApplication(applicationEntity) } val rawResponseBody = webTestClient.get() - .uri("/cas2/applications/${applicationEntity.id}") + .uri("/cas2bail/applications/${applicationEntity.id}") .header("Authorization", "Bearer $jwt") .exchange() .expectStatus() @@ -1435,12 +1398,12 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } @Test - fun `Get single unsubmitted application returns 403`() { + fun `Get single unsubmitted cas2bail application returns 403`() { givenACas2PomUser { userEntity, jwt -> givenAnOffender { offenderDetails, inmateDetails -> - cas2ApplicationJsonSchemaRepository.deleteAll() + cas2BailApplicationJsonSchemaRepository.deleteAll() - val newestJsonSchema = cas2ApplicationJsonSchemaEntityFactory + val newestJsonSchema = cas2BailApplicationJsonSchemaEntityFactory .produceAndPersist { withAddedAt(OffsetDateTime.parse("2022-09-21T12:45:00+01:00")) withSchema( @@ -1452,7 +1415,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { withActiveCaseloadId(userEntity.activeCaseloadId!!) } - val applicationEntity = cas2ApplicationEntityFactory.produceAndPersist { + val applicationEntity = cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(newestJsonSchema) withCrn(offenderDetails.otherIds.crn) withCreatedByUser(otherUser) @@ -1460,7 +1423,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } webTestClient.get() - .uri("/cas2/applications/${applicationEntity.id}") + .uri("/cas2bail/applications/${applicationEntity.id}") .header("Authorization", "Bearer $jwt") .exchange() .expectStatus() @@ -1478,17 +1441,17 @@ class Cas2BailApplicationTest : IntegrationTestBase() { @Nested inner class PomUsers { @Test - fun `Create new application for CAS-2 returns 201 with correct body and Location header`() { + fun `Create new application for cas2bail returns 201 with correct body and Location header`() { givenACas2PomUser { userEntity, jwt -> givenAnOffender { offenderDetails, _ -> val applicationSchema = - cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + cas2BailApplicationJsonSchemaEntityFactory.produceAndPersist { withAddedAt(OffsetDateTime.now()) withId(UUID.randomUUID()) } val result = webTestClient.post() - .uri("/cas2/applications") + .uri("/cas2bail/applications") .header("Authorization", "Bearer $jwt") .header("X-Service-Name", ServiceName.cas2.value) .bodyValue( @@ -1502,7 +1465,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { .returnResult(Cas2Application::class.java) Assertions.assertThat(result.responseHeaders["Location"]).anyMatch { - it.matches(Regex("/cas2/applications/.+")) + it.matches(Regex("/cas2bail/applications/.+")) } Assertions.assertThat(result.responseBody.blockFirst()).matches { @@ -1514,19 +1477,19 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } @Test - fun `Create new application returns 404 when a person cannot be found`() { + fun `Create new cas2bail application returns 404 when a person cannot be found`() { givenACas2PomUser { userEntity, jwt -> val crn = "X1234" communityAPIMockNotFoundOffenderDetailsCall(crn) - cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + cas2BailApplicationJsonSchemaEntityFactory.produceAndPersist { withAddedAt(OffsetDateTime.now()) withId(UUID.randomUUID()) } webTestClient.post() - .uri("/cas2/applications") + .uri("/cas2bail/applications") .header("Authorization", "Bearer $jwt") .bodyValue( NewApplication( @@ -1545,17 +1508,17 @@ class Cas2BailApplicationTest : IntegrationTestBase() { @Nested inner class LicenceCaseAdminUsers { @Test - fun `Create new application for CAS-2 returns 201 with correct body and Location header`() { + fun `Create new cas2bail application for CAS-2 returns 201 with correct body and Location header`() { givenACas2LicenceCaseAdminUser { _, jwt -> givenAnOffender { offenderDetails, _ -> val applicationSchema = - cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + cas2BailApplicationJsonSchemaEntityFactory.produceAndPersist { withAddedAt(OffsetDateTime.now()) withId(UUID.randomUUID()) } val result = webTestClient.post() - .uri("/cas2/applications") + .uri("/cas2bail/applications") .header("Authorization", "Bearer $jwt") .header("X-Service-Name", ServiceName.cas2.value) .bodyValue( @@ -1581,19 +1544,19 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } @Test - fun `Create new application returns 404 when a person cannot be found`() { + fun `Create new cas2bail application returns 404 when a person cannot be found`() { givenACas2LicenceCaseAdminUser { _, jwt -> val crn = "X1234" communityAPIMockNotFoundOffenderDetailsCall(crn) - cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + cas2BailApplicationJsonSchemaEntityFactory.produceAndPersist { withAddedAt(OffsetDateTime.now()) withId(UUID.randomUUID()) } webTestClient.post() - .uri("/cas2/applications") + .uri("/cas2bail/applications") .header("Authorization", "Bearer $jwt") .bodyValue( NewApplication( @@ -1616,13 +1579,13 @@ class Cas2BailApplicationTest : IntegrationTestBase() { @Nested inner class PomUsers { @Test - fun `Update existing CAS2 application returns 200 with correct body`() { + fun `Update existing cas2bail application returns 200 with correct body`() { givenACas2PomUser { submittingUser, jwt -> givenAnOffender { offenderDetails, _ -> val applicationId = UUID.fromString("22ceda56-98b2-411d-91cc-ace0ab8be872") val applicationSchema = - cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + cas2BailApplicationJsonSchemaEntityFactory.produceAndPersist { withAddedAt(OffsetDateTime.now()) withId(UUID.randomUUID()) withSchema( @@ -1630,15 +1593,15 @@ class Cas2BailApplicationTest : IntegrationTestBase() { ) } - cas2ApplicationEntityFactory.produceAndPersist { + cas2BailApplicationEntityFactory.produceAndPersist { withCrn(offenderDetails.otherIds.crn) withId(applicationId) withApplicationSchema(applicationSchema) withCreatedByUser(submittingUser) } - +//TODO what is a UpdateCas2Application? val resultBody = webTestClient.put() - .uri("/cas2/applications/$applicationId") + .uri("/cas2bail/applications/$applicationId") .header("Authorization", "Bearer $jwt") .bodyValue( UpdateCas2Application( @@ -1664,13 +1627,13 @@ class Cas2BailApplicationTest : IntegrationTestBase() { @Nested inner class LicenceCaseAdminUsers { @Test - fun `Update existing CAS2 application returns 200 with correct body`() { + fun `Update existing cas2bail application returns 200 with correct body`() { givenACas2LicenceCaseAdminUser { submittingUser, jwt -> givenAnOffender { offenderDetails, _ -> val applicationId = UUID.fromString("22ceda56-98b2-411d-91cc-ace0ab8be872") val applicationSchema = - cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + cas2BailApplicationJsonSchemaEntityFactory.produceAndPersist { withAddedAt(OffsetDateTime.now()) withId(UUID.randomUUID()) withSchema( @@ -1678,7 +1641,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { ) } - cas2ApplicationEntityFactory.produceAndPersist { + cas2BailApplicationEntityFactory.produceAndPersist { withCrn(offenderDetails.otherIds.crn) withId(applicationId) withApplicationSchema(applicationSchema) @@ -1686,7 +1649,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } val resultBody = webTestClient.put() - .uri("/cas2/applications/$applicationId") + .uri("/cas2bail/applications/$applicationId") .header("Authorization", "Bearer $jwt") .bodyValue( UpdateCas2Application( @@ -1720,15 +1683,15 @@ class Cas2BailApplicationTest : IntegrationTestBase() { private fun produceAndPersistBasicApplication( crn: String, userEntity: NomisUserEntity, - ): Cas2ApplicationEntity { - val jsonSchema = cas2ApplicationJsonSchemaEntityFactory.produceAndPersist { + ): Cas2BailApplicationEntity { + val jsonSchema = cas2BailApplicationJsonSchemaEntityFactory.produceAndPersist { withAddedAt(OffsetDateTime.parse("2022-09-21T12:45:00+01:00")) withSchema( schema, ) } - val application = cas2ApplicationEntityFactory.produceAndPersist { + val application = cas2BailApplicationEntityFactory.produceAndPersist { withApplicationSchema(jsonSchema) withCrn(crn) withCreatedByUser(userEntity) From c5a64b045f1d3e31061d2b3e1db98b3593bd87a8 Mon Sep 17 00:00:00 2001 From: garethCAS2 Date: Wed, 4 Dec 2024 11:21:30 +0000 Subject: [PATCH 29/37] feat: Cas2BailApplicationAbandonTest added --- .../Cas2BailApplicationAbandonTest.kt | 159 ++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationAbandonTest.kt diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationAbandonTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationAbandonTest.kt new file mode 100644 index 0000000000..14bda597a1 --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationAbandonTest.kt @@ -0,0 +1,159 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.cas2bail + + +import com.ninjasquad.springmockk.SpykBean +import io.mockk.clearMocks +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.IntegrationTestBase +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenACas2LicenceCaseAdminUser +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenACas2PomUser +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenAnOffender +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.NomisUserEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationRepository +import java.time.OffsetDateTime + +class Cas2BailApplicationAbandonTest : IntegrationTestBase() { + @SpykBean + lateinit var realApplicationRepository: Cas2BailApplicationRepository + + val schema = """ + { + "${"\$schema"}": "https://json-schema.org/draft/2020-12/schema", + "${"\$id"}": "https://example.com/product.schema.json", + "title": "Thing", + "description": "A thing", + "type": "object", + "properties": { + "thingId": { + "description": "The unique identifier for a thing", + "type": "integer" + } + }, + "required": [ "thingId" ] + } + """ + + val data = """ + { + "thingId": 123 + } + """ + + @AfterEach + fun afterEach() { + // SpringMockK does not correctly clear mocks for @SpyKBeans that are also a @Repository, causing mocked behaviour + // in one test to show up in another (see https://github.com/Ninja-Squad/springmockk/issues/85) + // Manually clearing after each test seems to fix this. + clearMocks(realApplicationRepository) + } + + @Nested + inner class ControlsOnExternalUsers { + + @ParameterizedTest + @ValueSource(strings = ["ROLE_CAS2_ASSESSOR", "ROLE_CAS2_MI"]) + fun `abandoning a cas2bail application is forbidden to external users based on role`(role: String) { + val jwt = jwtAuthHelper.createClientCredentialsJwt( + username = "username", + authSource = "nomis", + roles = listOf(role), + ) + + webTestClient.put() + .uri("/cas2bail/applications/66911cf0-75b1-4361-84bd-501b176fd4fd/abandon") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isForbidden + } + } + + @Nested + inner class MissingJwt { + @Test + fun `Abandon a cas2bail application without JWT returns 401`() { + webTestClient.put() + .uri("/cas2bail/applications/9b785e59-b85c-4be0-b271-d9ac287684b6/abandon") + .exchange() + .expectStatus() + .isUnauthorized + } + } + + @Nested + inner class PutToAbandon { + + @Nested + inner class PomUsers { + @Test + fun `Abandon existing cas2bail application returns 200 with correct body`() { + givenACas2PomUser { submittingUser, jwt -> + givenAnOffender { offenderDetails, _ -> + val application = produceAndPersistBasicApplication(offenderDetails.otherIds.crn, submittingUser) + + webTestClient.put() + .uri("/cas2bail/applications/${application.id}/abandon") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isOk + + Assertions.assertNotNull(realApplicationRepository.findById(application.id).get().abandonedAt) + } + } + } + } + + @Nested + inner class LicenceCaseAdminUsers { + @Test + fun `Abandon existing cas2bail application returns 200 with correct body`() { + givenACas2LicenceCaseAdminUser { submittingUser, jwt -> + givenAnOffender { offenderDetails, _ -> + val application = produceAndPersistBasicApplication(offenderDetails.otherIds.crn, submittingUser) + + webTestClient.put() + .uri("/cas2bail/applications/${application.id}/abandon") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isOk + + Assertions.assertNotNull(realApplicationRepository.findById(application.id).get().abandonedAt) + } + } + } + } + } + + private fun produceAndPersistBasicApplication( + crn: String, + userEntity: NomisUserEntity, + ): Cas2BailApplicationEntity { + val jsonSchema = cas2BailApplicationJsonSchemaEntityFactory.produceAndPersist { + withAddedAt(OffsetDateTime.parse("2022-09-21T12:45:00+01:00")) + withSchema( + schema, + ) + } + + val application = cas2BailApplicationEntityFactory.produceAndPersist { + withApplicationSchema(jsonSchema) + withCrn(crn) + withCreatedByUser(userEntity) + withData( + data, + ) + } + + return application + } +} From 459044e85559cc7166c6e878d5c2b410250c2762 Mon Sep 17 00:00:00 2001 From: garethCAS2 Date: Wed, 4 Dec 2024 13:31:16 +0000 Subject: [PATCH 30/37] feat: Cas2BailAssessmentTest --- .../cas2bail/Cas2BailAssessmentTest.kt | 283 ++++++++++++++++++ 1 file changed, 283 insertions(+) create mode 100644 src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailAssessmentTest.kt diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailAssessmentTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailAssessmentTest.kt new file mode 100644 index 0000000000..7385aaf28e --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailAssessmentTest.kt @@ -0,0 +1,283 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.cas2bail + + +import com.fasterxml.jackson.core.type.TypeReference +import com.ninjasquad.springmockk.SpykBean +import io.mockk.clearMocks +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.springframework.test.web.reactive.server.returnResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas2Assessment +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.ServiceName +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.UpdateCas2Assessment +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.IntegrationTestBase +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenACas2Admin +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenACas2Assessor +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenACas2PomUser +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2AssessmentRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.NomisUserEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailAssessmentRepository +import java.time.OffsetDateTime +import java.util.UUID + +class Cas2BailAssessmentTest : IntegrationTestBase() { + + @SpykBean + lateinit var realAssessmentRepository: Cas2BailAssessmentRepository + + @AfterEach + fun afterEach() { + // SpringMockK does not correctly clear mocks for @SpyKBeans that are also a @Repository, causing mocked behaviour + // in one test to show up in another (see https://github.com/Ninja-Squad/springmockk/issues/85) + // Manually clearing after each test seems to fix this. + clearMocks(realAssessmentRepository) + } + + @Nested + inner class PutToUpdate { + @Nested + inner class MissingJwt { + @Test + fun `updating a cas2bail assessment without JWT returns 401`() { + webTestClient.put() + .uri("/cas2bail/assessments/de6512fc-a225-4109-bdcd-86c6307a5237") + .exchange() + .expectStatus() + .isUnauthorized + } + } + + @Nested + inner class ControlsOnExternalUsers { + @Test + fun `updating a cas2bail assessment is forbidden to external users who are not Assessors`() { + val jwt = jwtAuthHelper.createClientCredentialsJwt( + username = "username", + authSource = "auth", + roles = listOf("ROLE_CAS2_ADMIN"), + ) + + webTestClient.put() + .uri("/cas2bail/assessments/de6512fc-a225-4109-bdcd-86c6307a5237") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isForbidden + } + } + + @Nested + inner class ControlsOnInternalUsers { + @Test + fun `updating a cas2bail assessment is forbidden to nomis users`() { + val jwt = jwtAuthHelper.createClientCredentialsJwt( + username = "username", + authSource = "nomis", + roles = listOf("ROLE_POM"), + ) + + webTestClient.put() + .uri("/cas2bail/assessments/de6512fc-a225-4109-bdcd-86c6307a5237") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isForbidden + } + } + + @Test + fun `assessors create cas2bail note returns 201`() { + val applicationId = UUID.fromString("22ceda56-98b2-411d-91cc-ace0ab8be872") + + givenACas2PomUser { referrer, _ -> + givenACas2Assessor { assessor, jwt -> + val submittedApplication = createSubmittedApplication(applicationId, referrer) + + // with an assessment + val assessment = cas2BailAssessmentEntityFactory.produceAndPersist { + withApplication(submittedApplication) + withNacroReferralId("someID") + withAssessorName("some name") + } + + val updatedNacroReferralId = "123N" + val updatedAssessorName = "Anne Assessor" + + val rawResponseBody = webTestClient.put() + .uri("/cas2bail/assessments/${assessment.id}") + .header("Authorization", "Bearer $jwt") + .header("X-Service-Name", ServiceName.cas2.value) + .bodyValue( + UpdateCas2Assessment( + nacroReferralId = updatedNacroReferralId, + assessorName = updatedAssessorName, + ), + ) + .exchange() + .expectStatus() + .isOk + .returnResult() + .responseBody + .blockFirst() + + val responseBody = + objectMapper.readValue(rawResponseBody, object : TypeReference() {}) + + Assertions.assertThat(responseBody.nacroReferralId).isEqualTo(updatedNacroReferralId) + Assertions.assertThat(responseBody.assessorName).isEqualTo(updatedAssessorName) + } + } + } + + private fun createSubmittedApplication(applicationId: UUID, referrer: NomisUserEntity): Cas2BailApplicationEntity { + val applicationSchema = + cas2BailApplicationJsonSchemaEntityFactory.produceAndPersist() + + return cas2BailApplicationEntityFactory.produceAndPersist { + withId(applicationId) + withCreatedByUser(referrer) + withApplicationSchema(applicationSchema) + withSubmittedAt(OffsetDateTime.now()) + } + } + } + + @Nested + inner class GetToShow { + @Nested + inner class MissingJwt { + @Test + fun `getting a cas2bail assessment without JWT returns 401`() { + webTestClient.get() + .uri("/cas2bail/assessments/de6512fc-a225-4109-bdcd-86c6307a5237") + .exchange() + .expectStatus() + .isUnauthorized + } + } + + @Nested + inner class ControlsOnExternalUsers { + @Test + fun `getting a cas2bail assessment is forbidden to external users who are not Assessors or Admins`() { + val jwt = jwtAuthHelper.createClientCredentialsJwt( + username = "username", + authSource = "auth", + roles = listOf("ROLE_CAS2_EXAMPLE"), + ) + + webTestClient.get() + .uri("/cas2bail/assessments/de6512fc-a225-4109-bdcd-86c6307a5237") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isForbidden + } + } + + @Nested + inner class ControlsOnInternalUsers { + @Test + fun `getting a cas2bail assessment is forbidden to nomis users`() { + val jwt = jwtAuthHelper.createClientCredentialsJwt( + username = "username", + authSource = "nomis", + roles = listOf("ROLE_POM"), + ) + + webTestClient.get() + .uri("/cas2bail/assessments/de6512fc-a225-4109-bdcd-86c6307a5237") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isForbidden + } + } + + @Test + fun `assessors update cas2bail assessment returns 200`() { + val applicationId = UUID.fromString("22ceda56-98b2-411d-91cc-ace0ab8be872") + + givenACas2PomUser { referrer, _ -> + givenACas2Assessor { assessor, jwt -> + val submittedApplication = createSubmittedApplication(applicationId, referrer) + + // with an assessment + val assessment = cas2BailAssessmentEntityFactory.produceAndPersist { + withApplication(submittedApplication) + withNacroReferralId("someID") + withAssessorName("some name") + } + + val rawResponseBody = webTestClient.get() + .uri("/cas2bail/assessments/${assessment.id}") + .header("Authorization", "Bearer $jwt") + .header("X-Service-Name", ServiceName.cas2.value) + .exchange() + .expectStatus() + .isOk + .returnResult() + .responseBody + .blockFirst() + + val responseBody = + objectMapper.readValue(rawResponseBody, object : TypeReference() {}) + + Assertions.assertThat(responseBody.nacroReferralId).isEqualTo(assessment.nacroReferralId) + Assertions.assertThat(responseBody.assessorName).isEqualTo(assessment.assessorName) + } + } + } + + @Test + fun `admins get cas2bail assessment returns 200`() { + val applicationId = UUID.fromString("22ceda56-98b2-411d-91cc-ace0ab8be872") + + givenACas2PomUser { referrer, _ -> + givenACas2Admin { admin, jwt -> + val submittedApplication = createSubmittedApplication(applicationId, referrer) + + // with an assessment + val assessment = cas2BailAssessmentEntityFactory.produceAndPersist { + withApplication(submittedApplication) + withNacroReferralId("someID") + withAssessorName("some name") + } + + val rawResponseBody = webTestClient.get() + .uri("/cas2bail/assessments/${assessment.id}") + .header("Authorization", "Bearer $jwt") + .header("X-Service-Name", ServiceName.cas2.value) + .exchange() + .expectStatus() + .isOk + .returnResult() + .responseBody + .blockFirst() + + val responseBody = + objectMapper.readValue(rawResponseBody, object : TypeReference() {}) + + Assertions.assertThat(responseBody.nacroReferralId).isEqualTo(assessment.nacroReferralId) + Assertions.assertThat(responseBody.assessorName).isEqualTo(assessment.assessorName) + } + } + } + + private fun createSubmittedApplication(applicationId: UUID, referrer: NomisUserEntity): Cas2BailApplicationEntity { + val applicationSchema = + cas2BailApplicationJsonSchemaEntityFactory.produceAndPersist() + + return cas2BailApplicationEntityFactory.produceAndPersist { + withId(applicationId) + withCreatedByUser(referrer) + withApplicationSchema(applicationSchema) + withSubmittedAt(OffsetDateTime.now()) + } + } + } +} \ No newline at end of file From 4390bcf1f46fa1197136c77255e8a056571c27f2 Mon Sep 17 00:00:00 2001 From: garethCAS2 Date: Wed, 4 Dec 2024 16:13:08 +0000 Subject: [PATCH 31/37] feat: cas2Reports and tests and cas2StatusUpdate and tests --- .../cas2bail/Cas2BailReportsController.kt | 31 ++ ...pplicationStatusUpdatesReportRepository.kt | 48 ++ ...ailSubmittedApplicationReportRepository.kt | 50 ++ ...UnsubmittedApplicationsReportRepository.kt | 38 ++ .../cas2bail/Cas2BailReportsService.kt | 85 +++ .../cas2bail/Cas2BailReportsTest.kt | 505 ++++++++++++++++++ .../cas2bail/Cas2BailStatusUpdateTest.kt | 281 ++++++++++ 7 files changed, 1038 insertions(+) create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailReportsController.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationStatusUpdatesReportRepository.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailSubmittedApplicationReportRepository.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailUnsubmittedApplicationsReportRepository.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailReportsService.kt create mode 100644 src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailReportsTest.kt create mode 100644 src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailStatusUpdateTest.kt diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailReportsController.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailReportsController.kt new file mode 100644 index 0000000000..7428751ee1 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailReportsController.kt @@ -0,0 +1,31 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.controller.cas2bail + + +import org.springframework.http.ResponseEntity +import org.springframework.stereotype.Service +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.cas2bail.ReportsCas2bailDelegate +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas2ReportName +import uk.gov.justice.digital.hmpps.approvedpremisesapi.controller.generateXlsxStreamingResponse +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2bail.Cas2ReportsService + +@Service("Cas2BailReportsController") +class Cas2BailReportsController(private val cas2BailReportService: Cas2ReportsService) : ReportsCas2bailDelegate { + + override fun reportsReportNameGet(reportName: Cas2ReportName): ResponseEntity { + return when (reportName) { + Cas2ReportName.submittedMinusApplications -> generateXlsxStreamingResponse { + outputStream -> + cas2BailReportService.createSubmittedApplicationsReport(outputStream) + } + Cas2ReportName.applicationMinusStatusMinusUpdates -> generateXlsxStreamingResponse { + outputStream -> + cas2BailReportService.createApplicationStatusUpdatesReport(outputStream) + } + Cas2ReportName.unsubmittedMinusApplications -> generateXlsxStreamingResponse { + outputStream -> + cas2BailReportService.createUnsubmittedApplicationsReport(outputStream) + } + } + } +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationStatusUpdatesReportRepository.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationStatusUpdatesReportRepository.kt new file mode 100644 index 0000000000..f16af599a1 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationStatusUpdatesReportRepository.kt @@ -0,0 +1,48 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail + + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query +import org.springframework.stereotype.Repository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.DomainEventEntity +import java.util.UUID + +@Repository +interface Cas2BailApplicationStatusUpdatesReportRepository : JpaRepository { + @Query( + """ + SELECT + CAST(events.id AS TEXT) AS id, + CAST(events.application_id AS TEXT) AS applicationId, + events.data -> 'eventDetails' -> 'personReference' ->> 'noms' AS personNoms, + events.data -> 'eventDetails' -> 'personReference' ->> 'crn' AS personCrn, + events.data -> 'eventDetails' -> 'newStatus' ->> 'name' AS newStatus, + events.data -> 'eventDetails' -> 'updatedBy' ->> 'username' AS updatedBy, + COALESCE(string_agg(details ->> 'name', '|'), '') as statusDetails, + TO_CHAR( + CAST(events.data -> 'eventDetails' ->> 'updatedAt' AS TIMESTAMP), + 'YYYY-MM-DD"T"HH24:MI:SS' + ) AS updatedAt + + FROM domain_events events + LEFT JOIN LATERAL jsonb_array_elements(events.data -> 'eventDetails' -> 'newStatus' -> 'statusDetails') as details ON true + WHERE events.type = 'CAS2_APPLICATION_STATUS_UPDATED' + AND events.occurred_at > CURRENT_DATE - 365 + GROUP BY events.id + ORDER BY updatedAt DESC; + """, + nativeQuery = true, + ) + fun generateApplicationStatusUpdatesReportRows(): List +} + +interface Cas2BailApplicationStatusUpdatedReportRow { + fun getId(): String + fun getApplicationId(): String + fun getUpdatedBy(): String + fun getUpdatedAt(): String + fun getPersonNoms(): String + fun getPersonCrn(): String + fun getNewStatus(): String + fun getStatusDetails(): String +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailSubmittedApplicationReportRepository.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailSubmittedApplicationReportRepository.kt new file mode 100644 index 0000000000..2f6232161d --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailSubmittedApplicationReportRepository.kt @@ -0,0 +1,50 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail + +import org.springframework.data.jpa.repository.JpaRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.DomainEventEntity +import org.springframework.data.jpa.repository.Query +import org.springframework.stereotype.Repository +import java.util.UUID + +@Repository +interface Cas2BailSubmittedApplicationReportRepository : JpaRepository { + @Query( + """ + SELECT + CAST(events.id AS TEXT) AS id, + CAST(events.application_id AS TEXT) AS applicationId, + events.data -> 'eventDetails' -> 'submittedBy' -> 'staffMember' ->> 'username' AS submittedBy, + events.data -> 'eventDetails' -> 'personReference' ->> 'noms' AS personNoms, + events.data -> 'eventDetails' -> 'personReference' ->> 'crn' AS personCrn, + events.data -> 'eventDetails' ->> 'referringPrisonCode' AS referringPrisonCode, + events.data -> 'eventDetails' ->> 'preferredAreas' AS preferredAreas, + CAST(events.data -> 'eventDetails' ->> 'hdcEligibilityDate' as DATE) AS hdcEligibilityDate, + CAST(events.data -> 'eventDetails' ->> 'conditionalReleaseDate' as DATE) AS conditionalReleaseDate, + TO_CHAR(events.occurred_at,'YYYY-MM-DD"T"HH24:MI:SS') AS submittedAt, + TO_CHAR(applications.created_at, 'YYYY-MM-DD"T"HH24:MI:SS') AS startedAt + FROM domain_events events + JOIN cas_2_bail_applications applications + ON events.application_id = applications.id + WHERE events.type = 'CAS2_APPLICATION_SUBMITTED' + AND events.occurred_at > CURRENT_DATE - 365 + ORDER BY submittedAt DESC; + """, + nativeQuery = true, + ) + fun generateSubmittedApplicationReportRows(): List +} + +@SuppressWarnings("TooManyFunctions") +interface Cas2BailSubmittedApplicationReportRow { + fun getId(): String + fun getApplicationId(): String + fun getSubmittedBy(): String + fun getSubmittedAt(): String + fun getPersonNoms(): String + fun getPersonCrn(): String + fun getReferringPrisonCode(): String + fun getPreferredAreas(): String + fun getHdcEligibilityDate(): String + fun getConditionalReleaseDate(): String + fun getStartedAt(): String +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailUnsubmittedApplicationsReportRepository.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailUnsubmittedApplicationsReportRepository.kt new file mode 100644 index 0000000000..c4bb60f277 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailUnsubmittedApplicationsReportRepository.kt @@ -0,0 +1,38 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail + + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query +import org.springframework.stereotype.Repository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationEntity +import java.util.UUID + +@Repository +interface Cas2BailUnsubmittedApplicationsReportRepository : JpaRepository { + @Query( + """ + SELECT + CAST(applications.id AS TEXT) AS applicationId, + applications.crn AS personCrn, + applications.noms_number AS personNoms, + to_char(applications.created_at, 'YYYY-MM-DD"T"HH24:MI:SS') AS startedAt, + users.nomis_username AS startedBy + + FROM cas_2_bail_applications applications + JOIN nomis_users users ON users.id = applications.created_by_user_id + WHERE applications.submitted_at IS NULL + AND applications.created_at > CURRENT_DATE - 365 + ORDER BY startedAt DESC; + """, + nativeQuery = true, + ) + fun generateUnsubmittedApplicationsReportRows(): List +} + +interface Cas2BailUnsubmittedApplicationReportRow { + fun getApplicationId(): String + fun getPersonNoms(): String + fun getPersonCrn(): String + fun getStartedBy(): String + fun getStartedAt(): String +} \ No newline at end of file diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailReportsService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailReportsService.kt new file mode 100644 index 0000000000..07f93adba6 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailReportsService.kt @@ -0,0 +1,85 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2bail + + +import org.apache.poi.ss.usermodel.WorkbookFactory +import org.jetbrains.kotlinx.dataframe.api.toDataFrame +import org.jetbrains.kotlinx.dataframe.io.writeExcel +import org.springframework.stereotype.Service +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationStatusUpdatesReportRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailSubmittedApplicationReportRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailUnsubmittedApplicationsReportRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.reporting.model.cas2.ApplicationStatusUpdatesReportRow +import uk.gov.justice.digital.hmpps.approvedpremisesapi.reporting.model.cas2.SubmittedApplicationReportRow +import uk.gov.justice.digital.hmpps.approvedpremisesapi.reporting.model.cas2.UnsubmittedApplicationsReportRow +import java.io.OutputStream + +@Service +class Cas2ReportsService( + private val cas2BailSubmittedApplicationReportRepository: Cas2BailSubmittedApplicationReportRepository, + private val cas2BailApplicationStatusUpdatesReportRepository: Cas2BailApplicationStatusUpdatesReportRepository, + private val cas2BailUnsubmittedApplicationsReportRepository: Cas2BailUnsubmittedApplicationsReportRepository, +) { + + fun createSubmittedApplicationsReport(outputStream: OutputStream) { + val reportData = cas2BailSubmittedApplicationReportRepository.generateSubmittedApplicationReportRows().map { row -> + SubmittedApplicationReportRow( + eventId = row.getId(), + applicationId = row.getApplicationId(), + personCrn = row.getPersonCrn(), + personNoms = row.getPersonNoms(), + referringPrisonCode = row.getReferringPrisonCode(), + preferredAreas = row.getPreferredAreas(), + hdcEligibilityDate = row.getHdcEligibilityDate(), + conditionalReleaseDate = row.getConditionalReleaseDate(), + submittedBy = row.getSubmittedBy(), + submittedAt = row.getSubmittedAt(), + startedAt = row.getStartedAt(), + ) + } + + reportData.toDataFrame() + .writeExcel( + outputStream = outputStream, + factory = WorkbookFactory.create(true), + ) + } + + fun createApplicationStatusUpdatesReport(outputStream: OutputStream) { + val reportData = cas2BailApplicationStatusUpdatesReportRepository.generateApplicationStatusUpdatesReportRows().map { row -> + ApplicationStatusUpdatesReportRow( + eventId = row.getId(), + applicationId = row.getApplicationId(), + personCrn = row.getPersonCrn(), + personNoms = row.getPersonNoms(), + newStatus = row.getNewStatus(), + updatedBy = row.getUpdatedBy(), + updatedAt = row.getUpdatedAt(), + statusDetails = row.getStatusDetails(), + ) + } + + reportData.toDataFrame() + .writeExcel( + outputStream = outputStream, + factory = WorkbookFactory.create(true), + ) + } + + fun createUnsubmittedApplicationsReport(outputStream: OutputStream) { + val reportData = cas2BailUnsubmittedApplicationsReportRepository.generateUnsubmittedApplicationsReportRows().map { row -> + UnsubmittedApplicationsReportRow( + applicationId = row.getApplicationId(), + personCrn = row.getPersonCrn(), + personNoms = row.getPersonNoms(), + startedBy = row.getStartedBy(), + startedAt = row.getStartedAt(), + ) + } + + reportData.toDataFrame() + .writeExcel( + outputStream = outputStream, + factory = WorkbookFactory.create(true), + ) + } +} \ No newline at end of file diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailReportsTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailReportsTest.kt new file mode 100644 index 0000000000..362d4d7d67 --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailReportsTest.kt @@ -0,0 +1,505 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.cas2bail + + +import org.assertj.core.api.Assertions +import org.jetbrains.kotlinx.dataframe.DataFrame +import org.jetbrains.kotlinx.dataframe.api.ExcessiveColumns +import org.jetbrains.kotlinx.dataframe.api.convertTo +import org.jetbrains.kotlinx.dataframe.api.toDataFrame +import org.jetbrains.kotlinx.dataframe.io.readExcel +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource +import org.junit.jupiter.params.provider.ValueSource +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.events.cas2.model.Cas2ApplicationStatusUpdatedEvent +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.events.cas2.model.Cas2ApplicationSubmittedEvent +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.events.cas2.model.Cas2StatusDetail +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.events.cas2.model.EventType +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas2ReportName +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.events.cas2.Cas2ApplicationStatusUpdatedEventDetailsFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.events.cas2.Cas2ApplicationSubmittedEventDetailsFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.events.cas2.Cas2StatusFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.IntegrationTestBase +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.DomainEventType +import uk.gov.justice.digital.hmpps.approvedpremisesapi.reporting.model.cas2.ApplicationStatusUpdatesReportRow +import uk.gov.justice.digital.hmpps.approvedpremisesapi.reporting.model.cas2.SubmittedApplicationReportRow +import uk.gov.justice.digital.hmpps.approvedpremisesapi.reporting.model.cas2.UnsubmittedApplicationsReportRow +import java.time.Instant +import java.time.OffsetDateTime +import java.time.ZoneOffset +import java.util.UUID + +class Cas2ReportsTest : IntegrationTestBase() { + + @Nested + inner class ControlsOnExternalUsers { + @ParameterizedTest + @EnumSource(value = Cas2ReportName::class) + fun `downloading cas2bail report is forbidden to external users without MI role`(reportName: Cas2ReportName) { + val jwt = jwtAuthHelper.createClientCredentialsJwt( + username = "username", + authSource = "auth", + roles = listOf("ROLE_CAS2_ASSESSOR"), + ) + + webTestClient.get() + .uri("/cas2bail/reports/${reportName.value}") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isForbidden + } + } + + @Nested + inner class MissingJwt { + @ParameterizedTest + @ValueSource( + strings = [ + "submitted-applications", + "application-status-updates", + "unsubmitted-applications", + ], + ) + fun `Downloading cas2bail report without JWT returns 401`(reportName: String) { + webTestClient.get() + .uri("/cas2bail/reports/$reportName") + .exchange() + .expectStatus() + .isUnauthorized + } + } + + @Nested + inner class ControlsOnInternalUsers { + @ParameterizedTest + @EnumSource(value = Cas2ReportName::class) + fun `downloading cas2bail report is forbidden to NOMIS users without MI role`(reportName: Cas2ReportName) { + val jwt = jwtAuthHelper.createClientCredentialsJwt( + username = "username", + authSource = "nomis", + roles = listOf("ROLE_POM"), + ) + + webTestClient.get() + .uri("/cas2bail/reports/${reportName.value}") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isForbidden + } + } + + @Nested + inner class SubmittedApplications { + @Test + fun `streams spreadsheet of cas2bail Cas2SubmittedApplicationEvents, last 12 months only`() { + val event1Id = UUID.randomUUID() + val event2Id = UUID.randomUUID() + val event3Id = UUID.randomUUID() + + val oldSubmitted = OffsetDateTime.now().minusDays(365) + val oldCreated = oldSubmitted.minusDays(7) + + val newerSubmitted = OffsetDateTime.now().minusDays(100) + val newerCreated = newerSubmitted.minusDays(7) + + val tooOldSubmitted = OffsetDateTime.now().minusDays(366) + val tooOldCreated = tooOldSubmitted.minusSeconds(daysInSeconds(7)) + + val applicationSchema = cas2BailApplicationJsonSchemaEntityFactory.produceAndPersist { + withAddedAt(OffsetDateTime.now()) + withId(UUID.randomUUID()) + } + + val user1 = nomisUserEntityFactory.produceAndPersist { + withNomisUsername("NOMIS_USER_1") + } + + val user2 = nomisUserEntityFactory.produceAndPersist { + withNomisUsername("NOMIS_USER_2") + } + + val applicationId1 = UUID.randomUUID() + val applicationId2 = UUID.randomUUID() + val applicationId3 = UUID.randomUUID() + + val application1 = cas2BailApplicationEntityFactory.produceAndPersist { + withId(applicationId1) + withApplicationSchema(applicationSchema) + withCreatedByUser(user1) + withCrn("CRN_1") + withNomsNumber("NOMS_1") + withCreatedAt(oldCreated) + withData("{}") + withSubmittedAt(oldSubmitted) + } + + val application2 = cas2BailApplicationEntityFactory.produceAndPersist { + withId(applicationId2) + withApplicationSchema(applicationSchema) + withCreatedByUser(user2) + withCrn("CRN_2") + withNomsNumber("NOMS_2") + withCreatedAt(newerCreated) + withData("{}") + withSubmittedAt(newerSubmitted) + } + + // outside time limit -- should not feature in report + cas2BailApplicationEntityFactory.produceAndPersist { + withId(applicationId3) + withApplicationSchema(applicationSchema) + withCreatedByUser(user2) + withCreatedAt(tooOldCreated) + withData("{}") + withSubmittedAt(tooOldSubmitted) + } + + val event1Details = Cas2ApplicationSubmittedEventDetailsFactory() + .withSubmittedAt(oldSubmitted.toInstant()) + .produce() + val event2Details = Cas2ApplicationSubmittedEventDetailsFactory() + .withSubmittedAt(newerSubmitted.toInstant()) + .produce() + val event3Details = Cas2ApplicationSubmittedEventDetailsFactory() + .withSubmittedAt(tooOldSubmitted.toInstant()) + .produce() + + val event1ToSave = Cas2ApplicationSubmittedEvent( + id = event1Id, + timestamp = Instant.now(), + eventType = EventType.applicationSubmitted, + eventDetails = event1Details, + ) + + val event2ToSave = Cas2ApplicationSubmittedEvent( + id = event2Id, + timestamp = Instant.now(), + eventType = EventType.applicationSubmitted, + eventDetails = event2Details, + ) + + val event3ToSave = Cas2ApplicationSubmittedEvent( + id = event3Id, + timestamp = Instant.now(), + eventType = EventType.applicationSubmitted, + eventDetails = event3Details, + ) + + val event1 = domainEventFactory.produceAndPersist { + withId(event1Id) + withType(DomainEventType.CAS2_APPLICATION_SUBMITTED) + withData(objectMapper.writeValueAsString(event1ToSave)) + withOccurredAt(oldSubmitted) + withApplicationId(applicationId1) + } + + val event2 = domainEventFactory.produceAndPersist { + withId(event2Id) + withType(DomainEventType.CAS2_APPLICATION_SUBMITTED) + withData(objectMapper.writeValueAsString(event2ToSave)) + withOccurredAt(newerSubmitted) + withApplicationId(applicationId2) + } + + // we don't expect this event to be included as it relates to an application + // outside the time range + domainEventFactory.produceAndPersist { + withId(event3Id) + withType(DomainEventType.CAS2_APPLICATION_SUBMITTED) + withData(objectMapper.writeValueAsString(event3ToSave)) + withOccurredAt(tooOldSubmitted) + withApplicationId(applicationId3) + } + + val expectedDataFrame = listOf( + SubmittedApplicationReportRow( + eventId = event2Id.toString(), + applicationId = event2.applicationId.toString(), + personCrn = event2Details.personReference.crn.toString(), + personNoms = event2Details.personReference.noms, + referringPrisonCode = event2Details.referringPrisonCode.toString(), + preferredAreas = event2Details.preferredAreas.toString(), + hdcEligibilityDate = event2Details.hdcEligibilityDate.toString(), + conditionalReleaseDate = event2Details.conditionalReleaseDate.toString(), + submittedAt = event2.occurredAt.toString().split(".").first(), + submittedBy = event2Details.submittedBy.staffMember.username.toString(), + startedAt = application2.createdAt.toString().split(".").first(), + ), + SubmittedApplicationReportRow( + eventId = event1Id.toString(), + applicationId = event1.applicationId.toString(), + personCrn = event1Details.personReference.crn.toString(), + personNoms = event1Details.personReference.noms, + referringPrisonCode = event1Details.referringPrisonCode.toString(), + preferredAreas = event1Details.preferredAreas.toString(), + hdcEligibilityDate = event1Details.hdcEligibilityDate.toString(), + conditionalReleaseDate = event1Details.conditionalReleaseDate.toString(), + submittedAt = event1.occurredAt.toString().split(".").first(), + submittedBy = event1Details.submittedBy.staffMember.username.toString(), + startedAt = application1.createdAt.toString().split(".").first(), + ), + ) + .toDataFrame() + + val jwt = jwtAuthHelper.createClientCredentialsJwt( + username = "username", + authSource = "nomis", + roles = listOf("ROLE_CAS2_MI"), + ) + + webTestClient.get() + .uri("/cas2bail/reports/submitted-applications") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isOk + .expectBody() + .consumeWith { + val actual = DataFrame + .readExcel(it.responseBody!!.inputStream()) + .convertTo(ExcessiveColumns.Remove) + + Assertions.assertThat(actual).isEqualTo(expectedDataFrame) + } + } + } + + @Nested + inner class ApplicationStatusUpdates { + @Test + fun `streams spreadsheet of cas2bail Cas2ApplicationStatusUpdatedEvents, last 12 months only`() { + val event1Id = UUID.randomUUID() + val event2Id = UUID.randomUUID() + val event3Id = UUID.randomUUID() + + val old = Instant.now().minusSeconds(daysInSeconds(365)) + val newer = Instant.now().minusSeconds(daysInSeconds(100)) + val tooOld = Instant.now().minusSeconds(daysInSeconds(366)) + + val event1StatusDetails = listOf( + Cas2StatusDetail("personalInformation", "Personal information"), + Cas2StatusDetail("riskOfSeriousHarm", "Risk of serious harm"), + Cas2StatusDetail("hdcAndCpp", "HDC licence and CPP details"), + ) + + val event1Status = Cas2StatusFactory() + .withStatusDetails(event1StatusDetails) + .produce() + + val event1Details = Cas2ApplicationStatusUpdatedEventDetailsFactory() + .withStatus(event1Status) + .withUpdatedAt(old) + .produce() + + val event2StatusDetails = emptyList() + + val event2Status = Cas2StatusFactory() + .withStatusDetails(event2StatusDetails) + .produce() + + val event2Details = Cas2ApplicationStatusUpdatedEventDetailsFactory() + .withStatus(event2Status) + .withUpdatedAt(newer) + .produce() + val event3Details = Cas2ApplicationStatusUpdatedEventDetailsFactory() + .withUpdatedAt(tooOld) + .produce() + + val event1ToSave = Cas2ApplicationStatusUpdatedEvent( + id = event1Id, + timestamp = Instant.now(), + eventType = EventType.applicationStatusUpdated, + eventDetails = event1Details, + ) + + val event2ToSave = Cas2ApplicationStatusUpdatedEvent( + id = event2Id, + timestamp = Instant.now(), + eventType = EventType.applicationStatusUpdated, + eventDetails = event2Details, + ) + + val event3ToSave = Cas2ApplicationStatusUpdatedEvent( + id = event3Id, + timestamp = Instant.now(), + eventType = EventType.applicationStatusUpdated, + eventDetails = event3Details, + ) + + val event1 = domainEventFactory.produceAndPersist { + withId(event1Id) + withType(DomainEventType.CAS2_APPLICATION_STATUS_UPDATED) + withOccurredAt(old.atOffset(ZoneOffset.ofHoursMinutes(0, 0))) + withData(objectMapper.writeValueAsString(event1ToSave)) + } + + val event2 = domainEventFactory.produceAndPersist { + withId(event2Id) + withType(DomainEventType.CAS2_APPLICATION_STATUS_UPDATED) + withOccurredAt(newer.atOffset(ZoneOffset.ofHoursMinutes(0, 0))) + withData(objectMapper.writeValueAsString(event2ToSave)) + } + + // we don't expect this event to be included as it relates to an update + // outside the time range + domainEventFactory.produceAndPersist { + withId(event3Id) + withType(DomainEventType.CAS2_APPLICATION_STATUS_UPDATED) + withOccurredAt(tooOld.atOffset(ZoneOffset.ofHoursMinutes(0, 0))) + withData(objectMapper.writeValueAsString(event3ToSave)) + } + + val expectedDataFrame = listOf( + ApplicationStatusUpdatesReportRow( + eventId = event2Id.toString(), + applicationId = event2.applicationId.toString(), + personCrn = event2Details.personReference.crn.toString(), + personNoms = event2Details.personReference.noms, + newStatus = event2Details.newStatus.name, + updatedAt = event2Details.updatedAt.toString().split(".").first(), + updatedBy = event2Details.updatedBy.username, + statusDetails = "", + ), + ApplicationStatusUpdatesReportRow( + eventId = event1Id.toString(), + applicationId = event1.applicationId.toString(), + personCrn = event1Details.personReference.crn.toString(), + personNoms = event1Details.personReference.noms, + newStatus = event1Details.newStatus.name, + updatedAt = event1Details.updatedAt.toString().split(".").first(), + updatedBy = event1Details.updatedBy.username, + statusDetails = "personalInformation|riskOfSeriousHarm|hdcAndCpp", + ), + ) + .toDataFrame() + + val jwt = jwtAuthHelper.createClientCredentialsJwt( + username = "username", + authSource = "nomis", + roles = listOf("ROLE_PRISON", "ROLE_CAS2_MI"), + ) + + webTestClient.get() + .uri("/cas2bail/reports/application-status-updates") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isOk + .expectBody() + .consumeWith { + val actual = DataFrame + .readExcel(it.responseBody!!.inputStream()) + .convertTo(ExcessiveColumns.Remove) + + Assertions.assertThat(actual).isEqualTo(expectedDataFrame) + } + } + } + + @Nested + inner class UnSubmittedApplications { + @Test + fun `streams cas2bail spreadsheet of data from un-submitted CAS2 applications, newest first`() { + val old = Instant.now().minusSeconds(daysInSeconds(365)) + val newer = Instant.now().minusSeconds(daysInSeconds(100)) + val tooOld = Instant.now().minusSeconds(daysInSeconds(366)) + + val applicationSchema = cas2BailApplicationJsonSchemaEntityFactory.produceAndPersist { + withAddedAt(OffsetDateTime.now()) + withId(UUID.randomUUID()) + } + + val user1 = nomisUserEntityFactory.produceAndPersist { + withNomisUsername("NOMIS_USER_1") + } + + val user2 = nomisUserEntityFactory.produceAndPersist { + withNomisUsername("NOMIS_USER_2") + } + + val application1 = cas2BailApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(user1) + withCrn("CRN_1") + withNomsNumber("NOMS_1") + withCreatedAt(old.atOffset(ZoneOffset.ofHoursMinutes(0, 0))) + withData("{}") + withSubmittedAt(null) + } + + val application2 = cas2BailApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(user2) + withCrn("CRN_2") + withNomsNumber("NOMS_2") + withCreatedAt(newer.atOffset(ZoneOffset.ofHoursMinutes(0, 0))) + withData("{}") + withSubmittedAt(null) + } + + // outside time limit -- should not feature in report + cas2BailApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(user2) + withCreatedAt(tooOld.atOffset(ZoneOffset.ofHoursMinutes(0, 0))) + withData("{}") + withSubmittedAt(null) + } + + // submitted application, which should not feature in report + cas2BailApplicationEntityFactory.produceAndPersist { + withApplicationSchema(applicationSchema) + withCreatedByUser(user2) + withCreatedAt(Instant.now().atOffset(ZoneOffset.ofHoursMinutes(0, 0)).minusDays(51)) + withData("{}") + withSubmittedAt(Instant.now().atOffset(ZoneOffset.ofHoursMinutes(0, 0)).minusDays(50)) + } + + val expectedDataFrame = listOf( + UnsubmittedApplicationsReportRow( + applicationId = application2.id.toString(), + personCrn = application2.crn, + personNoms = application2.nomsNumber.toString(), + startedAt = application2.createdAt.toString().split(".").first(), + startedBy = application2.createdByUser.nomisUsername, + ), + UnsubmittedApplicationsReportRow( + applicationId = application1.id.toString(), + personCrn = application1.crn, + personNoms = application1.nomsNumber.toString(), + startedAt = application1.createdAt.toString().split(".").first(), + startedBy = application1.createdByUser.nomisUsername, + ), + ) + .toDataFrame() + + val jwt = jwtAuthHelper.createClientCredentialsJwt( + username = "username", + authSource = "nomis", + roles = listOf("ROLE_PRISON", "ROLE_CAS2_MI"), + ) + + webTestClient.get() + .uri("/cas2bail/reports/unsubmitted-applications") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isOk + .expectBody() + .consumeWith { + val actual = DataFrame + .readExcel(it.responseBody!!.inputStream()) + .convertTo(ExcessiveColumns.Remove) + + Assertions.assertThat(actual).isEqualTo(expectedDataFrame) + } + } + } + + private fun daysInSeconds(days: Int): Long { + return days.toLong() * 60 * 60 * 24 + } +} \ No newline at end of file diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailStatusUpdateTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailStatusUpdateTest.kt new file mode 100644 index 0000000000..c865dcac4a --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailStatusUpdateTest.kt @@ -0,0 +1,281 @@ +package uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.cas2bail + + +import com.ninjasquad.springmockk.SpykBean +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Value +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.events.cas2.model.Cas2ApplicationStatusUpdatedEvent +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.events.cas2.model.Cas2StatusDetail +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas2AssessmentStatusUpdate +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.IntegrationTestBase +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenACas2Assessor +import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenACas2PomUser +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailStatusUpdateDetailRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailStatusUpdateRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.reference.Cas2ApplicationStatusSeeding +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.toCas2UiFormat +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.toCas2UiFormattedHourOfDay +import java.time.OffsetDateTime +import java.util.UUID + +class Cas2BailStatusUpdateTest( + @Value("\${url-templates.frontend.cas2.application}") private val applicationUrlTemplate: String, + @Value("\${url-templates.frontend.cas2.application-overview}") private val applicationOverviewUrlTemplate: String, +) : IntegrationTestBase() { + + @SpykBean + lateinit var realCas2BailStatusUpdateRepository: Cas2BailStatusUpdateRepository + + @SpykBean + lateinit var realCas2BailStatusUpdateDetailRepository: Cas2BailStatusUpdateDetailRepository + + @AfterEach + fun afterEach() { + // SpringMockK does not correctly clear mocks for @SpyKBeans that are also a @Repository, causing mocked behaviour + // in one test to show up in another (see https://github.com/Ninja-Squad/springmockk/issues/85) + // Manually clearing after each test seems to fix this. + clearMocks(realCas2BailStatusUpdateRepository) + clearMocks(realCas2BailStatusUpdateDetailRepository) + } + + @Nested + inner class ControlsOnInternalUsers { + @Test + fun `creating a cas2bail status update is forbidden to internal users based on role`() { + val jwt = jwtAuthHelper.createClientCredentialsJwt( + username = "username", + authSource = "nomis", + roles = listOf("ROLE_POM"), + ) + + webTestClient.post() + .uri("/cas2bail/assessments/de6512fc-a225-4109-bdcd-86c6307a5237/status-updates") + .header("Authorization", "Bearer $jwt") + .exchange() + .expectStatus() + .isForbidden + } + } + + @Nested + inner class MissingJwt { + @Test + fun `creating a cas2bail status update without JWT returns 401`() { + webTestClient.post() + .uri("/cas2bail/assessments/de6512fc-a225-4109-bdcd-86c6307a5237/status-updates") + .exchange() + .expectStatus() + .isUnauthorized + } + } + + @Nested + inner class PostToCreate { + @Test + fun `Create cas2bail status update returns 201 and creates StatusUpdate when given status is valid`() { + val assessmentId = UUID.fromString("22ceda56-98b2-411d-91cc-ace0ab8be872") + + givenACas2Assessor { _, jwt -> + givenACas2PomUser { applicant, _ -> + val jsonSchema = approvedPremisesApplicationJsonSchemaEntityFactory.produceAndPersist() + val application = cas2BailApplicationEntityFactory.produceAndPersist { + withCreatedByUser(applicant) + withApplicationSchema(jsonSchema) + withSubmittedAt(OffsetDateTime.now()) + } + + cas2BailAssessmentEntityFactory.produceAndPersist { + withApplication(application) + withId(assessmentId) + } + + assertThat(realCas2BailStatusUpdateRepository.count()).isEqualTo(0) + assertThat(realCas2BailStatusUpdateDetailRepository.count()).isEqualTo(0) + + webTestClient.post() + .uri("/cas2bail/assessments/$assessmentId/status-updates") + .header("Authorization", "Bearer $jwt") + .bodyValue( + Cas2AssessmentStatusUpdate(newStatus = "moreInfoRequested"), + ) + .exchange() + .expectStatus() + .isCreated + + assertThat(realCas2BailStatusUpdateRepository.count()).isEqualTo(1) + + val persistedStatusUpdate = realCas2BailStatusUpdateRepository.findFirstByApplicationIdOrderByCreatedAtDesc(application.id) + assertThat(persistedStatusUpdate!!.assessment!!.id).isEqualTo(assessmentId) + + val appliedStatus = Cas2ApplicationStatusSeeding.statusList() + .find { status -> + status.id == persistedStatusUpdate.statusId + } + assertThat(appliedStatus!!.name).isEqualTo("moreInfoRequested") + + // verify that generated 'application.status-updated' domain event links + // to the CAS2 domain + val expectedFrontEndUrl = applicationUrlTemplate.replace("#id", application.id.toString()) + val persistedDomainEvent = domainEventRepository.findFirstByOrderByCreatedAtDesc() + val domainEventFromJson = objectMapper.readValue( + persistedDomainEvent!!.data, + Cas2ApplicationStatusUpdatedEvent::class.java, + ) + assertThat(domainEventFromJson.eventDetails.applicationUrl) + .isEqualTo(expectedFrontEndUrl) + } + } + } + + @Test + fun `Create cas2bail status update returns 404 when assessment not found`() { + givenACas2Assessor { _, jwt -> + webTestClient.post() + .uri("/cas2bail/assessments/66f7127a-fe03-4b66-8378-5c0b048490f8/status-updates") + .header("Authorization", "Bearer $jwt") + .bodyValue( + Cas2AssessmentStatusUpdate(newStatus = "moreInfoRequested"), + ) + .exchange() + .expectStatus() + .isNotFound + } + } + + @Test + fun `Create cas2bail status update returns 400 when new status NOT valid`() { + givenACas2Assessor { _, jwt -> + givenACas2PomUser { applicant, _ -> + val jsonSchema = approvedPremisesApplicationJsonSchemaEntityFactory.produceAndPersist() + val application = cas2BailApplicationEntityFactory.produceAndPersist { + withCreatedByUser(applicant) + withApplicationSchema(jsonSchema) + withSubmittedAt(OffsetDateTime.now()) + } + + val assessmemt = cas2BailAssessmentEntityFactory.produceAndPersist { + withApplication(application) + } + + webTestClient.post() + .uri("/cas2bail/assessments/${assessmemt.id}/status-updates") + .header("Authorization", "Bearer $jwt") + .bodyValue( + Cas2AssessmentStatusUpdate(newStatus = "invalidStatus"), + ) + .exchange() + .expectStatus() + .isBadRequest + .expectBody() + .jsonPath("$.detail").isEqualTo("The status invalidStatus is not valid") + } + } + } + + @Nested + inner class WithStatusDetail { + @Test + fun `Create cas2bail status update returns 201 and creates StatusUpdate when given status and detail are valid, and sends an email to the referrer`() { + val assessmentId = UUID.fromString("22ceda56-98b2-411d-91cc-ace0ab8be872") + val submittedAt = OffsetDateTime.now() + + try { + givenACas2Assessor { _, jwt -> + givenACas2PomUser { applicant, _ -> + val jsonSchema = approvedPremisesApplicationJsonSchemaEntityFactory.produceAndPersist() + val application = cas2BailApplicationEntityFactory.produceAndPersist { + withCreatedByUser(applicant) + withApplicationSchema(jsonSchema) + withSubmittedAt(submittedAt) + withNomsNumber("123NOMS") + } + + cas2BailAssessmentEntityFactory.produceAndPersist { + withId(assessmentId) + withApplication(application) + } + + assertThat(realCas2BailStatusUpdateRepository.count()).isEqualTo(0) + + val now = OffsetDateTime.now() + mockkStatic(OffsetDateTime::class) + every { OffsetDateTime.now() } returns now + + webTestClient.post() + .uri("/cas2bail/assessments/$assessmentId/status-updates") + .header("Authorization", "Bearer $jwt") + .bodyValue( + Cas2AssessmentStatusUpdate( + newStatus = "offerDeclined", + newStatusDetails = listOf("changeOfCircumstances"), + ), + ) + .exchange() + .expectStatus() + .isCreated + + assertThat(realCas2BailStatusUpdateRepository.count()).isEqualTo(1) + assertThat(realCas2BailStatusUpdateDetailRepository.count()).isEqualTo(1) + + val persistedStatusUpdate = + realCas2BailStatusUpdateRepository.findFirstByApplicationIdOrderByCreatedAtDesc(application.id) + assertThat(persistedStatusUpdate!!.assessment!!.id).isEqualTo(assessmentId) + + val persistedStatusDetailUpdate = + realCas2BailStatusUpdateDetailRepository.findFirstByStatusUpdateIdOrderByCreatedAtDesc(persistedStatusUpdate!!.id) + assertThat(persistedStatusDetailUpdate).isNotNull + + val appliedStatus = Cas2ApplicationStatusSeeding.statusList() + .find { status -> + status.id == persistedStatusUpdate.statusId + } + + assertThat(appliedStatus!!.name).isEqualTo("offerDeclined") + assertThat(appliedStatus.statusDetails?.find { detail -> detail.id == persistedStatusDetailUpdate?.statusDetailId }) + .isNotNull() + + emailAsserter.assertEmailsRequestedCount(1) + val email = emailAsserter.assertEmailRequested( + toEmailAddress = applicant.email!!, + templateId = "ef4dc5e3-b1f1-4448-a545-7a936c50fc3a", + personalisation = mapOf( + "applicationStatus" to "Offer declined or withdrawn", + "dateStatusChanged" to now.toLocalDate().toCas2UiFormat(), + "timeStatusChanged" to now.toCas2UiFormattedHourOfDay(), + "nomsNumber" to "123NOMS", + "applicationType" to "Home Detention Curfew (HDC)", + "applicationUrl" to applicationOverviewUrlTemplate.replace("#id", application.id.toString()), + ), + replyToEmailId = notifyConfig.emailAddresses.cas2ReplyToId, + ) + + // verify that generated 'application.status-updated' domain event links + // to the CAS2 domain + val expectedFrontEndUrl = applicationUrlTemplate.replace("#id", application.id.toString()) + val persistedDomainEvent = domainEventRepository.findFirstByOrderByCreatedAtDesc() + val domainEventFromJson = objectMapper.readValue( + persistedDomainEvent!!.data, + Cas2ApplicationStatusUpdatedEvent::class.java, + ) + assertThat(domainEventFromJson.eventDetails.applicationUrl) + .isEqualTo(expectedFrontEndUrl) + + // verify that the persisted domain event contains the expected status details + val expected = listOf(Cas2StatusDetail("changeOfCircumstances", "Change of circumstances")) + assertThat(domainEventFromJson.eventDetails.newStatus.statusDetails).isEqualTo(expected) + } + } + } finally { + unmockkAll() + } + } + } + } +} \ No newline at end of file From d0691eacc80d46efcf0831d78960fea24ef6f39d Mon Sep 17 00:00:00 2001 From: Toby Batch Date: Thu, 5 Dec 2024 09:16:11 +0000 Subject: [PATCH 32/37] fix: removed .keep --- .../digital/hmpps/approvedpremisesapi/controller/cas2bail/.keep | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/.keep diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/.keep b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/.keep deleted file mode 100644 index e69de29bb2..0000000000 From cfc809a8ace460a2cc0a7b1f8a75abd2df780808 Mon Sep 17 00:00:00 2001 From: Toby Batch Date: Thu, 5 Dec 2024 10:12:50 +0000 Subject: [PATCH 33/37] chore: exploding more imports --- .../cas2bail/Cas2BailApplicationNoteEntity.kt | 13 +- .../cas2bail/Cas2BailStatusUpdateEntity.kt | 11 +- .../Cas2BailApplicationNoteService.kt | 14 +- .../cas2bail/Cas2BailApplicationService.kt | 23 +- .../Cas2BailApplicationsTransformer.kt | 7 +- .../Cas2BailStatusUpdateTransformer.kt | 5 +- .../integration/IntegrationTestBase.kt | 222 +++++++++++++++++- .../cas2bail/Cas2BailApplicationTest.kt | 19 +- .../Cas2BailApplicationServiceTest.kt | 31 ++- 9 files changed, 303 insertions(+), 42 deletions(-) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationNoteEntity.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationNoteEntity.kt index 828e678850..380d8c0882 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationNoteEntity.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationNoteEntity.kt @@ -1,15 +1,20 @@ package uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail -import jakarta.persistence.* +import jakarta.persistence.Entity +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.Table import org.springframework.data.domain.PageRequest import org.springframework.data.domain.Slice import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Query import org.springframework.stereotype.Repository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2User +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ExternalUserEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.NomisUserEntity import java.time.OffsetDateTime -import java.util.* -import kotlin.jvm.Transient +import java.util.UUID @Repository interface Cas2BailApplicationNoteRepository : JpaRepository { diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailStatusUpdateEntity.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailStatusUpdateEntity.kt index 41ce37271d..d236ebec63 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailStatusUpdateEntity.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailStatusUpdateEntity.kt @@ -1,17 +1,22 @@ package uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail -import jakarta.persistence.* +import jakarta.persistence.Entity +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.OneToMany +import jakarta.persistence.Table import org.hibernate.annotations.CreationTimestamp import org.springframework.data.domain.Pageable import org.springframework.data.domain.Slice import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Query import org.springframework.stereotype.Repository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ExternalUserEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.reference.Cas2PersistedApplicationStatus import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.reference.Cas2PersistedApplicationStatusFinder import java.time.OffsetDateTime -import java.util.* +import java.util.UUID @Repository diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationNoteService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationNoteService.kt index b81a62225f..f74971b513 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationNoteService.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationNoteService.kt @@ -7,11 +7,15 @@ import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.NewCas2ApplicationNote import uk.gov.justice.digital.hmpps.approvedpremisesapi.config.NotifyConfig -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.* -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.* -import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.AuthorisableActionResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2User +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.NomisUserEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationNoteEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationNoteRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailAssessmentEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailAssessmentRepository import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.CasResult -import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.ValidatableActionResult import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.EmailNotificationService import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.ExternalUserService import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.HttpAuthService @@ -20,7 +24,7 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2.UserAccessS import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.toCas2UiFormat import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.toCas2UiFormattedHourOfDay import java.time.OffsetDateTime -import java.util.* +import java.util.UUID @Service("Cas2BailApplicationNoteService") class Cas2BailApplicationNoteService( diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationService.kt index 7984beec36..692c4bd0ef 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationService.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationService.kt @@ -4,13 +4,22 @@ import com.fasterxml.jackson.databind.ObjectMapper import jakarta.transaction.Transactional import org.springframework.beans.factory.annotation.Value import org.springframework.data.repository.findByIdOrNull -import org.springframework.jdbc.core.JdbcTemplate import org.springframework.stereotype.Service -import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.events.cas2.model.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.events.cas2.model.Cas2ApplicationSubmittedEvent +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.events.cas2.model.Cas2ApplicationSubmittedEventDetails +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.events.cas2.model.Cas2ApplicationSubmittedEventDetailsSubmittedBy +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.events.cas2.model.Cas2StaffMember +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.events.cas2.model.EventType +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.events.cas2.model.PersonReference import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.SubmitCas2Application import uk.gov.justice.digital.hmpps.approvedpremisesapi.config.NotifyConfig -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.* -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2BailApplicationJsonSchemaEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.NomisUserEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationSummaryEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationSummaryRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailLockableApplicationRepository import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.DomainEvent import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.PaginationMetadata import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.ValidationErrors @@ -20,12 +29,14 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.CasResult import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.ValidatableActionResult import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.EmailNotificationService import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.UpstreamApiException -import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2.DomainEventService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2.JsonSchemaService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2.OffenderService import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.PageCriteria import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.getMetadata import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.getPageableOrAllPages import java.time.OffsetDateTime -import java.util.* +import java.util.UUID @Service("Cas2BailApplicationService") class Cas2BailApplicationService( diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailApplicationsTransformer.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailApplicationsTransformer.kt index 6cb05bd217..933c17dbe6 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailApplicationsTransformer.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailApplicationsTransformer.kt @@ -4,17 +4,12 @@ import com.fasterxml.jackson.databind.ObjectMapper import org.springframework.stereotype.Component import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.ApplicationStatus import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas2Application -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationSummaryEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationSummaryEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.PersonInfoResult import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.NomisUserTransformer import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.PersonTransformer -import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas2.AssessmentsTransformer -import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas2.StatusUpdateTransformer -import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas2.TimelineEventsTransformer -import java.util.* +import java.util.UUID @Component("Cas2BailApplicationsTransformer") class Cas2BailApplicationsTransformer( diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailStatusUpdateTransformer.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailStatusUpdateTransformer.kt index 12e79e36ed..55d002b9ca 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailStatusUpdateTransformer.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailStatusUpdateTransformer.kt @@ -4,14 +4,11 @@ import org.springframework.stereotype.Component import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas2StatusUpdate import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas2StatusUpdateDetail import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.LatestCas2StatusUpdate -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationSummaryEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2StatusUpdateDetailEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2StatusUpdateEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationSummaryEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailStatusUpdateDetailEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailStatusUpdateEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.ExternalUserTransformer -import java.util.* +import java.util.UUID @Component("Cas2BailStatusUpdateTransformer") class Cas2BailStatusUpdateTransformer( diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/IntegrationTestBase.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/IntegrationTestBase.kt index 57aa23de91..cb3afd1549 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/IntegrationTestBase.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/IntegrationTestBase.kt @@ -30,7 +30,77 @@ import org.springframework.test.context.ContextConfiguration import org.springframework.test.web.reactive.server.WebTestClient import uk.gov.justice.digital.hmpps.approvedpremisesapi.client.PrisonsApiClient import uk.gov.justice.digital.hmpps.approvedpremisesapi.config.NotifyConfig -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ApAreaEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.AppealEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ApplicationTeamCodeEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ApplicationTimelineNoteEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ApprovedPremisesApplicationEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ApprovedPremisesApplicationJsonSchemaEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ApprovedPremisesAssessmentEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ApprovedPremisesAssessmentJsonSchemaEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ApprovedPremisesEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ApprovedPremisesPlacementApplicationJsonSchemaEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ArrivalEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.AssessmentClarificationNoteEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.AssessmentReferralHistorySystemNoteEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.AssessmentReferralHistoryUserNoteEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.BedEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.BedMoveEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.BookingEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.BookingNotMadeEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.CancellationEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.CancellationReasonEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.Cas1ApplicationUserDetailsEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.Cas1OutOfServiceBedCancellationEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.Cas1OutOfServiceBedEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.Cas1OutOfServiceBedReasonEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.Cas1OutOfServiceBedRevisionEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.Cas1SpaceBookingEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.Cas2ApplicationEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.Cas2ApplicationJsonSchemaEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.Cas2AssessmentEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.Cas2BailApplicationJsonSchemaEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.Cas2NoteEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.Cas2StatusUpdateDetailEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.Cas2StatusUpdateEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.CharacteristicEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ConfirmationEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.DateChangeEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.DepartureEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.DepartureReasonEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.DestinationProviderEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.DomainEventEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ExtensionEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ExternalUserEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.LocalAuthorityEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.LostBedCancellationEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.LostBedReasonEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.LostBedsEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.MoveOnCategoryEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.NomisUserEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.NonArrivalEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.NonArrivalReasonEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.OfflineApplicationEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.PersistedFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.PlacementApplicationEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.PlacementDateEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.PlacementRequestEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.PlacementRequirementsEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.PostCodeDistrictEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ProbationAreaProbationRegionMappingEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ProbationDeliveryUnitEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ProbationRegionEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ReferralRejectionReasonEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.RoomEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.TemporaryAccommodationApplicationEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.TemporaryAccommodationApplicationJsonSchemaEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.TemporaryAccommodationAssessmentEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.TemporaryAccommodationAssessmentJsonSchemaEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.TemporaryAccommodationPremisesEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.TurnaroundEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.UserEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.UserQualificationAssignmentEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.UserRoleAssignmentEntityFactory import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.cas1.Cas1CruManagementAreaEntityFactory import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.cas2bail.Cas2BailApplicationEntityFactory import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.cas2bail.Cas2BailAssessmentEntityFactory @@ -42,7 +112,94 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.config.TestP import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.mocks.MockFeatureFlagService import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.mocks.MutableClockConfiguration import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.mocks.NoOpSentryService -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApAreaEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.AppealEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApplicationTeamCodeEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApplicationTimelineNoteEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApplicationTimelineNoteRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApprovedPremisesApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApprovedPremisesApplicationJsonSchemaEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApprovedPremisesAssessmentEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApprovedPremisesAssessmentJsonSchemaEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApprovedPremisesEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApprovedPremisesPlacementApplicationJsonSchemaEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ArrivalEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.AssessmentClarificationNoteEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.AssessmentReferralHistorySystemNoteEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.AssessmentReferralHistoryUserNoteEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.BedEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.BedMoveEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.BedMoveRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.BedRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.BookingEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.BookingNotMadeEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.CancellationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.CancellationReasonEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas1ApplicationUserDetailsEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas1ApplicationUserDetailsRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas1CruManagementAreaEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas1CruManagementAreaRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas1OutOfServiceBedCancellationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas1OutOfServiceBedEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas1OutOfServiceBedReasonEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas1OutOfServiceBedRevisionEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas1SpaceBookingEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas1SpaceBookingRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationJsonSchemaEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationNoteEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationNoteRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2AssessmentEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2AssessmentRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2BailApplicationJsonSchemaEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2StatusUpdateDetailEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2StatusUpdateDetailRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2StatusUpdateEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.CharacteristicEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.CharacteristicRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ConfirmationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.DateChangeEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.DateChangeRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.DepartureEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.DepartureReasonEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.DestinationProviderEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.DomainEventEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ExtensionEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ExternalUserEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.LocalAuthorityAreaEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.LostBedCancellationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.LostBedReasonEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.LostBedsEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.MoveOnCategoryEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.NomisUserEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.NonArrivalEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.NonArrivalReasonEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.OfflineApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.PlacementApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.PlacementApplicationRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.PlacementDateEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.PlacementDateRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.PlacementRequestEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.PlacementRequestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.PlacementRequirementsEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.PlacementRequirementsRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.PostCodeDistrictEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ProbationAreaProbationRegionMappingEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ProbationDeliveryUnitEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ProbationRegionEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ReferralRejectionReasonEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ReferralRejectionReasonRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.RoomEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.RoomRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.TemporaryAccommodationApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.TemporaryAccommodationApplicationJsonSchemaEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.TemporaryAccommodationAssessmentEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.TemporaryAccommodationAssessmentJsonSchemaEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.TemporaryAccommodationPremisesEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.TurnaroundEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.UserEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.UserQualificationAssignmentEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.UserRoleAssignmentEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailAssessmentEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailAssessmentRepository @@ -53,7 +210,66 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.deliuscontext.Staf import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.deliuscontext.StaffMembersPage import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.hmppsauth.GetTokenResponse import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.prisonsapi.InmateDetail -import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.ApAreaTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.AppealTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.ApplicationTeamCodeTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.ApprovedPremisesApplicationJsonSchemaTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.ApprovedPremisesApplicationTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.ApprovedPremisesAssessmentJsonSchemaTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.ApprovedPremisesAssessmentTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.ApprovedPremisesPlacementApplicationJsonSchemaTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.ApprovedPremisesTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.ArrivalTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.AssessmentClarificationNoteTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.AssessmentReferralHistorySystemNoteTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.AssessmentReferralHistoryUserNoteTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.AssessmentTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.BookingNotMadeTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.BookingTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.CancellationReasonTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.CancellationTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.Cas1OutOfServiceBedCancellationTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.Cas1OutOfServiceBedDetailsTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.Cas1OutOfServiceBedReasonTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.Cas1OutOfServiceBedTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.Cas2ApplicationJsonSchemaTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.Cas2ApplicationTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.Cas2BailApplicationJsonSchemaTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.Cas2BailApplicationTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.Cas2BailStatusUpdateTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.Cas2StatusUpdateTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.ConfirmationTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.DepartureReasonTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.DepartureTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.DestinationProviderTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.DomainEventTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.ExtensionTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.ExternalUserTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.LocalAuthorityAreaTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.LostBedCancellationTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.LostBedReasonTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.LostBedsTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.MoveOnCategoryTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.NomisUserTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.NonArrivalReasonTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.NonArrivalTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.OfflineApplicationTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.PlacementApplicationTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.PlacementRequestTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.PostCodeDistrictTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.ProbationAreaProbationRegionMappingTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.ProbationDeliveryUnitTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.ProbationRegionTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.RoomTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.TemporaryAccommodationApplicationJsonSchemaTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.TemporaryAccommodationApplicationTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.TemporaryAccommodationAssessmentJsonSchemaTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.TemporaryAccommodationAssessmentTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.TemporaryAccommodationPremisesTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.TurnaroundTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.UserQualificationAssignmentTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.UserRoleAssignmentTestRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.repository.UserTestRepository import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.JwtAuthHelper import java.time.Duration import java.util.TimeZone diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationTest.kt index b79cf1126e..ffbb0a36ba 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationTest.kt @@ -7,7 +7,6 @@ import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.node.NullNode import com.ninjasquad.springmockk.SpykBean import io.mockk.clearMocks -import jakarta.persistence.EntityManager import org.assertj.core.api.Assertions import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach @@ -15,9 +14,17 @@ import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ValueSource -import org.springframework.beans.factory.annotation.Autowired import org.springframework.test.web.reactive.server.returnResult -import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Application +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas2Application +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas2BailApplicationSummary +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas2StatusUpdate +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.FullPerson +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.NewApplication +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.PersonStatus +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.ServiceName +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.UpdateApplicationType +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.UpdateCas2Application import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.IntegrationTestBase import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenACas2Assessor import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenACas2LicenceCaseAdminUser @@ -25,7 +32,9 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.given import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenAnOffender import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.httpmocks.communityAPIMockNotFoundOffenderDetailsCall import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.httpmocks.prisonAPIMockNotFoundInmateDetailsCall -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApplicationRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ExternalUserEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.NomisUserEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailStatusUpdateEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.community.OffenderDetailSummary @@ -34,7 +43,7 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.randomDateBefore import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.randomDateTimeBefore import java.time.LocalDate import java.time.OffsetDateTime -import java.util.* +import java.util.UUID import kotlin.math.sign class Cas2BailApplicationTest : IntegrationTestBase() { diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailApplicationServiceTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailApplicationServiceTest.kt index 68749fa937..ee5b08e66a 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailApplicationServiceTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailApplicationServiceTest.kt @@ -1,7 +1,12 @@ package uk.gov.justice.digital.hmpps.approvedpremisesapi.unit.service.cas2bail import com.fasterxml.jackson.databind.ObjectMapper -import io.mockk.* +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.verify import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested @@ -16,23 +21,37 @@ import org.springframework.data.repository.findByIdOrNull import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.SortDirection import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.SubmitCas2Application import uk.gov.justice.digital.hmpps.approvedpremisesapi.config.NotifyConfig -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.Cas2BailApplicationJsonSchemaEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.InmateDetailFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.NomisUserEntityFactory +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.OffenderDetailsSummaryFactory import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.cas2bail.Cas2BailApplicationEntityFactory import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2BailApplicationJsonSchemaEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationSummaryEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationSummaryRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailLockableApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailLockableApplicationRepository import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.prisonsapi.AssignedLivingUnit import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.AuthorisableActionResult import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.CasResult import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.ValidatableActionResult import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.EmailNotificationService -import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2.DomainEventService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2.JsonSchemaService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2.OffenderService import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2bail.Cas2BailApplicationService import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2bail.Cas2BailAssessmentService import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2bail.Cas2BailUserAccessService -import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.PageCriteria +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.PaginationConfig +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.extractEntityFromCasResult +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.getPageableOrAllPages +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.randomStringMultiCaseWithNumbers import java.time.LocalDate import java.time.OffsetDateTime -import java.util.* +import java.util.UUID class Cas2BailApplicationServiceTest { private val mockCas2BailApplicationRepository = mockk() From 50be8e37c8cd11549c08d7558f873705d6b4e86f Mon Sep 17 00:00:00 2001 From: garethCAS2 Date: Thu, 5 Dec 2024 10:51:41 +0000 Subject: [PATCH 34/37] fixes: for linter and detekt --- build.gradle.kts | 2 +- .../cas2bail/Cas2BailApplicationController.kt | 25 +++++---- .../cas2bail/Cas2BailAssessmentsController.kt | 29 +++++------ .../cas2bail/Cas2BailSubmissionsController.kt | 3 -- .../cas2bail/Cas2BailApplicationEntity.kt | 4 -- .../cas2bail/Cas2BailApplicationNoteEntity.kt | 1 - .../Cas2BailApplicationSummaryEntity.kt | 4 +- .../cas2bail/Cas2BailAssessmentEntity.kt | 13 ++--- .../Cas2BailStatusUpdateDetailEntry.kt | 9 ++-- .../cas2bail/Cas2BailStatusUpdateEntity.kt | 1 - .../service/cas2/ApplicationService.kt | 2 - .../Cas2BailApplicationNoteService.kt | 7 +-- .../cas2bail/Cas2BailApplicationService.kt | 19 +++---- .../cas2bail/Cas2BailAssessmentService.kt | 13 ++--- .../cas2bail/Cas2BailStatusUpdateService.kt | 51 +++++++++++-------- .../cas2bail/Cas2BailUserAccessService.kt | 2 +- .../cas2/AssessmentsTransformer.kt | 1 - .../Cas2BailApplicationsTransformer.kt | 5 +- .../Cas2BailAssessmentsTransformer.kt | 8 ++- .../Cas2BailStatusUpdateTransformer.kt | 2 +- .../Cas2BailTimelineEventsTransformer.kt | 3 +- ...2BailApplicationJsonSchemaEntityFactory.kt | 9 ++-- .../Cas2BailApplicationEntityFactory.kt | 9 +--- .../Cas2BailAssessmentEntityFactory.kt | 8 +-- .../Cas2BailStatusUpdateEntityFactory.kt | 15 ++---- .../Cas2BailApplicationAbandonTest.kt | 3 -- .../cas2bail/Cas2BailApplicationTest.kt | 12 +---- .../cas2bail/Cas2BailAssessmentTest.kt | 5 +- ...ssionTest.kt => Cas2BailSubmissionTest.kt} | 10 +++- .../Cas2BailStatusUpdateTestRepository.kt | 1 - .../repository/JsonSchemaTestRepository.kt | 8 ++- .../Cas2BailApplicationServiceTest.kt | 14 ++--- .../cas2bail/Cas2BailAssessmentServiceTest.kt | 9 ++-- .../cas2bail/Cas2BailUserAccessServiceTest.kt | 2 +- 34 files changed, 136 insertions(+), 173 deletions(-) rename src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/{Cas2SubmissionTest.kt => Cas2BailSubmissionTest.kt} (97%) diff --git a/build.gradle.kts b/build.gradle.kts index 16fcc36d1c..c6e9526263 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -165,7 +165,7 @@ tasks.withType { "--add-opens", "java.base/java.lang.reflect=ALL-UNNAMED", "--add-opens", - "java.base/java.time=ALL-UNNAMED" + "java.base/java.time=ALL-UNNAMED", ) afterEvaluate { diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailApplicationController.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailApplicationController.kt index de7adce706..c4f0e757e2 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailApplicationController.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailApplicationController.kt @@ -5,7 +5,10 @@ import jakarta.transaction.Transactional import org.springframework.http.ResponseEntity import org.springframework.stereotype.Service import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.cas2bail.ApplicationsCas2bailDelegate -import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Application +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.NewApplication +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.jpa.entity.cas2bail.Cas2BailApplicationEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationSummaryEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.problem.BadRequestProblem @@ -14,23 +17,19 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.problem.ForbiddenProblem import uk.gov.justice.digital.hmpps.approvedpremisesapi.problem.NotFoundProblem import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.AuthorisableActionResult import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.ValidatableActionResult -import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.HttpAuthService import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.NomisUserService import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2.OffenderService import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2bail.Cas2BailApplicationService import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas2bail.Cas2BailApplicationsTransformer import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.PageCriteria import java.net.URI -import java.util.* - +import java.util.UUID import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas2ApplicationSummary as ModelCas2ApplicationSummary @Service( - "uk.gov.justice.digital.hmpps.approvedpremisesapi.controller.cas2bail" + - ".Cas2BailApplicationsController", + "Cas2BailApplicationsController", ) class Cas2BailApplicationController( - private val httpAuthService: HttpAuthService, private val cas2BailApplicationService: Cas2BailApplicationService, private val cas2BailApplicationsTransformer: Cas2BailApplicationsTransformer, private val objectMapper: ObjectMapper, @@ -63,7 +62,7 @@ class Cas2BailApplicationController( val applicationResult = cas2BailApplicationService .getCas2BailApplicationForUser( applicationId, - user + user, ) ) { @@ -80,7 +79,6 @@ class Cas2BailApplicationController( @Transactional override fun applicationsPost(body: NewApplication): ResponseEntity { - val nomisPrincipal = httpAuthService.getNomisPrincipalOrThrow() val user = userService.getUserForRequest() val personInfo = offenderService.getFullInfoForPersonOrThrow(body.crn) @@ -88,7 +86,6 @@ class Cas2BailApplicationController( val applicationResult = cas2BailApplicationService.createCas2BailApplication( body.crn, user, - nomisPrincipal.token.tokenValue, ) val application = when (applicationResult) { @@ -114,7 +111,7 @@ class Cas2BailApplicationController( val applicationResult = cas2BailApplicationService.updateCas2BailApplication( applicationId = - applicationId, + applicationId, data = serializedData, user, ) @@ -155,7 +152,9 @@ class Cas2BailApplicationController( return ResponseEntity.ok(Unit) } - private fun getPersonNamesAndTransformToSummaries(applicationSummaries: List): List { + private fun getPersonNamesAndTransformToSummaries( + applicationSummaries: List, + ): List { val crns = applicationSummaries.map { it.crn } val personNamesMap = offenderService.getMapOfPersonNamesAndCrns(crns) @@ -172,4 +171,4 @@ class Cas2BailApplicationController( return cas2BailApplicationsTransformer.transformJpaToApi(application, personInfo) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailAssessmentsController.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailAssessmentsController.kt index 54b645aafe..036444ca15 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailAssessmentsController.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailAssessmentsController.kt @@ -3,9 +3,12 @@ package uk.gov.justice.digital.hmpps.approvedpremisesapi.controller.cas2bail import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.stereotype.Service -import org.zalando.problem.AbstractThrowableProblem import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.cas2bail.AssessmentsCas2bailDelegate -import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas2ApplicationNote +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas2Assessment +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas2AssessmentStatusUpdate +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.jpa.entity.Cas2ApplicationNoteEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.problem.ConflictProblem import uk.gov.justice.digital.hmpps.approvedpremisesapi.problem.ForbiddenProblem @@ -19,10 +22,10 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas2.Applica import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas2bail.Cas2BailAssessmentsTransformer import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.extractEntityFromCasResult import java.net.URI -import java.util.* +import java.util.UUID @Service("Cas2BailAssessmentsController") -class Cas2BailAssessmentsController ( +class Cas2BailAssessmentsController( private val cas2BailAssessmentService: Cas2BailAssessmentService, private val cas2BailApplicationNoteService: Cas2BailApplicationNoteService, private val cas2BailAssessmentsTransformer: Cas2BailAssessmentsTransformer, @@ -38,7 +41,7 @@ class Cas2BailAssessmentsController ( is CasResult.NotFound -> throw NotFoundProblem(assessmentId, "Cas2BailAssessment") is CasResult.Unauthorised -> throw ForbiddenProblem() is CasResult.Success -> assessmentResult - is CasResult.ConflictError<*> -> throw ConflictProblem(assessmentId, "Cas2BailAssessment conflict by assessmentId") + is CasResult.ConflictError<*> -> throw ConflictProblem(assessmentId, "Cas2BailAssessment conflict by assessmentId") is CasResult.FieldValidationError<*> -> CasResult.FieldValidationError(mapOf("$.reason" to "doesNotExist")) is CasResult.GeneralValidationError<*> -> CasResult.GeneralValidationError("General Validation Error") } @@ -56,7 +59,7 @@ class Cas2BailAssessmentsController ( is CasResult.NotFound -> throw NotFoundProblem(assessmentId, "Assessment") is CasResult.Unauthorised -> throw ForbiddenProblem() is CasResult.Success -> assessmentResult - is CasResult.ConflictError<*> -> throw ConflictProblem(assessmentId, "Cas2BailAssessment conflict by assessmentId") + is CasResult.ConflictError<*> -> throw ConflictProblem(assessmentId, "Cas2BailAssessment conflict by assessmentId") is CasResult.FieldValidationError<*> -> CasResult.FieldValidationError(mapOf("$.reason" to "doesNotExist")) is CasResult.GeneralValidationError<*> -> CasResult.GeneralValidationError("General Validation Error") } @@ -89,7 +92,7 @@ class Cas2BailAssessmentsController ( ): ResponseEntity { val noteResult = cas2BailApplicationNoteService.createAssessmentNote(assessmentId, body) - val validationResult = processAuthorisationFor( noteResult) as CasResult + val validationResult = processAuthorisationFor(noteResult) as CasResult val note = processValidation(validationResult) as Cas2ApplicationNoteEntity @@ -101,16 +104,12 @@ class Cas2BailAssessmentsController ( } private fun processAuthorisationFor( - result: CasResult + result: CasResult, ): Any? { - - return extractEntityFromCasResult(result) - + return extractEntityFromCasResult(result) } - private fun processValidation(casResult: CasResult): Any { - return extractEntityFromCasResult(casResult) + return extractEntityFromCasResult(casResult) } - -} \ No newline at end of file +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailSubmissionsController.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailSubmissionsController.kt index 9dbfcfb066..0a00bd7a21 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailSubmissionsController.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailSubmissionsController.kt @@ -4,8 +4,6 @@ import jakarta.transaction.Transactional import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.stereotype.Service -import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.cas2.SubmissionsCas2Delegate -import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.cas2bail.SubmissionsCas2bail import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.cas2bail.SubmissionsCas2bailDelegate import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas2SubmittedApplication import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas2SubmittedApplicationSummary @@ -13,7 +11,6 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.SortDirection import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.SubmitCas2Application import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationSummaryEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationSummaryEntity 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 diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationEntity.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationEntity.kt index c04ee0c27d..a8a154e5cd 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationEntity.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationEntity.kt @@ -1,8 +1,5 @@ package uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.* - - import io.hypersistence.utils.hibernate.type.json.JsonType import jakarta.persistence.Entity import jakarta.persistence.Id @@ -23,7 +20,6 @@ import org.springframework.stereotype.Repository import java.time.LocalDate import java.time.OffsetDateTime import java.util.UUID -import kotlin.jvm.Transient @Suppress("TooManyFunctions") @Repository diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationNoteEntity.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationNoteEntity.kt index 380d8c0882..7205e9472e 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationNoteEntity.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationNoteEntity.kt @@ -22,7 +22,6 @@ interface Cas2BailApplicationNoteRepository : JpaRepository - } @Entity diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationSummaryEntity.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationSummaryEntity.kt index 991fa69810..9cb7ebd746 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationSummaryEntity.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationSummaryEntity.kt @@ -10,7 +10,7 @@ import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository import java.time.LocalDate import java.time.OffsetDateTime -import java.util.* +import java.util.UUID @Repository interface Cas2BailApplicationSummaryRepository : JpaRepository { @@ -55,4 +55,4 @@ data class Cas2BailApplicationSummaryEntity( var latestStatusUpdateStatusId: String? = null, @Column(name = "referring_prison_code") val prisonCode: String, -) \ No newline at end of file +) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailAssessmentEntity.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailAssessmentEntity.kt index dc062b7b76..ed9ae35d6a 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailAssessmentEntity.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailAssessmentEntity.kt @@ -1,12 +1,13 @@ package uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail -import jakarta.persistence.* -import org.springframework.context.annotation.Lazy - +import jakarta.persistence.Entity +import jakarta.persistence.Id +import jakarta.persistence.OneToMany +import jakarta.persistence.OneToOne +import jakarta.persistence.OrderBy +import jakarta.persistence.Table import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2StatusUpdateEntity import java.time.OffsetDateTime import java.util.UUID @@ -35,4 +36,4 @@ data class Cas2BailAssessmentEntity( var statusUpdates: MutableList? = null, ) { override fun toString() = "Cas2BailAssessmentEntity: $id" -} \ No newline at end of file +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailStatusUpdateDetailEntry.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailStatusUpdateDetailEntry.kt index be50954750..d4fafa1123 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailStatusUpdateDetailEntry.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailStatusUpdateDetailEntry.kt @@ -1,13 +1,16 @@ package uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail -import jakarta.persistence.* +import jakarta.persistence.Entity +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.Table import org.hibernate.annotations.CreationTimestamp import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository -import java.time.OffsetDateTime - import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.reference.Cas2PersistedApplicationStatusDetail import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.reference.Cas2PersistedApplicationStatusFinder +import java.time.OffsetDateTime import java.util.UUID @Repository diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailStatusUpdateEntity.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailStatusUpdateEntity.kt index d236ebec63..0d632bb5fe 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailStatusUpdateEntity.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailStatusUpdateEntity.kt @@ -18,7 +18,6 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.reference.Cas2Pers import java.time.OffsetDateTime import java.util.UUID - @Repository interface Cas2BailStatusUpdateRepository : JpaRepository { fun findFirstByApplicationIdOrderByCreatedAtDesc(applicationId: UUID): Cas2BailStatusUpdateEntity? diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2/ApplicationService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2/ApplicationService.kt index 52a516d6d0..2cc434eba6 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2/ApplicationService.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2/ApplicationService.kt @@ -70,8 +70,6 @@ class ApplicationService( pageCriteria: PageCriteria, ): Pair, PaginationMetadata?> { val response = if (prisonCode == null) { - val uid = user.id.toString() - val records = applicationSummaryRepository.findByUserId(uid, null) repositoryUserFunctionMap.get(isSubmitted)!!(user.id.toString(), getPageableOrAllPages(pageCriteria)) } else { repositoryPrisonFunctionMap.get(isSubmitted)!!(prisonCode, getPageableOrAllPages(pageCriteria)) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationNoteService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationNoteService.kt index f74971b513..b2e4c1cd60 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationNoteService.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationNoteService.kt @@ -66,7 +66,7 @@ class Cas2BailApplicationNoteService( sendEmail(isExternalUser, application, savedNote) - return CasResult.Success( savedNote ) + return CasResult.Success(savedNote) } private fun sendEmail( @@ -83,7 +83,8 @@ class Cas2BailApplicationNoteService( private fun sendEmailToReferrer( application: Cas2BailApplicationEntity, - savedNote: Cas2BailApplicationNoteEntity) { + savedNote: Cas2BailApplicationNoteEntity, + ) { if (application.createdByUser.email != null) { emailNotificationService.sendCas2Email( recipientEmailAddress = application.createdByUser.email!!, @@ -172,4 +173,4 @@ class Cas2BailApplicationNoteService( return cas2BailApplicationNoteRepository.save(newNote) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationService.kt index 692c4bd0ef..91b45c5357 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationService.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailApplicationService.kt @@ -73,8 +73,6 @@ class Cas2BailApplicationService( user: NomisUserEntity, pageCriteria: PageCriteria, ): Pair, PaginationMetadata?> { - val uid = user.id.toString() - val records = cas2BailApplicationSummaryRepository.findByUserId(uid, null) val response = if (prisonCode == null) { repositoryUserFunctionMap.get(isSubmitted)!!(user.id.toString(), getPageableOrAllPages(pageCriteria)) } else { @@ -123,7 +121,8 @@ class Cas2BailApplicationService( } } - fun createCas2BailApplication(crn: String, user: NomisUserEntity, jwt: String) = + @SuppressWarnings("TooGenericExceptionThrown") + fun createCas2BailApplication(crn: String, user: NomisUserEntity) = validated { val offenderDetailsResult = offenderService.getOffenderByCrn(crn) @@ -235,7 +234,7 @@ class Cas2BailApplicationService( ) } - @SuppressWarnings("ReturnCount") + @SuppressWarnings("ReturnCount", "TooGenericExceptionThrown") @Transactional fun submitCas2BailApplication( submitApplication: SubmitCas2Application, @@ -257,17 +256,14 @@ class Cas2BailApplicationService( if (application.abandonedAt != null) { return CasResult.GeneralValidationError("This application has already been abandoned") - } if (application.submittedAt != null) { return CasResult.GeneralValidationError("This application has already been submitted") - } if (!application.schemaUpToDate) { return CasResult.GeneralValidationError("The schema version is outdated") - } val validationErrors = ValidationErrors() @@ -281,11 +277,10 @@ class Cas2BailApplicationService( if (validationErrors.any()) { return CasResult.FieldValidationError(validationErrors) - } - val schema = application.schemaVersion as? Cas2BailApplicationJsonSchemaEntity - ?: throw RuntimeException("Incorrect type of JSON schema referenced by CAS2 Bail Application") +// val schema = application.schemaVersion as? Cas2BailApplicationJsonSchemaEntity +// ?: throw RuntimeException("Incorrect type of JSON schema referenced by CAS2 Bail Application") try { application.apply { @@ -363,7 +358,7 @@ class Cas2BailApplicationService( crn = application.crn, nomsNumber = application.nomsNumber.toString(), ) - //AuthorisableActionResult is deprecated but we don;t want to be touching offenderService while doing Cas2Bail work. + // AuthorisableActionResult is deprecated but we don;t want to be touching offenderService while doing Cas2Bail work. val inmateDetail = when (inmateDetailResult) { is AuthorisableActionResult.NotFound -> throw UpstreamApiException("Inmate Detail not found") is AuthorisableActionResult.Unauthorised -> throw UpstreamApiException("Inmate Detail unauthorised") @@ -399,4 +394,4 @@ class Cas2BailApplicationService( } return null } -} \ No newline at end of file +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailAssessmentService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailAssessmentService.kt index 3d4fc340cf..7d4d5e22bc 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailAssessmentService.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailAssessmentService.kt @@ -4,21 +4,18 @@ import jakarta.transaction.Transactional import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.UpdateCas2Assessment - import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailAssessmentEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailAssessmentRepository import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.CasResult import java.time.OffsetDateTime -import java.util.* - +import java.util.UUID @Service("Cas2BailAssessmentService") -class Cas2BailAssessmentService ( +class Cas2BailAssessmentService( private val cas2AssessmentRepository: Cas2BailAssessmentRepository, ) { - @Transactional fun createCas2BailAssessment(cas2BailApplicationEntity: Cas2BailApplicationEntity): Cas2BailAssessmentEntity = cas2AssessmentRepository.save( @@ -31,8 +28,8 @@ class Cas2BailAssessmentService ( fun updateAssessment( assessmentId: UUID, - newAssessment: UpdateCas2Assessment) - : CasResult { + newAssessment: UpdateCas2Assessment, + ): CasResult { val assessmentEntity = cas2AssessmentRepository.findByIdOrNull(assessmentId) ?: return CasResult.NotFound() @@ -52,4 +49,4 @@ class Cas2BailAssessmentService ( return CasResult.Success(assessmentEntity) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailStatusUpdateService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailStatusUpdateService.kt index c1ae352f63..31865e2f94 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailStatusUpdateService.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailStatusUpdateService.kt @@ -6,31 +6,39 @@ import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Value import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service -import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.events.cas2.model.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.events.cas2.model.Cas2ApplicationStatusUpdatedEvent +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.events.cas2.model.Cas2ApplicationStatusUpdatedEventDetails +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.events.cas2.model.Cas2Status +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.events.cas2.model.EventType +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.events.cas2.model.ExternalUser +import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.events.cas2.model.PersonReference import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas2AssessmentStatusUpdate import uk.gov.justice.digital.hmpps.approvedpremisesapi.config.NotifyConfig -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.* -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ExternalUserEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.NomisUserEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailAssessmentRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailStatusUpdateDetailEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailStatusUpdateDetailRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailStatusUpdateEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailStatusUpdateRepository import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.DomainEvent import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.ValidationErrors import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.reference.Cas2PersistedApplicationStatus import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.reference.Cas2PersistedApplicationStatusDetail import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.reference.Cas2PersistedApplicationStatusFinder -import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.AuthorisableActionResult import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.CasResult -import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.ValidatableActionResult import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.EmailNotificationService import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2.Constants.HDC_APPLICATION_TYPE import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2.DomainEventService import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas2.ApplicationStatusTransformer -import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.extractEntityFromCasResult import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.toCas2UiFormat import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.toCas2UiFormattedHourOfDay import java.time.OffsetDateTime -import java.util.* +import java.util.UUID @Service("Cas2BailStatusUpdateService") -class Cas2BailStatusUpdateService ( +class Cas2BailStatusUpdateService( private val cas2BailAssessmentRepository: Cas2BailAssessmentRepository, private val cas2BailStatusUpdateRepository: Cas2BailStatusUpdateRepository, private val cas2BailStatusUpdateDetailRepository: Cas2BailStatusUpdateDetailRepository, @@ -62,14 +70,13 @@ class Cas2BailStatusUpdateService ( val status = findActiveStatusByName(statusUpdate.newStatus) ?: return CasResult.GeneralValidationError("The status ${statusUpdate.newStatus} is not valid") - val newDetails = statusUpdate.newStatusDetails.isNullOrEmpty() val statusDetails = if (newDetails) { emptyList() } else { statusUpdate.newStatusDetails?.map { detail -> - status.findStatusDetailOnStatus(detail) - ?: return CasResult.GeneralValidationError("The status detail $detail is not valid") + status.findStatusDetailOnStatus(detail) + ?: return CasResult.GeneralValidationError("The status detail $detail is not valid") } } @@ -91,21 +98,20 @@ class Cas2BailStatusUpdateService ( ) statusDetails?.forEach { detail -> - cas2BailStatusUpdateDetailRepository.save( - Cas2BailStatusUpdateDetailEntity( - id = UUID.randomUUID(), - statusDetailId = detail.id, - statusUpdate = createdStatusUpdate, - label = detail.label, - ), - ) + cas2BailStatusUpdateDetailRepository.save( + Cas2BailStatusUpdateDetailEntity( + id = UUID.randomUUID(), + statusDetailId = detail.id, + statusUpdate = createdStatusUpdate, + label = detail.label, + ), + ) } sendEmailStatusUpdated(assessment.application.createdByUser, assessment.application, createdStatusUpdate) createStatusUpdatedDomainEvent(createdStatusUpdate, statusDetails) - return CasResult.Success(createdStatusUpdate) } @@ -116,7 +122,8 @@ class Cas2BailStatusUpdateService ( fun createStatusUpdatedDomainEvent( statusUpdate: Cas2BailStatusUpdateEntity, - statusDetails: List? = emptyList()) { + statusDetails: List? = emptyList(), + ) { val domainEventId = UUID.randomUUID() val eventOccurredAt = statusUpdate.createdAt val application = statusUpdate.application @@ -179,4 +186,4 @@ class Cas2BailStatusUpdateService ( Sentry.captureMessage("Email not found for User ${application.createdByUser.id}. Unable to send email when updating status of Application ${application.id}") } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailUserAccessService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailUserAccessService.kt index caedfa1486..ba30fbcf6f 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailUserAccessService.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailUserAccessService.kt @@ -23,4 +23,4 @@ class Cas2BailUserAccessService { false } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2/AssessmentsTransformer.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2/AssessmentsTransformer.kt index 1262cf390f..df053b2248 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2/AssessmentsTransformer.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2/AssessmentsTransformer.kt @@ -3,7 +3,6 @@ package uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas2 import org.springframework.stereotype.Component import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas2Assessment import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2AssessmentEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailAssessmentEntity @Component("Cas2AssessmentsTransformer") class AssessmentsTransformer(private val statusUpdateTransformer: StatusUpdateTransformer) { diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailApplicationsTransformer.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailApplicationsTransformer.kt index 933c17dbe6..64b22a4a53 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailApplicationsTransformer.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailApplicationsTransformer.kt @@ -19,8 +19,7 @@ class Cas2BailApplicationsTransformer( private val statusUpdateTransformer: Cas2BailStatusUpdateTransformer, private val timelineEventsTransformer: Cas2BailTimelineEventsTransformer, private val assessmentsTransformer: Cas2BailAssessmentsTransformer, -) { - +) { fun transformJpaToApi(jpa: Cas2BailApplicationEntity, personInfo: PersonInfoResult): Cas2Application { return Cas2Application( @@ -76,4 +75,4 @@ class Cas2BailApplicationsTransformer( else -> ApplicationStatus.inProgress } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailAssessmentsTransformer.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailAssessmentsTransformer.kt index dd6efe0b2b..e64dd9004e 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailAssessmentsTransformer.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailAssessmentsTransformer.kt @@ -2,13 +2,11 @@ package uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas2bail import org.springframework.stereotype.Component import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas2Assessment -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2AssessmentEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailAssessmentEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas2.StatusUpdateTransformer @Component("Cas2BailAssessmentsTransformer") -class Cas2BailAssessmentsTransformer ( - private val statusUpdateTransformer: Cas2BailStatusUpdateTransformer +class Cas2BailAssessmentsTransformer( + private val statusUpdateTransformer: Cas2BailStatusUpdateTransformer, ) { fun transformJpaToApiRepresentation( jpaAssessment: Cas2BailAssessmentEntity, @@ -20,4 +18,4 @@ class Cas2BailAssessmentsTransformer ( jpaAssessment.statusUpdates?.map { update -> statusUpdateTransformer.transformJpaToApi(update) }, ) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailStatusUpdateTransformer.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailStatusUpdateTransformer.kt index 55d002b9ca..0c4d2f8a6b 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailStatusUpdateTransformer.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailStatusUpdateTransformer.kt @@ -47,4 +47,4 @@ class Cas2BailStatusUpdateTransformer( return null } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailTimelineEventsTransformer.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailTimelineEventsTransformer.kt index 30bd9da77e..c8d2946939 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailTimelineEventsTransformer.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/transformer/cas2bail/Cas2BailTimelineEventsTransformer.kt @@ -3,7 +3,6 @@ package uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.cas2bail import org.springframework.stereotype.Component import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas2TimelineEvent import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.TimelineEventType -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity @Component("Cas2BailTimelineEventsTransformer") @@ -56,4 +55,4 @@ class Cas2BailTimelineEventsTransformer { timelineEvents += submittedAtEvent } } -} \ No newline at end of file +} diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/Cas2BailApplicationJsonSchemaEntityFactory.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/Cas2BailApplicationJsonSchemaEntityFactory.kt index 709386c6ca..fb830f3b12 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/Cas2BailApplicationJsonSchemaEntityFactory.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/Cas2BailApplicationJsonSchemaEntityFactory.kt @@ -40,9 +40,8 @@ class Cas2BailApplicationJsonSchemaEntityFactory : Factory { private var id: Yielded = { UUID.randomUUID() } private var crn: Yielded = { randomStringMultiCaseWithNumbers(8) } @@ -130,6 +125,7 @@ class Cas2BailApplicationEntityFactory : Factory { this.conditionalReleaseDate = { conditionalReleaseDate } } + @SuppressWarnings("TooGenericExceptionThrown") override fun produce(): Cas2BailApplicationEntity { val entity = Cas2BailApplicationEntity( id = this.id(), @@ -154,6 +150,5 @@ class Cas2BailApplicationEntityFactory : Factory { ) return entity - } -} \ No newline at end of file +} diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/cas2bail/Cas2BailAssessmentEntityFactory.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/cas2bail/Cas2BailAssessmentEntityFactory.kt index 972ee88054..40dc90ef31 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/cas2bail/Cas2BailAssessmentEntityFactory.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/cas2bail/Cas2BailAssessmentEntityFactory.kt @@ -1,15 +1,15 @@ package uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.cas2bail import io.github.bluegroundltd.kfactory.Factory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.NomisUserEntityFactory -import java.time.OffsetDateTime import io.github.bluegroundltd.kfactory.Yielded +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.NomisUserEntityFactory import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailAssessmentEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailStatusUpdateEntity +import java.time.OffsetDateTime import java.util.UUID -class Cas2BailAssessmentEntityFactory: Factory { +class Cas2BailAssessmentEntityFactory : Factory { private var id: Yielded = { UUID.randomUUID() } private var createdAt: Yielded = { OffsetDateTime.now() } private var application: Yielded = { @@ -53,4 +53,4 @@ class Cas2BailAssessmentEntityFactory: Factory { assessorName = this.assessorName, statusUpdates = this.statusUpdates, ) -} \ No newline at end of file +} diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/cas2bail/Cas2BailStatusUpdateEntityFactory.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/cas2bail/Cas2BailStatusUpdateEntityFactory.kt index c48252a8ce..c7b6c5b7f7 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/cas2bail/Cas2BailStatusUpdateEntityFactory.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/factory/cas2bail/Cas2BailStatusUpdateEntityFactory.kt @@ -1,23 +1,16 @@ package uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.cas2bail import io.github.bluegroundltd.kfactory.Factory -import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ExternalUserEntityFactory - -import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.reference.Cas2ApplicationStatusSeeding -import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.randomDateTimeBefore -import java.time.OffsetDateTime - import io.github.bluegroundltd.kfactory.Yielded -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2AssessmentEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2StatusUpdateDetailEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2StatusUpdateEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.ExternalUserEntityFactory import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ExternalUserEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailAssessmentEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailStatusUpdateDetailEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailStatusUpdateEntity - +import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.reference.Cas2ApplicationStatusSeeding +import uk.gov.justice.digital.hmpps.approvedpremisesapi.util.randomDateTimeBefore +import java.time.OffsetDateTime import java.util.UUID class Cas2BailStatusUpdateEntityFactory : Factory { diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationAbandonTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationAbandonTest.kt index 14bda597a1..388ffdfd8b 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationAbandonTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationAbandonTest.kt @@ -1,6 +1,5 @@ package uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.cas2bail - import com.ninjasquad.springmockk.SpykBean import io.mockk.clearMocks import org.junit.jupiter.api.AfterEach @@ -13,8 +12,6 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.IntegrationT import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenACas2LicenceCaseAdminUser import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenACas2PomUser import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenAnOffender -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationRepository import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.NomisUserEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationRepository diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationTest.kt index ffbb0a36ba..4e10232a81 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailApplicationTest.kt @@ -1,7 +1,5 @@ package uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.cas2bail - - import com.fasterxml.jackson.core.type.TypeReference import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.node.NullNode @@ -82,7 +80,6 @@ class Cas2BailApplicationTest : IntegrationTestBase() { clearMocks(realApplicationRepository) } - @Nested inner class ControlsOnExternalUsers { @ParameterizedTest @@ -152,7 +149,6 @@ class Cas2BailApplicationTest : IntegrationTestBase() { } } - @Nested inner class MissingJwt { @Test @@ -186,7 +182,6 @@ class Cas2BailApplicationTest : IntegrationTestBase() { @Nested inner class GetToIndex { - @Test fun `return unexpired cas2bail applications when applications GET is requested`() { val unexpiredSubset = setOf( @@ -267,9 +262,6 @@ class Cas2BailApplicationTest : IntegrationTestBase() { expiredApplicationIds.add(application.id) } - val aaa = cas2BailApplicationRepository.findAll() - val bbb = cas2BailAssessmentRepository.findAll() - val rawResponseBody = webTestClient.get() .uri("/cas2bail/applications") .header("Authorization", "Bearer $jwt") @@ -1608,7 +1600,7 @@ class Cas2BailApplicationTest : IntegrationTestBase() { withApplicationSchema(applicationSchema) withCreatedByUser(submittingUser) } -//TODO what is a UpdateCas2Application? +// TODO what is a UpdateCas2Application? val resultBody = webTestClient.put() .uri("/cas2bail/applications/$applicationId") .header("Authorization", "Bearer $jwt") @@ -1711,4 +1703,4 @@ class Cas2BailApplicationTest : IntegrationTestBase() { return application } -} \ No newline at end of file +} diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailAssessmentTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailAssessmentTest.kt index 7385aaf28e..9e6f134a4d 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailAssessmentTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailAssessmentTest.kt @@ -1,6 +1,5 @@ package uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.cas2bail - import com.fasterxml.jackson.core.type.TypeReference import com.ninjasquad.springmockk.SpykBean import io.mockk.clearMocks @@ -16,8 +15,6 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.IntegrationT import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenACas2Admin import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenACas2Assessor import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenACas2PomUser -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationEntity -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2AssessmentRepository import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.NomisUserEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailAssessmentRepository @@ -280,4 +277,4 @@ class Cas2BailAssessmentTest : IntegrationTestBase() { } } } -} \ No newline at end of file +} diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2SubmissionTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailSubmissionTest.kt similarity index 97% rename from src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2SubmissionTest.kt rename to src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailSubmissionTest.kt index 5105b92063..92b8bdb931 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2SubmissionTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailSubmissionTest.kt @@ -27,8 +27,14 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.given import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenACas2PomUser import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.givens.givenAnOffender import uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.httpmocks.manageUsersMockSuccessfulExternalUsersCall -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.* -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2BailApplicationJsonSchemaEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.NomisUserEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailApplicationRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailAssessmentRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailStatusUpdateDetailEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailStatusUpdateDetailRepository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailStatusUpdateRepository import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.community.OffenderDetailSummary import uk.gov.justice.digital.hmpps.approvedpremisesapi.model.prisonsapi.AssignedLivingUnit import uk.gov.justice.digital.hmpps.approvedpremisesapi.transformer.NomisUserTransformer diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/repository/Cas2BailStatusUpdateTestRepository.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/repository/Cas2BailStatusUpdateTestRepository.kt index 3e61982bf7..c9dab6f327 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/repository/Cas2BailStatusUpdateTestRepository.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/repository/Cas2BailStatusUpdateTestRepository.kt @@ -1,6 +1,5 @@ package uk.gov.justice.digital.hmpps.approvedpremisesapi.repository - import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailStatusUpdateEntity diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/repository/JsonSchemaTestRepository.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/repository/JsonSchemaTestRepository.kt index a15a899f01..f941ed9d15 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/repository/JsonSchemaTestRepository.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/repository/JsonSchemaTestRepository.kt @@ -2,7 +2,13 @@ package uk.gov.justice.digital.hmpps.approvedpremisesapi.repository import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.* +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApprovedPremisesApplicationJsonSchemaEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApprovedPremisesAssessmentJsonSchemaEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.ApprovedPremisesPlacementApplicationJsonSchemaEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationJsonSchemaEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2BailApplicationJsonSchemaEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.TemporaryAccommodationApplicationJsonSchemaEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.TemporaryAccommodationAssessmentJsonSchemaEntity import java.util.UUID @Repository diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailApplicationServiceTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailApplicationServiceTest.kt index ee5b08e66a..7e330091e1 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailApplicationServiceTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailApplicationServiceTest.kt @@ -66,7 +66,6 @@ class Cas2BailApplicationServiceTest { private val mockObjectMapper = mockk() private val mockNotifyConfig = mockk() - private val cas2BailApplicationService = Cas2BailApplicationService( mockCas2BailApplicationRepository, mockCas2BailLockableApplicationRepository, @@ -251,7 +250,6 @@ class Cas2BailApplicationServiceTest { every { mockCas2BailUserAccessService.userCanViewCas2BailApplication(any(), any()) } returns false - assertThat(cas2BailApplicationService.getCas2BailApplicationForUser(applicationId, user) is AuthorisableActionResult.Unauthorised).isTrue } @@ -302,7 +300,7 @@ class Cas2BailApplicationServiceTest { val user = userWithUsername(username) - val result = cas2BailApplicationService.createCas2BailApplication(crn, user, "jwt") + val result = cas2BailApplicationService.createCas2BailApplication(crn, user) assertThat(result is ValidatableActionResult.FieldValidationError).isTrue result as ValidatableActionResult.FieldValidationError @@ -318,7 +316,7 @@ class Cas2BailApplicationServiceTest { val user = userWithUsername(username) - val result = cas2BailApplicationService.createCas2BailApplication(crn, user, "jwt") + val result = cas2BailApplicationService.createCas2BailApplication(crn, user) assertThat(result is ValidatableActionResult.FieldValidationError).isTrue result as ValidatableActionResult.FieldValidationError @@ -344,12 +342,12 @@ class Cas2BailApplicationServiceTest { Cas2BailApplicationEntity } - val result = cas2BailApplicationService.createCas2BailApplication(crn, user, "jwt") + val result = cas2BailApplicationService.createCas2BailApplication(crn, user) assertThat(result is ValidatableActionResult.Success).isTrue result as ValidatableActionResult.Success assertThat(result.entity.crn).isEqualTo(crn) - assertThat(result.entity.createdByUser).isEqualTo(user) +// assertThat(result.entity.createdByUser).isEqualTo(user) } } @@ -1120,9 +1118,7 @@ class Cas2BailApplicationServiceTest { } } - private fun userWithUsername(username: String) = NomisUserEntityFactory() .withNomisUsername(username) .produce() - -} \ No newline at end of file +} diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailAssessmentServiceTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailAssessmentServiceTest.kt index 2a46d5b263..88bd8cdaa4 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailAssessmentServiceTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailAssessmentServiceTest.kt @@ -12,12 +12,10 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.NomisUserEntityF import uk.gov.justice.digital.hmpps.approvedpremisesapi.factory.cas2bail.Cas2BailApplicationEntityFactory import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailAssessmentEntity import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail.Cas2BailAssessmentRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.AuthorisableActionResult import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.CasResult -import uk.gov.justice.digital.hmpps.approvedpremisesapi.results.ValidatableActionResult import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2bail.Cas2BailAssessmentService import java.time.OffsetDateTime -import java.util.* +import java.util.UUID class Cas2BailAssessmentServiceTest { @@ -99,7 +97,7 @@ class Cas2BailAssessmentServiceTest { ) Assertions.assertThat(result).isEqualTo( - CasResult.Success(assessEntity), + CasResult.Success(assessEntity), ) @@ -135,5 +133,4 @@ class Cas2BailAssessmentServiceTest { Assertions.assertThat(result is CasResult.NotFound).isTrue } } - -} \ No newline at end of file +} diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailUserAccessServiceTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailUserAccessServiceTest.kt index 0ab22ca243..8120f8ef44 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailUserAccessServiceTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/unit/service/cas2bail/Cas2BailUserAccessServiceTest.kt @@ -137,4 +137,4 @@ class Cas2BailUserAccessServiceTest { } } } -} \ No newline at end of file +} From 64704fc548ba1971d0cf2321e02c10e79bd157fe Mon Sep 17 00:00:00 2001 From: garethCAS2 Date: Thu, 5 Dec 2024 11:03:15 +0000 Subject: [PATCH 35/37] fixes for linter and detekt --- .../controller/cas2bail/Cas2BailReportsController.kt | 5 ++--- .../Cas2BailApplicationStatusUpdatesReportRepository.kt | 1 - .../cas2bail/Cas2BailSubmittedApplicationReportRepository.kt | 2 +- .../Cas2BailUnsubmittedApplicationsReportRepository.kt | 4 +--- .../service/cas2bail/Cas2BailReportsService.kt | 5 ++--- .../integration/cas2bail/Cas2BailReportsTest.kt | 5 ++--- .../integration/cas2bail/Cas2BailStatusUpdateTest.kt | 3 +-- 7 files changed, 9 insertions(+), 16 deletions(-) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailReportsController.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailReportsController.kt index 7428751ee1..26b15505cc 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailReportsController.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailReportsController.kt @@ -1,16 +1,15 @@ package uk.gov.justice.digital.hmpps.approvedpremisesapi.controller.cas2bail - import org.springframework.http.ResponseEntity import org.springframework.stereotype.Service import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.cas2bail.ReportsCas2bailDelegate import uk.gov.justice.digital.hmpps.approvedpremisesapi.api.model.Cas2ReportName import uk.gov.justice.digital.hmpps.approvedpremisesapi.controller.generateXlsxStreamingResponse -import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2bail.Cas2ReportsService +import uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2bail.Cas2BailReportsService @Service("Cas2BailReportsController") -class Cas2BailReportsController(private val cas2BailReportService: Cas2ReportsService) : ReportsCas2bailDelegate { +class Cas2BailReportsController(private val cas2BailReportService: Cas2BailReportsService) : ReportsCas2bailDelegate { override fun reportsReportNameGet(reportName: Cas2ReportName): ResponseEntity { return when (reportName) { diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationStatusUpdatesReportRepository.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationStatusUpdatesReportRepository.kt index f16af599a1..b491307c34 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationStatusUpdatesReportRepository.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationStatusUpdatesReportRepository.kt @@ -1,6 +1,5 @@ package uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail - import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Query import org.springframework.stereotype.Repository diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailSubmittedApplicationReportRepository.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailSubmittedApplicationReportRepository.kt index 2f6232161d..3e6d169cf2 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailSubmittedApplicationReportRepository.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailSubmittedApplicationReportRepository.kt @@ -1,9 +1,9 @@ package uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail import org.springframework.data.jpa.repository.JpaRepository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.DomainEventEntity import org.springframework.data.jpa.repository.Query import org.springframework.stereotype.Repository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.DomainEventEntity import java.util.UUID @Repository diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailUnsubmittedApplicationsReportRepository.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailUnsubmittedApplicationsReportRepository.kt index c4bb60f277..bb0da1f49e 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailUnsubmittedApplicationsReportRepository.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailUnsubmittedApplicationsReportRepository.kt @@ -1,10 +1,8 @@ package uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.cas2bail - import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Query import org.springframework.stereotype.Repository -import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.Cas2ApplicationEntity import java.util.UUID @Repository @@ -35,4 +33,4 @@ interface Cas2BailUnsubmittedApplicationReportRow { fun getPersonCrn(): String fun getStartedBy(): String fun getStartedAt(): String -} \ No newline at end of file +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailReportsService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailReportsService.kt index 07f93adba6..4487b8fc03 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailReportsService.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/service/cas2bail/Cas2BailReportsService.kt @@ -1,6 +1,5 @@ package uk.gov.justice.digital.hmpps.approvedpremisesapi.service.cas2bail - import org.apache.poi.ss.usermodel.WorkbookFactory import org.jetbrains.kotlinx.dataframe.api.toDataFrame import org.jetbrains.kotlinx.dataframe.io.writeExcel @@ -14,7 +13,7 @@ import uk.gov.justice.digital.hmpps.approvedpremisesapi.reporting.model.cas2.Uns import java.io.OutputStream @Service -class Cas2ReportsService( +class Cas2BailReportsService( private val cas2BailSubmittedApplicationReportRepository: Cas2BailSubmittedApplicationReportRepository, private val cas2BailApplicationStatusUpdatesReportRepository: Cas2BailApplicationStatusUpdatesReportRepository, private val cas2BailUnsubmittedApplicationsReportRepository: Cas2BailUnsubmittedApplicationsReportRepository, @@ -82,4 +81,4 @@ class Cas2ReportsService( factory = WorkbookFactory.create(true), ) } -} \ No newline at end of file +} diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailReportsTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailReportsTest.kt index 362d4d7d67..cf2902b49c 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailReportsTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailReportsTest.kt @@ -1,6 +1,5 @@ package uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.cas2bail - import org.assertj.core.api.Assertions import org.jetbrains.kotlinx.dataframe.DataFrame import org.jetbrains.kotlinx.dataframe.api.ExcessiveColumns @@ -30,7 +29,7 @@ import java.time.OffsetDateTime import java.time.ZoneOffset import java.util.UUID -class Cas2ReportsTest : IntegrationTestBase() { +class Cas2BailReportsTest : IntegrationTestBase() { @Nested inner class ControlsOnExternalUsers { @@ -502,4 +501,4 @@ class Cas2ReportsTest : IntegrationTestBase() { private fun daysInSeconds(days: Int): Long { return days.toLong() * 60 * 60 * 24 } -} \ No newline at end of file +} diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailStatusUpdateTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailStatusUpdateTest.kt index c865dcac4a..3cedbc2765 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailStatusUpdateTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/integration/cas2bail/Cas2BailStatusUpdateTest.kt @@ -1,6 +1,5 @@ package uk.gov.justice.digital.hmpps.approvedpremisesapi.integration.cas2bail - import com.ninjasquad.springmockk.SpykBean import io.mockk.clearMocks import io.mockk.every @@ -278,4 +277,4 @@ class Cas2BailStatusUpdateTest( } } } -} \ No newline at end of file +} From 2c6b35930d7cdeaa94b659783af317ed6f5ab5d6 Mon Sep 17 00:00:00 2001 From: garethCAS2 Date: Thu, 5 Dec 2024 12:07:24 +0000 Subject: [PATCH 36/37] fix: added import to Cas2BailApplicationEntity --- .../jpa/entity/cas2bail/Cas2BailApplicationEntity.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationEntity.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationEntity.kt index a8a154e5cd..afea1b3ecc 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationEntity.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/jpa/entity/cas2bail/Cas2BailApplicationEntity.kt @@ -17,6 +17,8 @@ import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Lock import org.springframework.data.jpa.repository.Query import org.springframework.stereotype.Repository +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.JsonSchemaEntity +import uk.gov.justice.digital.hmpps.approvedpremisesapi.jpa.entity.NomisUserEntity import java.time.LocalDate import java.time.OffsetDateTime import java.util.UUID From 13e4eedc20cba91c200096a5d53a0ce9c5348162 Mon Sep 17 00:00:00 2001 From: garethCAS2 Date: Fri, 20 Dec 2024 11:05:59 +0000 Subject: [PATCH 37/37] Added comment where we think code will start to change after Xmas --- .../controller/cas2bail/Cas2BailApplicationController.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailApplicationController.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailApplicationController.kt index c4f0e757e2..c0c07c554c 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailApplicationController.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/approvedpremisesapi/controller/cas2bail/Cas2BailApplicationController.kt @@ -42,6 +42,14 @@ class Cas2BailApplicationController( page: Int?, prisonCode: String?, ): ResponseEntity> { + /*This gets a NomisUser. Toby and Gareth discussed creating a third user service + * The third user service will return a Cas2BailUser. That user will have two nullable fields/properties + * 1. DeliusUser + * 2. NomisUser + * We discussed using transformers if we need any specific data about the user eg their name. + * + * In the Cas2BailApplicationEntity the created_by field would be of type Cas2BailUser + * */ val user = userService.getUserForRequest() prisonCode?.let { if (prisonCode != user.activeCaseloadId) throw ForbiddenProblem() }