Parcours établissement — Partie I : Étude de l'existant
État au 2026-11-08, établi à partir du code : - zorni-portal : branche
fix/establishement-forms. - zorni-controller :develop, plus les changements locaux non commités. - vivapilot :develop. - partnersapi, communicationapi et notificationsapi :origin/developdu 2026-10-27. - Keycloak : 24.0.0, avec le thèmezornietrealm-export.json(zorni-infra).Les pages https://docs.mondoviva.com sur le sujet ont aussi été consultées : Partners API, Rôles et permissions, Admin > Établissement.
⚠️ La copie locale de
partnersapi(develop, avril 2026) est en retard. Elle force encoreACTIVATEDà la création. Le comportement décrit ici est celui d'origin/develop.
Le document a deux parties : - Partie I : Étude de l'existant. Ce que fait le code aujourd'hui, et les écarts constatés. - Partie II : Évolutions. Ce qu'on propose de changer, par priorité.
Priorités : 🔴 sécurité ou bloquant, 🟠 fonctionnel important, 🟢 confort.
Suite
Les évolutions proposées (E1 à E6) sont décrites dans Partie II : Évolutions.
1. Acteurs et applications
| Acteur | Application | Rôle |
|---|---|---|
| Visiteur, puis partenaire | zorni-portal (/, puis /admin/*) |
Inscription, onboarding, gestion organisme, établissements et utilisateurs |
| Agent support Mondoviva (ZA) | vivapilot (back-office) | Consultation, activation, demande de complément, suspension |
| Passerelle GraphQL | zorni-controller | Guards, appels REST aux APIs, mise à jour des attributs Keycloak |
| Source de vérité | partnersapi | Organisations, établissements, statuts, autorisations (@PreAuthorize) |
| Identité | Keycloak : zorni-realm (portail), vivapilot-realm (back-office) |
Comptes et attributs type, owner, parentOrganism, applicationRoles, firstConnection |
| Messagerie | communicationapi (mails), notificationsapi (Kafka notification.created) |
Notifications |
2. Modèle de rôles
2.1 Attributs Keycloak (repris tels quels dans le JWT)
| Attribut | Exemple | Sens |
|---|---|---|
type |
PRO / PART |
Partenaire professionnel ou client |
owner |
true |
Compte qui s'est inscrit via « Devenir partenaire » (créateur de l'organisme) |
parentOrganism |
6df7b5c9-… |
Organisme de rattachement. Renseigné quand le rôle PSA sur une ORGANIZATION est ajouté |
applicationRoles |
[{"entityId":"973a691b-…","role":"PA","entityType":"ESTABLISHMENT"}] |
Rôles par entité. Chaque entrée est une chaîne JSON dans l'attribut multi-valué |
firstConnection |
true / false |
Déclenche la création de la fiche Person au premier login |
Droits d'édition dans le User Profile (realm-export.json). Seul parentOrganism est réservé à l'admin :
| Attribut | edit |
|---|---|
type, owner, applicationRoles, firstConnection |
admin, user |
parentOrganism |
admin |
2.2 Rôles applicatifs
| Code | Nom dans la doc | Portée | Droits principaux (partnersapi AuthorizationManager) |
|---|---|---|---|
| PSA | PRO_SUP_ADMIN | ORGANIZATION | Administrer l'organisme, créer et supprimer des établissements, gérer les informations financières, tout voir dans l'organisme |
| PA | PRO_ADMIN | ESTABLISHMENT | Lire et modifier l'établissement et le représentant légal, gérer les utilisateurs de l'établissement |
| PU | PRO_USER | ESTABLISHMENT | Lecture de l'établissement, gestion des circuits dont il est responsable |
| ZA | ZORNI_ADMIN | ORGANIZATION mondoviva |
Liste de toutes les organisations et de tous les établissements, activation, suspension, réactivation |
2.3 Matrice effective sur partnersapi (origin/develop)
| Opération | PSA (son orga) | PA (son étab.) | PU | ZA | Owner |
|---|---|---|---|---|---|
| Créer organisation | – | – | – | – | ✅ (isOwner()) |
| Lire organisation | ✅ | ✅ (membre) | ✅ (membre) | ✅ | – |
| Modifier ou supprimer organisation | ✅ | ❌ | ❌ | ❌ | – |
| Lister organisations / suspendre / réactiver orga | ❌ | ❌ | ❌ | ✅ | – |
| Créer établissement | ✅ | ❌ | ❌ | ❌ | – |
| Lister établissements | ✅ (tous) | ✅ (ceux où il a un rôle) | idem | ✅ (tous) | – |
| Lire établissement | ✅ | ✅ | ✅ | ✅ | – |
| Modifier établissement | ✅ | ✅ | ❌ | ❌ | – |
| Supprimer établissement | ✅ | ❌ | ❌ | ❌ | – |
| Représentant légal : lire, gérer | ✅ / ✅ | ✅ / ✅ | ❌ | lire seulement | – |
| Informations financières : lire, gérer | ✅ / ✅ | ❌ | ❌ | lire seulement | – |
| Activer / suspendre / réactiver établissement | ❌ | ❌ | ❌ | ✅ | – |
2.4 Analyse des jetons fournis
Jeton owner ([email protected], realm zorni-realm, client React-auth, identique au compte de la capture Keycloak) :
- type: PRO
- owner: true
- parentOrganism: 6df7b5c9-…
- 4 applicationRoles :
- PSA sur l'organisme 6df7b5c9-… ;
- PA sur les 3 établissements qu'il a créés (973a691b-…, 002da8f2-…, 206efccd-…).
- resource_access : seulement account (manage-account, delete-account).
Comparaison des deux profils :
Owner [email protected] |
Invité [email protected] |
|
|---|---|---|
owner |
true |
false |
| Rôle organisme | PSA | aucun (rattaché par parentOrganism seulement) |
| Rôles établissement | PA ×3 (créateur) | PA ×1 (attribué par le PSA) |
| Créer établissement / gérer finance | ✅ / ✅ | ❌ / ❌ |
realm-management/manage-users |
❌ | ✅ 🔴 |
aud |
account |
realm-management, account |
L'owner reçoit automatiquement un rôle PA sur chaque établissement qu'il crée (
createEstablishment). Côté portail, le menu de gestion (canManage) est ouvert àhasPARole || isAuthUserOwner || PSA.
Constats :
1. 🔴 Le controller donne realm-management/manage-users aux utilisateurs PRO.
- Où : UserService.UpdateUserByIDandAccessToken → ClientService.getRoleID (client realm-management) → UpdateUserRoleByID, qui POST { name: 'manage-users' } sur les role-mappings de l'utilisateur.
- Quand : à MT_ADD_PERSON et à MT_ACTIVATE_PERSON, pour tout type === PRO.
- Le jeton invité porte ce rôle. Le jeton owner ne l'a pas, sans doute parce que son compte date d'avant ce code ou que le rôle lui a été retiré à la main. À vérifier.
- Conséquence : avec son propre jeton, un partenaire peut appeler l'API d'administration Keycloak (/admin/realms/zorni-realm/users/...). Il peut alors :
- modifier ses propres applicationRoles, par exemple s'ajouter PSA sur n'importe quel organisme ;
- modifier les attributs, l'email ou le mot de passe de n'importe quel utilisateur du realm.
2. 🔴 type, owner et applicationRoles sont modifiables par l'utilisateur lui-même (voir le tableau du §2.1).
- Grâce à account: manage-account, ces attributs peuvent être modifiés via l'Account API.
- Ils peuvent aussi être envoyés dans le formulaire d'inscription (voir §4, étape 1).
Conclusion déduite de la configuration, non testée.
3. ℹ️ Contrôle ZA côté controller.
- Le controller exige que le jeton vienne de oauth.realm (vivapilot-realm). Un utilisateur du zorni-realm qui se donnerait ZA ne passe donc pas SupportAgentGuard.
- partnersapi, lui, fait confiance à hasAuthority('ZA') sans vérifier le realm émetteur.
3. Cycle de vie (statuts)
3.1 Organisation (Organization.StatusEnum)
stateDiagram-v2
[*] --> MISSING_INFORMATIONS : POST /organizations (incomplète)
[*] --> COMPLETED : POST /organizations (complète)
MISSING_INFORMATIONS --> COMPLETED : PUT (infos complétées)
COMPLETED --> SUSPENDED : PATCH suspend (ZA)
SUSPENDED --> COMPLETED : PATCH reactivate (ZA)
- Un établissement ne peut être créé que si l'organisation est
COMPLETED(ESTABLISHMENT_PARENT_ORGANIZATION_NOT_COMPLETED_YET). - Une organisation
SUSPENDEDne peut pas être modifiée. - Suspendre l'organisation suspend aussi ses établissements
ACTIVATED, puis leurs publications. - La réactiver ne réactive pas les établissements : il faut les réactiver un par un.
3.2 Établissement (EstablishmentStatus)
stateDiagram-v2
[*] --> MISSING_INFORMATIONS : création incomplète
[*] --> WAITING_FOR_APPROVAL : création complète
MISSING_INFORMATIONS --> WAITING_FOR_APPROVAL : PUT (complété)
WAITING_FOR_APPROVAL --> MISSING_INFORMATIONS : PUT (redevenu incomplet)
WAITING_FOR_APPROVAL --> ACTIVATED : PATCH /activate (ZA)
ACTIVATED --> SUSPENDED : PATCH /suspend (ZA)
SUSPENDED --> ACTIVATED : PATCH /reactivate (ZA)
ACTIVATED --> [*] : DELETE (soft delete, PSA)
Règle de complétude (isEstablishmentComplete). Un établissement est complet si :
- company n'est pas null ;
- tourismAgreements n'est pas null ;
- il a au moins une activité ;
- il a au moins un représentant légal ;
- il a au moins une information financière.
Cette règle ne vérifie ni la présence des documents (Modèle J, pièce d'identité), ni leurs dates d'expiration.
Règles de transition :
- Mise à jour (PUT) : le statut envoyé est ignoré. Il est recalculé seulement si l'établissement est en MISSING_INFORMATIONS ou WAITING_FOR_APPROVAL. Un établissement ACTIVATED modifié reste ACTIVATED, sans nouvelle validation.
- Activation : seulement depuis WAITING_FOR_APPROVAL, sinon erreur ESTABLISHMENT_NOT_WAITING_FOR_APPROVAL. L'activation renseigne activationDate, mais n'enregistre pas l'agent qui a activé. Évolution : voir E2.5.
- Suspension : seulement depuis ACTIVATED.
- travelsapi suspend d'abord les publications DRAFTS et PUBLISHED.
- travelsapi refuse (403) si une réservation est CREATED, CONFIRMED ou IN_PROGRESS.
- Il n'y a pas de compensation : on peut rejouer l'opération.
- L'agent et le motif ne sont pas enregistrés.
- Réactivation : l'établissement repasse ACTIVATED. Ses publications suspendues reviennent en DRAFTS et doivent être republiées.
- Suppression : soft delete (deleted = true) et suppression des trips. Réservée au PSA.
4. Parcours détaillé
Étape 1 : « Devenir partenaire » (portail, visiteur)
Header.js:handleBecomePartnerredirige vers l'URL d'inscription Keycloak, construite parapp/auth/regestration.js:- Le paramètre
typeest lu dans le navigateur, par un script du thème (zorni-infra/keycloak/themes/zorni/login/register.ftl). Au chargement de la page :Le formulaire envoie aussiconst userType = new URLSearchParams(window.location.search).get('type'); userTypeInput.value = userType || 'PART'; // champ caché user.attributes.type ownerInput.value = userType === 'PRO'; // champ caché user.attributes.owneruser.attributes.firstConnection=trueen champ caché. - Keycloak gère la vérification de l'email.
Le paramètre type=PRO est-il perdu au rafraîchissement ? Testé sur le Keycloak local (24.0.0) :
| Cas | URL affichée | type dans l'URL |
Valeur envoyée |
|---|---|---|---|
| Premier affichage, ou F5 sur cette page | /protocol/openid-connect/registrations?…&type=PRO (200, pas de redirection) |
✅ | PRO / owner=true |
| Formulaire soumis avec une erreur (email déjà pris, mots de passe différents, politique de mot de passe…) | /login-actions/registration?session_code=…&execution=…&client_id=…&tab_id=… |
❌ | PART / owner=false |
| F5 sur la page d'erreur ci-dessus | idem | ❌ | PART / owner=false |
| Lien « S'inscrire » depuis la page de connexion, ou inscription via un fournisseur social | sans type |
❌ | PART |
Dans le cas d'erreur, Keycloak renvoie pourtant bien value="PRO" et value="true" dans les champs cachés (il les reprend du formulaire posté). C'est le script qui les écrase avec PART et false, car l'URL n'a plus le paramètre.
Résultat : au moindre aller-retour d'erreur, le futur partenaire est créé comme client (PART), sans que rien ne le signale. Ensuite :
- il n'a pas accès à /admin ;
- MT_ADD_ORGANIZATION est refusée par ProOwnerGuard.
Il faut alors corriger le compte à la main dans Keycloak.
👉 Réponse : un simple F5 sur la page initiale garde type=PRO. Mais il est perdu dès que le formulaire revient en erreur, et sur tout rafraîchissement qui suit. Évolution : voir E1.2.
Étape 2 : premier login (portail)
useZornidécode le JWT et en tirefirstConnection,ownerettype.usePersonlance alors l'une de deux actions :- Owner, ou utilisateur non PRO :
MT_ADD_PERSONcrée la fiche Person dans personapi. Le controller passe ensuitefirstConnection=falseet ajouterealm-management/manage-users(voir §2.4). - Invité (
PRO,!owner) :MT_ACTIVATE_PERSONactive la Person déjà créée lors de l'invitation. - Le bouton « Devenir partenaire » est remplacé par « Dashboard » (
/admin).
Étape 3 : création de l'organisme (owner)
- Écran
/admin/organisme/add(AddOrganisme) : appel àMT_ADD_ORGANIZATION. - Guard controller :
ProOwnerGuard(type=PROetowner=true). - partnersapi :
isOwner(). Statut initialMISSING_INFORMATIONSouCOMPLETED. - Le controller ajoute ensuite :
- l'attachment
PSA / ORGANIZATIONdans personapi ; - le même rôle dans
applicationRoles; parentOrganism = organizationId.- Modification :
MT_UPDATE_ORGANIZATION, avecProOwnerParentOrganismGuard(owner etparentOrganismégal à l'id). /admin/organisme(OrganismeMain.js) est une maquette statique : valeurs en dur (« 12 rue Exemple », « En attente de validation ») et aucun appel API.
Étape 4 : création de l'établissement (PSA)
- Écran
/admin/establishments/add(AddEstablishementNew.js). Le parcours compte 3 onglets, et chaque « Suivant » enregistre l'onglet.
| Onglet | Données | Mutations |
|---|---|---|
| 01 Établissement | Raison sociale*, forme juridique, ICE, RC (n°, tribunal, Modèle J en upload), IF, TP, siège, licence tourisme (n°, date de délivrance), assurance RC pro (compagnie, police, date d'expiration), activités | Upload (UPLOAD_RESOURCES.PARTNER, SUPPORTING_DOCUMENT), puis MT_ADD_ESTABLISHMENT ou MT_UPDATE_ESTABLISHMENT |
| 02 Gouvernance | Représentant légal : nom, pièce d'identité (type, n°, date d'expiration, scan), email, téléphone, part du capital, nationalité | Upload (ID), puis MT_ADD_LEGAL_REPRESENTATIVE ou MT_UPDATE_LEGAL_REPRESENTATIVE |
| 03 Finance | Banque, titulaire, RIB | MT_ADD_FINANCIAL_INFORMATION ou MT_UPDATE_FINANCIAL_INFORMATION |
- À la création, le controller (
EstablishmentService.createEstablishment) : - ajoute l'attachment
PA / ESTABLISHMENTà la Person ; - ajoute le rôle dans
applicationRoles; - supprime l'établissement si l'une des deux étapes échoue (rollback).
- Statut obtenu :
MISSING_INFORMATIONSaprès l'onglet 01 ;WAITING_FOR_APPROVALdès que le représentant légal et les informations financières sont liés.- Le portail envoie
status: "ACTIVATED"dans l'input. Ce champ est ignoré par partnersapi. - Toast « Dossier soumis », puis redirection vers la fiche.
Étape 5 : invitation des collaborateurs (PSA / PA)
/admin/utilisateurs: la mutationcreateUser:- crée l'utilisateur Keycloak ;
- appelle
invitePartner, qui crée la Person et les attachments ; - envoie l'email
UPDATE_PASSWORD(execute-actions-email). - Rôles :
MT_MANAGE_ATTACHMENTajoute, modifie ou supprime des rôles PA ou PU par établissement. Le controller les répercute dansapplicationRoles. - Premier login de l'invité : voir étape 2 (
MT_ACTIVATE_PERSON).
⚠️
createUser,relaunchUser,deleteUseretMT_MANAGE_ATTACHMENTn'ont aucun guard de portée dans le controller.Seul le guard global (
AuthGuard, jeton valide) s'applique. Le controller modifie ensuite les rôles avec le service account Keycloak, et partnersapi n'intervient pas.Tout utilisateur authentifié peut donc ajouter un rôle à n'importe quel email. Constat issu de la lecture du code, non testé.
Étape 6 : instruction par le support (vivapilot, ZA)
- Liste
/etablissements:QR_LIST_ESTABLISHMENTsansorganizationId(guardProParentOrganismOrSupportAgentGuard). - Les filtres statut et recherche s'appliquent sur la page chargée.
- partnersapi supporte pourtant
statusetsearchcôté serveur. - Fiche :
QR_GET_ESTABLISHMENT, puis les onglets Entreprise / Gouvernance (QR_GET_LEGAL_REPRESENTATIVE) / Finance (QR_GET_FINANCIAL_INFORMATION). L'organisme est affiché par son nom (QR_GET_ORGANIZATION). - Actions :
- Activer : visible si
WAITING_FOR_APPROVAL. LanceMT_ACTIVATE_ESTABLISHMENT(SupportAgentGuard, puisPATCH /establishments/{id}/activate). - Demander un complément : modale avec une description et une case « Documents requis ». UI seulement, aucune mutation n'est branchée.
- Suspendre : bouton désactivé.
- Envoyer un message : bouton désactivé.
Étape 7 : vie de l'établissement actif
- Le PSA ou le PA peut modifier les informations. L'établissement reste
ACTIVATED, sans revalidation. - Les circuits (publications) sont visibles dans vivapilot (
/circuits).
Étape 8 : suspension et réactivation (ZA)
- partnersapi expose déjà :
PATCH /establishments/{id}/suspendetPATCH /establishments/{id}/reactivate;PATCH /organizations/{id}/suspendetPATCH /organizations/{id}/reactivate.- Rien n'est branché côté controller ni côté vivapilot :
- Il n'existe pas de
MT_SUSPEND_ESTABLISHMENTni deMT_REACTIVATE_ESTABLISHMENT. - 🔴 L'enum GraphQL
EstablishmentStatusEnumdu controller contientDISABLEDmais pasSUSPENDED. Dès qu'un établissement est suspendu, unQR_GET_ESTABLISHMENTou unQR_LIST_ESTABLISHMENTqui le contient échouera à la sérialisation (« Enum "EstablishmentStatusEnum" cannot represent value: "SUSPENDED" »).
sequenceDiagram
autonumber
actor V as Visiteur
participant P as zorni-portal
participant K as Keycloak (zorni-realm)
participant C as zorni-controller
participant PA as partnersapi
participant PE as personapi
actor ZA as Agent ZA (vivapilot)
participant T as travelsapi
V->>P: Devenir partenaire
P->>K: /registrations?type=PRO
Note over K: type et owner posés par le JS du thème (champs cachés)
K-->>P: compte owner, firstConnection=true
P->>C: MT_ADD_PERSON
C->>PE: POST /persons
C->>K: firstConnection=false, + manage-users
P->>C: MT_ADD_ORGANIZATION
C->>PA: POST /organizations
C->>K: applicationRoles += PSA, parentOrganism
P->>C: MT_ADD_ESTABLISHMENT (+ représentant légal + finance)
C->>PA: POST /establishments
PA-->>C: MISSING_INFORMATIONS → WAITING_FOR_APPROVAL
C->>K: applicationRoles += PA
ZA->>C: QR_LIST / QR_GET_ESTABLISHMENT
ZA->>C: MT_ACTIVATE_ESTABLISHMENT
C->>PA: PATCH /activate → ACTIVATED
Note over ZA,PA: Suspension : API prête, non exposée
ZA-->>PA: PATCH /suspend (à brancher)
PA->>T: suspendre publications (403 si réservations actives)
5. Notifications existantes
- communicationapi possède des templates mail prêts, rangés dans
Account/: organization-creation-request,organization-activation-confirmation,organization-info-completion-request;establishment-creation-request,establishment-activation-confirmation,establishment-info-completion-request;profile-completion-request,user-account-activation-confirmation.- Ces mails sont envoyés via
POST /mailsavec unecategory. - Aucun service ne les déclenche pour le parcours établissement : ni le controller, ni partnersapi.
- notificationsapi consomme le topic Kafka
notification.created(champsresource,resourceId,notificationType,establishmentId,receiverId) et republie un événement d'envoi. - Aucun événement n'est publié lors des transitions de statut.
- Aucune alerte n'existe avant l'expiration des documents.
- Pourtant, les dates sont en base :
expiryDatede l'assurance,identityExpiryDatedu représentant légal. - La licence tourisme n'a qu'une
issueDate, et le Modèle J n'a aucune date.
6. Écarts avec la documentation (docs.mondoviva.com)
- Rôles et permissions :
- n'explicite pas la correspondance PSA/PA/PU/ZA avec PRO_SUP_ADMIN/PRO_ADMIN/PRO_USER/ZORNI_ADMIN ;
- laisse « à définir » les droits d'activation et de création d'utilisateur ;
- ne mentionne ni
ownerni les informations financières (PSA seulement). - Les diagrammes de séquence Partners API :
- montrent un
delete()physique, sans statuts ni validation ; - décrivent des endpoints
/organizations/{id}/userset/establishments/{id}/servicesabsents du code. - Les pages Admin > Établissement / Organisme sont vides.
7. Récapitulatif des écarts constatés
| # | Gravité | Où | Constat | Évolution |
|---|---|---|---|---|
| C1 | 🔴 | zorni-controller + Keycloak | Le controller donne realm-management/manage-users aux PRO au premier login (UpdateUserRoleByID) |
E1.1 |
| C2 | 🔴 | Keycloak (User Profile) | type, owner et applicationRoles sont modifiables par l'utilisateur lui-même |
E1.3 |
| C3 | 🔴 | Keycloak (thème) | type=PRO est lu dans l'URL par le JS : il est perdu après une erreur du formulaire, et le partenaire est créé PART |
E1.2 |
| C4 | 🔴 | zorni-controller | createUser, deleteUser, relaunchUser et MT_MANAGE_ATTACHMENT n'ont pas de guard de portée |
E1.4 |
| C5 | 🔴 | zorni-controller | SUSPENDED est absent de EstablishmentStatusEnum |
E2.1 |
| C6 | 🟠 | controller et vivapilot | Suspension et réactivation ne sont pas exposées (l'API partnersapi est prête) | E2.2 |
| C7 | 🟠 | controller et vivapilot | « Demander un complément » n'est pas branché | E2.3 |
| C8 | 🟠 | partnersapi | L'activation, la suspension et la réactivation n'enregistrent pas l'agent ZA | E2.5 |
| C9 | 🟠 | Toute la plateforme | Aucun mail ni aucune notification n'est envoyé lors des transitions, alors que les templates existent | E2.6 |
| C10 | 🟠 | partnersapi | Pas de revalidation après modification d'un établissement actif ; documents et dates non contrôlés | E2.4, E4.4 |
| C11 | 🟠 | Toute la plateforme | Pas d'alerte avant l'expiration des documents | E5 |
| C12 | 🟠 | zorni-portal | OrganismeMain.js est une maquette statique |
E4.1 |
| C13 | 🟠 | vivapilot / controller | Filtres statut et recherche appliqués sur la page chargée seulement | E3.1 |
| C14 | 🟢 | zorni-portal | status: "ACTIVATED" est envoyé à la création (ignoré) |
E4.3 |
| C15 | 🟢 | partnersapi | hasAuthority('ZA') ne vérifie pas le realm émetteur |
E1.5 |
| C16 | 🟢 | Postes de dev | La copie locale de partnersapi/develop est en retard sur origin |
– |
| C17 | 🟢 | docs.mondoviva.com | Rôles, statuts et séquences sont incomplets ou obsolètes | E6 |
➡️ Suite : Partie II : Évolutions