From 61b28ed906e448f1b906a377686a03bbd0d5af1d Mon Sep 17 00:00:00 2001 From: Ivan Gabriele Date: Thu, 22 Aug 2024 15:08:51 +0200 Subject: [PATCH 1/8] Add FAO area per species in manual pno create/update Backend --- .../mission_actions/PatchableMissionAction.kt | 2 +- .../ComputeManualPriorNotification.kt | 8 +++-- .../CreateOrUpdateManualPriorNotification.kt | 12 ++++++-- .../api/bff/PriorNotificationController.kt | 6 ++-- ...ManualPriorNotificationComputeDataInput.kt | 2 +- ...lPriorNotificationFishingCatchDataInput.kt | 3 +- .../ManualPriorNotificationFormDataInput.kt | 2 +- ...PriorNotificationFishingCatchDataOutput.kt | 10 +++++-- .../ManualPriorNotificationFormDataOutput.kt | 29 +++++++++++-------- ...teOrUpdateManualPriorNotificationITests.kt | 4 +-- ...teOrUpdateManualPriorNotificationUTests.kt | 2 +- .../UpdateLogbookPriorNotificationITests.kt | 5 ++-- .../VerifyAndSendPriorNotificationITests.kt | 4 ++- .../bff/PriorNotificationControllerUTests.kt | 8 ++--- 14 files changed, 60 insertions(+), 37 deletions(-) diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/mission/mission_actions/PatchableMissionAction.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/mission/mission_actions/PatchableMissionAction.kt index 72e950e723..85af7dd296 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/mission/mission_actions/PatchableMissionAction.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/mission/mission_actions/PatchableMissionAction.kt @@ -4,7 +4,7 @@ import java.time.ZonedDateTime import java.util.* data class PatchableMissionAction( - val actionDatetimeUtc : Optional?, + val actionDatetimeUtc: Optional?, val actionEndDatetimeUtc: Optional?, val observationsByUnit: Optional?, ) diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/ComputeManualPriorNotification.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/ComputeManualPriorNotification.kt index 8dc44d2d0c..ffe143961b 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/ComputeManualPriorNotification.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/ComputeManualPriorNotification.kt @@ -22,16 +22,18 @@ class ComputeManualPriorNotification( private val computeRiskFactor: ComputeRiskFactor, ) { fun execute( - faoArea: String, fishingCatches: List, + /** When there is a single FAO area shared by all fishing catches. */ + globalFaoArea: String?, portLocode: String, tripGearCodes: List, vesselId: Int, ): ManualPriorNotificationComputedValues { val vessel = vesselRepository.findVesselById(vesselId) - val faoAreas = listOf(faoArea) - val fishingCatchesWithFaoArea = fishingCatches.map { it.copy(faoZone = faoArea) } + val faoAreas = globalFaoArea?.let { listOf(globalFaoArea) } ?: fishingCatches.mapNotNull { it.faoZone } + val fishingCatchesWithFaoArea = globalFaoArea?.let { fishingCatches.map { it.copy(faoZone = globalFaoArea) } } + ?: fishingCatches val specyCodes = fishingCatches.mapNotNull { it.species } val vesselCfr = vessel?.internalReferenceNumber val vesselFlagCountryCode = vessel?.flagState diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/CreateOrUpdateManualPriorNotification.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/CreateOrUpdateManualPriorNotification.kt index 2437c47a4b..e218ca175f 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/CreateOrUpdateManualPriorNotification.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/CreateOrUpdateManualPriorNotification.kt @@ -35,8 +35,13 @@ class CreateOrUpdateManualPriorNotification( didNotFishAfterZeroNotice: Boolean, expectedArrivalDate: ZonedDateTime, expectedLandingDate: ZonedDateTime, - faoArea: String, fishingCatches: List, + /** + * Single FAO area shared by all fishing catches. + * + * Take precedence over the FAO area of each fishing catch if set. + */ + globalFaoArea: String?, hasPortEntranceAuthorization: Boolean, hasPortLandingAuthorization: Boolean, note: String?, @@ -49,8 +54,8 @@ class CreateOrUpdateManualPriorNotification( // /!\ Backend computed vessel risk factor is only used as a real time Frontend indicator. // The Backend should NEVER update `risk_factors` DB table, only the pipeline is allowed to update it. val computedValues = computeManualPriorNotification.execute( - faoArea, fishingCatches, + globalFaoArea, portLocode, tripGearCodes, vesselId, @@ -60,7 +65,8 @@ class CreateOrUpdateManualPriorNotification( pnoVesselSubscriptionRepository.has(vesselId) || pnoSegmentSubscriptionRepository.has(portLocode, computedValues.tripSegments.map { it.segment }) - val fishingCatchesWithFaoArea = fishingCatches.map { it.copy(faoZone = faoArea) } + val fishingCatchesWithFaoArea = globalFaoArea?.let { fishingCatches.map { it.copy(faoZone = globalFaoArea) } } + ?: fishingCatches val tripGears = getTripGears(tripGearCodes) val tripSegments = computedValues.tripSegments.map { it.toLogbookTripSegment() } val vessel = vesselRepository.findVesselById(vesselId) diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/bff/PriorNotificationController.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/bff/PriorNotificationController.kt index c55c7b2d68..855f699a30 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/bff/PriorNotificationController.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/bff/PriorNotificationController.kt @@ -187,8 +187,8 @@ class PriorNotificationController( val fishingCatches = manualPriorNotificationComputeDataInput.fishingCatches.map { it.toLogbookFishingCatch() } val manualPriorNotificationComputedValues = computeManualPriorNotification.execute( - faoArea = manualPriorNotificationComputeDataInput.faoArea, fishingCatches = fishingCatches, + globalFaoArea = manualPriorNotificationComputeDataInput.globalFaoArea, portLocode = manualPriorNotificationComputeDataInput.portLocode, tripGearCodes = manualPriorNotificationComputeDataInput.tripGearCodes, vesselId = manualPriorNotificationComputeDataInput.vesselId, @@ -230,7 +230,7 @@ class PriorNotificationController( didNotFishAfterZeroNotice = manualPriorNotificationFormDataInput.didNotFishAfterZeroNotice, expectedArrivalDate = manualPriorNotificationFormDataInput.expectedArrivalDate, expectedLandingDate = manualPriorNotificationFormDataInput.expectedLandingDate, - faoArea = manualPriorNotificationFormDataInput.faoArea, + globalFaoArea = manualPriorNotificationFormDataInput.globalFaoArea, fishingCatches = manualPriorNotificationFormDataInput.fishingCatches.map { it.toLogbookFishingCatch() }, note = manualPriorNotificationFormDataInput.note, portLocode = manualPriorNotificationFormDataInput.portLocode, @@ -260,7 +260,7 @@ class PriorNotificationController( didNotFishAfterZeroNotice = manualPriorNotificationFormDataInput.didNotFishAfterZeroNotice, expectedArrivalDate = manualPriorNotificationFormDataInput.expectedArrivalDate, expectedLandingDate = manualPriorNotificationFormDataInput.expectedLandingDate, - faoArea = manualPriorNotificationFormDataInput.faoArea, + globalFaoArea = manualPriorNotificationFormDataInput.globalFaoArea, fishingCatches = manualPriorNotificationFormDataInput.fishingCatches.map { it.toLogbookFishingCatch() }, note = manualPriorNotificationFormDataInput.note, portLocode = manualPriorNotificationFormDataInput.portLocode, diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/input/ManualPriorNotificationComputeDataInput.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/input/ManualPriorNotificationComputeDataInput.kt index ba96dd953e..34d1cdca9a 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/input/ManualPriorNotificationComputeDataInput.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/input/ManualPriorNotificationComputeDataInput.kt @@ -1,8 +1,8 @@ package fr.gouv.cnsp.monitorfish.infrastructure.api.input data class ManualPriorNotificationComputeDataInput( - val faoArea: String, val fishingCatches: List, + val globalFaoArea: String?, val portLocode: String, val tripGearCodes: List, val vesselId: Int, diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/input/ManualPriorNotificationFishingCatchDataInput.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/input/ManualPriorNotificationFishingCatchDataInput.kt index d4538396a8..2f05e44a10 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/input/ManualPriorNotificationFishingCatchDataInput.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/input/ManualPriorNotificationFishingCatchDataInput.kt @@ -3,6 +3,7 @@ package fr.gouv.cnsp.monitorfish.infrastructure.api.input import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookFishingCatch data class ManualPriorNotificationFishingCatchDataInput( + val faoArea: String?, val quantity: Double?, val specyCode: String, val specyName: String, @@ -13,7 +14,7 @@ data class ManualPriorNotificationFishingCatchDataInput( conversionFactor = null, economicZone = null, effortZone = null, - faoZone = null, + faoZone = faoArea, freshness = null, nbFish = quantity, packaging = null, diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/input/ManualPriorNotificationFormDataInput.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/input/ManualPriorNotificationFormDataInput.kt index e8151beb8b..c6fc8bd1c1 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/input/ManualPriorNotificationFormDataInput.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/input/ManualPriorNotificationFormDataInput.kt @@ -10,8 +10,8 @@ data class ManualPriorNotificationFormDataInput( val didNotFishAfterZeroNotice: Boolean, val expectedArrivalDate: ZonedDateTime, val expectedLandingDate: ZonedDateTime, - val faoArea: String, val fishingCatches: List, + val globalFaoArea: String?, val note: String?, val portLocode: String, val purpose: LogbookMessagePurpose, diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/ManualPriorNotificationFishingCatchDataOutput.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/ManualPriorNotificationFishingCatchDataOutput.kt index 99e2384a41..18a45afb10 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/ManualPriorNotificationFishingCatchDataOutput.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/ManualPriorNotificationFishingCatchDataOutput.kt @@ -2,14 +2,19 @@ package fr.gouv.cnsp.monitorfish.infrastructure.api.outputs import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookFishingCatch -class ManualPriorNotificationFishingCatchDataOutput( +data class ManualPriorNotificationFishingCatchDataOutput( + val faoArea: String?, val quantity: Double?, val specyCode: String, val specyName: String, val weight: Double, ) { companion object { - fun fromLogbookFishingCatch(logbookFishingCatch: LogbookFishingCatch): ManualPriorNotificationFishingCatchDataOutput { + fun fromLogbookFishingCatch( + logbookFishingCatch: LogbookFishingCatch, + withFaoArea: Boolean, + ): ManualPriorNotificationFishingCatchDataOutput { + val faoArea = if (withFaoArea) logbookFishingCatch.faoZone else null val specyCode = requireNotNull(logbookFishingCatch.species) { "`logbookFishingCatch.species` is null." } @@ -21,6 +26,7 @@ class ManualPriorNotificationFishingCatchDataOutput( } return ManualPriorNotificationFishingCatchDataOutput( + faoArea, quantity = logbookFishingCatch.nbFish, specyCode, specyName, diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/ManualPriorNotificationFormDataOutput.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/ManualPriorNotificationFormDataOutput.kt index ca73ef5b3d..2034152a0e 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/ManualPriorNotificationFormDataOutput.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/ManualPriorNotificationFormDataOutput.kt @@ -13,8 +13,8 @@ data class ManualPriorNotificationFormDataOutput( val didNotFishAfterZeroNotice: Boolean, val expectedArrivalDate: String, val expectedLandingDate: String, - val faoArea: String?, val fishingCatches: List, + val globalFaoArea: String?, val note: String?, val portLocode: String, val reportId: String, @@ -42,16 +42,6 @@ data class ManualPriorNotificationFormDataOutput( "`message.predictedLandingDatetimeUtc` is null." }, ).toString() - // At the moment, manual prior notifications only have a single global FAO area field in Frontend, - // so we transform that single FAO area into an FAO area per fishing catch when we save it, - // while setting the global `PNO.faoZone` to `null`. - // We need to reverse this transformation when we output the data. - val globalFaoArea = requireNotNull(pnoMessage.catchOnboard.firstOrNull()?.faoZone) { - "`message.catchOnboard.firstOrNull()?.faoZone` is null." - } - val fishingCatchDataOutputs = pnoMessage.catchOnboard.map { - ManualPriorNotificationFishingCatchDataOutput.fromLogbookFishingCatch(it) - } val portLocode = requireNotNull(pnoMessage.port) { "`pnoMessage.port` is null." } val purpose = requireNotNull(pnoMessage.purpose) { "`pnoMessage.purpose` is null." } val reportId = requireNotNull(priorNotification.reportId) { "`priorNotification.reportId` is null." } @@ -69,6 +59,21 @@ data class ManualPriorNotificationFormDataOutput( val hasPortEntranceAuthorization = pnoMessage.hasPortEntranceAuthorization ?: true val hasPortLandingAuthorization = pnoMessage.hasPortLandingAuthorization ?: true + // In Frontend form, manual prior notifications can: + // - either have a single global FAO area field + // - or have an FAO area field per fishing catch + // while in Backend, we always have an FAO area field per fishing catch. + // So we need to check if all fishing catches have the same FAO area to know which case we are in. + val hasGlobalFaoArea = pnoMessage.catchOnboard.mapNotNull { it.faoZone }.distinct().size == 1 + val globalFaoArea = if (hasGlobalFaoArea) { + pnoMessage.catchOnboard.first().faoZone + } else { + null + } + val fishingCatchDataOutputs = pnoMessage.catchOnboard.map { + ManualPriorNotificationFishingCatchDataOutput.fromLogbookFishingCatch(it, !hasGlobalFaoArea) + } + return ManualPriorNotificationFormDataOutput( hasPortEntranceAuthorization = hasPortEntranceAuthorization, hasPortLandingAuthorization = hasPortLandingAuthorization, @@ -76,7 +81,7 @@ data class ManualPriorNotificationFormDataOutput( didNotFishAfterZeroNotice = priorNotification.didNotFishAfterZeroNotice, expectedArrivalDate = expectedArrivalDate, expectedLandingDate = expectedLandingDate, - faoArea = globalFaoArea, + globalFaoArea = globalFaoArea, fishingCatches = fishingCatchDataOutputs, note = pnoMessage.note, portLocode = portLocode, diff --git a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/CreateOrUpdateManualPriorNotificationITests.kt b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/CreateOrUpdateManualPriorNotificationITests.kt index 3c6f007532..405c81a314 100644 --- a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/CreateOrUpdateManualPriorNotificationITests.kt +++ b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/CreateOrUpdateManualPriorNotificationITests.kt @@ -358,7 +358,7 @@ class CreateOrUpdateManualPriorNotificationITests : AbstractDBTests() { didNotFishAfterZeroNotice = false, expectedArrivalDate = ZonedDateTime.now(), expectedLandingDate = ZonedDateTime.now(), - faoArea = "FAKE_FAO_AREA", + globalFaoArea = "FAKE_FAO_AREA", fishingCatches = emptyList(), hasPortEntranceAuthorization = false, hasPortLandingAuthorization = false, @@ -407,7 +407,7 @@ class CreateOrUpdateManualPriorNotificationITests : AbstractDBTests() { didNotFishAfterZeroNotice = false, expectedArrivalDate = ZonedDateTime.now(), expectedLandingDate = ZonedDateTime.now(), - faoArea = "FAKE_FAO_AREA", + globalFaoArea = "FAKE_FAO_AREA", fishingCatches = emptyList(), hasPortEntranceAuthorization = false, hasPortLandingAuthorization = false, diff --git a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/CreateOrUpdateManualPriorNotificationUTests.kt b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/CreateOrUpdateManualPriorNotificationUTests.kt index b6af92652f..b9df0ed9c4 100644 --- a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/CreateOrUpdateManualPriorNotificationUTests.kt +++ b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/CreateOrUpdateManualPriorNotificationUTests.kt @@ -91,7 +91,7 @@ class CreateOrUpdateManualPriorNotificationUTests { didNotFishAfterZeroNotice = false, expectedArrivalDate = ZonedDateTime.parse("2024-01-01T00:00:00Z"), expectedLandingDate = ZonedDateTime.parse("2024-01-01T00:00:00Z"), - faoArea = "FAKE_FAO_AREA", + globalFaoArea = "FAKE_FAO_AREA", fishingCatches = emptyList(), note = null, portLocode = "FAKE_PORT_LOCODE", diff --git a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/UpdateLogbookPriorNotificationITests.kt b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/UpdateLogbookPriorNotificationITests.kt index b2cf2ae234..3c3f23bacc 100644 --- a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/UpdateLogbookPriorNotificationITests.kt +++ b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/UpdateLogbookPriorNotificationITests.kt @@ -276,11 +276,12 @@ class UpdateLogbookPriorNotificationITests : AbstractDBTests() { assertThat(afterPriorNotification.reportId).isEqualTo(testCase.reportId) assertThat(afterPriorNotification.isManuallyCreated) .isEqualTo(testCase.expectedAfterBooleanState.isManualPriorNotification) - assertThat(afterPnoValue.isInVerificationScope).isEqualTo(testCase.expectedAfterBooleanState.isInVerificationScope) + assertThat(afterPnoValue.isInVerificationScope).isEqualTo( + testCase.expectedAfterBooleanState.isInVerificationScope, + ) assertThat(afterPnoValue.isVerified).isEqualTo(testCase.expectedAfterBooleanState.isVerified) assertThat(afterPnoValue.isSent).isEqualTo(testCase.expectedAfterBooleanState.isSent) assertThat(afterPnoValue.isBeingSent).isEqualTo(testCase.expectedAfterBooleanState.isBeingSent) assertThat(afterPriorNotification.state).isEqualTo(testCase.expectedAfterState) - } } diff --git a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/VerifyAndSendPriorNotificationITests.kt b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/VerifyAndSendPriorNotificationITests.kt index 480928616c..15de691a2a 100644 --- a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/VerifyAndSendPriorNotificationITests.kt +++ b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/VerifyAndSendPriorNotificationITests.kt @@ -509,7 +509,9 @@ class VerifyAndSendPriorNotificationITests : AbstractDBTests() { assertThat(afterPriorNotification.reportId).isEqualTo(testCase.reportId) assertThat(afterPriorNotification.isManuallyCreated) .isEqualTo(testCase.expectedAfterBooleanState.isManualPriorNotification) - assertThat(afterPnoValue.isInVerificationScope).isEqualTo(testCase.expectedAfterBooleanState.isInVerificationScope) + assertThat(afterPnoValue.isInVerificationScope).isEqualTo( + testCase.expectedAfterBooleanState.isInVerificationScope, + ) assertThat(afterPnoValue.isVerified).isEqualTo(testCase.expectedAfterBooleanState.isVerified) assertThat(afterPnoValue.isSent).isEqualTo(testCase.expectedAfterBooleanState.isSent) assertThat(afterPnoValue.isBeingSent).isEqualTo(testCase.expectedAfterBooleanState.isBeingSent) diff --git a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/bff/PriorNotificationControllerUTests.kt b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/bff/PriorNotificationControllerUTests.kt index e0c676640b..03e516370d 100644 --- a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/bff/PriorNotificationControllerUTests.kt +++ b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/bff/PriorNotificationControllerUTests.kt @@ -182,7 +182,7 @@ class PriorNotificationControllerUTests { // When val requestBody = objectMapper.writeValueAsString( ManualPriorNotificationComputeDataInput( - faoArea = "FAO AREA 51", + globalFaoArea = "FAO AREA 51", fishingCatches = emptyList(), portLocode = "FRABC", tripGearCodes = emptyList(), @@ -261,7 +261,7 @@ class PriorNotificationControllerUTests { didNotFishAfterZeroNotice = false, expectedArrivalDate = ZonedDateTime.now(), expectedLandingDate = ZonedDateTime.now(), - faoArea = "FAO AREA 51", + globalFaoArea = "FAO AREA 51", fishingCatches = emptyList(), note = null, portLocode = "FRABVC", @@ -294,7 +294,7 @@ class PriorNotificationControllerUTests { didNotFishAfterZeroNotice = anyOrNull(), expectedArrivalDate = anyOrNull(), expectedLandingDate = anyOrNull(), - faoArea = anyOrNull(), + globalFaoArea = anyOrNull(), fishingCatches = anyOrNull(), hasPortEntranceAuthorization = anyOrNull(), hasPortLandingAuthorization = anyOrNull(), @@ -317,7 +317,7 @@ class PriorNotificationControllerUTests { didNotFishAfterZeroNotice = false, expectedArrivalDate = ZonedDateTime.now(), expectedLandingDate = ZonedDateTime.now(), - faoArea = "FAO AREA 51", + globalFaoArea = "FAO AREA 51", fishingCatches = emptyList(), note = null, portLocode = "FRABVC", From 84c63f23bf0a3b5004322e245d8ff472d6978f3d Mon Sep 17 00:00:00 2001 From: Ivan Gabriele Date: Thu, 22 Aug 2024 15:09:49 +0200 Subject: [PATCH 2/8] Add FAO area per species field in manual prior notification form --- .../form.spec.ts | 14 +- .../PriorNotification.types.ts | 5 +- .../ManualPriorNotificationForm/Content.tsx | 8 +- .../ManualPriorNotificationForm/Form.tsx | 17 +- .../ManualPriorNotificationForm/constants.ts | 15 +- .../fields/FormikFaoAreaSelect/constants.ts | 6 + .../fields/FormikFaoAreaSelect/index.tsx | 62 +++++++ .../{utils.tsx => FormikExtraField.tsx} | 11 +- .../FormikFishingCatchesMultiSelect/index.tsx | 158 +++++++++++------- .../FormikFishingCatchesMultiSelect/utils.ts | 31 ++++ .../ManualPriorNotificationForm/index.tsx | 5 +- .../ManualPriorNotificationForm/types.ts | 1 + .../ManualPriorNotificationForm/utils.tsx | 2 +- .../openManualPriorNotificationForm.ts | 7 +- 14 files changed, 245 insertions(+), 97 deletions(-) create mode 100644 frontend/src/features/PriorNotification/components/ManualPriorNotificationForm/fields/FormikFaoAreaSelect/constants.ts create mode 100644 frontend/src/features/PriorNotification/components/ManualPriorNotificationForm/fields/FormikFaoAreaSelect/index.tsx rename frontend/src/features/PriorNotification/components/ManualPriorNotificationForm/fields/FormikFishingCatchesMultiSelect/{utils.tsx => FormikExtraField.tsx} (85%) create mode 100644 frontend/src/features/PriorNotification/components/ManualPriorNotificationForm/fields/FormikFishingCatchesMultiSelect/utils.ts diff --git a/frontend/cypress/e2e/side_window/manual_prior_notification_form/form.spec.ts b/frontend/cypress/e2e/side_window/manual_prior_notification_form/form.spec.ts index 0c4a995d8e..d5254f7a6b 100644 --- a/frontend/cypress/e2e/side_window/manual_prior_notification_form/form.spec.ts +++ b/frontend/cypress/e2e/side_window/manual_prior_notification_form/form.spec.ts @@ -46,7 +46,7 @@ context('Side Window > Manual Prior Notification Form > Form', () => { cy.fill('Quantité (SWO)', 20) cy.fill('Engins utilisés', ['OTP', 'PTB'], { index: 1 }) - cy.fill('Zone de pêche', '21.4.T') + cy.fill('Zone globale de capture', '21.4.T') cy.fill("Points d'attention identifiés par le CNSP", "Un point d'attention.") cy.fill('Saisi par', 'BOB') @@ -189,7 +189,7 @@ context('Side Window > Manual Prior Notification Form > Form', () => { cy.contains('Veuillez sélectionner au moins un engin.').should('not.exist') - cy.fill('Zone de pêche', '21.4.T') + cy.fill('Zone globale de capture', '21.4.T') cy.contains('Veuillez indiquer la zone FAO.').should('not.exist') @@ -238,7 +238,7 @@ context('Side Window > Manual Prior Notification Form > Form', () => { cy.fill('Poids (COD)', 5000) cy.fill('Engins utilisés', ['OTB'], { index: 1 }) - cy.fill('Zone de pêche', '27.7.d') + cy.fill('Zone globale de capture', '27.7.d') cy.fill('Saisi par', 'BOB') cy.wait('@computePriorNotification') @@ -299,7 +299,7 @@ context('Side Window > Manual Prior Notification Form > Form', () => { ) cy.fill('Engins utilisés', ['OTB', 'Chaluts de fond (non spécifiés)' /* (TB) */], { index: 1 }) - cy.fill('Zone de pêche', '27.5.b') + cy.fill('Zone globale de capture', '27.5.b') cy.wait('@computePriorNotification') cy.getDataCy('VesselRiskFactor').contains('1.9').should('exist') @@ -369,7 +369,7 @@ context('Side Window > Manual Prior Notification Form > Form', () => { cy.countRequestsByAlias('@computePriorNotification', 1500).should('be.equal', 0) - cy.fill('Zone de pêche', '27.7.d') + cy.fill('Zone globale de capture', '27.7.d') cy.wait('@computePriorNotification') cy.countRequestsByAlias('@computePriorNotification').should('be.equal', 1) @@ -418,7 +418,7 @@ context('Side Window > Manual Prior Notification Form > Form', () => { cy.wait('@computePriorNotification') cy.countRequestsByAlias('@computePriorNotification').should('be.equal', 5) - cy.fill('Zone de pêche', '27.7.d') + cy.fill('Zone globale de capture', '27.7.d') cy.wait('@computePriorNotification') // cy.countRequestsByAlias('@computePriorNotification').should('be.equal', 6) @@ -478,7 +478,7 @@ context('Side Window > Manual Prior Notification Form > Form', () => { cy.fill('Poids (COD)', 5000) cy.fill('Engins utilisés', ['OTB'], { index: 1 }) - cy.fill('Zone de pêche', '27.7.d') + cy.fill('Zone globale de capture', '27.7.d') cy.fill('Saisi par', 'BOB') cy.clickButton('Créer le préavis') diff --git a/frontend/src/features/PriorNotification/PriorNotification.types.ts b/frontend/src/features/PriorNotification/PriorNotification.types.ts index c47184ca36..7d89aceb12 100644 --- a/frontend/src/features/PriorNotification/PriorNotification.types.ts +++ b/frontend/src/features/PriorNotification/PriorNotification.types.ts @@ -75,8 +75,8 @@ export namespace PriorNotification { didNotFishAfterZeroNotice: boolean expectedArrivalDate: string expectedLandingDate: string - faoArea: string fishingCatches: FormDataFishingCatch[] + globalFaoArea: string | undefined hasPortEntranceAuthorization: boolean hasPortLandingAuthorization: boolean note: string | undefined @@ -104,7 +104,7 @@ export namespace PriorNotification { export type ManualComputeRequestData = Pick< ManualFormData, - 'faoArea' | 'fishingCatches' | 'portLocode' | 'tripGearCodes' | 'vesselId' + 'fishingCatches' | 'globalFaoArea' | 'portLocode' | 'tripGearCodes' | 'vesselId' > /** Real-time computed values displayed within a prior notification form. */ export type ManualComputedValues = Pick< @@ -116,6 +116,7 @@ export namespace PriorNotification { } export type FormDataFishingCatch = { + faoArea?: string | undefined quantity?: number | undefined specyCode: string specyName: string diff --git a/frontend/src/features/PriorNotification/components/ManualPriorNotificationForm/Content.tsx b/frontend/src/features/PriorNotification/components/ManualPriorNotificationForm/Content.tsx index ff56a763ba..11fa441df3 100644 --- a/frontend/src/features/PriorNotification/components/ManualPriorNotificationForm/Content.tsx +++ b/frontend/src/features/PriorNotification/components/ManualPriorNotificationForm/Content.tsx @@ -102,13 +102,17 @@ export function Content({ detail, isValidatingOnChange, onClose, onSubmit, onVer // If we don't have enough data to compute the values, we can't update them const nextComputationRequestData = getDefinedObject(nextPartialComputationRequestData, [ - 'faoArea', 'fishingCatches', 'portLocode', 'tripGearCodes', 'vesselId' ]) - if (!nextComputationRequestData) { + if ( + !nextComputationRequestData || + // If there is neither a global FAO area nor any FAO area per fishing catch, we can't compute the values + (!nextFormValues.globalFaoArea && + !nextComputationRequestData.fishingCatches.some(fishingCatch => !!fishingCatch.faoArea)) + ) { // but we need to unset existing computed values in case they were computed before dispatch(priorNotificationActions.unsetEditedPriorNotificationComputedValues()) diff --git a/frontend/src/features/PriorNotification/components/ManualPriorNotificationForm/Form.tsx b/frontend/src/features/PriorNotification/components/ManualPriorNotificationForm/Form.tsx index d25cf064f4..81aa0baec1 100644 --- a/frontend/src/features/PriorNotification/components/ManualPriorNotificationForm/Form.tsx +++ b/frontend/src/features/PriorNotification/components/ManualPriorNotificationForm/Form.tsx @@ -3,7 +3,6 @@ import { getHasAuthorizedLandingDownload } from '@features/PriorNotification/com import { PriorNotification } from '@features/PriorNotification/PriorNotification.types' import { priorNotificationActions } from '@features/PriorNotification/slice' import { useFormikDirtyOnceEffect } from '@hooks/useFormikDirtyOnceEffect' -import { useGetFaoAreasAsOptions } from '@hooks/useGetFaoAreasAsOptions' import { useGetGearsAsOptions } from '@hooks/useGetGearsAsOptions' import { useGetPortsAsOptions } from '@hooks/useGetPortsAsOptions' import { useMainAppDispatch } from '@hooks/useMainAppDispatch' @@ -21,6 +20,7 @@ import { useFormikContext } from 'formik' import { useRef } from 'react' import styled from 'styled-components' +import { FormikFaoAreaSelect } from './fields/FormikFaoAreaSelect' import { FormikFishingCatchesMultiSelect } from './fields/FormikFishingCatchesMultiSelect' import { FormikVesselSelect } from './fields/FormikVesselSelect' @@ -34,7 +34,6 @@ export function Form({ isReadOnly }: FormProps) { const { values } = useFormikContext() const dispatch = useMainAppDispatch() - const { faoAreasAsOptions } = useGetFaoAreasAsOptions() const { gearsAsOptions } = useGetGearsAsOptions() const { portsAsOptions } = useGetPortsAsOptions() @@ -110,7 +109,9 @@ export function Form({ isReadOnly }: FormProps) { virtualized /> - + + + - -
{isThirdPartyVessel.current && ( diff --git a/frontend/src/features/PriorNotification/components/ManualPriorNotificationForm/constants.ts b/frontend/src/features/PriorNotification/components/ManualPriorNotificationForm/constants.ts index 39ba8a9a20..98b4a33491 100644 --- a/frontend/src/features/PriorNotification/components/ManualPriorNotificationForm/constants.ts +++ b/frontend/src/features/PriorNotification/components/ManualPriorNotificationForm/constants.ts @@ -9,10 +9,14 @@ import PurposeCode = PriorNotification.PurposeCode export const BLUEFIN_TUNA_EXTENDED_SPECY_CODES = ['BF1', 'BF2', 'BF3'] const FISHING_CATCH_VALIDATION_SCHEMA: ObjectSchema = object({ + faoArea: string().when('$hasGlobalFaoArea', { + is: false, + then: schema => schema.required('Veuillez indiquer la zone FAO pour chaque espèce.') + }), quantity: number(), specyCode: string().required(), specyName: string().required(), - weight: number().required() + weight: number().required('Veuillez indiquer le poids pour chaque espèce.') }) export const FORM_VALIDATION_SCHEMA: ObjectSchema = object({ @@ -23,12 +27,16 @@ export const FORM_VALIDATION_SCHEMA: ObjectSchema schema.required('Veuillez indiquer la date de débarquement prévue.') }), - faoArea: string().required('Veuillez indiquer la zone FAO.'), fishingCatches: array() .of(FISHING_CATCH_VALIDATION_SCHEMA.required()) .ensure() .required() .min(1, 'Veuillez sélectionner au moins une espèce.'), + globalFaoArea: string().when('$hasGlobalFaoArea', { + is: true, + then: schema => schema.required('Veuillez indiquer la zone FAO.') + }), + hasGlobalFaoArea: boolean().required(), hasPortEntranceAuthorization: boolean().nonNullable().required(), hasPortLandingAuthorization: boolean().nonNullable().required(), isExpectedLandingDateSameAsExpectedArrivalDate: boolean().required(), @@ -47,8 +55,9 @@ export const INITIAL_FORM_VALUES: ManualPriorNotificationFormValues = { didNotFishAfterZeroNotice: false, expectedArrivalDate: undefined, expectedLandingDate: undefined, - faoArea: undefined, fishingCatches: [], + globalFaoArea: undefined, + hasGlobalFaoArea: true, hasPortEntranceAuthorization: true, hasPortLandingAuthorization: true, isExpectedLandingDateSameAsExpectedArrivalDate: false, diff --git a/frontend/src/features/PriorNotification/components/ManualPriorNotificationForm/fields/FormikFaoAreaSelect/constants.ts b/frontend/src/features/PriorNotification/components/ManualPriorNotificationForm/fields/FormikFaoAreaSelect/constants.ts new file mode 100644 index 0000000000..3519e4e50c --- /dev/null +++ b/frontend/src/features/PriorNotification/components/ManualPriorNotificationForm/fields/FormikFaoAreaSelect/constants.ts @@ -0,0 +1,6 @@ +import type { Option } from '@mtes-mct/monitor-ui' + +export const HAS_GLOBAL_FAO_AREA_AS_OPTIONS: Array> = [ + { label: 'Une seule zone de capture', value: true }, + { label: 'Différentes zones de capture', value: false } +] diff --git a/frontend/src/features/PriorNotification/components/ManualPriorNotificationForm/fields/FormikFaoAreaSelect/index.tsx b/frontend/src/features/PriorNotification/components/ManualPriorNotificationForm/fields/FormikFaoAreaSelect/index.tsx new file mode 100644 index 0000000000..b87ffc430c --- /dev/null +++ b/frontend/src/features/PriorNotification/components/ManualPriorNotificationForm/fields/FormikFaoAreaSelect/index.tsx @@ -0,0 +1,62 @@ +import { useGetFaoAreasAsOptions } from '@hooks/useGetFaoAreasAsOptions' +import { FormikMultiRadio, FormikSelect, usePrevious } from '@mtes-mct/monitor-ui' +import { useFormikContext } from 'formik' +import { omit } from 'lodash' +import { useEffect } from 'react' + +import { HAS_GLOBAL_FAO_AREA_AS_OPTIONS } from './constants' + +import type { ManualPriorNotificationFormValues } from '../../types' + +type FormikFaoAreaSelectProps = Readonly<{ + isReadOnly: boolean +}> +export function FormikFaoAreaSelect({ isReadOnly }: FormikFaoAreaSelectProps) { + const { setFieldValue, values } = useFormikContext() + const { faoAreasAsOptions } = useGetFaoAreasAsOptions() + + const hadGlobalFaoArea = usePrevious(values.hasGlobalFaoArea) + + useEffect( + () => { + if (hadGlobalFaoArea === undefined || values.hasGlobalFaoArea === hadGlobalFaoArea) { + return + } + + if (values.hasGlobalFaoArea === false) { + setFieldValue('globalFaoArea', undefined) + + return + } + + const nextFishingCatches = values.fishingCatches.map(fishingCatch => omit(fishingCatch, ['faoArea'])) + + setFieldValue('fishingCatches', nextFishingCatches) + }, + + // eslint-disable-next-line react-hooks/exhaustive-deps + [hadGlobalFaoArea, values.fishingCatches, values.hasGlobalFaoArea] + ) + + return ( + <> + + + + + ) +} diff --git a/frontend/src/features/PriorNotification/components/ManualPriorNotificationForm/fields/FormikFishingCatchesMultiSelect/utils.tsx b/frontend/src/features/PriorNotification/components/ManualPriorNotificationForm/fields/FormikFishingCatchesMultiSelect/FormikExtraField.tsx similarity index 85% rename from frontend/src/features/PriorNotification/components/ManualPriorNotificationForm/fields/FormikFishingCatchesMultiSelect/utils.tsx rename to frontend/src/features/PriorNotification/components/ManualPriorNotificationForm/fields/FormikFishingCatchesMultiSelect/FormikExtraField.tsx index 7ce0f538d8..faf11b5792 100644 --- a/frontend/src/features/PriorNotification/components/ManualPriorNotificationForm/fields/FormikFishingCatchesMultiSelect/utils.tsx +++ b/frontend/src/features/PriorNotification/components/ManualPriorNotificationForm/fields/FormikFishingCatchesMultiSelect/FormikExtraField.tsx @@ -5,11 +5,12 @@ import { BLUEFIN_TUNA_EXTENDED_SPECY_CODES } from '../../constants' import type { PriorNotification } from '@features/PriorNotification/PriorNotification.types' -export function getFishingsCatchesExtraFields( - specyCode: string, - fishingsCatchesIndex: number, +type FormikExtraFieldProps = Readonly<{ allFishingsCatches: PriorNotification.FormDataFishingCatch[] -) { + fishingsCatchesIndex: number + specyCode: string +}> +export function FormikExtraField({ allFishingsCatches, fishingsCatchesIndex, specyCode }: FormikExtraFieldProps) { // BFT - Bluefin Tuna => + BF1, BF2, BF3 if (specyCode === 'BFT') { return ( @@ -22,6 +23,7 @@ export function getFishingsCatchesExtraFields( {extendedSpecyCode} -export function FormikFishingCatchesMultiSelect({ readOnly }: FormikFishingCatchesMultiSelectProps) { - const [input, meta, helper] = useField('fishingCatches') +export function FormikFishingCatchesMultiSelect({ isReadOnly }: FormikFishingCatchesMultiSelectProps) { + const { errors, setFieldValue, values } = useFormikContext() const { speciesAsOptions } = useGetSpeciesAsOptions() const { data: speciesAndGroups } = useGetSpeciesQuery() + const { faoAreasAsOptions } = useGetFaoAreasAsOptions() + + const validationError = getFishingsCatchesValidationError(errors) const filteredSpeciesAsOptions = useMemo( () => speciesAsOptions?.filter(specyOption => - input.value.every(fishingCatch => fishingCatch.specyCode !== specyOption.value.code) + values.fishingCatches.every(fishingCatch => fishingCatch.specyCode !== specyOption.value.code) ) ?? [], - [speciesAsOptions, input.value] + [speciesAsOptions, values.fishingCatches] ) - const add = (nextSpecy: Specy | undefined) => { - const specyOption = speciesAsOptions?.find(({ value }) => value.code === nextSpecy?.code) - if (!specyOption) { - return - } - - const specyName = speciesAndGroups?.species.find(specy => specy.code === specyOption.value.code)?.name - assertNotNullish(specyName) - const nextFishingCatches = [...input.value, ...getFishingsCatchesInitialValues(specyOption.value.code, specyName)] - - helper.setValue(nextFishingCatches) - } - - const remove = (specyCode: string | undefined) => { - const nextFishingCatches = input.value.filter(fishingCatch => - specyCode === 'BFT' - ? !['BFT', ...BLUEFIN_TUNA_EXTENDED_SPECY_CODES].includes(fishingCatch.specyCode) - : fishingCatch.specyCode !== specyCode - ) - - helper.setValue(nextFishingCatches) - } - const customSearch = useMemo( () => filteredSpeciesAsOptions.length @@ -77,6 +59,36 @@ export function FormikFishingCatchesMultiSelect({ readOnly }: FormikFishingCatch [filteredSpeciesAsOptions] ) + const add = (nextSpecy: Specy | undefined) => { + const specyOption = speciesAsOptions?.find(({ value }) => value.code === nextSpecy?.code) + if (!specyOption) { + return + } + + const specyName = speciesAndGroups?.species.find(specy => specy.code === specyOption.value.code)?.name + assertNotNullish(specyName) + const nextFishingCatches = [ + ...values.fishingCatches, + ...getFishingsCatchesInitialValues(specyOption.value.code, specyName) + ] + + setFieldValue('fishingCatches', nextFishingCatches) + } + + const remove = (specyCode: string | undefined) => { + if (isReadOnly) { + return + } + + const nextFishingCatches = values.fishingCatches.filter(fishingCatch => + specyCode === 'BFT' + ? !['BFT', ...BLUEFIN_TUNA_EXTENDED_SPECY_CODES].includes(fishingCatch.specyCode) + : fishingCatch.specyCode !== specyCode + ) + + setFieldValue('fishingCatches', nextFishingCatches) + } + if (!filteredSpeciesAsOptions.length || !customSearch) { return } @@ -86,45 +98,59 @@ export function FormikFishingCatchesMultiSelect({ readOnly }: FormikFishingCatch