Skip to content

Commit 3e723c4

Browse files
HMAI-312 - Add prison filtering to case notes (#747)
* Added integration tests * Service changes * Controller changes * Add test to verify filters get passed into service * Added params to integration tests
1 parent e980a36 commit 3e723c4

File tree

7 files changed

+220
-89
lines changed

7 files changed

+220
-89
lines changed

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

+17-8
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,22 @@ import io.swagger.v3.oas.annotations.media.Content
66
import io.swagger.v3.oas.annotations.media.Schema
77
import io.swagger.v3.oas.annotations.responses.ApiResponse
88
import io.swagger.v3.oas.annotations.tags.Tag
9+
import jakarta.validation.ValidationException
910
import org.springframework.beans.factory.annotation.Autowired
1011
import org.springframework.format.annotation.DateTimeFormat
1112
import org.springframework.web.bind.annotation.GetMapping
1213
import org.springframework.web.bind.annotation.PathVariable
14+
import org.springframework.web.bind.annotation.RequestAttribute
1315
import org.springframework.web.bind.annotation.RequestMapping
1416
import org.springframework.web.bind.annotation.RequestParam
1517
import org.springframework.web.bind.annotation.RestController
1618
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.exception.EntityNotFoundException
1719
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.exception.ForbiddenByUpstreamServiceException
18-
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.extensions.decodeUrlCharacters
1920
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.filters.CaseNoteFilter
2021
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.CaseNote
2122
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApi
2223
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApiError
24+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.roleconfig.ConsumerFilters
2325
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.GetCaseNotesForPersonService
2426
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.internal.AuditService
2527
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.util.PaginatedResponse
@@ -33,18 +35,20 @@ class CaseNotesController(
3335
@Autowired val getCaseNoteForPersonService: GetCaseNotesForPersonService,
3436
@Autowired val auditService: AuditService,
3537
) {
36-
@GetMapping("{encodedHmppsId}/case-notes")
38+
@GetMapping("{hmppsId}/case-notes")
3739
@Operation(
3840
summary = "Returns case notes associated with a person.",
41+
description = "<b>Applicable filters</b>: <ul><li>prisons</li></ul>",
3942
responses = [
4043
ApiResponse(responseCode = "200", useReturnTypeSchema = true, description = "Successfully found case notes for a person with the provided HMPPS ID."),
41-
ApiResponse(responseCode = "404", content = [Content(schema = Schema(ref = "#/components/schemas/PersonNotFound"))]),
44+
ApiResponse(responseCode = "400", content = [Content(schema = Schema(ref = "#/components/schemas/BadRequest"))]),
4245
ApiResponse(responseCode = "403", content = [Content(schema = Schema(ref = "#/components/schemas/ForbiddenResponse"))]),
46+
ApiResponse(responseCode = "404", content = [Content(schema = Schema(ref = "#/components/schemas/PersonNotFound"))]),
4347
ApiResponse(responseCode = "500", content = [Content(schema = Schema(ref = "#/components/schemas/InternalServerError"))]),
4448
],
4549
)
4650
fun getCaseNotesForPerson(
47-
@Parameter(description = "A URL-encoded HMPPS identifier", example = "2008%2F0545166T") @PathVariable encodedHmppsId: String,
51+
@Parameter(description = "The HMPPS ID of the person", example = "G2996UX") @PathVariable hmppsId: String,
4852
@Parameter(description = "Filter case notes from this date")
4953
@RequestParam(required = false, name = "startDate")
5054
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
@@ -57,11 +61,15 @@ class CaseNotesController(
5761
@RequestParam(required = false, name = "locationId") locationId: String?,
5862
@RequestParam(required = false, defaultValue = "1", name = "page") page: Int,
5963
@RequestParam(required = false, defaultValue = "10", name = "perPage") perPage: Int,
64+
@RequestAttribute filters: ConsumerFilters?,
6065
): PaginatedResponse<CaseNote> {
61-
val hmppsId = encodedHmppsId.decodeUrlCharacters()
62-
val response = getCaseNoteForPersonService.execute(CaseNoteFilter(hmppsId, startDate, endDate, locationId))
66+
val response = getCaseNoteForPersonService.execute(CaseNoteFilter(hmppsId, startDate, endDate, locationId), filters)
6367

64-
if (response.hasErrorCausedBy(UpstreamApiError.Type.ENTITY_NOT_FOUND, causedBy = UpstreamApi.CASE_NOTES)) {
68+
if (response.hasError(UpstreamApiError.Type.BAD_REQUEST)) {
69+
throw ValidationException("Invalid id: $hmppsId")
70+
}
71+
72+
if (response.hasError(UpstreamApiError.Type.ENTITY_NOT_FOUND)) {
6573
throw EntityNotFoundException("Could not find person with id: $hmppsId")
6674
}
6775

@@ -70,6 +78,7 @@ class CaseNotesController(
7078
}
7179

7280
auditService.createEvent("GET_CASE_NOTES", mapOf("hmppsId" to hmppsId))
73-
return response.data.paginateWith(page, perPage)
81+
82+
return response.data.orEmpty().paginateWith(page, perPage)
7483
}
7584
}

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

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

33
import java.time.LocalDateTime
44

5-
class CaseNoteFilter(
5+
data class CaseNoteFilter(
66
val hmppsId: String,
77
val startDate: LocalDateTime? = null,
88
val endDate: LocalDateTime? = null,

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

+14-8
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,31 @@ import uk.gov.justice.digital.hmpps.hmppsintegrationapi.gateways.CaseNotesGatewa
66
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.filters.CaseNoteFilter
77
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.CaseNote
88
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Response
9+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApi
10+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApiError
11+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.roleconfig.ConsumerFilters
912

1013
@Service
1114
class GetCaseNotesForPersonService(
1215
@Autowired val caseNotesGateway: CaseNotesGateway,
1316
@Autowired val getPersonService: GetPersonService,
1417
) {
15-
fun execute(filter: CaseNoteFilter): Response<List<CaseNote>> {
16-
val personResponse = getPersonService.execute(hmppsId = filter.hmppsId)
17-
val nomisNumber = personResponse.data?.identifiers?.nomisNumber
18+
fun execute(
19+
filter: CaseNoteFilter,
20+
filters: ConsumerFilters?,
21+
): Response<List<CaseNote>?> {
22+
val personResponse = getPersonService.getNomisNumberWithPrisonFilter(filter.hmppsId, filters)
23+
if (personResponse.errors.isNotEmpty()) {
24+
return Response(data = emptyList(), errors = personResponse.errors)
25+
}
1826

19-
var caseNotes: Response<List<CaseNote>> = Response(data = emptyList())
27+
val nomisNumber = personResponse.data?.nomisNumber ?: return Response(data = null, errors = listOf(UpstreamApiError(UpstreamApi.NOMIS, UpstreamApiError.Type.ENTITY_NOT_FOUND)))
2028

21-
if (nomisNumber != null) {
22-
caseNotes = caseNotesGateway.getCaseNotesForPerson(id = nomisNumber, filter)
23-
}
29+
val caseNotes = caseNotesGateway.getCaseNotesForPerson(id = nomisNumber, filter)
2430

2531
return Response(
2632
data = caseNotes.data,
27-
errors = personResponse.errors + caseNotes.errors,
33+
errors = caseNotes.errors,
2834
)
2935
}
3036
}

src/main/resources/globals.yml

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ globals:
1414
- "/v1/persons/.*/visit-orders"
1515
- "/v1/persons/.*/visit/future"
1616
- "/v1/persons/.*/alerts"
17+
- "/v1/persons/.*/case-notes"
1718
- "/v1/persons/.*/name"
1819
- "/v1/persons/.*/cell-location"
1920
- "/v1/persons/.*/risks/categories"

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

+96-59
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,8 @@ import io.kotest.core.spec.style.DescribeSpec
44
import io.kotest.matchers.shouldBe
55
import io.kotest.matchers.string.shouldContain
66
import org.mockito.Mockito
7-
import org.mockito.internal.verification.VerificationModeFactory
8-
import org.mockito.kotlin.any
9-
import org.mockito.kotlin.argThat
107
import org.mockito.kotlin.doThrow
8+
import org.mockito.kotlin.times
119
import org.mockito.kotlin.verify
1210
import org.mockito.kotlin.whenever
1311
import org.springframework.beans.factory.annotation.Autowired
@@ -24,10 +22,10 @@ import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.CaseNote
2422
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Response
2523
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApi
2624
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApiError
25+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.roleconfig.ConsumerFilters
2726
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.GetCaseNotesForPersonService
2827
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.internal.AuditService
29-
import java.net.URLEncoder
30-
import java.nio.charset.StandardCharsets
28+
import java.time.LocalDateTime
3129

3230
@WebMvcTest(controllers = [CaseNotesController::class])
3331
@ActiveProfiles("test")
@@ -37,43 +35,108 @@ class CaseNotesControllerTest(
3735
@MockitoBean val auditService: AuditService,
3836
) : DescribeSpec(
3937
{
40-
val hmppsId = "9999/11111A"
41-
val encodedHmppsId = URLEncoder.encode(hmppsId, StandardCharsets.UTF_8)
42-
val path = "/v1/persons/$encodedHmppsId/case-notes"
38+
val hmppsId = "G2996UX"
39+
val locationId = "MDI"
40+
val startDate: LocalDateTime = LocalDateTime.now()
41+
val endDate: LocalDateTime = LocalDateTime.now()
42+
val path = "/v1/persons/$hmppsId/case-notes?startDate=$startDate&endDate=$endDate&locationId=$locationId"
43+
val caseNoteFilter = CaseNoteFilter(hmppsId, startDate, endDate, locationId)
4344
val mockMvc = IntegrationAPIMockMvc(springMockMvc)
4445
val pageCaseNote =
4546
listOf(
4647
CaseNote(caseNoteId = "abcd1234"),
4748
)
49+
val filters = null
50+
4851
describe("GET $path") {
4952
beforeTest {
5053
Mockito.reset(getCaseNotesForPersonService)
5154
Mockito.reset(auditService)
52-
whenever(getCaseNotesForPersonService.execute(any())).thenReturn(
55+
56+
whenever(getCaseNotesForPersonService.execute(caseNoteFilter, filters)).thenReturn(
5357
Response(
5458
data = pageCaseNote,
5559
errors = emptyList(),
5660
),
5761
)
5862
}
5963

60-
it("returns a 200 OK status code") {
61-
val result = mockMvc.performAuthorised(path)
64+
it("logs audit") {
65+
mockMvc.performAuthorised(path)
6266

63-
result.response.status.shouldBe(HttpStatus.OK.value())
67+
verify(
68+
auditService,
69+
times(1),
70+
).createEvent("GET_CASE_NOTES", mapOf("hmppsId" to hmppsId))
6471
}
6572

66-
it("gets the case notes for a person with the matching ID") {
67-
mockMvc.performAuthorised(path)
73+
it("passes filters into service") {
74+
mockMvc.performAuthorisedWithCN(path, "limited-prisons")
6875

6976
verify(
7077
getCaseNotesForPersonService,
71-
VerificationModeFactory.times(1),
72-
).execute(argThat<CaseNoteFilter> { it -> it.hmppsId == hmppsId })
78+
times(1),
79+
).execute(
80+
caseNoteFilter,
81+
ConsumerFilters(prisons = listOf("XYZ")),
82+
)
83+
}
84+
85+
it("returns the case notes for a person with the matching ID with a 200 status code") {
86+
val result = mockMvc.performAuthorised(path)
87+
result.response.status.shouldBe(HttpStatus.OK.value())
88+
result.response.contentAsString.shouldContain(
89+
"""
90+
{
91+
"data": [
92+
{
93+
"caseNoteId": "abcd1234",
94+
"offenderIdentifier": null,
95+
"type": null,
96+
"typeDescription": null,
97+
"subType": null,
98+
"subTypeDescription": null,
99+
"creationDateTime": null,
100+
"occurrenceDateTime": null,
101+
"text": null,
102+
"locationId": null,
103+
"sensitive": false,
104+
"amendments": []
105+
}
106+
],
107+
"pagination": {
108+
"isLastPage": true,
109+
"count": 1,
110+
"page": 1,
111+
"perPage": 10,
112+
"totalCount": 1,
113+
"totalPages": 1
114+
}
115+
}
116+
""".removeWhitespaceAndNewlines(),
117+
)
118+
}
119+
120+
it("returns a 400 when the upstream service returns bad request") {
121+
whenever(getCaseNotesForPersonService.execute(caseNoteFilter, filters)).thenReturn(
122+
Response(
123+
data = emptyList(),
124+
errors =
125+
listOf(
126+
UpstreamApiError(
127+
type = UpstreamApiError.Type.BAD_REQUEST,
128+
causedBy = UpstreamApi.NOMIS,
129+
),
130+
),
131+
),
132+
)
133+
134+
val result = mockMvc.performAuthorised(path)
135+
result.response.status.shouldBe(HttpStatus.BAD_REQUEST.value())
73136
}
74137

75138
it("returns a 403 when the upstream service provides a 403") {
76-
whenever(getCaseNotesForPersonService.execute(any())).thenReturn(
139+
whenever(getCaseNotesForPersonService.execute(caseNoteFilter, filters)).thenReturn(
77140
Response(
78141
data = emptyList(),
79142
errors =
@@ -85,61 +148,35 @@ class CaseNotesControllerTest(
85148
),
86149
),
87150
)
151+
88152
val result = mockMvc.performAuthorised(path)
89153
result.response.status.shouldBe(HttpStatus.FORBIDDEN.value())
90154
}
91155

92-
it("returns the case notes for a person with the matching ID") {
93-
val result = mockMvc.performAuthorised(path)
94-
95-
result.response.contentAsString.shouldContain(
96-
"""
97-
{
98-
"data": [
99-
{
100-
"caseNoteId": "abcd1234",
101-
"offenderIdentifier": null,
102-
"type": null,
103-
"typeDescription": null,
104-
"subType": null,
105-
"subTypeDescription": null,
106-
"creationDateTime": null,
107-
"occurrenceDateTime": null,
108-
"text": null,
109-
"locationId": null,
110-
"sensitive": false,
111-
"amendments": []
112-
}
113-
],
114-
"pagination": {
115-
"isLastPage": true,
116-
"count": 1,
117-
"page": 1,
118-
"perPage": 10,
119-
"totalCount": 1,
120-
"totalPages": 1
121-
}
122-
}
123-
""".removeWhitespaceAndNewlines(),
156+
it("returns a 400 when the upstream service returns entity not found") {
157+
whenever(getCaseNotesForPersonService.execute(caseNoteFilter, filters)).thenReturn(
158+
Response(
159+
data = emptyList(),
160+
errors =
161+
listOf(
162+
UpstreamApiError(
163+
type = UpstreamApiError.Type.ENTITY_NOT_FOUND,
164+
causedBy = UpstreamApi.NOMIS,
165+
),
166+
),
167+
),
124168
)
125-
}
126-
127-
it("logs audit") {
128-
mockMvc.performAuthorised(path)
129169

130-
verify(
131-
auditService,
132-
VerificationModeFactory.times(1),
133-
).createEvent("GET_CASE_NOTES", mapOf("hmppsId" to hmppsId))
170+
val result = mockMvc.performAuthorised(path)
171+
result.response.status.shouldBe(HttpStatus.NOT_FOUND.value())
134172
}
135173

136174
it("fails with the appropriate error when an upstream service is down") {
137-
whenever(getCaseNotesForPersonService.execute(any())).doThrow(
175+
whenever(getCaseNotesForPersonService.execute(caseNoteFilter, filters)).doThrow(
138176
WebClientResponseException(500, "MockError", null, null, null, null),
139177
)
140178

141179
val response = mockMvc.performAuthorised(path)
142-
143180
assert(response.response.status == 500)
144181
assert(
145182
response.response.contentAsString.equals(

src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/integration/person/CaseNotesIntegrationTest.kt

+25-1
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,36 @@ import org.junit.jupiter.api.Test
44
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content
55
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
66
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.integration.IntegrationTestBase
7+
import java.time.LocalDateTime
78

89
class CaseNotesIntegrationTest : IntegrationTestBase() {
10+
private final val locationId = "MDI"
11+
private final val startDate: LocalDateTime = LocalDateTime.now()
12+
private final val endDate: LocalDateTime = LocalDateTime.now()
13+
private final val path = "$basePath/$crn/case-notes?startDate=$startDate&endDate=$endDate&locationId=$locationId"
14+
915
@Test
1016
fun `returns case notes for a person`() {
11-
callApi("$basePath/$crn/case-notes")
17+
callApi(path)
1218
.andExpect(status().isOk)
1319
.andExpect(content().json(getExpectedResponse("person-case-notes")))
1420
}
21+
22+
@Test
23+
fun `returns a 400 if the hmppsId is invalid`() {
24+
callApi("$basePath/$invalidNomsId/case-notes")
25+
.andExpect(status().isBadRequest)
26+
}
27+
28+
@Test
29+
fun `returns a 404 for if consumer has empty list of prisons`() {
30+
callApiWithCN(path, noPrisonsCn)
31+
.andExpect(status().isNotFound)
32+
}
33+
34+
@Test
35+
fun `returns a 404 for prisoner in wrong prison`() {
36+
callApiWithCN(path, limitedPrisonsCn)
37+
.andExpect(status().isNotFound)
38+
}
1539
}

0 commit comments

Comments
 (0)