Skip to content

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/develop du 2026-10-27. - Keycloak : 24.0.0, avec le thème zorni et realm-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 encore ACTIVATED à 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 SUSPENDED ne 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 : handleBecomePartner redirige vers l'URL d'inscription Keycloak, construite par app/auth/regestration.js :
    {authServerUrl}/realms/zorni-realm/protocol/openid-connect/registrations
      ?client_id=React-auth&redirect_uri={origin}/callback&...&type=PRO
    
  • Le paramètre type est lu dans le navigateur, par un script du thème (zorni-infra/keycloak/themes/zorni/login/register.ftl). Au chargement de la page :
    const 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.owner
    
    Le formulaire envoie aussi user.attributes.firstConnection=true en 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)

  • useZorni décode le JWT et en tire firstConnection, owner et type.
  • usePerson lance alors l'une de deux actions :
  • Owner, ou utilisateur non PRO : MT_ADD_PERSON crée la fiche Person dans personapi. Le controller passe ensuite firstConnection=false et ajoute realm-management/manage-users (voir §2.4).
  • Invité (PRO, !owner) : MT_ACTIVATE_PERSON active 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=PRO et owner=true).
  • partnersapi : isOwner(). Statut initial MISSING_INFORMATIONS ou COMPLETED.
  • Le controller ajoute ensuite :
  • l'attachment PSA / ORGANIZATION dans personapi ;
  • le même rôle dans applicationRoles ;
  • parentOrganism = organizationId.
  • Modification : MT_UPDATE_ORGANIZATION, avec ProOwnerParentOrganismGuard (owner et parentOrganism é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_INFORMATIONS après l'onglet 01 ;
  • WAITING_FOR_APPROVAL dè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)

  1. /admin/utilisateurs : la mutation createUser :
  2. crée l'utilisateur Keycloak ;
  3. appelle invitePartner, qui crée la Person et les attachments ;
  4. envoie l'email UPDATE_PASSWORD (execute-actions-email).
  5. Rôles : MT_MANAGE_ATTACHMENT ajoute, modifie ou supprime des rôles PA ou PU par établissement. Le controller les répercute dans applicationRoles.
  6. Premier login de l'invité : voir étape 2 (MT_ACTIVATE_PERSON).

⚠️ createUser, relaunchUser, deleteUser et MT_MANAGE_ATTACHMENT n'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_ESTABLISHMENT sans organizationId (guard ProParentOrganismOrSupportAgentGuard).
  • Les filtres statut et recherche s'appliquent sur la page chargée.
  • partnersapi supporte pourtant status et search cô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. Lance MT_ACTIVATE_ESTABLISHMENT (SupportAgentGuard, puis PATCH /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}/suspend et PATCH /establishments/{id}/reactivate ;
  • PATCH /organizations/{id}/suspend et PATCH /organizations/{id}/reactivate.
  • Rien n'est branché côté controller ni côté vivapilot :
  • Il n'existe pas de MT_SUSPEND_ESTABLISHMENT ni de MT_REACTIVATE_ESTABLISHMENT.
  • 🔴 L'enum GraphQL EstablishmentStatusEnum du controller contient DISABLED mais pas SUSPENDED. Dès qu'un établissement est suspendu, un QR_GET_ESTABLISHMENT ou un QR_LIST_ESTABLISHMENT qui 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 /mails avec une category.
  • Aucun service ne les déclenche pour le parcours établissement : ni le controller, ni partnersapi.
  • notificationsapi consomme le topic Kafka notification.created (champs resource, 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 : expiryDate de l'assurance, identityExpiryDate du 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 owner ni 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}/users et /establishments/{id}/services absents 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