Le calcul précédent `getSystemLoadAverage() / getAvailableProcessors() * 100`
utilisait :
- getSystemLoadAverage() : charge 1min du NODE Linux hôte entier (12 CPU sur prod VPS)
- getAvailableProcessors() : limite conteneur K8s (1 CPU pour le pod UnionFlow)
Résultat : load 1.5 sur le node → cpuUsage = (1.5 / 1) * 100 = 150% capé à 100%.
Déclenchement constant d'alertes "CPU 100%" (19 faux positifs / 24h sur prod
le 20-21/04/2026) dès que le node fait autre chose que dormir.
Correctif : utilise com.sun.management.OperatingSystemMXBean.getProcessCpuLoad()
qui retourne la charge CPU du process JVM courant (0.0-1.0), conscient du
conteneur. Branche -1 préservée (API non dispo sur certaines JVM).
Tests mis à jour : AlertMonitoringServiceMockStaticCoverageTest injecte
désormais un com.sun.management.OperatingSystemMXBean et mocke
getProcessCpuLoad() (compatible mock-maker-inline déjà configuré).
AlertMonitoringServiceTest conserve sa logique OS-agnostique via des
thresholds extrêmes (-1 toujours / 100 jamais).
- PUT /{id}: check appartenance ADMIN_ORGANISATION (403 si pas membre)
- GET /statistiques: stats scoped à l'org active pour ADMIN_ORGANISATION
- OrganisationService.obtenirStatistiquesParOrganisation(): stats mono-org
- MembreOrganisationRepository.findByMembreEmailAndOrganisationId()
- application-dev: IP 192.168.1.145→localhost pour Keycloak
- OrganisationService.convertToResponse: ne plus setter typeAssociation ni typeLibelle
- Utiliser uniquement typeOrganisation et typeOrganisationLibelle
- Corriger tests: assertions sur typeOrganisationLibelle au lieu de typeLibelle
- ADMIN_ENTITE→ADMIN_ORGANISATION dans Javadoc et README
- OrganisationModuleService: retirer TONTINE de MUTUELLE (BCEAO-réglementé, incompatible)
- Ajouter TONTINE aux ASSOCIATION et CLUB_SERVICE (pratique courante Afrique de l'Ouest)
- V18 migration: aligner modules_requis avec le code Java
La V18 a été modifiée (fusion MUTUELLE_EPARGNE+CREDIT → MUTUELLE).
En prod, le checksum en DB ne correspond plus → Flyway refuse de démarrer.
repair-at-start met à jour la schema_history automatiquement.
Problème : après paiement Wave, le membre restait SIMPLEMEMBER car :
1. La méthode bouclait sur TOUS les liens de l'org et promouvait le
premier non-ACTIF (pas forcément le payeur)
2. Si aucun lien n'existait, créait un lien avec SIMPLEMEMBER sans promotion
3. Early return après la première promotion → si le mauvais membre était
promu, le payeur restait bloqué
Nouvelle logique :
1. Identifier le caller (JWT email) = le membre qui a réellement payé
2. Vérifier/créer son lien MembreOrganisation (activer si en attente)
3. Promouvoir ce membre spécifique en ORGADMIN (DB)
4. Syncer Keycloak ADMIN_ORGANISATION (non-bloquant)
Résultat : après paiement, le payeur est automatiquement promu ORGADMIN
avec statut ACTIF et accède directement à son dashboard.
TypeReferenceService.creer() :
- Si code non fourni ou vide : auto-généré depuis le libellé via genererCodeDepuisLibelle()
- Si code fourni : nettoyé via normaliserCode() (strip accents, UPPER_SNAKE_CASE)
- Dédoublonnage automatique : si le code existe, suffixe _2, _3, etc.
Méthodes ajoutées :
- genererCodeDepuisLibelle(String) : libellé → UPPER_SNAKE_CASE sans accents
Ex: 'Mutuelle d''Épargne' → 'MUTUELLE_D_EPARGNE'
- normaliserCode(String) : Normalizer.NFD + strip diacritics + [^A-Za-z0-9] → _
- assurerUniciteCode(String, String, UUID) : suffixe incrémental si doublon
Les types système (est_systeme=true, seeded en V18) gardent leurs codes figés.
Seuls les types créés par l'utilisateur bénéficient de l'auto-génération.
Une mutuelle (MEC/COOPEC) fait TOUJOURS épargne ET crédit conjointement
dans le cadre réglementaire BCEAO/UEMOA. La séparation en deux types
n'avait pas de réalité terrain.
V18 corrigée :
- MUTUELLE_EPARGNE + MUTUELLE_CREDIT supprimés
- MUTUELLE ajouté : modules EPARGNE,CREDIT,FINANCE,LCB_FT (complet)
- COOPERATIVE enrichi : ajout EPARGNE + VOTES (réalité terrain — les coopératives
ont des AG avec votes et proposent souvent de l'épargne à leurs membres)
Passe de 17 → 16 types d'organisation.
Le mapping mobile _mapTypeOrganisationBilling garde les anciens codes en fallback
pour rétrocompatibilité.
- pom.xml : mise à jour dépendances
- application.properties : ajustements config
- MembreServiceTest, EntityCoverageTest : tests mis à jour pour nouveautés
- .gitignore : ajout du.exe.stackdump (dump Windows bash)
- WebhookWave : entité pour logs webhooks Wave (idempotence + audit)
- WaveRedirectResource : endpoint de retour après paiement Wave
(redirige vers l'app mobile avec le statut)
- KeycloakAdminHttpClient (nouveau) : client HTTP natif (java.net.http.HttpClient)
pour contourner les problèmes de désérialisation avec RESTEasy sur certains
endpoints Keycloak 26+ (bruteForceStrategy, cpuInfo inconnus).
Utilise ObjectMapper avec FAIL_ON_UNKNOWN_PROPERTIES=false.
- AdminUserService : utilisation correcte de AdminUserServiceClient + AdminRoleServiceClient
avec AdminServiceTokenHeadersFactory pour l'auth.
- ModuleAccessFilter : améliorations de la logique @RequiresModule.
Migrations :
- V25 : numero_transaction nullable dans paiements (legacy V1 NOT NULL bloquant INSERT)
- V26 : autres colonnes legacy NOT NULL V1 (type_paiement, statut_paiement, etc.)
rendues nullables pour alignement avec l'entité Paiement
Refactor Paiement/PaiementObjet : mise à jour entités, repository, resource, service
pour cohérence avec le nouveau module Versement. Tests associés supprimés/ajustés.
verifierOwnershipEtProtectionAdmin() appelé sur les 5 endpoints lifecycle
(radier-adhesion, archiver-adhesion, activer/suspendre/radier par membre):
1. Ownership: un ADMIN_ORGANISATION ne peut agir que sur les membres des
organisations dont il est responsable (sinon 403).
2. Anti-admin: un ADMIN_ORGANISATION ne peut pas agir sur un autre ORGADMIN
ou SUPERADMIN (sinon 403).
3. SUPER_ADMIN/ADMIN passent directement (accès total).
Comble les failles SEC-01/SEC-02 de l'audit technique.
- Nouveau MembreRoleSyncService.ensureOrgAdminRole : auto-crée un MembreRole ORGADMIN
quand un user avec rôle Keycloak ADMIN_ORGANISATION se connecte sans entrée DB
(couvre les comptes créés directement dans Keycloak).
- OrganisationContextFilter appelle syncService.ensureOrgAdminRole quand le rôle
Keycloak est présent mais MembreRole absent (non bloquant sur erreur).
- MembreRoleRepository.countAdminsByOrganisationId : count strict (ORGADMIN + actif
+ dateDebut/dateFin valides) avec fallback sur codes alternatifs si strict=0.
- OrganisationService.convertToResponse : nombreAdministrateurs dynamique via
MembreRoleRepository (remplace le champ Organisation jamais mis à jour).
- V30: ajoute membre_organisation_id/organisation_id/date_debut/fin/commentaire si absents,
rend membre_id nullable (legacy V1), remplace uk_membre_role par uk_mr_membre_org_role,
ajoute indexes. Idempotent via DO blocks.
- V31: rend destinataire_id, titre, nombre_tentatives nullables dans notifications
(colonnes legacy V1 que l'entité n'utilise plus, bloquaient les INSERT).
@Provider enregistre le filtre GLOBALEMENT sur tous les REST clients.
JwtPropagationFilter s'exécutait après AdminServiceTokenHeadersFactory et
écrasait le token de service account avec le JWT utilisateur mobile
→ LUM recevait un token du realm unionflow (kid inconnu) → 401.
La propagation JWT est déjà gérée par OidcTokenPropagationHeadersFactory
sur UserServiceClient/RoleServiceClient via @RegisterClientHeaders.
JwtPropagationFilter est conservé sans @Provider pour référence future.
Le service admin injectait UserServiceClient/RoleServiceClient (propagation du token
utilisateur unionflow) au lieu des clients Admin dédiés (service account lions-user-manager).
Résultat : le token JWT de l'utilisateur mobile était envoyé à LUM → 401 car LUM ne
connaît pas les clés du realm unionflow.
Correctif :
- AdminUserService -> AdminUserServiceClient + AdminRoleServiceClient (service account)
- UserServiceClient + RoleServiceClient remis à OidcTokenPropagationHeadersFactory
(ces clients non-admin propagent le token utilisateur pour des usages futurs)
Les appels vers lions-user-manager nécessitent un token du realm lions-user-manager
(service account). OidcTokenPropagationHeadersFactory transmettait le token utilisateur
du realm unionflow → 401 systématique. AdminServiceTokenHeadersFactory injecte le bon
token via l'OIDC client admin-service.
- application-dev.properties : token.issuer utilise ${DEV_HOST:localhost} pour valider
les JWT émis par Keycloak via l'IP LAN (mobile physique sur réseau local)
- .env : DEV_HOST=192.168.1.13 — source unique côté backend, en sync avec
android/local.properties → dev.host côté mobile
- V24 : suppression des 8 tables fantômes issues des migrations pré-consolidation
(document, permission, favori, ticket, suggestion, suggestion_vote, configuration,
role_permission) — toutes vides, les entités JPA pointaient déjà vers les tables
plurielles correctes. Les contraintes uk_role_permission et uk_suggestion_vote
sont maintenant sur les vraies tables (roles_permissions, suggestion_votes).
ConversationResponse/MessageResponse fields (muted, pinned, archived,
edited, deleted) are primitive booleans — Lombok @Builder generates
.muted() not .isMuted(). Also use Boolean.TRUE.equals() for null-safe
unboxing from entity Boolean wrapper fields.
MembreDashboardServiceTest and OrganisationServiceTest used record
accessor syntax (e.g. result.nom()) on DTO classes that were converted
from records to @Data classes — now using getters (result.getNom()).
BUG-01: BudgetService.toResponse() — remplace doubleValue()>0 par
compareTo(BigDecimal.ZERO)>0 (précision BigDecimal) ; ajoute 2 tests
couvrant varianceRate=0 (totalPlanned=0) et varianceRate=-40%
AUTH: MembreKeycloakSyncService.changerMotDePassePremierLogin() — élargit
le catch de ForbiddenException vers WebApplicationException avec vérification
du statut HTTP (le REST client MicroProfile ne garantit pas la sous-classe)
DATA-01: MembreService.desactiverMembre() — décrémente nombreMembres sur
toutes les orgs actives du membre et passe le statutMembre à DESACTIVE
La colonne version était VARCHAR(20) au lieu de BIGINT, causant une erreur
Hibernate validate au démarrage. L'entité ModuleDisponible n'a pas de champ
version propre — la colonne était orpheline et conflicte avec BaseEntity @Version.
Fix idempotent : suppression VARCHAR + ajout BIGINT DEFAULT 0 (table vide).
- TypeReference: ajout des champs categorie et modulesRequis (colonnes DB existantes depuis V18
mais non mappées en JPA — Hibernate validate échouait silencieusement)
- OrganisationService.creerOrganisation(): lit types_reference.modules_requis pour initialiser
Organisation.modulesActifs, au lieu de dépendre uniquement du switch hardcodé dans
OrganisationModuleService.getModulesParType()
Avant: un type créé via CRUD (ex: TANTANPION) tombait dans le default du switch → aucun
module métier → rôles métier assignables mais menus jamais affichés.
Après: tout type avec modules_requis renseigné dans types_reference active correctement
ses modules à la création de l'organisation.