Skip to content

Commit

Permalink
SDIT-1378 Provide Incident Reconciliation counts (#672)
Browse files Browse the repository at this point in the history
  • Loading branch information
karenmillermoj committed Jun 24, 2024
1 parent 317e9fd commit f683937
Show file tree
Hide file tree
Showing 13 changed files with 592 additions and 21 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
apiVersion: batch/v1
kind: CronJob
metadata:
name: {{ include "app.fullname" . }}-report-incidents
labels:
{{- include "app.labels" . | nindent 4 }}
spec:
schedule: "15 2 * * 1"
suspend: true
concurrencyPolicy: Forbid
failedJobsHistoryLimit: 5
startingDeadlineSeconds: 600
successfulJobsHistoryLimit: 5
jobTemplate:
spec:
# Tidy up all jobs after 4 days
ttlSecondsAfterFinished: 345600
template:
spec:
containers:
- name: report-incidents
image: ghcr.io/ministryofjustice/hmpps-devops-tools
args:
- /bin/sh
- -c
- curl --retry 2 -XPUT http://hmpps-prisoner-from-nomis-migration/incidents/reports/reconciliation
securityContext:
capabilities:
drop:
- ALL
runAsNonRoot: true
allowPrivilegeEscalation: false
seccompProfile:
type: RuntimeDefault
restartPolicy: Never
5 changes: 5 additions & 0 deletions helm_deploy/hmpps-prisoner-from-nomis-migration/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ generic-service:
deny all;
return 401;
}
location /incidents/reports/reconciliation {
deny all;
return 401;
}
host: app-hostname.local # override per environment
tlsSecretName: prisoner-from-nomis-cert

Expand Down Expand Up @@ -162,3 +166,4 @@ generic-prometheus-alerts:

cron:
retry_dlqs_schedule: "*/10 * * * *"
# TODO incidentsReportSchedule: "5 7 * * *"
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package uk.gov.justice.digital.hmpps.prisonerfromnomismigration.config

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class CoroutineScopes {
// since everything is non-blocking, we don't need to worry about blocking threads so use whatever thread
// the last coroutine was on, continue on that one rather than switching threads
@Bean
fun nonBlockingReportScope() = CoroutineScope(Dispatchers.Unconfined)
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ class ReactiveResourceServerConfiguration {
@Bean
fun resourceServerCustomizer() = ResourceServerConfigurationCustomizer {
unauthorizedRequestPaths {
addPaths = setOf("/queue-admin/retry-all-dlqs")
addPaths = setOf(
"/queue-admin/retry-all-dlqs",
"/incidents/reports/reconciliation",
)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package uk.gov.justice.digital.hmpps.prisonerfromnomismigration.incidents

import kotlinx.coroutines.reactive.awaitSingle
import kotlinx.coroutines.reactor.awaitSingle
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.data.domain.PageImpl
import org.springframework.stereotype.Service
import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.reactive.function.client.awaitBody
import uk.gov.justice.digital.hmpps.prisonerfromnomismigration.incidents.model.NomisCode
import uk.gov.justice.digital.hmpps.prisonerfromnomismigration.incidents.model.NomisHistory
import uk.gov.justice.digital.hmpps.prisonerfromnomismigration.incidents.model.NomisHistoryQuestion
Expand All @@ -23,8 +24,10 @@ import uk.gov.justice.digital.hmpps.prisonerfromnomismigration.nomissync.model.C
import uk.gov.justice.digital.hmpps.prisonerfromnomismigration.nomissync.model.History
import uk.gov.justice.digital.hmpps.prisonerfromnomismigration.nomissync.model.HistoryQuestion
import uk.gov.justice.digital.hmpps.prisonerfromnomismigration.nomissync.model.HistoryResponse
import uk.gov.justice.digital.hmpps.prisonerfromnomismigration.nomissync.model.IncidentAgencyId
import uk.gov.justice.digital.hmpps.prisonerfromnomismigration.nomissync.model.IncidentIdResponse
import uk.gov.justice.digital.hmpps.prisonerfromnomismigration.nomissync.model.IncidentResponse
import uk.gov.justice.digital.hmpps.prisonerfromnomismigration.nomissync.model.IncidentsReconciliationResponse
import uk.gov.justice.digital.hmpps.prisonerfromnomismigration.nomissync.model.Offender
import uk.gov.justice.digital.hmpps.prisonerfromnomismigration.nomissync.model.OffenderParty
import uk.gov.justice.digital.hmpps.prisonerfromnomismigration.nomissync.model.Question
Expand Down Expand Up @@ -64,6 +67,36 @@ class IncidentsNomisApiService(@Qualifier("nomisApiWebClient") private val webCl
.retrieve()
.bodyToMono(typeReference<RestResponsePage<IncidentIdResponse>>())
.awaitSingle()

suspend fun getAllAgencies(): List<IncidentAgencyId> =
webClient.get()
.uri("/incidents/reconciliation/agencies")
.retrieve()
.awaitBody()

suspend fun getIncidentsReconciliation(prisonId: String): IncidentsReconciliationResponse =
webClient.get()
.uri("incidents/reconciliation/agency/{agencyId}/counts", prisonId)
.retrieve()
.awaitBody()

/* TODO
suspend fun getOpenIncidentIds(
pageNumber: Long,
pageSize: Long,
): PageImpl<IncidentIdResponse> =
webClient.get()
.uri {
it.path("/incidents/ids")
.queryParam("open", true)
.queryParam("page", pageNumber)
.queryParam("size", pageSize)
.build()
}
.retrieve()
.bodyToMono(typeReference<RestResponsePage<IncidentIdResponse>>())
.awaitSingle()
*/
}

fun IncidentResponse.toMigrateUpsertNomisIncident() =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package uk.gov.justice.digital.hmpps.prisonerfromnomismigration.incidents

import com.microsoft.applicationinsights.TelemetryClient
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.web.bind.annotation.RestController
import uk.gov.justice.digital.hmpps.prisonerfromnomismigration.helpers.trackEvent

@RestController
@RequestMapping("/incidents/reports/reconciliation", produces = [MediaType.APPLICATION_JSON_VALUE])
class IncidentsReconciliationResource(
private val incidentsReconciliationService: IncidentsReconciliationService,
private val telemetryClient: TelemetryClient,
private val reportScope: CoroutineScope,

private val nomisIncidentsApiService: IncidentsNomisApiService,

) {
private companion object {
val log: Logger = LoggerFactory.getLogger(this::class.java)
}

@PutMapping
@ResponseStatus(HttpStatus.ACCEPTED)
suspend fun incidentsReconciliation() {
nomisIncidentsApiService.getAllAgencies()
.also { agencyIds ->
telemetryClient.trackEvent(
"incidents-reports-reconciliation-requested",
mapOf("prisonCount" to agencyIds.size),
)

reportScope.launch {
runCatching { incidentsReconciliationService.generateReconciliationReport(agencyIds) }
.onSuccess {
log.info("Incidents reconciliation report completed with ${it.size} mismatches")
telemetryClient.trackEvent(
"incidents-reports-reconciliation-report",
mapOf("mismatch-count" to it.size.toString(), "success" to "true") + it.asMap(),
)
}
.onFailure {
telemetryClient.trackEvent("incidents-reports-reconciliation-report", mapOf("success" to "false"))
log.error("Incidents reconciliation report failed", it)
}
}
}
}
}
private fun List<MismatchIncidents>.asMap(): Map<String, String> {
return this.associate {
it.agencyId to
("open-dps=${it.dpsOpenIncidents}:open-nomis=${it.nomisOpenIncidents}; closed-dps=${it.dpsClosedIncidents}:closed-nomis=${it.nomisClosedIncidents}")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package uk.gov.justice.digital.hmpps.prisonerfromnomismigration.incidents

import com.microsoft.applicationinsights.TelemetryClient
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.withContext
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import uk.gov.justice.digital.hmpps.prisonerfromnomismigration.helpers.trackEvent
import uk.gov.justice.digital.hmpps.prisonerfromnomismigration.nomissync.model.IncidentAgencyId
import uk.gov.justice.digital.hmpps.prisonerfromnomismigration.service.doApiCallWithRetries

@Service
class IncidentsReconciliationService(
private val telemetryClient: TelemetryClient,
private val incidentsService: IncidentsService,
private val nomisIncidentsApiService: IncidentsNomisApiService,
) {
private companion object {
val log: Logger = LoggerFactory.getLogger(this::class.java)
}

suspend fun generateReconciliationReport(agencies: List<IncidentAgencyId>): List<MismatchIncidents> =
withContext(Dispatchers.Unconfined) {
agencies.map { async { checkIncidentsMatch(it.agencyId) } }
}.awaitAll().filterNotNull()

suspend fun checkIncidentsMatch(agencyId: String): MismatchIncidents? = runCatching {
val nomisIncidents = doApiCallWithRetries { nomisIncidentsApiService.getIncidentsReconciliation(agencyId) }
val dpsOpenIncidentsCount = doApiCallWithRetries { incidentsService.getOpenIncidentsCount(agencyId) }
val dpsClosedIncidentsCount = doApiCallWithRetries { incidentsService.getClosedIncidentsCount(agencyId) }
val nomisOpenIncidentsCount = nomisIncidents.incidentCount.openIncidents
val nomisClosedIncidentsCount = nomisIncidents.incidentCount.closedIncidents

return if (nomisOpenIncidentsCount != dpsOpenIncidentsCount || nomisClosedIncidentsCount != dpsClosedIncidentsCount) {
MismatchIncidents(
agencyId = agencyId,
dpsOpenIncidents = dpsOpenIncidentsCount,
nomisOpenIncidents = nomisOpenIncidentsCount,
dpsClosedIncidents = dpsOpenIncidentsCount,
nomisClosedIncidents = nomisClosedIncidentsCount,
)
.also { mismatch ->
log.info("Incidents Mismatch found $mismatch")
telemetryClient.trackEvent(
"incidents-reports-reconciliation-mismatch",
mapOf(
"agencyId" to mismatch.agencyId,
"dpsOpenIncidents" to mismatch.dpsOpenIncidents,
"nomisOpenIncidents" to mismatch.nomisOpenIncidents,
"dpsClosedIncidents" to mismatch.dpsClosedIncidents,
"nomisClosedIncidents" to mismatch.nomisClosedIncidents,
),
)
}
} else {
null
}
}.onFailure {
log.error("Unable to match incidents for agency with $agencyId ", it)
telemetryClient.trackEvent(
"incidents-reports-reconciliation-mismatch-error",
mapOf(
"agencyId" to agencyId,
),
)
}.getOrNull()
}

data class MismatchIncidents(
val agencyId: String,
val dpsOpenIncidents: Long,
val nomisOpenIncidents: Long,
val dpsClosedIncidents: Long,
val nomisClosedIncidents: Long,
)
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,15 @@ import uk.gov.justice.digital.hmpps.prisonerfromnomismigration.helpers.awaitBody
import uk.gov.justice.digital.hmpps.prisonerfromnomismigration.incidents.model.NomisSyncReportId
import uk.gov.justice.digital.hmpps.prisonerfromnomismigration.incidents.model.NomisSyncRequest
import uk.gov.justice.digital.hmpps.prisonerfromnomismigration.incidents.model.ReportBasic
import uk.gov.justice.digital.hmpps.prisonerfromnomismigration.incidents.model.SimplePageReportBasic

@Service
class IncidentsService(@Qualifier("incidentsApiWebClient") private val webClient: WebClient) {
companion object {
val openStatusValues = listOf("AWAITING_ANALYSIS", "IN_ANALYSIS", "INFORMATION_REQUIRED", "INFORMATION_AMENDED", "POST_INCIDENT_UPDATE", "INCIDENT_UPDATED")
val closedStatusValues = listOf("CLOSED", "DUPLICATE")
}

suspend fun upsertIncident(migrateRequest: NomisSyncRequest): NomisSyncReportId =
webClient.post()
.uri("/sync/upsert")
Expand All @@ -29,4 +35,19 @@ class IncidentsService(@Qualifier("incidentsApiWebClient") private val webClient
.uri("/incident-reports/incident-number/{nomisIncidentId}", nomisIncidentId)
.retrieve()
.awaitBody()

suspend fun getIncidents(agencyId: String, statusValues: List<String>): SimplePageReportBasic =
webClient.get()
.uri {
it.path("/incident-reports")
.queryParam("prisonId", agencyId)
.queryParam("status", statusValues)
.queryParam("size", 1)
.build()
}
.retrieve()
.awaitBody()

suspend fun getOpenIncidentsCount(agencyId: String) = getIncidents(agencyId, openStatusValues).totalElements
suspend fun getClosedIncidentsCount(agencyId: String) = getIncidents(agencyId, closedStatusValues).totalElements
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package uk.gov.justice.digital.hmpps.prisonerfromnomismigration.service

import org.slf4j.Logger
import org.slf4j.LoggerFactory

val log: Logger = LoggerFactory.getLogger("HelperApi")

suspend fun <T> doApiCallWithRetries(api: suspend () -> T) = runCatching {
api()
}.recoverCatching {
log.warn("Retrying API call 1", it)
api()
}.recoverCatching {
log.warn("Retrying API call 2", it)
api()
}.getOrThrow()
Loading

0 comments on commit f683937

Please sign in to comment.