package dev.lions.unionflow.server.service; import dev.lions.unionflow.server.api.dto.notification.request.CreateNotificationRequest; import dev.lions.unionflow.server.api.enums.membre.StatutMembre; import dev.lions.unionflow.server.entity.Membre; import dev.lions.unionflow.server.entity.MembreOrganisation; import dev.lions.unionflow.server.repository.MembreOrganisationRepository; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import jakarta.transaction.Transactional; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; import java.util.UUID; import org.jboss.logging.Logger; /** * Service de cycle de vie des membres au sein d'une organisation. * *

Gère les transitions de statut automatiques et manuelles : *

* *

Les tâches automatiques sont déclenchées par {@link MemberLifecycleScheduler}. */ @ApplicationScoped public class MemberLifecycleService { private static final Logger LOG = Logger.getLogger(MemberLifecycleService.class); /** Durée de validité par défaut d'une invitation (7 jours). */ public static final int INVITATION_EXPIRY_DAYS = 7; /** Délai de rappel avant expiration (24h avant). */ public static final int INVITATION_REMINDER_HOURS = 24; @Inject MembreOrganisationRepository membreOrgRepository; @Inject NotificationService notificationService; // ========================================================================= // Transitions manuelles // ========================================================================= /** * Active un membre dans une organisation (transition → ACTIF). * * @param membreOrgId UUID du lien membre-organisation * @param adminId UUID de l'administrateur qui valide * @param motif Motif optionnel de l'activation */ @Transactional public MembreOrganisation activerMembre(UUID membreOrgId, UUID adminId, String motif) { MembreOrganisation lien = charger(membreOrgId); verifierTransitionAutorisee(lien.getStatutMembre(), StatutMembre.EN_ATTENTE_VALIDATION, StatutMembre.INVITE, StatutMembre.SUSPENDU); lien.setStatutMembre(StatutMembre.ACTIF); lien.setDateChangementStatut(LocalDate.now()); lien.setMotifStatut(motif != null ? motif : "Validation par l'administrateur"); membreOrgRepository.persist(lien); // Mettre à jour le compteur de membres de l'organisation lien.getOrganisation().ajouterMembre(); LOG.infof("Membre %s activé dans organisation %s par admin %s", lien.getMembre().getId(), lien.getOrganisation().getId(), adminId); // Notifier le membre envoyerNotification(lien.getMembre(), "Adhésion activée", "Votre adhésion à " + lien.getOrganisation().getNom() + " est maintenant active."); return lien; } /** * Suspend un membre dans une organisation (ACTIF → SUSPENDU). */ @Transactional public MembreOrganisation suspendreMembre(UUID membreOrgId, UUID adminId, String motif) { MembreOrganisation lien = charger(membreOrgId); verifierTransitionAutorisee(lien.getStatutMembre(), StatutMembre.ACTIF); lien.setStatutMembre(StatutMembre.SUSPENDU); lien.setDateChangementStatut(LocalDate.now()); lien.setMotifStatut(motif != null ? motif : "Suspension par l'administrateur"); membreOrgRepository.persist(lien); LOG.infof("Membre %s suspendu dans organisation %s", lien.getMembre().getId(), lien.getOrganisation().getId()); envoyerNotification(lien.getMembre(), "Adhésion suspendue", "Votre adhésion à " + lien.getOrganisation().getNom() + " a été suspendue. Motif : " + motif); return lien; } /** * Radie un membre d'une organisation (→ RADIE). */ @Transactional public MembreOrganisation radierMembre(UUID membreOrgId, UUID adminId, String motif) { MembreOrganisation lien = charger(membreOrgId); lien.setStatutMembre(StatutMembre.RADIE); lien.setDateChangementStatut(LocalDate.now()); lien.setMotifStatut(motif != null ? motif : "Radiation par l'administrateur"); membreOrgRepository.persist(lien); // Mettre à jour le compteur de membres de l'organisation lien.getOrganisation().retirerMembre(); LOG.infof("Membre %s radié de l'organisation %s. Motif: %s", lien.getMembre().getId(), lien.getOrganisation().getId(), motif); envoyerNotification(lien.getMembre(), "Adhésion radiée", "Votre adhésion à " + lien.getOrganisation().getNom() + " a été radiée."); return lien; } /** * Archive un membre (→ ARCHIVE) sans supprimer l'historique. */ @Transactional public MembreOrganisation archiverMembre(UUID membreOrgId, String motif) { MembreOrganisation lien = charger(membreOrgId); lien.setStatutMembre(StatutMembre.ARCHIVE); lien.setDateChangementStatut(LocalDate.now()); lien.setMotifArchivage(motif); membreOrgRepository.persist(lien); // Mettre à jour le compteur de membres de l'organisation lien.getOrganisation().retirerMembre(); LOG.infof("Membre %s archivé dans l'organisation %s", lien.getMembre().getId(), lien.getOrganisation().getId()); return lien; } /** * Crée une invitation (statut INVITE) pour un nouveau membre. * * @param membre Le membre à inviter (doit exister en base) * @param organisation L'organisation cible * @param adminId L'administrateur qui invite * @param roleOrg Rôle proposé dans l'organisation (optionnel) * @return le lien MembreOrganisation créé avec le token d'invitation */ @Transactional public MembreOrganisation inviterMembre( dev.lions.unionflow.server.entity.Membre membre, dev.lions.unionflow.server.entity.Organisation organisation, UUID adminId, String roleOrg) { // Vérifier s'il n'existe pas déjà un lien actif membreOrgRepository.findByMembreIdAndOrganisationId(membre.getId(), organisation.getId()) .ifPresent(existing -> { throw new IllegalStateException("Le membre est déjà lié à cette organisation."); }); String token = UUID.randomUUID().toString().replace("-", ""); LocalDateTime now = LocalDateTime.now(); MembreOrganisation lien = MembreOrganisation.builder() .membre(membre) .organisation(organisation) .statutMembre(StatutMembre.INVITE) .dateInvitation(now) .dateExpirationInvitation(now.plusDays(INVITATION_EXPIRY_DAYS)) .tokenInvitation(token) .invitePar(adminId) .roleOrg(roleOrg) .build(); membreOrgRepository.persist(lien); LOG.infof("Invitation créée pour membre %s dans organisation %s (expire: %s)", membre.getId(), organisation.getId(), lien.getDateExpirationInvitation()); // Notifier le membre envoyerNotification(membre, "Invitation à rejoindre " + organisation.getNom(), "Vous avez été invité à rejoindre " + organisation.getNom() + ". Votre invitation expire dans " + INVITATION_EXPIRY_DAYS + " jours."); return lien; } /** * Accepte une invitation via son token (INVITE → EN_ATTENTE_VALIDATION). */ @Transactional public MembreOrganisation accepterInvitation(String token) { MembreOrganisation lien = membreOrgRepository .find("tokenInvitation = ?1 and statutMembre = ?2", token, StatutMembre.INVITE) .firstResultOptional() .orElseThrow(() -> new IllegalArgumentException("Invitation introuvable ou déjà utilisée.")); if (lien.getDateExpirationInvitation() != null && lien.getDateExpirationInvitation().isBefore(LocalDateTime.now())) { throw new IllegalStateException("Cette invitation a expiré."); } lien.setStatutMembre(StatutMembre.EN_ATTENTE_VALIDATION); lien.setDateChangementStatut(LocalDate.now()); lien.setTokenInvitation(null); // Invalider le token après usage membreOrgRepository.persist(lien); LOG.infof("Invitation acceptée par membre %s dans organisation %s", lien.getMembre().getId(), lien.getOrganisation().getId()); return lien; } // ========================================================================= // Traitements automatiques (appelés par MemberLifecycleScheduler) // ========================================================================= /** * Expire les invitations dont la date limite est dépassée. * Les membres INVITE passent au statut RADIE. * * @return nombre d'invitations expirées */ @Transactional public int expirerInvitations() { LocalDateTime now = LocalDateTime.now(); List expiredInvitations = membreOrgRepository.findInvitationsExpirees(now); int count = 0; for (MembreOrganisation lien : expiredInvitations) { lien.setStatutMembre(StatutMembre.RADIE); lien.setDateChangementStatut(LocalDate.now()); lien.setMotifStatut("Invitation expirée sans réponse"); membreOrgRepository.persist(lien); count++; LOG.infof("Invitation expirée : membre %s dans org %s", lien.getMembre().getId(), lien.getOrganisation().getId()); } if (count > 0) { LOG.infof("Expiration des invitations : %d invitation(s) expirée(s)", count); } return count; } /** * Envoie des rappels pour les invitations qui expirent dans les prochaines 24h. * * @return nombre de rappels envoyés */ @Transactional public int envoyerRappelsInvitation() { LocalDateTime now = LocalDateTime.now(); LocalDateTime dans24h = now.plusHours(INVITATION_REMINDER_HOURS); List aRappeler = membreOrgRepository.findInvitationsExpirantBientot(now, dans24h); int count = 0; for (MembreOrganisation lien : aRappeler) { envoyerNotification(lien.getMembre(), "Rappel : votre invitation expire bientôt", "Votre invitation à rejoindre " + lien.getOrganisation().getNom() + " expire dans moins de 24h. Acceptez-la maintenant."); count++; } if (count > 0) { LOG.infof("Rappels d'invitation : %d rappel(s) envoyé(s)", count); } return count; } // ========================================================================= // Helpers privés // ========================================================================= private MembreOrganisation charger(UUID membreOrgId) { return membreOrgRepository.findByIdOptional(membreOrgId) .orElseThrow(() -> new IllegalArgumentException( "Lien membre-organisation introuvable : " + membreOrgId)); } private void verifierTransitionAutorisee(StatutMembre current, StatutMembre... autorises) { for (StatutMembre autorise : autorises) { if (autorise.equals(current)) return; } throw new IllegalStateException( "Transition non autorisée depuis le statut " + current); } private void envoyerNotification(Membre membre, String titre, String corps) { try { if (notificationService != null) { CreateNotificationRequest req = CreateNotificationRequest.builder() .typeNotification("SYSTEME") .priorite("NORMALE") .sujet(titre) .corps(corps) .membreId(membre.getId()) .build(); notificationService.creerNotification(req); } } catch (Exception e) { LOG.warnf("Impossible d'envoyer la notification à %s : %s", membre.getId(), e.getMessage()); } } }