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