Skip to content

Commit

Permalink
[Act-Rep] Correction du double comptage des contrôles (#3172)
Browse files Browse the repository at this point in the history
## Linked issues

- Resolve #3157
- Resolve #2929

----

- [ ] Tests E2E (Cypress)
  • Loading branch information
louptheron authored May 29, 2024
2 parents 48e9eca + 217db88 commit 37da601
Show file tree
Hide file tree
Showing 19 changed files with 937 additions and 103 deletions.
Original file line number Diff line number Diff line change
@@ -1,38 +1,74 @@
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<FaoZonesAndSpecy>) {
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<FaoZonesAndSpecy> {
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<FaoZonesAndSpecy>, private val operationalZones: List<String>) {
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<String> {
return this.species.map { it.second }.distinct()
}

private fun getOperationalZones(): List<String> {
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<String>, tripFaoCodes: List<String>): 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 ->
// The JDP FAO zone is included in at least one trip FAO code
tripFaoCodes.any { tripFaoCode -> tripFaoCode.contains(jdpFaoCode) }
}

return@any isSpecyFoundInJdpSPecies && isFaoZoneFoundInJdpFaoZones
return@any isSpecyFoundInJdpSpecies && isFaoZoneFoundInJdpFaoZones
}

val hasSpeciesInEUQuotas = if (isThirdCountryVessel) {
Expand All @@ -43,4 +79,67 @@ enum class JointDeploymentPlan(private val species: List<FaoZonesAndSpecy>) {

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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ typealias FaoZonesAndSpecy = Pair<FaoZones, String>
* * 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<FaoZonesAndSpecy> = generateSpeciesWithFaoCode(
MED_FAO_CODES,
MEDITERRANEAN_OPERATIONAL_ZONES,
listOf(
"ANE",
"HOM",
Expand Down Expand Up @@ -67,12 +67,11 @@ val MEDITERRANEAN_AND_EASTERN_ATLANTIC_SPECIES: List<FaoZonesAndSpecy> = 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<FaoZonesAndSpecy> = generateSpeciesWithFaoCode(
NS_01_FAO_CODES,
NORTH_SEA_OPERATIONAL_ZONES,
listOf(
"HOM",
"JAX",
Expand Down Expand Up @@ -115,12 +114,11 @@ val NORTH_SEA_SPECIES: List<FaoZonesAndSpecy> = generateSpeciesWithFaoCode(
),
)

val WW_01_FAO_CODES = listOf("27.6", "27.7", "27.8", "27.9", "27.10")
val WESTERN_WATERS_SPECIES: List<FaoZonesAndSpecy> = 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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ interface MissionActionsRepository {
fun findById(id: Int): MissionAction
fun findByMissionId(missionId: Int): List<MissionAction>
fun findVesselMissionActionsAfterDateTime(vesselId: Int, afterDateTime: ZonedDateTime): List<MissionAction>
fun findControlsInDates(beforeDateTime: ZonedDateTime, afterDateTime: ZonedDateTime): List<MissionAction>
fun findSeaAndLandControlBetweenDates(beforeDateTime: ZonedDateTime, afterDateTime: ZonedDateTime): List<MissionAction>
fun findMissionActionsIn(missionIds: List<Int>): List<MissionAction>
fun save(missionAction: MissionAction): MissionAction
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand All @@ -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}")
Expand Down Expand Up @@ -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,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ControlUnit>,
val vessel: VesselDataOutput,
Expand All @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -153,6 +155,7 @@ class CaffeineConfiguration {
controlAnteriorityCache,
controlUnitsCache,
currentSegmentsCache,
segmentsByYearCache,
districtCache,
faoAreasCache,
findBeaconCache,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -21,6 +22,7 @@ class JpaFleetSegmentRepository(
}
}

@Cacheable(value = ["segments_by_year"])
override fun findAllByYear(year: Int): List<FleetSegment> {
return dbFleetSegmentRepository.findAllByYearEquals(year).map {
it.toFleetSegment()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class JpaMissionActionsRepository(
return dbMissionActionsRepository.findById(id).get().toMissionAction(mapper)
}

override fun findControlsInDates(beforeDateTime: ZonedDateTime, afterDateTime: ZonedDateTime): List<MissionAction> {
override fun findSeaAndLandControlBetweenDates(beforeDateTime: ZonedDateTime, afterDateTime: ZonedDateTime): List<MissionAction> {
return dbMissionActionsRepository.findAllByActionDatetimeUtcBeforeAndActionDatetimeUtcAfterAndIsDeletedIsFalseAndActionTypeIn(
beforeDateTime.toInstant(),
afterDateTime.toInstant(),
Expand Down
Loading

0 comments on commit 37da601

Please sign in to comment.