From 91af5ed729d5add950473e205d35d5b42f794b27 Mon Sep 17 00:00:00 2001 From: Ivan Gabriele Date: Mon, 3 Jun 2024 23:59:56 +0200 Subject: [PATCH 01/26] Add & integrate manual prior notifications SQL table --- .../domain/entities/logbook/LogbookMessage.kt | 7 +- .../GetPriorNotification.kt | 20 +- .../use_cases/vessel/GetLogbookMessages.kt | 16 +- .../light/outputs/LogbookMessageDataOutput.kt | 2 +- .../api/outputs/LogbookMessageDataOutput.kt | 16 +- .../outputs/PriorNotificationDataOutput.kt | 6 + .../database/entities/LogbookReportEntity.kt | 54 +++-- .../interfaces/DBLogbookReportRepository.kt | 198 +++++++++++++----- ....256__Create_prior_notifications_table.sql | 44 ++++ .../V666.2.1__Insert_more_dummy_vessels.sql | 4 + .../V666.5.2__Insert_prior_notifications.sql | 7 + .../V666.2.1__Insert_more_dummy_vessels.jsonc | 26 +++ ...V666.5.2__Insert_prior_notifications.jsonc | 40 ++++ .../entities/logbook/LogbookMessageUTests.kt | 3 + .../use_cases/GetLogbookMessagesUTests.kt | 17 +- .../monitorfish/domain/use_cases/TestUtils.kt | 75 +++++++ .../GetPriorNotificationUTests.kt | 6 + .../GetPriorNotificationsUTests.kt | 6 + .../bff/PriorNotificationControllerITests.kt | 9 + .../JpaFleetSegmentRepositoryITests.kt | 4 +- .../JpaLogbookReportRepositoryITests.kt | 18 ++ .../features/Logbook/LogbookMessage.types.ts | 5 +- .../PriorNotification.types.ts | 3 + 23 files changed, 487 insertions(+), 99 deletions(-) create mode 100644 backend/src/main/resources/db/migration/internal/V0.256__Create_prior_notifications_table.sql create mode 100644 backend/src/main/resources/db/testdata/V666.5.2__Insert_prior_notifications.sql create mode 100644 backend/src/main/resources/db/testdata/json/V666.5.2__Insert_prior_notifications.jsonc 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..84c21daf7a 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 @@ -12,7 +12,7 @@ import fr.gouv.cnsp.monitorfish.domain.entities.logbook.Gear as LogbookGear data class LogbookMessage( val id: Long, val reportId: String? = null, - val operationNumber: String, + val operationNumber: String?, val tripNumber: String? = null, val referencedReportId: String? = null, val operationDateTime: ZonedDateTime, @@ -29,19 +29,22 @@ data class LogbookMessage( val integrationDateTime: ZonedDateTime, val analyzedByRules: List, var rawMessage: String? = null, - val transmissionFormat: LogbookTransmissionFormat, + val transmissionFormat: LogbookTransmissionFormat?, val software: String? = null, var acknowledgment: Acknowledgment? = null, + var createdAt: ZonedDateTime?, var isCorrectedByNewerMessage: Boolean = false, var isDeleted: Boolean = false, val isEnriched: Boolean = false, + val isManuallyCreated: Boolean, var isSentByFailoverSoftware: Boolean = false, val message: LogbookMessageValue? = null, val messageType: String? = null, val operationType: LogbookOperationType, val tripGears: List? = emptyList(), val tripSegments: List? = emptyList(), + val updatedAt: ZonedDateTime?, ) { private val logger = LoggerFactory.getLogger(LogbookMessage::class.java) 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..fcf0612cb2 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 @@ -37,15 +37,17 @@ class GetPriorNotification( .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 logbookMessageWithRawMessage = logbookMessage.operationNumber?.let { operationNumber -> + logbookMessage.copy( + rawMessage = try { + logbookRawMessageRepository.findRawMessage(operationNumber) + } catch (e: NoERSMessagesFound) { + logger.warn(e.message) + + null + }, + ) + } ?: logbookMessage val port = try { priorNotification.logbookMessageTyped.typedMessage.port?.let { portLocode -> 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/infrastructure/api/light/outputs/LogbookMessageDataOutput.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/light/outputs/LogbookMessageDataOutput.kt index c5dc57a22d..a519e9c346 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, 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..725c81b347 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,17 +20,20 @@ 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 createdAt: ZonedDateTime?, + val isCorrectedByNewerMessage: Boolean, + val isDeleted: Boolean, + val isManuallyCreated: Boolean, val isSentByFailoverSoftware: Boolean, val message: LogbookMessageValue?, val messageType: String?, val operationType: LogbookOperationType, val tripGears: List?, val tripSegments: List?, + val updatedAt: ZonedDateTime?, ) { companion object { fun fromLogbookMessage(logbookMessage: LogbookMessage): LogbookMessageDataOutput { @@ -58,14 +61,17 @@ data class LogbookMessageDataOutput( rawMessage = logbookMessage.rawMessage, acknowledgment = logbookMessage.acknowledgment, + createdAt = logbookMessage.createdAt, isCorrectedByNewerMessage = logbookMessage.isCorrectedByNewerMessage, isDeleted = logbookMessage.isDeleted, + isManuallyCreated = logbookMessage.isManuallyCreated, isSentByFailoverSoftware = logbookMessage.isSentByFailoverSoftware, message = logbookMessage.message, messageType = logbookMessage.messageType, operationType = logbookMessage.operationType, tripGears = tripGears, tripSegments = tripSegments, + updatedAt = logbookMessage.updatedAt, ) } } 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..0c06173d33 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 @@ -11,12 +11,14 @@ data class PriorNotificationDataOutput( /** 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?, @@ -28,6 +30,7 @@ data class PriorNotificationDataOutput( val tripGears: List, val tripSegments: List, val types: List, + val updatedAt: String?, val vesselId: Int?, val vesselExternalReferenceNumber: String?, val vesselFlagCountryCode: CountryCode, @@ -68,11 +71,13 @@ data class PriorNotificationDataOutput( return PriorNotificationDataOutput( id = referenceReportId, acknowledgment = acknowledgment, + createdAt = logbookMessage.createdAt.toString(), expectedArrivalDate = message.predictedArrivalDatetimeUtc?.toString(), expectedLandingDate = message.predictedLandingDatetimeUtc?.toString(), hasVesselRiskFactorSegments = priorNotification.vesselRiskFactor?.segments?.isNotEmpty(), fingerprint = priorNotification.fingerprint, isCorrection = logbookMessage.operationType === LogbookOperationType.COR, + isManuallyCreated = logbookMessage.isManuallyCreated, isVesselUnderCharter = priorNotification.vessel.underCharter, onBoardCatches, portLocode = priorNotification.port?.locode, @@ -84,6 +89,7 @@ data class PriorNotificationDataOutput( tripGears, tripSegments, types, + updatedAt = logbookMessage.updatedAt.toString(), vesselId = priorNotification.vessel.id, vesselExternalReferenceNumber = priorNotification.vessel.externalReferenceNumber, vesselFlagCountryCode = priorNotification.vessel.flagState, 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..cadad62f5e 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 @@ -13,6 +13,7 @@ import org.hibernate.annotations.Type import org.hibernate.dialect.PostgreSQLEnumJdbcType import java.time.Instant import java.time.ZoneOffset.UTC +import java.time.ZonedDateTime @Entity @Table(name = "logbook_reports") @@ -23,58 +24,64 @@ 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, + val reportDateTime: Instant?, @Column(name = "cfr") - val internalReferenceNumber: String? = null, + val internalReferenceNumber: String?, @Column(name = "ircs") - val ircs: String? = null, + val ircs: String?, @Column(name = "external_identification") - val externalReferenceNumber: String? = null, + val externalReferenceNumber: String?, @Column(name = "vessel_name") - val vesselName: String? = null, + val vesselName: String?, // ISO Alpha-3 country code @Column(name = "flag_state") - val flagState: String? = null, + val flagState: 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, + val tripGears: String?, @Type(JsonBinaryType::class) @Column(name = "trip_segments", nullable = true, columnDefinition = "jsonb") - val tripSegments: String? = null, + val tripSegments: String?, + @Column(name = "is_manually_created", nullable = false) + val isManuallyCreated: Boolean, + @Column(name = "created_at") + val createdAt: ZonedDateTime?, + @Column(name = "updated_at") + val updatedAt: ZonedDateTime?, ) { companion object { fun fromLogbookMessage( @@ -98,10 +105,16 @@ data class LogbookReportEntity( software = logbookMessage.software, transmissionFormat = logbookMessage.transmissionFormat, + createdAt = logbookMessage.createdAt, isEnriched = logbookMessage.isEnriched, + isManuallyCreated = logbookMessage.isManuallyCreated, message = mapper.writeValueAsString(logbookMessage.message), messageType = logbookMessage.messageType, + operationCountry = null, operationType = logbookMessage.operationType, + tripGears = null, + tripSegments = null, + updatedAt = logbookMessage.updatedAt, ) } @@ -129,12 +142,15 @@ data class LogbookReportEntity( software = software, transmissionFormat = transmissionFormat, + createdAt = createdAt, isEnriched = isEnriched, + isManuallyCreated = isManuallyCreated, message = message, messageType = messageType, operationType = operationType, tripGears = tripGears, tripSegments = tripSegments, + updatedAt = updatedAt, ) } 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..de3880e2f7 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,32 +15,103 @@ interface DBLogbookReportRepository : @Query( """ WITH - dat_and_cor_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, - (SELECT array_agg(catchOnboard->>'species') FROM jsonb_array_elements(lr.value->'catchOnboard') AS catchOnboard) AS specy_codes, - (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 + dat_and_cor_prior_notifications AS ( + SELECT * + FROM logbook_reports WHERE -- This filter helps Timescale optimize the query since `operation_datetime_utc` is indexed - lr.operation_datetime_utc + operation_datetime_utc BETWEEN CAST(:willArriveAfter AS TIMESTAMP) - INTERVAL '48 hours' AND CAST(:willArriveBefore AS TIMESTAMP) + INTERVAL '48 hours' - AND lr.log_type = 'PNO' - AND lr.operation_type IN ('DAT', 'COR') - AND lr.enriched = TRUE + AND log_type = 'PNO' + AND operation_type IN ('DAT', 'COR') + AND enriched = TRUE -- Flag States - AND (:flagStates IS NULL OR lr.flag_state IN (:flagStates)) + AND (:flagStates IS NULL OR flag_state IN (:flagStates)) + + -- Port Locodes + AND (:portLocodes IS NULL OR value->>'port' IN (:portLocodes)) + + -- Search Query + AND (:searchQuery IS NULL OR unaccent(lower(vessel_name)) ILIKE CONCAT('%', unaccent(lower(:searchQuery)), '%')) + + -- Will Arrive After + AND value->>'predictedArrivalDatetimeUtc' >= :willArriveAfter + + -- Will Arrive Before + AND value->>'predictedArrivalDatetimeUtc' <= :willArriveBefore + + UNION ALL + + SELECT + id, + CAST(NULL AS TEXT) AS operation_number, + CAST(NULL AS TEXT) AS operation_country, + operation_datetime_utc, + CAST('DAT' AS TEXT) AS operation_type, + report_id, + CAST(NULL AS TEXT) AS referenced_report_id, + report_datetime_utc, + cfr, + CAST(NULL AS TEXT) AS ircs, + CAST(NULL AS TEXT) AS external_identification, + vessel_name, + flag_state, + CAST(NULL AS TEXT) AS imo, + CAST('PNO' AS TEXT) AS log_type, + value, + integration_datetime_utc, + CAST(NULL AS TEXT) AS trip_number, + analyzed_by_rules, + CAST(TRUE AS BOOLEAN) AS trip_number_was_computed, + -- TODO /!\ CHECK IF THIS IS WHAT WE WANT /!\ + CAST(NULL AS public.logbook_message_transmission_format) AS transmission_format, + CAST(NULL AS TEXT) AS software, + CAST(TRUE AS BOOLEAN) AS enriched, + trip_gears, + trip_segments, + is_manually_created, + created_at, + updated_at + FROM prior_notifications + WHERE + -- TODO /!\ INDEX operation_datetime_utc WITH TIMESCALE /!\ + -- This filter helps Timescale optimize the query since `operation_datetime_utc` is indexed + operation_datetime_utc + BETWEEN CAST(:willArriveAfter AS TIMESTAMP) - INTERVAL '48 hours' + AND CAST(:willArriveBefore AS TIMESTAMP) + INTERVAL '48 hours' + + -- Flag States + AND (:flagStates IS NULL OR flag_state IN (:flagStates)) + + -- Port Locodes + AND (:portLocodes IS NULL OR value->>'port' IN (:portLocodes)) + -- Search Query + AND (:searchQuery IS NULL OR unaccent(lower(vessel_name)) ILIKE CONCAT('%', unaccent(lower(:searchQuery)), '%')) + + -- Will Arrive After + AND value->>'predictedArrivalDatetimeUtc' >= :willArriveAfter + + -- Will Arrive Before + AND value->>'predictedArrivalDatetimeUtc' <= :willArriveBefore + ), + + dat_and_cor_prior_notifications_with_extra_columns AS ( + SELECT + dacpn.*, + (SELECT array_agg(pnoTypes->>'pnoTypeName') FROM jsonb_array_elements(dacpn.value->'pnoTypes') AS pnoTypes) AS prior_notification_type_names, + (SELECT array_agg(catchOnboard->>'species') FROM jsonb_array_elements(dacpn.value->'catchOnboard') AS catchOnboard) AS specy_codes, + (SELECT array_agg(tripGears->>'gear') FROM jsonb_array_elements(dacpn.trip_gears) AS tripGears) AS trip_gear_codes, + (SELECT array_agg(tripSegments->>'segment') FROM jsonb_array_elements(dacpn.trip_segments) AS tripSegments) AS trip_segment_codes + FROM dat_and_cor_prior_notifications dacpn + LEFT JOIN risk_factors rf ON dacpn.cfr = rf.cfr + LEFT JOIN vessels v ON dacpn.cfr = v.cfr + WHERE -- Is Less Than Twelve Meters Vessel - AND ( + ( :isLessThanTwelveMetersVessel IS NULL OR (:isLessThanTwelveMetersVessel = TRUE AND v.length < 12) OR (:isLessThanTwelveMetersVessel = FALSE AND v.length >= 12) @@ -51,23 +122,11 @@ interface DBLogbookReportRepository : -- Last Controlled Before AND (:lastControlledBefore IS NULL OR rf.last_control_datetime_utc <= CAST(:lastControlledBefore AS TIMESTAMP)) - - -- Port Locodes - AND (:portLocodes IS NULL OR lr.value->>'port' IN (:portLocodes)) - - -- Search Query - AND (:searchQuery IS NULL OR unaccent(lower(lr.vessel_name)) ILIKE CONCAT('%', unaccent(lower(:searchQuery)), '%')) - - -- Will Arrive After - AND lr.value->>'predictedArrivalDatetimeUtc' >= :willArriveAfter - - -- Will Arrive Before - AND lr.value->>'predictedArrivalDatetimeUtc' <= :willArriveBefore ), distinct_cfrs AS ( SELECT DISTINCT cfr - FROM dat_and_cor_logbook_reports_with_extra_columns + FROM dat_and_cor_prior_notifications_with_extra_columns ), cfr_reporting_counts AS ( @@ -83,17 +142,17 @@ interface DBLogbookReportRepository : GROUP BY cfr ), - dat_and_cor_logbook_reports_with_extra_columns_and_reporting_count AS ( + dat_and_cor_prior_notifications_with_extra_columns_and_reporting_count AS ( SELECT - daclr.*, + dacpnwecarc.*, 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_prior_notifications_with_extra_columns dacpnwecarc + LEFT JOIN cfr_reporting_counts crc ON dacpnwecarc.cfr = crc.cfr ), - dat_and_cor_logbook_reports AS ( + filtered_dat_and_cor_prior_notifications AS ( SELECT * - FROM dat_and_cor_logbook_reports_with_extra_columns_and_reporting_count + FROM dat_and_cor_prior_notifications_with_extra_columns_and_reporting_count WHERE -- Has One Or More Reportings ( @@ -115,7 +174,7 @@ interface DBLogbookReportRepository : AND (:tripSegmentCodesAsSqlArrayString IS NULL OR trip_segment_codes && CAST(:tripSegmentCodesAsSqlArrayString AS TEXT[])) ), - del_logbook_reports AS ( + del_prior_notifications AS ( SELECT lr.*, CAST(NULL AS TEXT[]) AS prior_notification_type_names, @@ -124,7 +183,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_prior_notifications fdacpn ON lr.referenced_report_id = fdacpn.report_id WHERE -- This filter helps Timescale optimize the query since `operation_datetime_utc` is indexed lr.operation_datetime_utc @@ -134,7 +193,7 @@ interface DBLogbookReportRepository : AND lr.operation_type = 'DEL' ), - ret_logbook_reports AS ( + ret_prior_notifications AS ( SELECT lr.*, CAST(NULL AS TEXT[]) AS prior_notification_type_names, @@ -143,7 +202,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_prior_notifications fdacpn ON lr.referenced_report_id = fdacpn.report_id WHERE -- This filter helps Timescale optimize the query since `operation_datetime_utc` is indexed lr.operation_datetime_utc @@ -154,17 +213,17 @@ interface DBLogbookReportRepository : ) SELECT * - FROM dat_and_cor_logbook_reports + FROM filtered_dat_and_cor_prior_notifications UNION SELECT * - FROM del_logbook_reports + FROM del_prior_notifications UNION SELECT * - FROM ret_logbook_reports; + FROM ret_prior_notifications; """, nativeQuery = true, ) @@ -192,23 +251,68 @@ 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 = ?1 + 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 = ?1 + 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) + + UNION ALL + + SELECT + id, + CAST(NULL AS TEXT) AS operation_number, + CAST(NULL AS TEXT) AS operation_country, + operation_datetime_utc, + CAST('DAT' AS TEXT) AS operation_type, + CAST(report_id AS TEXT) AS report_id, + CAST(NULL AS TEXT) AS referenced_report_id, + report_datetime_utc, + cfr, + CAST(NULL AS TEXT) AS ircs, + CAST(NULL AS TEXT) AS external_identification, + vessel_name, + flag_state, + CAST(NULL AS TEXT) AS imo, + CAST('PNO' AS TEXT) AS log_type, + value, + integration_datetime_utc, + CAST(NULL AS TEXT) AS trip_number, + analyzed_by_rules, + CAST(TRUE AS BOOLEAN) AS trip_number_was_computed, + -- TODO /!\ CHECK IF THIS IS WHAT WE WANT /!\ + CAST(NULL AS public.logbook_message_transmission_format) AS transmission_format, + CAST(NULL AS TEXT) AS software, + CAST(TRUE AS BOOLEAN) AS enriched, + trip_gears, + trip_segments, + is_manually_created, + created_at, + updated_at + FROM prior_notifications + WHERE + report_id = ?1 + ORDER BY - report_datetime_utc + report_datetime_utc; """, nativeQuery = true, ) diff --git a/backend/src/main/resources/db/migration/internal/V0.256__Create_prior_notifications_table.sql b/backend/src/main/resources/db/migration/internal/V0.256__Create_prior_notifications_table.sql new file mode 100644 index 0000000000..975b54d945 --- /dev/null +++ b/backend/src/main/resources/db/migration/internal/V0.256__Create_prior_notifications_table.sql @@ -0,0 +1,44 @@ +CREATE EXTENSION IF NOT EXISTS pgcrypto; + +CREATE TABLE public.prior_notifications ( + -- Is there a risk of racing condition with `logbook_report_id_seq`? + id BIGINT DEFAULT nextval('public.logbook_report_id_seq'::regclass) NOT NULL, + -- operation_number VARCHAR(100), + -- operation_country VARCHAR(3), + operation_datetime_utc TIMESTAMP WITHOUT TIME ZONE NOT NULL, + -- operation_type VARCHAR(3), + report_id VARCHAR(36) PRIMARY KEY DEFAULT gen_random_uuid(), + -- referenced_report_id VARCHAR(100), + report_datetime_utc TIMESTAMP WITHOUT TIME ZONE, + cfr VARCHAR(12), + -- ircs VARCHAR(7), + -- external_identification VARCHAR(14), + vessel_name VARCHAR(100), + flag_state VARCHAR(3), + -- imo VARCHAR(20), + -- log_type VARCHAR(100), + value JSONB, + integration_datetime_utc TIMESTAMP WITHOUT TIME ZONE, + -- trip_number VARCHAR(100), + analyzed_by_rules VARCHAR(100)[], + -- trip_number_was_computed BOOLEAN DEFAULT FALSE, + -- transmission_format public.logbook_message_transmission_format NOT NULL, + -- software VARCHAR(100), + -- enriched BOOLEAN DEFAULT FALSE NOT NULL, + trip_gears JSONB, + trip_segments JSONB, + is_manually_created BOOLEAN DEFAULT TRUE NOT NULL, + created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, + updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL +); + +ALTER TABLE public.logbook_reports + ADD COLUMN is_manually_created BOOLEAN DEFAULT FALSE NOT NULL, + ADD COLUMN created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now(), + ADD COLUMN updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now(); + +UPDATE public.logbook_reports +SET + created_at = operation_datetime_utc, + updated_at = operation_datetime_utc +WHERE operation_datetime_utc IS NOT NULL; 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..9a7ca720ce 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 @@ -24,3 +24,7 @@ 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); diff --git a/backend/src/main/resources/db/testdata/V666.5.2__Insert_prior_notifications.sql b/backend/src/main/resources/db/testdata/V666.5.2__Insert_prior_notifications.sql new file mode 100644 index 0000000000..29be1037af --- /dev/null +++ b/backend/src/main/resources/db/testdata/V666.5.2__Insert_prior_notifications.sql @@ -0,0 +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 prior_notifications (id, report_id, cfr, flag_state, integration_datetime_utc, operation_datetime_utc, report_datetime_utc, vessel_name, trip_gears, trip_segments, value) VALUES (116, 'd2bd7d28-3130-4e3f-bf0d-5e919a5657be', 'CFR112', 'FRA', NOW() AT TIME ZONE 'UTC' - INTERVAL '15 minutes', NOW() AT TIME ZONE 'UTC' - INTERVAL '15 minutes', NOW() AT TIME ZONE 'UTC' - INTERVAL '15 minutes', 'POISSON PAS NET', '[{"gear":"LNP","mesh":8,"dimensions":"50;200"}]', '[{"segment":"NWW09","segmentName":"Lignes"}]', '{"catchOnboard":[{"weight":72,"nbFish":null,"species":"SOS"}],"pnoTypes":[{"pnoTypeName":"Préavis type D","minimumNotificationPeriod":4,"hasDesignatedPorts":true}],"port":"FRVNE","predictedArrivalDatetimeUtc":null,"predictedLandingDatetimeUtc":null,"purpose":"LAN","tripStartDate":null}'); +UPDATE 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 id = 116; +UPDATE 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 id = 116; +UPDATE 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 id = 116; 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..685320563d 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 @@ -160,6 +160,32 @@ "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 } ] } diff --git a/backend/src/main/resources/db/testdata/json/V666.5.2__Insert_prior_notifications.jsonc b/backend/src/main/resources/db/testdata/json/V666.5.2__Insert_prior_notifications.jsonc new file mode 100644 index 0000000000..5e2c03e86d --- /dev/null +++ b/backend/src/main/resources/db/testdata/json/V666.5.2__Insert_prior_notifications.jsonc @@ -0,0 +1,40 @@ +[ + { + "table": "prior_notifications", + "data": [ + { + "id": 116, + "report_id": "d2bd7d28-3130-4e3f-bf0d-5e919a5657be", + "cfr": "CFR112", + "flag_state": "FRA", + "integration_datetime_utc:sql": "NOW() AT TIME ZONE 'UTC' - INTERVAL '15 minutes'", + "operation_datetime_utc:sql": "NOW() AT TIME ZONE 'UTC' - INTERVAL '15 minutes'", + "report_datetime_utc:sql": "NOW() AT TIME ZONE 'UTC' - INTERVAL '15 minutes'", + "vessel_name": "POISSON PAS NET", + "trip_gears:jsonb": [{ "gear": "LNP", "mesh": 8, "dimensions": "50;200" }], + "trip_segments:jsonb": [{ "segment": "NWW09", "segmentName": "Lignes" }], + "value:jsonb": { + "catchOnboard": [ + { + "weight": 72.0, + "nbFish": null, + "species": "SOS" + } + ], + "pnoTypes": [ + { + "pnoTypeName": "Préavis type D", + "minimumNotificationPeriod": 4.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 '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\"')" + } + } + ] + } +] diff --git a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/LogbookMessageUTests.kt b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/LogbookMessageUTests.kt index ec9c19135f..cd3eebfb29 100644 --- a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/LogbookMessageUTests.kt +++ b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/LogbookMessageUTests.kt @@ -28,14 +28,17 @@ class LogbookMessageUTests { reportId = reportId, referencedReportId = referenceReportId, analyzedByRules = emptyList(), + createdAt = ZonedDateTime.now(), integrationDateTime = ZonedDateTime.now(), isEnriched = false, + isManuallyCreated = false, message = message, operationDateTime = ZonedDateTime.now(), operationNumber = "FAKE_OPERATION_NUMBER_$reportId", operationType = operationType, reportDateTime = reportDateTime, transmissionFormat = LogbookTransmissionFormat.ERS, + updatedAt = ZonedDateTime.now(), ) } } 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..da0dc5aab1 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,23 @@ class GetLogbookMessagesUTests { getDummyRETLogbookMessages() + LogbookMessage( id = 2, - analyzedByRules = listOf(), - operationNumber = "", reportId = "9065646816", referencedReportId = "9065646811", - operationType = LogbookOperationType.RET, - messageType = "", + analyzedByRules = listOf(), + createdAt = ZonedDateTime.now(), + integrationDateTime = ZonedDateTime.now(), + isEnriched = false, + isManuallyCreated = 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(), + updatedAt = 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..7707554d35 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 @@ -126,6 +126,8 @@ object TestUtils { LogbookMessage( id = 2, analyzedByRules = listOf(), + createdAt = ZonedDateTime.now(), + isManuallyCreated = false, operationNumber = "", tripNumber = "345", reportId = "", @@ -147,10 +149,13 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), + updatedAt = ZonedDateTime.now(), ), LogbookMessage( id = 1, analyzedByRules = listOf(), + createdAt = ZonedDateTime.now(), + isManuallyCreated = false, operationNumber = "", tripNumber = "345", reportId = "", @@ -172,10 +177,13 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), + updatedAt = ZonedDateTime.now(), ), LogbookMessage( id = 3, analyzedByRules = listOf(), + createdAt = ZonedDateTime.now(), + isManuallyCreated = false, operationNumber = "", tripNumber = "345", reportId = "", @@ -198,10 +206,13 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), + updatedAt = ZonedDateTime.now(), ), LogbookMessage( id = 3, analyzedByRules = listOf(), + createdAt = ZonedDateTime.now(), + isManuallyCreated = false, operationNumber = "", tripNumber = "345", reportId = "", @@ -224,10 +235,13 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), + updatedAt = ZonedDateTime.now(), ), LogbookMessage( id = 4, analyzedByRules = listOf(), + createdAt = ZonedDateTime.now(), + isManuallyCreated = false, operationNumber = "", tripNumber = "345", reportId = "", @@ -243,10 +257,13 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), + updatedAt = ZonedDateTime.now(), ), LogbookMessage( id = 5, analyzedByRules = listOf(), + createdAt = ZonedDateTime.now(), + isManuallyCreated = false, operationNumber = "", tripNumber = "345", reportId = "", @@ -262,6 +279,7 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), + updatedAt = ZonedDateTime.now(), ), ) } @@ -299,6 +317,8 @@ object TestUtils { LogbookMessage( id = 1, analyzedByRules = listOf(), + createdAt = ZonedDateTime.now(), + isManuallyCreated = false, operationNumber = "", tripNumber = "345", reportId = "", @@ -321,10 +341,13 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), + updatedAt = ZonedDateTime.now(), ), LogbookMessage( id = 2, analyzedByRules = listOf(), + createdAt = ZonedDateTime.now(), + isManuallyCreated = false, operationNumber = "", tripNumber = "345", reportId = "", @@ -347,10 +370,13 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), + updatedAt = ZonedDateTime.now(), ), LogbookMessage( id = 3, analyzedByRules = listOf(), + createdAt = ZonedDateTime.now(), + isManuallyCreated = false, operationNumber = "", tripNumber = "345", reportId = "", @@ -373,6 +399,7 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), + updatedAt = ZonedDateTime.now(), ), ) } @@ -403,6 +430,8 @@ object TestUtils { LogbookMessage( id = 1, analyzedByRules = listOf(), + createdAt = ZonedDateTime.now(), + isManuallyCreated = false, operationNumber = "9065646811", tripNumber = "345", reportId = "9065646811", @@ -424,10 +453,13 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), + updatedAt = ZonedDateTime.now(), ), LogbookMessage( id = 2, analyzedByRules = listOf(), + createdAt = ZonedDateTime.now(), + isManuallyCreated = false, operationNumber = "", tripNumber = "345", reportId = "", @@ -450,6 +482,7 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), + updatedAt = ZonedDateTime.now(), ), ) } @@ -487,6 +520,8 @@ object TestUtils { LogbookMessage( id = 1, analyzedByRules = listOf(), + createdAt = ZonedDateTime.now(), + isManuallyCreated = false, operationNumber = "", tripNumber = "345", reportId = "9065646811", @@ -507,10 +542,13 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), + updatedAt = ZonedDateTime.now(), ), LogbookMessage( id = 2, analyzedByRules = listOf(), + createdAt = ZonedDateTime.now(), + isManuallyCreated = false, operationNumber = "", reportId = "9065646816", referencedReportId = "9065646811", @@ -531,10 +569,13 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), + updatedAt = ZonedDateTime.now(), ), LogbookMessage( id = 3, analyzedByRules = listOf(), + createdAt = ZonedDateTime.now(), + isManuallyCreated = false, operationNumber = "", tripNumber = "345", reportId = "9065646813", @@ -555,10 +596,13 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), + updatedAt = ZonedDateTime.now(), ), LogbookMessage( id = 4, analyzedByRules = listOf(), + createdAt = ZonedDateTime.now(), + isManuallyCreated = false, operationNumber = "", reportId = "9065646818", referencedReportId = "9065646813", @@ -579,10 +623,13 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), + updatedAt = ZonedDateTime.now(), ), LogbookMessage( id = 5, analyzedByRules = listOf(), + createdAt = ZonedDateTime.now(), + isManuallyCreated = false, operationNumber = "", referencedReportId = "9065646813", operationType = LogbookOperationType.DEL, @@ -602,10 +649,13 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), + updatedAt = ZonedDateTime.now(), ), LogbookMessage( id = 6, analyzedByRules = listOf(), + createdAt = ZonedDateTime.now(), + isManuallyCreated = false, operationNumber = "5h499-erh5u7-pm3ae8c5trj78j67dfh", tripNumber = "SCR-TTT20200505030505", reportId = "zegj15-zeg56-errg569iezz3659g", @@ -626,6 +676,7 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), + updatedAt = ZonedDateTime.now(), ), ) } @@ -714,6 +765,8 @@ object TestUtils { LogbookMessage( id = 1, analyzedByRules = listOf(), + createdAt = ZonedDateTime.now(), + isManuallyCreated = false, operationNumber = "456846844658", tripNumber = "125345", reportId = "456846844658", @@ -724,10 +777,13 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), + updatedAt = ZonedDateTime.now(), ), LogbookMessage( id = 2, analyzedByRules = listOf(), + createdAt = ZonedDateTime.now(), + isManuallyCreated = false, operationNumber = "47177857577", tripNumber = "125345", reportId = "47177857577", @@ -738,12 +794,15 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), + updatedAt = ZonedDateTime.now(), ), ), Pair( LogbookMessage( id = 3, analyzedByRules = listOf(), + createdAt = ZonedDateTime.now(), + isManuallyCreated = false, operationNumber = "48545254254", tripNumber = "125345", reportId = "48545254254", @@ -754,10 +813,13 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), + updatedAt = ZonedDateTime.now(), ), LogbookMessage( id = 4, analyzedByRules = listOf(), + createdAt = ZonedDateTime.now(), + isManuallyCreated = false, operationNumber = "004045204504", tripNumber = "125345", reportId = "004045204504", @@ -768,6 +830,7 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), + updatedAt = ZonedDateTime.now(), ), ), ) @@ -838,6 +901,8 @@ object TestUtils { LogbookMessage( id = 1, analyzedByRules = listOf(), + createdAt = ZonedDateTime.now(), + isManuallyCreated = false, operationNumber = "456846844658", tripNumber = "125345", reportId = "456846844658", @@ -848,10 +913,13 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), + updatedAt = ZonedDateTime.now(), ), LogbookMessage( id = 2, analyzedByRules = listOf(), + createdAt = ZonedDateTime.now(), + isManuallyCreated = false, operationNumber = "47177857577", tripNumber = "125345", reportId = "47177857577", @@ -862,12 +930,15 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), + updatedAt = ZonedDateTime.now(), ), ), Pair( LogbookMessage( id = 3, analyzedByRules = listOf(), + createdAt = ZonedDateTime.now(), + isManuallyCreated = false, operationNumber = "48545254254", tripNumber = "125345", reportId = "48545254254", @@ -878,10 +949,13 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), + updatedAt = ZonedDateTime.now(), ), LogbookMessage( id = 4, analyzedByRules = listOf(), + createdAt = ZonedDateTime.now(), + isManuallyCreated = false, operationNumber = "004045204504", tripNumber = "125345", reportId = "004045204504", @@ -892,6 +966,7 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), + updatedAt = ZonedDateTime.now(), ), ), ) 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..845f663220 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 @@ -56,16 +56,19 @@ class GetPriorNotificationUTests { reportId = "FAKE_REPORT_ID_1", referencedReportId = null, analyzedByRules = emptyList(), + createdAt = ZonedDateTime.now(), isDeleted = false, integrationDateTime = ZonedDateTime.now(), isCorrectedByNewerMessage = false, isEnriched = true, + isManuallyCreated = false, message = PNO(), messageType = "PNO", operationDateTime = ZonedDateTime.now(), operationNumber = "1", operationType = LogbookOperationType.DAT, transmissionFormat = LogbookTransmissionFormat.ERS, + updatedAt = ZonedDateTime.now(), ), ), reportingCount = null, @@ -116,16 +119,19 @@ class GetPriorNotificationUTests { reportId = null, referencedReportId = "FAKE_REPORT_ID_2", analyzedByRules = emptyList(), + createdAt = ZonedDateTime.now(), isDeleted = false, integrationDateTime = ZonedDateTime.now(), isCorrectedByNewerMessage = true, isEnriched = true, + isManuallyCreated = false, message = PNO(), messageType = "PNO", operationDateTime = ZonedDateTime.now(), operationNumber = "2", operationType = LogbookOperationType.COR, transmissionFormat = LogbookTransmissionFormat.ERS, + updatedAt = ZonedDateTime.now(), ), ), reportingCount = null, 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/GetPriorNotificationsUTests.kt index 2d0be88054..342253c34a 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/GetPriorNotificationsUTests.kt @@ -66,15 +66,18 @@ class GetPriorNotificationsUTests { reportId = "FAKE_REPORT_ID_1", referencedReportId = null, analyzedByRules = emptyList(), + createdAt = ZonedDateTime.now(), integrationDateTime = ZonedDateTime.now(), isCorrectedByNewerMessage = false, isDeleted = false, isEnriched = false, + isManuallyCreated = false, message = PNO(), operationDateTime = ZonedDateTime.now(), operationNumber = "1", operationType = LogbookOperationType.DAT, transmissionFormat = LogbookTransmissionFormat.ERS, + updatedAt = ZonedDateTime.now(), ), ), reportingCount = null, @@ -103,15 +106,18 @@ class GetPriorNotificationsUTests { reportId = "FAKE_REPORT_ID_2_COR", referencedReportId = "FAKE_NONEXISTENT_REPORT_ID_2", analyzedByRules = emptyList(), + createdAt = ZonedDateTime.now(), integrationDateTime = ZonedDateTime.now(), isCorrectedByNewerMessage = false, isDeleted = false, isEnriched = false, + isManuallyCreated = false, message = PNO(), operationDateTime = ZonedDateTime.now(), operationNumber = "1", operationType = LogbookOperationType.COR, transmissionFormat = LogbookTransmissionFormat.ERS, + updatedAt = ZonedDateTime.now(), ), ), reportingCount = null, 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..54685bedb7 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 @@ -57,15 +57,18 @@ class PriorNotificationControllerITests { reportId = "FAKE_REPORT_ID_1", referencedReportId = null, analyzedByRules = emptyList(), + createdAt = ZonedDateTime.now(), integrationDateTime = ZonedDateTime.now(), isCorrectedByNewerMessage = false, isDeleted = false, isEnriched = false, + isManuallyCreated = false, message = PNO(), operationDateTime = ZonedDateTime.now(), operationNumber = "1", operationType = LogbookOperationType.DAT, transmissionFormat = LogbookTransmissionFormat.ERS, + updatedAt = ZonedDateTime.now(), ), ), reportingCount = null, @@ -94,15 +97,18 @@ class PriorNotificationControllerITests { reportId = "FAKE_REPORT_ID_2_COR", referencedReportId = "FAKE_NONEXISTENT_REPORT_ID_2", analyzedByRules = emptyList(), + createdAt = ZonedDateTime.now(), integrationDateTime = ZonedDateTime.now(), isCorrectedByNewerMessage = true, isDeleted = false, isEnriched = false, + isManuallyCreated = false, message = PNO(), operationDateTime = ZonedDateTime.now(), operationNumber = "1", operationType = LogbookOperationType.COR, transmissionFormat = LogbookTransmissionFormat.ERS, + updatedAt = ZonedDateTime.now(), ), ), reportingCount = null, @@ -168,15 +174,18 @@ class PriorNotificationControllerITests { reportId = "FAKE_REPORT_ID_1", referencedReportId = null, analyzedByRules = emptyList(), + createdAt = ZonedDateTime.now(), integrationDateTime = ZonedDateTime.now(), isCorrectedByNewerMessage = false, isDeleted = false, isEnriched = true, + isManuallyCreated = false, message = PNO(), operationDateTime = ZonedDateTime.now(), operationNumber = "1", operationType = LogbookOperationType.DAT, transmissionFormat = LogbookTransmissionFormat.ERS, + updatedAt = ZonedDateTime.now(), ), ), reportingCount = null, 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..b297adeb06 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 @@ -1142,11 +1142,29 @@ class JpaLogbookReportRepositoryITests : AbstractDBTests() { return LogbookReportEntity( reportId = reportId, referencedReportId = referenceReportId, + analyzedByRules = null, + createdAt = ZonedDateTime.now(), + externalReferenceNumber = null, + flagState = null, integrationDateTime = ZonedDateTime.now().toInstant(), + internalReferenceNumber = null, + imo = null, + isManuallyCreated = false, + ircs = null, + message = null, + messageType = null, + operationCountry = null, operationDateTime = ZonedDateTime.now().toInstant(), operationNumber = "FAKE_OPERATION_NUMBER_$reportId", operationType = operationType, + reportDateTime = null, + software = null, transmissionFormat = LogbookTransmissionFormat.ERS, + tripGears = null, + tripNumber = null, + tripSegments = null, + updatedAt = ZonedDateTime.now(), + vesselName = null, ) } } 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..257ca55ebc 100644 --- a/frontend/src/features/PriorNotification/PriorNotification.types.ts +++ b/frontend/src/features/PriorNotification/PriorNotification.types.ts @@ -4,6 +4,7 @@ import type { LogbookMessage } from '@features/Logbook/LogbookMessage.types' export namespace PriorNotification { export type 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 From 6652754ebc0dc9f1740abb6987fa0232f60a82ac Mon Sep 17 00:00:00 2001 From: Ivan Gabriele Date: Tue, 4 Jun 2024 15:20:34 +0200 Subject: [PATCH 02/26] Add manual prior notification create & update in Backend --- .../monitorfish/config/MapperConfiguration.kt | 21 ++- .../entities/alerts/PNOAndLANCatches.kt | 6 +- .../domain/entities/logbook/Haul.kt | 2 +- .../{Catch.kt => LogbookFishingCatch.kt} | 2 +- .../domain/entities/logbook/LogbookMessage.kt | 17 +- .../logbook/{Gear.kt => LogbookTripGear.kt} | 2 +- .../domain/entities/logbook/messages/DEP.kt | 8 +- .../domain/entities/logbook/messages/DIS.kt | 4 +- .../domain/entities/logbook/messages/LAN.kt | 4 +- .../domain/entities/logbook/messages/PNO.kt | 23 ++- .../domain/entities/logbook/messages/RTP.kt | 4 +- .../repositories/LogbookReportRepository.kt | 9 +- .../PriorNotificationRepository.kt | 9 + .../ExecutePnoAndLanWeightToleranceRule.kt | 18 +- .../CreateOrUpdatePriorNotification.kt | 72 ++++++++ .../GetPriorNotification.kt | 5 +- .../api/bff/PriorNotificationController.kt | 44 +++++ .../api/input/LogbookFishingCatchInput.kt | 30 ++++ .../api/input/LogbookMessagePnoDataInput.kt | 35 ++++ .../api/input/LogbookTripGearDataInput.kt | 19 +++ .../api/input/PriorNotificationDataInput.kt | 7 + .../outputs/LogbookMessageCatchDataOutput.kt | 4 +- .../outputs/LogbookMessageGearDataOutput.kt | 4 +- .../database/entities/LogbookReportEntity.kt | 4 +- .../entities/ManualPriorNotificationEntity.kt | 113 +++++++++++++ .../JpaLogbookReportRepository.kt | 35 ++-- .../JpaPriorNotificationRepository.kt | 19 +++ .../interfaces/DBLogbookReportRepository.kt | 8 +- .../DBPriorNotificationRepository.kt | 8 + ...eate_manual_prior_notifications_table.sql} | 5 +- ...nsert_dummy_manual_prior_notifications.sql | 7 + .../V666.5.2__Insert_prior_notifications.sql | 7 - ...rt_dummy_manual_prior_notifications.jsonc} | 2 +- .../domain/mappers/ERSMapperUTests.kt | 16 +- .../monitorfish/domain/use_cases/TestUtils.kt | 157 ++++++------------ .../GetPriorNotificationUTests.kt | 6 +- .../GetPriorNotificationsUTests.kt | 6 +- .../bff/PriorNotificationControllerITests.kt | 13 +- frontend/scripts/generate_test_data_seeds.mjs | 13 +- 39 files changed, 556 insertions(+), 212 deletions(-) rename backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/{Catch.kt => LogbookFishingCatch.kt} (95%) rename backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/{Gear.kt => LogbookTripGear.kt} (91%) create mode 100644 backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/repositories/PriorNotificationRepository.kt create mode 100644 backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/CreateOrUpdatePriorNotification.kt create mode 100644 backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/input/LogbookFishingCatchInput.kt create mode 100644 backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/input/LogbookMessagePnoDataInput.kt create mode 100644 backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/input/LogbookTripGearDataInput.kt create mode 100644 backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/input/PriorNotificationDataInput.kt create mode 100644 backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/entities/ManualPriorNotificationEntity.kt create mode 100644 backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaPriorNotificationRepository.kt create mode 100644 backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/interfaces/DBPriorNotificationRepository.kt rename backend/src/main/resources/db/migration/internal/{V0.256__Create_prior_notifications_table.sql => V0.256__Create_manual_prior_notifications_table.sql} (91%) create mode 100644 backend/src/main/resources/db/testdata/V666.5.2__Insert_dummy_manual_prior_notifications.sql delete mode 100644 backend/src/main/resources/db/testdata/V666.5.2__Insert_prior_notifications.sql rename backend/src/main/resources/db/testdata/json/{V666.5.2__Insert_prior_notifications.jsonc => V666.5.2__Insert_dummy_manual_prior_notifications.jsonc} (97%) 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 84c21daf7a..d693507c02 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,10 +7,9 @@ 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 tripNumber: String? = null, @@ -23,8 +22,6 @@ data class LogbookMessage( // 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, @@ -33,7 +30,7 @@ data class LogbookMessage( val software: String? = null, var acknowledgment: Acknowledgment? = null, - var createdAt: ZonedDateTime?, + var createdAt: ZonedDateTime? = null, var isCorrectedByNewerMessage: Boolean = false, var isDeleted: Boolean = false, val isEnriched: Boolean = false, @@ -42,9 +39,11 @@ 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(), - val updatedAt: ZonedDateTime?, + val updatedAt: ZonedDateTime? = null, ) { private val logger = LoggerFactory.getLogger(LogbookMessage::class.java) @@ -428,7 +427,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 } @@ -437,7 +436,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..17c50dde6f 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,32 @@ package fr.gouv.cnsp.monitorfish.domain.entities.logbook.messages -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.prior_notification.PriorNotificationType 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() var predictedArrivalDatetimeUtc: ZonedDateTime? = null var predictedLandingDatetimeUtc: ZonedDateTime? = null + var purpose: String? = null + var statisticalRectangle: String? = null 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/repositories/LogbookReportRepository.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/repositories/LogbookReportRepository.kt index c3b9210830..d316ea3018 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,8 +1,10 @@ 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.exceptions.NoLogbookFishingTripFound import java.time.ZonedDateTime @@ -66,8 +68,11 @@ interface LogbookReportRepository { fun findLastReportSoftware(internalReferenceNumber: String): String? + // TODO Is it used? + 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/PriorNotificationRepository.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/repositories/PriorNotificationRepository.kt new file mode 100644 index 0000000000..e2f9b8fcda --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/repositories/PriorNotificationRepository.kt @@ -0,0 +1,9 @@ +package fr.gouv.cnsp.monitorfish.domain.repositories + +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 + +interface PriorNotificationRepository { + fun save(logbookMessageTyped: LogbookMessageTyped): PriorNotification +} 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..5ca450abd5 --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/CreateOrUpdatePriorNotification.kt @@ -0,0 +1,72 @@ +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.repositories.LogbookReportRepository +import fr.gouv.cnsp.monitorfish.domain.repositories.PriorNotificationRepository +import fr.gouv.cnsp.monitorfish.domain.repositories.VesselRepository +import java.time.ZonedDateTime + +@UseCase +class CreateOrUpdatePriorNotification( + private val logbookReportRepository: LogbookReportRepository, + private val priorNotificationRepository: PriorNotificationRepository, + private val vesselRepository: VesselRepository, +) { + fun execute( + reportId: String?, + logbookMessage: PNO, + tripGears: List, + vesselId: Int, + ): PriorNotification { + val currentLogbookMessage = reportId?.let { + logbookReportRepository.findPriorNotificationByReportId(reportId) + } + val id = currentLogbookMessage?.let { currentLogbookMessage.logbookMessageTyped.logbookMessage.id } + // TODO To calculate. + val tripSegments = emptyList() + val vessel = vesselRepository.findVesselById(vesselId) + + val pnoLogbookMessage = LogbookMessage( + id = id, + 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.toString(), + imo = vessel?.imo, + reportDateTime = null, + integrationDateTime = ZonedDateTime.now(), + analyzedByRules = emptyList(), + rawMessage = null, + transmissionFormat = null, + software = null, + acknowledgment = null, + createdAt = ZonedDateTime.now(), + // TODO Check if it's `true` when it's updated (normally it should always be `false`). + isCorrectedByNewerMessage = false, + isDeleted = false, + isEnriched = false, + isManuallyCreated = true, + isSentByFailoverSoftware = false, + message = logbookMessage, + messageType = "PNO", + operationType = LogbookOperationType.DAT, + tripGears = tripGears, + tripSegments = tripSegments, + updatedAt = ZonedDateTime.now(), + ) + val logbookMessageTyped = LogbookMessageTyped(pnoLogbookMessage, PNO::class.java) + + val createdOrUpdatedPriorNotification = priorNotificationRepository.save(logbookMessageTyped) + + return createdOrUpdatedPriorNotification + } +} 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 fcf0612cb2..3e665983ee 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 @@ -70,7 +70,10 @@ class GetPriorNotification( } val finalPriorNotification = priorNotification.copy( - logbookMessageTyped = LogbookMessageTyped(logbookMessageWithRawMessage, PNO::class.java), + logbookMessageTyped = LogbookMessageTyped( + logbookMessageWithRawMessage, + PNO::class.java, + ), port = port, seafront = seafront, vessel = vessel, 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..bf48d41c45 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 @@ -4,9 +4,11 @@ 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.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.input.PriorNotificationDataInput 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 @@ -22,6 +24,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, @@ -141,4 +144,45 @@ class PriorNotificationController( fun getAllTypes(): List { return getPriorNotificationTypes.execute() } + + @PostMapping("") + @Operation(summary = "Create a new prior notification") + fun create( + @RequestBody + priorNotificationDataInput: PriorNotificationDataInput, + ): PriorNotificationDetailDataOutput { + val logbookMessage = priorNotificationDataInput.logbookMessage.toPNO() + val tripGears = priorNotificationDataInput.tripGears.map { it.toLogbookTripGear() } + + return PriorNotificationDetailDataOutput.fromPriorNotification( + createOrUpdatePriorNotification.execute( + null, + logbookMessage, + tripGears, + priorNotificationDataInput.vesselId, + ), + ) + } + + @PutMapping("/{logbookMessageReportId}") + @Operation(summary = "Update a prior notification by its (logbook message) `reportId`") + fun update( + @PathParam("Logbook message `reportId`") + @PathVariable(name = "logbookMessageReportId") + logbookMessageReportId: String, + @RequestBody + priorNotificationDataInput: PriorNotificationDataInput, + ): PriorNotificationDetailDataOutput { + val logbookMessage = priorNotificationDataInput.logbookMessage.toPNO() + val tripGears = priorNotificationDataInput.tripGears.map { it.toLogbookTripGear() } + + return PriorNotificationDetailDataOutput.fromPriorNotification( + createOrUpdatePriorNotification.execute( + logbookMessageReportId, + logbookMessage, + tripGears, + priorNotificationDataInput.vesselId, + ), + ) + } } 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..2368b22118 --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/input/LogbookFishingCatchInput.kt @@ -0,0 +1,30 @@ +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, +) { + 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/LogbookMessagePnoDataInput.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/input/LogbookMessagePnoDataInput.kt new file mode 100644 index 0000000000..0e21733b62 --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/input/LogbookMessagePnoDataInput.kt @@ -0,0 +1,35 @@ +package fr.gouv.cnsp.monitorfish.infrastructure.api.input + +import fr.gouv.cnsp.monitorfish.domain.entities.logbook.messages.PNO +import java.time.ZonedDateTime + +data class LogbookMessageValueForPnoDataInput( + val estimatedArrivalDate: String, + val estimatedLandingDate: String, + val fishingCatchesOnboard: List, + val fishingCatchesToUnload: List, + val portLocode: String, + val portName: String, +) { + fun toPNO(): PNO { + return PNO().apply { + catchOnboard = fishingCatchesOnboard.map { it.toLogbookFishingCatch() } + catchToLand = fishingCatchesToUnload.map { it.toLogbookFishingCatch() } + economicZone = null + effortZone = null + // TODO fill with zone + faoZone = null + latitude = null + longitude = null + // TODO Will be calculated + pnoTypes = emptyList() + predictedArrivalDatetimeUtc = ZonedDateTime.parse(estimatedArrivalDate) + predictedLandingDatetimeUtc = ZonedDateTime.parse(estimatedLandingDate) + port = portLocode + portName = portName + purpose = "LAN" + statisticalRectangle = null + tripStartDate = null + } + } +} 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..c4bcc887d5 --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/input/PriorNotificationDataInput.kt @@ -0,0 +1,7 @@ +package fr.gouv.cnsp.monitorfish.infrastructure.api.input + +data class PriorNotificationDataInput( + val logbookMessage: LogbookMessageValueForPnoDataInput, + val tripGears: List, + val vesselId: Int, +) 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/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/database/entities/LogbookReportEntity.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/entities/LogbookReportEntity.kt index cadad62f5e..53674a876c 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 @@ -120,7 +120,7 @@ data class LogbookReportEntity( fun toLogbookMessage(mapper: ObjectMapper): LogbookMessage { val message = getERSMessageValueFromJSON(mapper, message, messageType, operationType) - val tripGears = deserializeJSONList(mapper, tripGears, Gear::class.java) + val tripGears = deserializeJSONList(mapper, tripGears, LogbookTripGear::class.java) val tripSegments = deserializeJSONList(mapper, tripSegments, LogbookTripSegment::class.java) return LogbookMessage( @@ -156,7 +156,7 @@ data class LogbookReportEntity( fun toPriorNotification(mapper: ObjectMapper, relatedModels: List): PriorNotification { val referenceLogbookMessage = toLogbookMessage(mapper) - val fingerprint = listOf(referenceLogbookMessage.id) + val fingerprint = listOf(referenceLogbookMessage.id!!) .plus(relatedModels.mapNotNull { it.id }) .sorted() .joinToString(separator = ".") 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..b0d2441fdf --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/entities/ManualPriorNotificationEntity.kt @@ -0,0 +1,113 @@ +package fr.gouv.cnsp.monitorfish.infrastructure.database.entities + +import com.neovisionaries.i18n.CountryCode +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.Vessel +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.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 = "cfr") + val cfr: String?, + + // ISO Alpha-3 country code + @Column(name = "flag_state") + val flagState: String?, + + @Column(name = "integration_datetime_utc") + val integrationDateTime: ZonedDateTime, + + @Column(name = "operation_datetime_utc") + val operationDateTime: ZonedDateTime, + + @Column(name = "report_datetime_utc") + val reportDateTime: ZonedDateTime?, + + @Column(name = "trip_gears", nullable = true, columnDefinition = "jsonb") + @Type(JsonBinaryType::class) + val tripGears: List?, + + @Column(name = "trip_segments", nullable = true, columnDefinition = "jsonb") + @Type(JsonBinaryType::class) + val tripSegments: List?, + + @Type(JsonBinaryType::class) + @Column(name = "value", nullable = true, columnDefinition = "jsonb") + val value: PNO, + + @Column(name = "vessel_name") + val vesselName: String?, + + @Column(name = "created_at", insertable = false, updatable = false) + @CreationTimestamp + val createdAt: ZonedDateTime? = null, + + @Column(name = "updated_at") + @UpdateTimestamp + var updatedAt: ZonedDateTime? = null, +) { + companion object { + fun fromLogbookMessageTyped(logbookMessageTyped: LogbookMessageTyped): ManualPriorNotificationEntity { + val pnoLogbookMessage = logbookMessageTyped.logbookMessage + val pnoLogbookMessageValue = logbookMessageTyped.typedMessage + + return ManualPriorNotificationEntity( + reportId = pnoLogbookMessage.reportId, + cfr = pnoLogbookMessage.internalReferenceNumber, + flagState = pnoLogbookMessage.flagState, + integrationDateTime = pnoLogbookMessage.integrationDateTime, + operationDateTime = pnoLogbookMessage.operationDateTime, + reportDateTime = pnoLogbookMessage.reportDateTime, + tripGears = pnoLogbookMessage.tripGears, + tripSegments = pnoLogbookMessage.tripSegments, + value = pnoLogbookMessageValue, + vesselName = pnoLogbookMessage.vesselName, + createdAt = pnoLogbookMessage.createdAt, + updatedAt = pnoLogbookMessage.updatedAt, + ) + } + } + + fun toPriorNotification(): PriorNotification { + val pnoLogbookMessage = LogbookMessage( + id = null, + reportId = reportId, + analyzedByRules = emptyList(), + integrationDateTime = integrationDateTime, + internalReferenceNumber = cfr, + isManuallyCreated = true, + message = value, + operationDateTime = operationDateTime, + operationNumber = null, + operationType = LogbookOperationType.DAT, + reportDateTime = reportDateTime, + transmissionFormat = null, + vesselName = vesselName, + createdAt = createdAt, + updatedAt = updatedAt, + ) + val fingerprint = listOf(reportId!!, updatedAt.toString()).joinToString(separator = ".") + // For pratical reasons `vessel` can't be `null`, so we temporarely set it to "Navire inconnu" + val vessel = Vessel(id = -1, flagState = CountryCode.UNDEFINED, hasLogbookEsacapt = false) + val logbookMessageTyped = LogbookMessageTyped(pnoLogbookMessage, PNO::class.java) + + return PriorNotification( + fingerprint, + logbookMessageTyped, + vessel = vessel, + ) + } +} 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..106f11f51d 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,11 +1,9 @@ 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.* 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.exceptions.EntityConversionException import fr.gouv.cnsp.monitorfish.domain.exceptions.NoERSMessagesFound @@ -49,18 +47,19 @@ 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 { @@ -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/JpaPriorNotificationRepository.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaPriorNotificationRepository.kt new file mode 100644 index 0000000000..e2746d3a39 --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaPriorNotificationRepository.kt @@ -0,0 +1,19 @@ +package fr.gouv.cnsp.monitorfish.infrastructure.database.repositories + +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.repositories.PriorNotificationRepository +import fr.gouv.cnsp.monitorfish.infrastructure.database.entities.ManualPriorNotificationEntity +import fr.gouv.cnsp.monitorfish.infrastructure.database.repositories.interfaces.DBPriorNotificationRepository +import org.springframework.stereotype.Repository + +@Repository +class JpaPriorNotificationRepository(private val dbPriorNotificationRepository: DBPriorNotificationRepository) : + PriorNotificationRepository { + override fun save(logbookMessageTyped: LogbookMessageTyped): PriorNotification { + return dbPriorNotificationRepository + .save(ManualPriorNotificationEntity.fromLogbookMessageTyped(logbookMessageTyped)) + .toPriorNotification() + } +} 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 de3880e2f7..41a53caeab 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 @@ -64,7 +64,7 @@ interface DBLogbookReportRepository : value, integration_datetime_utc, CAST(NULL AS TEXT) AS trip_number, - analyzed_by_rules, + CAST(NULL AS TEXT[]) AS analyzed_by_rules, CAST(TRUE AS BOOLEAN) AS trip_number_was_computed, -- TODO /!\ CHECK IF THIS IS WHAT WE WANT /!\ CAST(NULL AS public.logbook_message_transmission_format) AS transmission_format, @@ -75,7 +75,7 @@ interface DBLogbookReportRepository : is_manually_created, created_at, updated_at - FROM prior_notifications + FROM manual_prior_notifications WHERE -- TODO /!\ INDEX operation_datetime_utc WITH TIMESCALE /!\ -- This filter helps Timescale optimize the query since `operation_datetime_utc` is indexed @@ -296,7 +296,7 @@ interface DBLogbookReportRepository : value, integration_datetime_utc, CAST(NULL AS TEXT) AS trip_number, - analyzed_by_rules, + CAST(NULL AS TEXT[]) AS analyzed_by_rules, CAST(TRUE AS BOOLEAN) AS trip_number_was_computed, -- TODO /!\ CHECK IF THIS IS WHAT WE WANT /!\ CAST(NULL AS public.logbook_message_transmission_format) AS transmission_format, @@ -307,7 +307,7 @@ interface DBLogbookReportRepository : is_manually_created, created_at, updated_at - FROM prior_notifications + FROM manual_prior_notifications WHERE report_id = ?1 diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/interfaces/DBPriorNotificationRepository.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/interfaces/DBPriorNotificationRepository.kt new file mode 100644 index 0000000000..b5106a51a4 --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/interfaces/DBPriorNotificationRepository.kt @@ -0,0 +1,8 @@ +package fr.gouv.cnsp.monitorfish.infrastructure.database.repositories.interfaces + +import fr.gouv.cnsp.monitorfish.infrastructure.database.entities.ManualPriorNotificationEntity +import org.springframework.data.repository.CrudRepository + +interface DBPriorNotificationRepository : CrudRepository { + fun save(priorNotificationEntity: ManualPriorNotificationEntity): ManualPriorNotificationEntity +} diff --git a/backend/src/main/resources/db/migration/internal/V0.256__Create_prior_notifications_table.sql b/backend/src/main/resources/db/migration/internal/V0.256__Create_manual_prior_notifications_table.sql similarity index 91% rename from backend/src/main/resources/db/migration/internal/V0.256__Create_prior_notifications_table.sql rename to backend/src/main/resources/db/migration/internal/V0.256__Create_manual_prior_notifications_table.sql index 975b54d945..ae7c3c4ace 100644 --- a/backend/src/main/resources/db/migration/internal/V0.256__Create_prior_notifications_table.sql +++ b/backend/src/main/resources/db/migration/internal/V0.256__Create_manual_prior_notifications_table.sql @@ -1,7 +1,6 @@ CREATE EXTENSION IF NOT EXISTS pgcrypto; -CREATE TABLE public.prior_notifications ( - -- Is there a risk of racing condition with `logbook_report_id_seq`? +CREATE TABLE public.manual_prior_notifications ( id BIGINT DEFAULT nextval('public.logbook_report_id_seq'::regclass) NOT NULL, -- operation_number VARCHAR(100), -- operation_country VARCHAR(3), @@ -20,7 +19,7 @@ CREATE TABLE public.prior_notifications ( value JSONB, integration_datetime_utc TIMESTAMP WITHOUT TIME ZONE, -- trip_number VARCHAR(100), - analyzed_by_rules VARCHAR(100)[], + -- analyzed_by_rules VARCHAR(100)[], -- trip_number_was_computed BOOLEAN DEFAULT FALSE, -- transmission_format public.logbook_message_transmission_format NOT NULL, -- software VARCHAR(100), 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..f288573ef1 --- /dev/null +++ b/backend/src/main/resources/db/testdata/V666.5.2__Insert_dummy_manual_prior_notifications.sql @@ -0,0 +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 manual_prior_notifications (id, report_id, cfr, flag_state, integration_datetime_utc, operation_datetime_utc, report_datetime_utc, vessel_name, trip_gears, trip_segments, value) VALUES (116, 'd2bd7d28-3130-4e3f-bf0d-5e919a5657be', 'CFR112', 'FRA', NOW() AT TIME ZONE 'UTC' - INTERVAL '15 minutes', NOW() AT TIME ZONE 'UTC' - INTERVAL '15 minutes', NOW() AT TIME ZONE 'UTC' - INTERVAL '15 minutes', 'POISSON PAS NET', '[{"gear":"LNP","mesh":8,"dimensions":"50;200"}]', '[{"segment":"NWW09","segmentName":"Lignes"}]', '{"catchOnboard":[{"weight":72,"nbFish":null,"species":"SOS"}],"pnoTypes":[{"pnoTypeName":"Préavis type D","minimumNotificationPeriod":4,"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 id = 116; +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 id = 116; +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 id = 116; diff --git a/backend/src/main/resources/db/testdata/V666.5.2__Insert_prior_notifications.sql b/backend/src/main/resources/db/testdata/V666.5.2__Insert_prior_notifications.sql deleted file mode 100644 index 29be1037af..0000000000 --- a/backend/src/main/resources/db/testdata/V666.5.2__Insert_prior_notifications.sql +++ /dev/null @@ -1,7 +0,0 @@ --- /!\ 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 prior_notifications (id, report_id, cfr, flag_state, integration_datetime_utc, operation_datetime_utc, report_datetime_utc, vessel_name, trip_gears, trip_segments, value) VALUES (116, 'd2bd7d28-3130-4e3f-bf0d-5e919a5657be', 'CFR112', 'FRA', NOW() AT TIME ZONE 'UTC' - INTERVAL '15 minutes', NOW() AT TIME ZONE 'UTC' - INTERVAL '15 minutes', NOW() AT TIME ZONE 'UTC' - INTERVAL '15 minutes', 'POISSON PAS NET', '[{"gear":"LNP","mesh":8,"dimensions":"50;200"}]', '[{"segment":"NWW09","segmentName":"Lignes"}]', '{"catchOnboard":[{"weight":72,"nbFish":null,"species":"SOS"}],"pnoTypes":[{"pnoTypeName":"Préavis type D","minimumNotificationPeriod":4,"hasDesignatedPorts":true}],"port":"FRVNE","predictedArrivalDatetimeUtc":null,"predictedLandingDatetimeUtc":null,"purpose":"LAN","tripStartDate":null}'); -UPDATE 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 id = 116; -UPDATE 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 id = 116; -UPDATE 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 id = 116; diff --git a/backend/src/main/resources/db/testdata/json/V666.5.2__Insert_prior_notifications.jsonc b/backend/src/main/resources/db/testdata/json/V666.5.2__Insert_dummy_manual_prior_notifications.jsonc similarity index 97% rename from backend/src/main/resources/db/testdata/json/V666.5.2__Insert_prior_notifications.jsonc rename to backend/src/main/resources/db/testdata/json/V666.5.2__Insert_dummy_manual_prior_notifications.jsonc index 5e2c03e86d..f5f4851c68 100644 --- a/backend/src/main/resources/db/testdata/json/V666.5.2__Insert_prior_notifications.jsonc +++ b/backend/src/main/resources/db/testdata/json/V666.5.2__Insert_dummy_manual_prior_notifications.jsonc @@ -1,6 +1,6 @@ [ { - "table": "prior_notifications", + "table": "manual_prior_notifications", "data": [ { "id": 116, 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/TestUtils.kt b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/TestUtils.kt index 7707554d35..32f0ffc75a 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() @@ -126,7 +126,6 @@ object TestUtils { LogbookMessage( id = 2, analyzedByRules = listOf(), - createdAt = ZonedDateTime.now(), isManuallyCreated = false, operationNumber = "", tripNumber = "345", @@ -149,12 +148,10 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), - updatedAt = ZonedDateTime.now(), ), LogbookMessage( id = 1, analyzedByRules = listOf(), - createdAt = ZonedDateTime.now(), isManuallyCreated = false, operationNumber = "", tripNumber = "345", @@ -177,12 +174,10 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), - updatedAt = ZonedDateTime.now(), ), LogbookMessage( id = 3, analyzedByRules = listOf(), - createdAt = ZonedDateTime.now(), isManuallyCreated = false, operationNumber = "", tripNumber = "345", @@ -191,8 +186,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, @@ -206,12 +200,10 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), - updatedAt = ZonedDateTime.now(), ), LogbookMessage( id = 3, analyzedByRules = listOf(), - createdAt = ZonedDateTime.now(), isManuallyCreated = false, operationNumber = "", tripNumber = "345", @@ -220,8 +212,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,12 +226,10 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), - updatedAt = ZonedDateTime.now(), ), LogbookMessage( id = 4, analyzedByRules = listOf(), - createdAt = ZonedDateTime.now(), isManuallyCreated = false, operationNumber = "", tripNumber = "345", @@ -249,20 +238,17 @@ 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, integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), - updatedAt = ZonedDateTime.now(), ), LogbookMessage( id = 5, analyzedByRules = listOf(), - createdAt = ZonedDateTime.now(), isManuallyCreated = false, operationNumber = "", tripNumber = "345", @@ -271,30 +257,28 @@ 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, integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), - updatedAt = ZonedDateTime.now(), ), ) } 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() @@ -317,7 +301,6 @@ object TestUtils { LogbookMessage( id = 1, analyzedByRules = listOf(), - createdAt = ZonedDateTime.now(), isManuallyCreated = false, operationNumber = "", tripNumber = "345", @@ -326,8 +309,7 @@ object TestUtils { messageType = "DEP", software = "FT/VISIOCaptures V1.4.7", message = dep, - reportDateTime = - ZonedDateTime.of( + reportDateTime = ZonedDateTime.of( 2020, 5, 5, @@ -341,12 +323,10 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), - updatedAt = ZonedDateTime.now(), ), LogbookMessage( id = 2, analyzedByRules = listOf(), - createdAt = ZonedDateTime.now(), isManuallyCreated = false, operationNumber = "", tripNumber = "345", @@ -355,8 +335,7 @@ object TestUtils { messageType = "FAR", software = "FP/VISIOCaptures V1.4.7", message = far, - reportDateTime = - ZonedDateTime.of( + reportDateTime = ZonedDateTime.of( 2020, 5, 5, @@ -370,12 +349,10 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), - updatedAt = ZonedDateTime.now(), ), LogbookMessage( id = 3, analyzedByRules = listOf(), - createdAt = ZonedDateTime.now(), isManuallyCreated = false, operationNumber = "", tripNumber = "345", @@ -384,8 +361,7 @@ object TestUtils { messageType = "PNO", software = "TurboCatch (3.6-1)", message = pno, - reportDateTime = - ZonedDateTime.of( + reportDateTime = ZonedDateTime.of( 2020, 5, 5, @@ -399,17 +375,16 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), - updatedAt = ZonedDateTime.now(), ), ) } 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() @@ -430,7 +405,6 @@ object TestUtils { LogbookMessage( id = 1, analyzedByRules = listOf(), - createdAt = ZonedDateTime.now(), isManuallyCreated = false, operationNumber = "9065646811", tripNumber = "345", @@ -438,8 +412,7 @@ object TestUtils { operationType = LogbookOperationType.DAT, messageType = "FAR", message = far, - reportDateTime = - ZonedDateTime.of( + reportDateTime = ZonedDateTime.of( 2020, 5, 5, @@ -453,12 +426,10 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), - updatedAt = ZonedDateTime.now(), ), LogbookMessage( id = 2, analyzedByRules = listOf(), - createdAt = ZonedDateTime.now(), isManuallyCreated = false, operationNumber = "", tripNumber = "345", @@ -467,8 +438,7 @@ object TestUtils { operationType = LogbookOperationType.COR, messageType = "FAR", message = correctedFar, - reportDateTime = - ZonedDateTime.of( + reportDateTime = ZonedDateTime.of( 2020, 5, 5, @@ -482,17 +452,16 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), - updatedAt = ZonedDateTime.now(), ), ) } 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() @@ -520,7 +489,6 @@ object TestUtils { LogbookMessage( id = 1, analyzedByRules = listOf(), - createdAt = ZonedDateTime.now(), isManuallyCreated = false, operationNumber = "", tripNumber = "345", @@ -542,12 +510,10 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), - updatedAt = ZonedDateTime.now(), ), LogbookMessage( id = 2, analyzedByRules = listOf(), - createdAt = ZonedDateTime.now(), isManuallyCreated = false, operationNumber = "", reportId = "9065646816", @@ -569,12 +535,10 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), - updatedAt = ZonedDateTime.now(), ), LogbookMessage( id = 3, analyzedByRules = listOf(), - createdAt = ZonedDateTime.now(), isManuallyCreated = false, operationNumber = "", tripNumber = "345", @@ -596,12 +560,10 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), - updatedAt = ZonedDateTime.now(), ), LogbookMessage( id = 4, analyzedByRules = listOf(), - createdAt = ZonedDateTime.now(), isManuallyCreated = false, operationNumber = "", reportId = "9065646818", @@ -623,12 +585,10 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), - updatedAt = ZonedDateTime.now(), ), LogbookMessage( id = 5, analyzedByRules = listOf(), - createdAt = ZonedDateTime.now(), isManuallyCreated = false, operationNumber = "", referencedReportId = "9065646813", @@ -649,12 +609,10 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), - updatedAt = ZonedDateTime.now(), ), LogbookMessage( id = 6, analyzedByRules = listOf(), - createdAt = ZonedDateTime.now(), isManuallyCreated = false, operationNumber = "5h499-erh5u7-pm3ae8c5trj78j67dfh", tripNumber = "SCR-TTT20200505030505", @@ -676,7 +634,6 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), - updatedAt = ZonedDateTime.now(), ), ) } @@ -685,47 +642,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 @@ -765,7 +722,6 @@ object TestUtils { LogbookMessage( id = 1, analyzedByRules = listOf(), - createdAt = ZonedDateTime.now(), isManuallyCreated = false, operationNumber = "456846844658", tripNumber = "125345", @@ -777,12 +733,11 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), - updatedAt = ZonedDateTime.now(), + reportDateTime = ZonedDateTime.now(), ), LogbookMessage( id = 2, analyzedByRules = listOf(), - createdAt = ZonedDateTime.now(), isManuallyCreated = false, operationNumber = "47177857577", tripNumber = "125345", @@ -794,14 +749,13 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), - updatedAt = ZonedDateTime.now(), + reportDateTime = ZonedDateTime.now(), ), ), Pair( LogbookMessage( id = 3, analyzedByRules = listOf(), - createdAt = ZonedDateTime.now(), isManuallyCreated = false, operationNumber = "48545254254", tripNumber = "125345", @@ -813,12 +767,11 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), - updatedAt = ZonedDateTime.now(), + reportDateTime = ZonedDateTime.now(), ), LogbookMessage( id = 4, analyzedByRules = listOf(), - createdAt = ZonedDateTime.now(), isManuallyCreated = false, operationNumber = "004045204504", tripNumber = "125345", @@ -830,7 +783,7 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), - updatedAt = ZonedDateTime.now(), + reportDateTime = ZonedDateTime.now(), ), ), ) @@ -840,37 +793,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 @@ -901,7 +854,6 @@ object TestUtils { LogbookMessage( id = 1, analyzedByRules = listOf(), - createdAt = ZonedDateTime.now(), isManuallyCreated = false, operationNumber = "456846844658", tripNumber = "125345", @@ -913,12 +865,11 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), - updatedAt = ZonedDateTime.now(), + reportDateTime = ZonedDateTime.now(), ), LogbookMessage( id = 2, analyzedByRules = listOf(), - createdAt = ZonedDateTime.now(), isManuallyCreated = false, operationNumber = "47177857577", tripNumber = "125345", @@ -930,14 +881,13 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), - updatedAt = ZonedDateTime.now(), + reportDateTime = ZonedDateTime.now(), ), ), Pair( LogbookMessage( id = 3, analyzedByRules = listOf(), - createdAt = ZonedDateTime.now(), isManuallyCreated = false, operationNumber = "48545254254", tripNumber = "125345", @@ -949,12 +899,11 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), - updatedAt = ZonedDateTime.now(), + reportDateTime = ZonedDateTime.now(), ), LogbookMessage( id = 4, analyzedByRules = listOf(), - createdAt = ZonedDateTime.now(), isManuallyCreated = false, operationNumber = "004045204504", tripNumber = "125345", @@ -966,7 +915,7 @@ object TestUtils { integrationDateTime = ZonedDateTime.now(), isEnriched = false, operationDateTime = ZonedDateTime.now(), - updatedAt = ZonedDateTime.now(), + reportDateTime = ZonedDateTime.now(), ), ), ) 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 845f663220..6c3b1b25b7 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 @@ -56,7 +56,6 @@ class GetPriorNotificationUTests { reportId = "FAKE_REPORT_ID_1", referencedReportId = null, analyzedByRules = emptyList(), - createdAt = ZonedDateTime.now(), isDeleted = false, integrationDateTime = ZonedDateTime.now(), isCorrectedByNewerMessage = false, @@ -67,8 +66,8 @@ class GetPriorNotificationUTests { operationDateTime = ZonedDateTime.now(), operationNumber = "1", operationType = LogbookOperationType.DAT, + reportDateTime = ZonedDateTime.now(), transmissionFormat = LogbookTransmissionFormat.ERS, - updatedAt = ZonedDateTime.now(), ), ), reportingCount = null, @@ -119,7 +118,6 @@ class GetPriorNotificationUTests { reportId = null, referencedReportId = "FAKE_REPORT_ID_2", analyzedByRules = emptyList(), - createdAt = ZonedDateTime.now(), isDeleted = false, integrationDateTime = ZonedDateTime.now(), isCorrectedByNewerMessage = true, @@ -130,8 +128,8 @@ class GetPriorNotificationUTests { operationDateTime = ZonedDateTime.now(), operationNumber = "2", operationType = LogbookOperationType.COR, + reportDateTime = ZonedDateTime.now(), transmissionFormat = LogbookTransmissionFormat.ERS, - updatedAt = ZonedDateTime.now(), ), ), reportingCount = null, 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/GetPriorNotificationsUTests.kt index 342253c34a..afe25c8662 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/GetPriorNotificationsUTests.kt @@ -66,7 +66,6 @@ class GetPriorNotificationsUTests { reportId = "FAKE_REPORT_ID_1", referencedReportId = null, analyzedByRules = emptyList(), - createdAt = ZonedDateTime.now(), integrationDateTime = ZonedDateTime.now(), isCorrectedByNewerMessage = false, isDeleted = false, @@ -76,8 +75,8 @@ class GetPriorNotificationsUTests { operationDateTime = ZonedDateTime.now(), operationNumber = "1", operationType = LogbookOperationType.DAT, + reportDateTime = ZonedDateTime.now(), transmissionFormat = LogbookTransmissionFormat.ERS, - updatedAt = ZonedDateTime.now(), ), ), reportingCount = null, @@ -106,7 +105,6 @@ class GetPriorNotificationsUTests { reportId = "FAKE_REPORT_ID_2_COR", referencedReportId = "FAKE_NONEXISTENT_REPORT_ID_2", analyzedByRules = emptyList(), - createdAt = ZonedDateTime.now(), integrationDateTime = ZonedDateTime.now(), isCorrectedByNewerMessage = false, isDeleted = false, @@ -116,8 +114,8 @@ class GetPriorNotificationsUTests { operationDateTime = ZonedDateTime.now(), operationNumber = "1", operationType = LogbookOperationType.COR, + reportDateTime = ZonedDateTime.now(), transmissionFormat = LogbookTransmissionFormat.ERS, - updatedAt = ZonedDateTime.now(), ), ), reportingCount = null, 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 54685bedb7..6d92381d60 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 @@ -57,7 +61,6 @@ class PriorNotificationControllerITests { reportId = "FAKE_REPORT_ID_1", referencedReportId = null, analyzedByRules = emptyList(), - createdAt = ZonedDateTime.now(), integrationDateTime = ZonedDateTime.now(), isCorrectedByNewerMessage = false, isDeleted = false, @@ -67,8 +70,8 @@ class PriorNotificationControllerITests { operationDateTime = ZonedDateTime.now(), operationNumber = "1", operationType = LogbookOperationType.DAT, + reportDateTime = ZonedDateTime.now(), transmissionFormat = LogbookTransmissionFormat.ERS, - updatedAt = ZonedDateTime.now(), ), ), reportingCount = null, @@ -97,7 +100,6 @@ class PriorNotificationControllerITests { reportId = "FAKE_REPORT_ID_2_COR", referencedReportId = "FAKE_NONEXISTENT_REPORT_ID_2", analyzedByRules = emptyList(), - createdAt = ZonedDateTime.now(), integrationDateTime = ZonedDateTime.now(), isCorrectedByNewerMessage = true, isDeleted = false, @@ -107,8 +109,8 @@ class PriorNotificationControllerITests { operationDateTime = ZonedDateTime.now(), operationNumber = "1", operationType = LogbookOperationType.COR, + reportDateTime = ZonedDateTime.now(), transmissionFormat = LogbookTransmissionFormat.ERS, - updatedAt = ZonedDateTime.now(), ), ), reportingCount = null, @@ -174,7 +176,6 @@ class PriorNotificationControllerITests { reportId = "FAKE_REPORT_ID_1", referencedReportId = null, analyzedByRules = emptyList(), - createdAt = ZonedDateTime.now(), integrationDateTime = ZonedDateTime.now(), isCorrectedByNewerMessage = false, isDeleted = false, @@ -184,8 +185,8 @@ class PriorNotificationControllerITests { operationDateTime = ZonedDateTime.now(), operationNumber = "1", operationType = LogbookOperationType.DAT, + reportDateTime = ZonedDateTime.now(), transmissionFormat = LogbookTransmissionFormat.ERS, - updatedAt = ZonedDateTime.now(), ), ), reportingCount = null, diff --git a/frontend/scripts/generate_test_data_seeds.mjs b/frontend/scripts/generate_test_data_seeds.mjs index 6902d6e93f..12ea347527 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,11 +120,11 @@ for (const file of jsonFiles) { const dataTables = Array.isArray(jsonSourceAsObject) ? jsonSourceAsObject : [jsonSourceAsObject] const sqlStatementBlocks = dataTables .map(dataTable => { - const { data: rows, table } = dataTable + const { data: rows, id, table } = dataTable return rows.map(row => { const insertStatement = generateInsertStatement(row, table) - const updateStatements = generateUpdateStatements(row, table) + const updateStatements = generateUpdateStatements(row, table, id) return [insertStatement, ...updateStatements, ''].join('\n') }) From 63af5c284bcc22d164054ebb89b0b512819c5de3 Mon Sep 17 00:00:00 2001 From: Ivan Gabriele Date: Thu, 6 Jun 2024 02:21:46 +0200 Subject: [PATCH 03/26] Fix many issues and refactor manual prior notifications in Backend --- .../domain/entities/logbook/LogbookMessage.kt | 3 - .../domain/entities/logbook/messages/PNO.kt | 13 ++ .../sorters/LogbookReportSortColumn.kt | 9 - .../prior_notification/PriorNotification.kt | 102 +++++++++- .../filters/PriorNotificationsFilter.kt} | 4 +- .../sorters/PriorNotificationsSortColumn.kt | 9 + .../exceptions/BackendInternalException.kt | 24 +++ .../exceptions/BackendUsageErrorCode.kt | 18 ++ .../exceptions/BackendUsageException.kt | 19 ++ .../repositories/LogbookReportRepository.kt | 6 +- .../ManualPriorNotificationRepository.kt | 12 ++ .../PriorNotificationRepository.kt | 9 - .../domain/repositories/VesselRepository.kt | 2 +- .../CreateOrUpdatePriorNotification.kt | 126 +++++++++--- .../GetPriorNotification.kt | 101 ++-------- .../GetPriorNotifications.kt | 91 ++------- .../domain/use_cases/vessel/GetVesselById.kt | 14 ++ .../api/ControllersExceptionHandler.kt | 33 ++- .../api/bff/PriorNotificationController.kt | 107 ++++++---- .../api/bff/VesselController.kt | 18 +- .../api/input/LogbookFishingCatchInput.kt | 12 ++ .../api/input/PriorNotificationDataInput.kt | 12 +- .../outputs/BackendInternalErrorDataOutput.kt | 13 ++ .../outputs/BackendRequestErrorDataOutput.kt | 17 ++ .../outputs/BackendUsageErrorDataOutput.kt | 18 ++ .../api/outputs/LogbookMessageDataOutput.kt | 6 - .../outputs/PriorNotificationDataOutput.kt | 120 +++-------- .../PriorNotificationDetailDataOutput.kt | 4 +- .../PriorNotificationListItemDataOutput.kt | 110 ++++++++++ .../database/entities/LogbookReportEntity.kt | 42 ++-- .../entities/ManualPriorNotificationEntity.kt | 176 ++++++++++------ .../JpaLogbookReportRepository.kt | 8 +- .../JpaManualPriorNotificationRepository.kt | 54 +++++ .../JpaPriorNotificationRepository.kt | 19 -- .../repositories/JpaVesselRepository.kt | 6 +- .../interfaces/DBLogbookReportRepository.kt | 188 +++++------------- .../DBManualPriorNotificationRepository.kt | 139 +++++++++++++ .../DBPriorNotificationRepository.kt | 8 - .../exceptions/BackendRequestErrorCode.kt | 14 ++ .../exceptions/BackendRequestException.kt | 24 +++ .../utils/ZonedDateTimeDeserializer.kt | 13 ++ .../utils/ZonedDateTimeSerializer.kt | 20 ++ ...reate_manual_prior_notifications_table.sql | 42 +--- ...nsert_dummy_manual_prior_notifications.sql | 8 +- ...ert_dummy_manual_prior_notifications.jsonc | 12 +- .../entities/logbook/LogbookMessageUTests.kt | 3 - .../use_cases/GetLogbookMessagesUTests.kt | 3 - .../monitorfish/domain/use_cases/TestUtils.kt | 25 --- .../GetPriorNotificationTypesUTests.kt | 2 +- .../GetPriorNotificationUTests.kt | 55 +++-- ...t => GetPriorNotificationsITestsDetail.kt} | 38 ++-- ...t => GetPriorNotificationsUTestsDetail.kt} | 64 +++--- .../bff/PriorNotificationControllerITests.kt | 65 +++--- .../api/bff/VesselControllerITests.kt | 3 + .../JpaLogbookReportRepositoryITests.kt | 44 ++-- 55 files changed, 1271 insertions(+), 836 deletions(-) delete mode 100644 backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/sorters/LogbookReportSortColumn.kt rename backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/{logbook/filters/LogbookReportFilter.kt => prior_notification/filters/PriorNotificationsFilter.kt} (83%) create mode 100644 backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/prior_notification/sorters/PriorNotificationsSortColumn.kt create mode 100644 backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/exceptions/BackendInternalException.kt create mode 100644 backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/exceptions/BackendUsageErrorCode.kt create mode 100644 backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/exceptions/BackendUsageException.kt create mode 100644 backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/repositories/ManualPriorNotificationRepository.kt delete mode 100644 backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/repositories/PriorNotificationRepository.kt create mode 100644 backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/vessel/GetVesselById.kt create mode 100644 backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/BackendInternalErrorDataOutput.kt create mode 100644 backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/BackendRequestErrorDataOutput.kt create mode 100644 backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/BackendUsageErrorDataOutput.kt create mode 100644 backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/PriorNotificationListItemDataOutput.kt create mode 100644 backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaManualPriorNotificationRepository.kt delete mode 100644 backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaPriorNotificationRepository.kt create mode 100644 backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/interfaces/DBManualPriorNotificationRepository.kt delete mode 100644 backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/interfaces/DBPriorNotificationRepository.kt create mode 100644 backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/exceptions/BackendRequestErrorCode.kt create mode 100644 backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/exceptions/BackendRequestException.kt create mode 100644 backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/utils/ZonedDateTimeDeserializer.kt create mode 100644 backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/utils/ZonedDateTimeSerializer.kt rename backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/{GetPriorNotificationsITests.kt => GetPriorNotificationsITestsDetail.kt} (86%) rename backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/{GetPriorNotificationsUTests.kt => GetPriorNotificationsUTestsDetail.kt} (75%) 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 d693507c02..1800271b09 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 @@ -30,11 +30,9 @@ data class LogbookMessage( val software: String? = null, var acknowledgment: Acknowledgment? = null, - var createdAt: ZonedDateTime? = null, var isCorrectedByNewerMessage: Boolean = false, var isDeleted: Boolean = false, val isEnriched: Boolean = false, - val isManuallyCreated: Boolean, var isSentByFailoverSoftware: Boolean = false, val message: LogbookMessageValue? = null, val messageType: String? = null, @@ -43,7 +41,6 @@ data class LogbookMessage( val reportDateTime: ZonedDateTime?, val tripGears: List? = emptyList(), val tripSegments: List? = emptyList(), - val updatedAt: ZonedDateTime? = null, ) { private val logger = LoggerFactory.getLogger(LogbookMessage::class.java) 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 17c50dde6f..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,7 +1,11 @@ package fr.gouv.cnsp.monitorfish.domain.entities.logbook.messages +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`. @@ -24,9 +28,18 @@ class PNO() : LogbookMessageValue { /** Port locode. */ var port: String? = null var portName: String? = null + + @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/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 d316ea3018..552b47c394 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 @@ -3,14 +3,14 @@ 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( @@ -53,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 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/PriorNotificationRepository.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/repositories/PriorNotificationRepository.kt deleted file mode 100644 index e2f9b8fcda..0000000000 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/repositories/PriorNotificationRepository.kt +++ /dev/null @@ -1,9 +0,0 @@ -package fr.gouv.cnsp.monitorfish.domain.repositories - -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 - -interface PriorNotificationRepository { - fun save(logbookMessageTyped: LogbookMessageTyped): PriorNotification -} 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/prior_notification/CreateOrUpdatePriorNotification.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/CreateOrUpdatePriorNotification.kt index 5ca450abd5..7de2620808 100644 --- 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 @@ -4,69 +4,145 @@ 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.repositories.LogbookReportRepository -import fr.gouv.cnsp.monitorfish.domain.repositories.PriorNotificationRepository +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 logbookReportRepository: LogbookReportRepository, - private val priorNotificationRepository: PriorNotificationRepository, + 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?, - logbookMessage: PNO, - tripGears: List, + sentAt: String, + tripGearCodes: List, vesselId: Int, ): PriorNotification { - val currentLogbookMessage = reportId?.let { - logbookReportRepository.findPriorNotificationByReportId(reportId) - } - val id = currentLogbookMessage?.let { currentLogbookMessage.logbookMessageTyped.logbookMessage.id } + 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 = id, + 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.toString(), - imo = vessel?.imo, - reportDateTime = null, + 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, - createdAt = ZonedDateTime.now(), - // TODO Check if it's `true` when it's updated (normally it should always be `false`). isCorrectedByNewerMessage = false, isDeleted = false, - isEnriched = false, - isManuallyCreated = true, + isEnriched = true, isSentByFailoverSoftware = false, - message = logbookMessage, + message = message, messageType = "PNO", operationType = LogbookOperationType.DAT, tripGears = tripGears, tripSegments = tripSegments, - updatedAt = ZonedDateTime.now(), ) val logbookMessageTyped = LogbookMessageTyped(pnoLogbookMessage, PNO::class.java) - val createdOrUpdatedPriorNotification = priorNotificationRepository.save(logbookMessageTyped) + 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 3e665983ee..ad168a8fd2 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,110 +1,45 @@ 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.operationNumber?.let { operationNumber -> - logbookMessage.copy( - rawMessage = try { - logbookRawMessageRepository.findRawMessage(operationNumber) - } catch (e: NoERSMessagesFound) { - logger.warn(e.message) - - null - }, - ) - } ?: logbookMessage - - 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, - ) - - finalPriorNotification.logbookMessageTyped.logbookMessage - .enrichGearPortAndSpecyNames(allGears, allPorts, allSpecies) - - finalPriorNotification - } - - val priorNotification = enrichPriorNotificationWithReportingCount(priorNotificationWithoutReportingCount) + val manualPriorNotification = manualPriorNotificationRepository.findByReportId(reportId) + val automaticPriorNotification = logbookReportRepository.findPriorNotificationByReportId(reportId) + val priorNotification = manualPriorNotification + ?: automaticPriorNotification + ?: throw BackendUsageException(BackendUsageErrorCode.NOT_FOUND) + + 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/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 bf48d41c45..4f38abb1bf 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,17 +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.input.PriorNotificationDataInput -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.outputs.* import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.Parameter import io.swagger.v3.oas.annotations.tags.Tag @@ -78,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, @@ -88,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, @@ -106,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 -> @@ -120,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), ) } @@ -150,39 +159,49 @@ class PriorNotificationController( fun create( @RequestBody priorNotificationDataInput: PriorNotificationDataInput, - ): PriorNotificationDetailDataOutput { - val logbookMessage = priorNotificationDataInput.logbookMessage.toPNO() - val tripGears = priorNotificationDataInput.tripGears.map { it.toLogbookTripGear() } - - return PriorNotificationDetailDataOutput.fromPriorNotification( - createOrUpdatePriorNotification.execute( - null, - logbookMessage, - tripGears, - priorNotificationDataInput.vesselId, - ), + ): PriorNotificationDataOutput { + val cretedPriorNotification = 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(cretedPriorNotification) } - @PutMapping("/{logbookMessageReportId}") - @Operation(summary = "Update a prior notification by its (logbook message) `reportId`") + @PutMapping("/{reportId}") + @Operation(summary = "Update a prior notification by its `reportId`") fun update( @PathParam("Logbook message `reportId`") - @PathVariable(name = "logbookMessageReportId") - logbookMessageReportId: String, + @PathVariable(name = "reportId") + reportId: String, @RequestBody priorNotificationDataInput: PriorNotificationDataInput, - ): PriorNotificationDetailDataOutput { - val logbookMessage = priorNotificationDataInput.logbookMessage.toPNO() - val tripGears = priorNotificationDataInput.tripGears.map { it.toLogbookTripGear() } - - return PriorNotificationDetailDataOutput.fromPriorNotification( - createOrUpdatePriorNotification.execute( - logbookMessageReportId, - logbookMessage, - tripGears, - priorNotificationDataInput.vesselId, - ), + ): 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 index 2368b22118..614b692dd1 100644 --- 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 @@ -10,6 +10,18 @@ data class LogbookFishingCatchInput( 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(), 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 index c4bcc887d5..67c21cc820 100644 --- 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 @@ -1,7 +1,15 @@ package fr.gouv.cnsp.monitorfish.infrastructure.api.input data class PriorNotificationDataInput( - val logbookMessage: LogbookMessageValueForPnoDataInput, - val tripGears: List, + 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/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/LogbookMessageDataOutput.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/outputs/LogbookMessageDataOutput.kt index 725c81b347..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 @@ -23,17 +23,14 @@ data class LogbookMessageDataOutput( val rawMessage: String?, val acknowledgment: Acknowledgment?, - val createdAt: ZonedDateTime?, val isCorrectedByNewerMessage: Boolean, val isDeleted: Boolean, - val isManuallyCreated: Boolean, val isSentByFailoverSoftware: Boolean, val message: LogbookMessageValue?, val messageType: String?, val operationType: LogbookOperationType, val tripGears: List?, val tripSegments: List?, - val updatedAt: ZonedDateTime?, ) { companion object { fun fromLogbookMessage(logbookMessage: LogbookMessage): LogbookMessageDataOutput { @@ -61,17 +58,14 @@ data class LogbookMessageDataOutput( rawMessage = logbookMessage.rawMessage, acknowledgment = logbookMessage.acknowledgment, - createdAt = logbookMessage.createdAt, isCorrectedByNewerMessage = logbookMessage.isCorrectedByNewerMessage, isDeleted = logbookMessage.isDeleted, - isManuallyCreated = logbookMessage.isManuallyCreated, isSentByFailoverSoftware = logbookMessage.isSentByFailoverSoftware, message = logbookMessage.message, messageType = logbookMessage.messageType, operationType = logbookMessage.operationType, tripGears = tripGears, tripSegments = tripSegments, - updatedAt = logbookMessage.updatedAt, ) } } 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 0c06173d33..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,108 +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 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?, + 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, - createdAt = logbookMessage.createdAt.toString(), - expectedArrivalDate = message.predictedArrivalDatetimeUtc?.toString(), - expectedLandingDate = message.predictedLandingDatetimeUtc?.toString(), - hasVesselRiskFactorSegments = priorNotification.vesselRiskFactor?.segments?.isNotEmpty(), - fingerprint = priorNotification.fingerprint, - isCorrection = logbookMessage.operationType === LogbookOperationType.COR, - isManuallyCreated = logbookMessage.isManuallyCreated, - 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, - updatedAt = logbookMessage.updatedAt.toString(), - 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 53674a876c..7990620eff 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 @@ -13,7 +13,6 @@ import org.hibernate.annotations.Type import org.hibernate.dialect.PostgreSQLEnumJdbcType import java.time.Instant import java.time.ZoneOffset.UTC -import java.time.ZonedDateTime @Entity @Table(name = "logbook_reports") @@ -76,12 +75,6 @@ data class LogbookReportEntity( @Type(JsonBinaryType::class) @Column(name = "trip_segments", nullable = true, columnDefinition = "jsonb") val tripSegments: String?, - @Column(name = "is_manually_created", nullable = false) - val isManuallyCreated: Boolean, - @Column(name = "created_at") - val createdAt: ZonedDateTime?, - @Column(name = "updated_at") - val updatedAt: ZonedDateTime?, ) { companion object { fun fromLogbookMessage( @@ -105,16 +98,13 @@ data class LogbookReportEntity( software = logbookMessage.software, transmissionFormat = logbookMessage.transmissionFormat, - createdAt = logbookMessage.createdAt, isEnriched = logbookMessage.isEnriched, - isManuallyCreated = logbookMessage.isManuallyCreated, message = mapper.writeValueAsString(logbookMessage.message), messageType = logbookMessage.messageType, operationCountry = null, operationType = logbookMessage.operationType, tripGears = null, tripSegments = null, - updatedAt = logbookMessage.updatedAt, ) } @@ -138,38 +128,48 @@ data class LogbookReportEntity( tripNumber = tripNumber, flagState = flagState, imo = imo, - analyzedByRules = analyzedByRules ?: listOf(), + analyzedByRules = analyzedByRules ?: emptyList(), software = software, transmissionFormat = transmissionFormat, - createdAt = createdAt, isEnriched = isEnriched, - isManuallyCreated = isManuallyCreated, message = message, messageType = messageType, operationType = operationType, tripGears = tripGears, tripSegments = tripSegments, - updatedAt = updatedAt, ) } 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, vessel = vessel, + + // These props need to be calculated in the use case + port = null, + reportingCount = null, + seafront = null, + vesselRiskFactor = null, ) } 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 index b0d2441fdf..6960722953 100644 --- 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 @@ -1,15 +1,17 @@ package fr.gouv.cnsp.monitorfish.infrastructure.database.entities -import com.neovisionaries.i18n.CountryCode 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.Vessel +import fr.gouv.cnsp.monitorfish.domain.entities.vessel.UNKNOWN_VESSEL +import fr.gouv.cnsp.monitorfish.domain.exceptions.BackendInternalException 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 @@ -20,21 +22,28 @@ data class ManualPriorNotificationEntity( @GeneratedValue(strategy = GenerationType.IDENTITY) val reportId: String?, + @Column(name = "author_trigram") + val authorTrigram: String, + @Column(name = "cfr") - val cfr: String?, + val cfr: 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, // ISO Alpha-3 country code @Column(name = "flag_state") val flagState: String?, - @Column(name = "integration_datetime_utc") - val integrationDateTime: ZonedDateTime, + @Column(name = "note") + val note: String?, - @Column(name = "operation_datetime_utc") - val operationDateTime: ZonedDateTime, - - @Column(name = "report_datetime_utc") - val reportDateTime: ZonedDateTime?, + @Column(name = "sent_at") + val sentAt: ZonedDateTime, @Column(name = "trip_gears", nullable = true, columnDefinition = "jsonb") @Type(JsonBinaryType::class) @@ -44,70 +53,109 @@ data class ManualPriorNotificationEntity( @Type(JsonBinaryType::class) val tripSegments: List?, - @Type(JsonBinaryType::class) + @Column(name = "updated_at") + @UpdateTimestamp + val updatedAt: ZonedDateTime? = null, + @Column(name = "value", nullable = true, columnDefinition = "jsonb") + @Type(JsonBinaryType::class) val value: PNO, @Column(name = "vessel_name") val vesselName: String?, - - @Column(name = "created_at", insertable = false, updatable = false) - @CreationTimestamp - val createdAt: ZonedDateTime? = null, - - @Column(name = "updated_at") - @UpdateTimestamp - var updatedAt: ZonedDateTime? = null, ) { + companion object { - fun fromLogbookMessageTyped(logbookMessageTyped: LogbookMessageTyped): ManualPriorNotificationEntity { - val pnoLogbookMessage = logbookMessageTyped.logbookMessage - val pnoLogbookMessageValue = logbookMessageTyped.typedMessage - - return ManualPriorNotificationEntity( - reportId = pnoLogbookMessage.reportId, - cfr = pnoLogbookMessage.internalReferenceNumber, - flagState = pnoLogbookMessage.flagState, - integrationDateTime = pnoLogbookMessage.integrationDateTime, - operationDateTime = pnoLogbookMessage.operationDateTime, - reportDateTime = pnoLogbookMessage.reportDateTime, - tripGears = pnoLogbookMessage.tripGears, - tripSegments = pnoLogbookMessage.tripSegments, - value = pnoLogbookMessageValue, - vesselName = pnoLogbookMessage.vesselName, - createdAt = pnoLogbookMessage.createdAt, - updatedAt = pnoLogbookMessage.updatedAt, - ) + 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 { - val pnoLogbookMessage = LogbookMessage( - id = null, - reportId = reportId, - analyzedByRules = emptyList(), - integrationDateTime = integrationDateTime, - internalReferenceNumber = cfr, - isManuallyCreated = true, - message = value, - operationDateTime = operationDateTime, - operationNumber = null, - operationType = LogbookOperationType.DAT, - reportDateTime = reportDateTime, - transmissionFormat = null, - vesselName = vesselName, - createdAt = createdAt, - updatedAt = updatedAt, - ) - val fingerprint = listOf(reportId!!, updatedAt.toString()).joinToString(separator = ".") - // For pratical reasons `vessel` can't be `null`, so we temporarely set it to "Navire inconnu" - val vessel = Vessel(id = -1, flagState = CountryCode.UNDEFINED, hasLogbookEsacapt = false) - val logbookMessageTyped = LogbookMessageTyped(pnoLogbookMessage, PNO::class.java) - - return PriorNotification( - fingerprint, - logbookMessageTyped, - vessel = vessel, - ) + 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 = "PNO", + operationDateTime = createdAt, + operationNumber = null, + operationType = LogbookOperationType.DAT, + reportDateTime = sentAt, + transmissionFormat = null, + 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(), + vessel = vessel, + + // These props need to be calculated in the use case + port = null, + reportingCount = null, + seafront = null, + 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/repositories/JpaLogbookReportRepository.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaLogbookReportRepository.kt index 106f11f51d..03d68cfd9a 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 @@ -2,9 +2,9 @@ package fr.gouv.cnsp.monitorfish.infrastructure.database.repositories import com.fasterxml.jackson.databind.ObjectMapper import fr.gouv.cnsp.monitorfish.domain.entities.logbook.* -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.EntityConversionException import fr.gouv.cnsp.monitorfish.domain.exceptions.NoERSMessagesFound import fr.gouv.cnsp.monitorfish.domain.exceptions.NoLogbookFishingTripFound @@ -30,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, @@ -62,12 +62,12 @@ class JpaLogbookReportRepository( } } - 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 { 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..54e6abad45 --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaManualPriorNotificationRepository.kt @@ -0,0 +1,54 @@ +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 { + return dbManualPriorNotificationRepository + .findAll( + flagStates = filter.flagStates ?: emptyList(), + hasOneOrMoreReportings = filter.hasOneOrMoreReportings, + isLessThanTwelveMetersVessel = filter.isLessThanTwelveMetersVessel, + 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/JpaPriorNotificationRepository.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaPriorNotificationRepository.kt deleted file mode 100644 index e2746d3a39..0000000000 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaPriorNotificationRepository.kt +++ /dev/null @@ -1,19 +0,0 @@ -package fr.gouv.cnsp.monitorfish.infrastructure.database.repositories - -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.repositories.PriorNotificationRepository -import fr.gouv.cnsp.monitorfish.infrastructure.database.entities.ManualPriorNotificationEntity -import fr.gouv.cnsp.monitorfish.infrastructure.database.repositories.interfaces.DBPriorNotificationRepository -import org.springframework.stereotype.Repository - -@Repository -class JpaPriorNotificationRepository(private val dbPriorNotificationRepository: DBPriorNotificationRepository) : - PriorNotificationRepository { - override fun save(logbookMessageTyped: LogbookMessageTyped): PriorNotification { - return dbPriorNotificationRepository - .save(ManualPriorNotificationEntity.fromLogbookMessageTyped(logbookMessageTyped)) - .toPriorNotification() - } -} 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 41a53caeab..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,103 +15,31 @@ interface DBLogbookReportRepository : @Query( """ WITH - dat_and_cor_prior_notifications AS ( - SELECT * - FROM logbook_reports - WHERE - -- This filter helps Timescale optimize the query since `operation_datetime_utc` is indexed - operation_datetime_utc - BETWEEN CAST(:willArriveAfter AS TIMESTAMP) - INTERVAL '48 hours' - AND CAST(:willArriveBefore AS TIMESTAMP) + INTERVAL '48 hours' - - AND log_type = 'PNO' - AND operation_type IN ('DAT', 'COR') - AND enriched = TRUE - - -- Flag States - AND (:flagStates IS NULL OR flag_state IN (:flagStates)) - - -- Port Locodes - AND (:portLocodes IS NULL OR value->>'port' IN (:portLocodes)) - - -- Search Query - AND (:searchQuery IS NULL OR unaccent(lower(vessel_name)) ILIKE CONCAT('%', unaccent(lower(:searchQuery)), '%')) - - -- Will Arrive After - AND value->>'predictedArrivalDatetimeUtc' >= :willArriveAfter - - -- Will Arrive Before - AND value->>'predictedArrivalDatetimeUtc' <= :willArriveBefore - - UNION ALL - + dat_and_cor_pno_logbook_reports_with_extra_columns AS ( SELECT - id, - CAST(NULL AS TEXT) AS operation_number, - CAST(NULL AS TEXT) AS operation_country, - operation_datetime_utc, - CAST('DAT' AS TEXT) AS operation_type, - report_id, - CAST(NULL AS TEXT) AS referenced_report_id, - report_datetime_utc, - cfr, - CAST(NULL AS TEXT) AS ircs, - CAST(NULL AS TEXT) AS external_identification, - vessel_name, - flag_state, - CAST(NULL AS TEXT) AS imo, - CAST('PNO' AS TEXT) AS log_type, - value, - integration_datetime_utc, - CAST(NULL AS TEXT) AS trip_number, - CAST(NULL AS TEXT[]) AS analyzed_by_rules, - CAST(TRUE AS BOOLEAN) AS trip_number_was_computed, - -- TODO /!\ CHECK IF THIS IS WHAT WE WANT /!\ - CAST(NULL AS public.logbook_message_transmission_format) AS transmission_format, - CAST(NULL AS TEXT) AS software, - CAST(TRUE AS BOOLEAN) AS enriched, - trip_gears, - trip_segments, - is_manually_created, - created_at, - updated_at - FROM manual_prior_notifications + lr.*, + (SELECT array_agg(pnoTypes->>'pnoTypeName') FROM jsonb_array_elements(lr.value->'pnoTypes') AS pnoTypes) AS prior_notification_type_names, + (SELECT array_agg(catchOnboard->>'species') FROM jsonb_array_elements(lr.value->'catchOnboard') AS catchOnboard) AS specy_codes, + (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 risk_factors rf ON lr.cfr = rf.cfr + LEFT JOIN vessels v ON lr.cfr = v.cfr WHERE - -- TODO /!\ INDEX operation_datetime_utc WITH TIMESCALE /!\ -- This filter helps Timescale optimize the query since `operation_datetime_utc` is indexed - operation_datetime_utc + lr.operation_datetime_utc BETWEEN CAST(:willArriveAfter AS TIMESTAMP) - INTERVAL '48 hours' AND CAST(:willArriveBefore AS TIMESTAMP) + INTERVAL '48 hours' - -- Flag States - AND (:flagStates IS NULL OR flag_state IN (:flagStates)) - - -- Port Locodes - AND (:portLocodes IS NULL OR value->>'port' IN (:portLocodes)) - - -- Search Query - AND (:searchQuery IS NULL OR unaccent(lower(vessel_name)) ILIKE CONCAT('%', unaccent(lower(:searchQuery)), '%')) - - -- Will Arrive After - AND value->>'predictedArrivalDatetimeUtc' >= :willArriveAfter + AND lr.log_type = 'PNO' + AND lr.operation_type IN ('DAT', 'COR') + AND lr.enriched = TRUE - -- Will Arrive Before - AND value->>'predictedArrivalDatetimeUtc' <= :willArriveBefore - ), + -- Flag States + AND (:flagStates IS NULL OR lr.flag_state IN (:flagStates)) - dat_and_cor_prior_notifications_with_extra_columns AS ( - SELECT - dacpn.*, - (SELECT array_agg(pnoTypes->>'pnoTypeName') FROM jsonb_array_elements(dacpn.value->'pnoTypes') AS pnoTypes) AS prior_notification_type_names, - (SELECT array_agg(catchOnboard->>'species') FROM jsonb_array_elements(dacpn.value->'catchOnboard') AS catchOnboard) AS specy_codes, - (SELECT array_agg(tripGears->>'gear') FROM jsonb_array_elements(dacpn.trip_gears) AS tripGears) AS trip_gear_codes, - (SELECT array_agg(tripSegments->>'segment') FROM jsonb_array_elements(dacpn.trip_segments) AS tripSegments) AS trip_segment_codes - FROM dat_and_cor_prior_notifications dacpn - LEFT JOIN risk_factors rf ON dacpn.cfr = rf.cfr - LEFT JOIN vessels v ON dacpn.cfr = v.cfr - WHERE -- Is Less Than Twelve Meters Vessel - ( + AND ( :isLessThanTwelveMetersVessel IS NULL OR (:isLessThanTwelveMetersVessel = TRUE AND v.length < 12) OR (:isLessThanTwelveMetersVessel = FALSE AND v.length >= 12) @@ -122,11 +50,23 @@ interface DBLogbookReportRepository : -- Last Controlled Before AND (:lastControlledBefore IS NULL OR rf.last_control_datetime_utc <= CAST(:lastControlledBefore AS TIMESTAMP)) + + -- Port Locodes + AND (:portLocodes IS NULL OR lr.value->>'port' IN (:portLocodes)) + + -- Search Query + AND (:searchQuery IS NULL OR unaccent(lower(lr.vessel_name)) ILIKE CONCAT('%', unaccent(lower(:searchQuery)), '%')) + + -- Will Arrive After + AND lr.value->>'predictedArrivalDatetimeUtc' >= :willArriveAfter + + -- Will Arrive Before + AND lr.value->>'predictedArrivalDatetimeUtc' <= :willArriveBefore ), distinct_cfrs AS ( SELECT DISTINCT cfr - FROM dat_and_cor_prior_notifications_with_extra_columns + FROM dat_and_cor_pno_logbook_reports_with_extra_columns ), cfr_reporting_counts AS ( @@ -142,17 +82,17 @@ interface DBLogbookReportRepository : GROUP BY cfr ), - dat_and_cor_prior_notifications_with_extra_columns_and_reporting_count AS ( + dat_and_cor_pno_logbook_reports_with_extra_columns_and_reporting_count AS ( SELECT - dacpnwecarc.*, + dacplrwecarc.*, COALESCE(crc.reporting_count, 0) AS reporting_count - FROM dat_and_cor_prior_notifications_with_extra_columns dacpnwecarc - LEFT JOIN cfr_reporting_counts crc ON dacpnwecarc.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 ), - filtered_dat_and_cor_prior_notifications AS ( + filtered_dat_and_cor_pno_logbook_reports AS ( SELECT * - FROM dat_and_cor_prior_notifications_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 ( @@ -174,7 +114,7 @@ interface DBLogbookReportRepository : AND (:tripSegmentCodesAsSqlArrayString IS NULL OR trip_segment_codes && CAST(:tripSegmentCodesAsSqlArrayString AS TEXT[])) ), - del_prior_notifications AS ( + del_pno_logbook_reports AS ( SELECT lr.*, CAST(NULL AS TEXT[]) AS prior_notification_type_names, @@ -183,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 filtered_dat_and_cor_prior_notifications fdacpn ON lr.referenced_report_id = fdacpn.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 @@ -193,7 +133,7 @@ interface DBLogbookReportRepository : AND lr.operation_type = 'DEL' ), - ret_prior_notifications AS ( + ret_pno_logbook_reports AS ( SELECT lr.*, CAST(NULL AS TEXT[]) AS prior_notification_type_names, @@ -202,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 filtered_dat_and_cor_prior_notifications fdacpn ON lr.referenced_report_id = fdacpn.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 @@ -213,17 +153,17 @@ interface DBLogbookReportRepository : ) SELECT * - FROM filtered_dat_and_cor_prior_notifications + FROM filtered_dat_and_cor_pno_logbook_reports UNION SELECT * - FROM del_prior_notifications + FROM del_pno_logbook_reports UNION SELECT * - FROM ret_prior_notifications; + FROM ret_pno_logbook_reports; """, nativeQuery = true, ) @@ -252,7 +192,7 @@ interface DBLogbookReportRepository : SELECT report_id FROM logbook_reports WHERE - report_id = ?1 + report_id = :reportId AND log_type = 'PNO' AND operation_type = 'DAT' AND enriched = TRUE @@ -263,7 +203,7 @@ interface DBLogbookReportRepository : SELECT report_id FROM logbook_reports WHERE - referenced_report_id = ?1 + referenced_report_id = :reportId AND log_type = 'PNO' AND operation_type = 'COR' AND enriched = TRUE @@ -274,45 +214,7 @@ interface DBLogbookReportRepository : 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) - - UNION ALL - - SELECT - id, - CAST(NULL AS TEXT) AS operation_number, - CAST(NULL AS TEXT) AS operation_country, - operation_datetime_utc, - CAST('DAT' AS TEXT) AS operation_type, - CAST(report_id AS TEXT) AS report_id, - CAST(NULL AS TEXT) AS referenced_report_id, - report_datetime_utc, - cfr, - CAST(NULL AS TEXT) AS ircs, - CAST(NULL AS TEXT) AS external_identification, - vessel_name, - flag_state, - CAST(NULL AS TEXT) AS imo, - CAST('PNO' AS TEXT) AS log_type, - value, - integration_datetime_utc, - CAST(NULL AS TEXT) AS trip_number, - CAST(NULL AS TEXT[]) AS analyzed_by_rules, - CAST(TRUE AS BOOLEAN) AS trip_number_was_computed, - -- TODO /!\ CHECK IF THIS IS WHAT WE WANT /!\ - CAST(NULL AS public.logbook_message_transmission_format) AS transmission_format, - CAST(NULL AS TEXT) AS software, - CAST(TRUE AS BOOLEAN) AS enriched, - trip_gears, - trip_segments, - is_manually_created, - created_at, - updated_at - FROM manual_prior_notifications - WHERE - report_id = ?1 - - 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..b6245d15ba --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/interfaces/DBManualPriorNotificationRepository.kt @@ -0,0 +1,139 @@ +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 + LEFT JOIN vessels v ON mpn.cfr = v.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)) + + -- Is Less Than Twelve Meters Vessel + AND ( + :isLessThanTwelveMetersVessel IS NULL + OR (:isLessThanTwelveMetersVessel = TRUE AND v.length < 12) + OR (:isLessThanTwelveMetersVessel = FALSE AND v.length >= 12) + ) + + -- 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?, + isLessThanTwelveMetersVessel: 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/database/repositories/interfaces/DBPriorNotificationRepository.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/interfaces/DBPriorNotificationRepository.kt deleted file mode 100644 index b5106a51a4..0000000000 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/interfaces/DBPriorNotificationRepository.kt +++ /dev/null @@ -1,8 +0,0 @@ -package fr.gouv.cnsp.monitorfish.infrastructure.database.repositories.interfaces - -import fr.gouv.cnsp.monitorfish.infrastructure.database.entities.ManualPriorNotificationEntity -import org.springframework.data.repository.CrudRepository - -interface DBPriorNotificationRepository : CrudRepository { - fun save(priorNotificationEntity: 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__Create_manual_prior_notifications_table.sql b/backend/src/main/resources/db/migration/internal/V0.256__Create_manual_prior_notifications_table.sql index ae7c3c4ace..a0ce832544 100644 --- a/backend/src/main/resources/db/migration/internal/V0.256__Create_manual_prior_notifications_table.sql +++ b/backend/src/main/resources/db/migration/internal/V0.256__Create_manual_prior_notifications_table.sql @@ -1,43 +1,23 @@ CREATE EXTENSION IF NOT EXISTS pgcrypto; CREATE TABLE public.manual_prior_notifications ( - id BIGINT DEFAULT nextval('public.logbook_report_id_seq'::regclass) NOT NULL, - -- operation_number VARCHAR(100), - -- operation_country VARCHAR(3), - operation_datetime_utc TIMESTAMP WITHOUT TIME ZONE NOT NULL, - -- operation_type VARCHAR(3), + -- Common columns with `logbook_reports` + report_id VARCHAR(36) PRIMARY KEY DEFAULT gen_random_uuid(), - -- referenced_report_id VARCHAR(100), - report_datetime_utc TIMESTAMP WITHOUT TIME ZONE, + report_datetime_utc TIMESTAMP WITH TIME ZONE, cfr VARCHAR(12), - -- ircs VARCHAR(7), - -- external_identification VARCHAR(14), vessel_name VARCHAR(100), flag_state VARCHAR(3), - -- imo VARCHAR(20), - -- log_type VARCHAR(100), value JSONB, - integration_datetime_utc TIMESTAMP WITHOUT TIME ZONE, - -- trip_number VARCHAR(100), - -- analyzed_by_rules VARCHAR(100)[], - -- trip_number_was_computed BOOLEAN DEFAULT FALSE, - -- transmission_format public.logbook_message_transmission_format NOT NULL, - -- software VARCHAR(100), - -- enriched BOOLEAN DEFAULT FALSE NOT NULL, trip_gears JSONB, trip_segments JSONB, - is_manually_created BOOLEAN DEFAULT TRUE NOT NULL, - created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, - updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL -); -ALTER TABLE public.logbook_reports - ADD COLUMN is_manually_created BOOLEAN DEFAULT FALSE NOT NULL, - ADD COLUMN created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now(), - ADD COLUMN updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now(); + -- Columns specific to `manual_prior_notifications` -UPDATE public.logbook_reports -SET - created_at = operation_datetime_utc, - updated_at = operation_datetime_utc -WHERE operation_datetime_utc IS NOT NULL; + 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.5.2__Insert_dummy_manual_prior_notifications.sql b/backend/src/main/resources/db/testdata/V666.5.2__Insert_dummy_manual_prior_notifications.sql index f288573ef1..bd055542e5 100644 --- 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 @@ -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 manual_prior_notifications (id, report_id, cfr, flag_state, integration_datetime_utc, operation_datetime_utc, report_datetime_utc, vessel_name, trip_gears, trip_segments, value) VALUES (116, 'd2bd7d28-3130-4e3f-bf0d-5e919a5657be', 'CFR112', 'FRA', NOW() AT TIME ZONE 'UTC' - INTERVAL '15 minutes', NOW() AT TIME ZONE 'UTC' - INTERVAL '15 minutes', NOW() AT TIME ZONE 'UTC' - INTERVAL '15 minutes', 'POISSON PAS NET', '[{"gear":"LNP","mesh":8,"dimensions":"50;200"}]', '[{"segment":"NWW09","segmentName":"Lignes"}]', '{"catchOnboard":[{"weight":72,"nbFish":null,"species":"SOS"}],"pnoTypes":[{"pnoTypeName":"Préavis type D","minimumNotificationPeriod":4,"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 id = 116; -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 id = 116; -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 id = 116; +INSERT INTO manual_prior_notifications (report_id, author_trigram, cfr, did_not_fish_after_zero_notice, flag_state, note, sent_at, trip_gears, trip_segments, vessel_name, value) VALUES ('d2bd7d28-3130-4e3f-bf0d-5e919a5657be', 'ABC', 'CFR112', false, 'FRA', NULL, NOW() AT TIME ZONE 'UTC' - INTERVAL '15 minutes', '[{"gear":"LNP","mesh":8,"dimensions":"50;200"}]', '[{"segment":"NWW09","segmentName":"Lignes"}]', 'POISSON PAS NET', '{"catchOnboard":[{"weight":72,"nbFish":null,"species":"SOS"}],"faoZone":"21.1.A","pnoTypes":[{"pnoTypeName":"Préavis type D","minimumNotificationPeriod":4,"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 = 'd2bd7d28-3130-4e3f-bf0d-5e919a5657be'; +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 = 'd2bd7d28-3130-4e3f-bf0d-5e919a5657be'; +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 = 'd2bd7d28-3130-4e3f-bf0d-5e919a5657be'; 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 index f5f4851c68..22dd5730b4 100644 --- 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 @@ -1,18 +1,19 @@ [ { "table": "manual_prior_notifications", + "id": "report_id", "data": [ { - "id": 116, "report_id": "d2bd7d28-3130-4e3f-bf0d-5e919a5657be", + "author_trigram": "ABC", "cfr": "CFR112", + "did_not_fish_after_zero_notice": false, "flag_state": "FRA", - "integration_datetime_utc:sql": "NOW() AT TIME ZONE 'UTC' - INTERVAL '15 minutes'", - "operation_datetime_utc:sql": "NOW() AT TIME ZONE 'UTC' - INTERVAL '15 minutes'", - "report_datetime_utc:sql": "NOW() AT TIME ZONE 'UTC' - INTERVAL '15 minutes'", - "vessel_name": "POISSON PAS NET", + "note": null, + "sent_at:sql": "NOW() AT TIME ZONE 'UTC' - INTERVAL '15 minutes'", "trip_gears:jsonb": [{ "gear": "LNP", "mesh": 8, "dimensions": "50;200" }], "trip_segments:jsonb": [{ "segment": "NWW09", "segmentName": "Lignes" }], + "vessel_name": "POISSON PAS NET", "value:jsonb": { "catchOnboard": [ { @@ -21,6 +22,7 @@ "species": "SOS" } ], + "faoZone": "21.1.A", "pnoTypes": [ { "pnoTypeName": "Préavis type D", diff --git a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/LogbookMessageUTests.kt b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/LogbookMessageUTests.kt index cd3eebfb29..ec9c19135f 100644 --- a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/LogbookMessageUTests.kt +++ b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/entities/logbook/LogbookMessageUTests.kt @@ -28,17 +28,14 @@ class LogbookMessageUTests { reportId = reportId, referencedReportId = referenceReportId, analyzedByRules = emptyList(), - createdAt = ZonedDateTime.now(), integrationDateTime = ZonedDateTime.now(), isEnriched = false, - isManuallyCreated = false, message = message, operationDateTime = ZonedDateTime.now(), operationNumber = "FAKE_OPERATION_NUMBER_$reportId", operationType = operationType, reportDateTime = reportDateTime, transmissionFormat = LogbookTransmissionFormat.ERS, - updatedAt = ZonedDateTime.now(), ) } } 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 da0dc5aab1..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 @@ -283,10 +283,8 @@ class GetLogbookMessagesUTests { reportId = "9065646816", referencedReportId = "9065646811", analyzedByRules = listOf(), - createdAt = ZonedDateTime.now(), integrationDateTime = ZonedDateTime.now(), isEnriched = false, - isManuallyCreated = false, message = lastAck, messageType = "", operationDateTime = ZonedDateTime.now(), @@ -296,7 +294,6 @@ class GetLogbookMessagesUTests { 12, ), transmissionFormat = LogbookTransmissionFormat.ERS, - updatedAt = 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 32f0ffc75a..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 @@ -126,7 +126,6 @@ object TestUtils { LogbookMessage( id = 2, analyzedByRules = listOf(), - isManuallyCreated = false, operationNumber = "", tripNumber = "345", reportId = "", @@ -152,7 +151,6 @@ object TestUtils { LogbookMessage( id = 1, analyzedByRules = listOf(), - isManuallyCreated = false, operationNumber = "", tripNumber = "345", reportId = "", @@ -178,7 +176,6 @@ object TestUtils { LogbookMessage( id = 3, analyzedByRules = listOf(), - isManuallyCreated = false, operationNumber = "", tripNumber = "345", reportId = "", @@ -204,7 +201,6 @@ object TestUtils { LogbookMessage( id = 3, analyzedByRules = listOf(), - isManuallyCreated = false, operationNumber = "", tripNumber = "345", reportId = "", @@ -230,7 +226,6 @@ object TestUtils { LogbookMessage( id = 4, analyzedByRules = listOf(), - isManuallyCreated = false, operationNumber = "", tripNumber = "345", reportId = "", @@ -249,7 +244,6 @@ object TestUtils { LogbookMessage( id = 5, analyzedByRules = listOf(), - isManuallyCreated = false, operationNumber = "", tripNumber = "345", reportId = "", @@ -301,7 +295,6 @@ object TestUtils { LogbookMessage( id = 1, analyzedByRules = listOf(), - isManuallyCreated = false, operationNumber = "", tripNumber = "345", reportId = "", @@ -327,7 +320,6 @@ object TestUtils { LogbookMessage( id = 2, analyzedByRules = listOf(), - isManuallyCreated = false, operationNumber = "", tripNumber = "345", reportId = "", @@ -353,7 +345,6 @@ object TestUtils { LogbookMessage( id = 3, analyzedByRules = listOf(), - isManuallyCreated = false, operationNumber = "", tripNumber = "345", reportId = "", @@ -405,7 +396,6 @@ object TestUtils { LogbookMessage( id = 1, analyzedByRules = listOf(), - isManuallyCreated = false, operationNumber = "9065646811", tripNumber = "345", reportId = "9065646811", @@ -430,7 +420,6 @@ object TestUtils { LogbookMessage( id = 2, analyzedByRules = listOf(), - isManuallyCreated = false, operationNumber = "", tripNumber = "345", reportId = "", @@ -489,7 +478,6 @@ object TestUtils { LogbookMessage( id = 1, analyzedByRules = listOf(), - isManuallyCreated = false, operationNumber = "", tripNumber = "345", reportId = "9065646811", @@ -514,7 +502,6 @@ object TestUtils { LogbookMessage( id = 2, analyzedByRules = listOf(), - isManuallyCreated = false, operationNumber = "", reportId = "9065646816", referencedReportId = "9065646811", @@ -539,7 +526,6 @@ object TestUtils { LogbookMessage( id = 3, analyzedByRules = listOf(), - isManuallyCreated = false, operationNumber = "", tripNumber = "345", reportId = "9065646813", @@ -564,7 +550,6 @@ object TestUtils { LogbookMessage( id = 4, analyzedByRules = listOf(), - isManuallyCreated = false, operationNumber = "", reportId = "9065646818", referencedReportId = "9065646813", @@ -589,7 +574,6 @@ object TestUtils { LogbookMessage( id = 5, analyzedByRules = listOf(), - isManuallyCreated = false, operationNumber = "", referencedReportId = "9065646813", operationType = LogbookOperationType.DEL, @@ -613,7 +597,6 @@ object TestUtils { LogbookMessage( id = 6, analyzedByRules = listOf(), - isManuallyCreated = false, operationNumber = "5h499-erh5u7-pm3ae8c5trj78j67dfh", tripNumber = "SCR-TTT20200505030505", reportId = "zegj15-zeg56-errg569iezz3659g", @@ -722,7 +705,6 @@ object TestUtils { LogbookMessage( id = 1, analyzedByRules = listOf(), - isManuallyCreated = false, operationNumber = "456846844658", tripNumber = "125345", reportId = "456846844658", @@ -738,7 +720,6 @@ object TestUtils { LogbookMessage( id = 2, analyzedByRules = listOf(), - isManuallyCreated = false, operationNumber = "47177857577", tripNumber = "125345", reportId = "47177857577", @@ -756,7 +737,6 @@ object TestUtils { LogbookMessage( id = 3, analyzedByRules = listOf(), - isManuallyCreated = false, operationNumber = "48545254254", tripNumber = "125345", reportId = "48545254254", @@ -772,7 +752,6 @@ object TestUtils { LogbookMessage( id = 4, analyzedByRules = listOf(), - isManuallyCreated = false, operationNumber = "004045204504", tripNumber = "125345", reportId = "004045204504", @@ -854,7 +833,6 @@ object TestUtils { LogbookMessage( id = 1, analyzedByRules = listOf(), - isManuallyCreated = false, operationNumber = "456846844658", tripNumber = "125345", reportId = "456846844658", @@ -870,7 +848,6 @@ object TestUtils { LogbookMessage( id = 2, analyzedByRules = listOf(), - isManuallyCreated = false, operationNumber = "47177857577", tripNumber = "125345", reportId = "47177857577", @@ -888,7 +865,6 @@ object TestUtils { LogbookMessage( id = 3, analyzedByRules = listOf(), - isManuallyCreated = false, operationNumber = "48545254254", tripNumber = "125345", reportId = "48545254254", @@ -904,7 +880,6 @@ object TestUtils { LogbookMessage( id = 4, analyzedByRules = listOf(), - isManuallyCreated = false, operationNumber = "004045204504", tripNumber = "125345", reportId = "004045204504", 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 6c3b1b25b7..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( @@ -60,7 +65,6 @@ class GetPriorNotificationUTests { integrationDateTime = ZonedDateTime.now(), isCorrectedByNewerMessage = false, isEnriched = true, - isManuallyCreated = false, message = PNO(), messageType = "PNO", operationDateTime = ZonedDateTime.now(), @@ -70,20 +74,13 @@ class GetPriorNotificationUTests { 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, ), ) @@ -93,6 +90,7 @@ class GetPriorNotificationUTests { gearRepository, logbookRawMessageRepository, logbookReportRepository, + manualPriorNotificationRepository, portRepository, reportingRepository, riskFactorRepository, @@ -110,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( @@ -122,7 +124,6 @@ class GetPriorNotificationUTests { integrationDateTime = ZonedDateTime.now(), isCorrectedByNewerMessage = true, isEnriched = true, - isManuallyCreated = false, message = PNO(), messageType = "PNO", operationDateTime = ZonedDateTime.now(), @@ -132,20 +133,13 @@ class GetPriorNotificationUTests { 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, ), ) @@ -155,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 86% 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..4eb963eb8c 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 @@ -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,17 +193,17 @@ 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("CFR101") assertThat(firstPriorNotificationWithKnownVessel.logbookMessageTyped.logbookMessage.internalReferenceNumber) .isEqualTo("CFR101") assertThat(firstPriorNotificationWithKnownVessel.logbookMessageTyped.logbookMessage.vesselName) @@ -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 @@ -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 75% 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 afe25c8662..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( @@ -70,7 +75,6 @@ class GetPriorNotificationsUTests { isCorrectedByNewerMessage = false, isDeleted = false, isEnriched = false, - isManuallyCreated = false, message = PNO(), operationDateTime = ZonedDateTime.now(), operationNumber = "1", @@ -79,25 +83,22 @@ class GetPriorNotificationsUTests { 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( @@ -109,7 +110,6 @@ class GetPriorNotificationsUTests { isCorrectedByNewerMessage = false, isDeleted = false, isEnriched = false, - isManuallyCreated = false, message = PNO(), operationDateTime = ZonedDateTime.now(), operationNumber = "1", @@ -118,20 +118,13 @@ class GetPriorNotificationsUTests { 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, ), ), @@ -141,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 6d92381d60..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 @@ -53,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,7 +69,6 @@ class PriorNotificationControllerITests { isCorrectedByNewerMessage = false, isDeleted = false, isEnriched = false, - isManuallyCreated = false, message = PNO(), operationDateTime = ZonedDateTime.now(), operationNumber = "1", @@ -74,25 +77,28 @@ class PriorNotificationControllerITests { 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( @@ -104,7 +110,6 @@ class PriorNotificationControllerITests { isCorrectedByNewerMessage = true, isDeleted = false, isEnriched = false, - isManuallyCreated = false, message = PNO(), operationDateTime = ZonedDateTime.now(), operationNumber = "1", @@ -113,19 +118,18 @@ class PriorNotificationControllerITests { 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, ), @@ -168,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( @@ -180,7 +188,6 @@ class PriorNotificationControllerITests { isCorrectedByNewerMessage = false, isDeleted = false, isEnriched = true, - isManuallyCreated = false, message = PNO(), operationDateTime = ZonedDateTime.now(), operationNumber = "1", @@ -189,19 +196,21 @@ class PriorNotificationControllerITests { 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/JpaLogbookReportRepositoryITests.kt b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaLogbookReportRepositoryITests.kt index b297adeb06..4be441a7b7 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", @@ -636,7 +637,7 @@ class JpaLogbookReportRepositoryITests : AbstractDBTests() { val expectedLogbookReportIdsWithOneOrMoreReportings = listOf(102L) // 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", @@ -721,7 +722,7 @@ class JpaLogbookReportRepositoryITests : AbstractDBTests() { @Transactional fun `findAllPriorNotifications Should return PNO logbook reports 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", @@ -1143,18 +1144,16 @@ class JpaLogbookReportRepositoryITests : AbstractDBTests() { reportId = reportId, referencedReportId = referenceReportId, analyzedByRules = null, - createdAt = ZonedDateTime.now(), externalReferenceNumber = null, flagState = null, - integrationDateTime = ZonedDateTime.now().toInstant(), + integrationDateTime = Instant.now(), internalReferenceNumber = null, imo = null, - isManuallyCreated = false, ircs = null, message = null, messageType = null, operationCountry = null, - operationDateTime = ZonedDateTime.now().toInstant(), + operationDateTime = Instant.now(), operationNumber = "FAKE_OPERATION_NUMBER_$reportId", operationType = operationType, reportDateTime = null, @@ -1163,7 +1162,6 @@ class JpaLogbookReportRepositoryITests : AbstractDBTests() { tripGears = null, tripNumber = null, tripSegments = null, - updatedAt = ZonedDateTime.now(), vesselName = null, ) } From 250b10c9286513a5165cfda341256a1d65d28d6f Mon Sep 17 00:00:00 2001 From: Ivan Gabriele Date: Thu, 6 Jun 2024 02:23:18 +0200 Subject: [PATCH 04/26] Add prior notification form in Frontend --- frontend/package-lock.json | 6 +- frontend/src/api/constants.ts | 3 +- frontend/src/api/faoAreas.ts | 10 +- frontend/src/api/vessel.ts | 22 +- .../PriorNotification.types.ts | 25 +- .../components/PriorNotificationForm/Form.tsx | 103 ++++++++ .../PriorNotificationForm/Header.tsx | 71 ++++++ .../PriorNotificationForm/TagBar.tsx | 19 ++ .../PriorNotificationForm/constants.ts | 43 ++++ .../FormikFishingCatchesMultiSelect.tsx | 204 ++++++++++++++++ .../fields/FormikVesselSelect.tsx | 116 +++++++++ .../PriorNotificationForm/index.tsx | 227 ++++++++++++++++++ .../components/PriorNotificationForm/types.ts | 9 + .../components/PriorNotificationForm/utils.ts | 5 + .../PriorNotificationList/ButtonsGroupRow.tsx | 37 ++- .../PriorNotificationList/index.tsx | 31 ++- .../PriorNotification/priorNotificationApi.ts | 51 +++- .../src/features/PriorNotification/slice.ts | 13 + .../useCases/openPriorNotificationCard.ts | 2 +- frontend/src/features/Vessel/Vessel.types.ts | 39 +++ frontend/src/features/Vessel/vesselApi.ts | 20 ++ frontend/src/features/VesselSearch/index.tsx | 42 ++-- frontend/src/hooks/useGetFaoAreasAsOptions.ts | 36 +++ frontend/src/hooks/useGetGearsAsOptions.ts | 36 +++ frontend/src/hooks/useGetPortsAsOptions.ts | 35 +++ frontend/src/libs/DisplayedError/constants.ts | 1 + 26 files changed, 1143 insertions(+), 63 deletions(-) create mode 100644 frontend/src/features/PriorNotification/components/PriorNotificationForm/Form.tsx create mode 100644 frontend/src/features/PriorNotification/components/PriorNotificationForm/Header.tsx create mode 100644 frontend/src/features/PriorNotification/components/PriorNotificationForm/TagBar.tsx create mode 100644 frontend/src/features/PriorNotification/components/PriorNotificationForm/constants.ts create mode 100644 frontend/src/features/PriorNotification/components/PriorNotificationForm/fields/FormikFishingCatchesMultiSelect.tsx create mode 100644 frontend/src/features/PriorNotification/components/PriorNotificationForm/fields/FormikVesselSelect.tsx create mode 100644 frontend/src/features/PriorNotification/components/PriorNotificationForm/index.tsx create mode 100644 frontend/src/features/PriorNotification/components/PriorNotificationForm/types.ts create mode 100644 frontend/src/features/PriorNotification/components/PriorNotificationForm/utils.ts create mode 100644 frontend/src/features/Vessel/vesselApi.ts create mode 100644 frontend/src/hooks/useGetFaoAreasAsOptions.ts create mode 100644 frontend/src/hooks/useGetGearsAsOptions.ts create mode 100644 frontend/src/hooks/useGetPortsAsOptions.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ac76a6c5d2..d34b9bb20f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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/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/PriorNotification/PriorNotification.types.ts b/frontend/src/features/PriorNotification/PriorNotification.types.ts index 257ca55ebc..22b259b528 100644 --- a/frontend/src/features/PriorNotification/PriorNotification.types.ts +++ b/frontend/src/features/PriorNotification/PriorNotification.types.ts @@ -2,7 +2,7 @@ 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 @@ -50,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/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..ae58842dfd --- /dev/null +++ b/frontend/src/features/PriorNotification/components/PriorNotificationForm/Header.tsx @@ -0,0 +1,71 @@ +import { Accent, Icon, IconButton } from '@mtes-mct/monitor-ui' +import styled from 'styled-components' + +type HeaderProps = Readonly<{ + onClose: () => void +}> +export function Header({ onClose }: HeaderProps) { + return ( + + + <TitleRow> + <TitleRowIconBox> + <Icon.Fishery /> + </TitleRowIconBox> + + <span>{`AJOUTER UN NOUVEAU PRÉAVIS (< 12 M)`}</span> + </TitleRow> + + + + + ) +} + +const Wrapper = styled.div` + align-items: flex-start; + border-bottom: 1px solid ${p => p.theme.color.lightGray}; + box-shadow: 0px 3px 6px #cccfd680; + 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; + } +` 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..ab037867d7 --- /dev/null +++ b/frontend/src/features/PriorNotification/components/PriorNotificationForm/constants.ts @@ -0,0 +1,43 @@ +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(), + didNotFishAfterZeroNotice: boolean().required(), + expectedArrivalDate: string().required(), + expectedLandingDate: string(), + faoArea: string().required(), + fishingCatches: array().of(FISHING_CATCH_VALIDATION_SCHEMA.required()).ensure().required().min(1), + isExpectedLandingDateSameAsExpectedArrivalDate: boolean().required(), + note: string(), + portLocode: string().required(), + sentAt: string().required(), + tripGearCodes: array().of(string().required()).ensure().required().min(1), + vesselId: number().required() +}) + +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.tsx b/frontend/src/features/PriorNotification/components/PriorNotificationForm/fields/FormikFishingCatchesMultiSelect.tsx new file mode 100644 index 0000000000..ff0b702b6a --- /dev/null +++ b/frontend/src/features/PriorNotification/components/PriorNotificationForm/fields/FormikFishingCatchesMultiSelect.tsx @@ -0,0 +1,204 @@ +import { useGetSpeciesAsOptions } from '@hooks/useGetSpeciesAsOptions' +import { FormikNumberInput, Select, SingleTag } from '@mtes-mct/monitor-ui' +import { useField } from 'formik' +import { Fragment } from 'react/jsx-runtime' +import styled from 'styled-components' + +import { BLUEFIN_TUNA_EXTENDED_SPECY_CODES } from '../constants' + +import type { PriorNotification } from '../../../PriorNotification.types' + +export function FormikFishingCatchesMultiSelect() { + const [input, , helper] = useField('fishingCatches') + const { speciesAsOptions } = useGetSpeciesAsOptions() + + 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 nextFishingCatches = [ + ...input.value, + ...(specyCode === 'BFT' + ? [ + { + quantity: undefined, + specyCode: 'BFT', + specyName: specyOption.label, + weight: 0 + }, + { + quantity: 0, + specyCode: 'BF1', + specyName: specyOption.label, + weight: 0 + }, + { + quantity: 0, + specyCode: 'BF2', + specyName: specyOption.label, + weight: 0 + }, + { + quantity: 0, + specyCode: 'BF3', + specyName: specyOption.label, + weight: 0 + } + ] + : [ + { + quantity: undefined, + specyCode: specyOption.value, + specyName: specyOption.label, + weight: 0 + } + ]) + ] + + helper.setValue(nextFishingCatches) + } + + const remove = (specyCode: string | undefined) => { + const nextFishingCatches = input.value.filter(fishingCatch => + specyCode === 'BFT' + ? !['BFT', 'BF1', 'BF2', 'BF3'].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' } From 0f2ca0e826c886eedf45addd30de10c6a2451d69 Mon Sep 17 00:00:00 2001 From: Ivan Gabriele Date: Thu, 6 Jun 2024 03:01:42 +0200 Subject: [PATCH 05/26] Update SQL migration file name index following rebase --- .../db/migration/internal/V0.256__Update_vessels_table.sql | 2 +- ....sql => V0.257__Create_manual_prior_notifications_table.sql} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename backend/src/main/resources/db/migration/internal/{V0.256__Create_manual_prior_notifications_table.sql => V0.257__Create_manual_prior_notifications_table.sql} (100%) 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.256__Create_manual_prior_notifications_table.sql b/backend/src/main/resources/db/migration/internal/V0.257__Create_manual_prior_notifications_table.sql similarity index 100% rename from backend/src/main/resources/db/migration/internal/V0.256__Create_manual_prior_notifications_table.sql rename to backend/src/main/resources/db/migration/internal/V0.257__Create_manual_prior_notifications_table.sql From b5a6a3a0f62cefe93166c54d127a09cc2ccb1eab Mon Sep 17 00:00:00 2001 From: Ivan Gabriele Date: Thu, 6 Jun 2024 03:49:50 +0200 Subject: [PATCH 06/26] Add missing trip gear codes in manual prior notification in Backend --- .../database/entities/ManualPriorNotificationEntity.kt | 1 + 1 file changed, 1 insertion(+) 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 index 6960722953..2736addb6d 100644 --- 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 @@ -115,6 +115,7 @@ data class ManualPriorNotificationEntity( operationType = LogbookOperationType.DAT, reportDateTime = sentAt, transmissionFormat = null, + tripGears = tripGears, vesselName = vesselName, ) // For pratical reasons `vessel` can't be `null`, so we temporarely set it to "Navire inconnu" From bd38eb1e30ec8b396d4673f9327c99945993930a Mon Sep 17 00:00:00 2001 From: Ivan Gabriele Date: Thu, 6 Jun 2024 03:54:57 +0200 Subject: [PATCH 07/26] Initialize prior notification sentAt form value with current date --- .../fields/FormikVesselSelect.tsx | 28 +++++++++---------- .../PriorNotificationForm/index.tsx | 5 ++-- .../components/PriorNotificationForm/utils.ts | 9 ++++++ 3 files changed, 25 insertions(+), 17 deletions(-) diff --git a/frontend/src/features/PriorNotification/components/PriorNotificationForm/fields/FormikVesselSelect.tsx b/frontend/src/features/PriorNotification/components/PriorNotificationForm/fields/FormikVesselSelect.tsx index 716c96c93c..8f65681d38 100644 --- a/frontend/src/features/PriorNotification/components/PriorNotificationForm/fields/FormikVesselSelect.tsx +++ b/frontend/src/features/PriorNotification/components/PriorNotificationForm/fields/FormikVesselSelect.tsx @@ -81,21 +81,19 @@ export function FormikVesselSelect() { ) return ( - <> - - - - {!!meta.error && {meta.error}} - - + + + + {!!meta.error && {meta.error}} + ) } diff --git a/frontend/src/features/PriorNotification/components/PriorNotificationForm/index.tsx b/frontend/src/features/PriorNotification/components/PriorNotificationForm/index.tsx index 7c74f49d9d..bb245b7a62 100644 --- a/frontend/src/features/PriorNotification/components/PriorNotificationForm/index.tsx +++ b/frontend/src/features/PriorNotification/components/PriorNotificationForm/index.tsx @@ -12,16 +12,17 @@ import { useCallback, useEffect, useRef, useState } from 'react' import styled from 'styled-components' import { LoadingSpinnerWall } from 'ui/LoadingSpinnerWall' -import { FORM_VALIDATION_SCHEMA, INITIAL_FORM_VALUES } from './constants' +import { FORM_VALIDATION_SCHEMA } from './constants' import { Form } from './Form' import { Header } from './Header' import { TagBar } from './TagBar' +import { getInitialFormValues } from './utils' import type { FormValues } from './types' import type { PriorNotification } from '@features/PriorNotification/PriorNotification.types' export function PriorNotificationForm() { - const formInitialValuesRef = useRef(INITIAL_FORM_VALUES) + const formInitialValuesRef = useRef(getInitialFormValues()) const dispatch = useMainAppDispatch() const editedPriorNotificationReportId = useMainAppSelector( diff --git a/frontend/src/features/PriorNotification/components/PriorNotificationForm/utils.ts b/frontend/src/features/PriorNotification/components/PriorNotificationForm/utils.ts index 1a4eb1c76b..13b29e1d12 100644 --- a/frontend/src/features/PriorNotification/components/PriorNotificationForm/utils.ts +++ b/frontend/src/features/PriorNotification/components/PriorNotificationForm/utils.ts @@ -1,5 +1,14 @@ +import { INITIAL_FORM_VALUES } from './constants' + import type { FormValues } from './types' +export function getInitialFormValues(): FormValues { + return { + ...INITIAL_FORM_VALUES, + sentAt: new Date().toISOString() + } +} + export function isZeroNotice(formValues: FormValues) { return formValues.fishingCatches.every(fishingCatch => fishingCatch.weight === 0) } From ba666a1355bf054824fc2300ccd5d803a02ba350 Mon Sep 17 00:00:00 2001 From: Ivan Gabriele Date: Thu, 6 Jun 2024 04:44:16 +0200 Subject: [PATCH 08/26] Add prior notification valid form submission logic & edition header --- .../PriorNotificationCard/Header.tsx | 3 +- .../components/PriorNotificationForm/Card.tsx | 98 +++++++++++++++++++ .../PriorNotificationForm/Header.tsx | 31 +++++- .../PriorNotificationForm/index.tsx | 86 +++------------- 4 files changed, 140 insertions(+), 78 deletions(-) create mode 100644 frontend/src/features/PriorNotification/components/PriorNotificationForm/Card.tsx 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/Header.tsx b/frontend/src/features/PriorNotification/components/PriorNotificationForm/Header.tsx index ae58842dfd..9f82f5a05b 100644 --- a/frontend/src/features/PriorNotification/components/PriorNotificationForm/Header.tsx +++ b/frontend/src/features/PriorNotification/components/PriorNotificationForm/Header.tsx @@ -1,10 +1,17 @@ +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({ onClose }: HeaderProps) { +export function Header({ isNewPriorNotification, onClose, vesselId }: HeaderProps) { + const { data: vessel } = useGetVesselQuery(vesselId ?? skipToken) + return ( @@ -13,8 +20,21 @@ export function Header({ onClose }: HeaderProps) { <Icon.Fishery /> </TitleRowIconBox> - <span>{`AJOUTER UN NOUVEAU PRÉAVIS (< 12 M)`}</span> + {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> + )} @@ -25,7 +45,7 @@ export function Header({ onClose }: HeaderProps) { const Wrapper = styled.div` align-items: flex-start; border-bottom: 1px solid ${p => p.theme.color.lightGray}; - box-shadow: 0px 3px 6px #cccfd680; + box-shadow: 0px 3px 6px ${p => p.theme.color.lightGray}; display: flex; padding: 24px 32px; @@ -69,3 +89,8 @@ const TitleRowIconBox = styled.span` vertical-align: -3.5px; } ` + +const VesselName = styled.span` + font-size: 16px; + font-weight: 700; +` diff --git a/frontend/src/features/PriorNotification/components/PriorNotificationForm/index.tsx b/frontend/src/features/PriorNotification/components/PriorNotificationForm/index.tsx index bb245b7a62..9a2630fef0 100644 --- a/frontend/src/features/PriorNotification/components/PriorNotificationForm/index.tsx +++ b/frontend/src/features/PriorNotification/components/PriorNotificationForm/index.tsx @@ -1,21 +1,17 @@ -import { FrontendErrorBoundary } from '@components/FrontendErrorBoundary' import { priorNotificationApi } from '@features/PriorNotification/priorNotificationApi' import { priorNotificationActions } from '@features/PriorNotification/slice' import { useMainAppDispatch } from '@hooks/useMainAppDispatch' import { useMainAppSelector } from '@hooks/useMainAppSelector' import { DisplayedErrorKey } from '@libs/DisplayedError/constants' import { FrontendApiError } from '@libs/FrontendApiError' -import { Accent, Button } from '@mtes-mct/monitor-ui' import { displayOrLogError } from 'domain/use_cases/error/displayOrLogError' import { Formik } from 'formik' import { useCallback, useEffect, useRef, useState } from 'react' import styled from 'styled-components' import { LoadingSpinnerWall } from 'ui/LoadingSpinnerWall' +import { Card } from './Card' import { FORM_VALIDATION_SCHEMA } from './constants' -import { Form } from './Form' -import { Header } from './Header' -import { TagBar } from './TagBar' import { getInitialFormValues } from './utils' import type { FormValues } from './types' @@ -29,6 +25,7 @@ export function PriorNotificationForm() { state => state.priorNotification.editedPriorNotificationReportId ) + const [shouldValidateOnChange, setShouldValidateOnChange] = useState(false) const [isLoading, setIsLoading] = useState(true) const close = () => { @@ -115,9 +112,9 @@ export function PriorNotificationForm() { - + - + ) } @@ -129,40 +126,15 @@ export function PriorNotificationForm() { - {({ errors, submitForm }) => ( - - -
- - -
{JSON.stringify(errors, null, 2)}
- - - -

- 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. -

- -
- - - - -
- - -
- - - )} + setShouldValidateOnChange(true)} + reportId={editedPriorNotificationReportId} + /> ) @@ -185,44 +157,10 @@ const Background = styled.div` flex-grow: 1; ` -const Card = styled.div` +const LoadingCard = 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; - } -` From d901eb9269c3f7edf555baa37e7c7d4b59e65a57 Mon Sep 17 00:00:00 2001 From: Ivan Gabriele Date: Thu, 6 Jun 2024 05:01:39 +0200 Subject: [PATCH 09/26] Fix mapping for swordfish & bluefin tuna cases in prior notification form --- .../FormikFishingCatchesMultiSelect.tsx | 52 ++++--------------- .../components/PriorNotificationForm/utils.ts | 46 +++++++++++++++- 2 files changed, 56 insertions(+), 42 deletions(-) diff --git a/frontend/src/features/PriorNotification/components/PriorNotificationForm/fields/FormikFishingCatchesMultiSelect.tsx b/frontend/src/features/PriorNotification/components/PriorNotificationForm/fields/FormikFishingCatchesMultiSelect.tsx index ff0b702b6a..050ebff8fa 100644 --- a/frontend/src/features/PriorNotification/components/PriorNotificationForm/fields/FormikFishingCatchesMultiSelect.tsx +++ b/frontend/src/features/PriorNotification/components/PriorNotificationForm/fields/FormikFishingCatchesMultiSelect.tsx @@ -1,16 +1,21 @@ +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 { 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, , helper] = useField('fishingCatches') const { speciesAsOptions } = useGetSpeciesAsOptions() + const { data: speciesAndGroups } = useGetSpeciesQuery() const filteredSpeciesAsOptions = speciesAsOptions?.filter(specyOption => input.value.every(fishingCatch => fishingCatch.specyCode !== specyOption.value) @@ -22,44 +27,9 @@ export function FormikFishingCatchesMultiSelect() { return } - const nextFishingCatches = [ - ...input.value, - ...(specyCode === 'BFT' - ? [ - { - quantity: undefined, - specyCode: 'BFT', - specyName: specyOption.label, - weight: 0 - }, - { - quantity: 0, - specyCode: 'BF1', - specyName: specyOption.label, - weight: 0 - }, - { - quantity: 0, - specyCode: 'BF2', - specyName: specyOption.label, - weight: 0 - }, - { - quantity: 0, - specyCode: 'BF3', - specyName: specyOption.label, - weight: 0 - } - ] - : [ - { - quantity: undefined, - specyCode: specyOption.value, - specyName: specyOption.label, - weight: 0 - } - ]) - ] + const specyName = speciesAndGroups?.species.find(specy => specy.code === specyOption.value)?.name + assertNotNullish(specyName) + const nextFishingCatches = [...input.value, ...getFishingsCatchesInitialValues(specyOption.value, specyName)] helper.setValue(nextFishingCatches) } @@ -108,7 +78,7 @@ export function FormikFishingCatchesMultiSelect() { {/* BFT - Bluefin Tuna => + BF1, BF2, BF3 */} {fishingCatch.specyCode === 'BFT' && ( <> - {BLUEFIN_TUNA_EXTENDED_SPECY_CODES.forEach((extendedSpecyCode, extendedIndex) => ( + {BLUEFIN_TUNA_EXTENDED_SPECY_CODES.map((extendedSpecyCode, extendedIndex) => ( )} - {/* SWC - Swordfish */} - {fishingCatch.specyCode === 'SWC' && ( + {/* SWO - Swordfish */} + {fishingCatch.specyCode === 'SWO' && ( ({ + quantity: 0, + specyCode: extendedSpecyCode, + specyName, + weight: 0 + })) + ] + + case 'SWO': + return [ + { + quantity: 0, + specyCode: 'SWO', + specyName, + weight: 0 + } + ] + + default: + return [ + { + quantity: undefined, + specyCode, + specyName, + weight: 0 + } + ] + } +} + export function isZeroNotice(formValues: FormValues) { return formValues.fishingCatches.every(fishingCatch => fishingCatch.weight === 0) } From d3cd547ed46c40c364ae830b8f6aa848e0adc0ef Mon Sep 17 00:00:00 2001 From: Ivan Gabriele Date: Thu, 6 Jun 2024 11:13:35 +0200 Subject: [PATCH 10/26] Remove unused report_datetime_utc column in new manual_prior_notifications SQL table --- .../internal/V0.257__Create_manual_prior_notifications_table.sql | 1 - 1 file changed, 1 deletion(-) 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 index a0ce832544..7b6f420491 100644 --- 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 @@ -4,7 +4,6 @@ CREATE TABLE public.manual_prior_notifications ( -- Common columns with `logbook_reports` report_id VARCHAR(36) PRIMARY KEY DEFAULT gen_random_uuid(), - report_datetime_utc TIMESTAMP WITH TIME ZONE, cfr VARCHAR(12), vessel_name VARCHAR(100), flag_state VARCHAR(3), From 4bac197d924f81762fc356e178d80222a06b15b2 Mon Sep 17 00:00:00 2001 From: Ivan Gabriele Date: Thu, 6 Jun 2024 13:24:40 +0200 Subject: [PATCH 11/26] Add bluefin tuna & swordfish dummy manual prior notifications in test data --- ...=> V666.11__Insert_dummy_risk_factors.sql} | 2 + ...=> V666.19.0__Insert_dummy_reportings.sql} | 0 ...666.19.1__Insert_more_dummy_reportings.sql | 10 ++ .../V666.2.1__Insert_more_dummy_vessels.sql | 6 +- ...nsert_dummy_manual_prior_notifications.sql | 13 +- ... V666.11__Insert_dummy_risk_factors.jsonc} | 61 +++++++++ ...6.19.1__Insert_more_dummy_reportings.jsonc | 128 ++++++++++++++++++ .../V666.2.1__Insert_more_dummy_vessels.jsonc | 32 ++++- ...5.1__Insert_more_pno_logbook_reports.jsonc | 11 +- ...ert_dummy_manual_prior_notifications.jsonc | 112 ++++++++++++++- 10 files changed, 363 insertions(+), 12 deletions(-) rename backend/src/main/resources/db/testdata/{V666.11__Insert_risk_factors.sql => V666.11__Insert_dummy_risk_factors.sql} (82%) rename backend/src/main/resources/db/testdata/{V666.19__Insert_dummy_reportings.sql => V666.19.0__Insert_dummy_reportings.sql} (100%) create mode 100644 backend/src/main/resources/db/testdata/V666.19.1__Insert_more_dummy_reportings.sql rename backend/src/main/resources/db/testdata/json/{V666.11__Insert_risk_factors.jsonc => V666.11__Insert_dummy_risk_factors.jsonc} (81%) create mode 100644 backend/src/main/resources/db/testdata/json/V666.19.1__Insert_more_dummy_reportings.jsonc 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 82% 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..5fee5aa080 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,5 @@ 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}]'); 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 100% 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 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..fcc95e71c2 --- /dev/null +++ b/backend/src/main/resources/db/testdata/V666.19.1__Insert_more_dummy_reportings.sql @@ -0,0 +1,10 @@ +-- /!\ 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":"ABC","controlUnitId":10012,"description":"Une description d''infraction.","dml":"DML 29","natinfCode":27689,"reportingActor":"UNIT","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 '20 days', false, 'EXTIMM101', 'FR', 'CFR101', 'IRCS101', NULL, NULL, 'INFRACTION_SUSPICION', NULL, '{"authorContact":"Jean Bon (0623456789)","authorTrigram":"ABC","controlUnitId":10012,"description":"Une description d''infraction.","dml":"DML 29","natinfCode":27689,"reportingActor":"UNIT","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 '10 days', false, 'EXTIMM115', 'FR', 'CFR115', 'IRCS115', NULL, NULL, 'INFRACTION_SUSPICION', NULL, '{"authorContact":"Jean Bon (0623456789)","authorTrigram":"ABC","controlUnitId":10012,"description":"Une description d''infraction.","dml":"DML 29","natinfCode":27689,"reportingActor":"UNIT","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 '20 days', false, 'EXTIMM115', 'FR', 'CFR115', 'IRCS115', NULL, NULL, 'INFRACTION_SUSPICION', NULL, '{"authorContact":"Jean Bon (0623456789)","authorTrigram":"ABC","controlUnitId":10012,"description":"Une description d''infraction.","dml":"DML 29","natinfCode":27689,"reportingActor":"UNIT","seaFront":"NAMO","title":"Suspicion d''infraction 212","type":"INFRACTION_SUSPICION"}', 115, 'INTERNAL_REFERENCE_NUMBER', 'DOS FIN'); 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 9a7ca720ce..d28ae7fd7f 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); @@ -28,3 +28,7 @@ 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 (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', 22.3, 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 index bd055542e5..6674269158 100644 --- 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 @@ -1,7 +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 manual_prior_notifications (report_id, author_trigram, cfr, did_not_fish_after_zero_notice, flag_state, note, sent_at, trip_gears, trip_segments, vessel_name, value) VALUES ('d2bd7d28-3130-4e3f-bf0d-5e919a5657be', 'ABC', 'CFR112', false, 'FRA', NULL, NOW() AT TIME ZONE 'UTC' - INTERVAL '15 minutes', '[{"gear":"LNP","mesh":8,"dimensions":"50;200"}]', '[{"segment":"NWW09","segmentName":"Lignes"}]', 'POISSON PAS NET', '{"catchOnboard":[{"weight":72,"nbFish":null,"species":"SOS"}],"faoZone":"21.1.A","pnoTypes":[{"pnoTypeName":"Préavis type D","minimumNotificationPeriod":4,"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 = 'd2bd7d28-3130-4e3f-bf0d-5e919a5657be'; -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 = 'd2bd7d28-3130-4e3f-bf0d-5e919a5657be'; -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 = 'd2bd7d28-3130-4e3f-bf0d-5e919a5657be'; +INSERT INTO manual_prior_notifications (report_id, author_trigram, cfr, did_not_fish_after_zero_notice, flag_state, note, sent_at, trip_gears, trip_segments, vessel_name, value) VALUES ('00000000-0000-4000-0000-000000000001', 'ABC', 'CFR112', false, 'FRA', NULL, NOW() AT TIME ZONE 'UTC' - INTERVAL '15 minutes', '[{"gear":"LNP"}]', '[{"segment":"NWW09","segmentName":"Lignes"}]', '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, did_not_fish_after_zero_notice, flag_state, note, sent_at, trip_gears, trip_segments, vessel_name, value) VALUES ('00000000-0000-4000-0000-000000000002', 'ABC', 'CFR115', false, 'BEL', NULL, NOW() AT TIME ZONE 'UTC' - INTERVAL '15 minutes', '[{"gear":"TB"},{"gear":"TBS"}]', '[{"segment":"NWW03","segmentName":"Chalut de fond en eau profonde"},{"segment":"NWW05","segmentName":"Chalut à perche"}]', 'DOS FIN', '{"catchOnboard":[{"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"}],"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'; 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 81% 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..4a02a59bd0 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,67 @@ "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 + } + ] } ] } 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..57240c2961 --- /dev/null +++ b/backend/src/main/resources/db/testdata/json/V666.19.1__Insert_more_dummy_reportings.jsonc @@ -0,0 +1,128 @@ +[ + { + "table": "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": "ABC", + "controlUnitId": 10012, + "description": "Une description d'infraction.", + "dml": "DML 29", + "natinfCode": 27689, + "reportingActor": "UNIT", + "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 '20 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": "ABC", + "controlUnitId": 10012, + "description": "Une description d'infraction.", + "dml": "DML 29", + "natinfCode": 27689, + "reportingActor": "UNIT", + "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 '10 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": "ABC", + "controlUnitId": 10012, + "description": "Une description d'infraction.", + "dml": "DML 29", + "natinfCode": 27689, + "reportingActor": "UNIT", + "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 '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": "ABC", + "controlUnitId": 10012, + "description": "Une description d'infraction.", + "dml": "DML 29", + "natinfCode": 27689, + "reportingActor": "UNIT", + "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 685320563d..15d0d53988 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 @@ -186,6 +187,35 @@ "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": 22.3, + "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 index 22dd5730b4..dfb33ef22d 100644 --- 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 @@ -3,15 +3,16 @@ "table": "manual_prior_notifications", "id": "report_id", "data": [ + // - Vessel: POISSON PAS NET { - "report_id": "d2bd7d28-3130-4e3f-bf0d-5e919a5657be", + "report_id": "00000000-0000-4000-0000-000000000001", "author_trigram": "ABC", "cfr": "CFR112", "did_not_fish_after_zero_notice": false, "flag_state": "FRA", "note": null, "sent_at:sql": "NOW() AT TIME ZONE 'UTC' - INTERVAL '15 minutes'", - "trip_gears:jsonb": [{ "gear": "LNP", "mesh": 8, "dimensions": "50;200" }], + "trip_gears:jsonb": [{ "gear": "LNP" }], "trip_segments:jsonb": [{ "segment": "NWW09", "segmentName": "Lignes" }], "vessel_name": "POISSON PAS NET", "value:jsonb": { @@ -22,12 +23,19 @@ "species": "SOS" } ], + "catchToLand": [ + { + "weight": 72.0, + "nbFish": null, + "species": "SOS" + } + ], "faoZone": "21.1.A", "pnoTypes": [ { - "pnoTypeName": "Préavis type D", + "pnoTypeName": "Préavis type A", "minimumNotificationPeriod": 4.0, - "hasDesignatedPorts": true + "hasDesignatedPorts": false } ], "port": "FRVNE", @@ -36,6 +44,102 @@ "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", + "did_not_fish_after_zero_notice": false, + "flag_state": "BEL", + "note": null, + "sent_at:sql": "NOW() AT TIME ZONE 'UTC' - INTERVAL '15 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" } + ], + "vessel_name": "DOS FIN", + "value:jsonb": { + "catchOnboard": [ + { + "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" + } + ], + "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\"')" + } } ] } From 99d5c5851028b5bfdf12ca63b30b03bcee5fcf12 Mon Sep 17 00:00:00 2001 From: Ivan Gabriele Date: Thu, 6 Jun 2024 13:47:49 +0200 Subject: [PATCH 12/26] Remove useless isLessThanTwelveMetersVessel filter in manual prior notifications SQL query --- .../repositories/JpaManualPriorNotificationRepository.kt | 6 +++++- .../interfaces/DBManualPriorNotificationRepository.kt | 9 --------- 2 files changed, 5 insertions(+), 10 deletions(-) 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 index 54e6abad45..e788fe21dd 100644 --- 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 @@ -15,11 +15,15 @@ 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, - isLessThanTwelveMetersVessel = filter.isLessThanTwelveMetersVessel, lastControlledAfter = filter.lastControlledAfter, lastControlledBefore = filter.lastControlledBefore, portLocodes = filter.portLocodes ?: emptyList(), 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 index b6245d15ba..1d12d338e4 100644 --- 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 @@ -17,7 +17,6 @@ interface DBManualPriorNotificationRepository : JpaRepository>'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 - LEFT JOIN vessels v ON mpn.cfr = v.cfr WHERE -- TODO /!\ INDEX created_at WITH TIMESCALE /!\ -- This filter helps Timescale optimize the query since `created_at` is indexed @@ -28,13 +27,6 @@ interface DBManualPriorNotificationRepository : JpaRepository= 12) - ) - -- Last Controlled After AND (:lastControlledAfter IS NULL OR rf.last_control_datetime_utc >= CAST(:lastControlledAfter AS TIMESTAMP)) @@ -112,7 +104,6 @@ interface DBManualPriorNotificationRepository : JpaRepository, hasOneOrMoreReportings: Boolean?, - isLessThanTwelveMetersVessel: Boolean?, lastControlledAfter: String?, lastControlledBefore: String?, portLocodes: List, From 4eba4c86be131e7546c440a21591a4a8366cbf7d Mon Sep 17 00:00:00 2001 From: Ivan Gabriele Date: Mon, 10 Jun 2024 11:30:34 +0200 Subject: [PATCH 13/26] Add missing trip segments in Backend manual prior notifications --- .../infrastructure/api/bff/PriorNotificationController.kt | 4 ++-- .../infrastructure/database/entities/LogbookReportEntity.kt | 2 +- .../database/entities/ManualPriorNotificationEntity.kt | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) 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 4f38abb1bf..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 @@ -160,7 +160,7 @@ class PriorNotificationController( @RequestBody priorNotificationDataInput: PriorNotificationDataInput, ): PriorNotificationDataOutput { - val cretedPriorNotification = createOrUpdatePriorNotification.execute( + val createdPriorNotification = createOrUpdatePriorNotification.execute( priorNotificationDataInput.authorTrigram, priorNotificationDataInput.didNotFishAfterZeroNotice, priorNotificationDataInput.expectedArrivalDate, @@ -175,7 +175,7 @@ class PriorNotificationController( priorNotificationDataInput.vesselId, ) - return PriorNotificationDataOutput.fromPriorNotification(cretedPriorNotification) + return PriorNotificationDataOutput.fromPriorNotification(createdPriorNotification) } @PutMapping("/{reportId}") 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 7990620eff..b05b34a886 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 @@ -163,12 +163,12 @@ data class LogbookReportEntity( note = null, sentAt = enrichedLogbookMessageTyped.logbookMessage.reportDateTime?.toString(), updatedAt = updatedAt, - vessel = vessel, // These props need to be calculated in the use case port = null, reportingCount = null, seafront = null, + vessel = vessel, vesselRiskFactor = null, ) } 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 index 2736addb6d..13cfebdce0 100644 --- 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 @@ -116,6 +116,7 @@ data class ManualPriorNotificationEntity( 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" @@ -132,12 +133,12 @@ data class ManualPriorNotificationEntity( reportId = reportId, sentAt = sentAt.toString(), updatedAt = updatedAt.toString(), - vessel = vessel, // These props need to be calculated in the use case port = null, reportingCount = null, seafront = null, + vessel = vessel, vesselRiskFactor = null, ) } catch (e: IllegalArgumentException) { From 008a3219e67fec4fd24bde03c1edadb03ca4223b Mon Sep 17 00:00:00 2001 From: Ivan Gabriele Date: Mon, 10 Jun 2024 11:31:36 +0200 Subject: [PATCH 14/26] Add JpaManualPriorNotificationRepository inte tests --- .../V666.11__Insert_dummy_risk_factors.sql | 2 + .../V666.2.1__Insert_more_dummy_vessels.sql | 10 +- ...nsert_dummy_manual_prior_notifications.sql | 21 +- .../V666.11__Insert_dummy_risk_factors.jsonc | 61 +++ .../V666.2.1__Insert_more_dummy_vessels.jsonc | 55 +- ...ert_dummy_manual_prior_notifications.jsonc | 201 ++++++- .../JpaLogbookReportRepositoryITests.kt | 2 +- ...ManualPriorNotificationRepositoryITests.kt | 492 ++++++++++++++++++ 8 files changed, 837 insertions(+), 7 deletions(-) create mode 100644 backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaManualPriorNotificationRepositoryITests.kt diff --git a/backend/src/main/resources/db/testdata/V666.11__Insert_dummy_risk_factors.sql b/backend/src/main/resources/db/testdata/V666.11__Insert_dummy_risk_factors.sql index 5fee5aa080..fde60fb433 100644 --- a/backend/src/main/resources/db/testdata/V666.11__Insert_dummy_risk_factors.sql +++ b/backend/src/main/resources/db/testdata/V666.11__Insert_dummy_risk_factors.sql @@ -12,3 +12,5 @@ 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, '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.2.1__Insert_more_dummy_vessels.sql b/backend/src/main/resources/db/testdata/V666.2.1__Insert_more_dummy_vessels.sql index d28ae7fd7f..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 @@ -31,4 +31,12 @@ 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 (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', 22.3, false); +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 index 6674269158..8fe17af653 100644 --- 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 @@ -1,12 +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, did_not_fish_after_zero_notice, flag_state, note, sent_at, trip_gears, trip_segments, vessel_name, value) VALUES ('00000000-0000-4000-0000-000000000001', 'ABC', 'CFR112', false, 'FRA', NULL, NOW() AT TIME ZONE 'UTC' - INTERVAL '15 minutes', '[{"gear":"LNP"}]', '[{"segment":"NWW09","segmentName":"Lignes"}]', '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}'); +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, did_not_fish_after_zero_notice, flag_state, note, sent_at, trip_gears, trip_segments, vessel_name, value) VALUES ('00000000-0000-4000-0000-000000000002', 'ABC', 'CFR115', false, 'BEL', NULL, NOW() AT TIME ZONE 'UTC' - INTERVAL '15 minutes', '[{"gear":"TB"},{"gear":"TBS"}]', '[{"segment":"NWW03","segmentName":"Chalut de fond en eau profonde"},{"segment":"NWW05","segmentName":"Chalut à perche"}]', 'DOS FIN', '{"catchOnboard":[{"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"}],"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}'); +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":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"}],"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_dummy_risk_factors.jsonc b/backend/src/main/resources/db/testdata/json/V666.11__Insert_dummy_risk_factors.jsonc index 4a02a59bd0..e048c12c79 100644 --- a/backend/src/main/resources/db/testdata/json/V666.11__Insert_dummy_risk_factors.jsonc +++ b/backend/src/main/resources/db/testdata/json/V666.11__Insert_dummy_risk_factors.jsonc @@ -315,6 +315,67 @@ "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.2.1__Insert_more_dummy_vessels.jsonc b/backend/src/main/resources/db/testdata/json/V666.2.1__Insert_more_dummy_vessels.jsonc index 15d0d53988..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 @@ -214,7 +214,60 @@ "external_immatriculation": "EXTIMM116", "vessel_name": "NAVIRE RENOMMÉ (NOUVEAU NOM)", "flag_state": "FR", - "length": 22.3, + "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.2__Insert_dummy_manual_prior_notifications.jsonc b/backend/src/main/resources/db/testdata/json/V666.5.2__Insert_dummy_manual_prior_notifications.jsonc index dfb33ef22d..fc2b2f6a31 100644 --- 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 @@ -8,12 +8,14 @@ "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 '15 minutes'", + "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": [ @@ -56,15 +58,17 @@ "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 '15 minutes'", + "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": { "catchOnboard": [ @@ -140,6 +144,199 @@ "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/infrastructure/database/repositories/JpaLogbookReportRepositoryITests.kt b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/repositories/JpaLogbookReportRepositoryITests.kt index 4be441a7b7..6bba962868 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 @@ -720,7 +720,7 @@ 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 = PriorNotificationsFilter( lastControlledAfter = "2024-01-01T00:00:00Z", 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) + } +} From c1723d9479dc792eb6f8f6d3c1f74f92e3f6e46b Mon Sep 17 00:00:00 2001 From: Ivan Gabriele Date: Tue, 11 Jun 2024 08:45:55 +0200 Subject: [PATCH 15/26] Fix DateRangePicker calendar popup width via monitor-ui upgrade --- frontend/package-lock.json | 8 ++++---- frontend/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d34b9bb20f..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", 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", From d63543b586a8a1dd4dbd90a3bc044d010a01f7a7 Mon Sep 17 00:00:00 2001 From: Ivan Gabriele Date: Tue, 11 Jun 2024 08:48:31 +0200 Subject: [PATCH 16/26] Update fishing catches field label in prior notification form --- .../fields/FormikFishingCatchesMultiSelect.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/features/PriorNotification/components/PriorNotificationForm/fields/FormikFishingCatchesMultiSelect.tsx b/frontend/src/features/PriorNotification/components/PriorNotificationForm/fields/FormikFishingCatchesMultiSelect.tsx index 050ebff8fa..f13e836ede 100644 --- a/frontend/src/features/PriorNotification/components/PriorNotificationForm/fields/FormikFishingCatchesMultiSelect.tsx +++ b/frontend/src/features/PriorNotification/components/PriorNotificationForm/fields/FormikFishingCatchesMultiSelect.tsx @@ -48,8 +48,8 @@ export function FormikFishingCatchesMultiSelect() { <> Date: Tue, 11 Jun 2024 17:49:22 +0200 Subject: [PATCH 23/26] Add prior notification form validation e2e test --- .../prior_notification_form/form.spec.ts | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) 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 index 2291cf7a6b..e6a9bd99e1 100644 --- a/frontend/cypress/e2e/side_window/prior_notification_form/form.spec.ts +++ b/frontend/cypress/e2e/side_window/prior_notification_form/form.spec.ts @@ -137,4 +137,81 @@ context('Side Window > Prior Notification Form > Form', () => { }) }) }) + + 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') + }) }) From 4490c99d454e7bd346f61a1397735dbff1f6812a Mon Sep 17 00:00:00 2001 From: Ivan Gabriele Date: Wed, 12 Jun 2024 07:36:06 +0200 Subject: [PATCH 24/26] Update Backend tests following test data update --- ...666.19.1__Insert_more_dummy_reportings.sql | 2 ++ ...6.19.1__Insert_more_dummy_reportings.jsonc | 1 + .../GetPriorNotificationsITestsDetail.kt | 10 +++++----- .../JpaLogbookReportRepositoryITests.kt | 2 +- .../JpaReportingRepositoryITests.kt | 6 +++--- frontend/scripts/generate_test_data_seeds.mjs | 20 +++++++++++-------- 6 files changed, 24 insertions(+), 17 deletions(-) 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 index 430a83e7cf..d6aa1c4f17 100644 --- 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 @@ -8,3 +8,5 @@ INSERT INTO reportings (id, archived, creation_date, deleted, external_reference 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/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 index 16f1bf3e46..e3398f94fe 100644 --- 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 @@ -1,6 +1,7 @@ [ { "table": "reportings", + "afterAll": "SELECT setval('reportings_id_seq', (SELECT MAX(id) FROM reportings));", "data": [ // - Vessel: VIVA ESPANA // - With 2 reportings diff --git a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/GetPriorNotificationsITestsDetail.kt b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/GetPriorNotificationsITestsDetail.kt index 4eb963eb8c..0010423937 100644 --- a/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/GetPriorNotificationsITestsDetail.kt +++ b/backend/src/test/kotlin/fr/gouv/cnsp/monitorfish/domain/use_cases/prior_notification/GetPriorNotificationsITestsDetail.kt @@ -109,7 +109,7 @@ class GetPriorNotificationsITestsDetail : 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) } @@ -203,11 +203,11 @@ class GetPriorNotificationsITestsDetail : AbstractDBTests() { 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) } @@ -223,7 +223,7 @@ class GetPriorNotificationsITestsDetail : 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) } 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 6bba962868..81d91ae65c 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 @@ -634,7 +634,7 @@ 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 = PriorNotificationsFilter( 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/scripts/generate_test_data_seeds.mjs b/frontend/scripts/generate_test_data_seeds.mjs index 12ea347527..7b1807a9c3 100644 --- a/frontend/scripts/generate_test_data_seeds.mjs +++ b/frontend/scripts/generate_test_data_seeds.mjs @@ -120,14 +120,18 @@ for (const file of jsonFiles) { const dataTables = Array.isArray(jsonSourceAsObject) ? jsonSourceAsObject : [jsonSourceAsObject] const sqlStatementBlocks = dataTables .map(dataTable => { - const { data: rows, id, table } = dataTable - - return rows.map(row => { - const insertStatement = generateInsertStatement(row, table) - const updateStatements = generateUpdateStatements(row, table, id) - - 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() From b96c08c367d248fa9929b9d8a1ad705edbe9418b Mon Sep 17 00:00:00 2001 From: Ivan Gabriele Date: Wed, 12 Jun 2024 07:38:22 +0200 Subject: [PATCH 25/26] Refactor a few things following review in prior notification Backend --- .../repositories/LogbookReportRepository.kt | 2 +- .../CreateOrUpdatePriorNotification.kt | 2 +- .../GetPriorNotification.kt | 6 ++-- .../api/input/LogbookMessagePnoDataInput.kt | 35 ------------------- .../light/outputs/LogbookMessageDataOutput.kt | 1 + .../entities/ManualPriorNotificationEntity.kt | 2 +- 6 files changed, 6 insertions(+), 42 deletions(-) delete mode 100644 backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/input/LogbookMessagePnoDataInput.kt 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 552b47c394..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 @@ -68,7 +68,7 @@ interface LogbookReportRepository { fun findLastReportSoftware(internalReferenceNumber: String): String? - // TODO Is it used? + // Only used in tests fun save(message: LogbookMessage) fun savePriorNotification(logbookMessageTyped: LogbookMessageTyped): PriorNotification 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 index 7de2620808..b0911388a1 100644 --- 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 @@ -65,7 +65,7 @@ class CreateOrUpdatePriorNotification( isEnriched = true, isSentByFailoverSoftware = false, message = message, - messageType = "PNO", + messageType = LogbookMessageTypeMapping.PNO.name, operationType = LogbookOperationType.DAT, tripGears = tripGears, tripSegments = tripSegments, 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 ad168a8fd2..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 @@ -25,10 +25,8 @@ class GetPriorNotification( val allSpecies = speciesRepository.findAll() val allVessels = vesselRepository.findAll() - val manualPriorNotification = manualPriorNotificationRepository.findByReportId(reportId) - val automaticPriorNotification = logbookReportRepository.findPriorNotificationByReportId(reportId) - val priorNotification = manualPriorNotification - ?: automaticPriorNotification + val priorNotification = manualPriorNotificationRepository.findByReportId(reportId) + ?: logbookReportRepository.findPriorNotificationByReportId(reportId) ?: throw BackendUsageException(BackendUsageErrorCode.NOT_FOUND) priorNotification.enrich(allPorts, allRiskFactors, allVessels) diff --git a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/input/LogbookMessagePnoDataInput.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/input/LogbookMessagePnoDataInput.kt deleted file mode 100644 index 0e21733b62..0000000000 --- a/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/api/input/LogbookMessagePnoDataInput.kt +++ /dev/null @@ -1,35 +0,0 @@ -package fr.gouv.cnsp.monitorfish.infrastructure.api.input - -import fr.gouv.cnsp.monitorfish.domain.entities.logbook.messages.PNO -import java.time.ZonedDateTime - -data class LogbookMessageValueForPnoDataInput( - val estimatedArrivalDate: String, - val estimatedLandingDate: String, - val fishingCatchesOnboard: List, - val fishingCatchesToUnload: List, - val portLocode: String, - val portName: String, -) { - fun toPNO(): PNO { - return PNO().apply { - catchOnboard = fishingCatchesOnboard.map { it.toLogbookFishingCatch() } - catchToLand = fishingCatchesToUnload.map { it.toLogbookFishingCatch() } - economicZone = null - effortZone = null - // TODO fill with zone - faoZone = null - latitude = null - longitude = null - // TODO Will be calculated - pnoTypes = emptyList() - predictedArrivalDatetimeUtc = ZonedDateTime.parse(estimatedArrivalDate) - predictedLandingDatetimeUtc = ZonedDateTime.parse(estimatedLandingDate) - port = portLocode - portName = portName - purpose = "LAN" - statisticalRectangle = null - tripStartDate = null - } - } -} 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 a519e9c346..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 @@ -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/database/entities/ManualPriorNotificationEntity.kt b/backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/entities/ManualPriorNotificationEntity.kt index 13cfebdce0..6e1973f5f2 100644 --- 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 @@ -109,7 +109,7 @@ data class ManualPriorNotificationEntity( integrationDateTime = createdAt, internalReferenceNumber = cfr, message = value, - messageType = "PNO", + messageType = LogbookMessageTypeMapping.PNO.name, operationDateTime = createdAt, operationNumber = null, operationType = LogbookOperationType.DAT, From 0fc8ce4a3f64467d34a86a8c5871ddbc2b804bc7 Mon Sep 17 00:00:00 2001 From: Ivan Gabriele Date: Wed, 12 Jun 2024 08:21:06 +0200 Subject: [PATCH 26/26] Abstract common prior notification & logbook report entities props in Backend --- .../domain/entities/logbook/LogbookMessage.kt | 2 +- .../database/entities/LogbookReportEntity.kt | 46 +++++++------------ .../entities/ManualPriorNotificationEntity.kt | 30 ++++++------ .../abstractions/AbstractLogbookEntity.kt | 29 ++++++++++++ .../JpaLogbookReportRepository.kt | 4 +- .../JpaLogbookReportRepositoryITests.kt | 2 +- 6 files changed, 62 insertions(+), 51 deletions(-) create mode 100644 backend/src/main/kotlin/fr/gouv/cnsp/monitorfish/infrastructure/database/entities/abstractions/AbstractLogbookEntity.kt 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 1800271b09..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 @@ -19,7 +19,7 @@ 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, // Reception date of the report by the data center 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 b05b34a886..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 @@ -39,17 +40,10 @@ data class LogbookReportEntity( val referencedReportId: String?, @Column(name = "report_datetime_utc") val reportDateTime: Instant?, - @Column(name = "cfr") - val internalReferenceNumber: String?, @Column(name = "ircs") val ircs: String?, @Column(name = "external_identification") val externalReferenceNumber: String?, - @Column(name = "vessel_name") - val vesselName: String?, - // ISO Alpha-3 country code - @Column(name = "flag_state") - val flagState: String?, @Column(name = "imo") val imo: String?, @Column(name = "log_type") @@ -69,19 +63,26 @@ data class LogbookReportEntity( val software: String?, @Column(name = "enriched") val isEnriched: Boolean = false, - @Type(JsonBinaryType::class) - @Column(name = "trip_gears", nullable = true, columnDefinition = "jsonb") - val tripGears: String?, - @Type(JsonBinaryType::class) - @Column(name = "trip_segments", nullable = true, columnDefinition = "jsonb") - val tripSegments: String?, + + /** 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, @@ -110,12 +111,10 @@ data class LogbookReportEntity( fun toLogbookMessage(mapper: ObjectMapper): LogbookMessage { val message = getERSMessageValueFromJSON(mapper, message, messageType, operationType) - val tripGears = deserializeJSONList(mapper, tripGears, LogbookTripGear::class.java) - val tripSegments = deserializeJSONList(mapper, tripSegments, LogbookTripSegment::class.java) return LogbookMessage( id = id!!, - internalReferenceNumber = internalReferenceNumber, + internalReferenceNumber = cfr, referencedReportId = referencedReportId, externalReferenceNumber = externalReferenceNumber, ircs = ircs, @@ -172,17 +171,4 @@ data class LogbookReportEntity( 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 index 6e1973f5f2..3c2d19827a 100644 --- 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 @@ -5,6 +5,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.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 @@ -25,9 +26,6 @@ data class ManualPriorNotificationEntity( @Column(name = "author_trigram") val authorTrigram: String, - @Column(name = "cfr") - val cfr: String, - @Column(name = "created_at", insertable = false, updatable = false) @CreationTimestamp val createdAt: ZonedDateTime? = null, @@ -35,24 +33,12 @@ data class ManualPriorNotificationEntity( @Column(name = "did_not_fish_after_zero_notice") val didNotFishAfterZeroNotice: Boolean, - // ISO Alpha-3 country code - @Column(name = "flag_state") - val flagState: String?, - @Column(name = "note") val note: String?, @Column(name = "sent_at") val sentAt: ZonedDateTime, - @Column(name = "trip_gears", nullable = true, columnDefinition = "jsonb") - @Type(JsonBinaryType::class) - val tripGears: List?, - - @Column(name = "trip_segments", nullable = true, columnDefinition = "jsonb") - @Type(JsonBinaryType::class) - val tripSegments: List?, - @Column(name = "updated_at") @UpdateTimestamp val updatedAt: ZonedDateTime? = null, @@ -61,8 +47,18 @@ data class ManualPriorNotificationEntity( @Type(JsonBinaryType::class) val value: PNO, - @Column(name = "vessel_name") - val vesselName: String?, + /** 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 { 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 03d68cfd9a..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 @@ -252,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 } 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 81d91ae65c..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 @@ -1147,7 +1147,7 @@ class JpaLogbookReportRepositoryITests : AbstractDBTests() { externalReferenceNumber = null, flagState = null, integrationDateTime = Instant.now(), - internalReferenceNumber = null, + cfr = null, imo = null, ircs = null, message = null,