Système de suivi des binômes
Dernière mise à jour : 13/04/2026
Vue d'ensemble
Le suivi des binômes est un système automatisé qui envoie des messages (emails / SMS) aux jeunes et bénévoles à des étapes clés de leur mentorat. Il permet de :
- Détecter les binômes inactifs ou en difficulté
- Suivre l'avancement de l'accompagnement
- Collecter les retours des deux parties (statut + commentaire libre)
- Déclencher des actions admin automatiques selon les réponses
Architecture technique
Schéma de données
step_sequence → Séquence de suivi (ex: DEMA1N, MVLS)
├── steps → Étapes individuelles (J+15, J+45, J+90…)
│ ├── step_message (jeuneStep) → Config message jeune
│ │ └── message_template → Template email/SMS
│ ├── step_message (benevoleStep)→ Config message bénévole
│ │ └── message_template → Template email/SMS
│ └── suivistatus[] → Enregistrements de suivi créés
├── extendedStepInterval → Intervalle de prolongation (jours)
└── binomes[] → Binômes rattachés
suivistatus
├── status → Statut global (EN_ATTENTE, ACTIF, INACTIF, AMBIGU)
├── statusJeune → Statut côté jeune (EN_ATTENTE, RAS, Inactif)
├── statusBenevole → Statut côté bénévole
├── commentJeune → Commentaire libre jeune
├── commentBenevole → Commentaire libre bénévole
├── dayStep → Jour de déclenchement (ex: 15, 45, 90…)
└── readJeune / readBenevole → Dates de lecture admin
step_message_binome → Journal des messages envoyés
├── dateEnvoi → Date d'envoi (toujours renseigné, envoi immédiat)
├── destination → Email ou téléphone
├── error → true = erreur à l'envoi
├── message → Contenu SMS ou erreur (vide pour Brevo)
├── subject → Sujet du mail / identifiant du template
└── dayStep → Jour de la step
Tables clés
| Table | Rôle |
|---|---|
step_sequence |
Séquence de suivi (nom, intervalle prolongation) |
steps |
Étape dans une séquence (jour, suivi activé, étendu) |
step_message |
Configuration d'un message (type mail/sms, destination jeune/bénévole, template) |
message_template |
Template du message (HTML ou Brevo template ID) |
step_message_binome |
Journal effectif des messages envoyés par binôme |
suivistatus |
Enregistrement de suivi par binôme et étape |
Séquences
La sélection de la séquence se fait via StepSequenceService.getStepSequenceBySandbox() :
- Sandbox MVLS → séquence nommée 'MVLS'
- Sandbox DEMA1N (ou autre) → séquence avec id: '3' (hardcodé)
Flux complet : du cron à la réponse
1. Cron prepareMessages
Fichier : back/src/binomes/services/cron-tasks.service.ts
Fréquence : toutes les 30 minutes ('2 */30 * * * *'), sur le Worker uniquement (WORKER_SERVER === '1')
Après chaque exécution, countRecentMessages(startedAt) compte les messages envoyés pendant cette exécution et les affiche dans les logs du worker.
Algorithme
1. Récupérer tous les binômes ACTIF/INACTIF/BAD/EN_ATTENTE/AMBIGU
dont nextStepDate ≤ aujourd'hui OU nextStepDate IS NULL
2. Grouper par sandbox
3. Pour chaque sandbox → charger la séquence + steps + templates
4. Pour chaque binôme :
a. Si nextStepDate est null → recalculer (recalculateDateAndLastStep)
b. Calculer dayFromStart = jours depuis creationDate
c. Calculer dayOfNextStep = jours entre creationDate et nextStepDate
d. Si dayFromStart < dayOfNextStep → recalculer
e. Chercher la step correspondant à dayOfNextStep :
- Si trouvée et pas encore de suivistatus → launchStep
- Si dayOfNextStep > dernière step → lancer step étendue (prolongation)
- Sinon → recalculer
f. Mettre à jour nextStepDate et lastStepDone
Détail de launchStep
1. Charger les relations manquantes du binôme (jeune, bénévole, admin)
2. Créer un suivistatus (EN_ATTENTE si suiviActivated)
3. Si suiviActivated :
- Passer le binôme en statut EN_ATTENTE
- Logique spéciale J+15 (VIP/partenaire) et J+45 (programme EL)
→ passage en state "Appel de suivi"
4. Pour chaque message (jeune + bénévole) de la step :
- Déterminer le destinataire et l'URL
- Appeler createMessage
2. Création des messages (createMessage)
Fichier : back/src/binomes/services/messages.service.ts (ligne 671)
Signature :
createMessage(template, destination, binome, partUrl, step, messageType, dayStep?, suiviId?, destinationTypeParam?)
Paramètres :
| Paramètre | Type | Provenance |
|---|---|---|
template |
MessageTemplate |
Vient de step.jeuneStep.template ou step.benevoleStep.template |
destination |
string |
Email ou téléphone, déterminé par getDestination() |
binome |
Binome |
Le binôme courant (avec relations chargées) |
partUrl |
string |
/jeune/{jeuneId} ou /benevole/{benevoleId} — sert à construire les URLs de réponse |
step |
Steps |
La step en cours de traitement |
messageType |
string |
'mail' ou 'sms' (vient de StepMessage.type) |
dayStep |
number? |
Jour depuis la création du binôme (ex: 15, 45, 90…) |
suiviId |
string? |
ID du suivistatus créé par launchStep (pour les URLs MVLS) |
destinationTypeParam |
'jeune' \| 'benevole'? |
Type explicite du destinataire |
Algorithme détaillé
1. BRANCHEMENT : template.brevoTemplateId existe ?
┌─ OUI → CHEMIN BREVO ─────────────────────────────────────────────┐
│ │
│ a. Déterminer le destinationType ('jeune' ou 'benevole') │
│ Priorité : │
│ 1) Paramètre explicite destinationTypeParam │
│ 2) Nom du template contient '-jeune' ou '-benevole' │
│ 3) Fallback : comparaison email destination vs email jeune │
│ │
│ b. Construire les variables Brevo │
│ - MVLS → prepareMvlsBrevoParams(binome, destinationType, …) │
│ - DEMA1N → prepareSuiviBrevoParams(binome, step, dayStep, partUrl) │
│ │
│ c. Nettoyer l'email (retirer le préfixe 'mvls_') │
│ │
│ d. Valider brevoTemplateId (conversion Number, check NaN) │
│ │
│ e. Appeler mailService.sendForTemplate({ │
│ templateId, messageVersions: [{ to, params }] │
│ }) │
│ → Envoi IMMÉDIAT via API Brevo POST /smtp/email │
│ → En dev/staging : redirige vers Mailcatcher via │
│ sendBrevoTemplateViaMailcatcher │
│ │
│ f. Créer StepMessageBinome (journal) : │
│ - Succès : dateEnvoi=now, error=false, message='' │
│ - Échec : dateEnvoi=now, error=true, message=erreur │
│ │
│ g. return (sortie de la fonction) │
└────────────────────────────────────────────────────────────────────┘
┌─ NON + messageType === 'sms' → CHEMIN SMS (IMMÉDIAT) ─────────────┐
│ │
│ a. Construire le contenu SMS : │
│ mailService.replaceVariables(template.html, {jeune,benevole, │
│ admin}) │
│ │
│ b. Envoyer via smsService.send(PhoneData) │
│ → Envoi IMMÉDIAT │
│ │
│ c. Créer StepMessageBinome (journal) : │
│ - Succès : dateEnvoi=now, error=false, message=contenu SMS │
│ - Échec : dateEnvoi=now, error=true, message=erreur │
│ │
│ d. return (sortie de la fonction) │
└────────────────────────────────────────────────────────────────────┘
┌─ SINON → ALERTE (template mail sans brevoTemplateId) ────────────┐
│ │
│ Tous les templates mail doivent avoir un brevoTemplateId. │
│ Si ce cas se présente : │
│ → console.error + Sentry.captureMessage (niveau error) │
│ → Le message n'est PAS envoyé │
└────────────────────────────────────────────────────────────────────┘
2. CATCH : Si erreur à n'importe quelle étape
→ Sentry.captureException + log console
→ Le message n'est pas créé / partiellement créé
Trois chemins d'envoi — Résumé
| Critère | Chemin Brevo (email) | Chemin SMS | Fallback (alerte) |
|---|---|---|---|
| Condition | template.brevoTemplateId renseigné |
messageType === 'sms' |
Aucun des deux |
| Variables | prepareSuiviBrevoParams ou prepareMvlsBrevoParams |
replaceVariables sur template.html |
— |
| Envoi | Immédiat via API Brevo | Immédiat via smsService.send |
Pas d'envoi (alerte Sentry) |
| Journal | StepMessageBinome avec dateEnvoi: now |
StepMessageBinome avec dateEnvoi: now |
Aucun |
| Contenu stocké | message: '' (template dans Brevo) |
message: contenu SMS |
— |
Détermination du destinataire (getDestination)
Appelé dans launchStep avant createMessage. Lit StepMessage.type et StepMessage.destination :
Si type === 'mail' :
- destination 'jeune' → email = binome.jeune.user.email
partUrl = /jeune/{jeuneId}
- destination 'benevole' → email = binome.benevole.user.email
partUrl = /benevole/{benevoleId}
Si type === 'sms' :
- destination 'jeune' → phone = binome.jeune.phone
- destination 'benevole' → phone = binome.benevole.phone
Construction du hash de sécurité (suivi)
Le hash sert à authentifier les réponses sans connexion utilisateur :
Hash = MD5( binomeId + partUrl + '/step/' + stepId + '/' + dayStep )
Exemple pour un jeune, step 26, J+15 :
MD5( "42" + "/jeune/7" + "/step/" + "26" + "/15" )
= MD5( "42/jeune/7/step/26/15" )
Ce hash est inclus dans l'URL de chaque bouton du mail. Le ReponseController recalcule le même hash pour valider la réponse.
Variables Brevo (DEMA1N) — prepareSuiviBrevoParams
| Variable | Description |
|---|---|
PRENOM_JEUNE, NOM_JEUNE |
Identité jeune |
PRENOM_BENEVOLE, NOM_BENEVOLE |
Identité bénévole |
PRENOM_ADMIN, NOM_ADMIN, CODE_ADMIN |
Admin associé (fallback "L'équipe DEMA1N.org") |
schoolCursus, filiereJeune |
Infos scolaires |
besoinsJeune |
Besoins du jeune (noms séparés par des virgules) |
sousBesoinsJeune |
Sous-besoins du jeune (noms séparés par des retours à la ligne) |
moodJeune |
Humeur du jeune (ex: "Au top", "Non renseigné") |
secteurJeune |
Secteurs d'intérêt du jeune |
boutonProfilBenevole |
URL du profil bénévole (/compte/benevole/profil) |
boutonDashboardJeune |
URL du dashboard jeune (/compte/jeune/dashboard) |
boutonRasJeune / boutonRasBenevole |
Lien "Nous échangeons" / "On avance" |
boutonInactifJeune / boutonInactifBenevole |
Lien "Il y a un problème" |
boutonObjectifJeune / boutonObjectifBenevole |
Lien "Objectifs atteints" |
Ces boutons ne sont renseignés que si step.suiviActivated && partUrl (sinon absents du mail).
Variables Brevo (MVLS) — prepareMvlsBrevoParams
| Variable | Description |
|---|---|
PRENOM, PRENOM_admin |
Prénom du destinataire / admin |
prenom_mentor, tel_mentor, mail_mentor |
Infos mentor (email sans préfixe mvls_) |
PRENOM_LY, tel_LY, mail_LY |
Infos lycéen |
CP1 |
Date J+5 formatée (uniquement à dayStep === 0) |
contactok |
URL Inspire : réponse "contact ok" (token MD5 spécifique) |
nocontact |
URL Inspire : réponse "pas de contact" (token MD5 spécifique) |
Variables SMS
Les SMS utilisent mailService.replaceVariables(template.html, {jeune, benevole, admin}) pour injecter les données dans le template. Les variables disponibles sont les champs directs des entités (dot notation, ex: {{jeune.user.firstName}}).
3. Réponse au suivi — Flux email
Quand un jeune ou bénévole clique sur un bouton dans l'email de suivi, il est redirigé vers une URL de la forme :
/reponse/{type}/{id}/{actif}/{hash}?daystep={dayStep}
type:jeuneoubenevoleid: ID du jeune ou bénévoleactif:1(RAS/actif),0(Inactif),2(Objectif atteint)hash: MD5 de{binomeId}/{type}/{id}/step/{stepId}/{dayStep}daystep: jour de la step (query param)
Flux front
- Page
front/pages/reponse/_type/_id/_actif/_hash.vue - Au chargement :
POST /reponse→ enregistre le statut - Affichage conditionnel selon
actif: 1: message positif + champ commentaire libre0: sélecteur de raison + champ commentaire2: message objectif atteint + commentaire- Soumission du commentaire :
POST /reponse/comment
Flux backend (ReponseController)
POST /reponse (sans auth — accès via hash) :
1. Retrouve le jeune/bénévole avec ses binômes et suivistatus
2. Vérifie le hash MD5 pour identifier le binôme et la step
3. Vérifie que c'est le suivi le plus récent (anti-réponse sur ancien mail)
4. Appelle changeSuivistatus(statusId, type, status, state)
5. Retourne le CP actuel (CP1/CP2/CP3/CP4) pour le wording front
POST /reponse/comment (sans auth) :
1. Même vérification de hash
2. Enregistre le commentaire via suivistatusService.commentStatus()
3. Si bénévole + "Je souhaite que l'on m'appelle" → state "Appel de suivi"
4. Si bénévole + J≥45 + "on est en contact" → marque prise de contact
5. Si commentaire d'inactivité → émet un événement verbatim
5. Réponse au suivi — Flux dashboard
Les utilisateurs connectés peuvent aussi répondre depuis leur dashboard.
POST /reponse/dashboard (auth JWT) :
- Identifie jeune/bénévole via l'utilisateur connecté
- Appelle changeSuivistatus avec le suivistatusId fourni
POST /reponse/dashboard/comment (auth JWT) :
- Même principe pour le commentaire
6. Calcul du statut global (getSuivistatusStatus)
La combinaison des réponses jeune + bénévole donne le statut global :
| statusJeune | statusBenevole | → status global |
|---|---|---|
RAS |
RAS |
ACTIF |
RAS |
EN_ATTENTE |
ACTIF |
EN_ATTENTE |
RAS |
ACTIF |
EN_ATTENTE |
EN_ATTENTE |
EN_ATTENTE |
Inactif |
EN_ATTENTE |
INACTIF |
EN_ATTENTE |
Inactif |
INACTIF |
Inactif |
Inactif |
INACTIF |
RAS |
Inactif |
AMBIGU |
Inactif |
RAS |
AMBIGU |
Conséquences sur le state du binôme :
- INACTIF ou AMBIGU + state "Autonome" → state "A traiter"
- ACTIF → state "Autonome"
- actif = 2 (objectif atteint) → state "A cloturer"
7. Prolongation (steps étendues)
Quand toutes les steps définies d'une séquence sont épuisées, le système continue de créer des suivis à intervalle fixe (extendedStepInterval jours) en utilisant les steps marquées extended: true.
Cela ne s'applique pas au sandbox MVLS (pas de prolongation).
Affichage front
Dashboard jeune/bénévole
- Timeline (
parcours-timeline.vue) : affiche les 3 derniers suivis + le prochain - Réponse in-app (
cp-question.vue) : boutons RAS/Inactif sur l'étape courante - Carte binôme (
matchCardJeune/Benevole.vue) : nombre de jours avant le prochain suivi - Frise détaillée (
binomeSteps.vue) : tous les J+ avec icônes messages/commentaires
Back-office
- Liste binômes (
bo/binomes/index.vue) : colonne "étape de suivi" avec pastille notification - Fiche binôme (
bo/binomes/_id/index.vue) : frisebinomeStepscomplète - Logs (
bo/binomes/_id/logs.vue) : tableau desStepMessageBinome(date, destinataire, sujet, contenu)
Vuex
store/user.js: calcule les dates d'étapes lors dufetchFullUser, dispatchbinome/generateRealDatesstore/binome.js: stockestepsDates(gettergeneratedStepsDates) pour alimenter la timeline
Fichiers clés
Backend
| Fichier | Rôle |
|---|---|
services/cron-tasks.service.ts |
Cron prepareMessages + log du nombre de messages envoyés |
services/messages.service.ts |
Logique de préparation, création, envoi immédiat et comptage des messages |
services/suivistatus.service.ts |
CRUD suivistatus, calcul statut global |
services/stepSequence.service.ts |
Récupération des séquences |
services/mail.service.ts |
API Brevo, construction HTML, remplacement de variables |
controllers/reponse.controller.ts |
Endpoints de réponse (email et dashboard) |
entities/Steps.entity.ts |
Entité step |
entities/StepSequence.entity.ts |
Entité séquence |
entities/StepMessage.entity.ts |
Config message par step |
entities/MessageTemplate.entity.ts |
Template (HTML ou Brevo ID) |
entities/StepMessageBinome.entity.ts |
Journal des messages envoyés |
entities/Suivistatus.entity.ts |
Enregistrement de suivi |
Frontend
| Fichier | Rôle |
|---|---|
pages/reponse/_type/_id/_actif/_hash.vue |
Page de réponse email |
components/compte/dashboard/parcours-timeline.vue |
Timeline dashboard |
components/compte/dashboard/tooltip/cp-question.vue |
Réponse in-app |
components/rating/binomeSteps.vue |
Frise J+ détaillée |
pages/bo/binomes/_id/logs.vue |
Logs admin |
services/reponse.js |
Service API réponses |
store/user.js / store/binome.js |
State management dates |
static/model/ReponsesSuivis.js |
Wording des CPs |
Diagramme de flux simplifié
Cron prepareMessages (toutes les 30 min)
│
▼
Pour chaque binôme avec nextStepDate ≤ aujourd'hui
│
├── Step trouvée ?
│ │
│ ├─ OUI → launchStep()
│ │ ├── Créer suivistatus (EN_ATTENTE)
│ │ ├── Passer binôme EN_ATTENTE
│ │ └── Pour chaque message (jeune + bénévole) :
│ │ ├── Template Brevo ? → Envoi immédiat API Brevo
│ │ ├── SMS ? → Envoi immédiat SMS
│ │ └── Sinon → ALERTE (pas d'envoi)
│ │
│ └─ NON + après dernière step → lancer step étendue
│
└── Mettre à jour nextStepDate + lastStepDone
│
▼
countRecentMessages(startedAt) → log "N message(s) envoyé(s)"
Utilisateur clique lien email
│
▼
GET /reponse/{type}/{id}/{actif}/{hash}
│
├── POST /reponse → changeSuivistatus
└── POST /reponse/comment → commentStatus
│
▼
getSuivistatusStatus → statut global
│
▼
changeStatusBinome + changeStateBinome