package dev.lions.unionflow.server.service; import dev.lions.unionflow.server.api.dto.notification.request.CreateNotificationRequest; import dev.lions.unionflow.server.api.dto.notification.request.CreateTemplateNotificationRequest; import dev.lions.unionflow.server.api.dto.notification.response.NotificationResponse; import dev.lions.unionflow.server.api.dto.notification.response.TemplateNotificationResponse; import dev.lions.unionflow.server.entity.*; import dev.lions.unionflow.server.repository.*; import dev.lions.unionflow.server.service.KeycloakService; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import jakarta.transaction.Transactional; import jakarta.ws.rs.NotFoundException; import java.time.LocalDateTime; import java.util.List; import io.quarkus.mailer.Mail; import io.quarkus.mailer.Mailer; import java.util.UUID; import java.util.stream.Collectors; import org.jboss.logging.Logger; /** * Service métier pour la gestion des notifications * * @author UnionFlow Team * @version 3.0 * @since 2025-01-29 */ @ApplicationScoped public class NotificationService { private static final Logger LOG = Logger.getLogger(NotificationService.class); @Inject NotificationRepository notificationRepository; @Inject TemplateNotificationRepository templateNotificationRepository; @Inject MembreRepository membreRepository; @Inject OrganisationRepository organisationRepository; @Inject Mailer mailer; @Inject KeycloakService keycloakService; @Inject FirebasePushService firebasePushService; /** * Crée un nouveau template de notification * * @param templateDTO DTO du template à créer * @return DTO du template créé */ @Transactional public TemplateNotificationResponse creerTemplate(CreateTemplateNotificationRequest request) { LOG.infof("Création d'un nouveau template: %s", request.code()); // Vérifier l'unicité du code if (templateNotificationRepository.findByCode(request.code()).isPresent()) { throw new IllegalArgumentException("Un template avec ce code existe déjà: " + request.code()); } TemplateNotification template = convertToEntity(request); template.setCreePar(keycloakService.getCurrentUserEmail()); templateNotificationRepository.persist(template); LOG.infof("Template créé avec succès: ID=%s, Code=%s", template.getId(), template.getCode()); return convertToDTO(template); } /** * Crée une nouvelle notification * * @param notificationDTO DTO de la notification à créer * @return DTO de la notification créée */ @Transactional public NotificationResponse creerNotification(CreateNotificationRequest request) { LOG.infof("Création d'une nouvelle notification: %s", request.typeNotification()); Notification notification = convertToEntity(request); notification.setCreePar(keycloakService.getCurrentUserEmail()); notificationRepository.persist(notification); LOG.infof("Notification créée avec succès: ID=%s", notification.getId()); // Envoi immédiat selon le canal if ("EMAIL".equals(notification.getTypeNotification())) { try { envoyerEmail(notification); } catch (Exception e) { LOG.errorf("Erreur lors de l'envoi de l'email pour la notification %s: %s", notification.getId(), e.getMessage()); } } else if ("PUSH".equals(notification.getTypeNotification())) { try { envoyerPush(notification); } catch (Exception e) { LOG.warnf("Erreur push notification %s (non bloquant): %s", notification.getId(), e.getMessage()); } } return convertToDTO(notification); } /** * Marque une notification comme lue * * @param id ID de la notification * @return DTO de la notification mise à jour */ @Transactional public NotificationResponse marquerCommeLue(UUID id) { LOG.infof("Marquage de la notification comme lue: ID=%s", id); Notification notification = notificationRepository .findNotificationById(id) .orElseThrow(() -> new NotFoundException("Notification non trouvée avec l'ID: " + id)); notification.setStatut("LUE"); notification.setDateLecture(LocalDateTime.now()); notification.setModifiePar(keycloakService.getCurrentUserEmail()); notificationRepository.persist(notification); LOG.infof("Notification marquée comme lue: ID=%s", id); return convertToDTO(notification); } /** * Trouve une notification par son ID * * @param id ID de la notification * @return DTO de la notification */ public NotificationResponse trouverNotificationParId(UUID id) { return notificationRepository .findNotificationById(id) .map(this::convertToDTO) .orElseThrow(() -> new NotFoundException("Notification non trouvée avec l'ID: " + id)); } /** * Liste toutes les notifications d'un membre * * @param membreId ID du membre * @return Liste des notifications */ public List listerNotificationsParMembre(UUID membreId) { return notificationRepository.findByMembreId(membreId).stream() .map(this::convertToDTO) .collect(Collectors.toList()); } /** * Liste les notifications non lues d'un membre * * @param membreId ID du membre * @return Liste des notifications non lues */ public List listerNotificationsNonLuesParMembre(UUID membreId) { return notificationRepository.findNonLuesByMembreId(membreId).stream() .map(this::convertToDTO) .collect(Collectors.toList()); } /** * Liste les notifications en attente d'envoi * * @return Liste des notifications en attente */ public List listerNotificationsEnAttenteEnvoi() { return notificationRepository.findEnAttenteEnvoi().stream() .map(this::convertToDTO) .collect(Collectors.toList()); } /** * Envoie des notifications groupées à plusieurs membres (WOU/DRY) * * @param membreIds Liste des IDs des membres destinataires * @param sujet Sujet de la notification * @param corps Corps du message * @param canaux Canaux d'envoi (EMAIL, SMS, etc.) * @return Nombre de notifications créées */ @Transactional public int envoyerNotificationsGroupees( List membreIds, String sujet, String corps, List canaux) { if (membreIds == null || membreIds.isEmpty()) { throw new IllegalArgumentException("La liste des membres ne peut pas être vide"); } LOG.infof( "Envoi de notifications groupées à %d membres - sujet: %s", membreIds.size(), sujet); int notificationsCreees = 0; for (UUID membreId : membreIds) { try { Membre membre = membreRepository .findByIdOptional(membreId) .orElseThrow( () -> new IllegalArgumentException( "Membre non trouvé avec l'ID: " + membreId)); // Parcourir les canaux demandés if (canaux == null || canaux.isEmpty()) { canaux = List.of("IN_APP"); } for (String canal : canaux) { try { String type = canal; Notification notification = new Notification(); notification.setMembre(membre); notification.setSujet(sujet); notification.setCorps(corps); notification.setTypeNotification(type); // Utiliser le canal demandé notification.setPriorite("NORMALE"); notification.setStatut("EN_ATTENTE"); notification.setDateEnvoiPrevue(java.time.LocalDateTime.now()); notification.setCreePar(keycloakService.getCurrentUserEmail()); notificationRepository.persist(notification); notificationsCreees++; // Envoi immédiat si EMAIL if ("EMAIL".equals(type)) { envoyerEmail(notification); } } catch (IllegalArgumentException e) { LOG.warnf("Type de notification inconnu: %s", canal); } } } catch (Exception e) { LOG.warnf( "Erreur lors de la création de la notification pour le membre %s: %s", membreId, e.getMessage()); } } LOG.infof( "%d notifications créées sur %d membres demandés", notificationsCreees, membreIds.size()); return notificationsCreees; } // ======================================== // MÉTHODES PRIVÉES // ======================================== /** Convertit une entité TemplateNotification en DTO */ private TemplateNotificationResponse convertToDTO(TemplateNotification template) { if (template == null) { return null; } TemplateNotificationResponse dto = new TemplateNotificationResponse(); dto.setId(template.getId()); dto.setCode(template.getCode()); dto.setSujet(template.getSujet()); dto.setCorpsTexte(template.getCorpsTexte()); dto.setCorpsHtml(template.getCorpsHtml()); dto.setVariablesDisponibles(template.getVariablesDisponibles()); dto.setCanauxSupportes(template.getCanauxSupportes()); dto.setLangue(template.getLangue()); dto.setDescription(template.getDescription()); dto.setDateCreation(template.getDateCreation()); dto.setDateModification(template.getDateModification()); dto.setActif(template.getActif()); return dto; } /** Convertit un DTO en entité TemplateNotification */ private TemplateNotification convertToEntity(CreateTemplateNotificationRequest dto) { if (dto == null) { return null; } TemplateNotification template = new TemplateNotification(); template.setCode(dto.code()); template.setSujet(dto.sujet()); template.setCorpsTexte(dto.corpsTexte()); template.setCorpsHtml(dto.corpsHtml()); template.setVariablesDisponibles(dto.variablesDisponibles()); template.setCanauxSupportes(dto.canauxSupportes()); template.setLangue(dto.langue() != null ? dto.langue() : "fr"); template.setDescription(dto.description()); return template; } /** Convertit une entité Notification en DTO */ private NotificationResponse convertToDTO(Notification notification) { if (notification == null) { return null; } NotificationResponse dto = new NotificationResponse(); dto.setId(notification.getId()); dto.setTypeNotification(notification.getTypeNotification()); dto.setPriorite(notification.getPriorite()); dto.setStatut(notification.getStatut()); dto.setSujet(notification.getSujet()); dto.setCorps(notification.getCorps()); dto.setDateEnvoiPrevue(notification.getDateEnvoiPrevue()); dto.setDateEnvoi(notification.getDateEnvoi()); dto.setDateLecture(notification.getDateLecture()); dto.setNombreTentatives(notification.getNombreTentatives()); dto.setMessageErreur(notification.getMessageErreur()); dto.setDonneesAdditionnelles(notification.getDonneesAdditionnelles()); if (notification.getMembre() != null) { dto.setMembreId(notification.getMembre().getId()); } if (notification.getOrganisation() != null) { dto.setOrganisationId(notification.getOrganisation().getId()); } if (notification.getTemplate() != null) { dto.setTemplateId(notification.getTemplate().getId()); } dto.setDateCreation(notification.getDateCreation()); dto.setDateModification(notification.getDateModification()); dto.setActif(notification.getActif()); return dto; } /** Convertit un DTO en entité Notification */ private Notification convertToEntity(CreateNotificationRequest dto) { if (dto == null) { return null; } Notification notification = new Notification(); notification.setTypeNotification(dto.typeNotification()); notification.setPriorite( dto.priorite() != null ? dto.priorite() : "NORMALE"); notification.setStatut("EN_ATTENTE"); notification.setSujet(dto.sujet()); notification.setCorps(dto.corps()); notification.setDateEnvoiPrevue( dto.dateEnvoiPrevue() != null ? dto.dateEnvoiPrevue() : LocalDateTime.now()); notification.setDateLecture(null); notification.setNombreTentatives(0); notification.setMessageErreur(null); notification.setDonneesAdditionnelles(dto.donneesAdditionnelles()); // Relations if (dto.membreId() != null) { Membre membre = membreRepository .findByIdOptional(dto.membreId()) .orElseThrow( () -> new NotFoundException("Membre non trouvé avec l'ID: " + dto.membreId())); notification.setMembre(membre); } if (dto.organisationId() != null) { Organisation org = organisationRepository .findByIdOptional(dto.organisationId()) .orElseThrow( () -> new NotFoundException( "Organisation non trouvée avec l'ID: " + dto.organisationId())); notification.setOrganisation(org); } if (dto.templateId() != null) { TemplateNotification template = templateNotificationRepository .findTemplateNotificationById(dto.templateId()) .orElseThrow( () -> new NotFoundException( "Template non trouvé avec l'ID: " + dto.templateId())); notification.setTemplate(template); } return notification; } /** * Envoie une notification push FCM pour une notification. */ private void envoyerPush(Notification notification) { if (notification.getMembre() == null) { LOG.warnf("Impossible d'envoyer le push pour la notification %s : pas de membre", notification.getId()); notification.setStatut("ECHEC_ENVOI"); notification.setMessageErreur("Pas de membre défini"); return; } String fcmToken = notification.getMembre().getFcmToken(); if (fcmToken == null || fcmToken.isBlank()) { LOG.debugf("Membre %s sans token FCM — push ignoré", notification.getMembre().getId()); notification.setStatut("IGNOREE"); notification.setMessageErreur("Pas de token FCM"); return; } boolean ok = firebasePushService.envoyerNotification( fcmToken, notification.getSujet(), notification.getCorps(), java.util.Map.of("notificationId", notification.getId().toString())); if (ok) { notification.setStatut("ENVOYEE"); notification.setDateEnvoi(java.time.LocalDateTime.now()); } else { notification.setStatut("ECHEC_ENVOI"); notification.setMessageErreur("FCM: envoi échoué"); } notificationRepository.persist(notification); } /** * Envoie un email pour une notification */ private void envoyerEmail(Notification notification) { if (notification.getMembre() == null || notification.getMembre().getEmail() == null) { LOG.warnf("Impossible d'envoyer l'email pour la notification %s : pas d'email", notification.getId()); notification.setStatut("ECHEC_ENVOI"); notification.setMessageErreur("Pas d'email défini pour le membre"); return; } try { LOG.infof("Envoi de l'email à %s", notification.getMembre().getEmail()); String corps = notification.getCorps(); boolean isHtml = corps != null && (corps.startsWith("