Skip to content

Commit 8407425

Browse files
1 parent b3fdb81 commit 8407425

File tree

16 files changed

+319
-108
lines changed

16 files changed

+319
-108
lines changed

projects/prison-case-notes-to-probation/deploy/values-dev.yml

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ generic-service:
1010
LOGGING_LEVEL_COM_AMAZON_SQS: DEBUG
1111
LOGGING_LEVEL_UK_GOV_DIGITAL_JUSTICE_HMPPS: DEBUG
1212
SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_HMPPS-AUTH_TOKEN-URI: http://hmpps-auth.hmpps-auth-dev.svc.cluster.local/auth/oauth/token
13+
INTEGRATIONS_PRISON_CASE_NOTES_BASE_URL: https://dev.offender-case-notes.service.justice.gov.uk
14+
1315

1416
generic-prometheus-alerts:
1517
businessHoursOnly: true

projects/prison-case-notes-to-probation/deploy/values-preprod.yml

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ generic-service:
88
env:
99
SENTRY_ENVIRONMENT: preprod
1010
SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_HMPPS-AUTH_TOKEN-URI: http://hmpps-auth.hmpps-auth-preprod.svc.cluster.local/auth/oauth/token
11+
INTEGRATIONS_PRISON_CASE_NOTES_BASE_URL: https://preprod.offender-case-notes.service.justice.gov.uk
1112

1213
generic-prometheus-alerts:
1314
businessHoursOnly: true

projects/prison-case-notes-to-probation/deploy/values-prod.yml

+1
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ generic-service:
55
env:
66
SENTRY_ENVIRONMENT: prod
77
SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_HMPPS-AUTH_TOKEN-URI: http://hmpps-auth.hmpps-auth-prod.svc.cluster.local/auth/oauth/token
8+
INTEGRATIONS_PRISON_CASE_NOTES_BASE_URL: https://offender-case-notes.service.justice.gov.uk

projects/prison-case-notes-to-probation/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/CaseNotesDataLoader.kt

+1
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ class CaseNotesDataLoader(
7575
StaffGenerator.DEFAULT = staffRepository.save(StaffGenerator.DEFAULT)
7676

7777
offenderRepository.save(OffenderGenerator.DEFAULT)
78+
offenderRepository.save(OffenderGenerator.NEW_IDENTIFIER)
7879

7980
eventRepository.save(EventGenerator.CUSTODIAL_EVENT)
8081
disposalTypeRepository.save(EventGenerator.CUSTODIAL_EVENT.disposal!!.disposalType)

projects/prison-case-notes-to-probation/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/CaseNoteMessageGenerator.kt

+1
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ object CaseNoteMessageGenerator {
88
val NEW_TO_DELIUS: HmppsDomainEvent = ResourceLoader.message("case-note-new-to-delius")
99
val NOT_FOUND: HmppsDomainEvent = ResourceLoader.message("case-note-not-found")
1010
val RESETTLEMENT_PASSPORT: HmppsDomainEvent = ResourceLoader.message("resettlement-passport-casenote")
11+
val NOMS_NUMBER_ADDED: HmppsDomainEvent = ResourceLoader.message("noms-number-added")
1112
}

projects/prison-case-notes-to-probation/src/dev/kotlin/uk/gov/justice/digital/hmpps/data/generator/OffenderGenerator.kt

+2
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,7 @@ import uk.gov.justice.digital.hmpps.integrations.delius.entity.Offender
44

55
object OffenderGenerator {
66
const val EXISTING_OFFENDER_ID = "AA0001A"
7+
const val NEW_PRISON_IDENTIFIER = "A4578BX"
78
val DEFAULT = Offender(IdGenerator.getAndIncrement(), EXISTING_OFFENDER_ID)
9+
val NEW_IDENTIFIER = Offender(IdGenerator.getAndIncrement(), NEW_PRISON_IDENTIFIER)
810
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"Type": "Notification",
3+
"Message": "{\"eventType\":\"probation-case.prison-identifier.added\",\"version\":1,\"occurredAt\":\"2025-01-15T09:47:39+01:00\",\"publishedAt\":\"2025-01-15T09:47:39+01:00\",\"description\":\"A prisoner identifier has been added\",\"personReference\":{\"identifiers\":[{\"type\":\"NOMS\",\"value\":\"A4578BX\"}]}}",
4+
"Timestamp": "2025-01-15T09:47:39+01:00",
5+
"MessageAttributes": {
6+
"eventType": {
7+
"Type": "String",
8+
"Value": "probation-case.prison-identifier.added"
9+
}
10+
}
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
{
2+
"content": [
3+
{
4+
"caseNoteId": "ff0cfa55-3193-41c9-9332-7766c03a06ff",
5+
"eventId": 77701,
6+
"offenderIdentifier": "A4578BX",
7+
"type": "GEN",
8+
"subType": "OSE",
9+
"creationDateTime": "2025-01-12T11:22:33+01:00",
10+
"occurrenceDateTime": "2025-01-11T10:11:22+01:00",
11+
"authorName": "Some Name",
12+
"text": "note content",
13+
"locationId": "LEI",
14+
"amendments": []
15+
},
16+
{
17+
"caseNoteId": "2dd32070-4af0-4d3c-ad8d-5108a43a9322",
18+
"eventId": 77702,
19+
"offenderIdentifier": "A4578BX",
20+
"type": "ALERT",
21+
"subType": "ACTIVE",
22+
"creationDateTime": "2025-01-12T11:22:33+01:00",
23+
"occurrenceDateTime": "2025-01-11T10:11:22+01:00",
24+
"authorName": "Some Name",
25+
"text": "note content",
26+
"locationId": "LEI",
27+
"amendments": []
28+
},
29+
{
30+
"caseNoteId": "7f283166-2c7e-404e-9b0c-fa21cfbd338e",
31+
"eventId": 77704,
32+
"offenderIdentifier": "A4578BX",
33+
"type": "KA",
34+
"subType": "KS",
35+
"creationDateTime": "2025-01-12T11:22:33+01:00",
36+
"occurrenceDateTime": "2025-01-11T10:11:22+01:00",
37+
"authorName": "Some Name",
38+
"text": "note content",
39+
"locationId": "LEI",
40+
"amendments": []
41+
},
42+
{
43+
"caseNoteId": "d9e02743-3e8a-4ab8-923b-01d4214afeaf",
44+
"eventId": 77705,
45+
"offenderIdentifier": "A4578BX",
46+
"type": "PRISON",
47+
"subType": "RELEASE",
48+
"creationDateTime": "2025-01-12T11:22:33+01:00",
49+
"occurrenceDateTime": "2025-01-11T10:11:22+01:00",
50+
"authorName": "Some Name",
51+
"text": "note content",
52+
"locationId": "LEI",
53+
"amendments": []
54+
},
55+
{
56+
"caseNoteId": "bdc8b4af-01a3-44b5-9160-23c4d289f9a2",
57+
"eventId": 77703,
58+
"offenderIdentifier": "A4578BX",
59+
"type": "TRANSFER",
60+
"subType": "FROMTOL",
61+
"creationDateTime": "2025-01-12T11:22:33+01:00",
62+
"occurrenceDateTime": "2025-01-11T10:11:22+01:00",
63+
"authorName": "Some Name",
64+
"text": "note content",
65+
"locationId": "TRN",
66+
"amendments": []
67+
}
68+
]
69+
}

projects/prison-case-notes-to-probation/src/dev/resources/simulations/mappings/get-case-note.json

+14
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,20 @@
5757
"developerMessage": "Resource with id [AA0001A] not found."
5858
}
5959
}
60+
},
61+
{
62+
"request": {
63+
"method": "POST",
64+
"url": "/search/case-notes/A4578BX"
65+
},
66+
"response": {
67+
"headers": {
68+
"Accept": "application/json",
69+
"Content-Type": "application/json"
70+
},
71+
"status": 200,
72+
"bodyFileName": "migrate-for-new-identifier.json"
73+
}
6074
}
6175
]
6276
}

projects/prison-case-notes-to-probation/src/integrationTest/kotlin/uk/gov/justice/digital/hmpps/CaseNotesIntegrationTest.kt

+23
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import uk.gov.justice.digital.hmpps.audit.repository.AuditedInteractionRepositor
2121
import uk.gov.justice.digital.hmpps.data.generator.*
2222
import uk.gov.justice.digital.hmpps.datetime.DeliusDateTimeFormatter
2323
import uk.gov.justice.digital.hmpps.integrations.delius.repository.CaseNoteRepository
24+
import uk.gov.justice.digital.hmpps.integrations.delius.repository.OffenderRepository
2425
import uk.gov.justice.digital.hmpps.integrations.delius.repository.StaffRepository
2526
import uk.gov.justice.digital.hmpps.messaging.HmppsChannelManager
2627
import uk.gov.justice.digital.hmpps.telemetry.TelemetryService
@@ -32,6 +33,9 @@ const val CASE_NOTE_MERGED = "CaseNoteMerged"
3233
@SpringBootTest
3334
class CaseNotesIntegrationTest {
3435

36+
@Autowired
37+
private lateinit var offenderRepository: OffenderRepository
38+
3539
@Value("\${messaging.consumer.queue}")
3640
private lateinit var queueName: String
3741

@@ -167,4 +171,23 @@ class CaseNotesIntegrationTest {
167171
val staff = staffRepository.findById(saved.staffId).orElseThrow()
168172
assertThat(staff.code, equalTo("${ProbationAreaGenerator.DEFAULT.code}B001"))
169173
}
174+
175+
@Test
176+
fun `migrate case notes succesfully when noms number added`() {
177+
val offender = requireNotNull(offenderRepository.findByNomsIdAndSoftDeletedIsFalse("A4578BX"))
178+
val originals = caseNoteRepository.findAll().filter { it.offenderId == offender.id }
179+
assert(originals.isEmpty())
180+
181+
channelManager.getChannel(queueName).publishAndWait(
182+
prepMessage(CaseNoteMessageGenerator.NOMS_NUMBER_ADDED, wireMockserver.port())
183+
)
184+
185+
verify(telemetryService).trackEvent(
186+
eq("CaseNotesMigrated"),
187+
eq(mapOf("nomsId" to "A4578BX", "cause" to "probation-case.prison-identifier.added", "count" to "4")),
188+
anyMap()
189+
)
190+
val saved = caseNoteRepository.findAll().filter { it.offenderId == offender.id }
191+
assertThat(saved.size, equalTo(4))
192+
}
170193
}
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,44 @@
11
package uk.gov.justice.digital.hmpps.integrations.prison
22

3+
import org.springframework.web.bind.annotation.RequestBody
34
import org.springframework.web.service.annotation.GetExchange
5+
import org.springframework.web.service.annotation.PostExchange
46
import java.net.URI
7+
import java.time.LocalDateTime
58

69
interface PrisonCaseNotesClient {
710
@GetExchange
811
fun getCaseNote(baseUrl: URI): PrisonCaseNote?
12+
13+
@PostExchange
14+
fun searchCaseNotes(
15+
uri: URI,
16+
@RequestBody searchCaseNotes: SearchCaseNotes
17+
): CaseNotesResults
18+
}
19+
20+
data class SearchCaseNotes(
21+
val typeSubTypes: Set<TypeSubTypeRequest>,
22+
val occurredFrom: LocalDateTime? = null,
23+
val occurredTo: LocalDateTime? = null,
24+
val includeSensitive: Boolean = true,
25+
val page: Int = 1,
26+
val size: Int = Int.MAX_VALUE,
27+
val sort: String = "occurredAt,desc",
28+
) {
29+
companion object {
30+
val TYPES_OF_INTEREST = setOf(
31+
TypeSubTypeRequest("PRISON", setOf("RELEASE")),
32+
TypeSubTypeRequest("TRANSFER", setOf("FROMTOL")),
33+
TypeSubTypeRequest("GEN", setOf("OSE")),
34+
TypeSubTypeRequest("ALERT"),
35+
TypeSubTypeRequest("OMIC"),
36+
TypeSubTypeRequest("OMIC_OPD"),
37+
TypeSubTypeRequest("KA"),
38+
)
39+
}
940
}
41+
42+
data class TypeSubTypeRequest(val type: String, val subTypes: Set<String> = setOf())
43+
44+
data class CaseNotesResults(val content: List<PrisonCaseNote>)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package uk.gov.justice.digital.hmpps.messaging
2+
3+
import org.springframework.http.HttpStatus
4+
import org.springframework.stereotype.Service
5+
import org.springframework.web.client.HttpStatusCodeException
6+
import uk.gov.justice.digital.hmpps.datetime.DeliusDateTimeFormatter
7+
import uk.gov.justice.digital.hmpps.exceptions.OffenderNotFoundException
8+
import uk.gov.justice.digital.hmpps.integrations.delius.service.DeliusService
9+
import uk.gov.justice.digital.hmpps.integrations.prison.PrisonCaseNote
10+
import uk.gov.justice.digital.hmpps.integrations.prison.PrisonCaseNoteFilters
11+
import uk.gov.justice.digital.hmpps.integrations.prison.PrisonCaseNotesClient
12+
import uk.gov.justice.digital.hmpps.integrations.prison.toDeliusCaseNote
13+
import uk.gov.justice.digital.hmpps.message.HmppsDomainEvent
14+
import uk.gov.justice.digital.hmpps.telemetry.TelemetryService
15+
import java.net.URI
16+
17+
@Service
18+
class CaseNotePublished(
19+
private val prisonCaseNotesClient: PrisonCaseNotesClient,
20+
private val deliusService: DeliusService,
21+
private val telemetryService: TelemetryService,
22+
) {
23+
fun handle(event: HmppsDomainEvent) {
24+
val caseNoteId = event.additionalInformation["caseNoteId"]
25+
if (caseNoteId == null) {
26+
telemetryService.trackEvent(
27+
"MissingCaseNoteId",
28+
mapOf(
29+
"eventType" to event.eventType,
30+
"nomsNumber" to event.personReference.findNomsNumber()!!
31+
)
32+
)
33+
return
34+
}
35+
36+
val prisonCaseNote = try {
37+
prisonCaseNotesClient.getCaseNote(URI.create(event.detailUrl!!))
38+
} catch (ex: HttpStatusCodeException) {
39+
when (ex.statusCode) {
40+
HttpStatus.NOT_FOUND -> {
41+
telemetryService.trackEvent("CaseNoteNotFound", mapOf("detailUrl" to event.detailUrl!!))
42+
return
43+
}
44+
45+
else -> throw ex
46+
}
47+
}
48+
49+
val reasonToIgnore: Lazy<String?> = lazy {
50+
PrisonCaseNoteFilters.filters.firstOrNull { it.predicate.invoke(prisonCaseNote!!) }?.reason
51+
}
52+
53+
if (prisonCaseNote == null || reasonToIgnore.value != null) {
54+
val reason = if (prisonCaseNote == null) {
55+
"case note was not able to be retrieved"
56+
} else {
57+
reasonToIgnore.value!!
58+
}
59+
telemetryService.trackEvent(
60+
"CaseNoteIgnored",
61+
(prisonCaseNote?.properties() ?: emptyMap()) + ("reason" to reason)
62+
)
63+
return
64+
}
65+
66+
try {
67+
deliusService.mergeCaseNote(prisonCaseNote.toDeliusCaseNote(event.occurredAt))
68+
telemetryService.trackEvent("CaseNoteMerged", prisonCaseNote.properties())
69+
} catch (e: Exception) {
70+
telemetryService.trackEvent(
71+
"CaseNoteMergeFailed",
72+
prisonCaseNote.properties() + ("exception" to (e.message ?: ""))
73+
)
74+
if (e !is OffenderNotFoundException) throw e
75+
}
76+
}
77+
78+
private fun PrisonCaseNote.properties() = mapOf(
79+
"caseNoteId" to id,
80+
"type" to type,
81+
"subType" to subType,
82+
"eventId" to eventId.toString(),
83+
"created" to DeliusDateTimeFormatter.format(creationDateTime),
84+
"occurrence" to DeliusDateTimeFormatter.format(occurrenceDateTime),
85+
"location" to locationId
86+
)
87+
}

0 commit comments

Comments
 (0)