Skip to content

Commit

Permalink
[BTD-603] No match end-point (#126)
Browse files Browse the repository at this point in the history
* [BTD-603] No match end-point

* [BTD-603] Addressed review comments

* [BTD-603] Addressed review comments

* [BTD-603] Addressed review comments
  • Loading branch information
paddynski-moj authored Mar 10, 2025
1 parent 0d45e67 commit 41a3a27
Show file tree
Hide file tree
Showing 8 changed files with 174 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,20 @@ package uk.gov.justice.digital.hmpps.learnerrecordsapi.models.request
import jakarta.validation.constraints.Pattern
import uk.gov.justice.digital.hmpps.learnerrecordsapi.models.db.MatchEntity

open class ConfirmNoMatchRequest(
open val matchType: MatchType,

open val countOfReturnedUlns: String,

) {

open fun asMatchEntity(nomisId: String): MatchEntity = MatchEntity(
nomisId = nomisId,
matchType = matchType.toString(),
countOfReturnedUlns = countOfReturnedUlns.orEmpty(),
)
}

class ConfirmMatchRequest(

@field:Pattern(regexp = "^[0-9]{1,10}\$")
Expand All @@ -14,18 +28,15 @@ class ConfirmMatchRequest(
@field:Pattern(regexp = "^[A-Za-z' ,.-]{3,35}$")
val familyName: String,

val matchType: MatchType? = null,
override val matchType: MatchType,

val countOfReturnedUlns: String? = null,
override val countOfReturnedUlns: String,

) {
fun asMatchEntity(nomisId: String): MatchEntity = MatchEntity(
null,
nomisId,
matchingUln,
givenName,
familyName,
matchType.toString(),
countOfReturnedUlns.orEmpty(),
) : ConfirmNoMatchRequest(matchType, countOfReturnedUlns) {

override fun asMatchEntity(nomisId: String): MatchEntity = super.asMatchEntity(nomisId).copy(
matchedUln = matchingUln,
givenName = givenName,
familyName = familyName,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import com.fasterxml.jackson.annotation.JsonValue

enum class MatchType(val description: String) {
NO_MATCH_RETURNED_FROM_LRS("No match returned from LRS"),
NO_MATCH_SELECTED("No match selected"),
EXACT_MATCH("Exact match"),
POSSIBLE_MATCH("Possible match"),
LINKED_LEARNER_MATCH("linked learner match"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package uk.gov.justice.digital.hmpps.learnerrecordsapi.openapi

import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.Parameter
import io.swagger.v3.oas.annotations.enums.ParameterIn
import io.swagger.v3.oas.annotations.media.Content
import io.swagger.v3.oas.annotations.media.ExampleObject
import io.swagger.v3.oas.annotations.media.Schema
import io.swagger.v3.oas.annotations.parameters.RequestBody
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.security.SecurityRequirement
import uk.gov.justice.digital.hmpps.learnerrecordsapi.config.Roles.ROLE_LEARNERS_UI
import uk.gov.justice.digital.hmpps.learnerrecordsapi.models.request.ConfirmNoMatchRequest
import uk.gov.justice.hmpps.kotlin.common.ErrorResponse

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@Operation(
summary = "Confirm that a match is not possible",
description = "Confirm a no match for a NomisID",
parameters = [
Parameter(name = "X-Username", `in` = ParameterIn.HEADER, required = true),
Parameter(name = "nomisId", `in` = ParameterIn.PATH, required = true),
],
requestBody = RequestBody(
description = "Match Type, and Count of Returned ULNs to match with the NomisID in the path",
required = true,
content = [
Content(
mediaType = "application/json",
schema = Schema(implementation = ConfirmNoMatchRequest::class),
examples = [
ExampleObject(
name = "Confirm no match Request",
value = """
{
"matchType": "NO_MATCH_RETURNED_FROM_LRS",
"countOfReturnedUlns": "0"
}
""",
),
],
),
],
),
security = [SecurityRequirement(name = ROLE_LEARNERS_UI)],
responses = [
ApiResponse(
responseCode = "201",
description = "The request was successful and the no match was recorded.",
),
ApiResponse(
responseCode = "401",
description = "Unauthorized to access this endpoint",
content = [Content(mediaType = "application/json", schema = Schema(implementation = ErrorResponse::class))],
),
ApiResponse(
responseCode = "403",
description = "Forbidden to access this endpoint",
content = [Content(mediaType = "application/json", schema = Schema(implementation = ErrorResponse::class))],
),
],
)
annotation class NoMatchConfirmApi
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,18 @@ import uk.gov.justice.digital.hmpps.learnerrecordsapi.logging.LoggerUtil.log
import uk.gov.justice.digital.hmpps.learnerrecordsapi.models.lrsapi.response.exceptions.MatchNotFoundException
import uk.gov.justice.digital.hmpps.learnerrecordsapi.models.lrsapi.response.exceptions.MatchNotPossibleException
import uk.gov.justice.digital.hmpps.learnerrecordsapi.models.request.ConfirmMatchRequest
import uk.gov.justice.digital.hmpps.learnerrecordsapi.models.request.ConfirmNoMatchRequest
import uk.gov.justice.digital.hmpps.learnerrecordsapi.models.response.CheckMatchResponse
import uk.gov.justice.digital.hmpps.learnerrecordsapi.models.response.CheckMatchStatus
import uk.gov.justice.digital.hmpps.learnerrecordsapi.models.response.LearnerEventsResponse
import uk.gov.justice.digital.hmpps.learnerrecordsapi.openapi.LearnerEventsByNomisIdApi
import uk.gov.justice.digital.hmpps.learnerrecordsapi.openapi.MatchCheckApi
import uk.gov.justice.digital.hmpps.learnerrecordsapi.openapi.MatchConfirmApi
import uk.gov.justice.digital.hmpps.learnerrecordsapi.openapi.NoMatchConfirmApi
import uk.gov.justice.digital.hmpps.learnerrecordsapi.service.LearnerEventsService
import uk.gov.justice.digital.hmpps.learnerrecordsapi.service.MatchService
import uk.gov.justice.hmpps.kotlin.auth.HmppsAuthenticationHolder
import uk.gov.justice.hmpps.sqs.audit.HmppsAuditService
import java.net.URI

@RestController
@RequestMapping(value = ["/match"], produces = ["application/json"])
Expand Down Expand Up @@ -77,7 +78,21 @@ class MatchResource(
): ResponseEntity<Void> {
logger.log("Received a post request to confirm match endpoint", confirmMatchRequest)
matchService.saveMatch(nomisId, confirmMatchRequest)
return ResponseEntity.created(URI.create("/match/$nomisId")).build()
return ResponseEntity.status(HttpStatus.CREATED).build()
}

@PreAuthorize("hasRole('$ROLE_LEARNERS_UI')")
@PostMapping(value = ["/{nomisId}/no-match"])
@Tag(name = "Match")
@NoMatchConfirmApi
suspend fun confirmNoMatch(
@RequestHeader("X-Username", required = true) userName: String,
@PathVariable(name = "nomisId", required = true) nomisId: String,
@RequestBody @Valid confirmNoMatchRequest: ConfirmNoMatchRequest,
): ResponseEntity<Void> {
logger.log("Received a post request to confirm no match endpoint")
matchService.saveNoMatch(nomisId, confirmNoMatchRequest)
return ResponseEntity.status(HttpStatus.CREATED).build()
}

@PreAuthorize("hasRole('$ROLE_LEARNERS_RO')")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package uk.gov.justice.digital.hmpps.learnerrecordsapi.service

import org.springframework.stereotype.Service
import uk.gov.justice.digital.hmpps.learnerrecordsapi.models.request.ConfirmMatchRequest
import uk.gov.justice.digital.hmpps.learnerrecordsapi.models.request.ConfirmNoMatchRequest
import uk.gov.justice.digital.hmpps.learnerrecordsapi.models.response.CheckMatchResponse
import uk.gov.justice.digital.hmpps.learnerrecordsapi.models.response.CheckMatchStatus
import uk.gov.justice.digital.hmpps.learnerrecordsapi.repository.MatchRepository
Expand Down Expand Up @@ -34,6 +35,11 @@ class MatchService(
return matchRepository.save(entity).id
}

fun saveNoMatch(nomisId: String, confirmNoMatchRequest: ConfirmNoMatchRequest): Long? {
val entity = confirmNoMatchRequest.asMatchEntity(nomisId)
return matchRepository.save(entity).id
}

fun getDataForSubjectAccessRequest(
nomisId: String,
fromDate: LocalDate?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import uk.gov.justice.digital.hmpps.learnerrecordsapi.integration.wiremock.LRSAp
import uk.gov.justice.digital.hmpps.learnerrecordsapi.models.db.MatchEntity
import uk.gov.justice.digital.hmpps.learnerrecordsapi.models.lrsapi.response.LearningEvent
import uk.gov.justice.digital.hmpps.learnerrecordsapi.models.request.ConfirmMatchRequest
import uk.gov.justice.digital.hmpps.learnerrecordsapi.models.request.ConfirmNoMatchRequest
import uk.gov.justice.digital.hmpps.learnerrecordsapi.models.request.Gender
import uk.gov.justice.digital.hmpps.learnerrecordsapi.models.request.LearnerEventsRequest
import uk.gov.justice.digital.hmpps.learnerrecordsapi.models.request.MatchType
Expand Down Expand Up @@ -76,8 +77,6 @@ class MatchResourceIntTest : IntegrationTestBase() {
val matchedUln = "A"
val givenName = "John"
val familyName = "Smith"
val dateOfBirth = "1990-01-01"
val gender = "MALE"

private fun checkGetWebCall(
nomisId: String,
Expand All @@ -86,8 +85,6 @@ class MatchResourceIntTest : IntegrationTestBase() {
expectedUln: String? = null,
expectedGivenName: String? = null,
expectedFamilyName: String? = null,
expectedDateOfBirth: String? = null,
expectedGender: String? = null,
) {
listOf(ROLE_LEARNERS_RO, ROLE_LEARNERS_UI).forEach { role ->
val executedRequest = webTestClient.get()
Expand Down Expand Up @@ -152,8 +149,6 @@ class MatchResourceIntTest : IntegrationTestBase() {
matchedUln,
givenName,
familyName,
dateOfBirth,
gender,
)
}

Expand Down Expand Up @@ -182,6 +177,7 @@ class MatchResourceIntTest : IntegrationTestBase() {
val actualResponse = postMatch(nomisId, uln, 201)
verify(matchService, times(1)).saveMatch(any(), any())
actualResponse.expectStatus().isCreated
checkSavedUln(nomisId, uln)
}

@Test
Expand Down Expand Up @@ -405,4 +401,28 @@ class MatchResourceIntTest : IntegrationTestBase() {
)
assertThat(actualResponse).isEqualTo(expectedResponse)
}

private fun postNoMatch(nomisId: String, expectedStatus: Int): WebTestClient.ResponseSpec = webTestClient.post()
.uri("/match/$nomisId/no-match")
.headers(setAuthorisation(roles = listOf(ROLE_LEARNERS_UI)))
.header("X-Username", "TestUser")
.bodyValue(ConfirmNoMatchRequest(MatchType.NO_MATCH_RETURNED_FROM_LRS, "0"))
.accept(MediaType.parseMediaType("application/json"))
.exchange()
.expectStatus()
.isEqualTo(expectedStatus)

private fun checkSavedUln(nomisId: String, expected: String) {
val entity = matchRepository.findFirstByNomisIdOrderByIdDesc(nomisId)
assertThat(entity?.matchedUln).isEqualTo(expected)
}

@Test
fun `POST to confirm no match should return 201 CREATED with a response confirming a match`() {
val nomisId = "A1417AE"
val actualResponse = postNoMatch(nomisId, 201)
verify(matchService, times(1)).saveNoMatch(any(), any())
actualResponse.expectStatus().isCreated
checkSavedUln(nomisId, "")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import uk.gov.justice.digital.hmpps.learnerrecordsapi.config.Roles.ROLE_LEARNERS
import uk.gov.justice.digital.hmpps.learnerrecordsapi.config.Roles.ROLE_LEARNERS_UI
import uk.gov.justice.digital.hmpps.learnerrecordsapi.models.lrsapi.response.exceptions.MatchNotFoundException
import uk.gov.justice.digital.hmpps.learnerrecordsapi.models.lrsapi.response.exceptions.MatchNotPossibleException
import uk.gov.justice.digital.hmpps.learnerrecordsapi.models.request.ConfirmNoMatchRequest
import uk.gov.justice.digital.hmpps.learnerrecordsapi.models.request.MatchType
import uk.gov.justice.digital.hmpps.learnerrecordsapi.models.response.CheckMatchResponse
import uk.gov.justice.digital.hmpps.learnerrecordsapi.models.response.CheckMatchStatus
import uk.gov.justice.digital.hmpps.learnerrecordsapi.service.LearnerEventsService
Expand Down Expand Up @@ -135,6 +137,20 @@ class MatchResourceTest {
val actual = matchResource.findLearnerEventsByNomisId(nomisId, "")
assertThat(actual.statusCode).isEqualTo(HttpStatus.OK)
}

@Test
fun `should save No Match`(): Unit = runTest {
`when`(mockMatchService.saveNoMatch(any(), any())).thenReturn(1L)
val actual = matchResource.confirmNoMatch(
"",
nomisId,
ConfirmNoMatchRequest(
matchType = MatchType.NO_MATCH_RETURNED_FROM_LRS,
countOfReturnedUlns = "1",
),
)
assertThat(actual.statusCode).isEqualTo(HttpStatus.CREATED)
}
}

fun CheckMatchResponse.setStatus() = this.copy(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,16 @@ import org.mockito.junit.jupiter.MockitoExtension
import org.mockito.kotlin.any
import uk.gov.justice.digital.hmpps.learnerrecordsapi.models.db.MatchEntity
import uk.gov.justice.digital.hmpps.learnerrecordsapi.models.request.ConfirmMatchRequest
import uk.gov.justice.digital.hmpps.learnerrecordsapi.models.request.Gender
import uk.gov.justice.digital.hmpps.learnerrecordsapi.models.request.ConfirmNoMatchRequest
import uk.gov.justice.digital.hmpps.learnerrecordsapi.models.request.MatchType
import uk.gov.justice.digital.hmpps.learnerrecordsapi.repository.MatchRepository
import uk.gov.justice.digital.hmpps.learnerrecordsapi.utils.toISOFormat
import java.time.LocalDate

@ExtendWith(MockitoExtension::class)
class MatchServiceTest {
private val nomisId = "A1234BC"
private val matchedUln = "a1234"
private val givenName = "John"
private val familyName = "Smith"
private val dateOfBirth = LocalDate.now().toISOFormat()
private val gender = Gender.MALE.name

private lateinit var mockMatchRepository: MatchRepository
private lateinit var matchService: MatchService
Expand Down Expand Up @@ -86,4 +82,26 @@ class MatchServiceTest {
)
assertThat(savedId).isEqualTo(id)
}

@Test
fun `saveNoMatch should return id of the saved entity`() {
val id = 1L

`when`(mockMatchRepository.save(any())).thenReturn(
MatchEntity(
id = id,
nomisId = nomisId,
matchedUln = "",
),
)

val savedId = matchService.saveNoMatch(
nomisId,
ConfirmNoMatchRequest(
matchType = MatchType.NO_MATCH_RETURNED_FROM_LRS,
countOfReturnedUlns = "1",
),
)
assertThat(savedId).isEqualTo(id)
}
}

0 comments on commit 41a3a27

Please sign in to comment.