Skip to content

Commit

Permalink
Contrôle - Ajout de champs actionEndDateTime et observationsByUnit (
Browse files Browse the repository at this point in the history
#3394)

## Linked issues

- Resolve #3214
- Resolve #3197

----

- [ ] Tests E2E (Cypress)
  • Loading branch information
louptheron authored Aug 1, 2024
2 parents b45cf73 + fedc024 commit 17562ce
Show file tree
Hide file tree
Showing 15 changed files with 287 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package fr.gouv.cnsp.monitorfish.config

/**
* Warning: All properties patched MUST be declared as `var`, and not `val`
*/
@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class Patchable
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package fr.gouv.cnsp.monitorfish.domain.entities.mission.mission_actions

import com.neovisionaries.i18n.CountryCode
import fr.gouv.cnsp.monitorfish.config.Patchable
import fr.gouv.cnsp.monitorfish.domain.entities.mission.ControlUnit
import java.time.ZonedDateTime

Expand All @@ -17,6 +18,8 @@ data class MissionAction(
val faoAreas: List<String> = listOf(),
val actionType: MissionActionType,
val actionDatetimeUtc: ZonedDateTime,
@Patchable
var actionEndDatetimeUtc: ZonedDateTime? = null,
val emitsVms: ControlCheck? = null,
val emitsAis: ControlCheck? = null,
val flightGoals: List<FlightGoal> = listOf(),
Expand Down Expand Up @@ -64,6 +67,8 @@ data class MissionAction(
val isComplianceWithWaterRegulationsControl: Boolean? = null,
val isSafetyEquipmentAndStandardsComplianceControl: Boolean? = null,
val isSeafarersControl: Boolean? = null,
@Patchable
var observationsByUnit: String? = null,
) {
fun verify() {
val controlTypes = listOf(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package fr.gouv.cnsp.monitorfish.domain.entities.mission.mission_actions

import java.time.ZonedDateTime
import java.util.*

data class PatchableMissionAction(
val actionEndDatetimeUtc: Optional<ZonedDateTime>?,
val observationsByUnit: Optional<String>?,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package fr.gouv.cnsp.monitorfish.domain.mappers

import fr.gouv.cnsp.monitorfish.config.Patchable
import fr.gouv.cnsp.monitorfish.config.UseCase
import java.util.*
import kotlin.reflect.KMutableProperty
import kotlin.reflect.full.hasAnnotation
import kotlin.reflect.full.memberProperties

@UseCase
class PatchEntity<T : Any, S : Any> {

/**
* Patches the target entity with values from the source entity.
*
* This function updates the target entity with values from the source entity for properties
* annotated with @Patchable. If a property in the source entity is null, the existing value
* in the target entity is retained. If a property in the source entity is an Optional, it
* is handled accordingly.
*
* @param target The target entity to be patched.
* @param source The source entity providing the patch values.
*/
fun execute(target: T, source: S): T {
val sourceProperties = source::class.memberProperties
val targetProperties = target::class.memberProperties

for (sourceProp in sourceProperties) {
val targetProp =
targetProperties.filter { it.hasAnnotation<Patchable>() }.find { it.name == sourceProp.name }
if (targetProp != null && targetProp is KMutableProperty<*>) {
val sourceValue = sourceProp.getter.call(source)
val existingValue = targetProp.getter.call(target)
val finalValue = if (sourceValue is Optional<*>) {
getValueFromOptional(existingValue, sourceValue)
} else {
sourceValue ?: existingValue
}

targetProp.setter.call(target, finalValue)
}
}

return target
}

private fun getValueFromOptional(existingValue: Any?, optional: Optional<*>?): Any? {
return when {
optional == null -> existingValue
optional.isPresent -> optional.get()
else -> null
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package fr.gouv.cnsp.monitorfish.domain.use_cases.mission.mission_actions

import fr.gouv.cnsp.monitorfish.config.UseCase
import fr.gouv.cnsp.monitorfish.domain.entities.mission.mission_actions.MissionAction
import fr.gouv.cnsp.monitorfish.domain.entities.mission.mission_actions.PatchableMissionAction
import fr.gouv.cnsp.monitorfish.domain.exceptions.BackendUsageErrorCode
import fr.gouv.cnsp.monitorfish.domain.exceptions.BackendUsageException
import fr.gouv.cnsp.monitorfish.domain.mappers.PatchEntity
import fr.gouv.cnsp.monitorfish.domain.repositories.MissionActionsRepository

@UseCase
class PatchMissionAction(
private val missionActionsRepository: MissionActionsRepository,
private val patchMissionAction: PatchEntity<MissionAction, PatchableMissionAction>,
) {

fun execute(id: Int, patchableEnvActionEntity: PatchableMissionAction): MissionAction {
return try {
val previousMissionAction = missionActionsRepository.findById(id)

val updatedMissionAction = patchMissionAction.execute(previousMissionAction, patchableEnvActionEntity)

missionActionsRepository.save(updatedMissionAction)
} catch (e: Exception) {
throw BackendUsageException(BackendUsageErrorCode.NOT_FOUND, "Action $id not found", e)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ data class MissionActionDataOutput(
val missionId: Int,
val actionType: MissionActionType,
val actionDatetimeUtc: ZonedDateTime,
val actionEndDatetimeUtc: ZonedDateTime? = null,
val emitsVms: ControlCheck? = null,
val emitsAis: ControlCheck? = null,
val logbookMatchesActivity: ControlCheck? = null,
Expand Down Expand Up @@ -60,6 +61,7 @@ data class MissionActionDataOutput(
val isComplianceWithWaterRegulationsControl: Boolean? = null,
val isSafetyEquipmentAndStandardsComplianceControl: Boolean? = null,
val isSeafarersControl: Boolean? = null,
val observationsByUnit: String? = null,
) {
companion object {
fun fromMissionAction(missionAction: MissionAction) = MissionActionDataOutput(
Expand All @@ -76,6 +78,7 @@ data class MissionActionDataOutput(
missionId = missionAction.missionId,
actionType = missionAction.actionType,
actionDatetimeUtc = missionAction.actionDatetimeUtc,
actionEndDatetimeUtc = missionAction.actionEndDatetimeUtc,
emitsVms = missionAction.emitsVms,
emitsAis = missionAction.emitsAis,
logbookMatchesActivity = missionAction.logbookMatchesActivity,
Expand Down Expand Up @@ -116,6 +119,7 @@ data class MissionActionDataOutput(
isComplianceWithWaterRegulationsControl = missionAction.isComplianceWithWaterRegulationsControl,
isSafetyEquipmentAndStandardsComplianceControl = missionAction.isSafetyEquipmentAndStandardsComplianceControl,
isSeafarersControl = missionAction.isSeafarersControl,
observationsByUnit = missionAction.observationsByUnit,
)
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
package fr.gouv.cnsp.monitorfish.infrastructure.api.public_api

import fr.gouv.cnsp.monitorfish.domain.use_cases.mission.mission_actions.GetMissionActions
import fr.gouv.cnsp.monitorfish.domain.use_cases.mission.mission_actions.PatchMissionAction
import fr.gouv.cnsp.monitorfish.infrastructure.api.outputs.MissionActionDataOutput
import fr.gouv.cnsp.monitorfish.infrastructure.api.public_api.input.PatchableMissionActionDataInput
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.Parameter
import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import jakarta.websocket.server.PathParam
import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping("/api/v1/mission_actions")
@Tag(name = "APIs for mission actions")
class PublicMissionActionsController(
private val getMissionActions: GetMissionActions,
private val patchMissionAction: PatchMissionAction,
) {

@GetMapping("")
Expand All @@ -26,4 +28,19 @@ class PublicMissionActionsController(
): List<MissionActionDataOutput> {
return getMissionActions.execute(missionId).map { MissionActionDataOutput.fromMissionAction(it) }
}

@PatchMapping(value = ["/{actionId}"], consumes = ["application/json"])
@Operation(summary = "Update a mission action")
@ResponseStatus(HttpStatus.OK)
fun updateMissionAction(
@PathParam("Action id")
@PathVariable(name = "actionId")
actionId: Int,
@RequestBody
actionInput: PatchableMissionActionDataInput,
): MissionActionDataOutput {
val updatedMissionAction = patchMissionAction.execute(actionId, actionInput.toPatchableMissionAction())

return MissionActionDataOutput.fromMissionAction(updatedMissionAction)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package fr.gouv.cnsp.monitorfish.infrastructure.api.public_api.input

import fr.gouv.cnsp.monitorfish.domain.entities.mission.mission_actions.PatchableMissionAction
import java.time.ZonedDateTime
import java.util.*

data class PatchableMissionActionDataInput(
val actionEndDatetimeUtc: Optional<ZonedDateTime>?,
val observationsByUnit: Optional<String>?,
) {
fun toPatchableMissionAction(): PatchableMissionAction {
return PatchableMissionAction(
actionEndDatetimeUtc = actionEndDatetimeUtc,
observationsByUnit = observationsByUnit,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ class MissionActionEntity(
val actionType: MissionActionType,
@Column(name = "action_datetime_utc")
val actionDatetimeUtc: Instant,
@Column(name = "action_end_datetime_utc")
val actionEndDatetimeUtc: Instant? = null,
@Column(name = "emits_vms")
@Enumerated(EnumType.STRING)
val emitsVms: ControlCheck? = null,
Expand Down Expand Up @@ -141,6 +143,8 @@ class MissionActionEntity(
val isSafetyEquipmentAndStandardsComplianceControl: Boolean? = null,
@Column(name = "is_seafarers_control")
val isSeafarersControl: Boolean? = null,
@Column(name = "observations_by_unit")
val observationsByUnit: String? = null,
) {

companion object {
Expand All @@ -162,6 +166,7 @@ class MissionActionEntity(
flightGoals = missionAction.flightGoals.map { it.value },
actionType = missionAction.actionType,
actionDatetimeUtc = missionAction.actionDatetimeUtc.toInstant(),
actionEndDatetimeUtc = missionAction.actionEndDatetimeUtc?.let { it.toInstant() },
emitsVms = missionAction.emitsVms,
emitsAis = missionAction.emitsAis,
logbookMatchesActivity = missionAction.logbookMatchesActivity,
Expand Down Expand Up @@ -201,6 +206,7 @@ class MissionActionEntity(
isComplianceWithWaterRegulationsControl = missionAction.isComplianceWithWaterRegulationsControl,
isSafetyEquipmentAndStandardsComplianceControl = missionAction.isSafetyEquipmentAndStandardsComplianceControl,
isSeafarersControl = missionAction.isSeafarersControl,
observationsByUnit = missionAction.observationsByUnit,
)
}

Expand All @@ -223,6 +229,7 @@ class MissionActionEntity(
} ?: listOf(),
actionType = actionType,
actionDatetimeUtc = actionDatetimeUtc.atZone(ZoneOffset.UTC),
actionEndDatetimeUtc = actionEndDatetimeUtc?.let { it.atZone(ZoneOffset.UTC) },
emitsVms = emitsVms,
emitsAis = emitsAis,
logbookMatchesActivity = logbookMatchesActivity,
Expand Down Expand Up @@ -278,6 +285,7 @@ class MissionActionEntity(
isComplianceWithWaterRegulationsControl = isComplianceWithWaterRegulationsControl,
isSafetyEquipmentAndStandardsComplianceControl = isSafetyEquipmentAndStandardsComplianceControl,
isSeafarersControl = isSeafarersControl,
observationsByUnit = observationsByUnit,
)

private fun <T> deserializeJSONList(mapper: ObjectMapper, json: String?, clazz: Class<T>): List<T> = json?.let {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE public.mission_actions
ADD COLUMN action_end_datetime_utc timestamp with time zone NULL;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE public.mission_actions
ADD COLUMN observations_by_unit text;
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package fr.gouv.cnsp.monitorfish.domain.use_cases.mission.mission_actions

import com.neovisionaries.i18n.CountryCode
import com.nhaarman.mockitokotlin2.any
import com.nhaarman.mockitokotlin2.argumentCaptor
import com.nhaarman.mockitokotlin2.given
import com.nhaarman.mockitokotlin2.verify
import fr.gouv.cnsp.monitorfish.domain.entities.mission.mission_actions.Completion
import fr.gouv.cnsp.monitorfish.domain.entities.mission.mission_actions.MissionAction
import fr.gouv.cnsp.monitorfish.domain.entities.mission.mission_actions.MissionActionType
import fr.gouv.cnsp.monitorfish.domain.entities.mission.mission_actions.PatchableMissionAction
import fr.gouv.cnsp.monitorfish.domain.mappers.PatchEntity
import fr.gouv.cnsp.monitorfish.domain.repositories.MissionActionsRepository
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.test.context.junit.jupiter.SpringExtension
import java.time.ZonedDateTime
import java.util.*

@ExtendWith(SpringExtension::class)
class PatchMissionActionUTests {

@MockBean
private lateinit var missionActionsRepository: MissionActionsRepository

private val patchMissionAction: PatchEntity<MissionAction, PatchableMissionAction> = PatchEntity()

@Test
fun `execute Should patch an existing action`() {
// Given
val action = MissionAction(
id = null,
vesselId = null,
missionId = 1,
actionDatetimeUtc = ZonedDateTime.now(),
portLocode = "AEFAT",
actionType = MissionActionType.LAND_CONTROL,
gearOnboard = listOf(),
seizureAndDiversion = true,
isDeleted = false,
hasSomeGearsSeized = false,
hasSomeSpeciesSeized = false,
completedBy = "XYZ",
isFromPoseidon = false,
flagState = CountryCode.FR,
userTrigram = "LTH",
completion = Completion.TO_COMPLETE,
)
given(missionActionsRepository.findById(any())).willReturn(action)
val expectedDateTime = ZonedDateTime.now()
val patch = PatchableMissionAction(
actionEndDatetimeUtc = Optional.of(expectedDateTime),
observationsByUnit = Optional.of("An observation"),
)

// When
PatchMissionAction(missionActionsRepository, patchMissionAction).execute(123, patch)

// Then
argumentCaptor<MissionAction>().apply {
verify(missionActionsRepository).save(capture())

assertThat(allValues.first().observationsByUnit).isEqualTo("An observation")
assertThat(allValues.first().actionEndDatetimeUtc).isEqualTo(expectedDateTime)
assertThat(allValues.first().userTrigram).isEqualTo("LTH")
assertThat(allValues.first().actionType).isEqualTo(MissionActionType.LAND_CONTROL)
}
}
}
Empty file.
Loading

0 comments on commit 17562ce

Please sign in to comment.