Skip to content

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_reservation et algo_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

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) :

  1. Charge tous les bénévoles matchables depuis la DB locale (loadMatchableBenevoles)
  2. Calcule un score de rating classique (ratingService.calculateBinomeRating) pour chacun
  3. Exclut les bénévoles avec score -99 (disqualifiés)
  4. 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).

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 : 1. Il a une AdminReservation active (30 min), OU 2. Il apparaît dans une InstantMatchingReservation active (15 min)

Les deux systèmes se cumulent.


Implémentation backend

  1. postBinome (BinomeController) : Bloque la création si le jeune ou le bénévole est en matching instantané actif.
  2. Listes de matching (RatingController) : jeuneList et benevoleList excluent les profils en matching instantané actif.
  3. Listes BO (JeuneRepository, BenevoleRepository) : findAndFilter exclut 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)

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.

Option admin « Matching instant »

Élément Détail
Stockage administrator.options.options.matching_instant
Édition Page BO /bo/admin/cs/:id/edit — case à cocher
Liste admins Colonne + filtre checkbox sur /bo/admin/cs
API GET /admin/role/all?filterMatchingInstant=true ; filtre via JSON_EXTRACT(options, '$.options.matching_instant')
Valeur par défaut matching_instant: false à la création du rôle

Assignation d'un admin à la création du binôme instantané

Lors de createBinomeInstant, un admin de suivi est sélectionné :

Règle Détail
Critères Admins de la région du jeune (schoolRegion), matching_instant: true, profil admin ou superadmin, sandbox compatible
Choix Aléatoire parmi les éligibles (pickRandomAdminForInstantMatching)
Si aucun adminId = null ; binôme créé quand même
Assignation binome.adminId, jeune.adminId, benevole.adminAssocieId

Bénévoles éligibles : sans admin associé

Contexte Vérification
Microservice checkBenevoleDisponible : si adminId === '__instant_matching__' et benevole.adminAssocieId != null → exclu
Fallback local loadMatchableBenevoles : filtre benevole.adminAssocieId != null → exclu

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.validatecreateBinomeInstant() instant
Back-office — admin avec option matching instantané BinomeController.postBinomecreateBinome() 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 (validatecreateBinomeInstant) : Matching instantané - nouvel algo - {DD/MM/YYYY} (score v2) ou Matching instantané - algo classique - {DD/MM/YYYY} (score legacy)
  • Parcours BO (postBinome avec isInstantMatching) : texte basé sur le query param algo

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 flag wasProposedInInstantMatching: boolean
  • GET /rating/jeuneList/:benevoleId : idem pour les jeunes
  • GET /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) : 1. Vérifier jeune existant, req.user propriétaire, état APTE et Autonome 2. Vérifier via findCurrentForJeune — si réservations existantes, les renvoyer avec flag expired 3. Appel microservice getPersonnalizedBenevolesOnePerCategory() — 1 bénévole matchable par catégorie ; si microservice indisponible → fallback local top 3 par rating classique (type_mentor = NULL) 4. Mélange aléatoire en préservant la paire (bénévole + type_mentor) 5. Créer 3 lignes InstantMatchingReservation avec orderIndex 0/1/2, etat: 'proposé', score, type_mentor 6. 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: false si aucun - Réservations soft-deleted → erreur 400 "Le matching instantané a déjà été proposé."


Endpoint 2 : POST /instant-matching/refresh

Quand : Jeune revient sur la page (rechargement, retour, timer proche de l'expiration).

Entrée : { jeuneId: string }

Logique : 1. Vérifier jeune + ownership 2. Récupérer réservations non-supprimées via findCurrentForJeune 3. Si aucune → { success: true, reservations: null } 4. 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 5. Tous disponibles : 3 nouvelles lignes avec mêmes score / type_mentor 6. Au moins un indisponible : appel microservice getPersonnalizedBenevolesOnePerCategory() pour 3 nouveaux (fallback getTopBenevolesForJeune() si microservice indisponible → type_mentor = NULL) 7. 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 : 1. Vérifier jeune + ownership 2. Vérifier que le bénévole fait partie des réservations actuelles 3. updateEtatForValidate(jeuneId, benevoleId) — sélectionné → sélectionné, autres → autre selectionné 4. 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 : 1. Vérifier jeune + ownership 2. Vérifier que le bénévole est dans les réservations actives 3. Validations de sécurité : jeune pas déjà MATCHE, jeune et bénévole Autonome, bénévole non-multibinome pas MATCHE, limites binômes, onlyOneBinome, PNP/partenaire, PNP/VIP, même sandbox 4. updateEtatForValidate(jeuneId, benevoleId, true) — sélectionné → matché, autres → autre selectionné 5. Soft-delete des 3 réservations 6. createBinomeInstant() : type = instant, typeAlgo = v2/classic, status EN_ATTENTE, firstMessageJeune stocké 7. Mails : événement instant.binome.createdinstant.benevole-0 et instant.jeune-0 ; lastStepDone=0, nextStepDate = +15 jours 8. Statuts : jeune → MATCHE, bénévole → MATCHE 9. Commentaire automatique sur le binôme 10. Si sandbox MVLS : mvlsService.publishBinomeMessage 11. 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 / onlyOneBinome409 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 }

Logique : 1. Vérifier jeune + ownership 2. updateEtatForJeune(jeuneId, 'refusé') 3. Soft-delete via softDeleteAllCurrentForJeune 4. Commentaire individuel : "Matching instantané refusé" (adminId: null) 5. Le jeune reste APTE

Sortie : { success: boolean }


Endpoint 6 : POST /instant-matching/cancel

Quand : Le jeune annule et ne veut plus être disponible.

Entrée : { jeuneId: string }

Logique : 1. Vérifier jeune + ownership 2. updateEtatForJeune(jeuneId, 'desactivation') 3. Soft-delete via softDeleteAllCurrentForJeune 4. Commentaire individuel : "Matching instantané - désactivation" 5. 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

  1. Valeurs CF depuis Benevole.secteurMetier + PosteBenevole[].secteur (déduplication case-insensitive)
  2. Chaque valeur CF traduite en { niveau1, niveau2 } via taxonomie_secteurs_professions.json (comparaison uniquement, jamais pour affichage)
  3. Si la traduction correspond à une entrée LLM → l'entrée LLM est "couverte", on affiche la valeur CF d'origine
  4. Entrées LLM non couvertes affichées par niveau1 uniquement
  5. 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

  1. Jeune termine l'inscription → POST /instant-matching/propose
  2. Sélection des 3 bénévoles : microservice (1 par catégorie) ou fallback local avec calculateBinomeRating
  3. Création de 3 lignes InstantMatchingReservation avec type_mentor = clé mentors ou NULL (fallback/BO)
  4. 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 → /propose renvoie 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
  • /propose renvoie les mêmes 3 avec expired: true
  • Le front appelle /refresh pour 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 : 1. propose/refresh → réservations avec champ etat 2. etat === 'sélectionné'picked défini, affichage formulaire 3. Clic "Confirmer" → POST /instant-matching/confirm-selection 4. 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é", jeune reste APTE
Annulation cancel → soft-delete, commentaire "désactivation", jeune NON_DISPONIBLE
Expiration Profils redeviennent matchables BO ; réservations conservées ; /proposeexpired: 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 : aléatoire (région du jeune + matching_instant: true) ; null si aucun éligible.

Mails (événement instant.binome.created) : - instant.benevole-0 (Brevo #3955) : PRENOM, PRENOM_admin, PRENOM_JEUNE, schoolCursus, filiereJeune, texte_matching - instant.jeune-0 : PRENOM, PRENOM_admin, PRENOM_mentor


Comportements spécifiques

Quand le timer expire sur /compte/jeune/mentors : 1. Popup : souhait de renouveler la réservation 2. "Mettre à jour" → POST /instant-matching/refresh 3. Si le jeune recharge avec une réservation expirée, la popup s'affiche immédiatement

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' ET multibinome ET activeBinomeCount < 2) - state === 'Autonome' - Pas de réservation admin active - Pas de réservation instant matching pour un autre jeune


Régions éligibles au matching instantané

Constante INSTANT_MATCHING_REGIONS (front/constants/reservation.js) :

['Île-de-France', 'Grand Est', 'Bourgogne-Franche-Comté']
Comportement Condition
Champ "about" masqué à l'onboarding schoolRegion dans INSTANT_MATCHING_REGIONS

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)

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/constants/reservation.js ADMIN_RESERVATION_MS, INSTANT_MATCHING_RESERVATION_MS, isInstantMatchingActive(), INSTANT_MATCHING_REGIONS