MVLS — Fonctionnement et règles métier

Programme qui met en relation des lycéens de Terminale (Générale et Technologique) avec des étudiants bénévoles (éclaireurs) sur 9 mois à distance.

Passage annuel de niveau

Voir docs/metier/niveau.md, section MVLS, pour le fonctionnement du passage de niveaux et son interaction avec le statut MVLS.

Qui est considéré comme inscrit

  • LY (lycéen) inscrit : MvlsLyceen.charte === true
  • EE (éclaireur) inscrit : MvlsEclaireur.charte === true

charte passe à true à la soumission finale du questionnaire (partial = false). C'est ce qui déclenche l'envoi à dema1n via RabbitMQ (USER_CREATED sur user___mvls). Les sauvegardes intermédiaires (changement d'étape dans le questionnaire) n'envoient pas à dema1n.

Toute mise à jour ultérieure de parcours lycéen ne déclenche un renvoi vers dema1n que si charte === true est déjà posé.

users.mvls ≠ inscrit MVLS : le champ users.mvls = 'mvls' est positionné dès le début du questionnaire MVLS, avant la soumission finale. Il ne suffit pas à qualifier un lycéen d'inscrit — seul MvlsLyceen.charte === true fait foi. Ne pas utiliser users.mvls comme critère d'inscription dans la logique métier.

Éligibilité

Lycéens

  • En Terminale Générale ou Technologique
  • ET établissement partenaire (accès public) OU boursier / établissement prioritaire (accès prioritaire)
  • ET pas blacklisté (etablissement.isDenied === true sur l'un de ses établissements)
  • ET dans les dates d'ouverture configurées dans MvlsOptions (prio ou autre)

Éclaireurs

  • Au moins un parcours éclaireur : suffit pour l'accès (canAccessMvls), l'onglet Mentorat est visible toute l'année
  • Les dates d'ouverture éclaireur (MvlsOptions) ne conditionnent que l'inscription : hors période, /mentorat affiche un message d'attente au lieu du bouton « Devenir mentor », et la pop-up « Deviens mentor » de l'accueil (JoinModal.vue) n'est pas affichée

Configuration des dates (MvlsOptions)

Les dates citées dans la section Éligibilité vivent dans la table mvls-options (une seule ligne, id: 1). Elles sont éditables via l'admin (/adminspace/mvls) ou l'API (GET /mvls-options, POST /mvls-options/dates-{lyceen-prio|eclaireur} avec { dateFermeture, dateOuverture }).

Principe : penser en période de fermeture, pas en année

Les dates MVLS ne portent que sur le jour et le mois. L'année stockée en base (souvent 2001 dans l'interface) est un artifice technique : elle n'a aucune signification métier et n'a pas à être mise à jour chaque année.

Concrètement, la configuration se répète automatiquement sur chaque année civile. Il ne faut donc pas raisonner en « du 7 décembre 2025 au 10 juillet 2026 » : on configure plutôt « on ferme le 10 juillet, on rouvre le 7 décembre », quelle que soit l'année en cours.

Modèle mental recommandé : la période de fermeture

Plutôt que de définir une « fenêtre d'ouverture » dans une année donnée (ce qui obligerait à gérer le passage d'une année à l'autre), on définit quand les inscriptions sont fermées et quand elles rouvrent :

Champ Question à se poser
dateFermeture À partir de quand ferme-t-on les inscriptions ?
dateOuverture À partir de quand rouvre-t-on les inscriptions ?

Entre ces deux dates (sur le calendrier civil), les inscriptions sont fermées. Le reste de l'année, elles sont ouvertes.

Règle de validation : fermeture avant ouverture

Sur le calendrier civil (jour/mois), la fermeture doit être strictement avant l'ouverture. Cela garantit que la période ouverte est continue et traverse le 1er janvier :

        fermeture          1er janv.          ouverture
            │                  │                  │
── ouvert ──┤──── fermé ───────┼────── fermé ───┤──── ouvert ──
            ▼                  ▼                  ▼
         10 juillet                              7 décembre

Exemple (lycéens prioritaires ou éclaireurs) :

  • Fermeture : 10 juillet → les inscriptions se ferment à la fin de cette journée
  • Ouverture : 7 décembre → les inscriptions rouvrent à partir de cette date
  • Période fermée : du 11 juillet au 6 décembre (toutes les années)
  • Période ouverte : du 7 décembre au 10 juillet (toutes les années, en traversant le 1er janvier)

Cette règle (fermeture < ouverture en MMDD) est appliquée à la sauvegarde côté API et

Ce qu'il ne faut pas faire

  • Ne pas choisir une ouverture antérieure à la fermeture sur le même calendrier civil (ex. ouverture 1er mars, fermeture 30 juin) : cela décrirait une fenêtre dans une seule année et ne correspond pas au modèle métier MVLS.
  • Ne pas modifier l'année en base : seuls le jour et le mois comptent.
  • Ne pas interpréter les dates comme des événements ponctuels : ce sont des créneaux récurrents chaque année.

Pop-up mentorat (accueil lycéen)

MvlsPrioJoinModal.vue invite les lycéens prioritaires à découvrir l'onglet Mentorat. Elle s'affiche si le lycéen est en Terminale et prioritaire (boursier OU établissement isPrioritaire), pas blacklisté, pas déjà inscrit, et n'a pas fermé la pop-up (users.hasSeenMvlsPrioPopup, posé aussi au clic sur le bouton : plus jamais réaffichée). Endpoint : GET /mvls-options/popup-phase → { phase: 'parcoursup' | 'entree' | null }.

  • Elle suit la période d'inscription lycéens prioritaires (dateOuvertureLyceenPrio → dateFermetureLyceenPrio), hors période elle n'apparaît pas.
  • Le texte change à dateBasculePopupLyceenPrio (à renseigner dans le BO, sans valeur par défaut : vide, la pop-up reste sur parcoursup toute la période) : avant = aide pour le dossier Parcoursup (parcoursup), après = démarches d'entrée et premiers mois de cours (entree).
  • Les 3 dates sont éditables dans l'admin (/adminspace/mvls, carte « Lycéen prioritaire », un seul bouton : la période est enregistrée avant la bascule). La date de bascule est refusée à l'enregistrement si elle sort de la période d'inscription ; si on change ensuite les dates prioritaires, la garder à l'intérieur.

Statuts lycéen

Stocké dans MvlsLyceen.statut (string libre, pas d'enum). Synchronisé dans les deux sens avec Jeune.status de dema1n via RabbitMQ (user___mvls) : certains statuts sont posés par Inspire, d'autres reçus de dema1n.

Statut Signification Posé par
EN_ATTENTE_ACTIVATION Questionnaire commencé, attente d'activation du compte par le lycéen Inspire, à la création de la ligne
EN_ATTENTE_PARENT Compte activé, validation parentale requise (lycéen < 18 ans) Inspire, à l'activation
APTE En attente de mise en relation Inspire (activation majeur, consentement parental, update-statut), dema1n
MATCHE Binôme trouvé et actif dema1n
NON_DISPONIBLE Indisponible : fin de binôme, campagne d'été, matching instantané annulé, ou déclaré par le lycéen dema1n, Inspire (update-statut)
HORS_PROGRAMME Hors programme : passé post-bac (USER_POSTBAC, cron du 10/01), inscription bloquée, ou action manuelle BO dema1n
SORTI A quitté le programme Inspire (cancelAndQuitMvls), dema1n (quitAllBinomes)

EN_ATTENTE_ACTIVATION est posé dès la première sauvegarde du questionnaire (création de la ligne, même partielle), pour les mineurs comme pour les majeurs. Pour un mineur, l'activation du compte précède le consentement parental : EN_ATTENTE_ACTIVATION → EN_ATTENTE_PARENT → APTE.

Via POST /mvls-lyceen/update-statut, le lycéen ne peut passer qu'en APTE ou NON_DISPONIBLE, et jamais en APTE depuis EN_ATTENTE_ACTIVATION ou EN_ATTENTE_PARENT.

Le statut MATCHE a un impact sur le passage de niveau automatique — voir docs/metier/niveau.md.

Statuts éclaireur

Stocké dans MvlsEclaireur.statut (string libre, pas d'enum). Synchronisé dans les deux sens avec Benevole.status de dema1n via RabbitMQ (user___mvls).

Statut Signification Posé par
null Questionnaire non finalisé (charte pas encore validée) —
APTE Inscrit, en attente de mise en relation Inspire (soumission finale, réinscription « C'est reparti »), dema1n (fin de binôme si inscriptions éclaireur ouvertes)
MATCHE Binôme trouvé et actif dema1n
NON_DISPONIBLE Indisponible : fin de binôme hors période d'inscription éclaireur, déclaré depuis le profil, campagne d'été, indisponibilité temporaire (nonDispoUntil), ou choix admin en fin de binôme dema1n
HORS_PROGRAMME Hors programme — uniquement par action manuelle BO dema1n
SORTI A quitté le programme Inspire (cancelAndQuitMvls), dema1n (quitAllBinomes)

Le statut reste null pendant les sauvegardes intermédiaires et passe à APTE à la soumission finale (partial = false, charte = true), sans écraser un statut déjà posé (ex. MATCHE). Côté dema1n, le bénévole MVLS est créé directement APTE : pas de passage par EN_ATTENTE_ACTIVATION ni EN_ATTENTE_FORMATION.

Fin de binôme (BO dema1n). Quand un admin termine ou annule un binôme MVLS, le statut bénévole proposé par défaut dépend de la période d'inscription éclaireur (dateOuvertureEclaireur / dateFermetureEclaireur) : APTE si elle est ouverte, NON_DISPONIBLE sinon. Cela évite que les mentors de l'année précédente restent matchables hors période ; ils se réinscrivent via « C'est reparti » à la réouverture (voir section « Page de suivi — statut NON_DISPONIBLE »). L'admin peut toujours choisir un autre statut.

Statuts dema1n hors MVLS

dema1n connaît d'autres statuts (INSCRIT, EN_COURS_ONBOARDING, EN_COURS_INSCRIPTION, EN_ATTENTE_FORMATION, ASSOCIE_PARCOURS) propres aux parcours hors MVLS : les flux MVLS ne les posent pas. Listes complètes côté dema1n : front/static/model/StatusList.js (jeune) et StatusBenevoleList.js (bénévole).

Page de suivi — étapes affichées

/mentorat/suivi (Suivi.vue) est une seule page : un stepper vertical dont getSuiviSteps() (front/src/utils/mvls-suivi.utils.ts) fixe les étapes selon statut. Les « 2 étapes » d'un statut sont donc 2 pastilles du même stepper, pas 2 pages. Seule l'étape active déplie son contenu (la première à l'arrivée, puis celle sur laquelle on clique) ; les autres n'affichent que leur libellé.

Étapes → libellé et contenu (composants de pages/userspace/mentorat/components/Suivi/) :

Étape Libellé Contenu lycéen Contenu éclaireur
awaiting-validation En attente de la validation AttenteValidation : activer son compte via le mail idem
awaiting-parent-validation En attente de la validation parentale AttenteValidation parent : attendre l'accord des parents idem
awaiting-mer En attente de mise en relation AttenteMerLyceen : mises en relation dès janvier, lien discussions AttenteMerEclaireur : mises en relation dès janvier, boutons formation et mise à jour du profil
mentor-found Mentor trouvé, Mentoré si éclaireur MentorTrouveLyceen : carte de l'éclaireur, bouton « Valider mon binôme » si EN_ATTENTE_JEUNE AttenteMerEclaireur uniquement si EN_ATTENTE_JEUNE (libellé alors « En attente de mise en relation »), sinon rien
mer Mise en relation aucun (jalon final, jamais déplié) aucun, sauf une note sous le libellé si APTE (« mises en relation possibles jusqu'à la rentrée »)

Statut → étapes affichées (dans l'ordre, la première est ouverte par défaut) :

Statut Étapes affichées
EN_ATTENTE_ACTIVATION awaiting-validation, awaiting-mer
EN_ATTENTE_PARENT awaiting-parent-validation, awaiting-mer
APTE awaiting-mer, mer
MATCHE mentor-found, mer
NON_DISPONIBLE FinDeSuivi à la place du stepper (ci-dessous)

Binôme en cours (isBinomeEnCours, front/src/utils/mvls.utils.ts) : la page redirige vers /mentorat/ton-mentor (lycéen) ou /mentorat/ton-lyceen (éclaireur). EN_ATTENTE_JEUNE n'est pas « en cours » côté front : le lycéen valide le binôme depuis la page de suivi, et l'éclaireur voit « En attente de mise en relation » (contenu AttenteMerEclaireur) à la place de « Mentoré trouvé » tant que le lycéen n'a pas validé.

Flux d'inscription

Lycéen (questionnaire MVLS)

Le point d'entrée public destiné aux ateliers est /inscription-mvls-ateliers. Il vérifie la période d'ouverture prioritaire (GET /mvls-options/is-open-pp) et redirige vers /accueil hors période. L'intention MVLS (sessionStorage inspire_mvls_intent) est posée par le guard de routage dès l'arrivée sur la page. Le CTA envoie le SSO vers /mentorat?atelier pour que le callback puisse distinguer un retour atelier.

Après le retour du SSO, les utilisateurs des cas 2, 3 et 4 sautent la question Classe : users.niveau et parcours-lyceens.niveau sont posés à Terminale. Si l'URL de retour (callback ou targetUrl) contient le query param atelier, ils sont aussi marqués avec users.origineInscriptionMvls = 'atelier_mvls'. Le cas 1 n'est pas concerné puisqu'il est déjà connecté et ne passe pas par le callback SSO.

Ordre des pages : Scolarisation → Besoins → Domaines → Après le bac → Accompagnement → Informations personnelles. L'étape Ressenti n'est plus présentée (colonnes etudesSup / travailEfficace / preparationSup conservées, nullable).

Si le lycéen n'a pas de questionnaire Inspire finalisé, le funnel MVLS commence par :

  1. Skip de la page rôle (rôle lycéen automatique, intent sessionStorage inspire_mvls_intent)
  2. Mon profil Inspire
  3. Lycée (département, établissement, isBoursierSecondaire)
  4. Bac (filiere, niveau = Terminale)
  5. POST /parcours-lyceen/formulaire (stocke surveyResult sans redemander le questionnaire orientation complet)
  6. Puis les pages MVLS ci-dessus

Si Inspire est déjà finalisé (algoResult.results), on n'affiche que les pages MVLS.

Questionnaire (partial saves)
  → soumission finale (charte=true) → USER_CREATED envoyé à dema1n
  → EN_ATTENTE_ACTIVATION
  → email lycéen → activation token
  → (si mineur) EN_ATTENTE_PARENT → email parent → APTE
  → (si majeur) APTE
  → (dema1n) → MATCHE

Éclaireur

Questionnaire (partial saves)
  → soumission finale (charte=true) → USER_CREATED envoyé à dema1n
  → APTE → (dema1n) → MATCHE

Recette du funnel lycéen

Depuis front/ : yarn test:mvls. Le script rejoue les 4 cas d'inscription (connecté avec Inspire finalisé / via SSO avec Inspire finalisé / sans compte / compte Inspire incomplet) sur les modules réels — mvls-intent et le guard de routage use-auth/router — seul sso.service étant remplacé par un stub. Sources dans front/test/mvls-funnel/.

Intégration dema1n (RabbitMQ)

Routing key sortante : user___mvls

Événement Déclencheur
USER_CREATED Soumission finale du formulaire LY ou EE
USER_UPDATED Mise à jour de profil ou de parcours (seulement si charte === true)

Les binômes et suivis arrivent de dema1n via RabbitMQ — voir RabbitMQ.