From b586ba4a27d963c42ac9a033006a0758957d9e89 Mon Sep 17 00:00:00 2001 From: Alan Le Ruyet Date: Wed, 6 Dec 2023 11:07:56 +0100 Subject: [PATCH 01/13] feat: progress --- .../http/routes/campaignWebhook.controller.ts | 28 ------------- .../routes/{auth => }/emails.controller.ts | 41 +++++++++++++++---- .../http/routes/sendApplication.controller.ts | 21 +--------- server/src/http/server.ts | 4 +- server/src/services/brevo.service.ts | 35 ++++++---------- shared/routes/application.routes.ts | 19 --------- shared/routes/campaignWebhook.routes.ts | 27 ------------ shared/routes/emails.routes.ts | 3 +- shared/routes/index.ts | 2 - 9 files changed, 48 insertions(+), 132 deletions(-) delete mode 100644 server/src/http/routes/campaignWebhook.controller.ts rename server/src/http/routes/{auth => }/emails.controller.ts (76%) delete mode 100644 shared/routes/campaignWebhook.routes.ts diff --git a/server/src/http/routes/campaignWebhook.controller.ts b/server/src/http/routes/campaignWebhook.controller.ts deleted file mode 100644 index 5c21526bed..0000000000 --- a/server/src/http/routes/campaignWebhook.controller.ts +++ /dev/null @@ -1,28 +0,0 @@ -import Boom from "boom" -import { zRoutes } from "shared/index" - -import config from "@/config" - -import { addEmailToBlacklist, removeEmailFromLbaCompanies } from "../../services/application.service" -import { BrevoEventStatus } from "../../services/brevo.service" -import { Server } from "../server" - -export default function (server: Server) { - server.post( - "/campaign/webhook", - { - schema: zRoutes.post["/campaign/webhook"], - }, - async (req, res) => { - const { apikey } = req.query - if (apikey !== config.smtp.brevoWebhookApiKey) { - throw Boom.unauthorized() - } - if (req.body.event === BrevoEventStatus.HARD_BOUNCE) { - await Promise.all([addEmailToBlacklist(req.body.email, "campaign"), removeEmailFromLbaCompanies(req.body.email)]) - } - - return res.status(200).send({ result: "ok" }) - } - ) -} diff --git a/server/src/http/routes/auth/emails.controller.ts b/server/src/http/routes/emails.controller.ts similarity index 76% rename from server/src/http/routes/auth/emails.controller.ts rename to server/src/http/routes/emails.controller.ts index fd5630d8dc..146e1299d1 100644 --- a/server/src/http/routes/auth/emails.controller.ts +++ b/server/src/http/routes/emails.controller.ts @@ -3,14 +3,14 @@ import { zRoutes } from "shared/index" import config from "@/config" -import { logger } from "../../../common/logger" -import { Etablissement } from "../../../common/model" -import { addEmailToBlacklist } from "../../../services/application.service" -import * as appointmentService from "../../../services/appointment.service" -import { BrevoEventStatus } from "../../../services/brevo.service" -import dayjs from "../../../services/dayjs.service" -import * as eligibleTrainingsForAppointmentService from "../../../services/eligibleTrainingsForAppointment.service" -import { Server } from "../../server" +import { logger } from "../../common/logger" +import { Etablissement } from "../../common/model" +import { addEmailToBlacklist, removeEmailFromLbaCompanies } from "../../services/application.service" +import * as appointmentService from "../../services/appointment.service" +import { BrevoEventStatus } from "../../services/brevo.service" +import dayjs from "../../services/dayjs.service" +import * as eligibleTrainingsForAppointmentService from "../../services/eligibleTrainingsForAppointment.service" +import { Server } from "../server" /** * Email controllers. @@ -31,7 +31,30 @@ export default (server: Server) => { throw Boom.forbidden() } - const { date, event } = req.body + const { date, event, email } = req.body + + if (event === BrevoEventStatus.HARD_BOUNCE) { + // ???? comment identifier la source du blacklisting + + await Promise.all([addEmailToBlacklist(email, "campaign"), removeEmailFromLbaCompanies(email)]) + } + + // server.post( + // "/application/webhook", + // { + // schema: zRoutes.post["/application/webhook"], + // }, + // async (req, res) => { + // const { apikey } = req.query + // if (apikey !== config.smtp.brevoWebhookApiKey) { + // throw Boom.unauthorized() + // } + + // await updateApplicationStatus({ payload: req.body }) + // return res.status(200).send({ result: "ok" }) + // } + // ) + const messageId = req.body["message-id"] const eventDate = dayjs.utc(date).tz("Europe/Paris").toDate() diff --git a/server/src/http/routes/sendApplication.controller.ts b/server/src/http/routes/sendApplication.controller.ts index 58aa5dec15..d53bf77f83 100644 --- a/server/src/http/routes/sendApplication.controller.ts +++ b/server/src/http/routes/sendApplication.controller.ts @@ -1,13 +1,10 @@ -import Boom from "boom" import mongoose from "mongoose" import { zRoutes } from "shared/index" -import config from "@/config" - import { Application } from "../../common/model/index" import { decryptWithIV } from "../../common/utils/encryptString" import { sentryCaptureException } from "../../common/utils/sentryUtils" -import { sendNotificationToApplicant, updateApplicationStatus, validateFeedbackApplicationComment } from "../../services/application.service" +import { sendNotificationToApplicant, validateFeedbackApplicationComment } from "../../services/application.service" import { Server } from "../server" const rateLimitConfig = { @@ -77,20 +74,4 @@ export default function (server: Server) { return res.status(200).send({ result: "ok" }) } ) - - server.post( - "/application/webhook", - { - schema: zRoutes.post["/application/webhook"], - }, - async (req, res) => { - const { apikey } = req.query - if (apikey !== config.smtp.brevoWebhookApiKey) { - throw Boom.unauthorized() - } - - await updateApplicationStatus({ payload: req.body }) - return res.status(200).send({ result: "ok" }) - } - ) } diff --git a/server/src/http/server.ts b/server/src/http/server.ts index cb2411b805..a27c3c6854 100644 --- a/server/src/http/server.ts +++ b/server/src/http/server.ts @@ -33,10 +33,9 @@ import eligibleTrainingsForAppointmentRoute from "./routes/admin/eligibleTrainin import adminEtablissementRoute from "./routes/admin/etablissement.controller" import formationsRoute from "./routes/admin/formations.controller" import appointmentRequestRoute from "./routes/appointmentRequest.controller" -import emailsRoute from "./routes/auth/emails.controller" import login from "./routes/auth/login.controller" -import campaignWebhook from "./routes/campaignWebhook.controller" import { coreRoutes } from "./routes/core.controller" +import emailsRoute from "./routes/emails.controller" import etablissementRoute from "./routes/etablissement.controller" import etablissementsRecruteurRoute from "./routes/etablissementRecruteur.controller" import formulaireRoute from "./routes/formulaire.controller" @@ -134,7 +133,6 @@ export async function bind(app: Server) { metiers(typedSubApp) rome(typedSubApp) updateLbaCompany(typedSubApp) - campaignWebhook(typedSubApp) sendApplication(typedSubApp) sendApplicationAPI(typedSubApp) unsubscribeLbaCompany(typedSubApp) diff --git a/server/src/services/brevo.service.ts b/server/src/services/brevo.service.ts index 6db32331a0..edb762cc70 100644 --- a/server/src/services/brevo.service.ts +++ b/server/src/services/brevo.service.ts @@ -10,45 +10,36 @@ apiKey.apiKey = config.smtp.brevoApiKey const apiInstance = new SibApiV3Sdk.WebhooksApi() -let applicationStatusWebhook = new SibApiV3Sdk.CreateWebhook() +let emailWebhook = new SibApiV3Sdk.CreateWebhook() export const enum BrevoEventStatus { HARD_BOUNCE = "hard_bounce", } -applicationStatusWebhook = { - description: "Changements d'états des emails de candidatures", - url: `${config.publicUrl}/api/application/webhook?apikey=${config.smtp.brevoWebhookApiKey}`, - events: ["hardBounce"], - type: "transactional", -} - -let campaignHarbounceWebhook = new SibApiV3Sdk.CreateWebhook() - -campaignHarbounceWebhook = { - description: "Traitement des harbounces des emails des campagnes", - url: `${config.publicUrl}/api/campaign/webhook?apikey=${config.smtp.brevoWebhookApiKey}`, - events: ["hardBounce"], - type: "marketing", +emailWebhook = { + description: "Changements d'états des emails de candidatures ou de rendez-vous ou de marketing", + // url: `${config.publicUrl}/api/email/webhook?apikey=${config.smtp.brevoWebhookApiKey}`, + url: `https://labonnealternance-recette.apprentissage.beta.gouv.fr/api/emails/webhook?apikey=${config.smtp.brevoWebhookApiKey}`, + events: ["hardBounce", "delivered", "request", "click", "uniqueOpened"], } /** * Initialise les webhooks Brevo au démarrage du docker server. Echoue sans conséquences s'ils existent déjà */ export const initBrevoWebhooks = () => { - if (config.env !== "production") { - return - } + // if (config.env !== "production") { + // return + // } - apiInstance.createWebhook(applicationStatusWebhook).then( + apiInstance.createWebhook({ ...emailWebhook, type: "transactional" }).then( function (data) { - logger.info("Brevo webhook API called successfully for application email status changes. Returned data: " + JSON.stringify(data)) + logger.info("Brevo webhook API called successfully for email (appointment, application) status changes. Returned data: " + JSON.stringify(data)) }, function (error) { - logger.error("Brevo webhook API Error for application email status changes. Returned data: " + error.response.res.text) + logger.error("Brevo webhook API Error for email (appointment, application) status changes. Returned data: " + error.response.res.text) } ) - apiInstance.createWebhook(campaignHarbounceWebhook).then( + apiInstance.createWebhook({ ...emailWebhook, events: ["hardBounce"], type: "marketing" }).then( function (data) { logger.info("Brevo webhook API called successfully for campaign hardbounce detection. Returned data: " + JSON.stringify(data)) }, diff --git a/shared/routes/application.routes.ts b/shared/routes/application.routes.ts index a3c1a8e151..fa941f35da 100644 --- a/shared/routes/application.routes.ts +++ b/shared/routes/application.routes.ts @@ -1,4 +1,3 @@ -import { extensions } from "../helpers/zodHelpers/zodPrimitives" import { z } from "../helpers/zodWithOpenApi" import { ZLbacError } from "../models" import { ZApplicationUI } from "../models/applications.model" @@ -85,23 +84,5 @@ export const zApplicationRoutes = { }, securityScheme: null, }, - "/application/webhook": { - path: "/application/webhook", - method: "post", - querystring: z - .object({ - apikey: z.string(), - }) - .strict(), - body: extensions.brevoWebhook(), - response: { - "200": z - .object({ - result: z.literal("ok"), - }) - .strict(), - }, - securityScheme: null, - }, }, } as const satisfies IRoutesDef diff --git a/shared/routes/campaignWebhook.routes.ts b/shared/routes/campaignWebhook.routes.ts deleted file mode 100644 index 354a177116..0000000000 --- a/shared/routes/campaignWebhook.routes.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { extensions } from "../helpers/zodHelpers/zodPrimitives" -import { z } from "../helpers/zodWithOpenApi" - -import { IRoutesDef } from "./common.routes" - -export const zCampaignWebhookRoutes = { - post: { - "/campaign/webhook": { - method: "post", - path: "/campaign/webhook", - querystring: z - .object({ - apikey: z.string(), - }) - .strict(), - body: extensions.brevoWebhook(), - response: { - "200": z - .object({ - result: z.literal("ok"), - }) - .strict(), - }, - securityScheme: null, - }, - }, -} as const satisfies IRoutesDef diff --git a/shared/routes/emails.routes.ts b/shared/routes/emails.routes.ts index 44ddf1b25f..0aee5e6beb 100644 --- a/shared/routes/emails.routes.ts +++ b/shared/routes/emails.routes.ts @@ -4,7 +4,6 @@ import { z } from "../helpers/zodWithOpenApi" import { IRoutesDef } from "./common.routes" export const zEmailsRoutes = { - // TODO_SECURITY_FIX à ajouter dans le init . ne faire qu'un seul webhook au lieu de trois post: { "/emails/webhook": { method: "post", @@ -13,7 +12,7 @@ export const zEmailsRoutes = { .object({ apiKey: z.string(), }) - .passthrough(), + .strict(), body: extensions.brevoWebhook(), response: { "200": z.object({}).strict(), diff --git a/shared/routes/index.ts b/shared/routes/index.ts index 50faebf0d6..b655fb8a42 100644 --- a/shared/routes/index.ts +++ b/shared/routes/index.ts @@ -3,7 +3,6 @@ import z, { ZodType } from "zod" import { zApplicationRoutes } from "./application.routes" import { zAppointmentsRoute } from "./appointments.routes" -import { zCampaignWebhookRoutes } from "./campaignWebhook.routes" import { IRouteSchema, IRouteSchemaWrite } from "./common.routes" import { zCoreRoutes } from "./core.routes" import { zEligibleTrainingsForAppointmentRoutes } from "./eligibleTrainingsForAppointment.routes" @@ -77,7 +76,6 @@ const zRoutesPost1 = { const zRoutesPost2 = { ...zFormulaireRoute.post, ...zRecruiterRoutes.post, - ...zCampaignWebhookRoutes.post, } const zRoutesPost3 = { From 2a4a36ffec2f5680b59f07a1ab144ad10c81f226 Mon Sep 17 00:00:00 2001 From: Alan Le Ruyet Date: Wed, 6 Dec 2023 17:09:59 +0100 Subject: [PATCH 02/13] =?UTF-8?q?feat:=20=C3=A0=20tester?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/http/routes/emails.controller.ts | 249 ++++++++++---------- server/src/services/application.service.ts | 21 +- 2 files changed, 128 insertions(+), 142 deletions(-) diff --git a/server/src/http/routes/emails.controller.ts b/server/src/http/routes/emails.controller.ts index 146e1299d1..9615d01f2e 100644 --- a/server/src/http/routes/emails.controller.ts +++ b/server/src/http/routes/emails.controller.ts @@ -5,13 +5,136 @@ import config from "@/config" import { logger } from "../../common/logger" import { Etablissement } from "../../common/model" -import { addEmailToBlacklist, removeEmailFromLbaCompanies } from "../../services/application.service" +import { addEmailToBlacklist, findApplicationByMessageId, removeEmailFromLbaCompanies, updateApplicationStatusFromHardbounce } from "../../services/application.service" import * as appointmentService from "../../services/appointment.service" import { BrevoEventStatus } from "../../services/brevo.service" import dayjs from "../../services/dayjs.service" import * as eligibleTrainingsForAppointmentService from "../../services/eligibleTrainingsForAppointment.service" import { Server } from "../server" +const processWebhookEvent = async (payload) => { + const { date, event, email } = payload + const messageId = payload["message-id"] + + // application + if (event === BrevoEventStatus.HARD_BOUNCE) { + const application = await findApplicationByMessageId({ + messageId, + email, + }) + + if (application) { + await updateApplicationStatusFromHardbounce({ payload, application }) + return + } + } + + const eventDate = dayjs.utc(date).tz("Europe/Paris").toDate() + + // If mail sent from appointment model + const appointment = await appointmentService.findOne({ "to_cfa_mails.message_id": { $regex: messageId } }) + if (appointment) { + const previousEmail = appointment.to_cfa_mails.find((mail) => mail.message_id.includes(messageId)) + + if (!previousEmail) return + + await appointment.update({ + $push: { + to_etablissement_emails: { + campaign: previousEmail.campaign, + status: event, + message_id: previousEmail.message_id, + webhook_status_at: eventDate, + }, + }, + }) + + // Disable eligibleTrainingsForAppointments in case of hard_bounce + if (event === BrevoEventStatus.HARD_BOUNCE) { + const eligibleTrainingsForAppointmentsWithEmail = await eligibleTrainingsForAppointmentService.find({ cfa_recipient_email: appointment.cfa_recipient_email }) + + await Promise.all( + eligibleTrainingsForAppointmentsWithEmail.map(async (eligibleTrainingsForAppointment) => { + await eligibleTrainingsForAppointment.update({ referrers: [] }) + + logger.info('Widget parameters disabled for "hard_bounce" reason', { + eligibleTrainingsForAppointmentId: eligibleTrainingsForAppointment._id, + cfa_recipient_email: appointment.cfa_recipient_email, + }) + }) + ) + await addEmailToBlacklist(appointment.cfa_recipient_email as string, "rdv-transactional") + } + + return + } + + // If mail sent from etablissement model + const [etablissementFound] = await Etablissement.find({ "mailing.message_id": { $regex: messageId } }) + if (etablissementFound) { + const previousEmail = etablissementFound?.to_etablissement_emails?.find((mail) => mail?.message_id?.includes(messageId)) + + await etablissementFound.update({ + $push: { + to_etablissement_emails: { + campaign: previousEmail?.campaign, + status: event, + message_id: previousEmail?.message_id, + webhook_status_at: eventDate, + }, + }, + }) + return + } + + // If mail sent from appointment (to the candidat) + const [appointmentCandidatFound] = await appointmentService.find({ + "to_applicant_mails.message_id": { $regex: messageId }, + }) + if (appointmentCandidatFound) { + const previousEmail = appointmentCandidatFound?.to_applicant_mails?.find((mail) => mail.message_id.includes(messageId)) + + if (!previousEmail) return + + await appointmentCandidatFound.update({ + $push: { + to_applicant_mails: { + campaign: previousEmail.campaign, + status: event, + message_id: previousEmail.message_id, + webhook_status_at: eventDate, + }, + }, + }) + return + } + + // If mail sent from appointment (to the CFA) + const [appointmentCfaFound] = await appointmentService.find({ "to_cfa_mails.message_id": { $regex: messageId } }) + if (appointmentCfaFound && appointmentCfaFound?.to_cfa_mails) { + const previousEmail = appointmentCfaFound.to_cfa_mails.find((mail) => mail.message_id.includes(messageId)) + + if (!previousEmail) { + return + } + + await appointmentCfaFound.update({ + $push: { + to_cfa_mails: { + campaign: previousEmail.campaign, + status: event, + message_id: previousEmail.message_id, + webhook_status_at: eventDate, + }, + }, + }) + return + } + + if (event === BrevoEventStatus.HARD_BOUNCE) { + await Promise.all([addEmailToBlacklist(email, "campaign"), removeEmailFromLbaCompanies(email)]) + } +} /** * Email controllers. */ @@ -31,129 +154,7 @@ export default (server: Server) => { throw Boom.forbidden() } - const { date, event, email } = req.body - - if (event === BrevoEventStatus.HARD_BOUNCE) { - // ???? comment identifier la source du blacklisting - - await Promise.all([addEmailToBlacklist(email, "campaign"), removeEmailFromLbaCompanies(email)]) - } - - // server.post( - // "/application/webhook", - // { - // schema: zRoutes.post["/application/webhook"], - // }, - // async (req, res) => { - // const { apikey } = req.query - // if (apikey !== config.smtp.brevoWebhookApiKey) { - // throw Boom.unauthorized() - // } - - // await updateApplicationStatus({ payload: req.body }) - // return res.status(200).send({ result: "ok" }) - // } - // ) - - const messageId = req.body["message-id"] - const eventDate = dayjs.utc(date).tz("Europe/Paris").toDate() - - const appointment = await appointmentService.findOne({ "to_cfa_mails.message_id": { $regex: messageId } }) - - // If mail sent from appointment model - if (appointment) { - const previousEmail = appointment.to_cfa_mails.find((mail) => mail.message_id.includes(messageId)) - - if (!previousEmail) return - - await appointment.update({ - $push: { - to_etablissement_emails: { - campaign: previousEmail.campaign, - status: event, - message_id: previousEmail.message_id, - webhook_status_at: eventDate, - }, - }, - }) - - // Disable eligibleTrainingsForAppointments in case of hard_bounce - if (event === BrevoEventStatus.HARD_BOUNCE) { - const eligibleTrainingsForAppointmentsWithEmail = await eligibleTrainingsForAppointmentService.find({ cfa_recipient_email: appointment.cfa_recipient_email }) - - await Promise.all( - eligibleTrainingsForAppointmentsWithEmail.map(async (eligibleTrainingsForAppointment) => { - await eligibleTrainingsForAppointment.update({ referrers: [] }) - - logger.info('Widget parameters disabled for "hard_bounce" reason', { - eligibleTrainingsForAppointmentId: eligibleTrainingsForAppointment._id, - cfa_recipient_email: appointment.cfa_recipient_email, - }) - }) - ) - await addEmailToBlacklist(appointment.cfa_recipient_email as string, "rdv-transactional") - } - } - - const [etablissementFound] = await Etablissement.find({ "mailing.message_id": { $regex: messageId } }) - - // If mail sent from etablissement model - if (etablissementFound) { - const previousEmail = etablissementFound?.to_etablissement_emails?.find((mail) => mail?.message_id?.includes(messageId)) - - await etablissementFound.update({ - $push: { - to_etablissement_emails: { - campaign: previousEmail?.campaign, - status: event, - message_id: previousEmail?.message_id, - webhook_status_at: eventDate, - }, - }, - }) - } - - const [appointmentCandidatFound] = await appointmentService.find({ - "to_applicant_mails.message_id": { $regex: messageId }, - }) - - // If mail sent from appointment (to the candidat) - if (appointmentCandidatFound) { - const previousEmail = appointmentCandidatFound?.to_applicant_mails?.find((mail) => mail.message_id.includes(messageId)) - - if (!previousEmail) return - - await appointmentCandidatFound.update({ - $push: { - to_applicant_mails: { - campaign: previousEmail.campaign, - status: event, - message_id: previousEmail.message_id, - webhook_status_at: eventDate, - }, - }, - }) - } - - const [appointmentCfaFound] = await appointmentService.find({ "to_cfa_mails.message_id": { $regex: messageId } }) - - // If mail sent from appointment (to the CFA) - if (appointmentCfaFound && appointmentCfaFound?.to_cfa_mails) { - const previousEmail = appointmentCfaFound.to_cfa_mails.find((mail) => mail.message_id.includes(messageId)) - - if (!previousEmail) return - - await appointmentCfaFound.update({ - $push: { - to_cfa_mails: { - campaign: previousEmail.campaign, - status: event, - message_id: previousEmail.message_id, - webhook_status_at: eventDate, - }, - }, - }) - } + await processWebhookEvent(req.body) return res.status(200).send({}) } diff --git a/server/src/services/application.service.ts b/server/src/services/application.service.ts index a9ded2a57c..4708d77d75 100644 --- a/server/src/services/application.service.ts +++ b/server/src/services/application.service.ts @@ -15,7 +15,6 @@ import { prepareMessageForMail } from "../common/utils/fileUtils.js" import { sentryCaptureException } from "../common/utils/sentryUtils.js" import config from "../config.js" -import { BrevoEventStatus } from "./brevo.service.js" import { scan } from "./clamav.service" import { getOffreAvecInfoMandataire } from "./formulaire.service" import mailer from "./mailer.service.js" @@ -86,7 +85,7 @@ export const addEmailToBlacklist = async (email: string, blacklistingOrigin: str * @param {string} email * @returns {Promise} */ -const findApplicationByMessageId = async ({ messageId, email }: { messageId: string; email: string }) => +export const findApplicationByMessageId = async ({ messageId, email }: { messageId: string; email: string }) => Application.findOne({ company_email: email, to_company_message_id: messageId }) /** @@ -552,7 +551,7 @@ export const sendNotificationToApplicant = async ({ /** * @description updates application and triggers action from email webhook */ -export const updateApplicationStatus = async ({ payload }: { payload: any }): Promise => { +export const updateApplicationStatusFromHardbounce = async ({ payload, application }: { payload: any; application: IApplication }): Promise => { /* Format payload cf. https://developers.brevo.com/docs/how-to-use-webhooks { "event": "delivered", @@ -572,27 +571,13 @@ export const updateApplicationStatus = async ({ payload }: { payload: any }): Pr } */ - const { event, subject, email } = payload - - if (event !== BrevoEventStatus.HARD_BOUNCE) { - return - } + const { subject, email } = payload if (!subject.startsWith("Candidature en alternance") && !subject.startsWith("Candidature spontanée")) { // les messages qui ne sont pas de candidature vers une entreprise sont ignorés return } - const application = await findApplicationByMessageId({ - messageId: payload["message-id"], - email, - }) - - if (!application) { - logger.error(`Application webhook : application not found. message_id=${payload["message-id"]} email=${email} subject=${subject}`) - return - } - await addEmailToBlacklist(email, application.job_origin ?? "unknown") if (application.job_origin === "lba") { From 9fb80449454aaf88a0e71bf8f195da6e0b261369 Mon Sep 17 00:00:00 2001 From: Alan Le Ruyet Date: Wed, 6 Dec 2023 17:21:22 +0100 Subject: [PATCH 03/13] feat: remove dev comments --- server/src/services/brevo.service.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/src/services/brevo.service.ts b/server/src/services/brevo.service.ts index edb762cc70..d49bc2f819 100644 --- a/server/src/services/brevo.service.ts +++ b/server/src/services/brevo.service.ts @@ -27,9 +27,9 @@ emailWebhook = { * Initialise les webhooks Brevo au démarrage du docker server. Echoue sans conséquences s'ils existent déjà */ export const initBrevoWebhooks = () => { - // if (config.env !== "production") { - // return - // } + if (config.env !== "production") { + return + } apiInstance.createWebhook({ ...emailWebhook, type: "transactional" }).then( function (data) { From 1c5ba4f1b37b757dc7d79095429fcf98086053bd Mon Sep 17 00:00:00 2001 From: Alan Le Ruyet Date: Thu, 7 Dec 2023 17:14:42 +0100 Subject: [PATCH 04/13] feat: split en fonction dans chaque service --- server/src/http/routes/emails.controller.ts | 135 ++----------------- server/src/services/application.service.ts | 28 ++++ server/src/services/appointment.service.ts | 112 +++++++++++++++ server/src/services/etablissement.service.ts | 27 ++++ 4 files changed, 180 insertions(+), 122 deletions(-) diff --git a/server/src/http/routes/emails.controller.ts b/server/src/http/routes/emails.controller.ts index 9615d01f2e..9217b10245 100644 --- a/server/src/http/routes/emails.controller.ts +++ b/server/src/http/routes/emails.controller.ts @@ -2,138 +2,29 @@ import Boom from "boom" import { zRoutes } from "shared/index" import config from "@/config" +import { processEstablishmentEvent } from "@/services/etablissement.service" -import { logger } from "../../common/logger" -import { Etablissement } from "../../common/model" -import { addEmailToBlacklist, findApplicationByMessageId, removeEmailFromLbaCompanies, updateApplicationStatusFromHardbounce } from "../../services/application.service" +import { processApplicationEvent, processHardBounce } from "../../services/application.service" import * as appointmentService from "../../services/appointment.service" -import { BrevoEventStatus } from "../../services/brevo.service" -import dayjs from "../../services/dayjs.service" -import * as eligibleTrainingsForAppointmentService from "../../services/eligibleTrainingsForAppointment.service" import { Server } from "../server" const processWebhookEvent = async (payload) => { - const { date, event, email } = payload - const messageId = payload["message-id"] + let shouldContinue = await processApplicationEvent(payload) + if (!shouldContinue) return - // application - if (event === BrevoEventStatus.HARD_BOUNCE) { - const application = await findApplicationByMessageId({ - messageId, - email, - }) + shouldContinue = await appointmentService.processAppointmentWebhookEvent(payload) + if (!shouldContinue) return - if (application) { - await updateApplicationStatusFromHardbounce({ payload, application }) - return - } - } - - const eventDate = dayjs.utc(date).tz("Europe/Paris").toDate() - - // If mail sent from appointment model - const appointment = await appointmentService.findOne({ "to_cfa_mails.message_id": { $regex: messageId } }) - if (appointment) { - const previousEmail = appointment.to_cfa_mails.find((mail) => mail.message_id.includes(messageId)) - - if (!previousEmail) return - - await appointment.update({ - $push: { - to_etablissement_emails: { - campaign: previousEmail.campaign, - status: event, - message_id: previousEmail.message_id, - webhook_status_at: eventDate, - }, - }, - }) - - // Disable eligibleTrainingsForAppointments in case of hard_bounce - if (event === BrevoEventStatus.HARD_BOUNCE) { - const eligibleTrainingsForAppointmentsWithEmail = await eligibleTrainingsForAppointmentService.find({ cfa_recipient_email: appointment.cfa_recipient_email }) - - await Promise.all( - eligibleTrainingsForAppointmentsWithEmail.map(async (eligibleTrainingsForAppointment) => { - await eligibleTrainingsForAppointment.update({ referrers: [] }) - - logger.info('Widget parameters disabled for "hard_bounce" reason', { - eligibleTrainingsForAppointmentId: eligibleTrainingsForAppointment._id, - cfa_recipient_email: appointment.cfa_recipient_email, - }) - }) - ) - await addEmailToBlacklist(appointment.cfa_recipient_email as string, "rdv-transactional") - } + shouldContinue = await processEstablishmentEvent(payload) + if (!shouldContinue) return - return - } - - // If mail sent from etablissement model - const [etablissementFound] = await Etablissement.find({ "mailing.message_id": { $regex: messageId } }) - if (etablissementFound) { - const previousEmail = etablissementFound?.to_etablissement_emails?.find((mail) => mail?.message_id?.includes(messageId)) - - await etablissementFound.update({ - $push: { - to_etablissement_emails: { - campaign: previousEmail?.campaign, - status: event, - message_id: previousEmail?.message_id, - webhook_status_at: eventDate, - }, - }, - }) - return - } - - // If mail sent from appointment (to the candidat) - const [appointmentCandidatFound] = await appointmentService.find({ - "to_applicant_mails.message_id": { $regex: messageId }, - }) - if (appointmentCandidatFound) { - const previousEmail = appointmentCandidatFound?.to_applicant_mails?.find((mail) => mail.message_id.includes(messageId)) - - if (!previousEmail) return - - await appointmentCandidatFound.update({ - $push: { - to_applicant_mails: { - campaign: previousEmail.campaign, - status: event, - message_id: previousEmail.message_id, - webhook_status_at: eventDate, - }, - }, - }) - return - } - - // If mail sent from appointment (to the CFA) - const [appointmentCfaFound] = await appointmentService.find({ "to_cfa_mails.message_id": { $regex: messageId } }) - if (appointmentCfaFound && appointmentCfaFound?.to_cfa_mails) { - const previousEmail = appointmentCfaFound.to_cfa_mails.find((mail) => mail.message_id.includes(messageId)) - - if (!previousEmail) { - return - } + shouldContinue = await appointmentService.processAppointmentToApplicantWebhookEvent(payload) + if (!shouldContinue) return - await appointmentCfaFound.update({ - $push: { - to_cfa_mails: { - campaign: previousEmail.campaign, - status: event, - message_id: previousEmail.message_id, - webhook_status_at: eventDate, - }, - }, - }) - return - } + shouldContinue = await appointmentService.processAppointmentToCfaWebhookEvent(payload) + if (!shouldContinue) return - if (event === BrevoEventStatus.HARD_BOUNCE) { - await Promise.all([addEmailToBlacklist(email, "campaign"), removeEmailFromLbaCompanies(email)]) - } + await processHardBounce(payload) } /** * Email controllers. diff --git a/server/src/services/application.service.ts b/server/src/services/application.service.ts index 4708d77d75..9e45ca787e 100644 --- a/server/src/services/application.service.ts +++ b/server/src/services/application.service.ts @@ -15,6 +15,7 @@ import { prepareMessageForMail } from "../common/utils/fileUtils.js" import { sentryCaptureException } from "../common/utils/sentryUtils.js" import config from "../config.js" +import { BrevoEventStatus } from "./brevo.service.js" import { scan } from "./clamav.service" import { getOffreAvecInfoMandataire } from "./formulaire.service" import mailer from "./mailer.service.js" @@ -663,3 +664,30 @@ export const getApplicationByCompanyCount = async (sirets: ILbaCompany["siret"][ return applicationCountByCompany } + +export const processApplicationEvent = async (payload) => { + const { event, email } = payload + const messageId = payload["message-id"] + + // application + if (event === BrevoEventStatus.HARD_BOUNCE) { + const application = await findApplicationByMessageId({ + messageId, + email, + }) + + if (application) { + await updateApplicationStatusFromHardbounce({ payload, application }) + return false + } + } + return true +} + +export const processHardBounce = async (payload) => { + const { event, email } = payload + + if (event === BrevoEventStatus.HARD_BOUNCE) { + await Promise.all([addEmailToBlacklist(email, "campaign"), removeEmailFromLbaCompanies(email)]) + } +} diff --git a/server/src/services/appointment.service.ts b/server/src/services/appointment.service.ts index 383fab2544..a710ea7149 100644 --- a/server/src/services/appointment.service.ts +++ b/server/src/services/appointment.service.ts @@ -2,6 +2,7 @@ import dayjs from "dayjs" import type { FilterQuery, ObjectId } from "mongoose" import { IAppointment, IEligibleTrainingsForAppointment, IEtablissement, IUser } from "shared" +import { logger } from "@/common/logger" import { mailType } from "@/common/model/constants/appointments" import { ReferrerObject } from "@/common/model/constants/referrers" import { getStaticFilePath } from "@/common/utils/getStaticFilePath" @@ -9,7 +10,10 @@ import config from "@/config" import { Appointment } from "../common/model/index" +import { addEmailToBlacklist } from "./application.service" import { createRdvaAppointmentIdPageLink } from "./appLinks.service" +import { BrevoEventStatus } from "./brevo.service" +import * as eligibleTrainingsForAppointmentService from "./eligibleTrainingsForAppointment.service" import mailer from "./mailer.service" export type NewAppointment = Pick< @@ -143,3 +147,111 @@ export const sendFormateurAppointmentEmail = async ( } ) } + +export const processAppointmentToApplicantWebhookEvent = async (payload) => { + const { date, event } = payload + const messageId = payload["message-id"] + + const eventDate = dayjs.utc(date).tz("Europe/Paris").toDate() + + // If mail sent from appointment (to the candidat) + const [appointmentCandidatFound] = await find({ + "to_applicant_mails.message_id": { $regex: messageId }, + }) + if (appointmentCandidatFound) { + const previousEmail = appointmentCandidatFound?.to_applicant_mails?.find((mail) => mail.message_id.includes(messageId)) + + if (!previousEmail) return false + + await appointmentCandidatFound.update({ + $push: { + to_applicant_mails: { + campaign: previousEmail.campaign, + status: event, + message_id: previousEmail.message_id, + webhook_status_at: eventDate, + }, + }, + }) + return false + } + return true +} + +export const processAppointmentWebhookEvent = async (payload) => { + const { date, event } = payload + const messageId = payload["message-id"] + + const eventDate = dayjs.utc(date).tz("Europe/Paris").toDate() + + // If mail sent from appointment model + const appointment = await findOne({ "to_cfa_mails.message_id": { $regex: messageId } }) + if (appointment) { + const previousEmail = appointment.to_cfa_mails.find((mail) => mail.message_id.includes(messageId)) + + if (!previousEmail) { + return false + } + + await appointment.update({ + $push: { + to_etablissement_emails: { + campaign: previousEmail.campaign, + status: event, + message_id: previousEmail.message_id, + webhook_status_at: eventDate, + }, + }, + }) + + // Disable eligibleTrainingsForAppointments in case of hard_bounce + if (event === BrevoEventStatus.HARD_BOUNCE) { + const eligibleTrainingsForAppointmentsWithEmail = await eligibleTrainingsForAppointmentService.find({ cfa_recipient_email: appointment.cfa_recipient_email }) + + await Promise.all( + eligibleTrainingsForAppointmentsWithEmail.map(async (eligibleTrainingsForAppointment) => { + await eligibleTrainingsForAppointment.update({ referrers: [] }) + + logger.info('Widget parameters disabled for "hard_bounce" reason', { + eligibleTrainingsForAppointmentId: eligibleTrainingsForAppointment._id, + cfa_recipient_email: appointment.cfa_recipient_email, + }) + }) + ) + await addEmailToBlacklist(appointment.cfa_recipient_email as string, "rdv-transactional") + } + + return false + } + return true +} + +export const processAppointmentToCfaWebhookEvent = async (payload) => { + const { date, event } = payload + const messageId = payload["message-id"] + + const eventDate = dayjs.utc(date).tz("Europe/Paris").toDate() + + // If mail sent from appointment (to the CFA) + const [appointmentCfaFound] = await find({ "to_cfa_mails.message_id": { $regex: messageId } }) + if (appointmentCfaFound && appointmentCfaFound?.to_cfa_mails) { + const previousEmail = appointmentCfaFound.to_cfa_mails.find((mail) => mail.message_id.includes(messageId)) + + if (!previousEmail) { + return false + } + + await appointmentCfaFound.update({ + $push: { + to_cfa_mails: { + campaign: previousEmail.campaign, + status: event, + message_id: previousEmail.message_id, + webhook_status_at: eventDate, + }, + }, + }) + return false + } + return true +} diff --git a/server/src/services/etablissement.service.ts b/server/src/services/etablissement.service.ts index 1f8c08144e..6efd576e25 100644 --- a/server/src/services/etablissement.service.ts +++ b/server/src/services/etablissement.service.ts @@ -840,3 +840,30 @@ export const sendEmailConfirmationEntreprise = async (user: IUserRecruteur, recr await sendUserConfirmationEmail(user) } } + +export const processEstablishmentEvent = async (payload) => { + const { date, event } = payload + const messageId = payload["message-id"] + + const eventDate = dayjs.utc(date).tz("Europe/Paris").toDate() + + // If mail sent from etablissement model + const [etablissementFound] = await Etablissement.find({ "mailing.message_id": { $regex: messageId } }) + if (etablissementFound) { + const previousEmail = etablissementFound?.to_etablissement_emails?.find((mail) => mail?.message_id?.includes(messageId)) + + await etablissementFound.update({ + $push: { + to_etablissement_emails: { + campaign: previousEmail?.campaign, + status: event, + message_id: previousEmail?.message_id, + webhook_status_at: eventDate, + }, + }, + }) + return false + } + + return true +} From 1e024f1714bd01823692c9ca637641c415a2dc19 Mon Sep 17 00:00:00 2001 From: Alan Le Ruyet Date: Fri, 8 Dec 2023 09:57:37 +0100 Subject: [PATCH 05/13] feat: rename function --- server/src/http/routes/emails.controller.ts | 6 +++--- server/src/services/application.service.ts | 10 ++++++++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/server/src/http/routes/emails.controller.ts b/server/src/http/routes/emails.controller.ts index 9217b10245..be67aff2f9 100644 --- a/server/src/http/routes/emails.controller.ts +++ b/server/src/http/routes/emails.controller.ts @@ -4,12 +4,12 @@ import { zRoutes } from "shared/index" import config from "@/config" import { processEstablishmentEvent } from "@/services/etablissement.service" -import { processApplicationEvent, processHardBounce } from "../../services/application.service" +import { processApplicationWebhookEvent, processHardBounceWebhookEvent } from "../../services/application.service" import * as appointmentService from "../../services/appointment.service" import { Server } from "../server" const processWebhookEvent = async (payload) => { - let shouldContinue = await processApplicationEvent(payload) + let shouldContinue = await processApplicationWebhookEvent(payload) if (!shouldContinue) return shouldContinue = await appointmentService.processAppointmentWebhookEvent(payload) @@ -24,7 +24,7 @@ const processWebhookEvent = async (payload) => { shouldContinue = await appointmentService.processAppointmentToCfaWebhookEvent(payload) if (!shouldContinue) return - await processHardBounce(payload) + await processHardBounceWebhookEvent(payload) } /** * Email controllers. diff --git a/server/src/services/application.service.ts b/server/src/services/application.service.ts index 9e45ca787e..6277672354 100644 --- a/server/src/services/application.service.ts +++ b/server/src/services/application.service.ts @@ -665,7 +665,10 @@ export const getApplicationByCompanyCount = async (sirets: ILbaCompany["siret"][ return applicationCountByCompany } -export const processApplicationEvent = async (payload) => { +/** + * met à jour une candidature si l'événement reçu correspond à une hardbounce + */ +export const processApplicationWebhookEvent = async (payload) => { const { event, email } = payload const messageId = payload["message-id"] @@ -684,7 +687,10 @@ export const processApplicationEvent = async (payload) => { return true } -export const processHardBounce = async (payload) => { +/** + * réagit à un hardbounce non lié à aux autres processeurs de webhook email + */ +export const processHardBounceWebhookEvent = async (payload) => { const { event, email } = payload if (event === BrevoEventStatus.HARD_BOUNCE) { From eb957777336b4bc743669ff2a960743d5a2fbd2d Mon Sep 17 00:00:00 2001 From: Alan Le Ruyet Date: Fri, 8 Dec 2023 10:28:26 +0100 Subject: [PATCH 06/13] feat: renaming here too --- server/src/http/routes/emails.controller.ts | 4 ++-- server/src/services/etablissement.service.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server/src/http/routes/emails.controller.ts b/server/src/http/routes/emails.controller.ts index be67aff2f9..2a8110d67c 100644 --- a/server/src/http/routes/emails.controller.ts +++ b/server/src/http/routes/emails.controller.ts @@ -2,7 +2,7 @@ import Boom from "boom" import { zRoutes } from "shared/index" import config from "@/config" -import { processEstablishmentEvent } from "@/services/etablissement.service" +import { processEstablishmentWebhookEvent } from "@/services/etablissement.service" import { processApplicationWebhookEvent, processHardBounceWebhookEvent } from "../../services/application.service" import * as appointmentService from "../../services/appointment.service" @@ -15,7 +15,7 @@ const processWebhookEvent = async (payload) => { shouldContinue = await appointmentService.processAppointmentWebhookEvent(payload) if (!shouldContinue) return - shouldContinue = await processEstablishmentEvent(payload) + shouldContinue = await processEstablishmentWebhookEvent(payload) if (!shouldContinue) return shouldContinue = await appointmentService.processAppointmentToApplicantWebhookEvent(payload) diff --git a/server/src/services/etablissement.service.ts b/server/src/services/etablissement.service.ts index 6efd576e25..28d4daf0d3 100644 --- a/server/src/services/etablissement.service.ts +++ b/server/src/services/etablissement.service.ts @@ -841,7 +841,7 @@ export const sendEmailConfirmationEntreprise = async (user: IUserRecruteur, recr } } -export const processEstablishmentEvent = async (payload) => { +export const processEstablishmentWebhookEvent = async (payload) => { const { date, event } = payload const messageId = payload["message-id"] From 8f95d0123747dd1eddf4a4e9d6a4eca437bfa8da Mon Sep 17 00:00:00 2001 From: Alan Le Ruyet Date: Fri, 8 Dec 2023 15:13:30 +0100 Subject: [PATCH 07/13] feat: simplification / factorisation / correction --- server/src/http/routes/emails.controller.ts | 7 +-- server/src/services/appointment.service.ts | 62 ++++---------------- server/src/services/etablissement.service.ts | 11 ++-- 3 files changed, 21 insertions(+), 59 deletions(-) diff --git a/server/src/http/routes/emails.controller.ts b/server/src/http/routes/emails.controller.ts index 2a8110d67c..95430df0a1 100644 --- a/server/src/http/routes/emails.controller.ts +++ b/server/src/http/routes/emails.controller.ts @@ -12,16 +12,13 @@ const processWebhookEvent = async (payload) => { let shouldContinue = await processApplicationWebhookEvent(payload) if (!shouldContinue) return - shouldContinue = await appointmentService.processAppointmentWebhookEvent(payload) - if (!shouldContinue) return - - shouldContinue = await processEstablishmentWebhookEvent(payload) + shouldContinue = await appointmentService.processAppointmentToCfaWebhookEvent(payload) if (!shouldContinue) return shouldContinue = await appointmentService.processAppointmentToApplicantWebhookEvent(payload) if (!shouldContinue) return - shouldContinue = await appointmentService.processAppointmentToCfaWebhookEvent(payload) + shouldContinue = await processEstablishmentWebhookEvent(payload) if (!shouldContinue) return await processHardBounceWebhookEvent(payload) diff --git a/server/src/services/appointment.service.ts b/server/src/services/appointment.service.ts index a710ea7149..a4eae01aff 100644 --- a/server/src/services/appointment.service.ts +++ b/server/src/services/appointment.service.ts @@ -155,20 +155,19 @@ export const processAppointmentToApplicantWebhookEvent = async (payload) => { const eventDate = dayjs.utc(date).tz("Europe/Paris").toDate() // If mail sent from appointment (to the candidat) - const [appointmentCandidatFound] = await find({ - "to_applicant_mails.message_id": { $regex: messageId }, + const appointmentCandidatFound = await findOne({ + "to_applicant_mails.message_id": messageId, }) - if (appointmentCandidatFound) { - const previousEmail = appointmentCandidatFound?.to_applicant_mails?.find((mail) => mail.message_id.includes(messageId)) - - if (!previousEmail) return false + if (appointmentCandidatFound && appointmentCandidatFound.to_applicant_mails?.length) { + // deuxième condition ci-dessus utile uniquement pour typescript car to_applicant_mails peut être null selon le typage + const firstEmailEvent = appointmentCandidatFound.to_applicant_mails[0] await appointmentCandidatFound.update({ $push: { to_applicant_mails: { - campaign: previousEmail.campaign, + campaign: firstEmailEvent.campaign, status: event, - message_id: previousEmail.message_id, + message_id: firstEmailEvent.message_id, webhook_status_at: eventDate, }, }, @@ -178,27 +177,22 @@ export const processAppointmentToApplicantWebhookEvent = async (payload) => { return true } -export const processAppointmentWebhookEvent = async (payload) => { +export const processAppointmentToCfaWebhookEvent = async (payload) => { const { date, event } = payload const messageId = payload["message-id"] - const eventDate = dayjs.utc(date).tz("Europe/Paris").toDate() // If mail sent from appointment model - const appointment = await findOne({ "to_cfa_mails.message_id": { $regex: messageId } }) + const appointment = await findOne({ "to_cfa_mails.message_id": messageId }) if (appointment) { - const previousEmail = appointment.to_cfa_mails.find((mail) => mail.message_id.includes(messageId)) - - if (!previousEmail) { - return false - } + const firstEmailEvent = appointment.to_cfa_mails[0] await appointment.update({ $push: { to_etablissement_emails: { - campaign: previousEmail.campaign, + campaign: firstEmailEvent.campaign, status: event, - message_id: previousEmail.message_id, + message_id: firstEmailEvent.message_id, webhook_status_at: eventDate, }, }, @@ -218,39 +212,9 @@ export const processAppointmentWebhookEvent = async (payload) => { }) }) ) - await addEmailToBlacklist(appointment.cfa_recipient_email as string, "rdv-transactional") - } - - return false - } - return true -} - -export const processAppointmentToCfaWebhookEvent = async (payload) => { - const { date, event } = payload - const messageId = payload["message-id"] - - const eventDate = dayjs.utc(date).tz("Europe/Paris").toDate() - - // If mail sent from appointment (to the CFA) - const [appointmentCfaFound] = await find({ "to_cfa_mails.message_id": { $regex: messageId } }) - if (appointmentCfaFound && appointmentCfaFound?.to_cfa_mails) { - const previousEmail = appointmentCfaFound.to_cfa_mails.find((mail) => mail.message_id.includes(messageId)) - - if (!previousEmail) { - return false + await addEmailToBlacklist(appointment.cfa_recipient_email, "rdv-transactional") } - await appointmentCfaFound.update({ - $push: { - to_cfa_mails: { - campaign: previousEmail.campaign, - status: event, - message_id: previousEmail.message_id, - webhook_status_at: eventDate, - }, - }, - }) return false } return true diff --git a/server/src/services/etablissement.service.ts b/server/src/services/etablissement.service.ts index 28d4daf0d3..6c25c58ef7 100644 --- a/server/src/services/etablissement.service.ts +++ b/server/src/services/etablissement.service.ts @@ -848,16 +848,17 @@ export const processEstablishmentWebhookEvent = async (payload) => { const eventDate = dayjs.utc(date).tz("Europe/Paris").toDate() // If mail sent from etablissement model - const [etablissementFound] = await Etablissement.find({ "mailing.message_id": { $regex: messageId } }) - if (etablissementFound) { - const previousEmail = etablissementFound?.to_etablissement_emails?.find((mail) => mail?.message_id?.includes(messageId)) + const etablissementFound = await Etablissement.findOne({ "to_etablissement_emails.message_id": messageId }) + if (etablissementFound && etablissementFound.to_etablissement_emails?.length) { + const firstEmailEvent = etablissementFound.to_etablissement_emails[0] + // adding one entry to Etablissement email message logs await etablissementFound.update({ $push: { to_etablissement_emails: { - campaign: previousEmail?.campaign, + campaign: firstEmailEvent.campaign, status: event, - message_id: previousEmail?.message_id, + message_id: firstEmailEvent.message_id, webhook_status_at: eventDate, }, }, From 033773a3ff19ac88911a5d481c64e9fceea19b3a Mon Sep 17 00:00:00 2001 From: Alan Le Ruyet Date: Mon, 11 Dec 2023 16:31:38 +0100 Subject: [PATCH 08/13] fix: syntax --- server/src/services/brevo.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/services/brevo.service.ts b/server/src/services/brevo.service.ts index d49bc2f819..cf4834901d 100644 --- a/server/src/services/brevo.service.ts +++ b/server/src/services/brevo.service.ts @@ -19,7 +19,7 @@ export const enum BrevoEventStatus { emailWebhook = { description: "Changements d'états des emails de candidatures ou de rendez-vous ou de marketing", // url: `${config.publicUrl}/api/email/webhook?apikey=${config.smtp.brevoWebhookApiKey}`, - url: `https://labonnealternance-recette.apprentissage.beta.gouv.fr/api/emails/webhook?apikey=${config.smtp.brevoWebhookApiKey}`, + url: `https://labonnealternance-recette.apprentissage.beta.gouv.fr/api/emails/webhook?apiKey=${config.smtp.brevoWebhookApiKey}`, events: ["hardBounce", "delivered", "request", "click", "uniqueOpened"], } From ab2fcf27dde362ebf92b3423d0446281cb75dc86 Mon Sep 17 00:00:00 2001 From: Alan Le Ruyet Date: Thu, 14 Dec 2023 16:08:45 +0100 Subject: [PATCH 09/13] =?UTF-8?q?feat:=20ajustements=20mod=C3=A8les=20webh?= =?UTF-8?q?ook=20brevo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shared/helpers/zodHelpers/zodPrimitives.ts | 25 ++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/shared/helpers/zodHelpers/zodPrimitives.ts b/shared/helpers/zodHelpers/zodPrimitives.ts index 3910701293..e00863dd11 100644 --- a/shared/helpers/zodHelpers/zodPrimitives.ts +++ b/shared/helpers/zodHelpers/zodPrimitives.ts @@ -63,15 +63,28 @@ export const extensions = { event: z.string(), email: z.string(), id: z.number(), - date: z.string(), ts: z.number(), - "message-id": z.string(), - ts_event: z.number(), - subject: z.string(), + date: z.string().nullish(), + "message-id": z.string().nullish(), + ts_event: z.number().nullish(), + subject: z.string().nullish(), + "X-Mailin-custom": z.string().nullish(), + sending_ip: z.string().nullish(), + ts_epoch: z.number().nullish(), + template_id: z.number().nullish(), tag: z.string().nullish(), - sending_ip: z.string(), - ts_epoch: z.number(), tags: z.array(z.string()).nullish(), + link: z.string().nullish(), + reason: z.string().nullish(), + date_sent: z.string().nullish(), + date_event: z.string().nullish(), + ts_sent: z.number().nullish(), + camp_id: z.number().nullish(), + campaign_name: z.string().nullish(), + URL: z.string().nullish(), + list_id: z.array(z.number()).nullish(), + key: z.string().nullish(), + content: z.array(z.string()).nullish(), }) .strict(), buildEnum: (enumObject: Record) => { From 0fca8ad64052cd5ad54686909bf052cf1a540def Mon Sep 17 00:00:00 2001 From: Alan Le Ruyet Date: Thu, 14 Dec 2023 16:13:28 +0100 Subject: [PATCH 10/13] feat: reretrait --- .../src/http/routes/application.controller.ts | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/server/src/http/routes/application.controller.ts b/server/src/http/routes/application.controller.ts index 608efc5b11..374d7bb3bf 100644 --- a/server/src/http/routes/application.controller.ts +++ b/server/src/http/routes/application.controller.ts @@ -6,7 +6,7 @@ import config from "@/config" import { Application } from "../../common/model/index" import { sentryCaptureException } from "../../common/utils/sentryUtils" -import { sendMailToApplicant, updateApplicationStatus } from "../../services/application.service" +import { sendMailToApplicant } from "../../services/application.service" import { Server } from "../server" const rateLimitConfig = { @@ -70,20 +70,4 @@ export default function (server: Server) { return res.status(200).send({ result: "ok" }) } ) - - server.post( - "/application/webhook", - { - schema: zRoutes.post["/application/webhook"], - }, - async (req, res) => { - const { apikey } = req.query - if (apikey !== config.smtp.brevoWebhookApiKey) { - throw Boom.unauthorized() - } - - await updateApplicationStatus({ payload: req.body }) - return res.status(200).send({ result: "ok" }) - } - ) } From af41f1d164d6cb36ed1cdf7b75f6c919c8a01f70 Mon Sep 17 00:00:00 2001 From: Alan Le Ruyet Date: Thu, 14 Dec 2023 16:17:30 +0100 Subject: [PATCH 11/13] feat: has been removed --- server/src/http/server.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/server/src/http/server.ts b/server/src/http/server.ts index 5f16a91694..6384f914b5 100644 --- a/server/src/http/server.ts +++ b/server/src/http/server.ts @@ -133,7 +133,6 @@ export async function bind(app: Server) { metiers(typedSubApp) rome(typedSubApp) updateLbaCompany(typedSubApp) - campaignWebhook(typedSubApp) application(typedSubApp) applicationAPI(typedSubApp) unsubscribeLbaCompany(typedSubApp) From 03dce9538885215cac545f82a7313aa231b9a157 Mon Sep 17 00:00:00 2001 From: Alan Le Ruyet Date: Thu, 14 Dec 2023 18:39:45 +0100 Subject: [PATCH 12/13] feat: sending webhook zod error to sentry because we want to know --- server/src/http/middlewares/errorMiddleware.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/http/middlewares/errorMiddleware.ts b/server/src/http/middlewares/errorMiddleware.ts index 356ab52687..847d5cfbd6 100644 --- a/server/src/http/middlewares/errorMiddleware.ts +++ b/server/src/http/middlewares/errorMiddleware.ts @@ -64,7 +64,7 @@ export function errorMiddleware(server: Server) { ...(error.data ? { data: error.data } : {}), } - if (error.output.statusCode >= 500) { + if (error.output.statusCode >= 500 || (_request.url.startsWith("/api/emails/webhook") && error.output.statusCode >= 400)) { server.log.error(rawError) captureException(rawError) } From dfc1648ce4ebf7e03dacaa523da4d8b6f6370f1a Mon Sep 17 00:00:00 2001 From: Alan Le Ruyet Date: Mon, 18 Dec 2023 12:00:28 +0100 Subject: [PATCH 13/13] feat: cleaning --- server/src/services/application.service.ts | 20 ++------------------ server/src/services/brevo.service.ts | 3 +-- 2 files changed, 3 insertions(+), 20 deletions(-) diff --git a/server/src/services/application.service.ts b/server/src/services/application.service.ts index 8473bbe853..595ec67423 100644 --- a/server/src/services/application.service.ts +++ b/server/src/services/application.service.ts @@ -565,24 +565,8 @@ export const sendMailToApplicant = async ({ * @description updates application and triggers action from email webhook */ export const updateApplicationStatusFromHardbounce = async ({ payload, application }: { payload: any; application: IApplication }): Promise => { - /* Format payload cf. https://developers.brevo.com/docs/how-to-use-webhooks - { - "event": "delivered", - "email": "example@example.com", - "id": 26224, - "date": "YYYY-MM-DD HH:mm:ss", - "ts": 1598634509, - "message-id": "", - "ts_event": 1598034509, - "subject": "Subject Line", - "tag": "[\"transactionalTag\"]", - "sending_ip": "185.41.28.109", - "ts_epoch": 1598634509223, - "tags": [ - "myFirstTransactional" - ] - } - */ + /* Format payload cf. https://developers.brevo.com/docs/transactional-webhooks + https://developers.brevo.com/docs/marketing-webhooks */ const { subject, email } = payload diff --git a/server/src/services/brevo.service.ts b/server/src/services/brevo.service.ts index cf4834901d..4951baa125 100644 --- a/server/src/services/brevo.service.ts +++ b/server/src/services/brevo.service.ts @@ -18,8 +18,7 @@ export const enum BrevoEventStatus { emailWebhook = { description: "Changements d'états des emails de candidatures ou de rendez-vous ou de marketing", - // url: `${config.publicUrl}/api/email/webhook?apikey=${config.smtp.brevoWebhookApiKey}`, - url: `https://labonnealternance-recette.apprentissage.beta.gouv.fr/api/emails/webhook?apiKey=${config.smtp.brevoWebhookApiKey}`, + url: `${config.publicUrl}/api/emails/webhook?apiKey=${config.smtp.brevoWebhookApiKey}`, events: ["hardBounce", "delivered", "request", "click", "uniqueOpened"], }