Skip to content

Commit 6082786

Browse files
authored
INC-1265: Add api endpoint that deactivates all incentive levels in a prison (#453)
* Add api endpoint that deactivates all incentive levels in a prison which is needed when a prison is closed * Nest incentive level tests deeper so that test cases for the same endpoint are grouped together * Audit & notify about _changed_ prison incentive levels when a whole prison is deactivated rather than all levels. The endpoint still returns all levels.
1 parent 8d2be8c commit 6082786

File tree

6 files changed

+2664
-2258
lines changed

6 files changed

+2664
-2258
lines changed

doc/architecture/decisions/0004-reference-data-sync.md

+4
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,10 @@ Authorised requests will require roles with write scope.
431431
}
432432
```
433433

434+
#### Deactivate all levels in a prison
435+
436+
`DELETE /incentive/prison-levels/{prisonId}`
437+
434438
#### Deactivate a level in a prison
435439

436440
`DELETE /incentive/prison-levels/{prisonId}/level/{levelCode}`

src/main/kotlin/uk/gov/justice/digital/hmpps/incentivesapi/resource/PrisonIncentiveLevelResource.kt

+40
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,46 @@ class PrisonIncentiveLevelResource(
243243
?: throw NoDataWithCodeFoundException("incentive level", levelCode)
244244
}
245245

246+
@DeleteMapping("{prisonId}")
247+
@PreAuthorize("hasRole('MAINTAIN_PRISON_IEP_LEVELS') and hasAuthority('SCOPE_write')")
248+
@Operation(
249+
summary = "Deactivate all incentive levels for a prison",
250+
description = "This can be used when a prison closes. " +
251+
"Returns all incentive levels in this prison include those that were already inactive. " +
252+
"Deactivating a level is only possible if there are no prisoners currently on it." +
253+
"\n\nRequires role: MAINTAIN_PRISON_IEP_LEVELS with write scope" +
254+
"\n\nRaises HMPPS domain event: \"incentives.prison-level.changed\"",
255+
responses = [
256+
ApiResponse(
257+
responseCode = "200",
258+
description = "Prison incentive levels deactivated",
259+
),
260+
ApiResponse(
261+
responseCode = "400",
262+
description = "There are prisoners on some incentive level at this prison",
263+
content = [Content(mediaType = "application/json", schema = Schema(implementation = ErrorResponse::class))],
264+
),
265+
ApiResponse(
266+
responseCode = "401",
267+
description = "Unauthorized to access this endpoint",
268+
content = [Content(mediaType = "application/json", schema = Schema(implementation = ErrorResponse::class))],
269+
),
270+
ApiResponse(
271+
responseCode = "403",
272+
description = "Incorrect permissions to use this endpoint",
273+
content = [Content(mediaType = "application/json", schema = Schema(implementation = ErrorResponse::class))],
274+
),
275+
],
276+
)
277+
suspend fun deactivateAllPrisonIncentiveLevels(
278+
@Schema(description = "Prison id", example = "MDI", required = true, minLength = 3, maxLength = 6)
279+
@PathVariable
280+
prisonId: String,
281+
): List<PrisonIncentiveLevel> {
282+
prisonIncentiveLevelService.deactivateAllPrisonIncentiveLevels(prisonId)
283+
return prisonIncentiveLevelService.getAllPrisonIncentiveLevels(prisonId)
284+
}
285+
246286
@DeleteMapping("{prisonId}/level/{levelCode}")
247287
@PreAuthorize("hasRole('MAINTAIN_PRISON_IEP_LEVELS') and hasAuthority('SCOPE_write')")
248288
@Operation(

src/main/kotlin/uk/gov/justice/digital/hmpps/incentivesapi/service/PrisonIncentiveLevelAuditedService.kt

+7
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@ class PrisonIncentiveLevelAuditedService(
3838
}
3939
}
4040

41+
override suspend fun deactivateAllPrisonIncentiveLevels(prisonId: String): List<PrisonIncentiveLevelDTO> {
42+
return super.deactivateAllPrisonIncentiveLevels(prisonId)
43+
.onEach {
44+
auditAndPublishPrisonLevelChangeEvent(it)
45+
}
46+
}
47+
4148
private suspend fun auditAndPublishPrisonLevelChangeEvent(prisonIncentiveLevel: PrisonIncentiveLevelDTO) {
4249
auditService.sendMessage(
4350
AuditType.PRISON_INCENTIVE_LEVEL_UPDATED,

src/main/kotlin/uk/gov/justice/digital/hmpps/incentivesapi/service/PrisonIncentiveLevelService.kt

+26
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package uk.gov.justice.digital.hmpps.incentivesapi.service
22

33
import jakarta.validation.ValidationException
44
import kotlinx.coroutines.flow.Flow
5+
import kotlinx.coroutines.flow.collect
56
import kotlinx.coroutines.flow.map
67
import kotlinx.coroutines.flow.toList
78
import org.springframework.stereotype.Service
@@ -167,6 +168,31 @@ class PrisonIncentiveLevelService(
167168
}
168169
}
169170

171+
/**
172+
* Deactivate all incentive levels for a given prison; useful for when prisons are closed.
173+
* NB: returns only the incentive levels that were deactivated
174+
*/
175+
@Transactional
176+
suspend fun deactivateAllPrisonIncentiveLevels(prisonId: String): List<PrisonIncentiveLevelDTO> {
177+
val prisonIncentiveLevels = prisonIncentiveLevelRepository.findAllByPrisonId(prisonId).toList()
178+
val prisonIncentiveLevelsWithPrisoners = prisonIncentiveLevels.filter { prisonIncentiveLevel ->
179+
countPrisonersService.prisonersExistOnLevelInPrison(prisonId, prisonIncentiveLevel.levelCode)
180+
}
181+
if (prisonIncentiveLevelsWithPrisoners.isNotEmpty()) {
182+
throw ValidationExceptionWithErrorCode(
183+
"A level must remain active if there are prisoners on it currently",
184+
ErrorCode.PrisonIncentiveLevelActiveIfPrisonersExist,
185+
moreInfo = prisonIncentiveLevelsWithPrisoners.map(PrisonIncentiveLevel::levelCode).joinToString(","),
186+
)
187+
}
188+
return prisonIncentiveLevelRepository.saveAll(
189+
prisonIncentiveLevels.filter(PrisonIncentiveLevel::active)
190+
.map { prisonIncentiveLevel ->
191+
prisonIncentiveLevel.withUpdate(PrisonIncentiveLevelUpdateDTO(active = false))
192+
},
193+
).toListOfDTO()
194+
}
195+
170196
private fun PrisonIncentiveLevel.withUpdate(update: PrisonIncentiveLevelUpdateDTO): PrisonIncentiveLevel = copy(
171197
active = update.active ?: active,
172198
defaultOnAdmission = update.defaultOnAdmission ?: defaultOnAdmission,

0 commit comments

Comments
 (0)