feat(members): desactiverMembre cascade complète (Keycloak, Kafka, audit, mono-admin)

Refactor de MembreService.desactiverMembre en 8 étapes transactionnelles :

1. GARDE-FOU mono-admin : refuse 409 Conflict si le membre est le seul
   ORGADMIN d'au moins une org (évite l'orphelinage).
2. DB : actif=false + statutCompte='DESACTIVE'.
3. Adhésions actives → SUSPENDU + décrément nombreMembres.
4. MembreRole (ORGADMIN, TRESORIER...) → actif=false, dateFin=today.
5. Notifications pending (EN_ATTENTE, ECHEC_TEMPORAIRE) → ANNULEE.
6. Keycloak (lions-user-manager) : user.enabled=false → login bloqué.
7. Kafka : publishMemberDeactivated(membre) sur unionflow.members.events
   → consumers peuvent réagir (comptes épargne, inscriptions, approvals, etc.)
8. AuditLog MEMBRE_DESACTIVE : opérateur, timestamp, compteurs (RGPD/compliance).

Côté liste :
- listerMembres/compterMembres : filtre actif=true par défaut (SuperAdmin).
- MembreRepository.findDistinctByOrganisationIdIn : idem pour OrgAdmin.

Services ajoutés :
- AuditService.logMembreDesactive
- KafkaEventProducer.publishMemberDeactivated
This commit is contained in:
dahoud
2026-04-15 20:12:55 +00:00
parent 4816d1ac50
commit aa4350ffbb
4 changed files with 209 additions and 22 deletions

View File

@@ -43,6 +43,9 @@ public class KafkaEventProducer {
@Channel("contributions-events-out")
Emitter<Record<String, String>> contributionsEventsEmitter;
@Channel("chat-messages-out")
Emitter<Record<String, String>> chatMessagesEmitter;
/**
* Publie un event d'approbation en attente.
*/
@@ -116,6 +119,28 @@ public class KafkaEventProducer {
publishToChannel(membersEventsEmitter, memberId.toString(), event, "members-events");
}
/**
* Publie un event de désactivation de membre (soft delete).
* Les consommateurs peuvent réagir : bloquer comptes épargne, annuler inscriptions,
* reassigner approvals pending, nettoyer notifications, etc.
*/
public void publishMemberDeactivated(dev.lions.unionflow.server.entity.Membre membre) {
if (membre == null || membre.getId() == null) return;
Map<String, Object> data = new java.util.HashMap<>();
data.put("membreId", membre.getId().toString());
data.put("email", membre.getEmail());
data.put("nomComplet", membre.getNomComplet());
data.put("numeroMembre", membre.getNumeroMembre());
// organisationId principal (si présent) pour routage par org
String orgId = membre.getMembresOrganisations() != null
&& !membre.getMembresOrganisations().isEmpty()
&& membre.getMembresOrganisations().get(0).getOrganisation() != null
? membre.getMembresOrganisations().get(0).getOrganisation().getId().toString()
: "";
var event = buildEvent("MEMBER_DEACTIVATED", orgId, data);
publishToChannel(membersEventsEmitter, membre.getId().toString(), event, "members-events");
}
/**
* Publie un event de cotisation payée.
*/
@@ -124,6 +149,15 @@ public class KafkaEventProducer {
publishToChannel(contributionsEventsEmitter, contributionId.toString(), event, "contributions-events");
}
/**
* Publie un event de nouveau message de chat.
* Les clients WebSocket de l'organisation sont notifiés pour rafraîchir leurs messages.
*/
public void publishNouveauMessage(UUID conversationId, String organizationId, Map<String, Object> messageData) {
var event = buildEvent("NOUVEAU_MESSAGE", organizationId, messageData);
publishToChannel(chatMessagesEmitter, conversationId.toString(), event, "chat-messages");
}
/**
* Construit un event avec structure standardisée.
*/