Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MAN-26: Added caseload search filter and sort #4257

Merged
merged 2 commits into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ class DataLoader(
entityManager.flush()
entityManager.persist(PersonGenerator.CASELOAD_PERSON_1)
entityManager.persist(PersonGenerator.CASELOAD_PERSON_2)
entityManager.persist(PersonGenerator.CASELOAD_PERSON_3)
}

private fun EntityManager.persistAll(vararg entities: Any) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ object PersonGenerator {

val CASELOAD_PERSON_1 = generateCaseload(PERSON_1, DEFAULT_STAFF, ContactGenerator.DEFAULT_TEAM)
val CASELOAD_PERSON_2 = generateCaseload(PERSON_2, ContactGenerator.STAFF_1, ContactGenerator.DEFAULT_TEAM)
val CASELOAD_PERSON_3 = generateCaseload(PERSON_2, DEFAULT_STAFF, ContactGenerator.DEFAULT_TEAM)

fun generateEvent(
person: Person,
Expand Down Expand Up @@ -490,6 +491,13 @@ object PersonGenerator {
)

fun generateCaseloadPerson(id: Long, crn: String, forename: String, middleName: String?, surname: String) =
CaseloadPerson(id = id, crn = crn, forename = forename, secondName = middleName, surname = surname)
CaseloadPerson(
id = id,
crn = crn,
forename = forename,
secondName = middleName,
dateOfBirth = LocalDate.now().minusYears(50),
surname = surname
)
}

Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,11 @@ import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
import uk.gov.justice.digital.hmpps.advice.ErrorResponse
import uk.gov.justice.digital.hmpps.api.model.Name
import uk.gov.justice.digital.hmpps.api.model.user.StaffCaseload
import uk.gov.justice.digital.hmpps.api.model.user.TeamCaseload
import uk.gov.justice.digital.hmpps.api.model.user.TeamStaff
import uk.gov.justice.digital.hmpps.api.model.user.UserTeam
import uk.gov.justice.digital.hmpps.api.model.user.*
import uk.gov.justice.digital.hmpps.data.generator.ContactGenerator.DEFAULT_PROVIDER
import uk.gov.justice.digital.hmpps.data.generator.ContactGenerator.DEFAULT_STAFF
import uk.gov.justice.digital.hmpps.data.generator.ContactGenerator.DEFAULT_TEAM
Expand All @@ -25,6 +24,7 @@ import uk.gov.justice.digital.hmpps.data.generator.PersonGenerator.OVERVIEW
import uk.gov.justice.digital.hmpps.data.generator.personalDetails.PersonDetailsGenerator.PERSONAL_DETAILS
import uk.gov.justice.digital.hmpps.service.name
import uk.gov.justice.digital.hmpps.test.MockMvcExtensions.contentAsJson
import uk.gov.justice.digital.hmpps.test.MockMvcExtensions.withJson
import uk.gov.justice.digital.hmpps.test.MockMvcExtensions.withToken

@AutoConfigureMockMvc
Expand Down Expand Up @@ -120,4 +120,199 @@ internal class UserIntegrationTest {
assertThat(res.caseload[1].crn, equalTo(PERSONAL_DETAILS.crn))
assertThat(res.caseload[1].caseName, equalTo(PERSONAL_DETAILS.name()))
}

@Test
fun `caseload search returns all when no search criteria or sort specified`() {

val user = USER
val res = mockMvc
.perform(
post("/caseload/user/${user.username}/search").withToken()
.withJson(UserSearchFilter(nameOrCrn = null, nextContact = null, sentence = null))
)
.andExpect(status().isOk)
.andReturn().response.contentAsJson<StaffCaseload>()

assertThat(res.caseload.size, equalTo(2))
}

@Test
fun `caseload search returns one when name part specified`() {

val user = USER
val res = mockMvc
.perform(
post("/caseload/user/${user.username}/search").withToken()
.withJson(UserSearchFilter(nameOrCrn = "Blog", nextContact = null, sentence = null))
)
.andExpect(status().isOk)
.andReturn().response.contentAsJson<StaffCaseload>()

assertThat(res.caseload.size, equalTo(1))
assertThat(res.caseload[0].crn, equalTo("X000005"))
}

@Test
fun `caseload search returns one case when 2 name parts specified`() {

val user = USER
val res = mockMvc
.perform(
post("/caseload/user/${user.username}/search").withToken()
.withJson(UserSearchFilter(nameOrCrn = "Caroline Blog", nextContact = null, sentence = null))
)
.andExpect(status().isOk)
.andReturn().response.contentAsJson<StaffCaseload>()

assertThat(res.caseload.size, equalTo(1))
assertThat(res.caseload[0].crn, equalTo("X000005"))
}

@Test
fun `caseload search returns 1 case when sentence specified in filter`() {

val user = USER
val res = mockMvc
.perform(
post("/caseload/user/${user.username}/search").withToken()
.withJson(UserSearchFilter(nameOrCrn = null, nextContact = null, sentence = "Murder"))
)
.andExpect(status().isOk)
.andReturn().response.contentAsJson<StaffCaseload>()

assertThat(res.caseload.size, equalTo(1))
assertThat(res.caseload[0].crn, equalTo("X000004"))
assertThat(res.caseload[0].latestSentence, equalTo("Murder"))
}

@Test
fun `caseload search returns 1 case when nextAppointment type specified in filter`() {

val user = USER
val res = mockMvc
.perform(
post("/caseload/user/${user.username}/search").withToken()
.withJson(UserSearchFilter(nameOrCrn = null, nextContact = "doorstep", sentence = null))
)
.andExpect(status().isOk)
.andReturn().response.contentAsJson<StaffCaseload>()

assertThat(res.caseload.size, equalTo(1))
assertThat(res.caseload[0].crn, equalTo("X000004"))
assertThat(res.caseload[0].nextAppointment?.description, equalTo("Initial Appointment on Doorstep (NS)"))
}

@Test
fun `caseload search returns null sentence type first case when sentence type is in the sort criteria as descending`() {

val user = USER
val res = mockMvc
.perform(
post("/caseload/user/${user.username}/search?sortBy=sentence.desc").withToken()
.withJson(UserSearchFilter(nameOrCrn = null, nextContact = null, sentence = null))
)
.andExpect(status().isOk)
.andReturn().response.contentAsJson<StaffCaseload>()

assertThat(res.caseload.size, equalTo(2))
assertThat(res.caseload[0].crn, equalTo("X000005"))
assertThat(res.caseload[0].latestSentence, equalTo(null))
}

@Test
fun `caseload search returns null next appointment first case when next contact is in the sort criteria as descending`() {

val user = USER
val res = mockMvc
.perform(
post("/caseload/user/${user.username}/search?sortBy=nextContact.desc").withToken()
.withJson(UserSearchFilter(nameOrCrn = null, nextContact = null, sentence = null))
)
.andExpect(status().isOk)
.andReturn().response.contentAsJson<StaffCaseload>()

assertThat(res.caseload.size, equalTo(2))
assertThat(res.caseload[0].crn, equalTo("X000005"))
assertThat(res.caseload[0].nextAppointment, equalTo(null))
}

@Test
fun `caseload search returns crn with name bloggs first when sorting by surname asc`() {

val user = USER
val res = mockMvc
.perform(
post("/caseload/user/${user.username}/search?sortBy=surname.asc").withToken()
.withJson(UserSearchFilter(nameOrCrn = null, nextContact = null, sentence = null))
)
.andExpect(status().isOk)
.andReturn().response.contentAsJson<StaffCaseload>()

assertThat(res.caseload.size, equalTo(2))
assertThat(res.caseload[0].crn, equalTo("X000005"))
assertThat(res.caseload[0].caseName.surname, equalTo("Bloggs"))
}

@Test
fun `caseload search returns crn with name Surname first when sorting by surname desc`() {

val user = USER
val res = mockMvc
.perform(
post("/caseload/user/${user.username}/search?sortBy=surname.desc").withToken()
.withJson(UserSearchFilter(nameOrCrn = null, nextContact = null, sentence = null))
)
.andExpect(status().isOk)
.andReturn().response.contentAsJson<StaffCaseload>()

assertThat(res.caseload.size, equalTo(2))
assertThat(res.caseload[0].crn, equalTo("X000004"))
assertThat(res.caseload[0].caseName.surname, equalTo("Surname"))
}

@Test
fun `caseload search throws a bad request when the sortBy is not in the correct format`() {

val user = USER
val res = mockMvc
.perform(
post("/caseload/user/${user.username}/search?sortBy=surname.desc.ssss").withToken()
.withJson(UserSearchFilter(nameOrCrn = null, nextContact = null, sentence = null))
)
.andExpect(status().isBadRequest)
.andReturn().response.contentAsJson<ErrorResponse>()
assertThat(res.message, equalTo("Sort criteria invalid format"))
}

@Test
fun `caseload search throws a bad request when the sortBy is not implemented`() {

val user = USER
val res = mockMvc
.perform(
post("/caseload/user/${user.username}/search?sortBy=sausages.desc").withToken()
.withJson(UserSearchFilter(nameOrCrn = null, nextContact = null, sentence = null))
)
.andExpect(status().isBadRequest)
.andReturn().response.contentAsJson<ErrorResponse>()
assertThat(res.message, equalTo("Sort by sausages.desc is not implemented"))
}

@Test
fun `caseload search throws a not found when the user is not known`() {
mockMvc
.perform(
post("/caseload/user/NOT_KNOWN/search?sortBy=sentence.desc").withToken()
.withJson(UserSearchFilter(nameOrCrn = null, nextContact = null, sentence = null))
)
.andExpect(status().isNotFound)
}

@Test
fun `caseload search throws a bad request when no request body is found`() {
val user = USER
mockMvc
.perform(post("/caseload/user/${user.username}/search?sortBy=sentence.desc").withToken())
.andExpect(status().isBadRequest)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ package uk.gov.justice.digital.hmpps.api.controller
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Sort
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.*
import uk.gov.justice.digital.hmpps.api.model.user.UserSearchFilter
import uk.gov.justice.digital.hmpps.exception.InvalidRequestException
import uk.gov.justice.digital.hmpps.integrations.delius.user.entity.CaseloadOrderType
import uk.gov.justice.digital.hmpps.service.UserService

@RestController
Expand All @@ -21,6 +25,16 @@ class CaseloadController(private val userService: UserService) {
@RequestParam(required = false, defaultValue = "100") size: Int
) = userService.getUserCaseload(username, PageRequest.of(page, size))

@PostMapping("/user/{username}/search")
@Operation(summary = "Gets caseloads for the user based on search filter")
fun searchUserCaseload(
@PathVariable username: String,
@RequestParam(required = false, defaultValue = "0") page: Int,
@RequestParam(required = false, defaultValue = "100") size: Int,
@RequestParam(required = false, defaultValue = "nextContact.desc") sortBy: String,
@RequestBody body: UserSearchFilter
) = userService.searchUserCaseload(username, body, PageRequest.of(page, size, sort(sortBy)))

@GetMapping("/user/{username}/teams")
@Operation(summary = "Gets the users teams")
fun getUserTeams(@PathVariable username: String) = userService.getUserTeams(username)
Expand All @@ -37,4 +51,17 @@ class CaseloadController(private val userService: UserService) {
@GetMapping("/team/{teamCode}/staff")
@Operation(summary = "Gets the staff within the team")
fun getTeamStaff(@PathVariable teamCode: String) = userService.getTeamStaff(teamCode)

private fun sort(sortString: String): Sort {

val regex = Regex(pattern = "[A-Z]+\\.(ASC|DESC)", options = setOf(RegexOption.IGNORE_CASE))
if (!regex.matches(sortString)) {
throw InvalidRequestException("Sort criteria invalid format")
}
val sortBy = sortString.split(".")[0].replace("(?<=.)[A-Z]".toRegex(), "_$0").uppercase()
val direction = sortString.split(".")[1].uppercase()
val sortType = runCatching { CaseloadOrderType.valueOf(sortBy) }.getOrNull()
?: throw InvalidRequestException("Sort by $sortString is not implemented")
return Sort.by(Sort.Direction.valueOf(direction), sortType.sortColumn)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,9 @@ import java.time.ZonedDateTime
data class NextAppointment(
val date: ZonedDateTime,
val description: String
)

data class Appointment(
val date: ZonedDateTime,
val description: String
)
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
package uk.gov.justice.digital.hmpps.api.model.user

import uk.gov.justice.digital.hmpps.api.model.Name
import uk.gov.justice.digital.hmpps.api.model.overview.Appointment
import java.time.LocalDate

data class StaffCase(
val caseName: Name,
val crn: String
val crn: String,
val dob: LocalDate,
val nextAppointment: Appointment? = null,
val previousAppointment: Appointment? = null,
val latestSentence: String? = null
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package uk.gov.justice.digital.hmpps.api.model.user

data class UserSearchFilter(
val nameOrCrn: String? = null,
val sentence: String? = null,
val nextContact: String? = null
)
Loading
Loading