Skip to content

Commit 302847f

Browse files
authored
Hia 188 person search by DOB (#361)
* Updated getPersons to be queried on DOB * Updated tests * Updated openApi spec
1 parent 4d87385 commit 302847f

File tree

10 files changed

+209
-121
lines changed

10 files changed

+209
-121
lines changed

openapi.yml

+12
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,18 @@ paths:
2626
type: string
2727
required: false
2828
description: The last name of the person
29+
- in: query
30+
name: pnc_number
31+
schema:
32+
type: string
33+
required: false
34+
description: A URL-encoded pnc identifier
35+
- in: query
36+
name: date_of_birth
37+
schema:
38+
type: string
39+
required: false
40+
description: The date of birth of the person
2941
- in: query
3042
name: search_within_aliases
3143
schema:

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

+20-4
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.GetPersonsServi
2020
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.internal.AuditService
2121
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.util.PaginatedResponse
2222
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.util.paginateWith
23+
import java.time.LocalDate
24+
import java.time.format.DateTimeFormatter
2325

2426
@RestController
2527
@RequestMapping("/v1/persons")
@@ -32,20 +34,25 @@ class PersonController(
3234

3335
@GetMapping
3436
fun getPersons(
35-
@RequestParam(required = false, name = "pnc_number") pncNumber: String?,
3637
@RequestParam(required = false, name = "first_name") firstName: String?,
3738
@RequestParam(required = false, name = "last_name") lastName: String?,
39+
@RequestParam(required = false, name = "pnc_number") pncNumber: String?,
40+
@RequestParam(required = false, name = "date_of_birth") dateOfBirth: String?,
3841
@RequestParam(required = false, defaultValue = "false", name = "search_within_aliases") searchWithinAliases: Boolean,
3942
@RequestParam(required = false, defaultValue = "1", name = "page") page: Int,
4043
@RequestParam(required = false, defaultValue = "10", name = "perPage") perPage: Int,
4144
): PaginatedResponse<Person?> {
42-
if (firstName == null && lastName == null && pncNumber == null) {
45+
if (firstName == null && lastName == null && pncNumber == null && dateOfBirth == null) {
4346
throw ValidationException("No query parameters specified.")
4447
}
4548

46-
val response = getPersonsService.execute(firstName, lastName, pncNumber, searchWithinAliases)
49+
if (dateOfBirth != null && !isValidISODateFormat(dateOfBirth)) {
50+
throw ValidationException("Invalid date format. Please use yyyy-MM-dd.")
51+
}
52+
53+
val response = getPersonsService.execute(firstName, lastName, pncNumber, dateOfBirth, searchWithinAliases)
4754

48-
auditService.createEvent("SEARCH_PERSON", "Person searched with first name: $firstName, last name: $lastName, search within aliases: $searchWithinAliases, pnc number: $pncNumber")
55+
auditService.createEvent("SEARCH_PERSON", "Person searched with first name: $firstName, last name: $lastName, search within aliases: $searchWithinAliases, pnc number: $pncNumber, date of birth: $dateOfBirth")
4956
return response.data.paginateWith(page, perPage)
5057
}
5158

@@ -79,4 +86,13 @@ class PersonController(
7986
auditService.createEvent("GET_PERSON_IMAGE", "Image with id: $hmppsId has been retrieved")
8087
return response.data.paginateWith(page, perPage)
8188
}
89+
90+
private fun isValidISODateFormat(dateString: String): Boolean {
91+
return try {
92+
LocalDate.parse(dateString, DateTimeFormatter.ISO_DATE)
93+
true
94+
} catch (e: Exception) {
95+
false
96+
}
97+
}
8298
}

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ class PrisonerOffenderSearchGateway(@Value("\${services.prisoner-offender-search
1818
@Autowired
1919
lateinit var hmppsAuthGateway: HmppsAuthGateway
2020

21-
fun getPersons(firstName: String? = null, lastName: String? = null, hmppsId: String? = null, searchWithinAliases: Boolean = false): Response<List<Person>> {
21+
fun getPersons(firstName: String?, lastName: String?, hmppsId: String?, dateOfBirth: String?, searchWithinAliases: Boolean = false): Response<List<Person>> {
2222
val maxNumberOfResults = 9999
2323
val requestBody =
24-
mapOf("firstName" to firstName, "lastName" to lastName, "includeAliases" to searchWithinAliases, "prisonerIdentifier" to hmppsId)
24+
mapOf("firstName" to firstName, "lastName" to lastName, "includeAliases" to searchWithinAliases, "dateOfBirth" to dateOfBirth, "prisonerIdentifier" to hmppsId)
2525
.filterValues { it != null }
2626

2727
val result = webClient.request<POSGlobalSearch>(HttpMethod.POST, "/global-search?size=$maxNumberOfResults", authenticationHeader(), UpstreamApi.PRISONER_OFFENDER_SEARCH, requestBody)

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@ class ProbationOffenderSearchGateway(@Value("\${services.probation-offender-sear
6363
}
6464
}
6565

66-
fun getPersons(firstName: String?, surname: String?, pncNumber: String?, searchWithinAliases: Boolean = false): Response<List<Person>> {
67-
val requestBody = mapOf("firstName" to firstName, "surname" to surname, "pncNumber" to pncNumber, "includeAliases" to searchWithinAliases)
66+
fun getPersons(firstName: String?, surname: String?, pncNumber: String?, dateOfBirth: String?, searchWithinAliases: Boolean = false): Response<List<Person>> {
67+
val requestBody = mapOf("firstName" to firstName, "surname" to surname, "pncNumber" to pncNumber, "dateOfBirth" to dateOfBirth, "includeAliases" to searchWithinAliases)
6868
.filterValues { it != null }
6969

7070
val result = webClient.requestList<Offender>(HttpMethod.POST, "/search", authenticationHeader(), UpstreamApi.PROBATION_OFFENDER_SEARCH, requestBody)

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

+4-4
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,16 @@ class GetPersonsService(
1313
@Autowired val probationOffenderSearchGateway: ProbationOffenderSearchGateway,
1414
) {
1515

16-
fun execute(firstName: String?, lastName: String?, pncNumber: String?, searchWithinAliases: Boolean = false): Response<List<Person>> {
16+
fun execute(firstName: String?, lastName: String?, pncNumber: String?, dateOfBirth: String?, searchWithinAliases: Boolean = false): Response<List<Person>> {
1717
var hmppsId: String? = null
1818

19-
val personsFromProbationOffenderSearch = probationOffenderSearchGateway.getPersons(firstName, lastName, pncNumber, searchWithinAliases)
19+
val personsFromProbationOffenderSearch = probationOffenderSearchGateway.getPersons(firstName, lastName, pncNumber, dateOfBirth, searchWithinAliases)
2020

21-
if (pncNumber != null) {
21+
if (!pncNumber.isNullOrEmpty()) {
2222
hmppsId = personsFromProbationOffenderSearch.data.firstOrNull()?.identifiers?.deliusCrn
2323
}
2424

25-
val responseFromPrisonerOffenderSearch = prisonerOffenderSearchGateway.getPersons(firstName, lastName, hmppsId, searchWithinAliases)
25+
val responseFromPrisonerOffenderSearch = prisonerOffenderSearchGateway.getPersons(firstName, lastName, hmppsId, dateOfBirth, searchWithinAliases)
2626
return Response(data = responseFromPrisonerOffenderSearch.data + personsFromProbationOffenderSearch.data)
2727
}
2828
}

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

+27-59
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,14 @@ internal class PersonControllerTest(
4848
val basePath = "/v1/persons"
4949
val firstName = "Barry"
5050
val lastName = "Allen"
51+
val dateOfBirth = "2023-03-01"
5152
val mockMvc = IntegrationAPIMockMvc(springMockMvc)
5253

5354
describe("GET $basePath") {
5455
beforeTest {
5556
Mockito.reset(getPersonsService)
5657
Mockito.reset(auditService)
57-
whenever(getPersonsService.execute(firstName, lastName, null)).thenReturn(
58+
whenever(getPersonsService.execute(firstName, lastName, null, dateOfBirth)).thenReturn(
5859
Response(
5960
data =
6061
listOf(
@@ -76,90 +77,50 @@ internal class PersonControllerTest(
7677
}
7778

7879
it("gets a person with matching search criteria") {
79-
mockMvc.performAuthorised("$basePath?first_name=$firstName&last_name=$lastName&pnc_number=$pncNumber")
80+
mockMvc.performAuthorised("$basePath?first_name=$firstName&last_name=$lastName&pnc_number=$pncNumber&date_of_birth=$dateOfBirth")
8081

81-
verify(getPersonsService, times(1)).execute(firstName, lastName, pncNumber)
82+
verify(getPersonsService, times(1)).execute(firstName, lastName, pncNumber, dateOfBirth)
8283
}
8384

8485
it("gets a person with matching first name") {
8586
mockMvc.performAuthorised("$basePath?first_name=$firstName")
86-
87-
verify(getPersonsService, times(1)).execute(firstName, null, null)
87+
verify(getPersonsService, times(1)).execute(firstName, null, null, null)
8888
}
8989

9090
it("gets a person with matching last name") {
9191
mockMvc.performAuthorised("$basePath?last_name=$lastName")
92-
93-
verify(getPersonsService, times(1)).execute(null, lastName, null)
92+
verify(getPersonsService, times(1)).execute(null, lastName, null, null)
9493
}
9594

9695
it("gets a person with matching alias") {
9796
mockMvc.performAuthorised("$basePath?first_name=$firstName&search_within_aliases=true")
98-
99-
verify(getPersonsService, times(1)).execute(firstName, null, null, searchWithinAliases = true)
97+
verify(getPersonsService, times(1)).execute(firstName, null, null, null, searchWithinAliases = true)
10098
}
10199

102100
it("gets a person with matching pncNumber") {
103101
mockMvc.performAuthorised("$basePath?pnc_number=$pncNumber")
102+
verify(getPersonsService, times(1)).execute(null, null, pncNumber, null)
103+
}
104+
105+
it("gets a person with matching date of birth") {
106+
mockMvc.performAuthorised("$basePath?date_of_birth=$dateOfBirth")
104107

105-
verify(getPersonsService, times(1)).execute(null, null, pncNumber)
108+
verify(getPersonsService, times(1)).execute(null, null, null, dateOfBirth)
106109
}
107110

108111
it("defaults to not searching within aliases") {
109112
mockMvc.performAuthorised("$basePath?first_name=$firstName")
110113

111-
verify(getPersonsService, times(1)).execute(firstName, null, null)
112-
}
113-
114-
it("returns a person with matching first and last name") {
115-
val result = mockMvc.performAuthorised("$basePath?first_name=$firstName&last_name=$lastName")
116-
result.response.contentAsString.shouldContain(
117-
"""
118-
"data": [
119-
{
120-
"firstName":"Barry",
121-
"lastName":"Allen",
122-
"middleName":"Jonas",
123-
"dateOfBirth":"2023-03-01",
124-
"gender": null,
125-
"ethnicity": null,
126-
"aliases":[],
127-
"identifiers": {
128-
"nomisNumber": null,
129-
"croNumber": null,
130-
"deliusCrn": null
131-
},
132-
"pncId": null,
133-
"hmppsId": null
134-
},
135-
{
136-
"firstName":"Barry",
137-
"lastName":"Allen",
138-
"middleName":"Rock",
139-
"dateOfBirth":"2022-07-22",
140-
"gender": null,
141-
"ethnicity": null,
142-
"aliases":[],
143-
"identifiers": {
144-
"nomisNumber": null,
145-
"croNumber": null,
146-
"deliusCrn": null
147-
},
148-
"pncId": null,
149-
"hmppsId": null
150-
}
151-
]
152-
""".removeWhitespaceAndNewlines(),
153-
)
114+
verify(getPersonsService, times(1)).execute(firstName, null, null, null)
154115
}
155116

156117
it("logs audit") {
157-
mockMvc.performAuthorised("$basePath?first_name=$firstName&last_name=$lastName&pnc_number=$pncNumber")
158-
verify(auditService, times(1)).createEvent("SEARCH_PERSON", "Person searched with first name: $firstName, last name: $lastName, search within aliases: false, pnc number: $pncNumber")
118+
mockMvc.performAuthorised("$basePath?first_name=$firstName&last_name=$lastName&pnc_number=$pncNumber&date_of_birth=$dateOfBirth")
119+
verify(auditService, times(1)).createEvent("SEARCH_PERSON", "Person searched with first name: $firstName, last name: $lastName, search within aliases: false, pnc number: $pncNumber, date of birth: $dateOfBirth")
159120
}
160121

161122
it("returns paginated results") {
162-
whenever(getPersonsService.execute(firstName, lastName, null)).thenReturn(
123+
whenever(getPersonsService.execute(firstName, lastName, null, dateOfBirth)).thenReturn(
163124
Response(
164125
data =
165126
List(20) { i ->
@@ -172,7 +133,7 @@ internal class PersonControllerTest(
172133
),
173134
)
174135

175-
val result = mockMvc.performAuthorised("$basePath?first_name=$firstName&last_name=$lastName&page=3&perPage=5")
136+
val result = mockMvc.performAuthorised("$basePath?first_name=$firstName&last_name=$lastName&date_of_birth=$dateOfBirth&page=3&perPage=5")
176137

177138
result.response.contentAsString.shouldContainJsonKeyValue("$.pagination.page", 3)
178139
result.response.contentAsString.shouldContainJsonKeyValue("$.pagination.totalPages", 4)
@@ -182,7 +143,7 @@ internal class PersonControllerTest(
182143
val firstNameThatDoesNotExist = "Bob21345"
183144
val lastNameThatDoesNotExist = "Gun36773"
184145

185-
whenever(getPersonsService.execute(firstNameThatDoesNotExist, lastNameThatDoesNotExist, null)).thenReturn(
146+
whenever(getPersonsService.execute(firstNameThatDoesNotExist, lastNameThatDoesNotExist, null, null)).thenReturn(
186147
Response(
187148
data = emptyList(),
188149
),
@@ -194,7 +155,7 @@ internal class PersonControllerTest(
194155
}
195156

196157
it("returns a 200 OK status code") {
197-
val result = mockMvc.performAuthorised("$basePath?first_name=$firstName&last_name=$lastName")
158+
val result = mockMvc.performAuthorised("$basePath?first_name=$firstName&last_name=$lastName&date_of_birth=$dateOfBirth")
198159

199160
result.response.status.shouldBe(HttpStatus.OK.value())
200161
}
@@ -205,6 +166,13 @@ internal class PersonControllerTest(
205166
result.response.status.shouldBe(HttpStatus.BAD_REQUEST.value())
206167
result.response.contentAsString.shouldContain("No query parameters specified.")
207168
}
169+
170+
it("returns a 400 BAD REQUEST status code when no search criteria provided") {
171+
val result = mockMvc.performAuthorised("$basePath?date_of_birth=12323423234")
172+
173+
result.response.status.shouldBe(HttpStatus.BAD_REQUEST.value())
174+
result.response.contentAsString.shouldContain("Invalid date format. Please use yyyy-MM-dd.")
175+
}
208176
}
209177

210178
describe("GET $basePath/{id}") {

0 commit comments

Comments
 (0)