Skip to content

Commit

Permalink
feat(lba-706): mise à jour des templates RDVA (#1759)
Browse files Browse the repository at this point in the history
* feat: template delegations

* fix: mongo query error

* feat: origin lba uniquement en attendant mieux

* feat: template accusé réception candidat prdv ok

* feat: template mail prdv ok

* feat: réponse à candidat ok

* feat: draft new

* feat: modèle hardbounce cfa

* feat: cleaning cannoli pour rf et lba

* feat: mapping job origin

* fix: path

* fix: lien recherche emploi prdv

* fix: ajout images manquantes

* feat: normalisation pj Rf

* feat: normalisation rf et utm training links

* feat: cleaning
  • Loading branch information
alanlr authored and remy-auricoste committed Jan 20, 2025
1 parent 55d5f45 commit 27528cf
Show file tree
Hide file tree
Showing 23 changed files with 1,036 additions and 350 deletions.
10 changes: 10 additions & 0 deletions .talismanrc
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ fileignoreconfig:
checksum: 5fbdf21310d8dfaf259eb39a5ca1e72a53b15493cf3a59ed42c4cb5c8f3a62ab
- filename: server/static/templates/mail-bienvenue.mjml.ejs
checksum: 34eb30b463153df565aa89f8e42f08eb77633b6bfa4673f4d6ef5f40633c6101
- filename: server/static/templates/mail-candidat-confirmation-rdv.mjml.ejs
checksum: 23f500afe4d0984a4680e387e6e67223bdbdc5d954e984f400451b26bac248ce
- filename: server/static/templates/mail-candidat-entretien.mjml.ejs
checksum: 53067858567e2878f5f65da06be9efc6902a691b36299167d54343ab30162e49
- filename: server/static/templates/mail-candidat-nsp.mjml.ejs
Expand All @@ -121,8 +123,16 @@ fileignoreconfig:
checksum: 0b806bca197dba97415902339e84f924cb45dae766cf8d3cfc47d80d2bdd203c
- filename: server/static/templates/mail-candidat-refus.mjml.ejs
checksum: 3fcfa9586df3283a3f50cf6751b735b646d3f3ac64e8e4a8453999e9e5aea881
- filename: server/static/templates/mail-cfa-delegation.mjml.ejs
checksum: 569e084a81ffdfbd3b427a60c0712ec2f1af2031fe120572e55235691e84dc5b
- filename: server/static/templates/mail-cfa-demande-de-contact.mjml.ejs
checksum: 6e15fdba953753e2d4f249c3a289615dea942ac18c0a95241309ab15ad7f60a2
- filename: server/static/templates/mail-cfa-notification-hardbounce-cfa.mjml.ejs
checksum: ad25d72b5287060499c4e44bfe93eacd8dbfb7b40122b912eb9c3cf657f6aed7
- filename: server/static/templates/mail-echec-envoi-candidature.mjml.ejs
checksum: 9055c5495ced85a674770e7fb8a81b3bd84949f9ff8f6e6450989aa3b7cda326
- filename: server/static/templates/mail-reponse-cfa.mjml.ejs
checksum: a5a36d078b7c75e6da58c06c032b52d0f57d89bf5913065bb1aaab098f3e80af
- filename: server/tests/fixture/connectedUser.fixture.ts
checksum: bfbb2f218580ce302d3bfcd9ba7494a46ff0a05ba02ca0f38a801ea8c6bf9be2
- filename: server/tests/integration/http/formationV1.test.ts
Expand Down
4 changes: 4 additions & 0 deletions server/src/http/controllers/appointmentRequest.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,10 @@ export default (server: Server) => {
nom_cfa: eligibleTrainingsForAppointment?.etablissement_formateur_raison_sociale,
cfa_email: eligibleTrainingsForAppointment?.lieu_formation_email,
cfa_phone: formationCatalogue?.num_tel,
images: {
logoLba: `${config.publicUrl}/images/emails/logo_LBA.png?raw=true`,
logoRf: `${config.publicUrl}/images/emails/logo_rf.png?raw=true`,
},
},
})
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export default function (server: Server) {
data: {
images: {
logoLba: `${imagePath}logo_LBA.png`,
logoRF: `${imagePath}logo_rf.png`,
logoRf: `${imagePath}logo_rf.png`,
},
},
})
Expand Down
2 changes: 1 addition & 1 deletion server/src/services/application.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ const PARTNER_NAMES = {
const images: object = {
images: {
logoLba: `${imagePath}logo_LBA.png`,
logoRF: `${imagePath}logo_rf.png`,
logoRf: `${imagePath}logo_rf.png`,
logoGrimp: `${imagePath}logo_grimp.png`,
icoInfo: `${imagePath}icone_info.png`,
icoCandidat: `${imagePath}icone_candidat.png`,
Expand Down
64 changes: 61 additions & 3 deletions server/src/services/appointment.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ import { mailType } from "shared/constants/appointment"
import { ReferrerObject } from "shared/constants/referers"

import { getStaticFilePath } from "@/common/utils/getStaticFilePath"
import { sentryCaptureException } from "@/common/utils/sentryUtils"
import config from "@/config"

import { getDbCollection } from "../common/utils/mongodbUtils"

import { createRdvaAppointmentIdPageLink } from "./appLinks.service"
import mailer, { sanitizeForEmail } from "./mailer.service"
import { getReferrerByKeyName } from "./referrers.service"
import { getLBALink } from "./trainingLinks.service"

const createAppointment = async (params: Omit<IAppointment, "_id" | "created_at">) => {
const appointment: IAppointment = { ...params, _id: new ObjectId(), created_at: new Date() }
Expand All @@ -28,7 +31,12 @@ const findOneAndUpdate = (conditions: Filter<IAppointment>, values) => getDbColl

export { createAppointment, find, findById, findOne, findOneAndUpdate }

const getMailData = (candidate: IUser, appointment: IAppointment, eligibleTrainingsForAppointment: IEligibleTrainingsForAppointment, referrerObj: ReferrerObject) => {
const getMailData = async (candidate: IUser, appointment: IAppointment, eligibleTrainingsForAppointment: IEligibleTrainingsForAppointment, referrerObj: ReferrerObject) => {
const lbaLink = await getLBALink({
id: "0",
cle_ministere_educatif: appointment.cle_ministere_educatif,
utm_data: { utm_source: "lba-brevo-transactionnel", utm_medium: "email", utm_campaign: "lba_candidat-rdva-accuse-envoi_promo-emplois" },
})
const mailData = {
appointmentId: appointment._id,
user: {
Expand All @@ -47,16 +55,20 @@ const getMailData = (candidate: IUser, appointment: IAppointment, eligibleTraini
},
formation: {
intitule: eligibleTrainingsForAppointment.training_intitule_long,
searchLink: lbaLink,
},
appointment: {
reasons: appointment.applicant_reasons,
referrerLink: referrerObj.url,
appointment_origin: sanitizeForEmail(referrerObj.full_name),
created_at: appointment.created_at,
},
images: {
logoLba: `${config.publicUrl}/images/emails/logo_LBA.png?raw=true`,
logoRf: `${config.publicUrl}/images/emails/logo_rf.png?raw=true`,
logoFooter: `${config.publicUrl}/assets/logo-republique-francaise.webp?raw=true`,
peopleLaptop: `${config.publicUrl}/assets/people-laptop.webp?raw=true`,
idee: `${config.publicUrl}/images/emails/icone_idee.png?raw=true`,
},
}
return mailData
Expand All @@ -73,7 +85,7 @@ export const sendCandidateAppointmentEmail = async (
to: candidate.email,
subject: `${subjectPrefix ?? ""}Votre demande de RDV auprès de ${eligibleTrainingsForAppointment.etablissement_formateur_raison_sociale}`,
template: getStaticFilePath("./templates/mail-candidat-confirmation-rdv.mjml.ejs"),
data: getMailData(candidate, appointment, eligibleTrainingsForAppointment, referrerObj),
data: await getMailData(candidate, appointment, eligibleTrainingsForAppointment, referrerObj),
})
await findOneAndUpdate(
{ _id: appointment._id },
Expand Down Expand Up @@ -107,7 +119,7 @@ export const sendFormateurAppointmentEmail = async (
subject: `${subjectPrefix ?? ""} ${referrerObj.full_name} - un candidat a un message pour vous`,
template: getStaticFilePath("./templates/mail-cfa-demande-de-contact.mjml.ejs"),
data: {
...getMailData(candidate, appointment, eligibleTrainingsForAppointment, referrerObj),
...(await getMailData(candidate, appointment, eligibleTrainingsForAppointment, referrerObj)),
link: createRdvaAppointmentIdPageLink(appointment.cfa_recipient_email, etablissement.formateur_siret, etablissement._id.toString(), appointment._id.toString()),
},
})
Expand Down Expand Up @@ -198,11 +210,57 @@ export const isHardbounceEventFromAppointmentApplicant = async (payload) => {
}
return false
}

export const sendCandidateAppointmentHardbounceEmail = async (appointment: IAppointment) => {
const { cle_ministere_educatif, applicant_id } = appointment
const training = await getDbCollection("eligible_trainings_for_appointments").findOne({ cle_ministere_educatif })

if (!training) {
sentryCaptureException(new Error("Elligible training not found while processing appointment hardbounce"), { extra: { cle_ministere_educatif, applicant_id } })
return
}

const formation = await getDbCollection("formationcatalogues").findOne({ cle_ministere_educatif })
if (!formation) {
sentryCaptureException(new Error("Catalogue formation not found while processing appointment hardbounce"), { extra: { cle_ministere_educatif, applicant_id } })
return
}

const user = await getDbCollection("users").findOne({ _id: applicant_id })
if (!user) {
sentryCaptureException(new Error("Applicant not found while processing appointment hardbounce"), { extra: { cle_ministere_educatif, applicant_id } })
return
}

const email = await mailer.sendEmail({
to: user.email,
subject: `Le centre de formation ${training?.etablissement_formateur_raison_sociale} n’a pas reçu votre demande`,
template: getStaticFilePath("./templates/mail-cfa-notification-hardbounce-cfa.mjml.ejs"),
data: { phone: formation.num_tel, ...(await getMailData(user, appointment, training, getReferrerByKeyName(appointment.appointment_origin))) },
})

await findOneAndUpdate(
{ _id: appointment._id },
{
$push: {
to_applicant_mails: {
campaign: mailType.CFA_HARDBOUNCE,
status: null,
message_id: email.messageId,
email_sent_at: dayjs().toDate(),
},
},
}
)
}

export const isHardbounceEventFromAppointmentCfa = async (payload) => {
const messageId = payload["message-id"]

const appointment = await findOne({ "to_cfa_mails.message_id": messageId })

if (appointment) {
await sendCandidateAppointmentHardbounceEmail(appointment)
return true
}
return false
Expand Down
26 changes: 18 additions & 8 deletions server/src/services/formulaire.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import equal from "fast-deep-equal"
import { Filter, ObjectId, UpdateFilter } from "mongodb"
import { IDelegation, IJob, IJobCreate, IJobWithRomeDetail, IRecruiter, IRecruiterWithApplicationCount, IUserRecruteur, JOB_STATUS, removeAccents } from "shared"
import { getDirectJobPath } from "shared/constants/lbaitem"
import { OPCOS_LABEL, RECRUITER_STATUS } from "shared/constants/recruteur"
import { OPCOS_LABEL, RECRUITER_STATUS, RECRUITER_USER_ORIGIN } from "shared/constants/recruteur"
import { EntrepriseStatus, IEntreprise } from "shared/models/entreprise.model"
import { AccessEntityType, AccessStatus } from "shared/models/roleManagement.model"
import { IUserWithAccount } from "shared/models/userWithAccount.model"
Expand All @@ -18,7 +18,7 @@ import { getDbCollection } from "../common/utils/mongodbUtils"
import config from "../config"

import { getUser2ManagingOffer } from "./application.service"
import { createCfaUnsubscribeToken, createViewDelegationLink } from "./appLinks.service"
import { createViewDelegationLink } from "./appLinks.service"
import { getCatalogueFormations } from "./catalogue.service"
import dayjs from "./dayjs.service"
import { sendEmailConfirmationEntreprise } from "./etablissement.service"
Expand Down Expand Up @@ -598,7 +598,7 @@ export const patchJobDelegation = async (id: IJob["_id"], delegations: IJob["del
await getDbCollection("recruiters").findOneAndUpdate(
{ "jobs._id": id },
{
$set: { delegations, "jobs.$.job_update_date": new Date(), updatedAt: new Date() },
$set: { "jobs.$.delegations": delegations, "jobs.$.job_update_date": new Date(), updatedAt: new Date() },
},
{ returnDocument: "after" }
)
Expand Down Expand Up @@ -762,31 +762,41 @@ export const getJobWithRomeDetail = async (id: string | ObjectId): Promise<IJobW
}
}

const getJobOrigin = async (recruiter: IRecruiter) => {
const userWithAccount = await getDbCollection("userswithaccounts").findOne({ _id: new ObjectId(recruiter.managed_by!) })
return (userWithAccount && userWithAccount.origin && RECRUITER_USER_ORIGIN[userWithAccount.origin]) ?? "La bonne alternance"
}

/**
* @description Sends the mail informing the CFA that a company wants the CFA to handle the offer.
*/
export async function sendDelegationMailToCFA(email: string, offre: IJob, recruiter: IRecruiter, siret_code: string) {
const unsubscribeOF = await getDbCollection("unsubscribedofs").findOne({ establishment_siret: siret_code })
if (unsubscribeOF) return
const unsubscribeToken = createCfaUnsubscribeToken(email, siret_code)

const jobOrigin = await getJobOrigin(recruiter)

await mailer.sendEmail({
to: email,
subject: `Une entreprise recrute dans votre domaine`,
template: getStaticFilePath("./templates/mail-cfa-delegation.mjml.ejs"),
data: {
images: {
logoLba: `${config.publicUrl}/images/emails/logo_LBA.png?raw=true`,
logoRf: `${config.publicUrl}/images/emails/logo_rf.png?raw=true`,
},
enterpriseName: recruiter.establishment_raison_sociale,
jobName: offre.rome_appellation_label,
contractType: (offre.job_type ?? []).join(", "),
trainingLevel: offre.job_level_label,
startDate: dayjs(offre.job_start_date).format("DD/MM/YYYY"),
duration: offre.job_duration,
rhythm: offre.job_rythm,
offerButton: createViewDelegationLink(email, recruiter.establishment_id, offre._id.toString(), siret_code),
createAccountButton: `${config.publicUrl}/espace-pro/creation/cfa`,
unsubscribeUrl: `${config.publicUrl}/espace-pro/proposition/formulaire/${recruiter.establishment_id}/offre/${offre._id}/siret/${siret_code}/unsubscribe?token=${unsubscribeToken}`,
jobOrigin,
offerButton:
createViewDelegationLink(email, recruiter.establishment_id, offre._id.toString(), siret_code) +
"&utm_source=lba-brevo-transactionnel&utm_medium=email&utm_campaign=lba_cfa-mer-entreprise_consulter-coord-entreprise",
createAccountButton: `${config.publicUrl}/organisme-de-formation?utm_source=lba-brevo-transactionnel&utm_medium=email&utm_campaign=lba_cfa-mer-entreprise_creer-compte`,
policyUrl: `${config.publicUrl}/politique-de-confidentialite?utm_source=lba-brevo-transactionnel&utm_medium=email&utm_campaign=lba_cfa-mer-entreprise_politique-confidentialite`,
},
})
}
Expand Down
23 changes: 16 additions & 7 deletions server/src/services/trainingLinks.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ interface IWish {
uai_formateur?: string | null
uai_formateur_responsable?: string | null
code_insee?: string | null
utm_data?: {
utm_source: string
utm_medium: string
utm_campaign: string
}
}

interface ILinks {
Expand All @@ -33,7 +38,7 @@ interface ILinks {
lien_lba: string
}

const utmData = { utm_source: "lba", utm_medium: "email", utm_campaign: "promotion-emploi-jeunes-voeux" }
const defaultUtmData = { utm_source: "lba", utm_medium: "email", utm_campaign: "promotion-emploi-jeunes-voeux" }

const buildEmploiUrl = ({ baseUrl = `${config.publicUrl}/recherche-emploi`, params }: { baseUrl?: string; params: Record<string, string | string[]> }) => {
const url = new URL(baseUrl)
Expand Down Expand Up @@ -148,6 +153,8 @@ const getPrdvLink = async (wish: IWish): Promise<string> => {
return ""
}

const utmParams = wish.utm_data ? wish.utm_data : defaultUtmData

const elligibleFormation = await getDbCollection("eligible_trainings_for_appointments").findOne(
{
cle_ministere_educatif: wish.cle_ministere_educatif,
Expand All @@ -163,22 +170,24 @@ const getPrdvLink = async (wish: IWish): Promise<string> => {
if (elligibleFormation) {
return buildEmploiUrl({
baseUrl: `${config.publicUrl}/espace-pro/form`,
params: { referrer: "lba", cleMinistereEducatif: wish.cle_ministere_educatif, ...utmData },
params: { referrer: "lba", cleMinistereEducatif: wish.cle_ministere_educatif, ...utmParams },
})
}

return ""
}

const getLBALink = async (wish: IWish): Promise<string> => {
export const getLBALink = async (wish: IWish): Promise<string> => {
// Try getting formations first
const formations = await getTrainingsFromParameters(wish)

const utmParams = wish.utm_data ? wish.utm_data : defaultUtmData

// Handle single formation case
if (formations?.length === 1) {
const { rome_codes, lieu_formation_geo_coordonnees } = formations[0]
const [latitude, longitude] = lieu_formation_geo_coordonnees!.split(",")
return buildEmploiUrl({ params: { romes: rome_codes as string[], lat: latitude, lon: longitude, radius: "60", ...utmData } })
return buildEmploiUrl({ params: { romes: rome_codes as string[], lat: latitude, lon: longitude, radius: "60", ...utmParams } })
}

// Extract postcode and get coordinates if available
Expand Down Expand Up @@ -226,13 +235,13 @@ const getLBALink = async (wish: IWish): Promise<string> => {
lat = formation.lieu_formation_geo_coordonnees?.split(",")[0] || wLat
lon = formation.lieu_formation_geo_coordonnees?.split(",")[1] || wLon
}
return buildEmploiUrl({ params: { ...(romes.length ? { romes } : {}), lat, lon, radius: "60", ...utmData } })
return buildEmploiUrl({ params: { ...(romes.length ? { romes } : {}), lat, lon, radius: "60", ...utmParams } })
} else {
// No formations found, use user coordinates if available
if (romes.length) {
return buildEmploiUrl({ params: { romes, lat: wLat ?? undefined, lon: wLon ?? undefined, radius: "60", ...utmData } })
return buildEmploiUrl({ params: { romes, lat: wLat ?? undefined, lon: wLon ?? undefined, radius: "60", ...utmParams } })
} else {
return buildEmploiUrl({ baseUrl: config.publicUrl, params: { ...utmData } })
return buildEmploiUrl({ baseUrl: config.publicUrl, params: { ...utmParams } })
}
}
}
Expand Down
Loading

0 comments on commit 27528cf

Please sign in to comment.