diff --git a/.github/actions/analyse/action.yml b/.github/actions/analyse/action.yml index a6ab05950a..120554f576 100644 --- a/.github/actions/analyse/action.yml +++ b/.github/actions/analyse/action.yml @@ -26,7 +26,7 @@ runs: shell: bash - name: Publish test reports - uses: mikepenz/action-junit-report@a427a90771729d8f85b6ab0cdaa1a5929cab985d # v5 + uses: mikepenz/action-junit-report@992d97d6eb2e5f3de985fbf9df6a04386874114d # v5 if: always() && github.actor != 'dependabot[bot]' with: check_name: |- @@ -51,7 +51,7 @@ runs: - name: Sonar analysis if: github.actor != 'dependabot[bot]' - run: ./gradlew sonar + run: ./gradlew sonar -Dsonar.projectVersion="$VERSION" shell: bash env: SONAR_TOKEN: ${{ inputs.sonar-token }} diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index da4c8e6a39..a57ba5a5a0 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -158,14 +158,14 @@ jobs: - name: Deploy main if: github.ref_name == 'main' - uses: JamesIves/github-pages-deploy-action@62fec3add6773ec5dbbf18d2ee4260911aa35cf4 # v4.6.9 + uses: JamesIves/github-pages-deploy-action@15de0f09300eea763baee31dff6c6184995c5f6a # v4.7.2 with: folder: tech-docs target-folder: tech-docs - name: Deploy branch if: github.ref_name != 'main' - uses: JamesIves/github-pages-deploy-action@62fec3add6773ec5dbbf18d2ee4260911aa35cf4 # v4.6.9 + uses: JamesIves/github-pages-deploy-action@15de0f09300eea763baee31dff6c6184995c5f6a # v4.7.2 with: folder: tech-docs target-folder: tech-docs-drafts/${{ github.ref_name }} diff --git a/.github/workflows/schema-spy.yml b/.github/workflows/schema-spy.yml index 5393daee35..5758338e3e 100644 --- a/.github/workflows/schema-spy.yml +++ b/.github/workflows/schema-spy.yml @@ -34,7 +34,7 @@ jobs: DB_PASSWORD: ${{ secrets.SCHEMA_SPY_PASSWORD }} - name: Publish HTML report - uses: JamesIves/github-pages-deploy-action@62fec3add6773ec5dbbf18d2ee4260911aa35cf4 # v4.6.9 + uses: JamesIves/github-pages-deploy-action@15de0f09300eea763baee31dff6c6184995c5f6a # v4.7.2 with: folder: schema-spy-report target-folder: schema-spy-report diff --git a/build.gradle.kts b/build.gradle.kts index cc97ce7780..b6993f3239 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,9 +8,9 @@ import uk.gov.justice.digital.hmpps.plugins.ClassPathPlugin import uk.gov.justice.digital.hmpps.plugins.JibConfigPlugin plugins { - kotlin("jvm") version "2.0.21" - kotlin("plugin.spring") version "2.0.21" apply false - kotlin("plugin.jpa") version "2.0.21" apply false + kotlin("jvm") version "2.1.0" + kotlin("plugin.spring") version "2.1.0" apply false + kotlin("plugin.jpa") version "2.1.0" apply false id("org.springframework.boot") version "3.4.0" apply false id("io.spring.dependency-management") version "1.1.6" apply false id("com.gorylenko.gradle-git-properties") version "2.4.2" apply false diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index da0ca2b316..00f7dce988 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -1,6 +1,6 @@ plugins { id("com.google.cloud.tools.jib") version "3.4.4" apply false - id("org.sonarqube") version "6.0.0.5145" apply false + id("org.sonarqube") version "6.0.1.5171" apply false `kotlin-dsl` } @@ -12,5 +12,5 @@ repositories { dependencies { implementation("com.google.cloud.tools:jib-gradle-plugin:3.4.4") - implementation("org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:6.0.0.5145") + implementation("org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:6.0.1.5171") } diff --git a/projects/approved-premises-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/EventDetailsGenerator.kt b/projects/approved-premises-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/EventDetailsGenerator.kt index 92bb18ed0e..eb5b90b22a 100644 --- a/projects/approved-premises-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/EventDetailsGenerator.kt +++ b/projects/approved-premises-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/EventDetailsGenerator.kt @@ -98,7 +98,6 @@ object EventDetailsGenerator { private fun staffMember(staff: Staff, username: String? = null) = StaffMember( username = username, staffCode = staff.code, - staffIdentifier = staff.id, forenames = staff.forename, surname = staff.surname ) diff --git a/projects/approved-premises-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/StaffMemberGenerator.kt b/projects/approved-premises-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/StaffMemberGenerator.kt index 21d0e51b56..404359225e 100644 --- a/projects/approved-premises-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/StaffMemberGenerator.kt +++ b/projects/approved-premises-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/StaffMemberGenerator.kt @@ -7,13 +7,11 @@ object StaffMemberGenerator { fun generate( staffCode: String = "N54A001", - staffIdentifier: Long = 1501234567, forenames: String = "John", surname: String = "Smith", username: String? = null ) = StaffMember( staffCode = staffCode, - staffIdentifier = staffIdentifier, forenames = forenames, surname = surname, username = username diff --git a/projects/approved-premises-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/StaffControllerIntegrationTest.kt b/projects/approved-premises-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/StaffControllerIntegrationTest.kt index 062c1a9210..71700dcb56 100644 --- a/projects/approved-premises-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/StaffControllerIntegrationTest.kt +++ b/projects/approved-premises-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/StaffControllerIntegrationTest.kt @@ -85,7 +85,6 @@ class StaffControllerIntegrationTest { assertThat(res.code, equalTo(StaffGenerator.DEFAULT_STAFF.code)) assertThat(res.email, equalTo("john.smith@moj.gov.uk")) assertThat(res.telephoneNumber, equalTo("07321165373")) - assertThat(res.staffIdentifier, equalTo(StaffGenerator.DEFAULT_STAFF.id)) assertThat(res.teams[0].borough?.code, equalTo(StaffGenerator.DEFAULT_STAFF.teams[0].district.borough.code)) assertThat( res.teams[0].borough?.description, diff --git a/projects/approved-premises-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/approvedpremises/StaffMember.kt b/projects/approved-premises-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/approvedpremises/StaffMember.kt index 00205427ac..c673ab72f3 100644 --- a/projects/approved-premises-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/approvedpremises/StaffMember.kt +++ b/projects/approved-premises-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/approvedpremises/StaffMember.kt @@ -2,7 +2,6 @@ package uk.gov.justice.digital.hmpps.integrations.approvedpremises data class StaffMember( val staffCode: String, - val staffIdentifier: Long, val forenames: String, val surname: String, val username: String? diff --git a/projects/approved-premises-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/model/StaffResponse.kt b/projects/approved-premises-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/model/StaffResponse.kt index 279c09cbc7..19968827f7 100644 --- a/projects/approved-premises-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/model/StaffResponse.kt +++ b/projects/approved-premises-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/model/StaffResponse.kt @@ -26,7 +26,6 @@ data class ProbationArea( data class StaffDetail( val email: String?, val telephoneNumber: String?, - val staffIdentifier: Long, val teams: List = emptyList(), val probationArea: ProbationArea, val username: String?, diff --git a/projects/approved-premises-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/service/StaffService.kt b/projects/approved-premises-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/service/StaffService.kt index daae5fa43b..34d4909418 100644 --- a/projects/approved-premises-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/service/StaffService.kt +++ b/projects/approved-premises-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/service/StaffService.kt @@ -77,7 +77,6 @@ class StaffService( code = probationArea.code, description = probationArea.description ), - active = isActive(), - staffIdentifier = id + active = isActive() ) } diff --git a/projects/common-platform-and-delius/src/dev/resources/simulations/__files/address-lookup-no-results.json b/projects/common-platform-and-delius/src/dev/resources/simulations/__files/address-lookup-no-results.json new file mode 100644 index 0000000000..d9e9027a2a --- /dev/null +++ b/projects/common-platform-and-delius/src/dev/resources/simulations/__files/address-lookup-no-results.json @@ -0,0 +1,4 @@ +{ + "header": { "totalresults": 0 }, + "results": [] +} \ No newline at end of file diff --git a/projects/common-platform-and-delius/src/dev/resources/simulations/__files/address-lookup-single-result.json b/projects/common-platform-and-delius/src/dev/resources/simulations/__files/address-lookup-single-result.json new file mode 100644 index 0000000000..a9cbd5f4e4 --- /dev/null +++ b/projects/common-platform-and-delius/src/dev/resources/simulations/__files/address-lookup-single-result.json @@ -0,0 +1,41 @@ +{ + "header": { + "totalresults": 1 + }, + "results": [ + { + "DPA": { + "UPRN": 123456789012, + "UDPRN": 12345678, + "ADDRESS": "123 Test Street, Test, AB1 2CD", + "POSTCODE": "AB1 2CD", + "BUILDING_NUMBER": 123, + "THOROUGHFARE_NAME": "Test Street", + "POST_TOWN": "Test", + "RPC": "2", + "X_COORDINATE": 123456.78, + "Y_COORDINATE": 876543.21, + "STATUS": "APPROVED", + "MATCH": 0.9, + "LANGUAGE": "EN", + "COUNTRY_CODE": "E", + "COUNTRY_CODE_DESCRIPTION": "This record is within England", + "LOCAL_CUSTODIAN_CODE": 1760, + "LOCAL_CUSTODIAN_CODE_DESCRIPTION": "Test", + "CLASSIFICATION_CODE": "CO01GV", + "CLASSIFICATION_CODE_DESCRIPTION": "Central Government Service", + "POSTAL_ADDRESS_CODE": "D", + "POSTAL_ADDRESS_CODE_DESCRIPTION": "A record which is linked to PAF", + "LOGICAL_STATUS_CODE": 1, + "BLPU_STATE_CODE": 2, + "BLPU_STATE_CODE_DESCRIPTION": "In use", + "TOPOGRAPHY_LAYER_TOID": "osgb1234567890123456", + "LAST_UPDATE_DATE": "2024-01-01", + "ENTRY_DATE": "2024-01-01", + "DELIVERY_POINT_SUFFIX": "1A", + "PARISH_CODE": "E87654321", + "WARD_CODE": "E12345678" + } + } + ] +} \ No newline at end of file diff --git a/projects/common-platform-and-delius/src/dev/resources/simulations/mappings/address-lookup-mapping.json b/projects/common-platform-and-delius/src/dev/resources/simulations/mappings/address-lookup-mapping.json new file mode 100644 index 0000000000..1fd96c6ba2 --- /dev/null +++ b/projects/common-platform-and-delius/src/dev/resources/simulations/mappings/address-lookup-mapping.json @@ -0,0 +1,13 @@ +{ + "request": { + "method": "GET", + "urlPath": "/address-lookup/search/places/v1/find" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "bodyFileName": "address-lookup-single-result.json" + } +} \ No newline at end of file diff --git a/projects/common-platform-and-delius/src/dev/resources/simulations/mappings/probation-search-mapping.json b/projects/common-platform-and-delius/src/dev/resources/simulations/mappings/probation-search-mapping.json new file mode 100644 index 0000000000..85f814a949 --- /dev/null +++ b/projects/common-platform-and-delius/src/dev/resources/simulations/mappings/probation-search-mapping.json @@ -0,0 +1,13 @@ +{ + "request": { + "method": "POST", + "urlPath": "/probation-search/match" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "bodyFileName": "probation-search-no-results.json" + } +} \ No newline at end of file diff --git a/projects/common-platform-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/IntegrationTest.kt b/projects/common-platform-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/IntegrationTest.kt index 0fe0dc3ba7..445c668efd 100644 --- a/projects/common-platform-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/IntegrationTest.kt +++ b/projects/common-platform-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/IntegrationTest.kt @@ -97,16 +97,6 @@ internal class IntegrationTest { @Test fun `When a message with no prosecution cases is found no insert is performed`() { - wireMockServer.stubFor( - post(urlPathEqualTo("/probation-search/match")) - .willReturn( - aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBodyFile("probation-search-no-results.json") - ) - ) - val notification = Notification(message = MessageGenerator.COMMON_PLATFORM_EVENT_NO_CASES) channelManager.getChannel(queueName).publishAndWait(notification) thenNoRecordsAreInserted() @@ -114,16 +104,6 @@ internal class IntegrationTest { @Test fun `When a message without a judicial result of remanded in custody is found`() { - wireMockServer.stubFor( - post(urlPathEqualTo("/probation-search/match")) - .willReturn( - aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBodyFile("probation-search-no-results.json") - ) - ) - val notification = Notification(message = MessageGenerator.COMMON_PLATFORM_EVENT_NO_REMAND) channelManager.getChannel(queueName).publishAndWait(notification) thenNoRecordsAreInserted() @@ -131,16 +111,6 @@ internal class IntegrationTest { @Test fun `When a person under 10 years old is found no insert is performed`() { - wireMockServer.stubFor( - post(urlPathEqualTo("/probation-search/match")) - .willReturn( - aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBodyFile("probation-search-no-results.json") - ) - ) - val notification = Notification(message = MessageGenerator.COMMON_PLATFORM_EVENT_DOB_ERROR) channelManager.getChannel(queueName).publishAndWait(notification) thenNoRecordsAreInserted() @@ -148,16 +118,6 @@ internal class IntegrationTest { @Test fun `When a probation search match is not detected then a person is inserted`() { - wireMockServer.stubFor( - post(urlPathEqualTo("/probation-search/match")) - .willReturn( - aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBodyFile("probation-search-no-results.json") - ) - ) - val notification = Notification(message = MessageGenerator.COMMON_PLATFORM_EVENT) channelManager.getChannel(queueName).publishAndWait(notification) @@ -181,16 +141,6 @@ internal class IntegrationTest { @Test fun `When a hearing message with missing required fields is detected no records are inserted`() { - wireMockServer.stubFor( - post(urlPathEqualTo("/probation-search/match")) - .willReturn( - aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBodyFile("probation-search-no-results.json") - ) - ) - val notification = Notification(message = MessageGenerator.COMMON_PLATFORM_EVENT_NULL_FIELDS) channelManager.getChannel(queueName).publishAndWait(notification) thenNoRecordsAreInserted() @@ -198,20 +148,11 @@ internal class IntegrationTest { @Test fun `When a hearing with an address is received then an address record is inserted`() { - wireMockServer.stubFor( - post(urlPathEqualTo("/probation-search/match")) - .willReturn( - aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBodyFile("probation-search-no-results.json") - ) - ) - val notification = Notification(message = MessageGenerator.COMMON_PLATFORM_EVENT) channelManager.getChannel(queueName).publishAndWait(notification) verify(personService).insertAddress(any()) + verify(personService).findAddressByFreeText(any()) verify(addressRepository).save(check { assertThat(it.start, Matchers.equalTo(LocalDate.now())) @@ -235,16 +176,6 @@ internal class IntegrationTest { @Test fun `When a hearing with an empty address is received then an address record is not inserted`() { - wireMockServer.stubFor( - post(urlPathEqualTo("/probation-search/match")) - .willReturn( - aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBodyFile("probation-search-no-results.json") - ) - ) - val notification = Notification(message = MessageGenerator.COMMON_PLATFORM_EVENT_BLANK_ADDRESS) channelManager.getChannel(queueName).publishAndWait(notification) @@ -264,16 +195,6 @@ internal class IntegrationTest { @Order(1) @Test fun `engagement created and address created sns messages are published on insert person`() { - wireMockServer.stubFor( - post(urlPathEqualTo("/probation-search/match")) - .willReturn( - aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBodyFile("probation-search-no-results.json") - ) - ) - val notification = Notification(message = MessageGenerator.COMMON_PLATFORM_EVENT) channelManager.getChannel(queueName).publishAndWait(notification) @@ -286,6 +207,17 @@ internal class IntegrationTest { assertThat(it.telephoneNumber, Matchers.equalTo("01234567890")) }) + verify(addressRepository).save(check { + assertThat(it.start, Matchers.equalTo(LocalDate.now())) + assertNull(it.endDate) + assertNotNull(it.notes) + assertThat(it.softDeleted, Matchers.equalTo(false)) + assertThat(it.status.code, Matchers.equalTo(ReferenceData.StandardRefDataCode.ADDRESS_MAIN_STATUS.code)) + assertThat(it.noFixedAbode, Matchers.equalTo(false)) + assertThat(it.type.code, Matchers.equalTo(ReferenceData.StandardRefDataCode.AWAITING_ASSESSMENT.code)) + assertThat(it.typeVerified, Matchers.equalTo(false)) + }) + val topic = hmppsChannelManager.getChannel(topicName) val messages = topic.pollFor(2) val messageTypes = messages.mapNotNull { it.eventType } @@ -331,6 +263,62 @@ internal class IntegrationTest { } } + @Test + fun `court hearing address is inserted when no address lookup is found`() { + wireMockServer.stubFor( + get(urlPathEqualTo("/address-lookup/search/places/v1/find")) + .willReturn( + okJson( + """ + { + "header": { "totalresults": 0 }, + "results": [] + } + """ + ) + ) + ) + val notification = Notification(message = MessageGenerator.COMMON_PLATFORM_EVENT) + channelManager.getChannel(queueName).publishAndWait(notification) + + verify(addressRepository).save(check { + assertThat(it.start, Matchers.equalTo(LocalDate.now())) + assertThat(it.streetName, Matchers.containsString("Example Address Line 1")) + assertThat(it.district, Matchers.containsString("Example Address Line 2")) + assertThat(it.town, Matchers.containsString("Example Address Line 3")) + assertThat(it.postcode, Matchers.containsString("AA1 1AA")) + assertNull(it.endDate) + assertNull(it.notes) + assertThat(it.softDeleted, Matchers.equalTo(false)) + assertThat(it.status.code, Matchers.equalTo(ReferenceData.StandardRefDataCode.ADDRESS_MAIN_STATUS.code)) + assertThat(it.noFixedAbode, Matchers.equalTo(false)) + assertThat(it.type.code, Matchers.equalTo(ReferenceData.StandardRefDataCode.AWAITING_ASSESSMENT.code)) + assertThat(it.typeVerified, Matchers.equalTo(false)) + }) + } + + @Test + fun `Address lookup api is inserted when result is found`() { + val notification = Notification(message = MessageGenerator.COMMON_PLATFORM_EVENT) + channelManager.getChannel(queueName).publishAndWait(notification) + + verify(addressRepository).save(check { + assertThat(it.start, Matchers.equalTo(LocalDate.now())) + assertThat(it.notes, Matchers.containsString("UPRN: 123456789012")) + assertThat(it.postcode, Matchers.containsString("AB1 2CD")) + assertThat(it.streetName, Matchers.containsString("Test Street")) + assertThat(it.addressNumber, Matchers.containsString("123")) + assertThat(it.town, Matchers.containsString("Test")) + assertNull(it.endDate) + assertNotNull(it.notes) + assertThat(it.softDeleted, Matchers.equalTo(false)) + assertThat(it.status.code, Matchers.equalTo(ReferenceData.StandardRefDataCode.ADDRESS_MAIN_STATUS.code)) + assertThat(it.noFixedAbode, Matchers.equalTo(false)) + assertThat(it.type.code, Matchers.equalTo(ReferenceData.StandardRefDataCode.AWAITING_ASSESSMENT.code)) + assertThat(it.typeVerified, Matchers.equalTo(false)) + }) + } + private fun thenNoRecordsAreInserted() { verify(personService, never()).insertAddress(any()) verify(addressRepository, never()).save(any()) @@ -338,4 +326,9 @@ internal class IntegrationTest { verify(auditedInteractionService, Mockito.never()) .createAuditedInteraction(any(), any(), eq(AuditedInteraction.Outcome.SUCCESS), any(), anyOrNull()) } + + @AfterEach + fun resetWireMock() { + wireMockServer.resetAll() + } } \ No newline at end of file diff --git a/projects/common-platform-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/config/RestClientConfig.kt b/projects/common-platform-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/config/RestClientConfig.kt index 9341e1a5a1..0ff40a2d0c 100644 --- a/projects/common-platform-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/config/RestClientConfig.kt +++ b/projects/common-platform-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/config/RestClientConfig.kt @@ -3,9 +3,16 @@ package uk.gov.justice.digital.hmpps.config import org.springframework.beans.factory.annotation.Value import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import org.springframework.http.client.JdkClientHttpRequestFactory +import org.springframework.http.client.support.HttpRequestWrapper import org.springframework.web.client.RestClient +import org.springframework.web.util.UriComponentsBuilder +import uk.gov.justice.digital.hmpps.config.security.RetryInterceptor import uk.gov.justice.digital.hmpps.config.security.createClient +import uk.gov.justice.digital.hmpps.integrations.client.OsClient import uk.gov.justice.digital.hmpps.integrations.client.ProbationSearchClient +import java.net.http.HttpClient +import java.time.Duration @Configuration class RestClientConfig(private val oauth2Client: RestClient) { @@ -13,4 +20,25 @@ class RestClientConfig(private val oauth2Client: RestClient) { @Bean fun probationSearchClient(@Value("\${integrations.probation-search.url}") apiBaseUrl: String): ProbationSearchClient = createClient(oauth2Client.mutate().baseUrl(apiBaseUrl).build()) + + @Bean + fun osPlacesRestClient( + @Value("\${os-places.api.key}") apiKey: String, + @Value("\${os-places.api.url}") baseUrl: String + ): OsClient = createClient( + RestClient.builder() + .requestFactory(withTimeouts(Duration.ofSeconds(1), Duration.ofSeconds(5))) + .requestInterceptor(RetryInterceptor()) + .requestInterceptor { request, body, execution -> + execution.execute(object : HttpRequestWrapper(request) { + override fun getURI() = + UriComponentsBuilder.fromUri(request.uri).queryParam("key", apiKey).build(true).toUri() + }, body) + } + .baseUrl(baseUrl) + .build()) + + fun withTimeouts(connection: Duration, read: Duration) = + JdkClientHttpRequestFactory(HttpClient.newBuilder().connectTimeout(connection).build()) + .also { it.setReadTimeout(read) } } diff --git a/projects/common-platform-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/client/OsClient.kt b/projects/common-platform-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/client/OsClient.kt new file mode 100644 index 0000000000..d3a7583fef --- /dev/null +++ b/projects/common-platform-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/client/OsClient.kt @@ -0,0 +1,146 @@ +package uk.gov.justice.digital.hmpps.integrations.client + +import com.fasterxml.jackson.annotation.JsonProperty +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.service.annotation.GetExchange + +interface OsClient { + @GetExchange("/search/places/v1/find") + fun searchByFreeText( + @RequestParam query: String, + @RequestParam maxResults: Int, + @RequestParam minMatch: Double, + ): OsPlacesResponse +} + +data class OsPlacesResponse( + val header: OsPlacesHeader, + val results: List? +) + +data class OsPlacesHeader( + @JsonProperty("uri") + val uri: String?, + @JsonProperty("query") + val query: String?, + @JsonProperty("offset") + val offset: Int?, + @JsonProperty("totalresults") + val totalResults: Int?, + @JsonProperty("format") + val format: String?, + @JsonProperty("dataset") + val dataset: String?, + @JsonProperty("lr") + val lr: String?, + @JsonProperty("maxresults") + val maxResults: Int?, + @JsonProperty("matchprecision") + val matchPrecision: Double?, + @JsonProperty("filter") + val filter: String?, + @JsonProperty("srs") + val srs: String?, + @JsonProperty("epoch") + val epoch: String?, + @JsonProperty("lastupdate") + val lastUpdate: String?, + @JsonProperty("output_srs") + val outputSrs: String? +) + +class DpaWrapper { + @JsonProperty("DPA") + val dpa: Dpa? = null +} + +data class Dpa( + @JsonProperty("UPRN") + val uprn: Long, + @JsonProperty("UDPRN") + val udprn: Long, + @JsonProperty("ADDRESS") + val address: String, + @JsonProperty("PO_BOX_NUMBER") + val poBoxNumber: String?, + @JsonProperty("ORGANISATION_NAME") + val organisationName: String?, + @JsonProperty("DEPARTMENT_NAME") + val departmentName: String?, + @JsonProperty("SUB_BUILDING_NAME") + val subBuildingName: String?, + @JsonProperty("BUILDING_NAME") + val buildingName: String?, + @JsonProperty("BUILDING_NUMBER") + val buildingNumber: Int?, + @JsonProperty("DEPENDENT_THOROUGHFARE_NAME") + val dependentThoroughfareName: String?, + @JsonProperty("THOROUGHFARE_NAME") + val thoroughfareName: String?, + @JsonProperty("DOUBLE_DEPENDENT_LOCALITY") + val doubleDependentLocality: String?, + @JsonProperty("DEPENDENT_LOCALITY") + val dependentLocality: String?, + @JsonProperty("POST_TOWN") + val postTown: String, + @JsonProperty("POSTCODE") + val postcode: String, + @JsonProperty("RPC") + val rpc: String, + @JsonProperty("X_COORDINATE") + val xCoordinate: Double, + @JsonProperty("Y_COORDINATE") + val yCoordinate: Double, + @JsonProperty("LNG") + val lng: Double?, + @JsonProperty("LAT") + val lat: Double?, + @JsonProperty("STATUS") + val status: String, + @JsonProperty("MATCH") + val match: Float?, + @JsonProperty("MATCH_DESCRIPTION") + val matchDescription: String?, + @JsonProperty("LANGUAGE") + val language: String, + @JsonProperty("COUNTRY_CODE") + val countryCode: String, + @JsonProperty("COUNTRY_CODE_DESCRIPTION") + val countryCodeDescription: String, + @JsonProperty("LOCAL_CUSTODIAN_CODE") + val localCustodianCode: Int, + @JsonProperty("LOCAL_CUSTODIAN_CODE_DESCRIPTION") + val localCustodianCodeDescription: String, + @JsonProperty("CLASSIFICATION_CODE") + val classificationCode: String, + @JsonProperty("CLASSIFICATION_CODE_DESCRIPTION") + val classificationCodeDescription: String, + @JsonProperty("POSTAL_ADDRESS_CODE") + val postalAddressCode: String, + @JsonProperty("POSTAL_ADDRESS_CODE_DESCRIPTION") + val postalAddressCodeDescription: String, + @JsonProperty("LOGICAL_STATUS_CODE") + val logicalStatusCode: Int, + @JsonProperty("BLPU_STATE_CODE") + val blpuStateCode: Int, + @JsonProperty("BLPU_STATE_CODE_DESCRIPTION") + val blpuStateCodeDescription: String?, + @JsonProperty("TOPOGRAPHY_LAYER_TOID") + val topographyLayerToid: String, + @JsonProperty("PARENT_UPRN") + val parentUprn: Long?, + @JsonProperty("LAST_UPDATE_DATE") + val lastUpdateDate: String, + @JsonProperty("ENTRY_DATE") + val entryDate: String, + @JsonProperty("LEGAL_NAME") + val legalName: String?, + @JsonProperty("BLPU_STATE_DATE") + val blpuStateDate: String?, + @JsonProperty("DELIVERY_POINT_SUFFIX") + val deliveryPointSuffix: String, + @JsonProperty("PARISH_CODE") + val parishCode: String, + @JsonProperty("WARD_CODE") + val wardCode: String +) \ No newline at end of file diff --git a/projects/common-platform-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/service/PersonService.kt b/projects/common-platform-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/service/PersonService.kt index c28a0aa9a8..0fd616c676 100644 --- a/projects/common-platform-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/service/PersonService.kt +++ b/projects/common-platform-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/service/PersonService.kt @@ -4,6 +4,8 @@ import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import uk.gov.justice.digital.hmpps.audit.service.AuditableService import uk.gov.justice.digital.hmpps.audit.service.AuditedInteractionService +import uk.gov.justice.digital.hmpps.integrations.client.OsClient +import uk.gov.justice.digital.hmpps.integrations.client.OsPlacesResponse import uk.gov.justice.digital.hmpps.integrations.delius.audit.BusinessInteractionCode import uk.gov.justice.digital.hmpps.integrations.delius.entity.* import uk.gov.justice.digital.hmpps.integrations.delius.person.entity.PersonAddress @@ -24,7 +26,8 @@ class PersonService( private val teamRepository: TeamRepository, private val staffRepository: StaffRepository, private val referenceDataRepository: ReferenceDataRepository, - private val personAddressRepository: PersonAddressRepository + private val personAddressRepository: PersonAddressRepository, + private val osClient: OsClient ) : AuditableService(auditedInteractionService) { @Transactional @@ -75,20 +78,51 @@ class PersonService( val savedEquality = equalityRepository.save(equality) - val savedAddress = - defendant.personDefendant.personDetails.address.takeIf { it.containsInformation() }?.let { + val addressInfo = defendant.personDefendant.personDetails.address + val osPlacesResponse = addressInfo?.takeIf { it.containsInformation() && !it.postcode.isNullOrBlank() } + ?.let { findAddressByFreeText(it) } + + val deliveryPointAddress = osPlacesResponse?.results?.firstOrNull()?.dpa + + val savedAddress = if (deliveryPointAddress != null) { + insertAddress( + PersonAddress( + id = null, + start = LocalDate.now(), + status = referenceDataRepository.mainAddressStatus(), + person = savedPerson, + type = referenceDataRepository.awaitingAssessmentAddressType(), + postcode = deliveryPointAddress.postcode, + notes = "UPRN: ${deliveryPointAddress.uprn}", + buildingName = listOfNotNull( + deliveryPointAddress.subBuildingName, + deliveryPointAddress.buildingName + ).joinToString(" "), + addressNumber = deliveryPointAddress.buildingNumber?.toString(), + streetName = deliveryPointAddress.thoroughfareName, + town = deliveryPointAddress.postTown, + district = deliveryPointAddress.localCustodianCodeDescription + ) + ) + } else { + addressInfo?.takeIf { it.containsInformation() }?.let { insertAddress( PersonAddress( id = null, start = LocalDate.now(), status = referenceDataRepository.mainAddressStatus(), person = savedPerson, - notes = it.buildNotes(), postcode = it.postcode, - type = referenceDataRepository.awaitingAssessmentAddressType() + type = referenceDataRepository.awaitingAssessmentAddressType(), + streetName = it.address1, + district = it.address2, + town = it.address3, + county = listOfNotNull(it.address4, it.address5).joinToString(", ") ) ) } + } + audit["offenderId"] = savedPerson.id InsertPersonResult(savedPerson, savedManager, savedEquality, savedAddress) } @@ -137,14 +171,19 @@ class PersonService( ).any { !it.isNullOrBlank() } } - fun Address.buildNotes(): String { - return listOf( - "Address record automatically created by common-platform-delius-service with the following information:", - "Address1: ${this.address1 ?: "N/A"}", - "Address2: ${this.address2 ?: "N/A"}", - "Address3: ${this.address3 ?: "N/A"}", - "Address4: ${this.address4 ?: "N/A"}", - "Postcode: ${this.postcode ?: "N/A"}" - ).joinToString("\n") + fun findAddressByFreeText(address: Address): OsPlacesResponse { + val freeText = address.toFreeText() + return osClient.searchByFreeText(query = freeText, maxResults = 1, minMatch = 0.6) + } + + fun Address.toFreeText(): String { + return listOfNotNull( + this.address1?.trim(), + this.address2?.trim(), + this.address3?.trim(), + this.address4?.trim(), + this.address5?.trim(), + this.postcode?.trim() + ).joinToString(", ") } } \ No newline at end of file diff --git a/projects/common-platform-and-delius/src/main/resources/application.yml b/projects/common-platform-and-delius/src/main/resources/application.yml index da687f9b77..7dcaf4a748 100644 --- a/projects/common-platform-and-delius/src/main/resources/application.yml +++ b/projects/common-platform-and-delius/src/main/resources/application.yml @@ -57,6 +57,11 @@ messaging.consumer.queue: message-queue integrations: probation-search.url: http://localhost:${wiremock.port}/probation-search +os-places: + api: + url: http://localhost:${wiremock.port}/address-lookup + key: key + oauth2: client-id: common-platform-and-delius client-secret: common-platform-and-delius diff --git a/projects/feature-flags/container/Dockerfile b/projects/feature-flags/container/Dockerfile index 0a9f16f10f..6563f30be4 100644 --- a/projects/feature-flags/container/Dockerfile +++ b/projects/feature-flags/container/Dockerfile @@ -1,4 +1,4 @@ -FROM ghcr.io/flipt-io/flipt:v1.52.0 +FROM ghcr.io/flipt-io/flipt:v1.53.0 # Run any pending migrations on startup CMD ["sh", "-c", "./flipt migrate && ./flipt"] \ No newline at end of file diff --git a/projects/manage-supervision-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/DataLoader.kt b/projects/manage-supervision-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/DataLoader.kt index 7da807bc5b..cebec85ff1 100644 --- a/projects/manage-supervision-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/DataLoader.kt +++ b/projects/manage-supervision-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/DataLoader.kt @@ -10,6 +10,7 @@ import org.springframework.transaction.annotation.Transactional import uk.gov.justice.digital.hmpps.audit.BusinessInteraction import uk.gov.justice.digital.hmpps.data.generator.* import uk.gov.justice.digital.hmpps.data.generator.CourtAppearanceGenerator.COURT_APPEARANCE +import uk.gov.justice.digital.hmpps.data.generator.PersonGenerator.GENDER_MALE import uk.gov.justice.digital.hmpps.data.generator.personalDetails.PersonDetailsGenerator import uk.gov.justice.digital.hmpps.integrations.delius.audit.BusinessInteractionCode import uk.gov.justice.digital.hmpps.user.AuditUserRepository @@ -41,6 +42,7 @@ class DataLoader( ContactGenerator.DEFAULT_BOROUGH, ContactGenerator.DEFAULT_DISTRICT, ContactGenerator.DEFAULT_STAFF, + ContactGenerator.LIMITED_ACCESS_STAFF, ContactGenerator.STAFF_1, ContactGenerator.DEFAULT_TEAM, ContactGenerator.LOCATION_BRK_1, @@ -49,7 +51,9 @@ class DataLoader( entityManager.persist(ContactGenerator.USER_1) - entityManager.persist(PersonGenerator.OVERVIEW.gender) + entityManager.persist(ContactGenerator.LIMITED_ACCESS_USER) + + entityManager.persist(GENDER_MALE) PersonGenerator.DISABILITIES.forEach { entityManager.persist(it.type) } PersonGenerator.PROVISIONS.forEach { entityManager.persist(it.type) } @@ -62,7 +66,6 @@ class DataLoader( entityManager.persistCollection(PersonGenerator.PROVISIONS) entityManager.persistCollection(PersonGenerator.PERSONAL_CIRCUMSTANCES) entityManager.persist(PersonGenerator.OVERVIEW) - entityManager.persist(CourtGenerator.BHAM) entityManager.persist(PersonGenerator.EVENT_1) entityManager.persist(PersonGenerator.EVENT_2) @@ -188,6 +191,9 @@ class DataLoader( PersonDetailsGenerator.LANGUAGE_RD, PersonDetailsGenerator.GENDER_IDENTITY_RD, PersonDetailsGenerator.PERSONAL_DETAILS, + PersonDetailsGenerator.RESTRICTION, + PersonDetailsGenerator.EXCLUSION, + PersonDetailsGenerator.RESTRICTION_EXCLUSION, PersonDetailsGenerator.DISABILITY_1_RD, PersonDetailsGenerator.DISABILITY_2_RD, PersonDetailsGenerator.PERSONAL_CIRCUMSTANCE_1_RD, @@ -222,10 +228,23 @@ class DataLoader( entityManager.flush() entityManager.merge(PersonGenerator.PERSON_1) entityManager.merge(PersonGenerator.PERSON_2) + entityManager.merge(PersonGenerator.CL_EXCLUDED) + entityManager.merge(PersonGenerator.CL_RESTRICTED) + entityManager.merge(PersonGenerator.CL_RESTRICTED_EXCLUDED) entityManager.flush() entityManager.persist(PersonGenerator.CASELOAD_PERSON_1) entityManager.persist(PersonGenerator.CASELOAD_PERSON_2) entityManager.persist(PersonGenerator.CASELOAD_PERSON_3) + + entityManager.persist(PersonGenerator.CASELOAD_LIMITED_ACCESS_EXCLUSION) + entityManager.persist(PersonGenerator.CASELOAD_LIMITED_ACCESS_RESTRICTION) + entityManager.persist(PersonGenerator.CASELOAD_LIMITED_ACCESS_BOTH) + entityManager.persist(PersonGenerator.CASELOAD_LIMITED_ACCESS_NEITHER) + + entityManager.persist(LimitedAccessGenerator.EXCLUSION) + entityManager.persist(LimitedAccessGenerator.RESTRICTION) + entityManager.persist(LimitedAccessGenerator.BOTH_EXCLUSION) + entityManager.persist(LimitedAccessGenerator.BOTH_RESTRICTION) } private fun EntityManager.persistAll(vararg entities: Any) { diff --git a/projects/manage-supervision-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/ContactGenerator.kt b/projects/manage-supervision-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/ContactGenerator.kt index 44bc3978a9..7e5db3feb1 100644 --- a/projects/manage-supervision-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/ContactGenerator.kt +++ b/projects/manage-supervision-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/ContactGenerator.kt @@ -30,6 +30,7 @@ object ContactGenerator { val DEFAULT_STAFF = generateStaff("N01BDT1", "John", "Smith", emptyList()) val STAFF_1 = generateStaff("N01BDT2", "Jim", "Brown", emptyList()) + val LIMITED_ACCESS_STAFF = generateStaff("N01BDT3", "Limited", "Access", emptyList()) val DEFAULT_TEAM = generateTeam(code = "TEAM11", description = "Main Team", staff = listOf(DEFAULT_STAFF, STAFF_1)) @@ -48,6 +49,14 @@ object ContactGenerator { username = "JimBrown" ) + val LIMITED_ACCESS_USER = User( + id = IdGenerator.getAndIncrement(), + forename = "Limited", + surname = "Access", + staff = LIMITED_ACCESS_STAFF, + username = "LimitedAccess" + ) + val COMMUNICATION_CATEGORY_RD = ReferenceData(IdGenerator.getAndIncrement(), "LT", "Communication") val BREACH_CONTACT_TYPE = generateContactType("BRE02", false, "Breach Contact Type") diff --git a/projects/manage-supervision-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/LimitedAccessGenerator.kt b/projects/manage-supervision-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/LimitedAccessGenerator.kt new file mode 100644 index 0000000000..85e0630b52 --- /dev/null +++ b/projects/manage-supervision-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/LimitedAccessGenerator.kt @@ -0,0 +1,38 @@ +package uk.gov.justice.digital.hmpps.data.generator + +import uk.gov.justice.digital.hmpps.data.generator.personalDetails.PersonDetailsGenerator +import uk.gov.justice.digital.hmpps.entity.Exclusion +import uk.gov.justice.digital.hmpps.entity.LimitedAccessPerson +import uk.gov.justice.digital.hmpps.entity.LimitedAccessUser +import uk.gov.justice.digital.hmpps.entity.Restriction +import uk.gov.justice.digital.hmpps.integrations.delius.overview.entity.Person +import uk.gov.justice.digital.hmpps.integrations.delius.user.entity.User +import java.time.LocalDateTime + +object LimitedAccessGenerator { + val EXCLUSION = generateExclusion(PersonDetailsGenerator.EXCLUSION) + val RESTRICTION = + generateRestriction(PersonDetailsGenerator.RESTRICTION, endDateTime = LocalDateTime.now().plusDays(1)) + val BOTH_EXCLUSION = generateExclusion(PersonDetailsGenerator.RESTRICTION_EXCLUSION) + val BOTH_RESTRICTION = generateRestriction( + PersonDetailsGenerator.RESTRICTION_EXCLUSION, + endDateTime = LocalDateTime.now().plusDays(1) + ) + + fun generateExclusion( + person: Person, + user: User = ContactGenerator.LIMITED_ACCESS_USER, + endDateTime: LocalDateTime? = null, + id: Long = IdGenerator.getAndIncrement() + ) = Exclusion(person.limitedAccess(), user.limitedAccess(), endDateTime, id) + + fun generateRestriction( + person: Person, + user: User = ContactGenerator.USER_1, + endDateTime: LocalDateTime? = null, + id: Long = IdGenerator.getAndIncrement() + ) = Restriction(person.limitedAccess(), user.limitedAccess(), endDateTime, id) + + private fun Person.limitedAccess() = LimitedAccessPerson(crn, exclusionMessage, restrictionMessage, id) + private fun User.limitedAccess() = LimitedAccessUser(username, id) +} diff --git a/projects/manage-supervision-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/PersonGenerator.kt b/projects/manage-supervision-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/PersonGenerator.kt index bd15c59bd5..c0d760977d 100644 --- a/projects/manage-supervision-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/PersonGenerator.kt +++ b/projects/manage-supervision-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/PersonGenerator.kt @@ -1,6 +1,7 @@ package uk.gov.justice.digital.hmpps.data.generator import uk.gov.justice.digital.hmpps.data.generator.ContactGenerator.DEFAULT_STAFF +import uk.gov.justice.digital.hmpps.data.generator.ContactGenerator.LIMITED_ACCESS_STAFF import uk.gov.justice.digital.hmpps.data.generator.ContactGenerator.USER import uk.gov.justice.digital.hmpps.data.generator.personalDetails.PersonDetailsGenerator import uk.gov.justice.digital.hmpps.integrations.delius.compliance.Nsi @@ -23,6 +24,7 @@ import java.time.ZonedDateTime object PersonGenerator { + val GENDER_MALE = ReferenceData(IdGenerator.getAndIncrement(), "M", "Male") val OVERVIEW = generateOverview("X000004") val EVENT_1 = generateEvent( OVERVIEW, @@ -185,10 +187,43 @@ object PersonGenerator { PersonDetailsGenerator.PERSONAL_DETAILS.surname ) + val CL_EXCLUDED = + generateCaseloadPerson( + PersonDetailsGenerator.EXCLUSION.id, + PersonDetailsGenerator.EXCLUSION.crn, + PersonDetailsGenerator.EXCLUSION.forename, + PersonDetailsGenerator.EXCLUSION.secondName, + PersonDetailsGenerator.EXCLUSION.surname + ) + val CL_RESTRICTED = generateCaseloadPerson( + PersonDetailsGenerator.RESTRICTION.id, + PersonDetailsGenerator.RESTRICTION.crn, + PersonDetailsGenerator.RESTRICTION.forename, + PersonDetailsGenerator.RESTRICTION.secondName, + PersonDetailsGenerator.RESTRICTION.surname + ) + + val CL_RESTRICTED_EXCLUDED = generateCaseloadPerson( + PersonDetailsGenerator.RESTRICTION_EXCLUSION.id, + PersonDetailsGenerator.RESTRICTION_EXCLUSION.crn, + PersonDetailsGenerator.RESTRICTION_EXCLUSION.forename, + PersonDetailsGenerator.RESTRICTION_EXCLUSION.secondName, + PersonDetailsGenerator.RESTRICTION_EXCLUSION.surname + ) + val CASELOAD_PERSON_1 = generateCaseload(PERSON_1, DEFAULT_STAFF, ContactGenerator.DEFAULT_TEAM) val CASELOAD_PERSON_2 = generateCaseload(PERSON_2, ContactGenerator.STAFF_1, ContactGenerator.DEFAULT_TEAM) val CASELOAD_PERSON_3 = generateCaseload(PERSON_2, DEFAULT_STAFF, ContactGenerator.DEFAULT_TEAM) + val CASELOAD_LIMITED_ACCESS_EXCLUSION = + generateCaseload(CL_EXCLUDED, LIMITED_ACCESS_STAFF, ContactGenerator.DEFAULT_TEAM) + val CASELOAD_LIMITED_ACCESS_RESTRICTION = + generateCaseload(CL_RESTRICTED, LIMITED_ACCESS_STAFF, ContactGenerator.DEFAULT_TEAM) + val CASELOAD_LIMITED_ACCESS_BOTH = + generateCaseload(CL_RESTRICTED_EXCLUDED, LIMITED_ACCESS_STAFF, ContactGenerator.DEFAULT_TEAM) + val CASELOAD_LIMITED_ACCESS_NEITHER = + generateCaseload(PERSON_2, LIMITED_ACCESS_STAFF, ContactGenerator.DEFAULT_TEAM) + fun generateEvent( person: Person, id: Long = IdGenerator.getAndIncrement(), @@ -295,8 +330,10 @@ object PersonGenerator { telephoneNumber: String? = "4321", preferredName: String? = "Dee", dateOfBirth: LocalDate = LocalDate.now().minusYears(50), - gender: ReferenceData = ReferenceData(IdGenerator.getAndIncrement(), "M", "Male"), - id: Long = IdGenerator.getAndIncrement() + gender: ReferenceData = GENDER_MALE, + id: Long = IdGenerator.getAndIncrement(), + exclusionMessage: String? = null, + restrictionMessage: String? = null ) = Person( id = id, crn = crn, @@ -314,7 +351,9 @@ object PersonGenerator { religion = null, sexualOrientation = null, genderIdentity = null, - genderIdentityDescription = null + genderIdentityDescription = null, + exclusionMessage = exclusionMessage, + restrictionMessage = restrictionMessage ) fun generateRequirement( diff --git a/projects/manage-supervision-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/personalDetails/PersonDetailsGenerator.kt b/projects/manage-supervision-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/personalDetails/PersonDetailsGenerator.kt index 8194095ab6..4107ac8f1a 100644 --- a/projects/manage-supervision-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/personalDetails/PersonDetailsGenerator.kt +++ b/projects/manage-supervision-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/personalDetails/PersonDetailsGenerator.kt @@ -35,6 +35,21 @@ object PersonDetailsGenerator { requiresInterpreter = true ) + val EXCLUSION = generatePersonDetails( + "E123456", + exclusionMessage = "There is an exclusion on this person" + ) + val RESTRICTION = + generatePersonDetails( + "R123456", + restrictionMessage = "There is a restriction on this person" + ) + val RESTRICTION_EXCLUSION = generatePersonDetails( + "B123456", + exclusionMessage = "You are excluded from viewing this case", + restrictionMessage = "You are restricted from viewing this case" + ) + val ALIAS_1 = generateAlias("Sam", "Edward", "Smith", PERSONAL_DETAILS.id) val ALIAS_2 = generateAlias("Joe", "Richard", "Jones", PersonGenerator.OVERVIEW.id) @@ -236,6 +251,7 @@ object PersonDetailsGenerator { endDate: LocalDate? = null, verified: Boolean? = null, notes: String? = null + ) = PersonAddress( personId = personId, id = IdGenerator.getAndIncrement(), @@ -255,23 +271,25 @@ object PersonDetailsGenerator { typeVerified = verified, telephoneNumber = "0191876865", lastUpdatedUser = USER, - notes = notes + notes = notes, ) fun generatePersonDetails( crn: String, - forename: String, - secondName: String, - surname: String, - preferredName: String, - gender: ReferenceData, - religion: ReferenceData, - sexualOrientation: ReferenceData, - language: ReferenceData, - previousSurname: String, - genderIdentity: ReferenceData, - genderIdentityDescription: String, - requiresInterpreter: Boolean = false + forename: String = "TestForename", + secondName: String = "SecondName", + surname: String = "Surname", + preferredName: String = "PreferredName", + gender: ReferenceData = GENDER_FEMALE, + religion: ReferenceData = RELIGION_DEFAULT, + sexualOrientation: ReferenceData = SEXUAL_ORIENTATION, + language: ReferenceData = LANGUAGE_RD, + previousSurname: String = "PreviousSurname", + genderIdentity: ReferenceData = GENDER_IDENTITY_RD, + genderIdentityDescription: String = "genderIdentityDescription", + requiresInterpreter: Boolean = false, + exclusionMessage: String? = null, + restrictionMessage: String? = null ) = Person( id = IdGenerator.getAndIncrement(), crn = crn, @@ -292,6 +310,8 @@ object PersonDetailsGenerator { genderIdentity = genderIdentity, genderIdentityDescription = genderIdentityDescription, requiresInterpreter = requiresInterpreter, + exclusionMessage = exclusionMessage, + restrictionMessage = restrictionMessage ) fun generateDocument( diff --git a/projects/manage-supervision-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/AppointmentOutcomeIntegrationTest.kt b/projects/manage-supervision-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/AppointmentOutcomeIntegrationTest.kt index 91bcdb0ac1..ebb7122973 100644 --- a/projects/manage-supervision-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/AppointmentOutcomeIntegrationTest.kt +++ b/projects/manage-supervision-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/AppointmentOutcomeIntegrationTest.kt @@ -16,6 +16,7 @@ import uk.gov.justice.digital.hmpps.api.model.appointment.AppointmentDetail import uk.gov.justice.digital.hmpps.api.model.appointment.CreateAppointment import uk.gov.justice.digital.hmpps.api.model.appointment.Outcome import uk.gov.justice.digital.hmpps.api.model.appointment.User +import uk.gov.justice.digital.hmpps.data.generator.AppointmentGenerator.APPOINTMENT_TYPES import uk.gov.justice.digital.hmpps.data.generator.AppointmentGenerator.ATTENDED_COMPLIED import uk.gov.justice.digital.hmpps.data.generator.OffenderManagerGenerator.STAFF_USER_1 import uk.gov.justice.digital.hmpps.data.generator.OffenderManagerGenerator.TEAM @@ -66,6 +67,9 @@ class AppointmentOutcomeIntegrationTest { fun `when an appointment outcome does not exist returns a 404 response`() { val response = createAppointment() + val expectedContactType = + APPOINTMENT_TYPES.firstOrNull { type -> type.code == CreateAppointment.Type.PlannedOfficeVisitNS.code } + mockMvc .perform( MockMvcRequestBuilders.patch("/appointment") @@ -76,7 +80,7 @@ class AppointmentOutcomeIntegrationTest { .andExpect( jsonPath( "$.message", - equalTo("ContactTypeOutcome with contact_type_id 8 and outcome code of ABC not found") + equalTo("ContactTypeOutcome with contact_type_id ${expectedContactType?.id} and outcome code of ABC not found") ) ) diff --git a/projects/manage-supervision-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/ComplianceIntegrationTest.kt b/projects/manage-supervision-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/ComplianceIntegrationTest.kt index 2f47d6831f..28981688d5 100644 --- a/projects/manage-supervision-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/ComplianceIntegrationTest.kt +++ b/projects/manage-supervision-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/ComplianceIntegrationTest.kt @@ -34,17 +34,17 @@ internal class ComplianceIntegrationTest { res.previousOrders.breaches, equalTo(2) ) - assertThat(res.currentSentences[0].rarCategory, equalTo("Main")) - assertThat(res.currentSentences[1].rarCategory, equalTo(null)) - assertThat(res.currentSentences[0].eventNumber, equalTo("7654321")) - assertThat(res.currentSentences[1].eventNumber, equalTo("1234567")) - assertThat(res.currentSentences[0].activeBreach?.status, equalTo("An NSI Status")) - assertThat(res.currentSentences[0].compliance.breachStarted, equalTo(true)) - assertThat(res.currentSentences[0].compliance.currentBreaches, equalTo(1)) - assertThat(res.currentSentences[0].activity.waitingForEvidenceCount, equalTo(0)) - assertThat(res.currentSentences[0].activity.compliedAppointmentsCount, equalTo(2)) - assertThat(res.currentSentences[0].activity.outcomeNotRecordedCount, equalTo(2)) - assertThat(res.currentSentences[0].activity.acceptableAbsenceCount, equalTo(0)) + assertThat(res.currentSentences[1].rarCategory, equalTo("Main")) + assertThat(res.currentSentences[0].rarCategory, equalTo(null)) + assertThat(res.currentSentences[1].eventNumber, equalTo("7654321")) + assertThat(res.currentSentences[0].eventNumber, equalTo("1234567")) + assertThat(res.currentSentences[1].activeBreach?.status, equalTo("An NSI Status")) + assertThat(res.currentSentences[1].compliance.breachStarted, equalTo(true)) + assertThat(res.currentSentences[1].compliance.currentBreaches, equalTo(1)) + assertThat(res.currentSentences[1].activity.waitingForEvidenceCount, equalTo(0)) + assertThat(res.currentSentences[1].activity.compliedAppointmentsCount, equalTo(2)) + assertThat(res.currentSentences[1].activity.outcomeNotRecordedCount, equalTo(2)) + assertThat(res.currentSentences[1].activity.acceptableAbsenceCount, equalTo(0)) assertThat(res.previousOrders.orders[1].status, equalTo(TERMINATION_REASON.description)) } diff --git a/projects/manage-supervision-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/LaoCaseloadIntegrationTest.kt b/projects/manage-supervision-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/LaoCaseloadIntegrationTest.kt new file mode 100644 index 0000000000..019035815d --- /dev/null +++ b/projects/manage-supervision-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/LaoCaseloadIntegrationTest.kt @@ -0,0 +1,109 @@ +package uk.gov.justice.digital.hmpps + +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.junit.jupiter.api.Assertions.assertNotEquals +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status +import uk.gov.justice.digital.hmpps.api.model.user.StaffCaseload +import uk.gov.justice.digital.hmpps.data.generator.ContactGenerator.LIMITED_ACCESS_USER +import uk.gov.justice.digital.hmpps.data.generator.personalDetails.PersonDetailsGenerator.EXCLUSION +import uk.gov.justice.digital.hmpps.data.generator.personalDetails.PersonDetailsGenerator.PERSONAL_DETAILS +import uk.gov.justice.digital.hmpps.data.generator.personalDetails.PersonDetailsGenerator.RESTRICTION +import uk.gov.justice.digital.hmpps.data.generator.personalDetails.PersonDetailsGenerator.RESTRICTION_EXCLUSION +import uk.gov.justice.digital.hmpps.service.UserAccess +import uk.gov.justice.digital.hmpps.test.MockMvcExtensions.contentAsJson +import uk.gov.justice.digital.hmpps.test.MockMvcExtensions.withJson +import uk.gov.justice.digital.hmpps.test.MockMvcExtensions.withToken + +@AutoConfigureMockMvc +@SpringBootTest(webEnvironment = RANDOM_PORT) +internal class LaoCaseloadIntegrationTest { + @Autowired + lateinit var mockMvc: MockMvc + + @Test + fun `all caseload activity for an lao user`() { + val person = LIMITED_ACCESS_USER + val res = mockMvc + .perform(get("/caseload/user/${person.username}").withToken()) + .andExpect(status().isOk) + .andReturn().response.contentAsJson() + + val caseload = res.caseload.sortedBy { it.crn } + + assertThat(caseload[0].crn, equalTo(RESTRICTION_EXCLUSION.crn)) + assertThat(caseload[1].crn, equalTo(EXCLUSION.crn)) + assertThat(caseload[2].crn, equalTo(RESTRICTION.crn)) + + + assertThat(caseload[0].limitedAccess, equalTo(true)) + assertThat(caseload[0].caseName, equalTo(null)) + assertThat(caseload[0].latestSentence, equalTo(null)) + assertThat(caseload[0].nextAppointment, equalTo(null)) + assertThat(caseload[0].previousAppointment, equalTo(null)) + assertThat(caseload[0].numberOfAdditionalSentences, equalTo(null)) + + assertThat(caseload[1].limitedAccess, equalTo(true)) + assertThat(caseload[1].caseName, equalTo(null)) + assertThat(caseload[1].latestSentence, equalTo(null)) + assertThat(caseload[1].nextAppointment, equalTo(null)) + assertThat(caseload[1].previousAppointment, equalTo(null)) + assertThat(caseload[1].numberOfAdditionalSentences, equalTo(null)) + + assertThat(caseload[2].limitedAccess, equalTo(true)) + assertThat(caseload[2].caseName, equalTo(null)) + assertThat(caseload[2].latestSentence, equalTo(null)) + assertThat(caseload[2].nextAppointment, equalTo(null)) + assertThat(caseload[2].previousAppointment, equalTo(null)) + assertThat(caseload[2].numberOfAdditionalSentences, equalTo(null)) + + assertThat(caseload[3].limitedAccess, equalTo(false)) + assertNotEquals(caseload[3].caseName, null) + } + + @Test + fun `check lao access for a user with list of crns`() { + val person = LIMITED_ACCESS_USER + val crns = listOf(RESTRICTION_EXCLUSION.crn, EXCLUSION.crn, RESTRICTION.crn, PERSONAL_DETAILS.crn) + val res = mockMvc + .perform( + MockMvcRequestBuilders.post("/user/${person.username}/access").withToken() + .withJson(crns) + ) + .andExpect(status().isOk) + .andReturn().response.contentAsJson() + + val userAccess = res.access.sortedBy { it.crn } + + assertThat(userAccess[0].userExcluded, equalTo(true)) + assertThat(userAccess[0].userRestricted, equalTo(true)) + + assertThat(userAccess[1].userExcluded, equalTo(true)) + assertThat(userAccess[1].userRestricted, equalTo(false)) + + assertThat(userAccess[2].userExcluded, equalTo(false)) + assertThat(userAccess[2].userRestricted, equalTo(true)) + + assertThat(userAccess[3].userExcluded, equalTo(false)) + assertThat(userAccess[3].userRestricted, equalTo(false)) + } + + @Test + fun `check lao access returns 400 when no crns are provided`() { + val person = LIMITED_ACCESS_USER + mockMvc + .perform( + MockMvcRequestBuilders.post("/user/${person.username}/access").withToken() + .withJson(emptyList()) + ) + .andExpect(status().isBadRequest) + } +} diff --git a/projects/manage-supervision-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/OverviewIntegrationTest.kt b/projects/manage-supervision-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/OverviewIntegrationTest.kt index 8abf7ed4a4..76068d88f0 100644 --- a/projects/manage-supervision-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/OverviewIntegrationTest.kt +++ b/projects/manage-supervision-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/OverviewIntegrationTest.kt @@ -66,21 +66,21 @@ internal class OverviewIntegrationTest { equalTo(ContactGenerator.FIRST_APPT_CONTACT.type.description) ) assertThat(res.sentences.size, equalTo(2)) - assertThat(res.sentences[0].mainOffence.description, equalTo(MAIN_OFFENCE_1.offence.description)) + assertThat(res.sentences[1].mainOffence.description, equalTo(MAIN_OFFENCE_1.offence.description)) assertThat( - res.sentences[0].additionalOffences[0].description, + res.sentences[1].additionalOffences[0].description, equalTo(ADDITIONAL_OFFENCE_1.offence.description) ) assertThat( - res.sentences[0].additionalOffences[1].description, + res.sentences[1].additionalOffences[1].description, equalTo(ADDITIONAL_OFFENCE_2.offence.description) ) assertThat(res.previousOrders.count, equalTo(2)) assertThat(res.previousOrders.breaches, equalTo(2)) - assertThat(res.sentences[0].rar?.completed, equalTo(1)) - assertThat(res.sentences[0].rar?.scheduled, equalTo(1)) - assertThat(res.sentences[0].rar?.totalDays, equalTo(2)) - assertThat(res.sentences[0].eventNumber, equalTo(EVENT_1.eventNumber)) + assertThat(res.sentences[1].rar?.completed, equalTo(1)) + assertThat(res.sentences[1].rar?.scheduled, equalTo(1)) + assertThat(res.sentences[1].rar?.totalDays, equalTo(2)) + assertThat(res.sentences[1].eventNumber, equalTo(EVENT_1.eventNumber)) assertThat(res.personalDetails.dateOfBirth, equalTo(OVERVIEW.dateOfBirth)) assertThat(res.personalDetails.dateOfBirth, equalTo(OVERVIEW.dateOfBirth)) assertThat(res.registrations, equalTo(listOf("Restraining Order", "Domestic Abuse Perpetrator"))) diff --git a/projects/manage-supervision-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/UserIntegrationTest.kt b/projects/manage-supervision-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/UserIntegrationTest.kt index ce23327ab7..e873292089 100644 --- a/projects/manage-supervision-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/UserIntegrationTest.kt +++ b/projects/manage-supervision-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/UserIntegrationTest.kt @@ -256,7 +256,7 @@ internal class UserIntegrationTest { assertThat(res.caseload.size, equalTo(2)) assertThat(res.caseload[0].crn, equalTo("X000005")) - assertThat(res.caseload[0].caseName.surname, equalTo("Bloggs")) + assertThat(res.caseload[0].caseName?.surname, equalTo("Bloggs")) } @Test @@ -273,7 +273,7 @@ internal class UserIntegrationTest { assertThat(res.caseload.size, equalTo(2)) assertThat(res.caseload[0].crn, equalTo("X000004")) - assertThat(res.caseload[0].caseName.surname, equalTo("Surname")) + assertThat(res.caseload[0].caseName?.surname, equalTo("Surname")) assertThat(res.metaData?.contactTypes?.size, equalTo(1)) assertThat(res.metaData?.sentenceTypes?.size, equalTo(1)) } diff --git a/projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/controller/UserAccessController.kt b/projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/controller/UserAccessController.kt index 3978f62ff5..707c27de01 100644 --- a/projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/controller/UserAccessController.kt +++ b/projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/controller/UserAccessController.kt @@ -1,10 +1,9 @@ package uk.gov.justice.digital.hmpps.api.controller import io.swagger.v3.oas.annotations.tags.Tag +import jakarta.validation.constraints.Size import org.springframework.security.access.prepost.PreAuthorize -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PathVariable -import org.springframework.web.bind.annotation.RestController +import org.springframework.web.bind.annotation.* import uk.gov.justice.digital.hmpps.service.UserAccessService @RestController @@ -14,4 +13,11 @@ class UserAccessController(private val userAccessService: UserAccessService) { @GetMapping("/user/{username}/access/{crn}") fun checkAccess(@PathVariable username: String, @PathVariable crn: String) = userAccessService.caseAccessFor(username, crn) + + @PostMapping("/user/{username}/access") + fun checkUserAccess( + @PathVariable username: String, + @Size(min = 1, max = 500, message = "Please provide between 1 and 500 crns") + @RequestBody crns: List + ) = userAccessService.userAccessFor(username, crns) } \ No newline at end of file diff --git a/projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/model/user/StaffCase.kt b/projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/model/user/StaffCase.kt index 309bb162e8..5a3d4b47b8 100644 --- a/projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/model/user/StaffCase.kt +++ b/projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/api/model/user/StaffCase.kt @@ -5,11 +5,12 @@ import uk.gov.justice.digital.hmpps.api.model.overview.Appointment import java.time.LocalDate data class StaffCase( - val caseName: Name, + val caseName: Name? = null, val crn: String, - val dob: LocalDate, + val dob: LocalDate? = null, val nextAppointment: Appointment? = null, val previousAppointment: Appointment? = null, val latestSentence: String? = null, - val numberOfAdditionalSentences: Long + val numberOfAdditionalSentences: Long? = null, + val limitedAccess: Boolean? = false ) diff --git a/projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/overview/entity/Event.kt b/projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/overview/entity/Event.kt index 29219dec8a..b0322567e7 100644 --- a/projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/overview/entity/Event.kt +++ b/projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/overview/entity/Event.kt @@ -72,7 +72,7 @@ interface EventRepository : JpaRepository { "LEFT JOIN FETCH m.offence mo " + "LEFT JOIN FETCH ao.offence aoo " + "WHERE e.personId = :personId " + - "ORDER BY e.eventNumber DESC" + "ORDER BY e.dateCreated DESC" ) fun findByPersonId(personId: Long): List } diff --git a/projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/overview/entity/Person.kt b/projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/overview/entity/Person.kt index cd076453ef..96e1d6107b 100644 --- a/projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/overview/entity/Person.kt +++ b/projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/overview/entity/Person.kt @@ -84,9 +84,12 @@ class Person( @Column(columnDefinition = "number") @Convert(converter = NumericBooleanConverter::class) - val softDeleted: Boolean = false + val softDeleted: Boolean = false, -) + val exclusionMessage: String? = null, + val restrictionMessage: String? = null, + + ) interface PersonSummaryEntity { val id: Long @@ -108,12 +111,12 @@ interface PersonRepository : JpaRepository { select p.offender_id as id, p.first_name as forename, - p.second_name as secondName, - p.third_name as thirdName, + p.second_name as secondname, + p.third_name as thirdname, p.surname, p.crn, p.pnc_number as pnc, - p.date_of_birth_date as dateOfBirth + p.date_of_birth_date as dateofbirth from offender p where p.crn = :crn and p.soft_deleted = 0 """, nativeQuery = true diff --git a/projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/service/UserService.kt b/projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/service/UserService.kt index 124fd668ac..1c1c6f7dd0 100644 --- a/projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/service/UserService.kt +++ b/projects/manage-supervision-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/service/UserService.kt @@ -15,18 +15,21 @@ class UserService( private val userRepository: UserRepository, private val caseloadRepository: CaseloadRepository, private val staffRepository: StaffRepository, - private val teamRepository: TeamRepository + private val teamRepository: TeamRepository, + private val userAccessService: UserAccessService ) { @Transactional fun getUserCaseload(username: String, pageable: Pageable): StaffCaseload { val user = userRepository.getUser(username) val caseload = caseloadRepository.findByStaffId(user.staff!!.id, pageable) + + val userAccess = userAccessService.userAccessFor(username, caseload.content.map { it.person.crn }) return StaffCaseload( totalElements = caseload.totalElements.toInt(), totalPages = caseload.totalPages, provider = user.staff.provider.description, - caseload = caseload.content.map { it.toStaffCase() }, + caseload = caseload.content.map { it.toStaffCase(userAccess.access.firstOrNull { ua -> ua.crn == it.person.crn }) }, staff = Name(forename = user.staff.forename, surname = user.staff.surname), ) } @@ -51,12 +54,13 @@ class UserService( .map { KeyPair(it.code.trim(), it.description) } val contactTypes = caseloadRepository.findContactTypesForStaff(user.staff.id).map { KeyPair(it.code.trim(), it.description) } + val userAccess = userAccessService.checkLimitedAccessFor(caseload.content.map { it.person.crn }) return StaffCaseload( totalElements = caseload.totalElements.toInt(), totalPages = caseload.totalPages, provider = user.staff.provider.description, - caseload = caseload.content.map { it.toStaffCase() }, + caseload = caseload.content.map { it.toStaffCase(userAccess.access.first { ua -> ua.crn == it.person.crn }) }, staff = Name(forename = user.staff.forename, surname = user.staff.surname), metaData = MetaData(sentenceTypes = sentenceTypes, contactTypes = contactTypes), sortedBy = sortedBy @@ -67,7 +71,7 @@ class UserService( fun getTeamCaseload(teamCode: String, pageable: Pageable): TeamCaseload { val team = teamRepository.getTeam(teamCode) val caseload = caseloadRepository.findByTeamCode(team.code, pageable) - caseload.content + return TeamCaseload( totalElements = caseload.totalElements.toInt(), totalPages = caseload.totalPages, @@ -94,12 +98,13 @@ class UserService( } } -fun Caseload.toStaffCase() = StaffCase( +fun Caseload.toStaffCase(caseAccess: CaseAccess? = null) = StaffCase( + limitedAccess = caseAccess.isLao(), caseName = Name( forename = person.forename, middleName = listOfNotNull(person.secondName, person.thirdName).joinToString(" "), surname = person.surname - ), + ).takeIf { !caseAccess.isLao() }, crn = person.crn, nextAppointment = nextAppointment?.let { Appointment( @@ -107,17 +112,18 @@ fun Caseload.toStaffCase() = StaffCase( description = it.type.description, date = it.appointmentDatetime ) - }, + }.takeIf { !caseAccess.isLao() }, previousAppointment = previousAppointment?.let { Appointment( id = it.id, description = it.type.description, date = it.appointmentDatetime ) - }, - dob = person.dateOfBirth, - latestSentence = latestSentence?.disposal?.type?.description, - numberOfAdditionalSentences = latestSentence?.let { it.totalNumberOfSentences - 1L } ?: 0L + }.takeIf { !caseAccess.isLao() }, + dob = person.dateOfBirth.takeIf { !caseAccess.isLao() }, + latestSentence = latestSentence?.disposal?.type?.description.takeIf { !caseAccess.isLao() }, + numberOfAdditionalSentences = (latestSentence?.let { it.totalNumberOfSentences - 1L } + ?: 0L).takeIf { !caseAccess.isLao() }, ) fun Caseload.toTeamCase() = TeamCase( @@ -129,3 +135,5 @@ fun Caseload.toTeamCase() = TeamCase( crn = person.crn, staff = Staff(name = Name(forename = staff.forename, surname = staff.surname), code = staff.code) ) + +fun CaseAccess?.isLao() = this != null && (this.userExcluded || this.userRestricted) diff --git a/projects/manage-supervision-and-delius/src/test/kotlin/uk/gov/justice/digital/hmpps/service/UserServiceTest.kt b/projects/manage-supervision-and-delius/src/test/kotlin/uk/gov/justice/digital/hmpps/service/UserServiceTest.kt index e8b20e4740..626d5027a2 100644 --- a/projects/manage-supervision-and-delius/src/test/kotlin/uk/gov/justice/digital/hmpps/service/UserServiceTest.kt +++ b/projects/manage-supervision-and-delius/src/test/kotlin/uk/gov/justice/digital/hmpps/service/UserServiceTest.kt @@ -4,6 +4,8 @@ import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.equalTo import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.ArgumentMatchers.anyList +import org.mockito.ArgumentMatchers.anyString import org.mockito.InjectMocks import org.mockito.Mock import org.mockito.junit.jupiter.MockitoExtension @@ -36,12 +38,16 @@ internal class UserServiceTest { @Mock lateinit var teamRepository: TeamRepository + @Mock + lateinit var userAccessService: UserAccessService + @InjectMocks lateinit var service: UserService @Test fun `calls get person activity function`() { val username = "username" + whenever(userAccessService.userAccessFor(anyString(), anyList())).thenReturn(UserAccess(emptyList())) whenever(userRepository.findByUsername(username)).thenReturn(USER) whenever(caseloadRepository.findByStaffId(USER.staff!!.id, Pageable.ofSize(1))).thenReturn( PageImpl( diff --git a/projects/prison-custody-status-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/messaging/PrisonerMovement.kt b/projects/prison-custody-status-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/messaging/PrisonerMovement.kt index 2e610986db..98639cd9a3 100644 --- a/projects/prison-custody-status-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/messaging/PrisonerMovement.kt +++ b/projects/prison-custody-status-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/messaging/PrisonerMovement.kt @@ -67,13 +67,15 @@ sealed interface PrisonerMovement { } fun PrisonerMovement.releaseDateValid(custody: Custody): Boolean { - return !occurredAt.isBefore(custody.disposal.date) && - !(custody.mostRecentRelease()?.recall?.date?.let { occurredAt.isBefore(it) } ?: false) + val release = custody.mostRecentRelease() + val recallDate = release?.recall?.date + return occurredAt >= custody.disposal.date && (release == null || (recallDate != null && occurredAt >= recallDate)) } -fun PrisonerMovement.receivedDateValid(custody: Custody): Boolean = - !occurredAt.isAfter(ZonedDateTime.now()) && (custody.mostRecentRelease()?.date?.let { !occurredAt.isBefore(it) } - ?: true) +fun PrisonerMovement.receivedDateValid(custody: Custody): Boolean { + val releaseDate = custody.mostRecentRelease()?.date + return occurredAt <= ZonedDateTime.now() && (releaseDate == null || occurredAt >= releaseDate) +} fun PrisonerMovement.statusDateValid(custody: Custody): Boolean = occurredAt <= ZonedDateTime.now() && occurredAt.toLocalDate() >= custody.statusChangeDate diff --git a/projects/prison-custody-status-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/messaging/actions/UpdateLocationAction.kt b/projects/prison-custody-status-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/messaging/actions/UpdateLocationAction.kt index fa823b69ec..3b57af6506 100644 --- a/projects/prison-custody-status-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/messaging/actions/UpdateLocationAction.kt +++ b/projects/prison-custody-status-to-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/messaging/actions/UpdateLocationAction.kt @@ -117,13 +117,25 @@ class UpdateLocationAction( } private fun checkPreconditions(prisonerMovement: PrisonerMovement, custody: Custody): ActionResult? { - if ((prisonerMovement is PrisonerMovement.Received && custody.institution?.nomisCdeCode == prisonerMovement.toPrisonId) || - (prisonerMovement is PrisonerMovement.Received && !prisonerMovement.receivedDateValid(custody)) || - (prisonerMovement is PrisonerMovement.Released && !prisonerMovement.releaseDateValid(custody)) || - !prisonerMovement.locationDateValid(custody) + if (prisonerMovement is PrisonerMovement.Received && custody.institution?.nomisCdeCode == prisonerMovement.toPrisonId) { + return ActionResult.Ignored("PrisonerLocationCorrect", prisonerMovement.telemetryProperties()) + } + + if (prisonerMovement is PrisonerMovement.Received && !prisonerMovement.receivedDateValid(custody)) { + return ActionResult.Ignored("PrisonerLocationCorrect", prisonerMovement.telemetryProperties()) + } + + if (prisonerMovement is PrisonerMovement.Released && + !(prisonerMovement.isHospitalRelease() || prisonerMovement.isIrcRelease() || prisonerMovement.isAbsconded()) && + !prisonerMovement.releaseDateValid(custody) ) { return ActionResult.Ignored("PrisonerLocationCorrect", prisonerMovement.telemetryProperties()) } + + if (!prisonerMovement.locationDateValid(custody)) { + return ActionResult.Ignored("PrisonerLocationCorrect", prisonerMovement.telemetryProperties()) + } + return null } } diff --git a/projects/prison-custody-status-to-delius/src/main/resources/application-prisoner-movement.yml b/projects/prison-custody-status-to-delius/src/main/resources/application-prisoner-movement.yml index 4006e56e9e..d885f180c5 100644 --- a/projects/prison-custody-status-to-delius/src/main/resources/application-prisoner-movement.yml +++ b/projects/prison-custody-status-to-delius/src/main/resources/application-prisoner-movement.yml @@ -29,7 +29,7 @@ prisoner.movement.configs: # Unlawfully at large - UAL - UAL_ECL - # IRC + # Immigration removal centre - DD - DE - DL diff --git a/projects/prison-custody-status-to-delius/src/test/kotlin/uk/gov/justice/digital/hmpps/messaging/actions/UpdateLocationActionTest.kt b/projects/prison-custody-status-to-delius/src/test/kotlin/uk/gov/justice/digital/hmpps/messaging/actions/UpdateLocationActionTest.kt index 8b27ab9fb6..95c8dbf056 100644 --- a/projects/prison-custody-status-to-delius/src/test/kotlin/uk/gov/justice/digital/hmpps/messaging/actions/UpdateLocationActionTest.kt +++ b/projects/prison-custody-status-to-delius/src/test/kotlin/uk/gov/justice/digital/hmpps/messaging/actions/UpdateLocationActionTest.kt @@ -5,7 +5,6 @@ import org.hamcrest.Matchers.equalTo import org.hamcrest.Matchers.instanceOf import org.junit.jupiter.api.Assertions.assertInstanceOf import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments @@ -20,7 +19,6 @@ import org.mockito.kotlin.whenever import uk.gov.justice.digital.hmpps.data.generator.* import uk.gov.justice.digital.hmpps.data.generator.EventGenerator.custodialEvent import uk.gov.justice.digital.hmpps.data.generator.EventGenerator.previouslyReleasedEvent -import uk.gov.justice.digital.hmpps.exception.IgnorableMessageException import uk.gov.justice.digital.hmpps.integrations.delius.contact.ContactService import uk.gov.justice.digital.hmpps.integrations.delius.custody.entity.Custody import uk.gov.justice.digital.hmpps.integrations.delius.custody.entity.CustodyHistoryRepository @@ -65,10 +63,7 @@ internal class UpdateLocationActionTest { @ParameterizedTest @MethodSource("noChangeMovements") fun `no changes made when location is correct`(custody: Custody, prisonerMovement: PrisonerMovement) { - if (prisonerMovement.type == RELEASED && prisonerMovement.reason.isBlank()) { - whenever(institutionRepository.findByNomisCdeCode(InstitutionGenerator.DEFAULT.nomisCdeCode!!)) - .thenReturn(InstitutionGenerator.DEFAULT) - } else if (prisonerMovement.isAbsconded()) { + if (prisonerMovement.isAbsconded()) { whenever(institutionRepository.findByNomisCdeCode(InstitutionGenerator.DEFAULT.nomisCdeCode!!)) .thenReturn(InstitutionGenerator.DEFAULT) whenever(institutionRepository.findByCode(InstitutionCode.UNLAWFULLY_AT_LARGE.code)) diff --git a/projects/redrive-dead-letter-queues/container/Dockerfile b/projects/redrive-dead-letter-queues/container/Dockerfile index 4b87db468a..6221b86b0e 100644 --- a/projects/redrive-dead-letter-queues/container/Dockerfile +++ b/projects/redrive-dead-letter-queues/container/Dockerfile @@ -1,4 +1,4 @@ -FROM public.ecr.aws/aws-cli/aws-cli:2.22.4 +FROM public.ecr.aws/aws-cli/aws-cli:2.22.12 USER root SHELL ["/bin/bash", "-o", "pipefail", "-c"] diff --git a/settings.gradle.kts b/settings.gradle.kts index 3b9f479bf5..06f3688957 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -74,16 +74,16 @@ dependencyResolutionManagement { create("libs") { library("asyncapi", "org.openfolder:kotlin-asyncapi-spring-web:3.0.3") library("aws-autoconfigure", "io.awspring.cloud:spring-cloud-aws-autoconfigure:3.2.1") - library("aws-query-protocol", "software.amazon.awssdk:aws-query-protocol:2.29.20") + library("aws-query-protocol", "software.amazon.awssdk:aws-query-protocol:2.29.29") library("aws-sns", "io.awspring.cloud:spring-cloud-aws-starter-sns:3.2.1") library("aws-sqs", "io.awspring.cloud:spring-cloud-aws-starter-sqs:3.2.1") library("aws-starter", "io.awspring.cloud:spring-cloud-aws-starter:3.2.1") - library("aws-sts", "software.amazon.awssdk:sts:2.29.20") + library("aws-sts", "software.amazon.awssdk:sts:2.29.29") library("azure-app-insights", "com.microsoft.azure:applicationinsights-web:3.6.2") library("azure-identity", "com.azure:azure-identity:1.14.2") library("flipt", "io.flipt:flipt-java:1.1.1") library("html2md", "com.vladsch.flexmark:flexmark-html2md-converter:0.64.8") - library("microsoft-graph", "com.microsoft.graph:microsoft-graph:6.21.0") + library("microsoft-graph", "com.microsoft.graph:microsoft-graph:6.23.0") library("mockito-inline", "org.mockito:mockito-inline:5.2.0") library("mockito-kotlin", "org.mockito.kotlin:mockito-kotlin:5.4.0") library("notify", "uk.gov.service.notify:notifications-java-client:5.2.1-RELEASE") @@ -91,9 +91,9 @@ dependencyResolutionManagement { "opentelemetry-annotations", "io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations:2.10.0" ) - library("sentry", "io.sentry:sentry-spring-boot-starter-jakarta:7.18.0") + library("sentry", "io.sentry:sentry-spring-boot-starter-jakarta:7.18.1") library("springdoc", "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0") - library("wiremock", "org.wiremock:wiremock-standalone:3.9.2") + library("wiremock", "org.wiremock:wiremock-standalone:3.10.0") bundle( "aws-messaging",