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 à
UpdateUserRoleByIDdansUpdateUserByIDandAccessToken. Le controller fait déjà les opérations d'administration avec son service accountgraphql-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;
}
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,owneretfirstConnectiondisparaissent 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àPROou s'il a desapplicationRoles.
E1.3 🔴 Verrouiller les attributs sensibles (C2)
- Dans le User Profile du
zorni-realm, passertype,owner,applicationRoles,firstConnectionetparentOrganismenedit: [admin], etview: [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,deleteUseretMT_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àEstablishmentStatusEnumdans le controller. - Retirer
DISABLEDs'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_ESTABLISHMENTetMT_REACTIVATE_ESTABLISHMENT;MT_SUSPEND_ORGANIZATIONetMT_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 FlywayV4__add_establishment_activated_by.sql), renseignée dansactivateEstablishment; - la valeur vient de
ZorniCaller.userId(), c'est-à-dire dusubdu jeton ZA (zorni-tools-lib, déjà injecté viaZorniRoleScope) ; - 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ésentationEstablishment. Il est ignoré en entrée, commestatus. - zorni-controller :
- aucun changement sur
MT_ACTIVATE_ESTABLISHMENT, le jeton de l'agent est déjà transmis parAxiosAuthInterceptor; - exposer
activatedByetactivationDatedans le type GraphQL. - vivapilot : afficher « Activé le … par … » dans la fiche.
- L'agent appartient à
vivapilot-realm: sonsubn'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
subvia l'API admin du realm.
- stocker un instantané
- 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 : 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
- 🟠 Filtres statut et recherche côté serveur (C13). partnersapi accepte déjà
statusetsearch; le controller ne transmet aujourd'hui queactivated. - 🟠 KPI : nombre d'établissements en attente de validation, nombre avec des documents expirant sous 30 jours (E5).
- 🟢 Checklist KYC par document dans la fiche (conforme / non conforme / commentaire), qui alimente la demande de complément.
- 🟢 Brancher « Envoyer un message » sur communicationapi.
E4. Portail partenaire
- 🟠 Remplacer la maquette
OrganismeMain.jspar une vraie page :QR_GET_ORGANIZATION, statut réel, formulaire d'édition (C12). - 🟠 Afficher au partenaire le statut de son établissement, avec ce qui manque et ce qui a été demandé par le support.
- 🟢 Retirer
status: "ACTIVATED"debuildEstablishmentInput(C14). - 🟢 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]
- 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.
- Utiliser
@Scheduled(cron = "0 0 7 * * *", zone = "Africa/Casablanca"). - Protéger le job par ShedLock (verrou en base) pour qu'une seule réplique Kubernetes l'exécute.
- Alternatives écartées :
- un
CronJobKubernetes 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.
- un
- Requête. Pour chaque type de document, sélectionner les établissements non supprimés, en statut
ACTIVATED,WAITING_FOR_APPROVALouMISSING_INFORMATIONS, dont la date d'expiration est ≤ aujourd'hui + 60 jours. Ajouter des index surexpiry_dateetidentity_expiry_date(migration Flyway). - 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) ); - 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.
- Comme
expiry_datefait partie de la clé, un document renouvelé (nouvelle date) réarme automatiquement les paliers. - Destinataires. Un établissement ne connaît pas ses utilisateurs : il faut un appel à personapi (attachments par
entityId) ou à Keycloak (recherche parapplicationRoles). Recommandation : personapi exposeGET /persons?attachmentIds=...&roles=PSA,PA, ou on réutiliseQR_LIST_PERSON, qui filtre déjà parattachementIds. - Canaux.
- In-app : publier sur
notification.createdavecresource=ESTABLISHMENT,resourceId,establishmentIdet le nouveauNotificationType.DOCUMENT_EXPIRING. SireceiverIdest absent, notificationsapi diffuse déjà à l'établissement. - Email : nouvelle catégorie
DOCUMENT_EXPIRY_REMINDERdans communicationapi, avec un templateAccount/document-expiry-reminder.html. Variables : établissement, document, date, jours restants, lien vers/admin/establishments/{id}/edit. - Support : à J-7 et J0, une notification au groupe ZA, ou la création automatique d'un ticket dans support-api (catégorie « Conformité »).
- Exposition pour vivapilot et le portail. Ajouter à la représentation de l'établissement un champ calculé :
- vivapilot :
- badge sur la liste ;
- KPI « Documents expirant (30 j) » ;
- filtre
complianceStatuscôté serveur ; - bannière dans la fiche.
- Portail : bannière « Votre attestation d'assurance expire le … », avec un bouton « Mettre à jour ».
- Configuration. Les paliers, le fuseau, le délai de grâce et l'activation du job passent par
application.ymlet des variables d'environnement Kubernetes. Prévoir un feature flagzorni.compliance.expiry-alerts.enabled. - Tests.
- Tests unitaires du calcul de palier avec une
Clockinjectée (dates figées). - Test d'intégration du job sur une base Testcontainers.
- 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 | – |