Skip to content

Commit

Permalink
Pre-check control unit deletion
Browse files Browse the repository at this point in the history
  • Loading branch information
ivangabriele committed Oct 18, 2023
1 parent a2172be commit e14e6ae
Show file tree
Hide file tree
Showing 21 changed files with 287 additions and 163 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import org.springframework.data.domain.Pageable
import java.time.Instant

interface IMissionRepository {
fun findById(missionId: Int): MissionDTO
fun count(): Long

fun delete(missionId: Int)

fun findAll(
startedAfter: Instant,
startedBefore: Instant?,
Expand All @@ -18,7 +21,9 @@ interface IMissionRepository {
pageable: Pageable,
): List<MissionDTO>

fun findByControlUnitId(controlUnitId: Int): List<MissionEntity>

fun findById(missionId: Int): MissionDTO

fun save(mission: MissionEntity): MissionDTO
fun delete(missionId: Int)
fun count(): Long
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ interface IReportingRepository {
status: List<String>?,
): List<ReportingDTO>

fun findByControlUnitId(controlUnitId: Int): List<ReportingEntity>

fun findByMissionId(missionId: Int): List<ReportingDTO>

fun findById(reportingId: Int): ReportingDTO
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package fr.gouv.cacem.monitorenv.domain.use_cases.controlUnit

import fr.gouv.cacem.monitorenv.config.UseCase
import fr.gouv.cacem.monitorenv.domain.repositories.IMissionRepository
import fr.gouv.cacem.monitorenv.domain.repositories.IReportingRepository

@UseCase
class CanDeleteControlUnit(
private val missionRepository: IMissionRepository,
private val reportingRepository: IReportingRepository,
) {
fun execute(controlUnitId: Int): Boolean {
val missions = missionRepository.findByControlUnitId(controlUnitId)
val reportings = reportingRepository.findByControlUnitId(controlUnitId)

return missions.isEmpty() && reportings.isEmpty()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package fr.gouv.cacem.monitorenv.infrastructure.api.adapters.publicapi.outputs

data class BooleanDataOutput(
val value: Boolean,
) {
companion object {
fun get(value: Boolean): BooleanDataOutput {
return BooleanDataOutput(
value,
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package fr.gouv.cacem.monitorenv.infrastructure.api.endpoints.publicapi

import fr.gouv.cacem.monitorenv.domain.use_cases.controlUnit.*
import fr.gouv.cacem.monitorenv.infrastructure.api.adapters.publicapi.inputs.CreateOrUpdateControlUnitDataInput
import fr.gouv.cacem.monitorenv.infrastructure.api.adapters.publicapi.outputs.BooleanDataOutput
import fr.gouv.cacem.monitorenv.infrastructure.api.adapters.publicapi.outputs.ControlUnitDataOutput
import fr.gouv.cacem.monitorenv.infrastructure.api.adapters.publicapi.outputs.FullControlUnitDataOutput
import io.swagger.v3.oas.annotations.Operation
Expand All @@ -16,6 +17,7 @@ import org.springframework.web.bind.annotation.*
class ApiControlUnitsController(
private val archiveControlUnit: ArchiveControlUnit,
private val createOrUpdateControlUnit: CreateOrUpdateControlUnit,
private val canDeleteControlUnit: CanDeleteControlUnit,
private val deleteControlUnit: DeleteControlUnit,
private val getControlUnits: GetControlUnits,
private val getControlUnitById: GetControlUnitById,
Expand All @@ -30,6 +32,16 @@ class ApiControlUnitsController(
archiveControlUnit.execute(controlUnitId)
}

@GetMapping("/{controlUnitId}/can_delete")
@Operation(summary = "Can this control unit be deleted?")
fun canDelete(
@PathParam("Control unit ID")
@PathVariable(name = "controlUnitId")
controlUnitId: Int,
): BooleanDataOutput {
return canDeleteControlUnit.execute(controlUnitId).let { BooleanDataOutput.get(it) }
}

@PostMapping("", consumes = ["application/json"])
@Operation(summary = "Create a control unit")
@ResponseStatus(HttpStatus.CREATED)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ class JpaMissionRepository(
).map { it.toMissionDTO(mapper) }
}

override fun findByControlUnitId(controlUnitId: Int): List<MissionEntity> {
return dbMissionRepository.findByControlUnitId(controlUnitId).map { it.toMissionEntity(mapper) }
}

override fun findById(missionId: Int): MissionDTO {
return dbMissionRepository.findById(missionId).get().toMissionDTO(mapper)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ class JpaReportingRepository(
private val dbControlUnitRepository: IDBControlUnitRepository,
private val mapper: ObjectMapper,
) : IReportingRepository {

@Transactional
@Modifying(clearAutomatically = true, flushAutomatically = true)
override fun attachReportingsToMission(reportingIds: List<Int>, missionId: Int) {
Expand Down Expand Up @@ -62,6 +61,10 @@ class JpaReportingRepository(
.map { it.toReportingDTO(mapper) }
}

override fun findByControlUnitId(controlUnitId: Int): List<ReportingEntity> {
return dbReportingRepository.findByControlUnitId(controlUnitId).map { it.toReporting() }
}

override fun findByMissionId(missionId: Int): List<ReportingDTO> {
return dbReportingRepository.findByMissionId(missionId).map { it.toReportingDTO(mapper) }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@ import java.time.Instant

@DynamicUpdate
interface IDBMissionRepository : JpaRepository<MissionModel, Int> {
@Modifying(clearAutomatically = true)
@Query(
value = """
UPDATE missions
SET deleted = TRUE
WHERE id = :id
""",
nativeQuery = true,
)
fun delete(id: Int)

// see https://github.com/spring-projects/spring-data-jpa/issues/2491
// and https://stackoverflow.com/questions/55169797/pass-liststring-into-postgres-function-as-parameter
// for ugly casting of passed parameters
Expand Down Expand Up @@ -69,14 +80,6 @@ interface IDBMissionRepository : JpaRepository<MissionModel, Int> {
pageable: Pageable,
): List<MissionModel>

@Modifying(clearAutomatically = true)
@Query(
value = """
UPDATE missions
SET deleted = TRUE
WHERE id = :id
""",
nativeQuery = true,
)
fun delete(id: Int)
@Query("SELECT mm FROM MissionModel mm JOIN mm.controlUnits mmcu WHERE mmcu.unit.id = :controlUnitId")
fun findByControlUnitId(controlUnitId: Int): List<MissionModel>
}
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,19 @@ interface IDBReportingRepository : JpaRepository<ReportingModel, Int> {
status: String?,
): List<ReportingModel>

@Query(
value =
"""
SELECT *
FROM reportings
WHERE control_unit_id = :controlUnitId
""",
nativeQuery = true,
)
fun findByControlUnitId(
controlUnitId: Int,
): List<ReportingModel>

@Query(
value =
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ class ApiControlUnitsControllerITests {
@MockBean
private lateinit var archiveControlUnit: ArchiveControlUnit

@MockBean
private lateinit var canDeleteControlUnit: CanDeleteControlUnit

@MockBean
private lateinit var createOrUpdateControlUnit: CreateOrUpdateControlUnit

Expand All @@ -49,7 +52,47 @@ class ApiControlUnitsControllerITests {
private lateinit var objectMapper: ObjectMapper

@Test
fun `Should create a control unit`() {
fun `archive() should archive a control unit`() {
val controlUnitId = 1

mockMvc.perform(
post("/api/v2/control_units/$controlUnitId/archive"),
)
.andExpect(status().isOk)

BDDMockito.verify(archiveControlUnit).execute(controlUnitId)
}

@Test
fun `canDelete() should check if a control unit can be deleted`() {
val controlUnitId = 1
val canDelete = true

given(canDeleteControlUnit.execute(controlUnitId)).willReturn(canDelete)

mockMvc.perform(
get("/api/v2/control_units/$controlUnitId/can_delete"),
)
.andExpect(status().isOk)
.andExpect(MockMvcResultMatchers.jsonPath("$.value").value(canDelete))

BDDMockito.verify(canDeleteControlUnit).execute(controlUnitId)
}

@Test
fun `delete() should delete a control unit`() {
val controlUnitId = 1

mockMvc.perform(
delete("/api/v2/control_units/$controlUnitId"),
)
.andExpect(status().isOk)

BDDMockito.verify(deleteControlUnit).execute(controlUnitId)
}

@Test
fun `create() should create a control unit`() {
val expectedCreatedControlUnit = ControlUnitEntity(
id = 1,
administrationId = 0,
Expand Down Expand Up @@ -84,7 +127,7 @@ class ApiControlUnitsControllerITests {
}

@Test
fun `Should get a control unit by its ID`() {
fun `get() should get a control unit by its ID`() {
val expectedFullControlUnit = FullControlUnitDTO(
administration = AdministrationEntity(
id = 0,
Expand Down Expand Up @@ -115,7 +158,7 @@ class ApiControlUnitsControllerITests {
}

@Test
fun `Should get all control units`() {
fun `getAll() should get all control units`() {
val expectedControlUnits = listOf(
FullControlUnitDTO(
administration = AdministrationEntity(
Expand Down Expand Up @@ -166,7 +209,7 @@ class ApiControlUnitsControllerITests {
}

@Test
fun `Should update a control unit`() {
fun `update() should update a control unit`() {
val expectedUpdatedControlUnit = ControlUnitEntity(
id = 1,
administrationId = 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ class JpaMissionRepositoryITests : AbstractDBTests() {
@Autowired
private lateinit var jpaMissionRepository: JpaMissionRepository

@Test
@Transactional
fun `findByControlUnitId() should find the matching missions`() {
val foundMissions = jpaMissionRepository.findByControlUnitId(10002)

assertThat(foundMissions).hasSize(18)
}

@Test
@Transactional
fun `save should create a new mission`() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,20 @@ import org.springframework.boot.test.context.SpringBootTest
import org.springframework.data.domain.Pageable
import org.springframework.transaction.annotation.Transactional
import java.time.ZonedDateTime
import java.util.UUID
import java.util.*

@SpringBootTest(properties = ["monitorenv.scheduling.enabled=false"])
class JpaReportingITests : AbstractDBTests() {
@Autowired private lateinit var jpaReportingRepository: JpaReportingRepository
@Autowired
private lateinit var jpaReportingRepository: JpaReportingRepository

@Test
@Transactional
fun `findByControlUnitId() should find the matching reportings`() {
val foundReportings = jpaReportingRepository.findByControlUnitId(10000)

assertThat(foundReportings).hasSize(1)
}

@Test
@Transactional
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@ context('Back Office > Control Unit Table > Row Actions', () => {
cy.intercept('DELETE', `/api/v2/control_units/10000`).as('deleteControlUnit')

cy.getTableRowById(10000).clickButton('Supprimer cette unité de contrôle')
cy.clickButton('Supprimer')

cy.wait('@deleteControlUnit')

cy.get('.Component-Dialog').should('be.visible')
cy.contains('Suppression impossible').should('be.visible')
Expand Down
13 changes: 11 additions & 2 deletions frontend/src/api/controlUnitsAPI.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { monitorenvPublicApi } from './api'
import { ApiErrorCode } from './types'
import { ApiErrorCode, type BackendApiBooleanResponse } from './types'
import { FrontendApiError } from '../libs/FrontendApiError'
import { newUserError } from '../libs/UserError'

import type { ControlUnit } from '../domain/entities/controlUnit'

const DELETE_CONTROL_UNIT_ERROR_MESSAGE = [
export const DELETE_CONTROL_UNIT_ERROR_MESSAGE = [
'Cette unité est rattachée à des missions ou des signalements.',
"Veuillez l'en détacher avant de la supprimer ou bien l'archiver."
].join(' ')
const CAN_DELETE_CONTROL_UNIT_ERROR_MESSAGE = "Nous n'avons pas pu vérifier si cette unité de contrôle est supprimable."
const GET_CONTROL_UNIT_ERROR_MESSAGE = "Nous n'avons pas pu récupérer cette unité de contrôle."
const GET_CONTROL_UNITS_ERROR_MESSAGE = "Nous n'avons pas pu récupérer la liste des unités de contrôle."

Expand All @@ -22,6 +23,13 @@ export const controlUnitsAPI = monitorenvPublicApi.injectEndpoints({
})
}),

canDeleteControlUnit: builder.query<boolean, number>({
providesTags: () => [{ type: 'ControlUnits' }],
query: controlUnitId => `/v2/control_units/${controlUnitId}/can_delete`,
transformErrorResponse: response => new FrontendApiError(CAN_DELETE_CONTROL_UNIT_ERROR_MESSAGE, response),
transformResponse: (response: BackendApiBooleanResponse) => response.value
}),

createControlUnit: builder.mutation<void, ControlUnit.NewControlUnitData>({
invalidatesTags: () => [{ type: 'Administrations' }, { type: 'ControlUnits' }],
query: newControlUnitData => ({
Expand Down Expand Up @@ -71,6 +79,7 @@ export const controlUnitsAPI = monitorenvPublicApi.injectEndpoints({

export const {
useArchiveControlUnitMutation,
useCanDeleteControlUnitQuery,
useCreateControlUnitMutation,
useDeleteControlUnitMutation,
useGetControlUnitQuery,
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ export interface BackendApiErrorResponse {
type: ApiErrorCode | null
}

export interface BackendApiBooleanResponse {
value: boolean
}

export interface CustomRTKErrorResponse {
data: BackendApiErrorResponse
status: number | 'FETCH_ERROR' | 'PARSING_ERROR' | 'TIMEOUT_ERROR' | 'CUSTOM_ERROR'
Expand Down
4 changes: 1 addition & 3 deletions frontend/src/features/BackOffice/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ export type DialogState = {

export enum BackOfficeConfirmationModalActionType {
'ARCHIVE_ADMINISTRATION' = 'ARCHIVE_ADMINISTRATION',
'ARCHIVE_CONTROL_UNIT' = 'ARCHIVE_CONTROL_UNIT',
'DELETE_ADMINISTRATION' = 'DELETE_ADMINISTRATION',
'DELETE_BASE' = 'DELETE_BASE',
'DELETE_CONTROL_UNIT' = 'DELETE_CONTROL_UNIT'
'DELETE_BASE' = 'DELETE_BASE'
}
Loading

0 comments on commit e14e6ae

Please sign in to comment.