Skip to content

Commit 711bc0b

Browse files
chiaramapellimtpopey2700
and
popey2700
authored
PND Alerts endpoint (#437)
* Create PND Alert endpoint * Remove regular alerts for PND authorization --------- Co-authored-by: popey2700 <alex.pope@madetech.com>
1 parent b40448c commit 711bc0b

File tree

12 files changed

+272
-3
lines changed

12 files changed

+272
-3
lines changed

openapi.yml

+44
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,50 @@ paths:
340340
NoQueryParametersBadRequestError:
341341
$ref: "#/components/examples/InternalServerError"
342342

343+
/v1/persons/{hmppsId}/alerts/pnd:
344+
get:
345+
tags:
346+
- persons
347+
- alerts
348+
summary: Returns alerts associated with a person.
349+
parameters:
350+
- $ref: "#/components/parameters/HmppsId"
351+
- $ref: "#/components/parameters/Page"
352+
- $ref: "#/components/parameters/PerPage"
353+
responses:
354+
"200":
355+
description: Successfully found alerts for a person with the provided HMPPS ID.
356+
content:
357+
application/json:
358+
schema:
359+
type: object
360+
properties:
361+
data:
362+
type: array
363+
minItems: 0
364+
items:
365+
$ref: "#/components/schemas/Alert"
366+
pagination:
367+
$ref: "#/components/schemas/Pagination"
368+
"404":
369+
description: Failed to find alerts a person with the provided HMPPS ID.
370+
content:
371+
application/json:
372+
schema:
373+
$ref: "#/components/schemas/Error"
374+
examples:
375+
PersonNotFoundError:
376+
$ref: "#/components/examples/PersonNotFoundError"
377+
"500":
378+
description: An upstream service was not responding, so we cannot verify the accuracy of any data we did get.
379+
content:
380+
application/json:
381+
schema:
382+
$ref: "#/components/schemas/Error"
383+
examples:
384+
NoQueryParametersBadRequestError:
385+
$ref: "#/components/examples/InternalServerError"
386+
343387
/v1/persons/{hmppsId}/sentences:
344388
get:
345389
tags:

src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/AlertsController.kt

+16
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,20 @@ class AlertsController(
3636
auditService.createEvent("GET_PERSON_ALERTS", mapOf("hmppsId" to hmppsId))
3737
return response.data.paginateWith(page, perPage)
3838
}
39+
40+
@GetMapping("{encodedHmppsId}/alerts/pnd")
41+
fun getPersonAlertsPND(
42+
@PathVariable encodedHmppsId: String,
43+
@RequestParam(required = false, defaultValue = "1", name = "page") page: Int,
44+
@RequestParam(required = false, defaultValue = "10", name = "perPage") perPage: Int,
45+
): PaginatedResponse<Alert> {
46+
val hmppsId = encodedHmppsId.decodeUrlCharacters()
47+
val response = getAlertsForPersonService.getAlertsForPnd(hmppsId)
48+
49+
if (response.hasError(UpstreamApiError.Type.ENTITY_NOT_FOUND)) {
50+
throw EntityNotFoundException("Could not find person with id: $hmppsId")
51+
}
52+
auditService.createEvent("GET_PERSON_ALERTS_PND", mapOf("hmppsId" to hmppsId))
53+
return response.data.paginateWith(page, perPage)
54+
}
3955
}

src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/models/hmpps/UpstreamApiError.kt

-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps
33
data class UpstreamApiError(val causedBy: UpstreamApi, val type: Type, val description: String? = null) {
44
enum class Type {
55
ENTITY_NOT_FOUND,
6-
ATTRIBUTE_NOT_FOUND,
76
BAD_REQUEST,
87
FORBIDDEN,
98
INTERNAL_SERVER_ERROR,

src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/GetAlertsForPersonService.kt

+39
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import org.springframework.stereotype.Service
55
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.gateways.NomisGateway
66
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Alert
77
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Response
8+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApi
9+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApiError
810

911
@Service
1012
class GetAlertsForPersonService(
@@ -25,4 +27,41 @@ class GetAlertsForPersonService(
2527
errors = nomisAlerts.errors + personResponse.errors,
2628
)
2729
}
30+
31+
fun getAlertsForPnd(hmppsId: String): Response<List<Alert>> {
32+
val personResponse = getPersonService.execute(hmppsId = hmppsId)
33+
val nomisNumber = personResponse.data?.identifiers?.nomisNumber
34+
var nomisAlerts: Response<List<Alert>> = Response(data = emptyList())
35+
36+
if (nomisNumber != null) {
37+
val allNomisAlerts = nomisGateway.getAlertsForPerson(nomisNumber)
38+
val filteredAlerts =
39+
allNomisAlerts.data?.filter {
40+
it.code in
41+
listOf(
42+
"BECTER", "HA", "XA", "XCA", "XEL", "XELH", "XER", "XHT", "XILLENT",
43+
"XIS", "XR", "XRF", "XSA", "HA2", "RCS", "RDV", "RKC", "RPB", "RPC",
44+
"RSS", "RST", "RDP", "REG", "RLG", "ROP", "RRV", "RTP", "RYP", "HS", "SC",
45+
)
46+
}.orEmpty()
47+
if (filteredAlerts.isEmpty()) {
48+
return Response(
49+
data = emptyList(),
50+
errors =
51+
listOf(
52+
UpstreamApiError(
53+
causedBy = UpstreamApi.PRISONER_OFFENDER_SEARCH,
54+
type = UpstreamApiError.Type.ENTITY_NOT_FOUND,
55+
),
56+
),
57+
)
58+
}
59+
nomisAlerts = Response(data = filteredAlerts, errors = allNomisAlerts.errors)
60+
}
61+
62+
return Response(
63+
data = nomisAlerts.data,
64+
errors = nomisAlerts.errors + personResponse.errors,
65+
)
66+
}
2867
}

src/main/resources/application-dev.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ authorisation:
5959
- "/v1/persons"
6060
- "/v1/persons/\\.*+[^/]*$"
6161
- "/v1/persons/.*/addresses"
62-
- "/v1/persons/.*/alerts"
62+
- "/v1/persons/.*/alerts/pnd"
6363
- "/v1/persons/.*/sentences"
6464
- "/v1/persons/.*/sentences/latest-key-dates-and-adjustments"
6565
- "/v1/persons/.*/risks/scores"

src/main/resources/application-local-docker.yml

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ authorisation:
3232
- "/v1/persons/.*/addresses"
3333
- "/v1/persons/.*/offences"
3434
- "/v1/persons/.*/alerts"
35+
- "/v1/persons/.*/alerts/pnd"
3536
- "/v1/persons/.*/sentences"
3637
- "/v1/persons/.*/sentences/latest-key-dates-and-adjustments"
3738
- "/v1/persons/.*/risks/scores"

src/main/resources/application-local.yml

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ authorisation:
3939
- "/v1/persons/.*/addresses"
4040
- "/v1/persons/.*/offences"
4141
- "/v1/persons/.*/alerts"
42+
- "/v1/persons/.*/alerts/pnd"
4243
- "/v1/persons/.*/sentences"
4344
- "/v1/persons/.*/sentences/latest-key-dates-and-adjustments"
4445
- "/v1/persons/.*/risks/scores"

src/main/resources/application-test.yml

+1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ authorisation:
4747
- "/v1/persons/.*/addresses"
4848
- "/v1/persons/.*/offences"
4949
- "/v1/persons/.*/alerts"
50+
- "/v1/persons/.*/alerts/pnd"
5051
- "/v1/persons/.*/sentences"
5152
- "/v1/persons/.*/sentences/latest-key-dates-and-adjustments"
5253
- "/v1/persons/.*/risks/scores"

src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/AlertsControllerTest.kt

+90
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ internal class AlertsControllerTest(
3939
val hmppsId = "9999/11111A"
4040
val encodedHmppsId = URLEncoder.encode(hmppsId, StandardCharsets.UTF_8)
4141
val path = "/v1/persons/$encodedHmppsId/alerts"
42+
val pndPath = "/v1/persons/$encodedHmppsId/alerts/pnd"
4243
val mockMvc = IntegrationAPIMockMvc(springMockMvc)
4344

4445
describe("GET $path") {
@@ -188,5 +189,94 @@ internal class AlertsControllerTest(
188189
)
189190
}
190191
}
192+
describe("GET $pndPath") {
193+
beforeTest {
194+
Mockito.reset(getAlertsForPersonService)
195+
Mockito.reset(auditService)
196+
whenever(getAlertsForPersonService.getAlertsForPnd(hmppsId)).thenReturn(
197+
Response(
198+
data =
199+
listOf(
200+
Alert(
201+
offenderNo = "A1111BB",
202+
type = "B",
203+
typeDescription = "Security again",
204+
code = "BBB",
205+
codeDescription = "For Release",
206+
comment = "IS83",
207+
dateCreated = LocalDate.parse("2022-09-01"),
208+
dateExpired = LocalDate.parse("2023-09-01"),
209+
expired = false,
210+
active = false,
211+
),
212+
),
213+
),
214+
)
215+
}
216+
217+
it("returns a 200 OK status code for PND") {
218+
val result = mockMvc.performAuthorised(pndPath)
219+
result.response.status.shouldBe(HttpStatus.OK.value())
220+
}
221+
222+
it("returns paginated results for PND") {
223+
whenever(getAlertsForPersonService.getAlertsForPnd(hmppsId)).thenReturn(
224+
Response(
225+
data =
226+
List(20) {
227+
Alert(
228+
offenderNo = "B0000ZZ",
229+
type = "Z",
230+
typeDescription = "Threat",
231+
code = "BBB",
232+
codeDescription = "Not For Release",
233+
comment = "IS91",
234+
dateCreated = LocalDate.parse("2022-09-01"),
235+
dateExpired = LocalDate.parse("2023-10-01"),
236+
expired = false,
237+
active = false,
238+
)
239+
},
240+
),
241+
)
242+
243+
val result = mockMvc.performAuthorised("$pndPath?page=1&perPage=10")
244+
245+
result.response.contentAsString.shouldContainJsonKeyValue("$.pagination.page", 1)
246+
result.response.contentAsString.shouldContainJsonKeyValue("$.pagination.totalPages", 2)
247+
}
248+
249+
it("returns an empty list embedded in a JSON object when no alerts are found for PND") {
250+
val hmppsIdForPersonWithNoAlerts = "1111/22334A"
251+
val encodedHmppsIdForPersonWithNoAlerts =
252+
URLEncoder.encode(hmppsIdForPersonWithNoAlerts, StandardCharsets.UTF_8)
253+
val alertPath = "/v1/persons/$encodedHmppsIdForPersonWithNoAlerts/alerts/pnd"
254+
255+
whenever(getAlertsForPersonService.getAlertsForPnd(hmppsIdForPersonWithNoAlerts)).thenReturn(
256+
Response(
257+
data = emptyList(),
258+
),
259+
)
260+
261+
val result = mockMvc.performAuthorised(alertPath)
262+
263+
result.response.contentAsString.shouldContain("\"data\":[]".removeWhitespaceAndNewlines())
264+
}
265+
266+
it("logs audit for PND") {
267+
mockMvc.performAuthorised(pndPath)
268+
269+
verify(
270+
auditService,
271+
VerificationModeFactory.times(1),
272+
).createEvent("GET_PERSON_ALERTS_PND", mapOf("hmppsId" to hmppsId))
273+
}
274+
275+
it("gets the alerts for PND for a person with the matching ID") {
276+
mockMvc.performAuthorised(pndPath)
277+
278+
verify(getAlertsForPersonService, VerificationModeFactory.times(1)).getAlertsForPnd(hmppsId)
279+
}
280+
}
191281
},
192282
)

src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/services/GetAlertsForPersonServiceTest.kt

+45-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package uk.gov.justice.digital.hmpps.hmppsintegrationapi.services
22

33
import io.kotest.core.spec.style.DescribeSpec
44
import io.kotest.matchers.collections.shouldHaveSize
5+
import io.kotest.matchers.shouldBe
56
import org.mockito.Mockito
67
import org.mockito.internal.verification.VerificationModeFactory
78
import org.mockito.kotlin.verify
@@ -30,7 +31,8 @@ internal class GetAlertsForPersonServiceTest(
3031
val hmppsId = "1234/56789B"
3132
val prisonerNumber = "Z99999ZZ"
3233
val deliusCrn = "X777776"
33-
val alert = Alert()
34+
val alert = Alert(code = "XA", codeDescription = "Test Alert XA")
35+
val nonMatchingAlert = Alert(code = "INVALID", codeDescription = "Invalid Alert")
3436

3537
val person =
3638
Person(firstName = "Qui-gon", lastName = "Jin", identifiers = Identifiers(nomisNumber = prisonerNumber, deliusCrn = deliusCrn))
@@ -47,6 +49,7 @@ internal class GetAlertsForPersonServiceTest(
4749
data =
4850
listOf(
4951
alert,
52+
nonMatchingAlert,
5053
),
5154
),
5255
)
@@ -108,5 +111,46 @@ internal class GetAlertsForPersonServiceTest(
108111
val response = getAlertsForPersonService.execute(hmppsId)
109112
response.errors.shouldHaveSize(1)
110113
}
114+
115+
it("records errors when it cannot find PND alerts for a person") {
116+
whenever(nomisGateway.getAlertsForPerson(id = prisonerNumber)).thenReturn(
117+
Response(
118+
data = emptyList(),
119+
errors =
120+
listOf(
121+
UpstreamApiError(
122+
causedBy = UpstreamApi.NOMIS,
123+
type = UpstreamApiError.Type.ENTITY_NOT_FOUND,
124+
),
125+
),
126+
),
127+
)
128+
129+
val response = getAlertsForPersonService.getAlertsForPnd(hmppsId)
130+
response.errors.shouldHaveSize(1)
131+
}
132+
133+
it("returns PND filtered data") {
134+
val response = getAlertsForPersonService.getAlertsForPnd(hmppsId)
135+
136+
response.data.shouldHaveSize(1)
137+
response.data[0].code shouldBe "XA"
138+
response.data[0].codeDescription shouldBe "Test Alert XA"
139+
}
140+
141+
it("returns an error when the alert code is not in the allowed list") {
142+
whenever(personService.execute(hmppsId = deliusCrn)).thenReturn(Response(person))
143+
whenever(personService.execute(hmppsId = hmppsId)).thenReturn(Response(person))
144+
whenever(nomisGateway.getAlertsForPerson(prisonerNumber)).thenReturn(
145+
Response(
146+
data = listOf(nonMatchingAlert),
147+
),
148+
)
149+
150+
val response = getAlertsForPersonService.getAlertsForPnd(hmppsId)
151+
152+
response.errors.shouldHaveSize(1)
153+
response.data.shouldHaveSize(0)
154+
}
111155
},
112156
)

src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/smoke/AuthoriseConfigTest.kt

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class AuthoriseConfigTest : DescribeSpec(
2727
"/v1/persons/.*/addresses",
2828
"/v1/persons/.*/offences",
2929
"/v1/persons/.*/alerts",
30+
"/v1/persons/.*/alerts/pnd",
3031
"/v1/persons/.*/sentences",
3132
"/v1/persons/.*/sentences/latest-key-dates-and-adjustments",
3233
"/v1/persons/.*/risks/scores",

src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/smoke/person/AlertsSmokeTest.kt

+33
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,39 @@ class AlertsSmokeTest : DescribeSpec(
2020
it("returns alerts for a person") {
2121
val response = httpClient.performAuthorised(basePath)
2222

23+
response.statusCode().shouldBe(HttpStatus.OK.value())
24+
response.body().shouldEqualJson(
25+
"""
26+
{
27+
"data": [
28+
{
29+
"offenderNo": "G3878UK",
30+
"type": "X",
31+
"typeDescription": "Security",
32+
"code": "XER",
33+
"codeDescription": "Escape Risk",
34+
"comment": "Profession lock pick.",
35+
"dateCreated": "2019-08-20",
36+
"dateExpired": "2020-08-20",
37+
"expired": true,
38+
"active": false
39+
}
40+
],
41+
"pagination": {
42+
"isLastPage": true,
43+
"count": 1,
44+
"page": 1,
45+
"perPage": 10,
46+
"totalCount": 1,
47+
"totalPages": 1
48+
}
49+
}
50+
""".removeWhitespaceAndNewlines(),
51+
)
52+
}
53+
it("returns PND alerts for a person") {
54+
val response = httpClient.performAuthorised("$basePath/pnd")
55+
2356
response.statusCode().shouldBe(HttpStatus.OK.value())
2457
response.body().shouldEqualJson(
2558
"""

0 commit comments

Comments
 (0)