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

HMAI-313 - Add prison filters to /v1/person/{hmppsId}/addresses and make available to private prisons #743

Merged
merged 5 commits into from
Mar 12, 2025
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 @@ -6,16 +6,18 @@ import io.swagger.v3.oas.annotations.media.Content
import io.swagger.v3.oas.annotations.media.Schema
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.tags.Tag
import jakarta.validation.ValidationException
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestAttribute
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.exception.EntityNotFoundException
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.extensions.decodeUrlCharacters
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Address
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.DataResponse
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApiError
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.roleconfig.ConsumerFilters
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.GetAddressesForPersonService
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.internal.AuditService

Expand All @@ -26,26 +28,33 @@ class AddressController(
@Autowired val auditService: AuditService,
@Autowired val getAddressesForPersonService: GetAddressesForPersonService,
) {
@GetMapping("{encodedHmppsId}/addresses")
@GetMapping("{hmppsId}/addresses")
@Operation(
summary = "Returns addresses associated with a person, ordered by startDate.",
description = "<b>Applicable filters</b>: <ul><li>prisons</li></ul>",
responses = [
ApiResponse(responseCode = "200", useReturnTypeSchema = true, description = "Successfully found a person with the provided HMPPS ID."),
ApiResponse(responseCode = "400", content = [Content(schema = Schema(ref = "#/components/schemas/BadRequest"))]),
ApiResponse(responseCode = "404", content = [Content(schema = Schema(ref = "#/components/schemas/PersonNotFound"))]),
ApiResponse(responseCode = "500", content = [Content(schema = Schema(ref = "#/components/schemas/InternalServerError"))]),
],
)
fun getPersonAddresses(
@Parameter(description = "A URL-encoded HMPPS identifier", example = "2008%2F0545166T") @PathVariable encodedHmppsId: String,
@Parameter(description = "The HMPPS ID of the person", example = "G2996UX") @PathVariable hmppsId: String,
@RequestAttribute filters: ConsumerFilters?,
): DataResponse<List<Address>> {
val hmppsId = encodedHmppsId.decodeUrlCharacters()
val response = getAddressesForPersonService.execute(hmppsId)
val response = getAddressesForPersonService.execute(hmppsId, filters)

if (response.hasError(UpstreamApiError.Type.BAD_REQUEST)) {
throw ValidationException("Invalid id: $hmppsId")
}

if (response.hasError(UpstreamApiError.Type.ENTITY_NOT_FOUND)) {
throw EntityNotFoundException("Could not find person with id: $hmppsId")
}

auditService.createEvent("GET_PERSON_ADDRESS", mapOf("hmppsId" to hmppsId))

return DataResponse(response.data)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,19 @@ import uk.gov.justice.digital.hmpps.hmppsintegrationapi.gateways.ProbationOffend
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Address
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Response
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApiError
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.roleconfig.ConsumerFilters

@Service
class GetAddressesForPersonService(
@Autowired val nomisGateway: NomisGateway,
@Autowired val getPersonService: GetPersonService,
@Autowired val probationOffenderSearchGateway: ProbationOffenderSearchGateway,
) {
fun execute(hmppsId: String): Response<List<Address>> {
val personResponse = getPersonService.execute(hmppsId = hmppsId)
fun execute(
hmppsId: String,
filters: ConsumerFilters?,
): Response<List<Address>> {
val personResponse = getPersonService.getNomisNumberWithPrisonFilter(hmppsId = hmppsId, filters)
if (personResponse.errors.isNotEmpty()) {
return Response(data = emptyList(), errors = personResponse.errors)
}
Expand All @@ -25,10 +29,7 @@ class GetAddressesForPersonService(
return Response(data = emptyList(), errors = addressesFromDelius.errors)
}

val nomisNumber = personResponse.data?.identifiers?.nomisNumber
if (nomisNumber == null) {
return addressesFromDelius
}
val nomisNumber = personResponse.data?.nomisNumber ?: return addressesFromDelius

val addressesFromNomis = nomisGateway.getAddressesForPerson(id = nomisNumber)
if (hasErrorOtherThanEntityNotFound(addressesFromNomis)) {
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/globals.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ globals:
- "/v1/hmpps/id/nomis-number/by-hmpps-id/[^/]*$"
- "/v1/persons"
- "/v1/persons/[^/]*$"
- "/v1/persons/.*/addresses"
- "/v1/persons/.*/contacts[^/]*$"
- "/v1/persons/.*/iep-level"
- "/v1/persons/.*/visitor/.*/restrictions"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import io.kotest.matchers.shouldBe
import org.mockito.Mockito
import org.mockito.internal.verification.VerificationModeFactory
import org.mockito.kotlin.doThrow
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
import org.springframework.beans.factory.annotation.Autowired
Expand All @@ -22,8 +23,6 @@ import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApi
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApiError
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.GetAddressesForPersonService
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.internal.AuditService
import java.net.URLEncoder
import java.nio.charset.StandardCharsets

@WebMvcTest(controllers = [AddressController::class])
@ActiveProfiles("test")
Expand All @@ -33,76 +32,84 @@ internal class AddressControllerTest(
@MockitoBean val auditService: AuditService,
) : DescribeSpec(
{
val hmppsId = "2003/13116M"
val encodedHmppsId = URLEncoder.encode(hmppsId, StandardCharsets.UTF_8)
val basePath = "/v1/persons"
val hmppsId = "G2996UX"
val path = "/v1/persons/$hmppsId/addresses"
val filters = null
val mockMvc = IntegrationAPIMockMvc(springMockMvc)

describe("GET $basePath/{encodedHmppsId}/addresses") {
describe("GET $path") {
beforeTest {
Mockito.reset(getAddressesForPersonService)
Mockito.reset(auditService)
whenever(getAddressesForPersonService.execute(hmppsId)).thenReturn(Response(data = listOf(generateTestAddress())))
}

it("returns a 200 OK status code") {
val result = mockMvc.performAuthorised("$basePath/$encodedHmppsId/addresses")

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

it("gets the addresses for a person with the matching ID") {
mockMvc.performAuthorised("$basePath/$encodedHmppsId/addresses")

verify(getAddressesForPersonService, VerificationModeFactory.times(1)).execute(hmppsId)
whenever(getAddressesForPersonService.execute(hmppsId, null)).thenReturn(Response(data = listOf(generateTestAddress())))
}

it("returns the addresses for a person with the matching ID") {
val result = mockMvc.performAuthorised("$basePath/$encodedHmppsId/addresses")
it("returns a 200 OK status code with data") {
val result = mockMvc.performAuthorised(path)
result.response.status.shouldBe(HttpStatus.OK.value())
result.response.contentAsString.shouldBe(
"""
{
"data": [
{
"country": "England",
"county": "Greater London",
"endDate": "2023-05-20",
"locality": "London Bridge",
"name": "The chocolate factory",
"noFixedAddress": false,
"number": "89",
"postcode": "SE1 1TZ",
"startDate": "2021-05-10",
"street": "Omeara",
"town": "London Town",
"types": [
{
"code": "A99",
"description": "Chocolate Factory"
},
"data": [
{
"code": "B99",
"description": "Glass Elevator"
"country": "England",
"county": "Greater London",
"endDate": "2023-05-20",
"locality": "London Bridge",
"name": "The chocolate factory",
"noFixedAddress": false,
"number": "89",
"postcode": "SE1 1TZ",
"startDate": "2021-05-10",
"street": "Omeara",
"town": "London Town",
"types": [
{
"code": "A99",
"description": "Chocolate Factory"
},
{
"code": "B99",
"description": "Glass Elevator"
}
],
"notes": "some interesting note"
}
],
"notes": "some interesting note"
]
}
]
}
""".removeWhitespaceAndNewlines(),
""".removeWhitespaceAndNewlines(),
)
}

it("logs audit") {
mockMvc.performAuthorised("$basePath/$encodedHmppsId/addresses")
mockMvc.performAuthorised(path)
verify(
auditService,
VerificationModeFactory.times(1),
).createEvent("GET_PERSON_ADDRESS", mapOf("hmppsId" to hmppsId))
}

it("returns a 400 BAD REQUEST status code when bad request error is returned from address service") {
whenever(getAddressesForPersonService.execute(hmppsId, filters)).thenReturn(
Response(
data = emptyList(),
errors =
listOf(
UpstreamApiError(
causedBy = UpstreamApi.NOMIS,
type = UpstreamApiError.Type.BAD_REQUEST,
),
),
),
)

val result = mockMvc.performAuthorised(path)
result.response.status.shouldBe(HttpStatus.BAD_REQUEST.value())
}

it("returns a 404 NOT FOUND status code when entity not found error is returned from address service") {
whenever(getAddressesForPersonService.execute(hmppsId)).thenReturn(
whenever(getAddressesForPersonService.execute(hmppsId, filters)).thenReturn(
Response(
data = emptyList(),
errors =
Expand All @@ -115,17 +122,16 @@ internal class AddressControllerTest(
),
)

val result = mockMvc.performAuthorised("$basePath/$encodedHmppsId/addresses")

val result = mockMvc.performAuthorised(path)
result.response.status.shouldBe(HttpStatus.NOT_FOUND.value())
}

it("returns a 500 INTERNAL SERVER ERROR status code when upstream api return expected error") {
whenever(getAddressesForPersonService.execute(hmppsId)).doThrow(
whenever(getAddressesForPersonService.execute(hmppsId, filters)).doThrow(
WebClientResponseException(500, "MockError", null, null, null, null),
)

val result = mockMvc.performAuthorised("$basePath/$encodedHmppsId/addresses")
val result = mockMvc.performAuthorised(path)
assert(result.response.status == 500)
assert(
result.response.contentAsString.equals(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,30 @@ import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.integration.IntegrationTestBase

class AddressIntegrationTest : IntegrationTestBase() {
val path = "$basePath/$nomsId/addresses"

@Test
fun `returns addresses for a person`() {
callApi("$basePath/$pnc/addresses")
callApi(path)
.andExpect(status().isOk)
.andExpect(content().json(getExpectedResponse("person-addresses")))
}

@Test
fun `returns a 400 if the hmppsId is invalid`() {
callApi("$basePath/$invalidNomsId/addresses")
.andExpect(status().isBadRequest)
}

@Test
fun `returns a 404 for if consumer has empty list of prisons`() {
callApiWithCN(path, noPrisonsCn)
.andExpect(status().isNotFound)
}

@Test
fun `returns a 404 for prisoner in wrong prison`() {
callApiWithCN(path, limitedPrisonsCn)
.andExpect(status().isNotFound)
}
}
Loading