diff --git a/projects/cas3-and-delius/build.gradle.kts b/projects/cas3-and-delius/build.gradle.kts index 24ec61554a..f910f13748 100644 --- a/projects/cas3-and-delius/build.gradle.kts +++ b/projects/cas3-and-delius/build.gradle.kts @@ -6,6 +6,7 @@ dependencies { implementation(project(":libs:audit")) implementation(project(":libs:commons")) implementation(project(":libs:messaging")) + implementation(project(":libs:oauth-client")) implementation("org.springframework.boot:spring-boot-starter-actuator") implementation("org.springframework.boot:spring-boot-starter-data-jpa") @@ -15,6 +16,8 @@ dependencies { implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + implementation(libs.openfeign) + implementation(libs.springdoc) dev(project(":libs:dev-tools")) dev("com.h2database:h2") diff --git a/projects/cas3-and-delius/deploy/database/access.yml b/projects/cas3-and-delius/deploy/database/access.yml index 44064d7d71..c9290b149f 100644 --- a/projects/cas3-and-delius/deploy/database/access.yml +++ b/projects/cas3-and-delius/deploy/database/access.yml @@ -2,8 +2,11 @@ database: access: username_key: /cas3-and-delius/db-username password_key: /cas3-and-delius/db-password + tables: + - audited_interaction + - contact audit: username: Cas3AndDelius - forename: Probation Integration # TODO change this to something meaningful for your service + forename: Transitional Accommodation (CAS3) surname: Service diff --git a/projects/cas3-and-delius/deploy/values-dev.yml b/projects/cas3-and-delius/deploy/values-dev.yml index ba4c3aa2fb..30dd7d7326 100644 --- a/projects/cas3-and-delius/deploy/values-dev.yml +++ b/projects/cas3-and-delius/deploy/values-dev.yml @@ -1,5 +1,3 @@ -enabled: false # TODO set this to true when you're ready to deploy your service - generic-service: ingress: host: cas3-and-delius-dev.hmpps.service.justice.gov.uk @@ -9,6 +7,9 @@ generic-service: env: SENTRY_ENVIRONMENT: dev + SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_HMPPS-AUTH_TOKEN-URI: https://sign-in-dev.hmpps.service.justice.gov.uk/auth/oauth/token + SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI: https://sign-in-dev.hmpps.service.justice.gov.uk/auth/.well-known/jwks.json + SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI: https://sign-in-dev.hmpps.service.justice.gov.uk/auth/issuer LOGGING_LEVEL_UK_GOV_DIGITAL_JUSTICE_HMPPS: DEBUG generic-prometheus-alerts: diff --git a/projects/cas3-and-delius/deploy/values-preprod.yml b/projects/cas3-and-delius/deploy/values-preprod.yml index 66ddb835f4..5b5c534d7a 100644 --- a/projects/cas3-and-delius/deploy/values-preprod.yml +++ b/projects/cas3-and-delius/deploy/values-preprod.yml @@ -9,6 +9,9 @@ generic-service: env: SENTRY_ENVIRONMENT: preprod + SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_HMPPS-AUTH_TOKEN-URI: https://sign-in-preprod.hmpps.service.justice.gov.uk/auth/oauth/token + SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI: https://sign-in-preprod.hmpps.service.justice.gov.uk/auth/.well-known/jwks.json + SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI: https://sign-in-preprod.hmpps.service.justice.gov.uk/auth/issuer generic-prometheus-alerts: businessHoursOnly: true \ No newline at end of file diff --git a/projects/cas3-and-delius/deploy/values-prod.yml b/projects/cas3-and-delius/deploy/values-prod.yml index 981cc00e94..559b80c12e 100644 --- a/projects/cas3-and-delius/deploy/values-prod.yml +++ b/projects/cas3-and-delius/deploy/values-prod.yml @@ -6,3 +6,6 @@ generic-service: env: SENTRY_ENVIRONMENT: prod + SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_HMPPS-AUTH_TOKEN-URI: https://sign-in.hmpps.service.justice.gov.uk/auth/oauth/token + SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI: https://sign-in.hmpps.service.justice.gov.uk/auth/.well-known/jwks.json + SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI: https://sign-in.hmpps.service.justice.gov.uk/auth/issuer diff --git a/projects/cas3-and-delius/deploy/values.yaml b/projects/cas3-and-delius/deploy/values.yaml index efd5592bf3..4db7bf639a 100644 --- a/projects/cas3-and-delius/deploy/values.yaml +++ b/projects/cas3-and-delius/deploy/values.yaml @@ -1,4 +1,3 @@ -# Common values generic-service: nameOverride: cas3-and-delius serviceAccountName: cas3-and-delius @@ -12,6 +11,9 @@ generic-service: namespace_secrets: common: SPRING_DATASOURCE_URL: DB_URL + cas3-and-delius-client-credentials: + OAUTH2_CLIENT-ID: CLIENT_ID + OAUTH2_CLIENT-SECRET: CLIENT_SECRET cas3-and-delius-database: SPRING_DATASOURCE_USERNAME: DB_USERNAME SPRING_DATASOURCE_PASSWORD: DB_PASSWORD diff --git a/projects/cas3-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/DataLoader.kt b/projects/cas3-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/DataLoader.kt index f6ea94c9a7..d184ad94c3 100644 --- a/projects/cas3-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/DataLoader.kt +++ b/projects/cas3-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/DataLoader.kt @@ -1,17 +1,23 @@ package uk.gov.justice.digital.hmpps.data import jakarta.annotation.PostConstruct +import jakarta.persistence.EntityManager import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty import org.springframework.boot.context.event.ApplicationReadyEvent import org.springframework.context.ApplicationListener import org.springframework.stereotype.Component +import org.springframework.transaction.annotation.Transactional +import uk.gov.justice.digital.hmpps.data.generator.BusinessInteractionGenerator +import uk.gov.justice.digital.hmpps.data.generator.ContactTypeGenerator +import uk.gov.justice.digital.hmpps.data.generator.PersonGenerator import uk.gov.justice.digital.hmpps.data.generator.UserGenerator import uk.gov.justice.digital.hmpps.user.AuditUserRepository @Component @ConditionalOnProperty("seed.database") class DataLoader( - private val auditUserRepository: AuditUserRepository + private val auditUserRepository: AuditUserRepository, + private val em: EntityManager ) : ApplicationListener { @PostConstruct @@ -19,7 +25,15 @@ class DataLoader( auditUserRepository.save(UserGenerator.AUDIT_USER) } + @Transactional override fun onApplicationEvent(are: ApplicationReadyEvent) { - // Perform dev/test database setup here, using JPA repositories and generator classes... + em.saveAll( + BusinessInteractionGenerator.UPDATE_CONTACT, + ContactTypeGenerator.CONTACT_TYPE, + PersonGenerator.PERSON_CRN, + PersonGenerator.generatePersonManager(PersonGenerator.PERSON_CRN) + ) } + + fun EntityManager.saveAll(vararg any: Any) = any.forEach { persist(it) } } diff --git a/projects/cas3-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/BusinessInteractionGenerator.kt b/projects/cas3-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/BusinessInteractionGenerator.kt new file mode 100644 index 0000000000..8118f011c1 --- /dev/null +++ b/projects/cas3-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/BusinessInteractionGenerator.kt @@ -0,0 +1,13 @@ +package uk.gov.justice.digital.hmpps.data.generator + +import uk.gov.justice.digital.hmpps.audit.BusinessInteraction +import uk.gov.justice.digital.hmpps.integrations.delius.audit.BusinessInteractionCode +import java.time.ZonedDateTime + +object BusinessInteractionGenerator { + val UPDATE_CONTACT = BusinessInteraction( + IdGenerator.getAndIncrement(), + BusinessInteractionCode.UPDATE_CONTACT.code, + ZonedDateTime.now().minusMonths(6) + ) +} diff --git a/projects/cas3-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/ContactTypeGenerator.kt b/projects/cas3-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/ContactTypeGenerator.kt new file mode 100644 index 0000000000..66a7f173d6 --- /dev/null +++ b/projects/cas3-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/ContactTypeGenerator.kt @@ -0,0 +1,11 @@ +package uk.gov.justice.digital.hmpps.data.generator + +import uk.gov.justice.digital.hmpps.integrations.delius.entity.ContactType + +object ContactTypeGenerator { + val CONTACT_TYPE = ContactType( + IdGenerator.getAndIncrement(), + "EARS", + false + ) +} diff --git a/projects/cas3-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/MessageGenerator.kt b/projects/cas3-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/MessageGenerator.kt index 5ed8de87fc..1a53205115 100644 --- a/projects/cas3-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/MessageGenerator.kt +++ b/projects/cas3-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/MessageGenerator.kt @@ -4,5 +4,5 @@ import uk.gov.justice.digital.hmpps.message.HmppsDomainEvent import uk.gov.justice.digital.hmpps.resourceloader.ResourceLoader object MessageGenerator { - val EXAMPLE = ResourceLoader.message("example-message") + val REFERRAL_SUBMITTED = ResourceLoader.message("referral-submitted") } diff --git a/projects/cas3-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/PersonGenerator.kt b/projects/cas3-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/PersonGenerator.kt new file mode 100644 index 0000000000..83ea3416fb --- /dev/null +++ b/projects/cas3-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/PersonGenerator.kt @@ -0,0 +1,14 @@ +package uk.gov.justice.digital.hmpps.data.generator + +import uk.gov.justice.digital.hmpps.integrations.delius.entity.Person +import uk.gov.justice.digital.hmpps.integrations.delius.entity.PersonManager + +object PersonGenerator { + val PERSON_CRN = generate("A000001", null) + + fun generate(crn: String, noms: String?, softDeleted: Boolean = false, id: Long = IdGenerator.getAndIncrement()) = + Person(crn, noms, softDeleted, id) + + fun generatePersonManager(person: Person) = + PersonManager(IdGenerator.getAndIncrement(), person.id, 1, 1, 1, 1) +} diff --git a/projects/cas3-and-delius/src/dev/resources/local-public-key.pub b/projects/cas3-and-delius/src/dev/resources/local-public-key.pub new file mode 100644 index 0000000000..9a59829590 --- /dev/null +++ b/projects/cas3-and-delius/src/dev/resources/local-public-key.pub @@ -0,0 +1,6 @@ +-----BEGIN PUBLIC KEY----- +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCc2Dlk/NOvXrjSs83W+Nj4bMfz +6svg2ulfcqTcEoA/Vy6FWK3pOX50VZph3V0Sbh8OTRyHgTw3BRQKrxE/TdsUWw8A +QxgbjBWypmm6I/gUGeiSgYwATZpdVbqmuNI0BRg5l/vgJki6K5cg+4fRazZXaHvN +ldzA6bUDdyt73u7qSwIDAQAB +-----END PUBLIC KEY----- \ No newline at end of file diff --git a/projects/cas3-and-delius/src/dev/resources/messages/example-message.json b/projects/cas3-and-delius/src/dev/resources/messages/example-message.json deleted file mode 100644 index 3ec3f10524..0000000000 --- a/projects/cas3-and-delius/src/dev/resources/messages/example-message.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "Type": "Notification", - "MessageId": "aa2c2828-167f-529b-8e19-73735a2fb85c", - "TopicArn": "arn:aws:sns:eu-west-2:000000000000:example", - "Message": "{\"eventType\":\"example\",\"version\":1,\"description\":\"Example event\",\"detailUrl\":\"http://localhost:{wiremock.port}/example/123\",\"occurredAt\":\"2022-07-27T15:22:08.452612281+01:00\",\"additionalInformation\":{\"exampleId\":\"123\"},\"personReference\":{\"identifiers\":[{\"type\":\"CRN\",\"value\":\"A000001\"}]}}", - "Timestamp": "2022-07-27T14:22:08.509Z", - "SignatureVersion": "1", - "Signature": "EXAMPLE", - "SigningCertURL": "https://sns.eu-west-2.amazonaws.com/EXAMPLE.pem", - "UnsubscribeURL": "https://sns.eu-west-2.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=EXAMPLE", - "MessageAttributes": { - "eventType": { - "Type": "String", - "Value": "example" - } - } -} \ No newline at end of file diff --git a/projects/cas3-and-delius/src/dev/resources/messages/referral-submitted.json b/projects/cas3-and-delius/src/dev/resources/messages/referral-submitted.json new file mode 100644 index 0000000000..1865b7aaef --- /dev/null +++ b/projects/cas3-and-delius/src/dev/resources/messages/referral-submitted.json @@ -0,0 +1,18 @@ +{ + "eventType": "accommodation.cas3.referral.submitted", + "version": 1, + "description": "A cas3 referral has been submitted", + "detailUrl": "http://localhost:{wiremock.port}/cas3-api/events/referral-submitted/1234", + "occurredAt": "2022-12-04T10:42:43+00:00", + "additionalInformation": { + "applicationId": "68df9f6c-3fcb-4ec6-8fcf-96551cd9b080" + }, + "personReference": { + "identifiers": [ + { + "type": "CRN", + "value": "A000001" + } + ] + } +} \ No newline at end of file diff --git a/projects/cas3-and-delius/src/dev/resources/simulations/__files/cas3-referral-submitted.json b/projects/cas3-and-delius/src/dev/resources/simulations/__files/cas3-referral-submitted.json new file mode 100644 index 0000000000..6696b2a007 --- /dev/null +++ b/projects/cas3-and-delius/src/dev/resources/simulations/__files/cas3-referral-submitted.json @@ -0,0 +1,13 @@ +{ + "id": "364145f9-0af8-488e-9901-b4c46cd9ba37", + "timestamp": "2022-11-30T14:53:44", + "eventType": "accommodation.cas3.referral.submitted", + "eventDetails": { + "applicationId": "68df9f6c-3fcb-4ec6-8fcf-96551cd9b080", + "applicationUrl": "https://approved-premises-dev.hmpps.service.justice.gov.uk/application/68df9f6c-3fcb-4ec6-8fcf-96551cd9b080", + "personReference": { + "crn": "A000001", + "noms": "A0001AA" + } + } +} \ No newline at end of file diff --git a/projects/cas3-and-delius/src/dev/resources/simulations/__files/hmpps-auth-token-body.json b/projects/cas3-and-delius/src/dev/resources/simulations/__files/hmpps-auth-token-body.json new file mode 100644 index 0000000000..b2a6163951 --- /dev/null +++ b/projects/cas3-and-delius/src/dev/resources/simulations/__files/hmpps-auth-token-body.json @@ -0,0 +1,10 @@ +{ + "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJwcm9iYXRpb24taW50ZWdyYXRpb24tZGV2IiwiZ3JhbnRfdHlwZSI6ImNsaWVudF9jcmVkZW50aWFscyIsInVzZXJfbmFtZSI6InByb2JhdGlvbi1pbnRlZ3JhdGlvbi1kZXYiLCJzY29wZSI6WyJyZWFkIiwid3JpdGUiXSwiYXV0aF9zb3VyY2UiOiJub25lIiwiaXNzIjoiaHR0cHM6Ly9zaWduLWluLWRldi5obXBwcy5zZXJ2aWNlLmp1c3RpY2UuZ292LnVrL2F1dGgvaXNzdWVyIiwiZXhwIjo5OTk5OTk5OTk5LCJhdXRob3JpdGllcyI6WyJST0xFX0FQUFJPVkVEX1BSRU1JU0VTX1NUQUZGIl0sImp0aSI6IjI1RHVSbjEtaHlIWmV3TGNkSkp4d1ZMMDNLVSIsImNsaWVudF9pZCI6InByb2JhdGlvbi1pbnRlZ3JhdGlvbi1kZXYiLCJpYXQiOjE2NjM3NTczMTF9.HmAw0LBKPSHHyDh1egCb1i2ubjzDQ9x43XKDt-Qg09GsS7RuroBUm2BmRoCXPIapSve-BaUBWGa_pPopsaX6VBlzHBOWZPu68HaCkzBa82fwvyVPI3s88eJBUemEOZWQ0RmCx8KiPjK53-rZEhx_aEMJSQoHJIrFK86TjLwphk4", + "token_type": "bearer", + "expires_in": 9999999999, + "scope": "read write", + "sub": "probation-integration-dev", + "auth_source": "none", + "jti": "fN29JHJy1N7gcYvqe-8B_k5T0mA", + "iss": "https://sign-in-dev.hmpps.service.justice.gov.uk/auth/issuer" +} \ No newline at end of file diff --git a/projects/cas3-and-delius/src/dev/resources/simulations/mappings/cas3-api.json b/projects/cas3-and-delius/src/dev/resources/simulations/mappings/cas3-api.json new file mode 100644 index 0000000000..40ef18ea15 --- /dev/null +++ b/projects/cas3-and-delius/src/dev/resources/simulations/mappings/cas3-api.json @@ -0,0 +1,17 @@ +{ + "mappings": [ + { + "request": { + "method": "GET", + "urlPath": "/cas3-api/events/referral-submitted/1234" + }, + "response": { + "headers": { + "Content-Type": "application/json" + }, + "status": 200, + "bodyFileName": "cas3-referral-submitted.json" + } + } + ] +} \ No newline at end of file diff --git a/projects/cas3-and-delius/src/dev/resources/simulations/mappings/hmpps-auth.json b/projects/cas3-and-delius/src/dev/resources/simulations/mappings/hmpps-auth.json new file mode 100644 index 0000000000..8a34b1316e --- /dev/null +++ b/projects/cas3-and-delius/src/dev/resources/simulations/mappings/hmpps-auth.json @@ -0,0 +1,13 @@ +{ + "request": { + "method": "POST", + "urlPath": "/auth/oauth/token" + }, + "response": { + "headers": { + "Content-Type": "application/json" + }, + "status": 200, + "bodyFileName": "hmpps-auth-token-body.json" + } +} \ No newline at end of file diff --git a/projects/cas3-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/CASIntegrationTest.kt b/projects/cas3-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/CASIntegrationTest.kt new file mode 100644 index 0000000000..bb6a9ed123 --- /dev/null +++ b/projects/cas3-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/CASIntegrationTest.kt @@ -0,0 +1,56 @@ +package uk.gov.justice.digital.hmpps + +import com.github.tomakehurst.wiremock.WireMockServer +import org.hamcrest.MatcherAssert +import org.hamcrest.Matchers +import org.junit.jupiter.api.MethodOrderer +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestMethodOrder +import org.mockito.Mockito +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Value +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.boot.test.mock.mockito.MockBean +import uk.gov.justice.digital.hmpps.integrations.delius.entity.ContactRepository +import uk.gov.justice.digital.hmpps.messaging.HmppsChannelManager +import uk.gov.justice.digital.hmpps.telemetry.TelemetryService +import uk.gov.justice.digital.hmpps.telemetry.notificationReceived + +@AutoConfigureMockMvc +@SpringBootTest(webEnvironment = RANDOM_PORT) +@TestMethodOrder(MethodOrderer.OrderAnnotation::class) +internal class CASIntegrationTest { + @Value("\${messaging.consumer.queue}") + lateinit var queueName: String + + @Autowired + lateinit var channelManager: HmppsChannelManager + + @Autowired + lateinit var wireMockServer: WireMockServer + + @Autowired + lateinit var contactRepository: ContactRepository + + @MockBean + lateinit var telemetryService: TelemetryService + + @Test + fun `message is processed correctly`() { + // Given an application-submitted event + val event = prepEvent("referral-submitted", wireMockServer.port()) + + // When it is received + channelManager.getChannel(queueName).publishAndWait(event) + + // Then it is logged to telemetry + Mockito.verify(telemetryService).notificationReceived(event) + + val contact = + contactRepository.getByExternalReference(event.message.additionalInformation["applicationId"] as String) + + MatcherAssert.assertThat(contact!!.type.code, Matchers.equalTo("EARS")) + } +} diff --git a/projects/cas3-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/IntegrationTest.kt b/projects/cas3-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/IntegrationTest.kt deleted file mode 100644 index 351cb4d397..0000000000 --- a/projects/cas3-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/IntegrationTest.kt +++ /dev/null @@ -1,41 +0,0 @@ -package uk.gov.justice.digital.hmpps - -import org.junit.jupiter.api.Test -import org.mockito.Mockito.atLeastOnce -import org.mockito.kotlin.verify -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.beans.factory.annotation.Value -import org.springframework.boot.test.context.SpringBootTest -import org.springframework.boot.test.mock.mockito.MockBean -import uk.gov.justice.digital.hmpps.data.generator.MessageGenerator -import uk.gov.justice.digital.hmpps.message.Notification -import uk.gov.justice.digital.hmpps.messaging.HmppsChannelManager -import uk.gov.justice.digital.hmpps.telemetry.TelemetryService -import uk.gov.justice.digital.hmpps.telemetry.notificationReceived -import java.util.concurrent.TimeoutException - -@SpringBootTest -internal class IntegrationTest { - @Value("\${messaging.consumer.queue}") - lateinit var queueName: String - - @Autowired lateinit var channelManager: HmppsChannelManager - - @MockBean lateinit var telemetryService: TelemetryService - - @Test - fun `message is logged to telemetry`() { - // Given a message - val notification = Notification(message = MessageGenerator.EXAMPLE) - - // When it is received - try { - channelManager.getChannel(queueName).publishAndWait(notification) - } catch (_: TimeoutException) { - // Note: Remove this try/catch when the MessageListener logic has been implemented - } - - // Then it is logged to telemetry - verify(telemetryService, atLeastOnce()).notificationReceived(notification) - } -} diff --git a/projects/cas3-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/config/FeignOAuth2Config.kt b/projects/cas3-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/config/FeignOAuth2Config.kt new file mode 100644 index 0000000000..e76147fcda --- /dev/null +++ b/projects/cas3-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/config/FeignOAuth2Config.kt @@ -0,0 +1,15 @@ +package uk.gov.justice.digital.hmpps.config + +import org.springframework.cloud.openfeign.EnableFeignClients +import org.springframework.context.annotation.Configuration +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager +import uk.gov.justice.digital.hmpps.config.feign.FeignConfig +import uk.gov.justice.digital.hmpps.integrations.approvedpremesis.Cas3ApiClient + +@Configuration +@EnableFeignClients(clients = [Cas3ApiClient::class]) +class FeignOAuth2Config( + authorizedClientManager: OAuth2AuthorizedClientManager +) : FeignConfig(authorizedClientManager) { + override fun registrationId() = "cas3-and-delius" +} diff --git a/projects/cas3-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/approvedpremesis/Cas3ApiClient.kt b/projects/cas3-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/approvedpremesis/Cas3ApiClient.kt new file mode 100644 index 0000000000..840795b2c5 --- /dev/null +++ b/projects/cas3-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/approvedpremesis/Cas3ApiClient.kt @@ -0,0 +1,15 @@ +package uk.gov.justice.digital.hmpps.integrations.approvedpremesis + +import org.springframework.cloud.openfeign.FeignClient +import org.springframework.web.bind.annotation.GetMapping +import uk.gov.justice.digital.hmpps.config.FeignOAuth2Config +import java.net.URI + +@FeignClient( + name = "cas3-api", + url = "https://dummy-url/to/be/overridden", + configuration = [FeignOAuth2Config::class] +) +interface Cas3ApiClient { + @GetMapping fun getApplicationSubmittedDetails(uri: URI): EventDetails +} diff --git a/projects/cas3-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/approvedpremesis/EventDetails.kt b/projects/cas3-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/approvedpremesis/EventDetails.kt new file mode 100644 index 0000000000..95191c2d3c --- /dev/null +++ b/projects/cas3-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/approvedpremesis/EventDetails.kt @@ -0,0 +1,14 @@ +package uk.gov.justice.digital.hmpps.integrations.approvedpremesis + +import java.time.ZonedDateTime + +data class EventDetails( + val id: String, + val timestamp: ZonedDateTime, + val eventType: String, + val eventDetails: T +) + +data class ApplicationSubmitted( + val applicationId: String +) diff --git a/projects/cas3-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/ContactService.kt b/projects/cas3-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/ContactService.kt new file mode 100644 index 0000000000..8bd3ce2a29 --- /dev/null +++ b/projects/cas3-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/ContactService.kt @@ -0,0 +1,71 @@ +package uk.gov.justice.digital.hmpps.integrations.delius + +import org.springframework.stereotype.Service +import uk.gov.justice.digital.hmpps.audit.service.AuditableService +import uk.gov.justice.digital.hmpps.audit.service.AuditedInteractionService +import uk.gov.justice.digital.hmpps.exception.NotFoundException +import uk.gov.justice.digital.hmpps.integrations.approvedpremesis.Cas3ApiClient +import uk.gov.justice.digital.hmpps.integrations.delius.audit.BusinessInteractionCode +import uk.gov.justice.digital.hmpps.integrations.delius.entity.Contact +import uk.gov.justice.digital.hmpps.integrations.delius.entity.ContactRepository +import uk.gov.justice.digital.hmpps.integrations.delius.entity.ContactType.Companion.REFERRAL_SUBMITTED +import uk.gov.justice.digital.hmpps.integrations.delius.entity.ContactTypeRepository +import uk.gov.justice.digital.hmpps.integrations.delius.entity.PersonManagerRepository +import uk.gov.justice.digital.hmpps.integrations.delius.entity.PersonRepository +import uk.gov.justice.digital.hmpps.integrations.delius.entity.getByCrn +import uk.gov.justice.digital.hmpps.message.HmppsDomainEvent +import uk.gov.justice.digital.hmpps.messaging.url +import uk.gov.justice.digital.hmpps.telemetry.TelemetryService +import java.time.ZonedDateTime + +@Service +class ContactService( + auditedInteractionService: AuditedInteractionService, + private val personRepository: PersonRepository, + private val personManagerRepository: PersonManagerRepository, + private val contactRepository: ContactRepository, + private val contactTypeRepository: ContactTypeRepository, + private val telemetryService: TelemetryService, + private val cas3ApiClient: Cas3ApiClient +) : AuditableService(auditedInteractionService) { + + fun createReferralSubmitted(event: HmppsDomainEvent) = audit(BusinessInteractionCode.UPDATE_CONTACT) { + val details = cas3ApiClient.getApplicationSubmittedDetails(event.url()).eventDetails + val crn = event.personReference.findCrn() + val externalReference = details.applicationId + val person = personRepository.getByCrn(crn!!) + + if (contactRepository.getByExternalReference(externalReference) != null) { + telemetryService.trackEvent("Duplicate ApplicationSubmitted event received for crn $crn") + } else { + contactRepository.save(newContact(event.occurredAt, person.id, REFERRAL_SUBMITTED, externalReference)) + } + } + + fun newContact(occurredAt: ZonedDateTime, personId: Long, typeCode: String, reference: String): Contact { + val contactType = contactTypeRepository.findByCode(REFERRAL_SUBMITTED) ?: throw NotFoundException( + "ContactType", + "code", + REFERRAL_SUBMITTED + ) + val comDetails = personManagerRepository.findActiveManager(personId) ?: throw NotFoundException( + "PersonManager", + "personId", + personId + ) + + return Contact( + offenderId = personId, + type = contactType, + notes = "", + date = occurredAt.toLocalDate(), + startTime = occurredAt, + isSensitive = contactType.isSensitive, + probationAreaId = comDetails.probationAreaId, + teamId = comDetails.teamId, + staffId = comDetails.staffId, + staffEmployeeId = comDetails.staffEmployeeId, + externalReference = reference + ) + } +} diff --git a/projects/cas3-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/audit/BusinessInteractionCode.kt b/projects/cas3-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/audit/BusinessInteractionCode.kt index f151195d11..1324d7c4f7 100644 --- a/projects/cas3-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/audit/BusinessInteractionCode.kt +++ b/projects/cas3-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/audit/BusinessInteractionCode.kt @@ -3,6 +3,5 @@ package uk.gov.justice.digital.hmpps.integrations.delius.audit import uk.gov.justice.digital.hmpps.audit.InteractionCode enum class BusinessInteractionCode(override val code: String) : InteractionCode { - // TODO Add any Delius interaction codes used by the service here - EXAMPLE_INTERACTION("EXAMPLEBI001") + UPDATE_CONTACT("CLBI007") } diff --git a/projects/cas3-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/entity/Contact.kt b/projects/cas3-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/entity/Contact.kt new file mode 100644 index 0000000000..f6b5c16457 --- /dev/null +++ b/projects/cas3-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/entity/Contact.kt @@ -0,0 +1,125 @@ +package uk.gov.justice.digital.hmpps.integrations.delius.entity + +import jakarta.persistence.Column +import jakarta.persistence.Convert +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.Lob +import jakarta.persistence.ManyToOne +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import jakarta.persistence.Version +import org.hibernate.annotations.Immutable +import org.hibernate.type.YesNoConverter +import org.springframework.data.annotation.CreatedBy +import org.springframework.data.annotation.LastModifiedBy +import org.springframework.data.jpa.domain.support.AuditingEntityListener +import org.springframework.data.jpa.repository.JpaRepository +import java.time.LocalDate +import java.time.ZonedDateTime + +@EntityListeners(AuditingEntityListener::class) +@Entity +@Table(name = "contact") +class Contact( + @Id + @Column(name = "contact_id", updatable = false) + @SequenceGenerator(name = "contact_id_seq", sequenceName = "contact_id_seq", allocationSize = 1) + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "contact_id_seq") + val id: Long = 0, + + val externalReference: String, + + @Column(updatable = false) + val offenderId: Long, + + @ManyToOne + @JoinColumn(name = "contact_type_id", updatable = false) + val type: ContactType, + + @Lob + val notes: String, + + @Column(name = "contact_date") + val date: LocalDate, + + @Column(name = "contact_start_time") + val startTime: ZonedDateTime, + + @Column(updatable = false) + val staffId: Long, + + @Column(updatable = false) + val staffEmployeeId: Long, + + @Column(updatable = false) + val teamId: Long, + + @Column(updatable = false) + val probationAreaId: Long, + + @Column(name = "sensitive") + @Convert(converter = YesNoConverter::class) + val isSensitive: Boolean = type.isSensitive, + + @Column(name = "created_datetime", updatable = false) + val createdDateTime: ZonedDateTime = ZonedDateTime.now(), + + @Column(name = "last_updated_datetime") + val lastModifiedDateTime: ZonedDateTime = ZonedDateTime.now(), + + @CreatedBy + @Column(name = "created_by_user_id", updatable = false) + var createdByUserId: Long = 0, + + @LastModifiedBy + @Column(name = "last_updated_user_id") + var lastModifiedUserId: Long = 0, + + @Version + @Column(name = "row_version") + val version: Long = 0, + + @Column(updatable = false) + val trustProviderTeamId: Long = teamId, + + @Column(updatable = false, columnDefinition = "NUMBER") + val trustProviderFlag: Boolean = false, + + @Column(updatable = false) + val partitionAreaId: Long = 0L, + + @Column(updatable = false, columnDefinition = "NUMBER") + val softDeleted: Boolean = false +) + +@Immutable +@Entity +@Table(name = "r_contact_type") +class ContactType( + @Id + @Column(name = "contact_type_id") + val id: Long, + + val code: String, + + @Column(name = "sensitive_contact") + @Convert(converter = YesNoConverter::class) + val isSensitive: Boolean +) { + companion object { + const val REFERRAL_SUBMITTED = "EARS" + } +} + +interface ContactRepository : JpaRepository { + fun getByExternalReference(externalReference: String): Contact? +} + +interface ContactTypeRepository : JpaRepository { + fun findByCode(code: String): ContactType? +} diff --git a/projects/cas3-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/entity/Person.kt b/projects/cas3-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/entity/Person.kt new file mode 100644 index 0000000000..11d981ca34 --- /dev/null +++ b/projects/cas3-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/entity/Person.kt @@ -0,0 +1,40 @@ +package uk.gov.justice.digital.hmpps.integrations.delius.entity + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.Id +import jakarta.persistence.Table +import org.hibernate.annotations.Immutable +import org.springframework.data.jpa.repository.JpaRepository +import uk.gov.justice.digital.hmpps.exception.NotFoundException + +@Immutable +@Entity +@Table(name = "offender") +class Person( + + @Column(columnDefinition = "char(7)") + val crn: String, + + @Column(columnDefinition = "char(7)") + val nomsNumber: String?, + + @Column(updatable = false, columnDefinition = "number") + val softDeleted: Boolean = false, + + @Id + @Column(name = "offender_id") + val id: Long +) + +interface PersonRepository : JpaRepository { + + fun findByCrnAndSoftDeletedIsFalse(crn: String): Person? + fun findByNomsNumberAndSoftDeletedIsFalse(nomsNumber: String): Person? +} + +fun PersonRepository.getByCrn(crn: String) = + findByCrnAndSoftDeletedIsFalse(crn) ?: throw NotFoundException("Person", "crn", crn) + +fun PersonRepository.getByNoms(noms: String) = + findByNomsNumberAndSoftDeletedIsFalse(noms) ?: throw NotFoundException("Person", "noms", noms) diff --git a/projects/cas3-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/entity/PersonManager.kt b/projects/cas3-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/entity/PersonManager.kt new file mode 100644 index 0000000000..45cff36c22 --- /dev/null +++ b/projects/cas3-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/entity/PersonManager.kt @@ -0,0 +1,50 @@ +package uk.gov.justice.digital.hmpps.integrations.delius.entity + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.Id +import jakarta.persistence.Table +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query + +@Entity +@Table(name = "offender_manager") +class PersonManager( + @Id + @Column(name = "offender_manager_id") + val id: Long, + + @Column(name = "offender_id") + val personId: Long, + + @Column(name = "provider_team_id") + val teamId: Long, + + @Column(name = "allocation_staff_id") + val staffId: Long, + + @Column(name = "provider_employee_id") + val staffEmployeeId: Long, + + @Column(name = "probation_area_id") + val probationAreaId: Long, + + @Column(name = "active_flag", columnDefinition = "NUMBER") + val active: Boolean = true, + + @Column(name = "soft_deleted", columnDefinition = "NUMBER", nullable = false) + val softDeleted: Boolean = false + +) + +interface PersonManagerRepository : JpaRepository { + @Query( + """ + select pm from PersonManager pm + where pm.personId = :personId + and pm.active = true + and pm.softDeleted = false + """ + ) + fun findActiveManager(personId: Long): PersonManager? +} diff --git a/projects/cas3-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/messaging/Handler.kt b/projects/cas3-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/messaging/Handler.kt index b3f03191d7..3ec1a3e203 100644 --- a/projects/cas3-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/messaging/Handler.kt +++ b/projects/cas3-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/messaging/Handler.kt @@ -2,18 +2,37 @@ package uk.gov.justice.digital.hmpps.messaging import org.springframework.stereotype.Component import uk.gov.justice.digital.hmpps.converter.NotificationConverter +import uk.gov.justice.digital.hmpps.integrations.delius.ContactService import uk.gov.justice.digital.hmpps.message.HmppsDomainEvent import uk.gov.justice.digital.hmpps.message.Notification import uk.gov.justice.digital.hmpps.telemetry.TelemetryService import uk.gov.justice.digital.hmpps.telemetry.notificationReceived +import java.net.URI @Component class Handler( override val converter: NotificationConverter, - private val telemetryService: TelemetryService + private val telemetryService: TelemetryService, + private val contactService: ContactService ) : NotificationHandler { override fun handle(notification: Notification) { telemetryService.notificationReceived(notification) - TODO("Not yet implemented") + val event = notification.message + when (event.eventType) { + "accommodation.cas3.referral.submitted" -> { + contactService.createReferralSubmitted(event) + telemetryService.trackEvent("ApplicationSubmitted", event.telemetryProperties()) + } + + else -> throw IllegalArgumentException("Unexpected event type ${event.eventType}") + } } + + fun HmppsDomainEvent.telemetryProperties() = mapOf( + "occurredAt" to occurredAt.toString(), + "crn" to crn() + ) } +fun HmppsDomainEvent.crn(): String = personReference.findCrn() ?: throw IllegalArgumentException("Missing CRN") + +fun HmppsDomainEvent.url(): URI = URI.create(detailUrl ?: throw IllegalArgumentException("Missing detail url")) diff --git a/projects/cas3-and-delius/src/main/resources/application.yml b/projects/cas3-and-delius/src/main/resources/application.yml index ddd62578ea..8d1c9d7a83 100644 --- a/projects/cas3-and-delius/src/main/resources/application.yml +++ b/projects/cas3-and-delius/src/main/resources/application.yml @@ -16,6 +16,25 @@ spring: global_temporary: create_tables: false drop_tables: false + security.oauth2.client: + registration: + cas3-and-delius: + provider: hmpps-auth + authorization-grant-type: client_credentials + client-id: ${oauth2.client-id} + client-secret: ${oauth2.client-secret} + provider: + hmpps-auth: + token-uri: http://localhost:${wiremock.port}/auth/oauth/token + cloud.openfeign.client.config: + default: + logger-level: full + connect-timeout: 5000 + read-timeout: 5000 + default-request-headers: + Accept: application/json + Content-Type: application/json +springdoc.default-produces-media-type: application/json delius.db.username: Cas3AndDelius # Should match value in [deploy/database/access.yml]. @@ -31,11 +50,22 @@ spring.config.activate.on-profile: [ "dev", "integration-test" ] spring: datasource.url: jdbc:h2:file:./dev;MODE=Oracle;DEFAULT_NULL_ORDERING=HIGH;AUTO_SERVER=true;AUTO_SERVER_PORT=9092 jpa.hibernate.ddl-auto: create-drop + security.oauth2.resourceserver.jwt.public-key-location: classpath:local-public-key.pub seed.database: true +wiremock.enabled: true +context.initializer.classes: uk.gov.justice.digital.hmpps.wiremock.WireMockInitialiser messaging.consumer.queue: message-queue +integrations: + cas3-api: + url: http://localhost:${wiremock.port}/cas3 + +oauth2: + client-id: $SERVICE_NAME + client-secret: $SERVICE_NAME + logging.level: uk.gov.justice.digital.hmpps: DEBUG org.hibernate.tool.schema: ERROR diff --git a/projects/cas3-and-delius/src/test/kotlin/uk/gov/justice/digital/hmpps/messaging/HandlerTest.kt b/projects/cas3-and-delius/src/test/kotlin/uk/gov/justice/digital/hmpps/messaging/HandlerTest.kt deleted file mode 100644 index 30c489ad76..0000000000 --- a/projects/cas3-and-delius/src/test/kotlin/uk/gov/justice/digital/hmpps/messaging/HandlerTest.kt +++ /dev/null @@ -1,39 +0,0 @@ -package uk.gov.justice.digital.hmpps.messaging - -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.mockito.InjectMocks -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.kotlin.verify -import uk.gov.justice.digital.hmpps.converter.NotificationConverter -import uk.gov.justice.digital.hmpps.data.generator.MessageGenerator -import uk.gov.justice.digital.hmpps.message.HmppsDomainEvent -import uk.gov.justice.digital.hmpps.message.Notification -import uk.gov.justice.digital.hmpps.telemetry.TelemetryService -import uk.gov.justice.digital.hmpps.telemetry.notificationReceived - -@ExtendWith(MockitoExtension::class) -internal class HandlerTest { - @Mock lateinit var telemetryService: TelemetryService - - @Mock lateinit var converter: NotificationConverter - - @InjectMocks lateinit var handler: Handler - - @Test - fun `message is logged to telemetry`() { - // Given a message - val notification = Notification(message = MessageGenerator.EXAMPLE) - - // When it is received - try { - handler.handle(notification) - } catch (_: NotImplementedError) { - // Note: Remove this try/catch when the Handler logic has been implemented - } - - // Then it is logged to telemetry - verify(telemetryService).notificationReceived(notification) - } -}