diff --git a/unionflow-server-api/src/main/java/dev/lions/unionflow/server/api/dto/notification/NotificationDTO.java b/unionflow-server-api/src/main/java/dev/lions/unionflow/server/api/dto/notification/NotificationDTO.java index 51cd181..b9b3357 100644 --- a/unionflow-server-api/src/main/java/dev/lions/unionflow/server/api/dto/notification/NotificationDTO.java +++ b/unionflow-server-api/src/main/java/dev/lions/unionflow/server/api/dto/notification/NotificationDTO.java @@ -1,659 +1,68 @@ package dev.lions.unionflow.server.api.dto.notification; -import com.fasterxml.jackson.annotation.JsonFormat; -import com.fasterxml.jackson.annotation.JsonInclude; -import dev.lions.unionflow.server.api.enums.notification.CanalNotification; +import dev.lions.unionflow.server.api.dto.base.BaseDTO; +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 jakarta.validation.constraints.*; +import jakarta.validation.constraints.NotNull; import java.time.LocalDateTime; -import java.util.List; -import java.util.Map; +import java.util.UUID; +import lombok.Getter; +import lombok.Setter; /** - * DTO pour les notifications UnionFlow - * - *

Ce DTO représente une notification complète avec toutes ses propriétés, métadonnées et - * informations de suivi. + * DTO pour la gestion des notifications * * @author UnionFlow Team - * @version 1.0 - * @since 2025-01-16 + * @version 3.0 + * @since 2025-01-29 */ -@JsonInclude(JsonInclude.Include.NON_NULL) -public class NotificationDTO { +@Getter +@Setter +public class NotificationDTO extends BaseDTO { - /** Identifiant unique de la notification */ - private String id; + private static final long serialVersionUID = 1L; /** Type de notification */ @NotNull(message = "Le type de notification est obligatoire") private TypeNotification typeNotification; - /** Statut actuel de la notification */ - @NotNull(message = "Le statut de notification est obligatoire") + /** Priorité */ + private PrioriteNotification priorite; + + /** Statut */ private StatutNotification statut; - /** Canal de notification utilisé */ - @NotNull(message = "Le canal de notification est obligatoire") - private CanalNotification canal; + /** Sujet */ + private String sujet; - /** Titre de la notification */ - @NotBlank(message = "Le titre ne peut pas être vide") - @Size(max = 100, message = "Le titre ne peut pas dépasser 100 caractères") - private String titre; + /** Corps du message */ + private String corps; - /** Corps du message de la notification */ - @NotBlank(message = "Le message ne peut pas être vide") - @Size(max = 500, message = "Le message ne peut pas dépasser 500 caractères") - private String message; + /** Date d'envoi prévue */ + private LocalDateTime dateEnvoiPrevue; - /** Message court pour l'affichage dans la barre de notification */ - @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 destinatairesIds; - - /** Identifiant de l'organisation concernée */ - private String organisationId; - - /** Données personnalisées de la notification */ - private Map 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 parametresAction; - - /** Boutons d'action rapide */ - private List 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") + /** Date d'envoi réelle */ private LocalDateTime dateEnvoi; - /** Date et heure d'expiration */ - @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") - private LocalDateTime dateExpiration; + /** Date de lecture */ + private LocalDateTime dateLecture; - /** Date et heure de dernière lecture */ - @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 */ + /** Nombre de tentatives */ private Integer nombreTentatives; - /** Nombre maximum de tentatives autorisées */ - 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 */ + /** Message d'erreur */ private String messageErreur; - /** Code d'erreur technique */ - private String codeErreur; + /** Données additionnelles (JSON) */ + private String donneesAdditionnelles; - /** Trace de la pile d'erreur (pour debug) */ - private String traceErreur; + /** ID du membre */ + private UUID membreId; - /** Métadonnées techniques */ - private Map metadonnees; + /** ID de l'organisation */ + private UUID organisationId; - /** Tags pour catégorisation */ - private List tags; - - /** 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 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 getDestinatairesIds() { - return destinatairesIds; - } - - public void setDestinatairesIds(List destinatairesIds) { - this.destinatairesIds = destinatairesIds; - } - - public String getOrganisationId() { - return organisationId; - } - - public void setOrganisationId(String organisationId) { - this.organisationId = organisationId; - } - - public Map getDonneesPersonnalisees() { - return donneesPersonnalisees; - } - - public void setDonneesPersonnalisees(Map 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 getParametresAction() { - return parametresAction; - } - - public void setParametresAction(Map parametresAction) { - this.parametresAction = parametresAction; - } - - public List getActionsRapides() { - return actionsRapides; - } - - public void setActionsRapides(List 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 getMetadonnees() { - return metadonnees; - } - - public void setMetadonnees(Map metadonnees) { - this.metadonnees = metadonnees; - } - - public List getTags() { - return tags; - } - - public void setTags(List 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); - } + /** ID du template */ + private UUID templateId; } diff --git a/unionflow-server-api/src/main/java/dev/lions/unionflow/server/api/dto/notification/TemplateNotificationDTO.java b/unionflow-server-api/src/main/java/dev/lions/unionflow/server/api/dto/notification/TemplateNotificationDTO.java new file mode 100644 index 0000000..db86a4a --- /dev/null +++ b/unionflow-server-api/src/main/java/dev/lions/unionflow/server/api/dto/notification/TemplateNotificationDTO.java @@ -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; +} + diff --git a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/repository/NotificationRepository.java b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/repository/NotificationRepository.java new file mode 100644 index 0000000..7d92bd8 --- /dev/null +++ b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/repository/NotificationRepository.java @@ -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 { + + /** + * Trouve toutes les notifications d'un membre + * + * @param membreId ID du membre + * @return Liste des notifications + */ + public List 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 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 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 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 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 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 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 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(); + } +} + diff --git a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/repository/TemplateNotificationRepository.java b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/repository/TemplateNotificationRepository.java new file mode 100644 index 0000000..b028efb --- /dev/null +++ b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/repository/TemplateNotificationRepository.java @@ -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 { + + /** + * Trouve un template par son code + * + * @param code Code du template + * @return Template ou Optional.empty() + */ + public Optional findByCode(String code) { + return find("code = ?1 AND actif = true", code).firstResultOptional(); + } + + /** + * Trouve tous les templates actifs + * + * @return Liste des templates actifs + */ + public List 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 findByLangue(String langue) { + return find("langue = ?1 AND actif = true ORDER BY code ASC", langue).list(); + } +} + diff --git a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/service/NotificationService.java b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/service/NotificationService.java index 84f1da3..f6437fd 100644 --- a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/service/NotificationService.java +++ b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/service/NotificationService.java @@ -1,484 +1,297 @@ package dev.lions.unionflow.server.service; 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.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.inject.Inject; import jakarta.transaction.Transactional; +import jakarta.ws.rs.NotFoundException; import java.time.LocalDateTime; -import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; -import org.eclipse.microprofile.config.inject.ConfigProperty; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; import org.jboss.logging.Logger; /** - * Service principal de gestion des notifications UnionFlow - * - *

Ce service orchestre l'envoi, la gestion et le suivi des notifications avec intégration - * Firebase, templates dynamiques et préférences utilisateur. + * Service métier pour la gestion des notifications * * @author UnionFlow Team - * @version 1.0 - * @since 2025-01-16 + * @version 3.0 + * @since 2025-01-29 */ @ApplicationScoped public class NotificationService { private static final Logger LOG = Logger.getLogger(NotificationService.class); - // @Inject - // FirebaseNotificationService firebaseService; + @Inject NotificationRepository notificationRepository; - // @Inject - // NotificationTemplateService templateService; + @Inject TemplateNotificationRepository templateNotificationRepository; - // @Inject - // PreferencesNotificationService preferencesService; + @Inject MembreRepository membreRepository; - // @Inject - // NotificationHistoryService historyService; + @Inject OrganisationRepository organisationRepository; - // @Inject - // 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 preferencesCache = - new ConcurrentHashMap<>(); - - // Statistiques en temps réel - private final Map statistiques = new ConcurrentHashMap<>(); + @Inject KeycloakService keycloakService; /** - * Envoie une notification simple + * Crée un nouveau template de notification * - * @param notification La notification à envoyer - * @return CompletableFuture avec le résultat de l'envoi - */ - public CompletableFuture 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> envoyerNotificationGroupe( - TypeNotification typeNotification, - String titre, - String message, - List destinatairesIds, - Map donneesPersonnalisees) { - - LOG.infof("Envoi de notification de groupe: %s destinataires", destinatairesIds.size()); - - return CompletableFuture.supplyAsync( - () -> { - List 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 batch = destinatairesIds.subList(i, fin); - - List> 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 + * @param templateDTO DTO du template à créer + * @return DTO du template créé */ @Transactional - public NotificationDTO programmerNotification( - NotificationDTO notification, LocalDateTime dateEnvoi) { - LOG.infof("Programmation de notification pour: %s", dateEnvoi); + public TemplateNotificationDTO creerTemplate(TemplateNotificationDTO templateDTO) { + LOG.infof("Création d'un nouveau template: %s", templateDTO.getCode()); - notification.setId(UUID.randomUUID().toString()); - notification.setStatut(StatutNotification.PROGRAMMEE); - notification.setDateEnvoiProgramme(dateEnvoi); - 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; + // Vérifier l'unicité du code + if (templateNotificationRepository.findByCode(templateDTO.getCode()).isPresent()) { + throw new IllegalArgumentException("Un template avec ce code existe déjà: " + templateDTO.getCode()); } + + 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 * - * @param notificationId ID de la notification - * @param utilisateurId ID de l'utilisateur - * @return true si le marquage a réussi + * @param id ID de la notification + * @return DTO de la notification mise à jour */ @Transactional - public boolean marquerCommeLue(String notificationId, String utilisateurId) { - LOG.debugf( - "Marquage comme lue: notification=%s, utilisateur=%s", notificationId, utilisateurId); + public NotificationDTO marquerCommeLue(UUID id) { + LOG.infof("Marquage de la notification comme lue: ID=%s", id); - try { - // À implémenter quand les services seront configurés - // NotificationDTO notification = historyService.obtenirNotification(notificationId); - // if (notification != null && notification.getDestinatairesIds().contains(utilisateurId)) { - // notification.setEstLue(true); - // notification.setDateDerniereLecture(LocalDateTime.now()); - // notification.setStatut(StatutNotification.LUE); - // historyService.mettreAJourNotification(notification); - // incrementerStatistique("notifications_lues"); - // return true; - // } + Notification notification = + notificationRepository + .findByIdOptional(id) + .orElseThrow(() -> new NotFoundException("Notification non trouvée avec l'ID: " + id)); - incrementerStatistique("notifications_lues"); - return true; + notification.setStatut(StatutNotification.LUE); + notification.setDateLecture(LocalDateTime.now()); + notification.setModifiePar(keycloakService.getCurrentUserEmail()); - } catch (Exception e) { - LOG.errorf(e, "Erreur lors du marquage comme lue: %s", notificationId); - return false; - } + notificationRepository.persist(notification); + LOG.infof("Notification marquée comme lue: ID=%s", id); + + return convertToDTO(notification); } /** - * Archive une notification + * Trouve une notification par son ID * - * @param notificationId ID de la notification - * @param utilisateurId ID de l'utilisateur - * @return true si l'archivage a réussi + * @param id ID de la notification + * @return DTO de la notification */ - @Transactional - public boolean archiverNotification(String notificationId, String utilisateurId) { - LOG.debugf("Archivage: notification=%s, utilisateur=%s", notificationId, utilisateurId); - - try { - // À 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; - } + public NotificationDTO trouverNotificationParId(UUID id) { + return notificationRepository + .findByIdOptional(id) + .map(this::convertToDTO) + .orElseThrow(() -> new NotFoundException("Notification non trouvée avec l'ID: " + id)); } /** - * Obtient les notifications d'un utilisateur + * Liste toutes les notifications d'un membre * - * @param utilisateurId ID de l'utilisateur - * @param includeArchivees Inclure les notifications archivées - * @param limite Nombre maximum de notifications à retourner + * @param membreId ID du membre * @return Liste des notifications */ - public List obtenirNotificationsUtilisateur( - String utilisateurId, boolean includeArchivees, int limite) { - - LOG.debugf("Récupération notifications utilisateur: %s", utilisateurId); - - 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<>(); - } + public List listerNotificationsParMembre(UUID membreId) { + return notificationRepository.findByMembreId(membreId).stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); } /** - * 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 obtenirStatistiques() { - Map stats = new HashMap<>(statistiques); - - // Ajout des statistiques calculées - 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; + public List listerNotificationsNonLuesParMembre(UUID membreId) { + return notificationRepository.findNonLuesByMembreId(membreId).stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); } /** - * Envoie une notification de test + * Liste les notifications en attente d'envoi * - * @param utilisateurId ID de l'utilisateur - * @param typeNotification Type de notification à tester - * @return La notification de test envoyée + * @return Liste des notifications en attente */ - public CompletableFuture envoyerNotificationTest( - String utilisateurId, TypeNotification typeNotification) { - - LOG.infof( - "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); + public List listerNotificationsEnAttenteEnvoi() { + return notificationRepository.findEnAttenteEnvoi().stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); } - // === MÉTHODES PRIVÉES === + // ======================================== + // MÉTHODES PRIVÉES + // ======================================== - /** Valide une notification avant envoi */ - private void validerNotification(NotificationDTO notification) { - if (notification.getTitre() == null || notification.getTitre().trim().isEmpty()) { - throw new IllegalArgumentException("Le titre de la notification est obligatoire"); + /** Convertit une entité TemplateNotification en DTO */ + private TemplateNotificationDTO convertToDTO(TemplateNotification template) { + if (template == null) { + return null; } - if (notification.getMessage() == null || notification.getMessage().trim().isEmpty()) { - throw new IllegalArgumentException("Le message de la notification est obligatoire"); - } + TemplateNotificationDTO dto = new TemplateNotificationDTO(); + 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 - || 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"); - } + return dto; } - /** Vérifie les préférences utilisateur pour une notification */ - private boolean verifierPreferencesUtilisateur(NotificationDTO notification) { - if (!notificationsEnabled) { - return false; + /** Convertit un DTO en entité TemplateNotification */ + private TemplateNotification convertToEntity(TemplateNotificationDTO dto) { + if (dto == null) { + return null; } - // Vérification pour chaque destinataire - for (String destinataireId : notification.getDestinatairesIds()) { - PreferencesNotificationDTO preferences = obtenirPreferencesUtilisateur(destinataireId); + TemplateNotification template = new TemplateNotification(); + template.setCode(dto.getCode()); + 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 false; - } + return template; + } - if (!preferences.isTypeActive(notification.getTypeNotification())) { - return false; - } - - if (!preferences.isCanalActif(notification.getCanal())) { - return false; - } - - if (preferences.isExpediteurBloque(notification.getExpediteurId())) { - return false; - } - - if (preferences.isEnModeSilencieux() - && !notification.getTypeNotification().isCritique() - && !preferences.getUrgentesIgnorentSilencieux()) { - return false; - } + /** Convertit une entité Notification en DTO */ + private NotificationDTO convertToDTO(Notification notification) { + if (notification == null) { + return null; } - 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) */ - private PreferencesNotificationDTO obtenirPreferencesUtilisateur(String utilisateurId) { - return preferencesCache.computeIfAbsent( - utilisateurId, - 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); - } - }); - } + /** Convertit un DTO en entité Notification */ + private Notification convertToEntity(NotificationDTO dto) { + if (dto == null) { + return null; + } - /** Incrémente une statistique */ - private void incrementerStatistique(String cle) { - statistiques.merge(cle, 1L, Long::sum); - } + Notification notification = new Notification(); + notification.setTypeNotification(dto.getTypeNotification()); + 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 */ - public void viderCachePreferences() { - preferencesCache.clear(); - LOG.info("Cache des préférences vidé"); - } + // Relations + if (dto.getMembreId() != null) { + Membre membre = + 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 */ - public void rechargerPreferencesUtilisateur(String utilisateurId) { - preferencesCache.remove(utilisateurId); - LOG.debugf("Préférences rechargées pour l'utilisateur: %s", utilisateurId); + if (dto.getOrganisationId() != null) { + Organisation org = + organisationRepository + .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; } }