Matching instantané
Voir aussi : Reservation Admin — réservation par un admin (30 min), même affichage rouge. Modèle de données : voir entities/instant_matching.md — tables
instant_matching_reservationetalgo_matching_trace.
Vue d'ensemble
Le matching instantané permet de réserver un jeune + 3 bénévoles ensemble pendant 15 minutes. Pendant ce temps, ces 4 profils ne sont matchables par personne (ni admin, ni autre jeune). Le jeune dispose de 15 minutes pour choisir un des 3 mentors proposés ; sa sélection déclenche la création du binôme.
| Critère | Valeur |
|---|---|
| Acteur | Système (automatique, pas d'admin) |
| Granularité | 1 groupe = 1 jeune + 3 bénévoles |
| Durée | 15 minutes |
| Libération | Sélection du jeune → createBinomeInstant() ou expiration |
Accès jeune (canAccess)
Un jeune peut accéder au matching instantané si toutes les conditions suivantes sont remplies :
| Critère | Valeur |
|---|---|
| Feature toggle | matching_instantane activé (BO superadmin → /bo/admin/featuretoggles) |
| Statut | APTE |
| Sandbox | DEMA1N |
| État | Autonome |
Implémentation alignée front/back : front/services/reservation.js et InstantMatchingService.canAccess.
Le programme (EL, PP, PNP, etc.) et la région scolaire ne conditionnent plus l'accès.
Extraction LLM (student_pivot)
Le champ libre jeune.precision n’existe plus dans l’inscription / onboarding V2. Pendant l’onboarding mentoré, juste avant la page genre, dema1n concatène les champs ouverts puis appelle POST {LLM_EXTRACT_SERVICE_URL}/student_pivot. La page secteurs n’est pas un point de passage fiable : elle est sautée quand les secteurs sont déjà remplis à l’inscription.
Type d’objectif (jeune.objectifs / besoins[0].slug) |
Texte envoyé |
|---|---|
IP (stage / alternance / emploi) |
Mon objectif du moment : Je cherche un stage, une alternance, ou un emploi pour un poste de {posteEnvisage}. Mon job idéal : {jobIdeal} |
Réussir mon année (reussir-annee) |
Mon job idéal : {jobIdeal} |
Candidater (candidater) |
Mon objectif du moment : Je voudrais intégrer les formations suivantes : {diplomeOuConcoursEnvisage}. Mon job idéal : {jobIdeal} |
Concours (concours) |
Mon objectif du moment : Je prépare le concours suivant : {diplomeOuConcoursEnvisage}. Mon job idéal : {jobIdeal} |
Perdu (lost) |
Mon job idéal : {jobIdeal} |
Pas d’extraction depuis le BO ni le profil, et pas de cron de rattrapage. Skip si user.sandbox === 'MVLS' ou si la concaténation est vide. Code : buildStudentPivotText, JeuneService.extractStudentPivotIfNeeded (déclenché par OnboardingService.extractStudentPivotBeforeGenre quand POST /onboarding/next renvoie le panel /onboarding/jeune/genre).
Microservice de matching
Le service MatchingAlgoService appelle un microservice externe pour le matching personnalisé.
Body envoyé au microservice
Chaque champ est construit à partir de la table jeune ou de jeune_extrait_precisions. Aucune valeur de repli : si la source est vide, on envoie '' (string) ou [] (array).
| Champ | Source | Cas vide |
|---|---|---|
jeune_programme |
jeune.programme |
'' |
jeune_cursus_actuel_ou_passe_cf |
jeune.school_cursus |
'' |
jeune_cursus_actuel_ou_passe_llm |
jeune_extrait_precisions.cursus_actuel_ou_passe_1/2 |
[] |
jeune_cursus_vise_llm |
jeune_extrait_precisions.cursus_vise_1/2/3 |
[] |
jeune_filiere_actuelle_ou_passee_cf |
jeune.filieres (array) |
[] |
jeune_filiere_actuelle_ou_passee_llm |
jeune_extrait_precisions.filiere_actuelle_ou_passee_1/2 |
[] |
jeune_filiere_visee_llm |
jeune_extrait_precisions.filiere_visee_1/2/3 |
[] |
jeune_secteur_vise_cf |
jeune.sectors (JSON array) |
[] |
jeune_secteur_vise_llm |
jeune_extrait_precisions.secteur_vise_X_1/2 (niveau1/niveau2) |
[] |
jeune_profession_visee_llm |
jeune_extrait_precisions.profession_visee_X_1/2 (niveau1/niveau2) |
[] |
jeune_poste_vise_llm |
jeune_extrait_precisions.poste_vise_1/2/3 (concaténés par ,) |
'' |
jeune_besoin |
jeune_extrait_precisions.objectif_1 puis fallback mapJeuneBesoin(jeune) |
Voir ci-dessous |
reponse_recepteur |
Paramètre d'appel (back_office ou front_jeune) |
— |
reponse_longueur |
Constante | 30 |
Format niveau1 / niveau2 : tableaux d'objets { niveau1: string, niveau2: string }, jusqu'à 3 paires par champ. Exemple :
"jeune_secteur_vise_llm": [
{ "niveau1": "Santé / Social / Environnement", "niveau2": "Santé" },
{ "niveau1": "Santé / Social / Environnement", "niveau2": "Social" }
]
jeune_besoin : si objectif_1 est renseigné, on l'envoie. Sinon, mapJeuneBesoin(jeune) dérive depuis jeune.besoins : slug study → "Atteindre un objectif d'études" (subs orienter/resultats/concours) ou "Définir mon projet d'études" ; slug pro → "Atteindre un objectif d'insertion pro" (subs candidate/interview/seek) ou "Définir mon projet pro" ; autre → BESOINS_MAP[slug] ou "Définir mon projet d'études".
Code : back/src/binomes/services/matching-algo.service.ts (buildRequestBody, epToLegacyFormat, mapJeuneBesoin).
Méthodes
| Méthode | Usage | Description |
|---|---|---|
getPersonnalizedBenevoles(jeune, adminId, reponseRecepteur) |
BO (algo=new), propose back_office | Top 50 bénévoles par compatibilité, liste plate |
getPersonnalizedBenevolesOnePerCategory(jeune, adminId, excludeIds?) |
propose front_jeune, refresh | 1 bénévole matchable par catégorie (femmes, non_femmes, generalistes, puis autres clés alpha). La clé est stockée sur la réservation (type_mentor). |
Catégories du microservice
La réponse contient des clés : femmes, non_femmes, generalistes. Pour le matching instantané côté jeune, on prend le premier disponible de chaque catégorie, puis on mélange l'ordre.
Fallback microservice indisponible
Si le microservice est inaccessible (timeout 30 s, erreur réseau ou HTTP) :
getPersonnalizedBenevolesOnePerCategory: 1 tentative sans retry → retourne[]getPersonnalizedBenevoles: 1 tentative + 1 retry (même timeout 30 s) → retourne[]
Dans les deux cas, le service bascule sur le fallback local via getTopBenevolesForJeune(jeune, 3) :
- Charge tous les bénévoles matchables depuis la DB locale (
loadMatchableBenevoles) - Calcule un score de rating classique (
ratingService.calculateBinomeRating) pour chacun - Exclut les bénévoles avec score
-99(disqualifiés) - Retourne les top 3 par score décroissant
Les mentors proposés ne sont pas aléatoires — c'est un classement déterministe par score. Le champ type_mentor est NULL sur les réservations créées via ce fallback (pas de catégorie femmes/non_femmes/generalistes).
Les bénévoles avec excluInstantMatching = true (réservés pour le matching manuel) sont exclus à la récupération :
- résultats du microservice (
checkBenevoleDisponibledansMatchingAlgoService) - fallback local Dema1n (
loadMatchableBenevoles) - refresh / disponibilité (
isBenevoleStillAvailableForJeune)
Ils restent matchables par un admin via le matching BO classique (le nouvel algo BO les écarte aussi).
Ce fallback s'applique aussi au refresh quand au moins un bénévole est devenu indisponible.
Logs
Appels logués avec préfixe [matching-algo] : body envoyé, réponse brute, mentors parsés, bénévoles exclus, résultat final. En cas d'erreur (ex. 500), le body est inclus dans le log.
Logique métier
Durée : 15 minutes
Une réservation est active si :
deletedAt IS NULL AND reservationDate + 15 minutes > NOW()
Expiration
Aucune action automatique (pas de cron). Les réservations expirées restent en base et sont ignorées via le filtrage à la lecture.
Profil non matchable
Un jeune ou un bénévole est non matchable si :
- Il a une
AdminReservationactive (30 min), OU - Il apparaît dans une
InstantMatchingReservationactive (15 min)
Les deux systèmes se cumulent.
Implémentation backend
- postBinome (BinomeController) : Bloque la création si le jeune ou le bénévole est en matching instantané actif.
- Listes de matching (RatingController) :
jeuneListetbenevoleListexcluent les profils en matching instantané actif. - Listes BO (JeuneRepository, BenevoleRepository) :
findAndFilterexclut les jeunes et bénévoles en matching instantané actif.
Affichage frontend (BO)
L'affichage du matching instantané réutilise celui de la réservation admin :
Condition de masquage du bouton Matcher :
!(item.resa || item.instantMatchingResa);
Le bouton Matcher n’apparaît que pour un jeune APTE (création de binôme, vivier BO, matching de masse inclus). La liste BO des jeunes exclut par défaut INSCRIT, NON_DISPONIBLE et SORTI.
Choix de l'algorithme de matching (jeunes uniquement)
Pour les jeunes, le bouton "Matcher" est un menu déroulant proposant 2 choix :
| Choix | Label | Route |
|---|---|---|
| 1 | Nouvel algo - bêta | /bo/jeunes/:id/matching?algo=new |
| 2 | Algo classique | /bo/jeunes/:id/matching?algo=classic |
Disponibilité du nouvel algo
| Contexte | Nouvel algo disponible |
|---|---|
| Jeunes (proposer des bénévoles à un jeune) | ✅ Tous les admins (admin + superadmin) |
| Bénévoles (proposer des jeunes à un bénévole) | ❌ Non — bouton simple, algo classique uniquement |
L'endpoint GET /rating/benevoleList/:jeuneId accepte ?algo=new (appel microservice). GET /rating/jeuneList/:benevoleId utilise toujours l'algo classique.
Assignation d'un admin
L'inscription jeune n'assigne plus de CD. Un jeune n'a un adminId que s'il passe par le matching instantané, ou par une assignation manuelle.
Tous les admins membres d'une équipe peuvent recevoir des binômes MI (plus de case « Matching instant »).
Lors de createBinomeInstant, un admin de suivi est sélectionné :
| Règle | Détail |
|---|---|
| Manuel | Binôme créé en BO (createBinome) : attribué au CD qui l'a créé. |
| Critères MI | Région du jeune (schoolRegion), profil admin ou superadmin, sandbox compatible, disponible = true, equipeId non null. |
| EL / PP / PNP / STEM | Même pool (région + dispo + équipe). Un jeune STEM n'est pas réservé à un sous-ensemble d'admins. |
| Choix | CD à la jauge (% remplissage) la plus basse (pickCdForInstantMatching). Égalité : tirage aléatoire. Jauge > 100 % reste éligible. |
| Si aucun | adminId = null ; binôme créé quand même ; LogAdminAssocie avec admin vide et origin = createBinomeInstant. |
| Assignation | binome.adminId, jeune.adminId, benevole.adminAssocieId (jeune/bénévole seulement si un CD a été trouvé) |
Bénévoles éligibles : sans admin associé
| Contexte | Vérification |
|---|---|
| Microservice | checkBenevoleDisponible : si adminId === '__instant_matching__' et benevole.adminAssocieId != null → exclu ; si excluInstantMatching → exclu |
| Fallback local | loadMatchableBenevoles : filtre benevole.adminAssocieId != null ou excluInstantMatching → exclu |
Réservation pour le matching manuel (excluInstantMatching)
Un admin peut marquer un mentor exclu MI depuis la fiche BO (/bo/benevoles/:id, toggle sous le nom). Champ benevole.exclu_instant_matching, défaut false.
- Toast à l’activation : « Ce mentor est maintenant exclu du MI »
- Filtre liste mentors : Exclu MI Oui / Non
- Le matching instantané n’inclut plus ce mentor (algo + fallback Dema1n)
- Le matching BO classique (liste depuis une fiche jeune, algo classique) le laisse disponible pour un appariement manuel
La page /bo/jeunes/:id/matching affiche un v-chip indiquant l'algorithme actif (?algo=new → "Nouvel algo -
bêta", sinon "Algo classique").
Type de binôme et traçabilité
| Parcours | Service | binome.type |
|---|---|---|
| Jeune — validation réservation 15 min | InstantMatchingService.validate → createBinomeInstant() |
instant |
| Back-office — matching manuel | BinomeController.postBinome → createBinome() |
manuel |
binome.type
| Valeur | Description |
|---|---|
manuel |
Créé via createBinome() (défaut BO, y compris matching instantané admin) |
auto |
Alternatif via createBinome() (double proposition multiproposition) |
instant |
Uniquement createBinomeInstant() (matching instantané jeune) |
binome.typeAlgo
Indique si le score vient du nouvel algo (v2) ou du rating classique (classic). v2 si la réservation porte un score microservice, classic si score legacy.
Commentaire automatique sur le binôme
- Parcours jeune (
validate→createBinomeInstant) :Matching instantané - nouvel algo - {DD/MM/YYYY}(score v2) ouMatching instantané - algo classique - {DD/MM/YYYY}(score legacy) - Parcours BO (
postBinomeavecisInstantMatching) : texte basé sur le query paramalgo
Filtre "Matching" sur la liste des binômes
| Valeur | Label | Types inclus |
|---|---|---|
instantane |
Instantané | type = 'instant' |
manuel |
Manuel | type IN ('manuel', 'auto') |
Indicateur visuel "déjà proposé en matching instantané"
Sur les pages de matching (BO), si un profil a déjà été dans une réservation avec le profil en cours (même expirée/terminée) :
- Page liste (
/matching/index.vue) : fond grisé sur la card du profil déjà proposé - Page confirmation (
/matching/:id) : message gris "{Prénom mentor} a déjà été proposé à {Prénom jeune} lors du matching instantané"
Le contrôle porte sur toutes les réservations passées (y compris expirées et soft-deleted).
Endpoints backend associés
GET /rating/benevoleList/:jeuneId: chaque bénévole enrichi d'un flagwasProposedInInstantMatching: booleanGET /rating/jeuneList/:benevoleId: idem pour les jeunesGET /rating/wasPreviouslyProposed?jeuneId=X&benevoleId=Y→{ wasProposed: boolean }— appelé directement par les pages de confirmation (pas via query param, pour éviter la falsification par URL)
Endpoints API (côté jeune)
| # | Méthode | Route | Description |
|---|---|---|---|
| 1 | POST | /instant-matching/propose |
Proposer 3 bénévoles |
| 2 | POST | /instant-matching/refresh |
Renouveler les 3 bénévoles réservés |
| 3 | POST | /instant-matching/confirm-selection |
Confirmer le choix (états : sélectionné / autre selectionné) |
| 4 | POST | /instant-matching/validate |
Valider et créer le binôme |
| 5 | POST | /instant-matching/refuse |
Refuser les 3 bénévoles |
| 6 | POST | /instant-matching/cancel |
Annuler et passer en non disponible |
Architecture : InstantMatchingController — auth (JwtAuthGuard) + vérification propriété (getAuthenticatedJeune) + délégation à InstantMatchingService.
Collection Bruno : back/bruno/instant-matching/.
Mise en forme des bénévoles proposés
Quand : Fin d'inscription du jeune (après finishInscriptionJeune, jeune APTE). Également à chaque chargement de la page de sélection.
Entrée : { jeuneId: string }
Comportement idempotent :
| Situation | Comportement | expired |
|---|---|---|
| Aucune réservation | Crée 3 réservations, renvoie les bénévoles | false |
| Réservations actives (< 15 min) | Renvoie les mêmes sans toucher au timer | false |
| Réservations expirées (> 15 min, non supprimées) | Renvoie les mêmes sans toucher au timer | true |
| Réservations soft-deleted | Erreur 400 — propositions déjà faites | — |
Logique (première fois, contexte front_jeune) :
- Vérifier jeune existant,
req.userpropriétaire, étatAPTEetAutonome - Vérifier via
findCurrentForJeune— si réservations existantes, les renvoyer avec flagexpired - Appel microservice
getPersonnalizedBenevolesOnePerCategory()— 1 bénévole matchable par catégorie ; si microservice indisponible → fallback local top 3 par rating classique (type_mentor = NULL) - Mélange aléatoire en préservant la paire (bénévole +
type_mentor) - Créer 3 lignes
InstantMatchingReservationavecorderIndex0/1/2,etat: 'proposé',score,type_mentor - Retourner les 3 bénévoles avec
expired: false
Contexte back_office : top 3 via getTopBenevolesForJeune() ; type_mentor = NULL.
Sortie :
{
success: boolean;
expired: boolean;
expiresAt?: string;
reservations: {
benevoleId: string;
firstName: string;
lastName: string;
etat: 'proposé' | 'sélectionné' | 'autre selectionné' | 'refusé' | 'desactivation';
rating: number;
ratingDetails: string;
department: string;
region: string;
secteurs: string[];
postes: string[];
cursus: string[];
filieres: string[];
diplomes: string[];
experience: string;
passions: string;
alternance: boolean;
}[];
}
Cas particuliers :
- Moins de 3 bénévoles disponibles : retourner ce qui est disponible, ou
success: falsesi aucun - Réservations soft-deleted → erreur
400 "Le matching instantané a déjà été proposé.", sauf s'il existe un binômeANNULEouTERMINEdontcreationDateest postérieure au début (reservationDate) de la dernière session MI : un nouveau lot est alors créé (les mêmes mentors peuvent ressortir).
Endpoint 2 : POST /instant-matching/refresh
Quand : Jeune revient sur la page (rechargement, retour, timer proche de l'expiration).
Entrée : { jeuneId: string }
Logique :
- Vérifier jeune + ownership
- Récupérer réservations non-supprimées via
findCurrentForJeune - Si aucune →
{ success: true, reservations: null } - Vérifier disponibilité de chaque bénévole : pas
MATCHE(sauf multibinome),Autonome, pas de réservation admin active, pas de réservation instant matching pour un autre jeune, pasexcluInstantMatching - Tous disponibles : 3 nouvelles lignes avec mêmes
score/type_mentor - Au moins un indisponible : appel microservice
getPersonnalizedBenevolesOnePerCategory()pour 3 nouveaux (fallbackgetTopBenevolesForJeune()si microservice indisponible →type_mentor = NULL) - Soft-delete anciennes réservations, créer 3 nouvelles (historique conservé)
Sortie :
{
success: boolean;
reservations: { ... }[] | null;
}
Endpoint 3 : POST /instant-matching/confirm-selection
Quand : Le jeune clique sur "Confirmer mon choix".
Entrée : { jeuneId: string, benevoleId: string }
Logique :
- Vérifier jeune + ownership
- Vérifier que le bénévole fait partie des réservations actuelles
updateEtatForValidate(jeuneId, benevoleId)— sélectionné →sélectionné, autres →autre selectionné- Pas de soft-delete, pas de création de binôme
Sortie : { success: boolean }
Erreur : bénévole non trouvé dans les réservations → 400
Endpoint 4 : POST /instant-matching/validate
Quand : Le jeune clique sur "Envoyer et valider le binôme".
Entrée : { jeuneId: string, benevoleId: string, firstMessageJeune?: string }
firstMessageJeune = concaténation des 3 champs du formulaire (présentation, objectifs, dispos) séparés par \n.
Logique :
- Vérifier jeune + ownership
- Vérifier que le bénévole est dans les réservations actives
- Validations de sécurité : jeune pas déjà
MATCHE, jeune et bénévoleAutonome, bénévole non-multibinome pasMATCHE, limites binômes,onlyOneBinome, PNP/partenaire, PNP/VIP, même sandbox updateEtatForValidate(jeuneId, benevoleId, true)— sélectionné →matché, autres →autre selectionné- Soft-delete des 3 réservations
createBinomeInstant():type = instant,typeAlgo = v2/classic, statusEN_ATTENTE,firstMessageJeunestocké- Mails : événement
instant.binome.created→instant.benevole-0etinstant.jeune-0;lastStepDone=0,nextStepDate = +15 jours - Statuts : jeune →
MATCHE, bénévole →MATCHE - Commentaire automatique sur le binôme
- Si sandbox MVLS :
mvlsService.publishBinomeMessage - Si sandbox non-MVLS : création des todo lists (besoins sans slug valide ignorés)
Sortie : { success: boolean, binomeId: string }
Erreurs :
- Bénévole non trouvé dans les réservations actives →
400 - Bénévole plus disponible / multibinome max atteint /
onlyOneBinome→409 Conflict - Jeune déjà matché, non autonome, PNP+partenaire/VIP, sandbox différente →
400
Endpoint 5 : POST /instant-matching/refuse
Quand : Le jeune ne souhaite aucun des 3 bénévoles.
Entrée : { jeuneId: string, comment?: string }
Logique :
- Vérifier jeune + ownership
updateEtatForJeune(jeuneId, 'refusé')- Soft-delete via
softDeleteAllCurrentForJeune - Commentaire individuel :
"Matching instantané refusé"ou"Matching instantané refusé - {comment}"(adminId: null) jeune.matchingInstantaneRefuse = true(filtre BO « Matching instantané refusé »)- Le jeune reste
APTE
Sortie : { success: boolean }
Le flag matchingInstantaneRefuse est remis à NULL dès que le jeune passe MATCHE (changeStatusJeune). Chaque changement du flag est journalisé dans log-matching-instantane-refuse (visible sur /bo/jeunes/:id/logs). L’historique BO (colonne Refusés) n’affiche que les vagues etat = 'refusé' ; s’il y a plus de commentaires que de vagues, seuls les N commentaires les plus récents sont associés (raison + date).
Endpoint 6 : POST /instant-matching/cancel
Quand : Le jeune annule et ne veut plus être disponible.
Entrée : { jeuneId: string }
Logique :
- Vérifier jeune + ownership
updateEtatForJeune(jeuneId, 'desactivation')- Soft-delete via
softDeleteAllCurrentForJeune - Commentaire individuel :
"Matching instantané - désactivation" changeStatusJeune(jeuneId, 'NON_DISPONIBLE', 'MATCHING_INSTANTANE')+ logs
Sortie : { success: boolean }
Mise en forme des bénévoles proposés (formatBenevoleResponse)
formatBenevoleResponse (instant-matching.service.ts) transforme un Benevole avec ses relations en objet plat pour l'affichage côté jeune. Fusionne données CF (champ fermé) et LLM (BenevoleExtraitPrecision).
Sources de données
| Champ | Sources CF | Sources LLM | Déduplication |
|---|---|---|---|
| secteurs | Benevole.secteurMetier + PosteBenevole[].secteur |
extraitPrecision.secteurXX (paires niveau1/niveau2) |
Taxonomique via taxonomie_secteurs_professions.json |
| postes | PosteBenevole[].poste (courant en premier) |
extraitPrecision.poste1/2/3 |
Case-insensitive |
| cursus | CursusBenevole[].cursus ("Autre" → cursusAutre) |
extraitPrecision.cursus1/2 |
Case-insensitive |
| filieres | CursusBenevole[].filiere ("Autre" → filiereAutre) |
extraitPrecision.filiere1/2 |
Case-insensitive |
| diplomes | CursusBenevole[].diplome |
— | Aucune |
Déduplication des secteurs
- Valeurs CF depuis
Benevole.secteurMetier+PosteBenevole[].secteur(déduplication case-insensitive) - Chaque valeur CF traduite en
{ niveau1, niveau2 }viataxonomie_secteurs_professions.json(comparaison uniquement, jamais pour affichage) - Si la traduction correspond à une entrée LLM → l'entrée LLM est "couverte", on affiche la valeur CF d'origine
- Entrées LLM non couvertes affichées par niveau1 uniquement
- Ordre : valeurs CF d'abord, puis niveau1 LLM non couverts
Fichiers : back/src/binomes/utils/merge-secteurs.ts (mergeSecteurs, mergePostes, mergeCursus, mergeFilieres) ; back/src/binomes/imports-files/taxonomie_secteurs_professions.json.
Les champs sont affichés en chips dans front/pages/compte/jeune/mentors/MentorCard.vue.
Flux fonctionnel
Création de la réservation
- Jeune termine l'inscription →
POST /instant-matching/propose - Sélection des 3 bénévoles : microservice (1 par catégorie) ou fallback local avec
calculateBinomeRating - Création de 3 lignes
InstantMatchingReservationavectype_mentor= clémentorsouNULL(fallback/BO) - Affichage des 3 bénévoles au jeune
Pendant 15 minutes
- Jeune et 3 bénévoles visibles dans les listes BO mais exclus de
findMatchable - Bouton Matcher masqué, icône "i" rouge
- Recharge page →
/proposerenvoie les mêmes 3 bénévoles sans toucher au timer (expired: false)
Après expiration (> 15 min)
- Profils redeviennent matchables dans les listes BO
/proposerenvoie les mêmes 3 avecexpired: true- Le front appelle
/refreshpour renouveler et remplacer les indisponibles
Affichage selon l'état (page /compte/jeune/mentors)
| État des réservations | Affichage |
|---|---|
| Tous en "proposé" | 3 cartes mentors, titre "Choisis ton mentor", bouton "Confirmer mon choix" |
| Un en "sélectionné" | Vue directe "Mentor choisi: {Prénom}", formulaire message, bouton "Envoyer et valider le binôme" |
Flux :
propose/refresh→ réservations avec champetatetat === 'sélectionné'→pickeddéfini, affichage formulaire- Clic "Confirmer" →
POST /instant-matching/confirm-selection - Clic "Valider" →
POST /instant-matching/validate
Libération
| Cas | Action |
|---|---|
| Validation | validate → sécurité, binôme type: 'instant', statuts MATCHE, soft-delete, commentaire, TodoLists (sauf MVLS), RabbitMQ (si MVLS) |
| Refus | refuse → soft-delete, commentaire "refusé", matchingInstantaneRefuse = true, jeune reste APTE |
| Annulation | cancel → soft-delete, commentaire "désactivation", jeune NON_DISPONIBLE |
| Expiration | Profils redeviennent matchables BO ; réservations conservées ; /propose → expired: true |
Création du binôme : createBinomeInstant
Fonction dédiée (pas createBinome) avec les spécificités suivantes :
| Paramètre | Valeur |
|---|---|
type |
Toujours instant |
typeAlgo |
v2 si réservation avec score microservice ; classic si score legacy |
status |
Directement EN_ATTENTE (pas de phase EN_ATTENTE_JEUNE, pas de premerBinome) |
firstMessageJeune |
Texte du formulaire (présentation / objectifs / dispos, séparés par \n) |
lastStepDone |
0 |
nextStepDate |
Création + 15 jours |
Admin : CD de la région, disponible, membre d'une équipe, jauge la plus basse (préférence STEM si le jeune est STEM) ; null si aucun éligible (log admin vide conservé). L'inscription seule n'assigne pas d'admin.
Mails (événement instant.binome.created) :
instant.benevole-0(Brevo #4347) :PRENOM,PRENOM_admin,PRENOM_JEUNE,schoolCursus,filiereJeune,texte_matching,objectif_du_moment_mentor(phrase à la 3e personne : prénom du jeune + verbe, contrairement au dashboard jeune qui utilise « tu »)instant.jeune-0:PRENOM,PRENOM_admin,PRENOM_mentor
Comportements spécifiques
Popup d'expiration côté jeune
Quand le timer expire sur /compte/jeune/mentors, deux modales distinctes selon l'écran :
Cas 1 — écran 3 mentors (picked = null) :
| Action | Comportement |
|---|---|
| Actualiser les propositions | POST /instant-matching/refresh — garde les 3 si toujours dispo, sinon nouveaux |
| Aucun de ces mentors ne me convient | Ouvre la modale « Demande de proposition manuelle » |
| Je ne souhaite plus être mentoré | Ouvre la modale « Désactivation du compte » |
Cas 2 — écran 1er contact (picked défini) :
| Action | Comportement |
|---|---|
| Vérifier sa disponibilité | POST /instant-matching/check-availability |
| Je ne souhaite plus être mentoré | Ouvre la modale « Désactivation du compte » |
check-availability :
- Mentor toujours dispo →
renewCurrentReservations(), toast « {prénom} est toujours disponible ! », reste sur l'écran 1er contact - Mentor indisponible →
refresh(), toast « {prénom} n'est plus disponible… », retour écran 3 mentors après quelques secondes - Erreur API → toast d'erreur, on reste sur l'écran 1er contact (on ne redescend pas au choix des 3 mentors)
Une réservation IM / admin expirée (timer écoulé, ligne encore en base) ne rend pas le mentor indisponible. Seules les réservations actives d'un autre jeune (ou d'un admin) bloquent.
Fermeture de page : beforeunload natif tant que le jeune est dans le parcours (réservation active, binôme non créé).
Si le jeune recharge avec une réservation expirée, propose restitue d'abord l'etat (sélectionné → cas 2, sinon cas 1) puis ouvre la modale adaptée.
Protections back-office
| Protection | Composant | Comportement |
|---|---|---|
| Bouton "Matcher" désactivé | ButtonsAdmin.vue |
Désactivé si isInInstantMatching (tous rôles y compris superadmin) |
| Message d'erreur | MatchingErrorMessages.vue |
"Ce jeune/bénévole est réservé pour un Matching Instantané..." |
| Icône "i" rouge | IDataTable.vue, ITableView.vue |
Si instantMatchingResa actif |
| Protection backend | postBinome |
Bloque si jeune ou bénévole en matching instantané actif |
Conditions de disponibilité lors d'un refresh
Un bénévole est maintenu si :
status === 'APTE'OU (status === 'MATCHE'ETmultibinomeETactiveBinomeCount < 2)state === 'Autonome'- Pas de réservation admin active (30 min)
- Pas de réservation instant matching active pour un autre jeune (15 min) — une réservation expirée ne bloque pas
excluInstantMatchingest faux (mentor non réservé pour le matching manuel)
Régions et onboarding (champ "about")
Constante INSTANT_MATCHING_REGIONS (front/services/reservation.js) :
['Île-de-France'];
| Comportement | Condition |
|---|---|
| Champ "about" masqué à l'onboarding | schoolRegion dans INSTANT_MATCHING_REGIONS |
Cette constante ne conditionne pas l'accès au matching instantané. Le champ "about" reste visible dans la page profil et en back-office.
Modifications de l'onboarding jeune
Étape "Situation" (id: 43)
| Élément | Valeur |
|---|---|
| Titre de l'étape | "Aide nous à te trouver le mentor idéal pour ta situation" |
| Titre du mood | "Comment te sens-tu en ce moment ?" |
| Titre du champ precision | "Décris ta situation" |
| Subtitle du champ precision | "Tes études, ton projet pour la suite, les difficultés que tu rencontres" |
| Champ precision | Obligatoire, 50 caractères minimum (compteur affiché) |
Carrousel d'aide
Le composant HelpCarousel (front/components/inscription/widgets/v2/helpCarousel.vue) remplace le panneau statique. Header : "Besoin d'aide pour remplir ce champ ?", 3 exemples en carrousel.
Comparaison avec AdminReservation
| Critère | AdminReservation | InstantMatchingReservation |
|---|---|---|
| Acteur | Admin | Système |
| Granularité | 1 admin ↔ 1 jeune OU 1 bénévole | 1 jeune + 3 bénévoles |
| Durée | 30 min | 15 min |
| Libération | Bouton admin ou expiration | Sélection jeune ou expiration |
| Affichage | Rouge | Rouge (identique) |
Auto-annulation mentor post-MER (48h)
Feature toggle : mentor_post_mer_48h (admin BO → Feature toggles). Désactivé par défaut.
Dans les 48h suivant la création d'un binôme, le mentor peut agir depuis son tableau de bord (/compte/benevole/dashboard) :
| Action | Endpoint | Effet |
|---|---|---|
| Je suis bien disponible | POST /binomes/:id/confirm-dispo |
Enregistre binome.mentor_dispo_confirmed_at |
| Désactiver le matching | POST /binomes/:id/cancel-from-dashboard |
Annule le binôme (cancellation_reason = Annulation mentor), jeune → APTE, mentor → NON_DISPONIBLE (sauf multibinôme : mentor reste MATCHE, flag multibinome retiré) |
- Log non-dispo mentor : origin
dashboardMentorCancel - Log multi-mentorat : si le mentor était en multi-mentorat, désactivation + entrée
log-multibinome(origin dashboard) - Mail jeune : template Brevo 4320 (
binome-annule-jeune-dashboard-mentor), variablesPRENOM_JEUNE,PRENOM_MENTOR
Constantes et utilitaires
| Fichier | Contenu |
|---|---|
back/src/binomes/constants/reservation.constants.ts |
Durées (15 min, 30 min), conditions SQL réutilisables |
back/src/binomes/utils/reservation-load.utils.ts |
needsFilteredReservations, relationsWithoutReservations |
front/services/reservation.js |
ADMIN_RESERVATION_MS, INSTANT_MATCHING_RESERVATION_MS, isInstantMatchingActive(), INSTANT_MATCHING_REGIONS, canAccess() |