diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/mission/mission_actions/actrep/JointDeploymentPlan.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/mission/mission_actions/actrep/JointDeploymentPlan.kt index 79545d1407..7fb5e319c4 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/mission/mission_actions/actrep/JointDeploymentPlan.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/mission/mission_actions/actrep/JointDeploymentPlan.kt @@ -1,30 +1,66 @@ package fr.gouv.cnsp.monitorfish.domain.entities.mission.mission_actions.actrep -import com.neovisionaries.i18n.CountryCode +import fr.gouv.cnsp.monitorfish.domain.entities.fao_area.FAOArea +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.use_cases.fleet_segment.hasFaoCodeIncludedIn -enum class JointDeploymentPlan(private val species: List) { - MEDITERRANEAN_AND_EASTERN_ATLANTIC(MEDITERRANEAN_AND_EASTERN_ATLANTIC_SPECIES), - NORTH_SEA(NORTH_SEA_SPECIES), - WESTERN_WATERS(WESTERN_WATERS_SPECIES), - ; +/** + * JDP MED / EASTERN ATLANTIC operational zones + * + * cf. https://extranet.legipeche.metier.developpement-durable.gouv.fr/fichier/pdf/med_jdp_2024_med_sg_final_fr_cle5197c6.pdf?arg=25289&cle=c065370e6727cc3f839e254fcc19c4b24e36dc9d&file=pdf%2Fmed_jdp_2024_med_sg_final_fr_cle5197c6.pdf + */ +val MEDITERRANEAN_OPERATIONAL_ZONES = listOf("37.1", "37.2", "37.3") +val EASTERN_ATLANTIC_OPERATIONAL_ZONES = listOf("34.1.2", "27.7", "27.8", "27.9", "27.10") - fun getFaoZonesAndSpeciesCodes(): List { - return this.species - } +/** + * JDP NORTH SEA operational zones + * + * cf. https://extranet.legipeche.metier.developpement-durable.gouv.fr/fichier/pdf/ed_decision_2023-24_-_ns_jdp_2024_planning_-_adoption_fr_cle11191a.pdf?arg=25287&cle=a5d3eecb0e2bdd9a229e8b34bf5ae11f96e89118&file=pdf%2Fed_decision_2023-24_-_ns_jdp_2024_planning_-_adoption_fr_cle11191a.pdf + */ +val NORTH_SEA_OPERATIONAL_ZONES = listOf("27.4", "27.3.a") + +/** + * JDP WESTERN WATERS operational zones + * + * cf. https://extranet.legipeche.metier.developpement-durable.gouv.fr/fichier/pdf/ed_decision_2023-25_-_ww_jdp_2024_planning_-_adoption_fr_cle128883.pdf?arg=25288&cle=9a2d7705425e766258f0d648353a05aa04249faf&file=pdf%2Fed_decision_2023-25_-_ww_jdp_2024_planning_-_adoption_fr_cle128883.pdf + */ +val WESTERN_WATERS_OPERATIONAL_ZONES = listOf("27.5", "27.6", "27.7", "27.8", "27.9", "27.10", "34.1.1", "34.1.2", "34.2.0") + +enum class JointDeploymentPlan(private val species: List, private val operationalZones: List) { + MEDITERRANEAN_AND_EASTERN_ATLANTIC( + MEDITERRANEAN_AND_EASTERN_ATLANTIC_SPECIES, + MEDITERRANEAN_OPERATIONAL_ZONES + EASTERN_ATLANTIC_OPERATIONAL_ZONES, + ), + NORTH_SEA(NORTH_SEA_SPECIES, NORTH_SEA_OPERATIONAL_ZONES), + WESTERN_WATERS(WESTERN_WATERS_SPECIES, WESTERN_WATERS_OPERATIONAL_ZONES), + ; fun getSpeciesCodes(): List { return this.species.map { it.second }.distinct() } + private fun getOperationalZones(): List { + return this.operationalZones + } + /** * See "DÉCISION D’EXÉCUTION (UE) 2023/2376 DE LA COMMISSION": * https://extranet.legipeche.metier.developpement-durable.gouv.fr/fichier/pdf/oj_l_202302376_fr_txt_cle6b198e.pdf?arg=24774&cle=7d14626b709ff7e8c62586bcd8683e7e9fcaa348&file=pdf%2Foj_l_202302376_fr_txt_cle6b198e.pdf */ - fun isLandControlApplicable(flagState: CountryCode, speciesOnboardCodes: List, tripFaoCodes: List): Boolean { - val isThirdCountryVessel = EU_THIRD_COUNTRIES.contains(flagState) + fun isLandControlApplicable(control: MissionAction): Boolean { + val speciesOnboardCodes = control.speciesOnboard.mapNotNull { it.speciesCode } + val tripFaoCodes = control.faoAreas + + val isThirdCountryVessel = EU_THIRD_COUNTRIES.contains(control.flagState) + + val isFirstJdpCurrentJdp = isAttributedJdp(control) + if (!isFirstJdpCurrentJdp) { + return false + } val hasSpeciesInJdp = this.species.any { (jdpFaoZones, jdpSpecy) -> - val isSpecyFoundInJdpSPecies = speciesOnboardCodes.contains(jdpSpecy) + val isSpecyFoundInJdpSpecies = speciesOnboardCodes.contains(jdpSpecy) val isFaoZoneFoundInJdpFaoZones = jdpFaoZones .any { jdpFaoCode -> @@ -32,7 +68,7 @@ enum class JointDeploymentPlan(private val species: List) { tripFaoCodes.any { tripFaoCode -> tripFaoCode.contains(jdpFaoCode) } } - return@any isSpecyFoundInJdpSPecies && isFaoZoneFoundInJdpFaoZones + return@any isSpecyFoundInJdpSpecies && isFaoZoneFoundInJdpFaoZones } val hasSpeciesInEUQuotas = if (isThirdCountryVessel) { @@ -43,4 +79,67 @@ enum class JointDeploymentPlan(private val species: List) { return hasSpeciesInJdp || hasSpeciesInEUQuotas } + + fun getFirstFaoAreaIncludedInJdp( + control: MissionAction, + ): FAOArea? { + val jdpFaoAreas = this.getOperationalZones() + + if (control.actionType == MissionActionType.SEA_CONTROL && !isAttributedJdp(control)) { + return null + } + + val firstFaoAreaIncludedInJdp = control.faoAreas + .map { FAOArea(it) } + .firstOrNull { controlFaoArea -> + jdpFaoAreas.any { controlFaoArea.hasFaoCodeIncludedIn(it) } + } + + return firstFaoAreaIncludedInJdp + } + + /** + * We use an arbitrary method to de-duplicated reporting of controls made in multiple fao areas, + * hence in multiple JDP. + * `JointDeploymentPlan.entries.firstOrNull` is the arbitrary rule to attach a control to only one JDP. + * see: https://github.com/MTES-MCT/monitorfish/issues/3157#issuecomment-2093036583 + */ + fun isAttributedJdp( + control: MissionAction, + ) = JointDeploymentPlan.entries + .firstOrNull { jdpEntry -> + /** + * There is an overlap between the `MEDITERRANEAN_AND_EASTERN_ATLANTIC` and the WESTERN_WATERS JDPs. + * We add a filter by species to avoid counting all controls done in + * `EASTERN_ATLANTIC_OPERATIONAL_ZONES without targeted species in catches. + */ + if (control.actionType == MissionActionType.SEA_CONTROL && jdpEntry == MEDITERRANEAN_AND_EASTERN_ATLANTIC) { + return@firstOrNull isMedJdpAttributed(control) + } + + return@firstOrNull jdpEntry.getOperationalZones().any { jdpFaoArea -> + control.faoAreas.any { controlFaoArea -> + FAOArea(controlFaoArea).hasFaoCodeIncludedIn(jdpFaoArea) + } + } + } == this + + private fun isMedJdpAttributed( + control: MissionAction, + ) = MEDITERRANEAN_AND_EASTERN_ATLANTIC.getOperationalZones().any { jdpFaoArea -> + /** + * Filter by FAO zone AND `EASTERN_ATLANTIC_SPECY` + * if the fao zone is included in the `EASTERN_ATLANTIC_OPERATIONAL_ZONES` + */ + if (EASTERN_ATLANTIC_OPERATIONAL_ZONES.contains(jdpFaoArea)) { + return@any control.faoAreas.any { controlFaoArea -> + FAOArea(controlFaoArea).hasFaoCodeIncludedIn(jdpFaoArea) && + control.speciesOnboard.map { it.speciesCode }.contains(EASTERN_ATLANTIC_SPECY.second) + } + } + + return@any control.faoAreas.any { controlFaoArea -> + FAOArea(controlFaoArea).hasFaoCodeIncludedIn(jdpFaoArea) + } + } } diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/mission/mission_actions/actrep/species.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/mission/mission_actions/actrep/species.kt index 13945bb2d6..73119d4839 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/mission/mission_actions/actrep/species.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/mission/mission_actions/actrep/species.kt @@ -17,9 +17,9 @@ typealias FaoZonesAndSpecy = Pair * * See issue: https://github.com/MTES-MCT/monitorfish/issues/1750 */ -val MED_FAO_CODES = listOf("37.1", "37.2", "37.3") +val EASTERN_ATLANTIC_SPECY = Pair(EASTERN_ATLANTIC_OPERATIONAL_ZONES, "BFT") val MEDITERRANEAN_AND_EASTERN_ATLANTIC_SPECIES: List = generateSpeciesWithFaoCode( - MED_FAO_CODES, + MEDITERRANEAN_OPERATIONAL_ZONES, listOf( "ANE", "HOM", @@ -67,12 +67,11 @@ val MEDITERRANEAN_AND_EASTERN_ATLANTIC_SPECIES: List = generat "ANN", ), ) + - // Eastern Atlantic part - listOf(Pair(listOf("27.7", "27.8", "27.9", "27.10"), "BFT")) + // Eastern Atlantic part for BFT + listOf(EASTERN_ATLANTIC_SPECY) -val NS_01_FAO_CODES = listOf("27.4", "27.3.a") val NORTH_SEA_SPECIES: List = generateSpeciesWithFaoCode( - NS_01_FAO_CODES, + NORTH_SEA_OPERATIONAL_ZONES, listOf( "HOM", "JAX", @@ -115,12 +114,11 @@ val NORTH_SEA_SPECIES: List = generateSpeciesWithFaoCode( ), ) -val WW_01_FAO_CODES = listOf("27.6", "27.7", "27.8", "27.9", "27.10") val WESTERN_WATERS_SPECIES: List = listOf( Pair(listOf("27.6", "27.7", "27.8", "27.9"), "PIL"), Pair(listOf("27.6", "27.7", "27.8", "27.9"), "ELE"), ) + generateSpeciesWithFaoCode( - WW_01_FAO_CODES, + listOf("27.6", "27.7", "27.8", "27.9", "27.10"), listOf( "ANE", "HOM", diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/repositories/MissionActionsRepository.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/repositories/MissionActionsRepository.kt index 1d70e3fe5b..ccc4688f7b 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/repositories/MissionActionsRepository.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/repositories/MissionActionsRepository.kt @@ -7,7 +7,7 @@ interface MissionActionsRepository { fun findById(id: Int): MissionAction fun findByMissionId(missionId: Int): List fun findVesselMissionActionsAfterDateTime(vesselId: Int, afterDateTime: ZonedDateTime): List - fun findControlsInDates(beforeDateTime: ZonedDateTime, afterDateTime: ZonedDateTime): List + fun findSeaAndLandControlBetweenDates(beforeDateTime: ZonedDateTime, afterDateTime: ZonedDateTime): List fun findMissionActionsIn(missionIds: List): List fun save(missionAction: MissionAction): MissionAction } diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/fao_areas/ComputeVesselFAOAreas.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/fao_areas/ComputeVesselFAOAreas.kt index 2a38407586..1cd52e00fd 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/fao_areas/ComputeVesselFAOAreas.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/fao_areas/ComputeVesselFAOAreas.kt @@ -13,7 +13,7 @@ import fr.gouv.cnsp.monitorfish.domain.use_cases.fleet_segment.removeRedundantFa * return computed fao zones from the given coordinates/port. * * Priority : - * 1. Fetch the fao zones from the `risk_factors` table + * 1. Fetch the fao zones from the `risk_factors` table to have the areas of the entire voyage * 2. Otherwise, * - Fetch the fao zones from the latitude/longitude if given * - Fetch the fao zones from the portLocode if given @@ -34,7 +34,7 @@ class ComputeVesselFAOAreas( return listOf() } - // Fetch the fao zones from the `risk_factors` table + // Fetch the fao zones from the `risk_factors` table to have the areas of the entire voyage if (internalReferenceNumber != null) { // Get faoZones from speciesOnboard in risk factors table (updated by the pipeline) val vesselRiskFactor = riskFactorRepository.findByInternalReferenceNumber(internalReferenceNumber) diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/mission/mission_actions/GetActivityReports.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/mission/mission_actions/GetActivityReports.kt index 5d36ec677b..7ea7a9a435 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/mission/mission_actions/GetActivityReports.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/mission/mission_actions/GetActivityReports.kt @@ -24,7 +24,7 @@ class GetActivityReports( private val logger = LoggerFactory.getLogger(GetActivityReports::class.java) fun execute(beforeDateTime: ZonedDateTime, afterDateTime: ZonedDateTime, jdp: JointDeploymentPlan): ActivityReports { - val controls = missionActionsRepository.findControlsInDates(beforeDateTime, afterDateTime) + val controls = missionActionsRepository.findSeaAndLandControlBetweenDates(beforeDateTime, afterDateTime) logger.info("Found ${controls.size} controls between dates [$afterDateTime, $beforeDateTime].") if (controls.isEmpty()) { @@ -44,22 +44,24 @@ class GetActivityReports( val filteredControls = controls.filter { control -> when (control.actionType) { MissionActionType.LAND_CONTROL -> { - val speciesOnboardCodes = control.speciesOnboard.mapNotNull { it.speciesCode } - val tripFaoCodes = control.faoAreas - - return@filter jdp.isLandControlApplicable(control.flagState, speciesOnboardCodes, tripFaoCodes) + return@filter jdp.isLandControlApplicable(control) } MissionActionType.SEA_CONTROL -> { val controlMission = missions.firstOrNull { mission -> mission.id == control.missionId } + val isUnderJdp = controlMission?.isUnderJdp == true if (controlMission == null) { logger.error( "Mission id '${control.missionId}' linked to SEA control id '${control.id}' could not be found. Is this mission deleted ?", ) } + if (control.faoAreas.isNotEmpty()) { + return@filter isUnderJdp && jdp.isAttributedJdp(control) + } + // The mission must be under JDP - return@filter controlMission?.isUnderJdp == true + return@filter isUnderJdp } else -> throw IllegalArgumentException("Bad control type: ${control.actionType}") @@ -104,11 +106,18 @@ class GetActivityReports( logger.warn(e.message) } } + val faoArea = jdp.getFirstFaoAreaIncludedInJdp(control) ActivityReport( action = control, activityCode = activityCode, controlUnits = controlMission.controlUnits, + faoArea = faoArea?.faoCode, + /** + * The fleet segment is set as null, as we need to integrate the EFCA segments referential + * see: https://github.com/MTES-MCT/monitorfish/issues/3157#issuecomment-2093036583 + */ + segment = null, vesselNationalIdentifier = controlledVessel.getNationalIdentifier(), vessel = controlledVessel, ) diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/mission/mission_actions/dtos/ActivityReport.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/mission/mission_actions/dtos/ActivityReport.kt index 42aaf98de1..53df863986 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/mission/mission_actions/dtos/ActivityReport.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/mission/mission_actions/dtos/ActivityReport.kt @@ -7,6 +7,8 @@ import fr.gouv.cnsp.monitorfish.domain.entities.vessel.Vessel data class ActivityReport( val action: MissionAction, + val faoArea: String?, + val segment: String?, val activityCode: ActivityCode, // The `districtCode` and `internalReferenceNumber` concatenation val vesselNationalIdentifier: String, diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/ActivityReportDataOutput.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/ActivityReportDataOutput.kt index 9eeb5c3745..64b4842291 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/ActivityReportDataOutput.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/ActivityReportDataOutput.kt @@ -7,6 +7,8 @@ import fr.gouv.cnsp.monitorfish.domain.use_cases.mission.mission_actions.dtos.Ac data class ActivityReportDataOutput( val action: MissionActionDataOutput, val activityCode: ActivityCode, + val faoArea: String?, + val segment: String?, val vesselNationalIdentifier: String, val controlUnits: List, val vessel: VesselDataOutput, @@ -15,6 +17,8 @@ data class ActivityReportDataOutput( fun fromActivityReport(activityReport: ActivityReport) = ActivityReportDataOutput( action = MissionActionDataOutput.fromMissionAction(activityReport.action), activityCode = activityReport.activityCode, + faoArea = activityReport.faoArea, + segment = activityReport.segment, vesselNationalIdentifier = activityReport.vesselNationalIdentifier, controlUnits = activityReport.controlUnits, vessel = VesselDataOutput.fromVessel(activityReport.vessel), diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/cache/CaffeineConfiguration.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/cache/CaffeineConfiguration.kt index 2a52fd44aa..dd0546bf9c 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/cache/CaffeineConfiguration.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/cache/CaffeineConfiguration.kt @@ -52,7 +52,8 @@ class CaffeineConfiguration { val riskFactors = "risk_factors" // Segments - val currentSegments = "current_segment" + val currentSegments = "current_segments" + val segmentsByYear = "segments_by_year" // Species val allSpecies = "all_species" @@ -119,6 +120,7 @@ class CaffeineConfiguration { // Segments val currentSegmentsCache = buildMinutesCache(currentSegments, ticker, 1) + val segmentsByYearCache = buildSecondsCache(segmentsByYear, ticker, 10) // Species val allSpeciesCache = buildMinutesCache(allSpecies, ticker, oneWeek) @@ -153,6 +155,7 @@ class CaffeineConfiguration { controlAnteriorityCache, controlUnitsCache, currentSegmentsCache, + segmentsByYearCache, districtCache, faoAreasCache, findBeaconCache, diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaFleetSegmentRepository.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaFleetSegmentRepository.kt index 29b357cc56..300e31cf90 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaFleetSegmentRepository.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaFleetSegmentRepository.kt @@ -8,6 +8,7 @@ import fr.gouv.cnsp.monitorfish.domain.use_cases.dtos.CreateOrUpdateFleetSegment import fr.gouv.cnsp.monitorfish.infrastructure.database.entities.FleetSegmentEntity import fr.gouv.cnsp.monitorfish.infrastructure.database.repositories.interfaces.DBFleetSegmentRepository import jakarta.transaction.Transactional +import org.springframework.cache.annotation.Cacheable import org.springframework.stereotype.Repository @Repository @@ -21,6 +22,7 @@ class JpaFleetSegmentRepository( } } + @Cacheable(value = ["segments_by_year"]) override fun findAllByYear(year: Int): List { return dbFleetSegmentRepository.findAllByYearEquals(year).map { it.toFleetSegment() diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaMissionActionsRepository.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaMissionActionsRepository.kt index 50c6ae9bcc..4dffb405a7 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaMissionActionsRepository.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaMissionActionsRepository.kt @@ -50,7 +50,7 @@ class JpaMissionActionsRepository( return dbMissionActionsRepository.findById(id).get().toMissionAction(mapper) } - override fun findControlsInDates(beforeDateTime: ZonedDateTime, afterDateTime: ZonedDateTime): List { + override fun findSeaAndLandControlBetweenDates(beforeDateTime: ZonedDateTime, afterDateTime: ZonedDateTime): List { return dbMissionActionsRepository.findAllByActionDatetimeUtcBeforeAndActionDatetimeUtcAfterAndIsDeletedIsFalseAndActionTypeIn( beforeDateTime.toInstant(), afterDateTime.toInstant(), diff --git a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/mission/mission_actions/actrep/JointDeploymentPlanUTests.kt b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/mission/mission_actions/actrep/JointDeploymentPlanUTests.kt index 00d9661086..9e77611f44 100644 --- a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/mission/mission_actions/actrep/JointDeploymentPlanUTests.kt +++ b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/mission/mission_actions/actrep/JointDeploymentPlanUTests.kt @@ -1,83 +1,392 @@ package fr.gouv.cnsp.monitorfish.domain.entities.mission.mission_actions.actrep import com.neovisionaries.i18n.CountryCode +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.SpeciesControl import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource import org.springframework.test.context.junit.jupiter.SpringExtension +import java.time.ZonedDateTime @ExtendWith(SpringExtension::class) class JointDeploymentPlanUTests { - @Test - fun `isLandControlConcerned Should return true When a targeted specy and fao code are contained in the control`() { + @ParameterizedTest + @EnumSource(JointDeploymentPlan::class) + fun `isLandControlApplicable Should return true When a targeted specy and fao code are contained in the control`( + jdp: JointDeploymentPlan, + ) { // Given - val jdp = JointDeploymentPlan.NORTH_SEA - val faoCodes = listOf("27.4.b", "27.4.c") - val species = listOf("HKE", "ANN", "BOR") + val control = MissionAction( + id = 3, + vesselId = 2, + missionId = 3, + actionDatetimeUtc = ZonedDateTime.now(), + actionType = MissionActionType.SEA_CONTROL, + faoAreas = listOf("27.4.b", "27.4.c"), + seizureAndDiversion = false, + speciesOnboard = getSpecies(listOf("HKE", "ANN", "BOR")), + speciesInfractions = listOf(), + isDeleted = false, + hasSomeGearsSeized = false, + hasSomeSpeciesSeized = false, + isFromPoseidon = false, + completion = Completion.TO_COMPLETE, + flagState = CountryCode.FR, + userTrigram = "LTH", + ) // When - val isLandControlConcerned = jdp.isLandControlApplicable(CountryCode.FR, species, faoCodes) + val isLandControlApplicable = jdp.isLandControlApplicable(control) // Then - assertThat(isLandControlConcerned).isTrue() + when (jdp) { + JointDeploymentPlan.MEDITERRANEAN_AND_EASTERN_ATLANTIC -> assertThat(isLandControlApplicable).isFalse() + JointDeploymentPlan.NORTH_SEA -> assertThat(isLandControlApplicable).isTrue() + JointDeploymentPlan.WESTERN_WATERS -> assertThat(isLandControlApplicable).isFalse() + } } - @Test - fun `isLandControlConcerned Should return false When a targeted specy is not contained in the control`() { + @ParameterizedTest + @EnumSource(JointDeploymentPlan::class) + fun `isLandControlApplicable Should return false When a targeted specy is not contained in the control`( + jdp: JointDeploymentPlan, + ) { // Given - val jdp = JointDeploymentPlan.NORTH_SEA - val faoCodes = listOf("27.4.b", "27.4.c") - // The "HKE" specy is missing - val species = listOf("ANN", "BOR") + val control = MissionAction( + id = 3, + vesselId = 2, + missionId = 3, + actionDatetimeUtc = ZonedDateTime.now(), + actionType = MissionActionType.SEA_CONTROL, + faoAreas = listOf("27.4.b", "27.4.c"), + seizureAndDiversion = false, + // The HKE specy is missing + speciesOnboard = getSpecies(listOf("ANN", "BOR")), + speciesInfractions = listOf(), + isDeleted = false, + hasSomeGearsSeized = false, + hasSomeSpeciesSeized = false, + isFromPoseidon = false, + completion = Completion.TO_COMPLETE, + flagState = CountryCode.FR, + userTrigram = "LTH", + ) // When - val isLandControlConcerned = jdp.isLandControlApplicable(CountryCode.FR, species, faoCodes) + val isLandControlApplicable = jdp.isLandControlApplicable(control) // Then - assertThat(isLandControlConcerned).isFalse() + when (jdp) { + JointDeploymentPlan.MEDITERRANEAN_AND_EASTERN_ATLANTIC -> assertThat(isLandControlApplicable).isFalse() + JointDeploymentPlan.NORTH_SEA -> assertThat(isLandControlApplicable).isFalse() + JointDeploymentPlan.WESTERN_WATERS -> assertThat(isLandControlApplicable).isFalse() + } } - @Test - fun `isLandControlConcerned Should return false When a targeted fao code is not contained in the control`() { + @ParameterizedTest + @EnumSource(JointDeploymentPlan::class) + fun `isLandControlApplicable Should return false When a targeted fao code is not contained in the control`( + jdp: JointDeploymentPlan, + ) { // Given - val jdp = JointDeploymentPlan.NORTH_SEA - // The "27.4" fao code is missing - val faoCodes = listOf("27.5.b", "27.5.c") - val species = listOf("HKE", "ANN", "BOR") + val control = MissionAction( + id = 3, + vesselId = 2, + missionId = 3, + actionDatetimeUtc = ZonedDateTime.now(), + actionType = MissionActionType.SEA_CONTROL, + // The "27.4" fao code is missing + faoAreas = listOf("27.5.b", "27.5.c"), + seizureAndDiversion = false, + speciesOnboard = getSpecies(listOf("HKE", "ANN", "BOR")), + speciesInfractions = listOf(), + isDeleted = false, + hasSomeGearsSeized = false, + hasSomeSpeciesSeized = false, + isFromPoseidon = false, + completion = Completion.TO_COMPLETE, + flagState = CountryCode.FR, + userTrigram = "LTH", + ) // When - val isLandControlConcerned = jdp.isLandControlApplicable(CountryCode.FR, species, faoCodes) + val isLandControlApplicable = jdp.isLandControlApplicable(control) // Then - assertThat(isLandControlConcerned).isFalse() + when (jdp) { + JointDeploymentPlan.MEDITERRANEAN_AND_EASTERN_ATLANTIC -> assertThat(isLandControlApplicable).isFalse() + JointDeploymentPlan.NORTH_SEA -> assertThat(isLandControlApplicable).isFalse() + JointDeploymentPlan.WESTERN_WATERS -> assertThat(isLandControlApplicable).isFalse() + } } - @Test - fun `isLandControlConcerned Should return true When a third country vessel has species in the EU quota list`() { + @ParameterizedTest + @EnumSource(JointDeploymentPlan::class) + fun `isLandControlApplicable Should return true When a third country vessel has species in the EU quota list`( + jdp: JointDeploymentPlan, + ) { // Given - val jdp = JointDeploymentPlan.NORTH_SEA - val faoCodes = listOf("27.5.b", "27.5.c") - // ALB is contained in the quotas - val species = listOf("HKE", "ANN", "BOR", "ALB") + val control = MissionAction( + id = 3, + vesselId = 2, + missionId = 3, + actionDatetimeUtc = ZonedDateTime.now(), + actionType = MissionActionType.SEA_CONTROL, + faoAreas = listOf("27.5.b", "27.5.c"), + seizureAndDiversion = false, + // ALB is contained in the quotas + speciesOnboard = getSpecies(listOf("HKE", "ANN", "BOR", "ALB")), + speciesInfractions = listOf(), + isDeleted = false, + hasSomeGearsSeized = false, + hasSomeSpeciesSeized = false, + isFromPoseidon = false, + completion = Completion.TO_COMPLETE, + flagState = CountryCode.GB, + userTrigram = "LTH", + ) // When - val isLandControlConcerned = jdp.isLandControlApplicable(CountryCode.GB, species, faoCodes) + val isLandControlApplicable = jdp.isLandControlApplicable(control) // Then - assertThat(isLandControlConcerned).isTrue() + when (jdp) { + JointDeploymentPlan.MEDITERRANEAN_AND_EASTERN_ATLANTIC -> assertThat(isLandControlApplicable).isFalse() + JointDeploymentPlan.NORTH_SEA -> assertThat(isLandControlApplicable).isFalse() + JointDeploymentPlan.WESTERN_WATERS -> assertThat(isLandControlApplicable).isTrue() + } } - @Test - fun `isLandControlConcerned Should return false When a third country vessel has no species in the EU quota list`() { + @ParameterizedTest + @EnumSource(JointDeploymentPlan::class) + fun `isLandControlApplicable Should return false When a third country vessel has no species in the EU quota list`( + jdp: JointDeploymentPlan, + ) { // Given - val jdp = JointDeploymentPlan.NORTH_SEA - val faoCodes = listOf("27.5.b", "27.5.c") - val species = listOf("HKE", "ANN", "BOR") + val control = MissionAction( + id = 3, + vesselId = 2, + missionId = 3, + actionDatetimeUtc = ZonedDateTime.now(), + actionType = MissionActionType.SEA_CONTROL, + faoAreas = listOf("27.5.b", "27.5.c"), + seizureAndDiversion = false, + speciesOnboard = getSpecies(listOf("HKE", "ANN", "BOR")), + speciesInfractions = listOf(), + isDeleted = false, + hasSomeGearsSeized = false, + hasSomeSpeciesSeized = false, + isFromPoseidon = false, + completion = Completion.TO_COMPLETE, + flagState = CountryCode.GB, + userTrigram = "LTH", + ) // When - val isLandControlConcerned = jdp.isLandControlApplicable(CountryCode.GB, species, faoCodes) + val isLandControlApplicable = jdp.isLandControlApplicable(control) // Then - assertThat(isLandControlConcerned).isFalse() + when (jdp) { + JointDeploymentPlan.MEDITERRANEAN_AND_EASTERN_ATLANTIC -> assertThat(isLandControlApplicable).isFalse() + JointDeploymentPlan.NORTH_SEA -> assertThat(isLandControlApplicable).isFalse() + JointDeploymentPlan.WESTERN_WATERS -> assertThat(isLandControlApplicable).isFalse() + } + } + + @ParameterizedTest + @EnumSource(JointDeploymentPlan::class) + fun `getFirstFaoAreaIncludedInJdp Should return the fao area When the first JDP found is the JDP of the Act-Rep`( + jdp: JointDeploymentPlan, + ) { + // Given + val control = MissionAction( + id = 3, + vesselId = 2, + missionId = 3, + actionDatetimeUtc = ZonedDateTime.now(), + actionType = MissionActionType.SEA_CONTROL, + faoAreas = listOf("27.7.b", "27.4.c"), + seizureAndDiversion = false, + speciesInfractions = listOf(), + isDeleted = false, + hasSomeGearsSeized = false, + hasSomeSpeciesSeized = false, + isFromPoseidon = false, + completion = Completion.TO_COMPLETE, + flagState = CountryCode.FR, + userTrigram = "LTH", + ) + + // When + val faoArea = jdp.getFirstFaoAreaIncludedInJdp(control) + + // Then + when (jdp) { + JointDeploymentPlan.MEDITERRANEAN_AND_EASTERN_ATLANTIC -> assertThat(faoArea?.faoCode).isNull() + JointDeploymentPlan.NORTH_SEA -> assertThat(faoArea?.faoCode).isEqualTo("27.4.c") + JointDeploymentPlan.WESTERN_WATERS -> assertThat(faoArea?.faoCode).isNull() + } + } + + @ParameterizedTest + @EnumSource(JointDeploymentPlan::class) + fun `getFirstFaoAreaIncludedInJdp Should return the fao area for all Act-Rep When the control is done at LAND (because the filtering is done in isLandControlApplicable)`( + jdp: JointDeploymentPlan, + ) { + // Given + val control = MissionAction( + id = 3, + vesselId = 2, + missionId = 3, + actionDatetimeUtc = ZonedDateTime.now(), + actionType = MissionActionType.LAND_CONTROL, + faoAreas = listOf("27.7.b", "27.4.c"), + seizureAndDiversion = false, + speciesInfractions = listOf(), + isDeleted = false, + hasSomeGearsSeized = false, + hasSomeSpeciesSeized = false, + isFromPoseidon = false, + completion = Completion.TO_COMPLETE, + flagState = CountryCode.FR, + userTrigram = "LTH", + ) + + // When + val faoArea = jdp.getFirstFaoAreaIncludedInJdp(control) + + // Then + when (jdp) { + JointDeploymentPlan.MEDITERRANEAN_AND_EASTERN_ATLANTIC -> assertThat(faoArea?.faoCode).isEqualTo("27.7.b") + JointDeploymentPlan.NORTH_SEA -> assertThat(faoArea?.faoCode).isEqualTo("27.4.c") + JointDeploymentPlan.WESTERN_WATERS -> assertThat(faoArea?.faoCode).isEqualTo("27.7.b") + } + } + + @ParameterizedTest + @EnumSource(JointDeploymentPlan::class) + fun `getFirstFaoAreaIncludedInJdp Should return the fao area When the control contains BFT for the MEDITERRANEAN_AND_EASTERN_ATLANTIC JDP`( + jdp: JointDeploymentPlan, + ) { + // Given + val firstSpecy = SpeciesControl() + firstSpecy.speciesCode = "BFT" + val secondSpecy = SpeciesControl() + secondSpecy.speciesCode = "HKE" + val control = MissionAction( + id = 3, + vesselId = 2, + missionId = 3, + actionDatetimeUtc = ZonedDateTime.now(), + actionType = MissionActionType.SEA_CONTROL, + faoAreas = listOf("27.7.b", "27.4.c"), + seizureAndDiversion = false, + speciesOnboard = listOf(firstSpecy, secondSpecy), + speciesInfractions = listOf(), + isDeleted = false, + hasSomeGearsSeized = false, + hasSomeSpeciesSeized = false, + isFromPoseidon = false, + completion = Completion.TO_COMPLETE, + flagState = CountryCode.FR, + userTrigram = "LTH", + ) + + // When + val faoArea = jdp.getFirstFaoAreaIncludedInJdp(control) + + // Then + when (jdp) { + JointDeploymentPlan.MEDITERRANEAN_AND_EASTERN_ATLANTIC -> assertThat(faoArea?.faoCode).isEqualTo("27.7.b") + JointDeploymentPlan.NORTH_SEA -> assertThat(faoArea?.faoCode).isNull() + JointDeploymentPlan.WESTERN_WATERS -> assertThat(faoArea?.faoCode).isNull() + } + } + + @ParameterizedTest + @EnumSource(JointDeploymentPlan::class) + fun `getFirstFaoAreaIncludedInJdp Should return the fao area for the NORTH_SEA JDP When the control does not contains BFT for the MEDITERRANEAN_AND_EASTERN_ATLANTIC JDP`( + jdp: JointDeploymentPlan, + ) { + // Given + val specy = SpeciesControl() + specy.speciesCode = "HKE" + val control = MissionAction( + id = 3, + vesselId = 2, + missionId = 3, + actionDatetimeUtc = ZonedDateTime.now(), + actionType = MissionActionType.SEA_CONTROL, + faoAreas = listOf("27.7.b", "27.4.c"), + seizureAndDiversion = false, + speciesOnboard = listOf(specy), + speciesInfractions = listOf(), + isDeleted = false, + hasSomeGearsSeized = false, + hasSomeSpeciesSeized = false, + isFromPoseidon = false, + completion = Completion.TO_COMPLETE, + flagState = CountryCode.FR, + userTrigram = "LTH", + ) + + // When + val faoArea = jdp.getFirstFaoAreaIncludedInJdp(control) + + // Then + when (jdp) { + JointDeploymentPlan.MEDITERRANEAN_AND_EASTERN_ATLANTIC -> assertThat(faoArea?.faoCode).isNull() + JointDeploymentPlan.NORTH_SEA -> assertThat(faoArea?.faoCode).isEqualTo("27.4.c") + JointDeploymentPlan.WESTERN_WATERS -> assertThat(faoArea?.faoCode).isNull() + } + } + + @ParameterizedTest + @EnumSource(JointDeploymentPlan::class) + fun `getFirstFaoAreaIncludedInJdp Should return the fao area for a LAND control`( + jdp: JointDeploymentPlan, + ) { + // Given + val control = MissionAction( + id = 3, + vesselId = 2, + missionId = 3, + actionDatetimeUtc = ZonedDateTime.now(), + actionType = MissionActionType.LAND_CONTROL, + faoAreas = listOf("27.4.a"), + seizureAndDiversion = false, + speciesOnboard = getSpecies(listOf("JAX", "CRF")), + speciesInfractions = listOf(), + isDeleted = false, + hasSomeGearsSeized = false, + hasSomeSpeciesSeized = false, + isFromPoseidon = false, + completion = Completion.TO_COMPLETE, + flagState = CountryCode.GB, + userTrigram = "LTH", + ) + + // When + val faoArea = jdp.getFirstFaoAreaIncludedInJdp(control) + + // Then + when (jdp) { + JointDeploymentPlan.MEDITERRANEAN_AND_EASTERN_ATLANTIC -> assertThat(faoArea?.faoCode).isNull() + JointDeploymentPlan.NORTH_SEA -> assertThat(faoArea?.faoCode).isEqualTo("27.4.a") + JointDeploymentPlan.WESTERN_WATERS -> assertThat(faoArea?.faoCode).isNull() + } + } + + private fun getSpecies(species: List): List { + return species.map { + val specy = SpeciesControl() + specy.speciesCode = it + + return@map specy + } } } diff --git a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/faoAreas/ComputeFAOAreasFromCoordinatesUTests.kt b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/faoAreas/ComputeFAOAreasFromCoordinatesUTests.kt index ec13ea7ea5..babab880c5 100644 --- a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/faoAreas/ComputeFAOAreasFromCoordinatesUTests.kt +++ b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/faoAreas/ComputeFAOAreasFromCoordinatesUTests.kt @@ -1,18 +1,19 @@ package fr.gouv.cnsp.monitorfish.domain.use_cases.faoAreas -import com.nhaarman.mockitokotlin2.* +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.fao_area.FAOArea import fr.gouv.cnsp.monitorfish.domain.repositories.FAOAreasRepository import fr.gouv.cnsp.monitorfish.domain.use_cases.fao_areas.ComputeFAOAreasFromCoordinates import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.locationtech.jts.geom.Coordinate import org.locationtech.jts.geom.Point import org.springframework.boot.test.mock.mockito.MockBean import org.springframework.test.context.junit.jupiter.SpringExtension -import java.time.* @ExtendWith(SpringExtension::class) class ComputeFAOAreasFromCoordinatesUTests { diff --git a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/mission/mission_actions/GetActivityReportsUTests.kt b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/mission/mission_actions/GetActivityReportsUTests.kt index 2f87c09a05..b0aeee57a8 100644 --- a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/mission/mission_actions/GetActivityReportsUTests.kt +++ b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/mission/mission_actions/GetActivityReportsUTests.kt @@ -8,18 +8,13 @@ import fr.gouv.cnsp.monitorfish.domain.entities.mission.ControlUnit import fr.gouv.cnsp.monitorfish.domain.entities.mission.Mission import fr.gouv.cnsp.monitorfish.domain.entities.mission.MissionSource import fr.gouv.cnsp.monitorfish.domain.entities.mission.MissionType -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.SpeciesControl +import fr.gouv.cnsp.monitorfish.domain.entities.mission.mission_actions.* import fr.gouv.cnsp.monitorfish.domain.entities.mission.mission_actions.actrep.ActivityCode import fr.gouv.cnsp.monitorfish.domain.entities.mission.mission_actions.actrep.JointDeploymentPlan import fr.gouv.cnsp.monitorfish.domain.entities.port.Port import fr.gouv.cnsp.monitorfish.domain.entities.vessel.Vessel -import fr.gouv.cnsp.monitorfish.domain.repositories.MissionActionsRepository -import fr.gouv.cnsp.monitorfish.domain.repositories.MissionRepository -import fr.gouv.cnsp.monitorfish.domain.repositories.PortRepository -import fr.gouv.cnsp.monitorfish.domain.repositories.VesselRepository +import fr.gouv.cnsp.monitorfish.domain.repositories.* +import fr.gouv.cnsp.monitorfish.domain.use_cases.fleet_segment.TestUtils import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -27,6 +22,7 @@ import org.springframework.boot.test.mock.mockito.MockBean import org.springframework.test.context.junit.jupiter.SpringExtension import java.time.ZoneOffset import java.time.ZonedDateTime +import fr.gouv.cnsp.monitorfish.domain.entities.fleet_segment.FleetSegment as FullFleetSegment @ExtendWith(SpringExtension::class) class GetActivityReportsUTests { @@ -40,12 +36,19 @@ class GetActivityReportsUTests { @MockBean private lateinit var vesselRepository: VesselRepository + @MockBean + private lateinit var fleetSegmentRepository: FleetSegmentRepository + @MockBean private lateinit var missionRepository: MissionRepository @Test - fun `execute Should return the activity report of a JDP control`() { + fun `execute Should filter controls done in two fao areas When the first JDP found for this control is NORTH_SEA`() { // Given + given(fleetSegmentRepository.findAllByYear(any())).willReturn( + TestUtils.getDummyFleetSegments(), + ) + val species = SpeciesControl() species.speciesCode = "HKE" @@ -56,7 +59,11 @@ class GetActivityReportsUTests { missionId = 1, actionDatetimeUtc = ZonedDateTime.now(), portLocode = "AEFAT", - faoAreas = listOf("27.4.b", "27.4.c"), + faoAreas = listOf("27.7.b", "27.4.c"), + segments = listOf( + FleetSegment("NWW01/02", "Trawl"), + FleetSegment("NS01/03", "North sea"), + ), actionType = MissionActionType.LAND_CONTROL, gearOnboard = listOf(), speciesOnboard = listOf(species), @@ -75,6 +82,7 @@ class GetActivityReportsUTests { missionId = 2, actionDatetimeUtc = ZonedDateTime.now(), actionType = MissionActionType.SEA_CONTROL, + faoAreas = listOf("27.7.b", "27.4.c"), seizureAndDiversion = false, speciesInfractions = listOf(), isDeleted = false, @@ -91,6 +99,7 @@ class GetActivityReportsUTests { missionId = 3, actionDatetimeUtc = ZonedDateTime.now(), actionType = MissionActionType.SEA_CONTROL, + faoAreas = listOf("27.7.b", "27.4.c"), seizureAndDiversion = false, speciesInfractions = listOf(), isDeleted = false, @@ -102,7 +111,146 @@ class GetActivityReportsUTests { completion = Completion.TO_COMPLETE, ), ) - given(missionActionsRepository.findControlsInDates(any(), any())).willReturn(controls) + given(missionActionsRepository.findSeaAndLandControlBetweenDates(any(), any())).willReturn(controls) + + val vessels = listOf( + Vessel( + id = 1, + internalReferenceNumber = "FR00022680", + vesselName = "MY AWESOME VESSEL", + flagState = CountryCode.FR, + declaredFishingGears = listOf("Trémails"), + vesselType = "Fishing", + districtCode = "AY", + ), + Vessel( + id = 2, + internalReferenceNumber = "FR00065455", + vesselName = "MY SECOND AWESOME VESSEL", + flagState = CountryCode.FR, + declaredFishingGears = listOf("Trémails"), + vesselType = "Fishing", + districtCode = "LO", + ), + ) + given(vesselRepository.findVesselsByIds(eq(listOf(1, 2)))).willReturn(vessels) + + val missions = listOf( + Mission( + 1, + missionTypes = listOf(MissionType.LAND), + missionSource = MissionSource.MONITORFISH, + isUnderJdp = false, + isGeometryComputedFromControls = false, + startDateTimeUtc = ZonedDateTime.of(2020, 5, 5, 3, 4, 5, 3, ZoneOffset.UTC), + ), + Mission( + 2, + missionTypes = listOf(MissionType.SEA), + missionSource = MissionSource.MONITORFISH, + isUnderJdp = true, + isGeometryComputedFromControls = false, + startDateTimeUtc = ZonedDateTime.of(2020, 5, 5, 3, 4, 5, 3, ZoneOffset.UTC), + ), + Mission( + 2, + missionTypes = listOf(MissionType.SEA), + missionSource = MissionSource.MONITORFISH, + isUnderJdp = false, + isGeometryComputedFromControls = false, + startDateTimeUtc = ZonedDateTime.of(2020, 5, 5, 3, 4, 5, 3, ZoneOffset.UTC), + ), + ) + given(missionRepository.findByIds(listOf(1, 2, 3))).willReturn(missions) + given(portRepository.findByLocode(eq("AEFAT"))).willReturn(Port("AEFAT", "Al Jazeera Port")) + + // When + val activityReports = GetActivityReports( + missionActionsRepository, + portRepository, + vesselRepository, + missionRepository, + ).execute( + ZonedDateTime.now(), + ZonedDateTime.now().minusDays(1), + JointDeploymentPlan.WESTERN_WATERS, + ) + + // Then + assertThat(activityReports.jdpSpecies).hasSize(35) + assertThat(activityReports.activityReports).hasSize(0) + } + + @Test + fun `execute Should include a control done in two fao areas as the first JDP found for this control is NORTH_SEA`() { + // Given + given(fleetSegmentRepository.findAllByYear(any())).willReturn( + TestUtils.getDummyFleetSegments(), + ) + + val species = SpeciesControl() + species.speciesCode = "HKE" + + val controls = listOf( + MissionAction( + id = 1, + vesselId = 1, + missionId = 1, + actionDatetimeUtc = ZonedDateTime.now(), + portLocode = "AEFAT", + faoAreas = listOf("27.7.b", "27.4.c"), + segments = listOf( + FleetSegment("NWW01/02", "Trawl"), + FleetSegment("NS01/03", "North sea"), + ), + actionType = MissionActionType.LAND_CONTROL, + gearOnboard = listOf(), + speciesOnboard = listOf(species), + seizureAndDiversion = true, + isDeleted = false, + hasSomeGearsSeized = false, + hasSomeSpeciesSeized = false, + isFromPoseidon = false, + completion = Completion.TO_COMPLETE, + flagState = CountryCode.FR, + userTrigram = "LTH", + ), + MissionAction( + id = 2, + vesselId = 1, + missionId = 2, + actionDatetimeUtc = ZonedDateTime.now(), + actionType = MissionActionType.SEA_CONTROL, + faoAreas = listOf("27.7.b", "27.4.c"), + seizureAndDiversion = false, + speciesInfractions = listOf(), + isDeleted = false, + hasSomeGearsSeized = false, + hasSomeSpeciesSeized = false, + isFromPoseidon = false, + completion = Completion.TO_COMPLETE, + flagState = CountryCode.FR, + userTrigram = "LTH", + ), + MissionAction( + id = 3, + vesselId = 2, + missionId = 3, + actionDatetimeUtc = ZonedDateTime.now(), + actionType = MissionActionType.SEA_CONTROL, + faoAreas = listOf("27.7.b", "27.4.c"), + seizureAndDiversion = false, + speciesInfractions = listOf(), + isDeleted = false, + hasSomeGearsSeized = false, + hasSomeSpeciesSeized = false, + isFromPoseidon = false, + completion = Completion.TO_COMPLETE, + flagState = CountryCode.FR, + userTrigram = "LTH", + ), + ) + given(missionActionsRepository.findSeaAndLandControlBetweenDates(any(), any())).willReturn(controls) val vessels = listOf( Vessel( @@ -169,18 +317,265 @@ class GetActivityReportsUTests { // Then assertThat(activityReports.jdpSpecies).hasSize(38) - assertThat(activityReports.activityReports).hasSize(2) - val landReport = activityReports.activityReports.first() - assertThat(landReport.activityCode).isEqualTo(ActivityCode.LAN) - assertThat(landReport.action.portName).isEqualTo("Al Jazeera Port") - val seaReport = activityReports.activityReports.last() - assertThat(seaReport.activityCode).isEqualTo(ActivityCode.FIS) - assertThat(landReport.vesselNationalIdentifier).isEqualTo("AYFR00022680") + assertThat(activityReports.activityReports).hasSize(1) + + activityReports.activityReports.first().let { seaReport -> + assertThat(seaReport.activityCode).isEqualTo(ActivityCode.FIS) + assertThat(seaReport.vesselNationalIdentifier).isEqualTo("AYFR00022680") + assertThat(seaReport.faoArea).isEqualTo("27.4.c") + assertThat(seaReport.segment).isNull() + } + } + + @Test + fun `execute Should add the fao area for a LAND control`() { + // Given + val species = SpeciesControl() + species.speciesCode = "HKE" + + given(fleetSegmentRepository.findAllByYear(any())).willReturn( + listOf( + FullFleetSegment( + "NS01/03", + "Otter trawls/Seines", + gears = listOf("OTB", "OTT", "TBN", "PTB", "SDN", "SSC", "SPR", "OT", "TBS", "OTM", "PTM", "TMS", "TM", "TX", "TB", "SX", "SV"), + targetSpecies = listOf("COD", "HAD", "WHG", "POK", "SOL", "PLE", "NEP", "HKE"), + faoAreas = listOf("27.2.a", "27.4.a", "27.4.b", "27.4.c"), + year = ZonedDateTime.now().year, + impactRiskFactor = 2.56, + ), + ), + ) + + val controls = listOf( + MissionAction( + id = 1, + vesselId = 1, + missionId = 1, + actionDatetimeUtc = ZonedDateTime.now(), + portLocode = "AEFAT", + faoAreas = listOf("27.4.a"), + segments = listOf( + FleetSegment("NS01/03", "North Sea"), + ), + actionType = MissionActionType.LAND_CONTROL, + gearOnboard = listOf(), + speciesOnboard = listOf(species), + seizureAndDiversion = true, + isDeleted = false, + hasSomeGearsSeized = false, + hasSomeSpeciesSeized = false, + isFromPoseidon = false, + completion = Completion.TO_COMPLETE, + flagState = CountryCode.FR, + userTrigram = "LTH", + ), + ) + given(missionActionsRepository.findSeaAndLandControlBetweenDates(any(), any())).willReturn(controls) + + val vessels = listOf( + Vessel( + id = 1, + internalReferenceNumber = "FR00022680", + vesselName = "MY AWESOME VESSEL", + flagState = CountryCode.FR, + declaredFishingGears = listOf("Trémails"), + vesselType = "Fishing", + districtCode = "AY", + ), + ) + given(vesselRepository.findVesselsByIds(eq(listOf(1)))).willReturn(vessels) + + val missions = listOf( + Mission( + 1, + missionTypes = listOf(MissionType.LAND), + missionSource = MissionSource.MONITORFISH, + isUnderJdp = false, + isGeometryComputedFromControls = false, + startDateTimeUtc = ZonedDateTime.of(2020, 5, 5, 3, 4, 5, 3, ZoneOffset.UTC), + ), + ) + given(missionRepository.findByIds(eq(listOf(1)))).willReturn(missions) + given(portRepository.findByLocode(eq("AEFAT"))).willReturn(Port("AEFAT", "Al Jazeera Port")) + + // When + val activityReports = GetActivityReports( + missionActionsRepository, + portRepository, + vesselRepository, + missionRepository, + ).execute( + ZonedDateTime.now(), + ZonedDateTime.now().minusDays(1), + JointDeploymentPlan.NORTH_SEA, + ) + + // Then + assertThat(activityReports.activityReports).hasSize(1) + + activityReports.activityReports.first().let { landReport -> + assertThat(landReport.activityCode).isEqualTo(ActivityCode.LAN) + assertThat(landReport.action.portName).isEqualTo("Al Jazeera Port") + assertThat(landReport.faoArea).isEqualTo("27.4.a") + assertThat(landReport.segment).isNull() + } + } + + @Test + fun `execute Should filter a control done outside the JDP FAO area`() { + // Given + given(fleetSegmentRepository.findAllByYear(any())).willReturn( + TestUtils.getDummyFleetSegments(), + ) + + val species = SpeciesControl() + species.speciesCode = "HKE" + + val controls = listOf( + MissionAction( + id = 2, + vesselId = 1, + missionId = 2, + actionDatetimeUtc = ZonedDateTime.now(), + actionType = MissionActionType.SEA_CONTROL, + // These fao areas are outside WESTERN WATERS + faoAreas = listOf("27.4.c", "27.4.b"), + seizureAndDiversion = false, + speciesInfractions = listOf(), + isDeleted = false, + hasSomeGearsSeized = false, + hasSomeSpeciesSeized = false, + isFromPoseidon = false, + completion = Completion.TO_COMPLETE, + flagState = CountryCode.FR, + userTrigram = "LTH", + ), + ) + given(missionActionsRepository.findSeaAndLandControlBetweenDates(any(), any())).willReturn(controls) + + val vessels = listOf( + Vessel( + id = 1, + internalReferenceNumber = "FR00022680", + vesselName = "MY AWESOME VESSEL", + flagState = CountryCode.FR, + declaredFishingGears = listOf("Trémails"), + vesselType = "Fishing", + districtCode = "AY", + ), + ) + given(vesselRepository.findVesselsByIds(eq(listOf(1)))).willReturn(vessels) + + val missions = listOf( + Mission( + 2, + missionTypes = listOf(MissionType.SEA), + missionSource = MissionSource.MONITORFISH, + isUnderJdp = true, + isGeometryComputedFromControls = false, + startDateTimeUtc = ZonedDateTime.of(2020, 5, 5, 3, 4, 5, 3, ZoneOffset.UTC), + ), + ) + given(missionRepository.findByIds(listOf(2))).willReturn(missions) + + // When + val activityReports = GetActivityReports( + missionActionsRepository, + portRepository, + vesselRepository, + missionRepository, + ).execute( + ZonedDateTime.now(), + ZonedDateTime.now().minusDays(1), + JointDeploymentPlan.WESTERN_WATERS, + ) + + // Then + assertThat(activityReports.activityReports).hasSize(0) + } + + @Test + fun `execute Should include a control done within the JDP FAO area`() { + // Given + given(fleetSegmentRepository.findAllByYear(any())).willReturn( + TestUtils.getDummyFleetSegments(), + ) + + val species = SpeciesControl() + species.speciesCode = "HKE" + + val controls = listOf( + MissionAction( + id = 2, + vesselId = 1, + missionId = 2, + actionDatetimeUtc = ZonedDateTime.now(), + actionType = MissionActionType.SEA_CONTROL, + // The first fao area "27.7.c" is within WESTERN_WATERS + // The second fao area "27.4.b" is within NORTH_SEA + faoAreas = listOf("27.7.c", "27.4.b"), + seizureAndDiversion = false, + speciesInfractions = listOf(), + isDeleted = false, + hasSomeGearsSeized = false, + hasSomeSpeciesSeized = false, + isFromPoseidon = false, + completion = Completion.TO_COMPLETE, + flagState = CountryCode.FR, + userTrigram = "LTH", + ), + ) + given(missionActionsRepository.findSeaAndLandControlBetweenDates(any(), any())).willReturn(controls) + + val vessels = listOf( + Vessel( + id = 1, + internalReferenceNumber = "FR00022680", + vesselName = "MY AWESOME VESSEL", + flagState = CountryCode.FR, + declaredFishingGears = listOf("Trémails"), + vesselType = "Fishing", + districtCode = "AY", + ), + ) + given(vesselRepository.findVesselsByIds(eq(listOf(1)))).willReturn(vessels) + + val missions = listOf( + Mission( + 2, + missionTypes = listOf(MissionType.SEA), + missionSource = MissionSource.MONITORFISH, + isUnderJdp = true, + isGeometryComputedFromControls = false, + startDateTimeUtc = ZonedDateTime.of(2020, 5, 5, 3, 4, 5, 3, ZoneOffset.UTC), + ), + ) + given(missionRepository.findByIds(listOf(2))).willReturn(missions) + + // When + val activityReports = GetActivityReports( + missionActionsRepository, + portRepository, + vesselRepository, + missionRepository, + ).execute( + ZonedDateTime.now(), + ZonedDateTime.now().minusDays(1), + JointDeploymentPlan.NORTH_SEA, + ) + + // Then + assertThat(activityReports.activityReports).hasSize(1) } @Test fun `execute Should not throw When a SEA mission is not found in the mission repository`() { // Given + given(fleetSegmentRepository.findAllByYear(any())).willReturn( + TestUtils.getDummyFleetSegments(), + ) + val species = SpeciesControl() species.speciesCode = "HKE" @@ -237,7 +632,7 @@ class GetActivityReportsUTests { completion = Completion.TO_COMPLETE, ), ) - given(missionActionsRepository.findControlsInDates(any(), any())).willReturn(controls) + given(missionActionsRepository.findSeaAndLandControlBetweenDates(any(), any())).willReturn(controls) val vessels = listOf( Vessel( @@ -306,6 +701,10 @@ class GetActivityReportsUTests { @Test fun `execute Should not throw When a LAND mission is not found in the mission repository`() { // Given + given(fleetSegmentRepository.findAllByYear(any())).willReturn( + TestUtils.getDummyFleetSegments(), + ) + val species = SpeciesControl() species.speciesCode = "HKE" @@ -346,7 +745,7 @@ class GetActivityReportsUTests { completion = Completion.TO_COMPLETE, ), ) - given(missionActionsRepository.findControlsInDates(any(), any())).willReturn(controls) + given(missionActionsRepository.findSeaAndLandControlBetweenDates(any(), any())).willReturn(controls) val vessels = listOf( Vessel( @@ -425,7 +824,7 @@ class GetActivityReportsUTests { userTrigram = "CPAMOI", ), ) - given(missionActionsRepository.findControlsInDates(any(), any())).willReturn(controls) + given(missionActionsRepository.findSeaAndLandControlBetweenDates(any(), any())).willReturn(controls) val vessels = listOf( Vessel( diff --git a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/bff/MissionActionsControllerITests.kt b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/bff/MissionActionsControllerITests.kt index 5afcfaa35e..cfd0067365 100644 --- a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/bff/MissionActionsControllerITests.kt +++ b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/bff/MissionActionsControllerITests.kt @@ -361,6 +361,8 @@ class MissionActionsControllerITests { activityCode = ActivityCode.FIS, vesselNationalIdentifier = "AYFR000654", controlUnits = listOf(ControlUnit(1234, "DIRM", false, "Cross Etel", listOf())), + faoArea = "27.7.c", + segment = "NS01/03", vessel = Vessel( id = 1, internalReferenceNumber = "FR00022680", diff --git a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaFleetSegmentRepositoryITests.kt b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaFleetSegmentRepositoryITests.kt index 1e9070ae79..7779e51f40 100644 --- a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaFleetSegmentRepositoryITests.kt +++ b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaFleetSegmentRepositoryITests.kt @@ -29,6 +29,8 @@ class JpaFleetSegmentRepositoryITests : AbstractDBTests() { @BeforeEach fun setup() { cacheManager.getCache("fleet_segments")?.clear() + cacheManager.getCache("current_segments")?.clear() + cacheManager.getCache("segments_by_year")?.clear() } @Test @@ -187,6 +189,7 @@ class JpaFleetSegmentRepositoryITests : AbstractDBTests() { jpaFleetSegmentRepository.delete(segmentToDelete.segment, currentYear - 1) // Then + cacheManager.getCache("segments_by_year")?.clear() val expectedFleetSegment = jpaFleetSegmentRepository.findAllByYear(currentYear - 1) assertThat(expectedFleetSegment).hasSize(22) assertThat(expectedFleetSegment).doesNotContain(segmentToDelete) @@ -228,6 +231,7 @@ class JpaFleetSegmentRepositoryITests : AbstractDBTests() { jpaFleetSegmentRepository.addYear(currentYear - 1, currentYear + 1) // Then + cacheManager.getCache("segments_by_year")?.clear() assertThat(jpaFleetSegmentRepository.findAllByYear(currentYear - 1)).hasSize(23) val updatedFleetSegments = jpaFleetSegmentRepository.findAllByYear(currentYear + 1).sortedBy { it.segment } assertThat(updatedFleetSegments).hasSize(23) diff --git a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaMissionActionRepositoryITests.kt b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaMissionActionRepositoryITests.kt index 1c1bf75e74..bb3a32883b 100644 --- a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaMissionActionRepositoryITests.kt +++ b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaMissionActionRepositoryITests.kt @@ -301,7 +301,7 @@ class JpaMissionActionRepositoryITests : AbstractDBTests() { val beforeDateTime = ZonedDateTime.now().plusWeeks(1) // When - val controls = jpaMissionActionsRepository.findControlsInDates(beforeDateTime, afterDateTime) + val controls = jpaMissionActionsRepository.findSeaAndLandControlBetweenDates(beforeDateTime, afterDateTime) // Then assertThat(controls).hasSize(1) @@ -318,7 +318,7 @@ class JpaMissionActionRepositoryITests : AbstractDBTests() { val afterDateTime = ZonedDateTime.parse("2020-01-17T00:00:00.000Z") // When - val controls = jpaMissionActionsRepository.findControlsInDates(beforeDateTime, afterDateTime) + val controls = jpaMissionActionsRepository.findSeaAndLandControlBetweenDates(beforeDateTime, afterDateTime) // Then assertThat(controls).hasSize(1) diff --git a/frontend/cypress/e2e/side_window/mission_list/export_activity_reports.spec.ts b/frontend/cypress/e2e/side_window/mission_list/export_activity_reports.spec.ts index d9a868efd3..9d908a341b 100644 --- a/frontend/cypress/e2e/side_window/mission_list/export_activity_reports.spec.ts +++ b/frontend/cypress/e2e/side_window/mission_list/export_activity_reports.spec.ts @@ -32,7 +32,7 @@ context('Side Window > Mission List > Export Activity Reports', () => { ) .should( 'contains', - '"LCross Etel","L","Cross Etel","","INSPECTION","2020018","7:19","7:19","FRA","FRA","","","","Vessel","FRA","AYFAK000999999"' + '"LCross Etel","L","Cross Etel","","INSPECTION","20200118","07:19","07:19","FRA","FRA","","","","Vessel","FRA","AYFAK000999999"' ) .should( 'contains', diff --git a/frontend/src/features/ActivityReport/components/ExportActivityReportsDialog/csvMap.ts b/frontend/src/features/ActivityReport/components/ExportActivityReportsDialog/csvMap.ts index ccdbbda3c6..25750dc6db 100644 --- a/frontend/src/features/ActivityReport/components/ExportActivityReportsDialog/csvMap.ts +++ b/frontend/src/features/ActivityReport/components/ExportActivityReportsDialog/csvMap.ts @@ -32,7 +32,7 @@ export const JDP_CSV_MAP_BASE: DownloadAsCsvMap = { transform: activity => { const dateTime = customDayjs(activity.action.actionDatetimeUtc) - return `${dateTime.year()}${dateTime.month()}${dateTime.date()}` + return dateTime.format('YYYYMMDD') } }, eventTime: { @@ -40,7 +40,7 @@ export const JDP_CSV_MAP_BASE: DownloadAsCsvMap = { transform: activity => { const dateTime = customDayjs(activity.action.actionDatetimeUtc) - return `${dateTime.hour()}:${dateTime.minute()}` + return dateTime.format('HH:mm') } }, // See MED JDP Decision 2018/030 (3.6.1.1) @@ -49,7 +49,7 @@ export const JDP_CSV_MAP_BASE: DownloadAsCsvMap = { transform: activity => { const dateTime = customDayjs(activity.action.actionDatetimeUtc) - return `${dateTime.hour()}:${dateTime.minute()}` + return dateTime.format('HH:mm') } }, leadingState: { @@ -89,11 +89,11 @@ export const JDP_CSV_MAP_BASE: DownloadAsCsvMap = { }, faoArea: { label: 'FAO_AREA_CODE', - transform: activity => activity.action.faoAreas[0] ?? '' + transform: activity => activity.faoArea ?? '' }, fleetSegment: { label: 'FLEET_SEGMENT', - transform: activity => activity.action.segments[0]?.segment ?? '' + transform: activity => activity.segment ?? '' }, latitude: { label: 'LA', diff --git a/frontend/src/features/ActivityReport/types.ts b/frontend/src/features/ActivityReport/types.ts index 3412871384..f73e83b942 100644 --- a/frontend/src/features/ActivityReport/types.ts +++ b/frontend/src/features/ActivityReport/types.ts @@ -11,6 +11,8 @@ export type ActivityReport = { action: MissionAction.MissionAction activityCode: ActivityCode controlUnits: LegacyControlUnit.LegacyControlUnit[] + faoArea: string | undefined + segment: string | undefined vessel: Vessel.Vessel vesselNationalIdentifier: string }