Skip to content

Commit

Permalink
feat(*): add new endpoint for get learning events by nomisId
Browse files Browse the repository at this point in the history
  • Loading branch information
malaw-moj committed Feb 26, 2025
1 parent 751a5b4 commit 5a52716
Show file tree
Hide file tree
Showing 10 changed files with 489 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import uk.gov.justice.digital.hmpps.learnerrecordsapi.logging.LoggerUtil
import uk.gov.justice.digital.hmpps.learnerrecordsapi.logging.LoggerUtil.errorLog
import uk.gov.justice.digital.hmpps.learnerrecordsapi.models.lrsapi.response.exceptions.DFEApiDownException
import uk.gov.justice.digital.hmpps.learnerrecordsapi.models.lrsapi.response.exceptions.LRSException
import uk.gov.justice.digital.hmpps.learnerrecordsapi.models.lrsapi.response.exceptions.MatchNotFoundException
import java.net.SocketTimeoutException

@RestControllerAdvice
Expand All @@ -29,6 +30,7 @@ class HmppsBoldLrsExceptionHandler {
val forbiddenAccessDenied = "Forbidden - Access Denied"
val dFEApiFailedToRespond = "DfE API failed to Respond"
val dfeApiDependencyFailed = "LRS API Dependency Failed - DfE API is under maintenance"
val individualNotMatched = "Individual with this NomisId has not been matched to a ULN yet"

data class ErrorResponse(
val status: HttpStatus,
Expand Down Expand Up @@ -217,10 +219,26 @@ class HmppsBoldLrsExceptionHandler {
status = HttpStatus.FAILED_DEPENDENCY,
errorCode = dFEApiFailedToRespond,
userMessage = dfeApiDependencyFailed,
developerMessage = "LRS API Dependency Failed - DfE API is under maintenance, please check DfE API maintenance window for more details",
moreInfo = "LRS API Dependency Failed - DfE API is under maintenance",
developerMessage = "$dfeApiDependencyFailed, please check DfE API maintenance window for more details",
moreInfo = dfeApiDependencyFailed,
)
logger.errorLog("LRS API Dependency Failed - DfE API is under maintenance")
logger.errorLog(dfeApiDependencyFailed)
return ResponseEntity(errorResponse, HttpStatus.FAILED_DEPENDENCY)
}

@ExceptionHandler(MatchNotFoundException::class)
fun handleMatchNotFoundException(
ex: MatchNotFoundException,
request: WebRequest,
): ResponseEntity<ErrorResponse> {
val errorResponse = ErrorResponse(
status = HttpStatus.NOT_FOUND,
errorCode = "Match not found",
userMessage = "No Match found for given NomisId ${ex.message}",
developerMessage = individualNotMatched,
moreInfo = individualNotMatched,
)
logger.errorLog(individualNotMatched)
return ResponseEntity(errorResponse, HttpStatus.NOT_FOUND)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package uk.gov.justice.digital.hmpps.learnerrecordsapi.models.lrsapi.response.exceptions

class MatchNotFoundException(nomisId: String) : RuntimeException() {
override val message: String = nomisId
}
Original file line number Diff line number Diff line change
Expand Up @@ -217,3 +217,167 @@ import uk.gov.justice.hmpps.kotlin.common.ErrorResponse
],
)
annotation class LearnerEventsApi

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@Operation(
summary = "Get learning events by Nomis ID",
description = "Get personal learning records and events by Nomis ID",
parameters = [Parameter(name = "X-Username", `in` = ParameterIn.HEADER, required = true)],
requestBody = RequestBody(
description = "Nomis ID of the learner",
required = true,
content = [
Content(
mediaType = "application/json",
examples = [
ExampleObject(
value = """
{
"nomisId": "123456"
}
""",
),
],
),
],
),
security = [SecurityRequirement(name = "learner-records-search-read-only-role")],
responses = [
ApiResponse(
responseCode = "200",
description = "The request was successful and a response was returned.",
content = [
Content(
mediaType = "application/json",
schema = Schema(implementation = LearnerEventsResponse::class),
examples = [
ExampleObject(
name = "Exact Match Response",
value = """
{
"searchParameters": {
"givenName": "Sean",
"familyName": "Findlay",
"uln": "1174112637",
"dateOfBirth": "1980-11-01",
"gender": "MALE"
},
"responseType": "Exact Match",
"foundUln": "1174112637",
"incomingUln": "1174112637",
"learnerRecord": [
{
"id": "2931",
"achievementProviderUkprn": "10030488",
"achievementProviderName": "LUTON PENTECOSTAL CHURCH",
"awardingOrganisationName": "UNKNOWN",
"qualificationType": "GCSE",
"subjectCode": "50079116",
"achievementAwardDate": "2011-10-24",
"credits": "0",
"source": "ILR",
"dateLoaded": "2012-05-31 16:47:04",
"underDataChallenge": "N",
"level": "",
"status": "F",
"subject": "GCSE in English Literature",
"grade": "9999999999",
"awardingOrganisationUkprn": "UNKNWN",
"collectionType": "W",
"returnNumber": "02",
"participationStartDate": "2011-10-02",
"participationEndDate": "2011-10-24"
}
]
}
""",
),
ExampleObject(
name = "Linked Learner Match Response",
value = """
{
"searchParameters": {
"givenName": "Connor",
"familyName": "Carroll",
"uln": "4444599390"
},
"responseType": "Linked Learner Match",
"foundUln": "6936002314",
"incomingUln": "4444599390",
"learnerRecord": [
{
"id": "4284",
"achievementProviderUkprn": "10032743",
"achievementProviderName": "PRIORSLEE PRIMARY SCHOOL ACADEMY TRUST",
"awardingOrganisationName": "UNKNOWN",
"qualificationType": "NVQ/GNVQ Key Skills Unit",
"subjectCode": "1000323X",
"achievementAwardDate": "2010-09-26",
"credits": "0",
"source": "ILR",
"dateLoaded": "2012-05-31 16:47:04",
"underDataChallenge": "N",
"level": "",
"status": "F",
"subject": "Key Skills in Application of Number - level 1",
"grade": "9999999999",
"awardingOrganisationUkprn": "UNKNWN",
"collectionType": "W",
"returnNumber": "02",
"participationStartDate": "2010-09-01",
"participationEndDate": "2010-09-26"
}
]
}
""",
),
ExampleObject(
name = "Learner opted to not share data Response",
value = """
{
"searchParameters": {
"givenName": "John",
"familyName": "Smith",
"uln": "1026922983"
},
"responseType": "Learner opted to not share data",
"foundUln": "1026922983",
"incomingUln": "1026922983",
"learnerRecord": []
}
""",
),
ExampleObject(
name = "Learner could not be verified Response",
value = """
{
"searchParameters": {
"givenName": "John",
"familyName": "Smith",
"uln": "1174112637"
},
"responseType": "Learner could not be verified",
"foundUln": "",
"incomingUln": "1174112637",
"learnerRecord": []
}
""",
),
],
),
],
),
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 LearnerEventsByNomisIdApi
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,38 @@ package uk.gov.justice.digital.hmpps.learnerrecordsapi.resource

import io.swagger.v3.oas.annotations.tags.Tag
import jakarta.validation.Valid
import net.minidev.json.JSONObject
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestHeader
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import uk.gov.justice.digital.hmpps.learnerrecordsapi.config.AuditEvent.createAuditEvent
import uk.gov.justice.digital.hmpps.learnerrecordsapi.logging.LoggerUtil
import uk.gov.justice.digital.hmpps.learnerrecordsapi.logging.LoggerUtil.log
import uk.gov.justice.digital.hmpps.learnerrecordsapi.models.db.MatchEntity
import uk.gov.justice.digital.hmpps.learnerrecordsapi.models.lrsapi.response.exceptions.MatchNotFoundException
import uk.gov.justice.digital.hmpps.learnerrecordsapi.models.request.LearnerEventsRequest
import uk.gov.justice.digital.hmpps.learnerrecordsapi.models.response.LearnerEventsResponse
import uk.gov.justice.digital.hmpps.learnerrecordsapi.openapi.LearnerEventsApi
import uk.gov.justice.digital.hmpps.learnerrecordsapi.openapi.LearnerEventsByNomisIdApi
import uk.gov.justice.digital.hmpps.learnerrecordsapi.service.LearnerEventsService
import uk.gov.justice.hmpps.sqs.audit.HmppsAuditService

@RestController
@PreAuthorize("hasRole('ROLE_LEARNER_RECORDS_SEARCH__RO')")
@RequestMapping(value = ["/learner-events"], produces = ["application/json"])
class LearnerEventsResource(
private val learnerEventsService: LearnerEventsService,
private val auditService: HmppsAuditService,
) {

val logger = LoggerUtil.getLogger<LearnerEventsResource>()
val searchLearnerEventsByULN = "SEARCH_LEARNER_EVENTS_BY_ULN"
val searchLearnerEventsByNomisId = "SEARCH_LEARNER_EVENTS_BY_NOMISID"

@PostMapping
@RequestMapping(value = ["/learner-events"], produces = ["application/json"])
@Tag(name = "Learning Events")
@LearnerEventsApi
suspend fun findByUln(
Expand All @@ -42,4 +45,24 @@ class LearnerEventsResource(
val learnerEventsResponse = learnerEventsService.getLearningEvents(learnerEventsRequest, userName)
return ResponseEntity.status(HttpStatus.OK).body(learnerEventsResponse)
}

@RequestMapping(value = ["/learner-events/nomisId"], produces = ["application/json"])
@Tag(name = "Learning Events By Nomis ID")
@LearnerEventsByNomisIdApi
suspend fun findLearnerEventsByNomisId(
@RequestBody nomisId: JSONObject,
@RequestHeader("X-Username", required = true) userName: String,
): ResponseEntity<LearnerEventsResponse> {
val nomisId = nomisId.getAsString("nomisId")
auditService.publishEvent(createAuditEvent(searchLearnerEventsByNomisId, userName, nomisId))
logger.log("Received a post request to learner events by Nomis ID endpoint", nomisId)
val matchEntity: MatchEntity? = learnerEventsService.getMatchEntityForNomisId(nomisId)
if (matchEntity == null) {
throw MatchNotFoundException(nomisId)
} else {
val learnerEventsRequest = learnerEventsService.formLearningEventRequestFromMatchEntity(matchEntity)
val learnerEventsResponse = learnerEventsService.getLearningEvents(learnerEventsRequest, userName)
return ResponseEntity.status(HttpStatus.OK).body(learnerEventsResponse)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ class MatchResource(
@PathVariable(name = "nomisId", required = true) nomisId: String,
@RequestBody @Valid confirmMatchRequest: ConfirmMatchRequest,
): ResponseEntity<Void> {
logger.log("Received a post request to confirm match endpoint", confirmMatchRequest)
logger.log("Received a post request to confirm match endpoint", confirmMatchRequest.toString())
matchService.saveMatch(confirmMatchRequest.asMatchEntity(nomisId))
return ResponseEntity.created(URI.create("/match/$nomisId")).build()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import uk.gov.justice.digital.hmpps.learnerrecordsapi.config.HttpClientConfigura
import uk.gov.justice.digital.hmpps.learnerrecordsapi.config.LRSConfiguration
import uk.gov.justice.digital.hmpps.learnerrecordsapi.logging.LoggerUtil
import uk.gov.justice.digital.hmpps.learnerrecordsapi.logging.LoggerUtil.debugLog
import uk.gov.justice.digital.hmpps.learnerrecordsapi.models.db.MatchEntity
import uk.gov.justice.digital.hmpps.learnerrecordsapi.models.lrsapi.response.LearningEventsResponse
import uk.gov.justice.digital.hmpps.learnerrecordsapi.models.lrsapi.response.exceptions.LRSException
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.response.LRSResponseType
import uk.gov.justice.digital.hmpps.learnerrecordsapi.models.response.LearnerEventsResponse
Expand All @@ -19,6 +21,8 @@ class LearnerEventsService(
private val httpClientConfiguration: HttpClientConfiguration,
@Autowired
private val lrsConfiguration: LRSConfiguration,
@Autowired
private val matchService: MatchService,
) : BaseService() {
private val logger: Logger = LoggerUtil.getLogger<LearnerEventsService>()

Expand Down Expand Up @@ -52,4 +56,14 @@ class LearnerEventsService(
learnerRecord = learningEventsResult.learnerRecord,
)
}

fun getMatchEntityForNomisId(nomisId: String): MatchEntity? = matchService.findMatch(MatchEntity(nomisId = nomisId))

fun formLearningEventRequestFromMatchEntity(matchEntity: MatchEntity): LearnerEventsRequest = LearnerEventsRequest(
matchEntity.givenName.orEmpty(),
matchEntity.familyName.orEmpty(),
matchEntity.matchedUln.orEmpty(),
matchEntity.dateOfBirth,
Gender.valueOf(matchEntity.gender.orEmpty()),
)
}
Loading

0 comments on commit 5a52716

Please sign in to comment.