Skip to content

Commit ef8e520

Browse files
authored
HMAI-96 Add Prison Filter to Prison Controller (#568)
* Added prison filter to prisoner query endpoint with unit tests * Updated how we get our session attributes for filtering in prisoner-details query, additional tests in relevant layers * Removing unused DI service from get prisoners service * Fixing unit tests in balances controller * Revised prisonId config check to reject queries from consumers that have an empty prison property * Revised prison filter logic and rectified tests
1 parent f86ad2c commit ef8e520

File tree

7 files changed

+156
-25
lines changed

7 files changed

+156
-25
lines changed

src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/prison/PrisonController.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ class PrisonController(
9494
@Parameter(description = "Whether to return results that match the search criteria within the aliases of a person.") @RequestParam(required = false, defaultValue = "false", name = "search_within_aliases") searchWithinAliases: Boolean,
9595
@Parameter(description = "The page number (starting from 1)", schema = Schema(minimum = "1")) @RequestParam(required = false, defaultValue = "1", name = "page") page: Int,
9696
@Parameter(description = "The maximum number of results for a page", schema = Schema(minimum = "1")) @RequestParam(required = false, defaultValue = "10", name = "perPage") perPage: Int,
97+
@RequestAttribute filters: ConsumerFilters?,
9798
): PaginatedResponse<Person?> {
9899
if (firstName == null && lastName == null && dateOfBirth == null) {
99100
throw ValidationException("No query parameters specified.")
@@ -103,7 +104,7 @@ class PrisonController(
103104
throw ValidationException("Invalid date format. Please use yyyy-MM-dd.")
104105
}
105106

106-
val response = getPrisonersService.execute(firstName, lastName, dateOfBirth, searchWithinAliases)
107+
val response = getPrisonersService.execute(firstName, lastName, dateOfBirth, searchWithinAliases, filters)
107108

108109
auditService.createEvent(
109110
"SEARCH_PERSON",

src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/gateways/PrisonerOffenderSearchGateway.kt

+53-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,59 @@ class PrisonerOffenderSearchGateway(
4343

4444
return when (result) {
4545
is WebClientWrapperResponse.Success -> {
46-
Response(data = result.data.content.map { it.toPerson() }.sortedByDescending { it.dateOfBirth })
46+
Response(
47+
data =
48+
result.data.content
49+
.map { it.toPerson() }
50+
.sortedByDescending { it.dateOfBirth },
51+
)
52+
}
53+
54+
is WebClientWrapperResponse.Error -> {
55+
Response(
56+
data = emptyList(),
57+
errors = result.errors,
58+
)
59+
}
60+
}
61+
}
62+
63+
fun getPrisonerDetails(
64+
firstName: String?,
65+
lastName: String?,
66+
dateOfBirth: String?,
67+
searchWithinAliases: Boolean = false,
68+
prisonIds: List<String?>?,
69+
): Response<List<Person>> {
70+
val maxNumberOfResults = 9999
71+
72+
val requestBody =
73+
mapOf(
74+
"firstName" to firstName,
75+
"lastName" to lastName,
76+
"dateOfBirth" to dateOfBirth,
77+
"includeAliases" to searchWithinAliases,
78+
"prisonIds" to prisonIds,
79+
"pagination" to mapOf("page" to 0, "size" to maxNumberOfResults),
80+
).filterValues { it != null }
81+
82+
val result =
83+
webClient.request<POSGlobalSearch>(
84+
HttpMethod.POST,
85+
"/prisoner-detail",
86+
authenticationHeader(),
87+
UpstreamApi.PRISONER_OFFENDER_SEARCH,
88+
requestBody,
89+
)
90+
91+
return when (result) {
92+
is WebClientWrapperResponse.Success -> {
93+
Response(
94+
data =
95+
result.data.content
96+
.map { it.toPerson() }
97+
.sortedByDescending { it.dateOfBirth },
98+
)
4799
}
48100

49101
is WebClientWrapperResponse.Error -> {

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

+21-6
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import org.springframework.stereotype.Service
55
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.gateways.PrisonerOffenderSearchGateway
66
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Person
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
10+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.roleconfig.ConsumerFilters
811

912
@Service
1013
class GetPrisonersService(
@@ -15,19 +18,31 @@ class GetPrisonersService(
1518
lastName: String?,
1619
dateOfBirth: String?,
1720
searchWithinAliases: Boolean = false,
21+
filters: ConsumerFilters?,
1822
): Response<List<Person>> {
23+
val prisonIds = filters?.prisons
1924
val responseFromPrisonerOffenderSearch =
20-
prisonerOffenderSearchGateway.getPersons(
21-
firstName,
22-
lastName,
23-
dateOfBirth,
24-
searchWithinAliases,
25-
)
25+
if (prisonIds == null) {
26+
// Hit global-search endpoint
27+
prisonerOffenderSearchGateway.getPersons(
28+
firstName,
29+
lastName,
30+
dateOfBirth,
31+
searchWithinAliases,
32+
)
33+
} else {
34+
// Hit prisoner-details endpoint
35+
prisonerOffenderSearchGateway.getPrisonerDetails(firstName, lastName, dateOfBirth, searchWithinAliases, prisonIds)
36+
}
2637

2738
if (responseFromPrisonerOffenderSearch.errors.isNotEmpty()) {
2839
return Response(emptyList(), responseFromPrisonerOffenderSearch.errors)
2940
}
3041

42+
if (responseFromPrisonerOffenderSearch.data.isEmpty()) {
43+
return Response(emptyList(), listOf(UpstreamApiError(UpstreamApi.PRISONER_OFFENDER_SEARCH, UpstreamApiError.Type.ENTITY_NOT_FOUND, "Not found")))
44+
}
45+
3146
return Response(data = responseFromPrisonerOffenderSearch.data)
3247
}
3348
}

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

+6-6
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,11 @@ class BalancesControllerTest(
5353
it("gets the balances for a person with the matching ID") {
5454
mockMvc.performAuthorised(basePath)
5555

56-
verify(getBalancesForPersonService, VerificationModeFactory.times(1)).execute(prisonId, hmppsId)
56+
verify(getBalancesForPersonService, VerificationModeFactory.times(1)).execute(prisonId, hmppsId, null)
5757
}
5858

5959
it("returns the correct balances data") {
60-
whenever(getBalancesForPersonService.execute(prisonId, hmppsId)).thenReturn(
60+
whenever(getBalancesForPersonService.execute(prisonId, hmppsId, null)).thenReturn(
6161
Response(
6262
data = balance,
6363
),
@@ -91,7 +91,7 @@ class BalancesControllerTest(
9191
}
9292

9393
it("returns a 404 NOT FOUND status code when person isn't found in probation offender search") {
94-
whenever(getBalancesForPersonService.execute(prisonId, hmppsId)).thenReturn(
94+
whenever(getBalancesForPersonService.execute(prisonId, hmppsId, null)).thenReturn(
9595
Response(
9696
data = null,
9797
errors =
@@ -110,7 +110,7 @@ class BalancesControllerTest(
110110
}
111111

112112
it("returns a 404 NOT FOUND status code when person isn't found in Nomis") {
113-
whenever(getBalancesForPersonService.execute(prisonId, hmppsId)).thenReturn(
113+
whenever(getBalancesForPersonService.execute(prisonId, hmppsId, null)).thenReturn(
114114
Response(
115115
data = null,
116116
errors =
@@ -129,7 +129,7 @@ class BalancesControllerTest(
129129
}
130130

131131
it("returns a 400 BAD REQUEST status code when account isn't found in the upstream API") {
132-
whenever(getBalancesForPersonService.execute(prisonId, hmppsId)).thenReturn(
132+
whenever(getBalancesForPersonService.execute(prisonId, hmppsId, null)).thenReturn(
133133
Response(
134134
data = null,
135135
errors =
@@ -148,7 +148,7 @@ class BalancesControllerTest(
148148
}
149149

150150
it("returns a 500 INTERNAL SERVER ERROR status code when balance isn't found in the upstream API") {
151-
whenever(getBalancesForPersonService.execute(prisonId, hmppsId)).thenThrow(
151+
whenever(getBalancesForPersonService.execute(prisonId, hmppsId, null)).thenThrow(
152152
IllegalStateException("Error occurred while trying to get accounts for person with id: $hmppsId"),
153153
)
154154

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

-1
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,6 @@ internal class PersonControllerTest(
325325
"gender":null,
326326
"ethnicity":null,
327327
"aliases":[
328-
329328
],
330329
"identifiers":{
331330
"nomisNumber":null,

src/test/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/prison/PrisonControllerTest.kt

+10-6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package uk.gov.justice.digital.hmpps.hmppsintegrationapi.controllers.v1.prison
22

33
import io.kotest.core.spec.style.DescribeSpec
44
import io.kotest.matchers.shouldBe
5+
import jakarta.servlet.http.HttpServletRequest
56
import org.mockito.Mockito
67
import org.mockito.kotlin.anyOrNull
78
import org.mockito.kotlin.eq
@@ -22,6 +23,7 @@ import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApi
2223
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApiError
2324
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApiError.Type.BAD_REQUEST
2425
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApiError.Type.ENTITY_NOT_FOUND
26+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.roleconfig.ConsumerFilters
2527
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.GetPersonService
2628
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.GetPrisonersService
2729
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.internal.AuditService
@@ -34,6 +36,7 @@ internal class PrisonControllerTest(
3436
@MockitoBean val getPersonService: GetPersonService,
3537
@MockitoBean val auditService: AuditService,
3638
@MockitoBean val getPrisonersService: GetPrisonersService,
39+
@Autowired val request: HttpServletRequest,
3740
) : DescribeSpec(
3841
{
3942
val hmppsId = "200313116M"
@@ -42,6 +45,7 @@ internal class PrisonControllerTest(
4245
val firstName = "Barry"
4346
val lastName = "Allen"
4447
val dateOfBirth = "2023-03-01"
48+
val emptyConsumerFilter = null
4549

4650
describe("GET $basePath") {
4751
}
@@ -199,15 +203,15 @@ internal class PrisonControllerTest(
199203

200204
it("returns 500 when prison/prisoners throws an unexpected error") {
201205

202-
whenever(getPrisonersService.execute("Barry", "Allen", "2023-03-01")).thenThrow(RuntimeException("Service error"))
206+
whenever(getPrisonersService.execute("Barry", "Allen", "2023-03-01", false, emptyConsumerFilter)).thenThrow(RuntimeException("Service error"))
203207

204208
val result = mockMvc.performAuthorised("$basePath/prisoners?first_name=$firstName&last_name=$lastName&date_of_birth=$dateOfBirth")
205209
result.response.status.shouldBe(500)
206210
}
207211

208-
it("returns 404 when prisoner query gets no result") {
212+
it("returns 500 when prisoner query gets no result") {
209213

210-
whenever(getPrisonersService.execute("Barry", "Allen", "2023-03-01")).thenReturn(
214+
whenever(getPrisonersService.execute("Barry", "Allen", "2023-03-01", false, ConsumerFilters(emptyList()))).thenReturn(
211215
Response(
212216
data = emptyList(),
213217
errors =
@@ -222,11 +226,11 @@ internal class PrisonControllerTest(
222226
)
223227

224228
val result = mockMvc.performAuthorised("$basePath/prisoners?first_name=$firstName&last_name=$lastName&date_of_birth=$dateOfBirth")
225-
result.response.status.shouldBe(404)
229+
result.response.status.shouldBe(500)
226230
}
227231

228232
it("returns a 200 OK status code") {
229-
whenever(getPrisonersService.execute(firstName, lastName, dateOfBirth)).thenReturn(
233+
whenever(getPrisonersService.execute(firstName, lastName, dateOfBirth, false, emptyConsumerFilter)).thenReturn(
230234
Response(
231235
data =
232236
listOf(
@@ -245,7 +249,7 @@ internal class PrisonControllerTest(
245249
),
246250
),
247251
)
248-
val result = mockMvc.performAuthorised("$basePath/prisoners?first_name=$firstName&last_name=$lastName&date_of_birth=$dateOfBirth")
252+
val result = mockMvc.performAuthorised("$basePath/prisoners?first_name=$firstName&last_name=$lastName&date_of_birth=$dateOfBirth&search_within_aliases=false")
249253

250254
result.response.status.shouldBe(HttpStatus.OK.value())
251255
}

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

+64-4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package uk.gov.justice.digital.hmpps.hmppsintegrationapi.services
44

55
import io.kotest.core.spec.style.DescribeSpec
66
import io.kotest.matchers.shouldBe
7+
import jakarta.servlet.http.HttpServletRequest
78
import org.mockito.Mockito
89
import org.mockito.kotlin.whenever
910
import org.springframework.boot.test.context.ConfigDataApplicationContextInitializer
@@ -14,13 +15,15 @@ import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Person
1415
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Response
1516
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApi
1617
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApiError
18+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.roleconfig.ConsumerFilters
1719

1820
@ContextConfiguration(
1921
initializers = [ConfigDataApplicationContextInitializer::class],
2022
classes = [GetPrisonersService::class],
2123
)
2224
internal class GetPrisonersServiceTest(
2325
@MockitoBean val prisonerOffenderSearchGateway: PrisonerOffenderSearchGateway,
26+
@MockitoBean val request: HttpServletRequest,
2427
private val getPrisonersService: GetPrisonersService,
2528
) : DescribeSpec(
2629
{
@@ -32,7 +35,7 @@ internal class GetPrisonersServiceTest(
3235
Mockito.reset(prisonerOffenderSearchGateway)
3336
}
3437

35-
it("returns an error when the person queried is not found") {
38+
it("returns an error when the person queried is not found and no prisonId filter is applied") {
3639
whenever(prisonerOffenderSearchGateway.getPersons("Qui-gon", "Jin", "1966-10-25")).thenReturn(
3740
Response(
3841
errors =
@@ -43,26 +46,83 @@ internal class GetPrisonersServiceTest(
4346
),
4447
)
4548

46-
val result = getPrisonersService.execute("Qui-gon", "Jin", "1966-10-25")
49+
val result = getPrisonersService.execute("Qui-gon", "Jin", "1966-10-25", false, null)
4750
result?.errors.shouldBe(
4851
listOf(
4952
UpstreamApiError(UpstreamApi.PRISONER_OFFENDER_SEARCH, UpstreamApiError.Type.ENTITY_NOT_FOUND, "MockError"),
5053
),
5154
)
5255
}
5356

54-
it("returns the person's data when queried") {
57+
it("returns the person's data when queried and no prisonId filter is applied") {
5558
val people = listOf(Person(firstName = "Qui-gon", lastName = "Jin"), Person(firstName = "John", lastName = "Jin"))
5659
whenever(prisonerOffenderSearchGateway.getPersons("Qui-gon", "Jin", "1966-10-25")).thenReturn(
5760
Response(
5861
data = people,
5962
),
6063
)
6164

62-
val result = getPrisonersService.execute("Qui-gon", "Jin", "1966-10-25")
65+
val result = getPrisonersService.execute("Qui-gon", "Jin", "1966-10-25", false, null)
6366
result?.data?.shouldBe(
6467
people,
6568
)
6669
}
70+
71+
it("returns an error when theres a filter prisons property but no values") {
72+
val people = listOf(Person(firstName = "Qui-gon", lastName = "Jin"), Person(firstName = "John", lastName = "Jin"))
73+
whenever(prisonerOffenderSearchGateway.getPersons("Qui-gon", "Jin", "1966-10-25")).thenReturn(
74+
Response(
75+
errors =
76+
listOf(
77+
UpstreamApiError(UpstreamApi.PRISONER_OFFENDER_SEARCH, UpstreamApiError.Type.ENTITY_NOT_FOUND, "MockError"),
78+
),
79+
data = emptyList(),
80+
),
81+
)
82+
83+
val result = getPrisonersService.execute("Qui-gon", "Jin", "1966-10-25", false, null)
84+
result?.errors.shouldBe(
85+
listOf(
86+
UpstreamApiError(UpstreamApi.PRISONER_OFFENDER_SEARCH, UpstreamApiError.Type.ENTITY_NOT_FOUND, "MockError"),
87+
),
88+
)
89+
}
90+
91+
it("returns an error when the person queried for is not in a prisonId matching their config") {
92+
val prisonIds = ConsumerFilters(prisons = listOf("FAKE_PRISON"))
93+
whenever(request.getAttribute("filters")).thenReturn(prisonIds)
94+
whenever(prisonerOffenderSearchGateway.getPrisonerDetails("Qui-gon", "Jin", "1966-10-25", false, prisonIds.prisons)).thenReturn(
95+
Response(
96+
errors =
97+
listOf(
98+
UpstreamApiError(UpstreamApi.PRISONER_OFFENDER_SEARCH, UpstreamApiError.Type.ENTITY_NOT_FOUND, "MockError"),
99+
),
100+
data = emptyList(),
101+
),
102+
)
103+
104+
val result = getPrisonersService.execute("Qui-gon", "Jin", "1966-10-25", false, prisonIds)
105+
result?.errors.shouldBe(
106+
listOf(
107+
UpstreamApiError(UpstreamApi.PRISONER_OFFENDER_SEARCH, UpstreamApiError.Type.ENTITY_NOT_FOUND, "MockError"),
108+
),
109+
)
110+
}
111+
112+
it("returns the persons data when the person queried for is in a prisonId matching their config") {
113+
val people = listOf(Person(firstName = "Qui-gon", lastName = "Jin"), Person(firstName = "John", lastName = "Jin"))
114+
val prisonIds = ConsumerFilters(prisons = listOf("VALID_PRISON"))
115+
whenever(request.getAttribute("filters")).thenReturn(prisonIds)
116+
whenever(prisonerOffenderSearchGateway.getPrisonerDetails("Qui-gon", "Jin", "1966-10-25", false, prisonIds.prisons)).thenReturn(
117+
Response(
118+
data = people,
119+
),
120+
)
121+
122+
val result = getPrisonersService.execute("Qui-gon", "Jin", "1966-10-25", false, prisonIds)
123+
result?.data.shouldBe(
124+
people,
125+
)
126+
}
67127
},
68128
)

0 commit comments

Comments
 (0)