Skip to content

Commit

Permalink
feat(LBA-2171): émission des contacts vers Brevo (#1653)
Browse files Browse the repository at this point in the history
  • Loading branch information
alanlr authored Nov 27, 2024
1 parent 13bf127 commit 13f2290
Show file tree
Hide file tree
Showing 8 changed files with 1,079 additions and 758 deletions.
1,518 changes: 763 additions & 755 deletions .infra/vault/vault.yml

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions .talismanrc
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
fileignoreconfig:
- filename: .infra/files/configs/mongodb/mongod.conf
checksum: 718bee5f44edc101636be8f11173ede5b728f2858abc3c26466ff9435f0d11de
- filename: .infra/vault/vault.yml
checksum: 40717e8b277d9c9812c89d62fd3e09d0d70cdd0bb76a460f09486a31b6dd9cc7
- filename: .infra/files/configs/mongodb/seed.gpg
checksum: 183a82c08eca1f53d2d258b71abc36e5b352286f46912fcb022859b52f9fb0cc
- filename: .infra/files/scripts/seed.sh
checksum: b1361c237cee243b74fc9e66fd59af80c538d6884b8f94c3659d25cdc2b873c2
- filename: .infra/local/mongod.conf
checksum: bb2ce0c27102259a5fa39da1fb4460af9ad6ad58adc715312e53dcd69c8e6be7
- filename: .infra/vault/vault.yml
checksum: 2905c16886220d7b6a6f283bf9ba3d3ffc0ab4d0dcfbfe7611e31505a9b324e4
checksum: 40717e8b277d9c9812c89d62fd3e09d0d70cdd0bb76a460f09486a31b6dd9cc7
- filename: cypress/e2e/check-admin-algo-company.cy.ts
checksum: 6a898ee25c84ce2c41da5666aa4f84e6e4aaf0a614dbc0a00153abe99e3617c1
- filename: cypress/e2e/manual/create-many-applications.cy.ts
Expand Down
1 change: 1 addition & 0 deletions server/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const config = {
},
brevoWebhookApiKey: env.get("LBA_BREVO_WEBHOOK_API_KEY").required().asString(),
brevoApiKey: env.get("LBA_BREVO_API_KEY").required().asString(),
brevoContactListId: env.get("LBA_BREVO_CONTACT_LIST_ID").asString(),
},
auth: {
passwordHashRounds: env.get("LBA_AUTH_PASSWORD_HASH_ROUNDS").required().asInt(),
Expand Down
287 changes: 287 additions & 0 deletions server/src/jobs/brevoContacts/sendContactsToBrevo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
import { Transform } from "stream"
import { pipeline } from "stream/promises"

import { ColumnOption, stringify } from "csv-stringify/sync"
import dayjs from "dayjs"
import { AccessEntityType, AccessStatus } from "shared/models"
import { UserEventType } from "shared/models/userWithAccount.model"
import SibApiV3Sdk from "sib-api-v3-sdk"

import { logger } from "@/common/logger"
import { sleep } from "@/common/utils/asyncUtils"
import { getDbCollection } from "@/common/utils/mongodbUtils"
import { sentryCaptureException } from "@/common/utils/sentryUtils"
import { notifyToSlack } from "@/common/utils/slackUtils"
import { streamGroupByCount } from "@/common/utils/streamUtils"
import config from "@/config"

type IBrevoContact = {
user_origin: string
user_first_name: string
user_last_name: string
user_email: string
role_createdAt: Date
role_authorized_type: AccessEntityType.CFA | AccessEntityType.ENTREPRISE
entreprise_enseigne: string | null
entreprise_raison_sociale: string | null
entreprise_siret: string | null
cfa_enseigne: string | null
cfa_raison_sociale: string | null
cfa_siret: string | null
job_count?: string | null
establishment_size?: string | null
}

let contactCount = 0

const defaultClient = SibApiV3Sdk.ApiClient.instance
const apiKey = defaultClient.authentications["api-key"]
apiKey.apiKey = config.smtp.brevoApiKey
const apiInstance = new SibApiV3Sdk.ContactsApi()

const formatter = (value) => value ?? ""

const postToBrevo = async (contacts: IBrevoContact[]) => {
contactCount += contacts.length

const requestContactImport = new SibApiV3Sdk.RequestContactImport()

const fileBody = stringify(contacts, {
delimiter: ";",
header: true,
columns: [
{
key: "user_email",
header: "EMAIL",
},
{ key: "user_first_name", header: "PRENOM" },
{ key: "user_last_name", header: "NOM" },
{
key: "user_origin",
header: "USER_ORIGIN",
formatter,
},
{
key: "role_authorized_type",
header: "ROLE_AUTHORIZED_TYPE",
formatter,
},
{
key: "role_createdAt",
header: "ROLE_CREATEDAT",
formatter: (value) => dayjs(value).format("YYYY-MM-DD"),
},
{
key: "entreprise_enseigne",
header: "ENTREPRISE_ENSEIGNE",
formatter,
},
{
key: "entreprise_raison_sociale",
header: "ENTREPRISE_RAISON_SOCIALE",
formatter,
},
{
key: "entreprise_siret",
header: "ENTREPRISE_SIRET",
formatter,
},
{
key: "cfa_enseigne",
header: "CFA_ENSEIGNE",
formatter,
},
{
key: "cfa_raison_sociale",
header: "CFA_RAISON_SOCIALE",
formatter,
},
{
key: "cfa_siret",
header: "CFA_SIRET",
formatter,
},
{
key: "job_count",
header: "JOB_COUNT",
formatter: (value) => value || "0",
},
{
key: "recruiter_establishment_size",
header: "EFFECTIFS",
formatter,
},
] as ColumnOption[],
})

requestContactImport.fileBody = fileBody
requestContactImport.listIds = [config.smtp.brevoContactListId]
requestContactImport.updateExistingContacts = true
requestContactImport.emptyContactsAttributes = true

let trys = 0
let sent = false
while (!sent && trys < 3) {
try {
trys++
await apiInstance.importContacts(requestContactImport)
sent = true
} catch (error: any) {
if (error.status == 429) {
await sleep(1000)
} else {
throw error
}
}
}
}

const getRoleManagement360Stream = async (type: AccessEntityType) => {
if (type === AccessEntityType.CFA) {
return await getDbCollection("rolemanagement360")
.find(
{ role_last_status: AccessStatus.GRANTED, user_last_status: UserEventType.ACTIF, role_authorized_type: AccessEntityType.CFA },
{
projection: {
_id: 0,
user_origin: 1,
user_first_name: 1,
user_last_name: 1,
user_email: 1,
role_createdAt: 1,
role_authorized_type: 1,
entreprise_enseigne: 1,
entreprise_raison_sociale: 1,
entreprise_siret: 1,
cfa_enseigne: 1,
cfa_raison_sociale: 1,
cfa_siret: 1,
},
}
)
.stream()
} else {
return await getDbCollection("rolemanagement360")
.aggregate([
{
$match: {
role_last_status: AccessStatus.GRANTED,
user_last_status: UserEventType.ACTIF,
role_authorized_type: AccessEntityType.ENTREPRISE,
},
},
{
$lookup: {
from: "recruiters",
let: {
siret: "$entreprise_siret",
email: "$user_email",
},
pipeline: [
{
$match: {
$expr: {
$and: [
{
$eq: ["$establishment_siret", "$$siret"],
},
{
$eq: ["$email", "$$email"],
},
],
},
},
},
],
as: "recruiters",
},
},
{
$unwind: {
path: "$recruiters",
},
},
{
$group: {
_id: {
_id: "$_id",
user_origin: "$user_origin",
user_first_name: "$user_first_name",
user_last_name: "$user_last_name",
user_email: "$user_email",
role_createdAt: "$role_createdAt",
role_authorized_type: "$role_authorized_type",
entreprise_enseigne: "$entreprise_enseigne",
entreprise_raison_sociale: "$entreprise_raison_sociale",
entreprise_siret: "$entreprise_siret",
cfa_enseigne: "$cfa_enseigne",
cfa_raison_sociale: "$cfa_raison_sociale",
cfa_siret: "$cfa_siret",
recruiter_establishment_size: "$recruiters.establishment_size",
},
job_count: {
$sum: {
$size: "$recruiters.jobs",
},
},
},
},
{
$project: {
user_origin: "$_id.user_origin",
user_first_name: "$_id.user_first_name",
user_last_name: "$_id.user_last_name",
user_email: "$_id.user_email",
role_createdAt: "$_id.role_createdAt",
role_authorized_type: "$_id.role_authorized_type",
entreprise_enseigne: "$_id.entreprise_enseigne",
entreprise_raison_sociale: "$_id.entreprise_raison_sociale",
entreprise_siret: "$_id.entreprise_siret",
cfa_enseigne: "$_id.cfa_enseigne",
cfa_raison_sociale: "$_id.cfa_raison_sociale",
cfa_siret: "$_id.cfa_siret",
job_count: 1,
recruiter_establishment_size: "$_id.recruiter_establishment_size",
_id: 0,
},
},
])
.stream()
}
}

const sendContacts = async (type: AccessEntityType) => {
const cursor = await getRoleManagement360Stream(type)

const postingTransform = new Transform({
objectMode: true,
transform(contacts, _, callback) {
postToBrevo(contacts as IBrevoContact[])
callback()
},
})

await pipeline(cursor, streamGroupByCount(1000), postingTransform)
}

export const sendContactsToBrevo = async () => {
logger.info("Sending contacts to Brevo ...")

try {
await sendContacts(AccessEntityType.CFA)

await sendContacts(AccessEntityType.ENTREPRISE)

await notifyToSlack({
subject: `Envoi des contacts vers Brevo`,
message: `${contactCount} envoyés vers Brevo.`,
error: contactCount === 0,
})
} catch (err) {
sentryCaptureException(err)
await notifyToSlack({ subject: "Envoi des contacts vers Brevo", message: `ECHEC envoi des contacts vers Brevo`, error: true })
throw err
}

logger.info(`${contactCount} contacts sent to brevo`)
}
5 changes: 5 additions & 0 deletions server/src/jobs/jobs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import anonymizeOldApplications from "./anonymization/anonymizeOldApplications"
import { anonimizeUsers } from "./anonymization/anonymizeUserRecruteurs"
import { anonymizeOldUsers } from "./anonymization/anonymizeUsers"
import { processApplications } from "./applications/processApplications"
import { sendContactsToBrevo } from "./brevoContacts/sendContactsToBrevo"
import { recreateIndexes } from "./database/recreateIndexes"
import { validateModels } from "./database/schemaValidation"
import updateDiplomesMetiers from "./diplomesMetiers/updateDiplomesMetiers"
Expand Down Expand Up @@ -207,6 +208,10 @@ export async function setupJobProcessor() {
cron_string: "0 15 * * SUN",
handler: updateReferentielCommune,
},
"Emission des contacts vers Brevo": {
cron_string: "30 22 * * *",
handler: sendContactsToBrevo,
},
},
jobs: {
"remove:duplicates:recruiters": {
Expand Down
5 changes: 5 additions & 0 deletions server/src/jobs/simpleJobDefinitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { anonimizeUsers } from "./anonymization/anonymizeUserRecruteurs"
import { anonymizeOldUsers } from "./anonymization/anonymizeUsers"
import fixApplications from "./applications/fixApplications"
import { processApplications } from "./applications/processApplications"
import { sendContactsToBrevo } from "./brevoContacts/sendContactsToBrevo"
import { obfuscateCollections } from "./database/obfuscateCollections"
import { importCatalogueFormationJob } from "./formationsCatalogue/formationsCatalogue"
import { updateParcoursupAndAffelnetInfoOnFormationCatalogue } from "./formationsCatalogue/updateParcoursupAndAffelnetInfoOnFormationCatalogue"
Expand Down Expand Up @@ -190,4 +191,8 @@ export const simpleJobDefinitions: SimpleJobDefinition[] = [
fct: fillComputedJobsPartners,
description: "Enrichit la collection computed_jobs_partners avec les données provenant d'API externes",
},
{
fct: sendContactsToBrevo,
description: "Envoi à Brevo la liste des contacts",
},
]
2 changes: 2 additions & 0 deletions shared/models/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import referentielOnisepModel from "./referentielOnisep.model"
import referentielOpcoModel from "./referentielOpco.model"
import reportedCompanyModel from "./reportedCompany.model"
import roleManagementModel from "./roleManagement.model"
import roleManagement360Model from "./roleManagement360.model"
import romeModel from "./rome.model"
import sessionModel from "./session.model"
import siretDiffusibleStatusModel from "./siretDiffusibleStatus.model"
Expand Down Expand Up @@ -77,6 +78,7 @@ const modelDescriptorMap = {
[referentielOpcoModel.collectionName]: referentielOpcoModel,
[romeModel.collectionName]: romeModel,
[roleManagementModel.collectionName]: roleManagementModel,
[roleManagement360Model.collectionName]: roleManagement360Model,
[sessionModel.collectionName]: sessionModel,
[siretDiffusibleStatusModel.collectionName]: siretDiffusibleStatusModel,
[unsubscribedLbaCompanyModel.collectionName]: unsubscribedLbaCompanyModel,
Expand Down
13 changes: 13 additions & 0 deletions shared/models/roleManagement360.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { z } from "zod"

import { IModelDescriptor } from "./common"

const collectionName = "rolemanagement360" as const

const ZRoleManagement360 = z.any()

export default {
zod: ZRoleManagement360,
indexes: [[{ role_last_status: 1, user_last_status: 1, role_authorized_type: 1 }, {}]],
collectionName,
} as const satisfies IModelDescriptor

0 comments on commit 13f2290

Please sign in to comment.