diff --git a/frontend/src/features/PriorNotification/components/shared/DownloadButton/__tests__/getHtmlContent.test.ts b/frontend/src/features/PriorNotification/components/shared/DownloadButton/__tests__/getHtmlContent.test.ts index 9888457b66..4cd18871eb 100644 --- a/frontend/src/features/PriorNotification/components/shared/DownloadButton/__tests__/getHtmlContent.test.ts +++ b/frontend/src/features/PriorNotification/components/shared/DownloadButton/__tests__/getHtmlContent.test.ts @@ -506,6 +506,215 @@ describe('PriorNotificationCard/utils.getHtmlContent()', () => { +`) + }) + + it('Should format the HTML template when isZero', () => { + // Given + const pno: Logbook.PnoMessage = { + acknowledgment: undefined, + activityDateTime: '2024-06-14T06:52:22.978603Z', + externalReferenceNumber: undefined, + flagState: 'ESP', + imo: undefined, + integrationDateTime: '2024-06-14T06:52:22.978603Z', + internalReferenceNumber: 'CFR101', + ircs: undefined, + isCorrectedByNewerMessage: false, + isDeleted: false, + isSentByFailoverSoftware: false, + message: { + authorTrigram: undefined, + catchOnboard: [ + { + conversionFactor: undefined, + economicZone: 'FRA', + effortZone: 'C', + faoZone: '27.8.a', + freshness: undefined, + nbFish: undefined, + packaging: undefined, + presentation: undefined, + preservationState: undefined, + species: 'FRF', + speciesName: 'DIVERS POISSONS(EAU DOUCE)', + statisticalRectangle: '23E6', + weight: 0 + } + ], + catchToLand: [], + createdBy: undefined, + economicZone: undefined, + effortZone: undefined, + faoZone: undefined, + hasPortEntranceAuthorization: false, + hasPortLandingAuthorization: true, + isBeingSent: false, + isInvalidated: false, + isInVerificationScope: false, + isSent: false, + isVerified: false, + latitude: undefined, + longitude: undefined, + note: undefined, + pnoTypes: [ + { + hasDesignatedPorts: false, + minimumNotificationPeriod: 4, + pnoTypeName: 'Préavis type A' + }, + { hasDesignatedPorts: true, minimumNotificationPeriod: 8, pnoTypeName: 'Préavis type B' } + ], + port: 'FRVNE', + portName: 'Vannes', + predictedArrivalDatetimeUtc: '2024-06-14T10:07:22Z', + predictedLandingDatetimeUtc: '2024-06-14T11:07:22Z', + purpose: PurposeCode.LAN, + riskFactor: 1.2, + statisticalRectangle: undefined, + tripStartDate: '2024-06-13T21:07:22Z', + updatedAt: undefined, + updatedBy: undefined + }, + messageType: MessageType.PNO as MessageType.PNO, + operationDateTime: '2024-06-14T06:52:22.978603Z', + operationNumber: 'FAKE_OPERATION_104', + operationType: OperationType.DAT as OperationType.DAT, + rawMessage: 'Message FLUX xml', + referencedReportId: undefined, + reportDateTime: '2024-06-14T06:52:22.978603Z', + reportId: 'FAKE_OPERATION_104', + tripGears: [ + { dimensions: '250;180', gear: 'TB', gearName: 'TB gear name', mesh: 100 }, + { + dimensions: '250;280', + gear: 'TBS', + gearName: 'TBS gear name', + mesh: 120.5 + } + ], + tripNumber: undefined, + tripSegments: [ + { code: 'NWW03', name: 'Chalut de fond en eau profonde' }, + { + code: 'NWW05', + name: 'Chalut à perche' + } + ], + vesselName: 'VIVA ESPANA' + } + const gears = [ + { + dimensions: '250;180', + gear: 'TB', + gearName: 'Chaluts de fond (non spécifiés)', + mesh: 100 + }, + { dimensions: '250;280', gear: 'TBS', gearName: 'Chaluts de fond à crevettes', mesh: 120.5 } + ] + + // When + const result = getHtmlContent(pno, gears) + + // Then + expect(result).toEqual(` + + + + + +
+

+

PREAVIS - Débarquement (préavis zéro)

+

+ VIVA ESPANA + +

+

+ +
+
+
+

INFOS DU PRÉAVIS

+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Préavis envoyéle 14/06/2024 à 06h52 UTC
Arrivée estiméele 14/06/2024 à 10h07 UTC
Débarque prévuele 14/06/2024 à 11h07 UTC
Port de débarqueVannes (FRVNE)
Décision CNSP + Interdiction donnée d'entrer au port
+ Autorisation donnée de débarquer
+
+
+
+

ACTIVITÉ DU NAVIRE

+
+ + +
Engin(s) utilisé(s)Chaluts de fond (non spécifiés) (TB) - Maillage 100 mm, Chaluts de fond à crevettes (TBS) - Maillage 120.5 mm
+

Espèces à bord par zone de pêche (tous les poids sont vifs) :

+ + + + + + + + + + + + + + + +
EspècesZones de pêcheQtés (kg)Nb
DIVERS POISSONS(EAU DOUCE) - (FRF)27.8.a (23E6)00
+
+
+ + `) }) }) diff --git a/frontend/src/features/PriorNotification/components/shared/DownloadButton/template.ts b/frontend/src/features/PriorNotification/components/shared/DownloadButton/template.ts index 6d5ef6f6cf..0d4333021d 100644 --- a/frontend/src/features/PriorNotification/components/shared/DownloadButton/template.ts +++ b/frontend/src/features/PriorNotification/components/shared/DownloadButton/template.ts @@ -8,7 +8,7 @@ export const generateHTML = (data: TemplateData) => `

-

PREAVIS - ${data.purpose}

+

PREAVIS - ${data.purpose}${data.isZero ? ' (préavis zéro)' : ''}

${data.vesselName} diff --git a/frontend/src/features/PriorNotification/components/shared/DownloadButton/types.ts b/frontend/src/features/PriorNotification/components/shared/DownloadButton/types.ts index a4fb2e32b4..69b80b6eb1 100644 --- a/frontend/src/features/PriorNotification/components/shared/DownloadButton/types.ts +++ b/frontend/src/features/PriorNotification/components/shared/DownloadButton/types.ts @@ -7,6 +7,7 @@ export type TemplateData = { internalReferenceNumber: string ircs: string isLanding: boolean + isZero: boolean port: string | undefined portEntranceAuthorization: string portLandingAuthorization: string diff --git a/frontend/src/features/PriorNotification/components/shared/DownloadButton/utils.tsx b/frontend/src/features/PriorNotification/components/shared/DownloadButton/utils.tsx index c237f27ace..40aca69476 100644 --- a/frontend/src/features/PriorNotification/components/shared/DownloadButton/utils.tsx +++ b/frontend/src/features/PriorNotification/components/shared/DownloadButton/utils.tsx @@ -1,6 +1,10 @@ import { getAlpha2CodeFromAlpha2or3Code } from '@components/CountryFlag/utils' import { buildCatchArray } from '@features/Logbook/utils' import { PriorNotification } from '@features/PriorNotification/PriorNotification.types' +import { + getPriorNotificationFishingCatchesFromLogbookMessageFishingCatches, + isPriorNotificationZero +} from '@features/PriorNotification/utils' import { customDayjs } from '@mtes-mct/monitor-ui' import { generateHTML } from './template' @@ -55,6 +59,10 @@ export function getHtmlContent( internalReferenceNumber: pno.internalReferenceNumber ?? 'Aucun', ircs: pno.ircs ?? 'Aucun', isLanding: pno.message.purpose === PriorNotification.PurposeCode.LAN, + isZero: + isPriorNotificationZero( + getPriorNotificationFishingCatchesFromLogbookMessageFishingCatches(pno.message.catchOnboard) + ) ?? false, port: pno.message.port, portEntranceAuthorization: pno.message.hasPortEntranceAuthorization ? `Autorisation donnée d'entrer au port
` diff --git a/pipeline/src/emails/templates/prior_notifications/base_template.jinja b/pipeline/src/emails/templates/prior_notifications/base_template.jinja index a1229ac63c..9960462010 100644 --- a/pipeline/src/emails/templates/prior_notifications/base_template.jinja +++ b/pipeline/src/emails/templates/prior_notifications/base_template.jinja @@ -12,7 +12,7 @@

-

PRÉAVIS - {{ purpose }}

+

PRÉAVIS - {{ purpose }}{{ " (préavis zéro)" if is_zero else "" }}

{{ vessel_name }} Pavillon diff --git a/pipeline/src/emails/templates/prior_notifications/email_body_template.jinja b/pipeline/src/emails/templates/prior_notifications/email_body_template.jinja index 1728d5bb91..ccaaf4f3e8 100644 --- a/pipeline/src/emails/templates/prior_notifications/email_body_template.jinja +++ b/pipeline/src/emails/templates/prior_notifications/email_body_template.jinja @@ -1,13 +1,13 @@ - Préavis {{ purpose_suffix }} - {{ vessel_name }} + Préavis {{ purpose_suffix }} - {{ vessel_name }}{{ " - préavis zéro" if is_zero else "" }}

Bonjour,

-

Veuillez trouver ci-joint un préavis {{ purpose_suffix }} :

+

Veuillez trouver ci-joint un préavis {{ purpose_suffix }}{{ " (préavis zéro)" if is_zero else "" }} :

  • Navire : {{ vessel_name }} - {{ cfr }}
  • Segment(s) de flotte : {{ trip_segments or "-" }}
  • diff --git a/pipeline/src/entities/pnos.py b/pipeline/src/entities/pnos.py index 517551cf2f..34f7b88a99 100644 --- a/pipeline/src/entities/pnos.py +++ b/pipeline/src/entities/pnos.py @@ -62,6 +62,8 @@ class PnoToRender: is_verified: bool is_being_sent: bool source: PnoSource + is_correction: bool + previous_notification_date_utc: datetime | None def __post_init__(self): datetime_attrs = [ @@ -70,6 +72,7 @@ def __post_init__(self): "predicted_arrival_datetime_utc", "predicted_landing_datetime_utc", "last_control_datetime_utc", + "previous_notification_date_utc", ] for att in datetime_attrs: @@ -85,6 +88,7 @@ def __post_init__(self): "vessel_length", "risk_factor", "last_control_datetime_utc", + "previous_notification_date_utc", ] for att in nullables_to_correct: @@ -130,6 +134,9 @@ class PreRenderedPno: is_landing: bool source: PnoSource purpose_suffix: str + is_zero: bool + is_correction: bool + previous_notification_date_utc: datetime | None @staticmethod def assert_equal(left: object, right: object): @@ -223,6 +230,10 @@ class RenderedPno: sms_content: str | None = None control_units: List[ControlUnit] | None = None additional_addressees: List[PnoAddressee] = None + is_zero: bool = False + is_correction: bool = False + previous_notification_date_utc: datetime | None = None + def get_addressees( self, communication_means: CommunicationMeans diff --git a/pipeline/src/flows/distribute_pnos.py b/pipeline/src/flows/distribute_pnos.py index 3e2d818b4c..abe51392f7 100644 --- a/pipeline/src/flows/distribute_pnos.py +++ b/pipeline/src/flows/distribute_pnos.py @@ -334,6 +334,9 @@ def format_sr_list(sr_list: list) -> str: source=pno.source, is_landing=pno.purpose == "LAN", purpose_suffix=purpose_suffix, + is_zero=is_prior_notification_zero(pno), + is_correction=pno.is_correction, + previous_notification_date_utc=pno.previous_notification_date_utc, ) @@ -487,6 +490,9 @@ def format_nullable_datetime(d: datetime, format: str = date_format): ), is_landing=pno.is_landing, purpose_suffix=pno.purpose_suffix, + is_zero=pno.is_zero, + is_correction=pno.is_correction, + previous_notification_date_utc=pno.previous_notification_date_utc, ) html_email_body = email_body_template.render( @@ -512,6 +518,9 @@ def format_nullable_datetime(d: datetime, format: str = date_format): is_landing=pno.is_landing, purpose_suffix=pno.purpose_suffix, purpose=pno.purpose, + is_zero=pno.is_zero, + is_correction=pno.is_correction, + previous_notification_date_utc=pno.previous_notification_date_utc, ) sms_date_format = "%d/%m/%Y, %Hh%M UTC" @@ -529,6 +538,8 @@ def format_nullable_datetime(d: datetime, format: str = date_format): ), port_name=pno.port_name, note=pno.note, + is_zero=pno.is_zero, + is_correction=pno.is_correction, ) pdf = weasyprint.HTML(string=html_for_pdf).write_pdf( @@ -748,10 +759,12 @@ def create_email( ) ] + (uploaded_attachments.get(pno.report_id) or []) + subject_suffix = " - préavis zéro" if pno.is_zero else "" + message = create_html_email( to=to, cc=cc, - subject=f"Préavis {pno.purpose_suffix} - {pno.vessel_name}", + subject=f"Préavis {pno.purpose_suffix} - {pno.vessel_name}{subject_suffix}", html=pno.html_email_body, from_=MONITORFISH_EMAIL_ADDRESS, images=[ @@ -1147,3 +1160,15 @@ def distribute_pnos_flow( execute_statement(update_manual_pnos_statement) return pnos_to_generate, pnos_to_distribute + +def is_prior_notification_zero(pno: PnoToRender) -> bool: + # ported from `isPriorNotificationZero` in /frontend/src/features/PriorNotification/utils.ts + + if not pno.catch_onboard: + return False + + for c in pno.catch_onboard: + if c.get("weight") != 0: + return False + + return True \ No newline at end of file diff --git a/pipeline/src/queries/monitorfish/pnos_to_generate.sql b/pipeline/src/queries/monitorfish/pnos_to_generate.sql index 21fdd15fab..aeb155661f 100644 --- a/pipeline/src/queries/monitorfish/pnos_to_generate.sql +++ b/pipeline/src/queries/monitorfish/pnos_to_generate.sql @@ -36,6 +36,16 @@ last_controls AS ( LEFT JOIN LATERAL jsonb_array_elements(last_control_infractions) AS inf ON true GROUP BY 1, 2, 3 +), + +last_successful_sends AS ( + SELECT + prior_notification_report_id, + prior_notification_source, + MAX(date_time_utc) AS last_successful_send_date_utc + FROM prior_notification_sent_messages + WHERE success = true + GROUP BY prior_notification_report_id, prior_notification_source ) (SELECT DISTINCT ON (r.report_id) -- In rare cases the same PNO with the same data and the same report_id is sent multiple times in messages with different operation numbers @@ -67,7 +77,9 @@ last_controls AS ( COALESCE(lc.last_control_infractions, '[]'::jsonb) AS last_control_infractions, (value->>'isVerified')::BOOLEAN AS is_verified, (value->>'isBeingSent')::BOOLEAN AS is_being_sent, - 'LOGBOOK' AS source + 'LOGBOOK' AS source, + r.operation_type = 'COR' AS is_correction, + lss.last_successful_send_date_utc AS previous_notification_date_utc FROM logbook_reports r LEFT JOIN vessels v ON v.cfr = r.cfr @@ -75,6 +87,10 @@ LEFT JOIN last_controls lc ON lc.cfr = r.cfr LEFT JOIN ports p ON p.locode = r.value->>'port' +LEFT JOIN last_successful_sends lss +ON r.operation_type = 'COR' + AND lss.prior_notification_report_id = r.referenced_report_id + AND lss.prior_notification_source = 'LOGBOOK' WHERE operation_datetime_utc >= :start_datetime_utc AND operation_datetime_utc < :end_datetime_utc @@ -133,7 +149,9 @@ UNION ALL COALESCE(lc.last_control_infractions, '[]'::jsonb) AS last_control_infractions, (value->>'isVerified')::BOOLEAN AS is_verified, (value->>'isBeingSent')::BOOLEAN AS is_being_sent, - 'MANUAL' AS source + 'MANUAL' AS source, + lss.last_successful_send_date_utc IS NOT NULL AS is_correction, + lss.last_successful_send_date_utc AS previous_notification_date_utc FROM manual_prior_notifications r LEFT JOIN vessels v ON v.id = r.vessel_id @@ -141,6 +159,9 @@ LEFT JOIN last_controls lc ON lc.vessel_id = r.vessel_id LEFT JOIN ports p ON p.locode = r.value->>'port' +LEFT JOIN last_successful_sends lss +ON lss.prior_notification_report_id = r.report_id + AND lss.prior_notification_source = 'MANUAL' WHERE ( (value->>'isInvalidated') IS NULL diff --git a/pipeline/src/sms/prior_notification.jinja b/pipeline/src/sms/prior_notification.jinja index d90a2fdc29..9c8b10282b 100644 --- a/pipeline/src/sms/prior_notification.jinja +++ b/pipeline/src/sms/prior_notification.jinja @@ -1,4 +1,4 @@ -PNO à {{ port_name }} +PNO{{ " zéro" if is_zero else "" }}{{ " COR" if is_correction else "" }} à {{ port_name }} - {{ vessel_name }}{{ " ({})".format(cfr) if cfr }} {{ "- Segment(s) {}".format(trip_segments) if trip_segments }} - Note de risque {{ "{:.2f}".format(risk_factor) }} diff --git a/pipeline/tests/test_data/emails/prior_notifications/expected_email_body_zero_1.html b/pipeline/tests/test_data/emails/prior_notifications/expected_email_body_zero_1.html new file mode 100644 index 0000000000..129f7409c7 --- /dev/null +++ b/pipeline/tests/test_data/emails/prior_notifications/expected_email_body_zero_1.html @@ -0,0 +1,67 @@ + + + + Préavis de débarquement - DEVINER FIGURE CONSCIENCE - préavis zéro + + + +
    +

    Bonjour,

    +

    Veuillez trouver ci-joint un préavis de débarquement (préavis zéro) :

    +
      +
    • Navire : DEVINER FIGURE CONSCIENCE - ABC000542519
    • +
    • Segment(s) de flotte : SHKE27 - Merlu en zone 27, SOTM - Chaluts pélagiques
    • +
    • Note de risque : 2.10
    • +
    +

    Infos du préavis

    +
      +
    • Préavis envoyé : le 05/05/2024 à 08h11 UTC
    • +
    • Arrivée estimée : le 06/05/2020 à 11h41 UTC
    • +
    • Raison du préavis : Débarquement
    • + +
    • Débarque prévue : le 06/05/2020 à 16h40 UTC
    • + +
    • Port de débarquement : Somewhere over the rainbow (FRCQF)
    • + +
    • Type(s) de préavis : Préavis type 1, Préavis type 2
    • + +
    + +

    [ ! ] Demande de contrôle du CNSP - Consultez le pdf du préavis et contactez le CNSP pour plus d'informations.

    + +
    +
    ---
    +
    +
    + Services Opérations - Opérateurs +
    Centre National de Surveillance des Pêches (CNSP) / FRENCH FMC +
    CROSSA Etel +
    40, Avenue Louis Bougo 56410 ETEL +
    Tel : +33 297293427
    +
    www.mer.gouv.fr +
    + + + + + + + + + + + + + +
    + Marianne + + Logo type signature +
    SECRÉTARIAT D'ÉTAT
    CHARGÉ DE LA MER
    ET DE LA BIODIVERSITé
    + liberté, égalité, fraternité +
    +
    +
    +
    + + \ No newline at end of file diff --git a/pipeline/tests/test_data/emails/prior_notifications/expected_pno_1.pdf b/pipeline/tests/test_data/emails/prior_notifications/expected_pno_1.pdf index 00500bbdcf..6a36a02877 100644 Binary files a/pipeline/tests/test_data/emails/prior_notifications/expected_pno_1.pdf and b/pipeline/tests/test_data/emails/prior_notifications/expected_pno_1.pdf differ diff --git a/pipeline/tests/test_data/emails/prior_notifications/expected_pno_2.pdf b/pipeline/tests/test_data/emails/prior_notifications/expected_pno_2.pdf index 0ca91aaf0f..21801153fc 100644 Binary files a/pipeline/tests/test_data/emails/prior_notifications/expected_pno_2.pdf and b/pipeline/tests/test_data/emails/prior_notifications/expected_pno_2.pdf differ diff --git a/pipeline/tests/test_data/emails/prior_notifications/expected_pno_zero_1.html b/pipeline/tests/test_data/emails/prior_notifications/expected_pno_zero_1.html new file mode 100644 index 0000000000..4c7e9e856a --- /dev/null +++ b/pipeline/tests/test_data/emails/prior_notifications/expected_pno_zero_1.html @@ -0,0 +1,421 @@ + + + + + + + + + + +
    +

    +

    PRÉAVIS - Débarquement (préavis zéro)

    +

    + DEVINER FIGURE CONSCIENCE + Pavillon +

    +

    +
      +
    • CFR - ABC000542519
    • +
    • Marquage ext. - RO237719
    • +
    • MMSI - aucun
    • +
    • Call Sign - FQ7058
    • +
    +

    Taille du navire - 13.4 m

    +

    Note de risque moyenne 2.1

    + +
    + +
    + +
    +

    INFOS DU PRÉAVIS

    +
    + + + + + + + + + + + +
    Préavis envoyéle 05/05/2024 à 08h11 UTC
    Arrivée estiméele 06/05/2020 à 11h41 UTC
    Raison du préavisDébarquement
    Débarque prévuele 06/05/2020 à 16h40 UTC
    Port de débarqueSomewhere over the rainbow (FRCQF)
    Type(s) de préavisPréavis type 1, Préavis type 2
    +
    + + +
    +

    INFOS POUR CONTRÔLE

    +
    + + + + + + +
    Segment(s) de flotteSHKE27 - Merlu en zone 27
    SOTM - Chaluts pélagiques
    Date du dernier contrôle03/06/2023 à 09h13 UTC
    Résultat du contrôle + + +

    + 4 infractions - + NATINFS 27724, 2606, 4761, 22206 +

    + + + + + + +
    + +

    Points d'attention relevés par le CNSP

    +

    Attention attention faites bien attention très attention sait-on jamais ce qui pourrait, ô grand Dieu des Poissons qui se font pêcher, nous arriver, si on ne fait pas assez attention à ce qu'y pêche c'te navire d'pêche qui fait d'la pêche de poissons.

    + +
    + + +
    +

    ACTIVITÉ DU NAVIRE

    +
    + + +
    Engin(s) utilisé(s)Chaluts jumeaux à panneaux (OTT) - Maillage 140 mm
    Chaluts jumeaux à panneaux (OTT)
    + +

    Espèces à bord par zone de pêche (tous les poids sont vifs) :

    + + + + + + + + + + + + + + + + + +
    EspècesZones de pêcheQtés (kg)Nb
    Pou Hasse Caille (GHL)27.8.a (47E3)0-
    + +
    + +
    + +
    +
    +

    Centre National de Surveillance des Pêches

    +

    40 avenue Louis Bougo

    +

    56410 Étel

    +

    (0)2 97 29 34 27

    + + + + + +
    + Logo CNSP + + Logo Secretariat d'Etat chargé de la Mer et de la Biodiversité +
    +
    + + \ No newline at end of file diff --git a/pipeline/tests/test_data/emails/prior_notifications/expected_pno_zero_1.pdf b/pipeline/tests/test_data/emails/prior_notifications/expected_pno_zero_1.pdf new file mode 100644 index 0000000000..64b0823b52 Binary files /dev/null and b/pipeline/tests/test_data/emails/prior_notifications/expected_pno_zero_1.pdf differ diff --git a/pipeline/tests/test_data/emails/prior_notifications/expected_sms_2.txt b/pipeline/tests/test_data/emails/prior_notifications/expected_sms_2.txt index fc27172c4d..18ab37ec37 100644 --- a/pipeline/tests/test_data/emails/prior_notifications/expected_sms_2.txt +++ b/pipeline/tests/test_data/emails/prior_notifications/expected_sms_2.txt @@ -1,4 +1,4 @@ -PNO à Somewhere over the top +PNO COR à Somewhere over the top - CAPITAINE HADDOCK (ABC000000000) - Note de risque 1.74 diff --git a/pipeline/tests/test_data/emails/prior_notifications/expected_sms_zero_1.txt b/pipeline/tests/test_data/emails/prior_notifications/expected_sms_zero_1.txt new file mode 100644 index 0000000000..7f48ec2adc --- /dev/null +++ b/pipeline/tests/test_data/emails/prior_notifications/expected_sms_zero_1.txt @@ -0,0 +1,9 @@ +PNO zéro à Somewhere over the rainbow +- DEVINER FIGURE CONSCIENCE (ABC000542519) +- Segment(s) Merlu en zone 27 (SHKE27), Chaluts pélagiques (SOTM) +- Note de risque 2.10 +- Arrivée est. 06/05/2020, 11h41 UTC +- Déb. prévue 06/05/2020, 16h40 UTC + + +[!] Points d'attention relevés par le CNSP, consultez le pdf. \ No newline at end of file diff --git a/pipeline/tests/test_data/remote_database/V666.37__Reset_test_prior_notification_sent_messages.sql b/pipeline/tests/test_data/remote_database/V666.37__Reset_test_prior_notification_sent_messages.sql index dfeeed40b1..845082345b 100644 --- a/pipeline/tests/test_data/remote_database/V666.37__Reset_test_prior_notification_sent_messages.sql +++ b/pipeline/tests/test_data/remote_database/V666.37__Reset_test_prior_notification_sent_messages.sql @@ -1 +1,36 @@ DELETE FROM public.prior_notification_sent_messages; + +-- Add successful send for original report #15 so the COR correction will have is_correction = true +INSERT INTO public.prior_notification_sent_messages ( + prior_notification_report_id, + prior_notification_source, + success, + date_time_utc, + communication_means, + recipient_address_or_number, + recipient_name, + recipient_organization, + error_message +) VALUES ( + '15', + 'LOGBOOK', + true, + ((now() AT TIME ZONE 'UTC') - INTERVAL '1 month 35 minutes')::TIMESTAMP, + 'EMAIL', + 'test@example.org', + 'Test Recipient', + 'Test Organization', + NULL +), +-- Add successful send for manual PNO report 00000000-0000-4000-0000-000000000007 so it will have is_correction = true +( + '00000000-0000-4000-0000-000000000007', + 'MANUAL', + true, + ((now() AT TIME ZONE 'UTC') - INTERVAL '20 minutes')::TIMESTAMP, + 'EMAIL', + 'test@example.org', + 'Test Recipient', + 'Test Organization', + NULL +); diff --git a/pipeline/tests/test_data/remote_database/V666.38__Reset_test_manual_prior_notifications.sql b/pipeline/tests/test_data/remote_database/V666.38__Reset_test_manual_prior_notifications.sql index faa6330cf3..f9ece7744b 100644 --- a/pipeline/tests/test_data/remote_database/V666.38__Reset_test_manual_prior_notifications.sql +++ b/pipeline/tests/test_data/remote_database/V666.38__Reset_test_manual_prior_notifications.sql @@ -7,4 +7,6 @@ INSERT INTO public.manual_prior_notifications ( ('00000000-0000-4000-0000-000000000003', NULL, 6, NOW() AT TIME ZONE 'UTC' - INTERVAL '3 months', false, 'FRA', NOW() AT TIME ZONE 'UTC' - INTERVAL '3 months', 'null', 'null', 'I DO 4H REPORT', '{"riskFactor": 2.8, "isBeingSent": true, "isInVerificationScope": false, "isSent": true, "isVerified": true, "catchOnboard":[{"faoZone": "37.1","weight": 172,"nbFish": 3,"species": "BFT"}],"catchToLand":[{"faoZone": "21.1.A","weight": 72,"nbFish": null,"species": "SOS"}],"note": null,"pnoTypes":[{"pnoTypeName": "Préavis type A","minimumNotificationPeriod": 4,"hasDesignatedPorts": false}],"port": "FRDPE","predictedArrivalDatetimeUtc": "2021-05-06T07:41:03.340Z","predictedLandingDatetimeUtc": "2021-05-06T11:41:03.340Z","purpose": "LAN","tripStartDate": "2021-05-04T11:41:03.340Z"}'), ('00000000-0000-4000-0000-000000000004', NULL, 6, NOW() AT TIME ZONE 'UTC' - INTERVAL '3 weeks', false, 'FRA', NOW() AT TIME ZONE 'UTC' - INTERVAL '3 weeks', NULL, NULL, 'I DO 4H REPORT', '{"riskFactor": 3.1, "isBeingSent": false, "isInVerificationScope": false, "isSent": true, "isVerified": false, "note": null,"pnoTypes":[{"pnoTypeName": "Préavis type A","minimumNotificationPeriod": 4,"hasDesignatedPorts": false}],"port": "FRDKK","predictedArrivalDatetimeUtc": "2021-05-06T07:41:03.340Z","predictedLandingDatetimeUtc": "2021-05-06T11:41:03.340Z","purpose": "LAN","tripStartDate": "2021-05-04T11:41:03.340Z"}'), ('00000000-0000-4000-0000-000000000005', NULL, 6, NOW() AT TIME ZONE 'UTC' - INTERVAL '3 days', false, 'FRA', NOW() AT TIME ZONE 'UTC' - INTERVAL '3 days', '[{"gear": "LNP"}, {"gear": "OTM", "mesh": 80}]', '[{"segment": "NWW09","segmentName": "Lignes"}, {"segment": "SWW01","segmentName": "Chaluts de fond"}]', 'I DO 4H REPORT', '{"riskFactor": 3.8, "isBeingSent": true, "isInVerificationScope": true, "isSent": true, "isVerified": true, "catchOnboard":[{"faoZone": "21.1.a","weight": 72,"nbFish": null,"species": "GHL"}, {"faoZone": "21.1.a","weight": 172,"nbFish": null,"species": "GHL"}, {"faoZone": "21.1.a","weight": 72,"nbFish": 3,"species": "BFT"}, {"faoZone": "27.2.a","weight": 32,"nbFish": 2,"species": "BFT"}, {"faoZone": "27.2.a","weight" : 502,"nbFish": 2,"species": "SWO"}, {"faoZone": "27.2.a","weight": 202,"nbFish": 1,"species": "SWO"}],"catchToLand":[{"faoZone": "21.1.a","weight": 72,"nbFish": null,"species": "GHL"}, {"faoZone": "21.1.a","weight": 172,"nbFish": null,"species": "GHL"}, {"faoZone": "21.1.a","weight": 72,"nbFish": 3,"species": "BFT"}, {"faoZone": "27.2.a","weight": 32,"nbFish": 2,"species": "BFT"}, {"faoZone": "27.2.a","weight": 502,"nbFish": 2,"species": "SWO"}, {"faoZone": "27.2.a","weight": 202,"nbFish": 1,"species": "SWO"}],"note": "Ceci est une note de préavis manuel","pnoTypes":[{"pnoTypeName": "Préavis type A","minimumNotificationPeriod": 4,"hasDesignatedPorts": false}, {"pnoTypeName": "Préavis type B","minimumNotificationPeriod": 4,"hasDesignatedPorts": true}],"port": "FRLEH","predictedArrivalDatetimeUtc": "2021-05-06T07:41:03.340Z","predictedLandingDatetimeUtc": "2021-05-06T11:41:03.340Z","purpose": "LAN","tripStartDate": "2021-05-04T11:41:03.340Z", "isInvalidated": false}'), - ('00000000-0000-4000-0000-000000000006', NULL, 6, NOW() AT TIME ZONE 'UTC' - INTERVAL '3 days', false, 'FRA', NOW() AT TIME ZONE 'UTC' - INTERVAL '3 days', '[{"gear": "LNP"}, {"gear": "OTM", "mesh": 80}]', '[{"segment": "NWW09","segmentName": "Lignes"}, {"segment": "SWW01","segmentName": "Chaluts de fond"}]', 'I DO 4H REPORT', '{"riskFactor": 3.8, "isBeingSent": true, "isInVerificationScope": true, "isSent": true, "isVerified": true, "catchOnboard":[{"faoZone": "21.1.a","weight": 72,"nbFish": null,"species": "GHL"}, {"faoZone": "21.1.a","weight": 172,"nbFish": null,"species": "GHL"}, {"faoZone": "21.1.a","weight": 72,"nbFish": 3,"species": "BFT"}, {"faoZone": "27.2.a","weight": 32,"nbFish": 2,"species": "BFT"}, {"faoZone": "27.2.a","weight" : 502,"nbFish": 2,"species": "SWO"}, {"faoZone": "27.2.a","weight": 202,"nbFish": 1,"species": "SWO"}],"catchToLand":[{"faoZone": "21.1.a","weight": 72,"nbFish": null,"species": "GHL"}, {"faoZone": "21.1.a","weight": 172,"nbFish": null,"species": "GHL"}, {"faoZone": "21.1.a","weight": 72,"nbFish": 3,"species": "BFT"}, {"faoZone": "27.2.a","weight": 32,"nbFish": 2,"species": "BFT"}, {"faoZone": "27.2.a","weight": 502,"nbFish": 2,"species": "SWO"}, {"faoZone": "27.2.a","weight": 202,"nbFish": 1,"species": "SWO"}],"note": "Ceci est une note de préavis manuel","pnoTypes":[{"pnoTypeName": "Préavis type A","minimumNotificationPeriod": 4,"hasDesignatedPorts": false}, {"pnoTypeName": "Préavis type B","minimumNotificationPeriod": 4,"hasDesignatedPorts": true}],"port": "FRLEH","predictedArrivalDatetimeUtc": "2021-05-06T07:41:03.340Z","predictedLandingDatetimeUtc": "2021-05-06T11:41:03.340Z","purpose": "LAN","tripStartDate": "2021-05-04T11:41:03.340Z", "isInvalidated": true}'); + ('00000000-0000-4000-0000-000000000006', NULL, 6, NOW() AT TIME ZONE 'UTC' - INTERVAL '3 days', false, 'FRA', NOW() AT TIME ZONE 'UTC' - INTERVAL '3 days', '[{"gear": "LNP"}, {"gear": "OTM", "mesh": 80}]', '[{"segment": "NWW09","segmentName": "Lignes"}, {"segment": "SWW01","segmentName": "Chaluts de fond"}]', 'I DO 4H REPORT', '{"riskFactor": 3.8, "isBeingSent": true, "isInVerificationScope": true, "isSent": true, "isVerified": true, "catchOnboard":[{"faoZone": "21.1.a","weight": 72,"nbFish": null,"species": "GHL"}, {"faoZone": "21.1.a","weight": 172,"nbFish": null,"species": "GHL"}, {"faoZone": "21.1.a","weight": 72,"nbFish": 3,"species": "BFT"}, {"faoZone": "27.2.a","weight": 32,"nbFish": 2,"species": "BFT"}, {"faoZone": "27.2.a","weight" : 502,"nbFish": 2,"species": "SWO"}, {"faoZone": "27.2.a","weight": 202,"nbFish": 1,"species": "SWO"}],"catchToLand":[{"faoZone": "21.1.a","weight": 72,"nbFish": null,"species": "GHL"}, {"faoZone": "21.1.a","weight": 172,"nbFish": null,"species": "GHL"}, {"faoZone": "21.1.a","weight": 72,"nbFish": 3,"species": "BFT"}, {"faoZone": "27.2.a","weight": 32,"nbFish": 2,"species": "BFT"}, {"faoZone": "27.2.a","weight": 502,"nbFish": 2,"species": "SWO"}, {"faoZone": "27.2.a","weight": 202,"nbFish": 1,"species": "SWO"}],"note": "Ceci est une note de préavis manuel","pnoTypes":[{"pnoTypeName": "Préavis type A","minimumNotificationPeriod": 4,"hasDesignatedPorts": false}, {"pnoTypeName": "Préavis type B","minimumNotificationPeriod": 4,"hasDesignatedPorts": true}],"port": "FRLEH","predictedArrivalDatetimeUtc": "2021-05-06T07:41:03.340Z","predictedLandingDatetimeUtc": "2021-05-06T11:41:03.340Z","purpose": "LAN","tripStartDate": "2021-05-04T11:41:03.340Z", "isInvalidated": true}'), + -- Manual PNO that will have is_correction = true (has been successfully sent before) + ('00000000-0000-4000-0000-000000000007', 'ABC000306959', 2, NOW() AT TIME ZONE 'UTC' - INTERVAL '10 minutes', false, 'FRA', NOW() AT TIME ZONE 'UTC' - INTERVAL '10 minutes', '[{"gear": "OTB", "mesh": 140, "dimensions": "250.0"}]', '[]', 'ÉTABLIR IMPRESSION LORSQUE', '{"riskFactor": 2.14443662414848, "isBeingSent": true, "isInVerificationScope": false, "isSent": false, "isVerified": false, "catchOnboard": [{"faoZone": "27.8.a", "weight": 150.0, "nbFish": null, "species": "GHL"}, {"faoZone": "27.8.a", "weight": 1450.0, "nbFish": null, "species": "HKE"}], "catchToLand": [{"faoZone": "27.8.a", "weight": 150.0, "nbFish": null, "species": "GHL"}, {"faoZone": "27.8.a", "weight": 1450.0, "nbFish": null, "species": "HKE"}], "note": null, "pnoTypes": [{"pnoTypeName": "Préavis type 2", "minimumNotificationPeriod": 4.0, "hasDesignatedPorts": true}], "port": "FRDKK", "purpose": "LAN", "predictedArrivalDatetimeUtc": "2020-05-06T11:41:03.340Z", "predictedLandingDatetimeUtc": "2020-05-06T15:41:03.340Z", "tripStartDate": "2020-05-04T19:41:03.340Z", "isInvalidated": false}'); diff --git a/pipeline/tests/test_data/remote_database/V666.5__Reset_test_logbook.sql b/pipeline/tests/test_data/remote_database/V666.5__Reset_test_logbook.sql index f8c3ddb9fb..d61a6c6f53 100644 --- a/pipeline/tests/test_data/remote_database/V666.5__Reset_test_logbook.sql +++ b/pipeline/tests/test_data/remote_database/V666.5__Reset_test_logbook.sql @@ -154,6 +154,7 @@ INSERT INTO logbook_raw_messages (operation_number, xml_message) VALUES ('13', 'Message ERS xml'), ('14', 'Message ERS xml'), ('15', 'Message ERS xml'), + ('15b', 'Message ERS xml'), ('16', 'Message ERS xml'), ('17', 'Message ERS xml'), ('18', 'Message ERS xml'), @@ -214,6 +215,15 @@ INSERT INTO logbook_reports ( ((now() AT TIME ZONE 'UTC') - INTERVAL '1 month 30 minutes')::TIMESTAMP, '20510003', 'ERS', true, '[{"gear": "OTB", "mesh": 140, "dimensions": "250.0"}]', '[]' ), +-- CORRECTION PNO - This will set is_correction = true +( + '15b', 'OOF', ((now() AT TIME ZONE 'UTC') - INTERVAL '1 month 28 minutes')::TIMESTAMP, 'COR', + '15', '15', ((now() AT TIME ZONE 'UTC') - INTERVAL '1 month 30 minutes')::TIMESTAMP, + '___TARGET___', 'TRGT', 'TARGET', 'NAVIRE CIBLE', 'FRA', null, 'PNO', + '{"port": "FRDPE", "purpose": "LAN", "catchOnboard": [{"nbFish": null, "faoZone": "27.8.a", "weight": 150.0, "species": "GHL"}, {"nbFish": null, "faoZone": "27.8.a", "weight": 1450.0, "species": "HKE"}, {"nbFish": 2, "faoZone": "27.8.a", "weight": 150.0, "species": "BFT"}, {"nbFish": 2, "faoZone": "27.8.a", "weight": 70.0, "species": "SWO"}, {"nbFish": 2, "faoZone": "27.8.b", "weight": 150.0, "species": "BFT"}, {"nbFish": null, "faoZone": "27.8.b", "weight": 250.0, "species": "GHL"}], "tripStartDate": "2020-05-04T19:41:03.340Z", "predictedArrivalDatetimeUtc": "2020-05-06T11:41:03.340Z", "pnoTypes": [], "isInVerificationScope": false, "isVerified": false, "isSent": false, "isBeingSent": true}', + ((now() AT TIME ZONE 'UTC') - INTERVAL '1 month 26 minutes')::TIMESTAMP, '20510003', 'ERS', + true, '[{"gear": "OTB", "mesh": 140, "dimensions": "250.0"}]', '[]' +), ( '16', 'OOE', ((now() AT TIME ZONE 'UTC') - INTERVAL '1 month 1 hour')::TIMESTAMP, 'RET', NULL, '11', NULL, diff --git a/pipeline/tests/test_flows/test_distribute_pnos.py b/pipeline/tests/test_flows/test_distribute_pnos.py index d2b792b9b2..c66a287282 100644 --- a/pipeline/tests/test_flows/test_distribute_pnos.py +++ b/pipeline/tests/test_flows/test_distribute_pnos.py @@ -68,6 +68,7 @@ ) from src.read_query import read_query from tests.mocks import mock_datetime_utcnow +from tests.test_helpers.test_snapshots import should_generate_snapshots @pytest.fixture @@ -121,17 +122,18 @@ def extracted_pnos() -> pd.DataFrame: now = datetime.utcnow() return pd.DataFrame( { - "id": [35.0, 36.0, 37.0, 38.0, 39.0, None, None, None, None], + "id": [35.0, 36.0, 37.0, 38.0, 40.0, None, None, None, None, None], "operation_datetime_utc": [ now - relativedelta(months=1, hours=1), now - relativedelta(months=1, minutes=25), now - relativedelta(months=1, hours=2), now - relativedelta(months=1, minutes=52), - now - relativedelta(months=1, minutes=32), + now - relativedelta(months=1, minutes=28), None, None, None, None, + None, # Manual correction ], "report_id": [ "11", @@ -143,19 +145,21 @@ def extracted_pnos() -> pd.DataFrame: "00000000-0000-4000-0000-000000000003", "00000000-0000-4000-0000-000000000004", "00000000-0000-4000-0000-000000000005", + "00000000-0000-4000-0000-000000000007", # Manual correction ], "report_datetime_utc": [ now - relativedelta(months=1, hours=1, minutes=2), now - relativedelta(months=1, minutes=27), now - relativedelta(months=1, hours=2, minutes=2), now - relativedelta(months=1, minutes=54), - now - relativedelta(months=1, minutes=34), + now - relativedelta(months=1, minutes=30), now - relativedelta(minutes=15), now - relativedelta(months=3), now - relativedelta(weeks=3), now - relativedelta(days=3), + now - relativedelta(minutes=10), # Manual correction ], - "vessel_id": [2, None, 1, 1, 7, 3, 6, 6, 6], + "vessel_id": [2, None, 1, 1, 7, 3, 6, 6, 6, 2], "cfr": [ "ABC000542519", "ABC000000000", @@ -166,6 +170,7 @@ def extracted_pnos() -> pd.DataFrame: None, None, None, + "ABC000306959", # Manual correction ], "ircs": [ "FQ7058", @@ -177,6 +182,7 @@ def extracted_pnos() -> pd.DataFrame: "ZZ000000", "ZZ000000", "ZZ000000", + "FQ7058", # Manual correction ], "external_identification": [ "RO237719", @@ -188,6 +194,7 @@ def extracted_pnos() -> pd.DataFrame: "ZZTOPACDC", "ZZTOPACDC", "ZZTOPACDC", + "RO237719", # Manual correction ], "vessel_name": [ "DEVINER FIGURE CONSCIENCE", @@ -199,6 +206,7 @@ def extracted_pnos() -> pd.DataFrame: "I DO 4H REPORT", "I DO 4H REPORT", "I DO 4H REPORT", + "ÉTABLIR IMPRESSION LORSQUE", # Manual correction ], "flag_state": [ "FRA", @@ -210,8 +218,9 @@ def extracted_pnos() -> pd.DataFrame: "FRA", "FRA", "FRA", + "FRA", # Manual correction ], - "purpose": ["LAN", "ACS", "OTH", "LAN", "LAN", "LAN", "LAN", "LAN", "LAN"], + "purpose": ["LAN", "ACS", "OTH", "LAN", "LAN", "LAN", "LAN", "LAN", "LAN", "LAN"], "catch_onboard": [ [ { @@ -407,6 +416,10 @@ def extracted_pnos() -> pd.DataFrame: {"nbFish": 2, "weight": 502, "faoZone": "27.2.a", "species": "SWO"}, {"nbFish": 1, "weight": 202, "faoZone": "27.2.a", "species": "SWO"}, ], + [ + {'nbFish': None, 'weight': 150.0, 'faoZone': '27.8.a', 'species': 'GHL'}, + {'nbFish': None, 'weight': 1450.0, 'faoZone': '27.8.a', 'species': 'HKE'}, + ], ], "port_locode": [ "FRCQF", @@ -418,6 +431,7 @@ def extracted_pnos() -> pd.DataFrame: "FRDPE", "FRDKK", "FRLEH", + "FRDKK", # Manual correction ], "port_name": [ "Somewhere over the rainbow", @@ -429,29 +443,32 @@ def extracted_pnos() -> pd.DataFrame: "Somewhere over the clouds", "Somewhere over the swell", "Somewhere over the ocean", + "Somewhere over the swell", # Manual correction ], - "facade": ["NAMO", "SA", "NAMO", "SA", None, "SA", None, "NAMO", "SA"], + "facade": ["NAMO", "SA", "NAMO", "SA", None, "SA", None, "NAMO", "SA", "NAMO"], "predicted_arrival_datetime_utc": [ now - relativedelta(months=1, hours=1) + relativedelta(hours=4), now - relativedelta(months=1, minutes=25) + relativedelta(hours=4), now - relativedelta(months=1, hours=2) + relativedelta(hours=4), now - relativedelta(months=1, minutes=52) + relativedelta(hours=4), - now - relativedelta(months=1, minutes=32) + relativedelta(hours=4), + now - relativedelta(months=1, minutes=28) + relativedelta(hours=4), datetime(2021, 5, 6, 7, 41, 3, 340000), datetime(2021, 5, 6, 7, 41, 3, 340000), datetime(2021, 5, 6, 7, 41, 3, 340000), datetime(2021, 5, 6, 7, 41, 3, 340000), + datetime(2020, 5, 6, 11, 41, 3, 340000), # Manual correction ], "predicted_landing_datetime_utc": [ now - relativedelta(months=1, hours=1) + relativedelta(hours=4), now - relativedelta(months=1, minutes=25) + relativedelta(hours=4), now - relativedelta(months=1, hours=2) + relativedelta(hours=4), now - relativedelta(months=1, minutes=52) + relativedelta(hours=4), - now - relativedelta(months=1, minutes=32) + relativedelta(hours=4), + now - relativedelta(months=1, minutes=28) + relativedelta(hours=4), datetime(2021, 5, 6, 11, 41, 3, 340000), datetime(2021, 5, 6, 11, 41, 3, 340000), datetime(2021, 5, 6, 11, 41, 3, 340000), datetime(2021, 5, 6, 11, 41, 3, 340000), + datetime(2020, 5, 6, 15, 41, 3, 340000), # Manual correction ], "trip_gears": [ [ @@ -466,6 +483,7 @@ def extracted_pnos() -> pd.DataFrame: [], [], [{"gear": "LNP"}, {"gear": "OTM", "mesh": 80}], + [{"gear": "OTB", "mesh": 140, "dimensions": "250.0"}], # Manual correction ], "trip_segments": [ [ @@ -488,6 +506,7 @@ def extracted_pnos() -> pd.DataFrame: {"segment": "NWW09", "segmentName": "Lignes"}, {"segment": "SWW01", "segmentName": "Chaluts de fond"}, ], + [], # Manual correction ], "pno_types": [ [ @@ -556,6 +575,9 @@ def extracted_pnos() -> pd.DataFrame: "minimumNotificationPeriod": 4, }, ], + [ + {'pnoTypeName': 'Préavis type 2', 'hasDesignatedPorts': True, 'minimumNotificationPeriod': 4.0}, + ], ], "note": [ None, @@ -567,9 +589,10 @@ def extracted_pnos() -> pd.DataFrame: None, None, "Ceci est une note de préavis manuel", + None, # Manual correction ], - "vessel_length": [13.4, None, 17.4, 17.4, 8.58, 11.5, 12.5, 12.5, 12.5], - "mmsi": [None, None, None, None, None, None, None, None, None], + "vessel_length": [13.4, None, 17.4, 17.4, 8.58, 11.5, 12.5, 12.5, 12.5, 13.4], + "mmsi": [None, None, None, None, None, None, None, None, None, None], "risk_factor": [ 2.09885592141872, 3.9, @@ -580,6 +603,7 @@ def extracted_pnos() -> pd.DataFrame: 2.8, 3.1, 3.8, + 2.14443662414848, # Manual correction ], "last_control_datetime_utc": [ now - relativedelta(years=1, days=2), @@ -591,6 +615,7 @@ def extracted_pnos() -> pd.DataFrame: None, None, None, + now - relativedelta(years=1, days=2), # Manual correction ], "last_control_infractions": [ [ @@ -611,9 +636,19 @@ def extracted_pnos() -> pd.DataFrame: [], [], [], + [ + { + "natinf": 27724, + "comments": "Infraction engin", + "infractionType": "WITHOUT_RECORD", + }, + {"natinf": 2606}, + {"natinf": 4761}, + {"natinf": 22206}, + ], # Manual correction ], - "is_verified": [True, True, False, False, False, False, True, False, True], - "is_being_sent": [True, True, False, True, True, True, True, False, True], + "is_verified": [True, True, False, False, False, False, True, False, True, False], + "is_being_sent": [True, True, False, True, True, True, True, False, True, True], "source": [ "LOGBOOK", "LOGBOOK", @@ -624,6 +659,31 @@ def extracted_pnos() -> pd.DataFrame: "MANUAL", "MANUAL", "MANUAL", + "MANUAL", # Manual correction + ], + "is_correction": [ + False, + False, + False, + False, + True, + False, + False, + False, + False, + True, # Manual correction 00000000-0000-4000-0000-000000000007 + ], + "previous_notification_date_utc": [ + None, + None, + None, + None, + now - relativedelta(days=31, minutes=35), + None, + None, + None, + None, + now - relativedelta(minutes=20), # Original send date for manual PNO ], } ) @@ -750,8 +810,84 @@ def pno_to_render_1() -> PnoToRender: is_verified=False, is_being_sent=True, source=PnoSource.LOGBOOK, + is_correction=False, + previous_notification_date_utc=None, ) +@pytest.fixture +def pno_zero_to_render_1() -> PnoToRender: + return PnoToRender( + id=35, + operation_datetime_utc=datetime(2024, 5, 5, 8, 13, 38, 259967), + report_id="11", + report_datetime_utc=datetime(2024, 5, 5, 8, 11, 38, 259967), + vessel_id=2, + cfr="ABC000542519", + ircs="FQ7058", + external_identification="RO237719", + vessel_name="DEVINER FIGURE CONSCIENCE", + flag_state="FRA", + purpose="LAN", + catch_onboard=[ + { + "nbFish": None, + "weight": 0.0, + "faoZone": "27.8.a", + "species": "GHL", + "statisticalRectangle": "47E3", + }, + ], + port_locode="FRCQF", + port_name="Somewhere over the rainbow", + facade="NAMO", + predicted_arrival_datetime_utc=datetime(2020, 5, 6, 11, 41, 3, 340000), + predicted_landing_datetime_utc=datetime(2020, 5, 6, 16, 40), + trip_gears=[ + {"gear": "OTT", "mesh": 140, "dimensions": "250.0"}, + {"gear": "OTT", "dimensions": "250.0"}, + ], + trip_segments=[ + {"segment": "SHKE27", "segmentName": "Merlu en zone 27"}, + {"segment": "SOTM", "segmentName": "Chaluts pélagiques"}, + ], + pno_types=[ + { + "pnoTypeName": "Préavis type 1", + "hasDesignatedPorts": True, + "minimumNotificationPeriod": 4.0, + }, + { + "pnoTypeName": "Préavis type 2", + "hasDesignatedPorts": False, + "minimumNotificationPeriod": 4.0, + }, + ], + note=( + "Attention attention faites bien attention très attention sait-on " + "jamais ce qui pourrait, ô grand Dieu des Poissons qui se font " + "pêcher, nous arriver, si on ne fait pas assez attention à ce qu'y " + "pêche c'te navire d'pêche qui fait d'la pêche de poissons." + ), + vessel_length=13.4, + mmsi=None, + risk_factor=2.09885592141872, + last_control_datetime_utc=datetime(2023, 6, 3, 9, 13, 38, 259967), + last_control_infractions=[ + { + "natinf": 27724, + "comments": "Infraction engin", + "infractionType": "WITHOUT_RECORD", + }, + {"natinf": 2606}, + {"natinf": 4761}, + {"natinf": 22206}, + ], + is_verified=False, + is_being_sent=True, + source=PnoSource.LOGBOOK, + is_correction=False, + previous_notification_date_utc=None, + ) @pytest.fixture def pre_rendered_pno_1_catch_onboard() -> pd.DataFrame: @@ -776,6 +912,20 @@ def pre_rendered_pno_1_catch_onboard() -> pd.DataFrame: } ) +@pytest.fixture +def pre_rendered_pno_zero_1_catch_onboard() -> pd.DataFrame: + return pd.DataFrame( + { + "Espèces": [ + "Pou Hasse Caille (GHL)", + ], + "Zones de pêche": [ + "27.8.a (47E3)", + ], + "Qtés (kg)": [0], + "Nb": ["-"], + } + ) @pytest.fixture def pre_rendered_pno_1(pre_rendered_pno_1_catch_onboard) -> PreRenderedPno: @@ -828,9 +978,68 @@ def pre_rendered_pno_1(pre_rendered_pno_1_catch_onboard) -> PreRenderedPno: source=PnoSource.LOGBOOK, purpose_suffix="de débarquement", is_landing=True, + is_zero=False, + is_correction=False, + previous_notification_date_utc=None, ) +@pytest.fixture +def pre_rendered_pno_zero_1(pre_rendered_pno_zero_1_catch_onboard) -> PreRenderedPno: + return PreRenderedPno( + id=35, + operation_datetime_utc=datetime(2024, 5, 5, 8, 13, 38, 259967), + report_id="11", + report_datetime_utc=datetime(2024, 5, 5, 8, 11, 38, 259967), + vessel_id=2, + cfr="ABC000542519", + ircs="FQ7058", + external_identification="RO237719", + vessel_name="DEVINER FIGURE CONSCIENCE", + flag_state="FRA", + purpose="Débarquement", + catch_onboard=pre_rendered_pno_zero_1_catch_onboard, + bft_summary=None, + port_locode="FRCQF", + port_name="Somewhere over the rainbow", + facade="NAMO", + predicted_arrival_datetime_utc=datetime(2020, 5, 6, 11, 41, 3, 340000), + predicted_landing_datetime_utc=datetime(2020, 5, 6, 16, 40), + trip_gears=[ + FishingGear(code="OTT", name="Chaluts jumeaux à panneaux", mesh=140), + FishingGear(code="OTT", name="Chaluts jumeaux à panneaux"), + ], + trip_segments=[ + FleetSegment(code="SHKE27", name="Merlu en zone 27"), + FleetSegment(code="SOTM", name="Chaluts pélagiques"), + ], + pno_types=["Préavis type 1", "Préavis type 2"], + note=( + "Attention attention faites bien attention très attention sait-on " + "jamais ce qui pourrait, ô grand Dieu des Poissons qui se font " + "pêcher, nous arriver, si on ne fait pas assez attention à ce qu'y " + "pêche c'te navire d'pêche qui fait d'la pêche de poissons." + ), + vessel_length=13.4, + mmsi=None, + risk_factor=2.09885592141872, + last_control_datetime_utc=datetime(2023, 6, 3, 9, 13, 38, 259967), + last_control_infractions=[ + Infraction(natinf=27724, comments="Infraction engin"), + Infraction(natinf=2606, comments=None), + Infraction(natinf=4761, comments=None), + Infraction(natinf=22206, comments=None), + ], + is_verified=False, + is_being_sent=True, + source=PnoSource.LOGBOOK, + purpose_suffix="de débarquement", + is_landing=True, + is_zero=True, + is_correction=False, + previous_notification_date_utc=None, + ) + @pytest.fixture def pno_to_render_2() -> PnoToRender: return PnoToRender( @@ -863,6 +1072,8 @@ def pno_to_render_2() -> PnoToRender: is_verified=True, is_being_sent=True, source=PnoSource.LOGBOOK, + is_correction=True, + previous_notification_date_utc=datetime(2024, 5, 5, 8, 40, 38, 259967), ) @@ -901,6 +1112,9 @@ def pre_rendered_pno_2() -> PreRenderedPno: source=PnoSource.LOGBOOK, purpose_suffix="d'accès aux services", is_landing=False, + is_zero=False, + is_correction=True, + previous_notification_date_utc=datetime(2024, 5, 5, 8, 40, 38, 259967), ) @@ -1328,30 +1542,39 @@ def some_more_sent_messages( @pytest.fixture def loaded_sent_messages() -> pd.DataFrame: + now = datetime.utcnow() return pd.DataFrame( { - "id": [1, 2, 3], - "prior_notification_report_id": ["Report-1", "Report-1", "Report-1"], - "prior_notification_source": ["LOGBOOK", "LOGBOOK", "LOGBOOK"], + "id": [1, 2, 3, 4, 5], + "prior_notification_report_id": ["15", "00000000-0000-4000-0000-000000000007", "Report-1", "Report-1", "Report-1"], + "prior_notification_source": ["LOGBOOK", "MANUAL", "LOGBOOK", "LOGBOOK", "LOGBOOK"], "date_time_utc": [ + now - relativedelta(months=1, minutes=35), + now - relativedelta(minutes=20), datetime(2023, 6, 6, 16, 10, 00), datetime(2023, 6, 6, 16, 10, 00), datetime(2023, 6, 6, 16, 10, 00), ], - "communication_means": ["EMAIL", "EMAIL", "SMS"], + "communication_means": ["EMAIL", "EMAIL", "EMAIL", "EMAIL", "SMS"], "recipient_address_or_number": [ + "test@example.org", + "test@example.org", "email1@control.unit", "email.in.error@control.unit", "00000000000", ], - "success": [True, False, True], - "error_message": [None, "Error when sending email", None], + "success": [True, True, True, False, True], + "error_message": [None, None, None, "Error when sending email", None], "recipient_name": [ + "Test Recipient", + "Test Recipient", "Unité de test numero 1", "Unité de test numero 2", "Unité de test numero 2", ], "recipient_organization": [ + "Test Organization", + "Test Organization", "Une Administration quelconque", "Une autre Administration", "Une autre Administration", @@ -1465,6 +1688,7 @@ def test_extract_pnos_to_generate(reset_test_data, extracted_pnos): "last_control_datetime_utc", "predicted_arrival_datetime_utc", "predicted_landing_datetime_utc", + "previous_notification_date_utc", ] pnos, generation_needed = extract_pnos_to_generate( @@ -1512,7 +1736,7 @@ def test_extract_pno_extra_subscriptions(reset_test_data, pno_extra_subscription def test_to_pnos_to_render(extracted_pnos): res = to_pnos_to_render(pnos=extracted_pnos) - assert len(res) == 9 + assert len(res) == 10 assert isinstance(res[0], PnoToRender) @@ -1537,6 +1761,15 @@ def test_pre_render_pno_2( ) PreRenderedPno.assert_equal(res, pre_rendered_pno_2) +def test_pre_render_pno_zero_1( + pno_zero_to_render_1, species_names, fishing_gear_names, pre_rendered_pno_zero_1 +): + res = pre_render_pno( + pno=pno_zero_to_render_1, + species_names=species_names, + fishing_gear_names=fishing_gear_names, + ) + PreRenderedPno.assert_equal(res, pre_rendered_pno_zero_1) @patch( "src.flows.distribute_pnos.EMAIL_FONTS_LOCATION", @@ -1568,15 +1801,72 @@ def test_render_pno_1( TEST_DATA_LOCATION / "emails/prior_notifications/expected_email_body_1.html" ) - ######################### Uncomment to replace test files ######################### - # with open(test_html_filepath, "w") as f: - # f.write(pno.html_for_pdf) + if should_generate_snapshots(): + with open(test_html_filepath, "w") as f: + f.write(pno.html_for_pdf) + + with open(test_email_body_file_path, "w") as f: + f.write(pno.html_email_body) + + with open(test_sms_filepath, "w") as f: + f.write(pno.sms_content) + + ################################################################################### + with open(test_html_filepath, "r") as f: + expected_html = f.read() + + with open(test_email_body_file_path, "r") as f: + expected_html_email_body = f.read() + + with open(test_sms_filepath, "r") as f: + expected_sms_content = f.read() + + assert isinstance(pno, RenderedPno) + assert pno.html_for_pdf == expected_html + assert pno.html_email_body == expected_html_email_body + assert pno.report_id == "11" + assert pno.source == PnoSource.LOGBOOK + assert pno.sms_content == expected_sms_content + +@patch( + "src.flows.distribute_pnos.EMAIL_FONTS_LOCATION", + Path("/email/fonts/location"), +) +@patch("src.flows.distribute_pnos.CNSP_LOGO_PATH", Path("/cnsp/logo/path")) +@patch("src.flows.distribute_pnos.SE_MER_LOGO_PATH", Path("/se_mer/logo/path")) +@patch( + "src.flows.distribute_pnos.STATE_FLAGS_ICONS_LOCATION", + Path("/state/flags/icons/location"), +) +def test_render_pno_zero_1( + html_for_pdf_template, pre_rendered_pno_zero_1, email_body_template, sms_template +): + pno = render_pno( + pno=pre_rendered_pno_zero_1, + html_for_pdf_template=html_for_pdf_template, + email_body_template=email_body_template, + sms_template=sms_template, + ) + + test_sms_filepath = ( + TEST_DATA_LOCATION / "emails/prior_notifications/expected_sms_zero_1.txt" + ) + test_html_filepath = ( + TEST_DATA_LOCATION / "emails/prior_notifications/expected_pno_zero_1.html" + ) + test_email_body_file_path = ( + TEST_DATA_LOCATION / "emails/prior_notifications/expected_email_body_zero_1.html" + ) + + if should_generate_snapshots(): + with open(test_html_filepath, "w") as f: + f.write(pno.html_for_pdf) - # with open(test_email_body_file_path, "w") as f: - # f.write(pno.html_email_body) + with open(test_email_body_file_path, "w") as f: + f.write(pno.html_email_body) - # with open(test_sms_filepath, "w") as f: - # f.write(pno.sms_content) + with open(test_sms_filepath, "w") as f: + f.write(pno.sms_content) ################################################################################### with open(test_html_filepath, "r") as f: @@ -1626,15 +1916,15 @@ def test_render_pno_2( TEST_DATA_LOCATION / "emails/prior_notifications/expected_email_body_2.html" ) - ######################### Uncomment to replace test files ######################### - # with open(test_html_filepath, "w") as f: - # f.write(pno.html_for_pdf) + if should_generate_snapshots(): + with open(test_html_filepath, "w") as f: + f.write(pno.html_for_pdf) - # with open(test_email_body_file_path, "w") as f: - # f.write(pno.html_email_body) + with open(test_email_body_file_path, "w") as f: + f.write(pno.html_email_body) - # with open(test_sms_filepath, "w") as f: - # f.write(pno.sms_content) + with open(test_sms_filepath, "w") as f: + f.write(pno.sms_content) ################################################################################### with open(test_html_filepath, "r") as f: @@ -1667,9 +1957,9 @@ def test_render_pno_1_pdf( test_filepath = TEST_DATA_LOCATION / "emails/prior_notifications/expected_pno_1.pdf" - ######################### Uncomment to replace test files ######################### - # with open(test_filepath, "wb") as f: - # f.write(pno.pdf_document) + if should_generate_snapshots(): + with open(test_filepath, "wb") as f: + f.write(pno.pdf_document) ################################################################################### with open(test_filepath, "rb") as f: @@ -1696,9 +1986,9 @@ def test_render_pno_2_pdf( test_filepath = TEST_DATA_LOCATION / "emails/prior_notifications/expected_pno_2.pdf" - ######################### Uncomment to replace test files ######################### - # with open(test_filepath, "wb") as f: - # f.write(pno.pdf_document) + if should_generate_snapshots(): + with open(test_filepath, "wb") as f: + f.write(pno.pdf_document) ################################################################################### with open(test_filepath, "rb") as f: @@ -1710,6 +2000,34 @@ def test_render_pno_2_pdf( assert pno.source == PnoSource.LOGBOOK assert isinstance(pno.generation_datetime_utc, datetime) +def test_render_pno_zero_1_pdf( + html_for_pdf_template, pre_rendered_pno_zero_1, email_body_template, sms_template +): + pno = render_pno( + pno=pre_rendered_pno_zero_1, + html_for_pdf_template=html_for_pdf_template, + email_body_template=email_body_template, + sms_template=sms_template, + ) + pdf = pypdf.PdfReader(io.BytesIO(pno.pdf_document)) + + test_filepath = TEST_DATA_LOCATION / "emails/prior_notifications/expected_pno_zero_1.pdf" + + if should_generate_snapshots(): + with open(test_filepath, "wb") as f: + f.write(pno.pdf_document) + + ################################################################################### + with open(test_filepath, "rb") as f: + expected_pdf = pypdf.PdfReader(io.BytesIO(f.read())) + + assert expected_pdf.pages[0].extract_text() == pdf.pages[0].extract_text() + assert expected_pdf.pages[1].extract_text() == pdf.pages[1].extract_text() + + assert pno.report_id == "11" + assert pno.source == PnoSource.LOGBOOK + assert isinstance(pno.generation_datetime_utc, datetime) + def test_load_pno_pdf_documents(reset_test_data): # Setup @@ -1914,8 +2232,8 @@ def test_execute_prior_notification_attachments_query_when_result_is_empty( @pytest.mark.parametrize( - "test_mode,pno_has_uploaded_attachments", - [(False, False), (False, True), (True, False), (True, True)], + "test_mode,pno_has_uploaded_attachments,is_zero", + [(False, False, False), (False, True, False), (True, False, False), (True, True, False), (False, False, True)], ) def test_create_email( pno_pdf_document_to_distribute_targeted_vessel_and_segments_assigned, @@ -1926,7 +2244,11 @@ def test_create_email( facade_email_addresses, test_mode, pno_has_uploaded_attachments, + is_zero, ): + if is_zero: + pno_pdf_document_to_distribute_targeted_vessel_and_segments_assigned.is_zero = True + pno_to_send = create_email( pno_pdf_document_to_distribute_targeted_vessel_and_segments_assigned, uploaded_attachments=prior_notification_attachments @@ -1955,8 +2277,10 @@ def test_create_email( assert pno_to_send.message["From"] == "monitorfish@test.email" assert pno_to_send.message["Bcc"] is None assert pno_to_send.message["Reply-To"] == "cnsp.france@test.email" + + expected_title_suffix = " - préavis zéro" if is_zero else "" assert ( - pno_to_send.message["Subject"] == "Préavis de débarquement - Le navire 123-abc" + pno_to_send.message["Subject"] == "Préavis de débarquement - Le navire 123-abc"+expected_title_suffix ) attachments = list(pno_to_send.message.iter_attachments()) @@ -2110,8 +2434,22 @@ def test_load_prior_notification_sent_messages( load_prior_notification_sent_messages(messages_sent_by_email + messages_sent_by_sms) final_sent_messages = read_query(query=query, db="monitorfish_remote") - assert len(initial_sent_messages) == 0 - pd.testing.assert_frame_equal(final_sent_messages, loaded_sent_messages) + assert len(initial_sent_messages) == 2 + + approximate_datetime_columns = [ + "date_time_utc", + ] + + pd.testing.assert_frame_equal( + final_sent_messages.drop(columns=approximate_datetime_columns), + loaded_sent_messages.drop(columns=approximate_datetime_columns), + ) + + for col in approximate_datetime_columns: + assert ( + (final_sent_messages[col].dropna() - loaded_sent_messages[col].dropna()).abs() + < timedelta(seconds=10) + ).all() def test_make_update_logbook_reports_statement( diff --git a/pipeline/tests/test_helpers/test_snapshots.py b/pipeline/tests/test_helpers/test_snapshots.py new file mode 100644 index 0000000000..810dc0103a --- /dev/null +++ b/pipeline/tests/test_helpers/test_snapshots.py @@ -0,0 +1,7 @@ +import os + +def should_generate_snapshots() -> bool: + val = os.getenv("TEST_GENERATE_SNAPSHOTS") + if val is None: + return False + return val.lower() == "true" \ No newline at end of file