Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: LBAC-1680 uniformisation routes webhook brevo #895

Merged
merged 16 commits into from
Dec 19, 2023
Merged
2 changes: 1 addition & 1 deletion .talismanrc
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ fileignoreconfig:
- filename: server/src/jobs/lba_recruteur/formulaire/misc/removeVersionKeyFromRecruiters.ts
checksum: 3cd111d8c109cfec357bae48af70d0cf5644d02cd2c4b9afc5b8aa07bccbd535
- filename: server/src/security/accessTokenService.ts
checksum: f05cafd17797362fc9bfb53062af2095ead2cbe2fa967fad23bd61b756052004
checksum: 2183e326a88ae3c10193a7033ab5fd421ce576cd1e234c323df247da335f74d7
- filename: server/src/services/application.service.ts
checksum: 935cd8f213565ba7bcc2925fca149aaa6cbe9bb5e393a13ab3525dff6ad17234
- filename: server/tests/integration/http/formationV1.test.ts
Expand Down
2 changes: 1 addition & 1 deletion server/src/http/middlewares/errorMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
18 changes: 1 addition & 17 deletions server/src/http/routes/application.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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" })
}
)
}
138 changes: 0 additions & 138 deletions server/src/http/routes/auth/emails.controller.ts

This file was deleted.

28 changes: 0 additions & 28 deletions server/src/http/routes/campaignWebhook.controller.ts

This file was deleted.

50 changes: 50 additions & 0 deletions server/src/http/routes/emails.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import Boom from "boom"
import { zRoutes } from "shared/index"

import config from "@/config"
import { processEstablishmentWebhookEvent } from "@/services/etablissement.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 processApplicationWebhookEvent(payload)
if (!shouldContinue) return

shouldContinue = await appointmentService.processAppointmentToCfaWebhookEvent(payload)
if (!shouldContinue) return

shouldContinue = await appointmentService.processAppointmentToApplicantWebhookEvent(payload)
if (!shouldContinue) return

shouldContinue = await processEstablishmentWebhookEvent(payload)
if (!shouldContinue) return

await processHardBounceWebhookEvent(payload)
}
/**
* Email controllers.
*/
export default (server: Server) => {
/**
* @description Update email status.
* @method {POST}
* @returns {Promise<void>}
*/
server.post(
"/emails/webhook",
{
schema: zRoutes.post["/emails/webhook"],
},
async (req, res) => {
if (req.query.apiKey !== config.smtp.brevoWebhookApiKey) {
throw Boom.forbidden()
}

await processWebhookEvent(req.body)

return res.status(200).send({})
}
)
}
4 changes: 1 addition & 3 deletions server/src/http/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,9 @@ import formationsRoute from "./routes/admin/formations.controller"
import application from "./routes/application.controller"
import applicationAPI from "./routes/applicationAPI.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"
Expand Down Expand Up @@ -134,7 +133,6 @@ export async function bind(app: Server) {
metiers(typedSubApp)
rome(typedSubApp)
updateLbaCompany(typedSubApp)
campaignWebhook(typedSubApp)
application(typedSubApp)
applicationAPI(typedSubApp)
unsubscribeLbaCompany(typedSubApp)
Expand Down
53 changes: 36 additions & 17 deletions server/src/services/application.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export const addEmailToBlacklist = async (email: string, blacklistingOrigin: str
* @param {string} email
* @returns {Promise<IApplication>}
*/
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 })

/**
Expand Down Expand Up @@ -564,7 +564,7 @@ export const sendMailToApplicant = async ({
/**
* @description updates application and triggers action from email webhook
*/
export const updateApplicationStatus = async ({ payload }: { payload: any }): Promise<void> => {
export const updateApplicationStatusFromHardbounce = async ({ payload, application }: { payload: any; application: IApplication }): Promise<void> => {
/* Format payload cf. https://developers.brevo.com/docs/how-to-use-webhooks
{
"event": "delivered",
Expand All @@ -584,27 +584,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") {
Expand Down Expand Up @@ -690,3 +676,36 @@ export const getApplicationByCompanyCount = async (sirets: ILbaCompany["siret"][

return applicationCountByCompany
}

/**
* 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"]

// application
if (event === BrevoEventStatus.HARD_BOUNCE) {
const application = await findApplicationByMessageId({
messageId,
email,
})

if (application) {
await updateApplicationStatusFromHardbounce({ payload, application })
return false
}
}
return true
}

/**
* 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) {
await Promise.all([addEmailToBlacklist(email, "campaign"), removeEmailFromLbaCompanies(email)])
}
}
Loading