Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Contrôle - Ajout de champs actionEndDateTime et observationsByUnit #3394

Merged
merged 3 commits into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}
}
}
Loading
Loading