Skip to content

Commit

Permalink
feat : relances automatiques par emails si pas de données / configura…
Browse files Browse the repository at this point in the history
…tion (#3292)
  • Loading branch information
totakoko authored Oct 18, 2023
1 parent a91d1ba commit d7ebce7
Show file tree
Hide file tree
Showing 15 changed files with 860 additions and 297 deletions.
2 changes: 2 additions & 0 deletions .talismanrc
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ fileignoreconfig:
checksum: c2769caf07b18b075ef9164b3711b7af46b1fac6de841ba66e377a31bcacf070
- filename: server/tests/integration/http/password.route.test.ts
checksum: 5dac6833c2dc7f76b3c8b74c3e102968cfcb69bd1073046bcd434f44717f1384
- filename: server/tests/integration/jobs/emails/reminder.test.ts
checksum: 6268b3f12640edc2569bfd412081dc9d8092b15140e07fa01a29697f3d476158
- filename: server/tests/integration/jobs/ingestion/process-ingestion.test.ts
checksum: af289c4843ed8ccada8725ee7bcddfe595a1b1ed4318e557ee19d4b103f3d24f
- filename: shared/constants/plausible-goals.ts
Expand Down
2 changes: 1 addition & 1 deletion server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
"lodash.sortby": "4.7.0",
"luxon": "2.3.0",
"migrate-mongo": "10.0.0",
"mjml": "4.11.0",
"mjml": "4.14.1",
"mongodb": "5.7.0",
"multiparty": "4.2.3",
"node-ovh-objectstorage": "2.0.5",
Expand Down
9 changes: 6 additions & 3 deletions server/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -621,9 +621,12 @@ program
.option("-q, --queued", "Run job asynchronously", false)
.action(createJobAction("fiabilisation:stats"));

/**
* Job d'affichage des stats fiabilisation
*/
program
.command("send-reminder-emails")
.description("Envoi des emails de relance")
.option("-q, --queued", "Run job asynchronously", false)
.action(createJobAction("send-reminder-emails"));

program
.command("dev:generate-ts-types")
.description("Generation des types TS à partir des schemas de la base de données")
Expand Down
88 changes: 87 additions & 1 deletion server/src/common/actions/organismes/organismes.actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { getMetiersBySiret } from "@/common/apis/apiLba";
import logger from "@/common/logger";
import { EffectifsQueue } from "@/common/model/@types/EffectifsQueue";
import { Organisme } from "@/common/model/@types/Organisme";
import { organismesDb, effectifsDb, organisationsDb } from "@/common/model/collections";
import { organismesDb, effectifsDb, organisationsDb, usersMigrationDb } from "@/common/model/collections";
import { AuthContext } from "@/common/model/internal/AuthContext";
import { OrganisationOrganismeFormation } from "@/common/model/organisations.model";
import { defaultValuesOrganisme } from "@/common/model/organismes.model";
Expand Down Expand Up @@ -691,6 +691,21 @@ export async function resetConfigurationERP(ctx: AuthContext, organismeId: Objec
},
}
);

// reset reminder states
await usersMigrationDb().updateMany(
{
_id: {
$in: await getMemberIdsOfOrganisme(organismeId),
},
},
{
$unset: {
reminder_missing_data_sent_date: 1,
reminder_missing_configuration_and_data_sent_date: 1,
},
}
);
}

export async function verifyOrganismeAPIKeyToUser(
Expand Down Expand Up @@ -936,3 +951,74 @@ export async function getInvalidSiretsFromDossierApprenant(data: Partial<Effecti
}
return invalidsSirets;
}

export async function getMemberIdsOfOrganisme(organismeId: ObjectId): Promise<ObjectId[]> {
const res = await organismesDb()
.aggregate<{ _id: ObjectId }>([
{
$match: {
_id: organismeId,
},
},
{
$project: {
siret: 1,
uai: 1,
},
},
{
$lookup: {
from: "organisations",
as: "organisation",
let: {
uai: { $ifNull: ["$uai", null] }, // on force par défaut à null plutôt que undefined pour correspondre avec l'organisation
siret: "$siret",
},
pipeline: [
{
$match: {
$expr: {
$and: [{ $eq: ["$siret", "$$siret"] }, { $eq: ["$uai", "$$uai"] }],
},
},
},
],
},
},
{ $unwind: { path: "$organisation" } },
{
$lookup: {
from: "usersMigration",
as: "users",
let: {
organisation_id: "$organisation._id",
},
pipeline: [
{
$match: {
$expr: {
$eq: ["$organisation_id", "$$organisation_id"],
},
},
},
{
$project: {
_id: 1,
},
},
],
},
},
{
$unwind: "$users",
},
{
$replaceRoot: {
newRoot: "$users",
},
},
])
.toArray();

return res.map((doc) => doc._id);
}
8 changes: 8 additions & 0 deletions server/src/common/model/@types/UsersMigration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,14 @@ export interface UsersMigration {
* Date de dernière mise à jour mot de passe
*/
password_updated_at?: Date;
/**
* Date d'envoi de la relance email pour données manquantes
*/
reminder_missing_data_sent_date?: Date;
/**
* Date d'envoi de la relance email pour configuration et données manquantes
*/
reminder_missing_configuration_and_data_sent_date?: Date;
emails?: {
token: string;
templateName: string;
Expand Down
4 changes: 4 additions & 0 deletions server/src/common/model/usersMigration.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ export const schema = object(
connection_history: arrayOf(date(), { description: "Historique des dates de connexion" }),
invalided_token: boolean({ description: "true si besoin de reset le token" }),
password_updated_at: date({ description: "Date de dernière mise à jour mot de passe" }),
reminder_missing_data_sent_date: date({ description: "Date d'envoi de la relance email pour données manquantes" }),
reminder_missing_configuration_and_data_sent_date: date({
description: "Date d'envoi de la relance email pour configuration et données manquantes",
}),
emails: arrayOf(
object(
{
Expand Down
19 changes: 19 additions & 0 deletions server/src/common/services/mailer/mailer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ const templatesTitleFuncs: TemplateTitleFuncs = {
notify_access_granted: () => "Votre demande d'accès a été acceptée",
notify_access_rejected: () => "Votre demande d'accès a été refusée",
notify_invitation_rejected: () => "Votre invitation à rejoindre le tableau de bord de l'apprentissage n'a pas abouti",
reminder_missing_configuration_and_data: () => "Finalisez votre configuration de votre moyen de transmission",
reminder_missing_data: () => "Nous n'avons pas reçu vos effectifs",
reset_password: () => "Réinitialisation du mot de passe",
validation_user_by_orga_gestionnaire: (payload) =>
`Demande d'accès à votre organisation ${payload.organisationLabel}`,
Expand Down Expand Up @@ -121,6 +123,23 @@ export type TemplatePayloads = {
email: string;
};
};
reminder_missing_configuration_and_data: {
recipient: {
civility: string;
nom: string;
prenom: string;
};
};
reminder_missing_data: {
recipient: {
civility: string;
nom: string;
prenom: string;
};
mode_de_transmission: string;
erp: string;
erp_unsupported: string;
};
reset_password: {
recipient: {
civility: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Db } from "mongodb";

export const up = async (db: Db) => {
// réinitialise la date de transmission quand 0 effectifs et last_transmission_date défini (10 en prod)
await db.collection("organismes").updateMany(
{
effectifs_count: 0,
last_transmission_date: {
$exists: true,
},
},
{
$unset: {
first_transmission_date: 1,
last_transmission_date: 1,
},
},
{
bypassDocumentValidation: true,
}
);
};
165 changes: 165 additions & 0 deletions server/src/jobs/emails/reminder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { differenceInDays } from "date-fns";
import { ERPS_BY_ID } from "shared";

import parentLogger from "@/common/logger";
import { usersMigrationDb } from "@/common/model/collections";
import { sendEmail } from "@/common/services/mailer/mailer";

const logger = parentLogger.child({
module: "job:send-reminder-emails",
});

export async function sendReminderEmails() {
// envoi séquentiel par précaution pour ne pas surcharger le SMTP

const users = await usersMigrationDb()
.aggregate([
{
$project: {
_id: 1,
email: 1,
civility: 1,
prenom: 1,
nom: 1,
organisation_id: 1,
created_at: 1,
reminder_missing_data_sent_date: 1,
reminder_missing_configuration_and_data_sent_date: 1,
},
},
{
$lookup: {
from: "organisations",
localField: "organisation_id",
foreignField: "_id",
as: "organisation",
pipeline: [
{
$match: {
type: "ORGANISME_FORMATION",
},
},
{
$lookup: {
from: "organismes",
as: "organisme",
let: {
uai: "$uai",
siret: "$siret",
},
pipeline: [
{
$match: {
$expr: {
$and: [{ $eq: ["$siret", "$$siret"] }, { $eq: ["$uai", "$$uai"] }],
},
},
},
{
$project: {
_id: 1,
siret: 1,
uai: 1,
last_transmission_date: 1,
mode_de_transmission: 1,
mode_de_transmission_configuration_date: 1,
erps: 1,
erp_unsupported: 1,
},
},
],
},
},
{ $unwind: { path: "$organisme" } },
],
},
},
{ $unwind: { path: "$organisation" } },
])
.toArray();

logger.info({ count: users.length }, "fetched OFA users");

// séquentiel car on n'utilise pas de pool de connexion au serveur SMTP
// et l'envoi d'un email prend ~1s
for (const user of users) {
const organisme = user.organisation.organisme;

// relance après 7 jours si pas de configuration ni de données
if (
!organisme.mode_de_transmission &&
!organisme.last_transmission_date &&
!user.reminder_missing_configuration_and_data_sent_date &&
differenceInDays(new Date(), user.created_at) >= 7
) {
logger.info(
{
user_id: user._id,
organisme_id: organisme._id,
siret: organisme.siret,
uai: organisme.uai,
},
"send email reminder_missing_configuration_and_data"
);
await sendEmail(user.email, "reminder_missing_configuration_and_data", {
recipient: {
civility: user.civility,
nom: user.nom,
prenom: user.prenom,
},
});
await usersMigrationDb().updateOne(
{
_id: user._id,
},
{
$set: {
reminder_missing_configuration_and_data_sent_date: new Date(),
},
}
);
}

// relance après 7 jours si organisme configuré mais pas de données
else if (
organisme.mode_de_transmission &&
!organisme.last_transmission_date &&
!user.reminder_missing_data_sent_date &&
differenceInDays(new Date(), organisme.mode_de_transmission_configuration_date) >= 7
) {
logger.info(
{
user_id: user._id,
organisme_id: organisme._id,
siret: organisme.siret,
uai: organisme.uai,
mode_de_transmission: organisme.mode_de_transmission,
mode_de_transmission_configuration_date: organisme.mode_de_transmission_configuration_date,
erp: organisme.erps?.[0],
erp_unsupported: organisme.erp_unsupported,
},
"send email reminder_missing_data"
);
await sendEmail(user.email, "reminder_missing_data", {
recipient: {
civility: user.civility,
nom: user.nom,
prenom: user.prenom,
},
mode_de_transmission: organisme.mode_de_transmission,
erp: ERPS_BY_ID[organisme.erps?.[0]]?.name ?? "",
erp_unsupported: organisme.erp_unsupported,
});
await usersMigrationDb().updateOne(
{
_id: user._id,
},
{
$set: {
reminder_missing_data_sent_date: new Date(),
},
}
);
}
}
}
Loading

0 comments on commit d7ebce7

Please sign in to comment.