Skip to content

Parcours établissement — Partie II : Évolutions

Les références C1 à C17 et les § renvoient à la Partie I : Étude de l'existant.

Priorités : 🔴 sécurité ou bloquant, 🟠 fonctionnel important, 🟢 confort.

E1. Sécurité, rôles et inscription

E1.1 🔴 Ne plus donner manage-users aux partenaires (C1)

  • Supprimer l'appel à UpdateUserRoleByID dans UpdateUserByIDandAccessToken. Le controller fait déjà les opérations d'administration avec son service account graphql-auth.
  • Retirer le rôle aux comptes existants : Users → Role mapping, ou un script via l'API admin.

E1.2 🔴 Fiabiliser le type de compte à l'inscription (C3)

Correctif immédiat (thème, une ligne) : ne plus écraser la valeur renvoyée par Keycloak quand l'URL n'a pas le paramètre.

const userType = getQueryParam('type');
if (userTypeInput && userType) {           // avant : toujours écrasé, 'PART' par défaut
    userTypeInput.value = userType;
    ownerInput.value = userType === 'PRO';
}
if (userTypeInput && !userTypeInput.value) { // premier affichage sans paramètre
    userTypeInput.value = 'PART';
    ownerInput.value = false;
}
Avec ce correctif, après une erreur, les champs cachés gardent PRO et true (renvoyés par Keycloak depuis formData).

Le type reste malgré tout décidé par le navigateur : n'importe qui peut poster type=PRO, owner=true, ou même applicationRoles. Il faut donc aussi une correction de fond. Deux options :

A. FormAction SPI Keycloak B. Mutation « devenir partenaire » (recommandée)
Principe Keycloak conserve les paramètres OIDC non standard dans la session d'authentification (client_request_param_type). Une FormAction personnalisée les lit à la création du compte, positionne type et owner côté serveur, et ignore ce qui vient du formulaire. L'inscription crée toujours un compte PART. « Devenir partenaire » mène à l'inscription ou à la connexion, puis le portail appelle MT_BECOME_PARTNER : le controller passe type=PRO, owner=true via l'API admin.
Survit aux erreurs et au F5 ✅ (stocké côté serveur) ✅ (rien ne dépend de l'URL)
Fonctionne avec la connexion sociale ❌ (flux à part) ✅
Permet à un client existant de devenir partenaire ❌ ✅
Coût JAR Java à builder et déployer avec l'image Keycloak 1 mutation + 1 écran de confirmation dans le portail
  • Dans les deux cas, les champs cachés type, owner et firstConnection disparaissent du thème.
  • Ces attributs passent en admin only (E1.3).
  • Garde-fou côté controller pour MT_BECOME_PARTNER : refuser si le compte est déjà PRO ou s'il a des applicationRoles.

E1.3 🔴 Verrouiller les attributs sensibles (C2)

  • Dans le User Profile du zorni-realm, passer type, owner, applicationRoles, firstConnection et parentOrganism en edit: [admin], et view: [admin] si possible.
  • Régler Unmanaged attributes sur Disabled.
  • Prérequis : E1.2 (option A ou B), car le formulaire d'inscription ne pourra plus poser ces attributs.

E1.4 🔴 Guards de portée sur la gestion des utilisateurs (C4)

  • Concerne createUser, relaunchUser, deleteUser et MT_MANAGE_ATTACHMENT.
  • Le PSA de l'organisme peut tout faire.
  • Le PA ne gère que ses établissements, et seulement les rôles PU et PA.

E1.5 🟢 Contrôle ZA dans partnersapi (C15)

Vérifier le realm émetteur (iss) avant d'accorder l'autorité ZA, comme le fait déjà le controller.

E1.6 🟢 Stockage des rôles

À moyen terme, migrer applicationRoles (JSON dans un attribut) vers des groupes Keycloak ou une table de rôles dans personapi.

E2. Cycle de vie de l'établissement

E2.1 🔴 Statut SUSPENDED dans le GraphQL (C5)

  • Ajouter SUSPENDED à EstablishmentStatusEnum dans le controller.
  • Retirer DISABLED s'il n'est plus utilisé.
  • Faire de même pour le statut d'organisation.

E2.2 🟠 Suspension et réactivation (C6)

  • Mutations ZA à ajouter :
  • MT_SUSPEND_ESTABLISHMENT et MT_REACTIVATE_ESTABLISHMENT ;
  • MT_SUSPEND_ORGANIZATION et MT_REACTIVATE_ORGANIZATION ;
  • QR_LIST_ORGANIZATION.
  • vivapilot :
  • brancher le bouton « Suspendre », avec un motif obligatoire ;
  • afficher le refus 403 « réservations actives » ;
  • ajouter un bouton « Réactiver ».

E2.3 🟠 Demande de complément (C7)

  • A : nouvel endpoint partnersapi PATCH /establishments/{id}/request-information, qui :
  • passe le statut en MISSING_INFORMATIONS ;
  • stocke { message, documentsRequired, requestedBy, requestedAt } ;
  • envoie le mail ESTABLISHMENT_INFO_COMPLETION_REQUEST ;
  • notifie le PSA et le PA.
  • B : une conversation dans communicationapi, liée à l'établissement.
  • Recommandation : A + B. Le statut bloque l'activation, et la conversation garde l'historique des échanges.
  • La modale vivapilot existe déjà (RequestComplementForm.js).

E2.4 🟠 Revalidation après modification sensible (C10)

Un changement de RIB, de représentant légal ou de document sur un établissement ACTIVATED devrait le repasser en WAITING_FOR_APPROVAL. À défaut, ajouter au minimum un flag pendingReview, visible dans vivapilot.

E2.5 🟠 Enregistrer l'agent ZA qui active, suspend ou réactive (C8)

  • partnersapi :
  • nouvelle colonne activated_by (migration Flyway V4__add_establishment_activated_by.sql), renseignée dans activateEstablishment ;
  • la valeur vient de ZorniCaller.userId(), c'est-à-dire du sub du jeton ZA (zorni-tools-lib, déjà injecté via ZorniRoleScope) ;
  • elle ne doit pas venir d'un paramètre de la requête, sinon n'importe qui pourrait se faire passer pour un autre agent.
  • Contrat : ajouter activatedBy (string, lecture seule) à la représentation Establishment. Il est ignoré en entrée, comme status.
  • zorni-controller :
  • aucun changement sur MT_ACTIVATE_ESTABLISHMENT, le jeton de l'agent est déjà transmis par AxiosAuthInterceptor ;
  • exposer activatedBy et activationDate dans le type GraphQL.
  • vivapilot : afficher « Activé le … par … » dans la fiche.
  • L'agent appartient à vivapilot-realm : son sub n'est pas forcément une Person de personapi.
  • Pour afficher un nom, deux options :
    • stocker un instantané activatedByName / activatedByEmail (claims du jeton) au moment de l'activation ;
    • résoudre le sub via l'API admin du realm.
  • Recommandation : l'instantané. Il reste lisible même si le compte de l'agent est supprimé.
  • Suspension et réactivation : même principe, avec en plus le motif de suspension.
  • Cible : une table d'historique plutôt qu'une colonne par transition :
    establishment_status_history (from, to, actorId, actorName, reason, date)
    
    Elle est affichée dans vivapilot, sur le modèle de l'historique des tickets.

E2.6 🟠 Mails et notifications aux transitions (C9)

Brancher les templates existants à chaque transition : - création → ESTABLISHMENT_CREATION_REQUEST ; - demande de complément → ESTABLISHMENT_INFO_COMPLETION_REQUEST ; - activation → ESTABLISHMENT_ACTIVATION_CONFIRMATION ; - suspension et réactivation → nouveaux templates à créer.

E2.7 🟢 Champs à exposer

Exposer dans le GraphQL reference et createdDate, déjà présents dans partnersapi.

E3. Back-office vivapilot

  1. 🟠 Filtres statut et recherche côté serveur (C13). partnersapi accepte déjà status et search ; le controller ne transmet aujourd'hui que activated.
  2. 🟠 KPI : nombre d'établissements en attente de validation, nombre avec des documents expirant sous 30 jours (E5).
  3. 🟢 Checklist KYC par document dans la fiche (conforme / non conforme / commentaire), qui alimente la demande de complément.
  4. 🟢 Brancher « Envoyer un message » sur communicationapi.

E4. Portail partenaire

  1. 🟠 Remplacer la maquette OrganismeMain.js par une vraie page : QR_GET_ORGANIZATION, statut réel, formulaire d'édition (C12).
  2. 🟠 Afficher au partenaire le statut de son établissement, avec ce qui manque et ce qui a été demandé par le support.
  3. 🟢 Retirer status: "ACTIVATED" de buildEstablishmentInput (C14).
  4. 🟢 Rendre obligatoires, dans le front et dans isEstablishmentComplete, les documents et leurs dates : Modèle J, assurance, pièce d'identité.

E5. Notification avant expiration d'un document (étude)

Documents concernés (données déjà en base, partnersapi)

Document Champ Entité
Assurance RC professionnelle expiryDate ProfessionalLiabilityInsuranceEntity
Pièce d'identité du représentant légal identityExpiryDate LegalRepresentativeEntity
Licence / agrément tourisme issueDate seulement TourismAgreementsEntity. Ajouter expiryDate si la licence a une durée de validité
Modèle J (registre de commerce) aucune date Optionnel : issuedAt avec une validité configurable (ex. 3 mois)

Besoin

  • Prévenir le partenaire (PSA de l'organisme et PA de l'établissement) avant l'expiration, par paliers configurables : J-60, J-30, J-7, J0.
  • Prévenir le support (ZA) à J-7 et J0, et faire remonter l'établissement dans vivapilot.
  • Ne jamais envoyer deux fois le même palier pour le même document.
  • Gérer le renouvellement : une nouvelle date réarme les paliers.
  • Option : à J+X sans renouvellement, l'établissement est signalé non conforme. Pas de suspension automatique : le ZA décide (E2.2).

Architecture recommandée

flowchart LR
    S["partnersapi<br/>@Scheduled quotidien + ShedLock"] -->|"SELECT documents<br/>expirant ≤ J+60"| DB[(partners DB)]
    S -->|"INSERT document_expiry_alert<br/>(idempotence)"| DB
    S -->|"Kafka notification.created<br/>type DOCUMENT_EXPIRING"| N[notificationsapi]
    S -->|"POST /mails<br/>DOCUMENT_EXPIRY_REMINDER"| M[communicationapi]
    N --> P[Portail : cloche]
    S -.->|"compliance<br/>dans la représentation"| V[vivapilot : KPI + filtre]
  1. Où tourne le job : dans partnersapi. C'est le propriétaire des données, donc pas d'appel réseau ni d'API supplémentaire.
  2. Utiliser @Scheduled(cron = "0 0 7 * * *", zone = "Africa/Casablanca").
  3. Protéger le job par ShedLock (verrou en base) pour qu'une seule réplique Kubernetes l'exécute.
  4. Alternatives écartées :
    • un CronJob Kubernetes qui appelle un endpoint technique : plus de configuration et un secret de plus ;
    • taskapi : son dernier commit date d'août 2025 et il n'a pas de scheduler.
  5. Requête. Pour chaque type de document, sélectionner les établissements non supprimés, en statut ACTIVATED, WAITING_FOR_APPROVAL ou MISSING_INFORMATIONS, dont la date d'expiration est ≤ aujourd'hui + 60 jours. Ajouter des index sur expiry_date et identity_expiry_date (migration Flyway).
  6. Idempotence. Créer une nouvelle table :
    CREATE TABLE document_expiry_alert (
      id               UUID PRIMARY KEY,
      establishment_id VARCHAR NOT NULL,
      document_type    VARCHAR NOT NULL,   -- INSURANCE | LEGAL_REP_ID | TOURISM_LICENSE | MODEL_J
      document_ref     VARCHAR NOT NULL,   -- id du représentant légal, etc.
      expiry_date      DATE    NOT NULL,
      threshold_days   INT     NOT NULL,   -- 60 | 30 | 7 | 0
      sent_at          TIMESTAMP NOT NULL,
      UNIQUE (document_ref, document_type, expiry_date, threshold_days)
    );
    
  7. Le palier retenu est le plus petit seuil atteint et pas encore envoyé : un job en retard n'envoie pas trois mails d'un coup.
  8. Comme expiry_date fait partie de la clé, un document renouvelé (nouvelle date) réarme automatiquement les paliers.
  9. Destinataires. Un établissement ne connaît pas ses utilisateurs : il faut un appel à personapi (attachments par entityId) ou à Keycloak (recherche par applicationRoles). Recommandation : personapi expose GET /persons?attachmentIds=...&roles=PSA,PA, ou on réutilise QR_LIST_PERSON, qui filtre déjà par attachementIds.
  10. Canaux.
  11. In-app : publier sur notification.created avec resource=ESTABLISHMENT, resourceId, establishmentId et le nouveau NotificationType.DOCUMENT_EXPIRING. Si receiverId est absent, notificationsapi diffuse déjà à l'établissement.
  12. Email : nouvelle catégorie DOCUMENT_EXPIRY_REMINDER dans communicationapi, avec un template Account/document-expiry-reminder.html. Variables : établissement, document, date, jours restants, lien vers /admin/establishments/{id}/edit.
  13. Support : à J-7 et J0, une notification au groupe ZA, ou la création automatique d'un ticket dans support-api (catégorie « Conformité »).
  14. Exposition pour vivapilot et le portail. Ajouter à la représentation de l'établissement un champ calculé :
    compliance: { status: OK | EXPIRING_SOON | EXPIRED, documents: [{ type, expiryDate, daysLeft }] }
    
  15. vivapilot :
    • badge sur la liste ;
    • KPI « Documents expirant (30 j) » ;
    • filtre complianceStatus côté serveur ;
    • bannière dans la fiche.
  16. Portail : bannière « Votre attestation d'assurance expire le … », avec un bouton « Mettre à jour ».
  17. Configuration. Les paliers, le fuseau, le délai de grâce et l'activation du job passent par application.yml et des variables d'environnement Kubernetes. Prévoir un feature flag zorni.compliance.expiry-alerts.enabled.
  18. Tests.
  19. Tests unitaires du calcul de palier avec une Clock injectée (dates figées).
  20. Test d'intégration du job sur une base Testcontainers.
  21. Test de non-duplication : le job lancé deux fois n'envoie qu'une alerte.

Estimation indicative

Lot Contenu Charge
1 partnersapi : migration, job, ShedLock, idempotence, événement Kafka 3 j
2 communicationapi : catégorie et template mail 1 j
3 notificationsapi : NotificationType.DOCUMENT_EXPIRING ; portail : bannière et cloche 1,5 j
4 partnersapi et controller : champ compliance et filtre ; vivapilot : badge, KPI, filtre 2 j
5 Licence tourisme : ajout de expiryDate (back et formulaire portail) 1 j

Points à trancher avec le métier

  • Les paliers exacts, et les destinataires de chaque palier.
  • Le comportement à J0 : simple signalement, retour en WAITING_FOR_APPROVAL, ou suspension après un délai de grâce.
  • La validité de la licence tourisme et du Modèle J.

E6. Documentation

Mettre à jour docs.mondoviva.com (C17) à partir des §2 et §3 : - correspondance des rôles ; - matrice des droits ; - diagrammes d'états ; - séquences d'activation et de suspension.

Synthèse des évolutions

Réf. Priorité Évolution Applications
E1.1 🔴 Ne plus donner manage-users aux PRO controller, Keycloak
E1.2 🔴 Type de compte fiable à l'inscription (correctif du thème + option B) Keycloak thème, portail, controller
E1.3 🔴 Attributs sensibles en admin only Keycloak
E1.4 🔴 Guards de portée sur la gestion des utilisateurs controller
E2.1 🔴 SUSPENDED dans le GraphQL controller
E2.2 🟠 Suspension et réactivation controller, vivapilot
E2.3 🟠 Demande de complément partnersapi, communicationapi, controller, vivapilot
E2.4 🟠 Revalidation après modification sensible partnersapi, vivapilot
E2.5 🟠 Agent ZA enregistré (activatedBy, historique) partnersapi, controller, vivapilot
E2.6 🟠 Mails et notifications aux transitions partnersapi / controller, communicationapi
E3.1 🟠 Filtres côté serveur controller, vivapilot
E4.1 🟠 Vraie page Organisme portail
E5 🟠 Alertes d'expiration des documents partnersapi, communicationapi, notificationsapi, portail, vivapilot
E1.5, E1.6, E2.7, E3.3, E3.4, E4.3, E4.4, E6 🟢 Confort et dette –