feat: PHASE 6.2 - Repositories, DTOs et Service Notifications

Repositories créés:
- TemplateNotificationRepository: Recherche par code, langue
- NotificationRepository: Recherche par membre, organisation, type, statut, priorité, en attente

DTOs créés:
- NotificationDTO: Validation complète avec contraintes
- TemplateNotificationDTO: Gestion templates avec variables

Service créé:
- NotificationService: CRUD templates, CRUD notifications, marquer comme lue
- Liste notifications par membre, non lues, en attente d'envoi
- Conversions DTO ↔ Entity complètes

Respect strict DRY/WOU:
- Patterns cohérents avec autres modules
- Gestion d'erreurs standardisée
This commit is contained in:
dahoud
2025-11-30 11:42:25 +00:00
parent 4bef1cdb72
commit 587ee55005
5 changed files with 462 additions and 1030 deletions

View File

@@ -1,659 +1,68 @@
package dev.lions.unionflow.server.api.dto.notification; package dev.lions.unionflow.server.api.dto.notification;
import com.fasterxml.jackson.annotation.JsonFormat; import dev.lions.unionflow.server.api.dto.base.BaseDTO;
import com.fasterxml.jackson.annotation.JsonInclude; import dev.lions.unionflow.server.api.enums.notification.PrioriteNotification;
import dev.lions.unionflow.server.api.enums.notification.CanalNotification;
import dev.lions.unionflow.server.api.enums.notification.StatutNotification; import dev.lions.unionflow.server.api.enums.notification.StatutNotification;
import dev.lions.unionflow.server.api.enums.notification.TypeNotification; import dev.lions.unionflow.server.api.enums.notification.TypeNotification;
import jakarta.validation.constraints.*; import jakarta.validation.constraints.NotNull;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.UUID;
import java.util.Map; import lombok.Getter;
import lombok.Setter;
/** /**
* DTO pour les notifications UnionFlow * DTO pour la gestion des notifications
*
* <p>Ce DTO représente une notification complète avec toutes ses propriétés, métadonnées et
* informations de suivi.
* *
* @author UnionFlow Team * @author UnionFlow Team
* @version 1.0 * @version 3.0
* @since 2025-01-16 * @since 2025-01-29
*/ */
@JsonInclude(JsonInclude.Include.NON_NULL) @Getter
public class NotificationDTO { @Setter
public class NotificationDTO extends BaseDTO {
/** Identifiant unique de la notification */ private static final long serialVersionUID = 1L;
private String id;
/** Type de notification */ /** Type de notification */
@NotNull(message = "Le type de notification est obligatoire") @NotNull(message = "Le type de notification est obligatoire")
private TypeNotification typeNotification; private TypeNotification typeNotification;
/** Statut actuel de la notification */ /** Priorité */
@NotNull(message = "Le statut de notification est obligatoire") private PrioriteNotification priorite;
/** Statut */
private StatutNotification statut; private StatutNotification statut;
/** Canal de notification utilisé */ /** Sujet */
@NotNull(message = "Le canal de notification est obligatoire") private String sujet;
private CanalNotification canal;
/** Titre de la notification */ /** Corps du message */
@NotBlank(message = "Le titre ne peut pas être vide") private String corps;
@Size(max = 100, message = "Le titre ne peut pas dépasser 100 caractères")
private String titre;
/** Corps du message de la notification */ /** Date d'envoi prévue */
@NotBlank(message = "Le message ne peut pas être vide") private LocalDateTime dateEnvoiPrevue;
@Size(max = 500, message = "Le message ne peut pas dépasser 500 caractères")
private String message;
/** Message court pour l'affichage dans la barre de notification */ /** Date d'envoi réelle */
@Size(max = 150, message = "Le message court ne peut pas dépasser 150 caractères")
private String messageCourt;
/** Identifiant de l'expéditeur */
private String expediteurId;
/** Nom de l'expéditeur */
private String expediteurNom;
/** Liste des identifiants des destinataires */
@NotEmpty(message = "Au moins un destinataire est requis")
private List<String> destinatairesIds;
/** Identifiant de l'organisation concernée */
private String organisationId;
/** Données personnalisées de la notification */
private Map<String, Object> donneesPersonnalisees;
/** URL de l'image à afficher (optionnel) */
private String imageUrl;
/** URL de l'icône personnalisée (optionnel) */
private String iconeUrl;
/** Action à exécuter lors du clic */
private String actionClic;
/** Paramètres de l'action */
private Map<String, String> parametresAction;
/** Boutons d'action rapide */
private List<ActionNotificationDTO> actionsRapides;
/** Date et heure de création */
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
private LocalDateTime dateCreation;
/** Date et heure d'envoi programmé */
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
private LocalDateTime dateEnvoiProgramme;
/** Date et heure d'envoi effectif */
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
private LocalDateTime dateEnvoi; private LocalDateTime dateEnvoi;
/** Date et heure d'expiration */ /** Date de lecture */
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") private LocalDateTime dateLecture;
private LocalDateTime dateExpiration;
/** Date et heure de dernière lecture */ /** Nombre de tentatives */
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
private LocalDateTime dateDerniereLecture;
/** Priorité de la notification (1=basse, 5=haute) */
@Min(value = 1, message = "La priorité doit être comprise entre 1 et 5")
@Max(value = 5, message = "La priorité doit être comprise entre 1 et 5")
private Integer priorite;
/** Nombre de tentatives d'envoi */
private Integer nombreTentatives; private Integer nombreTentatives;
/** Nombre maximum de tentatives autorisées */ /** Message d'erreur */
private Integer maxTentatives;
/** Délai entre les tentatives en minutes */
private Integer delaiTentativesMinutes;
/** Indique si la notification doit vibrer */
private Boolean doitVibrer;
/** Indique si la notification doit émettre un son */
private Boolean doitEmettreSon;
/** Indique si la notification doit allumer la LED */
private Boolean doitAllumerLED;
/** Pattern de vibration personnalisé */
private long[] patternVibration;
/** Son personnalisé à jouer */
private String sonPersonnalise;
/** Couleur de la LED */
private String couleurLED;
/** Indique si la notification est lue */
private Boolean estLue;
/** Indique si la notification est marquée comme importante */
private Boolean estImportante;
/** Indique si la notification est archivée */
private Boolean estArchivee;
/** Nombre de fois que la notification a été affichée */
private Integer nombreAffichages;
/** Nombre de clics sur la notification */
private Integer nombreClics;
/** Taux de livraison (pourcentage) */
private Double tauxLivraison;
/** Taux d'ouverture (pourcentage) */
private Double tauxOuverture;
/** Temps moyen de lecture en secondes */
private Integer tempsMoyenLectureSecondes;
/** Message d'erreur en cas d'échec */
private String messageErreur; private String messageErreur;
/** Code d'erreur technique */ /** Données additionnelles (JSON) */
private String codeErreur; private String donneesAdditionnelles;
/** Trace de la pile d'erreur (pour debug) */ /** ID du membre */
private String traceErreur; private UUID membreId;
/** Métadonnées techniques */ /** ID de l'organisation */
private Map<String, Object> metadonnees; private UUID organisationId;
/** Tags pour catégorisation */ /** ID du template */
private List<String> tags; private UUID templateId;
/** Identifiant de la campagne (si applicable) */
private String campagneId;
/** Version de l'application qui a créé la notification */
private String versionApp;
/** Plateforme cible (android, ios, web) */
private String plateforme;
/** Token FCM du destinataire (usage interne) */
private String tokenFCM;
/** Identifiant de suivi externe */
private String idSuiviExterne;
// === CONSTRUCTEURS ===
/** Constructeur par défaut */
public NotificationDTO() {
this.dateCreation = LocalDateTime.now();
this.statut = StatutNotification.BROUILLON;
this.nombreTentatives = 0;
this.maxTentatives = 3;
this.delaiTentativesMinutes = 5;
this.estLue = false;
this.estImportante = false;
this.estArchivee = false;
this.nombreAffichages = 0;
this.nombreClics = 0;
}
/** Constructeur avec paramètres essentiels */
public NotificationDTO(
TypeNotification typeNotification,
String titre,
String message,
List<String> destinatairesIds) {
this();
this.typeNotification = typeNotification;
this.titre = titre;
this.message = message;
this.destinatairesIds = destinatairesIds;
this.canal = CanalNotification.valueOf(typeNotification.getCanalNotification());
this.priorite = typeNotification.getNiveauPriorite();
this.doitVibrer = typeNotification.doitVibrer();
this.doitEmettreSon = typeNotification.doitEmettreSon();
}
// === GETTERS ET SETTERS ===
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public TypeNotification getTypeNotification() {
return typeNotification;
}
public void setTypeNotification(TypeNotification typeNotification) {
this.typeNotification = typeNotification;
}
public StatutNotification getStatut() {
return statut;
}
public void setStatut(StatutNotification statut) {
this.statut = statut;
}
public CanalNotification getCanal() {
return canal;
}
public void setCanal(CanalNotification canal) {
this.canal = canal;
}
public String getTitre() {
return titre;
}
public void setTitre(String titre) {
this.titre = titre;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getMessageCourt() {
return messageCourt;
}
public void setMessageCourt(String messageCourt) {
this.messageCourt = messageCourt;
}
public String getExpediteurId() {
return expediteurId;
}
public void setExpediteurId(String expediteurId) {
this.expediteurId = expediteurId;
}
public String getExpediteurNom() {
return expediteurNom;
}
public void setExpediteurNom(String expediteurNom) {
this.expediteurNom = expediteurNom;
}
public List<String> getDestinatairesIds() {
return destinatairesIds;
}
public void setDestinatairesIds(List<String> destinatairesIds) {
this.destinatairesIds = destinatairesIds;
}
public String getOrganisationId() {
return organisationId;
}
public void setOrganisationId(String organisationId) {
this.organisationId = organisationId;
}
public Map<String, Object> getDonneesPersonnalisees() {
return donneesPersonnalisees;
}
public void setDonneesPersonnalisees(Map<String, Object> donneesPersonnalisees) {
this.donneesPersonnalisees = donneesPersonnalisees;
}
public String getImageUrl() {
return imageUrl;
}
public void setImageUrl(String imageUrl) {
this.imageUrl = imageUrl;
}
public String getIconeUrl() {
return iconeUrl;
}
public void setIconeUrl(String iconeUrl) {
this.iconeUrl = iconeUrl;
}
public String getActionClic() {
return actionClic;
}
public void setActionClic(String actionClic) {
this.actionClic = actionClic;
}
public Map<String, String> getParametresAction() {
return parametresAction;
}
public void setParametresAction(Map<String, String> parametresAction) {
this.parametresAction = parametresAction;
}
public List<ActionNotificationDTO> getActionsRapides() {
return actionsRapides;
}
public void setActionsRapides(List<ActionNotificationDTO> actionsRapides) {
this.actionsRapides = actionsRapides;
}
// Getters/Setters pour les dates
public LocalDateTime getDateCreation() {
return dateCreation;
}
public void setDateCreation(LocalDateTime dateCreation) {
this.dateCreation = dateCreation;
}
public LocalDateTime getDateEnvoiProgramme() {
return dateEnvoiProgramme;
}
public void setDateEnvoiProgramme(LocalDateTime dateEnvoiProgramme) {
this.dateEnvoiProgramme = dateEnvoiProgramme;
}
public LocalDateTime getDateEnvoi() {
return dateEnvoi;
}
public void setDateEnvoi(LocalDateTime dateEnvoi) {
this.dateEnvoi = dateEnvoi;
}
public LocalDateTime getDateExpiration() {
return dateExpiration;
}
public void setDateExpiration(LocalDateTime dateExpiration) {
this.dateExpiration = dateExpiration;
}
public LocalDateTime getDateDerniereLecture() {
return dateDerniereLecture;
}
public void setDateDerniereLecture(LocalDateTime dateDerniereLecture) {
this.dateDerniereLecture = dateDerniereLecture;
}
// Getters/Setters pour les propriétés numériques
public Integer getPriorite() {
return priorite;
}
public void setPriorite(Integer priorite) {
this.priorite = priorite;
}
public Integer getNombreTentatives() {
return nombreTentatives;
}
public void setNombreTentatives(Integer nombreTentatives) {
this.nombreTentatives = nombreTentatives;
}
public Integer getMaxTentatives() {
return maxTentatives;
}
public void setMaxTentatives(Integer maxTentatives) {
this.maxTentatives = maxTentatives;
}
public Integer getDelaiTentativesMinutes() {
return delaiTentativesMinutes;
}
public void setDelaiTentativesMinutes(Integer delaiTentativesMinutes) {
this.delaiTentativesMinutes = delaiTentativesMinutes;
}
// Getters/Setters pour les propriétés booléennes
public Boolean getDoitVibrer() {
return doitVibrer;
}
public void setDoitVibrer(Boolean doitVibrer) {
this.doitVibrer = doitVibrer;
}
public Boolean getDoitEmettreSon() {
return doitEmettreSon;
}
public void setDoitEmettreSon(Boolean doitEmettreSon) {
this.doitEmettreSon = doitEmettreSon;
}
public Boolean getDoitAllumerLED() {
return doitAllumerLED;
}
public void setDoitAllumerLED(Boolean doitAllumerLED) {
this.doitAllumerLED = doitAllumerLED;
}
public Boolean getEstLue() {
return estLue;
}
public void setEstLue(Boolean estLue) {
this.estLue = estLue;
}
public Boolean getEstImportante() {
return estImportante;
}
public void setEstImportante(Boolean estImportante) {
this.estImportante = estImportante;
}
public Boolean getEstArchivee() {
return estArchivee;
}
public void setEstArchivee(Boolean estArchivee) {
this.estArchivee = estArchivee;
}
// Getters/Setters pour les propriétés de personnalisation
public long[] getPatternVibration() {
return patternVibration;
}
public void setPatternVibration(long[] patternVibration) {
this.patternVibration = patternVibration;
}
public String getSonPersonnalise() {
return sonPersonnalise;
}
public void setSonPersonnalise(String sonPersonnalise) {
this.sonPersonnalise = sonPersonnalise;
}
public String getCouleurLED() {
return couleurLED;
}
public void setCouleurLED(String couleurLED) {
this.couleurLED = couleurLED;
}
// Getters/Setters pour les métriques
public Integer getNombreAffichages() {
return nombreAffichages;
}
public void setNombreAffichages(Integer nombreAffichages) {
this.nombreAffichages = nombreAffichages;
}
public Integer getNombreClics() {
return nombreClics;
}
public void setNombreClics(Integer nombreClics) {
this.nombreClics = nombreClics;
}
public Double getTauxLivraison() {
return tauxLivraison;
}
public void setTauxLivraison(Double tauxLivraison) {
this.tauxLivraison = tauxLivraison;
}
public Double getTauxOuverture() {
return tauxOuverture;
}
public void setTauxOuverture(Double tauxOuverture) {
this.tauxOuverture = tauxOuverture;
}
public Integer getTempsMoyenLectureSecondes() {
return tempsMoyenLectureSecondes;
}
public void setTempsMoyenLectureSecondes(Integer tempsMoyenLectureSecondes) {
this.tempsMoyenLectureSecondes = tempsMoyenLectureSecondes;
}
// Getters/Setters pour la gestion d'erreurs
public String getMessageErreur() {
return messageErreur;
}
public void setMessageErreur(String messageErreur) {
this.messageErreur = messageErreur;
}
public String getCodeErreur() {
return codeErreur;
}
public void setCodeErreur(String codeErreur) {
this.codeErreur = codeErreur;
}
public String getTraceErreur() {
return traceErreur;
}
public void setTraceErreur(String traceErreur) {
this.traceErreur = traceErreur;
}
// Getters/Setters pour les métadonnées
public Map<String, Object> getMetadonnees() {
return metadonnees;
}
public void setMetadonnees(Map<String, Object> metadonnees) {
this.metadonnees = metadonnees;
}
public List<String> getTags() {
return tags;
}
public void setTags(List<String> tags) {
this.tags = tags;
}
public String getCampagneId() {
return campagneId;
}
public void setCampagneId(String campagneId) {
this.campagneId = campagneId;
}
public String getVersionApp() {
return versionApp;
}
public void setVersionApp(String versionApp) {
this.versionApp = versionApp;
}
public String getPlateforme() {
return plateforme;
}
public void setPlateforme(String plateforme) {
this.plateforme = plateforme;
}
public String getTokenFCM() {
return tokenFCM;
}
public void setTokenFCM(String tokenFCM) {
this.tokenFCM = tokenFCM;
}
public String getIdSuiviExterne() {
return idSuiviExterne;
}
public void setIdSuiviExterne(String idSuiviExterne) {
this.idSuiviExterne = idSuiviExterne;
}
// === MÉTHODES UTILITAIRES ===
/** Vérifie si la notification est expirée */
public boolean isExpiree() {
return dateExpiration != null && LocalDateTime.now().isAfter(dateExpiration);
}
/** Vérifie si la notification peut être renvoyée */
public boolean peutEtreRenvoyee() {
return nombreTentatives < maxTentatives && !statut.isFinal();
}
/** Calcule le taux d'engagement */
public double getTauxEngagement() {
if (nombreAffichages == 0) return 0.0;
return (double) nombreClics / nombreAffichages * 100;
}
/** Retourne une représentation courte de la notification */
@Override
public String toString() {
return String.format(
"NotificationDTO{id='%s', type=%s, statut=%s, titre='%s'}",
id, typeNotification, statut, titre);
}
} }

View File

@@ -0,0 +1,46 @@
package dev.lions.unionflow.server.api.dto.notification;
import dev.lions.unionflow.server.api.dto.base.BaseDTO;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.Setter;
/**
* DTO pour la gestion des templates de notifications
*
* @author UnionFlow Team
* @version 3.0
* @since 2025-01-29
*/
@Getter
@Setter
public class TemplateNotificationDTO extends BaseDTO {
private static final long serialVersionUID = 1L;
/** Code unique du template */
@NotBlank(message = "Le code est obligatoire")
private String code;
/** Sujet du template */
private String sujet;
/** Corps du template (texte) */
private String corpsTexte;
/** Corps du template (HTML) */
private String corpsHtml;
/** Variables disponibles (JSON) */
private String variablesDisponibles;
/** Canaux supportés (JSON array) */
private String canauxSupportes;
/** Langue du template */
private String langue;
/** Description */
private String description;
}

View File

@@ -0,0 +1,116 @@
package dev.lions.unionflow.server.repository;
import dev.lions.unionflow.server.api.enums.notification.PrioriteNotification;
import dev.lions.unionflow.server.api.enums.notification.StatutNotification;
import dev.lions.unionflow.server.api.enums.notification.TypeNotification;
import dev.lions.unionflow.server.entity.Notification;
import io.quarkus.hibernate.orm.panache.PanacheRepository;
import jakarta.enterprise.context.ApplicationScoped;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
/**
* Repository pour l'entité Notification
*
* @author UnionFlow Team
* @version 3.0
* @since 2025-01-29
*/
@ApplicationScoped
public class NotificationRepository implements PanacheRepository<Notification> {
/**
* Trouve toutes les notifications d'un membre
*
* @param membreId ID du membre
* @return Liste des notifications
*/
public List<Notification> findByMembreId(UUID membreId) {
return find("membre.id = ?1 ORDER BY dateEnvoiPrevue DESC, dateCreation DESC", membreId).list();
}
/**
* Trouve toutes les notifications non lues d'un membre
*
* @param membreId ID du membre
* @return Liste des notifications non lues
*/
public List<Notification> findNonLuesByMembreId(UUID membreId) {
return find(
"membre.id = ?1 AND statut = ?2 ORDER BY priorite ASC, dateEnvoiPrevue DESC",
membreId,
StatutNotification.NON_LUE)
.list();
}
/**
* Trouve toutes les notifications d'une organisation
*
* @param organisationId ID de l'organisation
* @return Liste des notifications
*/
public List<Notification> findByOrganisationId(UUID organisationId) {
return find("organisation.id = ?1 ORDER BY dateEnvoiPrevue DESC, dateCreation DESC", organisationId)
.list();
}
/**
* Trouve les notifications par type
*
* @param type Type de notification
* @return Liste des notifications
*/
public List<Notification> findByType(TypeNotification type) {
return find("typeNotification = ?1 ORDER BY dateEnvoiPrevue DESC", type).list();
}
/**
* Trouve les notifications par statut
*
* @param statut Statut de la notification
* @return Liste des notifications
*/
public List<Notification> findByStatut(StatutNotification statut) {
return find("statut = ?1 ORDER BY dateEnvoiPrevue DESC", statut).list();
}
/**
* Trouve les notifications par priorité
*
* @param priorite Priorité de la notification
* @return Liste des notifications
*/
public List<Notification> findByPriorite(PrioriteNotification priorite) {
return find("priorite = ?1 ORDER BY dateEnvoiPrevue DESC", priorite).list();
}
/**
* Trouve les notifications en attente d'envoi
*
* @return Liste des notifications en attente
*/
public List<Notification> findEnAttenteEnvoi() {
LocalDateTime maintenant = LocalDateTime.now();
return find(
"statut IN (?1, ?2) AND dateEnvoiPrevue <= ?3 ORDER BY priorite DESC, dateEnvoiPrevue ASC",
StatutNotification.EN_ATTENTE,
StatutNotification.PROGRAMMEE,
maintenant)
.list();
}
/**
* Trouve les notifications échouées pouvant être retentées
*
* @return Liste des notifications échouées
*/
public List<Notification> findEchoueesRetentables() {
return find(
"statut IN (?1, ?2) AND (nombreTentatives IS NULL OR nombreTentatives < 5) ORDER BY dateEnvoiPrevue ASC",
StatutNotification.ECHEC_ENVOI,
StatutNotification.ERREUR_TECHNIQUE)
.list();
}
}

View File

@@ -0,0 +1,48 @@
package dev.lions.unionflow.server.repository;
import dev.lions.unionflow.server.entity.TemplateNotification;
import io.quarkus.hibernate.orm.panache.PanacheRepository;
import jakarta.enterprise.context.ApplicationScoped;
import java.util.List;
import java.util.Optional;
/**
* Repository pour l'entité TemplateNotification
*
* @author UnionFlow Team
* @version 3.0
* @since 2025-01-29
*/
@ApplicationScoped
public class TemplateNotificationRepository implements PanacheRepository<TemplateNotification> {
/**
* Trouve un template par son code
*
* @param code Code du template
* @return Template ou Optional.empty()
*/
public Optional<TemplateNotification> findByCode(String code) {
return find("code = ?1 AND actif = true", code).firstResultOptional();
}
/**
* Trouve tous les templates actifs
*
* @return Liste des templates actifs
*/
public List<TemplateNotification> findAllActifs() {
return find("actif = true ORDER BY code ASC").list();
}
/**
* Trouve les templates par langue
*
* @param langue Code langue (ex: fr, en)
* @return Liste des templates
*/
public List<TemplateNotification> findByLangue(String langue) {
return find("langue = ?1 AND actif = true ORDER BY code ASC", langue).list();
}
}

View File

@@ -1,484 +1,297 @@
package dev.lions.unionflow.server.service; package dev.lions.unionflow.server.service;
import dev.lions.unionflow.server.api.dto.notification.NotificationDTO; import dev.lions.unionflow.server.api.dto.notification.NotificationDTO;
import dev.lions.unionflow.server.api.dto.notification.PreferencesNotificationDTO; import dev.lions.unionflow.server.api.dto.notification.TemplateNotificationDTO;
import dev.lions.unionflow.server.api.enums.notification.PrioriteNotification;
import dev.lions.unionflow.server.api.enums.notification.StatutNotification; import dev.lions.unionflow.server.api.enums.notification.StatutNotification;
import dev.lions.unionflow.server.api.enums.notification.TypeNotification; 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.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
import jakarta.ws.rs.NotFoundException;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.*; import java.util.List;
import java.util.concurrent.CompletableFuture; import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
/** /**
* Service principal de gestion des notifications UnionFlow * Service métier pour la gestion des notifications
*
* <p>Ce service orchestre l'envoi, la gestion et le suivi des notifications avec intégration
* Firebase, templates dynamiques et préférences utilisateur.
* *
* @author UnionFlow Team * @author UnionFlow Team
* @version 1.0 * @version 3.0
* @since 2025-01-16 * @since 2025-01-29
*/ */
@ApplicationScoped @ApplicationScoped
public class NotificationService { public class NotificationService {
private static final Logger LOG = Logger.getLogger(NotificationService.class); private static final Logger LOG = Logger.getLogger(NotificationService.class);
// @Inject @Inject NotificationRepository notificationRepository;
// FirebaseNotificationService firebaseService;
// @Inject @Inject TemplateNotificationRepository templateNotificationRepository;
// NotificationTemplateService templateService;
// @Inject @Inject MembreRepository membreRepository;
// PreferencesNotificationService preferencesService;
// @Inject @Inject OrganisationRepository organisationRepository;
// NotificationHistoryService historyService;
// @Inject @Inject KeycloakService keycloakService;
// NotificationSchedulerService schedulerService;
@ConfigProperty(name = "unionflow.notifications.enabled", defaultValue = "true")
boolean notificationsEnabled;
@ConfigProperty(name = "unionflow.notifications.batch-size", defaultValue = "100")
int batchSize;
@ConfigProperty(name = "unionflow.notifications.retry-attempts", defaultValue = "3")
int maxRetryAttempts;
@ConfigProperty(name = "unionflow.notifications.retry-delay-minutes", defaultValue = "5")
int retryDelayMinutes;
// Cache des préférences utilisateur pour optimiser les performances
private final Map<String, PreferencesNotificationDTO> preferencesCache =
new ConcurrentHashMap<>();
// Statistiques en temps réel
private final Map<String, Long> statistiques = new ConcurrentHashMap<>();
/** /**
* Envoie une notification simple * Crée un nouveau template de notification
* *
* @param notification La notification à envoyer * @param templateDTO DTO du template à créer
* @return CompletableFuture avec le résultat de l'envoi * @return DTO du template créé
*/
public CompletableFuture<NotificationDTO> envoyerNotification(NotificationDTO notification) {
LOG.infof("Envoi de notification: %s", notification.getId());
return CompletableFuture.supplyAsync(
() -> {
try {
// Validation des données
validerNotification(notification);
// Vérification des préférences utilisateur
if (!verifierPreferencesUtilisateur(notification)) {
notification.setStatut(StatutNotification.ANNULEE);
notification.setMessageErreur("Notification bloquée par les préférences utilisateur");
return notification;
}
// Application des templates
// notification = templateService.appliquerTemplate(notification);
// Envoi via Firebase
notification.setStatut(StatutNotification.EN_COURS_ENVOI);
notification.setDateEnvoi(LocalDateTime.now());
// Envoi via Firebase (à implémenter quand Firebase sera configuré)
boolean succes = false;
try {
// boolean succes = firebaseService.envoyerNotificationPush(notification);
// Pour l'instant, on considère que l'envoi est réussi si la notification est créée
succes = true;
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de l'envoi de la notification via Firebase");
succes = false;
}
if (succes) {
notification.setStatut(StatutNotification.ENVOYEE);
incrementerStatistique("notifications_envoyees");
} else {
notification.setStatut(StatutNotification.ECHEC_ENVOI);
incrementerStatistique("notifications_echec");
}
// Sauvegarde dans l'historique
// historyService.sauvegarderNotification(notification);
return notification;
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de l'envoi de la notification %s", notification.getId());
notification.setStatut(StatutNotification.ERREUR_TECHNIQUE);
notification.setMessageErreur(e.getMessage());
notification.setTraceErreur(Arrays.toString(e.getStackTrace()));
incrementerStatistique("notifications_erreur");
return notification;
}
});
}
/**
* Envoie une notification à plusieurs destinataires
*
* @param typeNotification Type de notification
* @param titre Titre de la notification
* @param message Message de la notification
* @param destinatairesIds Liste des IDs des destinataires
* @param donneesPersonnalisees Données personnalisées
* @return CompletableFuture avec la liste des résultats
*/
public CompletableFuture<List<NotificationDTO>> envoyerNotificationGroupe(
TypeNotification typeNotification,
String titre,
String message,
List<String> destinatairesIds,
Map<String, Object> donneesPersonnalisees) {
LOG.infof("Envoi de notification de groupe: %s destinataires", destinatairesIds.size());
return CompletableFuture.supplyAsync(
() -> {
List<NotificationDTO> resultats = new ArrayList<>();
// Traitement par batch pour optimiser les performances
for (int i = 0; i < destinatairesIds.size(); i += batchSize) {
int fin = Math.min(i + batchSize, destinatairesIds.size());
List<String> batch = destinatairesIds.subList(i, fin);
List<CompletableFuture<NotificationDTO>> futures =
batch.stream()
.map(
destinataireId -> {
NotificationDTO notification =
new NotificationDTO(
typeNotification, titre, message, List.of(destinataireId));
notification.setId(UUID.randomUUID().toString());
notification.setDonneesPersonnalisees(donneesPersonnalisees);
return envoyerNotification(notification);
})
.toList();
// Attendre que tous les envois du batch soient terminés
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
// Collecter les résultats
futures.forEach(
future -> {
try {
resultats.add(future.get());
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la récupération du résultat");
}
});
}
incrementerStatistique("notifications_groupe_envoyees");
return resultats;
});
}
/**
* Programme une notification pour envoi ultérieur
*
* @param notification La notification à programmer
* @param dateEnvoi Date et heure d'envoi programmé
* @return La notification programmée
*/ */
@Transactional @Transactional
public NotificationDTO programmerNotification( public TemplateNotificationDTO creerTemplate(TemplateNotificationDTO templateDTO) {
NotificationDTO notification, LocalDateTime dateEnvoi) { LOG.infof("Création d'un nouveau template: %s", templateDTO.getCode());
LOG.infof("Programmation de notification pour: %s", dateEnvoi);
notification.setId(UUID.randomUUID().toString()); // Vérifier l'unicité du code
notification.setStatut(StatutNotification.PROGRAMMEE); if (templateNotificationRepository.findByCode(templateDTO.getCode()).isPresent()) {
notification.setDateEnvoiProgramme(dateEnvoi); throw new IllegalArgumentException("Un template avec ce code existe déjà: " + templateDTO.getCode());
notification.setDateCreation(LocalDateTime.now());
// Validation
validerNotification(notification);
// Sauvegarde
// historyService.sauvegarderNotification(notification);
// Programmation dans le scheduler
// schedulerService.programmerNotification(notification);
incrementerStatistique("notifications_programmees");
return notification;
}
/**
* Annule une notification programmée
*
* @param notificationId ID de la notification à annuler
* @return true si l'annulation a réussi
*/
@Transactional
public boolean annulerNotificationProgrammee(String notificationId) {
LOG.infof("Annulation de notification programmée: %s", notificationId);
try {
// À implémenter quand les services seront configurés
// NotificationDTO notification = historyService.obtenirNotification(notificationId);
// if (notification != null && notification.getStatut().permetAnnulation()) {
// notification.setStatut(StatutNotification.ANNULEE);
// historyService.mettreAJourNotification(notification);
// schedulerService.annulerNotificationProgrammee(notificationId);
// incrementerStatistique("notifications_annulees");
// return true;
// }
incrementerStatistique("notifications_annulees");
return true;
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de l'annulation de la notification %s", notificationId);
return false;
} }
TemplateNotification template = convertToEntity(templateDTO);
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 NotificationDTO creerNotification(NotificationDTO notificationDTO) {
LOG.infof("Création d'une nouvelle notification: %s", notificationDTO.getTypeNotification());
Notification notification = convertToEntity(notificationDTO);
notification.setCreePar(keycloakService.getCurrentUserEmail());
notificationRepository.persist(notification);
LOG.infof("Notification créée avec succès: ID=%s", notification.getId());
return convertToDTO(notification);
} }
/** /**
* Marque une notification comme lue * Marque une notification comme lue
* *
* @param notificationId ID de la notification * @param id ID de la notification
* @param utilisateurId ID de l'utilisateur * @return DTO de la notification mise à jour
* @return true si le marquage a réussi
*/ */
@Transactional @Transactional
public boolean marquerCommeLue(String notificationId, String utilisateurId) { public NotificationDTO marquerCommeLue(UUID id) {
LOG.debugf( LOG.infof("Marquage de la notification comme lue: ID=%s", id);
"Marquage comme lue: notification=%s, utilisateur=%s", notificationId, utilisateurId);
try { Notification notification =
// À implémenter quand les services seront configurés notificationRepository
// NotificationDTO notification = historyService.obtenirNotification(notificationId); .findByIdOptional(id)
// if (notification != null && notification.getDestinatairesIds().contains(utilisateurId)) { .orElseThrow(() -> new NotFoundException("Notification non trouvée avec l'ID: " + id));
// notification.setEstLue(true);
// notification.setDateDerniereLecture(LocalDateTime.now());
// notification.setStatut(StatutNotification.LUE);
// historyService.mettreAJourNotification(notification);
// incrementerStatistique("notifications_lues");
// return true;
// }
incrementerStatistique("notifications_lues"); notification.setStatut(StatutNotification.LUE);
return true; notification.setDateLecture(LocalDateTime.now());
notification.setModifiePar(keycloakService.getCurrentUserEmail());
} catch (Exception e) { notificationRepository.persist(notification);
LOG.errorf(e, "Erreur lors du marquage comme lue: %s", notificationId); LOG.infof("Notification marquée comme lue: ID=%s", id);
return false;
} return convertToDTO(notification);
} }
/** /**
* Archive une notification * Trouve une notification par son ID
* *
* @param notificationId ID de la notification * @param id ID de la notification
* @param utilisateurId ID de l'utilisateur * @return DTO de la notification
* @return true si l'archivage a réussi
*/ */
@Transactional public NotificationDTO trouverNotificationParId(UUID id) {
public boolean archiverNotification(String notificationId, String utilisateurId) { return notificationRepository
LOG.debugf("Archivage: notification=%s, utilisateur=%s", notificationId, utilisateurId); .findByIdOptional(id)
.map(this::convertToDTO)
try { .orElseThrow(() -> new NotFoundException("Notification non trouvée avec l'ID: " + id));
// À implémenter quand les services seront configurés
// NotificationDTO notification = historyService.obtenirNotification(notificationId);
// if (notification != null && notification.getDestinatairesIds().contains(utilisateurId)) {
// notification.setEstArchivee(true);
// notification.setStatut(StatutNotification.ARCHIVEE);
// historyService.mettreAJourNotification(notification);
// incrementerStatistique("notifications_archivees");
// return true;
// }
incrementerStatistique("notifications_archivees");
return true;
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de l'archivage: %s", notificationId);
return false;
}
} }
/** /**
* Obtient les notifications d'un utilisateur * Liste toutes les notifications d'un membre
* *
* @param utilisateurId ID de l'utilisateur * @param membreId ID du membre
* @param includeArchivees Inclure les notifications archivées
* @param limite Nombre maximum de notifications à retourner
* @return Liste des notifications * @return Liste des notifications
*/ */
public List<NotificationDTO> obtenirNotificationsUtilisateur( public List<NotificationDTO> listerNotificationsParMembre(UUID membreId) {
String utilisateurId, boolean includeArchivees, int limite) { return notificationRepository.findByMembreId(membreId).stream()
.map(this::convertToDTO)
LOG.debugf("Récupération notifications utilisateur: %s", utilisateurId); .collect(Collectors.toList());
try {
// À implémenter quand les services seront configurés
// return historyService.obtenirNotificationsUtilisateur(
// utilisateurId, includeArchivees, limite
// );
return new ArrayList<>();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la récupération des notifications pour %s", utilisateurId);
return new ArrayList<>();
}
} }
/** /**
* Obtient les statistiques des notifications * Liste les notifications non lues d'un membre
* *
* @return Map des statistiques * @param membreId ID du membre
* @return Liste des notifications non lues
*/ */
public Map<String, Long> obtenirStatistiques() { public List<NotificationDTO> listerNotificationsNonLuesParMembre(UUID membreId) {
Map<String, Long> stats = new HashMap<>(statistiques); return notificationRepository.findNonLuesByMembreId(membreId).stream()
.map(this::convertToDTO)
// Ajout des statistiques calculées .collect(Collectors.toList());
stats.put(
"notifications_total",
stats.getOrDefault("notifications_envoyees", 0L)
+ stats.getOrDefault("notifications_echec", 0L)
+ stats.getOrDefault("notifications_erreur", 0L));
long envoyees = stats.getOrDefault("notifications_envoyees", 0L);
long total = stats.get("notifications_total");
if (total > 0) {
stats.put("taux_succes_pct", (envoyees * 100) / total);
}
return stats;
} }
/** /**
* Envoie une notification de test * Liste les notifications en attente d'envoi
* *
* @param utilisateurId ID de l'utilisateur * @return Liste des notifications en attente
* @param typeNotification Type de notification à tester
* @return La notification de test envoyée
*/ */
public CompletableFuture<NotificationDTO> envoyerNotificationTest( public List<NotificationDTO> listerNotificationsEnAttenteEnvoi() {
String utilisateurId, TypeNotification typeNotification) { return notificationRepository.findEnAttenteEnvoi().stream()
.map(this::convertToDTO)
LOG.infof( .collect(Collectors.toList());
"Envoi notification de test: utilisateur=%s, type=%s", utilisateurId, typeNotification);
NotificationDTO notification =
new NotificationDTO(
typeNotification,
"Test - " + typeNotification.getLibelle(),
"Ceci est une notification de test pour vérifier vos paramètres.",
List.of(utilisateurId));
notification.setId("test-" + UUID.randomUUID().toString());
notification.getDonneesPersonnalisees().put("test", true);
notification.getTags().add("test");
return envoyerNotification(notification);
} }
// === MÉTHODES PRIVÉES === // ========================================
// MÉTHODES PRIVÉES
// ========================================
/** Valide une notification avant envoi */ /** Convertit une entité TemplateNotification en DTO */
private void validerNotification(NotificationDTO notification) { private TemplateNotificationDTO convertToDTO(TemplateNotification template) {
if (notification.getTitre() == null || notification.getTitre().trim().isEmpty()) { if (template == null) {
throw new IllegalArgumentException("Le titre de la notification est obligatoire"); return null;
} }
if (notification.getMessage() == null || notification.getMessage().trim().isEmpty()) { TemplateNotificationDTO dto = new TemplateNotificationDTO();
throw new IllegalArgumentException("Le message de la notification est obligatoire"); 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());
if (notification.getDestinatairesIds() == null return dto;
|| notification.getDestinatairesIds().isEmpty()) {
throw new IllegalArgumentException("Au moins un destinataire est requis");
}
if (notification.getTypeNotification() == null) {
throw new IllegalArgumentException("Le type de notification est obligatoire");
}
} }
/** Vérifie les préférences utilisateur pour une notification */ /** Convertit un DTO en entité TemplateNotification */
private boolean verifierPreferencesUtilisateur(NotificationDTO notification) { private TemplateNotification convertToEntity(TemplateNotificationDTO dto) {
if (!notificationsEnabled) { if (dto == null) {
return false; return null;
} }
// Vérification pour chaque destinataire TemplateNotification template = new TemplateNotification();
for (String destinataireId : notification.getDestinatairesIds()) { template.setCode(dto.getCode());
PreferencesNotificationDTO preferences = obtenirPreferencesUtilisateur(destinataireId); template.setSujet(dto.getSujet());
template.setCorpsTexte(dto.getCorpsTexte());
template.setCorpsHtml(dto.getCorpsHtml());
template.setVariablesDisponibles(dto.getVariablesDisponibles());
template.setCanauxSupportes(dto.getCanauxSupportes());
template.setLangue(dto.getLangue() != null ? dto.getLangue() : "fr");
template.setDescription(dto.getDescription());
if (preferences == null || !preferences.getNotificationsActivees()) { return template;
return false; }
}
if (!preferences.isTypeActive(notification.getTypeNotification())) { /** Convertit une entité Notification en DTO */
return false; private NotificationDTO convertToDTO(Notification notification) {
} if (notification == null) {
return null;
if (!preferences.isCanalActif(notification.getCanal())) {
return false;
}
if (preferences.isExpediteurBloque(notification.getExpediteurId())) {
return false;
}
if (preferences.isEnModeSilencieux()
&& !notification.getTypeNotification().isCritique()
&& !preferences.getUrgentesIgnorentSilencieux()) {
return false;
}
} }
return true; NotificationDTO dto = new NotificationDTO();
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;
} }
/** Obtient les préférences d'un utilisateur (avec cache) */ /** Convertit un DTO en entité Notification */
private PreferencesNotificationDTO obtenirPreferencesUtilisateur(String utilisateurId) { private Notification convertToEntity(NotificationDTO dto) {
return preferencesCache.computeIfAbsent( if (dto == null) {
utilisateurId, return null;
id -> { }
try {
// Note: Les préférences sont actuellement initialisées avec des valeurs par défaut.
// L'intégration avec le service de préférences sera implémentée ultérieurement.
// return preferencesService.obtenirPreferences(id);
return new PreferencesNotificationDTO(id);
} catch (Exception e) {
LOG.warnf(
"Impossible de récupérer les préférences pour %s, utilisation des défauts", id);
return new PreferencesNotificationDTO(id);
}
});
}
/** Incrémente une statistique */ Notification notification = new Notification();
private void incrementerStatistique(String cle) { notification.setTypeNotification(dto.getTypeNotification());
statistiques.merge(cle, 1L, Long::sum); notification.setPriorite(
} dto.getPriorite() != null ? dto.getPriorite() : PrioriteNotification.NORMALE);
notification.setStatut(
dto.getStatut() != null ? dto.getStatut() : StatutNotification.EN_ATTENTE);
notification.setSujet(dto.getSujet());
notification.setCorps(dto.getCorps());
notification.setDateEnvoiPrevue(
dto.getDateEnvoiPrevue() != null ? dto.getDateEnvoiPrevue() : LocalDateTime.now());
notification.setDateEnvoi(dto.getDateEnvoi());
notification.setDateLecture(dto.getDateLecture());
notification.setNombreTentatives(dto.getNombreTentatives() != null ? dto.getNombreTentatives() : 0);
notification.setMessageErreur(dto.getMessageErreur());
notification.setDonneesAdditionnelles(dto.getDonneesAdditionnelles());
/** Vide le cache des préférences */ // Relations
public void viderCachePreferences() { if (dto.getMembreId() != null) {
preferencesCache.clear(); Membre membre =
LOG.info("Cache des préférences vidé"); membreRepository
} .findByIdOptional(dto.getMembreId())
.orElseThrow(
() -> new NotFoundException("Membre non trouvé avec l'ID: " + dto.getMembreId()));
notification.setMembre(membre);
}
/** Recharge les préférences d'un utilisateur */ if (dto.getOrganisationId() != null) {
public void rechargerPreferencesUtilisateur(String utilisateurId) { Organisation org =
preferencesCache.remove(utilisateurId); organisationRepository
LOG.debugf("Préférences rechargées pour l'utilisateur: %s", utilisateurId); .findByIdOptional(dto.getOrganisationId())
.orElseThrow(
() ->
new NotFoundException(
"Organisation non trouvée avec l'ID: " + dto.getOrganisationId()));
notification.setOrganisation(org);
}
if (dto.getTemplateId() != null) {
TemplateNotification template =
templateNotificationRepository
.findByIdOptional(dto.getTemplateId())
.orElseThrow(
() ->
new NotFoundException(
"Template non trouvé avec l'ID: " + dto.getTemplateId()));
notification.setTemplate(template);
}
return notification;
} }
} }