diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/config/MapperConfiguration.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/config/MapperConfiguration.kt index ecc5da2035..67c9511ed7 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/config/MapperConfiguration.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/config/MapperConfiguration.kt @@ -6,7 +6,7 @@ import com.fasterxml.jackson.databind.SerializationFeature import com.fasterxml.jackson.databind.jsontype.NamedType import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule import fr.gouv.cnsp.monitorfish.domain.entities.alerts.type.AlertTypeMapping -import fr.gouv.cnsp.monitorfish.domain.entities.logbook.Catch +import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookFishingCatch import fr.gouv.cnsp.monitorfish.domain.entities.logbook.ProtectedSpeciesCatch import fr.gouv.cnsp.monitorfish.domain.entities.reporting.ReportingTypeMapping import fr.gouv.cnsp.monitorfish.domain.entities.rules.type.RuleTypeMapping @@ -15,7 +15,7 @@ import org.springframework.context.annotation.Configuration import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder import java.util.* import fr.gouv.cnsp.monitorfish.domain.entities.alerts.type.IHasImplementation as IAlertsHasImplementation -import fr.gouv.cnsp.monitorfish.domain.entities.logbook.Gear as GearLogbook +import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookTripGear as GearLogbook import fr.gouv.cnsp.monitorfish.domain.entities.reporting.IHasImplementation as IReportingsHasImplementation import fr.gouv.cnsp.monitorfish.domain.entities.rules.type.IHasImplementation as IRulesHasImplementation @@ -30,7 +30,7 @@ class MapperConfiguration { mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) mapper.propertyNamingStrategy = PropertyNamingStrategies.LOWER_CAMEL_CASE - mapper.registerSubtypes(NamedType(Catch::class.java, "catch")) + mapper.registerSubtypes(NamedType(LogbookFishingCatch::class.java, "catch")) mapper.registerSubtypes(NamedType(ProtectedSpeciesCatch::class.java, "protectedSpeciesCatch")) mapper.registerSubtypes(NamedType(GearLogbook::class.java, "gear")) @@ -41,19 +41,28 @@ class MapperConfiguration { return mapper } - private fun registerRulesSubType(mapper: ObjectMapper, enumOfTypeToAdd: Class) where E : Enum?, E : IRulesHasImplementation? { + private fun registerRulesSubType( + mapper: ObjectMapper, + enumOfTypeToAdd: Class, + ) where E : Enum?, E : IRulesHasImplementation? { Arrays.stream(enumOfTypeToAdd.enumConstants) .map { enumItem -> NamedType(enumItem.getImplementation(), enumItem.name) } .forEach { type -> mapper.registerSubtypes(type) } } - private fun registerAlertsSubType(mapper: ObjectMapper, enumOfTypeToAdd: Class) where E : Enum?, E : IAlertsHasImplementation? { + private fun registerAlertsSubType( + mapper: ObjectMapper, + enumOfTypeToAdd: Class, + ) where E : Enum?, E : IAlertsHasImplementation? { Arrays.stream(enumOfTypeToAdd.enumConstants) .map { enumItem -> NamedType(enumItem.getImplementation(), enumItem.name) } .forEach { type -> mapper.registerSubtypes(type) } } - private fun registerReportingsSubType(mapper: ObjectMapper, enumOfTypeToAdd: Class) where E : Enum?, E : IReportingsHasImplementation? { + private fun registerReportingsSubType( + mapper: ObjectMapper, + enumOfTypeToAdd: Class, + ) where E : Enum?, E : IReportingsHasImplementation? { Arrays.stream(enumOfTypeToAdd.enumConstants) .map { enumItem -> NamedType(enumItem.getImplementation(), enumItem.name) } .forEach { type -> mapper.registerSubtypes(type) } diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/alerts/PNOAndLANCatches.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/alerts/PNOAndLANCatches.kt index 67d8eeaea9..7b3478c228 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/alerts/PNOAndLANCatches.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/alerts/PNOAndLANCatches.kt @@ -1,10 +1,10 @@ package fr.gouv.cnsp.monitorfish.domain.entities.alerts import com.fasterxml.jackson.annotation.JsonTypeName -import fr.gouv.cnsp.monitorfish.domain.entities.logbook.Catch +import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookFishingCatch @JsonTypeName("pnoAndLanCatches") data class PNOAndLANCatches( - var pno: Catch? = null, - var lan: Catch? = null, + var pno: LogbookFishingCatch? = null, + var lan: LogbookFishingCatch? = null, ) diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/Haul.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/Haul.kt index 3c5201d3be..ba9d821d02 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/Haul.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/Haul.kt @@ -8,7 +8,7 @@ import java.time.ZonedDateTime class Haul() { var gear: String? = null var gearName: String? = null - var catches: List = listOf() + var catches: List = listOf() var mesh: Double? = null var latitude: Double? = null var longitude: Double? = null diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/Catch.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/LogbookFishingCatch.kt similarity index 95% rename from backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/Catch.kt rename to backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/LogbookFishingCatch.kt index 452109b40d..51bc35ce74 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/Catch.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/LogbookFishingCatch.kt @@ -4,7 +4,7 @@ import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonTypeName @JsonTypeName("catch") -data class Catch( +data class LogbookFishingCatch( var weight: Double? = null, @JsonProperty("nbFish") var numberFish: Double? = null, diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/LogbookMessage.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/LogbookMessage.kt index f82dede799..a292e5520d 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/LogbookMessage.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/LogbookMessage.kt @@ -7,12 +7,11 @@ import fr.gouv.cnsp.monitorfish.domain.entities.species.Species import fr.gouv.cnsp.monitorfish.domain.exceptions.EntityConversionException import org.slf4j.LoggerFactory import java.time.ZonedDateTime -import fr.gouv.cnsp.monitorfish.domain.entities.logbook.Gear as LogbookGear data class LogbookMessage( - val id: Long, + val id: Long?, val reportId: String? = null, - val operationNumber: String, + val operationNumber: String?, val tripNumber: String? = null, val referencedReportId: String? = null, val operationDateTime: ZonedDateTime, @@ -20,16 +19,14 @@ data class LogbookMessage( val externalReferenceNumber: String? = null, val ircs: String? = null, val vesselName: String? = null, - // ISO Alpha-3 country code + /** ISO Alpha-3 country code. */ val flagState: String? = null, val imo: String? = null, - // Submission date of the report by the vessel - val reportDateTime: ZonedDateTime? = null, // Reception date of the report by the data center val integrationDateTime: ZonedDateTime, val analyzedByRules: List, var rawMessage: String? = null, - val transmissionFormat: LogbookTransmissionFormat, + val transmissionFormat: LogbookTransmissionFormat?, val software: String? = null, var acknowledgment: Acknowledgment? = null, @@ -40,7 +37,9 @@ data class LogbookMessage( val message: LogbookMessageValue? = null, val messageType: String? = null, val operationType: LogbookOperationType, - val tripGears: List? = emptyList(), + // Submission date of the report by the vessel + val reportDateTime: ZonedDateTime?, + val tripGears: List? = emptyList(), val tripSegments: List? = emptyList(), ) { private val logger = LoggerFactory.getLogger(LogbookMessage::class.java) @@ -425,7 +424,7 @@ data class LogbookMessage( } } - private fun addSpeciesName(catch: Catch, species: String, allSpecies: List) { + private fun addSpeciesName(catch: LogbookFishingCatch, species: String, allSpecies: List) { catch.speciesName = allSpecies.find { it.code == species }?.name } @@ -434,7 +433,7 @@ data class LogbookMessage( } private fun addGearName( - gear: LogbookGear, + gear: LogbookTripGear, gearCode: String, allGears: List, ) { diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/Gear.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/LogbookTripGear.kt similarity index 91% rename from backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/Gear.kt rename to backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/LogbookTripGear.kt index cfd4d8832d..7a79e10de1 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/Gear.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/LogbookTripGear.kt @@ -3,7 +3,7 @@ package fr.gouv.cnsp.monitorfish.domain.entities.logbook import com.fasterxml.jackson.annotation.JsonTypeName @JsonTypeName("gear") -class Gear() { +class LogbookTripGear() { /** Gear code. */ var gear: String? = null var gearName: String? = null diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/messages/DEP.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/messages/DEP.kt index 516a81f939..4e4fbfa230 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/messages/DEP.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/messages/DEP.kt @@ -1,16 +1,16 @@ package fr.gouv.cnsp.monitorfish.domain.entities.logbook.messages import com.fasterxml.jackson.annotation.JsonProperty -import fr.gouv.cnsp.monitorfish.domain.entities.logbook.Catch -import fr.gouv.cnsp.monitorfish.domain.entities.logbook.Gear +import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookFishingCatch +import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookTripGear import java.time.ZonedDateTime class DEP() : LogbookMessageValue { var anticipatedActivity: String? = null var departurePort: String? = null var departurePortName: String? = null - var speciesOnboard: List = listOf() - var gearOnboard: List = listOf() + var speciesOnboard: List = listOf() + var gearOnboard: List = listOf() @JsonProperty("departureDatetimeUtc") var departureDateTime: ZonedDateTime? = null diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/messages/DIS.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/messages/DIS.kt index 9e4dcc655b..36fa025f7e 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/messages/DIS.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/messages/DIS.kt @@ -1,11 +1,11 @@ package fr.gouv.cnsp.monitorfish.domain.entities.logbook.messages import com.fasterxml.jackson.annotation.JsonProperty -import fr.gouv.cnsp.monitorfish.domain.entities.logbook.Catch +import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookFishingCatch import java.time.ZonedDateTime class DIS() : LogbookMessageValue { - var catches: List = listOf() + var catches: List = listOf() @JsonProperty("discardDatetimeUtc") var discardDateTime: ZonedDateTime? = null diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/messages/LAN.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/messages/LAN.kt index b4c04c8cc3..e4f048315c 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/messages/LAN.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/messages/LAN.kt @@ -1,13 +1,13 @@ package fr.gouv.cnsp.monitorfish.domain.entities.logbook.messages import com.fasterxml.jackson.annotation.JsonProperty -import fr.gouv.cnsp.monitorfish.domain.entities.logbook.Catch +import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookFishingCatch import java.time.ZonedDateTime class LAN() : LogbookMessageValue { var port: String? = null var portName: String? = null - var catchLanded: List = listOf() + var catchLanded: List = listOf() var sender: String? = null @JsonProperty("landingDatetimeUtc") diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/messages/PNO.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/messages/PNO.kt index cac3210ee7..849e4caa81 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/messages/PNO.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/messages/PNO.kt @@ -1,25 +1,45 @@ package fr.gouv.cnsp.monitorfish.domain.entities.logbook.messages -import fr.gouv.cnsp.monitorfish.domain.entities.logbook.Catch +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import com.fasterxml.jackson.databind.annotation.JsonSerialize +import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookFishingCatch import fr.gouv.cnsp.monitorfish.domain.entities.prior_notification.PriorNotificationType +import fr.gouv.cnsp.monitorfish.utils.ZonedDateTimeDeserializer +import fr.gouv.cnsp.monitorfish.utils.ZonedDateTimeSerializer import java.time.ZonedDateTime +// TODO Rename to `LogbookMessageValueForPno`. class PNO() : LogbookMessageValue { - var faoZone: String? = null - var effortZone: String? = null + var catchOnboard: List = emptyList() + var catchToLand: List = emptyList() var economicZone: String? = null - var statisticalRectangle: String? = null + var effortZone: String? = null + + /** + * Global PNO FAO zone. + * + * Only used for cod fishing in the Baltic Sea (instead of regular "per caught species" zones). + */ + var faoZone: String? = null var latitude: Double? = null var longitude: Double? = null - var pnoTypes: List = listOf() - var purpose: String? = null + var pnoTypes: List = emptyList() /** Port locode. */ var port: String? = null var portName: String? = null - var catchOnboard: List = listOf() - var catchToLand: List = listOf() + + @JsonDeserialize(using = ZonedDateTimeDeserializer::class) + @JsonSerialize(using = ZonedDateTimeSerializer::class) var predictedArrivalDatetimeUtc: ZonedDateTime? = null + + @JsonDeserialize(using = ZonedDateTimeDeserializer::class) + @JsonSerialize(using = ZonedDateTimeSerializer::class) var predictedLandingDatetimeUtc: ZonedDateTime? = null + var purpose: String? = null + var statisticalRectangle: String? = null + + @JsonDeserialize(using = ZonedDateTimeDeserializer::class) + @JsonSerialize(using = ZonedDateTimeSerializer::class) var tripStartDate: ZonedDateTime? = null } diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/messages/RTP.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/messages/RTP.kt index 7e64e8ecea..9f9cc4639d 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/messages/RTP.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/messages/RTP.kt @@ -1,14 +1,14 @@ package fr.gouv.cnsp.monitorfish.domain.entities.logbook.messages import com.fasterxml.jackson.annotation.JsonProperty -import fr.gouv.cnsp.monitorfish.domain.entities.logbook.Gear +import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookTripGear import java.time.ZonedDateTime class RTP() : LogbookMessageValue { var reasonOfReturn: String? = null var port: String? = null var portName: String? = null - var gearOnboard: List = listOf() + var gearOnboard: List = listOf() @JsonProperty("returnDatetimeUtc") var dateTime: ZonedDateTime? = null diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/sorters/LogbookReportSortColumn.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/sorters/LogbookReportSortColumn.kt deleted file mode 100644 index 25b4ea30d8..0000000000 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/sorters/LogbookReportSortColumn.kt +++ /dev/null @@ -1,9 +0,0 @@ -package fr.gouv.cnsp.monitorfish.domain.entities.logbook.sorters - -enum class LogbookReportSortColumn { - EXPECTED_ARRIVAL_DATE, - EXPECTED_LANDING_DATE, - PORT_NAME, - VESSEL_NAME, - VESSEL_RISK_FACTOR, -} diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/prior_notification/PriorNotification.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/prior_notification/PriorNotification.kt index 0c5d62e3f1..3ac38a0bd5 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/prior_notification/PriorNotification.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/prior_notification/PriorNotification.kt @@ -1,19 +1,103 @@ package fr.gouv.cnsp.monitorfish.domain.entities.prior_notification import fr.gouv.cnsp.monitorfish.domain.entities.facade.Seafront +import fr.gouv.cnsp.monitorfish.domain.entities.gear.Gear import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookMessageTyped import fr.gouv.cnsp.monitorfish.domain.entities.logbook.messages.PNO import fr.gouv.cnsp.monitorfish.domain.entities.port.Port +import fr.gouv.cnsp.monitorfish.domain.entities.reporting.ReportingType +import fr.gouv.cnsp.monitorfish.domain.entities.reporting.filters.ReportingFilter import fr.gouv.cnsp.monitorfish.domain.entities.risk_factor.VesselRiskFactor +import fr.gouv.cnsp.monitorfish.domain.entities.species.Species +import fr.gouv.cnsp.monitorfish.domain.entities.vessel.UNKNOWN_VESSEL import fr.gouv.cnsp.monitorfish.domain.entities.vessel.Vessel +import fr.gouv.cnsp.monitorfish.domain.exceptions.CodeNotFoundException +import fr.gouv.cnsp.monitorfish.domain.exceptions.NoERSMessagesFound +import fr.gouv.cnsp.monitorfish.domain.repositories.LogbookRawMessageRepository +import fr.gouv.cnsp.monitorfish.domain.repositories.ReportingRepository +import org.slf4j.LoggerFactory data class PriorNotification( - /** Unique identifier concatenating all the DAT, COR, RET & DEL operations `id` used for data consolidation. */ - val fingerprint: String, - val logbookMessageTyped: LogbookMessageTyped, - val port: Port? = null, - val reportingCount: Int? = null, - val seafront: Seafront? = null, - val vessel: Vessel, - val vesselRiskFactor: VesselRiskFactor? = null, -) + val reportId: String?, + val authorTrigram: String?, + val createdAt: String?, + val didNotFishAfterZeroNotice: Boolean, + val isManuallyCreated: Boolean, + var logbookMessageTyped: LogbookMessageTyped, + val note: String?, + var port: Port?, + var reportingCount: Int?, + var seafront: Seafront?, + val sentAt: String?, + val updatedAt: String?, + var vessel: Vessel?, + var vesselRiskFactor: VesselRiskFactor?, +) { + /** Each prior notification and each of its updates have a unique fingerprint. */ + val fingerprint: String = listOf(reportId, updatedAt).joinToString(separator = ".") + private val logger = LoggerFactory.getLogger(PriorNotification::class.java) + + fun enrich(allPorts: List, allRiskFactors: List, allVessels: List) { + port = try { + logbookMessageTyped.typedMessage.port?.let { portLocode -> + allPorts.find { it.locode == portLocode } + } + } catch (e: CodeNotFoundException) { + null + } + + seafront = port?.facade?.let { Seafront.from(it) } + + // Default to UNKNOWN vessel when null or not found + vessel = logbookMessageTyped.logbookMessage + .internalReferenceNumber?.let { vesselInternalReferenceNumber -> + allVessels.find { it.internalReferenceNumber == vesselInternalReferenceNumber } + } ?: UNKNOWN_VESSEL + + vesselRiskFactor = vessel!!.internalReferenceNumber?.let { vesselInternalReferenceNumber -> + allRiskFactors.find { it.internalReferenceNumber == vesselInternalReferenceNumber } + } + } + + fun enrichLogbookMessage( + allGears: List, + allPorts: List, + allSpecies: List, + logbookRawMessageRepository: LogbookRawMessageRepository, + ) { + val logbookMessage = logbookMessageTyped.logbookMessage + val logbookMessageWithRawMessage = logbookMessage.operationNumber?.let { operationNumber -> + logbookMessage.copy( + rawMessage = try { + logbookRawMessageRepository.findRawMessage(operationNumber) + } catch (e: NoERSMessagesFound) { + logger.warn(e.message) + + null + }, + ) + } ?: logbookMessage + logbookMessageWithRawMessage.enrichGearPortAndSpecyNames(allGears, allPorts, allSpecies) + + logbookMessageTyped = LogbookMessageTyped( + logbookMessageWithRawMessage, + PNO::class.java, + ) + } + + fun enrichReportingCount(reportingRepository: ReportingRepository) { + val currentReportings = + vessel?.internalReferenceNumber?.let { vesselInternalReferenceNumber -> + reportingRepository.findAll( + ReportingFilter( + vesselInternalReferenceNumbers = listOf(vesselInternalReferenceNumber), + isArchived = false, + isDeleted = false, + types = listOf(ReportingType.INFRACTION_SUSPICION), + ), + ) + } + + reportingCount = currentReportings?.count() ?: 0 + } +} diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/filters/LogbookReportFilter.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/prior_notification/filters/PriorNotificationsFilter.kt similarity index 83% rename from backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/filters/LogbookReportFilter.kt rename to backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/prior_notification/filters/PriorNotificationsFilter.kt index 1a7a4cac30..551587bd22 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/filters/LogbookReportFilter.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/prior_notification/filters/PriorNotificationsFilter.kt @@ -1,6 +1,6 @@ -package fr.gouv.cnsp.monitorfish.domain.entities.logbook.filters +package fr.gouv.cnsp.monitorfish.domain.entities.prior_notification.filters -data class LogbookReportFilter( +data class PriorNotificationsFilter( val flagStates: List? = null, val hasOneOrMoreReportings: Boolean? = null, val isLessThanTwelveMetersVessel: Boolean? = null, diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/prior_notification/sorters/PriorNotificationsSortColumn.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/prior_notification/sorters/PriorNotificationsSortColumn.kt new file mode 100644 index 0000000000..c80ac37c7d --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/prior_notification/sorters/PriorNotificationsSortColumn.kt @@ -0,0 +1,9 @@ +package fr.gouv.cnsp.monitorfish.domain.entities.prior_notification.sorters + +enum class PriorNotificationsSortColumn { + EXPECTED_ARRIVAL_DATE, + EXPECTED_LANDING_DATE, + PORT_NAME, + VESSEL_NAME, + VESSEL_RISK_FACTOR, +} diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/exceptions/BackendInternalException.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/exceptions/BackendInternalException.kt new file mode 100644 index 0000000000..9813b0b830 --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/exceptions/BackendInternalException.kt @@ -0,0 +1,24 @@ +package fr.gouv.cnsp.monitorfish.domain.exceptions + +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +/** + * Exception to throw when the request is valid but the Backend failed while processing it. + * + * This is a Backend bug. + * + * ## Examples + * - An unexpected exception has been caught. + */ +open class BackendInternalException( + final override val message: String? = null, + originalException: Exception? = null, +) : Throwable(message) { + private val logger: Logger = LoggerFactory.getLogger(BackendInternalException::class.java) + + init { + logger.error("BackendInternalException: $message") + originalException?.let { logger.error("${it::class.simpleName}: ${it.message}") } + } +} diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/exceptions/BackendUsageErrorCode.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/exceptions/BackendUsageErrorCode.kt new file mode 100644 index 0000000000..b74fc86801 --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/exceptions/BackendUsageErrorCode.kt @@ -0,0 +1,18 @@ +package fr.gouv.cnsp.monitorfish.domain.exceptions + +/** + * Error code thrown when the request is valid but the Backend cannot process it. + * + * It's most likely a Frontend error. But it may also be a Backend bug. + * + * ## Examples + * - Attempting to create a resource that has already been created. + * - Attempting to delete a resource that doesn't exist anymore. + * + * ### ⚠️ Important + * **Don't forget to mirror any update here in the corresponding Frontend enum.** + */ +enum class BackendUsageErrorCode { + /** Thrown when a resource is expected to exist but doesn't. */ + NOT_FOUND, +} diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/exceptions/BackendUsageException.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/exceptions/BackendUsageException.kt new file mode 100644 index 0000000000..1f70677e45 --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/exceptions/BackendUsageException.kt @@ -0,0 +1,19 @@ +package fr.gouv.cnsp.monitorfish.domain.exceptions + +/** + * Exception to throw when the request is valid but the Backend cannot process it. + * + * It's most likely a Frontend error. But it may also be a Backend bug. + * + * ## Examples + * - Attempting to create a resource that has already been created. + * - Attempting to delete a resource that doesn't exist anymore. + * + * ### ⚠️ Important + * **Don't forget to mirror any update here in the corresponding Frontend enum.** + */ +open class BackendUsageException( + val code: BackendUsageErrorCode, + final override val message: String? = null, + val data: Any? = null, +) : Throwable(code.name) diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/repositories/LogbookReportRepository.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/repositories/LogbookReportRepository.kt index c3b9210830..a6085800cb 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/repositories/LogbookReportRepository.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/repositories/LogbookReportRepository.kt @@ -1,14 +1,16 @@ package fr.gouv.cnsp.monitorfish.domain.repositories import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookMessage +import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookMessageTyped import fr.gouv.cnsp.monitorfish.domain.entities.logbook.VoyageDatesAndTripNumber -import fr.gouv.cnsp.monitorfish.domain.entities.logbook.filters.LogbookReportFilter +import fr.gouv.cnsp.monitorfish.domain.entities.logbook.messages.PNO import fr.gouv.cnsp.monitorfish.domain.entities.prior_notification.PriorNotification +import fr.gouv.cnsp.monitorfish.domain.entities.prior_notification.filters.PriorNotificationsFilter import fr.gouv.cnsp.monitorfish.domain.exceptions.NoLogbookFishingTripFound import java.time.ZonedDateTime interface LogbookReportRepository { - fun findAllPriorNotifications(filter: LogbookReportFilter): List + fun findAllPriorNotifications(filter: PriorNotificationsFilter): List @Throws(NoLogbookFishingTripFound::class) fun findLastTripBeforeDateTime( @@ -51,7 +53,7 @@ interface LogbookReportRepository { // Only used in tests fun findById(id: Long): LogbookMessage - fun findPriorNotificationByReportId(reportId: String): PriorNotification + fun findPriorNotificationByReportId(reportId: String): PriorNotification? fun findLastMessageDate(): ZonedDateTime @@ -66,8 +68,11 @@ interface LogbookReportRepository { fun findLastReportSoftware(internalReferenceNumber: String): String? + // Only used in tests + fun save(message: LogbookMessage) + + fun savePriorNotification(logbookMessageTyped: LogbookMessageTyped): PriorNotification + // For test purpose fun deleteAll() - - fun save(message: LogbookMessage) } diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/repositories/ManualPriorNotificationRepository.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/repositories/ManualPriorNotificationRepository.kt new file mode 100644 index 0000000000..3d51166ddc --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/repositories/ManualPriorNotificationRepository.kt @@ -0,0 +1,12 @@ +package fr.gouv.cnsp.monitorfish.domain.repositories + +import fr.gouv.cnsp.monitorfish.domain.entities.prior_notification.PriorNotification +import fr.gouv.cnsp.monitorfish.domain.entities.prior_notification.filters.PriorNotificationsFilter + +interface ManualPriorNotificationRepository { + fun findAll(filter: PriorNotificationsFilter): List + + fun findByReportId(reportId: String): PriorNotification? + + fun save(newOrNextPriorNotification: PriorNotification): String +} diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/repositories/VesselRepository.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/repositories/VesselRepository.kt index 3b952c0af6..33040f7227 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/repositories/VesselRepository.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/repositories/VesselRepository.kt @@ -15,7 +15,7 @@ interface VesselRepository { fun findVesselsByIds(ids: List): List - fun findVesselById(vesselId: Int): Vessel? + fun findVesselById(vesselId: Int): Vessel fun search(searched: String): List } diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/alert/rules/ExecutePnoAndLanWeightToleranceRule.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/alert/rules/ExecutePnoAndLanWeightToleranceRule.kt index 5283baa77a..d47489a9ce 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/alert/rules/ExecutePnoAndLanWeightToleranceRule.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/alert/rules/ExecutePnoAndLanWeightToleranceRule.kt @@ -4,7 +4,7 @@ import fr.gouv.cnsp.monitorfish.config.UseCase import fr.gouv.cnsp.monitorfish.domain.entities.alerts.PNOAndLANAlert import fr.gouv.cnsp.monitorfish.domain.entities.alerts.PNOAndLANCatches import fr.gouv.cnsp.monitorfish.domain.entities.alerts.type.PNOAndLANWeightToleranceAlert -import fr.gouv.cnsp.monitorfish.domain.entities.logbook.Catch +import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookFishingCatch import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookMessage import fr.gouv.cnsp.monitorfish.domain.entities.logbook.messages.LAN import fr.gouv.cnsp.monitorfish.domain.entities.logbook.messages.PNO @@ -98,7 +98,11 @@ class ExecutePnoAndLanWeightToleranceRule( ) } - private fun getCatchesOverTolerance(lan: LAN, pno: PNO, value: PNOAndLANWeightTolerance): List { + private fun getCatchesOverTolerance( + lan: LAN, + pno: PNO, + value: PNOAndLANWeightTolerance, + ): List { val catchesLandedBySpecies = lan.catchLanded .groupBy { it.species } val catchesOnboardBySpecies = pno.catchOnboard @@ -113,7 +117,8 @@ class ExecutePnoAndLanWeightToleranceRule( ?.sum() val speciesName = catchesLandedBySpecies[lanSpeciesKey]?.first()?.speciesName - val lanCatch = Catch(species = lanSpeciesKey, speciesName = speciesName, weight = lanWeight) + val lanCatch = + LogbookFishingCatch(species = lanSpeciesKey, speciesName = speciesName, weight = lanWeight) if (lanWeight == null || !value.isAboveMinimumWeightThreshold(lanWeight)) { return@mapNotNull null @@ -122,7 +127,7 @@ class ExecutePnoAndLanWeightToleranceRule( try { val pnoWeight = getPNOWeight(catchesOnboardBySpecies, lanSpeciesKey) val pnoCatch = pnoWeight?.let { - Catch(species = lanSpeciesKey, speciesName = speciesName, weight = pnoWeight) + LogbookFishingCatch(species = lanSpeciesKey, speciesName = speciesName, weight = pnoWeight) } ?: return@mapNotNull PNOAndLANCatches(null, lanCatch) run { @@ -142,7 +147,10 @@ class ExecutePnoAndLanWeightToleranceRule( } } - private fun getPNOWeight(catchesOnboardBySpecies: Map>, lanSpeciesKey: String?): Double? { + private fun getPNOWeight( + catchesOnboardBySpecies: Map>, + lanSpeciesKey: String?, + ): Double? { val species = catchesOnboardBySpecies .keys .singleOrNull { species -> species == lanSpeciesKey } diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/CreateOrUpdatePriorNotification.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/CreateOrUpdatePriorNotification.kt new file mode 100644 index 0000000000..b0911388a1 --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/CreateOrUpdatePriorNotification.kt @@ -0,0 +1,148 @@ +package fr.gouv.cnsp.monitorfish.domain.use_cases.prior_notification + +import fr.gouv.cnsp.monitorfish.config.UseCase +import fr.gouv.cnsp.monitorfish.domain.entities.logbook.* +import fr.gouv.cnsp.monitorfish.domain.entities.logbook.messages.PNO +import fr.gouv.cnsp.monitorfish.domain.entities.prior_notification.PriorNotification +import fr.gouv.cnsp.monitorfish.domain.entities.prior_notification.PriorNotificationType +import fr.gouv.cnsp.monitorfish.domain.repositories.GearRepository +import fr.gouv.cnsp.monitorfish.domain.repositories.ManualPriorNotificationRepository +import fr.gouv.cnsp.monitorfish.domain.repositories.PortRepository +import fr.gouv.cnsp.monitorfish.domain.repositories.VesselRepository +import java.time.ZonedDateTime + +@UseCase +class CreateOrUpdatePriorNotification( + private val gearRepository: GearRepository, + private val manualPriorNotificationRepository: ManualPriorNotificationRepository, + private val portRepository: PortRepository, + private val vesselRepository: VesselRepository, + + private val getPriorNotification: GetPriorNotification, +) { + fun execute( + authorTrigram: String, + didNotFishAfterZeroNotice: Boolean, + expectedArrivalDate: String, + expectedLandingDate: String, + faoArea: String, + fishingCatches: List, + note: String?, + portLocode: String, + reportId: String?, + sentAt: String, + tripGearCodes: List, + vesselId: Int, + ): PriorNotification { + val message = getMessage(faoArea, expectedArrivalDate, expectedLandingDate, fishingCatches, portLocode) + val tripGears = getTripGears(tripGearCodes) + // TODO To calculate. + val tripSegments = emptyList() + val vessel = vesselRepository.findVesselById(vesselId) + + val pnoLogbookMessage = LogbookMessage( + id = null, + reportId = reportId, + operationNumber = null, + tripNumber = null, + referencedReportId = null, + operationDateTime = ZonedDateTime.now(), + internalReferenceNumber = vessel.internalReferenceNumber, + externalReferenceNumber = vessel.externalReferenceNumber, + ircs = vessel.ircs, + vesselName = vessel.vesselName, + flagState = vessel.flagState.alpha3, + imo = vessel.imo, + reportDateTime = ZonedDateTime.parse(sentAt), + integrationDateTime = ZonedDateTime.now(), + analyzedByRules = emptyList(), + rawMessage = null, + transmissionFormat = null, + software = null, + acknowledgment = null, + isCorrectedByNewerMessage = false, + isDeleted = false, + isEnriched = true, + isSentByFailoverSoftware = false, + message = message, + messageType = LogbookMessageTypeMapping.PNO.name, + operationType = LogbookOperationType.DAT, + tripGears = tripGears, + tripSegments = tripSegments, + ) + val logbookMessageTyped = LogbookMessageTyped(pnoLogbookMessage, PNO::class.java) + + val newOrNextPriorNotification = PriorNotification( + reportId = reportId, + authorTrigram = authorTrigram, + didNotFishAfterZeroNotice = didNotFishAfterZeroNotice, + isManuallyCreated = true, + logbookMessageTyped = logbookMessageTyped, + note = note, + sentAt = sentAt, + + // All these props are useless for the save operation. + createdAt = null, + port = null, + reportingCount = null, + seafront = null, + vessel = null, + vesselRiskFactor = null, + updatedAt = null, + ) + + val newOrCurrentReportId = manualPriorNotificationRepository.save(newOrNextPriorNotification) + val createdOrUpdatedPriorNotification = getPriorNotification.execute(newOrCurrentReportId) + + return createdOrUpdatedPriorNotification + } + + private fun getMessage( + faoArea: String, + expectedArrivalDate: String, + expectedLandingDate: String, + fishingCatches: List, + portLocode: String, + ): PNO { + val allPorts = portRepository.findAll() + + // TODO To calculate. + val pnoTypes = emptyList() + val portName = allPorts.find { it.locode == portLocode }?.name + val predictedArrivalDatetimeUtc = ZonedDateTime.parse(expectedArrivalDate) + val predictedLandingDatetimeUtc = ZonedDateTime.parse(expectedLandingDate) + + return PNO().apply { + this.catchOnboard = fishingCatches + this.catchToLand = fishingCatches + this.economicZone = null + this.effortZone = null + this.faoZone = faoArea + this.latitude = null + this.longitude = null + this.pnoTypes = pnoTypes + this.port = portLocode + this.portName = portName + this.predictedArrivalDatetimeUtc = predictedArrivalDatetimeUtc + this.predictedLandingDatetimeUtc = predictedLandingDatetimeUtc + this.purpose = "LAN" + this.statisticalRectangle = null + this.tripStartDate = null + } + } + + fun getTripGears(tripGearCodes: List): List { + val allGears = gearRepository.findAll() + + return tripGearCodes + .mapNotNull { gearCode -> allGears.find { it.code == gearCode } } + .map { + LogbookTripGear().apply { + this.gear = it.code + this.gearName = it.name + this.mesh = null + this.dimensions = null + } + } + } +} diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/GetPriorNotification.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/GetPriorNotification.kt index 14e9c254b0..7f15826fe6 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/GetPriorNotification.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/GetPriorNotification.kt @@ -1,105 +1,43 @@ package fr.gouv.cnsp.monitorfish.domain.use_cases.prior_notification import fr.gouv.cnsp.monitorfish.config.UseCase -import fr.gouv.cnsp.monitorfish.domain.entities.facade.Seafront -import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookMessageTyped -import fr.gouv.cnsp.monitorfish.domain.entities.logbook.messages.PNO import fr.gouv.cnsp.monitorfish.domain.entities.prior_notification.PriorNotification -import fr.gouv.cnsp.monitorfish.domain.entities.reporting.ReportingType -import fr.gouv.cnsp.monitorfish.domain.entities.reporting.filters.ReportingFilter -import fr.gouv.cnsp.monitorfish.domain.entities.vessel.UNKNOWN_VESSEL -import fr.gouv.cnsp.monitorfish.domain.exceptions.CodeNotFoundException -import fr.gouv.cnsp.monitorfish.domain.exceptions.NoERSMessagesFound +import fr.gouv.cnsp.monitorfish.domain.exceptions.BackendUsageErrorCode +import fr.gouv.cnsp.monitorfish.domain.exceptions.BackendUsageException import fr.gouv.cnsp.monitorfish.domain.repositories.* -import org.slf4j.LoggerFactory @UseCase class GetPriorNotification( private val gearRepository: GearRepository, private val logbookRawMessageRepository: LogbookRawMessageRepository, private val logbookReportRepository: LogbookReportRepository, + private val manualPriorNotificationRepository: ManualPriorNotificationRepository, private val portRepository: PortRepository, private val reportingRepository: ReportingRepository, private val riskFactorRepository: RiskFactorRepository, private val speciesRepository: SpeciesRepository, private val vesselRepository: VesselRepository, ) { - private val logger = LoggerFactory.getLogger(GetPriorNotification::class.java) - - fun execute(logbookMessageReportId: String): PriorNotification { + fun execute(reportId: String): PriorNotification { val allGears = gearRepository.findAll() val allPorts = portRepository.findAll() val allRiskFactors = riskFactorRepository.findAll() val allSpecies = speciesRepository.findAll() val allVessels = vesselRepository.findAll() - val priorNotificationWithoutReportingCount = logbookReportRepository - .findPriorNotificationByReportId(logbookMessageReportId) - .let { priorNotification -> - val logbookMessage = priorNotification.logbookMessageTyped.logbookMessage - val logbookMessageWithRawMessage = logbookMessage.copy( - rawMessage = try { - logbookRawMessageRepository.findRawMessage(logbookMessage.operationNumber) - } catch (e: NoERSMessagesFound) { - logger.warn(e.message) - - null - }, - ) - - val port = try { - priorNotification.logbookMessageTyped.typedMessage.port?.let { portLocode -> - allPorts.find { it.locode == portLocode } - } - } catch (e: CodeNotFoundException) { - null - } - - val seafront: Seafront? = port?.facade?.let { Seafront.from(it) } - - // Default to UNKNOWN vessel when null or not found - val vessel = priorNotification.logbookMessageTyped.logbookMessage - .internalReferenceNumber?.let { vesselInternalReferenceNumber -> - allVessels.find { it.internalReferenceNumber == vesselInternalReferenceNumber } - } ?: UNKNOWN_VESSEL - - val vesselRiskFactor = vessel.internalReferenceNumber?.let { vesselInternalReferenceNumber -> - allRiskFactors.find { it.internalReferenceNumber == vesselInternalReferenceNumber } - } - - val finalPriorNotification = priorNotification.copy( - logbookMessageTyped = LogbookMessageTyped(logbookMessageWithRawMessage, PNO::class.java), - port = port, - seafront = seafront, - vessel = vessel, - vesselRiskFactor = vesselRiskFactor, - ) + val priorNotification = manualPriorNotificationRepository.findByReportId(reportId) + ?: logbookReportRepository.findPriorNotificationByReportId(reportId) + ?: throw BackendUsageException(BackendUsageErrorCode.NOT_FOUND) - finalPriorNotification.logbookMessageTyped.logbookMessage - .enrichGearPortAndSpecyNames(allGears, allPorts, allSpecies) - - finalPriorNotification - } - - val priorNotification = enrichPriorNotificationWithReportingCount(priorNotificationWithoutReportingCount) + priorNotification.enrich(allPorts, allRiskFactors, allVessels) + priorNotification.enrichLogbookMessage( + allGears, + allPorts, + allSpecies, + logbookRawMessageRepository, + ) + priorNotification.enrichReportingCount(reportingRepository) return priorNotification } - - private fun enrichPriorNotificationWithReportingCount(priorNotification: PriorNotification): PriorNotification { - val currentReportings = priorNotification.vessel.internalReferenceNumber?.let { vesselInternalReferenceNumber -> - reportingRepository.findAll( - ReportingFilter( - vesselInternalReferenceNumbers = listOf(vesselInternalReferenceNumber), - isArchived = false, - isDeleted = false, - types = listOf(ReportingType.INFRACTION_SUSPICION), - ), - ) - } - - val reportingCount = currentReportings?.count() ?: 0 - - return priorNotification.copy(reportingCount = reportingCount) - } } diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/GetPriorNotifications.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/GetPriorNotifications.kt index 7596f969cd..a22fbc468b 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/GetPriorNotifications.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/GetPriorNotifications.kt @@ -1,14 +1,9 @@ package fr.gouv.cnsp.monitorfish.domain.use_cases.prior_notification import fr.gouv.cnsp.monitorfish.config.UseCase -import fr.gouv.cnsp.monitorfish.domain.entities.facade.Seafront -import fr.gouv.cnsp.monitorfish.domain.entities.logbook.filters.LogbookReportFilter -import fr.gouv.cnsp.monitorfish.domain.entities.logbook.sorters.LogbookReportSortColumn import fr.gouv.cnsp.monitorfish.domain.entities.prior_notification.PriorNotification -import fr.gouv.cnsp.monitorfish.domain.entities.reporting.ReportingType -import fr.gouv.cnsp.monitorfish.domain.entities.reporting.filters.ReportingFilter -import fr.gouv.cnsp.monitorfish.domain.entities.vessel.UNKNOWN_VESSEL -import fr.gouv.cnsp.monitorfish.domain.exceptions.CodeNotFoundException +import fr.gouv.cnsp.monitorfish.domain.entities.prior_notification.filters.PriorNotificationsFilter +import fr.gouv.cnsp.monitorfish.domain.entities.prior_notification.sorters.PriorNotificationsSortColumn import fr.gouv.cnsp.monitorfish.domain.repositories.* import org.springframework.data.domain.Sort @@ -16,15 +11,17 @@ import org.springframework.data.domain.Sort class GetPriorNotifications( private val gearRepository: GearRepository, private val logbookReportRepository: LogbookReportRepository, + private val manualPriorNotificationRepository: ManualPriorNotificationRepository, private val portRepository: PortRepository, private val reportingRepository: ReportingRepository, private val riskFactorRepository: RiskFactorRepository, private val speciesRepository: SpeciesRepository, private val vesselRepository: VesselRepository, + ) { fun execute( - filter: LogbookReportFilter, - sortColumn: LogbookReportSortColumn, + filter: PriorNotificationsFilter, + sortColumn: PriorNotificationsSortColumn, sortDirection: Sort.Direction, ): List { val allGears = gearRepository.findAll() @@ -33,42 +30,19 @@ class GetPriorNotifications( val allSpecies = speciesRepository.findAll() val allVessels = vesselRepository.findAll() - val incompletePriorNotifications = logbookReportRepository.findAllPriorNotifications(filter) - val priorNotificationsWithoutReportingCount = incompletePriorNotifications - .map { priorNotification -> - val port = try { - priorNotification.logbookMessageTyped.typedMessage.port?.let { portLocode -> - allPorts.find { it.locode == portLocode } - } - } catch (e: CodeNotFoundException) { - null - } - - // Default to UNKNOWN vessel when null or not found - val vessel = priorNotification.logbookMessageTyped.logbookMessage - .internalReferenceNumber?.let { vesselInternalReferenceNumber -> - allVessels.find { it.internalReferenceNumber == vesselInternalReferenceNumber } - } ?: UNKNOWN_VESSEL - - val vesselRiskFactor = vessel.internalReferenceNumber?.let { vesselInternalReferenceNumber -> - allRiskFactors.find { it.internalReferenceNumber == vesselInternalReferenceNumber } - } + val automaticPriorNotifications = logbookReportRepository.findAllPriorNotifications(filter) + val manualPriorNotifications = manualPriorNotificationRepository.findAll(filter) + val incompletePriorNotifications = automaticPriorNotifications + manualPriorNotifications - val seafront: Seafront? = port?.facade?.let { Seafront.from(it) } - - val finalPriorNotification = priorNotification.copy( - port = port, - seafront = seafront, - vessel = vessel, - vesselRiskFactor = vesselRiskFactor, - ) - - finalPriorNotification.logbookMessageTyped.logbookMessage + val priorNotifications = incompletePriorNotifications + .map { priorNotification -> + priorNotification.enrich(allPorts, allRiskFactors, allVessels) + priorNotification.enrichReportingCount(reportingRepository) + priorNotification.logbookMessageTyped.logbookMessage .enrichGearPortAndSpecyNames(allGears, allPorts, allSpecies) - finalPriorNotification + priorNotification } - val priorNotifications = enrichPriorNotificationsWithReportingCount(priorNotificationsWithoutReportingCount) val sortedPriorNotifications = when (sortDirection) { Sort.Direction.ASC -> priorNotifications.sortedWith( @@ -90,40 +64,17 @@ class GetPriorNotifications( return sortedPriorNotificationsWithoutDeletedOnes } - private fun enrichPriorNotificationsWithReportingCount( - priorNotifications: List, - ): List { - val currentReportings = reportingRepository.findAll( - ReportingFilter( - vesselInternalReferenceNumbers = priorNotifications.mapNotNull { it.vessel.internalReferenceNumber }, - isArchived = false, - isDeleted = false, - types = listOf(ReportingType.INFRACTION_SUSPICION), - ), - ) - - val priorNotificationsWithReportingCount = priorNotifications.map { priorNotification -> - val reportingCount = currentReportings.count { reporting -> - reporting.internalReferenceNumber == priorNotification.vessel.internalReferenceNumber - } - - priorNotification.copy(reportingCount = reportingCount) - } - - return priorNotificationsWithReportingCount - } - companion object { private fun getSortKey( priorNotification: PriorNotification, - sortColumn: LogbookReportSortColumn, + sortColumn: PriorNotificationsSortColumn, ): Comparable<*>? { return when (sortColumn) { - LogbookReportSortColumn.EXPECTED_ARRIVAL_DATE -> priorNotification.logbookMessageTyped.typedMessage.predictedArrivalDatetimeUtc - LogbookReportSortColumn.EXPECTED_LANDING_DATE -> priorNotification.logbookMessageTyped.typedMessage.predictedLandingDatetimeUtc - LogbookReportSortColumn.PORT_NAME -> priorNotification.port?.name - LogbookReportSortColumn.VESSEL_NAME -> priorNotification.logbookMessageTyped.logbookMessage.vesselName - LogbookReportSortColumn.VESSEL_RISK_FACTOR -> priorNotification.vesselRiskFactor?.riskFactor + PriorNotificationsSortColumn.EXPECTED_ARRIVAL_DATE -> priorNotification.logbookMessageTyped.typedMessage.predictedArrivalDatetimeUtc + PriorNotificationsSortColumn.EXPECTED_LANDING_DATE -> priorNotification.logbookMessageTyped.typedMessage.predictedLandingDatetimeUtc + PriorNotificationsSortColumn.PORT_NAME -> priorNotification.port?.name + PriorNotificationsSortColumn.VESSEL_NAME -> priorNotification.logbookMessageTyped.logbookMessage.vesselName + PriorNotificationsSortColumn.VESSEL_RISK_FACTOR -> priorNotification.vesselRiskFactor?.riskFactor } } } diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/vessel/GetLogbookMessages.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/vessel/GetLogbookMessages.kt index 837ca2def2..1f0b05dc60 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/vessel/GetLogbookMessages.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/vessel/GetLogbookMessages.kt @@ -36,15 +36,17 @@ class GetLogbookMessages( tripNumber, ) .sortedBy { it.reportDateTime } - .map { - try { - val rawMessage = logbookRawMessageRepository.findRawMessage(it.operationNumber) - it.rawMessage = rawMessage - } catch (e: NoERSMessagesFound) { - logger.warn(e.message) + .map { logbookMessage -> + logbookMessage.operationNumber?.let { operationNumber -> + try { + val rawMessage = logbookRawMessageRepository.findRawMessage(operationNumber) + logbookMessage.rawMessage = rawMessage + } catch (e: NoERSMessagesFound) { + logger.warn(e.message) + } } - it + logbookMessage } messages.forEach { it.enrich(messages, allGears, allPorts, allSpecies) } diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/vessel/GetVesselById.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/vessel/GetVesselById.kt new file mode 100644 index 0000000000..7e0b104ae0 --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/vessel/GetVesselById.kt @@ -0,0 +1,14 @@ +package fr.gouv.cnsp.monitorfish.domain.use_cases.vessel + +import fr.gouv.cnsp.monitorfish.config.UseCase +import fr.gouv.cnsp.monitorfish.domain.entities.vessel.Vessel +import fr.gouv.cnsp.monitorfish.domain.repositories.VesselRepository + +@UseCase +class GetVesselById( + private val vesselRepository: VesselRepository, +) { + fun execute(vesselId: Int): Vessel { + return vesselRepository.findVesselById(vesselId) + } +} diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/ControllersExceptionHandler.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/ControllersExceptionHandler.kt index 78a1bc6e05..5d428de6b9 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/ControllersExceptionHandler.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/ControllersExceptionHandler.kt @@ -2,14 +2,15 @@ package fr.gouv.cnsp.monitorfish.infrastructure.api import fr.gouv.cnsp.monitorfish.config.SentryConfig import fr.gouv.cnsp.monitorfish.domain.exceptions.* -import fr.gouv.cnsp.monitorfish.infrastructure.api.outputs.ApiError -import fr.gouv.cnsp.monitorfish.infrastructure.api.outputs.MissingParameterApiError +import fr.gouv.cnsp.monitorfish.infrastructure.api.outputs.* +import fr.gouv.cnsp.monitorfish.infrastructure.exceptions.BackendRequestException import io.sentry.Sentry import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.core.Ordered.LOWEST_PRECEDENCE import org.springframework.core.annotation.Order import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity import org.springframework.web.bind.MissingServletRequestParameterException import org.springframework.web.bind.annotation.ExceptionHandler import org.springframework.web.bind.annotation.ResponseStatus @@ -20,6 +21,34 @@ import org.springframework.web.bind.annotation.RestControllerAdvice class ControllersExceptionHandler(val sentryConfig: SentryConfig) { private val logger: Logger = LoggerFactory.getLogger(ControllersExceptionHandler::class.java) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + @ExceptionHandler(BackendInternalException::class) + fun handleBackendInternalException( + e: BackendInternalException, + ): BackendInternalErrorDataOutput { + return BackendInternalErrorDataOutput() + } + + @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY) + @ExceptionHandler(BackendRequestException::class) + fun handleBackendRequestException(e: BackendRequestException): BackendRequestErrorDataOutput { + return BackendRequestErrorDataOutput(code = e.code, data = e.data, message = null) + } + + @ExceptionHandler(BackendUsageException::class) + fun handleBackendUsageException(e: BackendUsageException): ResponseEntity { + val responseBody = BackendUsageErrorDataOutput(code = e.code, data = e.data, message = null) + + return if (e.code == BackendUsageErrorCode.NOT_FOUND) { + ResponseEntity(responseBody, HttpStatus.NOT_FOUND) + } else { + ResponseEntity(responseBody, HttpStatus.BAD_REQUEST) + } + } + + // ------------------------------------------------------------------------- + // Legacy exceptions + @ResponseStatus(HttpStatus.OK) @ExceptionHandler(NAFMessageParsingException::class) fun handleNAFMessageParsingException(e: Exception): ApiError { 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 d5966ef19a..db4a10a9dc 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 @@ -2,15 +2,14 @@ package fr.gouv.cnsp.monitorfish.infrastructure.api.bff import fr.gouv.cnsp.monitorfish.domain.entities.facade.SeafrontGroup import fr.gouv.cnsp.monitorfish.domain.entities.facade.hasSeafront -import fr.gouv.cnsp.monitorfish.domain.entities.logbook.filters.LogbookReportFilter -import fr.gouv.cnsp.monitorfish.domain.entities.logbook.sorters.LogbookReportSortColumn +import fr.gouv.cnsp.monitorfish.domain.entities.prior_notification.filters.PriorNotificationsFilter +import fr.gouv.cnsp.monitorfish.domain.entities.prior_notification.sorters.PriorNotificationsSortColumn +import fr.gouv.cnsp.monitorfish.domain.use_cases.prior_notification.CreateOrUpdatePriorNotification import fr.gouv.cnsp.monitorfish.domain.use_cases.prior_notification.GetPriorNotification import fr.gouv.cnsp.monitorfish.domain.use_cases.prior_notification.GetPriorNotificationTypes import fr.gouv.cnsp.monitorfish.domain.use_cases.prior_notification.GetPriorNotifications -import fr.gouv.cnsp.monitorfish.infrastructure.api.outputs.PaginatedListDataOutput -import fr.gouv.cnsp.monitorfish.infrastructure.api.outputs.PriorNotificationDataOutput -import fr.gouv.cnsp.monitorfish.infrastructure.api.outputs.PriorNotificationDetailDataOutput -import fr.gouv.cnsp.monitorfish.infrastructure.api.outputs.PriorNotificationsExtraDataOutput +import fr.gouv.cnsp.monitorfish.infrastructure.api.input.PriorNotificationDataInput +import fr.gouv.cnsp.monitorfish.infrastructure.api.outputs.* import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.Parameter import io.swagger.v3.oas.annotations.tags.Tag @@ -22,6 +21,7 @@ import org.springframework.web.bind.annotation.* @RequestMapping("/bff/v1/prior_notifications") @Tag(name = "Prior notifications endpoints") class PriorNotificationController( + private val createOrUpdatePriorNotification: CreateOrUpdatePriorNotification, private val getPriorNotification: GetPriorNotification, private val getPriorNotifications: GetPriorNotifications, private val getPriorNotificationTypes: GetPriorNotificationTypes, @@ -75,7 +75,7 @@ class PriorNotificationController( @Parameter(description = "Sort column.") @RequestParam(name = "sortColumn") - sortColumn: LogbookReportSortColumn, + sortColumn: PriorNotificationsSortColumn, @Parameter(description = "Sort order.") @RequestParam(name = "sortDirection") sortDirection: Sort.Direction, @@ -85,8 +85,8 @@ class PriorNotificationController( @Parameter(description = "Page number (0-indexed).") @RequestParam(name = "pageNumber") pageNumber: Int, - ): PaginatedListDataOutput { - val logbookReportFilter = LogbookReportFilter( + ): PaginatedListDataOutput { + val priorNotificationsFilter = PriorNotificationsFilter( flagStates = flagStates, hasOneOrMoreReportings = hasOneOrMoreReportings, isLessThanTwelveMetersVessel = isLessThanTwelveMetersVessel, @@ -103,10 +103,10 @@ class PriorNotificationController( ) val priorNotifications = getPriorNotifications - .execute(logbookReportFilter, sortColumn, sortDirection) - val priorNotificationDataOutputsFilteredBySeafrontGroup = priorNotifications + .execute(priorNotificationsFilter, sortColumn, sortDirection) + val priorNotificationListItemDataOutputsFilteredBySeafrontGroup = priorNotifications .filter { seafrontGroup.hasSeafront(it.seafront) } - .mapNotNull { PriorNotificationDataOutput.fromPriorNotification(it) } + .mapNotNull { PriorNotificationListItemDataOutput.fromPriorNotification(it) } val extraDataOutput = PriorNotificationsExtraDataOutput( perSeafrontGroupCount = SeafrontGroup.entries.associateWith { seafrontGroupEntry -> @@ -117,22 +117,34 @@ class PriorNotificationController( ) return PaginatedListDataOutput.fromListDataOutput( - priorNotificationDataOutputsFilteredBySeafrontGroup, + priorNotificationListItemDataOutputsFilteredBySeafrontGroup, pageNumber, pageSize, extraDataOutput, ) } - @GetMapping("/{logbookMessageReportId}") - @Operation(summary = "Get a prior notification by its (logbook message) `reportId`") + @GetMapping("/{reportId}") + @Operation(summary = "Get a prior notification by its `reportId`") fun getOne( @PathParam("Logbook message `reportId`") - @PathVariable(name = "logbookMessageReportId") - logbookMessageReportId: String, + @PathVariable(name = "reportId") + reportId: String, ): PriorNotificationDetailDataOutput { return PriorNotificationDetailDataOutput.fromPriorNotification( - getPriorNotification.execute(logbookMessageReportId), + getPriorNotification.execute(reportId), + ) + } + + @GetMapping("/{reportId}/data") + @Operation(summary = "Get a prior notification form data by its `reportId`") + fun getOneData( + @PathParam("Logbook message `reportId`") + @PathVariable(name = "reportId") + reportId: String, + ): PriorNotificationDataOutput { + return PriorNotificationDataOutput.fromPriorNotification( + getPriorNotification.execute(reportId), ) } @@ -141,4 +153,55 @@ class PriorNotificationController( fun getAllTypes(): List { return getPriorNotificationTypes.execute() } + + @PostMapping("") + @Operation(summary = "Create a new prior notification") + fun create( + @RequestBody + priorNotificationDataInput: PriorNotificationDataInput, + ): PriorNotificationDataOutput { + val createdPriorNotification = createOrUpdatePriorNotification.execute( + priorNotificationDataInput.authorTrigram, + priorNotificationDataInput.didNotFishAfterZeroNotice, + priorNotificationDataInput.expectedArrivalDate, + priorNotificationDataInput.expectedLandingDate, + priorNotificationDataInput.faoArea, + priorNotificationDataInput.fishingCatches.map { it.toLogbookFishingCatch() }, + priorNotificationDataInput.note, + priorNotificationDataInput.portLocode, + null, + priorNotificationDataInput.sentAt, + priorNotificationDataInput.tripGearCodes, + priorNotificationDataInput.vesselId, + ) + + return PriorNotificationDataOutput.fromPriorNotification(createdPriorNotification) + } + + @PutMapping("/{reportId}") + @Operation(summary = "Update a prior notification by its `reportId`") + fun update( + @PathParam("Logbook message `reportId`") + @PathVariable(name = "reportId") + reportId: String, + @RequestBody + priorNotificationDataInput: PriorNotificationDataInput, + ): PriorNotificationDataOutput { + val updatedPriorNotification = createOrUpdatePriorNotification.execute( + priorNotificationDataInput.authorTrigram, + priorNotificationDataInput.didNotFishAfterZeroNotice, + priorNotificationDataInput.expectedArrivalDate, + priorNotificationDataInput.expectedLandingDate, + priorNotificationDataInput.faoArea, + priorNotificationDataInput.fishingCatches.map { it.toLogbookFishingCatch() }, + priorNotificationDataInput.note, + priorNotificationDataInput.portLocode, + reportId, + priorNotificationDataInput.sentAt, + priorNotificationDataInput.tripGearCodes, + priorNotificationDataInput.vesselId, + ) + + return PriorNotificationDataOutput.fromPriorNotification(updatedPriorNotification) + } } diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/bff/VesselController.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/bff/VesselController.kt index 884258e9b4..9875186bde 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/bff/VesselController.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/bff/VesselController.kt @@ -8,14 +8,12 @@ import fr.gouv.cnsp.monitorfish.infrastructure.api.outputs.* import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.Parameter import io.swagger.v3.oas.annotations.tags.Tag +import jakarta.websocket.server.PathParam import kotlinx.coroutines.runBlocking import org.springframework.format.annotation.DateTimeFormat import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RequestParam -import org.springframework.web.bind.annotation.RestController +import org.springframework.web.bind.annotation.* import java.time.ZonedDateTime @RestController @@ -24,6 +22,7 @@ import java.time.ZonedDateTime class VesselController( private val getLastPositions: GetLastPositions, private val getVessel: GetVessel, + private val getVesselById: GetVesselById, private val getVesselPositions: GetVesselPositions, private val getVesselVoyage: GetVesselVoyage, private val searchVessels: SearchVessels, @@ -32,7 +31,6 @@ class VesselController( private val getVesselRiskFactor: GetVesselRiskFactor, private val getVesselLastTripNumbers: GetVesselLastTripNumbers, ) { - @GetMapping("") @Operation(summary = "Get all vessels' last position") fun getVessels(): List { @@ -45,6 +43,16 @@ class VesselController( } } + @GetMapping("/{vesselId}") + @Operation(summary = "Get a vessel by its ID") + fun getVesselById( + @PathParam("Vessel ID") + @PathVariable(name = "vesselId") + vesselId: Int, + ): VesselDataOutput { + return VesselDataOutput.fromVessel(getVesselById.execute(vesselId)) + } + @GetMapping("/find") @Operation(summary = "Get vessel information and positions") fun getVessel( diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/input/LogbookFishingCatchInput.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/input/LogbookFishingCatchInput.kt new file mode 100644 index 0000000000..614b692dd1 --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/input/LogbookFishingCatchInput.kt @@ -0,0 +1,42 @@ +package fr.gouv.cnsp.monitorfish.infrastructure.api.input + +import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookFishingCatch + +data class LogbookFishingCatchInput( + // TODO What to do with this prop that doesn't exist in `LogbookFishingCatch`? + val isIncidentalCatch: Boolean, + val quantity: Double?, + val specyCode: String, + val specyName: String, + val weight: Double, +) { + companion object { + fun fromLogbookFishingCatch(logbookFishingCatch: LogbookFishingCatch): LogbookFishingCatchInput { + return LogbookFishingCatchInput( + isIncidentalCatch = false, + quantity = logbookFishingCatch.numberFish, + specyCode = requireNotNull(logbookFishingCatch.species), + specyName = requireNotNull(logbookFishingCatch.speciesName), + weight = requireNotNull(logbookFishingCatch.weight), + ) + } + } + + fun toLogbookFishingCatch(): LogbookFishingCatch { + return LogbookFishingCatch( + conversionFactor = 1.toDouble(), + economicZone = null, + effortZone = null, + faoZone = null, + freshness = null, + numberFish = quantity, + packaging = null, + presentation = null, + preservationState = null, + species = specyCode, + speciesName = specyName, + statisticalRectangle = null, + weight = weight, + ) + } +} diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/input/LogbookTripGearDataInput.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/input/LogbookTripGearDataInput.kt new file mode 100644 index 0000000000..afc02df71b --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/input/LogbookTripGearDataInput.kt @@ -0,0 +1,19 @@ +package fr.gouv.cnsp.monitorfish.infrastructure.api.input + +import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookTripGear + +data class LogbookTripGearDataInput( + val code: String?, + val dimensions: String?, + val mesh: Double?, + val name: String?, +) { + fun toLogbookTripGear(): LogbookTripGear { + return LogbookTripGear().apply { + dimensions = dimensions + gear = code + gearName = name + mesh = mesh + } + } +} diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/input/PriorNotificationDataInput.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/input/PriorNotificationDataInput.kt new file mode 100644 index 0000000000..67c21cc820 --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/input/PriorNotificationDataInput.kt @@ -0,0 +1,15 @@ +package fr.gouv.cnsp.monitorfish.infrastructure.api.input + +data class PriorNotificationDataInput( + val authorTrigram: String, + val didNotFishAfterZeroNotice: Boolean, + val expectedArrivalDate: String, + val expectedLandingDate: String, + val faoArea: String, + val fishingCatches: List, + val note: String?, + val portLocode: String, + val sentAt: String, + val tripGearCodes: List, + val vesselId: Int, +) diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/light/outputs/LogbookMessageDataOutput.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/light/outputs/LogbookMessageDataOutput.kt index c5dc57a22d..f30ff76d63 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/light/outputs/LogbookMessageDataOutput.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/light/outputs/LogbookMessageDataOutput.kt @@ -8,7 +8,7 @@ import java.time.ZonedDateTime data class LogbookMessageDataOutput( val reportId: String? = null, - val operationNumber: String, + val operationNumber: String?, val tripNumber: String? = null, val referencedReportId: String? = null, var isCorrected: Boolean? = false, @@ -20,6 +20,7 @@ data class LogbookMessageDataOutput( val externalReferenceNumber: String? = null, val ircs: String? = null, val vesselName: String? = null, + /** ISO Alpha-3 country code. **/ val flagState: String? = null, val imo: String? = null, val messageType: String? = null, diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/BackendInternalErrorDataOutput.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/BackendInternalErrorDataOutput.kt new file mode 100644 index 0000000000..84a56b86f2 --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/BackendInternalErrorDataOutput.kt @@ -0,0 +1,13 @@ +package fr.gouv.cnsp.monitorfish.infrastructure.api.outputs + +/** + * Error output to use when the request is valid but the Backend cannot process it. + * + * This is a Backend bug. + * + * ## Examples + * - An unexpected exception has been caught. + */ +class BackendInternalErrorDataOutput { + val message: String = "An internal error occurred." +} diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/BackendRequestErrorDataOutput.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/BackendRequestErrorDataOutput.kt new file mode 100644 index 0000000000..cb2967d4c0 --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/BackendRequestErrorDataOutput.kt @@ -0,0 +1,17 @@ +package fr.gouv.cnsp.monitorfish.infrastructure.api.outputs + +import fr.gouv.cnsp.monitorfish.infrastructure.exceptions.BackendRequestErrorCode + +/** + * Error output to use when the request is invalid. + * + * It's most likely a Frontend bug. But it may also be a Backend bug. + * + * ## Examples + * - Request data inconsistency that can't be type-checked with a `DataInput` and throws deeper in the code. + */ +data class BackendRequestErrorDataOutput( + val code: BackendRequestErrorCode, + val data: Any? = null, + val message: String? = null, +) diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/BackendUsageErrorDataOutput.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/BackendUsageErrorDataOutput.kt new file mode 100644 index 0000000000..cc52eb1f4f --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/BackendUsageErrorDataOutput.kt @@ -0,0 +1,18 @@ +package fr.gouv.cnsp.monitorfish.infrastructure.api.outputs + +import fr.gouv.cnsp.monitorfish.domain.exceptions.BackendUsageErrorCode + +/** + * Error output to use when the request is valid but the Backend cannot process it. + * + * It's most likely a Frontend error. But it may also be a Backend bug. + * + * ## Examples + * - A user tries to create a resource that has already been created. + * - A user tries to delete a resource that doesn't exist anymore. + */ +data class BackendUsageErrorDataOutput( + val code: BackendUsageErrorCode, + val data: Any? = null, + val message: String? = null, +) diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/LogbookMessageCatchDataOutput.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/LogbookMessageCatchDataOutput.kt index c758d26070..ccb785325a 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/LogbookMessageCatchDataOutput.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/LogbookMessageCatchDataOutput.kt @@ -1,6 +1,6 @@ package fr.gouv.cnsp.monitorfish.infrastructure.api.outputs -import fr.gouv.cnsp.monitorfish.domain.entities.logbook.Catch +import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookFishingCatch class LogbookMessageCatchDataOutput( var weight: Double?, @@ -18,7 +18,7 @@ class LogbookMessageCatchDataOutput( var statisticalRectangle: String?, ) { companion object { - fun fromCatch(catch: Catch): LogbookMessageCatchDataOutput { + fun fromCatch(catch: LogbookFishingCatch): LogbookMessageCatchDataOutput { return LogbookMessageCatchDataOutput( weight = catch.weight, numberFish = catch.numberFish, diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/LogbookMessageDataOutput.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/LogbookMessageDataOutput.kt index 2bb700eada..32f8e0ce0a 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/LogbookMessageDataOutput.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/LogbookMessageDataOutput.kt @@ -8,7 +8,7 @@ import java.time.ZonedDateTime data class LogbookMessageDataOutput( val reportId: String?, - val operationNumber: String, + val operationNumber: String?, val tripNumber: String?, val referencedReportId: String?, val operationDateTime: ZonedDateTime?, @@ -20,11 +20,11 @@ data class LogbookMessageDataOutput( val vesselName: String?, val flagState: String?, val imo: String?, - var rawMessage: String?, + val rawMessage: String?, - var acknowledgment: Acknowledgment?, - var isCorrectedByNewerMessage: Boolean, - var isDeleted: Boolean, + val acknowledgment: Acknowledgment?, + val isCorrectedByNewerMessage: Boolean, + val isDeleted: Boolean, val isSentByFailoverSoftware: Boolean, val message: LogbookMessageValue?, val messageType: String?, diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/LogbookMessageGearDataOutput.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/LogbookMessageGearDataOutput.kt index 56a11f2d0f..56a9e4fa9d 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/LogbookMessageGearDataOutput.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/LogbookMessageGearDataOutput.kt @@ -1,6 +1,6 @@ package fr.gouv.cnsp.monitorfish.infrastructure.api.outputs -import fr.gouv.cnsp.monitorfish.domain.entities.logbook.Gear +import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookTripGear class LogbookMessageGearDataOutput( val gear: String, @@ -9,7 +9,7 @@ class LogbookMessageGearDataOutput( val dimensions: String?, ) { companion object { - fun fromGear(gear: Gear): LogbookMessageGearDataOutput? { + fun fromGear(gear: LogbookTripGear): LogbookMessageGearDataOutput? { return gear.gear?.let { gearCode -> LogbookMessageGearDataOutput( gear = gearCode, diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/PriorNotificationDataOutput.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/PriorNotificationDataOutput.kt index 64189b82a0..873778862f 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/PriorNotificationDataOutput.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/PriorNotificationDataOutput.kt @@ -1,102 +1,40 @@ package fr.gouv.cnsp.monitorfish.infrastructure.api.outputs -import com.neovisionaries.i18n.CountryCode -import fr.gouv.cnsp.monitorfish.domain.entities.facade.Seafront -import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookOperationType import fr.gouv.cnsp.monitorfish.domain.entities.prior_notification.PriorNotification -import org.slf4j.Logger -import org.slf4j.LoggerFactory +import fr.gouv.cnsp.monitorfish.infrastructure.api.input.LogbookFishingCatchInput data class PriorNotificationDataOutput( - /** Reference logbook message (report) `reportId`. */ - val id: String, - val acknowledgment: AcknowledgmentDataOutput?, - val expectedArrivalDate: String?, - val expectedLandingDate: String?, - val hasVesselRiskFactorSegments: Boolean?, - /** Unique identifier concatenating all the DAT, COR, RET & DEL operations `id` used for data consolidation. */ - val fingerprint: String, - val isCorrection: Boolean, - val isVesselUnderCharter: Boolean?, - val onBoardCatches: List, - val portLocode: String?, - val portName: String?, - val purposeCode: String?, - val reportingCount: Int?, - val seafront: Seafront?, - val sentAt: String?, - val tripGears: List, - val tripSegments: List, - val types: List, - val vesselId: Int?, - val vesselExternalReferenceNumber: String?, - val vesselFlagCountryCode: CountryCode, - val vesselInternalReferenceNumber: String?, - val vesselIrcs: String?, - val vesselLastControlDate: String?, - val vesselLength: Double?, - val vesselMmsi: String?, - val vesselName: String?, - val vesselRiskFactor: Double?, - val vesselRiskFactorImpact: Double?, - val vesselRiskFactorProbability: Double?, - val vesselRiskFactorDetectability: Double?, + val authorTrigram: String, + val didNotFishAfterZeroNotice: Boolean, + val expectedArrivalDate: String, + val expectedLandingDate: String, + val faoArea: String, + val fishingCatches: List, + val note: String?, + val portLocode: String, + val reportId: String, + val sentAt: String, + val tripGearCodes: List, + val vesselId: Int, ) { companion object { - val logger: Logger = LoggerFactory.getLogger(PriorNotificationDataOutput::class.java) - - fun fromPriorNotification(priorNotification: PriorNotification): PriorNotificationDataOutput? { + fun fromPriorNotification(priorNotification: PriorNotification): PriorNotificationDataOutput { val logbookMessage = priorNotification.logbookMessageTyped.logbookMessage - val referenceReportId = logbookMessage.getReferenceReportId() - if (referenceReportId == null) { - logger.warn("Prior notification has neither `reportId` nor `referencedReportId`: $priorNotification.") - - return null - } val message = priorNotification.logbookMessageTyped.typedMessage - val acknowledgment = logbookMessage.acknowledgment?.let { AcknowledgmentDataOutput.fromAcknowledgment(it) } - val onBoardCatches = message.catchOnboard.map { LogbookMessageCatchDataOutput.fromCatch(it) } - val tripGears = logbookMessage.tripGears?.mapNotNull { - LogbookMessageGearDataOutput.fromGear(it) - } ?: emptyList() - val tripSegments = logbookMessage.tripSegments?.map { - LogbookMessageTripSegmentDataOutput.fromLogbookTripSegment(it) - } ?: emptyList() - val types = message.pnoTypes.map { PriorNotificationTypeDataOutput.fromPriorNotificationType(it) } - return PriorNotificationDataOutput( - id = referenceReportId, - acknowledgment = acknowledgment, - expectedArrivalDate = message.predictedArrivalDatetimeUtc?.toString(), - expectedLandingDate = message.predictedLandingDatetimeUtc?.toString(), - hasVesselRiskFactorSegments = priorNotification.vesselRiskFactor?.segments?.isNotEmpty(), - fingerprint = priorNotification.fingerprint, - isCorrection = logbookMessage.operationType === LogbookOperationType.COR, - isVesselUnderCharter = priorNotification.vessel.underCharter, - onBoardCatches, - portLocode = priorNotification.port?.locode, - portName = priorNotification.port?.name, - purposeCode = message.purpose, - reportingCount = priorNotification.reportingCount, - seafront = priorNotification.seafront, - sentAt = logbookMessage.reportDateTime?.toString(), - tripGears, - tripSegments, - types, - vesselId = priorNotification.vessel.id, - vesselExternalReferenceNumber = priorNotification.vessel.externalReferenceNumber, - vesselFlagCountryCode = priorNotification.vessel.flagState, - vesselInternalReferenceNumber = priorNotification.vessel.internalReferenceNumber, - vesselIrcs = priorNotification.vessel.ircs, - vesselLastControlDate = priorNotification.vesselRiskFactor?.lastControlDatetime?.toString(), - vesselLength = priorNotification.vessel.length, - vesselMmsi = priorNotification.vessel.mmsi, - vesselName = priorNotification.vessel.vesselName, - vesselRiskFactor = priorNotification.vesselRiskFactor?.riskFactor, - vesselRiskFactorImpact = priorNotification.vesselRiskFactor?.impactRiskFactor, - vesselRiskFactorProbability = priorNotification.vesselRiskFactor?.probabilityRiskFactor, - vesselRiskFactorDetectability = priorNotification.vesselRiskFactor?.detectabilityRiskFactor, + authorTrigram = requireNotNull(priorNotification.authorTrigram), + didNotFishAfterZeroNotice = priorNotification.didNotFishAfterZeroNotice, + expectedArrivalDate = requireNotNull(message.predictedArrivalDatetimeUtc).toString(), + expectedLandingDate = requireNotNull(message.predictedLandingDatetimeUtc).toString(), + faoArea = requireNotNull(message.faoZone), + fishingCatches = message.catchOnboard.map { LogbookFishingCatchInput.fromLogbookFishingCatch(it) }, + note = priorNotification.note, + portLocode = requireNotNull(message.port), + reportId = requireNotNull(priorNotification.reportId), + sentAt = requireNotNull(priorNotification.sentAt), + tripGearCodes = requireNotNull(logbookMessage.tripGears).map { requireNotNull(it.gear) }, + vesselId = requireNotNull(priorNotification.vessel).id, ) } } diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/PriorNotificationDetailDataOutput.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/PriorNotificationDetailDataOutput.kt index 0560eccb46..4fb335716b 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/PriorNotificationDetailDataOutput.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/PriorNotificationDetailDataOutput.kt @@ -3,6 +3,7 @@ package fr.gouv.cnsp.monitorfish.infrastructure.api.outputs import fr.gouv.cnsp.monitorfish.domain.entities.prior_notification.PriorNotification class PriorNotificationDetailDataOutput( + // TODO Rename that to `reportId`. /** Reference logbook message (report) `reportId`. */ val id: String, /** Unique identifier concatenating all the DAT, COR, RET & DEL operations `id` used for data consolidation. */ @@ -12,6 +13,7 @@ class PriorNotificationDetailDataOutput( ) { companion object { fun fromPriorNotification(priorNotification: PriorNotification): PriorNotificationDetailDataOutput { + val isLessThanTwelveMetersVessel = requireNotNull(priorNotification.vessel).isLessThanTwelveMetersVessel() val logbookMessage = priorNotification.logbookMessageTyped.logbookMessage val referenceReportId = requireNotNull(logbookMessage.getReferenceReportId()) val logbookMessageDataOutput = LogbookMessageDataOutput.fromLogbookMessage(logbookMessage) @@ -19,7 +21,7 @@ class PriorNotificationDetailDataOutput( return PriorNotificationDetailDataOutput( id = referenceReportId, fingerprint = priorNotification.fingerprint, - isLessThanTwelveMetersVessel = priorNotification.vessel.isLessThanTwelveMetersVessel(), + isLessThanTwelveMetersVessel = isLessThanTwelveMetersVessel, logbookMessage = logbookMessageDataOutput, ) } diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/PriorNotificationListItemDataOutput.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/PriorNotificationListItemDataOutput.kt new file mode 100644 index 0000000000..f2a53fe1be --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/PriorNotificationListItemDataOutput.kt @@ -0,0 +1,110 @@ +package fr.gouv.cnsp.monitorfish.infrastructure.api.outputs + +import com.neovisionaries.i18n.CountryCode +import fr.gouv.cnsp.monitorfish.domain.entities.facade.Seafront +import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookOperationType +import fr.gouv.cnsp.monitorfish.domain.entities.prior_notification.PriorNotification +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +data class PriorNotificationListItemDataOutput( + /** Reference logbook message (report) `reportId`. */ + val id: String, + val acknowledgment: AcknowledgmentDataOutput?, + val createdAt: String?, + val expectedArrivalDate: String?, + val expectedLandingDate: String?, + val hasVesselRiskFactorSegments: Boolean?, + /** Unique identifier concatenating all the DAT, COR, RET & DEL operations `id` used for data consolidation. */ + val fingerprint: String, + val isCorrection: Boolean, + val isManuallyCreated: Boolean = false, + val isVesselUnderCharter: Boolean?, + val onBoardCatches: List, + val portLocode: String?, + val portName: String?, + val purposeCode: String?, + val reportingCount: Int?, + val seafront: Seafront?, + val sentAt: String?, + val tripGears: List, + val tripSegments: List, + val types: List, + val updatedAt: String?, + val vesselId: Int?, + val vesselExternalReferenceNumber: String?, + val vesselFlagCountryCode: CountryCode, + val vesselInternalReferenceNumber: String?, + val vesselIrcs: String?, + val vesselLastControlDate: String?, + val vesselLength: Double?, + val vesselMmsi: String?, + val vesselName: String?, + val vesselRiskFactor: Double?, + val vesselRiskFactorImpact: Double?, + val vesselRiskFactorProbability: Double?, + val vesselRiskFactorDetectability: Double?, +) { + companion object { + val logger: Logger = LoggerFactory.getLogger(PriorNotificationListItemDataOutput::class.java) + + fun fromPriorNotification(priorNotification: PriorNotification): PriorNotificationListItemDataOutput? { + val logbookMessage = priorNotification.logbookMessageTyped.logbookMessage + val referenceReportId = logbookMessage.getReferenceReportId() + if (referenceReportId == null) { + logger.warn("Prior notification has neither `reportId` nor `referencedReportId`: $priorNotification.") + + return null + } + val message = priorNotification.logbookMessageTyped.typedMessage + + val acknowledgment = logbookMessage.acknowledgment?.let { AcknowledgmentDataOutput.fromAcknowledgment(it) } + val onBoardCatches = message.catchOnboard.map { LogbookMessageCatchDataOutput.fromCatch(it) } + val tripGears = logbookMessage.tripGears?.mapNotNull { + LogbookMessageGearDataOutput.fromGear(it) + } ?: emptyList() + val tripSegments = logbookMessage.tripSegments?.map { + LogbookMessageTripSegmentDataOutput.fromLogbookTripSegment(it) + } ?: emptyList() + val types = message.pnoTypes.map { PriorNotificationTypeDataOutput.fromPriorNotificationType(it) } + val vessel = requireNotNull(priorNotification.vessel) + + return PriorNotificationListItemDataOutput( + id = referenceReportId, + acknowledgment = acknowledgment, + createdAt = priorNotification.createdAt.toString(), + expectedArrivalDate = message.predictedArrivalDatetimeUtc?.toString(), + expectedLandingDate = message.predictedLandingDatetimeUtc?.toString(), + hasVesselRiskFactorSegments = priorNotification.vesselRiskFactor?.segments?.isNotEmpty(), + fingerprint = priorNotification.fingerprint, + isCorrection = logbookMessage.operationType === LogbookOperationType.COR, + isManuallyCreated = priorNotification.isManuallyCreated, + isVesselUnderCharter = vessel.underCharter, + onBoardCatches, + portLocode = priorNotification.port?.locode, + portName = priorNotification.port?.name, + purposeCode = message.purpose, + reportingCount = priorNotification.reportingCount, + seafront = priorNotification.seafront, + sentAt = logbookMessage.reportDateTime?.toString(), + tripGears, + tripSegments, + types, + updatedAt = priorNotification.updatedAt.toString(), + vesselId = vessel.id, + vesselExternalReferenceNumber = vessel.externalReferenceNumber, + vesselFlagCountryCode = vessel.flagState, + vesselInternalReferenceNumber = vessel.internalReferenceNumber, + vesselIrcs = vessel.ircs, + vesselLastControlDate = priorNotification.vesselRiskFactor?.lastControlDatetime?.toString(), + vesselLength = vessel.length, + vesselMmsi = vessel.mmsi, + vesselName = vessel.vesselName, + vesselRiskFactor = priorNotification.vesselRiskFactor?.riskFactor, + vesselRiskFactorImpact = priorNotification.vesselRiskFactor?.impactRiskFactor, + vesselRiskFactorProbability = priorNotification.vesselRiskFactor?.probabilityRiskFactor, + vesselRiskFactorDetectability = priorNotification.vesselRiskFactor?.detectabilityRiskFactor, + ) + } + } +} diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/entities/LogbookReportEntity.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/entities/LogbookReportEntity.kt index 449513b064..be01022c3c 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/entities/LogbookReportEntity.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/entities/LogbookReportEntity.kt @@ -6,6 +6,7 @@ import fr.gouv.cnsp.monitorfish.domain.entities.logbook.messages.PNO import fr.gouv.cnsp.monitorfish.domain.entities.prior_notification.PriorNotification import fr.gouv.cnsp.monitorfish.domain.entities.vessel.UNKNOWN_VESSEL import fr.gouv.cnsp.monitorfish.domain.mappers.ERSMapper.getERSMessageValueFromJSON +import fr.gouv.cnsp.monitorfish.infrastructure.database.entities.abstractions.AbstractLogbookEntity import io.hypersistence.utils.hibernate.type.json.JsonBinaryType import jakarta.persistence.* import org.hibernate.annotations.JdbcType @@ -23,65 +24,65 @@ data class LogbookReportEntity( @Column(name = "id") val id: Long? = null, @Column(name = "operation_number") - val operationNumber: String, + val operationNumber: String?, @Column(name = "trip_number") - val tripNumber: String? = null, + val tripNumber: String?, @Column(name = "operation_country") - val operationCountry: String? = null, + val operationCountry: String?, @Column(name = "operation_datetime_utc") val operationDateTime: Instant, @Column(name = "operation_type") @Enumerated(EnumType.STRING) val operationType: LogbookOperationType, @Column(name = "report_id") - val reportId: String? = null, + val reportId: String?, @Column(name = "referenced_report_id") - val referencedReportId: String? = null, + val referencedReportId: String?, @Column(name = "report_datetime_utc") - val reportDateTime: Instant? = null, - @Column(name = "cfr") - val internalReferenceNumber: String? = null, + val reportDateTime: Instant?, @Column(name = "ircs") - val ircs: String? = null, + val ircs: String?, @Column(name = "external_identification") - val externalReferenceNumber: String? = null, - @Column(name = "vessel_name") - val vesselName: String? = null, - // ISO Alpha-3 country code - @Column(name = "flag_state") - val flagState: String? = null, + val externalReferenceNumber: String?, @Column(name = "imo") - val imo: String? = null, + val imo: String?, @Column(name = "log_type") - val messageType: String? = null, + val messageType: String?, @Column(name = "analyzed_by_rules", columnDefinition = "varchar(100)[]") - val analyzedByRules: List? = listOf(), + val analyzedByRules: List?, @Type(JsonBinaryType::class) @Column(name = "value", nullable = true, columnDefinition = "jsonb") - val message: String? = null, + val message: String?, @Column(name = "integration_datetime_utc") val integrationDateTime: Instant, @JdbcType(PostgreSQLEnumJdbcType::class) @Column(name = "transmission_format", columnDefinition = "logbook_message_transmission_format") @Enumerated(EnumType.STRING) - val transmissionFormat: LogbookTransmissionFormat, + val transmissionFormat: LogbookTransmissionFormat?, @Column(name = "software") - val software: String? = null, + val software: String?, @Column(name = "enriched") val isEnriched: Boolean = false, - @Type(JsonBinaryType::class) - @Column(name = "trip_gears", nullable = true, columnDefinition = "jsonb") - val tripGears: String? = null, - @Type(JsonBinaryType::class) - @Column(name = "trip_segments", nullable = true, columnDefinition = "jsonb") - val tripSegments: String? = null, + + /** ISO Alpha-3 country code. */ + override val flagState: String?, + override val cfr: String?, + override val tripGears: List?, + override val tripSegments: List?, + override val vesselName: String?, +) : AbstractLogbookEntity( + cfr = cfr, + flagState = flagState, + tripGears = tripGears, + tripSegments = tripSegments, + vesselName = vesselName, ) { companion object { fun fromLogbookMessage( mapper: ObjectMapper, logbookMessage: LogbookMessage, ) = LogbookReportEntity( - internalReferenceNumber = logbookMessage.internalReferenceNumber, + cfr = logbookMessage.internalReferenceNumber, referencedReportId = logbookMessage.referencedReportId, externalReferenceNumber = logbookMessage.externalReferenceNumber, ircs = logbookMessage.ircs, @@ -101,18 +102,19 @@ data class LogbookReportEntity( isEnriched = logbookMessage.isEnriched, message = mapper.writeValueAsString(logbookMessage.message), messageType = logbookMessage.messageType, + operationCountry = null, operationType = logbookMessage.operationType, + tripGears = null, + tripSegments = null, ) } fun toLogbookMessage(mapper: ObjectMapper): LogbookMessage { val message = getERSMessageValueFromJSON(mapper, message, messageType, operationType) - val tripGears = deserializeJSONList(mapper, tripGears, Gear::class.java) - val tripSegments = deserializeJSONList(mapper, tripSegments, LogbookTripSegment::class.java) return LogbookMessage( id = id!!, - internalReferenceNumber = internalReferenceNumber, + internalReferenceNumber = cfr, referencedReportId = referencedReportId, externalReferenceNumber = externalReferenceNumber, ircs = ircs, @@ -125,7 +127,7 @@ data class LogbookReportEntity( tripNumber = tripNumber, flagState = flagState, imo = imo, - analyzedByRules = analyzedByRules ?: listOf(), + analyzedByRules = analyzedByRules ?: emptyList(), software = software, transmissionFormat = transmissionFormat, @@ -140,33 +142,33 @@ data class LogbookReportEntity( fun toPriorNotification(mapper: ObjectMapper, relatedModels: List): PriorNotification { val referenceLogbookMessage = toLogbookMessage(mapper) - val fingerprint = listOf(referenceLogbookMessage.id) - .plus(relatedModels.mapNotNull { it.id }) - .sorted() - .joinToString(separator = ".") - val relatedLogbookMessages = relatedModels.map { it.toLogbookMessage(mapper) } + val relatedLogbookMessages = relatedModels + .map { it.toLogbookMessage(mapper) } + .sortedBy { it.operationDateTime } val enrichedLogbookMessageTyped = referenceLogbookMessage .toEnrichedLogbookMessageTyped(relatedLogbookMessages, PNO::class.java) - // For practical reasons `vessel` can't be `null`, so we temporarily set it to "Navire inconnu" + val updatedAt = relatedLogbookMessages.lastOrNull()?.let { it.operationDateTime.toString() } + ?: operationDateTime.toString() + // For pratical reasons `vessel` can't be `null`, so we temporarely set it to "Navire inconnu" val vessel = UNKNOWN_VESSEL return PriorNotification( - fingerprint, + reportId = reportId, + authorTrigram = null, + createdAt = operationDateTime.toString(), + didNotFishAfterZeroNotice = false, + isManuallyCreated = false, logbookMessageTyped = enrichedLogbookMessageTyped, + note = null, + sentAt = enrichedLogbookMessageTyped.logbookMessage.reportDateTime?.toString(), + updatedAt = updatedAt, + + // These props need to be calculated in the use case + port = null, + reportingCount = null, + seafront = null, vessel = vessel, + vesselRiskFactor = null, ) } - - private fun deserializeJSONList( - mapper: ObjectMapper, - json: String?, - clazz: Class, - ): List = - json?.let { - mapper.readValue( - json, - mapper.typeFactory - .constructCollectionType(MutableList::class.java, clazz), - ) - } ?: listOf() } diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/entities/ManualPriorNotificationEntity.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/entities/ManualPriorNotificationEntity.kt new file mode 100644 index 0000000000..3c2d19827a --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/entities/ManualPriorNotificationEntity.kt @@ -0,0 +1,159 @@ +package fr.gouv.cnsp.monitorfish.infrastructure.database.entities + +import fr.gouv.cnsp.monitorfish.domain.entities.logbook.* +import fr.gouv.cnsp.monitorfish.domain.entities.logbook.messages.PNO +import fr.gouv.cnsp.monitorfish.domain.entities.prior_notification.PriorNotification +import fr.gouv.cnsp.monitorfish.domain.entities.vessel.UNKNOWN_VESSEL +import fr.gouv.cnsp.monitorfish.domain.exceptions.BackendInternalException +import fr.gouv.cnsp.monitorfish.infrastructure.database.entities.abstractions.AbstractLogbookEntity +import io.hypersistence.utils.hibernate.type.json.JsonBinaryType +import jakarta.persistence.* +import org.hibernate.annotations.CreationTimestamp +import org.hibernate.annotations.Type +import org.hibernate.annotations.UpdateTimestamp +import java.time.Instant +import java.time.ZoneOffset +import java.time.ZonedDateTime + +@Entity +@Table(name = "manual_prior_notifications") +data class ManualPriorNotificationEntity( + @Id + @Column(name = "report_id", updatable = false) + @GeneratedValue(strategy = GenerationType.IDENTITY) + val reportId: String?, + + @Column(name = "author_trigram") + val authorTrigram: String, + + @Column(name = "created_at", insertable = false, updatable = false) + @CreationTimestamp + val createdAt: ZonedDateTime? = null, + + @Column(name = "did_not_fish_after_zero_notice") + val didNotFishAfterZeroNotice: Boolean, + + @Column(name = "note") + val note: String?, + + @Column(name = "sent_at") + val sentAt: ZonedDateTime, + + @Column(name = "updated_at") + @UpdateTimestamp + val updatedAt: ZonedDateTime? = null, + + @Column(name = "value", nullable = true, columnDefinition = "jsonb") + @Type(JsonBinaryType::class) + val value: PNO, + + /** ISO Alpha-3 country code. */ + override val flagState: String?, + override val cfr: String?, + override val tripGears: List?, + override val tripSegments: List?, + override val vesselName: String?, +) : AbstractLogbookEntity( + cfr = cfr, + flagState = flagState, + vesselName = vesselName, + tripGears = tripGears, + tripSegments = tripSegments, +) { + + companion object { + fun fromPriorNotification(priorNotification: PriorNotification): ManualPriorNotificationEntity { + try { + val pnoLogbookMessage = priorNotification.logbookMessageTyped.logbookMessage + val pnoLogbookMessageValue = priorNotification.logbookMessageTyped.typedMessage + val createdAt = priorNotification.createdAt?.let { ZonedDateTime.parse(it) } + val updatedAt = priorNotification.updatedAt?.let { ZonedDateTime.parse(it) } + + return ManualPriorNotificationEntity( + reportId = pnoLogbookMessage.reportId, + authorTrigram = requireNotNull(priorNotification.authorTrigram), + cfr = requireNotNull(pnoLogbookMessage.internalReferenceNumber), + createdAt = createdAt, + didNotFishAfterZeroNotice = priorNotification.didNotFishAfterZeroNotice, + flagState = pnoLogbookMessage.flagState, + note = priorNotification.note, + sentAt = ZonedDateTime.parse(requireNotNull(priorNotification.sentAt)), + tripGears = pnoLogbookMessage.tripGears, + tripSegments = pnoLogbookMessage.tripSegments, + updatedAt = updatedAt, + value = pnoLogbookMessageValue, + vesselName = pnoLogbookMessage.vesselName, + ) + } catch (e: IllegalArgumentException) { + throw BackendInternalException( + "Error while converting `PriorNotification` to `ManualPriorNotificationEntity` (likely because a non-nullable variable is null).", + e, + ) + } + } + } + + fun toPriorNotification(): PriorNotification { + try { + val createdAt = getUtcZonedDateTime(createdAt, reportId) + + val pnoLogbookMessage = LogbookMessage( + id = null, + reportId = requireNotNull(reportId), + analyzedByRules = emptyList(), + isEnriched = true, + integrationDateTime = createdAt, + internalReferenceNumber = cfr, + message = value, + messageType = LogbookMessageTypeMapping.PNO.name, + operationDateTime = createdAt, + operationNumber = null, + operationType = LogbookOperationType.DAT, + reportDateTime = sentAt, + transmissionFormat = null, + tripGears = tripGears, + tripSegments = tripSegments, + vesselName = vesselName, + ) + // For pratical reasons `vessel` can't be `null`, so we temporarely set it to "Navire inconnu" + val vessel = UNKNOWN_VESSEL + val logbookMessageTyped = LogbookMessageTyped(pnoLogbookMessage, PNO::class.java) + + return PriorNotification( + authorTrigram = authorTrigram, + createdAt = createdAt.toString(), + didNotFishAfterZeroNotice = didNotFishAfterZeroNotice, + isManuallyCreated = true, + logbookMessageTyped = logbookMessageTyped, + note = note, + reportId = reportId, + sentAt = sentAt.toString(), + updatedAt = updatedAt.toString(), + + // These props need to be calculated in the use case + port = null, + reportingCount = null, + seafront = null, + vessel = vessel, + vesselRiskFactor = null, + ) + } catch (e: IllegalArgumentException) { + throw BackendInternalException( + "Error while converting `ManualPriorNotificationEntity` to `PriorNotification` (likely because a non-nullable variable is null).", + e, + ) + } + } + + private fun getUtcZonedDateTime(dateTime: ZonedDateTime?, reportId: String?): ZonedDateTime { + return if (dateTime != null) { + dateTime + } else { + // TODO Impossible to add a `logger` property in this class. + // logger.warn("`dateTime` is null for reportId=$reportId. Replaced by EPOCH date.") + println("WARNING: `dateTime` is null for reportId=$reportId. Replaced by EPOCH date.") + + ZonedDateTime.ofInstant(Instant.EPOCH, ZoneOffset.UTC) + } + } +} diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/entities/abstractions/AbstractLogbookEntity.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/entities/abstractions/AbstractLogbookEntity.kt new file mode 100644 index 0000000000..7f49dda09b --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/entities/abstractions/AbstractLogbookEntity.kt @@ -0,0 +1,29 @@ +package fr.gouv.cnsp.monitorfish.infrastructure.database.entities.abstractions + +import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookTripGear +import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookTripSegment +import io.hypersistence.utils.hibernate.type.json.JsonBinaryType +import jakarta.persistence.Column +import jakarta.persistence.MappedSuperclass +import org.hibernate.annotations.Type + +@MappedSuperclass +abstract class AbstractLogbookEntity( + @Column(name = "cfr") + open val cfr: String?, + + /** ISO Alpha-3 country code. */ + @Column(name = "flag_state") + open val flagState: String?, + + @Column(name = "trip_gears", nullable = true, columnDefinition = "jsonb") + @Type(JsonBinaryType::class) + open val tripGears: List?, + + @Column(name = "trip_segments", nullable = true, columnDefinition = "jsonb") + @Type(JsonBinaryType::class) + open val tripSegments: List?, + + @Column(name = "vessel_name") + open val vesselName: String?, +) diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaLogbookReportRepository.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaLogbookReportRepository.kt index 854ab16cd7..9ccefbd69e 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaLogbookReportRepository.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaLogbookReportRepository.kt @@ -1,12 +1,10 @@ package fr.gouv.cnsp.monitorfish.infrastructure.database.repositories import com.fasterxml.jackson.databind.ObjectMapper -import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookMessage -import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookMessageTypeMapping -import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookOperationType -import fr.gouv.cnsp.monitorfish.domain.entities.logbook.VoyageDatesAndTripNumber -import fr.gouv.cnsp.monitorfish.domain.entities.logbook.filters.LogbookReportFilter +import fr.gouv.cnsp.monitorfish.domain.entities.logbook.* +import fr.gouv.cnsp.monitorfish.domain.entities.logbook.messages.PNO import fr.gouv.cnsp.monitorfish.domain.entities.prior_notification.PriorNotification +import fr.gouv.cnsp.monitorfish.domain.entities.prior_notification.filters.PriorNotificationsFilter import fr.gouv.cnsp.monitorfish.domain.exceptions.EntityConversionException import fr.gouv.cnsp.monitorfish.domain.exceptions.NoERSMessagesFound import fr.gouv.cnsp.monitorfish.domain.exceptions.NoLogbookFishingTripFound @@ -32,7 +30,7 @@ class JpaLogbookReportRepository( private val logger = LoggerFactory.getLogger(JpaLogbookReportRepository::class.java) private val postgresChunkSize = 5000 - override fun findAllPriorNotifications(filter: LogbookReportFilter): List { + override fun findAllPriorNotifications(filter: PriorNotificationsFilter): List { val allLogbookReportModels = dbERSRepository.findAllEnrichedPnoReferencesAndRelatedOperations( flagStates = filter.flagStates ?: emptyList(), hasOneOrMoreReportings = filter.hasOneOrMoreReportings, @@ -49,26 +47,27 @@ class JpaLogbookReportRepository( willArriveBefore = filter.willArriveBefore, ) - return mapToReferenceWithRelatedModels(allLogbookReportModels).mapNotNull { (referenceLogbookReportModel, relatedLogbookReportModels) -> - try { - referenceLogbookReportModel.toPriorNotification(mapper, relatedLogbookReportModels) - } catch (e: Exception) { - logger.warn( - "Error while converting logbook report models to prior notifications (reoportId = ${referenceLogbookReportModel.reportId}).", - e, - ) + return mapToReferenceWithRelatedModels(allLogbookReportModels) + .mapNotNull { (referenceLogbookReportModel, relatedLogbookReportModels) -> + try { + referenceLogbookReportModel.toPriorNotification(mapper, relatedLogbookReportModels) + } catch (e: Exception) { + logger.warn( + "Error while converting logbook report models to prior notifications (reoportId = ${referenceLogbookReportModel.reportId}).", + e, + ) - null + null + } } - } } - override fun findPriorNotificationByReportId(reportId: String): PriorNotification { + override fun findPriorNotificationByReportId(reportId: String): PriorNotification? { val allLogbookReportModels = dbERSRepository.findEnrichedPnoReferenceAndRelatedOperationsByReportId( reportId, ) if (allLogbookReportModels.isEmpty()) { - throw NoERSMessagesFound("No logbook report found for the given reportId: $reportId.") + return null } try { @@ -253,13 +252,13 @@ class JpaLogbookReportRepository( } return lanAndPnoMessagesWithoutCorrectedMessages.filter { - it.internalReferenceNumber != null && + it.cfr != null && it.tripNumber != null && it.messageType == LogbookMessageTypeMapping.LAN.name }.map { lanMessage -> val pnoMessage = lanAndPnoMessagesWithoutCorrectedMessages.singleOrNull { message -> - message.internalReferenceNumber == lanMessage.internalReferenceNumber && + message.cfr == lanMessage.cfr && message.tripNumber == lanMessage.tripNumber && message.messageType == LogbookMessageTypeMapping.PNO.name } @@ -341,6 +340,14 @@ class JpaLogbookReportRepository( dbERSRepository.save(LogbookReportEntity.fromLogbookMessage(mapper, message)) } + @Modifying + @Transactional + override fun savePriorNotification(logbookMessageTyped: LogbookMessageTyped): PriorNotification { + return dbERSRepository + .save(LogbookReportEntity.fromLogbookMessage(mapper, logbookMessageTyped.logbookMessage)) + .toPriorNotification(mapper, emptyList()) + } + private fun getCorrectedMessageIfAvailable( pnoMessage: LogbookReportEntity, messages: List, diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaManualPriorNotificationRepository.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaManualPriorNotificationRepository.kt new file mode 100644 index 0000000000..e788fe21dd --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaManualPriorNotificationRepository.kt @@ -0,0 +1,58 @@ +package fr.gouv.cnsp.monitorfish.infrastructure.database.repositories + +import fr.gouv.cnsp.monitorfish.domain.entities.prior_notification.PriorNotification +import fr.gouv.cnsp.monitorfish.domain.entities.prior_notification.filters.PriorNotificationsFilter +import fr.gouv.cnsp.monitorfish.domain.exceptions.BackendInternalException +import fr.gouv.cnsp.monitorfish.domain.repositories.ManualPriorNotificationRepository +import fr.gouv.cnsp.monitorfish.infrastructure.database.entities.ManualPriorNotificationEntity +import fr.gouv.cnsp.monitorfish.infrastructure.database.repositories.interfaces.DBManualPriorNotificationRepository +import fr.gouv.cnsp.monitorfish.infrastructure.database.repositories.utils.toSqlArrayString +import org.springframework.stereotype.Repository +import org.springframework.transaction.annotation.Transactional + +@Repository +class JpaManualPriorNotificationRepository( + private val dbManualPriorNotificationRepository: DBManualPriorNotificationRepository, +) : ManualPriorNotificationRepository { + override fun findAll(filter: PriorNotificationsFilter): List { + // Manual prior notifications are only for less than 12 meters vessels + if (filter.isLessThanTwelveMetersVessel == false) { + return emptyList() + } + + return dbManualPriorNotificationRepository + .findAll( + flagStates = filter.flagStates ?: emptyList(), + hasOneOrMoreReportings = filter.hasOneOrMoreReportings, + lastControlledAfter = filter.lastControlledAfter, + lastControlledBefore = filter.lastControlledBefore, + portLocodes = filter.portLocodes ?: emptyList(), + priorNotificationTypesAsSqlArrayString = toSqlArrayString(filter.priorNotificationTypes), + searchQuery = filter.searchQuery, + specyCodesAsSqlArrayString = toSqlArrayString(filter.specyCodes), + tripGearCodesAsSqlArrayString = toSqlArrayString(filter.tripGearCodes), + tripSegmentCodesAsSqlArrayString = toSqlArrayString(filter.tripSegmentCodes), + willArriveAfter = filter.willArriveAfter, + willArriveBefore = filter.willArriveBefore, + ).map { it.toPriorNotification() } + } + + override fun findByReportId(reportId: String): PriorNotification? { + return dbManualPriorNotificationRepository.findByReportId(reportId)?.toPriorNotification() + } + + @Transactional + override fun save(newOrNextPriorNotification: PriorNotification): String { + try { + val manualPriorNotificationEntity = dbManualPriorNotificationRepository + .save(ManualPriorNotificationEntity.fromPriorNotification(newOrNextPriorNotification)) + + return requireNotNull(manualPriorNotificationEntity.reportId) + } catch (e: IllegalArgumentException) { + throw BackendInternalException( + "Error while saving the prior notification (likely because a non-nullable variable is null).", + e, + ) + } + } +} diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaVesselRepository.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaVesselRepository.kt index f855167f78..91ce6f7dae 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaVesselRepository.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaVesselRepository.kt @@ -1,6 +1,8 @@ package fr.gouv.cnsp.monitorfish.infrastructure.database.repositories import fr.gouv.cnsp.monitorfish.domain.entities.vessel.Vessel +import fr.gouv.cnsp.monitorfish.domain.exceptions.BackendUsageErrorCode +import fr.gouv.cnsp.monitorfish.domain.exceptions.BackendUsageException import fr.gouv.cnsp.monitorfish.domain.repositories.VesselRepository import fr.gouv.cnsp.monitorfish.infrastructure.database.repositories.interfaces.DBVesselRepository import org.slf4j.Logger @@ -62,11 +64,11 @@ class JpaVesselRepository(private val dbVesselRepository: DBVesselRepository) : return dbVesselRepository.findAllByIds(ids).map { it.toVessel() } } - override fun findVesselById(vesselId: Int): Vessel? { + override fun findVesselById(vesselId: Int): Vessel { return try { dbVesselRepository.findById(vesselId).get().toVessel() } catch (e: NoSuchElementException) { - null + throw BackendUsageException(BackendUsageErrorCode.NOT_FOUND) } } diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/interfaces/DBLogbookReportRepository.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/interfaces/DBLogbookReportRepository.kt index e6a93b5b7d..6e3704cc16 100644 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/interfaces/DBLogbookReportRepository.kt +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/interfaces/DBLogbookReportRepository.kt @@ -15,7 +15,7 @@ interface DBLogbookReportRepository : @Query( """ WITH - dat_and_cor_logbook_reports_with_extra_columns AS ( + dat_and_cor_pno_logbook_reports_with_extra_columns AS ( SELECT lr.*, (SELECT array_agg(pnoTypes->>'pnoTypeName') FROM jsonb_array_elements(lr.value->'pnoTypes') AS pnoTypes) AS prior_notification_type_names, @@ -23,7 +23,6 @@ interface DBLogbookReportRepository : (SELECT array_agg(tripGears->>'gear') FROM jsonb_array_elements(lr.trip_gears) AS tripGears) AS trip_gear_codes, (SELECT array_agg(tripSegments->>'segment') FROM jsonb_array_elements(lr.trip_segments) AS tripSegments) AS trip_segment_codes FROM logbook_reports lr - LEFT JOIN ports p ON lr.value->>'port' = p.locode LEFT JOIN risk_factors rf ON lr.cfr = rf.cfr LEFT JOIN vessels v ON lr.cfr = v.cfr WHERE @@ -67,7 +66,7 @@ interface DBLogbookReportRepository : distinct_cfrs AS ( SELECT DISTINCT cfr - FROM dat_and_cor_logbook_reports_with_extra_columns + FROM dat_and_cor_pno_logbook_reports_with_extra_columns ), cfr_reporting_counts AS ( @@ -83,17 +82,17 @@ interface DBLogbookReportRepository : GROUP BY cfr ), - dat_and_cor_logbook_reports_with_extra_columns_and_reporting_count AS ( + dat_and_cor_pno_logbook_reports_with_extra_columns_and_reporting_count AS ( SELECT - daclr.*, + dacplrwecarc.*, COALESCE(crc.reporting_count, 0) AS reporting_count - FROM dat_and_cor_logbook_reports_with_extra_columns daclr - LEFT JOIN cfr_reporting_counts crc ON daclr.cfr = crc.cfr + FROM dat_and_cor_pno_logbook_reports_with_extra_columns dacplrwecarc + LEFT JOIN cfr_reporting_counts crc ON dacplrwecarc.cfr = crc.cfr ), - dat_and_cor_logbook_reports AS ( + filtered_dat_and_cor_pno_logbook_reports AS ( SELECT * - FROM dat_and_cor_logbook_reports_with_extra_columns_and_reporting_count + FROM dat_and_cor_pno_logbook_reports_with_extra_columns_and_reporting_count WHERE -- Has One Or More Reportings ( @@ -115,7 +114,7 @@ interface DBLogbookReportRepository : AND (:tripSegmentCodesAsSqlArrayString IS NULL OR trip_segment_codes && CAST(:tripSegmentCodesAsSqlArrayString AS TEXT[])) ), - del_logbook_reports AS ( + del_pno_logbook_reports AS ( SELECT lr.*, CAST(NULL AS TEXT[]) AS prior_notification_type_names, @@ -124,7 +123,7 @@ interface DBLogbookReportRepository : CAST(NULL AS TEXT[]) AS trip_segment_codes, CAST(NULL AS INTEGER) AS reporting_count FROM logbook_reports lr - JOIN dat_and_cor_logbook_reports daclr ON lr.referenced_report_id = daclr.report_id + JOIN filtered_dat_and_cor_pno_logbook_reports fdacplr ON lr.referenced_report_id = fdacplr.report_id WHERE -- This filter helps Timescale optimize the query since `operation_datetime_utc` is indexed lr.operation_datetime_utc @@ -134,7 +133,7 @@ interface DBLogbookReportRepository : AND lr.operation_type = 'DEL' ), - ret_logbook_reports AS ( + ret_pno_logbook_reports AS ( SELECT lr.*, CAST(NULL AS TEXT[]) AS prior_notification_type_names, @@ -143,7 +142,7 @@ interface DBLogbookReportRepository : CAST(NULL AS TEXT[]) AS trip_segment_codes, CAST(NULL AS INTEGER) AS reporting_count FROM logbook_reports lr - JOIN dat_and_cor_logbook_reports daclr ON lr.referenced_report_id = daclr.report_id + JOIN filtered_dat_and_cor_pno_logbook_reports fdacplr ON lr.referenced_report_id = fdacplr.report_id WHERE -- This filter helps Timescale optimize the query since `operation_datetime_utc` is indexed lr.operation_datetime_utc @@ -154,17 +153,17 @@ interface DBLogbookReportRepository : ) SELECT * - FROM dat_and_cor_logbook_reports + FROM filtered_dat_and_cor_pno_logbook_reports UNION SELECT * - FROM del_logbook_reports + FROM del_pno_logbook_reports UNION SELECT * - FROM ret_logbook_reports; + FROM ret_pno_logbook_reports; """, nativeQuery = true, ) @@ -192,23 +191,30 @@ interface DBLogbookReportRepository : -- It may not exist while a COR operation would still exist (orphan COR case) SELECT report_id FROM logbook_reports - WHERE report_id = ?1 AND log_type = 'PNO' AND operation_type = 'DAT' AND enriched = TRUE + WHERE + report_id = :reportId + AND log_type = 'PNO' + AND operation_type = 'DAT' + AND enriched = TRUE UNION -- Get the logbook report corrections which may be used as base for the "final" report SELECT report_id FROM logbook_reports - WHERE referenced_report_id = ?1 AND log_type = 'PNO' AND operation_type = 'COR' AND enriched = TRUE - ) + WHERE + referenced_report_id = :reportId + AND log_type = 'PNO' + AND operation_type = 'COR' + AND enriched = TRUE + ) SELECT * FROM logbook_reports WHERE report_id IN (SELECT * FROM dat_and_cor_logbook_report_report_ids) OR referenced_report_id IN (SELECT * FROM dat_and_cor_logbook_report_report_ids) - ORDER BY - report_datetime_utc + ORDER BY report_datetime_utc; """, nativeQuery = true, ) diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/interfaces/DBManualPriorNotificationRepository.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/interfaces/DBManualPriorNotificationRepository.kt new file mode 100644 index 0000000000..1d12d338e4 --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/interfaces/DBManualPriorNotificationRepository.kt @@ -0,0 +1,130 @@ +package fr.gouv.cnsp.monitorfish.infrastructure.database.repositories.interfaces + +import fr.gouv.cnsp.monitorfish.infrastructure.database.entities.ManualPriorNotificationEntity +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query + +interface DBManualPriorNotificationRepository : JpaRepository { + @Query( + """ + WITH + manual_prior_notifications_with_extra_columns AS ( + SELECT + mpn.*, + (SELECT array_agg(pnoTypes->>'pnoTypeName') FROM jsonb_array_elements(mpn.value->'pnoTypes') AS pnoTypes) AS prior_notification_type_names, + (SELECT array_agg(catchOnboard->>'species') FROM jsonb_array_elements(mpn.value->'catchOnboard') AS catchOnboard) AS specy_codes, + (SELECT array_agg(tripGears->>'gear') FROM jsonb_array_elements(mpn.trip_gears) AS tripGears) AS trip_gear_codes, + (SELECT array_agg(tripSegments->>'segment') FROM jsonb_array_elements(mpn.trip_segments) AS tripSegments) AS trip_segment_codes + FROM manual_prior_notifications mpn + LEFT JOIN risk_factors rf ON mpn.cfr = rf.cfr + WHERE + -- TODO /!\ INDEX created_at WITH TIMESCALE /!\ + -- This filter helps Timescale optimize the query since `created_at` is indexed + mpn.created_at + BETWEEN CAST(:willArriveAfter AS TIMESTAMP) - INTERVAL '48 hours' + AND CAST(:willArriveBefore AS TIMESTAMP) + INTERVAL '48 hours' + + -- Flag States + AND (:flagStates IS NULL OR mpn.flag_state IN (:flagStates)) + + -- Last Controlled After + AND (:lastControlledAfter IS NULL OR rf.last_control_datetime_utc >= CAST(:lastControlledAfter AS TIMESTAMP)) + + -- Last Controlled Before + AND (:lastControlledBefore IS NULL OR rf.last_control_datetime_utc <= CAST(:lastControlledBefore AS TIMESTAMP)) + + -- Port Locodes + AND (:portLocodes IS NULL OR mpn.value->>'port' IN (:portLocodes)) + + -- Search Query + AND (:searchQuery IS NULL OR unaccent(lower(mpn.vessel_name)) ILIKE CONCAT('%', unaccent(lower(:searchQuery)), '%')) + + -- Will Arrive After + AND mpn.value->>'predictedArrivalDatetimeUtc' >= :willArriveAfter + + -- Will Arrive Before + AND mpn.value->>'predictedArrivalDatetimeUtc' <= :willArriveBefore + ), + + distinct_cfrs AS ( + SELECT DISTINCT cfr + FROM manual_prior_notifications_with_extra_columns + ), + + cfr_reporting_counts AS ( + SELECT + dc.cfr, + COUNT(r.id) AS reporting_count + FROM distinct_cfrs dc + LEFT JOIN reportings r ON dc.cfr = r.internal_reference_number + WHERE + r.type = 'INFRACTION_SUSPICION' + AND r.archived = FALSE + AND r.deleted = FALSE + GROUP BY cfr + ), + + manual_prior_notifications_with_extra_columns_and_reporting_count AS ( + SELECT + mpnwecarc.*, + COALESCE(crc.reporting_count, 0) AS reporting_count + FROM manual_prior_notifications_with_extra_columns mpnwecarc + LEFT JOIN cfr_reporting_counts crc ON mpnwecarc.cfr = crc.cfr + ), + + filtered_manual_prior_notifications AS ( + SELECT * + FROM manual_prior_notifications_with_extra_columns_and_reporting_count + WHERE + -- Has One Or More Reportings + ( + :hasOneOrMoreReportings IS NULL + OR (:hasOneOrMoreReportings = TRUE AND reporting_count > 0) + OR (:hasOneOrMoreReportings = FALSE AND reporting_count = 0) + ) + + -- Prior Notification Types + AND (:priorNotificationTypesAsSqlArrayString IS NULL OR prior_notification_type_names && CAST(:priorNotificationTypesAsSqlArrayString AS TEXT[])) + + -- Specy Codes + AND (:specyCodesAsSqlArrayString IS NULL OR specy_codes && CAST(:specyCodesAsSqlArrayString AS TEXT[])) + + -- Trip Gear Codes + AND (:tripGearCodesAsSqlArrayString IS NULL OR trip_gear_codes && CAST(:tripGearCodesAsSqlArrayString AS TEXT[])) + + -- Trip Segment Codes + AND (:tripSegmentCodesAsSqlArrayString IS NULL OR trip_segment_codes && CAST(:tripSegmentCodesAsSqlArrayString AS TEXT[])) + ) + + SELECT * + FROM filtered_manual_prior_notifications + """, + nativeQuery = true, + ) + fun findAll( + flagStates: List, + hasOneOrMoreReportings: Boolean?, + lastControlledAfter: String?, + lastControlledBefore: String?, + portLocodes: List, + priorNotificationTypesAsSqlArrayString: String?, + searchQuery: String?, + specyCodesAsSqlArrayString: String?, + tripGearCodesAsSqlArrayString: String?, + tripSegmentCodesAsSqlArrayString: String?, + willArriveAfter: String, + willArriveBefore: String, + ): List + + @Query( + """ + SELECT * + FROM manual_prior_notifications + WHERE report_id = :reportId + """, + nativeQuery = true, + ) + fun findByReportId(reportId: String): ManualPriorNotificationEntity? + + fun save(entity: ManualPriorNotificationEntity): ManualPriorNotificationEntity +} diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/exceptions/BackendRequestErrorCode.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/exceptions/BackendRequestErrorCode.kt new file mode 100644 index 0000000000..2a04cc8e1a --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/exceptions/BackendRequestErrorCode.kt @@ -0,0 +1,14 @@ +package fr.gouv.cnsp.monitorfish.infrastructure.exceptions + +/** + * Infrastructure error code thrown when the request is invalid. + * + * It's most likely a Frontend bug. But it may also be a Backend bug. + * + * ## Examples + * - Request data inconsistency that can't be type-checked with a `DataInput` and throws deeper in the code. + * + * ## ⚠️ Important + * **Don't forget to mirror any update here in the corresponding Frontend enum.** + */ +enum class BackendRequestErrorCode diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/exceptions/BackendRequestException.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/exceptions/BackendRequestException.kt new file mode 100644 index 0000000000..75756d3c19 --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/exceptions/BackendRequestException.kt @@ -0,0 +1,24 @@ +package fr.gouv.cnsp.monitorfish.infrastructure.exceptions + +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +/** + * Infrastructure exception to throw when the request is invalid. + * + * It's most likely a Frontend bug. But it may also be a Backend bug. + * + * ## Examples + * - Request data inconsistency that can't be type-checked with a `DataInput` and throws deeper in the code. + */ +open class BackendRequestException( + val code: BackendRequestErrorCode, + final override val message: String? = null, + val data: Any? = null, +) : Throwable(code.name) { + private val logger: Logger = LoggerFactory.getLogger(BackendRequestException::class.java) + + init { + logger.warn("$code: ${message ?: "No message."}") + } +} diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/utils/ZonedDateTimeDeserializer.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/utils/ZonedDateTimeDeserializer.kt new file mode 100644 index 0000000000..46a5963e70 --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/utils/ZonedDateTimeDeserializer.kt @@ -0,0 +1,13 @@ +package fr.gouv.cnsp.monitorfish.utils + +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JsonDeserializer +import java.time.ZoneOffset +import java.time.ZonedDateTime + +class ZonedDateTimeDeserializer : JsonDeserializer() { + override fun deserialize(jsonParser: JsonParser, ctxt: DeserializationContext): ZonedDateTime { + return ZonedDateTime.parse(jsonParser.text).withZoneSameInstant(ZoneOffset.UTC) + } +} diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/utils/ZonedDateTimeSerializer.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/utils/ZonedDateTimeSerializer.kt new file mode 100644 index 0000000000..7bfc42f739 --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/utils/ZonedDateTimeSerializer.kt @@ -0,0 +1,20 @@ +package fr.gouv.cnsp.monitorfish.utils + +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.databind.JsonSerializer +import com.fasterxml.jackson.databind.SerializerProvider +import java.time.ZoneOffset +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter + +class ZonedDateTimeSerializer : JsonSerializer() { + override fun serialize( + value: ZonedDateTime, + jsonGenerator: JsonGenerator, + serializerProvider: SerializerProvider, + ) { + jsonGenerator.writeString( + value.withZoneSameInstant(ZoneOffset.UTC).format(DateTimeFormatter.ISO_INSTANT), + ) + } +} diff --git a/backend/src/main/resources/db/migration/internal/V0.256__Update_vessels_table.sql b/backend/src/main/resources/db/migration/internal/V0.256__Update_vessels_table.sql index 8646d9665c..184e89a4bd 100644 --- a/backend/src/main/resources/db/migration/internal/V0.256__Update_vessels_table.sql +++ b/backend/src/main/resources/db/migration/internal/V0.256__Update_vessels_table.sql @@ -1,3 +1,3 @@ ALTER TABLE public.vessels ADD COLUMN logbook_equipment_status VARCHAR, - ADD COLUMN has_esacapt BOOLEAN NOT NULL DEFAULT false; \ No newline at end of file + ADD COLUMN has_esacapt BOOLEAN NOT NULL DEFAULT false; diff --git a/backend/src/main/resources/db/migration/internal/V0.257__Create_manual_prior_notifications_table.sql b/backend/src/main/resources/db/migration/internal/V0.257__Create_manual_prior_notifications_table.sql new file mode 100644 index 0000000000..7b6f420491 --- /dev/null +++ b/backend/src/main/resources/db/migration/internal/V0.257__Create_manual_prior_notifications_table.sql @@ -0,0 +1,22 @@ +CREATE EXTENSION IF NOT EXISTS pgcrypto; + +CREATE TABLE public.manual_prior_notifications ( + -- Common columns with `logbook_reports` + + report_id VARCHAR(36) PRIMARY KEY DEFAULT gen_random_uuid(), + cfr VARCHAR(12), + vessel_name VARCHAR(100), + flag_state VARCHAR(3), + value JSONB, + trip_gears JSONB, + trip_segments JSONB, + + -- Columns specific to `manual_prior_notifications` + + author_trigram VARCHAR(3) NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL, + did_not_fish_after_zero_notice BOOLEAN, + note TEXT, + sent_at TIMESTAMP WITH TIME ZONE NOT NULL, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL +); diff --git a/backend/src/main/resources/db/testdata/V666.11__Insert_risk_factors.sql b/backend/src/main/resources/db/testdata/V666.11__Insert_dummy_risk_factors.sql similarity index 70% rename from backend/src/main/resources/db/testdata/V666.11__Insert_risk_factors.sql rename to backend/src/main/resources/db/testdata/V666.11__Insert_dummy_risk_factors.sql index 0876a22372..fde60fb433 100644 --- a/backend/src/main/resources/db/testdata/V666.11__Insert_risk_factors.sql +++ b/backend/src/main/resources/db/testdata/V666.11__Insert_dummy_risk_factors.sql @@ -10,3 +10,7 @@ INSERT INTO risk_factors (cfr, control_priority_level, control_rate_risk_factor, INSERT INTO risk_factors (cfr, control_priority_level, control_rate_risk_factor, departure_datetime_utc, detectability_risk_factor, external_immatriculation, impact_risk_factor, infraction_rate_risk_factor, infraction_score, ircs, last_control_datetime_utc, last_control_infraction, last_logbook_message_datetime_utc, number_controls_last_3_years, number_controls_last_5_years, number_gear_seizures_last_5_years, number_infractions_last_5_years, number_recent_controls, number_species_seizures_last_5_years, number_vessel_seizures_last_5_years, post_control_comments, probability_risk_factor, risk_factor, segment_highest_impact, segment_highest_priority, segments, total_weight_onboard, trip_number, vessel_id, gear_onboard, species_onboard) VALUES ('CFR109', 4, 5, '2023-12-31 14:00:00', 5, 'EXTIMM109', 4, 3, NULL, 'IRCS109', '2024-02-01 00:00:00', true, NOW(), 0, 0, 4, 5, 0, 3, 2, '', 4, 4, 'NWW10', 'PEL 03', '{"NWW10", "PEL 03"}', 12345.67, 123109, 109, '[]', '[]'); INSERT INTO risk_factors (cfr, control_priority_level, control_rate_risk_factor, departure_datetime_utc, detectability_risk_factor, external_immatriculation, impact_risk_factor, infraction_rate_risk_factor, infraction_score, ircs, last_control_datetime_utc, last_control_infraction, last_logbook_message_datetime_utc, number_controls_last_3_years, number_controls_last_5_years, number_gear_seizures_last_5_years, number_infractions_last_5_years, number_recent_controls, number_species_seizures_last_5_years, number_vessel_seizures_last_5_years, post_control_comments, probability_risk_factor, risk_factor, segment_highest_impact, segment_highest_priority, segments, total_weight_onboard, trip_number, vessel_id, gear_onboard, species_onboard) VALUES ('CFR109', 4, 5, '2024-03-31 14:00:00', 5, 'EXTIMM109', 4, 3, NULL, 'IRCS109', '2024-04-01 00:00:00', true, NOW(), 0, 0, 4, 5, 0, 3, 2, '', 4, 4, 'NWW10', 'PEL 03', '{"NWW10", "PEL 03"}', 12345.67, 123109, 109, '[]', '[]'); + +INSERT INTO risk_factors (cfr, control_priority_level, control_rate_risk_factor, departure_datetime_utc, detectability_risk_factor, external_immatriculation, impact_risk_factor, infraction_rate_risk_factor, infraction_score, ircs, last_control_datetime_utc, last_control_infraction, last_logbook_message_datetime_utc, number_controls_last_3_years, number_controls_last_5_years, number_gear_seizures_last_5_years, number_infractions_last_5_years, number_recent_controls, number_species_seizures_last_5_years, number_vessel_seizures_last_5_years, post_control_comments, probability_risk_factor, risk_factor, segment_highest_impact, segment_highest_priority, segments, total_weight_onboard, trip_number, vessel_id, gear_onboard, species_onboard) VALUES ('CFR115', 4, 5, NOW() - INTERVAL '1 day', 5, 'EXTIMM115', 4, 3, NULL, 'IRCS115', '2024-05-01 00:00:00', true, NOW(), 0, 0, 4, 5, 0, 3, 2, '', 4, 2.5, 'NWW10', 'PEL 03', '{"NWW10", "PEL 03"}', 12345.67, 123102, 115, '[{"gear":"OTB","mesh":70,"dimensions":45}]', '[{"gear":"OTB","faoZone":"27.8.b","species":"BLI","weight":13.46},{"gear":"OTB","faoZone":"27.8.c","species":"HKE","weight":235.6},{"gear":"OTB","faoZone":"27.8.b","species":"HKE","weight":235.6}]'); + +INSERT INTO risk_factors (cfr, control_priority_level, control_rate_risk_factor, departure_datetime_utc, detectability_risk_factor, external_immatriculation, impact_risk_factor, infraction_rate_risk_factor, infraction_score, ircs, last_control_datetime_utc, last_control_infraction, last_logbook_message_datetime_utc, number_controls_last_3_years, number_controls_last_5_years, number_gear_seizures_last_5_years, number_infractions_last_5_years, number_recent_controls, number_species_seizures_last_5_years, number_vessel_seizures_last_5_years, post_control_comments, probability_risk_factor, risk_factor, segment_highest_impact, segment_highest_priority, segments, total_weight_onboard, trip_number, vessel_id, gear_onboard, species_onboard) VALUES ('CFR117', 4, 5, NOW() - INTERVAL '1 day', 1.8, 'EXTIMM117', 2, 3, NULL, 'IRCS117', '2023-10-15 00:00:00', true, NOW(), 0, 0, 4, 5, 0, 3, 2, '', 3, 2.2, 'NWW10', 'PEL 03', '{"NWW10", "PEL 03"}', 12345.67, 123102, 117, '[{"gear":"OTB","mesh":70,"dimensions":45}]', '[{"gear":"OTB","faoZone":"27.8.b","species":"BLI","weight":13.46},{"gear":"OTB","faoZone":"27.8.c","species":"HKE","weight":235.6},{"gear":"OTB","faoZone":"27.8.b","species":"HKE","weight":235.6}]'); diff --git a/backend/src/main/resources/db/testdata/V666.19__Insert_dummy_reportings.sql b/backend/src/main/resources/db/testdata/V666.19.0__Insert_dummy_reportings.sql similarity index 99% rename from backend/src/main/resources/db/testdata/V666.19__Insert_dummy_reportings.sql rename to backend/src/main/resources/db/testdata/V666.19.0__Insert_dummy_reportings.sql index cf46cab9bf..60a115abe8 100644 --- a/backend/src/main/resources/db/testdata/V666.19__Insert_dummy_reportings.sql +++ b/backend/src/main/resources/db/testdata/V666.19.0__Insert_dummy_reportings.sql @@ -55,7 +55,7 @@ VALUES ('ALERT', 'MARIAGE ÎLE HASARD', 'ABC000180832', 'VP374069', 'CG1312', 'I false, ('{' || '"reportingActor": "UNIT",' || '"controlUnitId": 10012,' || - '"authorTrigram": "",' || + '"authorTrigram": "LTH",' || '"authorContact": "Jean Bon (0600000000)",' || '"title": "Pêche sans VMS ni JPE",' || '"description": "Pêche thon rouge sans VMS détecté ni JPE",' || diff --git a/backend/src/main/resources/db/testdata/V666.19.1__Insert_more_dummy_reportings.sql b/backend/src/main/resources/db/testdata/V666.19.1__Insert_more_dummy_reportings.sql new file mode 100644 index 0000000000..d6aa1c4f17 --- /dev/null +++ b/backend/src/main/resources/db/testdata/V666.19.1__Insert_more_dummy_reportings.sql @@ -0,0 +1,12 @@ +-- /!\ This file is automatically generated by a local script. +-- Do NOT update it directly, update the associated .jsonc file in /backend/src/main/resources/db/testdata/json/. + +INSERT INTO reportings (id, archived, creation_date, deleted, external_reference_number, flag_state, internal_reference_number, ircs, latitude, longitude, type, validation_date, value, vessel_id, vessel_identifier, vessel_name) VALUES (9, false, NOW() AT TIME ZONE 'UTC' - INTERVAL '10 days', false, 'EXTIMM101', 'FR', 'CFR101', 'IRCS101', NULL, NULL, 'INFRACTION_SUSPICION', NULL, '{"authorContact":"Jean Bon (0623456789)","authorTrigram":"LTH","controlUnitId":10012,"description":"Une description d''infraction.","dml":"DML 29","natinfCode":27689,"reportingActor":"OPS","seaFront":"NAMO","title":"Suspicion d''infraction 9","type":"INFRACTION_SUSPICION"}', 101, 'INTERNAL_REFERENCE_NUMBER', 'VIVA ESPANA'); + +INSERT INTO reportings (id, archived, creation_date, deleted, external_reference_number, flag_state, internal_reference_number, ircs, latitude, longitude, type, validation_date, value, vessel_id, vessel_identifier, vessel_name) VALUES (10, false, NOW() AT TIME ZONE 'UTC' - INTERVAL '15 days', false, 'EXTIMM101', 'FR', 'CFR101', 'IRCS101', NULL, NULL, 'INFRACTION_SUSPICION', NULL, '{"authorContact":"Jean Bon (0623456789)","authorTrigram":"LTH","controlUnitId":10012,"description":"Une description d''infraction.","dml":"DML 29","natinfCode":27689,"reportingActor":"OPS","seaFront":"NAMO","title":"Suspicion d''infraction 10","type":"INFRACTION_SUSPICION"}', 101, 'INTERNAL_REFERENCE_NUMBER', 'VIVA ESPANA'); + +INSERT INTO reportings (id, archived, creation_date, deleted, external_reference_number, flag_state, internal_reference_number, ircs, latitude, longitude, type, validation_date, value, vessel_id, vessel_identifier, vessel_name) VALUES (11, false, NOW() AT TIME ZONE 'UTC' - INTERVAL '20 days', false, 'EXTIMM115', 'FR', 'CFR115', 'IRCS115', NULL, NULL, 'INFRACTION_SUSPICION', NULL, '{"authorContact":"Jean Bon (0623456789)","authorTrigram":"LTH","controlUnitId":10012,"description":"Une description d''infraction.","dml":"DML 29","natinfCode":27689,"reportingActor":"OPS","seaFront":"NAMO","title":"Suspicion d''infraction 11","type":"INFRACTION_SUSPICION"}', 115, 'INTERNAL_REFERENCE_NUMBER', 'DOS FIN'); + +INSERT INTO reportings (id, archived, creation_date, deleted, external_reference_number, flag_state, internal_reference_number, ircs, latitude, longitude, type, validation_date, value, vessel_id, vessel_identifier, vessel_name) VALUES (12, false, NOW() AT TIME ZONE 'UTC' - INTERVAL '25 days', false, 'EXTIMM115', 'FR', 'CFR115', 'IRCS115', NULL, NULL, 'INFRACTION_SUSPICION', NULL, '{"authorContact":"Jean Bon (0623456789)","authorTrigram":"LTH","controlUnitId":10012,"description":"Une description d''infraction.","dml":"DML 29","natinfCode":27689,"reportingActor":"OPS","seaFront":"NAMO","title":"Suspicion d''infraction 212","type":"INFRACTION_SUSPICION"}', 115, 'INTERNAL_REFERENCE_NUMBER', 'DOS FIN'); + +SELECT setval('reportings_id_seq', (SELECT MAX(id) FROM reportings)); \ No newline at end of file diff --git a/backend/src/main/resources/db/testdata/V666.2.1__Insert_more_dummy_vessels.sql b/backend/src/main/resources/db/testdata/V666.2.1__Insert_more_dummy_vessels.sql index 3eb3dfa19d..2f3ad375e9 100644 --- a/backend/src/main/resources/db/testdata/V666.2.1__Insert_more_dummy_vessels.sql +++ b/backend/src/main/resources/db/testdata/V666.2.1__Insert_more_dummy_vessels.sql @@ -1,7 +1,7 @@ -- /!\ This file is automatically generated by a local script. -- Do NOT update it directly, update the associated .jsonc file in /backend/src/main/resources/db/testdata/json/. -INSERT INTO vessels (id, cfr, mmsi, ircs, external_immatriculation, vessel_name, flag_state, length, under_charter) VALUES (101, 'CFR101', 'MMSI101', 'IRCS101', 'EXTIMM101', 'VIVA ESPANA', 'ES', 15, false); +INSERT INTO vessels (id, cfr, mmsi, ircs, external_immatriculation, vessel_name, flag_state, length, under_charter) VALUES (101, 'CFR101', 'MMSI101', 'IRCS101', 'EXTIMM101', 'VIVA ESPANA', 'ES', 15, true); INSERT INTO vessels (id, cfr, mmsi, ircs, external_immatriculation, vessel_name, flag_state, length, under_charter) VALUES (102, 'CFR102', 'MMSI102', 'IRCS102', 'EXTIMM102', 'LEVE NEDERLAND', 'NL', 20, true); @@ -24,3 +24,19 @@ INSERT INTO vessels (id, cfr, mmsi, ircs, external_immatriculation, vessel_name, INSERT INTO vessels (id, cfr, mmsi, ircs, external_immatriculation, vessel_name, flag_state, length, under_charter) VALUES (110, 'CFR110', 'MMSI110', 'IRCS110', 'EXTIMM110', 'LA MER À BOIRE', 'FR', 12.5, false); INSERT INTO vessels (id, cfr, mmsi, ircs, external_immatriculation, vessel_name, flag_state, length, under_charter) VALUES (111, 'CFR111', 'MMSI111', 'IRCS111', 'EXTIMM111', 'LE MARIN D''EAU DOUCE', 'FR', 9.5, false); + +INSERT INTO vessels (id, cfr, mmsi, ircs, external_immatriculation, vessel_name, flag_state, length, under_charter) VALUES (112, 'CFR112', 'MMSI112', 'IRCS112', 'EXTIMM112', 'POISSON PAS NET', 'FR', 7.3, false); + +INSERT INTO vessels (id, cfr, mmsi, ircs, external_immatriculation, vessel_name, flag_state, length, under_charter) VALUES (113, 'CFR113', 'MMSI113', 'IRCS113', 'EXTIMM113', 'IN-ARÊTE-ABLE', 'FR', 8.5, false); + +INSERT INTO vessels (id, cfr, mmsi, ircs, external_immatriculation, vessel_name, flag_state, length, under_charter) VALUES (115, 'CFR115', 'MMSI115', 'IRCS115', 'EXTIMM115', 'DOS FIN', 'BE', 9.2, true); + +INSERT INTO vessels (id, cfr, mmsi, ircs, external_immatriculation, vessel_name, flag_state, length, under_charter) VALUES (116, 'CFR116', 'MMSI116', 'IRCS116', 'EXTIMM116', 'NAVIRE RENOMMÉ (NOUVEAU NOM)', 'FR', 11, false); + +INSERT INTO vessels (id, cfr, mmsi, ircs, external_immatriculation, vessel_name, flag_state, length, under_charter) VALUES (117, 'CFR117', 'MMSI117', 'IRCS117', 'EXTIMM117', 'QUEUE DE POISSON', 'FR', 10.9, false); + +INSERT INTO vessels (id, cfr, mmsi, ircs, external_immatriculation, vessel_name, flag_state, length, under_charter) VALUES (118, 'CFR118', 'MMSI118', 'IRCS118', 'EXTIMM118', 'GOUJON BOUGON', 'FR', 11.2, false); + +INSERT INTO vessels (id, cfr, mmsi, ircs, external_immatriculation, vessel_name, flag_state, length, under_charter) VALUES (119, 'CFR119', 'MMSI119', 'IRCS119', 'EXTIMM119', 'PAGEOT JO', 'FR', 11.1, false); + +INSERT INTO vessels (id, cfr, mmsi, ircs, external_immatriculation, vessel_name, flag_state, length, under_charter) VALUES (120, 'CFR120', 'MMSI120', 'IRCS120', 'EXTIMM120', 'VIVA L''ITALIA', 'IT', 11.1, false); diff --git a/backend/src/main/resources/db/testdata/V666.5.2__Insert_dummy_manual_prior_notifications.sql b/backend/src/main/resources/db/testdata/V666.5.2__Insert_dummy_manual_prior_notifications.sql new file mode 100644 index 0000000000..1dd1c325fc --- /dev/null +++ b/backend/src/main/resources/db/testdata/V666.5.2__Insert_dummy_manual_prior_notifications.sql @@ -0,0 +1,29 @@ +-- /!\ This file is automatically generated by a local script. +-- Do NOT update it directly, update the associated .jsonc file in /backend/src/main/resources/db/testdata/json/. + +INSERT INTO manual_prior_notifications (report_id, author_trigram, cfr, created_at, did_not_fish_after_zero_notice, flag_state, note, sent_at, trip_gears, trip_segments, updated_at, vessel_name, value) VALUES ('00000000-0000-4000-0000-000000000001', 'ABC', 'CFR112', NOW() AT TIME ZONE 'UTC' - INTERVAL '15 minutes', false, 'FRA', NULL, NOW() AT TIME ZONE 'UTC' - INTERVAL '30 minutes', '[{"gear":"LNP"}]', '[{"segment":"NWW09","segmentName":"Lignes"}]', NOW() AT TIME ZONE 'UTC' - INTERVAL '15 minutes', 'POISSON PAS NET', '{"catchOnboard":[{"weight":72,"nbFish":null,"species":"SOS"}],"catchToLand":[{"weight":72,"nbFish":null,"species":"SOS"}],"faoZone":"21.1.A","pnoTypes":[{"pnoTypeName":"Préavis type A","minimumNotificationPeriod":4,"hasDesignatedPorts":false}],"port":"FRVNE","predictedArrivalDatetimeUtc":null,"predictedLandingDatetimeUtc":null,"purpose":"LAN","tripStartDate":null}'); +UPDATE manual_prior_notifications SET value = JSONB_SET(value, '{predictedArrivalDatetimeUtc}', TO_JSONB(TO_CHAR(NOW() AT TIME ZONE 'UTC' + INTERVAL '3 hours', 'YYYY-MM-DD"T"HH24:MI:SS"Z"')), true) WHERE report_id = '00000000-0000-4000-0000-000000000001'; +UPDATE manual_prior_notifications SET value = JSONB_SET(value, '{predictedLandingDatetimeUtc}', TO_JSONB(TO_CHAR(NOW() AT TIME ZONE 'UTC' + INTERVAL '3.5 hours', 'YYYY-MM-DD"T"HH24:MI:SS"Z"')), true) WHERE report_id = '00000000-0000-4000-0000-000000000001'; +UPDATE manual_prior_notifications SET value = JSONB_SET(value, '{tripStartDate}', TO_JSONB(TO_CHAR(NOW() AT TIME ZONE 'UTC' - INTERVAL '10 hours', 'YYYY-MM-DD"T"HH24:MI:SS"Z"')), true) WHERE report_id = '00000000-0000-4000-0000-000000000001'; + +INSERT INTO manual_prior_notifications (report_id, author_trigram, cfr, created_at, did_not_fish_after_zero_notice, flag_state, note, sent_at, trip_gears, trip_segments, updated_at, vessel_name, value) VALUES ('00000000-0000-4000-0000-000000000002', 'ABC', 'CFR115', NOW() AT TIME ZONE 'UTC' - INTERVAL '15 minutes', false, 'BEL', NULL, NOW() AT TIME ZONE 'UTC' - INTERVAL '30 minutes', '[{"gear":"TB"},{"gear":"TBS"}]', '[{"segment":"NWW03","segmentName":"Chalut de fond en eau profonde"},{"segment":"NWW05","segmentName":"Chalut à perche"}]', NOW() AT TIME ZONE 'UTC' - INTERVAL '15 minutes', 'DOS FIN', '{"catchOnboard":[{"weight":300,"nbFish":10,"species":"BF1"},{"weight":100,"nbFish":20,"species":"BF3"},{"weight":400,"nbFish":80,"species":"SWO"},{"weight":600,"nbFish":null,"species":"BFT"},{"weight":200,"nbFish":25,"species":"BF2"}],"catchToLand":[{"weight":600,"nbFish":null,"species":"BFT"},{"weight":300,"nbFish":10,"species":"BF1"},{"weight":200,"nbFish":25,"species":"BF2"},{"weight":100,"nbFish":20,"species":"BF3"},{"weight":400,"nbFish":80,"species":"SWO"}],"faoZone":"21.1.B","pnoTypes":[{"pnoTypeName":"Préavis type B","minimumNotificationPeriod":4,"hasDesignatedPorts":false},{"pnoTypeName":"Préavis type C","minimumNotificationPeriod":8,"hasDesignatedPorts":true}],"port":"FRVNE","predictedArrivalDatetimeUtc":null,"predictedLandingDatetimeUtc":null,"purpose":"LAN","tripStartDate":null}'); +UPDATE manual_prior_notifications SET value = JSONB_SET(value, '{predictedArrivalDatetimeUtc}', TO_JSONB(TO_CHAR(NOW() AT TIME ZONE 'UTC' + INTERVAL '3 hours', 'YYYY-MM-DD"T"HH24:MI:SS"Z"')), true) WHERE report_id = '00000000-0000-4000-0000-000000000002'; +UPDATE manual_prior_notifications SET value = JSONB_SET(value, '{predictedLandingDatetimeUtc}', TO_JSONB(TO_CHAR(NOW() AT TIME ZONE 'UTC' + INTERVAL '4 hours', 'YYYY-MM-DD"T"HH24:MI:SS"Z"')), true) WHERE report_id = '00000000-0000-4000-0000-000000000002'; +UPDATE manual_prior_notifications SET value = JSONB_SET(value, '{tripStartDate}', TO_JSONB(TO_CHAR(NOW() AT TIME ZONE 'UTC' - INTERVAL '10 hours', 'YYYY-MM-DD"T"HH24:MI:SS"Z"')), true) WHERE report_id = '00000000-0000-4000-0000-000000000002'; + +INSERT INTO manual_prior_notifications (report_id, author_trigram, cfr, created_at, did_not_fish_after_zero_notice, flag_state, note, sent_at, trip_gears, trip_segments, updated_at, vessel_name, value) VALUES ('00000000-0000-4000-0000-000000000003', 'ABC', 'CFR117', NOW() AT TIME ZONE 'UTC' - INTERVAL '50 minutes', true, 'FRA', NULL, NOW() AT TIME ZONE 'UTC' - INTERVAL '30 minutes', '[{"gear":"TBS"}]', '[{"segment":"MED01","segmentName":"All Trawls 1"},{"segment":"MED02","segmentName":"All Trawls 2"}]', NOW() AT TIME ZONE 'UTC' - INTERVAL '5 minutes', 'QUEUE DE POISSON', '{"catchOnboard":[{"weight":0,"nbFish":null,"species":"BIB"}],"catchToLand":[{"weight":0,"nbFish":null,"species":"BIB"}],"faoZone":"21.1.C","pnoTypes":[{"pnoTypeName":"Préavis type E","minimumNotificationPeriod":4,"hasDesignatedPorts":false}],"port":"FRMRS","predictedArrivalDatetimeUtc":null,"predictedLandingDatetimeUtc":null,"purpose":"LAN","tripStartDate":null}'); +UPDATE manual_prior_notifications SET value = JSONB_SET(value, '{predictedArrivalDatetimeUtc}', TO_JSONB(TO_CHAR(NOW() AT TIME ZONE 'UTC' + INTERVAL '3 hours', 'YYYY-MM-DD"T"HH24:MI:SS"Z"')), true) WHERE report_id = '00000000-0000-4000-0000-000000000003'; +UPDATE manual_prior_notifications SET value = JSONB_SET(value, '{predictedLandingDatetimeUtc}', TO_JSONB(TO_CHAR(NOW() AT TIME ZONE 'UTC' + INTERVAL '3.5 hours', 'YYYY-MM-DD"T"HH24:MI:SS"Z"')), true) WHERE report_id = '00000000-0000-4000-0000-000000000003'; +UPDATE manual_prior_notifications SET value = JSONB_SET(value, '{tripStartDate}', TO_JSONB(TO_CHAR(NOW() AT TIME ZONE 'UTC' - INTERVAL '10 hours', 'YYYY-MM-DD"T"HH24:MI:SS"Z"')), true) WHERE report_id = '00000000-0000-4000-0000-000000000003'; + +INSERT INTO manual_prior_notifications (report_id, author_trigram, cfr, created_at, did_not_fish_after_zero_notice, flag_state, note, sent_at, trip_gears, trip_segments, updated_at, vessel_name, value) VALUES ('00000000-0000-4000-0000-000000000004', 'ABC', 'CFR118', '2023-01-01T08:45:00', true, 'FRA', NULL, '2023-01-01T08:30:00', '[{"gear":"OTB"}]', '[{"segment":"MED01","segmentName":"All Trawls 1"}]', '2023-01-01T08:45:00', 'GOUJON BOUGON', '{"catchOnboard":[{"weight":0,"nbFish":null,"species":"BIB"}],"catchToLand":[{"weight":0,"nbFish":null,"species":"BIB"}],"faoZone":"21.1.C","pnoTypes":[{"pnoTypeName":"Préavis type F","minimumNotificationPeriod":4,"hasDesignatedPorts":false}],"port":"FRNCE","predictedArrivalDatetimeUtc":"2023-01-01T10:00:00Z","predictedLandingDatetimeUtc":"2023-01-01T10:30:00Z","purpose":"LAN","tripStartDate":"2023-01-01T08:00:00Z"}'); + +INSERT INTO manual_prior_notifications (report_id, author_trigram, cfr, created_at, did_not_fish_after_zero_notice, flag_state, note, sent_at, trip_gears, trip_segments, updated_at, vessel_name, value) VALUES ('00000000-0000-4000-0000-000000000005', 'ABC', 'CFR116', NOW() AT TIME ZONE 'UTC' - INTERVAL '15 minutes', false, 'FRA', 'Pêche abandonnée pour cause de météo défavorable.', NOW() AT TIME ZONE 'UTC' - INTERVAL '30 minutes', '[{"gear":"OTT"}]', '[{"segment":"MED01","segmentName":"All Trawls 1"}]', NOW() AT TIME ZONE 'UTC' - INTERVAL '15 minutes', 'NAVIRE RENOMMÉ (ANCIEN NOM)', '{"catchOnboard":[{"weight":24.3,"nbFish":null,"species":"ALV"}],"catchToLand":[{"weight":24.3,"nbFish":null,"species":"ALV"}],"faoZone":"21.1.C","pnoTypes":[{"pnoTypeName":"Préavis type C","minimumNotificationPeriod":8,"hasDesignatedPorts":true}],"port":"FRMRS","predictedArrivalDatetimeUtc":null,"predictedLandingDatetimeUtc":null,"purpose":"LAN","tripStartDate":null}'); +UPDATE manual_prior_notifications SET value = JSONB_SET(value, '{predictedArrivalDatetimeUtc}', TO_JSONB(TO_CHAR(NOW() AT TIME ZONE 'UTC' + INTERVAL '3 hours', 'YYYY-MM-DD"T"HH24:MI:SS"Z"')), true) WHERE report_id = '00000000-0000-4000-0000-000000000005'; +UPDATE manual_prior_notifications SET value = JSONB_SET(value, '{predictedLandingDatetimeUtc}', TO_JSONB(TO_CHAR(NOW() AT TIME ZONE 'UTC' + INTERVAL '3.5 hours', 'YYYY-MM-DD"T"HH24:MI:SS"Z"')), true) WHERE report_id = '00000000-0000-4000-0000-000000000005'; +UPDATE manual_prior_notifications SET value = JSONB_SET(value, '{tripStartDate}', TO_JSONB(TO_CHAR(NOW() AT TIME ZONE 'UTC' - INTERVAL '10 hours', 'YYYY-MM-DD"T"HH24:MI:SS"Z"')), true) WHERE report_id = '00000000-0000-4000-0000-000000000005'; + +INSERT INTO manual_prior_notifications (report_id, author_trigram, cfr, created_at, did_not_fish_after_zero_notice, flag_state, note, sent_at, trip_gears, trip_segments, updated_at, vessel_name, value) VALUES ('00000000-0000-4000-0000-000000000006', 'ABC', 'CFR120', NOW() AT TIME ZONE 'UTC' - INTERVAL '15 minutes', false, 'ITA', 'Pêche abandonnée pour cause de météo défavorable.', NOW() AT TIME ZONE 'UTC' - INTERVAL '30 minutes', '[{"gear":"OTT"}]', '[{"segment":"MED01","segmentName":"All Trawls 1"}]', NOW() AT TIME ZONE 'UTC' - INTERVAL '15 minutes', 'VIVA L''ITALIA', '{"catchOnboard":[{"weight":0,"nbFish":null,"species":"AGS"}],"catchToLand":[{"weight":0,"nbFish":null,"species":"AGS"}],"faoZone":"21.1.C","pnoTypes":[{"pnoTypeName":"Préavis type C","minimumNotificationPeriod":8,"hasDesignatedPorts":true}],"port":"FRMRS","predictedArrivalDatetimeUtc":null,"predictedLandingDatetimeUtc":null,"purpose":"LAN","tripStartDate":null}'); +UPDATE manual_prior_notifications SET value = JSONB_SET(value, '{predictedArrivalDatetimeUtc}', TO_JSONB(TO_CHAR(NOW() AT TIME ZONE 'UTC' + INTERVAL '3 hours', 'YYYY-MM-DD"T"HH24:MI:SS"Z"')), true) WHERE report_id = '00000000-0000-4000-0000-000000000006'; +UPDATE manual_prior_notifications SET value = JSONB_SET(value, '{predictedLandingDatetimeUtc}', TO_JSONB(TO_CHAR(NOW() AT TIME ZONE 'UTC' + INTERVAL '3 hours', 'YYYY-MM-DD"T"HH24:MI:SS"Z"')), true) WHERE report_id = '00000000-0000-4000-0000-000000000006'; +UPDATE manual_prior_notifications SET value = JSONB_SET(value, '{tripStartDate}', TO_JSONB(TO_CHAR(NOW() AT TIME ZONE 'UTC' - INTERVAL '10 hours', 'YYYY-MM-DD"T"HH24:MI:SS"Z"')), true) WHERE report_id = '00000000-0000-4000-0000-000000000006'; diff --git a/backend/src/main/resources/db/testdata/json/V666.11__Insert_risk_factors.jsonc b/backend/src/main/resources/db/testdata/json/V666.11__Insert_dummy_risk_factors.jsonc similarity index 68% rename from backend/src/main/resources/db/testdata/json/V666.11__Insert_risk_factors.jsonc rename to backend/src/main/resources/db/testdata/json/V666.11__Insert_dummy_risk_factors.jsonc index aba1ae3867..e048c12c79 100644 --- a/backend/src/main/resources/db/testdata/json/V666.11__Insert_risk_factors.jsonc +++ b/backend/src/main/resources/db/testdata/json/V666.11__Insert_dummy_risk_factors.jsonc @@ -254,6 +254,128 @@ "vessel_id": 109, "gear_onboard:jsonb": [], "species_onboard:jsonb": [] + }, + + // - Vessel: DOS FIN + // - Last control date: 2024-05-01 + { + "cfr": "CFR115", + "control_priority_level": 4, + "control_rate_risk_factor": 5, + "departure_datetime_utc:sql": "NOW() - INTERVAL '1 day'", + "detectability_risk_factor": 5, + "external_immatriculation": "EXTIMM115", + "impact_risk_factor": 4, + "infraction_rate_risk_factor": 3, + "infraction_score": null, + "ircs": "IRCS115", + "last_control_datetime_utc": "2024-05-01 00:00:00", + "last_control_infraction": true, + "last_logbook_message_datetime_utc:sql": "NOW()", + "number_controls_last_3_years": 0, + "number_controls_last_5_years": 0, + "number_gear_seizures_last_5_years": 4, + "number_infractions_last_5_years": 5, + "number_recent_controls": 0, + "number_species_seizures_last_5_years": 3, + "number_vessel_seizures_last_5_years": 2, + "post_control_comments": "", + "probability_risk_factor": 4, + "risk_factor": 2.5, + "segment_highest_impact": "NWW10", + "segment_highest_priority": "PEL 03", + "segments": ["NWW10", "PEL 03"], + "total_weight_onboard": 12345.67, + "trip_number": 123102, + "vessel_id": 115, + "gear_onboard:jsonb": [ + { + "gear": "OTB", + "mesh": 70.0, + "dimensions": 45.0 + } + ], + "species_onboard:jsonb": [ + { + "gear": "OTB", + "faoZone": "27.8.b", + "species": "BLI", + "weight": 13.46 + }, + { + "gear": "OTB", + "faoZone": "27.8.c", + "species": "HKE", + "weight": 235.6 + }, + { + "gear": "OTB", + "faoZone": "27.8.b", + "species": "HKE", + "weight": 235.6 + } + ] + }, + + // - Vessel: QUEUE DE POISSON + // - Last control date: 2023-10-15 + { + "cfr": "CFR117", + "control_priority_level": 4, + "control_rate_risk_factor": 5, + "departure_datetime_utc:sql": "NOW() - INTERVAL '1 day'", + "detectability_risk_factor": 1.8, + "external_immatriculation": "EXTIMM117", + "impact_risk_factor": 2, + "infraction_rate_risk_factor": 3, + "infraction_score": null, + "ircs": "IRCS117", + "last_control_datetime_utc": "2023-10-15 00:00:00", + "last_control_infraction": true, + "last_logbook_message_datetime_utc:sql": "NOW()", + "number_controls_last_3_years": 0, + "number_controls_last_5_years": 0, + "number_gear_seizures_last_5_years": 4, + "number_infractions_last_5_years": 5, + "number_recent_controls": 0, + "number_species_seizures_last_5_years": 3, + "number_vessel_seizures_last_5_years": 2, + "post_control_comments": "", + "probability_risk_factor": 3, + "risk_factor": 2.2, + "segment_highest_impact": "NWW10", + "segment_highest_priority": "PEL 03", + "segments": ["NWW10", "PEL 03"], + "total_weight_onboard": 12345.67, + "trip_number": 123102, + "vessel_id": 117, + "gear_onboard:jsonb": [ + { + "gear": "OTB", + "mesh": 70.0, + "dimensions": 45.0 + } + ], + "species_onboard:jsonb": [ + { + "gear": "OTB", + "faoZone": "27.8.b", + "species": "BLI", + "weight": 13.46 + }, + { + "gear": "OTB", + "faoZone": "27.8.c", + "species": "HKE", + "weight": 235.6 + }, + { + "gear": "OTB", + "faoZone": "27.8.b", + "species": "HKE", + "weight": 235.6 + } + ] } ] } diff --git a/backend/src/main/resources/db/testdata/json/V666.19.1__Insert_more_dummy_reportings.jsonc b/backend/src/main/resources/db/testdata/json/V666.19.1__Insert_more_dummy_reportings.jsonc new file mode 100644 index 0000000000..e3398f94fe --- /dev/null +++ b/backend/src/main/resources/db/testdata/json/V666.19.1__Insert_more_dummy_reportings.jsonc @@ -0,0 +1,129 @@ +[ + { + "table": "reportings", + "afterAll": "SELECT setval('reportings_id_seq', (SELECT MAX(id) FROM reportings));", + "data": [ + // - Vessel: VIVA ESPANA + // - With 2 reportings + { + "id": 9, + "archived": false, + "creation_date:sql": "NOW() AT TIME ZONE 'UTC' - INTERVAL '10 days'", + "deleted": false, + "external_reference_number": "EXTIMM101", + "flag_state": "FR", + "internal_reference_number": "CFR101", + "ircs": "IRCS101", + "latitude": null, + "longitude": null, + "type": "INFRACTION_SUSPICION", + "validation_date": null, + "value:jsonb": { + "authorContact": "Jean Bon (0623456789)", + "authorTrigram": "LTH", + "controlUnitId": 10012, + "description": "Une description d'infraction.", + "dml": "DML 29", + "natinfCode": 27689, + "reportingActor": "OPS", + "seaFront": "NAMO", + "title": "Suspicion d'infraction 9", + "type": "INFRACTION_SUSPICION" + }, + "vessel_id": 101, + "vessel_identifier": "INTERNAL_REFERENCE_NUMBER", + "vessel_name": "VIVA ESPANA" + }, + { + "id": 10, + "archived": false, + "creation_date:sql": "NOW() AT TIME ZONE 'UTC' - INTERVAL '15 days'", + "deleted": false, + "external_reference_number": "EXTIMM101", + "flag_state": "FR", + "internal_reference_number": "CFR101", + "ircs": "IRCS101", + "latitude": null, + "longitude": null, + "type": "INFRACTION_SUSPICION", + "validation_date": null, + "value:jsonb": { + "authorContact": "Jean Bon (0623456789)", + "authorTrigram": "LTH", + "controlUnitId": 10012, + "description": "Une description d'infraction.", + "dml": "DML 29", + "natinfCode": 27689, + "reportingActor": "OPS", + "seaFront": "NAMO", + "title": "Suspicion d'infraction 10", + "type": "INFRACTION_SUSPICION" + }, + "vessel_id": 101, + "vessel_identifier": "INTERNAL_REFERENCE_NUMBER", + "vessel_name": "VIVA ESPANA" + }, + + // - Vessel: DOS FIN + // - With 2 reportings + { + "id": 11, + "archived": false, + "creation_date:sql": "NOW() AT TIME ZONE 'UTC' - INTERVAL '20 days'", + "deleted": false, + "external_reference_number": "EXTIMM115", + "flag_state": "FR", + "internal_reference_number": "CFR115", + "ircs": "IRCS115", + "latitude": null, + "longitude": null, + "type": "INFRACTION_SUSPICION", + "validation_date": null, + "value:jsonb": { + "authorContact": "Jean Bon (0623456789)", + "authorTrigram": "LTH", + "controlUnitId": 10012, + "description": "Une description d'infraction.", + "dml": "DML 29", + "natinfCode": 27689, + "reportingActor": "OPS", + "seaFront": "NAMO", + "title": "Suspicion d'infraction 11", + "type": "INFRACTION_SUSPICION" + }, + "vessel_id": 115, + "vessel_identifier": "INTERNAL_REFERENCE_NUMBER", + "vessel_name": "DOS FIN" + }, + { + "id": 12, + "archived": false, + "creation_date:sql": "NOW() AT TIME ZONE 'UTC' - INTERVAL '25 days'", + "deleted": false, + "external_reference_number": "EXTIMM115", + "flag_state": "FR", + "internal_reference_number": "CFR115", + "ircs": "IRCS115", + "latitude": null, + "longitude": null, + "type": "INFRACTION_SUSPICION", + "validation_date": null, + "value:jsonb": { + "authorContact": "Jean Bon (0623456789)", + "authorTrigram": "LTH", + "controlUnitId": 10012, + "description": "Une description d'infraction.", + "dml": "DML 29", + "natinfCode": 27689, + "reportingActor": "OPS", + "seaFront": "NAMO", + "title": "Suspicion d'infraction 212", + "type": "INFRACTION_SUSPICION" + }, + "vessel_id": 115, + "vessel_identifier": "INTERNAL_REFERENCE_NUMBER", + "vessel_name": "DOS FIN" + } + ] + } +] diff --git a/backend/src/main/resources/db/testdata/json/V666.2.1__Insert_more_dummy_vessels.jsonc b/backend/src/main/resources/db/testdata/json/V666.2.1__Insert_more_dummy_vessels.jsonc index 3da0f0a5ed..e97f612dbe 100644 --- a/backend/src/main/resources/db/testdata/json/V666.2.1__Insert_more_dummy_vessels.jsonc +++ b/backend/src/main/resources/db/testdata/json/V666.2.1__Insert_more_dummy_vessels.jsonc @@ -4,6 +4,7 @@ "data": [ // - Vessel: VIVA ESPANA // - Flag state: ES + // - Under charter { "id": 101, "cfr": "CFR101", @@ -13,7 +14,7 @@ "vessel_name": "VIVA ESPANA", "flag_state": "ES", "length": 15, - "under_charter": false + "under_charter": true }, // - Vessel: LEVE NEDERLAND @@ -160,6 +161,114 @@ "flag_state": "FR", "length": 9.5, "under_charter": false + }, + + // - Vessel: POISSON PAS NET + { + "id": 112, + "cfr": "CFR112", + "mmsi": "MMSI112", + "ircs": "IRCS112", + "external_immatriculation": "EXTIMM112", + "vessel_name": "POISSON PAS NET", + "flag_state": "FR", + "length": 7.3, + "under_charter": false + }, + + // - Vessel: IN-ARÊTE-ABLE + { + "id": 113, + "cfr": "CFR113", + "mmsi": "MMSI113", + "ircs": "IRCS113", + "external_immatriculation": "EXTIMM113", + "vessel_name": "IN-ARÊTE-ABLE", + "flag_state": "FR", + "length": 8.5, + "under_charter": false + }, + + // - Vessel: DOS FIN + // - Flag state: BE + // - Under charter + { + "id": 115, + "cfr": "CFR115", + "mmsi": "MMSI115", + "ircs": "IRCS115", + "external_immatriculation": "EXTIMM115", + "vessel_name": "DOS FIN", + "flag_state": "BE", + "length": 9.2, + "under_charter": true + }, + + // - Vessel: NAVIRE RENOMMÉ (NOUVEAU NOM) + // - RENAMED VESSEL NAME + { + "id": 116, + "cfr": "CFR116", + "mmsi": "MMSI116", + "ircs": "IRCS116", + "external_immatriculation": "EXTIMM116", + "vessel_name": "NAVIRE RENOMMÉ (NOUVEAU NOM)", + "flag_state": "FR", + "length": 11.0, + "under_charter": false + }, + + // - Vessel: QUEUE DE POISSON + { + "id": 117, + "cfr": "CFR117", + "mmsi": "MMSI117", + "ircs": "IRCS117", + "external_immatriculation": "EXTIMM117", + "vessel_name": "QUEUE DE POISSON", + "flag_state": "FR", + "length": 10.9, + "under_charter": false + }, + + // - Vessel: GOUJON BOUGON + { + "id": 118, + "cfr": "CFR118", + "mmsi": "MMSI118", + "ircs": "IRCS118", + "external_immatriculation": "EXTIMM118", + "vessel_name": "GOUJON BOUGON", + "flag_state": "FR", + "length": 11.2, + "under_charter": false + }, + + // - Vessel: PAGEOT JO + { + "id": 119, + "cfr": "CFR119", + "mmsi": "MMSI119", + "ircs": "IRCS119", + "external_immatriculation": "EXTIMM119", + "vessel_name": "PAGEOT JO", + "flag_state": "FR", + "length": 11.1, + "under_charter": false + }, + + // - Vessel: VIVA L'ITALIA + // - Flag state: IT + { + "id": 120, + "cfr": "CFR120", + "mmsi": "MMSI120", + "ircs": "IRCS120", + "external_immatriculation": "EXTIMM120", + "vessel_name": "VIVA L'ITALIA", + "flag_state": "IT", + "length": 11.1, + "under_charter": false } ] } diff --git a/backend/src/main/resources/db/testdata/json/V666.5.1__Insert_more_pno_logbook_reports.jsonc b/backend/src/main/resources/db/testdata/json/V666.5.1__Insert_more_pno_logbook_reports.jsonc index 89dd6f6c1e..6f985217e4 100644 --- a/backend/src/main/resources/db/testdata/json/V666.5.1__Insert_more_pno_logbook_reports.jsonc +++ b/backend/src/main/resources/db/testdata/json/V666.5.1__Insert_more_pno_logbook_reports.jsonc @@ -27,6 +27,7 @@ "table": "logbook_reports", "data": [ // - Vessel: PHENOMENE + // - With risk factor { "id": 101, "report_id": "FAKE_OPERATION_101", @@ -83,7 +84,7 @@ }, // - Vessel: COURANT MAIN PROFESSEUR - // - With reportings + // - With 1 reporting { "id": 102, "report_id": "FAKE_OPERATION_102", @@ -173,6 +174,10 @@ }, // - Vessel: VIVA ESPANA + // - With 2 reportings + // - With risk factor + // - Under charter + // - Flag state: ES { "id": 104, "report_id": "FAKE_OPERATION_104", @@ -283,6 +288,7 @@ }, // - Vessel: LEVE NEDERLAND + // - Flag state: NL { "id": 105, "report_id": "FAKE_OPERATION_105", @@ -387,9 +393,10 @@ } }, - // - Vessel: DES BARS (Unknown Flag State) + // - Vessel: DES BARS // - With RET // - Aknowledged + // - Flag state: UNKNOWN { "id": 107, "report_id": "FAKE_OPERATION_107", diff --git a/backend/src/main/resources/db/testdata/json/V666.5.2__Insert_dummy_manual_prior_notifications.jsonc b/backend/src/main/resources/db/testdata/json/V666.5.2__Insert_dummy_manual_prior_notifications.jsonc new file mode 100644 index 0000000000..b81a22c4e8 --- /dev/null +++ b/backend/src/main/resources/db/testdata/json/V666.5.2__Insert_dummy_manual_prior_notifications.jsonc @@ -0,0 +1,344 @@ +[ + { + "table": "manual_prior_notifications", + "id": "report_id", + "data": [ + // - Vessel: POISSON PAS NET + { + "report_id": "00000000-0000-4000-0000-000000000001", + "author_trigram": "ABC", + "cfr": "CFR112", + "created_at:sql": "NOW() AT TIME ZONE 'UTC' - INTERVAL '15 minutes'", + "did_not_fish_after_zero_notice": false, + "flag_state": "FRA", + "note": null, + "sent_at:sql": "NOW() AT TIME ZONE 'UTC' - INTERVAL '30 minutes'", + "trip_gears:jsonb": [{ "gear": "LNP" }], + "trip_segments:jsonb": [{ "segment": "NWW09", "segmentName": "Lignes" }], + "updated_at:sql": "NOW() AT TIME ZONE 'UTC' - INTERVAL '15 minutes'", + "vessel_name": "POISSON PAS NET", + "value:jsonb": { + "catchOnboard": [ + { + "weight": 72.0, + "nbFish": null, + "species": "SOS" + } + ], + "catchToLand": [ + { + "weight": 72.0, + "nbFish": null, + "species": "SOS" + } + ], + "faoZone": "21.1.A", + "pnoTypes": [ + { + "pnoTypeName": "Préavis type A", + "minimumNotificationPeriod": 4.0, + "hasDesignatedPorts": false + } + ], + "port": "FRVNE", + "predictedArrivalDatetimeUtc:sql": "TO_CHAR(NOW() AT TIME ZONE 'UTC' + INTERVAL '3 hours', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"')", + "predictedLandingDatetimeUtc:sql": "TO_CHAR(NOW() AT TIME ZONE 'UTC' + INTERVAL '3.5 hours', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"')", + "purpose": "LAN", + "tripStartDate:sql": "TO_CHAR(NOW() AT TIME ZONE 'UTC' - INTERVAL '10 hours', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"')" + } + }, + + // - Vessel: DOS FIN + // - Species: Bluefin tuna (BFT), Swordfish (SWO) + // - With risk factor + // - Under charter + // - With 2 reportings + // - Flag state: BEL + { + "report_id": "00000000-0000-4000-0000-000000000002", + "author_trigram": "ABC", + "cfr": "CFR115", + "created_at:sql": "NOW() AT TIME ZONE 'UTC' - INTERVAL '15 minutes'", + "did_not_fish_after_zero_notice": false, + "flag_state": "BEL", + "note": null, + "sent_at:sql": "NOW() AT TIME ZONE 'UTC' - INTERVAL '30 minutes'", + "trip_gears:jsonb": [{ "gear": "TB" }, { "gear": "TBS" }], + "trip_segments:jsonb": [ + { "segment": "NWW03", "segmentName": "Chalut de fond en eau profonde" }, + { "segment": "NWW05", "segmentName": "Chalut à perche" } + ], + "updated_at:sql": "NOW() AT TIME ZONE 'UTC' - INTERVAL '15 minutes'", + "vessel_name": "DOS FIN", + "value:jsonb": { + // Unsorted on purpose + "catchOnboard": [ + { + "weight": 300.0, + "nbFish": 10, + "species": "BF1" + }, + { + "weight": 100.0, + "nbFish": 20, + "species": "BF3" + }, + { + "weight": 400.0, + "nbFish": 80, + "species": "SWO" + }, + { + "weight": 600.0, + "nbFish": null, + "species": "BFT" + }, + { + "weight": 200.0, + "nbFish": 25, + "species": "BF2" + } + ], + "catchToLand": [ + { + "weight": 600.0, + "nbFish": null, + "species": "BFT" + }, + { + "weight": 300.0, + "nbFish": 10, + "species": "BF1" + }, + { + "weight": 200.0, + "nbFish": 25, + "species": "BF2" + }, + { + "weight": 100.0, + "nbFish": 20, + "species": "BF3" + }, + { + "weight": 400.0, + "nbFish": 80, + "species": "SWO" + } + ], + "faoZone": "21.1.B", + "pnoTypes": [ + { + "pnoTypeName": "Préavis type B", + "minimumNotificationPeriod": 4.0, + "hasDesignatedPorts": false + }, + { + "pnoTypeName": "Préavis type C", + "minimumNotificationPeriod": 8.0, + "hasDesignatedPorts": true + } + ], + "port": "FRVNE", + "predictedArrivalDatetimeUtc:sql": "TO_CHAR(NOW() AT TIME ZONE 'UTC' + INTERVAL '3 hours', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"')", + "predictedLandingDatetimeUtc:sql": "TO_CHAR(NOW() AT TIME ZONE 'UTC' + INTERVAL '4 hours', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"')", + "purpose": "LAN", + "tripStartDate:sql": "TO_CHAR(NOW() AT TIME ZONE 'UTC' - INTERVAL '10 hours', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"')" + } + }, + + // - Vessel: QUEUE DE POISSON + // - Landing date = Arrival date + // - Zero notice + // - Updated + // - Did not fish after zero notice + // - With risk factor + { + "report_id": "00000000-0000-4000-0000-000000000003", + "author_trigram": "ABC", + "cfr": "CFR117", + "created_at:sql": "NOW() AT TIME ZONE 'UTC' - INTERVAL '50 minutes'", + "did_not_fish_after_zero_notice": true, + "flag_state": "FRA", + "note": null, + "sent_at:sql": "NOW() AT TIME ZONE 'UTC' - INTERVAL '30 minutes'", + "trip_gears:jsonb": [{ "gear": "TBS" }], + "trip_segments:jsonb": [ + { "segment": "MED01", "segmentName": "All Trawls 1" }, + { "segment": "MED02", "segmentName": "All Trawls 2" } + ], + "updated_at:sql": "NOW() AT TIME ZONE 'UTC' - INTERVAL '5 minutes'", + "vessel_name": "QUEUE DE POISSON", + "value:jsonb": { + "catchOnboard": [ + { + "weight": 0, + "nbFish": null, + "species": "BIB" + } + ], + "catchToLand": [ + { + "weight": 0, + "nbFish": null, + "species": "BIB" + } + ], + "faoZone": "21.1.C", + "pnoTypes": [ + { + "pnoTypeName": "Préavis type E", + "minimumNotificationPeriod": 4.0, + "hasDesignatedPorts": false + } + ], + "port": "FRMRS", + "predictedArrivalDatetimeUtc:sql": "TO_CHAR(NOW() AT TIME ZONE 'UTC' + INTERVAL '3 hours', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"')", + "predictedLandingDatetimeUtc:sql": "TO_CHAR(NOW() AT TIME ZONE 'UTC' + INTERVAL '3.5 hours', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"')", + "purpose": "LAN", + "tripStartDate:sql": "TO_CHAR(NOW() AT TIME ZONE 'UTC' - INTERVAL '10 hours', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"')" + } + }, + + // - Vessel: GOUJON BOUGON + // - Arrival date: 2023 + { + "report_id": "00000000-0000-4000-0000-000000000004", + "author_trigram": "ABC", + "cfr": "CFR118", + "created_at": "2023-01-01T08:45:00", + "did_not_fish_after_zero_notice": true, + "flag_state": "FRA", + "note": null, + "sent_at": "2023-01-01T08:30:00", + "trip_gears:jsonb": [{ "gear": "OTB" }], + "trip_segments:jsonb": [{ "segment": "MED01", "segmentName": "All Trawls 1" }], + "updated_at": "2023-01-01T08:45:00", + "vessel_name": "GOUJON BOUGON", + "value:jsonb": { + "catchOnboard": [ + { + "weight": 0, + "nbFish": null, + "species": "BIB" + } + ], + "catchToLand": [ + { + "weight": 0, + "nbFish": null, + "species": "BIB" + } + ], + "faoZone": "21.1.C", + "pnoTypes": [ + { + "pnoTypeName": "Préavis type F", + "minimumNotificationPeriod": 4.0, + "hasDesignatedPorts": false + } + ], + "port": "FRNCE", + "predictedArrivalDatetimeUtc": "2023-01-01T10:00:00Z", + "predictedLandingDatetimeUtc": "2023-01-01T10:30:00Z", + "purpose": "LAN", + "tripStartDate": "2023-01-01T08:00:00Z" + } + }, + + // - Vessel: NAVIRE RENOMMÉ (ANCIEN NOM) + // - RENAMED TO: NAVIRE RENOMMÉ (NOUVEAU NOM) + { + "report_id": "00000000-0000-4000-0000-000000000005", + "author_trigram": "ABC", + "cfr": "CFR116", + "created_at:sql": "NOW() AT TIME ZONE 'UTC' - INTERVAL '15 minutes'", + "did_not_fish_after_zero_notice": false, + "flag_state": "FRA", + "note": "Pêche abandonnée pour cause de météo défavorable.", + "sent_at:sql": "NOW() AT TIME ZONE 'UTC' - INTERVAL '30 minutes'", + "trip_gears:jsonb": [{ "gear": "OTT" }], + "trip_segments:jsonb": [{ "segment": "MED01", "segmentName": "All Trawls 1" }], + "updated_at:sql": "NOW() AT TIME ZONE 'UTC' - INTERVAL '15 minutes'", + "vessel_name": "NAVIRE RENOMMÉ (ANCIEN NOM)", + "value:jsonb": { + "catchOnboard": [ + { + "weight": 24.3, + "nbFish": null, + "species": "ALV" + } + ], + "catchToLand": [ + { + "weight": 24.3, + "nbFish": null, + "species": "ALV" + } + ], + "faoZone": "21.1.C", + "pnoTypes": [ + { + "pnoTypeName": "Préavis type C", + "minimumNotificationPeriod": 8.0, + "hasDesignatedPorts": true + } + ], + "port": "FRMRS", + "predictedArrivalDatetimeUtc:sql": "TO_CHAR(NOW() AT TIME ZONE 'UTC' + INTERVAL '3 hours', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"')", + "predictedLandingDatetimeUtc:sql": "TO_CHAR(NOW() AT TIME ZONE 'UTC' + INTERVAL '3.5 hours', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"')", + "purpose": "LAN", + "tripStartDate:sql": "TO_CHAR(NOW() AT TIME ZONE 'UTC' - INTERVAL '10 hours', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"')" + } + }, + + // - Vessel: VIVA L'ITALIA + // - Landing date = Arrival date + // - Zero notice + // - Flag state: ITA + { + "report_id": "00000000-0000-4000-0000-000000000006", + "author_trigram": "ABC", + "cfr": "CFR120", + "created_at:sql": "NOW() AT TIME ZONE 'UTC' - INTERVAL '15 minutes'", + "did_not_fish_after_zero_notice": false, + "flag_state": "ITA", + "note": "Pêche abandonnée pour cause de météo défavorable.", + "sent_at:sql": "NOW() AT TIME ZONE 'UTC' - INTERVAL '30 minutes'", + "trip_gears:jsonb": [{ "gear": "OTT" }], + "trip_segments:jsonb": [{ "segment": "MED01", "segmentName": "All Trawls 1" }], + "updated_at:sql": "NOW() AT TIME ZONE 'UTC' - INTERVAL '15 minutes'", + "vessel_name": "VIVA L'ITALIA", + "value:jsonb": { + "catchOnboard": [ + { + "weight": 0, + "nbFish": null, + "species": "AGS" + } + ], + "catchToLand": [ + { + "weight": 0, + "nbFish": null, + "species": "AGS" + } + ], + "faoZone": "21.1.C", + "pnoTypes": [ + { + "pnoTypeName": "Préavis type C", + "minimumNotificationPeriod": 8.0, + "hasDesignatedPorts": true + } + ], + "port": "FRMRS", + "predictedArrivalDatetimeUtc:sql": "TO_CHAR(NOW() AT TIME ZONE 'UTC' + INTERVAL '3 hours', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"')", + "predictedLandingDatetimeUtc:sql": "TO_CHAR(NOW() AT TIME ZONE 'UTC' + INTERVAL '3 hours', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"')", + "purpose": "LAN", + "tripStartDate:sql": "TO_CHAR(NOW() AT TIME ZONE 'UTC' - INTERVAL '10 hours', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"')" + } + } + ] + } +] diff --git a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/mappers/ERSMapperUTests.kt b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/mappers/ERSMapperUTests.kt index f1a96eae46..553aab94bc 100644 --- a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/mappers/ERSMapperUTests.kt +++ b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/mappers/ERSMapperUTests.kt @@ -33,17 +33,17 @@ class ERSMapperUTests { @Test fun `getERSMessageValueFromJSON Should deserialize a FAR message When it is first serialized`() { // Given - val catch = Catch() - catch.economicZone = "FRA" - catch.effortZone = "C" - catch.faoZone = "27.8.a" - catch.statisticalRectangle = "23E6" - catch.species = "SCR" - catch.weight = 125.0 + val logbookFishingCatch = LogbookFishingCatch() + logbookFishingCatch.economicZone = "FRA" + logbookFishingCatch.effortZone = "C" + logbookFishingCatch.faoZone = "27.8.a" + logbookFishingCatch.statisticalRectangle = "23E6" + logbookFishingCatch.species = "SCR" + logbookFishingCatch.weight = 125.0 val haul = Haul() haul.gear = "OTB" - haul.catches = listOf(catch) + haul.catches = listOf(logbookFishingCatch) haul.mesh = 80.0 haul.latitude = 45.389 haul.longitude = -1.303 diff --git a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/GetLogbookMessagesUTests.kt b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/GetLogbookMessagesUTests.kt index 9d56cc670d..8df6c9595a 100644 --- a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/GetLogbookMessagesUTests.kt +++ b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/GetLogbookMessagesUTests.kt @@ -280,20 +280,20 @@ class GetLogbookMessagesUTests { getDummyRETLogbookMessages() + LogbookMessage( id = 2, - analyzedByRules = listOf(), - operationNumber = "", reportId = "9065646816", referencedReportId = "9065646811", - operationType = LogbookOperationType.RET, - messageType = "", + analyzedByRules = listOf(), + integrationDateTime = ZonedDateTime.now(), + isEnriched = false, message = lastAck, + messageType = "", + operationDateTime = ZonedDateTime.now(), + operationNumber = "", + operationType = LogbookOperationType.RET, reportDateTime = ZonedDateTime.of(2021, 5, 5, 3, 4, 5, 3, ZoneOffset.UTC).minusHours( 12, ), transmissionFormat = LogbookTransmissionFormat.ERS, - integrationDateTime = ZonedDateTime.now(), - isEnriched = false, - operationDateTime = ZonedDateTime.now(), ), ) given(logbookRawMessageRepository.findRawMessage(any())).willReturn("DUMMY XML MESSAGE") diff --git a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/TestUtils.kt b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/TestUtils.kt index 78cbbaeb96..2a370f505d 100644 --- a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/TestUtils.kt +++ b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/TestUtils.kt @@ -71,16 +71,16 @@ object TestUtils { } fun getDummyLogbookMessages(): List { - val gearOne = Gear() + val gearOne = LogbookTripGear() gearOne.gear = "OTB" - val gearTwo = Gear() + val gearTwo = LogbookTripGear() gearTwo.gear = "DRB" - val catchOne = Catch() + val catchOne = LogbookFishingCatch() catchOne.species = "TTV" - val catchTwo = Catch() + val catchTwo = LogbookFishingCatch() catchTwo.species = "SMV" - val catchThree = Catch() + val catchThree = LogbookFishingCatch() catchThree.species = "PNB" val dep = DEP() @@ -183,8 +183,7 @@ object TestUtils { messageType = "PNO", software = "e-Sacapt Secours ERSV3 V 1.0.7", message = pno, - reportDateTime = - ZonedDateTime.of( + reportDateTime = ZonedDateTime.of( 2020, 5, 5, @@ -209,8 +208,7 @@ object TestUtils { messageType = "COE", software = "e-Sacapt Secours ERSV3 V 1.0.7", message = coe, - reportDateTime = - ZonedDateTime.of( + reportDateTime = ZonedDateTime.of( 2020, 5, 5, @@ -235,8 +233,7 @@ object TestUtils { messageType = "COX", software = "e-Sacapt Secours ERSV3 V 1.0.7", message = cox, - reportDateTime = - ZonedDateTime.of(2020, 5, 5, 3, 4, 5, 3, UTC).minusHours(0).minusMinutes( + reportDateTime = ZonedDateTime.of(2020, 5, 5, 3, 4, 5, 3, UTC).minusHours(0).minusMinutes( 20, ), transmissionFormat = LogbookTransmissionFormat.ERS, @@ -254,8 +251,7 @@ object TestUtils { messageType = "CPS", software = "", message = cpsMessage, - reportDateTime = - ZonedDateTime.of(2020, 5, 5, 3, 4, 5, 3, UTC).minusHours(0).minusMinutes( + reportDateTime = ZonedDateTime.of(2020, 5, 5, 3, 4, 5, 3, UTC).minusHours(0).minusMinutes( 20, ), transmissionFormat = LogbookTransmissionFormat.ERS, @@ -267,16 +263,16 @@ object TestUtils { } fun getDummyFluxAndVisioCaptureLogbookMessages(): List { - val gearOne = Gear() + val gearOne = LogbookTripGear() gearOne.gear = "OTB" - val gearTwo = Gear() + val gearTwo = LogbookTripGear() gearTwo.gear = "DRB" - val catchOne = Catch() + val catchOne = LogbookFishingCatch() catchOne.species = "TTV" - val catchTwo = Catch() + val catchTwo = LogbookFishingCatch() catchTwo.species = "SMV" - val catchThree = Catch() + val catchThree = LogbookFishingCatch() catchThree.species = "PNB" val dep = DEP() @@ -306,8 +302,7 @@ object TestUtils { messageType = "DEP", software = "FT/VISIOCaptures V1.4.7", message = dep, - reportDateTime = - ZonedDateTime.of( + reportDateTime = ZonedDateTime.of( 2020, 5, 5, @@ -332,8 +327,7 @@ object TestUtils { messageType = "FAR", software = "FP/VISIOCaptures V1.4.7", message = far, - reportDateTime = - ZonedDateTime.of( + reportDateTime = ZonedDateTime.of( 2020, 5, 5, @@ -358,8 +352,7 @@ object TestUtils { messageType = "PNO", software = "TurboCatch (3.6-1)", message = pno, - reportDateTime = - ZonedDateTime.of( + reportDateTime = ZonedDateTime.of( 2020, 5, 5, @@ -378,11 +371,11 @@ object TestUtils { } fun getDummyCorrectedLogbookMessages(): List { - val catchOne = Catch() + val catchOne = LogbookFishingCatch() catchOne.species = "TTV" - val catchTwo = Catch() + val catchTwo = LogbookFishingCatch() catchTwo.species = "SMV" - val catchThree = Catch() + val catchThree = LogbookFishingCatch() catchThree.species = "PNB" val far = FAR() @@ -409,8 +402,7 @@ object TestUtils { operationType = LogbookOperationType.DAT, messageType = "FAR", message = far, - reportDateTime = - ZonedDateTime.of( + reportDateTime = ZonedDateTime.of( 2020, 5, 5, @@ -435,8 +427,7 @@ object TestUtils { operationType = LogbookOperationType.COR, messageType = "FAR", message = correctedFar, - reportDateTime = - ZonedDateTime.of( + reportDateTime = ZonedDateTime.of( 2020, 5, 5, @@ -455,11 +446,11 @@ object TestUtils { } fun getDummyRETLogbookMessages(): List { - val catchOne = Catch() + val catchOne = LogbookFishingCatch() catchOne.species = "TTV" - val catchTwo = Catch() + val catchTwo = LogbookFishingCatch() catchTwo.species = "SMV" - val catchThree = Catch() + val catchThree = LogbookFishingCatch() catchThree.species = "PNB" val far = FAR() @@ -634,47 +625,47 @@ object TestUtils { weightToAdd: Double = 0.0, addSpeciesToLAN: Boolean = false, ): List> { - val catchOne = Catch() + val catchOne = LogbookFishingCatch() catchOne.species = "TTV" catchOne.weight = 123.0 catchOne.conversionFactor = 1.0 - val catchTwo = Catch() + val catchTwo = LogbookFishingCatch() catchTwo.species = "SMV" catchTwo.weight = 961.5 catchTwo.conversionFactor = 1.22 - val catchThree = Catch() + val catchThree = LogbookFishingCatch() catchThree.species = "PNB" catchThree.weight = 69.7 catchThree.conversionFactor = 1.35 - val catchFour = Catch() + val catchFour = LogbookFishingCatch() catchFour.species = "CQL" catchFour.weight = 98.2 catchFour.conversionFactor = 1.0 - val catchFive = Catch() + val catchFive = LogbookFishingCatch() catchFive.species = "FGV" catchFive.weight = 25.5 - val catchSix = Catch() + val catchSix = LogbookFishingCatch() catchSix.species = "THB" catchSix.weight = 35.0 - val catchSeven = Catch() + val catchSeven = LogbookFishingCatch() catchSeven.species = "VGY" catchSeven.weight = 66666.0 - val catchEight = Catch() + val catchEight = LogbookFishingCatch() catchEight.species = "MQP" catchEight.weight = 11.1 - val catchNine = Catch() + val catchNine = LogbookFishingCatch() catchNine.species = "FPS" catchNine.weight = 22.0 - val catchTen = Catch() + val catchTen = LogbookFishingCatch() catchTen.species = "DPD" catchTen.weight = 2225.0 @@ -724,6 +715,7 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), + reportDateTime = ZonedDateTime.now(), ), LogbookMessage( id = 2, @@ -738,6 +730,7 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), + reportDateTime = ZonedDateTime.now(), ), ), Pair( @@ -754,6 +747,7 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), + reportDateTime = ZonedDateTime.now(), ), LogbookMessage( id = 4, @@ -768,6 +762,7 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), + reportDateTime = ZonedDateTime.now(), ), ), ) @@ -777,37 +772,37 @@ object TestUtils { weightToAdd: Double = 0.0, addSpeciesToLAN: Boolean = false, ): List> { - val catchOne = Catch() + val catchOne = LogbookFishingCatch() catchOne.species = "TTV" catchOne.weight = 123.0 - val catchTwo = Catch() + val catchTwo = LogbookFishingCatch() catchTwo.species = "SMV" catchTwo.weight = 961.5 - val catchThree = Catch() + val catchThree = LogbookFishingCatch() catchThree.species = "PNB" catchThree.weight = 69.7 - val catchFour = Catch() + val catchFour = LogbookFishingCatch() catchFour.species = "CQL" catchFour.weight = 98.2 - val catchFive = Catch() + val catchFive = LogbookFishingCatch() catchFive.species = "FGV" catchFive.weight = 25.5 - val catchSix = Catch() + val catchSix = LogbookFishingCatch() catchSix.species = "THB" catchSix.weight = 35.0 - val catchSeven = Catch() + val catchSeven = LogbookFishingCatch() catchSeven.species = "VGY" catchSeven.weight = 66666.0 - val catchEight = Catch() + val catchEight = LogbookFishingCatch() catchEight.species = "MQP" catchEight.weight = 11.1 - val catchNine = Catch() + val catchNine = LogbookFishingCatch() catchNine.species = "FPS" catchNine.weight = 22.0 - val catchTen = Catch() + val catchTen = LogbookFishingCatch() catchTen.species = "DPD" catchTen.weight = 2225.0 @@ -848,6 +843,7 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), + reportDateTime = ZonedDateTime.now(), ), LogbookMessage( id = 2, @@ -862,6 +858,7 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), + reportDateTime = ZonedDateTime.now(), ), ), Pair( @@ -878,6 +875,7 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), + reportDateTime = ZonedDateTime.now(), ), LogbookMessage( id = 4, @@ -892,6 +890,7 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), + reportDateTime = ZonedDateTime.now(), ), ), ) diff --git a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/GetPriorNotificationTypesUTests.kt b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/GetPriorNotificationTypesUTests.kt index a0461a230c..9e57c61eb0 100644 --- a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/GetPriorNotificationTypesUTests.kt +++ b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/GetPriorNotificationTypesUTests.kt @@ -1,7 +1,7 @@ package fr.gouv.cnsp.monitorfish.domain.use_cases.prior_notification import com.nhaarman.mockitokotlin2.given -import fr.gouv.cnsp.monitorfish.domain.repositories.* +import fr.gouv.cnsp.monitorfish.domain.repositories.LogbookReportRepository import org.assertj.core.api.Assertions import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith diff --git a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/GetPriorNotificationUTests.kt b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/GetPriorNotificationUTests.kt index 1b7b322494..c75ea3d481 100644 --- a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/GetPriorNotificationUTests.kt +++ b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/GetPriorNotificationUTests.kt @@ -1,6 +1,5 @@ package fr.gouv.cnsp.monitorfish.domain.use_cases.prior_notification -import com.neovisionaries.i18n.CountryCode import com.nhaarman.mockitokotlin2.given import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookMessage import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookMessageTyped @@ -8,7 +7,6 @@ import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookOperationType import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookTransmissionFormat import fr.gouv.cnsp.monitorfish.domain.entities.logbook.messages.PNO import fr.gouv.cnsp.monitorfish.domain.entities.prior_notification.PriorNotification -import fr.gouv.cnsp.monitorfish.domain.entities.vessel.Vessel import fr.gouv.cnsp.monitorfish.domain.repositories.* import org.assertj.core.api.Assertions import org.junit.jupiter.api.Test @@ -31,6 +29,9 @@ class GetPriorNotificationUTests { @MockBean private lateinit var portRepository: PortRepository + @MockBean + private lateinit var manualPriorNotificationRepository: ManualPriorNotificationRepository + @MockBean private lateinit var reportingRepository: ReportingRepository @@ -48,7 +49,11 @@ class GetPriorNotificationUTests { // Given given(logbookReportRepository.findPriorNotificationByReportId("FAKE_REPORT_ID_1")).willReturn( PriorNotification( - fingerprint = "1", + reportId = "1", + authorTrigram = null, + createdAt = null, + didNotFishAfterZeroNotice = false, + isManuallyCreated = false, logbookMessageTyped = LogbookMessageTyped( clazz = PNO::class.java, logbookMessage = LogbookMessage( @@ -65,23 +70,17 @@ class GetPriorNotificationUTests { operationDateTime = ZonedDateTime.now(), operationNumber = "1", operationType = LogbookOperationType.DAT, + reportDateTime = ZonedDateTime.now(), transmissionFormat = LogbookTransmissionFormat.ERS, ), ), + note = null, + port = null, reportingCount = null, seafront = null, - vessel = Vessel( - id = 1, - externalReferenceNumber = null, - flagState = CountryCode.FR, - internalReferenceNumber = null, - ircs = null, - length = null, - mmsi = null, - underCharter = null, - vesselName = null, - hasLogbookEsacapt = false, - ), + sentAt = null, + updatedAt = null, + vessel = null, vesselRiskFactor = null, ), ) @@ -91,6 +90,7 @@ class GetPriorNotificationUTests { gearRepository, logbookRawMessageRepository, logbookReportRepository, + manualPriorNotificationRepository, portRepository, reportingRepository, riskFactorRepository, @@ -108,7 +108,11 @@ class GetPriorNotificationUTests { // Given given(logbookReportRepository.findPriorNotificationByReportId("FAKE_REPORT_ID_2")).willReturn( PriorNotification( - fingerprint = "2.3", + reportId = "2", + authorTrigram = null, + createdAt = null, + didNotFishAfterZeroNotice = false, + isManuallyCreated = false, logbookMessageTyped = LogbookMessageTyped( clazz = PNO::class.java, logbookMessage = LogbookMessage( @@ -125,23 +129,17 @@ class GetPriorNotificationUTests { operationDateTime = ZonedDateTime.now(), operationNumber = "2", operationType = LogbookOperationType.COR, + reportDateTime = ZonedDateTime.now(), transmissionFormat = LogbookTransmissionFormat.ERS, ), ), + note = null, + port = null, reportingCount = null, seafront = null, - vessel = Vessel( - id = 2, - externalReferenceNumber = null, - flagState = CountryCode.UK, - internalReferenceNumber = null, - ircs = null, - length = null, - mmsi = null, - underCharter = null, - vesselName = null, - hasLogbookEsacapt = false, - ), + sentAt = null, + updatedAt = null, + vessel = null, vesselRiskFactor = null, ), ) @@ -151,6 +149,7 @@ class GetPriorNotificationUTests { gearRepository, logbookRawMessageRepository, logbookReportRepository, + manualPriorNotificationRepository, portRepository, reportingRepository, riskFactorRepository, diff --git a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/GetPriorNotificationsITests.kt b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/GetPriorNotificationsITestsDetail.kt similarity index 84% rename from backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/GetPriorNotificationsITests.kt rename to backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/GetPriorNotificationsITestsDetail.kt index e1fde2e423..0010423937 100644 --- a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/GetPriorNotificationsITests.kt +++ b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/GetPriorNotificationsITestsDetail.kt @@ -1,8 +1,8 @@ package fr.gouv.cnsp.monitorfish.domain.use_cases.prior_notification import fr.gouv.cnsp.monitorfish.config.MapperConfiguration -import fr.gouv.cnsp.monitorfish.domain.entities.logbook.filters.LogbookReportFilter -import fr.gouv.cnsp.monitorfish.domain.entities.logbook.sorters.LogbookReportSortColumn +import fr.gouv.cnsp.monitorfish.domain.entities.prior_notification.filters.PriorNotificationsFilter +import fr.gouv.cnsp.monitorfish.domain.entities.prior_notification.sorters.PriorNotificationsSortColumn import fr.gouv.cnsp.monitorfish.domain.repositories.* import fr.gouv.cnsp.monitorfish.infrastructure.database.repositories.AbstractDBTests import org.assertj.core.api.Assertions.assertThat @@ -19,7 +19,7 @@ import java.time.ZonedDateTime @ExtendWith(SpringExtension::class) @Import(MapperConfiguration::class) @SpringBootTest -class GetPriorNotificationsITests : AbstractDBTests() { +class GetPriorNotificationsITestsDetail : AbstractDBTests() { @Autowired private lateinit var getPriorNotifications: GetPriorNotifications @@ -44,11 +44,11 @@ class GetPriorNotificationsITests : AbstractDBTests() { @Autowired private lateinit var vesselRepository: VesselRepository - private val defaultFilter = LogbookReportFilter( + private val defaultFilter = PriorNotificationsFilter( willArriveAfter = "2000-01-01T00:00:00Z", willArriveBefore = "2099-12-31T00:00:00Z", ) - private val defaultSortColumn = LogbookReportSortColumn.EXPECTED_ARRIVAL_DATE + private val defaultSortColumn = PriorNotificationsSortColumn.EXPECTED_ARRIVAL_DATE private val defaultSortDirection = Sort.Direction.ASC private val defaultPageSize = 10 private val defaultPageNumber = 0 @@ -57,7 +57,7 @@ class GetPriorNotificationsITests : AbstractDBTests() { @Transactional fun `execute should return a list of prior notifications sorted by expected arrival date ascending`() { // Given - val sortColumn = LogbookReportSortColumn.EXPECTED_ARRIVAL_DATE + val sortColumn = PriorNotificationsSortColumn.EXPECTED_ARRIVAL_DATE val sortDirection = Sort.Direction.ASC // When @@ -77,7 +77,7 @@ class GetPriorNotificationsITests : AbstractDBTests() { @Transactional fun `execute should return a list of prior notifications sorted by expected arrival date descending`() { // Given - val sortColumn = LogbookReportSortColumn.EXPECTED_ARRIVAL_DATE + val sortColumn = PriorNotificationsSortColumn.EXPECTED_ARRIVAL_DATE val sortDirection = Sort.Direction.DESC // When @@ -97,7 +97,7 @@ class GetPriorNotificationsITests : AbstractDBTests() { @Transactional fun `execute should return a list of prior notifications sorted by expected landing date ascending`() { // Given - val sortColumn = LogbookReportSortColumn.EXPECTED_LANDING_DATE + val sortColumn = PriorNotificationsSortColumn.EXPECTED_LANDING_DATE val sortDirection = Sort.Direction.ASC // When @@ -109,7 +109,7 @@ class GetPriorNotificationsITests : AbstractDBTests() { assertThat( firstPriorNotificationWithNonNullLandingDate.logbookMessageTyped.typedMessage.predictedLandingDatetimeUtc, ) - .isEqualTo(ZonedDateTime.parse("2024-03-01T17:30:00Z")) + .isEqualTo(ZonedDateTime.parse("2023-01-01T10:30:00Z")) assertThat(result).hasSizeGreaterThan(0) } @@ -117,7 +117,7 @@ class GetPriorNotificationsITests : AbstractDBTests() { @Transactional fun `execute should return a list of prior notifications sorted by expected landing date descending`() { // Given - val sortColumn = LogbookReportSortColumn.EXPECTED_LANDING_DATE + val sortColumn = PriorNotificationsSortColumn.EXPECTED_LANDING_DATE val sortDirection = Sort.Direction.DESC // When @@ -137,7 +137,7 @@ class GetPriorNotificationsITests : AbstractDBTests() { @Transactional fun `execute should return a list of prior notifications sorted by port name ascending`() { // Given - val sortColumn = LogbookReportSortColumn.PORT_NAME + val sortColumn = PriorNotificationsSortColumn.PORT_NAME val sortDirection = Sort.Direction.ASC // When @@ -154,7 +154,7 @@ class GetPriorNotificationsITests : AbstractDBTests() { @Transactional fun `execute should return a list of prior notifications sorted by port name descending`() { // Given - val sortColumn = LogbookReportSortColumn.PORT_NAME + val sortColumn = PriorNotificationsSortColumn.PORT_NAME val sortDirection = Sort.Direction.DESC // When @@ -171,17 +171,17 @@ class GetPriorNotificationsITests : AbstractDBTests() { @Transactional fun `execute should return a list of prior notifications sorted by vessel name ascending`() { // Given - val sortColumn = LogbookReportSortColumn.VESSEL_NAME + val sortColumn = PriorNotificationsSortColumn.VESSEL_NAME val sortDirection = Sort.Direction.ASC // When val result = getPriorNotifications.execute(defaultFilter, sortColumn, sortDirection) // Then - val firstPriorNotificationWithKnownVessel = result.first { it.vessel.id != -1 } + val firstPriorNotificationWithKnownVessel = result.first { it.vessel!!.id != -1 } // We don't test the `.vessel.VesselName` since in the real world, // the vessel name may have changed between the logbook message date and now - assertThat(firstPriorNotificationWithKnownVessel.vessel.internalReferenceNumber).isEqualTo("CFR105") + assertThat(firstPriorNotificationWithKnownVessel.vessel!!.internalReferenceNumber).isEqualTo("CFR105") assertThat(firstPriorNotificationWithKnownVessel.logbookMessageTyped.logbookMessage.internalReferenceNumber) .isEqualTo("CFR105") assertThat(firstPriorNotificationWithKnownVessel.logbookMessageTyped.logbookMessage.vesselName) @@ -193,21 +193,21 @@ class GetPriorNotificationsITests : AbstractDBTests() { @Transactional fun `execute should return a list of prior notifications sorted by vessel name descending`() { // Given - val sortColumn = LogbookReportSortColumn.VESSEL_NAME + val sortColumn = PriorNotificationsSortColumn.VESSEL_NAME val sortDirection = Sort.Direction.DESC // When val result = getPriorNotifications.execute(defaultFilter, sortColumn, sortDirection) // Then - val firstPriorNotificationWithKnownVessel = result.first { it.vessel.id != -1 } + val firstPriorNotificationWithKnownVessel = result.first { it.vessel!!.id != -1 } // We don't test the `.vessel.VesselName` since in the real world, // the vessel name may have changed between the logbook message date and now - assertThat(firstPriorNotificationWithKnownVessel.vessel.internalReferenceNumber).isEqualTo("CFR101") + assertThat(firstPriorNotificationWithKnownVessel.vessel!!.internalReferenceNumber).isEqualTo("CFR120") assertThat(firstPriorNotificationWithKnownVessel.logbookMessageTyped.logbookMessage.internalReferenceNumber) - .isEqualTo("CFR101") + .isEqualTo("CFR120") assertThat(firstPriorNotificationWithKnownVessel.logbookMessageTyped.logbookMessage.vesselName) - .isEqualTo("VIVA ESPANA") + .isEqualTo("VIVA L'ITALIA") assertThat(result).hasSizeGreaterThan(0) } @@ -215,7 +215,7 @@ class GetPriorNotificationsITests : AbstractDBTests() { @Transactional fun `execute should return a list of prior notifications sorted by vessel risk factor ascending`() { // Given - val sortColumn = LogbookReportSortColumn.VESSEL_RISK_FACTOR + val sortColumn = PriorNotificationsSortColumn.VESSEL_RISK_FACTOR val sortDirection = Sort.Direction.ASC // When @@ -223,7 +223,7 @@ class GetPriorNotificationsITests : AbstractDBTests() { // Then val firstPriorNotificationWithNonNullRiskFactor = result.first { it.vesselRiskFactor != null } - assertThat(firstPriorNotificationWithNonNullRiskFactor.vesselRiskFactor!!.riskFactor).isEqualTo(2.473) + assertThat(firstPriorNotificationWithNonNullRiskFactor.vesselRiskFactor!!.riskFactor).isEqualTo(2.2) assertThat(result).hasSizeGreaterThan(0) } @@ -231,7 +231,7 @@ class GetPriorNotificationsITests : AbstractDBTests() { @Transactional fun `execute should return a list of prior notifications sorted by vessel risk factor descending`() { // Given - val sortColumn = LogbookReportSortColumn.VESSEL_RISK_FACTOR + val sortColumn = PriorNotificationsSortColumn.VESSEL_RISK_FACTOR val sortDirection = Sort.Direction.DESC // When diff --git a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/GetPriorNotificationsUTests.kt b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/GetPriorNotificationsUTestsDetail.kt similarity index 76% rename from backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/GetPriorNotificationsUTests.kt rename to backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/GetPriorNotificationsUTestsDetail.kt index 2d0be88054..4b13b9fcbd 100644 --- a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/GetPriorNotificationsUTests.kt +++ b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/GetPriorNotificationsUTestsDetail.kt @@ -1,16 +1,14 @@ package fr.gouv.cnsp.monitorfish.domain.use_cases.prior_notification -import com.neovisionaries.i18n.CountryCode import com.nhaarman.mockitokotlin2.given import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookMessage import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookMessageTyped import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookOperationType import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookTransmissionFormat -import fr.gouv.cnsp.monitorfish.domain.entities.logbook.filters.LogbookReportFilter import fr.gouv.cnsp.monitorfish.domain.entities.logbook.messages.PNO -import fr.gouv.cnsp.monitorfish.domain.entities.logbook.sorters.LogbookReportSortColumn import fr.gouv.cnsp.monitorfish.domain.entities.prior_notification.PriorNotification -import fr.gouv.cnsp.monitorfish.domain.entities.vessel.Vessel +import fr.gouv.cnsp.monitorfish.domain.entities.prior_notification.filters.PriorNotificationsFilter +import fr.gouv.cnsp.monitorfish.domain.entities.prior_notification.sorters.PriorNotificationsSortColumn import fr.gouv.cnsp.monitorfish.domain.repositories.* import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test @@ -21,13 +19,16 @@ import org.springframework.test.context.junit.jupiter.SpringExtension import java.time.ZonedDateTime @ExtendWith(SpringExtension::class) -class GetPriorNotificationsUTests { +class GetPriorNotificationsUTestsDetail { @MockBean private lateinit var gearRepository: GearRepository @MockBean private lateinit var logbookReportRepository: LogbookReportRepository + @MockBean + private lateinit var manualPriorNotificationRepository: ManualPriorNotificationRepository + @MockBean private lateinit var portRepository: PortRepository @@ -43,11 +44,11 @@ class GetPriorNotificationsUTests { @MockBean private lateinit var vesselRepository: VesselRepository - private val defaultFilter = LogbookReportFilter( + private val defaultFilter = PriorNotificationsFilter( willArriveAfter = "2000-01-01T00:00:00Z", willArriveBefore = "2099-12-31T00:00:00Z", ) - private val defaultSortColumn = LogbookReportSortColumn.EXPECTED_ARRIVAL_DATE + private val defaultSortColumn = PriorNotificationsSortColumn.EXPECTED_ARRIVAL_DATE private val defaultSortDirection = Sort.Direction.ASC private val defaultPageSize = 10 private val defaultPageNumber = 0 @@ -58,7 +59,11 @@ class GetPriorNotificationsUTests { given(logbookReportRepository.findAllPriorNotifications(defaultFilter)).willReturn( listOf( PriorNotification( - fingerprint = "1", + reportId = "FAKE_REPORT_ID_1", + authorTrigram = null, + createdAt = null, + didNotFishAfterZeroNotice = false, + isManuallyCreated = false, logbookMessageTyped = LogbookMessageTyped( clazz = PNO::class.java, logbookMessage = LogbookMessage( @@ -74,28 +79,26 @@ class GetPriorNotificationsUTests { operationDateTime = ZonedDateTime.now(), operationNumber = "1", operationType = LogbookOperationType.DAT, + reportDateTime = ZonedDateTime.now(), transmissionFormat = LogbookTransmissionFormat.ERS, ), ), + note = null, + port = null, reportingCount = null, seafront = null, - vessel = Vessel( - id = 1, - externalReferenceNumber = null, - flagState = CountryCode.FR, - internalReferenceNumber = null, - ircs = null, - length = null, - mmsi = null, - underCharter = null, - vesselName = null, - hasLogbookEsacapt = false, - ), + sentAt = null, + updatedAt = null, + vessel = null, vesselRiskFactor = null, ), PriorNotification( - fingerprint = "1", + reportId = "FAKE_REPORT_ID_2", + authorTrigram = null, + createdAt = null, + didNotFishAfterZeroNotice = false, + isManuallyCreated = false, logbookMessageTyped = LogbookMessageTyped( clazz = PNO::class.java, logbookMessage = LogbookMessage( @@ -111,23 +114,17 @@ class GetPriorNotificationsUTests { operationDateTime = ZonedDateTime.now(), operationNumber = "1", operationType = LogbookOperationType.COR, + reportDateTime = ZonedDateTime.now(), transmissionFormat = LogbookTransmissionFormat.ERS, ), ), + note = null, + port = null, reportingCount = null, seafront = null, - vessel = Vessel( - id = 2, - externalReferenceNumber = null, - flagState = CountryCode.UK, - internalReferenceNumber = null, - ircs = null, - length = null, - mmsi = null, - underCharter = null, - vesselName = null, - hasLogbookEsacapt = false, - ), + sentAt = null, + updatedAt = null, + vessel = null, vesselRiskFactor = null, ), ), @@ -137,6 +134,7 @@ class GetPriorNotificationsUTests { val result = GetPriorNotifications( gearRepository, logbookReportRepository, + manualPriorNotificationRepository, portRepository, reportingRepository, riskFactorRepository, diff --git a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/bff/PriorNotificationControllerITests.kt b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/bff/PriorNotificationControllerITests.kt index fef177cfce..ff66c62695 100644 --- a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/bff/PriorNotificationControllerITests.kt +++ b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/bff/PriorNotificationControllerITests.kt @@ -10,6 +10,7 @@ import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookTransmissionForma import fr.gouv.cnsp.monitorfish.domain.entities.logbook.messages.PNO import fr.gouv.cnsp.monitorfish.domain.entities.prior_notification.PriorNotification import fr.gouv.cnsp.monitorfish.domain.entities.vessel.Vessel +import fr.gouv.cnsp.monitorfish.domain.use_cases.prior_notification.CreateOrUpdatePriorNotification import fr.gouv.cnsp.monitorfish.domain.use_cases.prior_notification.GetPriorNotification import fr.gouv.cnsp.monitorfish.domain.use_cases.prior_notification.GetPriorNotificationTypes import fr.gouv.cnsp.monitorfish.domain.use_cases.prior_notification.GetPriorNotifications @@ -34,6 +35,9 @@ class PriorNotificationControllerITests { @Autowired private lateinit var api: MockMvc + @MockBean + private lateinit var createOrUpdatePriorNotification: CreateOrUpdatePriorNotification + @MockBean private lateinit var getPriorNotification: GetPriorNotification @@ -49,7 +53,11 @@ class PriorNotificationControllerITests { given(this.getPriorNotifications.execute(any(), any(), any())).willReturn( listOf( PriorNotification( - fingerprint = "1", + reportId = "FAKE_REPORT_ID_1", + authorTrigram = null, + createdAt = null, + didNotFishAfterZeroNotice = false, + isManuallyCreated = false, logbookMessageTyped = LogbookMessageTyped( clazz = PNO::class.java, logbookMessage = LogbookMessage( @@ -65,28 +73,32 @@ class PriorNotificationControllerITests { operationDateTime = ZonedDateTime.now(), operationNumber = "1", operationType = LogbookOperationType.DAT, + reportDateTime = ZonedDateTime.now(), transmissionFormat = LogbookTransmissionFormat.ERS, ), ), + note = null, + port = null, reportingCount = null, seafront = null, + sentAt = null, + updatedAt = null, vessel = Vessel( id = 1, - externalReferenceNumber = null, flagState = CountryCode.FR, - internalReferenceNumber = null, - ircs = null, - length = null, - mmsi = null, - underCharter = null, - vesselName = null, hasLogbookEsacapt = false, + internalReferenceNumber = "FAKE_CFR_1", + vesselName = "FAKE_VESSEL_NAME", ), vesselRiskFactor = null, ), PriorNotification( - fingerprint = "3", + reportId = "FAKE_REPORT_ID_2_COR", + authorTrigram = null, + createdAt = null, + didNotFishAfterZeroNotice = false, + isManuallyCreated = false, logbookMessageTyped = LogbookMessageTyped( clazz = PNO::class.java, logbookMessage = LogbookMessage( @@ -102,22 +114,22 @@ class PriorNotificationControllerITests { operationDateTime = ZonedDateTime.now(), operationNumber = "1", operationType = LogbookOperationType.COR, + reportDateTime = ZonedDateTime.now(), transmissionFormat = LogbookTransmissionFormat.ERS, ), ), - reportingCount = null, + note = null, + port = null, + reportingCount = 0, seafront = null, + sentAt = null, + updatedAt = null, vessel = Vessel( - id = 1, - externalReferenceNumber = null, - flagState = CountryCode.UK, - internalReferenceNumber = null, - ircs = null, - length = null, - mmsi = null, - underCharter = null, - vesselName = null, + id = 2, + flagState = CountryCode.FR, hasLogbookEsacapt = false, + internalReferenceNumber = "FAKE_CFR_2", + vesselName = "FAKE_VESSEL_NAME", ), vesselRiskFactor = null, ), @@ -160,7 +172,11 @@ class PriorNotificationControllerITests { // Given given(this.getPriorNotification.execute("FAKE_REPORT_ID_1")).willReturn( PriorNotification( - fingerprint = "1", + reportId = "FAKE_REPORT_ID_1", + authorTrigram = null, + createdAt = null, + didNotFishAfterZeroNotice = false, + isManuallyCreated = false, logbookMessageTyped = LogbookMessageTyped( clazz = PNO::class.java, logbookMessage = LogbookMessage( @@ -176,22 +192,25 @@ class PriorNotificationControllerITests { operationDateTime = ZonedDateTime.now(), operationNumber = "1", operationType = LogbookOperationType.DAT, + reportDateTime = ZonedDateTime.now(), transmissionFormat = LogbookTransmissionFormat.ERS, ), ), + note = null, + port = null, reportingCount = null, seafront = null, + sentAt = null, + updatedAt = null, vessel = Vessel( id = 1, - externalReferenceNumber = null, flagState = CountryCode.FR, - internalReferenceNumber = null, - ircs = null, + hasLogbookEsacapt = false, + internalReferenceNumber = "FAKE_CFR_1", length = 10.0, mmsi = null, underCharter = null, - vesselName = null, - hasLogbookEsacapt = false, + vesselName = "FAKE_VESSEL_NAME", ), vesselRiskFactor = null, ), diff --git a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/bff/VesselControllerITests.kt b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/bff/VesselControllerITests.kt index 1471119a8c..ca5a4f81d6 100644 --- a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/bff/VesselControllerITests.kt +++ b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/bff/VesselControllerITests.kt @@ -60,6 +60,9 @@ class VesselControllerITests { @MockBean private lateinit var getVessel: GetVessel + @MockBean + private lateinit var getVesselById: GetVesselById + @MockBean private lateinit var getVesselPositions: GetVesselPositions 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 7779e51f40..e3c141887b 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 @@ -11,6 +11,7 @@ import org.springframework.transaction.annotation.Transactional import java.time.LocalDate import java.time.ZonedDateTime import java.time.format.DateTimeFormatter +import kotlin.properties.Delegates class JpaFleetSegmentRepositoryITests : AbstractDBTests() { @Autowired @@ -19,7 +20,8 @@ class JpaFleetSegmentRepositoryITests : AbstractDBTests() { @Autowired lateinit var cacheManager: CacheManager - private val currentYear: Int + // https://stackoverflow.com/a/44386513/2736233 + private var currentYear by Delegates.notNull() init { val formatter = DateTimeFormatter.ofPattern("yyyy") diff --git a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaLogbookReportRepositoryITests.kt b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaLogbookReportRepositoryITests.kt index e440da6f0e..eba209f0f8 100644 --- a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaLogbookReportRepositoryITests.kt +++ b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaLogbookReportRepositoryITests.kt @@ -6,8 +6,8 @@ import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookMessageTypeMappin import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookOperationType import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookRawMessage import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookTransmissionFormat -import fr.gouv.cnsp.monitorfish.domain.entities.logbook.filters.LogbookReportFilter import fr.gouv.cnsp.monitorfish.domain.entities.logbook.messages.* +import fr.gouv.cnsp.monitorfish.domain.entities.prior_notification.filters.PriorNotificationsFilter import fr.gouv.cnsp.monitorfish.domain.exceptions.NoLogbookFishingTripFound import fr.gouv.cnsp.monitorfish.domain.use_cases.TestUtils import fr.gouv.cnsp.monitorfish.infrastructure.database.entities.LogbookReportEntity @@ -21,6 +21,7 @@ import org.springframework.boot.test.context.SpringBootTest import org.springframework.cache.CacheManager import org.springframework.context.annotation.Import import org.springframework.transaction.annotation.Transactional +import java.time.Instant import java.time.ZoneOffset.UTC import java.time.ZonedDateTime import java.util.* @@ -610,7 +611,7 @@ class JpaLogbookReportRepositoryITests : AbstractDBTests() { @Transactional fun `findAllPriorNotifications Should return PNO logbook reports from ESP & FRA vessels`() { // Given - val filter = LogbookReportFilter( + val filter = PriorNotificationsFilter( flagStates = listOf("ESP", "FRA"), willArriveAfter = "2000-01-01T00:00:00Z", willArriveBefore = "2100-01-01T00:00:00Z", @@ -633,10 +634,10 @@ class JpaLogbookReportRepositoryITests : AbstractDBTests() { @Test @Transactional fun `findAllPriorNotifications Should return PNO logbook reports with or without reportings`() { - val expectedLogbookReportIdsWithOneOrMoreReportings = listOf(102L) + val expectedLogbookReportIdsWithOneOrMoreReportings = listOf(102L, 104L) // Given - val firstFilter = LogbookReportFilter( + val firstFilter = PriorNotificationsFilter( hasOneOrMoreReportings = true, willArriveAfter = "2000-01-01T00:00:00Z", willArriveBefore = "2100-01-01T00:00:00Z", @@ -654,7 +655,7 @@ class JpaLogbookReportRepositoryITests : AbstractDBTests() { ).isTrue() // Given - val secondFilter = LogbookReportFilter( + val secondFilter = PriorNotificationsFilter( hasOneOrMoreReportings = false, willArriveAfter = "2000-01-01T00:00:00Z", willArriveBefore = "2100-01-01T00:00:00Z", @@ -677,7 +678,7 @@ class JpaLogbookReportRepositoryITests : AbstractDBTests() { @Transactional fun `findAllPriorNotifications Should return PNO logbook reports for less or more than 12 meters long vessels`() { // Given - val firstFilter = LogbookReportFilter( + val firstFilter = PriorNotificationsFilter( isLessThanTwelveMetersVessel = true, willArriveAfter = "2000-01-01T00:00:00Z", willArriveBefore = "2100-01-01T00:00:00Z", @@ -697,7 +698,7 @@ class JpaLogbookReportRepositoryITests : AbstractDBTests() { assertThat(firstResultVessels.all { it.length!! < 12 }).isTrue() // Given - val secondFilter = LogbookReportFilter( + val secondFilter = PriorNotificationsFilter( isLessThanTwelveMetersVessel = false, willArriveAfter = "2000-01-01T00:00:00Z", willArriveBefore = "2100-01-01T00:00:00Z", @@ -719,9 +720,9 @@ class JpaLogbookReportRepositoryITests : AbstractDBTests() { @Test @Transactional - fun `findAllPriorNotifications Should return PNO logbook reports controlled after or before January 1st, 2024`() { + fun `findAllPriorNotifications Should return PNO logbook reports for vessels controlled after or before January 1st, 2024`() { // Given - val firstFilter = LogbookReportFilter( + val firstFilter = PriorNotificationsFilter( lastControlledAfter = "2024-01-01T00:00:00Z", willArriveAfter = "2000-01-01T00:00:00Z", willArriveBefore = "2100-01-01T00:00:00Z", @@ -745,7 +746,7 @@ class JpaLogbookReportRepositoryITests : AbstractDBTests() { ).isTrue() // Given - val secondFilter = LogbookReportFilter( + val secondFilter = PriorNotificationsFilter( lastControlledBefore = "2024-01-01T00:00:00Z", willArriveAfter = "2000-01-01T00:00:00Z", willArriveBefore = "2100-01-01T00:00:00Z", @@ -773,7 +774,7 @@ class JpaLogbookReportRepositoryITests : AbstractDBTests() { @Transactional fun `findAllPriorNotifications Should return PNO logbook reports for FRSML & FRVNE ports`() { // Given - val filter = LogbookReportFilter( + val filter = PriorNotificationsFilter( portLocodes = listOf("FRSML", "FRVNE"), willArriveAfter = "2000-01-01T00:00:00Z", willArriveBefore = "2100-01-01T00:00:00Z", @@ -795,7 +796,7 @@ class JpaLogbookReportRepositoryITests : AbstractDBTests() { @Transactional fun `findAllPriorNotifications Should return PNO logbook reports for PHENOMENE vessel`() { // Given - val firstFilter = LogbookReportFilter( + val firstFilter = PriorNotificationsFilter( searchQuery = "pheno", willArriveAfter = "2000-01-01T00:00:00Z", willArriveBefore = "2100-01-01T00:00:00Z", @@ -815,7 +816,7 @@ class JpaLogbookReportRepositoryITests : AbstractDBTests() { assertThat(firstResultVessels.all { it.vesselName == "PHENOMENE" }).isTrue() // Given - val secondFilter = LogbookReportFilter( + val secondFilter = PriorNotificationsFilter( searchQuery = "hénO", willArriveAfter = "2000-01-01T00:00:00Z", willArriveBefore = "2100-01-01T00:00:00Z", @@ -839,7 +840,7 @@ class JpaLogbookReportRepositoryITests : AbstractDBTests() { @Transactional fun `findAllPriorNotifications Should return PNO logbook reports for COD & HKE species`() { // Given - val filter = LogbookReportFilter( + val filter = PriorNotificationsFilter( specyCodes = listOf("COD", "HKE"), willArriveAfter = "2000-01-01T00:00:00Z", willArriveBefore = "2100-01-01T00:00:00Z", @@ -862,7 +863,7 @@ class JpaLogbookReportRepositoryITests : AbstractDBTests() { @Transactional fun `findAllPriorNotifications Should return PNO logbook reports for Préavis type A & Préavis type C types`() { // Given - val filter = LogbookReportFilter( + val filter = PriorNotificationsFilter( priorNotificationTypes = listOf("Préavis type A", "Préavis type C"), willArriveAfter = "2000-01-01T00:00:00Z", willArriveBefore = "2100-01-01T00:00:00Z", @@ -885,7 +886,7 @@ class JpaLogbookReportRepositoryITests : AbstractDBTests() { @Transactional fun `findAllPriorNotifications Should return PNO logbook reports for SWW06 & NWW03 segments`() { // Given - val filter = LogbookReportFilter( + val filter = PriorNotificationsFilter( tripSegmentCodes = listOf("SWW06", "NWW03"), willArriveAfter = "2000-01-01T00:00:00Z", willArriveBefore = "2100-01-01T00:00:00Z", @@ -912,7 +913,7 @@ class JpaLogbookReportRepositoryITests : AbstractDBTests() { @Transactional fun `findAllPriorNotifications Should return PNO logbook reports for OTT & TB gears`() { // Given - val filter = LogbookReportFilter( + val filter = PriorNotificationsFilter( tripGearCodes = listOf("OTT", "TB"), willArriveAfter = "2000-01-01T00:00:00Z", willArriveBefore = "2100-01-01T00:00:00Z", @@ -935,7 +936,7 @@ class JpaLogbookReportRepositoryITests : AbstractDBTests() { @Transactional fun `findAllPriorNotifications Should return PNO logbook reports for vessels arriving after or before January 1st, 2024`() { // Given - val firstFilter = LogbookReportFilter( + val firstFilter = PriorNotificationsFilter( willArriveAfter = "2024-01-01T00:00:00Z", willArriveBefore = "2100-01-01T00:00:00Z", ) @@ -953,7 +954,7 @@ class JpaLogbookReportRepositoryITests : AbstractDBTests() { ).isTrue() // Given - val secondFilter = LogbookReportFilter( + val secondFilter = PriorNotificationsFilter( willArriveAfter = "2000-01-01T00:00:00Z", willArriveBefore = "2024-01-01T00:00:00Z", ) @@ -975,7 +976,7 @@ class JpaLogbookReportRepositoryITests : AbstractDBTests() { @Transactional fun `findAllPriorNotifications Should return the expected PNO logbook reports with multiple filters`() { // Given - val filter = LogbookReportFilter( + val filter = PriorNotificationsFilter( priorNotificationTypes = listOf("Préavis type A", "Préavis type C"), tripGearCodes = listOf("OTT", "TB"), willArriveAfter = "2024-01-01T00:00:00Z", @@ -1142,11 +1143,26 @@ class JpaLogbookReportRepositoryITests : AbstractDBTests() { return LogbookReportEntity( reportId = reportId, referencedReportId = referenceReportId, - integrationDateTime = ZonedDateTime.now().toInstant(), - operationDateTime = ZonedDateTime.now().toInstant(), + analyzedByRules = null, + externalReferenceNumber = null, + flagState = null, + integrationDateTime = Instant.now(), + cfr = null, + imo = null, + ircs = null, + message = null, + messageType = null, + operationCountry = null, + operationDateTime = Instant.now(), operationNumber = "FAKE_OPERATION_NUMBER_$reportId", operationType = operationType, + reportDateTime = null, + software = null, transmissionFormat = LogbookTransmissionFormat.ERS, + tripGears = null, + tripNumber = null, + tripSegments = null, + vesselName = null, ) } } diff --git a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaManualPriorNotificationRepositoryITests.kt b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaManualPriorNotificationRepositoryITests.kt new file mode 100644 index 0000000000..6de5a7f854 --- /dev/null +++ b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaManualPriorNotificationRepositoryITests.kt @@ -0,0 +1,492 @@ +package fr.gouv.cnsp.monitorfish.infrastructure.database.repositories + +import com.neovisionaries.i18n.CountryCode +import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookMessage +import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookMessageTyped +import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookOperationType +import fr.gouv.cnsp.monitorfish.domain.entities.logbook.messages.PNO +import fr.gouv.cnsp.monitorfish.domain.entities.prior_notification.PriorNotification +import fr.gouv.cnsp.monitorfish.domain.entities.prior_notification.filters.PriorNotificationsFilter +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.transaction.annotation.Transactional +import java.time.ZoneOffset +import java.time.ZonedDateTime + +class JpaManualPriorNotificationRepositoryITests : AbstractDBTests() { + @Autowired + private lateinit var jpaManualPriorNotificationRepository: JpaManualPriorNotificationRepository + + @Autowired + private lateinit var jpaRiskFactorRepository: JpaRiskFactorRepository + + @Autowired + private lateinit var jpaVesselRepository: JpaVesselRepository + + private var allPriorNotificationsLength: Int = 0 + + val defaultPriorNotificationsFilter = PriorNotificationsFilter( + willArriveAfter = "2000-01-01T00:00:00Z", + willArriveBefore = "2099-12-31T00:00:00Z", + ) + + @BeforeEach + fun beforeEach() { + allPriorNotificationsLength = jpaManualPriorNotificationRepository.findAll(defaultPriorNotificationsFilter).size + } + + @Test + @Transactional + fun `findAll Should return all manual prior notifications`() { + // When + val result = jpaManualPriorNotificationRepository.findAll(defaultPriorNotificationsFilter) + + // Then + assertThat(result).hasSizeGreaterThan(0) + } + + @Test + @Transactional + fun `findAll Should return manual prior notifications from BEL & ITA vessels`() { + // Given + val filter = defaultPriorNotificationsFilter.copy(flagStates = listOf("BEL", "ITA")) + + // When + val result = jpaManualPriorNotificationRepository.findAll(filter) + + // Then + assertThat(result).hasSizeBetween(1, allPriorNotificationsLength - 1) + val resultVessels = result.mapNotNull { + jpaVesselRepository.findFirstByInternalReferenceNumber( + it.logbookMessageTyped.logbookMessage.internalReferenceNumber!!, + ) + } + assertThat(resultVessels).hasSize(result.size) + assertThat(resultVessels.all { listOf(CountryCode.BE, CountryCode.IT).contains(it.flagState) }).isTrue() + } + + @Test + @Transactional + fun `findAll Should return manual prior notifications with or without reportings`() { + val expectedLogbookReportIdsWithOneOrMoreReportings = listOf("00000000-0000-4000-0000-000000000002") + + // Given + val firstFilter = defaultPriorNotificationsFilter.copy(hasOneOrMoreReportings = true) + + // When + val firstResult = jpaManualPriorNotificationRepository.findAll(firstFilter) + + // Then + assertThat(firstResult).hasSizeBetween(1, allPriorNotificationsLength - 1) + assertThat( + firstResult.all { + it.reportId in expectedLogbookReportIdsWithOneOrMoreReportings + }, + ).isTrue() + + // Given + val secondFilter = defaultPriorNotificationsFilter.copy(hasOneOrMoreReportings = false) + + // When + val secondResult = jpaManualPriorNotificationRepository.findAll(secondFilter) + + // Then + assertThat(secondResult).hasSizeBetween(1, allPriorNotificationsLength - 1) + println(secondResult.map { it.logbookMessageTyped.logbookMessage.id }) + assertThat( + secondResult.none { + it.reportId in expectedLogbookReportIdsWithOneOrMoreReportings + }, + ).isTrue() + } + + @Test + @Transactional + fun `findAll Should return all manual prior notifications for less than 12 meters long vessels and none for more than 12 meters long vessels`() { + // Given + val firstFilter = defaultPriorNotificationsFilter.copy(isLessThanTwelveMetersVessel = true) + + // When + val firstResult = jpaManualPriorNotificationRepository.findAll(firstFilter) + + // Then + assertThat(firstResult).hasSize(allPriorNotificationsLength) + val firstResultVessels = firstResult.mapNotNull { + jpaVesselRepository.findFirstByInternalReferenceNumber( + it.logbookMessageTyped.logbookMessage.internalReferenceNumber!!, + ) + } + assertThat(firstResultVessels).hasSize(firstResult.size) + assertThat(firstResultVessels.all { it.length!! < 12 }).isTrue() + + // Given + val secondFilter = defaultPriorNotificationsFilter.copy(isLessThanTwelveMetersVessel = false) + + // When + val secondResult = jpaManualPriorNotificationRepository.findAll(secondFilter) + + // Then + assertThat(secondResult).isEmpty() + } + + @Test + @Transactional + fun `findAll Should return manual prior notifications for vessels controlled after or before January 1st, 2024`() { + // Given + val firstFilter = defaultPriorNotificationsFilter.copy(lastControlledAfter = "2024-01-01T00:00:00Z") + + // When + val firstResult = jpaManualPriorNotificationRepository.findAll(firstFilter) + + // Then + assertThat(firstResult).hasSizeBetween(1, allPriorNotificationsLength - 1) + val firstResultRiskFactors = firstResult.mapNotNull { + jpaRiskFactorRepository.findFirstByInternalReferenceNumber( + it.logbookMessageTyped.logbookMessage.internalReferenceNumber!!, + ) + } + assertThat(firstResultRiskFactors).hasSize(firstResult.size) + assertThat( + firstResultRiskFactors.all { + it.lastControlDatetime!!.isAfter(ZonedDateTime.parse("2024-01-01T00:00:00Z")) + }, + ).isTrue() + + // Given + val secondFilter = defaultPriorNotificationsFilter.copy(lastControlledBefore = "2024-01-01T00:00:00Z") + + // When + val secondResult = jpaManualPriorNotificationRepository.findAll(secondFilter) + + // Then + assertThat(secondResult).hasSizeBetween(1, allPriorNotificationsLength - 1) + val secondResultRiskFactors = secondResult.mapNotNull { + jpaRiskFactorRepository.findFirstByInternalReferenceNumber( + it.logbookMessageTyped.logbookMessage.internalReferenceNumber!!, + ) + } + assertThat(secondResultRiskFactors).hasSize(secondResult.size) + assertThat( + secondResultRiskFactors.all { + it.lastControlDatetime!!.isBefore(ZonedDateTime.parse("2024-01-01T00:00:00Z")) + }, + ).isTrue() + } + + @Test + @Transactional + fun `findAll Should return manual prior notifications for FRNCE & FRVNE ports`() { + // Given + val filter = defaultPriorNotificationsFilter.copy(portLocodes = listOf("FRNCE", "FRVNE")) + + // When + val result = jpaManualPriorNotificationRepository.findAll(filter) + + // Then + assertThat(result).hasSizeBetween(1, allPriorNotificationsLength - 1) + assertThat( + result.all { + listOf("FRNCE", "FRVNE").contains(it.logbookMessageTyped.typedMessage.port) + }, + ).isTrue() + } + + @Test + @Transactional + fun `findAll Should return manual prior notifications for NAVIRE RENOMMÉ vessel`() { + // Given + val firstFilter = defaultPriorNotificationsFilter.copy(searchQuery = "renom") + + // When + val firstResult = jpaManualPriorNotificationRepository.findAll(firstFilter) + + // Then + assertThat(firstResult).hasSizeBetween(1, allPriorNotificationsLength - 1) + assertThat( + firstResult.all { it.logbookMessageTyped.logbookMessage.vesselName == "NAVIRE RENOMMÉ (ANCIEN NOM)" }, + ).isTrue() + val firstResultVessels = firstResult.mapNotNull { + jpaVesselRepository.findFirstByInternalReferenceNumber( + it.logbookMessageTyped.logbookMessage.internalReferenceNumber!!, + ) + } + assertThat(firstResultVessels).hasSize(firstResult.size) + assertThat(firstResultVessels.all { it.vesselName == "NAVIRE RENOMMÉ (NOUVEAU NOM)" }).isTrue() + + // Given + val secondFilter = defaultPriorNotificationsFilter.copy(searchQuery = "eNÔm") + + // When + val secondResult = jpaManualPriorNotificationRepository.findAll(secondFilter) + + // Then + assertThat(secondResult).hasSizeBetween(1, allPriorNotificationsLength - 1) + assertThat( + secondResult.all { it.logbookMessageTyped.logbookMessage.vesselName == "NAVIRE RENOMMÉ (ANCIEN NOM)" }, + ).isTrue() + val secondResultVessels = secondResult.mapNotNull { + jpaVesselRepository.findFirstByInternalReferenceNumber( + it.logbookMessageTyped.logbookMessage.internalReferenceNumber!!, + ) + } + assertThat(secondResultVessels).hasSize(secondResult.size) + assertThat(secondResultVessels.all { it.vesselName == "NAVIRE RENOMMÉ (NOUVEAU NOM)" }).isTrue() + } + + @Test + @Transactional + fun `findAll Should return manual prior notifications for BIB & BFT species`() { + // Given + val filter = defaultPriorNotificationsFilter.copy(specyCodes = listOf("BIB", "BFT")) + + // When + val result = jpaManualPriorNotificationRepository.findAll(filter) + + // Then + assertThat(result).hasSizeBetween(1, allPriorNotificationsLength - 1) + assertThat( + result.all { + it.logbookMessageTyped.typedMessage.catchOnboard + .any { catch -> listOf("BIB", "BFT").contains(catch.species) } + }, + ).isTrue() + assertThat( + result.all { + it.logbookMessageTyped.typedMessage.catchToLand + .any { catch -> listOf("BIB", "BFT").contains(catch.species) } + }, + ).isTrue() + } + + @Test + @Transactional + fun `findAll Should return manual prior notifications for Préavis type A & Préavis type C types`() { + // Given + val filter = + defaultPriorNotificationsFilter.copy(priorNotificationTypes = listOf("Préavis type A", "Préavis type C")) + + // When + val result = jpaManualPriorNotificationRepository.findAll(filter) + + // Then + assertThat(result).hasSizeBetween(1, allPriorNotificationsLength - 1) + assertThat( + result.all { + it.logbookMessageTyped.typedMessage.pnoTypes + .any { type -> listOf("Préavis type A", "Préavis type C").contains(type.name) } + }, + ).isTrue() + } + + @Test + @Transactional + fun `findAll Should return manual prior notifications for NWW05 & NWW09 segments`() { + // Given + val filter = defaultPriorNotificationsFilter.copy(tripSegmentCodes = listOf("NWW05", "NWW09")) + + // When + val result = jpaManualPriorNotificationRepository.findAll(filter) + + // Then + assertThat(result).hasSizeBetween(1, allPriorNotificationsLength - 1) + assertThat( + result.all { + it.logbookMessageTyped.logbookMessage.tripSegments!! + .any { tripSegment -> + listOf("NWW05", "NWW09").contains( + tripSegment.code, + ) + } + }, + ).isTrue() + } + + @Test + @Transactional + fun `findAll Should return manual prior notifications for LNP & TBS gears`() { + // Given + val filter = defaultPriorNotificationsFilter.copy(tripGearCodes = listOf("LNP", "TBS")) + + // When + val result = jpaManualPriorNotificationRepository.findAll(filter) + + // Then + assertThat(result).hasSizeBetween(1, allPriorNotificationsLength - 1) + assertThat( + result.all { + it.logbookMessageTyped.logbookMessage.tripGears!! + .any { tripGear -> listOf("LNP", "TBS").contains(tripGear.gear) } + }, + ).isTrue() + } + + @Test + @Transactional + fun `findAll Should return manual prior notifications for vessels arriving after or before January 1st, 2024`() { + // Given + val firstFilter = PriorNotificationsFilter( + willArriveAfter = "2024-01-01T00:00:00Z", + willArriveBefore = "2100-01-01T00:00:00Z", + ) + + // When + val firstResult = jpaManualPriorNotificationRepository.findAll(firstFilter) + + // Then + assertThat(firstResult).hasSizeBetween(1, allPriorNotificationsLength - 1) + assertThat( + firstResult.all { + it.logbookMessageTyped.typedMessage.predictedArrivalDatetimeUtc!! + .isAfter(ZonedDateTime.parse("2024-01-01T00:00:00Z")) + }, + ).isTrue() + + // Given + val secondFilter = PriorNotificationsFilter( + willArriveAfter = "2000-01-01T00:00:00Z", + willArriveBefore = "2024-01-01T00:00:00Z", + ) + + // When + val secondResult = jpaManualPriorNotificationRepository.findAll(secondFilter) + + // Then + assertThat(secondResult).hasSizeBetween(1, allPriorNotificationsLength - 1) + assertThat( + secondResult.all { + it.logbookMessageTyped.typedMessage.predictedArrivalDatetimeUtc!! + .isBefore(ZonedDateTime.parse("2024-01-01T00:00:00Z")) + }, + ).isTrue() + } + + @Test + @Transactional + fun `findAll Should return the expected manual prior notifications with multiple filters`() { + // Given + val filter = defaultPriorNotificationsFilter.copy( + priorNotificationTypes = listOf("Préavis type A", "Préavis type C"), + tripGearCodes = listOf("OTT", "TB"), + ) + + // When + val result = jpaManualPriorNotificationRepository.findAll(filter) + + // Then + assertThat(result).hasSizeBetween(1, allPriorNotificationsLength - 1) + assertThat( + result.all { + it.logbookMessageTyped.typedMessage.pnoTypes + .any { type -> listOf("Préavis type A", "Préavis type C").contains(type.name) } + }, + ).isTrue() + assertThat( + result.all { + it.logbookMessageTyped.logbookMessage.tripGears!! + .any { tripGear -> listOf("OTT", "TB").contains(tripGear.gear) } + }, + ).isTrue() + assertThat( + result.all { + it.logbookMessageTyped.typedMessage.predictedArrivalDatetimeUtc!! + .isAfter(ZonedDateTime.parse("2024-01-01T00:00:00Z")) + }, + ).isTrue() + } + + @Test + @Transactional + fun `findById Should return the expected manual prior notification`() { + // Given + val reportId = "00000000-0000-4000-0000-000000000002" + + // When + val result = jpaManualPriorNotificationRepository.findByReportId(reportId) + + // Then + assertThat(result!!.reportId).isEqualTo("00000000-0000-4000-0000-000000000002") + assertThat(result.logbookMessageTyped.logbookMessage.vesselName).isEqualTo("DOS FIN") + } + + @Test + @Transactional + fun `save Should create and update a manual prior notification`() { + val originalPriorNotificationsSize = jpaManualPriorNotificationRepository + .findAll(defaultPriorNotificationsFilter) + .size + + // Given + val newPriorNotification = + PriorNotification( + reportId = null, + authorTrigram = "ABC", + createdAt = null, + didNotFishAfterZeroNotice = false, + isManuallyCreated = false, + logbookMessageTyped = LogbookMessageTyped( + LogbookMessage( + id = null, + analyzedByRules = emptyList(), + internalReferenceNumber = "CFR123", + // Replaced by the generated `createdAt` during the save operation. + integrationDateTime = ZonedDateTime.now(), + message = PNO().apply { + catchOnboard = emptyList() + catchToLand = emptyList() + economicZone = null + effortZone = null + faoZone = null + latitude = null + longitude = null + pnoTypes = emptyList() + port = "FRVNE" + portName = "Vannes" + predictedArrivalDatetimeUtc = ZonedDateTime.now().withZoneSameInstant(ZoneOffset.UTC) + predictedLandingDatetimeUtc = ZonedDateTime.now().withZoneSameInstant(ZoneOffset.UTC) + purpose = "LAN" + statisticalRectangle = null + tripStartDate = null + }, + messageType = "PNO", + // Replaced by the generated `createdAt` during the save operation. + operationDateTime = ZonedDateTime.now(), + operationNumber = null, + operationType = LogbookOperationType.DAT, + // Replaced by the generated `sentAt` during the save operation. + reportDateTime = ZonedDateTime.now(), + transmissionFormat = null, + vesselName = "Vessel Name", + ), + PNO::class.java, + ), + note = null, + port = null, + reportingCount = null, + seafront = null, + sentAt = ZonedDateTime.now().toString(), + updatedAt = null, + vessel = null, + vesselRiskFactor = null, + ) + + // When + val createdPriorNotificationReportId = jpaManualPriorNotificationRepository.save(newPriorNotification) + val createdPriorNotification = jpaManualPriorNotificationRepository + .findByReportId(createdPriorNotificationReportId) + val priorNotifications = jpaManualPriorNotificationRepository + .findAll(defaultPriorNotificationsFilter) + .sortedBy { it.createdAt } + + // Then + val lastPriorNotification = priorNotifications.last() + assertThat(priorNotifications).hasSize(originalPriorNotificationsSize + 1) + assertThat(lastPriorNotification) + .usingRecursiveComparison() + .ignoringFields("logbookMessageTyped") + .isEqualTo(createdPriorNotification!!) + assertThat(lastPriorNotification.logbookMessageTyped.logbookMessage) + .isEqualTo(createdPriorNotification.logbookMessageTyped.logbookMessage) + } +} diff --git a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaReportingRepositoryITests.kt b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaReportingRepositoryITests.kt index 6ad40773cd..95127cdc6b 100644 --- a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaReportingRepositoryITests.kt +++ b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaReportingRepositoryITests.kt @@ -42,7 +42,7 @@ class JpaReportingRepositoryITests : AbstractDBTests() { val reporting = jpaReportingRepository.findAll() // Then - assertThat(reporting).hasSize(9) + assertThat(reporting).hasSize(13) assertThat(reporting.last().internalReferenceNumber).isEqualTo("FRFGRGR") assertThat(reporting.last().externalReferenceNumber).isEqualTo("RGD") val alert = reporting.last().value as ThreeMilesTrawlingAlert @@ -84,7 +84,7 @@ class JpaReportingRepositoryITests : AbstractDBTests() { val reportings = jpaReportingRepository.findAll() // Then - assertThat(reportings).hasSize(9) + assertThat(reportings).hasSize(13) assertThat(reportings.last().internalReferenceNumber).isEqualTo("FRFGRGR") assertThat(reportings.last().externalReferenceNumber).isEqualTo("RGD") assertThat(reportings.last().type).isEqualTo(ReportingType.INFRACTION_SUSPICION) @@ -126,7 +126,7 @@ class JpaReportingRepositoryITests : AbstractDBTests() { val reportings = jpaReportingRepository.findAll() // Then - assertThat(reportings).hasSize(9) + assertThat(reportings).hasSize(13) assertThat(reportings.last().internalReferenceNumber).isEqualTo("FRFGRGR") assertThat(reportings.last().externalReferenceNumber).isEqualTo("RGD") assertThat(reportings.last().type).isEqualTo(ReportingType.INFRACTION_SUSPICION) diff --git a/frontend/cypress/e2e/side_window/prior_notification_form/form.spec.ts b/frontend/cypress/e2e/side_window/prior_notification_form/form.spec.ts new file mode 100644 index 0000000000..e6a9bd99e1 --- /dev/null +++ b/frontend/cypress/e2e/side_window/prior_notification_form/form.spec.ts @@ -0,0 +1,217 @@ +import { customDayjs } from '@mtes-mct/monitor-ui' + +import { addSideWindowPriorNotification, editSideWindowPriorNotification } from './utils' +import { getUtcDateInMultipleFormats } from '../../utils/getUtcDateInMultipleFormats' +import { isDateCloseTo } from '../../utils/isDateCloseTo' + +context('Side Window > Prior Notification Form > Form', () => { + it('Should add and edit a prior notification', () => { + // ------------------------------------------------------------------------- + // Add + + const now = new Date() + const { utcDateAsString: arrivalDateAsString, utcDateTupleWithTime: arrivalDateTupleWithTime } = + getUtcDateInMultipleFormats(customDayjs().add(2, 'hours').startOf('minute').toISOString()) + const { utcDateAsString: landingDateAsString, utcDateTupleWithTime: landingDateTupleWithTime } = + getUtcDateInMultipleFormats(customDayjs().add(2.5, 'hours').startOf('minute').toISOString()) + + addSideWindowPriorNotification() + + cy.intercept('POST', '/bff/v1/prior_notifications').as('createPriorNotification') + + cy.getDataCy('vessel-search-input').click().wait(500) + cy.getDataCy('vessel-search-input').type('pageot', { delay: 100 }) + cy.getDataCy('vessel-search-item').first().click() + + cy.fill("Date et heure estimées d'arrivée au port", arrivalDateTupleWithTime) + cy.fill('Date et heure prévues de débarque', landingDateTupleWithTime) + cy.fill("Port d'arrivée", 'Vannes') + + cy.fill('Espèces à bord et à débarquer', 'AAX') + cy.fill('Poids (AAX)', 25) + cy.fill('Espèces à bord et à débarquer', 'BFT') + cy.fill('Poids (BFT)', 150) + cy.fill('Quantité (BF1)', 4) + cy.fill('Poids (BF1)', 40) + cy.fill('Quantité (BF2)', 5) + cy.fill('Poids (BF2)', 50) + cy.fill('Quantité (BF3)', 6) + cy.fill('Poids (BF3)', 60) + cy.fill('Espèces à bord et à débarquer', 'SWO') + cy.fill('Poids (SWO)', 200) + 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("Points d'attention identifiés par le CNSP", "Un point d'attention.") + cy.fill('Saisi par', 'BOB') + + cy.clickButton('Créer le préavis') + + cy.wait('@createPriorNotification').then(createInterception => { + if (!createInterception.response) { + assert.fail('`createInterception.response` is undefined.') + } + + const createdPriorNotification = createInterception.response.body + + assert.isString(createdPriorNotification.reportId) + assert.isTrue(isDateCloseTo(createdPriorNotification.sentAt, now, 15)) + assert.deepInclude(createdPriorNotification.fishingCatches, { + isIncidentalCatch: false, + quantity: null, + specyCode: 'AAX', + specyName: 'AAPTOSYAX GRYPUS', + weight: 25.0 + }) + assert.deepInclude(createdPriorNotification.fishingCatches, { + isIncidentalCatch: false, + quantity: null, + specyCode: 'BFT', + specyName: 'THON ROUGE', + weight: 150.0 + }) + assert.deepInclude(createdPriorNotification.fishingCatches, { + isIncidentalCatch: false, + quantity: 4.0, + specyCode: 'BF1', + specyName: 'THON ROUGE + 30 KG', + weight: 40.0 + }) + assert.deepInclude(createdPriorNotification.fishingCatches, { + isIncidentalCatch: false, + quantity: 5.0, + specyCode: 'BF2', + specyName: 'THON ROUGE 8 À 30 KG', + weight: 50.0 + }) + assert.deepInclude(createdPriorNotification.fishingCatches, { + isIncidentalCatch: false, + quantity: 6.0, + specyCode: 'BF3', + specyName: 'THON ROUGE 6.4 À 8 KG', + weight: 60.0 + }) + assert.deepInclude(createdPriorNotification.fishingCatches, { + isIncidentalCatch: false, + quantity: 20.0, + specyCode: 'SWO', + specyName: 'ESPADON', + weight: 200.0 + }) + assert.deepInclude(createInterception.request.body, { + authorTrigram: 'BOB', + didNotFishAfterZeroNotice: false, + expectedArrivalDate: arrivalDateAsString, + expectedLandingDate: landingDateAsString, + faoArea: '21.4.T', + note: "Un point d'attention.", + portLocode: 'FRVNE', + tripGearCodes: ['OTP', 'PTB'], + vesselId: 119 + }) + + // ----------------------------------------------------------------------- + // Edit + + editSideWindowPriorNotification('pageot') + + cy.intercept('PUT', `/bff/v1/prior_notifications/${createdPriorNotification.reportId}`).as( + 'updatePriorNotification' + ) + + cy.fill("Points d'attention identifiés par le CNSP", "Un point d'attention mis à jour.") + + cy.clickButton('Enregistrer') + + cy.wait('@updatePriorNotification').then(updateInterception => { + if (!updateInterception.response) { + assert.fail('`updateInterception.response` is undefined.') + } + + assert.deepInclude(updateInterception.request.body, { + ...createdPriorNotification, + note: "Un point d'attention mis à jour.", + reportId: createdPriorNotification.reportId + }) + }) + }) + }) + + it('Should display the expected form validation errors', () => { + // Base form validation errors + + const { utcDateTupleWithTime } = getUtcDateInMultipleFormats(customDayjs().toISOString()) + + addSideWindowPriorNotification() + + cy.intercept('POST', '/bff/v1/prior_notifications').as('createPriorNotification') + + cy.clickButton('Créer le préavis') + + cy.fill('Date et heure de réception du préavis', undefined) + + cy.contains('Veuillez indiquer le navire concerné.').should('exist') + cy.contains('Veuillez indiquer la date de réception du préavis.').should('exist') + cy.contains("Veuillez indiquer la date d'arrivée estimée.").should('exist') + cy.contains('Veuillez indiquer la date de débarquement prévue.').should('exist') + cy.contains("Veuillez indiquer le port d'arrivée.").should('exist') + cy.contains('Veuillez sélectionner au moins une espèce.').should('exist') + cy.contains('Veuillez sélectionner au moins un engin.').should('exist') + cy.contains('Veuillez indiquer la zone FAO.').should('exist') + cy.contains('Veuillez indiquer votre trigramme.').should('exist') + cy.contains('Créer le préavis').should('be.disabled') + + cy.getDataCy('vessel-search-input').click().wait(500) + cy.getDataCy('vessel-search-input').type('pageot', { delay: 100 }) + cy.getDataCy('vessel-search-item').first().click() + + cy.contains('Veuillez indiquer le navire concerné.').should('not.exist') + + cy.fill('Date et heure de réception du préavis', utcDateTupleWithTime) + + cy.contains('Veuillez indiquer la date de réception du préavis.').should('not.exist') + + cy.fill("Date et heure estimées d'arrivée au port", utcDateTupleWithTime) + + cy.contains("Veuillez indiquer la date d'arrivée estimée.").should('not.exist') + + cy.fill('Date et heure prévues de débarque', utcDateTupleWithTime) + + cy.contains('Veuillez indiquer la date de débarquement prévue.').should('not.exist') + + cy.fill("Port d'arrivée", 'Vannes') + + cy.contains("Veuillez indiquer le port d'arrivée.").should('not.exist') + + cy.fill('Espèces à bord et à débarquer', 'AAX') + + cy.contains('Veuillez sélectionner au moins une espèce.').should('not.exist') + + cy.fill('Engins utilisés', ['OTP'], { index: 1 }) + + cy.contains('Veuillez sélectionner au moins un engin.').should('not.exist') + + cy.fill('Zone de pêche', '21.4.T') + + cy.contains('Veuillez indiquer la zone FAO.').should('not.exist') + + cy.fill('Saisi par', 'BOB') + + cy.contains('Veuillez indiquer votre trigramme.').should('not.exist') + + cy.contains('Créer le préavis').should('be.enabled') + + // Other form validation errors + + cy.fill('Date et heure prévues de débarque', undefined) + + cy.contains('Veuillez indiquer la date de débarquement prévue.').should('exist') + cy.contains('Créer le préavis').should('be.disabled') + + cy.fill("équivalentes à celles de l'arrivée au port", true) + + cy.contains('Veuillez indiquer la date de débarquement prévue.').should('not.exist') + cy.contains('Créer le préavis').should('be.enabled') + }) +}) diff --git a/frontend/cypress/e2e/side_window/prior_notification_form/utils.ts b/frontend/cypress/e2e/side_window/prior_notification_form/utils.ts new file mode 100644 index 0000000000..6c82860595 --- /dev/null +++ b/frontend/cypress/e2e/side_window/prior_notification_form/utils.ts @@ -0,0 +1,44 @@ +import { SideWindowMenuLabel } from 'domain/entities/sideWindow/constants' + +export const addSideWindowPriorNotification = () => { + cy.viewport(1920, 1080) + cy.visit('/side_window') + cy.wait(500) + if (document.querySelector('[data-cy="first-loader"]')) { + cy.getDataCy('first-loader').should('not.be.visible') + } + + cy.clickButton(SideWindowMenuLabel.PRIOR_NOTIFICATION_LIST) + if (document.querySelector('[data-cy="first-loader"]')) { + cy.getDataCy('first-loader').should('not.be.visible') + } + cy.get('.Table-SimpleTable tr').should('have.length.to.be.greaterThan', 0) + + cy.clickButton('Ajouter un préavis') + if (document.querySelector('[data-cy="first-loader"]')) { + cy.getDataCy('first-loader').should('not.be.visible') + } +} + +export const editSideWindowPriorNotification = (vesselName: string) => { + cy.viewport(1920, 1080) + cy.visit('/side_window') + cy.wait(500) + if (document.querySelector('[data-cy="first-loader"]')) { + cy.getDataCy('first-loader').should('not.be.visible') + } + + cy.clickButton(SideWindowMenuLabel.PRIOR_NOTIFICATION_LIST) + if (document.querySelector('[data-cy="first-loader"]')) { + cy.getDataCy('first-loader').should('not.be.visible') + } + cy.get('.Table-SimpleTable tr').should('have.length.to.be.greaterThan', 0) + + cy.get('[data-cy="side-window-sub-menu-ALL"]').click() + cy.fill('Rechercher un navire', vesselName) + + cy.clickButton('Éditer le préavis') + if (document.querySelector('[data-cy="first-loader"]')) { + cy.getDataCy('first-loader').should('not.be.visible') + } +} diff --git a/frontend/cypress/e2e/side_window/reporting_list.spec.ts b/frontend/cypress/e2e/side_window/reporting_list.spec.ts index 36517395ab..1db0cb97cc 100644 --- a/frontend/cypress/e2e/side_window/reporting_list.spec.ts +++ b/frontend/cypress/e2e/side_window/reporting_list.spec.ts @@ -97,7 +97,7 @@ context('Reportings', () => { cy.get('[data-cy="side-window-sub-menu-NAMO"]').click() cy.wait(200) - cy.get('*[data-cy="side-window-current-reportings"]').should('have.length', 3) + cy.get('*[data-cy="side-window-current-reportings"]').should('have.length', 7) // When // Select reporting @@ -113,7 +113,7 @@ context('Reportings', () => { // Then // Should delete the row - cy.get('*[data-cy="side-window-current-reportings"]').should('have.length', 1) + cy.get('*[data-cy="side-window-current-reportings"]').should('have.length', 6) }) it('A Reporting Should be edited', () => { @@ -136,7 +136,7 @@ context('Reportings', () => { }) cy.wait(200) - cy.get('*[data-cy="side-window-current-reportings"]').should('have.length', 3) + cy.get('*[data-cy="side-window-current-reportings"]').should('have.length', 7) // We do not know if the edited reporting is the first or second row in the list cy.get('*[data-cy="side-window-current-reportings"]') .eq(1) @@ -148,8 +148,8 @@ context('Reportings', () => { return } - cy.get('*[data-cy="side-window-current-reportings"]').eq(2).contains('DML 56') - cy.get('*[data-cy="side-window-current-reportings"]').eq(2).contains(23581) + cy.get('*[data-cy="side-window-current-reportings"]').eq(6).contains('DML 56') + cy.get('*[data-cy="side-window-current-reportings"]').eq(6).contains(23581) }) }) @@ -159,7 +159,7 @@ context('Reportings', () => { cy.get('*[data-cy="side-window-reporting-tab"]').click() cy.get('[data-cy="side-window-sub-menu-NAMO"]').click() - cy.get('*[data-cy="side-window-current-reportings"]').should('have.length', 3) + cy.get('*[data-cy="side-window-current-reportings"]').should('have.length', 7) // When // We do not know if the edited reporting is the first or second row in the list diff --git a/frontend/cypress/e2e/utils/isDateCloseTo.ts b/frontend/cypress/e2e/utils/isDateCloseTo.ts new file mode 100644 index 0000000000..f437478350 --- /dev/null +++ b/frontend/cypress/e2e/utils/isDateCloseTo.ts @@ -0,0 +1,12 @@ +import dayjs, { type Dayjs, isDayjs } from 'dayjs' + +export function isDateCloseTo( + leftDate: string | Date | Dayjs, + rightDate: string | Date | Dayjs, + thresholdInSeconds: number +): boolean { + const leftDateAsDayjs: Dayjs = isDayjs(leftDate) ? leftDate : dayjs(leftDate) + const rightDateAsDayjs: Dayjs = isDayjs(rightDate) ? rightDate : dayjs(leftDate) + + return Math.abs(leftDateAsDayjs.diff(rightDateAsDayjs, 'second')) <= thresholdInSeconds +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ac76a6c5d2..0385c11893 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@dnd-kit/core": "6.1.0", "@dnd-kit/modifiers": "6.0.1", - "@mtes-mct/monitor-ui": "18.0.2", + "@mtes-mct/monitor-ui": "18.0.3", "@reduxjs/toolkit": "1.9.6", "@sentry/browser": "7.55.2", "@sentry/react": "7.52.1", @@ -2504,9 +2504,9 @@ "integrity": "sha512-HPnRdYO0WjFjRTSwO3frz1wKaU649OBFPX3Zo/2WZvuRi6zMiRGui8SnPQiQABgqCf8YikDe5t3HViTVw1WUzA==" }, "node_modules/@mtes-mct/monitor-ui": { - "version": "18.0.2", - "resolved": "https://registry.npmjs.org/@mtes-mct/monitor-ui/-/monitor-ui-18.0.2.tgz", - "integrity": "sha512-Vc/XUUCLEnePPsKGulcD54aqAqS3TNllZetokOsu87Xs884Dz6zPzTW+yqy4n28LV5mOUms7a4OcHrxqEJ6Z0A==", + "version": "18.0.3", + "resolved": "https://registry.npmjs.org/@mtes-mct/monitor-ui/-/monitor-ui-18.0.3.tgz", + "integrity": "sha512-UZGkcdv82Btm3ML/hAnJPQKlg2O7at6lCyhXgxeRHD5m2GIBCxUAxGBA75GAm20ULm3gKrl5Ai9Dh/TX7ybtvw==", "dependencies": { "@babel/runtime": "7.24.5", "@tanstack/react-table": "8.9.7", @@ -5513,9 +5513,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001561", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001561.tgz", - "integrity": "sha512-NTt0DNoKe958Q0BE0j0c1V9jbUzhBxHIEJy7asmGrpE0yG63KTV7PLHPnK2E1O9RsQrQ081I3NLuXGS6zht3cw==", + "version": "1.0.30001628", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001628.tgz", + "integrity": "sha512-S3BnR4Kh26TBxbi5t5kpbcUlLJb9lhtDXISDPwOfI+JoC+ik0QksvkZtUVyikw3hjnkgkMPSJ8oIM9yMm9vflA==", "funding": [ { "type": "opencollective", diff --git a/frontend/package.json b/frontend/package.json index f0deacc488..d1704301be 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -35,7 +35,7 @@ "dependencies": { "@dnd-kit/core": "6.1.0", "@dnd-kit/modifiers": "6.0.1", - "@mtes-mct/monitor-ui": "18.0.2", + "@mtes-mct/monitor-ui": "18.0.3", "@reduxjs/toolkit": "1.9.6", "@sentry/browser": "7.55.2", "@sentry/react": "7.52.1", diff --git a/frontend/scripts/generate_test_data_seeds.mjs b/frontend/scripts/generate_test_data_seeds.mjs index 6902d6e93f..7b1807a9c3 100644 --- a/frontend/scripts/generate_test_data_seeds.mjs +++ b/frontend/scripts/generate_test_data_seeds.mjs @@ -70,17 +70,22 @@ function generateInsertStatement(row, table) { return `INSERT INTO ${table} (${sqlColumns.join(', ')}) VALUES (${sqlValues.join(', ')});` } -function generateUpdateStatements(row, table) { +function generateUpdateStatements(row, table, id) { + const idColumnName = id ?? 'id' const updates = [] const processUpdates = (obj, path = []) => { Object.entries(obj).forEach(([key, value]) => { const currentPath = [...path, key.replace(/:sql$/, '')] if (key.endsWith(':sql')) { + const idColumnValue = row[idColumnName] + const escapedIdColumnValue = + typeof idColumnValue === 'string' ? `'${idColumnValue.replace(/'/g, "''")}'` : idColumnValue + updates.push( `UPDATE ${table} SET value = JSONB_SET(value, '{${currentPath.join( ',' - )}}', TO_JSONB(${value}), true) WHERE id = ${row.id};` + )}}', TO_JSONB(${value}), true) WHERE ${idColumnName} = ${escapedIdColumnValue};` ) } else if (typeof value === 'object' && value !== null) { processUpdates(value, currentPath) @@ -115,14 +120,18 @@ for (const file of jsonFiles) { const dataTables = Array.isArray(jsonSourceAsObject) ? jsonSourceAsObject : [jsonSourceAsObject] const sqlStatementBlocks = dataTables .map(dataTable => { - const { data: rows, table } = dataTable - - return rows.map(row => { - const insertStatement = generateInsertStatement(row, table) - const updateStatements = generateUpdateStatements(row, table) - - return [insertStatement, ...updateStatements, ''].join('\n') - }) + const { afterAll, beforeAll, data: rows, id, table } = dataTable + + return [ + beforeAll, + ...rows.map(row => { + const insertStatement = generateInsertStatement(row, table) + const updateStatements = generateUpdateStatements(row, table, id) + + return [insertStatement, ...updateStatements, ''].join('\n') + }), + afterAll + ].filter(Boolean) }) .flat() diff --git a/frontend/src/api/constants.ts b/frontend/src/api/constants.ts index e2d0476168..6f8ef2c649 100644 --- a/frontend/src/api/constants.ts +++ b/frontend/src/api/constants.ts @@ -32,5 +32,6 @@ export enum HttpStatusCode { export enum RtkCacheTagType { PriorNotification = 'PriorNotification', PriorNotificationTypes = 'PriorNotificationTypes', - PriorNotifications = 'PriorNotifications' + PriorNotifications = 'PriorNotifications', + Vessel = 'Vessel' } diff --git a/frontend/src/api/faoAreas.ts b/frontend/src/api/faoAreas.ts index 6816a3f5cc..1ccf065771 100644 --- a/frontend/src/api/faoAreas.ts +++ b/frontend/src/api/faoAreas.ts @@ -15,13 +15,17 @@ export const faoAreasApi = monitorfishApi.injectEndpoints({ computeVesselFaoAreas: builder.query({ query: params => `/fao_areas/compute?internalReferenceNumber=${params.internalReferenceNumber}&latitude=${ - params.latitude || '' - }&longitude=${params.longitude || ''}&portLocode=${params.portLocode || ''}` + params.latitude ?? '' + }&longitude=${params.longitude ?? ''}&portLocode=${params.portLocode ?? ''}` + }), + + getFaoAreas: builder.query({ + query: () => '/fao_areas' }) }) }) -export const { useComputeVesselFaoAreasQuery } = faoAreasApi +export const { useComputeVesselFaoAreasQuery, useGetFaoAreasQuery } = faoAreasApi /** * Get FAO areas diff --git a/frontend/src/api/vessel.ts b/frontend/src/api/vessel.ts index fd141a45a1..78e37d7951 100644 --- a/frontend/src/api/vessel.ts +++ b/frontend/src/api/vessel.ts @@ -10,11 +10,11 @@ const VESSEL_SEARCH_ERROR_MESSAGE = "Nous n'avons pas pu récupérer les navires const REPORTING_ERROR_MESSAGE = "Nous n'avons pas pu récupérer les signalements de ce navire" function getVesselIdentityAsEmptyStringWhenNull(identity: VesselIdentity) { - const vesselId = identity.vesselId || '' - const internalReferenceNumber = identity.internalReferenceNumber || '' - const externalReferenceNumber = identity.externalReferenceNumber || '' - const ircs = identity.ircs || '' - const vesselIdentifier = identity.vesselIdentifier || '' + const vesselId = identity.vesselId ?? '' + const internalReferenceNumber = identity.internalReferenceNumber ?? '' + const externalReferenceNumber = identity.externalReferenceNumber ?? '' + const ircs = identity.ircs ?? '' + const vesselIdentifier = identity.vesselIdentifier ?? '' return { externalReferenceNumber, internalReferenceNumber, ircs, vesselId, vesselIdentifier } } @@ -27,9 +27,9 @@ function getVesselIdentityAsEmptyStringWhenNull(identity: VesselIdentity) { async function getVesselFromAPI(identity: VesselIdentity, trackRequest: TrackRequest) { const { externalReferenceNumber, internalReferenceNumber, ircs, vesselId, vesselIdentifier } = getVesselIdentityAsEmptyStringWhenNull(identity) - const trackDepth = trackRequest.trackDepth || '' - const afterDateTime = trackRequest.afterDateTime?.toISOString() || '' - const beforeDateTime = trackRequest.beforeDateTime?.toISOString() || '' + const trackDepth = trackRequest.trackDepth ?? '' + const afterDateTime = trackRequest.afterDateTime?.toISOString() ?? '' + const beforeDateTime = trackRequest.beforeDateTime?.toISOString() ?? '' try { return await monitorfishApiKy @@ -55,9 +55,9 @@ async function getVesselFromAPI(identity: VesselIdentity, trackRequest: TrackReq async function getVesselPositionsFromAPI(identity: VesselIdentity, trackRequest: TrackRequest) { const { externalReferenceNumber, internalReferenceNumber, ircs, vesselIdentifier } = getVesselIdentityAsEmptyStringWhenNull(identity) - const trackDepth = trackRequest.trackDepth || '' - const afterDateTime = trackRequest.afterDateTime?.toISOString() || '' - const beforeDateTime = trackRequest.beforeDateTime?.toISOString() || '' + const trackDepth = trackRequest.trackDepth ?? '' + const afterDateTime = trackRequest.afterDateTime?.toISOString() ?? '' + const beforeDateTime = trackRequest.beforeDateTime?.toISOString() ?? '' try { return await monitorfishApiKy diff --git a/frontend/src/features/Logbook/LogbookMessage.types.ts b/frontend/src/features/Logbook/LogbookMessage.types.ts index ad3781f0c1..a25e9ecb0f 100644 --- a/frontend/src/features/Logbook/LogbookMessage.types.ts +++ b/frontend/src/features/Logbook/LogbookMessage.types.ts @@ -5,6 +5,7 @@ export namespace LogbookMessage { interface LogbookMessageBase { acknowledgment: Acknowledgment | undefined + createdAt: string externalReferenceNumber: string flagState: string | undefined imo: string | undefined @@ -13,11 +14,12 @@ export namespace LogbookMessage { ircs: string isCorrectedByNewerMessage: boolean isDeleted: boolean + isManuallyCreated: boolean isSentByFailoverSoftware: boolean message: MessageBase | undefined messageType: MessageType operationDateTime: string - operationNumber: string + operationNumber: string | undefined operationType: OperationType rawMessage: string referencedReportId: string | undefined @@ -26,6 +28,7 @@ export namespace LogbookMessage { tripGears: Gear[] | undefined tripNumber: string | undefined tripSegments: Segment[] | undefined + updatedAt: string vesselName: string } export interface PnoLogbookMessage extends LogbookMessageBase { diff --git a/frontend/src/features/PriorNotification/PriorNotification.types.ts b/frontend/src/features/PriorNotification/PriorNotification.types.ts index 7a13d70e8c..22b259b528 100644 --- a/frontend/src/features/PriorNotification/PriorNotification.types.ts +++ b/frontend/src/features/PriorNotification/PriorNotification.types.ts @@ -2,8 +2,9 @@ import type { Seafront } from '@constants/seafront' import type { LogbookMessage } from '@features/Logbook/LogbookMessage.types' export namespace PriorNotification { - export type PriorNotification = { + export interface PriorNotification { acknowledgment: LogbookMessage.Acknowledgment | undefined + createdAt: string expectedArrivalDate: string | undefined expectedLandingDate: string | undefined /** Unique identifier concatenating all the DAT, COR, RET & DEL operations `id` used for data consolidation. */ @@ -12,6 +13,7 @@ export namespace PriorNotification { /** Logbook message `reportId`. */ id: string isCorrection: boolean + isManuallyCreated: boolean isVesselUnderCharter: boolean | undefined onBoardCatches: LogbookMessage.Catch[] portLocode: string | undefined @@ -23,6 +25,7 @@ export namespace PriorNotification { tripGears: LogbookMessage.Gear[] tripSegments: LogbookMessage.Segment[] types: Type[] + updatedAt: string vesselExternalReferenceNumber: string | undefined vesselFlagCountryCode: string | undefined vesselId: number @@ -47,6 +50,29 @@ export namespace PriorNotification { logbookMessage: LogbookMessage.PnoLogbookMessage } + export type PriorNotificationData = { + authorTrigram: string + didNotFishAfterZeroNotice: boolean + expectedArrivalDate: string + expectedLandingDate: string + faoArea: string + fishingCatches: PriorNotificationDataFishingCatch[] + note: string | undefined + portLocode: string + reportId: string + sentAt: string + tripGearCodes: string[] + vesselId: number + } + export type NewPriorNotificationData = Omit + + export type PriorNotificationDataFishingCatch = { + quantity?: number | undefined + specyCode: string + specyName: string + weight: number + } + export type Type = { hasDesignatedPorts: number minimumNotificationPeriod: string diff --git a/frontend/src/features/PriorNotification/components/PriorNotificationCard/Header.tsx b/frontend/src/features/PriorNotification/components/PriorNotificationCard/Header.tsx index 20c5ec2a10..09f86b8fb4 100644 --- a/frontend/src/features/PriorNotification/components/PriorNotificationCard/Header.tsx +++ b/frontend/src/features/PriorNotification/components/PriorNotificationCard/Header.tsx @@ -4,7 +4,7 @@ import styled from 'styled-components' import { getFirstTitleRowText } from './utils' -import type { PriorNotification } from '@features/PriorNotification/PriorNotification.types' +import type { PriorNotification } from '../../PriorNotification.types' type HeaderProps = Readonly<{ onClose: () => void @@ -47,6 +47,7 @@ export function Header({ onClose, priorNotificationDetail }: HeaderProps) { const Wrapper = styled.div` align-items: flex-start; border-bottom: 1px solid ${p => p.theme.color.lightGray}; + box-shadow: 0px 3px 6px ${p => p.theme.color.lightGray}; display: flex; padding: 24px 32px; diff --git a/frontend/src/features/PriorNotification/components/PriorNotificationForm/Card.tsx b/frontend/src/features/PriorNotification/components/PriorNotificationForm/Card.tsx new file mode 100644 index 0000000000..ae60f06094 --- /dev/null +++ b/frontend/src/features/PriorNotification/components/PriorNotificationForm/Card.tsx @@ -0,0 +1,98 @@ +import { FrontendErrorBoundary } from '@components/FrontendErrorBoundary' +import { Accent, Button } from '@mtes-mct/monitor-ui' +import { useFormikContext } from 'formik' +import styled from 'styled-components' + +import { Form } from './Form' +import { Header } from './Header' +import { TagBar } from './TagBar' + +import type { FormValues } from './types' + +type CardProps = Readonly<{ + isValidatingOnChange: boolean + onClose: () => void + onSubmit: () => void + reportId: string | undefined +}> +export function Card({ isValidatingOnChange, onClose, onSubmit, reportId }: CardProps) { + const { isValid, submitForm, values } = useFormikContext() + + const handleSubmit = () => { + onSubmit() + + submitForm() + } + + return ( + + +
+ + + + +

+ Veuillez renseigner les champs du formulaire pour définir le type de préavis et son statut, ainsi que le + segment de flotte et la note de risque du navire. +

+ +
+ +
+ + +
+ + +
+ + + ) +} + +const Wrapper = styled.div` + background-color: ${p => p.theme.color.white}; + display: flex; + flex-direction: column; + height: 100%; + width: 560px; +` + +const Body = styled.div` + display: flex; + flex-direction: column; + flex-grow: 1; + overflow-y: auto; + padding: 32px; + + > p:first-child { + color: ${p => p.theme.color.slateGray}; + font-style: italic; + } + + > hr { + margin: 24px 0 0; + } + + > .Element-Field, + > .Element-Fieldset, + > .FieldGroup { + margin-top: 24px; + } +` + +const Footer = styled.div` + border-top: 1px solid ${p => p.theme.color.lightGray}; + display: flex; + justify-content: flex-end; + padding: 16px 32px; + + > .Element-Button:not(:first-child) { + margin-left: 16px; + } +` diff --git a/frontend/src/features/PriorNotification/components/PriorNotificationForm/Form.tsx b/frontend/src/features/PriorNotification/components/PriorNotificationForm/Form.tsx new file mode 100644 index 0000000000..e87981c291 --- /dev/null +++ b/frontend/src/features/PriorNotification/components/PriorNotificationForm/Form.tsx @@ -0,0 +1,103 @@ +import { useGetFaoAreasAsOptions } from '@hooks/useGetFaoAreasAsOptions' +import { useGetGearsAsOptions } from '@hooks/useGetGearsAsOptions' +import { useGetPortsAsOptions } from '@hooks/useGetPortsAsOptions' +import { + FormikCheckbox, + FormikDatePicker, + FormikMultiSelect, + FormikSelect, + FormikTextInput, + FormikTextarea +} from '@mtes-mct/monitor-ui' +import { useFormikContext } from 'formik' +import styled from 'styled-components' + +import { FormikFishingCatchesMultiSelect } from './fields/FormikFishingCatchesMultiSelect' +import { FormikVesselSelect } from './fields/FormikVesselSelect' + +import type { FormValues } from './types' + +export function Form() { + const { values } = useFormikContext() + + const { faoAreasAsOptions } = useGetFaoAreasAsOptions() + const { gearsAsOptions } = useGetGearsAsOptions() + const { portsAsOptions } = useGetPortsAsOptions() + + return ( + <> + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + ) +} + +const FieldGroup = styled.div.attrs({ className: 'FieldGroup' })` + display: flex; + flex-direction: column; + gap: 8px; + + textarea { + box-sizing: border-box !important; + } +` diff --git a/frontend/src/features/PriorNotification/components/PriorNotificationForm/Header.tsx b/frontend/src/features/PriorNotification/components/PriorNotificationForm/Header.tsx new file mode 100644 index 0000000000..9f82f5a05b --- /dev/null +++ b/frontend/src/features/PriorNotification/components/PriorNotificationForm/Header.tsx @@ -0,0 +1,96 @@ +import { CountryFlag } from '@components/CountryFlag' +import { useGetVesselQuery } from '@features/Vessel/vesselApi' +import { Accent, Icon, IconButton } from '@mtes-mct/monitor-ui' +import { skipToken } from '@reduxjs/toolkit/query' +import styled from 'styled-components' + +type HeaderProps = Readonly<{ + isNewPriorNotification: boolean + onClose: () => void + vesselId: number | undefined +}> +export function Header({ isNewPriorNotification, onClose, vesselId }: HeaderProps) { + const { data: vessel } = useGetVesselQuery(vesselId ?? skipToken) + + return ( + + + <TitleRow> + <TitleRowIconBox> + <Icon.Fishery /> + </TitleRowIconBox> + + {isNewPriorNotification && <span>{`AJOUTER UN NOUVEAU PRÉAVIS (< 12 M)`}</span>} + {!isNewPriorNotification && <span>{`PRÉAVIS NAVIRE < 12 M`}</span>} + </TitleRow> + + {!!vessel && ( + <TitleRow> + <TitleRowIconBox> + <CountryFlag countryCode={vessel?.flagState} size={[24, 18]} /> + </TitleRowIconBox> + + <span> + <VesselName>{vessel?.vesselName ?? '...'}</VesselName> ({vessel?.internalReferenceNumber ?? '...'}) + </span> + </TitleRow> + )} + + + + + ) +} + +const Wrapper = styled.div` + align-items: flex-start; + border-bottom: 1px solid ${p => p.theme.color.lightGray}; + box-shadow: 0px 3px 6px ${p => p.theme.color.lightGray}; + display: flex; + padding: 24px 32px; + + * { + font-size: 16px; + } +` + +const Title = styled.div` + display: flex; + flex-direction: column; + flex-grow: 1; + + > div { + &:nth-child(1) { + color: ${p => p.theme.color.slateGray}; + font-weight: 700; + } + &:nth-child(2) { + color: ${p => p.theme.color.gunMetal}; + margin-top: 8px; + } + } +` + +const TitleRow = styled.div` + align-items: flex-start; + display: flex; + line-height: 22px; +` + +const TitleRowIconBox = styled.span` + margin-right: 8px; + width: 24px; + + > .Element-IconBox { + vertical-align: -4px; + } + + > img { + vertical-align: -3.5px; + } +` + +const VesselName = styled.span` + font-size: 16px; + font-weight: 700; +` diff --git a/frontend/src/features/PriorNotification/components/PriorNotificationForm/TagBar.tsx b/frontend/src/features/PriorNotification/components/PriorNotificationForm/TagBar.tsx new file mode 100644 index 0000000000..7603e86061 --- /dev/null +++ b/frontend/src/features/PriorNotification/components/PriorNotificationForm/TagBar.tsx @@ -0,0 +1,19 @@ +import { THEME, Tag } from '@mtes-mct/monitor-ui' +import { useFormikContext } from 'formik' +import styled from 'styled-components' + +import { isZeroNotice } from './utils' + +import type { FormValues } from './types' + +export function TagBar() { + const { values } = useFormikContext() + + return {isZeroNotice(values) && Préavis Zéro} +} + +const Wrapper = styled.div` + display: flex; + gap: 8px; + margin-bottom: 8px; +` diff --git a/frontend/src/features/PriorNotification/components/PriorNotificationForm/constants.ts b/frontend/src/features/PriorNotification/components/PriorNotificationForm/constants.ts new file mode 100644 index 0000000000..43f2466f61 --- /dev/null +++ b/frontend/src/features/PriorNotification/components/PriorNotificationForm/constants.ts @@ -0,0 +1,50 @@ +import { ObjectSchema, array, boolean, number, object, string } from 'yup' + +import type { FormValues } from './types' +import type { PriorNotification } from '../../PriorNotification.types' + +export const BLUEFIN_TUNA_EXTENDED_SPECY_CODES = ['BF1', 'BF2', 'BF3'] + +const FISHING_CATCH_VALIDATION_SCHEMA: ObjectSchema = object({ + quantity: number(), + specyCode: string().required(), + specyName: string().required(), + weight: number().required() +}) + +export const FORM_VALIDATION_SCHEMA: ObjectSchema = object({ + authorTrigram: string().trim().required('Veuillez indiquer votre trigramme.'), + didNotFishAfterZeroNotice: boolean().required(), + expectedArrivalDate: string().required("Veuillez indiquer la date d'arrivée estimée."), + expectedLandingDate: string().when('$isExpectedLandingDateSameAsExpectedArrivalDate', { + is: false, + then: schema => 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.'), + isExpectedLandingDateSameAsExpectedArrivalDate: boolean().required(), + note: string(), + portLocode: string().required("Veuillez indiquer le port d'arrivée."), + sentAt: string().required('Veuillez indiquer la date de réception du préavis.'), + tripGearCodes: array().of(string().required()).ensure().required().min(1, 'Veuillez sélectionner au moins un engin.'), + vesselId: number().required('Veuillez indiquer le navire concerné.') +}) + +export const INITIAL_FORM_VALUES: FormValues = { + authorTrigram: undefined, + didNotFishAfterZeroNotice: false, + expectedArrivalDate: undefined, + expectedLandingDate: undefined, + faoArea: undefined, + fishingCatches: [], + isExpectedLandingDateSameAsExpectedArrivalDate: false, + note: undefined, + portLocode: undefined, + sentAt: undefined, + tripGearCodes: [], + vesselId: undefined +} diff --git a/frontend/src/features/PriorNotification/components/PriorNotificationForm/fields/FormikFishingCatchesMultiSelect/index.tsx b/frontend/src/features/PriorNotification/components/PriorNotificationForm/fields/FormikFishingCatchesMultiSelect/index.tsx new file mode 100644 index 0000000000..b76acb958e --- /dev/null +++ b/frontend/src/features/PriorNotification/components/PriorNotificationForm/fields/FormikFishingCatchesMultiSelect/index.tsx @@ -0,0 +1,113 @@ +import { useGetSpeciesQuery } from '@api/specy' +import { useGetSpeciesAsOptions } from '@hooks/useGetSpeciesAsOptions' +import { FormikNumberInput, Select, SingleTag } from '@mtes-mct/monitor-ui' +import { assertNotNullish } from '@utils/assertNotNullish' +import { useField } from 'formik' +import { Fragment } from 'react/jsx-runtime' +import styled from 'styled-components' + +import { InputRow } from './styles' +import { getFishingsCatchesExtraFields } from './utils' +import { BLUEFIN_TUNA_EXTENDED_SPECY_CODES } from '../../constants' +import { getFishingsCatchesInitialValues } from '../../utils' + +import type { PriorNotification } from '../../../../PriorNotification.types' + +// TODO Is the species name really useful since the Backend fills it? +export function FormikFishingCatchesMultiSelect() { + const [input, meta, helper] = useField('fishingCatches') + const { speciesAsOptions } = useGetSpeciesAsOptions() + const { data: speciesAndGroups } = useGetSpeciesQuery() + + const filteredSpeciesAsOptions = speciesAsOptions?.filter(specyOption => + input.value.every(fishingCatch => fishingCatch.specyCode !== specyOption.value) + ) + + const add = (specyCode: string | undefined) => { + const specyOption = speciesAsOptions?.find(({ value }) => value === specyCode) + if (!specyOption) { + return + } + + const specyName = speciesAndGroups?.species.find(specy => specy.code === specyOption.value)?.name + assertNotNullish(specyName) + const nextFishingCatches = [...input.value, ...getFishingsCatchesInitialValues(specyOption.value, 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) + } + + return ( + <> + ` box-sizing: border-box; - width: ${p => (p.isExtended ? p.extendedWidth : 320)}px; + width: ${p => (p.$isExtended && p.$extendedWidth !== undefined ? p.$extendedWidth : 320)}px; transition: all 0.7s; * { @@ -236,14 +240,12 @@ const Wrapper = styled.div<{ ` const Input = styled.input<{ - baseUrl: string - extendedWidth: number - flagState: string | undefined - hasError: boolean | undefined - isExtended: boolean + $baseUrl: string + $flagState: string | undefined + $hasError: boolean | undefined }>` margin: 0; - border: ${p => (p.hasError ? '1px solid red' : 'none')}; + border: ${p => (p.$hasError ? '1px solid red' : 'none')}; border-radius: 0; border-radius: 2px; color: ${p => p.theme.color.gunMetal}; @@ -255,11 +257,11 @@ const Input = styled.input<{ flex: 3; transition: all 0.7s; background: ${p => - p.flagState ? `url(${p.baseUrl}/flags/${p.flagState.toLowerCase()}.svg) no-repeat scroll, white` : 'white'}; + p.$flagState ? `url(${p.$baseUrl}/flags/${p.$flagState.toLowerCase()}.svg) no-repeat scroll, white` : 'white'}; background-size: 20px; background-position-y: center; background-position-x: 16px; - padding-left: ${p => (p.flagState ? 45 : 16)}px; + padding-left: ${p => (p.$flagState ? 45 : 16)}px; :disabled { background-color: var(--rs-input-disabled-bg); diff --git a/frontend/src/hooks/useGetFaoAreasAsOptions.ts b/frontend/src/hooks/useGetFaoAreasAsOptions.ts new file mode 100644 index 0000000000..a8b742797b --- /dev/null +++ b/frontend/src/hooks/useGetFaoAreasAsOptions.ts @@ -0,0 +1,36 @@ +import { useGetFaoAreasQuery } from '@api/faoAreas' +import { useMemo } from 'react' + +import type { Option } from '@mtes-mct/monitor-ui' + +/** + * Fetches FAO areas and returns them as options. + */ +export function useGetFaoAreasAsOptions() { + const { data: faoAreas, error, isLoading } = useGetFaoAreasQuery() + + const faoAreasAsOptions: Option[] | undefined = useMemo( + () => { + if (!faoAreas) { + return undefined + } + + return faoAreas + .map(faoArea => ({ + label: faoArea, + value: faoArea + })) + .sort((a, b) => a.label.localeCompare(b.label)) + }, + + // FAO areas are not expected to change. + // eslint-disable-next-line react-hooks/exhaustive-deps + [isLoading] + ) + + return { + error, + faoAreasAsOptions, + isLoading + } +} diff --git a/frontend/src/hooks/useGetGearsAsOptions.ts b/frontend/src/hooks/useGetGearsAsOptions.ts new file mode 100644 index 0000000000..25802facb0 --- /dev/null +++ b/frontend/src/hooks/useGetGearsAsOptions.ts @@ -0,0 +1,36 @@ +import { useGetGearsQuery } from '@api/gear' +import { useMemo } from 'react' + +import type { Option } from '@mtes-mct/monitor-ui' + +/** + * Fetches gears and returns them as options with their `code` property as option value. + */ +export function useGetGearsAsOptions() { + const { data: gears, error, isLoading } = useGetGearsQuery() + + const gearsAsOptions: Option[] | undefined = useMemo( + () => { + if (!gears) { + return undefined + } + + return gears + .map(gear => ({ + label: `${gear.name} (${gear.code})`, + value: gear.code + })) + .sort((a, b) => a.label.localeCompare(b.label)) + }, + + // Gears are not expected to change. + // eslint-disable-next-line react-hooks/exhaustive-deps + [isLoading] + ) + + return { + error, + gearsAsOptions, + isLoading + } +} diff --git a/frontend/src/hooks/useGetPortsAsOptions.ts b/frontend/src/hooks/useGetPortsAsOptions.ts new file mode 100644 index 0000000000..fb7990b701 --- /dev/null +++ b/frontend/src/hooks/useGetPortsAsOptions.ts @@ -0,0 +1,35 @@ +import { useGetPortsQuery } from '@api/port' +import { type Option } from '@mtes-mct/monitor-ui' +import { useMemo } from 'react' + +/** + * Fetches ports and returns them as options with their `locode` property as option value. + */ +export function useGetPortsAsOptions() { + const { data: ports, error, isLoading } = useGetPortsQuery() + + const portsAsOptions: Option[] | undefined = useMemo( + () => { + if (!ports) { + return undefined + } + + return ports + .map(port => ({ + label: `${port.name} (${port.locode})`, + value: port.locode + })) + .sort((a, b) => a.label.localeCompare(b.label)) + }, + + // Ports are not expected to change. + // eslint-disable-next-line react-hooks/exhaustive-deps + [isLoading] + ) + + return { + error, + isLoading, + portsAsOptions + } +} diff --git a/frontend/src/libs/DisplayedError/constants.ts b/frontend/src/libs/DisplayedError/constants.ts index e39930bfbb..0bb7886782 100644 --- a/frontend/src/libs/DisplayedError/constants.ts +++ b/frontend/src/libs/DisplayedError/constants.ts @@ -1,6 +1,7 @@ export enum DisplayedErrorKey { MISSION_FORM_ERROR = 'missionFormError', SIDE_WINDOW_PRIOR_NOTIFICATION_CARD_ERROR = 'sideWindowPriorNotificationCardError', + SIDE_WINDOW_PRIOR_NOTIFICATION_FORM_ERROR = 'sideWindowPriorNotificationFormError', SIDE_WINDOW_PRIOR_NOTIFICATION_LIST_ERROR = 'sideWindowPriorNotificationListError', VESSEL_SIDEBAR_ERROR = 'vesselSidebarError' }