From e33d8138df3909b83c3026021faf5ece265e3cb8 Mon Sep 17 00:00:00 2001 From: mehdilouraoui Date: Tue, 10 Dec 2024 14:41:05 +0100 Subject: [PATCH 01/19] =?UTF-8?q?feat:=20send=20mail=20to=20user=20with=20?= =?UTF-8?q?no=20activity=20since=C2=A010=20days=20from=20signup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migration.sql | 2 + prisma/schema.prisma | 2 + src/lib/prisma/prisma-email-queries.ts | 59 +++++++++++++++---- src/services/brevo/brevo-api.ts | 36 +++++++---- src/services/brevo/index.ts | 49 ++++++++++++--- 5 files changed, 116 insertions(+), 32 deletions(-) create mode 100644 prisma/migrations/20241210101752_add_no_activity_after_signup_type_to_email/migration.sql diff --git a/prisma/migrations/20241210101752_add_no_activity_after_signup_type_to_email/migration.sql b/prisma/migrations/20241210101752_add_no_activity_after_signup_type_to_email/migration.sql new file mode 100644 index 00000000..39318bc2 --- /dev/null +++ b/prisma/migrations/20241210101752_add_no_activity_after_signup_type_to_email/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "emailType" ADD VALUE 'noActivityAfterSignup'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index ec2e471b..8c4de914 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -130,6 +130,8 @@ enum emailType { projetAccessGranted projetAccessDeclined contactMessageSent + welcomeMessage + noActivityAfterSignup } enum emailStatus { diff --git a/src/lib/prisma/prisma-email-queries.ts b/src/lib/prisma/prisma-email-queries.ts index faaf9e8e..fdf28479 100644 --- a/src/lib/prisma/prisma-email-queries.ts +++ b/src/lib/prisma/prisma-email-queries.ts @@ -1,4 +1,4 @@ -import { email, emailStatus, emailType } from "@prisma/client"; +import { email, emailStatus, emailType, User } from "@prisma/client"; import { prismaClient } from "./prismaClient"; export const updateEmailStatus = async (id: string, status: emailStatus, brevoId?: string): Promise => { @@ -12,20 +12,26 @@ export const updateEmailStatus = async (id: string, status: emailStatus, brevoId }; export const createEmail = async ( - destinationAddress: string, + destinationAddress: string | string[], type: emailType, userProjetId?: number, extra?: any, -): Promise => { - return prismaClient.email.create({ - data: { - destination_address: destinationAddress, - user_projet_id: userProjetId, - type: type, - email_status: emailStatus.PENDING, - extra: extra, - }, - }); +): Promise => { + const addresses = Array.isArray(destinationAddress) ? destinationAddress : [destinationAddress]; + + const emailPromises = addresses.map((address) => + prismaClient.email.create({ + data: { + destination_address: address, + user_projet_id: userProjetId, + type: type, + email_status: emailStatus.PENDING, + extra: extra, + }, + }), + ); + + return Promise.all(emailPromises); }; export const getLastEmailForUserProjet = async (userProjetId: number, emailType: emailType): Promise => { @@ -34,3 +40,32 @@ export const getLastEmailForUserProjet = async (userProjetId: number, emailType: orderBy: { sending_time: "desc" }, }); }; + +export const getUserWithNoActivityAfterSignup = async (since = 10): Promise => { + const SINCE_DAYS_AGO = new Date(Date.now() - since * 24 * 60 * 60 * 1000); + + const noActivityEmails = await prismaClient.email.findMany({ + where: { + type: emailType.noActivityAfterSignup, + }, + select: { + destination_address: true, + }, + }); + + const emailAddresses = noActivityEmails.map((email) => email.destination_address); + + return prismaClient.user.findMany({ + where: { + created_at: { + lt: SINCE_DAYS_AGO, + }, + projets: { + none: {}, + }, + email: { + notIn: emailAddresses, + }, + }, + }); +}; diff --git a/src/services/brevo/brevo-api.ts b/src/services/brevo/brevo-api.ts index 5898dec9..ed4b9618 100644 --- a/src/services/brevo/brevo-api.ts +++ b/src/services/brevo/brevo-api.ts @@ -1,16 +1,26 @@ -export const brevoSendEmail = async (to: string, templateId: number, params?: Record) => { - return await fetch(`${process.env.BREVO_API_BASE_URL}/smtp/email`, { - method: "POST", - headers: { - "api-key": process.env.BREVO_API_KEY ?? "", - "Content-Type": "application/json", - }, - body: JSON.stringify({ - to: [{ email: to }], - templateId: templateId, - ...(params && { params: params }), - }), - }); +export const brevoSendEmail = async (to: string | string[], templateId: number, params?: Record) => { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 5000); + + try { + return await fetch(`${process.env.BREVO_API_BASE_URL}/smtp/email`, { + method: "POST", + headers: { + "api-key": process.env.BREVO_API_KEY ?? "", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + to: Array.isArray(to) ? to.map((email) => ({ email })) : [{ email: to }], + templateId: templateId, + ...(params && { params }), + }), + signal: controller.signal, + }); + } catch (error) { + throw new Error(`Erreur avec l'API Brevo : ${error}`); + } finally { + clearTimeout(timeout); + } }; type BrevoUpsertContactType = { diff --git a/src/services/brevo/index.ts b/src/services/brevo/index.ts index 90cce84b..c1c0c5f4 100644 --- a/src/services/brevo/index.ts +++ b/src/services/brevo/index.ts @@ -1,4 +1,8 @@ -import { createEmail, updateEmailStatus as updateEmailStatusQuery } from "@/src/lib/prisma/prisma-email-queries"; +import { + createEmail, + getUserWithNoActivityAfterSignup, + updateEmailStatus as updateEmailStatusQuery, +} from "@/src/lib/prisma/prisma-email-queries"; import { email, emailStatus, emailType } from "@prisma/client"; import { brevoSendEmail } from "./brevo-api"; import { ResponseAction } from "@/src/actions/actions-types"; @@ -44,6 +48,12 @@ export class EmailService { contactMessageSent: { templateId: 45, }, + welcomeMessage: { + templateId: 52, + }, + noActivityAfterSignup: { + templateId: 53, + }, }; } @@ -58,15 +68,15 @@ export class EmailService { userProjetId, extra, }: { - to: string; + to: string | string[]; emailType: emailType; params?: Record; userProjetId?: number; extra?: any; }): Promise { const { templateId } = this.templates[emailType]; + const dbEmails = await createEmail(to, emailType, userProjetId, extra); - const dbEmail = await createEmail(to, emailType, userProjetId, extra); try { const response = await brevoSendEmail(to, templateId, params); @@ -77,13 +87,14 @@ export class EmailService { const data = await response.json(); - let email = null; - email = await this.updateEmailStatus(dbEmail.id, emailStatus.SUCCESS, data.messageId); + const updatedEmails = await Promise.all( + dbEmails.map((dbEmail) => this.updateEmailStatus(dbEmail.id, emailStatus.SUCCESS, data.messageId)), + ); - return { type: "success", message: "EMAIL_SENT", email }; + return { type: "success", message: "EMAIL_SENT", email: updatedEmails[0] }; } catch (error) { captureError("Erreur lors de l'envoi du mail : ", error); - await this.updateEmailStatus(dbEmail.id, emailStatus.ERROR); + await Promise.all(dbEmails.map((dbEmail) => this.updateEmailStatus(dbEmail.id, emailStatus.ERROR))); return { type: "error", message: "TECHNICAL_ERROR" }; } } @@ -162,4 +173,28 @@ export class EmailService { async sendContactMessageReceivedEmail(data: ContactFormData) { return this.sendEmail({ to: data.email, emailType: emailType.contactMessageSent, extra: data }); } + + async sendWelcomeMessageEmail(data: Pick) { + return this.sendEmail({ + to: data.email, + emailType: emailType.welcomeMessage, + params: { NOM: data.nom }, + extra: data, + }); + } + + async sendNoActivityAfterSignupEmail() { + const users = await getUserWithNoActivityAfterSignup(); + const usersEmail = users?.map((user) => user.email); + + if (!usersEmail?.length) { + return { message: "Aucun utilisateur trouvé." }; + } else { + await this.sendEmail({ + to: usersEmail, + emailType: emailType.noActivityAfterSignup, + }); + return { message: `Email(s) envoyé(s) à ${usersEmail.length} utilisateur(s).` }; + } + } } From c89f0bfae4393ea5867c0dc7c3da0c666c00c3cd Mon Sep 17 00:00:00 2001 From: mehdilouraoui Date: Tue, 10 Dec 2024 14:44:29 +0100 Subject: [PATCH 02/19] update vars name --- src/lib/prisma/prisma-email-queries.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/prisma/prisma-email-queries.ts b/src/lib/prisma/prisma-email-queries.ts index fdf28479..d2bf51e8 100644 --- a/src/lib/prisma/prisma-email-queries.ts +++ b/src/lib/prisma/prisma-email-queries.ts @@ -53,7 +53,7 @@ export const getUserWithNoActivityAfterSignup = async (since = 10): Promise email.destination_address); + const emails = noActivityEmails.map((email) => email.destination_address); return prismaClient.user.findMany({ where: { @@ -64,7 +64,7 @@ export const getUserWithNoActivityAfterSignup = async (since = 10): Promise Date: Tue, 10 Dec 2024 15:06:19 +0100 Subject: [PATCH 03/19] update mail filter --- src/lib/prisma/prisma-email-queries.ts | 5 ++++- src/services/brevo/index.ts | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/lib/prisma/prisma-email-queries.ts b/src/lib/prisma/prisma-email-queries.ts index d2bf51e8..03544ac3 100644 --- a/src/lib/prisma/prisma-email-queries.ts +++ b/src/lib/prisma/prisma-email-queries.ts @@ -41,12 +41,15 @@ export const getLastEmailForUserProjet = async (userProjetId: number, emailType: }); }; -export const getUserWithNoActivityAfterSignup = async (since = 10): Promise => { +export const getUserWithNoActivityAfterSignup = async (lastSyncDate: Date, since = 10): Promise => { const SINCE_DAYS_AGO = new Date(Date.now() - since * 24 * 60 * 60 * 1000); const noActivityEmails = await prismaClient.email.findMany({ where: { type: emailType.noActivityAfterSignup, + sending_time: { + gte: lastSyncDate, + }, }, select: { destination_address: true, diff --git a/src/services/brevo/index.ts b/src/services/brevo/index.ts index c1c0c5f4..647f229a 100644 --- a/src/services/brevo/index.ts +++ b/src/services/brevo/index.ts @@ -183,8 +183,8 @@ export class EmailService { }); } - async sendNoActivityAfterSignupEmail() { - const users = await getUserWithNoActivityAfterSignup(); + async sendNoActivityAfterSignupEmail(lastSyncDate: Date) { + const users = await getUserWithNoActivityAfterSignup(lastSyncDate); const usersEmail = users?.map((user) => user.email); if (!usersEmail?.length) { From 1e464e8d38efb285d4a77b1e278f547190d60c95 Mon Sep 17 00:00:00 2001 From: mehdilouraoui Date: Tue, 10 Dec 2024 18:21:55 +0100 Subject: [PATCH 04/19] update message response --- src/services/brevo/index.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/services/brevo/index.ts b/src/services/brevo/index.ts index 647f229a..8a3c36a9 100644 --- a/src/services/brevo/index.ts +++ b/src/services/brevo/index.ts @@ -190,11 +190,16 @@ export class EmailService { if (!usersEmail?.length) { return { message: "Aucun utilisateur trouvé." }; } else { - await this.sendEmail({ + const response = await this.sendEmail({ to: usersEmail, emailType: emailType.noActivityAfterSignup, }); - return { message: `Email(s) envoyé(s) à ${usersEmail.length} utilisateur(s).` }; + + if (response.type === "success") { + return { message: `Email(s) envoyé(s) à ${usersEmail.length} utilisateur(s).`, success: true }; + } else { + return { message: "Erreur lors de l'envoi des emails.", success: false }; + } } } } From fe27632bca15e8c79b371860042a5378cbd1399f Mon Sep 17 00:00:00 2001 From: mehdilouraoui Date: Tue, 10 Dec 2024 18:34:55 +0100 Subject: [PATCH 05/19] feat: ajout d'un batch --- .../migration.sql | 2 ++ prisma/schema.prisma | 1 + scripts/csm-mail-batch.ts | 35 +++++++++++++++++++ src/lib/prisma/prisma-cron-jobs-queries.ts | 5 +++ 4 files changed, 43 insertions(+) create mode 100644 prisma/migrations/20241210172647_update_job_type/migration.sql create mode 100644 scripts/csm-mail-batch.ts diff --git a/prisma/migrations/20241210172647_update_job_type/migration.sql b/prisma/migrations/20241210172647_update_job_type/migration.sql new file mode 100644 index 00000000..da73385c --- /dev/null +++ b/prisma/migrations/20241210172647_update_job_type/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "JobType" ADD VALUE 'CSM_MAIL_BATCH'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 633d6e95..4f52290e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -291,6 +291,7 @@ model Analytics { enum JobType { SYNC_HUBSPOT + CSM_MAIL_BATCH } model cron_jobs { diff --git a/scripts/csm-mail-batch.ts b/scripts/csm-mail-batch.ts new file mode 100644 index 00000000..6460c091 --- /dev/null +++ b/scripts/csm-mail-batch.ts @@ -0,0 +1,35 @@ +import { getLastCsmMailBatch, saveCronJob } from "@/src/lib/prisma/prisma-cron-jobs-queries"; +import { captureError, customCaptureException } from "@/src/lib/sentry/sentryCustomMessage"; +import { EmailService } from "@/src/services/brevo"; + +const syncWithHubspot = async () => { + if (process.env.HUBSPOT_SYNC_ENV !== "true") { + console.log("La synchronisation n'a pas aboutie : éxecution hors d'un environnement de production."); + return; + } + + try { + const emailService = new EmailService(); + const startedDate = new Date(); + const lastSync = await getLastCsmMailBatch(); + const lastSyncDate = lastSync?.execution_end_time ?? new Date(0); + + console.log("Recherche des utilisateurs inactifs plus de 10 jours après leur inscription..."); + const emailNoActivity = await emailService.sendNoActivityAfterSignupEmail(lastSyncDate); + if (!emailNoActivity.success) { + captureError(emailNoActivity.message, { + executionTime: new Date(), + }); + process.exit(1); + } + + await saveCronJob(startedDate, new Date(), "CSM_MAIL_BATCH"); + console.log("Batch des mails CSM réussi !"); + process.exit(0); + } catch (error) { + customCaptureException("Erreur lors du batch des mails CSM.", {}); + process.exit(1); + } +}; + +syncWithHubspot(); diff --git a/src/lib/prisma/prisma-cron-jobs-queries.ts b/src/lib/prisma/prisma-cron-jobs-queries.ts index cfab6242..f5685fe1 100644 --- a/src/lib/prisma/prisma-cron-jobs-queries.ts +++ b/src/lib/prisma/prisma-cron-jobs-queries.ts @@ -7,6 +7,11 @@ export const getLastHubspotSync = async () => where: { job_type: "SYNC_HUBSPOT" }, orderBy: { execution_end_time: "desc" }, }); +export const getLastCsmMailBatch = async () => + await prismaClient.cron_jobs.findFirst({ + where: { job_type: "CSM_MAIL_BATCH" }, + orderBy: { execution_end_time: "desc" }, + }); export const saveCronJob = async (startTime: Date, endTime: Date, jobType: cron_jobs["job_type"]) => await prismaClient.cron_jobs.create({ From e1c7424231f985b13984eb7753bd42f0a3552f61 Mon Sep 17 00:00:00 2001 From: mehdilouraoui Date: Tue, 10 Dec 2024 18:35:35 +0100 Subject: [PATCH 06/19] update return message --- src/services/brevo/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/services/brevo/index.ts b/src/services/brevo/index.ts index 8a3c36a9..e48e2c55 100644 --- a/src/services/brevo/index.ts +++ b/src/services/brevo/index.ts @@ -198,7 +198,10 @@ export class EmailService { if (response.type === "success") { return { message: `Email(s) envoyé(s) à ${usersEmail.length} utilisateur(s).`, success: true }; } else { - return { message: "Erreur lors de l'envoi des emails.", success: false }; + return { + message: "Erreur lors de l'envoi des emails des utilisateurs inactifs depuis leur inscription.", + success: false, + }; } } } From b919840898494628ae95ca5b80f680fef6c452e0 Mon Sep 17 00:00:00 2001 From: mehdilouraoui Date: Tue, 10 Dec 2024 18:37:02 +0100 Subject: [PATCH 07/19] feat: csm batch --- scripts/csm-mail-batch.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/csm-mail-batch.ts b/scripts/csm-mail-batch.ts index 6460c091..1c80d7a3 100644 --- a/scripts/csm-mail-batch.ts +++ b/scripts/csm-mail-batch.ts @@ -27,7 +27,9 @@ const syncWithHubspot = async () => { console.log("Batch des mails CSM réussi !"); process.exit(0); } catch (error) { - customCaptureException("Erreur lors du batch des mails CSM.", {}); + customCaptureException("Erreur lors du batch des mails CSM.", { + executionTime: new Date(), + }); process.exit(1); } }; From dad713c76ecc9e8038dbe5f74b4ec87111c347e6 Mon Sep 17 00:00:00 2001 From: mehdilouraoui Date: Tue, 10 Dec 2024 18:43:06 +0100 Subject: [PATCH 08/19] change var name --- scripts/csm-mail-batch.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/csm-mail-batch.ts b/scripts/csm-mail-batch.ts index 1c80d7a3..efbf9486 100644 --- a/scripts/csm-mail-batch.ts +++ b/scripts/csm-mail-batch.ts @@ -2,8 +2,8 @@ import { getLastCsmMailBatch, saveCronJob } from "@/src/lib/prisma/prisma-cron-j import { captureError, customCaptureException } from "@/src/lib/sentry/sentryCustomMessage"; import { EmailService } from "@/src/services/brevo"; -const syncWithHubspot = async () => { - if (process.env.HUBSPOT_SYNC_ENV !== "true") { +const main = async () => { + if (process.env.CSM_MAIL_BATCH_ENV !== "true") { console.log("La synchronisation n'a pas aboutie : éxecution hors d'un environnement de production."); return; } @@ -34,4 +34,4 @@ const syncWithHubspot = async () => { } }; -syncWithHubspot(); +main(); From 80b78a112cdf9460dc281242cc7522eac0480957 Mon Sep 17 00:00:00 2001 From: mehdilouraoui Date: Wed, 11 Dec 2024 10:08:20 +0100 Subject: [PATCH 09/19] add flexibility for inactivity days --- scripts/csm-mail-batch.ts | 12 ++++++------ src/services/brevo/index.ts | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/scripts/csm-mail-batch.ts b/scripts/csm-mail-batch.ts index efbf9486..86ce5a48 100644 --- a/scripts/csm-mail-batch.ts +++ b/scripts/csm-mail-batch.ts @@ -14,14 +14,14 @@ const main = async () => { const lastSync = await getLastCsmMailBatch(); const lastSyncDate = lastSync?.execution_end_time ?? new Date(0); - console.log("Recherche des utilisateurs inactifs plus de 10 jours après leur inscription..."); - const emailNoActivity = await emailService.sendNoActivityAfterSignupEmail(lastSyncDate); - if (!emailNoActivity.success) { - captureError(emailNoActivity.message, { - executionTime: new Date(), - }); + const INACTIVITY_DAYS = 10; + console.log(`Recherche des utilisateurs inactifs depuis ${INACTIVITY_DAYS} jours...`); + const { success, message } = await emailService.sendNoActivityAfterSignupEmail(lastSyncDate, INACTIVITY_DAYS); + if (!success) { + captureError(message, { executionTime: new Date() }); process.exit(1); } + console.log(message); await saveCronJob(startedDate, new Date(), "CSM_MAIL_BATCH"); console.log("Batch des mails CSM réussi !"); diff --git a/src/services/brevo/index.ts b/src/services/brevo/index.ts index e48e2c55..2420f3b6 100644 --- a/src/services/brevo/index.ts +++ b/src/services/brevo/index.ts @@ -183,8 +183,8 @@ export class EmailService { }); } - async sendNoActivityAfterSignupEmail(lastSyncDate: Date) { - const users = await getUserWithNoActivityAfterSignup(lastSyncDate); + async sendNoActivityAfterSignupEmail(lastSyncDate: Date, since = 10) { + const users = await getUserWithNoActivityAfterSignup(lastSyncDate, since); const usersEmail = users?.map((user) => user.email); if (!usersEmail?.length) { From dd259d6dd36a6c74b194056baf84124f8f9c1c72 Mon Sep 17 00:00:00 2001 From: mehdilouraoui Date: Wed, 11 Dec 2024 10:09:22 +0100 Subject: [PATCH 10/19] change params wording --- src/lib/prisma/prisma-email-queries.ts | 7 +++++-- src/services/brevo/index.ts | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/lib/prisma/prisma-email-queries.ts b/src/lib/prisma/prisma-email-queries.ts index 03544ac3..e58631a0 100644 --- a/src/lib/prisma/prisma-email-queries.ts +++ b/src/lib/prisma/prisma-email-queries.ts @@ -41,8 +41,11 @@ export const getLastEmailForUserProjet = async (userProjetId: number, emailType: }); }; -export const getUserWithNoActivityAfterSignup = async (lastSyncDate: Date, since = 10): Promise => { - const SINCE_DAYS_AGO = new Date(Date.now() - since * 24 * 60 * 60 * 1000); +export const getUserWithNoActivityAfterSignup = async ( + lastSyncDate: Date, + inactivityDays = 10, +): Promise => { + const SINCE_DAYS_AGO = new Date(Date.now() - inactivityDays * 24 * 60 * 60 * 1000); const noActivityEmails = await prismaClient.email.findMany({ where: { diff --git a/src/services/brevo/index.ts b/src/services/brevo/index.ts index 2420f3b6..cbee8892 100644 --- a/src/services/brevo/index.ts +++ b/src/services/brevo/index.ts @@ -183,8 +183,8 @@ export class EmailService { }); } - async sendNoActivityAfterSignupEmail(lastSyncDate: Date, since = 10) { - const users = await getUserWithNoActivityAfterSignup(lastSyncDate, since); + async sendNoActivityAfterSignupEmail(lastSyncDate: Date, inactivityDays = 10) { + const users = await getUserWithNoActivityAfterSignup(lastSyncDate, inactivityDays); const usersEmail = users?.map((user) => user.email); if (!usersEmail?.length) { From 78b2e49ef00901194b9b72b9335e374335c92dab Mon Sep 17 00:00:00 2001 From: mehdilouraoui Date: Wed, 11 Dec 2024 11:10:58 +0100 Subject: [PATCH 11/19] edit cron json --- cron.json | 3 +++ package.json | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/cron.json b/cron.json index 4ac6b5a3..29116012 100644 --- a/cron.json +++ b/cron.json @@ -2,6 +2,9 @@ "jobs": [ { "command": "0 0 * * * npm run hubspot-sync" + }, + { + "command": "0 0 * * * npm run csm-mail-batch" } ] } diff --git a/package.json b/package.json index db286b11..6f099f25 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "lint": "next lint", "prettier": "npx prettier --check . --ignore-path .prettierignore", "fixPrettier": "prettier . --write", - "hubspot-sync": "npx tsx ./scripts/hubspot-sync.ts" + "hubspot-sync": "npx tsx ./scripts/hubspot-sync.ts", + "csm-mail-batch": "npx tsx ./scripts/csm-mail-batch.ts" }, "dependencies": { "@auth/prisma-adapter": "^1.6.0", From 60b4281f1a1d7a744b6e89423a165d9580cbbdea Mon Sep 17 00:00:00 2001 From: mehdilouraoui Date: Wed, 11 Dec 2024 11:12:16 +0100 Subject: [PATCH 12/19] fix: update env var --- scripts/csm-mail-batch.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/csm-mail-batch.ts b/scripts/csm-mail-batch.ts index 86ce5a48..4ef4ef9e 100644 --- a/scripts/csm-mail-batch.ts +++ b/scripts/csm-mail-batch.ts @@ -3,7 +3,7 @@ import { captureError, customCaptureException } from "@/src/lib/sentry/sentryCus import { EmailService } from "@/src/services/brevo"; const main = async () => { - if (process.env.CSM_MAIL_BATCH_ENV !== "true") { + if (process.env.CSM_MAIL_BATCH_ACTIVE !== "true") { console.log("La synchronisation n'a pas aboutie : éxecution hors d'un environnement de production."); return; } From 83aae316bb4c4b79960c95a161b991ef8338d7ea Mon Sep 17 00:00:00 2001 From: mehdilouraoui Date: Wed, 11 Dec 2024 11:15:07 +0100 Subject: [PATCH 13/19] fix: update logs --- scripts/csm-mail-batch.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/csm-mail-batch.ts b/scripts/csm-mail-batch.ts index 4ef4ef9e..1f13635b 100644 --- a/scripts/csm-mail-batch.ts +++ b/scripts/csm-mail-batch.ts @@ -4,7 +4,7 @@ import { EmailService } from "@/src/services/brevo"; const main = async () => { if (process.env.CSM_MAIL_BATCH_ACTIVE !== "true") { - console.log("La synchronisation n'a pas aboutie : éxecution hors d'un environnement de production."); + console.log("Le batch des mails CSM n'est pas activé sur cet environnement."); return; } From dd8da8762ed71b92326d5eec8d510daa91135380 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Ta=C3=AFeb?= Date: Wed, 11 Dec 2024 11:38:58 +0100 Subject: [PATCH 14/19] feat: send projet creation email --- .../migration.sql | 10 +++ .../migration.sql | 14 ++++ prisma/schema.prisma | 1 + scripts/send-csm-emails.ts | 17 ++++ src/helpers/dateUtils.ts | 2 + src/helpers/routes.ts | 3 + src/lib/prisma/prismaProjetQueries.ts | 31 +++++++- src/services/brevo/index.ts | 79 ++++++++++++++++++- 8 files changed, 152 insertions(+), 5 deletions(-) create mode 100644 prisma/migrations/20241210154607_add_email_template/migration.sql create mode 100644 prisma/migrations/20241211100239_change_email_type_for_projet_creation/migration.sql create mode 100644 scripts/send-csm-emails.ts diff --git a/prisma/migrations/20241210154607_add_email_template/migration.sql b/prisma/migrations/20241210154607_add_email_template/migration.sql new file mode 100644 index 00000000..2e686bc9 --- /dev/null +++ b/prisma/migrations/20241210154607_add_email_template/migration.sql @@ -0,0 +1,10 @@ +-- AlterEnum +-- This migration adds more than one value to an enum. +-- With PostgreSQL versions 11 and earlier, this is not possible +-- in a single migration. This can be worked around by creating +-- multiple migrations, each migration adding only one value to +-- the enum. + + +ALTER TYPE "emailType" ADD VALUE 'projetCreationRandomRex'; +ALTER TYPE "emailType" ADD VALUE 'projetCreationFixedRex'; diff --git a/prisma/migrations/20241211100239_change_email_type_for_projet_creation/migration.sql b/prisma/migrations/20241211100239_change_email_type_for_projet_creation/migration.sql new file mode 100644 index 00000000..1cae6d58 --- /dev/null +++ b/prisma/migrations/20241211100239_change_email_type_for_projet_creation/migration.sql @@ -0,0 +1,14 @@ +/* + Warnings: + + - The values [projetCreationRandomRex,projetCreationFixedRex] on the enum `emailType` will be removed. If these variants are still used in the database, this will fail. + +*/ +-- AlterEnum +BEGIN; +CREATE TYPE "emailType_new" AS ENUM ('projetCreation', 'projetInvitation', 'projetRequestAccess', 'projetAccessGranted', 'projetAccessDeclined', 'contactMessageSent', 'welcomeMessage'); +ALTER TABLE "email" ALTER COLUMN "type" TYPE "emailType_new" USING ("type"::text::"emailType_new"); +ALTER TYPE "emailType" RENAME TO "emailType_old"; +ALTER TYPE "emailType_new" RENAME TO "emailType"; +DROP TYPE "emailType_old"; +COMMIT; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f8268cd9..751c841c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -125,6 +125,7 @@ model user_projet { } enum emailType { + projetCreation projetInvitation projetRequestAccess projetAccessGranted diff --git a/scripts/send-csm-emails.ts b/scripts/send-csm-emails.ts new file mode 100644 index 00000000..86422e57 --- /dev/null +++ b/scripts/send-csm-emails.ts @@ -0,0 +1,17 @@ +import { customCaptureException } from "@/src/lib/sentry/sentryCustomMessage"; +import { EmailService } from "@/src/services/brevo"; + +const sendCsmEmails = async () => { + try { + if (process.env.SEND_CSM_EMAILS !== "true") { + console.log("L'envoi de mail CSM est désactivé pour cet environnement'."); + } else { + await new EmailService().sendProjetCreationEmail(); + } + } catch (error) { + customCaptureException("Erreur lors du batch d'envoi des mails CSM.", error); + process.exit(1); + } +}; + +sendCsmEmails(); diff --git a/src/helpers/dateUtils.ts b/src/helpers/dateUtils.ts index 799f084d..efecb00c 100644 --- a/src/helpers/dateUtils.ts +++ b/src/helpers/dateUtils.ts @@ -2,6 +2,8 @@ import type { Attribute } from "@strapi/strapi"; export const FAR_FUTURE = new Date(3024, 0, 0, 1); +export const removeDaysToDate = (date: Date, nbDays: number) => new Date(date.getTime() - nbDays * 24 * 60 * 60 * 1000); + export function monthDateToString(value: Date | null | undefined): string { return value ? `${value.getFullYear()}-${("0" + (value.getMonth() + 1)).slice(-2)}` : ""; } diff --git a/src/helpers/routes.ts b/src/helpers/routes.ts index 7b26b08c..575ee58d 100644 --- a/src/helpers/routes.ts +++ b/src/helpers/routes.ts @@ -6,6 +6,7 @@ export const PFMV_ROUTES = { FICHES_DIAGNOSTIC: "/fiches-diagnostic", MES_FICHES_SOLUTIONS: "/mon-projet/favoris", RETOURS_EXPERIENCE: "/projet", + RETOUR_EXPERIENCE: (slug: string) => `${PFMV_ROUTES.RETOURS_EXPERIENCE}/${slug}`, CONTACT: "/contact", CONTACT_SUCCESS: "/contact/success", NEWSLETTER: "/newsletter", @@ -45,6 +46,8 @@ export const PFMV_ROUTES = { `/espace-projet/${projetId}/financement/edit/${estimationId}`, }; +export const getFullUrl = (route: string): string => `${process.env.NEXT_PUBLIC_URL_SITE}${route}`; + export const GET_AIDES_TERRITOIRES_BY_AIDE_ID_URL = (aideId: number) => `/api/get-aides-territoires-aide-by-aide-id?aideId=${aideId}`; diff --git a/src/lib/prisma/prismaProjetQueries.ts b/src/lib/prisma/prismaProjetQueries.ts index 9a545472..5546acec 100644 --- a/src/lib/prisma/prismaProjetQueries.ts +++ b/src/lib/prisma/prismaProjetQueries.ts @@ -1,5 +1,5 @@ import { prismaClient } from "@/src/lib/prisma/prismaClient"; -import { InvitationStatus, Prisma, projet, RoleProjet, user_projet } from "@prisma/client"; +import { emailType, InvitationStatus, Prisma, projet, RoleProjet, user_projet } from "@prisma/client"; import { ProjetWithPublicRelations, ProjetWithRelations } from "./prismaCustomTypes"; import { generateRandomId } from "@/src/helpers/common"; import { GeoJsonProperties } from "geojson"; @@ -443,3 +443,32 @@ export const updateProjetVisibility = async ( include: projetIncludes, }); }; + +export const getProjetsForProjetCreationEmail = async ( + afterDate: Date, + beforeDate: Date, +): Promise => { + return prismaClient.projet.findMany({ + where: { + deleted_at: null, + created_at: { + gte: afterDate, + lte: beforeDate, + }, + NOT: { + users: { + some: { + role: "ADMIN", + email: { + some: { + type: emailType.projetCreation, + email_status: "SUCCESS", + }, + }, + }, + }, + }, + }, + include: projetIncludes, + }); +}; diff --git a/src/services/brevo/index.ts b/src/services/brevo/index.ts index 453dabfa..8780582f 100644 --- a/src/services/brevo/index.ts +++ b/src/services/brevo/index.ts @@ -4,10 +4,15 @@ import { brevoSendEmail } from "./brevo-api"; import { ResponseAction } from "@/src/actions/actions-types"; import { getOldestProjectAdmin } from "@/src/lib/prisma/prisma-user-projet-queries"; import { captureError } from "@/src/lib/sentry/sentryCustomMessage"; -import { UserProjetWithRelations, UserWithCollectivite } from "@/src/lib/prisma/prismaCustomTypes"; +import { ProjetWithRelations, UserProjetWithRelations, UserWithCollectivite } from "@/src/lib/prisma/prismaCustomTypes"; import { getPrimaryCollectiviteForUser } from "@/src/helpers/user"; -import { PFMV_ROUTES } from "@/src/helpers/routes"; +import { getFullUrl, PFMV_ROUTES } from "@/src/helpers/routes"; import { ContactFormData } from "@/src/forms/contact/contact-form-schema"; +import { getProjetsForProjetCreationEmail } from "@/src/lib/prisma/prismaProjetQueries"; +import { removeDaysToDate } from "@/src/helpers/dateUtils"; +import { getRetoursExperiences } from "@/src/lib/strapi/queries/retoursExperienceQueries"; +import { RetourExperienceResponse } from "@/src/components/ficheSolution/type"; +import shuffle from "lodash/shuffle"; interface Templates { templateId: number; @@ -24,6 +29,44 @@ export type EmailProjetPartageConfig = { destinationMail: string; }; +export type EmailProjetCreationParam = { + nomUtilisateur: string; + nomProjet: string; + rex1Titre?: string; + rex1Url?: string; + rex2Titre?: string; + rex2Url?: string; + rex3Titre?: string; + rex3Url?: string; + rex4Titre?: string; + rex4Url?: string; +}; + +const computeProjetCreationEmailParam = ( + projet: ProjetWithRelations, + rexExamples: RetourExperienceResponse[], +): EmailProjetCreationParam => { + if (rexExamples.length < 3) { + return { + nomProjet: projet.nom, + nomUtilisateur: projet.creator.nom || "", + }; + } else { + return { + nomProjet: projet.nom, + nomUtilisateur: projet.creator.nom || "", + rex1Titre: rexExamples[0].attributes.titre, + rex1Url: getFullUrl(PFMV_ROUTES.RETOUR_EXPERIENCE(rexExamples[0].attributes.slug)), + rex2Titre: rexExamples[1].attributes.titre, + rex2Url: getFullUrl(PFMV_ROUTES.RETOUR_EXPERIENCE(rexExamples[1].attributes.slug)), + rex3Titre: rexExamples[2].attributes.titre, + rex3Url: getFullUrl(PFMV_ROUTES.RETOUR_EXPERIENCE(rexExamples[2].attributes.slug)), + rex4Titre: rexExamples[3]?.attributes.titre, + ...(rexExamples[3] && { rex4Url: getFullUrl(PFMV_ROUTES.RETOUR_EXPERIENCE(rexExamples[3]?.attributes.slug)) }), + }; + } +}; + export class EmailService { private readonly templates: Record; @@ -47,6 +90,9 @@ export class EmailService { welcomeMessage: { templateId: 52, }, + projetCreation: { + templateId: 54, + }, }; } @@ -79,8 +125,7 @@ export class EmailService { const data = await response.json(); - let email = null; - email = await this.updateEmailStatus(dbEmail.id, emailStatus.SUCCESS, data.messageId); + const email = await this.updateEmailStatus(dbEmail.id, emailStatus.SUCCESS, data.messageId); return { type: "success", message: "EMAIL_SENT", email }; } catch (error) { @@ -173,4 +218,30 @@ export class EmailService { extra: data, }); } + + async sendProjetCreationEmail(lastSyncDate?: Date) { + const projets = await getProjetsForProjetCreationEmail( + removeDaysToDate(lastSyncDate || new Date(), 3), + removeDaysToDate(new Date(), 1), + ); + console.log(`Nb de mails de création de projet à envoyer : ${projets.length}`); + const allRex = await getRetoursExperiences(); + const shuffledRex = shuffle(allRex); + return await Promise.all( + projets.map(async (projet) => { + const rexExamples = shuffledRex + // @ts-ignore + .filter((rex) => rex.attributes.types_espaces?.includes(projet.type_espace)) + .slice(0, 4); + const emailParams = computeProjetCreationEmailParam(projet, rexExamples); + return await this.sendEmail({ + to: projet.creator.email, + emailType: emailType.projetCreation, + params: emailParams, + extra: emailParams, + userProjetId: projet.users.find((up) => up.role === "ADMIN")?.id, + }); + }), + ); + } } From 4e67fea6369bea71fecf77af4e1c3648fb82f670 Mon Sep 17 00:00:00 2001 From: mehdilouraoui Date: Wed, 11 Dec 2024 15:57:09 +0100 Subject: [PATCH 15/19] fix: get user with no activity query (wip) --- prisma/schema.prisma | 3 ++ src/lib/prisma/prisma-email-queries.ts | 51 +++++++++----------------- src/services/brevo/brevo-api.ts | 4 +- src/services/brevo/index.ts | 12 +++--- 4 files changed, 28 insertions(+), 42 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 633d6e95..437041fb 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -57,6 +57,7 @@ model User { Analytics Analytics[] sourcing_contacts_created projet_sourcing_contact[] accept_communication_produit Boolean @default(true) + emails email[] } model VerificationToken { @@ -149,6 +150,8 @@ model email { email_status emailStatus user_projet_id Int? user_projet user_projet? @relation(fields: [user_projet_id], references: [id]) + user User? @relation(fields: [user_id], references: [id]) + user_id String? extra Json? } diff --git a/src/lib/prisma/prisma-email-queries.ts b/src/lib/prisma/prisma-email-queries.ts index 03544ac3..bb2bdda6 100644 --- a/src/lib/prisma/prisma-email-queries.ts +++ b/src/lib/prisma/prisma-email-queries.ts @@ -12,26 +12,20 @@ export const updateEmailStatus = async (id: string, status: emailStatus, brevoId }; export const createEmail = async ( - destinationAddress: string | string[], + destinationAddress: string, type: emailType, userProjetId?: number, extra?: any, -): Promise => { - const addresses = Array.isArray(destinationAddress) ? destinationAddress : [destinationAddress]; - - const emailPromises = addresses.map((address) => - prismaClient.email.create({ - data: { - destination_address: address, - user_projet_id: userProjetId, - type: type, - email_status: emailStatus.PENDING, - extra: extra, - }, - }), - ); - - return Promise.all(emailPromises); +): Promise => { + return prismaClient.email.create({ + data: { + destination_address: destinationAddress, + user_projet_id: userProjetId, + type: type, + email_status: emailStatus.PENDING, + extra: extra, + }, + }); }; export const getLastEmailForUserProjet = async (userProjetId: number, emailType: emailType): Promise => { @@ -44,20 +38,6 @@ export const getLastEmailForUserProjet = async (userProjetId: number, emailType: export const getUserWithNoActivityAfterSignup = async (lastSyncDate: Date, since = 10): Promise => { const SINCE_DAYS_AGO = new Date(Date.now() - since * 24 * 60 * 60 * 1000); - const noActivityEmails = await prismaClient.email.findMany({ - where: { - type: emailType.noActivityAfterSignup, - sending_time: { - gte: lastSyncDate, - }, - }, - select: { - destination_address: true, - }, - }); - - const emails = noActivityEmails.map((email) => email.destination_address); - return prismaClient.user.findMany({ where: { created_at: { @@ -66,8 +46,13 @@ export const getUserWithNoActivityAfterSignup = async (lastSyncDate: Date, since projets: { none: {}, }, - email: { - notIn: emails, + emails: { + none: { + type: emailType.noActivityAfterSignup, + sending_time: { + gte: lastSyncDate, + }, + }, }, }, }); diff --git a/src/services/brevo/brevo-api.ts b/src/services/brevo/brevo-api.ts index ed4b9618..8863e7e9 100644 --- a/src/services/brevo/brevo-api.ts +++ b/src/services/brevo/brevo-api.ts @@ -1,4 +1,4 @@ -export const brevoSendEmail = async (to: string | string[], templateId: number, params?: Record) => { +export const brevoSendEmail = async (to: string, templateId: number, params?: Record) => { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 5000); @@ -10,7 +10,7 @@ export const brevoSendEmail = async (to: string | string[], templateId: number, "Content-Type": "application/json", }, body: JSON.stringify({ - to: Array.isArray(to) ? to.map((email) => ({ email })) : [{ email: to }], + to: [{ email: to }], templateId: templateId, ...(params && { params }), }), diff --git a/src/services/brevo/index.ts b/src/services/brevo/index.ts index e48e2c55..58ce48d6 100644 --- a/src/services/brevo/index.ts +++ b/src/services/brevo/index.ts @@ -68,14 +68,14 @@ export class EmailService { userProjetId, extra, }: { - to: string | string[]; + to: string; emailType: emailType; params?: Record; userProjetId?: number; extra?: any; }): Promise { const { templateId } = this.templates[emailType]; - const dbEmails = await createEmail(to, emailType, userProjetId, extra); + const dbEmail = await createEmail(to, emailType, userProjetId, extra); try { const response = await brevoSendEmail(to, templateId, params); @@ -87,14 +87,12 @@ export class EmailService { const data = await response.json(); - const updatedEmails = await Promise.all( - dbEmails.map((dbEmail) => this.updateEmailStatus(dbEmail.id, emailStatus.SUCCESS, data.messageId)), - ); + const email = await this.updateEmailStatus(dbEmail.id, emailStatus.SUCCESS, data.messageId); - return { type: "success", message: "EMAIL_SENT", email: updatedEmails[0] }; + return { type: "success", message: "EMAIL_SENT", email }; } catch (error) { captureError("Erreur lors de l'envoi du mail : ", error); - await Promise.all(dbEmails.map((dbEmail) => this.updateEmailStatus(dbEmail.id, emailStatus.ERROR))); + await this.updateEmailStatus(dbEmail.id, emailStatus.ERROR); return { type: "error", message: "TECHNICAL_ERROR" }; } } From 33fdede78d94d1bb1e635ded59a0175f5be4a795 Mon Sep 17 00:00:00 2001 From: mehdilouraoui Date: Wed, 11 Dec 2024 18:16:21 +0100 Subject: [PATCH 16/19] fix: update logic --- .../migration.sql | 5 +++ scripts/csm-mail-batch.ts | 9 +--- src/helpers/dateUtils.ts | 2 + src/lib/prisma/prisma-email-queries.ts | 45 +++++++++++-------- src/services/brevo/index.ts | 39 +++++++++------- 5 files changed, 58 insertions(+), 42 deletions(-) create mode 100644 prisma/migrations/20241211145747_add_user_id_to_email_table/migration.sql diff --git a/prisma/migrations/20241211145747_add_user_id_to_email_table/migration.sql b/prisma/migrations/20241211145747_add_user_id_to_email_table/migration.sql new file mode 100644 index 00000000..4f4bed94 --- /dev/null +++ b/prisma/migrations/20241211145747_add_user_id_to_email_table/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "email" ADD COLUMN "user_id" TEXT; + +-- AddForeignKey +ALTER TABLE "email" ADD CONSTRAINT "email_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/scripts/csm-mail-batch.ts b/scripts/csm-mail-batch.ts index 1f13635b..6b247ebc 100644 --- a/scripts/csm-mail-batch.ts +++ b/scripts/csm-mail-batch.ts @@ -1,5 +1,5 @@ import { getLastCsmMailBatch, saveCronJob } from "@/src/lib/prisma/prisma-cron-jobs-queries"; -import { captureError, customCaptureException } from "@/src/lib/sentry/sentryCustomMessage"; +import { customCaptureException } from "@/src/lib/sentry/sentryCustomMessage"; import { EmailService } from "@/src/services/brevo"; const main = async () => { @@ -16,12 +16,7 @@ const main = async () => { const INACTIVITY_DAYS = 10; console.log(`Recherche des utilisateurs inactifs depuis ${INACTIVITY_DAYS} jours...`); - const { success, message } = await emailService.sendNoActivityAfterSignupEmail(lastSyncDate, INACTIVITY_DAYS); - if (!success) { - captureError(message, { executionTime: new Date() }); - process.exit(1); - } - console.log(message); + await emailService.sendNoActivityAfterSignupEmail(lastSyncDate, INACTIVITY_DAYS); await saveCronJob(startedDate, new Date(), "CSM_MAIL_BATCH"); console.log("Batch des mails CSM réussi !"); diff --git a/src/helpers/dateUtils.ts b/src/helpers/dateUtils.ts index 799f084d..efecb00c 100644 --- a/src/helpers/dateUtils.ts +++ b/src/helpers/dateUtils.ts @@ -2,6 +2,8 @@ import type { Attribute } from "@strapi/strapi"; export const FAR_FUTURE = new Date(3024, 0, 0, 1); +export const removeDaysToDate = (date: Date, nbDays: number) => new Date(date.getTime() - nbDays * 24 * 60 * 60 * 1000); + export function monthDateToString(value: Date | null | undefined): string { return value ? `${value.getFullYear()}-${("0" + (value.getMonth() + 1)).slice(-2)}` : ""; } diff --git a/src/lib/prisma/prisma-email-queries.ts b/src/lib/prisma/prisma-email-queries.ts index 006db48c..ba0f768f 100644 --- a/src/lib/prisma/prisma-email-queries.ts +++ b/src/lib/prisma/prisma-email-queries.ts @@ -1,5 +1,6 @@ import { email, emailStatus, emailType, User } from "@prisma/client"; import { prismaClient } from "./prismaClient"; +import { removeDaysToDate } from "@/src/helpers/dateUtils"; export const updateEmailStatus = async (id: string, status: emailStatus, brevoId?: string): Promise => { return prismaClient.email.update({ @@ -11,19 +12,27 @@ export const updateEmailStatus = async (id: string, status: emailStatus, brevoId }); }; -export const createEmail = async ( - destinationAddress: string, - type: emailType, - userProjetId?: number, - extra?: any, -): Promise => { +export const createEmail = async ({ + to, + emailType, + userProjetId, + userId, + extra, +}: { + to: string; + emailType: emailType; + userProjetId?: number; + userId?: string; + extra?: any; +}): Promise => { return prismaClient.email.create({ data: { - destination_address: destinationAddress, + destination_address: to, user_projet_id: userProjetId, - type: type, + type: emailType, email_status: emailStatus.PENDING, extra: extra, + user_id: userId, }, }); }; @@ -35,26 +44,24 @@ export const getLastEmailForUserProjet = async (userProjetId: number, emailType: }); }; -export const getUserWithNoActivityAfterSignup = async ( - lastSyncDate: Date, - inactivityDays = 10, -): Promise => { - const SINCE_DAYS_AGO = new Date(Date.now() - inactivityDays * 24 * 60 * 60 * 1000); - +export const getUserWithNoActivityAfterSignup = async (lastSyncDate: Date, inactivityDays = 10): Promise => { return prismaClient.user.findMany({ where: { created_at: { - lt: SINCE_DAYS_AGO, + gte: removeDaysToDate(lastSyncDate, inactivityDays), + lte: removeDaysToDate(new Date(), inactivityDays), }, projets: { - none: {}, + every: { + deleted_at: { + not: null, + }, + }, }, emails: { none: { type: emailType.noActivityAfterSignup, - sending_time: { - gte: lastSyncDate, - }, + email_status: emailStatus.SUCCESS, }, }, }, diff --git a/src/services/brevo/index.ts b/src/services/brevo/index.ts index 0a02a3d2..1d493341 100644 --- a/src/services/brevo/index.ts +++ b/src/services/brevo/index.ts @@ -67,15 +67,17 @@ export class EmailService { params, userProjetId, extra, + userId, }: { to: string; emailType: emailType; params?: Record; userProjetId?: number; + userId?: string; extra?: any; }): Promise { const { templateId } = this.templates[emailType]; - const dbEmail = await createEmail(to, emailType, userProjetId, extra); + const dbEmail = await createEmail({ to, emailType, userProjetId, userId, extra }); try { const response = await brevoSendEmail(to, templateId, params); @@ -183,24 +185,29 @@ export class EmailService { async sendNoActivityAfterSignupEmail(lastSyncDate: Date, inactivityDays = 10) { const users = await getUserWithNoActivityAfterSignup(lastSyncDate, inactivityDays); - const usersEmail = users?.map((user) => user.email); - if (!usersEmail?.length) { + if (!users?.length) { return { message: "Aucun utilisateur trouvé." }; } else { - const response = await this.sendEmail({ - to: usersEmail, - emailType: emailType.noActivityAfterSignup, - }); - - if (response.type === "success") { - return { message: `Email(s) envoyé(s) à ${usersEmail.length} utilisateur(s).`, success: true }; - } else { - return { - message: "Erreur lors de l'envoi des emails des utilisateurs inactifs depuis leur inscription.", - success: false, - }; - } + const results = await Promise.all( + users.map(async (user) => { + const result = await this.sendEmail({ + to: user.email, + userId: user.id, + emailType: emailType.noActivityAfterSignup, + params: { + NOM: user.nom || "", + }, + }); + + if (result.type === "success") { + console.log(`Email envoyé à ${user.email} - type: ${emailType.noActivityAfterSignup}`); + } + + return result; + }), + ); + return results; } } } From baf1ff615ca780d0f73410436bf3ebee8c96a594 Mon Sep 17 00:00:00 2001 From: mehdilouraoui Date: Thu, 12 Dec 2024 11:08:14 +0100 Subject: [PATCH 17/19] feat: add user id in email --- src/lib/prisma/prisma-email-queries.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/lib/prisma/prisma-email-queries.ts b/src/lib/prisma/prisma-email-queries.ts index ba0f768f..46e0d9e7 100644 --- a/src/lib/prisma/prisma-email-queries.ts +++ b/src/lib/prisma/prisma-email-queries.ts @@ -52,11 +52,7 @@ export const getUserWithNoActivityAfterSignup = async (lastSyncDate: Date, inact lte: removeDaysToDate(new Date(), inactivityDays), }, projets: { - every: { - deleted_at: { - not: null, - }, - }, + none: {}, }, emails: { none: { From cb1d7ab16ddf608109018cba6240f2d986ca28b0 Mon Sep 17 00:00:00 2001 From: mehdilouraoui Date: Thu, 12 Dec 2024 16:14:43 +0100 Subject: [PATCH 18/19] feat: add created_at user in params --- scripts/csm-mail-batch.ts | 3 ++- .../aide/aide-estimations-card-warning-remaining-day.tsx | 2 +- .../tableau-de-bord/tableau-de-bord-maturite.tsx | 4 ++-- src/helpers/common.tsx | 9 --------- src/helpers/dateUtils.ts | 9 +++++++++ src/services/brevo/index.ts | 7 +++++-- 6 files changed, 19 insertions(+), 15 deletions(-) diff --git a/scripts/csm-mail-batch.ts b/scripts/csm-mail-batch.ts index 6b247ebc..95ba523f 100644 --- a/scripts/csm-mail-batch.ts +++ b/scripts/csm-mail-batch.ts @@ -1,3 +1,4 @@ +import { removeDaysToDate } from "@/src/helpers/dateUtils"; import { getLastCsmMailBatch, saveCronJob } from "@/src/lib/prisma/prisma-cron-jobs-queries"; import { customCaptureException } from "@/src/lib/sentry/sentryCustomMessage"; import { EmailService } from "@/src/services/brevo"; @@ -12,7 +13,7 @@ const main = async () => { const emailService = new EmailService(); const startedDate = new Date(); const lastSync = await getLastCsmMailBatch(); - const lastSyncDate = lastSync?.execution_end_time ?? new Date(0); + const lastSyncDate = lastSync?.execution_end_time ?? removeDaysToDate(new Date(), 10); const INACTIVITY_DAYS = 10; console.log(`Recherche des utilisateurs inactifs depuis ${INACTIVITY_DAYS} jours...`); diff --git a/src/components/financement/aide/aide-estimations-card-warning-remaining-day.tsx b/src/components/financement/aide/aide-estimations-card-warning-remaining-day.tsx index f5f5c56b..e98f0fc9 100644 --- a/src/components/financement/aide/aide-estimations-card-warning-remaining-day.tsx +++ b/src/components/financement/aide/aide-estimations-card-warning-remaining-day.tsx @@ -1,4 +1,4 @@ -import { daysUntilDate } from "@/src/helpers/common"; +import { daysUntilDate } from "@/src/helpers/dateUtils"; import clsx from "clsx"; export const AideEstimationsCardWarningRemainingDays = ({ diff --git a/src/components/tableau-de-bord/tableau-de-bord-maturite.tsx b/src/components/tableau-de-bord/tableau-de-bord-maturite.tsx index 290f86b2..1a4a12e7 100644 --- a/src/components/tableau-de-bord/tableau-de-bord-maturite.tsx +++ b/src/components/tableau-de-bord/tableau-de-bord-maturite.tsx @@ -1,8 +1,8 @@ import { useProjetsStore } from "@/src/stores/projets/provider"; import { Maturite } from "../maturite/maturite"; -import { daysUntilDate } from "@/src/helpers/common"; + import { Spinner } from "../common/spinner"; -import { getRelativeDate } from "@/src/helpers/dateUtils"; +import { daysUntilDate, getRelativeDate } from "@/src/helpers/dateUtils"; export const TableauDeBordMaturite = () => { const projet = useProjetsStore((state) => state.getCurrentProjet()); diff --git a/src/helpers/common.tsx b/src/helpers/common.tsx index fe8d3237..46bbdfb9 100644 --- a/src/helpers/common.tsx +++ b/src/helpers/common.tsx @@ -25,15 +25,6 @@ export const formatNumberWithSpaces = (num?: number | string): string => (num ? export const nullFunctionalComponent = () => <>; -export const daysUntilDate = (targetDate: Date | null): number | null => { - if (!targetDate) { - return null; - } - const MS_PER_DAY = 1000 * 60 * 60 * 24; - - return Math.ceil((targetDate.getTime() - new Date().getTime()) / MS_PER_DAY); -}; - export const extractNameInitiales = (name: string) => { const match = name.match(/^[^\s-]+|\S+$/g); return match ? match.map((word) => word[0].toUpperCase()).join("") : ""; diff --git a/src/helpers/dateUtils.ts b/src/helpers/dateUtils.ts index efecb00c..6696f377 100644 --- a/src/helpers/dateUtils.ts +++ b/src/helpers/dateUtils.ts @@ -41,3 +41,12 @@ export const getRelativeDate = (lastUpdate?: number | null) => !lastUpdate ? "Aujourd'hui" : lastUpdate === 1 ? "Hier" : `Il y a ${lastUpdate} jours`; const addLeadingZero = (value: number): string => ("0" + value).slice(-2); + +export const daysUntilDate = (targetDate: Date | null): number | null => { + if (!targetDate) { + return null; + } + const MS_PER_DAY = 1000 * 60 * 60 * 24; + + return Math.ceil((targetDate.getTime() - new Date().getTime()) / MS_PER_DAY); +}; diff --git a/src/services/brevo/index.ts b/src/services/brevo/index.ts index 1d493341..7659dbb6 100644 --- a/src/services/brevo/index.ts +++ b/src/services/brevo/index.ts @@ -12,6 +12,7 @@ import { UserProjetWithRelations, UserWithCollectivite } from "@/src/lib/prisma/ import { getPrimaryCollectiviteForUser } from "@/src/helpers/user"; import { PFMV_ROUTES } from "@/src/helpers/routes"; import { ContactFormData } from "@/src/forms/contact/contact-form-schema"; +import { daysUntilDate } from "@/src/helpers/dateUtils"; interface Templates { templateId: number; @@ -185,7 +186,8 @@ export class EmailService { async sendNoActivityAfterSignupEmail(lastSyncDate: Date, inactivityDays = 10) { const users = await getUserWithNoActivityAfterSignup(lastSyncDate, inactivityDays); - + const mails = users.map((user) => user.email); + console.log(mails); if (!users?.length) { return { message: "Aucun utilisateur trouvé." }; } else { @@ -196,7 +198,8 @@ export class EmailService { userId: user.id, emailType: emailType.noActivityAfterSignup, params: { - NOM: user.nom || "", + nom: user.nom || "", + date_creation_compte: Math.abs(daysUntilDate(user.created_at)!)?.toString() || "10", }, }); From b19a2c5c6032be336719973c195175e16f6a9550 Mon Sep 17 00:00:00 2001 From: mehdilouraoui Date: Fri, 13 Dec 2024 10:43:23 +0100 Subject: [PATCH 19/19] update following comments --- cron.json | 2 +- src/services/brevo/index.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cron.json b/cron.json index 29116012..109e3d3d 100644 --- a/cron.json +++ b/cron.json @@ -4,7 +4,7 @@ "command": "0 0 * * * npm run hubspot-sync" }, { - "command": "0 0 * * * npm run csm-mail-batch" + "command": "0 1 * * * npm run csm-mail-batch" } ] } diff --git a/src/services/brevo/index.ts b/src/services/brevo/index.ts index 18f540ef..bbbe2f3a 100644 --- a/src/services/brevo/index.ts +++ b/src/services/brevo/index.ts @@ -13,7 +13,7 @@ import { getPrimaryCollectiviteForUser } from "@/src/helpers/user"; import { getFullUrl, PFMV_ROUTES } from "@/src/helpers/routes"; import { ContactFormData } from "@/src/forms/contact/contact-form-schema"; import { getProjetsForProjetCreationEmail } from "@/src/lib/prisma/prismaProjetQueries"; -import { daysUntilDate, removeDaysToDate } from "@/src/helpers/dateUtils"; +import { daysUntilDate } from "@/src/helpers/dateUtils"; import { getRetoursExperiences } from "@/src/lib/strapi/queries/retoursExperienceQueries"; import { RetourExperienceResponse } from "@/src/components/ficheSolution/type"; import shuffle from "lodash/shuffle"; @@ -266,7 +266,7 @@ export class EmailService { emailType: emailType.noActivityAfterSignup, params: { nom: user.nom || "", - date_creation_compte: Math.abs(daysUntilDate(user.created_at)!)?.toString() || "10", + dateCreationCompte: Math.abs(daysUntilDate(user.created_at)!)?.toString() || "10", }, });