From bcdcef2eeb72020f72001708d119f699a457a477 Mon Sep 17 00:00:00 2001 From: Jon Brighton Date: Thu, 16 Jan 2025 22:29:55 +0000 Subject: [PATCH] CDPS-1086: Moving medical diet, allergy code over from hmpps-prison-person-api --- README.md | 2 +- build.gradle.kts | 17 +- gradle/wrapper/gradle-wrapper.properties | 2 +- .../values.yaml | 4 + helm_deploy/values-dev.yaml | 3 +- helm_deploy/values-preprod.yaml | 3 +- helm_deploy/values-prod.yaml | 3 +- .../HealthAndMedicationApi.kt | 4 +- .../annotation/NullishRange.kt | 17 ++ .../annotation/NullishReferenceDataCode.kt | 17 ++ .../NullishReferenceDataCodeList.kt | 16 ++ .../annotation/NullishShoeSize.kt | 17 ++ .../prisonersearch/PrisonerSearchClient.kt | 24 ++ .../client/prisonersearch/dto/PrisonerDto.kt | 6 + .../config/ClockConfiguration.kt | 15 + .../config/CustomisedJacksonObjectMapper.kt | 31 +++ .../HealthAndMedicationExceptionHandler.kt | 259 ++++++++++++++++++ .../config/OpenApiConfiguration.kt | 119 ++++++++ .../config/WebClientConfiguration.kt | 35 +++ .../dto/ReferenceDataCodeDto.kt | 70 +++++ .../dto/ReferenceDataDomainDto.kt | 74 +++++ .../dto/ReferenceDataSimpleDto.kt | 30 ++ .../request/PrisonerHealthUpdateRequest.kt | 43 +++ .../dto/response/HealthDto.kt | 16 ++ .../dto/response/ValueWithMetadata.kt | 15 + .../enums/HealthAndMedicationField.kt | 59 ++++ .../health/HealthPingCheck.kt | 2 +- .../healthandmedication/jpa/FieldHistory.kt | 103 +++++++ .../healthandmedication/jpa/FieldMetadata.kt | 56 ++++ .../healthandmedication/jpa/FoodAllergy.kt | 46 ++++ .../healthandmedication/jpa/HistoryItem.kt | 17 ++ .../healthandmedication/jpa/JsonObject.kt | 18 ++ .../jpa/JsonObjectConverter.kt | 14 + .../jpa/MedicalDietaryRequirement.kt | 49 ++++ .../healthandmedication/jpa/PrisonerHealth.kt | 126 +++++++++ .../jpa/ReferenceDataCode.kt | 59 ++++ .../jpa/ReferenceDataDomain.kt | 54 ++++ .../jpa/WithFieldHistory.kt | 54 ++++ .../repository/PrisonerHealthRepository.kt | 8 + .../repository/ReferenceDataCodeRepository.kt | 32 +++ .../ReferenceDataDomainRepository.kt | 36 +++ .../mapper/ReferenceDataCodeMapper.kt | 30 ++ .../mapper/ReferenceDataDomainMapper.kt | 22 ++ .../resource/HealthAndMedicationResource.kt | 100 +++++++ .../resource/ReferenceDataCodeResource.kt | 93 +++++++ .../resource/ReferenceDataDomainResource.kt | 91 ++++++ .../service/PrisonerHealthService.kt | 68 +++++ .../service/ReferenceDataCodeService.kt | 32 +++ .../service/ReferenceDataDomainService.kt | 21 ++ .../utils/AuthenticationFacade.kt | 35 +++ .../healthandmedication/utils/Nullish.kt | 85 ++++++ .../utils/PrisonerNumberUtils.kt | 5 + .../utils/ReferenceCodeUtils.kt | 18 ++ .../utils/UuidV7Generator.kt | 36 +++ .../validator/NullishRangeValidator.kt | 24 ++ .../NullishReferenceDataCodeListValidator.kt | 40 +++ .../NullishReferenceDataCodeValidator.kt | 37 +++ .../validator/NullishShoeSizeValidator.kt | 34 +++ .../HealthAndMedicationApiExceptionHandler.kt | 65 ----- .../config/OpenApiConfiguration.kt | 58 ---- .../config/WebClientConfiguration.kt | 18 -- .../resource/ExampleResource.kt | 77 ------ .../service/ExampleApiService.kt | 9 - src/main/resources/application.yml | 21 +- .../migration/common/V1__reference_data.sql | 54 ++++ .../migration/common/V2__field_metadata.sql | 15 + .../db/migration/common/V3__field_history.sql | 37 +++ .../db/migration/common/V4__health.sql | 12 + .../V5__medical_dietary_requirement.sql | 18 ++ .../V6__medical_dietary_requirement_data.sql | 17 ++ .../db/migration/common/V7__food_allergy.sql | 18 ++ .../common/V8__food_allergy_data.sql | 22 ++ .../resources/db/migration/common/placeholder | 0 .../test/V2_1__test_reference_data_seed.sql | 26 ++ .../test/V9_1__reference_data_subdomains.sql | 21 ++ src/main/resources/logback-spring.xml | 2 +- .../config/FixedClock.kt | 2 +- .../integration/IntegrationTestBase.kt | 6 +- .../integration/NotFoundTest.kt | 2 +- .../integration/OpenApiDocsIntTest.kt | 100 +++++++ .../integration/ResourceSecurityTest.kt | 15 +- .../integration/TestBase.kt | 6 +- .../integration/health/HealthCheckTest.kt | 4 +- .../integration/health/InfoTest.kt | 4 +- .../testcontainers/PostgresContainer.kt | 15 +- .../wiremock/HmppsAuthMockServer.kt | 7 +- .../integration/ExampleResourceIntTest.kt | 106 ------- .../integration/OpenApiDocsTest.kt | 105 ------- src/test/resources/application-test.yml | 4 + 89 files changed, 2726 insertions(+), 486 deletions(-) rename src/main/kotlin/uk/gov/justice/digital/hmpps/{healthandmedicationapi => healthandmedication}/HealthAndMedicationApi.kt (68%) create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/annotation/NullishRange.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/annotation/NullishReferenceDataCode.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/annotation/NullishReferenceDataCodeList.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/annotation/NullishShoeSize.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/client/prisonersearch/PrisonerSearchClient.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/client/prisonersearch/dto/PrisonerDto.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/config/ClockConfiguration.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/config/CustomisedJacksonObjectMapper.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/config/HealthAndMedicationExceptionHandler.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/config/OpenApiConfiguration.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/config/WebClientConfiguration.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/dto/ReferenceDataCodeDto.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/dto/ReferenceDataDomainDto.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/dto/ReferenceDataSimpleDto.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/dto/request/PrisonerHealthUpdateRequest.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/dto/response/HealthDto.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/dto/response/ValueWithMetadata.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/enums/HealthAndMedicationField.kt rename src/main/kotlin/uk/gov/justice/digital/hmpps/{healthandmedicationapi => healthandmedication}/health/HealthPingCheck.kt (84%) create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/FieldHistory.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/FieldMetadata.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/FoodAllergy.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/HistoryItem.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/JsonObject.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/JsonObjectConverter.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/MedicalDietaryRequirement.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/PrisonerHealth.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/ReferenceDataCode.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/ReferenceDataDomain.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/WithFieldHistory.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/repository/PrisonerHealthRepository.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/repository/ReferenceDataCodeRepository.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/repository/ReferenceDataDomainRepository.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/mapper/ReferenceDataCodeMapper.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/mapper/ReferenceDataDomainMapper.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/resource/HealthAndMedicationResource.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/resource/ReferenceDataCodeResource.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/resource/ReferenceDataDomainResource.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/service/PrisonerHealthService.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/service/ReferenceDataCodeService.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/service/ReferenceDataDomainService.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/utils/AuthenticationFacade.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/utils/Nullish.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/utils/PrisonerNumberUtils.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/utils/ReferenceCodeUtils.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/utils/UuidV7Generator.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/validator/NullishRangeValidator.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/validator/NullishReferenceDataCodeListValidator.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/validator/NullishReferenceDataCodeValidator.kt create mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/validator/NullishShoeSizeValidator.kt delete mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/config/HealthAndMedicationApiExceptionHandler.kt delete mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/config/OpenApiConfiguration.kt delete mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/config/WebClientConfiguration.kt delete mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/resource/ExampleResource.kt delete mode 100644 src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/service/ExampleApiService.kt create mode 100644 src/main/resources/db/migration/common/V1__reference_data.sql create mode 100644 src/main/resources/db/migration/common/V2__field_metadata.sql create mode 100644 src/main/resources/db/migration/common/V3__field_history.sql create mode 100644 src/main/resources/db/migration/common/V4__health.sql create mode 100644 src/main/resources/db/migration/common/V5__medical_dietary_requirement.sql create mode 100644 src/main/resources/db/migration/common/V6__medical_dietary_requirement_data.sql create mode 100644 src/main/resources/db/migration/common/V7__food_allergy.sql create mode 100644 src/main/resources/db/migration/common/V8__food_allergy_data.sql delete mode 100644 src/main/resources/db/migration/common/placeholder create mode 100644 src/main/resources/db/migration/test/V2_1__test_reference_data_seed.sql create mode 100644 src/main/resources/db/migration/test/V9_1__reference_data_subdomains.sql rename src/test/kotlin/uk/gov/justice/digital/hmpps/{healthandmedicationapi => healthandmedication}/config/FixedClock.kt (86%) rename src/test/kotlin/uk/gov/justice/digital/hmpps/{healthandmedicationapi => healthandmedication}/integration/IntegrationTestBase.kt (78%) rename src/test/kotlin/uk/gov/justice/digital/hmpps/{healthandmedicationapi => healthandmedication}/integration/NotFoundTest.kt (81%) create mode 100644 src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/integration/OpenApiDocsIntTest.kt rename src/test/kotlin/uk/gov/justice/digital/hmpps/{healthandmedicationapi => healthandmedication}/integration/ResourceSecurityTest.kt (84%) rename src/test/kotlin/uk/gov/justice/digital/hmpps/{healthandmedicationapi => healthandmedication}/integration/TestBase.kt (78%) rename src/test/kotlin/uk/gov/justice/digital/hmpps/{healthandmedicationapi => healthandmedication}/integration/health/HealthCheckTest.kt (88%) rename src/test/kotlin/uk/gov/justice/digital/hmpps/{healthandmedicationapi => healthandmedication}/integration/health/InfoTest.kt (81%) rename src/test/kotlin/uk/gov/justice/digital/hmpps/{healthandmedicationapi => healthandmedication}/integration/testcontainers/PostgresContainer.kt (78%) rename src/test/kotlin/uk/gov/justice/digital/hmpps/{healthandmedicationapi => healthandmedication}/integration/wiremock/HmppsAuthMockServer.kt (92%) delete mode 100644 src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/integration/ExampleResourceIntTest.kt delete mode 100644 src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/integration/OpenApiDocsTest.kt diff --git a/README.md b/README.md index 1608324..9cdef9c 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![Docker Repository on Quay](https://img.shields.io/badge/quay.io-repository-2496ED.svg?logo=docker)](https://quay.io/repository/hmpps/hmpps-health-and-medication-api) [![API docs](https://img.shields.io/badge/API_docs_-view-85EA2D.svg?logo=swagger)](https://health-and-medication-api-dev.prison.service.justice.gov.uk/swagger-ui/index.html) -This is a Spring Boot service, written in Kotlin, which owns a subset of data about a person in prison (migrated from NOMIS). +This is a Spring Boot service, written in Kotlin, which owns health and medication data about a person in prison. ## Contents diff --git a/build.gradle.kts b/build.gradle.kts index 39367d2..dbd5815 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("uk.gov.justice.hmpps.gradle-spring-boot") version "6.1.0" + id("uk.gov.justice.hmpps.gradle-spring-boot") version "6.0.9" kotlin("plugin.spring") version "2.0.21" kotlin("plugin.jpa") version "2.0.21" jacoco @@ -13,12 +13,15 @@ configurations { dependencies { // Spring Boot - implementation("uk.gov.justice.service.hmpps:hmpps-kotlin-spring-boot-starter:1.1.0") + implementation("uk.gov.justice.service.hmpps:hmpps-kotlin-spring-boot-starter:1.0.8") implementation("org.springframework.boot:spring-boot-starter-webflux") implementation("org.springframework.boot:spring-boot-starter-data-jpa") // OpenAPI - implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0") + implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0") + + // UUIDs + implementation("com.fasterxml.uuid:java-uuid-generator:5.1.0") // Database runtimeOnly("com.zaxxer:HikariCP") @@ -26,10 +29,10 @@ dependencies { runtimeOnly("org.postgresql:postgresql") // Test - testImplementation("uk.gov.justice.service.hmpps:hmpps-kotlin-spring-boot-starter-test:1.1.0") - testImplementation("org.testcontainers:junit-jupiter:1.20.3") - testImplementation("org.testcontainers:postgresql:1.20.3") - testImplementation("org.wiremock:wiremock-standalone:3.9.2") + testImplementation("uk.gov.justice.service.hmpps:hmpps-kotlin-spring-boot-starter-test:1.1.1") + testImplementation("org.testcontainers:junit-jupiter:1.20.4") + testImplementation("org.testcontainers:postgresql:1.20.4") + testImplementation("org.wiremock:wiremock-standalone:3.10.0") testImplementation("io.swagger.parser.v3:swagger-parser:2.1.24") { exclude(group = "io.swagger.core.v3") } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e2847c8..cea7a79 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/helm_deploy/hmpps-health-and-medication-api/values.yaml b/helm_deploy/hmpps-health-and-medication-api/values.yaml index 0b986c9..5e006d8 100644 --- a/helm_deploy/hmpps-health-and-medication-api/values.yaml +++ b/helm_deploy/hmpps-health-and-medication-api/values.yaml @@ -26,6 +26,10 @@ generic-service: # [name of environment variable as seen by app]: [key of kubernetes secret to load] namespace_secrets: + hmpps-health-and-medication-api: + CLIENT_ID: "CLIENT_ID" + CLIENT_SECRET: "CLIENT_SECRET" + application-insights: APPLICATIONINSIGHTS_CONNECTION_STRING: "APPLICATIONINSIGHTS_CONNECTION_STRING" diff --git a/helm_deploy/values-dev.yaml b/helm_deploy/values-dev.yaml index 42b8387..866943b 100644 --- a/helm_deploy/values-dev.yaml +++ b/helm_deploy/values-dev.yaml @@ -9,7 +9,8 @@ generic-service: env: APPLICATIONINSIGHTS_CONFIGURATION_FILE: "applicationinsights.dev.json" - HMPPS_AUTH_URL: "https://sign-in-dev.hmpps.service.justice.gov.uk/auth" + API_HMPPS_AUTH_BASE_URL: "https://sign-in-dev.hmpps.service.justice.gov.uk/auth" + API_PRISONER_SEARCH_BASE_URL: "https://prisoner-search-dev.prison.service.justice.gov.uk" scheduledDowntime: enabled: true diff --git a/helm_deploy/values-preprod.yaml b/helm_deploy/values-preprod.yaml index eeb3820..abd7d51 100644 --- a/helm_deploy/values-preprod.yaml +++ b/helm_deploy/values-preprod.yaml @@ -9,7 +9,8 @@ generic-service: env: APPLICATIONINSIGHTS_CONFIGURATION_FILE: "applicationinsights.dev.json" - HMPPS_AUTH_URL: "https://sign-in-preprod.hmpps.service.justice.gov.uk/auth" + API_HMPPS_AUTH_BASE_URL: "https://sign-in-preprod.hmpps.service.justice.gov.uk/auth" + API_PRISONER_SEARCH_BASE_URL: "https://prisoner-search-preprod.prison.service.justice.gov.uk" # CloudPlatform AlertManager receiver to route prometheus alerts to slack # See https://user-guide.cloud-platform.service.justice.gov.uk/documentation/monitoring-an-app/how-to-create-alarms.html#creating-your-own-custom-alerts diff --git a/helm_deploy/values-prod.yaml b/helm_deploy/values-prod.yaml index c6433d4..d7b2acc 100644 --- a/helm_deploy/values-prod.yaml +++ b/helm_deploy/values-prod.yaml @@ -6,7 +6,8 @@ generic-service: host: health-and-medication-api.hmpps.service.justice.gov.uk env: - HMPPS_AUTH_URL: "https://sign-in.hmpps.service.justice.gov.uk/auth" + API_HMPPS_AUTH_BASE_URL: "https://sign-in.hmpps.service.justice.gov.uk/auth" + API_PRISONER_SEARCH_BASE_URL: "https://prisoner-search.prison.service.justice.gov.uk" # CloudPlatform AlertManager receiver to route prometheus alerts to slack # See https://user-guide.cloud-platform.service.justice.gov.uk/documentation/monitoring-an-app/how-to-create-alarms.html#creating-your-own-custom-alerts diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/HealthAndMedicationApi.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/HealthAndMedicationApi.kt similarity index 68% rename from src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/HealthAndMedicationApi.kt rename to src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/HealthAndMedicationApi.kt index 568cc56..5a1fc7c 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/HealthAndMedicationApi.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/HealthAndMedicationApi.kt @@ -1,8 +1,10 @@ -package uk.gov.justice.digital.hmpps.healthandmedicationapi +package uk.gov.justice.digital.hmpps.healthandmedication import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication +const val SYSTEM_USERNAME = "HEALTH_AND_MEDICATION_API" + @SpringBootApplication class HealthAndMedicationApi diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/annotation/NullishRange.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/annotation/NullishRange.kt new file mode 100644 index 0000000..d0d19a7 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/annotation/NullishRange.kt @@ -0,0 +1,17 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.annotation + +import jakarta.validation.Constraint +import jakarta.validation.Payload +import uk.gov.justice.digital.hmpps.healthandmedication.validator.NullishRangeValidator +import kotlin.reflect.KClass + +@Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER) +@Retention(AnnotationRetention.RUNTIME) +@Constraint(validatedBy = [NullishRangeValidator::class]) +annotation class NullishRange( + val min: Int, + val max: Int, + val message: String = "The value must be within the specified range, null or Undefined.", + val groups: Array> = [], + val payload: Array> = [], +) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/annotation/NullishReferenceDataCode.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/annotation/NullishReferenceDataCode.kt new file mode 100644 index 0000000..c9e88a6 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/annotation/NullishReferenceDataCode.kt @@ -0,0 +1,17 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.annotation + +import jakarta.validation.Constraint +import jakarta.validation.Payload +import uk.gov.justice.digital.hmpps.healthandmedication.validator.NullishReferenceDataCodeValidator +import kotlin.reflect.KClass + +@Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER) +@Retention(AnnotationRetention.RUNTIME) +@Constraint(validatedBy = [NullishReferenceDataCodeValidator::class]) +annotation class NullishReferenceDataCode( + val domains: Array = [], + val allowNull: Boolean = true, + val message: String = "The value must be a reference domain code id of the correct domain, null, or Undefined.", + val groups: Array> = [], + val payload: Array> = [], +) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/annotation/NullishReferenceDataCodeList.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/annotation/NullishReferenceDataCodeList.kt new file mode 100644 index 0000000..85e0b1c --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/annotation/NullishReferenceDataCodeList.kt @@ -0,0 +1,16 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.annotation + +import jakarta.validation.Constraint +import jakarta.validation.Payload +import uk.gov.justice.digital.hmpps.healthandmedication.validator.NullishReferenceDataCodeListValidator +import kotlin.reflect.KClass + +@Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER) +@Retention(AnnotationRetention.RUNTIME) +@Constraint(validatedBy = [NullishReferenceDataCodeListValidator::class]) +annotation class NullishReferenceDataCodeList( + val domains: Array = [], + val message: String = "The value must be a a list of domain codes of the correct domain, an empty list, or Undefined.", + val groups: Array> = [], + val payload: Array> = [], +) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/annotation/NullishShoeSize.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/annotation/NullishShoeSize.kt new file mode 100644 index 0000000..dd8ab90 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/annotation/NullishShoeSize.kt @@ -0,0 +1,17 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.annotation + +import jakarta.validation.Constraint +import jakarta.validation.Payload +import uk.gov.justice.digital.hmpps.healthandmedication.validator.NullishShoeSizeValidator +import kotlin.reflect.KClass + +@Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER) +@Retention(AnnotationRetention.RUNTIME) +@Constraint(validatedBy = [NullishShoeSizeValidator::class]) +annotation class NullishShoeSize( + val min: String, + val max: String, + val message: String = "The value must be a whole or half shoe size within the specified range, null or Undefined.", + val groups: Array> = [], + val payload: Array> = [], +) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/client/prisonersearch/PrisonerSearchClient.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/client/prisonersearch/PrisonerSearchClient.kt new file mode 100644 index 0000000..ff7589a --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/client/prisonersearch/PrisonerSearchClient.kt @@ -0,0 +1,24 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.client.prisonersearch + +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.stereotype.Component +import org.springframework.web.reactive.function.client.WebClient +import org.springframework.web.reactive.function.client.WebClientResponseException.NotFound +import uk.gov.justice.digital.hmpps.healthandmedication.client.prisonersearch.dto.PrisonerDto +import uk.gov.justice.digital.hmpps.healthandmedication.config.DownstreamServiceException + +@Component +class PrisonerSearchClient(@Qualifier("prisonerSearchWebClient") private val webClient: WebClient) { + fun getPrisoner(prisonerNumber: String): PrisonerDto? = try { + webClient + .get() + .uri("/prisoner/{prisonerNumber}", prisonerNumber) + .retrieve() + .bodyToMono(PrisonerDto::class.java) + .block() + } catch (e: NotFound) { + null + } catch (e: Exception) { + throw DownstreamServiceException("Get prisoner request failed", e) + } +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/client/prisonersearch/dto/PrisonerDto.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/client/prisonersearch/dto/PrisonerDto.kt new file mode 100644 index 0000000..fceb948 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/client/prisonersearch/dto/PrisonerDto.kt @@ -0,0 +1,6 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.client.prisonersearch.dto + +data class PrisonerDto( + val prisonerNumber: String, + val prisonId: String? = null, +) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/config/ClockConfiguration.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/config/ClockConfiguration.kt new file mode 100644 index 0000000..4fc24b9 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/config/ClockConfiguration.kt @@ -0,0 +1,15 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.config + +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import java.time.Clock +import java.time.ZoneId + +@Configuration +class ClockConfiguration( + @Value("\${spring.jackson.time-zone}") private val timeZone: String, +) { + @Bean + fun clock(): Clock? = Clock.system(ZoneId.of(timeZone)) +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/config/CustomisedJacksonObjectMapper.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/config/CustomisedJacksonObjectMapper.kt new file mode 100644 index 0000000..dcab615 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/config/CustomisedJacksonObjectMapper.kt @@ -0,0 +1,31 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.config + +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer +import com.fasterxml.jackson.datatype.jsr310.ser.ZonedDateTimeSerializer +import org.springframework.beans.factory.annotation.Value +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import java.time.ZoneId +import java.time.format.DateTimeFormatter + +@Configuration +class CustomisedJacksonObjectMapper( + @Value("\${spring.jackson.time-zone}") private val timeZone: String, + @Value("\${spring.jackson.date-format}") private val zonedDateTimeFormat: String, +) { + @Bean + fun serialiser() = Jackson2ObjectMapperBuilderCustomizer { + val zoneId = ZoneId.of(timeZone) + val naiveDateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd").withZone(zoneId) + val naiveDateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss").withZone(zoneId) + val zonedDateTimeFormatter = DateTimeFormatter.ofPattern(zonedDateTimeFormat).withZone(zoneId) + + it.serializers( + LocalDateSerializer(naiveDateFormatter), + LocalDateTimeSerializer(naiveDateTimeFormatter), + ZonedDateTimeSerializer(zonedDateTimeFormatter), + ) + } +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/config/HealthAndMedicationExceptionHandler.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/config/HealthAndMedicationExceptionHandler.kt new file mode 100644 index 0000000..aa4bb6a --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/config/HealthAndMedicationExceptionHandler.kt @@ -0,0 +1,259 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.config + +import jakarta.servlet.ServletException +import jakarta.validation.ValidationException +import org.apache.commons.lang3.StringUtils +import org.slf4j.LoggerFactory +import org.springframework.http.HttpStatus +import org.springframework.http.HttpStatus.BAD_REQUEST +import org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR +import org.springframework.http.HttpStatus.METHOD_NOT_ALLOWED +import org.springframework.http.HttpStatus.NOT_FOUND +import org.springframework.http.HttpStatus.NOT_IMPLEMENTED +import org.springframework.http.ResponseEntity +import org.springframework.http.converter.HttpMessageNotReadableException +import org.springframework.security.access.AccessDeniedException +import org.springframework.web.HttpRequestMethodNotSupportedException +import org.springframework.web.bind.MethodArgumentNotValidException +import org.springframework.web.bind.MissingServletRequestParameterException +import org.springframework.web.bind.annotation.ExceptionHandler +import org.springframework.web.bind.annotation.RestControllerAdvice +import org.springframework.web.method.annotation.HandlerMethodValidationException +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException +import org.springframework.web.multipart.support.MissingServletRequestPartException +import org.springframework.web.servlet.resource.NoResourceFoundException +import uk.gov.justice.hmpps.kotlin.common.ErrorResponse + +@RestControllerAdvice +class HealthAndMedicationExceptionHandler { + + @ExceptionHandler(AccessDeniedException::class) + fun handleAccessDeniedException(e: AccessDeniedException): ResponseEntity = ResponseEntity + .status(HttpStatus.FORBIDDEN) + .body( + ErrorResponse( + status = HttpStatus.FORBIDDEN.value(), + userMessage = "Authentication problem. Check token and roles - ${e.message}", + developerMessage = e.message, + ), + ).also { log.info("Access denied exception: {}", e.message) } + + @ExceptionHandler(MissingServletRequestParameterException::class) + fun handleMissingServletRequestParameterException(e: MissingServletRequestParameterException): ResponseEntity = ResponseEntity + .status(BAD_REQUEST) + .body( + ErrorResponse( + status = BAD_REQUEST, + userMessage = "Validation failure: ${e.message}", + developerMessage = e.message, + ), + ).also { log.info("Missing servlet request parameter exception: {}", e.message) } + + @ExceptionHandler(MethodArgumentTypeMismatchException::class) + fun handleMethodArgumentTypeMismatchException(e: MethodArgumentTypeMismatchException): ResponseEntity { + val type = e.requiredType + val message = if (type.isEnum) { + "Parameter ${e.name} must be one of the following ${StringUtils.join(type.enumConstants, ", ")}" + } else { + "Parameter ${e.name} must be of type ${type.typeName}" + } + + return ResponseEntity + .status(BAD_REQUEST) + .body( + ErrorResponse( + status = BAD_REQUEST, + userMessage = "Validation failure: $message", + developerMessage = e.message, + ), + ).also { log.info("Method argument type mismatch exception: {}", e.message) } + } + + @ExceptionHandler(HttpMessageNotReadableException::class) + fun handleHttpMessageNotReadableException(e: HttpMessageNotReadableException): ResponseEntity = ResponseEntity + .status(BAD_REQUEST) + .body( + ErrorResponse( + status = BAD_REQUEST, + userMessage = "Validation failure: Couldn't read request body", + developerMessage = e.message, + ), + ).also { log.info("HTTP message not readable exception: {}", e.message) } + + @ExceptionHandler(MethodArgumentNotValidException::class) + fun handleValidationException(e: MethodArgumentNotValidException): ResponseEntity = ResponseEntity + .status(BAD_REQUEST) + .body( + ErrorResponse( + status = BAD_REQUEST, + userMessage = "Validation failure(s): ${ + e.allErrors.map { it.defaultMessage }.distinct().sorted().joinToString("\n") + }", + developerMessage = e.message, + ), + ).also { log.info("Validation exception: {}", e.message) } + + @ExceptionHandler(IllegalArgumentException::class) + fun handleIllegalArgumentException(e: IllegalArgumentException): ResponseEntity = ResponseEntity + .status(BAD_REQUEST) + .body( + ErrorResponse( + status = BAD_REQUEST, + userMessage = "Validation failure: ${e.message}", + developerMessage = e.message, + ), + ).also { log.info("Illegal argument exception: {}", e.message) } + + @ExceptionHandler(ValidationException::class) + fun handleValidationException(e: ValidationException): ResponseEntity = ResponseEntity + .status(BAD_REQUEST) + .body( + ErrorResponse( + status = BAD_REQUEST, + userMessage = "Validation failure: ${e.message}", + developerMessage = e.message, + ), + ).also { log.info("Validation exception: {}", e.message) } + + @ExceptionHandler(HandlerMethodValidationException::class) + fun handleHandlerMethodValidationException(e: HandlerMethodValidationException): ResponseEntity = e.allErrors.map { it.toString() }.distinct().sorted().joinToString("\n").let { validationErrors -> + ResponseEntity + .status(BAD_REQUEST) + .body( + ErrorResponse( + status = BAD_REQUEST, + userMessage = "Validation failure(s): ${ + e.allErrors.map { it.defaultMessage }.distinct().sorted().joinToString("\n") + }", + developerMessage = "${e.message} $validationErrors", + ), + ).also { log.info("Validation exception: $validationErrors\n {}", e.message) } + } + + @ExceptionHandler(HealthAndMedicationDataNotFoundException::class) + fun handlePrisonPersonDataNotFoundException(e: HealthAndMedicationDataNotFoundException): ResponseEntity = ResponseEntity + .status(NOT_FOUND) + .body( + ErrorResponse( + status = NOT_FOUND, + userMessage = "Health and medication data not found: ${e.message}", + developerMessage = e.message, + ), + ).also { log.info("Health and medication data not found exception: {}", e.message) } + + @ExceptionHandler(ReferenceDataDomainNotFoundException::class) + fun handleReferenceDataDomainNotFoundException(e: ReferenceDataDomainNotFoundException): ResponseEntity = ResponseEntity + .status(NOT_FOUND) + .body( + ErrorResponse( + status = NOT_FOUND, + userMessage = "Reference data domain not found: ${e.message}", + developerMessage = e.message, + ), + ).also { log.info("Reference data domain not found exception: {}", e.message) } + + @ExceptionHandler(ReferenceDataCodeNotFoundException::class) + fun handleReferenceDataCodeNotFoundException(e: ReferenceDataCodeNotFoundException): ResponseEntity = ResponseEntity + .status(NOT_FOUND) + .body( + ErrorResponse( + status = NOT_FOUND, + userMessage = "Reference data code not found: ${e.message}", + developerMessage = e.message, + ), + ).also { log.info("Reference data code not found exception: {}", e.message) } + + @ExceptionHandler(IllegalFieldHistoryException::class) + fun handleIllegalFieldHistoryException(e: IllegalFieldHistoryException): ResponseEntity = ResponseEntity + .status(INTERNAL_SERVER_ERROR) + .body( + ErrorResponse( + status = INTERNAL_SERVER_ERROR, + userMessage = "Illegal field history: ${e.message}", + developerMessage = e.message, + ), + ).also { log.error("Illegal field history exception: {}", e.message) } + + @ExceptionHandler(GenericNotFoundException::class) + fun handleGenericNotFoundException(e: GenericNotFoundException): ResponseEntity = ResponseEntity + .status(NOT_FOUND) + .body( + ErrorResponse( + status = NOT_FOUND, + userMessage = e.message, + developerMessage = e.message, + ), + ) + + @ExceptionHandler(NoResourceFoundException::class) + fun handleNoResourceFoundException(e: NoResourceFoundException): ResponseEntity = ResponseEntity + .status(NOT_FOUND) + .body( + ErrorResponse( + status = NOT_FOUND, + userMessage = "No resource found failure: ${e.message}", + developerMessage = e.message, + ), + ).also { log.info("No resource found exception: {}", e.message) } + + @ExceptionHandler(HttpRequestMethodNotSupportedException::class) + fun handleHttpRequestMethodNotSupportedException(e: HttpRequestMethodNotSupportedException): ResponseEntity = ResponseEntity + .status(METHOD_NOT_ALLOWED) + .body( + ErrorResponse( + status = METHOD_NOT_ALLOWED, + userMessage = "Method not allowed failure: ${e.message}", + developerMessage = e.message, + ), + ).also { log.info("Method not allowed exception: {}", e.message) } + + @ExceptionHandler(ServletException::class) + fun handleNotImplementedError(e: ServletException): ResponseEntity = if (e.rootCause is NotImplementedError) { + ResponseEntity + .status(NOT_IMPLEMENTED) + .body( + ErrorResponse( + status = NOT_IMPLEMENTED, + userMessage = "Not implemented: ${e.message}", + developerMessage = e.message, + ), + ).also { log.info("Not implemented: {}", e.message) } + } else { + handleUnexpectedException(e) + } + + @ExceptionHandler(Exception::class) + fun handleException(e: Exception): ResponseEntity = handleUnexpectedException(e) + + fun handleUnexpectedException(e: Exception): ResponseEntity = ResponseEntity + .status(INTERNAL_SERVER_ERROR) + .body( + ErrorResponse( + status = INTERNAL_SERVER_ERROR, + userMessage = "Unexpected error: ${e.message}", + developerMessage = e.message, + ), + ).also { log.error("Unexpected exception", e) } + + @ExceptionHandler(MissingServletRequestPartException::class) + fun handleMissingServletRequestPartException(e: MissingServletRequestPartException): ResponseEntity = ResponseEntity + .status(BAD_REQUEST) + .body( + ErrorResponse( + status = BAD_REQUEST, + userMessage = "Required request part is missing: ${e.message}", + developerMessage = e.message, + ), + ).also { log.info("Missing request part exception: {}", e.message) } + + private companion object { + private val log = LoggerFactory.getLogger(this::class.java) + } +} + +class HealthAndMedicationDataNotFoundException(prisonerNumber: String) : Exception("No data for '$prisonerNumber'") +class ReferenceDataDomainNotFoundException(code: String) : Exception("No data for domain '$code'") +class ReferenceDataCodeNotFoundException(code: String, domain: String) : Exception("No data for code '$code' in domain '$domain'") +class GenericNotFoundException(message: String) : Exception(message) +class IllegalFieldHistoryException(prisonerNumber: String) : Exception("Cannot update field history for prisoner: '$prisonerNumber'") +class DownstreamServiceException(message: String, cause: Throwable) : Exception(message, cause) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/config/OpenApiConfiguration.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/config/OpenApiConfiguration.kt new file mode 100644 index 0000000..5e1c7dc --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/config/OpenApiConfiguration.kt @@ -0,0 +1,119 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.config + +import io.swagger.v3.core.util.PrimitiveType +import io.swagger.v3.oas.models.Components +import io.swagger.v3.oas.models.OpenAPI +import io.swagger.v3.oas.models.info.Contact +import io.swagger.v3.oas.models.info.Info +import io.swagger.v3.oas.models.media.DateTimeSchema +import io.swagger.v3.oas.models.media.Schema +import io.swagger.v3.oas.models.media.StringSchema +import io.swagger.v3.oas.models.security.OAuthFlow +import io.swagger.v3.oas.models.security.OAuthFlows +import io.swagger.v3.oas.models.security.Scopes +import io.swagger.v3.oas.models.security.SecurityRequirement +import io.swagger.v3.oas.models.security.SecurityScheme +import io.swagger.v3.oas.models.servers.Server +import io.swagger.v3.oas.models.tags.Tag +import org.springdoc.core.customizers.OpenApiCustomizer +import org.springframework.beans.factory.annotation.Value +import org.springframework.boot.info.BuildProperties +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class OpenApiConfiguration( + buildProperties: BuildProperties, + @Value("\${api.hmpps-auth.base-url}") val oauthUrl: String, +) { + private val version: String = buildProperties.version + + @Bean + fun customOpenAPI(): OpenAPI = OpenAPI() + .servers( + listOf( + Server().url("https://health-and-medication-api-dev.hmpps.service.justice.gov.uk").description("Development"), + Server().url("https://health-and-medication-api-preprod.hmpps.service.justice.gov.uk").description("Pre-Production"), + Server().url("https://health-and-medication-api.hmpps.service.justice.gov.uk").description("Production"), + Server().url("http://localhost:8080").description("Local"), + ), + ) + .tags( + listOf( + // TODO: Remove the Popular and Examples tag and start adding your own tags to group your resources + Tag().name("Popular") + .description("The most popular endpoints. Look here first when deciding which endpoint to use."), + Tag().name("Examples").description("Endpoints for searching for a prisoner within a prison"), + ), + ) + .info( + Info().title("HMPPS Health And Medication Api").version(version) + .contact(Contact().name("HMPPS Digital Studio").email("feedback@digital.justice.gov.uk")), + ) + // TODO: Remove the default security schema and start adding your own schemas and roles to describe your + // service authorisation requirements + .components( + Components().addSecuritySchemes( + "bearer-jwt", + SecurityScheme() + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT") + .`in`(SecurityScheme.In.HEADER) + .name("Authorization"), + ) + .addSecuritySchemes( + "hmpps-auth", + SecurityScheme() + .flows(getFlows()) + .type(SecurityScheme.Type.OAUTH2) + .openIdConnectUrl("$oauthUrl/.well-known/openid-configuration"), + ), + ) + .addSecurityItem(SecurityRequirement().addList("bearer-jwt", listOf("read", "write"))) + + fun getFlows(): OAuthFlows { + val flows = OAuthFlows() + val clientCredflow = OAuthFlow() + clientCredflow.tokenUrl = "$oauthUrl/oauth/token" + val scopes = Scopes() + .addString("read", "Allows read of data") + .addString("write", "Allows write of data") + clientCredflow.scopes = scopes + val authflow = OAuthFlow() + authflow.authorizationUrl = "$oauthUrl/oauth/authorize" + authflow.tokenUrl = "$oauthUrl/oauth/token" + authflow.scopes = scopes + return flows.clientCredentials(clientCredflow).authorizationCode(authflow) + } + + @Bean + fun openAPICustomiser(): OpenApiCustomizer { + PrimitiveType.enablePartialTime() // Prevents generation of a LocalTime schema which causes conflicts with java.time.LocalTime + return OpenApiCustomizer { + it.components.schemas.forEach { (_, schema: Schema<*>) -> + val properties = schema.properties ?: mutableMapOf() + for (propertyName in properties.keys) { + val propertySchema = properties[propertyName]!! + if (propertySchema is DateTimeSchema) { + properties.replace( + propertyName, + StringSchema() + .example("2024-06-14T10:35:17+0100") + .format("yyyy-MM-dd'T'HH:mm:ssX") + .description(propertySchema.description) + .required(propertySchema.required), + ) + } + } + } + } + } +} + +private fun SecurityScheme.addBearerJwtRequirement(role: String): SecurityScheme = type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT") + .`in`(SecurityScheme.In.HEADER) + .name("Authorization") + .description("A HMPPS Auth access token with the `$role` role.") diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/config/WebClientConfiguration.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/config/WebClientConfiguration.kt new file mode 100644 index 0000000..3b6d718 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/config/WebClientConfiguration.kt @@ -0,0 +1,35 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.config + +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager +import org.springframework.web.reactive.function.client.WebClient +import org.springframework.web.reactive.function.client.WebClient.Builder +import uk.gov.justice.hmpps.kotlin.auth.authorisedWebClient +import uk.gov.justice.hmpps.kotlin.auth.healthWebClient +import java.time.Duration + +@Configuration +class WebClientConfiguration( + @Value("\${api.hmpps-auth.base-url}") private val hmppsAuthBaseUri: String, + @Value("\${api.hmpps-auth.health-timeout:20s}") private val hmppsAuthHealthTimeout: Duration, + + @Value("\${api.prisoner-search.base-url}") private val prisonerSearchBaseUri: String, + @Value("\${api.prisoner-search.timeout:30s}") private val prisonerSearchTimeout: Duration, + @Value("\${api.prisoner-search.health-timeout:20s}") private val prisonerSearchHealthTimeout: Duration, +) { + @Bean + fun hmppsAuthHealthWebClient(builder: Builder): WebClient = builder.healthWebClient(hmppsAuthBaseUri, hmppsAuthHealthTimeout) + + @Bean + fun prisonerSearchHealthWebClient(builder: Builder) = builder.healthWebClient(prisonerSearchBaseUri, prisonerSearchHealthTimeout) + + @Bean + fun prisonerSearchWebClient(authorizedClientManager: OAuth2AuthorizedClientManager, builder: Builder) = builder.authorisedWebClient( + authorizedClientManager, + "hmpps-health-and-medication-api", + prisonerSearchBaseUri, + prisonerSearchTimeout, + ) +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/dto/ReferenceDataCodeDto.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/dto/ReferenceDataCodeDto.kt new file mode 100644 index 0000000..3f35978 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/dto/ReferenceDataCodeDto.kt @@ -0,0 +1,70 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.dto + +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL +import io.swagger.v3.oas.annotations.media.Schema +import java.time.ZonedDateTime + +@Schema(description = "Reference Data Code") +@JsonInclude(NON_NULL) +data class ReferenceDataCodeDto( + @Schema(description = "Id", example = "FACIAL_HAIR_BEARDED") + val id: String, + + @Schema(description = "Short code for the reference data code", example = "FACIAL_HAIR") + val domain: String, + + @Schema(description = "Short code for reference data code", example = "BEARDED") + val code: String, + + @Schema(description = "Description of the reference data code", example = "Full Beard") + val description: String, + + @Schema( + description = "The sequence number of the reference data code. " + + "Used for ordering reference data correctly in lists and dropdowns. " + + "0 is default order by description.", + example = "3", + ) + val listSequence: Int, + + @Schema( + description = "Indicates that the reference data code is active and can be used. " + + "Inactive reference data codes are not returned by default in the API", + example = "true", + ) + val isActive: Boolean, + + @Schema( + description = "The date and time the reference data code was created", + ) + val createdAt: ZonedDateTime, + + @Schema( + description = "The username of the user who created the reference data code", + example = "USER1234", + ) + val createdBy: String, + + @Schema( + description = "The date and time the reference data code was last modified", + ) + val lastModifiedAt: ZonedDateTime?, + + @Schema( + description = "The username of the user who last modified the reference data code", + example = "USER1234", + ) + val lastModifiedBy: String?, + + @Schema( + description = "The date and time the reference data code was deactivated", + ) + val deactivatedAt: ZonedDateTime?, + + @Schema( + description = "The username of the user who deactivated the reference data code", + example = "USER1234", + ) + val deactivatedBy: String?, +) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/dto/ReferenceDataDomainDto.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/dto/ReferenceDataDomainDto.kt new file mode 100644 index 0000000..85d6401 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/dto/ReferenceDataDomainDto.kt @@ -0,0 +1,74 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.dto + +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL +import io.swagger.v3.oas.annotations.media.Schema +import java.time.ZonedDateTime + +@Schema(description = "Reference Data Domain") +@JsonInclude(NON_NULL) +data class ReferenceDataDomainDto( + @Schema(description = "Short code for the reference data domain", example = "HAIR") + val code: String, + + @Schema(description = "Description of the reference data domain", example = "Hair type or colour") + val description: String, + + @Schema( + description = "The sequence number of the reference data domain. " + + "Used for ordering domains correctly in lists. " + + "0 is default order by description.", + example = "3", + ) + val listSequence: Int, + + @Schema( + description = "Indicates that the reference data domain is active and can be used. " + + "Inactive reference data domains are not returned by default in the API", + example = "true", + ) + val isActive: Boolean, + + @Schema( + description = "The date and time the reference data domain was created", + ) + val createdAt: ZonedDateTime, + + @Schema( + description = "The username of the user who created the reference data domain", + example = "USER1234", + ) + val createdBy: String, + + @Schema( + description = "The date and time the reference data domain was last modified", + ) + val lastModifiedAt: ZonedDateTime?, + + @Schema( + description = "The username of the user who last modified the reference data domain", + example = "USER1234", + ) + val lastModifiedBy: String?, + + @Schema( + description = "The date and time the reference data domain was deactivated", + ) + val deactivatedAt: ZonedDateTime?, + + @Schema( + description = "The username of the user who deactivated the reference data domain", + example = "USER1234", + ) + val deactivatedBy: String?, + + @Schema( + description = "The reference data codes associated with this reference data domain", + ) + val referenceDataCodes: Collection, + + @Schema( + description = "Reference data domains that are considered sub-domains of this domain", + ) + val subDomains: Collection, +) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/dto/ReferenceDataSimpleDto.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/dto/ReferenceDataSimpleDto.kt new file mode 100644 index 0000000..bc513df --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/dto/ReferenceDataSimpleDto.kt @@ -0,0 +1,30 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.dto + +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL +import io.swagger.v3.oas.annotations.media.Schema + +@Schema(description = "Reference Data Simple DTO - for use in dropdowns") +@JsonInclude(NON_NULL) +data class ReferenceDataSimpleDto( + @Schema(description = "Id", example = "FACIAL_HAIR_BEARDED") + val id: String, + + @Schema(description = "Description of the reference data code", example = "Full Beard") + val description: String, + + @Schema( + description = "The sequence number of the reference data code. " + + "Used for ordering reference data correctly in lists and dropdowns. " + + "0 is default order by description.", + example = "3", + ) + val listSequence: Int, + + @Schema( + description = "Indicates that the reference data code is active and can be used. " + + "Inactive reference data codes are not returned by default in the API", + example = "true", + ) + val isActive: Boolean, +) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/dto/request/PrisonerHealthUpdateRequest.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/dto/request/PrisonerHealthUpdateRequest.kt new file mode 100644 index 0000000..932afc4 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/dto/request/PrisonerHealthUpdateRequest.kt @@ -0,0 +1,43 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.dto.request + +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL +import io.swagger.v3.oas.annotations.media.Schema +import io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED +import uk.gov.justice.digital.hmpps.healthandmedication.annotation.NullishReferenceDataCodeList +import uk.gov.justice.digital.hmpps.healthandmedication.utils.Nullish +import uk.gov.justice.digital.hmpps.healthandmedication.utils.getAttributeAsNullish + +@Schema( + description = "Request object for updating a prisoner's health information. Can include one or multiple fields. " + + "If an attribute is provided and set to 'null' it will be updated equal to 'null'. " + + "If it is not provided it is not updated", +) +@JsonInclude(NON_NULL) +data class PrisonerHealthUpdateRequest( + @Schema(hidden = true) + private val attributes: MutableMap = mutableMapOf(), +) { + @Schema( + description = "The food allergies the prisoner has. A list of `ReferenceDataCode`.`id`", + type = "string[]", + example = "[FOOD_ALLERGY_EGG, FOOD_ALLERGY_MILK]", + requiredMode = NOT_REQUIRED, + ) + @field:NullishReferenceDataCodeList( + domains = ["FOOD_ALLERGY"], + ) + val foodAllergies: Nullish> = getAttributeAsNullish>(attributes, "foodAllergies") + + @Schema( + description = "The medical dietary requirements the prisoner has. A list of `ReferenceDataCode`.`id`", + type = "string[]", + example = "[MEDICAL_DIET_LOW_FAT, FREE_FROM_EGG]", + requiredMode = NOT_REQUIRED, + ) + @field:NullishReferenceDataCodeList( + domains = ["MEDICAL_DIET", "FREE_FROM"], + ) + val medicalDietaryRequirements: Nullish> = + getAttributeAsNullish>(attributes, "medicalDietaryRequirements") +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/dto/response/HealthDto.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/dto/response/HealthDto.kt new file mode 100644 index 0000000..f1da86d --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/dto/response/HealthDto.kt @@ -0,0 +1,16 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.dto.response + +import io.swagger.v3.oas.annotations.media.Schema +import uk.gov.justice.digital.hmpps.healthandmedication.dto.ReferenceDataSimpleDto + +@Schema(description = "Prison person health") +data class HealthDto( + @Schema(description = "Smoker or vaper") + val smokerOrVaper: ValueWithMetadata? = null, + + @Schema(description = "Food allergies") + val foodAllergies: ValueWithMetadata>? = null, + + @Schema(description = "Medical dietary requirements") + val medicalDietaryRequirements: ValueWithMetadata>? = null, +) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/dto/response/ValueWithMetadata.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/dto/response/ValueWithMetadata.kt new file mode 100644 index 0000000..eb82346 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/dto/response/ValueWithMetadata.kt @@ -0,0 +1,15 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.dto.response + +import io.swagger.v3.oas.annotations.media.Schema +import java.time.ZonedDateTime + +data class ValueWithMetadata( + @Schema(description = "Value") + val value: T?, + + @Schema(description = "Timestamp this field was last modified") + val lastModifiedAt: ZonedDateTime, + + @Schema(description = "Username of the user that last modified this field", example = "USER1") + val lastModifiedBy: String, +) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/enums/HealthAndMedicationField.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/enums/HealthAndMedicationField.kt new file mode 100644 index 0000000..775b6cc --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/enums/HealthAndMedicationField.kt @@ -0,0 +1,59 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.enums + +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.FoodAllergies +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.FoodAllergy +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.JsonObject +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.MedicalDietaryRequirement +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.MedicalDietaryRequirements +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.ReferenceDataCode + +private val getInt: (FieldValues) -> Int? = { it.valueInt } +private val getString: (FieldValues) -> String? = { it.valueString } +private val getRef: (FieldValues) -> ReferenceDataCode? = { it.valueRef } +private val getJson: (FieldValues) -> JsonObject? = { it.valueJson } + +private val setInt: (FieldValues, Any?) -> Unit = { values, value -> values.valueInt = value as Int? } +private val setString: (FieldValues, Any?) -> Unit = { values, value -> values.valueString = value as String? } +private val setRef: (FieldValues, Any?) -> Unit = { values, value -> values.valueRef = value as ReferenceDataCode? } + +private val hasChangedInt: (old: FieldValues, new: Any?) -> Boolean = { old, new -> new != old.valueInt } +private val hasChangedString: (old: FieldValues, new: Any?) -> Boolean = { old, new -> new != old.valueString } +private val hasChangedRef: (old: FieldValues, new: Any?) -> Boolean = { old, new -> new != old.valueRef } + +enum class HealthAndMedicationField( + val get: (FieldValues) -> Any?, + val set: (FieldValues, Any?) -> Unit, + val hasChangedFrom: (FieldValues, Any?) -> Boolean, + val domain: String?, +) { + FOOD_ALLERGY( + getJson, + { values, value -> + run { + value as MutableSet + values.valueJson = JsonObject(FOOD_ALLERGY, FoodAllergies(value)) + } + }, + { old, new -> old.valueJson?.value != FoodAllergies(new as MutableSet) }, + "FOOD_ALLERGY", + ), + + MEDICAL_DIET( + getJson, + { values, value -> + run { + value as MutableSet + values.valueJson = JsonObject(MEDICAL_DIET, MedicalDietaryRequirements(value)) + } + }, + { old, new -> old.valueJson?.value != MedicalDietaryRequirements(new as MutableSet) }, + "MEDICAL_DIET", + ), +} + +interface FieldValues { + var valueInt: Int? + var valueString: String? + var valueRef: ReferenceDataCode? + var valueJson: JsonObject? +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/health/HealthPingCheck.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/health/HealthPingCheck.kt similarity index 84% rename from src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/health/HealthPingCheck.kt rename to src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/health/HealthPingCheck.kt index c8d4032..61e153d 100644 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/health/HealthPingCheck.kt +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/health/HealthPingCheck.kt @@ -1,4 +1,4 @@ -package uk.gov.justice.digital.hmpps.healthandmedicationapi.health +package uk.gov.justice.digital.hmpps.healthandmedication.health import org.springframework.beans.factory.annotation.Qualifier import org.springframework.stereotype.Component diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/FieldHistory.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/FieldHistory.kt new file mode 100644 index 0000000..233ee1e --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/FieldHistory.kt @@ -0,0 +1,103 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.jpa + +import jakarta.persistence.Column +import jakarta.persistence.Convert +import jakarta.persistence.Entity +import jakarta.persistence.EnumType.STRING +import jakarta.persistence.Enumerated +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import org.hibernate.Hibernate +import uk.gov.justice.digital.hmpps.healthandmedication.enums.FieldValues +import uk.gov.justice.digital.hmpps.healthandmedication.enums.HealthAndMedicationField +import java.time.ZonedDateTime + +@Entity +class FieldHistory( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val fieldHistoryId: Long = -1, + + // Allow this to mutate in order to handle merges + var prisonerNumber: String, + + @Enumerated(STRING) + @Column(updatable = false, nullable = false) + val field: HealthAndMedicationField, + + override var valueInt: Int? = null, + override var valueString: String? = null, + + @Convert(converter = JsonObjectConverter::class) + override var valueJson: JsonObject? = null, + + @ManyToOne + @JoinColumn(name = "valueRef", referencedColumnName = "id") + override var valueRef: ReferenceDataCode? = null, + + override val createdAt: ZonedDateTime = ZonedDateTime.now(), + override val createdBy: String, + override var mergedAt: ZonedDateTime? = null, + override var mergedFrom: String? = null, + +) : FieldValues, + HistoryItem { + + fun toMetadata() = FieldMetadata( + prisonerNumber = prisonerNumber, + field = field, + lastModifiedAt = createdAt, + lastModifiedBy = createdBy, + ) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || Hibernate.getClass(this) != Hibernate.getClass(other)) return false + + other as FieldHistory + + if (prisonerNumber != other.prisonerNumber) return false + if (field != other.field) return false + if (valueInt != other.valueInt) return false + if (valueString != other.valueString) return false + if (valueRef != other.valueRef) return false + if (valueJson != other.valueJson) return false + if (createdAt != other.createdAt) return false + if (createdBy != other.createdBy) return false + if (mergedAt != other.mergedAt) return false + if (mergedFrom != other.mergedFrom) return false + + return true + } + + override fun hashCode(): Int { + var result = prisonerNumber.hashCode() + result = 31 * result + field.hashCode() + result = 31 * result + (valueInt ?: 0) + result = 31 * result + (valueString?.hashCode() ?: 0) + result = 31 * result + (valueRef?.hashCode() ?: 0) + result = 31 * result + (valueJson?.hashCode() ?: 0) + result = 31 * result + createdAt.hashCode() + result = 31 * result + createdBy.hashCode() + result = 31 * result + (mergedAt?.hashCode() ?: 0) + result = 31 * result + (mergedFrom?.hashCode() ?: 0) + return result + } + + override fun toString(): String = "FieldHistory(" + + "fieldHistoryId=$fieldHistoryId, " + + "prisonerNumber='$prisonerNumber', " + + "field=$field, " + + "valueInt=$valueInt, " + + "valueString=$valueString, " + + "valueRef=$valueRef, " + + "valueJson=$valueJson, " + + "createdAt=$createdAt, " + + "createdBy='$createdBy', " + + "mergedAt=$mergedAt, " + + "mergedFrom=$mergedFrom, " + + ")" +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/FieldMetadata.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/FieldMetadata.kt new file mode 100644 index 0000000..2d72e80 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/FieldMetadata.kt @@ -0,0 +1,56 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.jpa + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EnumType.STRING +import jakarta.persistence.Enumerated +import jakarta.persistence.Id +import jakarta.persistence.IdClass +import org.hibernate.Hibernate +import uk.gov.justice.digital.hmpps.healthandmedication.enums.HealthAndMedicationField +import java.io.Serializable +import java.time.ZonedDateTime + +@Entity +@IdClass(FieldMetadataId::class) +class FieldMetadata( + @Id + @Column(updatable = false, nullable = false) + val prisonerNumber: String, + + @Id + @Enumerated(STRING) + @Column(updatable = false, nullable = false) + val field: HealthAndMedicationField, + + var lastModifiedAt: ZonedDateTime = ZonedDateTime.now(), + + var lastModifiedBy: String, +) { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || Hibernate.getClass(this) != Hibernate.getClass(other)) return false + + other as FieldMetadata + + if (prisonerNumber != other.prisonerNumber) return false + if (field != other.field) return false + if (lastModifiedAt.toInstant() != other.lastModifiedAt.toInstant()) return false + if (lastModifiedBy != other.lastModifiedBy) return false + + return true + } + + override fun hashCode(): Int { + var result = prisonerNumber.hashCode() + result = 31 * result + field.hashCode() + result = 31 * result + lastModifiedAt.toInstant().hashCode() + result = 31 * result + lastModifiedBy.hashCode() + return result + } + + override fun toString(): String = "FieldMetadata(prisonerNumber='$prisonerNumber', field=$field, lastModifiedAt=$lastModifiedAt, lastModifiedBy='$lastModifiedBy')" +} + +private data class FieldMetadataId(val prisonerNumber: String? = null, val field: HealthAndMedicationField? = null) : Serializable diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/FoodAllergy.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/FoodAllergy.kt new file mode 100644 index 0000000..2606e2c --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/FoodAllergy.kt @@ -0,0 +1,46 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.jpa + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType.IDENTITY +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import org.hibernate.Hibernate + +@Entity +class FoodAllergy( + @Column(name = "prisoner_number", updatable = false, nullable = false) + val prisonerNumber: String, + + @ManyToOne + @JoinColumn(name = "allergy", referencedColumnName = "id") + var allergy: ReferenceDataCode, +) { + @Id + @GeneratedValue(strategy = IDENTITY) + val id: Long = -1 + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || Hibernate.getClass(this) != Hibernate.getClass(other)) return false + + other as FoodAllergy + + if (prisonerNumber != other.prisonerNumber) return false + if (allergy != other.allergy) return false + + return true + } + + override fun hashCode(): Int { + var result = prisonerNumber.hashCode() + result = 31 * result + allergy.hashCode() + return result + } +} + +data class FoodAllergies(val allergies: List) { + constructor(allergies: Collection) : this(allergies.map { it.allergy.id }.sorted()) +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/HistoryItem.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/HistoryItem.kt new file mode 100644 index 0000000..53ecd26 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/HistoryItem.kt @@ -0,0 +1,17 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.jpa + +import java.time.ZonedDateTime + +interface HistoryItem : Comparable { + val createdAt: ZonedDateTime + val createdBy: String + var mergedAt: ZonedDateTime? + var mergedFrom: String? + + override fun compareTo(other: HistoryItem) = compareValuesBy( + this, + other, + { it.createdAt }, + { it.hashCode() }, + ) +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/JsonObject.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/JsonObject.kt new file mode 100644 index 0000000..6ce403d --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/JsonObject.kt @@ -0,0 +1,18 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.jpa + +import com.fasterxml.jackson.annotation.JsonSubTypes +import com.fasterxml.jackson.annotation.JsonTypeInfo +import com.fasterxml.jackson.annotation.JsonTypeInfo.As.EXTERNAL_PROPERTY +import com.fasterxml.jackson.annotation.JsonTypeInfo.Id.NAME +import uk.gov.justice.digital.hmpps.healthandmedication.enums.HealthAndMedicationField + +data class JsonObject( + val field: HealthAndMedicationField, + + @JsonTypeInfo(use = NAME, property = "field", include = EXTERNAL_PROPERTY) + @JsonSubTypes( + JsonSubTypes.Type(value = FoodAllergies::class, name = "FOOD_ALLERGY"), + JsonSubTypes.Type(value = MedicalDietaryRequirements::class, name = "MEDICAL_DIET"), + ) + val value: Any?, +) diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/JsonObjectConverter.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/JsonObjectConverter.kt new file mode 100644 index 0000000..064596b --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/JsonObjectConverter.kt @@ -0,0 +1,14 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.jpa + +import com.fasterxml.jackson.databind.ObjectMapper +import jakarta.persistence.AttributeConverter +import jakarta.persistence.Converter +import org.springframework.stereotype.Component + +@Converter +@Component +class JsonObjectConverter(val objectMapper: ObjectMapper) : AttributeConverter { + override fun convertToDatabaseColumn(jsonObject: JsonObject?): String? = jsonObject?.let(objectMapper::writeValueAsString) + + override fun convertToEntityAttribute(json: String?): JsonObject? = json?.let { objectMapper.readValue(it, JsonObject::class.java) } +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/MedicalDietaryRequirement.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/MedicalDietaryRequirement.kt new file mode 100644 index 0000000..cc3cfab --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/MedicalDietaryRequirement.kt @@ -0,0 +1,49 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.jpa + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType.IDENTITY +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import org.hibernate.Hibernate + +@Entity +class MedicalDietaryRequirement( + @Column(name = "prisoner_number", updatable = false, nullable = false) + val prisonerNumber: String, + + @ManyToOne + @JoinColumn( + name = "dietary_requirement", + referencedColumnName = "id", + ) var dietaryRequirement: ReferenceDataCode, +) { + @Id + @GeneratedValue(strategy = IDENTITY) + val id: Long = -1 + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || Hibernate.getClass(this) != Hibernate.getClass(other)) return false + + other as MedicalDietaryRequirement + + if (prisonerNumber != other.prisonerNumber) return false + if (dietaryRequirement != other.dietaryRequirement) return false + + return true + } + + override fun hashCode(): Int { + var result = prisonerNumber.hashCode() + result = 31 * result + dietaryRequirement.hashCode() + return result + } +} + +data class MedicalDietaryRequirements(val medicalDietaryRequirements: List) { + constructor(medicalDietaryRequirements: Collection) : + this(medicalDietaryRequirements.map { it.dietaryRequirement.id }.sorted()) +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/PrisonerHealth.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/PrisonerHealth.kt new file mode 100644 index 0000000..6b88f2a --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/PrisonerHealth.kt @@ -0,0 +1,126 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.jpa + +import jakarta.persistence.CascadeType.ALL +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.FetchType.LAZY +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.MapKey +import jakarta.persistence.OneToMany +import jakarta.persistence.Table +import org.hibernate.Hibernate +import org.hibernate.annotations.SortNatural +import uk.gov.justice.digital.hmpps.healthandmedication.dto.ReferenceDataSimpleDto +import uk.gov.justice.digital.hmpps.healthandmedication.dto.response.HealthDto +import uk.gov.justice.digital.hmpps.healthandmedication.dto.response.ValueWithMetadata +import uk.gov.justice.digital.hmpps.healthandmedication.enums.HealthAndMedicationField +import uk.gov.justice.digital.hmpps.healthandmedication.enums.HealthAndMedicationField.FOOD_ALLERGY +import uk.gov.justice.digital.hmpps.healthandmedication.enums.HealthAndMedicationField.MEDICAL_DIET +import uk.gov.justice.digital.hmpps.healthandmedication.mapper.toSimpleDto +import java.time.ZonedDateTime +import java.util.SortedSet +import kotlin.reflect.KMutableProperty0 + +@Entity +@Table(name = "health") +class PrisonerHealth( + @Id + @Column(name = "prisoner_number", updatable = false, nullable = false) + override val prisonerNumber: String, + + @ManyToOne + @JoinColumn(name = "smoker_or_vaper", referencedColumnName = "id") + var smokerOrVaper: ReferenceDataCode? = null, + + @OneToMany(mappedBy = "prisonerNumber", cascade = [ALL], orphanRemoval = true) + var foodAllergies: MutableSet = mutableSetOf(), + + @OneToMany(mappedBy = "prisonerNumber", cascade = [ALL], orphanRemoval = true) + var medicalDietaryRequirements: MutableSet = mutableSetOf(), + + // Stores snapshots of each update to a prisoner's health information + @OneToMany(mappedBy = "prisonerNumber", fetch = LAZY, cascade = [ALL], orphanRemoval = true) + @SortNatural + override val fieldHistory: SortedSet = sortedSetOf(), + + // Stores timestamps of when each individual field was changed + @OneToMany(mappedBy = "prisonerNumber", fetch = LAZY, cascade = [ALL], orphanRemoval = true) + @MapKey(name = "field") + override val fieldMetadata: MutableMap = mutableMapOf(), +) : WithFieldHistory() { + + override fun fieldAccessors(): Map> = mapOf( + FOOD_ALLERGY to ::foodAllergies, + MEDICAL_DIET to ::medicalDietaryRequirements, + ) + + fun toDto(): HealthDto = HealthDto( + foodAllergies = getReferenceDataListValueWithMetadata( + foodAllergies, + { allergies -> allergies.map { it.allergy } }, + FOOD_ALLERGY, + ), + medicalDietaryRequirements = getReferenceDataListValueWithMetadata( + medicalDietaryRequirements, + { dietaryRequirements -> dietaryRequirements.map { it.dietaryRequirement } }, + MEDICAL_DIET, + ), + ) + + override fun updateFieldHistory( + lastModifiedAt: ZonedDateTime, + lastModifiedBy: String, + ) = updateFieldHistory(lastModifiedAt, lastModifiedBy, allFields) + + private fun getRefDataValueWithMetadata( + value: KMutableProperty0, + field: HealthAndMedicationField, + ): ValueWithMetadata? = fieldMetadata[field]?.let { + ValueWithMetadata( + value.get()?.toSimpleDto(), + it.lastModifiedAt, + it.lastModifiedBy, + ) + } + + private fun getReferenceDataListValueWithMetadata( + value: T, + mapper: (T) -> List, + field: HealthAndMedicationField, + ): ValueWithMetadata>? = fieldMetadata[field]?.let { + ValueWithMetadata( + mapper(value).map { code -> code.toSimpleDto() }, + it.lastModifiedAt, + it.lastModifiedBy, + ) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || Hibernate.getClass(this) != Hibernate.getClass(other)) return false + + other as PrisonerHealth + + if (prisonerNumber != other.prisonerNumber) return false + if (foodAllergies != other.foodAllergies) return false + if (medicalDietaryRequirements != other.medicalDietaryRequirements) return false + + return true + } + + override fun hashCode(): Int { + var result = prisonerNumber.hashCode() + result = 31 * result + foodAllergies.hashCode() + result = 31 * result + medicalDietaryRequirements.hashCode() + return result + } + + companion object { + val allFields = listOf( + MEDICAL_DIET, + FOOD_ALLERGY, + ) + } +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/ReferenceDataCode.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/ReferenceDataCode.kt new file mode 100644 index 0000000..bc32a04 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/ReferenceDataCode.kt @@ -0,0 +1,59 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.jpa + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import org.hibernate.Hibernate +import java.time.ZonedDateTime + +@Entity +class ReferenceDataCode( + @Id + @Column(name = "id", updatable = false, nullable = false) + val id: String, + + @Column(name = "code", updatable = false, nullable = false) + val code: String, + + @ManyToOne + @JoinColumn(name = "domain", nullable = false) + val domain: ReferenceDataDomain, + + val description: String, + val listSequence: Int, + val createdAt: ZonedDateTime = ZonedDateTime.now(), + val createdBy: String, +) { + var lastModifiedAt: ZonedDateTime? = null + var lastModifiedBy: String? = null + var deactivatedAt: ZonedDateTime? = null + var deactivatedBy: String? = null + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || Hibernate.getClass(this) != Hibernate.getClass(other)) return false + + other as ReferenceDataCode + + if (id != other.id) return false + if (code != other.code) return false + if (domain != other.domain) return false + if (description != other.description) return false + if (listSequence != other.listSequence) return false + + return true + } + + override fun hashCode(): Int { + var result = id.hashCode() + result = 31 * result + (code.hashCode()) + result = 31 * result + (domain.hashCode()) + result = 31 * result + (description.hashCode()) + result = 31 * result + (listSequence) + return result + } + + override fun toString(): String = "ReferenceDataCode(id=$id, code=$code, domain=$domain, description='$description', listSequence=$listSequence)" +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/ReferenceDataDomain.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/ReferenceDataDomain.kt new file mode 100644 index 0000000..33a188d --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/ReferenceDataDomain.kt @@ -0,0 +1,54 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.jpa + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.Id +import jakarta.persistence.OneToMany +import org.hibernate.Hibernate +import java.time.ZonedDateTime + +@Entity +class ReferenceDataDomain( + @Id + @Column(name = "code", updatable = false, nullable = false) + val code: String, + + val description: String, + val listSequence: Int, + val createdAt: ZonedDateTime = ZonedDateTime.now(), + val createdBy: String, +) { + var lastModifiedAt: ZonedDateTime? = null + var lastModifiedBy: String? = null + var deactivatedAt: ZonedDateTime? = null + var deactivatedBy: String? = null + var parentDomainCode: String? = null + + @OneToMany(mappedBy = "domain") + var referenceDataCodes: MutableList = mutableListOf() + + @OneToMany(mappedBy = "parentDomainCode") + var subDomains: MutableList = mutableListOf() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || Hibernate.getClass(this) != Hibernate.getClass(other)) return false + + other as ReferenceDataDomain + + if (code != other.code) return false + if (description != other.description) return false + if (listSequence != other.listSequence) return false + + return true + } + + override fun hashCode(): Int { + var result = code.hashCode() + result = 31 * result + (description.hashCode()) + result = 31 * result + (listSequence) + return result + } + + override fun toString(): String = "ReferenceDataDomain(code=$code, description='$description', listSequence=$listSequence)" +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/WithFieldHistory.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/WithFieldHistory.kt new file mode 100644 index 0000000..a35a436 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/WithFieldHistory.kt @@ -0,0 +1,54 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.jpa + +import org.springframework.data.domain.AbstractAggregateRoot +import uk.gov.justice.digital.hmpps.healthandmedication.enums.HealthAndMedicationField +import java.time.ZonedDateTime +import java.util.SortedSet +import kotlin.reflect.KMutableProperty0 + +abstract class WithFieldHistory?> : AbstractAggregateRoot() { + abstract val prisonerNumber: String + abstract val fieldHistory: SortedSet + abstract val fieldMetadata: MutableMap + protected abstract fun fieldAccessors(): Map> + + abstract fun updateFieldHistory( + lastModifiedAt: ZonedDateTime, + lastModifiedBy: String, + ): Collection + + fun updateFieldHistory( + lastModifiedAt: ZonedDateTime, + lastModifiedBy: String, + fields: Collection, + ): Collection { + val changedFields = mutableSetOf() + + fieldAccessors() + .filter { fields.contains(it.key) } + .forEach { (field, currentValue) -> + val previousVersion = fieldHistory.lastOrNull { it.field == field } + if (previousVersion == null || field.hasChangedFrom(previousVersion, currentValue())) { + fieldMetadata[field] = FieldMetadata( + field = field, + prisonerNumber = this.prisonerNumber, + lastModifiedAt = lastModifiedAt, + lastModifiedBy = lastModifiedBy, + ) + + fieldHistory.add( + FieldHistory( + prisonerNumber = this.prisonerNumber, + field = field, + createdAt = lastModifiedAt, + createdBy = lastModifiedBy, + ).also { field.set(it, currentValue()) }, + ) + changedFields.add(field) + } + } + return changedFields + } + + public override fun domainEvents(): Collection = super.domainEvents() +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/repository/PrisonerHealthRepository.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/repository/PrisonerHealthRepository.kt new file mode 100644 index 0000000..b94f0ee --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/repository/PrisonerHealthRepository.kt @@ -0,0 +1,8 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.jpa.repository + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.PrisonerHealth + +@Repository +interface PrisonerHealthRepository : JpaRepository diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/repository/ReferenceDataCodeRepository.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/repository/ReferenceDataCodeRepository.kt new file mode 100644 index 0000000..d9cf62f --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/repository/ReferenceDataCodeRepository.kt @@ -0,0 +1,32 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.jpa.repository + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query +import org.springframework.data.repository.query.Param +import org.springframework.stereotype.Repository +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.ReferenceDataCode + +@Repository +interface ReferenceDataCodeRepository : JpaRepository { + + @Query( + """ + SELECT rdc + FROM ReferenceDataCode rdc + WHERE :domain = rdc.domain.code AND + (:includeInactive = true OR + (:includeInactive = false AND rdc.deactivatedAt IS NULL)) + ORDER BY CASE + WHEN rdc.listSequence = 0 THEN 999 + ELSE rdc.listSequence + END, + rdc.description + """, + ) + fun findAllByDomainAndIncludeInactive( + @Param("domain") domain: String, + @Param("includeInactive") includeInactive: Boolean, + ): Collection + + fun findByCodeAndDomainCode(@Param("code") code: String, @Param("domain") domain: String): ReferenceDataCode? +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/repository/ReferenceDataDomainRepository.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/repository/ReferenceDataDomainRepository.kt new file mode 100644 index 0000000..021ee55 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/jpa/repository/ReferenceDataDomainRepository.kt @@ -0,0 +1,36 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.jpa.repository + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query +import org.springframework.data.repository.query.Param +import org.springframework.stereotype.Repository +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.ReferenceDataDomain + +@Repository +interface ReferenceDataDomainRepository : JpaRepository { + + @Query( + """ + SELECT rdd + FROM ReferenceDataDomain rdd + WHERE ( + :includeInactive = true OR + (:includeInactive = false AND rdd.deactivatedAt IS NULL) + ) AND ( + (:includeSubDomains = true) OR + (:includeSubDomains = false AND rdd.parentDomainCode IS NULL) + ) + ORDER BY CASE + WHEN rdd.listSequence = 0 THEN 999 + ELSE rdd.listSequence + END, + rdd.description + """, + ) + fun findAllByIncludeInactive( + @Param("includeInactive") includeInactive: Boolean, + @Param("includeSubDomains") includeSubDomains: Boolean = false, + ): Collection + + fun findByCode(code: String): ReferenceDataDomain? +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/mapper/ReferenceDataCodeMapper.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/mapper/ReferenceDataCodeMapper.kt new file mode 100644 index 0000000..f2fa19d --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/mapper/ReferenceDataCodeMapper.kt @@ -0,0 +1,30 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.mapper + +import uk.gov.justice.digital.hmpps.healthandmedication.dto.ReferenceDataCodeDto +import uk.gov.justice.digital.hmpps.healthandmedication.dto.ReferenceDataSimpleDto +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.ReferenceDataCode +import java.time.ZonedDateTime + +fun ReferenceDataCode.toDto(): ReferenceDataCodeDto = ReferenceDataCodeDto( + id, + domain = domain.code, + code, + description, + listSequence, + isActive(), + createdAt, + createdBy, + lastModifiedAt, + lastModifiedBy, + deactivatedAt, + deactivatedBy, +) + +fun ReferenceDataCode.toSimpleDto(): ReferenceDataSimpleDto = ReferenceDataSimpleDto( + id, + description, + listSequence, + isActive(), +) + +fun ReferenceDataCode.isActive() = deactivatedAt?.isBefore(ZonedDateTime.now()) != true diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/mapper/ReferenceDataDomainMapper.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/mapper/ReferenceDataDomainMapper.kt new file mode 100644 index 0000000..226d5f4 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/mapper/ReferenceDataDomainMapper.kt @@ -0,0 +1,22 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.mapper + +import uk.gov.justice.digital.hmpps.healthandmedication.dto.ReferenceDataDomainDto +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.ReferenceDataDomain +import java.time.ZonedDateTime + +fun ReferenceDataDomain.toDto(): ReferenceDataDomainDto = ReferenceDataDomainDto( + code, + description, + listSequence, + isActive(), + createdAt, + createdBy, + lastModifiedAt, + lastModifiedBy, + deactivatedAt, + deactivatedBy, + referenceDataCodes.map { it.toDto() }, + subDomains.map { it.toDto() }, +) + +fun ReferenceDataDomain.isActive() = deactivatedAt?.isBefore(ZonedDateTime.now()) != true diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/resource/HealthAndMedicationResource.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/resource/HealthAndMedicationResource.kt new file mode 100644 index 0000000..35be460 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/resource/HealthAndMedicationResource.kt @@ -0,0 +1,100 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.resource + +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.media.Content +import io.swagger.v3.oas.annotations.media.Schema +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.tags.Tag +import jakarta.validation.Valid +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.security.access.prepost.PreAuthorize +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PatchMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.ResponseStatus +import org.springframework.web.bind.annotation.RestController +import uk.gov.justice.digital.hmpps.healthandmedication.config.HealthAndMedicationDataNotFoundException +import uk.gov.justice.digital.hmpps.healthandmedication.dto.request.PrisonerHealthUpdateRequest +import uk.gov.justice.digital.hmpps.healthandmedication.dto.response.HealthDto +import uk.gov.justice.digital.hmpps.healthandmedication.service.PrisonerHealthService +import uk.gov.justice.hmpps.kotlin.common.ErrorResponse + +@RestController +@Tag(name = "Health and Medication Data") +@RequestMapping("/prisoners/{prisonerNumber}", produces = [MediaType.APPLICATION_JSON_VALUE]) +class HealthAndMedicationResource(private val prisonerHealthService: PrisonerHealthService) { + + @GetMapping + @ResponseStatus(HttpStatus.OK) + @PreAuthorize("hasRole('ROLE_HEALTH_AND_MEDICATION_API__HEALTH_AND_MEDICATION_DATA__RO')") + @Operation( + description = "Requires role `ROLE_HEALTH_AND_MEDICATION_API__HEALTH_AND_MEDICATION_DATA__RO`", + responses = [ + ApiResponse( + responseCode = "200", + description = "Returns Health and Medication Data", + ), + ApiResponse( + responseCode = "401", + description = "Unauthorized to access this endpoint", + content = [Content(mediaType = "application/json", schema = Schema(implementation = ErrorResponse::class))], + ), + ApiResponse( + responseCode = "403", + description = "Missing required role. Requires ROLE_HEALTH_AND_MEDICATION_API__HEALTH_AND_MEDICATION_DATA__RO", + content = [Content(mediaType = "application/json", schema = Schema(implementation = ErrorResponse::class))], + ), + ApiResponse( + responseCode = "404", + description = "Data not found", + content = [Content(mediaType = "application/json", schema = Schema(implementation = ErrorResponse::class))], + ), + ], + ) + fun getHealthAndMedicationData( + @Schema(description = "The prisoner number", example = "A1234AA", required = true) + @PathVariable + prisonerNumber: String, + ) = prisonerHealthService.getHealth(prisonerNumber) + ?: throw HealthAndMedicationDataNotFoundException(prisonerNumber) + + @PatchMapping + @ResponseStatus(HttpStatus.OK) + @PreAuthorize("hasRole('ROLE_HEALTH_AND_MEDICATION_API__HEALTH_AND_MEDICATION_DATA__RW')") + @Operation( + summary = "Updates the health and medication data for a prisoner", + description = "Requires role `ROLE_HEALTH_AND_MEDICATION_API__HEALTH_AND_MEDICATION_DATA__RW`", + responses = [ + ApiResponse( + responseCode = "201", + description = "Returns prisoner's physical attributes", + ), + ApiResponse( + responseCode = "400", + description = "Bad request", + content = [Content(mediaType = "application/json", schema = Schema(implementation = ErrorResponse::class))], + ), + ApiResponse( + responseCode = "401", + description = "Unauthorized to access this endpoint", + content = [Content(mediaType = "application/json", schema = Schema(implementation = ErrorResponse::class))], + ), + ApiResponse( + responseCode = "403", + description = "Missing required role. Requires ROLE_HEALTH_AND_MEDICATION_API__HEALTH_AND_MEDICATION_DATA__RW", + content = [Content(mediaType = "application/json", schema = Schema(implementation = ErrorResponse::class))], + ), + ], + ) + fun setHealthAndMedicationData( + @Schema(description = "The prisoner number", example = "A1234AA", required = true) + @PathVariable + prisonerNumber: String, + @RequestBody + @Valid + prisonerHealthUpdateRequest: PrisonerHealthUpdateRequest, + ): HealthDto = prisonerHealthService.createOrUpdate(prisonerNumber, prisonerHealthUpdateRequest) +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/resource/ReferenceDataCodeResource.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/resource/ReferenceDataCodeResource.kt new file mode 100644 index 0000000..dc833fd --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/resource/ReferenceDataCodeResource.kt @@ -0,0 +1,93 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.controller + +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.Parameter +import io.swagger.v3.oas.annotations.media.ArraySchema +import io.swagger.v3.oas.annotations.media.Content +import io.swagger.v3.oas.annotations.media.Schema +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.security.access.prepost.PreAuthorize +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.ResponseStatus +import org.springframework.web.bind.annotation.RestController +import uk.gov.justice.digital.hmpps.healthandmedication.dto.ReferenceDataCodeDto +import uk.gov.justice.digital.hmpps.healthandmedication.service.ReferenceDataCodeService +import uk.gov.justice.hmpps.kotlin.common.ErrorResponse + +@RestController +@Tag(name = "Reference Data Codes", description = "Reference Data Codes for Health and Medication data") +@RequestMapping("/reference-data/domains/{domain}/codes", produces = [MediaType.APPLICATION_JSON_VALUE]) +class ReferenceDataCodeResource( + private val referenceDataCodeService: ReferenceDataCodeService, +) { + + @GetMapping + @ResponseStatus(HttpStatus.OK) + @PreAuthorize("hasRole('ROLE_HEALTH_AND_MEDICATION_API__REFERENCE_DATA__RO')") + @Operation( + summary = "Get all reference data codes for {domain}", + description = "Returns the list of reference data codes within {domain}. " + + "By default this endpoint only returns active reference data codes. " + + "The `includeInactive` parameter can be used to return all reference data codes. " + + "Requires role `ROLE_HEALTH_AND_MEDICATION_API__REFERENCE_DATA__RO`", + responses = [ + ApiResponse( + responseCode = "200", + description = "Reference data codes found", + content = [Content(array = ArraySchema(schema = Schema(implementation = ReferenceDataCodeDto::class)))], + ), + ApiResponse( + responseCode = "401", + description = "Unauthorised, requires a valid Oauth2 token", + content = [Content(schema = Schema(implementation = ErrorResponse::class))], + ), + ApiResponse( + responseCode = "404", + description = "Not found, the reference data domain was not found", + content = [Content(schema = Schema(implementation = ErrorResponse::class))], + ), + ], + ) + fun getReferenceDataCodes( + @PathVariable domain: String, + @Parameter( + description = "Include inactive reference data codes. Defaults to false. " + + "Requires role `ROLE_HEALTH_AND_MEDICATION_API__REFERENCE_DATA__RO`", + ) + includeInactive: Boolean = false, + ): Collection = referenceDataCodeService.getReferenceDataCodes(domain, includeInactive) + + @GetMapping("/{code}") + @ResponseStatus(HttpStatus.OK) + @PreAuthorize("hasRole('ROLE_HEALTH_AND_MEDICATION_API__REFERENCE_DATA__RO')") + @Operation( + summary = "Get a reference data code", + description = "Returns the reference data code.", + responses = [ + ApiResponse( + responseCode = "200", + description = "Reference data code retrieved", + content = [Content(schema = Schema(implementation = ReferenceDataCodeDto::class))], + ), + ApiResponse( + responseCode = "401", + description = "Unauthorised, requires a valid Oauth2 token", + content = [Content(schema = Schema(implementation = ErrorResponse::class))], + ), + ApiResponse( + responseCode = "404", + description = "Not found, the reference data code was not found", + content = [Content(schema = Schema(implementation = ErrorResponse::class))], + ), + ], + ) + fun getReferenceDataCode( + @PathVariable domain: String, + @PathVariable code: String, + ): ReferenceDataCodeDto = referenceDataCodeService.getReferenceDataCode(code, domain) +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/resource/ReferenceDataDomainResource.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/resource/ReferenceDataDomainResource.kt new file mode 100644 index 0000000..0e49414 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/resource/ReferenceDataDomainResource.kt @@ -0,0 +1,91 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.controller + +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.Parameter +import io.swagger.v3.oas.annotations.media.ArraySchema +import io.swagger.v3.oas.annotations.media.Content +import io.swagger.v3.oas.annotations.media.Schema +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.security.access.prepost.PreAuthorize +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.ResponseStatus +import org.springframework.web.bind.annotation.RestController +import uk.gov.justice.digital.hmpps.healthandmedication.dto.ReferenceDataDomainDto +import uk.gov.justice.digital.hmpps.healthandmedication.service.ReferenceDataDomainService +import uk.gov.justice.hmpps.kotlin.common.ErrorResponse + +@RestController +@Tag(name = "Reference Data Domains", description = "Reference Data Domains for Health and Medication data") +@RequestMapping("/reference-data/domains", produces = [MediaType.APPLICATION_JSON_VALUE]) +class ReferenceDataDomainResource( + private val referenceDataDomainService: ReferenceDataDomainService, +) { + + @GetMapping + @ResponseStatus(HttpStatus.OK) + @PreAuthorize("hasRole('ROLE_HEALTH_AND_MEDICATION_API__REFERENCE_DATA__RO')") + @Operation( + summary = "Get all reference data domains", + description = "Returns the list of reference data domains. " + + "By default this endpoint only returns active reference data domains. " + + "The `includeInactive` parameter can be used to return all reference data domains. " + + "Requires role `ROLE_HEALTH_AND_MEDICATION_API__REFERENCE_DATA__RO`", + responses = [ + ApiResponse( + responseCode = "200", + description = "Reference data domains found", + content = [Content(array = ArraySchema(schema = Schema(implementation = ReferenceDataDomainDto::class)))], + ), + ApiResponse( + responseCode = "401", + description = "Unauthorised, requires a valid Oauth2 token", + content = [Content(schema = Schema(implementation = ErrorResponse::class))], + ), + ], + ) + fun getReferenceDataDomains( + @Parameter( + description = "Include inactive reference data domains. Defaults to false.", + ) + includeInactive: Boolean = false, + @Parameter( + description = "Include sub-domains at the top level. Defaults to false", + ) + includeSubDomains: Boolean = false, + ): Collection = + referenceDataDomainService.getReferenceDataDomains(includeInactive, includeSubDomains) + + @GetMapping("/{domain}") + @ResponseStatus(HttpStatus.OK) + @PreAuthorize("hasRole('ROLE_HEALTH_AND_MEDICATION_API__REFERENCE_DATA__RO')") + @Operation( + summary = "Get a reference data domain", + description = "Returns the reference data domain, including all reference data codes linked to that domain. " + + "Requires role `ROLE_HEALTH_AND_MEDICATION_API__REFERENCE_DATA__RO`", + responses = [ + ApiResponse( + responseCode = "200", + description = "Reference data domain retrieved", + content = [Content(schema = Schema(implementation = ReferenceDataDomainDto::class))], + ), + ApiResponse( + responseCode = "401", + description = "Unauthorised, requires a valid Oauth2 token", + content = [Content(schema = Schema(implementation = ErrorResponse::class))], + ), + ApiResponse( + responseCode = "404", + description = "Not found, the reference data domain was not found", + content = [Content(schema = Schema(implementation = ErrorResponse::class))], + ), + ], + ) + fun getReferenceDataDomain( + @PathVariable domain: String, + ): ReferenceDataDomainDto = referenceDataDomainService.getReferenceDataDomain(domain) +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/service/PrisonerHealthService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/service/PrisonerHealthService.kt new file mode 100644 index 0000000..7b17cfc --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/service/PrisonerHealthService.kt @@ -0,0 +1,68 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.service + +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import uk.gov.justice.digital.hmpps.healthandmedication.client.prisonersearch.PrisonerSearchClient +import uk.gov.justice.digital.hmpps.healthandmedication.dto.request.PrisonerHealthUpdateRequest +import uk.gov.justice.digital.hmpps.healthandmedication.dto.response.HealthDto +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.FoodAllergy +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.MedicalDietaryRequirement +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.PrisonerHealth +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.repository.PrisonerHealthRepository +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.repository.ReferenceDataCodeRepository +import uk.gov.justice.digital.hmpps.healthandmedication.utils.AuthenticationFacade +import uk.gov.justice.digital.hmpps.healthandmedication.utils.toReferenceDataCode +import uk.gov.justice.digital.hmpps.healthandmedication.utils.validatePrisonerNumber +import java.time.Clock +import java.time.ZonedDateTime +import kotlin.jvm.optionals.getOrNull + +@Service +@Transactional(readOnly = true) +class PrisonerHealthService( + private val prisonerSearchClient: PrisonerSearchClient, + private val prisonerHealthRepository: PrisonerHealthRepository, + private val referenceDataCodeRepository: ReferenceDataCodeRepository, + private val authenticationFacade: AuthenticationFacade, + private val clock: Clock, +) { + fun getHealth(prisonerNumber: String): HealthDto? = prisonerHealthRepository.findById(prisonerNumber).getOrNull()?.toDto() + + @Transactional + fun createOrUpdate( + prisonerNumber: String, + request: PrisonerHealthUpdateRequest, + ): HealthDto { + val now = ZonedDateTime.now(clock) + val health = prisonerHealthRepository.findById(prisonerNumber).orElseGet { newHealthFor(prisonerNumber) }.apply { + request.foodAllergies.let> { + foodAllergies.apply { clear() }.addAll( + it!!.map { allergyCode -> + FoodAllergy( + prisonerNumber = prisonerNumber, + allergy = toReferenceDataCode(referenceDataCodeRepository, allergyCode)!!, + ) + }, + ) + } + + request.medicalDietaryRequirements.let> { + medicalDietaryRequirements.apply { clear() }.addAll( + it!!.map { dietaryCode -> + MedicalDietaryRequirement( + prisonerNumber = prisonerNumber, + dietaryRequirement = toReferenceDataCode(referenceDataCodeRepository, dietaryCode)!!, + ) + }, + ) + } + }.also { it.updateFieldHistory(now, authenticationFacade.getUserOrSystemInContext()) } + + return prisonerHealthRepository.save(health).toDto() + } + + private fun newHealthFor(prisonerNumber: String): PrisonerHealth { + validatePrisonerNumber(prisonerSearchClient, prisonerNumber) + return PrisonerHealth(prisonerNumber) + } +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/service/ReferenceDataCodeService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/service/ReferenceDataCodeService.kt new file mode 100644 index 0000000..39459c0 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/service/ReferenceDataCodeService.kt @@ -0,0 +1,32 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.service + +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import uk.gov.justice.digital.hmpps.healthandmedication.config.ReferenceDataCodeNotFoundException +import uk.gov.justice.digital.hmpps.healthandmedication.dto.ReferenceDataCodeDto +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.repository.ReferenceDataCodeRepository +import uk.gov.justice.digital.hmpps.healthandmedication.mapper.toDto + +@Service +@Transactional(readOnly = true) +class ReferenceDataCodeService( + private val referenceDataCodeRepository: ReferenceDataCodeRepository, +) { + + /* + * Exclude certain reference data codes + */ + private val excludedCodes = setOf( + Pair("FACIAL_HAIR", "NA"), + Pair("EYE", "MISSING"), + ) + + fun getReferenceDataCodes(domain: String, includeInactive: Boolean): Collection = + referenceDataCodeRepository.findAllByDomainAndIncludeInactive(domain, includeInactive) + .filterNot { excludedCodes.contains(Pair(it.domain.code, it.code)) } + .map { it.toDto() } + + fun getReferenceDataCode(code: String, domain: String): ReferenceDataCodeDto = + referenceDataCodeRepository.findByCodeAndDomainCode(code, domain)?.toDto() + ?: throw ReferenceDataCodeNotFoundException(code, domain) +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/service/ReferenceDataDomainService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/service/ReferenceDataDomainService.kt new file mode 100644 index 0000000..f879db9 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/service/ReferenceDataDomainService.kt @@ -0,0 +1,21 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.service + +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import uk.gov.justice.digital.hmpps.healthandmedication.config.ReferenceDataDomainNotFoundException +import uk.gov.justice.digital.hmpps.healthandmedication.dto.ReferenceDataDomainDto +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.repository.ReferenceDataDomainRepository +import uk.gov.justice.digital.hmpps.healthandmedication.mapper.toDto + +@Service +@Transactional(readOnly = true) +class ReferenceDataDomainService( + private val referenceDataDomainRepository: ReferenceDataDomainRepository, +) { + fun getReferenceDataDomains(includeInactive: Boolean, includeSubDomains: Boolean = false): Collection = + referenceDataDomainRepository.findAllByIncludeInactive(includeInactive, includeSubDomains).map { it.toDto() } + + fun getReferenceDataDomain(code: String): ReferenceDataDomainDto = + referenceDataDomainRepository.findByCode(code)?.toDto() + ?: throw ReferenceDataDomainNotFoundException(code) +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/utils/AuthenticationFacade.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/utils/AuthenticationFacade.kt new file mode 100644 index 0000000..17fbda1 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/utils/AuthenticationFacade.kt @@ -0,0 +1,35 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.utils + +import org.springframework.security.core.Authentication +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.security.core.userdetails.UserDetails +import org.springframework.stereotype.Component +import uk.gov.justice.digital.hmpps.healthandmedication.SYSTEM_USERNAME + +@Component +class AuthenticationFacade { + + fun getUserOrSystemInContext() = currentUsername ?: SYSTEM_USERNAME + + val authentication: Authentication + get() = SecurityContextHolder.getContext().authentication + + val currentUsername: String? + get() { + val username: String? + val userPrincipal = userPrincipal + username = when (userPrincipal) { + is String -> userPrincipal + is UserDetails -> userPrincipal.username + is Map<*, *> -> userPrincipal["username"] as String? + else -> null + } + return username + } + + private val userPrincipal: Any? + get() { + val auth = authentication + return auth.principal + } +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/utils/Nullish.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/utils/Nullish.kt new file mode 100644 index 0000000..e9a996e --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/utils/Nullish.kt @@ -0,0 +1,85 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.utils + +import kotlin.reflect.KMutableProperty0 + +/** + * Nullish sealed interface to define a type that can have a **Defined** `value` including `null`, or be **Undefined** + */ +sealed interface Nullish { + data object Undefined : Nullish + data class Defined(val value: T?) : Nullish + + /** + * `apply` the value of the Nullish object to the nullable property `prop` + * + * If the Nullish object is **Undefined**, do nothing + * + * Otherwise, set the **Defined** `value` of the Nullish on the `prop` even if the `value` is `null` + * + * @param prop the nullable property to set to the **Defined** `value` + */ + fun apply(prop: KMutableProperty0) { + when (this) { + Undefined -> return + is Defined -> prop.set(value) + } + } + + /** + * `apply` the value of the Nullish object to the nullable property `prop` after mapping via `fn` + * + * If the Nullish object is **Undefined**, do nothing + * + * Otherwise, map the **Defined** `value` of the Nullish using `fn` and set the mapped value + * on the `prop` even if the `value` is `null` + * + * @param prop the nullable property to set to a mapped version of the **Defined** `value` + * @param fn a function to map the `value` before setting on the `prop` + */ + fun apply(prop: KMutableProperty0, fn: (T) -> U?) { + when (this) { + is Undefined -> return + is Defined -> { + val currentValue = value + val modifiedValue = currentValue?.let { fn(it) } + prop.set(modifiedValue) + } + } + } + + /** + * If the Nullish object is defined, run the provided function + * @param fn The function to run on the `value` if defined + */ + fun let(fn: (T?) -> Unit) { + when (this) { + is Undefined -> return + is Defined -> { + fn(value) + } + } + } +} + +/** + * Get an attribute from the map of `attributes` and push it into a `Nullish` object + * + * @param map the map of attributes + * @param name the name of the attribute to get + */ +inline fun getAttributeAsNullish( + map: Map, + name: String, +): Nullish { + if (!map.containsKey(name)) { + @Suppress("UNCHECKED_CAST") + return Nullish.Undefined as Nullish + } + + val value = map[name] + if (value is T || value == null) { + return Nullish.Defined(value as? T) + } else { + throw IllegalArgumentException("$name is not an instance of ${T::class.java.simpleName}") + } +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/utils/PrisonerNumberUtils.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/utils/PrisonerNumberUtils.kt new file mode 100644 index 0000000..3093b16 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/utils/PrisonerNumberUtils.kt @@ -0,0 +1,5 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.utils + +import uk.gov.justice.digital.hmpps.healthandmedication.client.prisonersearch.PrisonerSearchClient + +fun validatePrisonerNumber(prisonerSearchClient: PrisonerSearchClient, prisonerNumber: String) = require(prisonerSearchClient.getPrisoner(prisonerNumber)?.prisonerNumber == prisonerNumber) { "Prisoner number '$prisonerNumber' not found" } diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/utils/ReferenceCodeUtils.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/utils/ReferenceCodeUtils.kt new file mode 100644 index 0000000..b490a09 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/utils/ReferenceCodeUtils.kt @@ -0,0 +1,18 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.utils + +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.ReferenceDataCode +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.repository.ReferenceDataCodeRepository + +fun toReferenceDataCode( + referenceDataCodeRepository: ReferenceDataCodeRepository, + id: String?, +): ReferenceDataCode? = id?.let { + referenceDataCodeRepository.findById(it) + .orElseThrow { IllegalArgumentException("Invalid reference data code: $it") } +} + +fun toReferenceDataCodeId(code: String?, domain: String?): String? = code?.let { c -> + domain?.let { d -> + "${d}_$c" + } +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/utils/UuidV7Generator.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/utils/UuidV7Generator.kt new file mode 100644 index 0000000..054c7af --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/utils/UuidV7Generator.kt @@ -0,0 +1,36 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.utils + +import com.fasterxml.uuid.Generators +import com.fasterxml.uuid.NoArgGenerator +import org.hibernate.annotations.IdGeneratorType +import org.hibernate.annotations.ValueGenerationType +import org.hibernate.engine.spi.SharedSessionContractImplementor +import org.hibernate.generator.BeforeExecutionGenerator +import org.hibernate.generator.EventType +import org.hibernate.generator.EventTypeSets +import java.util.EnumSet +import java.util.UUID + +class UuidV7Generator : BeforeExecutionGenerator { + companion object { + val uuidGenerator: NoArgGenerator = Generators.timeBasedEpochGenerator(null) + } + + override fun getEventTypes(): EnumSet = EventTypeSets.INSERT_ONLY + + override fun generate( + session: SharedSessionContractImplementor?, + owner: Any?, + currentValue: Any?, + eventType: EventType?, + ): UUID { + // NB: the default `org.hibernate.id.uuid.UuidGenerator` also ignores session, owner, currentValue and eventType + return uuidGenerator.generate() + } +} + +@IdGeneratorType(UuidV7Generator::class) +@ValueGenerationType(generatedBy = UuidV7Generator::class) +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FIELD) +annotation class GeneratedUuidV7 diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/validator/NullishRangeValidator.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/validator/NullishRangeValidator.kt new file mode 100644 index 0000000..d9ef984 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/validator/NullishRangeValidator.kt @@ -0,0 +1,24 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.validator + +import jakarta.validation.ConstraintValidator +import jakarta.validation.ConstraintValidatorContext +import uk.gov.justice.digital.hmpps.healthandmedication.annotation.NullishRange +import uk.gov.justice.digital.hmpps.healthandmedication.utils.Nullish +import uk.gov.justice.digital.hmpps.healthandmedication.utils.Nullish.Defined +import uk.gov.justice.digital.hmpps.healthandmedication.utils.Nullish.Undefined + +class NullishRangeValidator : ConstraintValidator> { + + private var min: Int = 0 + private var max: Int = 0 + + override fun initialize(constraintAnnotation: NullishRange) { + this.min = constraintAnnotation.min + this.max = constraintAnnotation.max + } + + override fun isValid(value: Nullish, context: ConstraintValidatorContext?): Boolean = when (value) { + Undefined -> true + is Defined -> value.value == null || value.value in min..max + } +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/validator/NullishReferenceDataCodeListValidator.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/validator/NullishReferenceDataCodeListValidator.kt new file mode 100644 index 0000000..6f9864e --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/validator/NullishReferenceDataCodeListValidator.kt @@ -0,0 +1,40 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.validator + +import jakarta.validation.ConstraintValidator +import jakarta.validation.ConstraintValidatorContext +import org.springframework.stereotype.Service +import uk.gov.justice.digital.hmpps.healthandmedication.annotation.NullishReferenceDataCodeList +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.repository.ReferenceDataCodeRepository +import uk.gov.justice.digital.hmpps.healthandmedication.utils.Nullish + +@Service +class NullishReferenceDataCodeListValidator( + private val referenceDataCodeRepository: ReferenceDataCodeRepository, +) : ConstraintValidator>> { + + private var validDomains = emptyList() + + override fun initialize(constraintAnnotation: NullishReferenceDataCodeList) { + this.validDomains = constraintAnnotation.domains.toList() + } + + override fun isValid(value: Nullish>?, context: ConstraintValidatorContext?): Boolean = + when (value) { + Nullish.Undefined -> true + is Nullish.Defined -> { + if (value.value == null) { + false + } else { + val validCodes = validDomains.flatMap { + referenceDataCodeRepository.findAllByDomainAndIncludeInactive( + domain = it, + includeInactive = false, + ) + }.map { it.id } + validCodes.containsAll(value.value) + } + } + + null -> false + } +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/validator/NullishReferenceDataCodeValidator.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/validator/NullishReferenceDataCodeValidator.kt new file mode 100644 index 0000000..5e1c864 --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/validator/NullishReferenceDataCodeValidator.kt @@ -0,0 +1,37 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.validator + +import jakarta.validation.ConstraintValidator +import jakarta.validation.ConstraintValidatorContext +import org.springframework.stereotype.Service +import uk.gov.justice.digital.hmpps.healthandmedication.annotation.NullishReferenceDataCode +import uk.gov.justice.digital.hmpps.healthandmedication.jpa.repository.ReferenceDataCodeRepository +import uk.gov.justice.digital.hmpps.healthandmedication.utils.Nullish +import kotlin.jvm.optionals.getOrNull + +@Service +class NullishReferenceDataCodeValidator( + private val referenceDataCodeRepository: ReferenceDataCodeRepository, +) : + ConstraintValidator> { + + private var validDomains = emptyList() + + private var allowNull = true + + override fun initialize(constraintAnnotation: NullishReferenceDataCode) { + this.validDomains = constraintAnnotation.domains.toList() + this.allowNull = constraintAnnotation.allowNull + } + + override fun isValid(value: Nullish?, context: ConstraintValidatorContext?): Boolean = + when (value) { + Nullish.Undefined -> true + is Nullish.Defined -> { + value.value?.let { + referenceDataCodeRepository.findById(it).getOrNull()?.domain?.code in validDomains + } ?: allowNull + } + + null -> false + } +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/validator/NullishShoeSizeValidator.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/validator/NullishShoeSizeValidator.kt new file mode 100644 index 0000000..969666b --- /dev/null +++ b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/validator/NullishShoeSizeValidator.kt @@ -0,0 +1,34 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.validator + +import jakarta.validation.ConstraintValidator +import jakarta.validation.ConstraintValidatorContext +import uk.gov.justice.digital.hmpps.healthandmedication.annotation.NullishShoeSize +import uk.gov.justice.digital.hmpps.healthandmedication.utils.Nullish +import uk.gov.justice.digital.hmpps.healthandmedication.utils.Nullish.Defined +import uk.gov.justice.digital.hmpps.healthandmedication.utils.Nullish.Undefined + +class NullishShoeSizeValidator : ConstraintValidator> { + + private var min: String = "0" + private var max: String = "0" + + override fun initialize(constraintAnnotation: NullishShoeSize) { + this.min = constraintAnnotation.min + this.max = constraintAnnotation.max + } + + override fun isValid(value: Nullish, context: ConstraintValidatorContext?): Boolean = when (value) { + Undefined -> true + is Defined -> value.value == null || isValidShoeSize(value.value) + } + + private fun isValidShoeSize(shoeSize: String): Boolean { + val validShowSize = """^([1-9]|1[0-9]|2[0-5])(\.5|\.0)?$""".toRegex() + if (!shoeSize.matches(validShowSize)) { + return false + } + + val number = shoeSize.toDouble() + return number in 1.0..25.0 + } +} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/config/HealthAndMedicationApiExceptionHandler.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/config/HealthAndMedicationApiExceptionHandler.kt deleted file mode 100644 index fa559f1..0000000 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/config/HealthAndMedicationApiExceptionHandler.kt +++ /dev/null @@ -1,65 +0,0 @@ -package uk.gov.justice.digital.hmpps.healthandmedicationapi.config - -import jakarta.validation.ValidationException -import org.slf4j.LoggerFactory -import org.springframework.http.HttpStatus.BAD_REQUEST -import org.springframework.http.HttpStatus.FORBIDDEN -import org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR -import org.springframework.http.HttpStatus.NOT_FOUND -import org.springframework.http.ResponseEntity -import org.springframework.security.access.AccessDeniedException -import org.springframework.web.bind.annotation.ExceptionHandler -import org.springframework.web.bind.annotation.RestControllerAdvice -import org.springframework.web.servlet.resource.NoResourceFoundException -import uk.gov.justice.hmpps.kotlin.common.ErrorResponse - -@RestControllerAdvice -class HealthAndMedicationApiExceptionHandler { - @ExceptionHandler(ValidationException::class) - fun handleValidationException(e: ValidationException): ResponseEntity = ResponseEntity - .status(BAD_REQUEST) - .body( - ErrorResponse( - status = BAD_REQUEST, - userMessage = "Validation failure: ${e.message}", - developerMessage = e.message, - ), - ).also { log.info("Validation exception: {}", e.message) } - - @ExceptionHandler(NoResourceFoundException::class) - fun handleNoResourceFoundException(e: NoResourceFoundException): ResponseEntity = ResponseEntity - .status(NOT_FOUND) - .body( - ErrorResponse( - status = NOT_FOUND, - userMessage = "No resource found failure: ${e.message}", - developerMessage = e.message, - ), - ).also { log.info("No resource found exception: {}", e.message) } - - @ExceptionHandler(AccessDeniedException::class) - fun handleAccessDeniedException(e: AccessDeniedException): ResponseEntity = ResponseEntity - .status(FORBIDDEN) - .body( - ErrorResponse( - status = FORBIDDEN, - userMessage = "Forbidden: ${e.message}", - developerMessage = e.message, - ), - ).also { log.debug("Forbidden (403) returned: {}", e.message) } - - @ExceptionHandler(Exception::class) - fun handleException(e: Exception): ResponseEntity = ResponseEntity - .status(INTERNAL_SERVER_ERROR) - .body( - ErrorResponse( - status = INTERNAL_SERVER_ERROR, - userMessage = "Unexpected error: ${e.message}", - developerMessage = e.message, - ), - ).also { log.error("Unexpected exception", e) } - - private companion object { - private val log = LoggerFactory.getLogger(this::class.java) - } -} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/config/OpenApiConfiguration.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/config/OpenApiConfiguration.kt deleted file mode 100644 index d12a914..0000000 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/config/OpenApiConfiguration.kt +++ /dev/null @@ -1,58 +0,0 @@ -package uk.gov.justice.digital.hmpps.healthandmedicationapi.config - -import io.swagger.v3.oas.models.Components -import io.swagger.v3.oas.models.OpenAPI -import io.swagger.v3.oas.models.info.Contact -import io.swagger.v3.oas.models.info.Info -import io.swagger.v3.oas.models.security.SecurityRequirement -import io.swagger.v3.oas.models.security.SecurityScheme -import io.swagger.v3.oas.models.servers.Server -import io.swagger.v3.oas.models.tags.Tag -import org.springframework.boot.info.BuildProperties -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration - -@Configuration -class OpenApiConfiguration(buildProperties: BuildProperties) { - private val version: String = buildProperties.version - - @Bean - fun customOpenAPI(): OpenAPI = OpenAPI() - .servers( - listOf( - Server().url("https://health-and-medication-api-dev.hmpps.service.justice.gov.uk").description("Development"), - Server().url("https://health-and-medication-api-preprod.hmpps.service.justice.gov.uk").description("Pre-Production"), - Server().url("https://health-and-medication-api.hmpps.service.justice.gov.uk").description("Production"), - Server().url("http://localhost:8080").description("Local"), - ), - ) - .tags( - listOf( - // TODO: Remove the Popular and Examples tag and start adding your own tags to group your resources - Tag().name("Popular") - .description("The most popular endpoints. Look here first when deciding which endpoint to use."), - Tag().name("Examples").description("Endpoints for searching for a prisoner within a prison"), - ), - ) - .info( - Info().title("HMPPS Health And Medication Api").version(version) - .contact(Contact().name("HMPPS Digital Studio").email("feedback@digital.justice.gov.uk")), - ) - // TODO: Remove the default security schema and start adding your own schemas and roles to describe your - // service authorisation requirements - .components( - Components().addSecuritySchemes( - "health-and-medication-api-ui-role", - SecurityScheme().addBearerJwtRequirement("ROLE_TEMPLATE_KOTLIN__UI"), - ), - ) - .addSecurityItem(SecurityRequirement().addList("health-and-medication-api-ui-role", listOf("read"))) -} - -private fun SecurityScheme.addBearerJwtRequirement(role: String): SecurityScheme = - type(SecurityScheme.Type.HTTP) - .scheme("bearer") - .bearerFormat("JWT") - .`in`(SecurityScheme.In.HEADER) - .name("Authorization") - .description("A HMPPS Auth access token with the `$role` role.") diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/config/WebClientConfiguration.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/config/WebClientConfiguration.kt deleted file mode 100644 index 9696526..0000000 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/config/WebClientConfiguration.kt +++ /dev/null @@ -1,18 +0,0 @@ -package uk.gov.justice.digital.hmpps.healthandmedicationapi.config - -import org.springframework.beans.factory.annotation.Value -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import org.springframework.web.reactive.function.client.WebClient -import uk.gov.justice.hmpps.kotlin.auth.healthWebClient -import java.time.Duration - -@Configuration -class WebClientConfiguration( - @Value("\${hmpps-auth.url}") val hmppsAuthBaseUri: String, - @Value("\${api.health-timeout:2s}") val healthTimeout: Duration, - @Value("\${api.timeout:20s}") val timeout: Duration, -) { - @Bean - fun hmppsAuthHealthWebClient(builder: WebClient.Builder): WebClient = builder.healthWebClient(hmppsAuthBaseUri, healthTimeout) -} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/resource/ExampleResource.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/resource/ExampleResource.kt deleted file mode 100644 index b7d5931..0000000 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/resource/ExampleResource.kt +++ /dev/null @@ -1,77 +0,0 @@ -package uk.gov.justice.digital.hmpps.healthandmedicationapi.resource - -import io.swagger.v3.oas.annotations.Operation -import io.swagger.v3.oas.annotations.media.Content -import io.swagger.v3.oas.annotations.media.Schema -import io.swagger.v3.oas.annotations.responses.ApiResponse -import io.swagger.v3.oas.annotations.security.SecurityRequirement -import io.swagger.v3.oas.annotations.tags.Tag -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.RequestMapping -import org.springframework.web.bind.annotation.RestController -import uk.gov.justice.digital.hmpps.healthandmedicationapi.service.ExampleApiService -import uk.gov.justice.hmpps.kotlin.common.ErrorResponse -import java.time.LocalDateTime - -// This controller is expected to be called from the UI - so the hmpps-template-typescript project. -// TODO: This is an example and should renamed / replaced -@RestController -// Role here is specific to the UI. -@PreAuthorize("hasRole('ROLE_TEMPLATE_KOTLIN__UI')") -@RequestMapping(value = ["/example"], produces = ["application/json"]) -class ExampleResource(private val exampleApiService: ExampleApiService) { - - @GetMapping("/time") - @Tag(name = "Examples") - @Operation( - summary = "Retrieve today's date and time", - description = "This is an example endpoint that calls a service to return the current date and time. Requires role ROLE_TEMPLATE_KOTLIN__UI", - security = [SecurityRequirement(name = "health-and-medication-api-ui-role")], - responses = [ - ApiResponse(responseCode = "200", description = "today's date and time"), - ApiResponse( - responseCode = "401", - description = "Unauthorized to access this endpoint", - content = [Content(mediaType = "application/json", schema = Schema(implementation = ErrorResponse::class))], - ), - ApiResponse( - responseCode = "403", - description = "Forbidden to access this endpoint", - content = [Content(mediaType = "application/json", schema = Schema(implementation = ErrorResponse::class))], - ), - ], - ) - fun getTime(): LocalDateTime = exampleApiService.getTime() - - @GetMapping("/message/{parameter}") - @Tag(name = "Popular") - @Operation( - summary = "Example message endpoint to call another API", - description = """This is an example endpoint that calls back to the kotlin template. - It will return a 404 response as the /example-external-api endpoint hasn't been implemented, so we use wiremock - in integration tests to simulate other responses. - Requires role ROLE_TEMPLATE_KOTLIN__UI""", - security = [SecurityRequirement(name = "health-and-medication-api-ui-role")], - responses = [ - ApiResponse(responseCode = "200", description = "a message with a parameter"), - ApiResponse( - responseCode = "401", - description = "Unauthorized to access this endpoint", - content = [Content(mediaType = "application/json", schema = Schema(implementation = ErrorResponse::class))], - ), - ApiResponse( - responseCode = "403", - description = "Forbidden to access this endpoint", - content = [Content(mediaType = "application/json", schema = Schema(implementation = ErrorResponse::class))], - ), - ApiResponse( - responseCode = "404", - description = "Not found", - content = [Content(mediaType = "application/json", schema = Schema(implementation = ErrorResponse::class))], - ), - ], - ) - fun getMessage(@PathVariable parameter: String) = "{ message: \"Hello $parameter\"}" -} diff --git a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/service/ExampleApiService.kt b/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/service/ExampleApiService.kt deleted file mode 100644 index c2c2230..0000000 --- a/src/main/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/service/ExampleApiService.kt +++ /dev/null @@ -1,9 +0,0 @@ -package uk.gov.justice.digital.hmpps.healthandmedicationapi.service - -import org.springframework.stereotype.Service -import java.time.LocalDateTime - -@Service -class ExampleApiService { - fun getTime(): LocalDateTime = LocalDateTime.now() -} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index fe96863..eec1212 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -10,15 +10,30 @@ spring: max-in-memory-size: 10MB jackson: - date-format: "yyyy-MM-dd HH:mm:ss" + date-format: "yyyy-MM-dd'T'HH:mm:ssZ" serialization: - WRITE_DATES_AS_TIMESTAMPS: false + write-dates-as-timestamps: false + write-dates-with-context-time-zone: true + write-dates-with-zone-id: false + time-zone: "Europe/London" security: oauth2: resourceserver: jwt: - jwk-set-uri: ${hmpps-auth.url}/.well-known/jwks.json + jwk-set-uri: ${api.hmpps-auth.base-url}/.well-known/jwks.json + + client: + registration: + hmpps-health-and-medication-api: + provider: hmpps-auth + client-id: ${client.id} + client-secret: ${client.secret} + authorization-grant-type: client_credentials + scope: read + provider: + hmpps-auth: + token-uri: ${api.hmpps-auth.base-url}/oauth/token jpa: open-in-view: false diff --git a/src/main/resources/db/migration/common/V1__reference_data.sql b/src/main/resources/db/migration/common/V1__reference_data.sql new file mode 100644 index 0000000..38c53ec --- /dev/null +++ b/src/main/resources/db/migration/common/V1__reference_data.sql @@ -0,0 +1,54 @@ +-- Reference Data Domain + +CREATE TABLE reference_data_domain +( + code VARCHAR(40) NOT NULL, + parent_domain_code VARCHAR(40), + description VARCHAR(100) NOT NULL, + list_sequence INT DEFAULT 0, + created_at TIMESTAMP WITH TIME ZONE NOT NULL, + created_by VARCHAR(40) NOT NULL, + last_modified_at TIMESTAMP WITH TIME ZONE, + last_modified_by VARCHAR(40), + deactivated_at TIMESTAMP WITH TIME ZONE, + deactivated_by VARCHAR(40), + + CONSTRAINT reference_data_domain_pk PRIMARY KEY (code), + CONSTRAINT reference_data_domain_parent_fk FOREIGN KEY (parent_domain_code) REFERENCES reference_data_domain (code) +); + +CREATE INDEX reference_data_domain_description_idx ON reference_data_domain (description); +CREATE INDEX reference_data_domain_list_sequence_idx ON reference_data_domain (list_sequence); +CREATE INDEX reference_data_domain_created_at_idx ON reference_data_domain (created_at); + +COMMENT ON TABLE reference_data_domain IS 'Reference data domains for health and medication data'; +COMMENT ON COLUMN reference_data_domain.list_sequence IS 'Used for ordering reference data correctly in lists. 0 is default order by description'; +COMMENT ON COLUMN reference_data_domain.parent_domain_code IS 'Used for creating subdomains of other codes'; + +-- Reference Data Code + +CREATE TABLE reference_data_code +( + id VARCHAR(81) NOT NULL, + domain VARCHAR(40) NOT NULL REFERENCES reference_data_domain (code), + code VARCHAR(40) NOT NULL, + description VARCHAR(100) NOT NULL, + list_sequence INT DEFAULT 0, + created_at TIMESTAMP WITH TIME ZONE NOT NULL, + created_by VARCHAR(40) NOT NULL, + last_modified_at TIMESTAMP WITH TIME ZONE, + last_modified_by VARCHAR(40), + deactivated_at TIMESTAMP WITH TIME ZONE, + deactivated_by VARCHAR(40), + + CONSTRAINT reference_data_code_pk PRIMARY KEY (id), + UNIQUE (code, domain) +); + +CREATE INDEX reference_data_code_description_idx ON reference_data_code (description); +CREATE INDEX reference_data_code_list_sequence_idx ON reference_data_code (list_sequence); +CREATE INDEX reference_data_code_created_at_idx ON reference_data_code (created_at); + +COMMENT ON TABLE reference_data_code IS 'Reference data codes for health and medication data'; +COMMENT ON COLUMN reference_data_code.id IS 'Primary key, uses `domain`_`code`'; +COMMENT ON COLUMN reference_data_code.list_sequence IS 'Used for ordering reference data correctly in lists and dropdowns. 0 is default order by description'; diff --git a/src/main/resources/db/migration/common/V2__field_metadata.sql b/src/main/resources/db/migration/common/V2__field_metadata.sql new file mode 100644 index 0000000..ddcc940 --- /dev/null +++ b/src/main/resources/db/migration/common/V2__field_metadata.sql @@ -0,0 +1,15 @@ +CREATE TABLE field_metadata +( + prisoner_number VARCHAR(7) NOT NULL, + field VARCHAR(40), + last_modified_at TIMESTAMP WITH TIME ZONE NOT NULL, + last_modified_by VARCHAR(40) NOT NULL, + + CONSTRAINT field_metadata_pk PRIMARY KEY (prisoner_number, field) +); + +COMMENT ON TABLE field_metadata IS 'Field level metadata such as when a field was modified and who by'; +COMMENT ON COLUMN field_metadata.prisoner_number IS 'First part of the primary key - the identifier of a prisoner (also often called prison number, NOMS number, offender number...)'; +COMMENT ON COLUMN field_metadata.field IS 'Second part of the primary key - the field name that the metadata is linked to'; +COMMENT ON COLUMN field_metadata.last_modified_at IS 'Timestamp of last modification'; +COMMENT ON COLUMN field_metadata.last_modified_by IS 'The username of the user modifying the record'; diff --git a/src/main/resources/db/migration/common/V3__field_history.sql b/src/main/resources/db/migration/common/V3__field_history.sql new file mode 100644 index 0000000..34e8fd9 --- /dev/null +++ b/src/main/resources/db/migration/common/V3__field_history.sql @@ -0,0 +1,37 @@ +CREATE TABLE field_history +( + field_history_id BIGSERIAL NOT NULL, + prisoner_number VARCHAR(7) NOT NULL, + field VARCHAR(40), + value_int INT, + value_string VARCHAR(40), + value_ref VARCHAR(81), + value_json TEXT, + created_at TIMESTAMP WITH TIME ZONE NOT NULL, + created_by VARCHAR(40) NOT NULL, + merged_at TIMESTAMP WITH TIME ZONE, + merged_from VARCHAR(7), + + CONSTRAINT field_history_pk PRIMARY KEY (field_history_id), + CONSTRAINT value_ref_reference_data_code_fk FOREIGN KEY (value_ref) REFERENCES reference_data_code (id), + CONSTRAINT field_history_one_value_only_ck CHECK ( + (value_int IS NULL AND value_string IS NULL AND value_ref IS NULL) OR + (value_int IS NOT NULL AND value_string IS NULL AND value_ref IS NULL) OR + (value_int IS NULL AND value_string IS NOT NULL AND value_ref IS NULL) OR + (value_int IS NULL AND value_string IS NULL AND value_ref IS NOT NULL) + ) +); + +CREATE INDEX field_history_prisoner_number_field_idx ON field_history (prisoner_number, field); + +COMMENT ON TABLE field_history IS 'The field level history of prisoner health and medication data'; +COMMENT ON COLUMN field_history.prisoner_number IS 'The identifier of a prisoner (also often called prison number, NOMS number, offender number...)'; +COMMENT ON COLUMN field_history.field IS 'The field that this history record is for'; +COMMENT ON COLUMN field_history.value_int IS 'The integer value for the field if the field represents an integer'; +COMMENT ON COLUMN field_history.value_string IS 'The string value for the field if the field represents a string'; +COMMENT ON COLUMN field_history.value_ref IS 'The reference_data_code.id for the field if the field represents a foreign key to reference_data_code'; +COMMENT ON COLUMN field_history.value_json IS 'Used for storing generic json data in text form'; +COMMENT ON COLUMN field_history.created_at IS 'Timestamp of when the history record was created'; +COMMENT ON COLUMN field_history.created_by IS 'The username of the user creating the history record'; +COMMENT ON COLUMN field_history.merged_at IS 'Timestamp of when the history record was merged from another prisoner number'; +COMMENT ON COLUMN field_history.merged_from IS 'The old prisoner number that this history item was merged from'; diff --git a/src/main/resources/db/migration/common/V4__health.sql b/src/main/resources/db/migration/common/V4__health.sql new file mode 100644 index 0000000..de7137f --- /dev/null +++ b/src/main/resources/db/migration/common/V4__health.sql @@ -0,0 +1,12 @@ +CREATE TABLE health +( + prisoner_number VARCHAR(7) NOT NULL, + smoker_or_vaper VARCHAR(81), + + CONSTRAINT health_pk PRIMARY KEY (prisoner_number), + CONSTRAINT smoker_reference_data_code_fk FOREIGN KEY (smoker_or_vaper) REFERENCES reference_data_code (id) +); + +COMMENT ON TABLE health IS 'The health related information of a prisoner'; +COMMENT ON COLUMN health.prisoner_number IS 'Primary key - the identifier of a prisoner (also often called prison number, NOMS number, offender number...)'; +COMMENT ON COLUMN health.smoker_or_vaper IS 'Whether the prisoner is a smoker or vaper, from reference data domain SMOKE'; diff --git a/src/main/resources/db/migration/common/V5__medical_dietary_requirement.sql b/src/main/resources/db/migration/common/V5__medical_dietary_requirement.sql new file mode 100644 index 0000000..6dd9649 --- /dev/null +++ b/src/main/resources/db/migration/common/V5__medical_dietary_requirement.sql @@ -0,0 +1,18 @@ +CREATE TABLE medical_dietary_requirement +( + id BIGSERIAL NOT NULL, + prisoner_number VARCHAR(7) NOT NULL, + dietary_requirement VARCHAR(81) NOT NULL, + other_text VARCHAR(255), + + CONSTRAINT medical_dietary_requirement_fk PRIMARY KEY (id), + CONSTRAINT dietary_requirement_fk FOREIGN KEY (dietary_requirement) REFERENCES reference_data_code (id) +); + +CREATE INDEX medical_dietary_requirement_prisoner_number_idx ON medical_dietary_requirement (prisoner_number); + +COMMENT ON TABLE medical_dietary_requirement IS 'The list of food allergies the prisoner has'; +COMMENT ON COLUMN medical_dietary_requirement.id IS 'The primary key, in case prisoners require multiple "other" values'; +COMMENT ON COLUMN medical_dietary_requirement.prisoner_number IS 'The identifier of a prisoner (also often called prison number, NOMS number, offender number...)'; +COMMENT ON COLUMN medical_dietary_requirement.dietary_requirement IS 'The dietary requirement relevant to a prisoner (the prisoner can have more than one)'; +COMMENT ON COLUMN medical_dietary_requirement.other_text IS 'The text used for when someone enters an allergy of "other"'; diff --git a/src/main/resources/db/migration/common/V6__medical_dietary_requirement_data.sql b/src/main/resources/db/migration/common/V6__medical_dietary_requirement_data.sql new file mode 100644 index 0000000..61c78b9 --- /dev/null +++ b/src/main/resources/db/migration/common/V6__medical_dietary_requirement_data.sql @@ -0,0 +1,17 @@ +-- Seed data for tests +INSERT INTO reference_data_domain (code, description, list_sequence, created_at, created_by) +VALUES ('MEDICAL_DIET', 'Medical diet', 0, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'); + +INSERT INTO reference_data_code (id, domain, code, description, list_sequence, created_at, created_by) +VALUES +('MEDICAL_DIET_COELIAC', 'MEDICAL_DIET', 'COELIAC', 'Coeliac (cannot eat gluten)', 0, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'), +('MEDICAL_DIET_DIABETIC_TYPE_1', 'MEDICAL_DIET', 'DIABETIC_TYPE_1', 'Diabetic Type 1', 1, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'), +('MEDICAL_DIET_DIABETIC_TYPE_2', 'MEDICAL_DIET', 'DIABETIC_TYPE_2', 'Diabetic Type 2', 2, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'), +('MEDICAL_DIET_DYSPHAGIA', 'MEDICAL_DIET', 'DYSPHAGIA', 'Dysphagia (has problems swallowing food)', 3, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'), +('MEDICAL_DIET_EATING_DISORDER', 'MEDICAL_DIET', 'EATING_DISORDER', 'Eating disorder', 4, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'), +('MEDICAL_DIET_LACTOSE_INTOLERANT', 'MEDICAL_DIET', 'LACTOSE_INTOLERANT', 'Lactose intolerant', 5, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'), +('MEDICAL_DIET_LOW_CHOLESTEROL', 'MEDICAL_DIET', 'LOW_CHOLESTEROL', 'Low cholesterol', 6, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'), +('MEDICAL_DIET_LOW_PHOSPHOROUS', 'MEDICAL_DIET', 'LOW_PHOSPHOROUS', 'Low Phosphorous Diet', 7, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'), +('MEDICAL_DIET_NUTRIENT_DEFICIENCY', 'MEDICAL_DIET', 'NUTRIENT_DEFICIENCY', 'Nutrient Deficiency', 7, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'), +('MEDICAL_DIET_OTHER', 'MEDICAL_DIET', 'OTHER', 'Other', 8, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'); + diff --git a/src/main/resources/db/migration/common/V7__food_allergy.sql b/src/main/resources/db/migration/common/V7__food_allergy.sql new file mode 100644 index 0000000..9f72df4 --- /dev/null +++ b/src/main/resources/db/migration/common/V7__food_allergy.sql @@ -0,0 +1,18 @@ +CREATE TABLE food_allergy +( + id BIGSERIAL NOT NULL, + prisoner_number VARCHAR(7) NOT NULL, + allergy VARCHAR(81) NOT NULL, + other_text VARCHAR(255), + + CONSTRAINT food_allergy_fk PRIMARY KEY (id), + CONSTRAINT allergy_fk FOREIGN KEY (allergy) REFERENCES reference_data_code (id) +); + +CREATE INDEX food_allergies_prisoner_number_idx ON food_allergy (prisoner_number); + +COMMENT ON TABLE food_allergy IS 'The list of food allergies the prisoner has'; +COMMENT ON COLUMN food_allergy.id IS 'The primary key, in case prisoners require multiple "other" values'; +COMMENT ON COLUMN food_allergy.prisoner_number IS 'The identifier of a prisoner (also often called prison number, NOMS number, offender number...)'; +COMMENT ON COLUMN food_allergy.allergy IS 'The allergy relevant to a prisoner (the prisoner can have more than one)'; +COMMENT ON COLUMN food_allergy.other_text IS 'The text used for when someone enters an allergy of "other"'; diff --git a/src/main/resources/db/migration/common/V8__food_allergy_data.sql b/src/main/resources/db/migration/common/V8__food_allergy_data.sql new file mode 100644 index 0000000..b2c4a3e --- /dev/null +++ b/src/main/resources/db/migration/common/V8__food_allergy_data.sql @@ -0,0 +1,22 @@ +-- Seed data for tests +INSERT INTO reference_data_domain (code, description, list_sequence, created_at, created_by) +VALUES ('FOOD_ALLERGY', 'Food allergy', 0, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'); + +INSERT INTO reference_data_code (id, domain, code, description, list_sequence, created_at, created_by) +VALUES +('FOOD_ALLERGY_CELERY', 'FOOD_ALLERGY', 'CELERY', 'Celery', 0, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'), +('FOOD_ALLERGY_GLUTEN', 'FOOD_ALLERGY', 'GLUTEN', 'Cereals containing gluten', 0, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'), +('FOOD_ALLERGY_CRUSTACEANS', 'FOOD_ALLERGY', 'CRUSTACEANS', 'Crustaceans', 0, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'), +('FOOD_ALLERGY_EGG', 'FOOD_ALLERGY', 'EGG', 'Egg', 0, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'), +('FOOD_ALLERGY_FISH', 'FOOD_ALLERGY', 'FISH', 'Fish', 0, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'), +('FOOD_ALLERGY_LUPIN', 'FOOD_ALLERGY', 'LUPIN', 'Lupin', 0, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'), +('FOOD_ALLERGY_MILK', 'FOOD_ALLERGY', 'MILK', 'Milk', 0, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'), +('FOOD_ALLERGY_MOLLUSCS', 'FOOD_ALLERGY', 'MOLLUSCS', 'Molluscs', 0, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'), +('FOOD_ALLERGY_MUSTARD', 'FOOD_ALLERGY', 'MUSTARD', 'Mustard', 0, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'), +('FOOD_ALLERGY_PEANUTS', 'FOOD_ALLERGY', 'PEANUTS', 'Peanuts', 0, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'), +('FOOD_ALLERGY_SESAME', 'FOOD_ALLERGY', 'SESAME', 'Sesame', 0, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'), +('FOOD_ALLERGY_SOYA', 'FOOD_ALLERGY', 'SOYA', 'Soya', 0, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'), +('FOOD_ALLERGY_SULPHUR_DIOXIDE', 'FOOD_ALLERGY', 'SULPHUR_DIOXIDE', 'Sulphur Dioxide', 0, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'), +('FOOD_ALLERGY_TREE_NUTS', 'FOOD_ALLERGY', 'TREE_NUTS', 'Tree nuts', 0, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'), +('FOOD_ALLERGY_OTHER', 'FOOD_ALLERGY', 'OTHER', 'Other', 0, '2025-01-16 00:00:00+0100', 'CONNECT_DPS'); + diff --git a/src/main/resources/db/migration/common/placeholder b/src/main/resources/db/migration/common/placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/resources/db/migration/test/V2_1__test_reference_data_seed.sql b/src/main/resources/db/migration/test/V2_1__test_reference_data_seed.sql new file mode 100644 index 0000000..591eb04 --- /dev/null +++ b/src/main/resources/db/migration/test/V2_1__test_reference_data_seed.sql @@ -0,0 +1,26 @@ +-- Seed data for tests +INSERT INTO reference_data_domain (code, description, list_sequence, created_at, created_by) VALUES +-- Medical dietary requirements +('MEDICAL_DIET_FREE_FROM', 'MEDICAL_DIET', 'FREE_FROM', 'Other', 0, '2024-07-21 14:00:00+0100', 'CONNECT_DPS'), +('MEDICAL_DIET_LOW_FAT', 'MEDICAL_DIET', 'LOW_FAT', 'Low fat', 0, '2024-07-21 14:00:00+0100', 'CONNECT_DPS'), +('MEDICAL_DIET_LOW_SALT', 'MEDICAL_DIET', 'LOW_SALT', 'Low salt', 0, '2024-07-21 14:00:00+0100', 'CONNECT_DPS'), +('MEDICAL_DIET_DIABETIC', 'MEDICAL_DIET', 'DIABETIC', 'Diabetic', 0, '2024-07-21 14:00:00+0100', 'CONNECT_DPS'), +('MEDICAL_DIET_LOW_CHOLESTEROL', 'MEDICAL_DIET', 'LOW_CHOLESTEROL', 'Low cholesterol', 0, '2024-07-21 14:00:00+0100', + 'CONNECT_DPS'), +('MEDICAL_DIET_COELIAC', 'MEDICAL_DIET', 'COELIAC', 'Coeliac', 0, '2024-07-21 14:00:00+0100', 'CONNECT_DPS'), +('MEDICAL_DIET_PREGNANT', 'MEDICAL_DIET', 'PREGNANT', 'Pregnant', 0, '2024-07-21 14:00:00+0100', 'CONNECT_DPS'), +('MEDICAL_DIET_DISORDERED_EATING', 'MEDICAL_DIET', 'DISORDERED_EATING', 'Disordered eating', 0, + '2024-07-21 14:00:00+0100', + 'CONNECT_DPS'), +('MEDICAL_DIET_OTHER', 'MEDICAL_DIET', 'OTHER', 'Other', 0, '2024-07-21 14:00:00+0100', 'CONNECT_DPS'); + + +INSERT INTO reference_data_domain (code, description, list_sequence, created_at, created_by, last_modified_at, + last_modified_by, deactivated_at, deactivated_by) +VALUES ('INACTIVE', 'Inactive domain for tests', 0, '2024-07-11 17:00:00+0100', 'OMS_OWNER', '2024-07-11 17:00:00+0100', + 'OMS_OWNER', '2024-07-11 17:00:00+0100', 'OMS_OWNER'); + +INSERT INTO reference_data_code (id, domain, code, description, list_sequence, created_at, created_by, last_modified_at, + last_modified_by, deactivated_at, deactivated_by) +VALUES ('FACE_INACTIVE', 'FACE', 'INACTIVE', 'Inactive code for tests', 0, '2024-07-11 17:00:00+0100', 'OMS_OWNER', + '2024-07-11 17:00:00+0100', 'OMS_OWNER', '2024-07-11 17:00:00+0100', 'OMS_OWNER'); diff --git a/src/main/resources/db/migration/test/V9_1__reference_data_subdomains.sql b/src/main/resources/db/migration/test/V9_1__reference_data_subdomains.sql new file mode 100644 index 0000000..53cfc71 --- /dev/null +++ b/src/main/resources/db/migration/test/V9_1__reference_data_subdomains.sql @@ -0,0 +1,21 @@ +-- Seed data for tests +INSERT INTO reference_data_domain (code, parent_domain_code, description, list_sequence, created_at, created_by) +VALUES ('FREE_FROM', 'MEDICAL_DIET', 'Medical diet - Free from', 0, '2024-07-21 14:00:00+0100', 'CONNECT_DPS'); + +INSERT INTO reference_data_code (id, domain, code, description, list_sequence, created_at, created_by) +VALUES +-- Hair +('FREE_FROM_MONOAMINE_OXIDASE_INHIBITORS', 'FREE_FROM', 'MONOAMINE_OXIDASE_INHIBITORS', + 'Any foods that interact with monoamine oxidase inhibitors', 0, '2024-07-11 17:00:00+0100', 'CONNECT_DPS'), +('FREE_FROM_CHEESE', 'FREE_FROM', 'CHEESE', 'Cheese', 0, '2024-07-11 17:00:00+0100', 'CONNECT_DPS'), +('FREE_FROM_DAIRY', 'FREE_FROM', 'DAIRY', 'Dairy', 0, '2024-07-11 17:00:00+0100', 'CONNECT_DPS'), +('FREE_FROM_EGG', 'FREE_FROM', 'EGG', 'Egg', 0, '2024-07-11 17:00:00+0100', 'CONNECT_DPS'), +('FREE_FROM_FAT', 'FREE_FROM', 'FAT', 'Fat', 0, '2024-07-11 17:00:00+0100', 'CONNECT_DPS'), +('FREE_FROM_FRIED_FOOD', 'FREE_FROM', 'FRIED_FOOD', 'Fried food', 0, '2024-07-11 17:00:00+0100', 'CONNECT_DPS'), +('FREE_FROM_FISH', 'FREE_FROM', 'FISH', 'Fish', 0, '2024-07-11 17:00:00+0100', 'CONNECT_DPS'), +('FREE_FROM_GARLIC', 'FREE_FROM', 'GARLIC', 'Garlic', 0, '2024-07-11 17:00:00+0100', 'CONNECT_DPS'), +('FREE_FROM_LACTOSE', 'FREE_FROM', 'LACTOSE', 'Lactose', 0, '2024-07-11 17:00:00+0100', 'CONNECT_DPS'), +('FREE_FROM_ONION', 'FREE_FROM', 'ONION', 'Onion', 0, '2024-07-11 17:00:00+0100', 'CONNECT_DPS'), +('FREE_FROM_PORK', 'FREE_FROM', 'PORK', 'Pork', 0, '2024-07-11 17:00:00+0100', 'CONNECT_DPS'), +('FREE_FROM_POTATO', 'FREE_FROM', 'POTATO', 'Potato', 0, '2024-07-11 17:00:00+0100', 'CONNECT_DPS'); + diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml index cbf0272..dad3934 100644 --- a/src/main/resources/logback-spring.xml +++ b/src/main/resources/logback-spring.xml @@ -12,7 +12,7 @@ - + diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/config/FixedClock.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/config/FixedClock.kt similarity index 86% rename from src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/config/FixedClock.kt rename to src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/config/FixedClock.kt index e779203..931c6b4 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/config/FixedClock.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/config/FixedClock.kt @@ -1,4 +1,4 @@ -package uk.gov.justice.digital.hmpps.healthandmedicationapi.config +package uk.gov.justice.digital.hmpps.healthandmedication.config import java.time.Clock import java.time.Instant diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/integration/IntegrationTestBase.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/integration/IntegrationTestBase.kt similarity index 78% rename from src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/integration/IntegrationTestBase.kt rename to src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/integration/IntegrationTestBase.kt index 8bfb36a..3d1b8a2 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/integration/IntegrationTestBase.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/integration/IntegrationTestBase.kt @@ -1,4 +1,4 @@ -package uk.gov.justice.digital.hmpps.healthandmedicationapi.integration +package uk.gov.justice.digital.hmpps.healthandmedication.integration import org.junit.jupiter.api.extension.ExtendWith import org.springframework.beans.factory.annotation.Autowired @@ -6,8 +6,8 @@ import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT import org.springframework.http.HttpHeaders import org.springframework.test.web.reactive.server.WebTestClient -import uk.gov.justice.digital.hmpps.healthandmedicationapi.integration.wiremock.HmppsAuthApiExtension -import uk.gov.justice.digital.hmpps.healthandmedicationapi.integration.wiremock.HmppsAuthApiExtension.Companion.hmppsAuth +import uk.gov.justice.digital.hmpps.healthandmedication.integration.wiremock.HmppsAuthApiExtension +import uk.gov.justice.digital.hmpps.healthandmedication.integration.wiremock.HmppsAuthApiExtension.Companion.hmppsAuth import uk.gov.justice.hmpps.test.kotlin.auth.JwtAuthorisationHelper @ExtendWith(HmppsAuthApiExtension::class) diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/integration/NotFoundTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/integration/NotFoundTest.kt similarity index 81% rename from src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/integration/NotFoundTest.kt rename to src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/integration/NotFoundTest.kt index 3311400..53c07d2 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/integration/NotFoundTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/integration/NotFoundTest.kt @@ -1,4 +1,4 @@ -package uk.gov.justice.digital.hmpps.healthandmedicationapi.integration +package uk.gov.justice.digital.hmpps.healthandmedication.integration import org.junit.jupiter.api.Test diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/integration/OpenApiDocsIntTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/integration/OpenApiDocsIntTest.kt new file mode 100644 index 0000000..1a50208 --- /dev/null +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/integration/OpenApiDocsIntTest.kt @@ -0,0 +1,100 @@ +package uk.gov.justice.digital.hmpps.healthandmedication.integration + +import io.swagger.v3.parser.OpenAPIV3Parser +import net.minidev.json.JSONArray +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT +import org.springframework.boot.test.web.server.LocalServerPort +import org.springframework.http.MediaType +import org.springframework.test.context.ActiveProfiles +import java.time.LocalDate +import java.time.format.DateTimeFormatter.ISO_DATE + +@SpringBootTest(webEnvironment = RANDOM_PORT) +@ActiveProfiles("test") +class OpenApiDocsIntTest : IntegrationTestBase() { + @LocalServerPort + private var port: Int = 0 + + @Test + fun `open api docs are available`() { + webTestClient.get() + .uri("/swagger-ui/index.html?configUrl=/v3/api-docs") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus().isOk + } + + @Test + fun `open api docs redirect to correct page`() { + webTestClient.get() + .uri("/swagger-ui.html") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus().is3xxRedirection + .expectHeader().value("Location") { it.contains("/swagger-ui/index.html?configUrl=/v3/api-docs/swagger-config") } + } + + @Test + fun `the open api json contains documentation`() { + webTestClient.get() + .uri("/v3/api-docs") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus().isOk + .expectBody().jsonPath("paths").isNotEmpty + } + + @Test + fun `the open api json is valid and contains documentation`() { + val result = OpenAPIV3Parser().readLocation("http://localhost:$port/v3/api-docs", null, null) + assertThat(result.messages).isEmpty() + assertThat(result.openAPI.paths).isNotEmpty + } + + @Test + fun `the swagger json contains the version number`() { + webTestClient.get() + .uri("/v3/api-docs") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus().isOk + .expectBody().jsonPath("info.version").isEqualTo(ISO_DATE.format(LocalDate.now())) + } + + @Test + fun `the generated swagger for date times includes offset`() { + webTestClient.get() + .uri("/v3/api-docs") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus().isOk + .expectBody() + .jsonPath("$.components.schemas.ReferenceDataCodeDto.properties.lastModifiedAt.example").isEqualTo("2024-06-14T10:35:17+0100") + .jsonPath("$.components.schemas.ReferenceDataCodeDto.properties.lastModifiedAt.description") + .isEqualTo("The date and time the reference data code was last modified") + .jsonPath("$.components.schemas.ReferenceDataCodeDto.properties.lastModifiedAt.type").isEqualTo("string") + .jsonPath("$.components.schemas.ReferenceDataCodeDto.properties.lastModifiedAt.format") + .isEqualTo("yyyy-MM-dd'T'HH:mm:ssX") + .jsonPath("$.components.schemas.ReferenceDataCodeDto.properties.lastModifiedAt.pattern").doesNotExist() + } + + @Test + fun `the security scheme is setup for bearer tokens`() { + val bearerJwts = JSONArray() + bearerJwts.addAll(listOf("read", "write")) + webTestClient.get() + .uri("/v3/api-docs") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus().isOk + .expectBody() + .jsonPath("$.components.securitySchemes.bearer-jwt.type").isEqualTo("http") + .jsonPath("$.components.securitySchemes.bearer-jwt.scheme").isEqualTo("bearer") + .jsonPath("$.components.securitySchemes.bearer-jwt.bearerFormat").isEqualTo("JWT") + .jsonPath("$.security[0].bearer-jwt") + .isEqualTo(bearerJwts) + } +} diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/integration/ResourceSecurityTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/integration/ResourceSecurityTest.kt similarity index 84% rename from src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/integration/ResourceSecurityTest.kt rename to src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/integration/ResourceSecurityTest.kt index 8002d6b..cabbe3f 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/integration/ResourceSecurityTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/integration/ResourceSecurityTest.kt @@ -1,4 +1,4 @@ -package uk.gov.justice.digital.hmpps.healthandmedicationapi.integration +package uk.gov.justice.digital.hmpps.healthandmedication.integration import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test @@ -50,10 +50,9 @@ class ResourceSecurityTest : IntegrationTestBase() { } } -private fun RequestMappingInfo.getMappings() = - methodsCondition.methods - .map { it.name } - .ifEmpty { listOf("") } // if no methods defined then match all rather than none - .flatMap { method -> - pathPatternsCondition?.patternValues?.map { "$method $it" } ?: emptyList() - } +private fun RequestMappingInfo.getMappings() = methodsCondition.methods + .map { it.name } + .ifEmpty { listOf("") } // if no methods defined then match all rather than none + .flatMap { method -> + pathPatternsCondition?.patternValues?.map { "$method $it" } ?: emptyList() + } diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/integration/TestBase.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/integration/TestBase.kt similarity index 78% rename from src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/integration/TestBase.kt rename to src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/integration/TestBase.kt index 9a70a9d..bdee0c8 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/integration/TestBase.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/integration/TestBase.kt @@ -1,10 +1,10 @@ -package uk.gov.justice.digital.hmpps.healthandmedicationapi.integration +package uk.gov.justice.digital.hmpps.healthandmedication.integration import org.springframework.test.context.ActiveProfiles import org.springframework.test.context.DynamicPropertyRegistry import org.springframework.test.context.DynamicPropertySource -import uk.gov.justice.digital.hmpps.healthandmedicationapi.config.FixedClock -import uk.gov.justice.digital.hmpps.healthandmedicationapi.integration.testcontainers.PostgresContainer +import uk.gov.justice.digital.hmpps.healthandmedication.config.FixedClock +import uk.gov.justice.digital.hmpps.healthandmedication.integration.testcontainers.PostgresContainer import java.time.Instant import java.time.ZoneId diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/integration/health/HealthCheckTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/integration/health/HealthCheckTest.kt similarity index 88% rename from src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/integration/health/HealthCheckTest.kt rename to src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/integration/health/HealthCheckTest.kt index ce1c97c..1c154cb 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/integration/health/HealthCheckTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/integration/health/HealthCheckTest.kt @@ -1,7 +1,7 @@ -package uk.gov.justice.digital.hmpps.healthandmedicationapi.integration.health +package uk.gov.justice.digital.hmpps.healthandmedication.integration.health import org.junit.jupiter.api.Test -import uk.gov.justice.digital.hmpps.healthandmedicationapi.integration.IntegrationTestBase +import uk.gov.justice.digital.hmpps.healthandmedication.integration.IntegrationTestBase class HealthCheckTest : IntegrationTestBase() { diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/integration/health/InfoTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/integration/health/InfoTest.kt similarity index 81% rename from src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/integration/health/InfoTest.kt rename to src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/integration/health/InfoTest.kt index e44cfb1..dbf5a05 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/integration/health/InfoTest.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/integration/health/InfoTest.kt @@ -1,8 +1,8 @@ -package uk.gov.justice.digital.hmpps.healthandmedicationapi.integration.health +package uk.gov.justice.digital.hmpps.healthandmedication.integration.health import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test -import uk.gov.justice.digital.hmpps.healthandmedicationapi.integration.IntegrationTestBase +import uk.gov.justice.digital.hmpps.healthandmedication.integration.IntegrationTestBase import java.time.LocalDateTime import java.time.format.DateTimeFormatter diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/integration/testcontainers/PostgresContainer.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/integration/testcontainers/PostgresContainer.kt similarity index 78% rename from src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/integration/testcontainers/PostgresContainer.kt rename to src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/integration/testcontainers/PostgresContainer.kt index 2dc9b84..f66e3fc 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/integration/testcontainers/PostgresContainer.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/integration/testcontainers/PostgresContainer.kt @@ -1,4 +1,4 @@ -package uk.gov.justice.digital.hmpps.healthandmedicationapi.integration.testcontainers +package uk.gov.justice.digital.hmpps.healthandmedication.integration.testcontainers import org.slf4j.LoggerFactory import org.testcontainers.containers.PostgreSQLContainer @@ -28,13 +28,12 @@ object PostgresContainer { } } - private fun isPostgresRunning(): Boolean = - try { - val serverSocket = ServerSocket(5432) - serverSocket.localPort == 0 - } catch (e: IOException) { - true - } + private fun isPostgresRunning(): Boolean = try { + val serverSocket = ServerSocket(5432) + serverSocket.localPort == 0 + } catch (e: IOException) { + true + } private val log = LoggerFactory.getLogger(this::class.java) } diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/integration/wiremock/HmppsAuthMockServer.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/integration/wiremock/HmppsAuthMockServer.kt similarity index 92% rename from src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/integration/wiremock/HmppsAuthMockServer.kt rename to src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/integration/wiremock/HmppsAuthMockServer.kt index 2b7cd41..33531e2 100644 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/integration/wiremock/HmppsAuthMockServer.kt +++ b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedication/integration/wiremock/HmppsAuthMockServer.kt @@ -1,4 +1,4 @@ -package uk.gov.justice.digital.hmpps.healthandmedicationapi.integration.wiremock +package uk.gov.justice.digital.hmpps.healthandmedication.integration.wiremock import com.github.tomakehurst.wiremock.WireMockServer import com.github.tomakehurst.wiremock.client.WireMock.aResponse @@ -14,7 +14,10 @@ import org.junit.jupiter.api.extension.ExtensionContext import java.time.LocalDateTime import java.time.ZoneOffset -class HmppsAuthApiExtension : BeforeAllCallback, AfterAllCallback, BeforeEachCallback { +class HmppsAuthApiExtension : + BeforeAllCallback, + AfterAllCallback, + BeforeEachCallback { companion object { @JvmField val hmppsAuth = HmppsAuthMockServer() diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/integration/ExampleResourceIntTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/integration/ExampleResourceIntTest.kt deleted file mode 100644 index d280306..0000000 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/integration/ExampleResourceIntTest.kt +++ /dev/null @@ -1,106 +0,0 @@ -package uk.gov.justice.digital.hmpps.healthandmedicationapi.integration - -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.DisplayName -import org.junit.jupiter.api.Nested -import org.junit.jupiter.api.Test -import uk.gov.justice.digital.hmpps.healthandmedicationapi.integration.wiremock.HmppsAuthApiExtension.Companion.hmppsAuth -import java.time.LocalDate - -class ExampleResourceIntTest : IntegrationTestBase() { - - @Nested - @DisplayName("GET /example/time") - inner class TimeEndpoint { - - @Test - fun `should return unauthorized if no token`() { - webTestClient.get() - .uri("/example/time") - .exchange() - .expectStatus() - .isUnauthorized - } - - @Test - fun `should return forbidden if no role`() { - webTestClient.get() - .uri("/example/time") - .headers(setAuthorisation()) - .exchange() - .expectStatus() - .isForbidden - } - - @Test - fun `should return forbidden if wrong role`() { - webTestClient.get() - .uri("/example/time") - .headers(setAuthorisation(roles = listOf("ROLE_WRONG"))) - .exchange() - .expectStatus() - .isForbidden - } - - @Test - fun `should return OK`() { - webTestClient.get() - .uri("/example/time") - .headers(setAuthorisation(roles = listOf("ROLE_TEMPLATE_KOTLIN__UI"))) - .exchange() - .expectStatus() - .isOk - .expectBody() - .jsonPath("$").value { - assertThat(it).startsWith("${LocalDate.now()}") - } - } - } - - @Nested - @DisplayName("GET /example/message/{parameter}") - inner class UserDetailsEndpoint { - - @Test - fun `should return unauthorized if no token`() { - webTestClient.get() - .uri("/example/message/{parameter}", "bob") - .exchange() - .expectStatus() - .isUnauthorized - } - - @Test - fun `should return forbidden if no role`() { - webTestClient.get() - .uri("/example/message/{parameter}", "bob") - .headers(setAuthorisation(roles = listOf())) - .exchange() - .expectStatus() - .isForbidden - } - - @Test - fun `should return forbidden if wrong role`() { - webTestClient.get() - .uri("/example/message/{parameter}", "bob") - .headers(setAuthorisation(roles = listOf("ROLE_WRONG"))) - .exchange() - .expectStatus() - .isForbidden - } - - @Test - fun `should return OK`() { - hmppsAuth.stubGrantToken() - webTestClient.get() - .uri("/example/message/{parameter}", "bob") - .headers(setAuthorisation(username = "AUTH_OK", roles = listOf("ROLE_TEMPLATE_KOTLIN__UI"))) - .exchange() - .expectStatus() - .isOk - .expectBody() - .jsonPath("$.message").isEqualTo("Hello bob") - } - } -} diff --git a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/integration/OpenApiDocsTest.kt b/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/integration/OpenApiDocsTest.kt deleted file mode 100644 index d3351fb..0000000 --- a/src/test/kotlin/uk/gov/justice/digital/hmpps/healthandmedicationapi/integration/OpenApiDocsTest.kt +++ /dev/null @@ -1,105 +0,0 @@ -package uk.gov.justice.digital.hmpps.healthandmedicationapi.integration - -import io.swagger.v3.parser.OpenAPIV3Parser -import net.minidev.json.JSONArray -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Test -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.CsvSource -import org.springframework.boot.test.web.server.LocalServerPort -import org.springframework.http.MediaType -import java.time.LocalDate -import java.time.format.DateTimeFormatter - -class OpenApiDocsTest : IntegrationTestBase() { - @LocalServerPort - private val port: Int = 0 - - @Test - fun `open api docs are available`() { - webTestClient.get() - .uri("/swagger-ui/index.html?configUrl=/v3/api-docs") - .accept(MediaType.APPLICATION_JSON) - .exchange() - .expectStatus().isOk - } - - @Test - fun `open api docs redirect to correct page`() { - webTestClient.get() - .uri("/swagger-ui.html") - .accept(MediaType.APPLICATION_JSON) - .exchange() - .expectStatus().is3xxRedirection - .expectHeader().value("Location") { it.contains("/swagger-ui/index.html?configUrl=/v3/api-docs/swagger-config") } - } - - @Test - fun `the open api json contains documentation`() { - webTestClient.get() - .uri("/v3/api-docs") - .accept(MediaType.APPLICATION_JSON) - .exchange() - .expectStatus().isOk - .expectBody() - .jsonPath("paths").isNotEmpty - } - - @Test - fun `the open api json contains the version number`() { - webTestClient.get() - .uri("/v3/api-docs") - .accept(MediaType.APPLICATION_JSON) - .exchange() - .expectStatus().isOk - .expectBody().jsonPath("info.version").isEqualTo(DateTimeFormatter.ISO_DATE.format(LocalDate.now())) - } - - @Test - fun `the open api json is valid and contains documentation`() { - val result = OpenAPIV3Parser().readLocation("http://localhost:$port/v3/api-docs", null, null) - assertThat(result.messages).isEmpty() - assertThat(result.openAPI.paths).isNotEmpty - } - - @Test - fun `the open api json path security requirements are valid`() { - val result = OpenAPIV3Parser().readLocation("http://localhost:$port/v3/api-docs", null, null) - - // The security requirements of each path don't appear to be validated like they are at https://editor.swagger.io/ - // We therefore need to grab all the valid security requirements and check that each path only contains those items - val securityRequirements = result.openAPI.security.flatMap { it.keys } - result.openAPI.paths.forEach { pathItem -> - assertThat(pathItem.value.get.security.flatMap { it.keys }).isSubsetOf(securityRequirements) - } - } - - @ParameterizedTest - @CsvSource(value = ["health-and-medication-api-ui-role, ROLE_TEMPLATE_KOTLIN__UI"]) - fun `the security scheme is setup for bearer tokens`(key: String, role: String) { - webTestClient.get() - .uri("/v3/api-docs") - .accept(MediaType.APPLICATION_JSON) - .exchange() - .expectStatus().isOk - .expectBody() - .jsonPath("$.components.securitySchemes.$key.type").isEqualTo("http") - .jsonPath("$.components.securitySchemes.$key.scheme").isEqualTo("bearer") - .jsonPath("$.components.securitySchemes.$key.description").value { - assertThat(it).contains(role) - } - .jsonPath("$.components.securitySchemes.$key.bearerFormat").isEqualTo("JWT") - .jsonPath("$.security[0].$key").isEqualTo(JSONArray().apply { this.add("read") }) - } - - @Test - fun `all endpoints have a security scheme defined`() { - webTestClient.get() - .uri("/v3/api-docs") - .accept(MediaType.APPLICATION_JSON) - .exchange() - .expectStatus().isOk - .expectBody() - .jsonPath("$.paths[*][*][?(!@.security)]").doesNotExist() - } -} diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index 2f16c67..288a3bc 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -5,6 +5,10 @@ management.endpoint: health.cache.time-to-live: 0 info.cache.time-to-live: 0 +api: + hmpps-auth.base-url: http://localhost:8090/auth + prisoner-search.base-url: http://localhost:8112 + hmpps-auth: url: "http://localhost:8090/auth"