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

Added endpoint to return NOMS number. #512

Merged
merged 3 commits into from
Nov 12, 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
@@ -1,6 +1,9 @@
package uk.gov.justice.digital.hmpps.hmppsintegrationapi.controllers.v1

import io.swagger.v3.oas.annotations.Operation
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
Expand All @@ -10,6 +13,7 @@ import uk.gov.justice.digital.hmpps.hmppsintegrationapi.exception.EntityNotFound
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.extensions.decodeUrlCharacters
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.DataResponse
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.HmppsId
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.NomisNumber
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApiError
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.GetHmppsIdService
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.internal.AuditService
Expand Down Expand Up @@ -37,4 +41,34 @@ class HmppsIdController(

return DataResponse(response.data)
}

@GetMapping("{encodedHmppsId}/nomis-number")
@Operation(
summary = "Return NOMS number for a given hmpps Id",
description = """Accepts a HMPPS Id (hmppsId) and looks up the corresponding NOMS number.
""",
responses = [
ApiResponse(responseCode = "200", useReturnTypeSchema = true),
ApiResponse(responseCode = "404", description = "NOMS number could not be found."),
ApiResponse(responseCode = "400", description = "Invalid hmppsId."),
],
)
fun getNomisNumberByHMPPSID(
@PathVariable encodedHmppsId: String,
): DataResponse<NomisNumber?> {
val hmppsId = encodedHmppsId.decodeUrlCharacters()

val response = getHmppsIdService.getNomisNumber(hmppsId)

if (response.hasError(UpstreamApiError.Type.ENTITY_NOT_FOUND)) {
throw EntityNotFoundException("Could not find nomis number for HMPPS ID: $hmppsId")
}
if (response.hasError(UpstreamApiError.Type.BAD_REQUEST)) {
throw ValidationException("Invalid HMPPS ID: $hmppsId")
}

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

return DataResponse(response.data)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps

data class NomisNumber(
val nomisNumber: String? = null,
)
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package uk.gov.justice.digital.hmpps.hmppsintegrationapi.services
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.HmppsId
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.NomisNumber
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Response

@Service
Expand All @@ -17,4 +18,13 @@ class GetHmppsIdService(
errors = personResponse.errors,
)
}

fun getNomisNumber(hmppsId: String): Response<NomisNumber?> {
val nomisResponse = getPersonService.getNomisNumber(hmppsId = hmppsId)

return Response(
data = NomisNumber(nomisNumber = nomisResponse.data?.nomisNumber),
errors = nomisResponse.errors,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.gateways.PrisonerOffenderSearchGateway
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.gateways.ProbationOffenderSearchGateway
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.NomisNumber
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.OffenderSearchResponse
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Person
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Response
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApi
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApiError

@Service
class GetPersonService(
Expand All @@ -19,6 +22,68 @@ class GetPersonService(
return Response(data = personFromProbationOffenderSearch.data, errors = personFromProbationOffenderSearch.errors)
}

enum class IdentifierType {
NOMS,
CRN,
UNKNOWN,
}

fun identifyHmppsId(input: String): IdentifierType {
val nomsPattern = Regex("^[A-Z]\\d{4}[A-Z]{2}$")
val crnPattern = Regex("^[A-Z]{2}\\d{6}$")

return when {
nomsPattern.matches(input) -> IdentifierType.NOMS
crnPattern.matches(input) -> IdentifierType.CRN
else -> IdentifierType.UNKNOWN
}
}

/**
* Identify whether the hmppsId is a noms number or a crn
* When it is a noms number then return it.
* When it is a CRN look up the prisoner in probation offender search and then return it
*/
fun getNomisNumber(hmppsId: String): Response<NomisNumber?> {
return when (identifyHmppsId(hmppsId)) {
IdentifierType.NOMS -> Response(data = NomisNumber(hmppsId))

IdentifierType.CRN -> {
val personFromProbationOffenderSearch = probationOffenderSearchGateway.getPerson(id = hmppsId)
val nomisNumber = personFromProbationOffenderSearch.data?.identifiers?.nomisNumber
val errors = personFromProbationOffenderSearch.errors.toMutableList()

if (nomisNumber == null) {
errors.add(
UpstreamApiError(
description = "NOMIS number not found",
type = UpstreamApiError.Type.ENTITY_NOT_FOUND,
causedBy = UpstreamApi.PROBATION_OFFENDER_SEARCH,
),
)
}

Response(
data = nomisNumber?.let { NomisNumber(it) },
errors = errors,
)
}

IdentifierType.UNKNOWN ->
Response(
data = null,
errors =
listOf(
UpstreamApiError(
description = "Invalid HMPPS ID: $hmppsId",
type = UpstreamApiError.Type.BAD_REQUEST,
causedBy = UpstreamApi.NOMIS,
),
),
)
}
}

fun getCombinedDataForPerson(hmppsId: String): Response<OffenderSearchResponse> {
val probationResponse = probationOffenderSearchGateway.getPerson(id = hmppsId)

Expand Down
1 change: 1 addition & 0 deletions src/main/resources/application-integration-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ authorisation:
- "/v1/persons/.*/cell-location"
- "/v1/epf/person-details/.*/[^/]*$"
- "/v1/hmpps/id/nomis-number/[^/]*$"
- "/v1/hmpps/id/.*/nomis-number"
- "/health"
- "/health/ping"
- "/health/readiness"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,56 @@ class HmppsIdIntegrationTest : IntegrationTestBase() {
),
)
}

@Test
fun `gets the nomis Id for a HMPPSID where the id is a crn`() {
callApi("/v1/hmpps/id/$crn/nomis-number")
.andExpect(status().isOk)
.andExpect(
content().json(
"""
{"data":{"nomisNumber":"G5555TT"}}
""",
),
)
}

@Test
fun `gets the nomis Id for a HMPPSID where the id is a NOMIS id`() {
callApi("/v1/hmpps/id/$nomsId/nomis-number")
.andExpect(status().isOk)
.andExpect(
content().json(
"""
{"data":{"nomisNumber":"G2996UX"}}
""",
),
)
}

@Test
fun `gets the nomis Id for a HMPPSID where the id is invalid`() {
callApi("/v1/hmpps/id/invalidId/nomis-number")
.andExpect(status().is4xxClientError)
.andExpect(
content().json(
"""
{"userMessage":"Invalid HMPPS ID: invalidId","developerMessage":"Validation failure: Invalid HMPPS ID: invalidId"}
""",
),
)
}

@Test
fun `gets the nomis Id for a HMPPSID where the id not found`() {
callApi("/v1/hmpps/id/invalidId/nomis-number")
.andExpect(status().is4xxClientError)
.andExpect(
content().json(
"""
{"userMessage":"Invalid HMPPS ID: invalidId","developerMessage":"Validation failure: Invalid HMPPS ID: invalidId","moreInfo":null}
""",
),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ abstract class IntegrationTestBase {
final val basePath = "/v1/persons"
final val pnc = URLEncoder.encode("2004/13116M", StandardCharsets.UTF_8)
final val nomsId = "G2996UX"
final val crn = "ABC123"
final val crn = "AB123123"

companion object {
private val hmppsAuthMockServer = HmppsAuthMockServer()
Expand Down
Loading