From a1dc4e1f0476ffdeb38f5d78c6672245c889cf86 Mon Sep 17 00:00:00 2001 From: "Joseph.Dundon" Date: Thu, 12 Dec 2024 16:26:50 +0000 Subject: [PATCH] PI-2670 - Feature flag to toggle data creation - Adds a feature flag that when enabled will save records to the database, and when disabled will not save records to the database - Expanded telemetry logging to include further details about record creation --- .../justice/digital/hmpps/IntegrationTest.kt | 22 ++- .../digital/hmpps/messaging/Handler.kt | 43 +++- .../digital/hmpps/service/PersonService.kt | 186 ++++++++++-------- .../digital/hmpps/messaging/HandlerTest.kt | 115 ++++++++--- 4 files changed, 245 insertions(+), 121 deletions(-) diff --git a/projects/common-platform-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/IntegrationTest.kt b/projects/common-platform-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/IntegrationTest.kt index 7b85be9565..f49cbde64e 100644 --- a/projects/common-platform-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/IntegrationTest.kt +++ b/projects/common-platform-and-delius/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/IntegrationTest.kt @@ -18,8 +18,10 @@ import org.springframework.boot.test.mock.mockito.SpyBean import uk.gov.justice.digital.hmpps.audit.entity.AuditedInteraction import uk.gov.justice.digital.hmpps.audit.service.AuditedInteractionService import uk.gov.justice.digital.hmpps.data.generator.MessageGenerator +import uk.gov.justice.digital.hmpps.flags.FeatureFlags import uk.gov.justice.digital.hmpps.integrations.delius.audit.BusinessInteractionCode import uk.gov.justice.digital.hmpps.integrations.delius.entity.Person +import uk.gov.justice.digital.hmpps.integrations.delius.entity.PersonManagerRepository import uk.gov.justice.digital.hmpps.integrations.delius.entity.PersonRepository import uk.gov.justice.digital.hmpps.integrations.delius.entity.ReferenceData import uk.gov.justice.digital.hmpps.integrations.delius.person.entity.PersonAddress @@ -63,12 +65,19 @@ internal class IntegrationTest { @SpyBean lateinit var addressRepository: PersonAddressRepository + @SpyBean + lateinit var personManagerRepository: PersonManagerRepository + @SpyBean lateinit var personService: PersonService + @MockBean + private lateinit var featureFlags: FeatureFlags + @BeforeEach fun setup() { doReturn("A111111").whenever(personService).generateCrn() + whenever(featureFlags.enabled("common-platform-record-creation-toggle")).thenReturn(true) } @Test @@ -319,10 +328,21 @@ internal class IntegrationTest { }) } + @Test + fun `When feature flag is disabled no records are inserted`() { + whenever(featureFlags.enabled("common-platform-record-creation-toggle")).thenReturn(false) + val notification = Notification(message = MessageGenerator.COMMON_PLATFORM_EVENT) + channelManager.getChannel(queueName).publishAndWait(notification) + verify(personService).insertPerson(any(), any()) + thenNoRecordsAreInserted() + verify(auditedInteractionService, Mockito.never()) + .createAuditedInteraction(any(), any(), eq(AuditedInteraction.Outcome.FAIL), any(), anyOrNull()) + } + private fun thenNoRecordsAreInserted() { - verify(personService, never()).insertAddress(any()) verify(addressRepository, never()).save(any()) verify(personRepository, never()).save(any()) + verify(personManagerRepository, never()).save(any()) verify(auditedInteractionService, Mockito.never()) .createAuditedInteraction(any(), any(), eq(AuditedInteraction.Outcome.SUCCESS), any(), anyOrNull()) } diff --git a/projects/common-platform-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/messaging/Handler.kt b/projects/common-platform-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/messaging/Handler.kt index 06d2a53e3f..e5d9220d9c 100644 --- a/projects/common-platform-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/messaging/Handler.kt +++ b/projects/common-platform-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/messaging/Handler.kt @@ -6,6 +6,7 @@ import org.openfolder.kotlinasyncapi.annotation.channel.Message import org.openfolder.kotlinasyncapi.annotation.channel.Publish import org.springframework.stereotype.Component import uk.gov.justice.digital.hmpps.converter.NotificationConverter +import uk.gov.justice.digital.hmpps.flags.FeatureFlags import uk.gov.justice.digital.hmpps.integrations.client.ProbationMatchRequest import uk.gov.justice.digital.hmpps.integrations.client.ProbationSearchClient import uk.gov.justice.digital.hmpps.message.Notification @@ -20,7 +21,8 @@ class Handler( private val notifier: Notifier, private val telemetryService: TelemetryService, private val personService: PersonService, - private val probationSearchClient: ProbationSearchClient + private val probationSearchClient: ProbationSearchClient, + private val featureFlags: FeatureFlags ) : NotificationHandler { @Publish(messages = [Message(title = "COMMON_PLATFORM_HEARING", payload = Schema(CommonPlatformHearing::class))]) @@ -41,8 +43,6 @@ class Handler( return } - val courtCode = notification.message.hearing.courtCentre.code - defendants.forEach { defendant -> val matchRequest = defendant.toProbationMatchRequest() ?: return@forEach @@ -54,19 +54,44 @@ class Handler( } // Insert each defendant as a person record - val savedEntities = personService.insertPerson(defendant, courtCode) - - notifier.caseCreated(savedEntities.person) - savedEntities.address?.let { notifier.addressCreated(it) } + val savedEntities = personService.insertPerson(defendant, notification.message.hearing.courtCentre.code) + + val eventName = if (featureFlags.enabled("common-platform-record-creation-toggle")) { + notifier.caseCreated(savedEntities.person) + savedEntities.address?.let { notifier.addressCreated(it) } + "PersonCreated" + } else { + "SimulatedPersonCreated" + } telemetryService.trackEvent( - "PersonCreated", mapOf( + eventName, + mapOf( "hearingId" to notification.message.hearing.id, "CRN" to savedEntities.person.crn, "personId" to savedEntities.person.id.toString(), + "firstName" to savedEntities.person.forename, + "surname" to savedEntities.person.surname, + "dob" to savedEntities.person.dateOfBirth.toString(), + "genderCode" to savedEntities.person.gender.description, + "pnc" to savedEntities.person.pncNumber.toString(), "personManagerId" to savedEntities.personManager.id.toString(), + "allocationDate" to savedEntities.personManager.allocationDate.toString(), + "allocationReason" to savedEntities.personManager.allocationReason.description, + "providerCode" to savedEntities.personManager.provider.code, + "teamCode" to savedEntities.personManager.team.code, + "staffCode" to savedEntities.personManager.staff.code, "equalityId" to savedEntities.equality.id.toString(), - "addressId" to savedEntities.address?.id.toString() + "addressId" to savedEntities.address?.id.toString(), + "buildingName" to savedEntities.address?.buildingName.toString(), + "addressNumber" to savedEntities.address?.addressNumber.toString(), + "streetName" to savedEntities.address?.streetName.toString(), + "town" to savedEntities.address?.town.toString(), + "district" to savedEntities.address?.district.toString(), + "county" to savedEntities.address?.county.toString(), + "type" to savedEntities.address?.type?.description.toString(), + "status" to savedEntities.address?.status?.description.toString(), + "postcode" to savedEntities.address?.postcode.toString(), ) ) } diff --git a/projects/common-platform-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/service/PersonService.kt b/projects/common-platform-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/service/PersonService.kt index 0fd616c676..9e75d0c272 100644 --- a/projects/common-platform-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/service/PersonService.kt +++ b/projects/common-platform-and-delius/src/main/kotlin/uk/gov/justice/digital/hmpps/service/PersonService.kt @@ -4,6 +4,7 @@ import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import uk.gov.justice.digital.hmpps.audit.service.AuditableService import uk.gov.justice.digital.hmpps.audit.service.AuditedInteractionService +import uk.gov.justice.digital.hmpps.flags.FeatureFlags import uk.gov.justice.digital.hmpps.integrations.client.OsClient import uk.gov.justice.digital.hmpps.integrations.client.OsPlacesResponse import uk.gov.justice.digital.hmpps.integrations.delius.audit.BusinessInteractionCode @@ -27,113 +28,138 @@ class PersonService( private val staffRepository: StaffRepository, private val referenceDataRepository: ReferenceDataRepository, private val personAddressRepository: PersonAddressRepository, - private val osClient: OsClient + private val osClient: OsClient, + private val featureFlags: FeatureFlags ) : AuditableService(auditedInteractionService) { @Transactional fun insertPerson(defendant: Defendant, courtCode: String): InsertPersonResult = - audit(BusinessInteractionCode.INSERT_PERSON) { audit -> + if (featureFlags.enabled("common-platform-record-creation-toggle")) { + audit(BusinessInteractionCode.INSERT_PERSON) { audit -> + val result = insertPersonLogic(defendant, courtCode) + audit["offenderId"] = result.person.id!! + result + } + } else { + insertPersonLogic(defendant, courtCode) + } - val dateOfBirth = defendant.personDefendant?.personDetails?.dateOfBirth - ?: throw IllegalArgumentException("Date of birth not found in message") + fun insertPersonLogic(defendant: Defendant, courtCode: String): InsertPersonResult { + val dateOfBirth = defendant.personDefendant?.personDetails?.dateOfBirth + ?: throw IllegalArgumentException("Date of birth not found in message") - // Under 10 years old validation - dateOfBirth.let { - val age = Period.between(it, LocalDate.now()).years - require(age > 10) { - "Date of birth would indicate person is under ten years old: $it" - } + // Under 10 years old validation + dateOfBirth.let { + val age = Period.between(it, LocalDate.now()).years + require(age > 10) { + "Date of birth would indicate person is under ten years old: $it" } + } - // Person record - val savedPerson = personRepository.save(defendant.toPerson()) - - val courtLinkedProvider = courtRepository.getByOuCode(courtCode).provider - val initialAllocation = referenceDataRepository.initialAllocationReason() - val unallocatedTeam = teamRepository.findByCode(courtLinkedProvider.code + "UAT") - val unallocatedStaff = staffRepository.findByCode(unallocatedTeam.code + "U") - - // Person manager record - val manager = PersonManager( - person = savedPerson, - staff = unallocatedStaff, - team = unallocatedTeam, - provider = courtLinkedProvider, - softDeleted = false, - active = true, - allocationReason = initialAllocation, - staffEmployeeID = unallocatedStaff.id, - trustProviderTeamId = unallocatedTeam.id, - allocationDate = LocalDateTime.of(1900, 1, 1, 0, 0) + // Person record + val savedPerson = if (featureFlags.enabled("common-platform-record-creation-toggle")) { + personRepository.save(defendant.toPerson()) + } else { + defendant.toPerson() + } - ) - val savedManager = personManagerRepository.save(manager) + val courtLinkedProvider = courtRepository.getByOuCode(courtCode).provider + val initialAllocation = referenceDataRepository.initialAllocationReason() + val unallocatedTeam = teamRepository.findByCode(courtLinkedProvider.code + "UAT") + val unallocatedStaff = staffRepository.findByCode(unallocatedTeam.code + "U") + + // Person manager record + val manager = PersonManager( + person = savedPerson, + staff = unallocatedStaff, + team = unallocatedTeam, + provider = courtLinkedProvider, + softDeleted = false, + active = true, + allocationReason = initialAllocation, + staffEmployeeID = unallocatedStaff.id, + trustProviderTeamId = unallocatedTeam.id, + allocationDate = LocalDateTime.of(1900, 1, 1, 0, 0) - // Equality record - val equality = Equality( - id = null, - personId = savedPerson.id!!, - softDeleted = false, - ) + ) - val savedEquality = equalityRepository.save(equality) + val savedManager = if (featureFlags.enabled("common-platform-record-creation-toggle")) { + personManagerRepository.save(manager) + } else { + manager + } - val addressInfo = defendant.personDefendant.personDetails.address - val osPlacesResponse = addressInfo?.takeIf { it.containsInformation() && !it.postcode.isNullOrBlank() } - ?.let { findAddressByFreeText(it) } + // Equality record + val equality = Equality( + id = null, + personId = savedPerson.id ?: 0L, + softDeleted = false, + ) - val deliveryPointAddress = osPlacesResponse?.results?.firstOrNull()?.dpa + val savedEquality = if (featureFlags.enabled("common-platform-record-creation-toggle")) { + equalityRepository.save(equality) + } else { + equality + } - val savedAddress = if (deliveryPointAddress != null) { + val addressInfo = defendant.personDefendant.personDetails.address + val osPlacesResponse = addressInfo?.takeIf { it.containsInformation() && !it.postcode.isNullOrBlank() } + ?.let { findAddressByFreeText(it) } + + val deliveryPointAddress = osPlacesResponse?.results?.firstOrNull()?.dpa + + val savedAddress = if (deliveryPointAddress != null) { + insertAddress( + PersonAddress( + id = null, + start = LocalDate.now(), + status = referenceDataRepository.mainAddressStatus(), + person = savedPerson, + type = referenceDataRepository.awaitingAssessmentAddressType(), + postcode = deliveryPointAddress.postcode, + notes = "UPRN: ${deliveryPointAddress.uprn}", + buildingName = listOfNotNull( + deliveryPointAddress.subBuildingName, + deliveryPointAddress.buildingName + ).joinToString(" "), + addressNumber = deliveryPointAddress.buildingNumber?.toString(), + streetName = deliveryPointAddress.thoroughfareName, + town = deliveryPointAddress.postTown, + district = deliveryPointAddress.localCustodianCodeDescription + ) + ) + } else { + addressInfo?.takeIf { it.containsInformation() }?.let { insertAddress( PersonAddress( id = null, start = LocalDate.now(), status = referenceDataRepository.mainAddressStatus(), person = savedPerson, + postcode = it.postcode, type = referenceDataRepository.awaitingAssessmentAddressType(), - postcode = deliveryPointAddress.postcode, - notes = "UPRN: ${deliveryPointAddress.uprn}", - buildingName = listOfNotNull( - deliveryPointAddress.subBuildingName, - deliveryPointAddress.buildingName - ).joinToString(" "), - addressNumber = deliveryPointAddress.buildingNumber?.toString(), - streetName = deliveryPointAddress.thoroughfareName, - town = deliveryPointAddress.postTown, - district = deliveryPointAddress.localCustodianCodeDescription + streetName = it.address1, + district = it.address2, + town = it.address3, + county = listOfNotNull(it.address4, it.address5).joinToString(", ") ) ) - } else { - addressInfo?.takeIf { it.containsInformation() }?.let { - insertAddress( - PersonAddress( - id = null, - start = LocalDate.now(), - status = referenceDataRepository.mainAddressStatus(), - person = savedPerson, - postcode = it.postcode, - type = referenceDataRepository.awaitingAssessmentAddressType(), - streetName = it.address1, - district = it.address2, - town = it.address3, - county = listOfNotNull(it.address4, it.address5).joinToString(", ") - ) - ) - } } - - audit["offenderId"] = savedPerson.id - InsertPersonResult(savedPerson, savedManager, savedEquality, savedAddress) } - - @Transactional - fun insertAddress(address: PersonAddress): PersonAddress = audit(BusinessInteractionCode.INSERT_ADDRESS) { audit -> - val savedAddress = personAddressRepository.save(address) - audit["addressId"] = address.id!! - savedAddress + return InsertPersonResult(savedPerson, savedManager, savedEquality, savedAddress) } + fun insertAddress(address: PersonAddress): PersonAddress = + if (featureFlags.enabled("common-platform-record-creation-toggle")) { + audit(BusinessInteractionCode.INSERT_ADDRESS) { audit -> + val result = personAddressRepository.save(address) + audit["offenderId"] = result.person.id!! + result + } + } else { + address + } + fun generateCrn(): String { return personRepository.getNextCrn() } diff --git a/projects/common-platform-and-delius/src/test/kotlin/uk/gov/justice/digital/hmpps/messaging/HandlerTest.kt b/projects/common-platform-and-delius/src/test/kotlin/uk/gov/justice/digital/hmpps/messaging/HandlerTest.kt index f58df9bbad..2a9701d1d6 100644 --- a/projects/common-platform-and-delius/src/test/kotlin/uk/gov/justice/digital/hmpps/messaging/HandlerTest.kt +++ b/projects/common-platform-and-delius/src/test/kotlin/uk/gov/justice/digital/hmpps/messaging/HandlerTest.kt @@ -4,16 +4,15 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.mockito.InjectMocks import org.mockito.Mock +import org.mockito.Mockito.anyMap import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.kotlin.any -import org.mockito.kotlin.never -import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever +import org.mockito.kotlin.* import uk.gov.justice.digital.hmpps.converter.NotificationConverter import uk.gov.justice.digital.hmpps.data.generator.MessageGenerator import uk.gov.justice.digital.hmpps.data.generator.PersonAddressGenerator import uk.gov.justice.digital.hmpps.data.generator.PersonGenerator import uk.gov.justice.digital.hmpps.data.generator.PersonManagerGenerator +import uk.gov.justice.digital.hmpps.flags.FeatureFlags import uk.gov.justice.digital.hmpps.integrations.client.* import uk.gov.justice.digital.hmpps.integrations.delius.entity.Equality import uk.gov.justice.digital.hmpps.message.Notification @@ -40,17 +39,14 @@ internal class HandlerTest { @Mock lateinit var notifier: Notifier + @Mock + private lateinit var featureFlags: FeatureFlags + @InjectMocks lateinit var handler: Handler @Test fun `inserts records when probation search match is not found`() { - whenever(probationSearchClient.match(any())).thenReturn( - ProbationMatchResponse( - matches = emptyList(), - matchedBy = "NONE" - ) - ) whenever(personService.insertPerson(any(), any())).thenReturn( InsertPersonResult( person = PersonGenerator.DEFAULT, @@ -60,6 +56,9 @@ internal class HandlerTest { ) ) + probationSearchMatchNotFound() + whenever(featureFlags.enabled("common-platform-record-creation-toggle")).thenReturn(true) + val notification = Notification(message = MessageGenerator.COMMON_PLATFORM_EVENT) handler.handle(notification) verify(telemetryService).notificationReceived(notification) @@ -70,25 +69,9 @@ internal class HandlerTest { @Test fun `does not insert person or address when match is found`() { - val fakeMatchResponse = ProbationMatchResponse( - matches = listOf( - OffenderMatch( - offender = OffenderDetail( - otherIds = IDs(crn = "X123456", pncNumber = "00000000000Z"), - firstName = "Name", - surname = "Name", - dateOfBirth = LocalDate.of(1980, 1, 1) - ) - ) - ), - matchedBy = "PNC" - ) - - whenever(probationSearchClient.match(any())).thenReturn(fakeMatchResponse) - + probationSearchMatchFound() val notification = Notification(message = MessageGenerator.COMMON_PLATFORM_EVENT) handler.handle(notification) - verify(telemetryService).notificationReceived(notification) verify(personService, never()).insertPerson(any(), any()) verify(notifier, never()).caseCreated(any()) @@ -98,9 +81,7 @@ internal class HandlerTest { @Test fun `When defendants with remanded in custody are not found then no inserts occur`() { val notification = Notification(message = MessageGenerator.COMMON_PLATFORM_EVENT_NO_REMAND) - handler.handle(notification) - verify(telemetryService).notificationReceived(notification) verify(personService, never()).insertPerson(any(), any()) verify(notifier, never()).caseCreated(any()) @@ -110,13 +91,85 @@ internal class HandlerTest { @Test fun `When a defendant is missing name or dob then records are not inserted and probation-search is not performed`() { val notification = Notification(message = MessageGenerator.COMMON_PLATFORM_EVENT_NULL_FIELDS) - handler.handle(notification) - verify(telemetryService).notificationReceived(notification) verify(probationSearchClient, never()).match(any()) verify(personService, never()).insertPerson(any(), any()) verify(notifier, never()).caseCreated(any()) verify(notifier, never()).addressCreated(any()) } + + @Test + fun `Person created logged when feature flag enabled`() { + probationSearchMatchNotFound() + + whenever(featureFlags.enabled("common-platform-record-creation-toggle")).thenReturn(true) + whenever(personService.insertPerson(any(), any())).thenReturn( + InsertPersonResult( + person = PersonGenerator.DEFAULT, + personManager = PersonManagerGenerator.DEFAULT, + equality = Equality(id = 1L, personId = 1L, softDeleted = false), + address = PersonAddressGenerator.MAIN_ADDRESS, + ) + ) + + val notification = Notification(message = MessageGenerator.COMMON_PLATFORM_EVENT) + handler.handle(notification) + + verify(telemetryService).notificationReceived(notification) + verify(telemetryService).trackEvent(eq("PersonCreated"), anyMap(), anyMap()) + verify(personService).insertPerson(any(), any()) + verify(notifier).caseCreated(any()) + verify(notifier).addressCreated(any()) + } + + @Test + fun `Simulated person created logged when feature flag disabled`() { + probationSearchMatchNotFound() + + whenever(featureFlags.enabled("common-platform-record-creation-toggle")).thenReturn(false) + whenever(personService.insertPerson(any(), any())).thenReturn( + InsertPersonResult( + person = PersonGenerator.DEFAULT, + personManager = PersonManagerGenerator.DEFAULT, + equality = Equality(id = 1L, personId = 1L, softDeleted = false), + address = PersonAddressGenerator.MAIN_ADDRESS, + ) + ) + + val notification = Notification(message = MessageGenerator.COMMON_PLATFORM_EVENT) + handler.handle(notification) + + verify(telemetryService).notificationReceived(notification) + verify(telemetryService).trackEvent(eq("SimulatedPersonCreated"), anyMap(), anyMap()) + verify(personService).insertPerson(any(), any()) + verify(notifier, never()).caseCreated(any()) + verify(notifier, never()).addressCreated(any()) + } + + private fun probationSearchMatchNotFound() { + whenever(probationSearchClient.match(any())).thenReturn( + ProbationMatchResponse( + matches = emptyList(), + matchedBy = "NONE" + ) + ) + } + + private fun probationSearchMatchFound() { + val fakeMatchResponse = ProbationMatchResponse( + matches = listOf( + OffenderMatch( + offender = OffenderDetail( + otherIds = IDs(crn = "X123456", pncNumber = "00000000000Z"), + firstName = "Name", + surname = "Name", + dateOfBirth = LocalDate.of(1980, 1, 1) + ) + ) + ), + matchedBy = "PNC" + ) + whenever(probationSearchClient.match(any())).thenReturn(fakeMatchResponse) + } }