From ebcb1fe108d676ffdbbbc940fbc548006eb38652 Mon Sep 17 00:00:00 2001 From: Marcus Aspin Date: Tue, 19 Mar 2024 12:05:05 +0000 Subject: [PATCH] PI-1284 Migrate prison-to-probation-update listener to prison-identifier-and-delius (#3478) --- .../digital/hmpps/messaging/HmppsDevQueue.kt | 8 +- ...AwsQueuePublisher.kt => QueuePublisher.kt} | 2 +- ...ficationPublisher.kt => TopicPublisher.kt} | 2 +- ...PublisherTest.kt => TopicPublisherTest.kt} | 4 +- .../prison-identifier-and-delius/README.md | 25 +- .../deploy/database/access.yml | 4 +- .../deploy/templates/update-noms-numbers.yaml | 2 +- .../deploy/values-dev.yml | 2 + .../deploy/values-preprod.yml | 2 + .../deploy/values-prod.yml | 2 + .../deploy/values.yaml | 2 + .../justice/digital/hmpps/data/DataLoader.kt | 34 ++- .../hmpps/data/generator/PersonGenerator.kt | 15 +- .../data/generator/ReferenceDataGenerator.kt | 25 ++ .../resources/messages/prisoner-merged.json | 20 ++ .../prisoner-received-inactive-booking.json | 26 ++ .../resources/messages/prisoner-received.json | 26 ++ .../__files/prison-api-A0001AA-booking.json | 12 + .../__files/prison-api-A0001AA-prisoner.json | 27 ++ .../__files/prison-api-A0001AA-sentence.json | 16 ++ ...n-api-A0001AA-unmatched-sentence-date.json | 16 ++ .../prison-api-A0002AA-booking-inactive.json | 12 + ...json => prisoner-search-results-body.json} | 0 ...son => prisoner-search-results-body2.json} | 0 ...son => prisoner-search-results-body3.json} | 0 ...son => prisoner-search-results-body4.json} | 0 .../probation-search-multiple-results.json | 33 +++ .../__files/probation-search-no-results.json | 4 + .../probation-search-single-result.json | 83 ++++++ .../simulations/mappings/prison-api.json | 56 ++++ .../simulations/mappings/prisoner-search.json | 16 +- .../digital/hmpps/MergeIntegrationTest.kt | 73 ++++++ .../hmpps/NomsNumberIntegrationTest.kt | 231 ---------------- .../hmpps/PrisonMatchingIntegrationTest.kt | 246 ++++++++++++++++++ .../hmpps/ProbationMatchingIntegrationTest.kt | 174 +++++++++++++ .../digital/hmpps/client/PrisonApiClient.kt | 45 ++++ .../PrisonerSearchClient.kt} | 21 +- .../hmpps/client/ProbationSearchClient.kt | 63 +++++ .../digital/hmpps/config/RestClientConfig.kt | 21 +- .../hmpps/controller/MatchingController.kt | 26 +- .../hmpps/entity/AdditionalIdentifier.kt | 59 +++++ .../{integrations/delius => }/entity/Event.kt | 23 +- .../delius => }/entity/Person.kt | 36 +-- .../justice/digital/hmpps/entity/Prisoner.kt | 30 +++ .../digital/hmpps/entity/ReferenceData.kt | 53 ++++ .../integrations/prison/PrisonSearchAPI.kt | 9 - .../digital/hmpps/messaging/Handler.kt | 48 ++++ .../digital/hmpps/messaging/Notifier.kt | 89 +++++++ .../digital/hmpps/model/MatchResult.kt | 38 +++ .../digital/hmpps/model/MergeResult.kt | 24 ++ .../digital/hmpps/model/PrisonIdentifiers.kt | 6 + .../Matches.kt => model/PrisonerMatches.kt} | 31 ++- .../{sevice/model => service}/DateMatcher.kt | 2 +- .../digital/hmpps/service/MatchWriter.kt | 73 ++++++ .../hmpps/service/PrisonMatchingService.kt | 86 ++++++ .../hmpps/service/ProbationMatchingService.kt | 112 ++++++++ .../digital/hmpps/sevice/MatchWriter.kt | 24 -- .../justice/digital/hmpps/sevice/Matcher.kt | 36 --- .../digital/hmpps/sevice/MatchingNotifier.kt | 31 --- .../digital/hmpps/sevice/MatchingService.kt | 117 --------- .../src/main/resources/application.yml | 6 +- 61 files changed, 1729 insertions(+), 580 deletions(-) rename libs/messaging/src/main/kotlin/uk/gov/justice/digital/hmpps/publisher/{AwsQueuePublisher.kt => QueuePublisher.kt} (98%) rename libs/messaging/src/main/kotlin/uk/gov/justice/digital/hmpps/publisher/{AwsNotificationPublisher.kt => TopicPublisher.kt} (97%) rename libs/messaging/src/test/kotlin/uk/gov/justice/digital/hmpps/publisher/{AwsNotificationPublisherTest.kt => TopicPublisherTest.kt} (94%) create mode 100644 projects/prison-identifier-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/ReferenceDataGenerator.kt create mode 100644 projects/prison-identifier-and-delius/src/dev/resources/messages/prisoner-merged.json create mode 100644 projects/prison-identifier-and-delius/src/dev/resources/messages/prisoner-received-inactive-booking.json create mode 100644 projects/prison-identifier-and-delius/src/dev/resources/messages/prisoner-received.json create mode 100644 projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/prison-api-A0001AA-booking.json create mode 100644 projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/prison-api-A0001AA-prisoner.json create mode 100644 projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/prison-api-A0001AA-sentence.json create mode 100644 projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/prison-api-A0001AA-unmatched-sentence-date.json create mode 100644 projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/prison-api-A0002AA-booking-inactive.json rename projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/{search-results-body.json => prisoner-search-results-body.json} (100%) rename projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/{search-results-body2.json => prisoner-search-results-body2.json} (100%) rename projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/{search-results-body3.json => prisoner-search-results-body3.json} (100%) rename projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/{search-results-body4.json => prisoner-search-results-body4.json} (100%) create mode 100644 projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/probation-search-multiple-results.json create mode 100644 projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/probation-search-no-results.json create mode 100644 projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/probation-search-single-result.json create mode 100644 projects/prison-identifier-and-delius/src/dev/resources/simulations/mappings/prison-api.json create mode 100644 projects/prison-identifier-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/MergeIntegrationTest.kt delete mode 100644 projects/prison-identifier-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/NomsNumberIntegrationTest.kt create mode 100644 projects/prison-identifier-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/PrisonMatchingIntegrationTest.kt create mode 100644 projects/prison-identifier-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/ProbationMatchingIntegrationTest.kt create mode 100644 projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/client/PrisonApiClient.kt rename projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/{integrations/prison/SearchResponse.kt => client/PrisonerSearchClient.kt} (53%) create mode 100644 projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/client/ProbationSearchClient.kt create mode 100644 projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/entity/AdditionalIdentifier.kt rename projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/{integrations/delius => }/entity/Event.kt (85%) rename projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/{integrations/delius => }/entity/Person.kt (83%) create mode 100644 projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/entity/Prisoner.kt create mode 100644 projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/entity/ReferenceData.kt delete mode 100644 projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/prison/PrisonSearchAPI.kt create mode 100644 projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/messaging/Handler.kt create mode 100644 projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/messaging/Notifier.kt create mode 100644 projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/model/MatchResult.kt create mode 100644 projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/model/MergeResult.kt create mode 100644 projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/model/PrisonIdentifiers.kt rename projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/{sevice/model/Matches.kt => model/PrisonerMatches.kt} (70%) rename projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/{sevice/model => service}/DateMatcher.kt (95%) create mode 100644 projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/service/MatchWriter.kt create mode 100644 projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/service/PrisonMatchingService.kt create mode 100644 projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/service/ProbationMatchingService.kt delete mode 100644 projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/sevice/MatchWriter.kt delete mode 100644 projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/sevice/Matcher.kt delete mode 100644 projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/sevice/MatchingNotifier.kt delete mode 100644 projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/sevice/MatchingService.kt diff --git a/libs/dev-tools/src/main/kotlin/uk/gov/justice/digital/hmpps/messaging/HmppsDevQueue.kt b/libs/dev-tools/src/main/kotlin/uk/gov/justice/digital/hmpps/messaging/HmppsDevQueue.kt index 635a8bf210..0bd99687ec 100644 --- a/libs/dev-tools/src/main/kotlin/uk/gov/justice/digital/hmpps/messaging/HmppsDevQueue.kt +++ b/libs/dev-tools/src/main/kotlin/uk/gov/justice/digital/hmpps/messaging/HmppsDevQueue.kt @@ -12,9 +12,7 @@ import uk.gov.justice.digital.hmpps.publisher.NotificationPublisher import java.time.Duration import java.time.LocalDateTime import java.time.LocalTime -import java.util.LinkedList -import java.util.Queue -import java.util.UUID +import java.util.* import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException import java.util.concurrent.locks.ReentrantLock @@ -139,7 +137,7 @@ class HmppsNotificationListener( @Component @ConditionalOnProperty("messaging.producer.topic") -class HmppsNotificationPublisher( +class TopicPublisher( @Value("\${messaging.producer.topic}") private val topicName: String, private val channelManager: HmppsChannelManager ) : NotificationPublisher { @@ -150,7 +148,7 @@ class HmppsNotificationPublisher( @Component @ConditionalOnProperty("messaging.producer.queue") -class HmppsQueuePublisher( +class QueuePublisher( @Value("\${messaging.producer.queue}") private val queueName: String, private val channelManager: HmppsChannelManager ) : NotificationPublisher { diff --git a/libs/messaging/src/main/kotlin/uk/gov/justice/digital/hmpps/publisher/AwsQueuePublisher.kt b/libs/messaging/src/main/kotlin/uk/gov/justice/digital/hmpps/publisher/QueuePublisher.kt similarity index 98% rename from libs/messaging/src/main/kotlin/uk/gov/justice/digital/hmpps/publisher/AwsQueuePublisher.kt rename to libs/messaging/src/main/kotlin/uk/gov/justice/digital/hmpps/publisher/QueuePublisher.kt index 8b294f7d75..3f89c21265 100644 --- a/libs/messaging/src/main/kotlin/uk/gov/justice/digital/hmpps/publisher/AwsQueuePublisher.kt +++ b/libs/messaging/src/main/kotlin/uk/gov/justice/digital/hmpps/publisher/QueuePublisher.kt @@ -15,7 +15,7 @@ import java.util.concurrent.Semaphore @Component @Conditional(AwsCondition::class) @ConditionalOnProperty("messaging.producer.queue") -class AwsQueuePublisher( +class QueuePublisher( private val sqsTemplate: SqsTemplate, private val objectMapper: ObjectMapper, @Value("\${messaging.producer.queue}") private val queue: String, diff --git a/libs/messaging/src/main/kotlin/uk/gov/justice/digital/hmpps/publisher/AwsNotificationPublisher.kt b/libs/messaging/src/main/kotlin/uk/gov/justice/digital/hmpps/publisher/TopicPublisher.kt similarity index 97% rename from libs/messaging/src/main/kotlin/uk/gov/justice/digital/hmpps/publisher/AwsNotificationPublisher.kt rename to libs/messaging/src/main/kotlin/uk/gov/justice/digital/hmpps/publisher/TopicPublisher.kt index 931edabea0..38ee8d9a7b 100644 --- a/libs/messaging/src/main/kotlin/uk/gov/justice/digital/hmpps/publisher/AwsNotificationPublisher.kt +++ b/libs/messaging/src/main/kotlin/uk/gov/justice/digital/hmpps/publisher/TopicPublisher.kt @@ -15,7 +15,7 @@ import uk.gov.justice.digital.hmpps.message.Notification @Component @Conditional(AwsCondition::class) @ConditionalOnProperty("messaging.producer.topic") -class AwsNotificationPublisher( +class TopicPublisher( private val notificationTemplate: SnsTemplate, @Value("\${messaging.producer.topic}") private val topic: String ) : NotificationPublisher { diff --git a/libs/messaging/src/test/kotlin/uk/gov/justice/digital/hmpps/publisher/AwsNotificationPublisherTest.kt b/libs/messaging/src/test/kotlin/uk/gov/justice/digital/hmpps/publisher/TopicPublisherTest.kt similarity index 94% rename from libs/messaging/src/test/kotlin/uk/gov/justice/digital/hmpps/publisher/AwsNotificationPublisherTest.kt rename to libs/messaging/src/test/kotlin/uk/gov/justice/digital/hmpps/publisher/TopicPublisherTest.kt index 8aea858907..8d80e8a21f 100644 --- a/libs/messaging/src/test/kotlin/uk/gov/justice/digital/hmpps/publisher/AwsNotificationPublisherTest.kt +++ b/libs/messaging/src/test/kotlin/uk/gov/justice/digital/hmpps/publisher/TopicPublisherTest.kt @@ -20,7 +20,7 @@ import uk.gov.justice.digital.hmpps.message.MessageAttributes import uk.gov.justice.digital.hmpps.message.Notification @ExtendWith(MockitoExtension::class) -class AwsNotificationPublisherTest { +class TopicPublisherTest { @Mock lateinit var notificationTemplate: SnsTemplate @@ -29,7 +29,7 @@ class AwsNotificationPublisherTest { @BeforeEach fun setup() { - publisher = AwsNotificationPublisher(notificationTemplate, "my-topic") + publisher = TopicPublisher(notificationTemplate, "my-topic") } @Test diff --git a/projects/prison-identifier-and-delius/README.md b/projects/prison-identifier-and-delius/README.md index 5479c1fccb..41a98845d9 100644 --- a/projects/prison-identifier-and-delius/README.md +++ b/projects/prison-identifier-and-delius/README.md @@ -14,19 +14,22 @@ HMPPS has a number of systems each holding information about a person's interact The matching process can be triggered by a request to the integration service API endpoint -| Business Event | API Endpoint | -|-----------------------|------------------------------| -| List of CRNs to Match | /person/populate-noms-number | +| Business Event | API Endpoint | +|----------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| List of CRNs to Match | [/person/match-by-crn](https://ministryofjustice.github.io/hmpps-probation-integration-services/tech-docs/projects/prison-identifier-and-delius/api-reference.html#person-match-by-crn) | +| List of NOMIS IDs to Match | [/person/match-by-noms](https://ministryofjustice.github.io/hmpps-probation-integration-services/tech-docs/projects/prison-identifier-and-delius/api-reference.html#person-match-by-noms) | -### Domain Event Processing (Not Yet Implemented) +### Domain Event Processing The matching process will be triggered by domain events raised by Delius, once these events are implemented -| Business Event | Message Event Type / Filter | -|------------------------------------|---------------------------------| -| New Sentence Added to Delius | probation-case.sentence.created | -| Sentence Changed in Delius | probation-case.sentence.amended | -| Sentence Moved to New Delius Event | probation-case.sentence.move | +| Business Event | Message Event Type / Filter | Status | +|------------------------------------|------------------------------------------|---------------------| +| New Sentence Added to Delius | probation-case.sentence.created | Not yet implemented | +| Sentence Changed in Delius | probation-case.sentence.amended | Not yet implemented | +| Sentence Moved to New Delius Event | probation-case.sentence.move | Not yet implemented | +| Prisoner received into NOMIS | prison-offender-events.prisoner.received | Active | +| Prisoner merged in NOMIS | prison-offender-events.prisoner.merged | Active | ## Workflows @@ -41,8 +44,8 @@ The matching process will be triggered by domain events raised by Delius, once t API endpoints are secured by roles supplied by the HMPPS Auth client used in the requests -| API Endpoint | Required Role | -|--------------|-------------------------------------------------| +| API Endpoint | Required Role | +|--------------|--------------------------------------------------| | All | ROLE\_PROBATION\_API_\_PRISON_IDENTIFIER__UPDATE | ## Concepts diff --git a/projects/prison-identifier-and-delius/deploy/database/access.yml b/projects/prison-identifier-and-delius/deploy/database/access.yml index c4d954648c..8715aba01d 100644 --- a/projects/prison-identifier-and-delius/deploy/database/access.yml +++ b/projects/prison-identifier-and-delius/deploy/database/access.yml @@ -3,8 +3,10 @@ database: username_key: /prison-identifier-and-delius/db-username password_key: /prison-identifier-and-delius/db-password tables: - - offender + - additional_identifier - custody + - offender + - offender_prisoner audit: username: PrisonIdentifierAndDelius diff --git a/projects/prison-identifier-and-delius/deploy/templates/update-noms-numbers.yaml b/projects/prison-identifier-and-delius/deploy/templates/update-noms-numbers.yaml index edd5736bf0..0ee3738da0 100644 --- a/projects/prison-identifier-and-delius/deploy/templates/update-noms-numbers.yaml +++ b/projects/prison-identifier-and-delius/deploy/templates/update-noms-numbers.yaml @@ -20,7 +20,7 @@ spec: args: - /bin/sh - -c - - 'curl -fsSL -X POST "https://$BASE_URL/person/populate-noms-number?dryRun=$DRY_RUN" --header "Authorization: Bearer $(curl -fsSL --request POST "$AUTH_URL?grant_type=client_credentials" --user "$CLIENT_ID:$CLIENT_SECRET" | jq -r .access_token)" --header "Content-Type: application/json"' + - 'curl -fsSL -X POST "https://$BASE_URL/person/match-by-crn?dryRun=$DRY_RUN" --header "Authorization: Bearer $(curl -fsSL --request POST "$AUTH_URL?grant_type=client_credentials" --user "$CLIENT_ID:$CLIENT_SECRET" | jq -r .access_token)" --header "Content-Type: application/json"' env: - name: AUTH_URL value: {{ index .Values "generic-service" "env" "SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_HMPPS-AUTH_TOKEN-URI" }} diff --git a/projects/prison-identifier-and-delius/deploy/values-dev.yml b/projects/prison-identifier-and-delius/deploy/values-dev.yml index b81b90e94c..8968cc8e8c 100644 --- a/projects/prison-identifier-and-delius/deploy/values-dev.yml +++ b/projects/prison-identifier-and-delius/deploy/values-dev.yml @@ -10,7 +10,9 @@ generic-service: SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_HMPPS-AUTH_TOKEN-URI: http://hmpps-auth.hmpps-auth-dev.svc.cluster.local/auth/oauth/token SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI: http://hmpps-auth.hmpps-auth-dev.svc.cluster.local/auth/.well-known/jwks.json + INTEGRATIONS_PRISON-API_URL: https://prison-api-dev.prison.service.justice.gov.uk INTEGRATIONS_PRISONER-SEARCH_URL: https://prisoner-search-dev.prison.service.justice.gov.uk + INTEGRATIONS_PROBATION-SEARCH_URL: https://probation-offender-search-dev.hmpps.service.justice.gov.uk SPRING_DATASOURCE_HIKARI_MAXIMUMPOOLSIZE: 5 SPRING_DATASOURCE_HIKARI_MINIMUMIDLE: 0 diff --git a/projects/prison-identifier-and-delius/deploy/values-preprod.yml b/projects/prison-identifier-and-delius/deploy/values-preprod.yml index 8b5a52033b..c4cba95593 100644 --- a/projects/prison-identifier-and-delius/deploy/values-preprod.yml +++ b/projects/prison-identifier-and-delius/deploy/values-preprod.yml @@ -10,7 +10,9 @@ generic-service: SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_HMPPS-AUTH_TOKEN-URI: http://hmpps-auth.hmpps-auth-preprod.svc.cluster.local/auth/oauth/token SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI: http://hmpps-auth.hmpps-auth-preprod.svc.cluster.local/auth/.well-known/jwks.json + INTEGRATIONS_PRISON-API_URL: https://prison-api-preprod.prison.service.justice.gov.uk INTEGRATIONS_PRISONER-SEARCH_URL: https://prisoner-search-preprod.prison.service.justice.gov.uk + INTEGRATIONS_PROBATION-SEARCH_URL: https://probation-offender-search-preprod.hmpps.service.justice.gov.uk generic-prometheus-alerts: businessHoursOnly: true diff --git a/projects/prison-identifier-and-delius/deploy/values-prod.yml b/projects/prison-identifier-and-delius/deploy/values-prod.yml index 157741a4dd..94daa630db 100644 --- a/projects/prison-identifier-and-delius/deploy/values-prod.yml +++ b/projects/prison-identifier-and-delius/deploy/values-prod.yml @@ -7,7 +7,9 @@ generic-service: SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_HMPPS-AUTH_TOKEN-URI: http://hmpps-auth.hmpps-auth-prod.svc.cluster.local/auth/oauth/token SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI: http://hmpps-auth.hmpps-auth-prod.svc.cluster.local/auth/.well-known/jwks.json + INTEGRATIONS_PRISON-API_URL: https://api.prison.service.justice.gov.uk INTEGRATIONS_PRISONER-SEARCH_URL: https://prisoner-search.prison.service.justice.gov.uk + INTEGRATIONS_PROBATION-SEARCH_URL: https://probation-offender-search.hmpps.service.justice.gov.uk noms: update: diff --git a/projects/prison-identifier-and-delius/deploy/values.yaml b/projects/prison-identifier-and-delius/deploy/values.yaml index 6db857f046..c3e74ed429 100644 --- a/projects/prison-identifier-and-delius/deploy/values.yaml +++ b/projects/prison-identifier-and-delius/deploy/values.yaml @@ -26,6 +26,8 @@ generic-service: prison-identifier-and-delius-queue: MESSAGING_CONSUMER_QUEUE: QUEUE_NAME MESSAGING_PRODUCER_QUEUE: QUEUE_NAME + hmpps-domain-events-topic: + MESSAGING_PRODUCER_TOPIC: topic_arn generic-prometheus-alerts: targetApplication: prison-identifier-and-delius \ No newline at end of file diff --git a/projects/prison-identifier-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/DataLoader.kt b/projects/prison-identifier-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/DataLoader.kt index b16be94c2d..b04620c859 100644 --- a/projects/prison-identifier-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/DataLoader.kt +++ b/projects/prison-identifier-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/DataLoader.kt @@ -11,10 +11,10 @@ import uk.gov.justice.digital.hmpps.data.generator.PersonGenerator import uk.gov.justice.digital.hmpps.data.generator.PersonGenerator.generateCustody import uk.gov.justice.digital.hmpps.data.generator.PersonGenerator.generateDisposal import uk.gov.justice.digital.hmpps.data.generator.PersonGenerator.generateEvent +import uk.gov.justice.digital.hmpps.data.generator.ReferenceDataGenerator import uk.gov.justice.digital.hmpps.data.generator.UserGenerator import uk.gov.justice.digital.hmpps.user.AuditUserRepository import java.time.LocalDate -import java.time.format.DateTimeFormatter @Component @ConditionalOnProperty("seed.database") @@ -31,44 +31,40 @@ class DataLoader( @Transactional override fun onApplicationEvent(are: ApplicationReadyEvent) { val personWithNomsEvent = generateEvent(PersonGenerator.PERSON_WITH_NOMS) - val personWithNomsDisposal = generateDisposal(LocalDate.now(), personWithNomsEvent) + val personWithNomsDisposal = generateDisposal(LocalDate.of(2022, 11, 11), personWithNomsEvent) val personWithNomsCustody = generateCustody(personWithNomsDisposal) val personWithNoNomsNumberEvent = generateEvent(PersonGenerator.PERSON_WITH_NO_NOMS) - val personWithNoNomsNumberDisposal = generateDisposal( - LocalDate.parse("12/12/2022", DateTimeFormatter.ofPattern("MM/dd/yyyy")), - personWithNoNomsNumberEvent - ) + val personWithNoNomsNumberDisposal = generateDisposal(LocalDate.of(2022, 12, 12), personWithNoNomsNumberEvent) val personWithNoNomsNumberCustody = generateCustody(personWithNoNomsNumberDisposal) val personWithMultiMatchEvent = generateEvent(PersonGenerator.PERSON_WITH_MULTI_MATCH) - val personWithMultiMatchDisposal = generateDisposal( - LocalDate.parse("12/12/2022", DateTimeFormatter.ofPattern("MM/dd/yyyy")), - personWithMultiMatchEvent - ) + val personWithMultiMatchDisposal = generateDisposal(LocalDate.of(2022, 12, 12), personWithMultiMatchEvent) val personWithMultiMatchCustody = generateCustody(personWithMultiMatchDisposal) val personWithNoMatchEvent = generateEvent(PersonGenerator.PERSON_WITH_NO_MATCH) - val personWithNoMatchDisposal = generateDisposal( - LocalDate.parse("12/12/2022", DateTimeFormatter.ofPattern("MM/dd/yyyy")), - personWithNoMatchEvent - ) + val personWithNoMatchDisposal = generateDisposal(LocalDate.of(2022, 12, 12), personWithNoMatchEvent) val personWithNoMatchCustody = generateCustody(personWithNoMatchDisposal) val personWithNomsInDeliusEvent = generateEvent(PersonGenerator.PERSON_WITH_NOMS_IN_DELIUS) - val personWithNomsInDeliusDisposal = generateDisposal( - LocalDate.parse("12/12/2022", DateTimeFormatter.ofPattern("MM/dd/yyyy")), - personWithNomsInDeliusEvent - ) + val personWithNomsInDeliusDisposal = generateDisposal(LocalDate.of(2022, 12, 12), personWithNomsInDeliusEvent) val personWithNomsInDeliusCustody = generateCustody(personWithNomsInDeliusDisposal) em.saveAll( - PersonGenerator.MALE, + ReferenceDataGenerator.GENDER_SET, + ReferenceDataGenerator.MALE, + ReferenceDataGenerator.CUSTODY_STATUS_SET, + ReferenceDataGenerator.CUSTODY_STATUS, + ReferenceDataGenerator.ADDITIONAL_IDENTIFIER_TYPE_SET, + ReferenceDataGenerator.DUPLICATE_NOMS, + ReferenceDataGenerator.FORMER_NOMS, PersonGenerator.PERSON_WITH_NOMS, PersonGenerator.PERSON_WITH_NO_NOMS, PersonGenerator.PERSON_WITH_MULTI_MATCH, PersonGenerator.PERSON_WITH_NO_MATCH, PersonGenerator.PERSON_WITH_NOMS_IN_DELIUS, + PersonGenerator.PERSON_WITH_DUPLICATE_NOMS, + PersonGenerator.PERSON_WITH_EXISTING_NOMS, personWithNomsEvent, personWithNomsDisposal, personWithNomsCustody, diff --git a/projects/prison-identifier-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/PersonGenerator.kt b/projects/prison-identifier-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/PersonGenerator.kt index 8ef0166bf1..4708cbfef4 100644 --- a/projects/prison-identifier-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/PersonGenerator.kt +++ b/projects/prison-identifier-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/PersonGenerator.kt @@ -1,22 +1,23 @@ package uk.gov.justice.digital.hmpps.data.generator -import uk.gov.justice.digital.hmpps.integrations.delius.entity.* +import uk.gov.justice.digital.hmpps.entity.* import java.time.LocalDate import java.time.format.DateTimeFormatter object PersonGenerator { - val MALE = generateGender("M") val PERSON_WITH_NOMS = generate("A000001", "E1234XS") val PERSON_WITH_NO_NOMS = generate("A000002", pncNumber = "07/220000004Q") - val PERSON_WITH_NOMS_IN_DELIUS = generate("A000005", pncNumber = "07/220000004Q") val PERSON_WITH_MULTI_MATCH = generate("A000003", forename = "Jack", surname = "Jones") val PERSON_WITH_NO_MATCH = generate("A000004", forename = "Fred", surname = "Jones", dobString = "12/12/2001") + val PERSON_WITH_NOMS_IN_DELIUS = generate("A000005", pncNumber = "07/220000004Q") + val PERSON_WITH_DUPLICATE_NOMS = generate("A000006", "G5541UN") + val PERSON_WITH_EXISTING_NOMS = generate("A000007", "A0007AA") fun generate( crn: String, noms: String? = null, pncNumber: String? = null, - gender: ReferenceData = MALE, + gender: ReferenceData = ReferenceDataGenerator.MALE, forename: String = "bob", surname: String = "smith", softDeleted: Boolean = false, @@ -32,6 +33,7 @@ object PersonGenerator { surname, noms, null, + null, pncNumber, gender, listOf(), @@ -45,7 +47,6 @@ object PersonGenerator { Disposal(id, startDate, event, active = true, softDeleted = false) fun generateCustody(disposal: Disposal, id: Long = IdGenerator.getAndIncrement()) = - Custody(id, null, disposal = disposal) - - fun generateGender(code: String, id: Long = IdGenerator.getAndIncrement()) = ReferenceData(id, code) + Custody(id, null, status = ReferenceDataGenerator.CUSTODY_STATUS, disposal = disposal) } + diff --git a/projects/prison-identifier-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/ReferenceDataGenerator.kt b/projects/prison-identifier-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/ReferenceDataGenerator.kt new file mode 100644 index 0000000000..df276d55ed --- /dev/null +++ b/projects/prison-identifier-and-delius/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/ReferenceDataGenerator.kt @@ -0,0 +1,25 @@ +package uk.gov.justice.digital.hmpps.data.generator + +import uk.gov.justice.digital.hmpps.entity.ReferenceData +import uk.gov.justice.digital.hmpps.entity.ReferenceDataSet + +object ReferenceDataGenerator { + val GENDER_SET = generateReferenceDataSet("GENDER") + val MALE = generateGender("M") + + val CUSTODY_STATUS_SET = generateReferenceDataSet("THROUGHCARE STATUS") + val CUSTODY_STATUS = generateCustodyStatus("A") + + val ADDITIONAL_IDENTIFIER_TYPE_SET = generateReferenceDataSet("ADDITIONAL IDENTIFIER TYPE") + val DUPLICATE_NOMS = generateIdentifierType("DNOMS") + val FORMER_NOMS = generateIdentifierType("XNOMS") + + fun generateGender(code: String, id: Long = IdGenerator.getAndIncrement()) = ReferenceData(id, code, GENDER_SET) + fun generateCustodyStatus(code: String, id: Long = IdGenerator.getAndIncrement()) = + ReferenceData(id, code, CUSTODY_STATUS_SET) + + fun generateIdentifierType(code: String, id: Long = IdGenerator.getAndIncrement()) = + ReferenceData(id, code, ADDITIONAL_IDENTIFIER_TYPE_SET) + + fun generateReferenceDataSet(name: String, id: Long = IdGenerator.getAndIncrement()) = ReferenceDataSet(id, name) +} \ No newline at end of file diff --git a/projects/prison-identifier-and-delius/src/dev/resources/messages/prisoner-merged.json b/projects/prison-identifier-and-delius/src/dev/resources/messages/prisoner-merged.json new file mode 100644 index 0000000000..8685e3dfe7 --- /dev/null +++ b/projects/prison-identifier-and-delius/src/dev/resources/messages/prisoner-merged.json @@ -0,0 +1,20 @@ +{ + "version": 1, + "eventType": "prison-offender-events.prisoner.merged", + "description": "A prisoner has been merged from A0001AA to B0001BB", + "occurredAt": "2024-03-14T16:14:47Z", + "publishedAt": "2024-03-14T16:14:48.060048501Z", + "additionalInformation": { + "nomsNumber": "B0007BB", + "removedNomsNumber": "A0007AA", + "reason": "MERGE" + }, + "personReference": { + "identifiers": [ + { + "type": "NOMS", + "value": "A0007AA" + } + ] + } +} \ No newline at end of file diff --git a/projects/prison-identifier-and-delius/src/dev/resources/messages/prisoner-received-inactive-booking.json b/projects/prison-identifier-and-delius/src/dev/resources/messages/prisoner-received-inactive-booking.json new file mode 100644 index 0000000000..5455be0f3c --- /dev/null +++ b/projects/prison-identifier-and-delius/src/dev/resources/messages/prisoner-received-inactive-booking.json @@ -0,0 +1,26 @@ +{ + "version": 1, + "eventType": "prison-offender-events.prisoner.received", + "description": "A prisoner has been received into prison", + "occurredAt": "2023-08-04T08:09:36.649098+01:00", + "publishedAt": "2023-08-04T09:06:46.65281576+01:00", + "additionalInformation": { + "nomsNumber": "A0002AA", + "reason": "ADMISSION", + "probableCause": "RECALL", + "source": "PRISON", + "nomisMovementReasonCode": "N", + "details": "ACTIVE IN:ADM-N", + "currentLocation": "IN_PRISON", + "prisonId": "WSI", + "currentPrisonStatus": "UNDER_PRISON_CARE" + }, + "personReference": { + "identifiers": [ + { + "type": "NOMS", + "value": "A0002AA" + } + ] + } +} \ No newline at end of file diff --git a/projects/prison-identifier-and-delius/src/dev/resources/messages/prisoner-received.json b/projects/prison-identifier-and-delius/src/dev/resources/messages/prisoner-received.json new file mode 100644 index 0000000000..58affb1eb1 --- /dev/null +++ b/projects/prison-identifier-and-delius/src/dev/resources/messages/prisoner-received.json @@ -0,0 +1,26 @@ +{ + "version": 1, + "eventType": "prison-offender-events.prisoner.received", + "description": "A prisoner has been received into prison", + "occurredAt": "2023-08-04T08:09:36.649098+01:00", + "publishedAt": "2023-08-04T09:06:46.65281576+01:00", + "additionalInformation": { + "nomsNumber": "A0001AA", + "reason": "ADMISSION", + "probableCause": "RECALL", + "source": "PRISON", + "nomisMovementReasonCode": "N", + "details": "ACTIVE IN:ADM-N", + "currentLocation": "IN_PRISON", + "prisonId": "WSI", + "currentPrisonStatus": "UNDER_PRISON_CARE" + }, + "personReference": { + "identifiers": [ + { + "type": "NOMS", + "value": "A0001AA" + } + ] + } +} \ No newline at end of file diff --git a/projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/prison-api-A0001AA-booking.json b/projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/prison-api-A0001AA-booking.json new file mode 100644 index 0000000000..ed101946d3 --- /dev/null +++ b/projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/prison-api-A0001AA-booking.json @@ -0,0 +1,12 @@ +{ + "offenderNo": "A0001AA", + "bookingId": 10000001, + "bookingNo": "00001A", + "offenderId": 10000001, + "rootOffenderId": 10000001, + "firstName": "Bob", + "lastName": "Smith", + "dateOfBirth": "1992-03-15", + "activeFlag": true, + "agencyId": "OUT" +} \ No newline at end of file diff --git a/projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/prison-api-A0001AA-prisoner.json b/projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/prison-api-A0001AA-prisoner.json new file mode 100644 index 0000000000..9413410dc9 --- /dev/null +++ b/projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/prison-api-A0001AA-prisoner.json @@ -0,0 +1,27 @@ +[ + { + "offenderNo": "A0001AA", + "firstName": "Bob", + "lastName": "Smith", + "dateOfBirth": "1992-03-15", + "gender": "Male", + "sexCode": "M", + "nationalities": "British", + "currentlyInPrison": "N", + "latestBookingId": 1000001, + "latestLocationId": "OUT", + "latestLocation": "Outside", + "pncNumber": "24/000001Y", + "croNumber": "00001/24M", + "ethnicity": "White: Eng./Welsh/Scot./N.Irish/British", + "ethnicityCode": "W1", + "birthCountry": "England", + "religion": "No Religion", + "religionCode": "NIL", + "convictedStatus": "Convicted", + "legalStatus": "SENTENCED", + "imprisonmentStatus": "ADIMP_ORA20", + "imprisonmentStatusDesc": "ORA 2020 Standard Determinate Sentence", + "receptionDate": "2024-02-15" + } +] \ No newline at end of file diff --git a/projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/prison-api-A0001AA-sentence.json b/projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/prison-api-A0001AA-sentence.json new file mode 100644 index 0000000000..9f1a8208c9 --- /dev/null +++ b/projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/prison-api-A0001AA-sentence.json @@ -0,0 +1,16 @@ +[ + { + "bookingId": 10000001, + "sentenceSequence": 1, + "termSequence": 1, + "sentenceType": "ADIMP_ORA20", + "sentenceTypeDescription": "ORA 2020 Standard Determinate Sentence", + "startDate": "2022-11-11", + "years": 10, + "lifeSentence": false, + "caseId": "0000001", + "sentenceTermCode": "IMP", + "lineSeq": 1, + "sentenceStartDate": "2022-11-11" + } +] \ No newline at end of file diff --git a/projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/prison-api-A0001AA-unmatched-sentence-date.json b/projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/prison-api-A0001AA-unmatched-sentence-date.json new file mode 100644 index 0000000000..b89408c750 --- /dev/null +++ b/projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/prison-api-A0001AA-unmatched-sentence-date.json @@ -0,0 +1,16 @@ +[ + { + "bookingId": 10000001, + "sentenceSequence": 1, + "termSequence": 1, + "sentenceType": "ADIMP_ORA20", + "sentenceTypeDescription": "ORA 2020 Standard Determinate Sentence", + "startDate": "2021-01-01", + "years": 10, + "lifeSentence": false, + "caseId": "0000001", + "sentenceTermCode": "IMP", + "lineSeq": 1, + "sentenceStartDate": "2021-01-01" + } +] \ No newline at end of file diff --git a/projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/prison-api-A0002AA-booking-inactive.json b/projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/prison-api-A0002AA-booking-inactive.json new file mode 100644 index 0000000000..3a9512fa0f --- /dev/null +++ b/projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/prison-api-A0002AA-booking-inactive.json @@ -0,0 +1,12 @@ +{ + "offenderNo": "A0001AA", + "bookingId": 20000002, + "bookingNo": "00002A", + "offenderId": 20000002, + "rootOffenderId": 20000002, + "firstName": "Bob", + "lastName": "Smith", + "dateOfBirth": "1992-03-15", + "activeFlag": false, + "agencyId": "OUT" +} \ No newline at end of file diff --git a/projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/search-results-body.json b/projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/prisoner-search-results-body.json similarity index 100% rename from projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/search-results-body.json rename to projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/prisoner-search-results-body.json diff --git a/projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/search-results-body2.json b/projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/prisoner-search-results-body2.json similarity index 100% rename from projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/search-results-body2.json rename to projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/prisoner-search-results-body2.json diff --git a/projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/search-results-body3.json b/projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/prisoner-search-results-body3.json similarity index 100% rename from projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/search-results-body3.json rename to projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/prisoner-search-results-body3.json diff --git a/projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/search-results-body4.json b/projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/prisoner-search-results-body4.json similarity index 100% rename from projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/search-results-body4.json rename to projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/prisoner-search-results-body4.json diff --git a/projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/probation-search-multiple-results.json b/projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/probation-search-multiple-results.json new file mode 100644 index 0000000000..a1150c6bb5 --- /dev/null +++ b/projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/probation-search-multiple-results.json @@ -0,0 +1,33 @@ +{ + "matches": [ + { + "offender": { + "offenderId": 1, + "title": "Mr", + "firstName": "Bill", + "middleNames": [], + "surname": "Jones", + "dateOfBirth": "1960-04-07", + "gender": "Male", + "otherIds": { + "crn": "A000002" + } + } + }, + { + "offender": { + "offenderId": 2, + "title": "Mr", + "firstName": "Bob", + "middleNames": [], + "surname": "Smith", + "dateOfBirth": "1960-04-07", + "gender": "Male", + "otherIds": { + "crn": "A000001" + } + } + } + ], + "matchedBy": "ALL_SUPPLIED" +} diff --git a/projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/probation-search-no-results.json b/projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/probation-search-no-results.json new file mode 100644 index 0000000000..a83d51cf3a --- /dev/null +++ b/projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/probation-search-no-results.json @@ -0,0 +1,4 @@ +{ + "matches": [], + "matchedBy": "NONE" +} diff --git a/projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/probation-search-single-result.json b/projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/probation-search-single-result.json new file mode 100644 index 0000000000..2ffb6260bc --- /dev/null +++ b/projects/prison-identifier-and-delius/src/dev/resources/simulations/__files/probation-search-single-result.json @@ -0,0 +1,83 @@ +{ + "matches": [ + { + "offender": { + "offenderId": 1, + "title": "Mr", + "firstName": "Bob", + "middleNames": [], + "surname": "Smith", + "dateOfBirth": "1960-04-07", + "gender": "Smith", + "otherIds": { + "crn": "A000001", + "croNumber": "AB12/123456C", + "niNumber": "JJ123456C" + }, + "contactDetails": { + "phoneNumbers": [], + "emailAddresses": [] + }, + "offenderProfile": { + "offenderLanguages": { + "requiresInterpreter": false + }, + "previousConviction": { + "detail": {} + } + }, + "offenderManagers": [ + { + "staff": { + "code": "N57UATU", + "forenames": "Unallocated", + "surname": "Staff" + }, + "partitionArea": "National Data", + "softDeleted": false, + "team": { + "code": "N57UAT", + "description": "Unallocated Team(N57)", + "district": { + "code": "N57UAT", + "description": "Unallocated Level 3(N57)" + }, + "borough": { + "code": "N57UAT", + "description": "Unallocated Level2(N57)" + } + }, + "probationArea": { + "code": "N57", + "description": "Kent Surrey Sussex Region", + "nps": false + }, + "fromDate": "1900-01-01", + "active": true, + "allocationReason": { + "code": "IN1", + "description": "Initial Allocation" + } + } + ], + "softDeleted": false, + "currentDisposal": "0", + "partitionArea": "National Data", + "currentRestriction": true, + "restrictionMessage": "This is a restricted offender record. Please contact a system administrator", + "currentExclusion": false, + "exclusionMessage": "You are excluded from viewing this offender record. Please contact a system administrator", + "currentTier": "UD0", + "activeProbationManagedSentence": false, + "probationStatus": { + "status": "NOT_SENTENCED", + "inBreach": false, + "preSentenceActivity": false, + "awaitingPsr": false + }, + "age": 63 + } + } + ], + "matchedBy": "ALL_SUPPLIED" +} diff --git a/projects/prison-identifier-and-delius/src/dev/resources/simulations/mappings/prison-api.json b/projects/prison-identifier-and-delius/src/dev/resources/simulations/mappings/prison-api.json new file mode 100644 index 0000000000..7c65c3cb96 --- /dev/null +++ b/projects/prison-identifier-and-delius/src/dev/resources/simulations/mappings/prison-api.json @@ -0,0 +1,56 @@ +{ + "mappings": [ + { + "request": { + "method": "GET", + "url": "/prison-api/api/prisoners/A0001AA" + }, + "response": { + "headers": { + "Content-Type": "application/json" + }, + "status": 200, + "bodyFileName": "prison-api-A0001AA-prisoner.json" + } + }, + { + "request": { + "method": "GET", + "url": "/prison-api/api/bookings/offenderNo/A0001AA?basicInfo=true&extraInfo=false" + }, + "response": { + "headers": { + "Content-Type": "application/json" + }, + "status": 200, + "bodyFileName": "prison-api-A0001AA-booking.json" + } + }, + { + "request": { + "method": "GET", + "url": "/prison-api/api/offender-sentences/booking/10000001/sentenceTerms" + }, + "response": { + "headers": { + "Content-Type": "application/json" + }, + "status": 200, + "bodyFileName": "prison-api-A0001AA-sentence.json" + } + }, + { + "request": { + "method": "GET", + "url": "/prison-api/api/bookings/offenderNo/A0002AA?basicInfo=true&extraInfo=false" + }, + "response": { + "headers": { + "Content-Type": "application/json" + }, + "status": 200, + "bodyFileName": "prison-api-A0002AA-booking-inactive.json" + } + } + ] +} \ No newline at end of file diff --git a/projects/prison-identifier-and-delius/src/dev/resources/simulations/mappings/prisoner-search.json b/projects/prison-identifier-and-delius/src/dev/resources/simulations/mappings/prisoner-search.json index 66fedfe764..3b55acef5f 100644 --- a/projects/prison-identifier-and-delius/src/dev/resources/simulations/mappings/prisoner-search.json +++ b/projects/prison-identifier-and-delius/src/dev/resources/simulations/mappings/prisoner-search.json @@ -3,7 +3,7 @@ { "request": { "method": "POST", - "url": "/global-search", + "url": "/prisoner-search/global-search", "bodyPatterns" : [ { "contains" : "\"prisonerIdentifier\":\"07/220000004Q\"" } ] @@ -13,13 +13,13 @@ "Content-Type": "application/json" }, "status": 200, - "bodyFileName": "search-results-body.json" + "bodyFileName": "prisoner-search-results-body.json" } }, { "request": { "method": "POST", - "url": "/global-search", + "url": "/prisoner-search/global-search", "bodyPatterns" : [ { "contains" : "\"firstName\":\"Jack\"" } ] @@ -29,13 +29,13 @@ "Content-Type": "application/json" }, "status": 200, - "bodyFileName": "search-results-body2.json" + "bodyFileName": "prisoner-search-results-body2.json" } }, { "request": { "method": "POST", - "url": "/global-search", + "url": "/prisoner-search/global-search", "bodyPatterns" : [ { "contains" : "\"firstName\":\"Fred\"" } ] @@ -45,13 +45,13 @@ "Content-Type": "application/json" }, "status": 200, - "bodyFileName": "search-results-body3.json" + "bodyFileName": "prisoner-search-results-body3.json" } }, { "request": { "method": "POST", - "url": "/global-search", + "url": "/prisoner-search/global-search", "bodyPatterns" : [ { "contains" : "\"prisonerIdentifier\":\"E1234XS\"" } ] @@ -61,7 +61,7 @@ "Content-Type": "application/json" }, "status": 200, - "bodyFileName": "search-results-body4.json" + "bodyFileName": "prisoner-search-results-body4.json" } } ] diff --git a/projects/prison-identifier-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/MergeIntegrationTest.kt b/projects/prison-identifier-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/MergeIntegrationTest.kt new file mode 100644 index 0000000000..ad3cf2ed05 --- /dev/null +++ b/projects/prison-identifier-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/MergeIntegrationTest.kt @@ -0,0 +1,73 @@ +package uk.gov.justice.digital.hmpps + +import com.github.tomakehurst.wiremock.WireMockServer +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.mockito.Mockito.verify +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT +import org.springframework.boot.test.mock.mockito.MockBean +import org.springframework.test.context.TestPropertySource +import uk.gov.justice.digital.hmpps.data.generator.PersonGenerator.PERSON_WITH_NOMS +import uk.gov.justice.digital.hmpps.messaging.Handler +import uk.gov.justice.digital.hmpps.telemetry.TelemetryService + +@AutoConfigureMockMvc +@SpringBootTest(webEnvironment = RANDOM_PORT) +@TestPropertySource(properties = ["messaging.consumer.dry-run=false"]) +internal class MergeIntegrationTest { + @Autowired + lateinit var wireMockServer: WireMockServer + + @Autowired + lateinit var handler: Handler + + @MockBean + lateinit var telemetryService: TelemetryService + + @Test + fun `merge replaces noms number`() { + handler.handle(prepEvent("prisoner-merged", wireMockServer.port())) + + verify(telemetryService).trackEvent( + "MergeResultSuccess", mapOf( + "reason" to "Replaced NOMS numbers for 1 records", + "existingNomsNumber" to "A0007AA", + "updatedNomsNumber" to "B0007BB", + "matches" to """[{"crn":"A000007"}]""", + "dryRun" to "false", + ) + ) + } + + @Test + fun `merge fails if the new noms number is already assigned`() { + val event = prepEvent("prisoner-merged", wireMockServer.port()).apply { + message.additionalInformation["nomsNumber"] = PERSON_WITH_NOMS.nomsNumber!! + } + + val exception = assertThrows { handler.handle(event) } + + assertThat(exception.message, equalTo("NOMS number E1234XS is already assigned to A000001")) + } + + @Test + fun `merge ignored if the old noms number is not in Delius`() { + val event = prepEvent("prisoner-merged", wireMockServer.port()).apply { + message.additionalInformation["removedNomsNumber"] = "Z9999ZZ" + } + + handler.handle(event) + + verify(telemetryService).trackEvent( + "MergeResultIgnored", mapOf( + "reason" to "No records found for NOMS number Z9999ZZ", + "dryRun" to "false", + ) + ) + } +} diff --git a/projects/prison-identifier-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/NomsNumberIntegrationTest.kt b/projects/prison-identifier-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/NomsNumberIntegrationTest.kt deleted file mode 100644 index cdf3179052..0000000000 --- a/projects/prison-identifier-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/NomsNumberIntegrationTest.kt +++ /dev/null @@ -1,231 +0,0 @@ -package uk.gov.justice.digital.hmpps - -import org.hamcrest.MatcherAssert.assertThat -import org.hamcrest.Matchers.equalTo -import org.junit.jupiter.api.MethodOrderer -import org.junit.jupiter.api.Order -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestMethodOrder -import org.mockito.kotlin.* -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc -import org.springframework.boot.test.context.SpringBootTest -import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT -import org.springframework.boot.test.mock.mockito.MockBean -import org.springframework.boot.test.mock.mockito.SpyBean -import org.springframework.data.repository.findByIdOrNull -import org.springframework.test.web.servlet.MockMvc -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post -import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status -import uk.gov.justice.digital.hmpps.data.generator.PersonGenerator -import uk.gov.justice.digital.hmpps.integrations.delius.entity.CustodyRepository -import uk.gov.justice.digital.hmpps.integrations.delius.entity.PersonRepository -import uk.gov.justice.digital.hmpps.telemetry.TelemetryService -import uk.gov.justice.digital.hmpps.test.MockMvcExtensions.withJson -import uk.gov.justice.digital.hmpps.test.MockMvcExtensions.withToken - -@AutoConfigureMockMvc -@SpringBootTest(webEnvironment = RANDOM_PORT) -@TestMethodOrder(MethodOrderer.OrderAnnotation::class) -internal class NomsNumberIntegrationTest { - - @Autowired - lateinit var mockMvc: MockMvc - - @SpyBean - lateinit var personRepository: PersonRepository - - @SpyBean - lateinit var custodyRepository: CustodyRepository - - @MockBean - lateinit var telemetryService: TelemetryService - - @Test - @Order(1) - fun `API call retuns not found in delius`() { - val crn = "ZZZ" - - mockMvc - .perform(post("/person/populate-noms-number").withToken().withJson(listOf(crn))) - .andExpect(status().is2xxSuccessful) - - verify(telemetryService, never()).trackEvent(any(), any(), any()) - } - - @Test - @Order(2) - fun `API call retuns Noms number already in delius`() { - val crn = PersonGenerator.PERSON_WITH_NOMS.crn - - mockMvc - .perform(post("/person/populate-noms-number").withToken().withJson(listOf(crn))) - .andExpect(status().is2xxSuccessful) - - val nameCapture = argumentCaptor() - val propertyCaptor = argumentCaptor>() - verify(telemetryService, timeout(5000)).trackEvent(nameCapture.capture(), propertyCaptor.capture(), any()) - - assertThat(nameCapture.firstValue, equalTo("SuccessfulMatch")) - assertThat( - propertyCaptor.firstValue, equalTo( - mapOf( - "crn" to "A000001", - "existingNomsId" to "E1234XS", - "custodialEvents" to "1", - "matchedNomsId" to "E1234XS", - "matchedBookingNumber" to "76543A", - "dryRun" to "true" - ) - ) - ) - } - - @Test - @Order(3) - fun `API call retuns single match via prison search api`() { - val crn = PersonGenerator.PERSON_WITH_NO_NOMS.crn - - mockMvc - .perform(post("/person/populate-noms-number?dryRun=false").withToken().withJson(listOf(crn))) - .andExpect(status().is2xxSuccessful) - - val nameCapture = argumentCaptor() - val propertyCaptor = argumentCaptor>() - verify(telemetryService, timeout(5000)).trackEvent(nameCapture.capture(), propertyCaptor.capture(), any()) - - assertThat(nameCapture.firstValue, equalTo("SuccessfulMatch")) - assertThat( - propertyCaptor.firstValue, equalTo( - mapOf( - "crn" to "A000002", - "custodialEvents" to "1", - "matchedNomsId" to "G5541UN", - "matchedBookingNumber" to "13831A", - "matchedSentenceDate" to "2022-12-12", - "sentenceDate" to "2022-12-12", - "dryRun" to "false" - ) - ) - ) - } - - @Test - @Order(4) - fun `API call retuns a single match from multiple matches found in prison search api does not update delius`() { - val crn = PersonGenerator.PERSON_WITH_MULTI_MATCH.crn - - mockMvc - .perform(post("/person/populate-noms-number").withToken().withJson(listOf(crn))) - .andExpect(status().is2xxSuccessful) - - val nameCapture = argumentCaptor() - val propertyCaptor = argumentCaptor>() - verify(telemetryService, timeout(5000)).trackEvent(nameCapture.capture(), propertyCaptor.capture(), any()) - - assertThat(nameCapture.firstValue, equalTo("SuccessfulMatch")) - assertThat( - propertyCaptor.firstValue, equalTo( - mapOf( - "crn" to "A000003", - "custodialEvents" to "1", - "matchedNomsId" to "G5541WW", - "matchedBookingNumber" to "13831A", - "matchedSentenceDate" to "2022-12-12", - "sentenceDate" to "2022-12-12", - "dryRun" to "true" - ) - ) - ) - } - - @Test - @Order(5) - fun `API call retuns a single match from multiple matches found in prison search api updated person in delius`() { - val crn = PersonGenerator.PERSON_WITH_MULTI_MATCH.crn - val custodyId = personRepository.findSentencedByCrn(crn).first().custody!!.id - - mockMvc - .perform(post("/person/populate-noms-number?dryRun=false").withToken().withJson(listOf(crn))) - .andExpect(status().is2xxSuccessful) - - val nameCapture = argumentCaptor() - val propertyCaptor = argumentCaptor>() - verify(telemetryService, timeout(5000)).trackEvent(nameCapture.capture(), propertyCaptor.capture(), any()) - - assertThat(nameCapture.firstValue, equalTo("SuccessfulMatch")) - assertThat( - propertyCaptor.firstValue, equalTo( - mapOf( - "crn" to "A000003", - "custodialEvents" to "1", - "matchedNomsId" to "G5541WW", - "matchedBookingNumber" to "13831A", - "matchedSentenceDate" to "2022-12-12", - "sentenceDate" to "2022-12-12", - "dryRun" to "false" - ) - ) - ) - - val custody = custodyRepository.findByIdOrNull(custodyId) - assertThat(custody?.bookingRef, equalTo("13831A")) - } - - @Test - @Order(6) - fun `API call cant determine a match from multiple matches found in prison search api`() { - val crn = PersonGenerator.PERSON_WITH_NO_MATCH.crn - - mockMvc - .perform(post("/person/populate-noms-number").withToken().withJson(listOf(crn))) - .andExpect(status().is2xxSuccessful) - - val nameCapture = argumentCaptor() - val propertyCaptor = argumentCaptor>() - verify(telemetryService, timeout(5000)).trackEvent(nameCapture.capture(), propertyCaptor.capture(), any()) - - assertThat(nameCapture.firstValue, equalTo("UnsuccessfulMatch")) - assertThat( - propertyCaptor.firstValue, equalTo( - mapOf( - "crn" to "A000004", - "custodialEvents" to "1", - "sentenceDates" to "2022-12-12", - "G5541WW" to "DateOfBirth:INCONCLUSIVE", - "A1234YZ" to "Name:PARTIAL, DateOfBirth:INCONCLUSIVE", - "dryRun" to "true" - ) - ) - ) - } - - @Test - @Order(7) - fun `API call retuns single match but noms number already in delius via prison search api`() { - val crn = PersonGenerator.PERSON_WITH_NOMS_IN_DELIUS.crn - - mockMvc - .perform(post("/person/populate-noms-number").withToken().withJson(listOf(crn))) - .andExpect(status().is2xxSuccessful) - - val nameCapture = argumentCaptor() - val propertyCaptor = argumentCaptor>() - verify(telemetryService, timeout(5000)).trackEvent(nameCapture.capture(), propertyCaptor.capture(), any()) - - assertThat(nameCapture.firstValue, equalTo("SuccessfulMatch")) - assertThat( - propertyCaptor.firstValue, equalTo( - mapOf( - "crn" to "A000005", - "custodialEvents" to "1", - "matchedNomsId" to "G5541UN", - "matchedBookingNumber" to "13831A", - "matchedSentenceDate" to "2022-12-12", - "sentenceDate" to "2022-12-12", - "dryRun" to "true" - ) - ) - ) - } -} diff --git a/projects/prison-identifier-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/PrisonMatchingIntegrationTest.kt b/projects/prison-identifier-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/PrisonMatchingIntegrationTest.kt new file mode 100644 index 0000000000..4ed3ea98ff --- /dev/null +++ b/projects/prison-identifier-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/PrisonMatchingIntegrationTest.kt @@ -0,0 +1,246 @@ +package uk.gov.justice.digital.hmpps + +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.hamcrest.Matchers.nullValue +import org.junit.jupiter.api.MethodOrderer +import org.junit.jupiter.api.Order +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestMethodOrder +import org.mockito.kotlin.any +import org.mockito.kotlin.never +import org.mockito.kotlin.timeout +import org.mockito.kotlin.verify +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT +import org.springframework.boot.test.mock.mockito.MockBean +import org.springframework.boot.test.mock.mockito.SpyBean +import org.springframework.data.repository.findByIdOrNull +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status +import uk.gov.justice.digital.hmpps.data.generator.PersonGenerator +import uk.gov.justice.digital.hmpps.data.generator.PersonGenerator.PERSON_WITH_DUPLICATE_NOMS +import uk.gov.justice.digital.hmpps.entity.AdditionalIdentifierRepository +import uk.gov.justice.digital.hmpps.entity.CustodyRepository +import uk.gov.justice.digital.hmpps.entity.PersonRepository +import uk.gov.justice.digital.hmpps.entity.getByCrn +import uk.gov.justice.digital.hmpps.telemetry.TelemetryService +import uk.gov.justice.digital.hmpps.test.MockMvcExtensions.withJson +import uk.gov.justice.digital.hmpps.test.MockMvcExtensions.withToken + +@AutoConfigureMockMvc +@SpringBootTest(webEnvironment = RANDOM_PORT) +@TestMethodOrder(MethodOrderer.OrderAnnotation::class) +internal class PrisonMatchingIntegrationTest { + + @Autowired + lateinit var mockMvc: MockMvc + + @SpyBean + lateinit var personRepository: PersonRepository + + @SpyBean + lateinit var custodyRepository: CustodyRepository + + @SpyBean + lateinit var additionalIdentifierRepository: AdditionalIdentifierRepository + + @MockBean + lateinit var telemetryService: TelemetryService + + @Test + @Order(1) + fun `crn does not exist`() { + val crn = "ZZZ" + + mockMvc + .perform(post("/person/match-by-crn").withToken().withJson(listOf(crn))) + .andExpect(status().is2xxSuccessful) + + verify(telemetryService, never()).trackEvent(any(), any(), any()) + } + + @Test + @Order(2) + fun `single match with existing noms number`() { + val crn = PersonGenerator.PERSON_WITH_NOMS.crn + + mockMvc + .perform(post("/person/match-by-crn").withToken().withJson(listOf(crn))) + .andExpect(status().is2xxSuccessful) + + verify(telemetryService, timeout(5000)).trackEvent( + "MatchResultSuccess", mapOf( + "reason" to "Matched CRN A000001 to NOMS number E1234XS", + "crn" to "A000001", + "potentialMatches" to """[{"nomsNumber":"E1234XS"}]""", + "existingNomsNumber" to "E1234XS", + "matchedNomsNumber" to "E1234XS", + "nomsNumberChanged" to "false", + "matchedBookingNumber" to "76543A", + "bookingNumberChanged" to "false", + "totalCustodialEvents" to "1", + "matchingCustodialEvents" to "0", + "dryRun" to "true" + ) + ) + } + + @Test + @Order(3) + fun `single match with no existing identifiers`() { + val crn = PersonGenerator.PERSON_WITH_NO_NOMS.crn + val custodyId = personRepository.findSentencedByCrn(crn).first().custody!!.id + + mockMvc + .perform(post("/person/match-by-crn?dryRun=false").withToken().withJson(listOf(crn))) + .andExpect(status().is2xxSuccessful) + + verify(telemetryService, timeout(5000)).trackEvent( + "MatchResultSuccess", mapOf( + "reason" to "Matched CRN A000002 to NOMS number G5541UN and custody $custodyId to 13831A", + "crn" to "A000002", + "potentialMatches" to """[{"nomsNumber":"G5541UN"}]""", + "matchedNomsNumber" to "G5541UN", + "nomsNumberChanged" to "true", + "matchedBookingNumber" to "13831A", + "custody" to "$custodyId", + "bookingNumberChanged" to "true", + "sentenceDateInDelius" to "2022-12-12", + "sentenceDateInNomis" to "2022-12-12", + "totalCustodialEvents" to "1", + "matchingCustodialEvents" to "1", + "dryRun" to "false" + ) + ) + + // also removes any duplicates + val duplicate = personRepository.getByCrn(PERSON_WITH_DUPLICATE_NOMS.crn) + assertThat(duplicate.nomsNumber, nullValue()) + val identifiers = additionalIdentifierRepository.findAll().filter { it.personId == duplicate.id } + .associate { it.type.code to it.identifier } + assertThat(identifiers, equalTo(mapOf("DNOMS" to "G5541UN"))) + } + + @Test + @Order(4) + fun `multiple potential matches from search, but one exact match - dry run does not update Delius`() { + val crn = PersonGenerator.PERSON_WITH_MULTI_MATCH.crn + val custodyId = personRepository.findSentencedByCrn(crn).first().custody!!.id + + mockMvc + .perform(post("/person/match-by-crn").withToken().withJson(listOf(crn))) + .andExpect(status().is2xxSuccessful) + + verify(telemetryService, timeout(5000)).trackEvent( + "MatchResultSuccess", mapOf( + "reason" to "Matched CRN A000003 to NOMS number G5541WW and custody $custodyId to 13831A", + "crn" to "A000003", + "potentialMatches" to """[{"nomsNumber":"G5541WW"},{"nomsNumber":"A1234YZ","Name":"PARTIAL","SentenceDate":"INCONCLUSIVE"}]""", + "matchedNomsNumber" to "G5541WW", + "nomsNumberChanged" to "true", + "matchedBookingNumber" to "13831A", + "bookingNumberChanged" to "true", + "custody" to "$custodyId", + "sentenceDateInDelius" to "2022-12-12", + "sentenceDateInNomis" to "2022-12-12", + "totalCustodialEvents" to "1", + "matchingCustodialEvents" to "1", + "dryRun" to "true" + ) + ) + + verify(personRepository, never()).save(any()) + verify(custodyRepository, never()).save(any()) + verify(additionalIdentifierRepository, never()).save(any()) + } + + @Test + @Order(5) + fun `multiple potential matches from search, but one exact match - no dry run`() { + val crn = PersonGenerator.PERSON_WITH_MULTI_MATCH.crn + val custodyId = personRepository.findSentencedByCrn(crn).first().custody!!.id + + mockMvc + .perform(post("/person/match-by-crn?dryRun=false").withToken().withJson(listOf(crn))) + .andExpect(status().is2xxSuccessful) + + verify(telemetryService, timeout(300_000)).trackEvent( + "MatchResultSuccess", mapOf( + "reason" to "Matched CRN A000003 to NOMS number G5541WW and custody $custodyId to 13831A", + "crn" to "A000003", + "potentialMatches" to """[{"nomsNumber":"G5541WW"},{"nomsNumber":"A1234YZ","Name":"PARTIAL","SentenceDate":"INCONCLUSIVE"}]""", + "matchedNomsNumber" to "G5541WW", + "nomsNumberChanged" to "true", + "matchedBookingNumber" to "13831A", + "bookingNumberChanged" to "true", + "custody" to "$custodyId", + "sentenceDateInDelius" to "2022-12-12", + "sentenceDateInNomis" to "2022-12-12", + "totalCustodialEvents" to "1", + "matchingCustodialEvents" to "1", + "dryRun" to "false" + ) + ) + + val person = personRepository.getByCrn(crn) + assertThat(person.nomsNumber, equalTo("G5541WW")) + val custody = custodyRepository.findByIdOrNull(custodyId)!! + assertThat(custody.prisonerNumber, equalTo("13831A")) + } + + @Test + @Order(6) + fun `no match - nothing updated`() { + val crn = PersonGenerator.PERSON_WITH_NO_MATCH.crn + + mockMvc + .perform(post("/person/match-by-crn").withToken().withJson(listOf(crn))) + .andExpect(status().is2xxSuccessful) + + verify(telemetryService, timeout(5000)).trackEvent( + "MatchResultNoMatch", mapOf( + "reason" to "No single match found in prison system", + "crn" to "A000004", + "potentialMatches" to """[{"nomsNumber":"G5541WW","DateOfBirth":"INCONCLUSIVE"},{"nomsNumber":"A1234YZ","Name":"PARTIAL","DateOfBirth":"INCONCLUSIVE"}]""", + "dryRun" to "true" + ) + ) + + verify(personRepository, never()).save(any()) + verify(custodyRepository, never()).save(any()) + verify(additionalIdentifierRepository, never()).save(any()) + } + + @Test + @Order(7) + fun `API call retuns single match but noms number already in delius via prison search api`() { + val crn = PersonGenerator.PERSON_WITH_NOMS_IN_DELIUS.crn + val custodyId = personRepository.findSentencedByCrn(crn).first().custody!!.id + + mockMvc + .perform(post("/person/match-by-crn").withToken().withJson(listOf(crn))) + .andExpect(status().is2xxSuccessful) + + verify(telemetryService, timeout(5000)).trackEvent( + "MatchResultSuccess", mapOf( + "reason" to "Matched CRN A000005 to NOMS number G5541UN and custody $custodyId to 13831A", + "crn" to "A000005", + "potentialMatches" to """[{"nomsNumber":"G5541UN"}]""", + "matchedNomsNumber" to "G5541UN", + "nomsNumberChanged" to "true", + "matchedBookingNumber" to "13831A", + "bookingNumberChanged" to "true", + "custody" to "$custodyId", + "sentenceDateInDelius" to "2022-12-12", + "sentenceDateInNomis" to "2022-12-12", + "totalCustodialEvents" to "1", + "matchingCustodialEvents" to "1", + "dryRun" to "true" + ) + ) + } +} diff --git a/projects/prison-identifier-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/ProbationMatchingIntegrationTest.kt b/projects/prison-identifier-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/ProbationMatchingIntegrationTest.kt new file mode 100644 index 0000000000..922ee7159b --- /dev/null +++ b/projects/prison-identifier-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/ProbationMatchingIntegrationTest.kt @@ -0,0 +1,174 @@ +package uk.gov.justice.digital.hmpps + +import com.github.tomakehurst.wiremock.WireMockServer +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.github.tomakehurst.wiremock.common.ContentTypes.APPLICATION_JSON +import com.github.tomakehurst.wiremock.common.ContentTypes.CONTENT_TYPE +import org.junit.jupiter.api.Test +import org.mockito.Mockito.verify +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Value +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT +import org.springframework.boot.test.mock.mockito.MockBean +import org.springframework.boot.test.mock.mockito.SpyBean +import org.springframework.test.annotation.DirtiesContext +import uk.gov.justice.digital.hmpps.entity.AdditionalIdentifierRepository +import uk.gov.justice.digital.hmpps.entity.CustodyRepository +import uk.gov.justice.digital.hmpps.entity.PersonRepository +import uk.gov.justice.digital.hmpps.messaging.HmppsChannelManager +import uk.gov.justice.digital.hmpps.telemetry.TelemetryService + +@AutoConfigureMockMvc +@SpringBootTest(webEnvironment = RANDOM_PORT) +internal class ProbationMatchingIntegrationTest { + + @Value("\${messaging.consumer.queue}") + lateinit var queueName: String + + @Autowired + lateinit var channelManager: HmppsChannelManager + + @Autowired + lateinit var wireMockServer: WireMockServer + + @SpyBean + lateinit var personRepository: PersonRepository + + @SpyBean + lateinit var custodyRepository: CustodyRepository + + @SpyBean + lateinit var additionalIdentifierRepository: AdditionalIdentifierRepository + + @MockBean + lateinit var telemetryService: TelemetryService + + @Test + fun `inactive booking is ignored`() { + val event = prepEvent("prisoner-received-inactive-booking", wireMockServer.port()) + + channelManager.getChannel(queueName).publishAndWait(event) + + verify(telemetryService).trackEvent( + "MatchResultIgnored", + mapOf("reason" to "No active booking", "dryRun" to "true") + ) + } + + @Test + fun `prisoner received updates identifiers`() { + withMatchResponse("probation-search-single-result.json") + + val event = prepEvent("prisoner-received", wireMockServer.port()) + channelManager.getChannel(queueName).publishAndWait(event) + + verify(telemetryService).trackEvent( + "MatchResultSuccess", mapOf( + "reason" to "Matched CRN A000001 to NOMS number A0001AA and custody ${custodyId("A000001")} to 00001A", + "dryRun" to "true", + "nomsNumber" to "A0001AA", + "bookingNo" to "00001A", + "matchedBy" to "ALL_SUPPLIED", + "potentialMatches" to """[{"crn":"A000001"}]""", + "existingNomsNumber" to "E1234XS", + "matchedNomsNumber" to "A0001AA", + "nomsNumberChanged" to "true", + "matchedBookingNumber" to "00001A", + "bookingNumberChanged" to "true", + "custody" to "${custodyId("A000001")}", + "sentenceDateInDelius" to "2022-11-11", + "sentenceDateInNomis" to "2022-11-11", + "totalCustodialEvents" to "1", + "matchingCustodialEvents" to "1" + ) + ) + } + + @Test + fun `multiple matches are refined by sentence date`() { + withMatchResponse("probation-search-multiple-results.json") + + val event = prepEvent("prisoner-received", wireMockServer.port()) + channelManager.getChannel(queueName).publishAndWait(event) + + verify(telemetryService).trackEvent( + "MatchResultSuccess", mapOf( + "reason" to "Matched CRN A000001 to NOMS number A0001AA and custody ${custodyId("A000001")} to 00001A", + "dryRun" to "true", + "nomsNumber" to "A0001AA", + "bookingNo" to "00001A", + "matchedBy" to "ALL_SUPPLIED", + "potentialMatches" to """[{"crn":"A000002"},{"crn":"A000001"}]""", + "existingNomsNumber" to "E1234XS", + "matchedNomsNumber" to "A0001AA", + "nomsNumberChanged" to "true", + "matchedBookingNumber" to "00001A", + "bookingNumberChanged" to "true", + "custody" to "${custodyId("A000001")}", + "sentenceDateInDelius" to "2022-11-11", + "sentenceDateInNomis" to "2022-11-11", + "totalCustodialEvents" to "1", + "matchingCustodialEvents" to "1" + ) + ) + } + + @Test + fun `no matches from probation search`() { + withMatchResponse("probation-search-no-results.json") + + val event = prepEvent("prisoner-received", wireMockServer.port()) + channelManager.getChannel(queueName).publishAndWait(event) + + verify(telemetryService).trackEvent( + "MatchResultNoMatch", mapOf( + "reason" to "No single match found in probation system", + "dryRun" to "true", + "nomsNumber" to "A0001AA", + "bookingNo" to "00001A", + "matchedBy" to "NONE", + "potentialMatches" to "[]", + "sentenceDateInNomis" to "2022-11-11" + ) + ) + } + + @Test + @DirtiesContext + fun `no matches on sentence date`() { + withMatchResponse("probation-search-single-result.json") + withJsonResponse( + "/prison-api/api/offender-sentences/booking/10000001/sentenceTerms", + "prison-api-A0001AA-unmatched-sentence-date.json" + ) + + val event = prepEvent("prisoner-received", wireMockServer.port()) + channelManager.getChannel(queueName).publishAndWait(event) + + verify(telemetryService).trackEvent( + "MatchResultNoMatch", mapOf( + "reason" to "No single match found in probation system", + "dryRun" to "true", + "nomsNumber" to "A0001AA", + "bookingNo" to "00001A", + "matchedBy" to "ALL_SUPPLIED", + "potentialMatches" to """[{"crn":"A000001"}]""", + "sentenceDateInNomis" to "2021-01-01" + ) + ) + } + + private fun withJsonResponse(url: String, filename: String) { + val response = aResponse().withStatus(200).withBodyFile(filename).withHeader(CONTENT_TYPE, APPLICATION_JSON) + wireMockServer.addStubMapping(get(url).willReturn(response).build()) + } + + private fun withMatchResponse(filename: String) { + val response = aResponse().withStatus(200).withBodyFile(filename).withHeader(CONTENT_TYPE, APPLICATION_JSON) + wireMockServer.addStubMapping(post("/probation-search/match").willReturn(response).build()) + } + + private fun custodyId(crn: String): Long = personRepository.findSentencedByCrn(crn).first().custody!!.id +} diff --git a/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/client/PrisonApiClient.kt b/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/client/PrisonApiClient.kt new file mode 100644 index 0000000000..c21eac5328 --- /dev/null +++ b/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/client/PrisonApiClient.kt @@ -0,0 +1,45 @@ +package uk.gov.justice.digital.hmpps.client + +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.service.annotation.GetExchange +import java.time.LocalDate + +interface PrisonApiClient { + @GetExchange(value = "/api/prisoners/{nomsNumber}") + fun getPrisoners(@PathVariable nomsNumber: String): List + + @GetExchange(value = "/api/bookings/offenderNo/{nomsNumber}") + fun getBooking( + @PathVariable nomsNumber: String, + @RequestParam basicInfo: Boolean = true, + @RequestParam extraInfo: Boolean = false + ): Booking + + @GetExchange(value = "/api/offender-sentences/booking/{bookingId}/sentenceTerms") + fun getSentenceTerms(@PathVariable bookingId: Long): List +} + +data class Prisoner( + val offenderNo: String, + val pncNumber: String?, + val croNumber: String?, + val firstName: String, + val lastName: String, + val dateOfBirth: LocalDate, +) + +data class Booking( + val bookingId: Long, + val bookingNo: String, + val offenderNo: String, + val activeFlag: Boolean, + val firstName: String, + val lastName: String, + val dateOfBirth: LocalDate, +) + +data class SentenceSummary( + val startDate: LocalDate?, + val consecutiveTo: Long?, +) \ No newline at end of file diff --git a/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/prison/SearchResponse.kt b/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/client/PrisonerSearchClient.kt similarity index 53% rename from projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/prison/SearchResponse.kt rename to projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/client/PrisonerSearchClient.kt index 6388f0e5f4..5a607ffb99 100644 --- a/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/prison/SearchResponse.kt +++ b/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/client/PrisonerSearchClient.kt @@ -1,13 +1,16 @@ -package uk.gov.justice.digital.hmpps.integrations.prison +package uk.gov.justice.digital.hmpps.client import com.fasterxml.jackson.annotation.JsonAlias +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.service.annotation.PostExchange import java.time.LocalDate -data class SearchResponse( - val content: List -) +interface PrisonerSearchClient { + @PostExchange(url = "/global-search") + fun globalSearch(@RequestBody body: PrisonerSearchRequest): PrisonerSearchResponse +} -data class SearchRequest( +data class PrisonerSearchRequest( val prisonerIdentifier: String?, val firstName: String, val lastName: String, @@ -16,7 +19,11 @@ data class SearchRequest( val includeAliases: Boolean = true ) -data class PrisonSearchResult( +data class PrisonerSearchResponse( + val content: List +) + +data class PrisonerSearchResult( val firstName: String, val lastName: String, val prisonerNumber: String, @@ -26,4 +33,4 @@ data class PrisonSearchResult( val croNumber: String?, val sentenceStartDate: LocalDate?, val dateOfBirth: LocalDate -) +) \ No newline at end of file diff --git a/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/client/ProbationSearchClient.kt b/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/client/ProbationSearchClient.kt new file mode 100644 index 0000000000..15f9ac7230 --- /dev/null +++ b/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/client/ProbationSearchClient.kt @@ -0,0 +1,63 @@ +package uk.gov.justice.digital.hmpps.client + +import com.fasterxml.jackson.annotation.JsonFormat +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.service.annotation.PostExchange +import java.time.LocalDate + +interface ProbationSearchClient { + @PostExchange(url = "/match") + fun match(@RequestBody body: ProbationMatchRequest): ProbationMatchResponse +} + +data class ProbationMatchRequest( + val firstName: String, + val surname: String, + @JsonFormat(pattern = "yyyy-MM-dd") + val dateOfBirth: LocalDate, + val nomsNumber: String, + val activeSentence: Boolean = true, + val pncNumber: String? = null, + val croNumber: String? = null, +) { + constructor(prisoner: Prisoner) : this( + firstName = prisoner.firstName, + surname = prisoner.lastName, + dateOfBirth = prisoner.dateOfBirth, + nomsNumber = prisoner.offenderNo, + pncNumber = prisoner.pncNumber, + croNumber = prisoner.croNumber, + activeSentence = true, + ) +} + +data class ProbationMatchResponse( + val matches: List, + val matchedBy: String, +) + +data class OffenderMatch( + val offender: OffenderDetail, +) + +data class OffenderDetail( + val otherIds: IDs, + val previousSurname: String? = null, + val title: String? = null, + val firstName: String? = null, + val middleNames: List? = null, + val surname: String? = null, + val dateOfBirth: LocalDate? = null, + val gender: String? = null, + val currentDisposal: String? = null, +) + +data class IDs( + val crn: String, + val pncNumber: String? = null, + val croNumber: String? = null, + val niNumber: String? = null, + val nomsNumber: String? = null, + val immigrationNumber: String? = null, + val mostRecentPrisonerNumber: String? = null, +) diff --git a/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/config/RestClientConfig.kt b/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/config/RestClientConfig.kt index 83f699f754..e1bde45ef2 100644 --- a/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/config/RestClientConfig.kt +++ b/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/config/RestClientConfig.kt @@ -4,18 +4,23 @@ import org.springframework.beans.factory.annotation.Value import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.web.client.RestClient +import uk.gov.justice.digital.hmpps.client.PrisonApiClient +import uk.gov.justice.digital.hmpps.client.PrisonerSearchClient +import uk.gov.justice.digital.hmpps.client.ProbationSearchClient import uk.gov.justice.digital.hmpps.config.security.createClient -import uk.gov.justice.digital.hmpps.integrations.prison.PrisonSearchApi @Configuration class RestClientConfig(private val oauth2Client: RestClient) { @Bean - fun prisonSearchAPI(@Value("\${integrations.prisoner-search.url}") apiBaseUrl: String): PrisonSearchApi { - return createClient( - oauth2Client.mutate() - .baseUrl(apiBaseUrl) - .build() - ) - } + fun prisonerSearchClient(@Value("\${integrations.prisoner-search.url}") apiBaseUrl: String): PrisonerSearchClient = + createClient(oauth2Client.mutate().baseUrl(apiBaseUrl).build()) + + @Bean + fun prisonApiClient(@Value("\${integrations.prison-api.url}") apiBaseUrl: String): PrisonApiClient = + createClient(oauth2Client.mutate().baseUrl(apiBaseUrl).build()) + + @Bean + fun probationSearchClient(@Value("\${integrations.probation-search.url}") apiBaseUrl: String): ProbationSearchClient = + createClient(oauth2Client.mutate().baseUrl(apiBaseUrl).build()) } diff --git a/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/controller/MatchingController.kt b/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/controller/MatchingController.kt index 8b7094ff05..a2eb21201e 100644 --- a/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/controller/MatchingController.kt +++ b/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/controller/MatchingController.kt @@ -1,20 +1,34 @@ package uk.gov.justice.digital.hmpps.controller import jakarta.validation.constraints.Size +import org.springframework.http.HttpStatus import org.springframework.security.access.prepost.PreAuthorize import org.springframework.web.bind.annotation.* -import uk.gov.justice.digital.hmpps.sevice.MatchingNotifier +import uk.gov.justice.digital.hmpps.messaging.Notifier @RestController @RequestMapping("person") -class MatchingController(private val matchingNotifier: MatchingNotifier) { +class MatchingController(private val notifier: Notifier) { + @ResponseStatus(HttpStatus.ACCEPTED) @PreAuthorize("hasRole('PROBATION_API__PRISON_IDENTIFIER__UPDATE')") - @RequestMapping(value = ["/populate-noms-number"], method = [RequestMethod.GET, RequestMethod.POST]) - fun populateNomsNumbers( + @RequestMapping(value = ["/match-by-crn"], method = [RequestMethod.GET, RequestMethod.POST]) + fun matchByCrn( @RequestParam(defaultValue = "true") dryRun: Boolean, - @Size(min = 1, max = 500, message = "Please provide between 1 and 500 crns") @RequestBody crns: List? + @Size(min = 1, max = 500, message = "Please provide between 1 and 500 CRNs. Leave blank to match all CRNs.") + @RequestBody crns: List? ) { - Thread.ofVirtual().start { matchingNotifier.sendForMatch(crns ?: listOf(), dryRun) } + Thread.ofVirtual().start { notifier.requestPrisonMatching(crns ?: listOf(), dryRun) } + } + + @ResponseStatus(HttpStatus.ACCEPTED) + @PreAuthorize("hasRole('PROBATION_API__PRISON_IDENTIFIER__UPDATE')") + @RequestMapping(value = ["/match-by-noms"], method = [RequestMethod.GET, RequestMethod.POST]) + fun matchByNoms( + @RequestParam(defaultValue = "true") dryRun: Boolean, + @Size(min = 1, max = 500, message = "Please provide between 1 and 500 NOMS numbers.") + @RequestBody nomsNumbers: List + ) { + Thread.ofVirtual().start { notifier.requestProbationMatching(nomsNumbers, dryRun) } } } diff --git a/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/entity/AdditionalIdentifier.kt b/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/entity/AdditionalIdentifier.kt new file mode 100644 index 0000000000..5f9c50e2df --- /dev/null +++ b/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/entity/AdditionalIdentifier.kt @@ -0,0 +1,59 @@ +package uk.gov.justice.digital.hmpps.entity + +import jakarta.persistence.* +import org.hibernate.annotations.SQLRestriction +import org.springframework.data.annotation.CreatedBy +import org.springframework.data.annotation.CreatedDate +import org.springframework.data.annotation.LastModifiedBy +import org.springframework.data.annotation.LastModifiedDate +import org.springframework.data.jpa.domain.support.AuditingEntityListener +import org.springframework.data.jpa.repository.JpaRepository +import java.time.ZonedDateTime + +@Entity +@EntityListeners(AuditingEntityListener::class) +@SQLRestriction("soft_deleted = 0") +class AdditionalIdentifier( + + @Column(columnDefinition = "varchar2(30)") + val identifier: String, + + @ManyToOne + @JoinColumn(name = "identifier_name_id") + val type: ReferenceData, + + @Column(name = "offender_id") + val personId: Long, + + @Column + val partitionAreaId: Long = 0, + + @Column(columnDefinition = "number") + val softDeleted: Boolean = false, + + @Id + @Column(name = "additional_identifier_id") + val id: Long = 0, + + @Version + @Column(name = "row_version") + val version: Long = 0, + + @Column(nullable = false, updatable = false) + @CreatedBy + var createdByUserId: Long = 0, + + @Column(nullable = false) + @LastModifiedBy + var lastUpdatedUserId: Long = 0, + + @Column(nullable = false, updatable = false) + @CreatedDate + var createdDatetime: ZonedDateTime = ZonedDateTime.now(), + + @Column(nullable = false) + @LastModifiedDate + var lastUpdatedDatetime: ZonedDateTime = ZonedDateTime.now() +) + +interface AdditionalIdentifierRepository : JpaRepository diff --git a/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/entity/Event.kt b/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/entity/Event.kt similarity index 85% rename from projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/entity/Event.kt rename to projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/entity/Event.kt index 4bfa2030de..8d11adaf3c 100644 --- a/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/entity/Event.kt +++ b/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/entity/Event.kt @@ -1,14 +1,6 @@ -package uk.gov.justice.digital.hmpps.integrations.delius.entity - -import jakarta.persistence.Column -import jakarta.persistence.Entity -import jakarta.persistence.EntityListeners -import jakarta.persistence.Id -import jakarta.persistence.JoinColumn -import jakarta.persistence.ManyToOne -import jakarta.persistence.OneToOne -import jakarta.persistence.Table -import jakarta.persistence.Version +package uk.gov.justice.digital.hmpps.entity + +import jakarta.persistence.* import org.hibernate.annotations.Immutable import org.hibernate.annotations.SQLRestriction import org.springframework.data.annotation.CreatedBy @@ -72,13 +64,18 @@ class Disposal( @Entity @EntityListeners(AuditingEntityListener::class) +@SQLRestriction("soft_deleted = 0 and (select cs.code_value from r_standard_reference_list cs where cs.standard_reference_list_id = custodial_status_id) <> 'P'") class Custody( @Id @Column(name = "custody_id") val id: Long, - @Column(name = "prisoner_number") - var bookingRef: String?, + @Column + var prisonerNumber: String?, + + @ManyToOne + @JoinColumn(name = "custodial_status_id") + val status: ReferenceData, @OneToOne @JoinColumn(name = "disposal_id", updatable = false) diff --git a/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/entity/Person.kt b/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/entity/Person.kt similarity index 83% rename from projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/entity/Person.kt rename to projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/entity/Person.kt index eb213e024b..86db3fb8d5 100644 --- a/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/delius/entity/Person.kt +++ b/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/entity/Person.kt @@ -1,9 +1,8 @@ -package uk.gov.justice.digital.hmpps.integrations.delius.entity +package uk.gov.justice.digital.hmpps.entity import jakarta.persistence.* import org.hibernate.annotations.Fetch import org.hibernate.annotations.FetchMode -import org.hibernate.annotations.Immutable import org.hibernate.annotations.SQLRestriction import org.springframework.data.annotation.CreatedBy import org.springframework.data.annotation.CreatedDate @@ -13,6 +12,7 @@ import org.springframework.data.jpa.domain.support.AuditingEntityListener import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Query import uk.gov.justice.digital.hmpps.exception.NotFoundException +import uk.gov.justice.digital.hmpps.service.withinDays import java.time.LocalDate import java.time.ZonedDateTime @@ -47,6 +47,9 @@ class Person( @Column(columnDefinition = "char(7)") var nomsNumber: String? = null, + @Column + var mostRecentPrisonerNumber: String? = null, + @Column val croNumber: String? = null, @@ -87,25 +90,11 @@ class Person( fun isSentenced() = events.any { it.disposal != null } fun sentenceDates() = events.mapNotNull { it.disposal?.startDate } -} -@Immutable -@Entity -@Table(name = "r_standard_reference_list") -class ReferenceData( - @Id - @Column(name = "standard_reference_list_id", nullable = false) - val id: Long, + fun custodies() = events.mapNotNull { it.disposal?.custody } - @Column(name = "code_value") - val code: String - -) { - fun prisonGenderCode() = when (code) { - "M", "F" -> code - "N" -> "NK" - else -> "ALL" - } + fun custodiesWithSentenceDateCloseTo(sentenceDate: LocalDate) = + custodies().filter { sentenceDate.withinDays(it.disposal.startDate) } } interface PersonRepository : JpaRepository { @@ -124,18 +113,19 @@ interface PersonRepository : JpaRepository { and e.softDeleted = false and e.active = true and c.softDeleted = false - and c.bookingRef is null + and c.prisonerNumber is null """ ) fun findSentencedByCrn(crn: String): List fun findByCrn(crn: String): Person? - @Query("select count(p) from Person p where p.nomsNumber = :nomsNumber and p.id <> :personId") - fun checkForDuplicateNoms(nomsNumber: String, personId: Long): Int - @Query("select p.crn from Person p where p.softDeleted = false") fun findAllCrns(): List + + fun findAllByNomsNumberAndIdNot(nomsNumber: String, id: Long): List + + fun findAllByNomsNumber(nomsNumber: String): List } fun PersonRepository.getByCrn(crn: String) = findByCrn(crn) ?: throw NotFoundException("Person", "crn", crn) diff --git a/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/entity/Prisoner.kt b/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/entity/Prisoner.kt new file mode 100644 index 0000000000..759602b338 --- /dev/null +++ b/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/entity/Prisoner.kt @@ -0,0 +1,30 @@ +package uk.gov.justice.digital.hmpps.entity + +import jakarta.persistence.* +import org.springframework.data.jpa.repository.JpaRepository +import java.io.Serializable + +@Entity +@Table(name = "offender_prisoner") +class Prisoner( + @EmbeddedId + val id: PrisonerId, + + val partitionAreaId: Long = 0, + + @Column(name = "row_version") + val version: Long = 0, +) + +@Embeddable +class PrisonerId( + @Column(name = "offender_id") + val personId: Long, + + val prisonerNumber: String, +) : Serializable + +interface PrisonerRepository : JpaRepository { + fun deleteAllByIdPersonId(personId: Long) +} + diff --git a/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/entity/ReferenceData.kt b/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/entity/ReferenceData.kt new file mode 100644 index 0000000000..7fcacac284 --- /dev/null +++ b/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/entity/ReferenceData.kt @@ -0,0 +1,53 @@ +package uk.gov.justice.digital.hmpps.entity + +import jakarta.persistence.* +import org.hibernate.annotations.Immutable +import org.springframework.data.jpa.repository.JpaRepository +import uk.gov.justice.digital.hmpps.exception.NotFoundException + +@Entity +@Immutable +@Table(name = "r_standard_reference_list") +class ReferenceData( + @Id + @Column(name = "standard_reference_list_id", nullable = false) + val id: Long, + + @Column(name = "code_value") + val code: String, + + @ManyToOne + @JoinColumn(name = "reference_data_master_id") + val set: ReferenceDataSet, +) { + fun prisonGenderCode() = when (code) { + "M", "F" -> code + "N" -> "NK" + else -> "ALL" + } +} + +@Entity +@Immutable +@Table(name = "r_reference_data_master") +class ReferenceDataSet( + @Id + @Column(name = "reference_data_master_id") + val id: Long, + + @Column(name = "code_set_name") + val name: String +) + +interface ReferenceDataRepository : JpaRepository { + fun findByCodeAndSetName(code: String, setName: String): ReferenceData? +} + +fun ReferenceDataRepository.getByCodeAndSetName(code: String, set: String): ReferenceData = + findByCodeAndSetName(code, set) ?: throw NotFoundException(set, "code", code) + +fun ReferenceDataRepository.duplicateNomsNumberIdentifierType(): ReferenceData = + getByCodeAndSetName("DNOMS", "ADDITIONAL IDENTIFIER TYPE") + +fun ReferenceDataRepository.formerNomsNumberIdentifierType(): ReferenceData = + getByCodeAndSetName("XNOMS", "ADDITIONAL IDENTIFIER TYPE") diff --git a/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/prison/PrisonSearchAPI.kt b/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/prison/PrisonSearchAPI.kt deleted file mode 100644 index 67db450287..0000000000 --- a/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/integrations/prison/PrisonSearchAPI.kt +++ /dev/null @@ -1,9 +0,0 @@ -package uk.gov.justice.digital.hmpps.integrations.prison - -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.service.annotation.PostExchange - -interface PrisonSearchApi { - @PostExchange(url = "/global-search") - fun matchPerson(@RequestBody body: SearchRequest): SearchResponse -} diff --git a/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/messaging/Handler.kt b/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/messaging/Handler.kt new file mode 100644 index 0000000000..91cc0e84d4 --- /dev/null +++ b/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/messaging/Handler.kt @@ -0,0 +1,48 @@ +package uk.gov.justice.digital.hmpps.messaging + +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Component +import uk.gov.justice.digital.hmpps.converter.NotificationConverter +import uk.gov.justice.digital.hmpps.message.HmppsDomainEvent +import uk.gov.justice.digital.hmpps.message.Notification +import uk.gov.justice.digital.hmpps.model.logResult +import uk.gov.justice.digital.hmpps.service.PrisonMatchingService +import uk.gov.justice.digital.hmpps.service.ProbationMatchingService +import uk.gov.justice.digital.hmpps.telemetry.TelemetryService +import uk.gov.justice.digital.hmpps.telemetry.notificationReceived + +@Component +class Handler( + override val converter: NotificationConverter, + private val telemetryService: TelemetryService, + private val probationMatchingService: ProbationMatchingService, + private val prisonMatchingService: PrisonMatchingService, + @Value("\${messaging.consumer.dry-run:true}") private val messagingDryRun: Boolean +) : NotificationHandler { + override fun handle(notification: Notification) { + telemetryService.notificationReceived(notification) + val message = notification.message + + when (notification.eventType) { + "prison-identifier.internal.prison-match-requested" -> prisonMatchingService + .matchAndUpdateIdentifiers(checkNotNull(message.personReference.findCrn()), message.dryRun) + .also { telemetryService.logResult(it, message.dryRun) } + + "prison-identifier.internal.probation-match-requested" -> probationMatchingService + .matchAndUpdateIdentifiers(checkNotNull(message.personReference.findNomsNumber()), message.dryRun) + .also { telemetryService.logResult(it, message.dryRun) } + + "prison-offender-events.prisoner.received" -> probationMatchingService + .matchAndUpdateIdentifiers(checkNotNull(message.personReference.findNomsNumber()), messagingDryRun) + .also { telemetryService.logResult(it, messagingDryRun) } + + "prison-offender-events.prisoner.merged" -> probationMatchingService + .replaceIdentifiers(message.oldNoms, message.newNoms, messagingDryRun) + .also { telemetryService.logResult(it, messagingDryRun) } + } + } + + val HmppsDomainEvent.dryRun get() = additionalInformation["dryRun"] == true + val HmppsDomainEvent.oldNoms get() = checkNotNull(additionalInformation["removedNomsNumber"]) as String + val HmppsDomainEvent.newNoms get() = checkNotNull(additionalInformation["nomsNumber"]) as String +} diff --git a/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/messaging/Notifier.kt b/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/messaging/Notifier.kt new file mode 100644 index 0000000000..69a3b0cd5a --- /dev/null +++ b/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/messaging/Notifier.kt @@ -0,0 +1,89 @@ +package uk.gov.justice.digital.hmpps.messaging + +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.stereotype.Service +import uk.gov.justice.digital.hmpps.entity.PersonRepository +import uk.gov.justice.digital.hmpps.message.* +import uk.gov.justice.digital.hmpps.model.PrisonIdentifiers +import uk.gov.justice.digital.hmpps.publisher.NotificationPublisher + +@Service +class Notifier( + private val personRepository: PersonRepository, + @Qualifier("queuePublisher") private val queuePublisher: NotificationPublisher, + @Qualifier("topicPublisher") private val topicPublisher: NotificationPublisher, +) { + companion object { + const val REQUEST_PRISON_MATCH = "prison-identifier.internal.prison-match-requested" + const val REQUEST_PROBATION_MATCH = "prison-identifier.internal.probation-match-requested" + } + + fun requestPrisonMatching(crns: List, dryRun: Boolean) { + crns.ifEmpty { personRepository.findAllCrns() }.asSequence() + .map { notification(REQUEST_PRISON_MATCH, PersonIdentifier("CRN", it), dryRun) } + .forEach { queuePublisher.publish(it) } + } + + fun requestProbationMatching(nomsNumbers: List, dryRun: Boolean) { + nomsNumbers.asSequence() + .map { notification(REQUEST_PROBATION_MATCH, PersonIdentifier("NOMS", it), dryRun) } + .forEach { queuePublisher.publish(it) } + } + + fun identifierAdded(crn: String, prisonIdentifiers: PrisonIdentifiers) { + topicPublisher.publish( + Notification( + message = HmppsDomainEvent( + version = 1, + eventType = "probation-case.prison-identifier.added", + description = "A probation case has been matched with a booking in the prison system. The prisoner and booking identifiers have been added to the probation case.", + personReference = PersonReference( + identifiers = listOf( + PersonIdentifier("CRN", crn), + PersonIdentifier("NOMS", prisonIdentifiers.prisonerNumber), + ), + ), + nullableAdditionalInformation = prisonIdentifiers.bookingNumber?.let { + AdditionalInformation( + info = mutableMapOf( + "bookingNumber" to it + ) + ) + } + ), + attributes = MessageAttributes("probation-case.prison-identifier.added") + ) + ) + } + + fun identifierUpdated(crn: String, nomsNumber: String, previousNomsNumber: String) { + topicPublisher.publish( + Notification( + message = HmppsDomainEvent( + version = 1, + eventType = "probation-case.prison-identifier.updated", + description = "A prisoner identifier has been updated following a merge. This been reflected on the probation case.", + personReference = PersonReference( + identifiers = listOf( + PersonIdentifier("CRN", crn), + PersonIdentifier("NOMS", nomsNumber), + ), + ), + nullableAdditionalInformation = AdditionalInformation(info = mutableMapOf("previousNomsNumber" to previousNomsNumber)), + ), + attributes = MessageAttributes("probation-case.prison-identifier.updated") + ) + ) + } + + private fun notification(eventType: String, identifier: PersonIdentifier, dryRun: Boolean) = + Notification( + message = HmppsDomainEvent( + eventType = eventType, + version = 1, + nullableAdditionalInformation = AdditionalInformation(mutableMapOf("dryRun" to dryRun)), + personReference = PersonReference(listOf(identifier)) + ), + attributes = MessageAttributes(eventType) + ) +} \ No newline at end of file diff --git a/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/model/MatchResult.kt b/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/model/MatchResult.kt new file mode 100644 index 0000000000..b194b66b40 --- /dev/null +++ b/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/model/MatchResult.kt @@ -0,0 +1,38 @@ +package uk.gov.justice.digital.hmpps.model + +import uk.gov.justice.digital.hmpps.entity.Custody +import uk.gov.justice.digital.hmpps.entity.Person +import uk.gov.justice.digital.hmpps.telemetry.TelemetryService + +sealed interface MatchResult { + val reason: String + val telemetryProperties: Map + + fun name() = "MatchResult${this::class.simpleName}" + + data class Ignored( + override val reason: String, + override val telemetryProperties: Map = mapOf() + ) : MatchResult + + data class NoMatch( + override val reason: String, + override val telemetryProperties: Map = mapOf() + ) : MatchResult + + data class Success( + val prisonIdentifiers: PrisonIdentifiers, + val person: Person, + val custody: Custody?, + override val telemetryProperties: Map = mapOf(), + override val reason: String = "Matched CRN ${person.crn} to NOMS number ${prisonIdentifiers.prisonerNumber}${custody?.let { " and custody ${custody.id} to ${prisonIdentifiers.bookingNumber}" } ?: ""}", + ) : MatchResult +} + +fun TelemetryService.logResult(result: MatchResult, dryRun: Boolean) { + trackEvent( + result.name(), + mapOf("reason" to result.reason, "dryRun" to dryRun.toString()) + + result.telemetryProperties.filterValues { it != null }.mapValues { it.value.toString() } + ) +} \ No newline at end of file diff --git a/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/model/MergeResult.kt b/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/model/MergeResult.kt new file mode 100644 index 0000000000..6e6b72d01d --- /dev/null +++ b/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/model/MergeResult.kt @@ -0,0 +1,24 @@ +package uk.gov.justice.digital.hmpps.model + +import uk.gov.justice.digital.hmpps.telemetry.TelemetryService + +sealed interface MergeResult { + val reason: String + val telemetryProperties: Map + + fun name() = "MergeResult${this::class.simpleName}" + + data class Ignored(override val reason: String, override val telemetryProperties: Map = mapOf()) : + MergeResult + + data class Success(override val reason: String, override val telemetryProperties: Map = mapOf()) : + MergeResult +} + +fun TelemetryService.logResult(result: MergeResult, dryRun: Boolean) { + trackEvent( + result.name(), + mapOf("reason" to result.reason, "dryRun" to dryRun.toString()) + + result.telemetryProperties.filterValues { it != null }.mapValues { it.value.toString() } + ) +} \ No newline at end of file diff --git a/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/model/PrisonIdentifiers.kt b/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/model/PrisonIdentifiers.kt new file mode 100644 index 0000000000..55bd4c1b0e --- /dev/null +++ b/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/model/PrisonIdentifiers.kt @@ -0,0 +1,6 @@ +package uk.gov.justice.digital.hmpps.model + +data class PrisonIdentifiers( + val prisonerNumber: String, + val bookingNumber: String? = null, +) \ No newline at end of file diff --git a/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/sevice/model/Matches.kt b/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/model/PrisonerMatches.kt similarity index 70% rename from projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/sevice/model/Matches.kt rename to projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/model/PrisonerMatches.kt index c089ed6ede..a38e6ee5a6 100644 --- a/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/sevice/model/Matches.kt +++ b/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/model/PrisonerMatches.kt @@ -1,15 +1,16 @@ -package uk.gov.justice.digital.hmpps.sevice.model +package uk.gov.justice.digital.hmpps.model -import uk.gov.justice.digital.hmpps.integrations.delius.entity.Person -import uk.gov.justice.digital.hmpps.integrations.prison.PrisonSearchResult +import uk.gov.justice.digital.hmpps.client.PrisonerSearchResult +import uk.gov.justice.digital.hmpps.entity.Person +import uk.gov.justice.digital.hmpps.service.DateMatcher +import uk.gov.justice.digital.hmpps.service.withinDays -data class PersonMatch( +data class PrisonerMatches( val person: Person, - val responses: List, - val nomsNumberShared: Boolean? + val responses: List ) { - private fun nameMatchType(prisoner: PrisonSearchResult): ComponentMatch.MatchType = + private fun nameMatchType(prisoner: PrisonerSearchResult): ComponentMatch.MatchType = when { prisoner.firstName.equals(person.forename, true) && prisoner.lastName.equals(person.surname, true) -> ComponentMatch.MatchType.MATCH @@ -20,9 +21,9 @@ data class PersonMatch( else -> ComponentMatch.MatchType.PARTIAL } - private fun nameMatch(prisoner: PrisonSearchResult) = ComponentMatch.Name(nameMatchType(prisoner)) + private fun nameMatch(prisoner: PrisonerSearchResult) = ComponentMatch.Name(nameMatchType(prisoner)) - private fun dobMatch(prisoner: PrisonSearchResult) = ComponentMatch.DateOfBirth( + private fun dobMatch(prisoner: PrisonerSearchResult) = ComponentMatch.DateOfBirth( when (prisoner.dateOfBirth) { person.dateOfBirth -> ComponentMatch.MatchType.MATCH in DateMatcher.variations(person.dateOfBirth) -> ComponentMatch.MatchType.PARTIAL @@ -30,7 +31,7 @@ data class PersonMatch( } ) - private fun identifierMatch(prisoner: PrisonSearchResult): ComponentMatch.Identifier? = listOf( + private fun identifierMatch(prisoner: PrisonerSearchResult): ComponentMatch.Identifier? = listOf( prisoner.prisonerNumber to person.nomsNumber, prisoner.pncNumber to person.pncNumber, prisoner.croNumber to person.croNumber @@ -43,7 +44,7 @@ data class PersonMatch( private fun exclusiveField(first: String?, second: String?): Boolean = (first == null && second != null) || (first != null && second == null) - private fun sentenceDateMatch(prisoner: PrisonSearchResult) = ComponentMatch.SentenceDate( + private fun sentenceDateMatch(prisoner: PrisonerSearchResult) = ComponentMatch.SentenceDate( when { person.isSentenced() && (prisoner.sentenceStartDate != null && person.sentenceDates() .any { it.withinDays(prisoner.sentenceStartDate) }) -> ComponentMatch.MatchType.MATCH @@ -53,7 +54,7 @@ data class PersonMatch( } ) - private fun componentMatches(prisoner: PrisonSearchResult): List = + private fun componentMatches(prisoner: PrisonerSearchResult): List = when (val idMatch = identifierMatch(prisoner)) { is ComponentMatch.Identifier -> listOf(idMatch) else -> listOfNotNull( @@ -68,7 +69,7 @@ data class PersonMatch( val matches = potentialMatches.filter { potential -> potential.matches.all { it.type == ComponentMatch.MatchType.MATCH } } - val match: PrisonSearchResult? = if (matches.size == 1) matches.first().prisoner else null + val match: PrisonerSearchResult? = if (matches.size == 1) matches.first().prisoner else null } sealed interface ComponentMatch { @@ -78,13 +79,11 @@ sealed interface ComponentMatch { data class Name(override val type: MatchType) : ComponentMatch data class DateOfBirth(override val type: MatchType) : ComponentMatch - data class Identifier(override val type: MatchType) : ComponentMatch - data class SentenceDate(override val type: MatchType) : ComponentMatch enum class MatchType { MATCH, PARTIAL, INCONCLUSIVE } } -data class PotentialMatch(val prisoner: PrisonSearchResult, val matches: List) \ No newline at end of file +data class PotentialMatch(val prisoner: PrisonerSearchResult, val matches: List) \ No newline at end of file diff --git a/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/sevice/model/DateMatcher.kt b/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/service/DateMatcher.kt similarity index 95% rename from projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/sevice/model/DateMatcher.kt rename to projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/service/DateMatcher.kt index a924abcdaf..0c1afa8c8f 100644 --- a/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/sevice/model/DateMatcher.kt +++ b/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/service/DateMatcher.kt @@ -1,4 +1,4 @@ -package uk.gov.justice.digital.hmpps.sevice.model +package uk.gov.justice.digital.hmpps.service import java.time.DateTimeException import java.time.LocalDate diff --git a/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/service/MatchWriter.kt b/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/service/MatchWriter.kt new file mode 100644 index 0000000000..c3292d5884 --- /dev/null +++ b/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/service/MatchWriter.kt @@ -0,0 +1,73 @@ +package uk.gov.justice.digital.hmpps.service + +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import uk.gov.justice.digital.hmpps.entity.* +import uk.gov.justice.digital.hmpps.model.PrisonIdentifiers + +@Service +class MatchWriter( + private val personRepository: PersonRepository, + private val custodyRepository: CustodyRepository, + private val additionalIdentifierRepository: AdditionalIdentifierRepository, + private val referenceDataRepository: ReferenceDataRepository, + private val prisonerRepository: PrisonerRepository, +) { + @Transactional + fun update(prisonIdentifiers: PrisonIdentifiers, person: Person, custody: Custody? = null) { + if (person.nomsNumber != prisonIdentifiers.prisonerNumber) { + removeDuplicateNomsNumbers(person, prisonIdentifiers.prisonerNumber) + updateNomsNumber(person, prisonIdentifiers.prisonerNumber) + } + if (custody != null && custody.prisonerNumber != prisonIdentifiers.prisonerNumber) { + custody.prisonerNumber = prisonIdentifiers.bookingNumber + custodyRepository.save(custody) + person.mostRecentPrisonerNumber = prisonIdentifiers.bookingNumber + personRepository.save(person) + person.rebuildPrisonerLinks() + } + } + + private fun removeDuplicateNomsNumbers(person: Person, nomsNumber: String) { + personRepository.findAllByNomsNumberAndIdNot(nomsNumber, person.id).forEach { duplicate -> + duplicate.identifyDuplicateNomsNumber(nomsNumber) + duplicate.nomsNumber = null + personRepository.save(duplicate) + } + } + + private fun updateNomsNumber(person: Person, nomsNumber: String) { + person.identifyFormerNomsNumber(person.nomsNumber) + person.nomsNumber = nomsNumber + personRepository.save(person) + } + + private fun Person.identifyDuplicateNomsNumber(nomsNumber: String) { + additionalIdentifierRepository.save( + AdditionalIdentifier( + personId = this.id, + identifier = nomsNumber, + type = referenceDataRepository.duplicateNomsNumberIdentifierType(), + ) + ) + } + + private fun Person.identifyFormerNomsNumber(nomsNumber: String?) { + nomsNumber?.let { + additionalIdentifierRepository.save( + AdditionalIdentifier( + personId = this.id, + identifier = nomsNumber, + type = referenceDataRepository.formerNomsNumberIdentifierType(), + ) + ) + } + } + + private fun Person.rebuildPrisonerLinks() { + prisonerRepository.deleteAllByIdPersonId(id) + events.mapNotNull { it.disposal?.custody?.prisonerNumber } + .map { prisonerNumber -> Prisoner(PrisonerId(id, prisonerNumber)) } + .let(prisonerRepository::saveAll) + } +} \ No newline at end of file diff --git a/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/service/PrisonMatchingService.kt b/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/service/PrisonMatchingService.kt new file mode 100644 index 0000000000..5308d286be --- /dev/null +++ b/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/service/PrisonMatchingService.kt @@ -0,0 +1,86 @@ +package uk.gov.justice.digital.hmpps.service + +import com.fasterxml.jackson.databind.ObjectMapper +import org.springframework.stereotype.Service +import uk.gov.justice.digital.hmpps.client.PrisonerSearchClient +import uk.gov.justice.digital.hmpps.client.PrisonerSearchRequest +import uk.gov.justice.digital.hmpps.entity.Person +import uk.gov.justice.digital.hmpps.entity.PersonRepository +import uk.gov.justice.digital.hmpps.entity.getByCrn +import uk.gov.justice.digital.hmpps.messaging.Notifier +import uk.gov.justice.digital.hmpps.model.* +import uk.gov.justice.digital.hmpps.model.MatchResult.NoMatch +import uk.gov.justice.digital.hmpps.model.MatchResult.Success + +@Service +class PrisonMatchingService( + private val personRepository: PersonRepository, + private val prisonerSearchClient: PrisonerSearchClient, + private val matchWriter: MatchWriter, + private val notifier: Notifier, + private val objectMapper: ObjectMapper, +) { + fun matchAndUpdateIdentifiers(crn: String, dryRun: Boolean): MatchResult { + val matchResult = findMatchingPrisonRecord(crn) + if (!dryRun && matchResult is Success) { + with(matchResult) { + matchWriter.update(prisonIdentifiers, person, custody) + notifier.identifierAdded(crn, prisonIdentifiers) + } + } + return matchResult + } + + private fun findMatchingPrisonRecord(crn: String): MatchResult { + // Get person on probation details + val person = personRepository.getByCrn(crn) + + // Get matching prisoner records + val searchResults = prisonerSearchClient.globalSearch(person.asSearchRequest()).content + val prisonerMatches = PrisonerMatches(person, searchResults) + val matchedPrisoner = prisonerMatches.match + ?: return NoMatch("No single match found in prison system", prisonerMatches.telemetry()) + + // Compare sentence dates + val identifiers = PrisonIdentifiers(matchedPrisoner.prisonerNumber, matchedPrisoner.bookingNumber) + val matchingCustodies = matchedPrisoner.sentenceStartDate + ?.let { person.custodiesWithSentenceDateCloseTo(it) } ?: emptyList() + val matchingCustody = matchingCustodies.singleOrNull() + + return Success( + identifiers, person, matchingCustody, prisonerMatches.telemetry() + mapOf( + "existingNomsNumber" to person.nomsNumber, + "matchedNomsNumber" to identifiers.prisonerNumber, + "nomsNumberChanged" to (identifiers.prisonerNumber != person.nomsNumber), + "existingBookingNumber" to matchingCustody?.prisonerNumber, + "matchedBookingNumber" to identifiers.bookingNumber, + "bookingNumberChanged" to (matchingCustody != null && identifiers.bookingNumber != matchingCustody.prisonerNumber), + "custody" to matchingCustody?.id, + "sentenceDateInDelius" to matchingCustody?.disposal?.startDate, + "sentenceDateInNomis" to matchedPrisoner.sentenceStartDate, + "totalCustodialEvents" to person.custodies().size, + "matchingCustodialEvents" to matchingCustodies.size, + ) + ) + } + + private fun Person.asSearchRequest() = PrisonerSearchRequest( + nomsNumber?.trim() ?: pncNumber?.trim() ?: croNumber?.trim(), + forename, + surname, + gender?.prisonGenderCode(), + dateOfBirth + ) + + fun PrisonerMatches.telemetry() = mapOf( + "crn" to person.crn, + "potentialMatches" to potentialMatches.telemetry() + ) + + fun List.telemetry(): String = objectMapper.writeValueAsString(map { potentialMatch -> + mapOf("nomsNumber" to potentialMatch.prisoner.prisonerNumber) + + potentialMatch.matches + .filter { it.type != ComponentMatch.MatchType.MATCH } + .associate { component -> component.name() to component.type } + }) +} \ No newline at end of file diff --git a/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/service/ProbationMatchingService.kt b/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/service/ProbationMatchingService.kt new file mode 100644 index 0000000000..6296b33c15 --- /dev/null +++ b/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/service/ProbationMatchingService.kt @@ -0,0 +1,112 @@ +package uk.gov.justice.digital.hmpps.service + +import com.fasterxml.jackson.databind.ObjectMapper +import org.springframework.stereotype.Service +import uk.gov.justice.digital.hmpps.client.* +import uk.gov.justice.digital.hmpps.entity.PersonRepository +import uk.gov.justice.digital.hmpps.entity.getByCrn +import uk.gov.justice.digital.hmpps.messaging.Notifier +import uk.gov.justice.digital.hmpps.model.MatchResult +import uk.gov.justice.digital.hmpps.model.MatchResult.* +import uk.gov.justice.digital.hmpps.model.MergeResult +import uk.gov.justice.digital.hmpps.model.PrisonIdentifiers +import java.time.LocalDate + +@Service +class ProbationMatchingService( + private val prisonApiClient: PrisonApiClient, + private val probationSearchClient: ProbationSearchClient, + private val personRepository: PersonRepository, + private val matchWriter: MatchWriter, + private val notifier: Notifier, + private val objectMapper: ObjectMapper, +) { + fun matchAndUpdateIdentifiers(nomsNumber: String, dryRun: Boolean): MatchResult { + val matchResult = findMatchingProbationRecord(nomsNumber) + if (!dryRun && matchResult is Success) { + with(matchResult) { + matchWriter.update(prisonIdentifiers, person, custody) + notifier.identifierAdded(person.crn, prisonIdentifiers) + } + } + return matchResult + } + + fun replaceIdentifiers(oldNomsNumber: String, newNomsNumber: String, dryRun: Boolean): MergeResult { + personRepository.findAllByNomsNumber(newNomsNumber).joinToString { it.crn }.takeIf { it.isNotEmpty() }?.let { + throw IllegalArgumentException("NOMS number $newNomsNumber is already assigned to $it") + } + + val existing = personRepository.findAllByNomsNumber(oldNomsNumber) + if (existing.isEmpty()) { + return MergeResult.Ignored("No records found for NOMS number $oldNomsNumber") + } + if (!dryRun) { + existing.forEach { + matchWriter.update(PrisonIdentifiers(newNomsNumber), it) + notifier.identifierUpdated(it.crn, newNomsNumber, oldNomsNumber) + } + } + return MergeResult.Success( + "Replaced NOMS numbers for ${existing.size} records", mapOf( + "existingNomsNumber" to oldNomsNumber, + "updatedNomsNumber" to newNomsNumber, + "matches" to objectMapper.writeValueAsString(existing.map { mapOf("crn" to it.crn) }) + ) + ) + } + + private fun findMatchingProbationRecord(nomsNumber: String): MatchResult { + // Get prisoner details + val booking = prisonApiClient.getBooking(nomsNumber).takeIf { it.activeFlag } + ?: return Ignored("No active booking") + val sentenceDate = prisonApiClient.getSentenceTerms(booking.bookingId).latestPrimarySentenceStartDate() + ?: return Ignored("No sentence start date") + val identifiers = PrisonIdentifiers(nomsNumber, booking.bookingNo) + val prisoner = prisonApiClient.getPrisoners(nomsNumber).single() + + // Get matching probation records + val matchResponse = probationSearchClient.match(ProbationMatchRequest(prisoner)) + + // Compare sentence dates + val matchingCustodies = matchResponse.matches.crns() + .flatMap { personRepository.getByCrn(it).custodiesWithSentenceDateCloseTo(sentenceDate) } + val matchingCustody = matchingCustodies.singleOrNull() + val matchingPerson = matchingCustodies.map { it.disposal.event.person }.distinctBy { it.crn }.singleOrNull() + ?: return NoMatch( + "No single match found in probation system", + booking.telemetry() + matchResponse.telemetry() + mapOf("sentenceDateInNomis" to sentenceDate) + ) + + return Success( + identifiers, matchingPerson, matchingCustody, booking.telemetry() + matchResponse.telemetry() + mapOf( + "existingNomsNumber" to matchingPerson.nomsNumber, + "matchedNomsNumber" to identifiers.prisonerNumber, + "nomsNumberChanged" to (identifiers.prisonerNumber != matchingPerson.nomsNumber), + "existingBookingNumber" to matchingCustody?.prisonerNumber, + "matchedBookingNumber" to identifiers.bookingNumber, + "bookingNumberChanged" to (matchingCustody != null && identifiers.bookingNumber != matchingCustody.prisonerNumber), + "custody" to matchingCustody?.id, + "sentenceDateInDelius" to matchingCustody?.disposal?.startDate, + "sentenceDateInNomis" to sentenceDate, + "totalCustodialEvents" to matchingPerson.custodies().size, + "matchingCustodialEvents" to matchingCustodies.size, + ) + ) + } + + private fun List.latestPrimarySentenceStartDate(): LocalDate? = + filter { it.startDate != null && it.consecutiveTo == null }.maxOfOrNull { it.startDate!! } + + private fun List.crns() = map { it.offender.otherIds.crn } + + private fun Booking.telemetry() = mapOf( + "nomsNumber" to offenderNo, + "bookingNo" to bookingNo, + ) + + private fun ProbationMatchResponse.telemetry() = mapOf( + "matchedBy" to matchedBy, + "potentialMatches" to objectMapper.writeValueAsString(matches.crns().map { mapOf("crn" to it) }) + ) +} \ No newline at end of file diff --git a/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/sevice/MatchWriter.kt b/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/sevice/MatchWriter.kt deleted file mode 100644 index dbbcf12e90..0000000000 --- a/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/sevice/MatchWriter.kt +++ /dev/null @@ -1,24 +0,0 @@ -package uk.gov.justice.digital.hmpps.sevice - -import org.springframework.stereotype.Service -import org.springframework.transaction.annotation.Transactional -import uk.gov.justice.digital.hmpps.integrations.delius.entity.Custody -import uk.gov.justice.digital.hmpps.integrations.delius.entity.CustodyRepository -import uk.gov.justice.digital.hmpps.integrations.delius.entity.PersonRepository -import uk.gov.justice.digital.hmpps.sevice.model.PersonMatch - -@Service -class MatchWriter( - private val personRepository: PersonRepository, - private val custodyRepository: CustodyRepository -) { - @Transactional - fun update(personMatch: PersonMatch, custody: Custody?, onException: (Exception) -> Unit) = try { - personMatch.match?.let { - personRepository.save(personMatch.person.apply { it.prisonerNumber }) - custody?.apply { this.bookingRef = it.bookingNumber }?.let(custodyRepository::save) - } - } catch (e: Exception) { - onException(e) - } -} \ No newline at end of file diff --git a/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/sevice/Matcher.kt b/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/sevice/Matcher.kt deleted file mode 100644 index 6d8f0e2cb8..0000000000 --- a/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/sevice/Matcher.kt +++ /dev/null @@ -1,36 +0,0 @@ -package uk.gov.justice.digital.hmpps.sevice - -import org.springframework.stereotype.Service -import uk.gov.justice.digital.hmpps.integrations.delius.entity.Person -import uk.gov.justice.digital.hmpps.integrations.delius.entity.PersonRepository -import uk.gov.justice.digital.hmpps.integrations.prison.PrisonSearchApi -import uk.gov.justice.digital.hmpps.integrations.prison.SearchRequest -import uk.gov.justice.digital.hmpps.sevice.model.PersonMatch - -@Service -class Matcher(private val personRepository: PersonRepository, private val prisonSearchApi: PrisonSearchApi) { - - fun matchCrn(crn: String): PersonMatch? = personRepository.findByCrn(crn)?.let { person -> - val searchRequest = person.asSearchRequest() - val searchResults = prisonSearchApi.matchPerson(searchRequest).content - val duplicateNoms = if (searchResults.size == 1) { - personRepository.checkForDuplicateNoms(searchResults.first().prisonerNumber, person.id) > 0 - } else { - null - } - PersonMatch( - person, - searchResults, - duplicateNoms - ) - } -} - -private fun Person.asSearchRequest() = - SearchRequest( - nomsNumber?.trim() ?: pncNumber?.trim() ?: croNumber?.trim(), - forename, - surname, - gender?.prisonGenderCode(), - dateOfBirth - ) \ No newline at end of file diff --git a/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/sevice/MatchingNotifier.kt b/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/sevice/MatchingNotifier.kt deleted file mode 100644 index bd18708e7d..0000000000 --- a/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/sevice/MatchingNotifier.kt +++ /dev/null @@ -1,31 +0,0 @@ -package uk.gov.justice.digital.hmpps.sevice - -import org.springframework.stereotype.Service -import uk.gov.justice.digital.hmpps.integrations.delius.entity.PersonRepository -import uk.gov.justice.digital.hmpps.message.* -import uk.gov.justice.digital.hmpps.publisher.NotificationPublisher - -@Service -class MatchingNotifier( - private val personRepository: PersonRepository, - private val notificationPublisher: NotificationPublisher -) { - fun sendForMatch(crns: List, dryRun: Boolean) { - crns.ifEmpty { personRepository.findAllCrns() } - .forEach { notificationPublisher.publish(notification(it, dryRun)) } - } - - private fun notification(crn: String, dryRun: Boolean): Notification = Notification( - HmppsDomainEvent( - INTERNAL_EVENT_TYPE, - 1, - nullableAdditionalInformation = AdditionalInformation(mutableMapOf("dryRun" to dryRun)), - personReference = PersonReference(listOf(PersonIdentifier("CRN", crn))) - ), - attributes = MessageAttributes(INTERNAL_EVENT_TYPE) - ) - - companion object { - const val INTERNAL_EVENT_TYPE = "prison-identifier.internal.match" - } -} \ No newline at end of file diff --git a/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/sevice/MatchingService.kt b/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/sevice/MatchingService.kt deleted file mode 100644 index c7654d4661..0000000000 --- a/projects/prison-identifier-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/sevice/MatchingService.kt +++ /dev/null @@ -1,117 +0,0 @@ -package uk.gov.justice.digital.hmpps.sevice - -import org.springframework.stereotype.Component -import org.springframework.stereotype.Service -import uk.gov.justice.digital.hmpps.converter.NotificationConverter -import uk.gov.justice.digital.hmpps.integrations.delius.entity.Custody -import uk.gov.justice.digital.hmpps.message.HmppsDomainEvent -import uk.gov.justice.digital.hmpps.message.Notification -import uk.gov.justice.digital.hmpps.messaging.NotificationHandler -import uk.gov.justice.digital.hmpps.sevice.model.ComponentMatch -import uk.gov.justice.digital.hmpps.sevice.model.PersonMatch -import uk.gov.justice.digital.hmpps.sevice.model.PotentialMatch -import uk.gov.justice.digital.hmpps.sevice.model.withinDays -import uk.gov.justice.digital.hmpps.telemetry.TelemetryService -import java.time.LocalDate -import java.time.format.DateTimeFormatter - -@Component -class MatchingHandler( - override val converter: NotificationConverter, - private val matchingService: MatchingService -) : NotificationHandler { - override fun handle(notification: Notification) { - notification.message.personReference.findCrn()?.let { - matchingService.matchWithPrisonData(it, notification.message.additionalInformation["dryRun"] == true) - } - } -} - -@Service -class MatchingService( - private val matcher: Matcher, - private val matchWriter: MatchWriter, - private val telemetryService: TelemetryService -) { - - fun matchWithPrisonData(crn: String, trialOnly: Boolean) = - match(crn)?.let { match -> - logToTelemetry(match, trialOnly) - if (!trialOnly && match is CompletedMatch.Successful) { - matchWriter.update(match.personMatch, match.custody) { - telemetryService.trackEvent( - "PersistingException", - mapOf("crn" to match.personMatch.person.crn, "exception" to it.message!!) - ) - } - } - } - - private fun match(crn: String): CompletedMatch? = try { - matcher.matchCrn(crn)?.let { personMatch -> - val matchedPrisoner = personMatch.match - val matchingSentence = personMatch.person.events.filter { - it.disposal?.custody != null && matchedPrisoner?.sentenceStartDate?.withinDays(it.disposal.startDate) == true - } - return when { - matchedPrisoner != null -> CompletedMatch.Successful( - personMatch, - matchingSentence.firstOrNull()?.disposal?.custody - ) - - else -> CompletedMatch.Unsuccessful( - personMatch, - personMatch.person.events.mapNotNull { it.disposal?.startDate } - ) - } - } - } catch (e: Exception) { - telemetryService.trackEvent("MatchingException", mapOf("crn" to crn, "exception" to (e.message ?: "Unknown"))) - throw e - } - - private fun logToTelemetry(completedMatch: CompletedMatch, trialOnly: Boolean) { - when (completedMatch) { - is CompletedMatch.Successful -> telemetryService.trackEvent( - "SuccessfulMatch", - completedMatch.telemetry() + ("dryRun" to trialOnly.toString()) - ) - - is CompletedMatch.Unsuccessful -> telemetryService.trackEvent( - "UnsuccessfulMatch", - completedMatch.telemetry() + ("dryRun" to trialOnly.toString()) - ) - } - } -} - -sealed interface CompletedMatch { - val personMatch: PersonMatch - - data class Successful(override val personMatch: PersonMatch, val custody: Custody?) : CompletedMatch - - data class Unsuccessful(override val personMatch: PersonMatch, val sentenceDates: List) : - CompletedMatch -} - -fun CompletedMatch.sharedTelemetry(): Map = listOfNotNull( - "crn" to personMatch.person.crn, - personMatch.person.nomsNumber?.let { "existingNomsId" to it }, - "custodialEvents" to personMatch.person.events.count { it.disposal?.custody != null }.toString(), - personMatch.match?.prisonerNumber?.let { "matchedNomsId" to it }, - personMatch.match?.bookingNumber?.let { "matchedBookingNumber" to it }, - personMatch.match?.sentenceStartDate?.let { "matchedSentenceDate" to it.format(DateTimeFormatter.ISO_DATE) } -).toMap() - -fun CompletedMatch.Successful.telemetry(): Map = - sharedTelemetry() + - listOfNotNull(custody?.disposal?.startDate?.let { "sentenceDate" to it.format(DateTimeFormatter.ISO_DATE) }).toMap() - -fun CompletedMatch.Unsuccessful.telemetry(): Map = - sharedTelemetry() + ("sentenceDates" to sentenceDates.joinToString { it.format(DateTimeFormatter.ISO_DATE) }) + - personMatch.matches.telemetry() + personMatch.potentialMatches.telemetry() - -fun List.telemetry(): Map = associate { potential -> - val potentials = potential.matches.filter { it.type != ComponentMatch.MatchType.MATCH } - potential.prisoner.prisonerNumber to potentials.joinToString(separator = ", ") { "${it.name()}:${it.type}" } -} \ No newline at end of file diff --git a/projects/prison-identifier-and-delius/src/main/resources/application.yml b/projects/prison-identifier-and-delius/src/main/resources/application.yml index 7cba987ff7..195aed2f72 100644 --- a/projects/prison-identifier-and-delius/src/main/resources/application.yml +++ b/projects/prison-identifier-and-delius/src/main/resources/application.yml @@ -50,6 +50,7 @@ spring: messaging: consumer.queue: prison-identifier producer.queue: prison-identifier + producer.topic: domain-events seed.database: true context.initializer.classes: uk.gov.justice.digital.hmpps.wiremock.WireMockInitialiser @@ -58,8 +59,9 @@ jwt.authorities: - ROLE_PROBATION_API__PRISON_IDENTIFIER__UPDATE integrations: - prisoner-search: - url: http://localhost:${wiremock.port} + prison-api.url: http://localhost:${wiremock.port}/prison-api + prisoner-search.url: http://localhost:${wiremock.port}/prisoner-search + probation-search.url: http://localhost:${wiremock.port}/probation-search oauth2: client-id: prison-identifier-and-delius