Files
unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/service/EvenementService.java
dahoud a7bcaf9277 feat(backend): endpoints inscriptions + feedback événements
Ajoute infrastructure complète pour gérer inscriptions et feedbacks événements.

## Entités
- FeedbackEvenement : note 1-5, commentaire, modération (PUBLIE/EN_ATTENTE/REJETE)
- InscriptionEvenement : déjà existait, utilisation ajoutée

## Repositories
- InscriptionEvenementRepository : findByMembreAndEvenement, findByEvenement, countConfirmees, isMembreInscrit
- FeedbackEvenementRepository : findByMembreAndEvenement, findPubliesByEvenement, calculateAverageNote

## Service (EvenementService)
Inscriptions :
- inscrireEvenement() : vérifie capacité, crée inscription CONFIRMEE
- desinscrireEvenement() : soft delete inscription
- getParticipants() : liste inscriptions confirmées
- getMesInscriptions() : inscriptions du membre connecté

Feedbacks :
- soumetteFeedback() : note 1-5 + commentaire, vérifie participation, événement terminé
- getFeedbacks() : liste feedbacks publiés
- getStatistiquesFeedback() : note moyenne + nombre feedbacks

## REST Endpoints (6 total)
Inscriptions :
- POST /api/evenements/{id}/inscriptions - S'inscrire
- DELETE /api/evenements/{id}/inscriptions - Se désinscrire
- GET /api/evenements/{id}/participants - Liste participants
- GET /api/evenements/mes-inscriptions - Mes inscriptions

Feedbacks :
- POST /api/evenements/{id}/feedback - Soumettre feedback (note+commentaire)
- GET /api/evenements/{id}/feedbacks - Liste feedbacks + stats

## Database
- Migration V7 : table feedbacks_evenement
- Contrainte unique: un feedback par membre/événement
- Index: membre_id, evenement_id, date_feedback, moderation_statut

Débloquer fonctionnalités événements mobile.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-16 20:11:03 +00:00

579 lines
20 KiB
Java

package dev.lions.unionflow.server.service;
import dev.lions.unionflow.server.entity.Evenement;
import dev.lions.unionflow.server.entity.FeedbackEvenement;
import dev.lions.unionflow.server.entity.InscriptionEvenement;
import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.repository.EvenementRepository;
import dev.lions.unionflow.server.repository.FeedbackEvenementRepository;
import dev.lions.unionflow.server.repository.InscriptionEvenementRepository;
import dev.lions.unionflow.server.repository.MembreRepository;
import dev.lions.unionflow.server.repository.OrganisationRepository;
import dev.lions.unionflow.server.service.KeycloakService;
import io.quarkus.panache.common.Page;
import io.quarkus.panache.common.Sort;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import java.time.LocalDateTime;
import jakarta.ws.rs.NotFoundException;
import org.hibernate.Hibernate;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import org.jboss.logging.Logger;
/**
* Service métier pour la gestion des événements Version simplifiée pour tester
* les imports et
* Lombok
*
* @author UnionFlow Team
* @version 1.0
* @since 2025-01-15
*/
@ApplicationScoped
public class EvenementService {
private static final Logger LOG = Logger.getLogger(EvenementService.class);
@Inject
EvenementRepository evenementRepository;
@Inject
MembreRepository membreRepository;
@Inject
OrganisationRepository organisationRepository;
@Inject
KeycloakService keycloakService;
@Inject
InscriptionEvenementRepository inscriptionRepository;
@Inject
FeedbackEvenementRepository feedbackRepository;
/**
* Crée un nouvel événement
*
* @param evenement l'événement à créer
* @return l'événement créé
* @throws IllegalArgumentException si les données sont invalides
*/
@Transactional
public Evenement creerEvenement(Evenement evenement) {
LOG.infof("Création événement: %s", evenement.getTitre());
// Validation des données
validerEvenement(evenement);
// Vérifier l'unicité du titre dans l'organisation
if (evenement.getOrganisation() != null) {
Optional<Evenement> existant = evenementRepository.findByTitre(evenement.getTitre());
if (existant.isPresent()
&& existant.get().getOrganisation().getId().equals(evenement.getOrganisation().getId())) {
throw new IllegalArgumentException(
"Un événement avec ce titre existe déjà dans cette organisation");
}
}
// Métadonnées de création
evenement.setCreePar(keycloakService.getCurrentUserEmail());
// Valeurs par défaut
if (evenement.getStatut() == null) {
evenement.setStatut("PLANIFIE");
}
if (evenement.getActif() == null) {
evenement.setActif(true);
}
if (evenement.getVisiblePublic() == null) {
evenement.setVisiblePublic(true);
}
if (evenement.getInscriptionRequise() == null) {
evenement.setInscriptionRequise(true);
}
evenementRepository.persist(evenement);
LOG.infof("Événement créé avec succès: ID=%s, Titre=%s", evenement.getId(), evenement.getTitre());
return evenement;
}
/**
* Met à jour un événement existant
*
* @param id l'UUID de l'événement
* @param evenementMisAJour les nouvelles données
* @return l'événement mis à jour
* @throws IllegalArgumentException si l'événement n'existe pas
*/
@Transactional
public Evenement mettreAJourEvenement(UUID id, Evenement evenementMisAJour) {
LOG.infof("Mise à jour événement ID: %s", id);
Evenement evenementExistant = evenementRepository
.findByIdOptional(id)
.orElseThrow(
() -> new NotFoundException("Événement non trouvé avec l'ID: " + id));
// Vérifier les permissions
if (!peutModifierEvenement(evenementExistant)) {
throw new SecurityException("Vous n'avez pas les permissions pour modifier cet événement");
}
// Validation des nouvelles données
validerEvenement(evenementMisAJour);
// Mise à jour des champs
evenementExistant.setTitre(evenementMisAJour.getTitre());
evenementExistant.setDescription(evenementMisAJour.getDescription());
evenementExistant.setDateDebut(evenementMisAJour.getDateDebut());
evenementExistant.setDateFin(evenementMisAJour.getDateFin());
evenementExistant.setLieu(evenementMisAJour.getLieu());
evenementExistant.setAdresse(evenementMisAJour.getAdresse());
evenementExistant.setTypeEvenement(evenementMisAJour.getTypeEvenement());
evenementExistant.setCapaciteMax(evenementMisAJour.getCapaciteMax());
evenementExistant.setPrix(evenementMisAJour.getPrix());
evenementExistant.setInscriptionRequise(evenementMisAJour.getInscriptionRequise());
evenementExistant.setDateLimiteInscription(evenementMisAJour.getDateLimiteInscription());
evenementExistant.setInstructionsParticulieres(
evenementMisAJour.getInstructionsParticulieres());
evenementExistant.setContactOrganisateur(evenementMisAJour.getContactOrganisateur());
evenementExistant.setMaterielRequis(evenementMisAJour.getMaterielRequis());
evenementExistant.setVisiblePublic(evenementMisAJour.getVisiblePublic());
if (evenementMisAJour.getStatut() != null) {
evenementExistant.setStatut(evenementMisAJour.getStatut());
}
// Métadonnées de modification
evenementExistant.setModifiePar(keycloakService.getCurrentUserEmail());
evenementRepository.update(evenementExistant);
LOG.infof("Événement mis à jour avec succès: ID=%s", id);
// Initialiser les relations lazy pour éviter LazyInitializationException lors
// de la sérialisation JSON
Hibernate.initialize(evenementExistant.getOrganisation());
Hibernate.initialize(evenementExistant.getOrganisateur());
return evenementExistant;
}
/** Trouve un événement par ID */
public Optional<Evenement> trouverParId(UUID id) {
return evenementRepository.findByIdOptional(id);
}
/** Liste tous les événements actifs avec pagination */
public List<Evenement> listerEvenementsActifs(Page page, Sort sort) {
return evenementRepository.findAllActifs(page, sort);
}
/** Liste les événements à venir */
public List<Evenement> listerEvenementsAVenir(Page page, Sort sort) {
return evenementRepository.findEvenementsAVenir(page, sort);
}
/** Liste les événements publics */
public List<Evenement> listerEvenementsPublics(Page page, Sort sort) {
return evenementRepository.findEvenementsPublics(page, sort);
}
/** Recherche d'événements par terme */
public List<Evenement> rechercherEvenements(String terme, Page page, Sort sort) {
return evenementRepository.rechercheAvancee(
terme, null, null, null, null, null, null, null, null, null, page, sort);
}
/** Liste les événements par type */
public List<Evenement> listerParType(String type, Page page, Sort sort) {
return evenementRepository.findByType(type, page, sort);
}
/**
* Supprime logiquement un événement
*
* @param id l'UUID de l'événement à supprimer
* @throws IllegalArgumentException si l'événement n'existe pas
*/
@Transactional
public void supprimerEvenement(UUID id) {
LOG.infof("Suppression événement ID: %s", id);
Evenement evenement = evenementRepository
.findByIdOptional(id)
.orElseThrow(
() -> new NotFoundException("Événement non trouvé avec l'ID: " + id));
// Vérifier les permissions
if (!peutModifierEvenement(evenement)) {
throw new SecurityException("Vous n'avez pas les permissions pour supprimer cet événement");
}
// Vérifier s'il y a des inscriptions
if (evenement.getNombreInscrits() > 0) {
throw new IllegalStateException("Impossible de supprimer un événement avec des inscriptions");
}
// Suppression logique
evenement.setActif(false);
evenement.setModifiePar(keycloakService.getCurrentUserEmail());
evenementRepository.update(evenement);
LOG.infof("Événement supprimé avec succès: ID=%s", id);
}
/**
* Change le statut d'un événement
*
* @param id l'UUID de l'événement
* @param nouveauStatut le nouveau statut
* @return l'événement mis à jour
*/
@Transactional
public Evenement changerStatut(UUID id, String nouveauStatut) {
LOG.infof("Changement statut événement ID: %s vers %s", id, nouveauStatut);
Evenement evenement = evenementRepository
.findByIdOptional(id)
.orElseThrow(
() -> new NotFoundException("Événement non trouvé avec l'ID: " + id));
// Vérifier les permissions
if (!peutModifierEvenement(evenement)) {
throw new SecurityException("Vous n'avez pas les permissions pour modifier cet événement");
}
// Valider le changement de statut
validerChangementStatut(evenement.getStatut(), nouveauStatut);
evenement.setStatut(nouveauStatut);
evenement.setModifiePar(keycloakService.getCurrentUserEmail());
evenementRepository.update(evenement);
LOG.infof("Statut événement changé avec succès: ID=%s, Nouveau statut=%s", id, nouveauStatut);
return evenement;
}
/**
* Compte le nombre total d'événements
*
* @return le nombre total d'événements
*/
public long countEvenements() {
return evenementRepository.count();
}
/**
* Compte le nombre d'événements actifs
*
* @return le nombre d'événements actifs
*/
public long countEvenementsActifs() {
return evenementRepository.countActifs();
}
/**
* Obtient les statistiques des événements
*
* @return les statistiques sous forme de Map
*/
public Map<String, Object> obtenirStatistiques() {
Map<String, Long> statsBase = evenementRepository.getStatistiques();
long total = statsBase.getOrDefault("total", 0L);
long actifs = statsBase.getOrDefault("actifs", 0L);
long aVenir = statsBase.getOrDefault("aVenir", 0L);
long enCours = statsBase.getOrDefault("enCours", 0L);
Map<String, Object> result = new java.util.HashMap<>();
result.put("total", total);
result.put("actifs", actifs);
result.put("aVenir", aVenir);
result.put("enCours", enCours);
result.put("passes", statsBase.getOrDefault("passes", 0L));
result.put("publics", statsBase.getOrDefault("publics", 0L));
result.put("avecInscription", statsBase.getOrDefault("avecInscription", 0L));
result.put("tauxActivite", total > 0 ? (actifs * 100.0 / total) : 0.0);
result.put("tauxEvenementsAVenir", total > 0 ? (aVenir * 100.0 / total) : 0.0);
result.put("tauxEvenementsEnCours", total > 0 ? (enCours * 100.0 / total) : 0.0);
result.put("timestamp", LocalDateTime.now());
return result;
}
// Méthodes privées de validation et permissions
/** Valide les données d'un événement */
private void validerEvenement(Evenement evenement) {
if (evenement.getTitre() == null || evenement.getTitre().trim().isEmpty()) {
throw new IllegalArgumentException("Le titre de l'événement est obligatoire");
}
if (evenement.getDateDebut() == null) {
throw new IllegalArgumentException("La date de début est obligatoire");
}
if (evenement.getDateDebut().isBefore(LocalDateTime.now().minusHours(1))) {
throw new IllegalArgumentException("La date de début ne peut pas être dans le passé");
}
if (evenement.getDateFin() != null
&& evenement.getDateFin().isBefore(evenement.getDateDebut())) {
throw new IllegalArgumentException(
"La date de fin ne peut pas être antérieure à la date de début");
}
if (evenement.getCapaciteMax() != null && evenement.getCapaciteMax() <= 0) {
throw new IllegalArgumentException("La capacité maximale doit être positive");
}
if (evenement.getPrix() != null
&& evenement.getPrix().compareTo(java.math.BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("Le prix ne peut pas être négatif");
}
}
/** Valide un changement de statut */
private void validerChangementStatut(
String statutActuel, String nouveauStatut) {
// Règles de transition simplifiées pour la version mobile
if ("TERMINE".equals(statutActuel) || "ANNULE".equals(statutActuel)) {
throw new IllegalArgumentException(
"Impossible de changer le statut d'un événement terminé ou annulé");
}
}
/** Vérifie les permissions de modification pour l'application mobile */
private boolean peutModifierEvenement(Evenement evenement) {
if (keycloakService.hasRole("ADMIN") || keycloakService.hasRole("ORGANISATEUR_EVENEMENT")) {
return true;
}
String utilisateurActuel = keycloakService.getCurrentUserEmail();
return utilisateurActuel != null && utilisateurActuel.equals(evenement.getCreePar());
}
/**
* Indique si l'utilisateur connecté est inscrit à l'événement.
* Utilisé par l'app mobile pour afficher le statut d'inscription sur la page détail.
*/
public boolean isUserInscrit(UUID evenementId) {
Evenement evenement = evenementRepository.findByIdOptional(evenementId).orElse(null);
if (evenement == null) {
return false;
}
String email = keycloakService.getCurrentUserEmail();
if (email == null || email.isBlank()) {
return false;
}
return membreRepository.findByEmail(email)
.map(m -> evenement.isMemberInscrit(m.getId()))
.orElse(false);
}
// === GESTION DES INSCRIPTIONS ===
/**
* Inscrit l'utilisateur connecté à un événement
*
* @param evenementId UUID de l'événement
* @return L'inscription créée
*/
@Transactional
public InscriptionEvenement inscrireEvenement(UUID evenementId) {
String email = keycloakService.getCurrentUserEmail();
if (email == null || email.isBlank()) {
throw new IllegalStateException("Utilisateur non authentifié");
}
Membre membre =
membreRepository
.findByEmail(email)
.orElseThrow(() -> new NotFoundException("Membre non trouvé"));
Evenement evenement =
evenementRepository
.findByIdOptional(evenementId)
.orElseThrow(() -> new NotFoundException("Événement non trouvé"));
// Vérifier si déjà inscrit
Optional<InscriptionEvenement> existante =
inscriptionRepository.findByMembreAndEvenement(membre.getId(), evenementId);
if (existante.isPresent()) {
throw new IllegalStateException("Vous êtes déjà inscrit à cet événement");
}
// Vérifier capacité
if (evenement.getCapaciteMax() != null) {
long nbInscrits = inscriptionRepository.countConfirmeesByEvenement(evenementId);
if (nbInscrits >= evenement.getCapaciteMax()) {
throw new IllegalStateException("L'événement est complet");
}
}
InscriptionEvenement inscription =
InscriptionEvenement.builder()
.membre(membre)
.evenement(evenement)
.statut(InscriptionEvenement.StatutInscription.CONFIRMEE.name())
.dateInscription(LocalDateTime.now())
.build();
inscriptionRepository.persist(inscription);
LOG.infof(
"Inscription créée: membre=%s, événement=%s", membre.getEmail(), evenement.getTitre());
return inscription;
}
/**
* Désinscrit l'utilisateur connecté d'un événement
*
* @param evenementId UUID de l'événement
*/
@Transactional
public void desinscrireEvenement(UUID evenementId) {
String email = keycloakService.getCurrentUserEmail();
if (email == null || email.isBlank()) {
throw new IllegalStateException("Utilisateur non authentifié");
}
Membre membre =
membreRepository
.findByEmail(email)
.orElseThrow(() -> new NotFoundException("Membre non trouvé"));
InscriptionEvenement inscription =
inscriptionRepository
.findByMembreAndEvenement(membre.getId(), evenementId)
.orElseThrow(() -> new NotFoundException("Inscription non trouvée"));
inscriptionRepository.softDelete(inscription);
LOG.infof("Désinscription: membre=%s, événement=%s", membre.getEmail(), evenementId);
}
/**
* Liste les participants d'un événement
*
* @param evenementId UUID de l'événement
* @return Liste des inscriptions confirmées
*/
public List<InscriptionEvenement> getParticipants(UUID evenementId) {
return inscriptionRepository.findConfirmeesByEvenement(evenementId);
}
/**
* Liste les inscriptions de l'utilisateur connecté
*
* @return Liste des inscriptions du membre
*/
public List<InscriptionEvenement> getMesInscriptions() {
String email = keycloakService.getCurrentUserEmail();
if (email == null || email.isBlank()) {
throw new IllegalStateException("Utilisateur non authentifié");
}
Membre membre =
membreRepository
.findByEmail(email)
.orElseThrow(() -> new NotFoundException("Membre non trouvé"));
return inscriptionRepository.findByMembre(membre.getId());
}
// === GESTION DES FEEDBACKS ===
/**
* Soumet un feedback pour un événement
*
* @param evenementId UUID de l'événement
* @param note Note de 1 à 5
* @param commentaire Commentaire optionnel
* @return Le feedback créé
*/
@Transactional
public FeedbackEvenement soumetteFeedback(
UUID evenementId, Integer note, String commentaire) {
String email = keycloakService.getCurrentUserEmail();
if (email == null || email.isBlank()) {
throw new IllegalStateException("Utilisateur non authentifié");
}
Membre membre =
membreRepository
.findByEmail(email)
.orElseThrow(() -> new NotFoundException("Membre non trouvé"));
Evenement evenement =
evenementRepository
.findByIdOptional(evenementId)
.orElseThrow(() -> new NotFoundException("Événement non trouvé"));
// Vérifier si déjà soumis
Optional<FeedbackEvenement> existant =
feedbackRepository.findByMembreAndEvenement(membre.getId(), evenementId);
if (existant.isPresent()) {
throw new IllegalStateException("Vous avez déjà soumis un feedback pour cet événement");
}
// Vérifier que le membre était inscrit
boolean etaitInscrit =
inscriptionRepository.isMembreInscrit(membre.getId(), evenementId);
if (!etaitInscrit) {
throw new IllegalStateException(
"Seuls les participants peuvent donner un feedback");
}
// Vérifier que l'événement est terminé
if (evenement.getDateFin() == null || evenement.getDateFin().isAfter(LocalDateTime.now())) {
throw new IllegalStateException(
"Vous ne pouvez donner un feedback qu'après la fin de l'événement");
}
FeedbackEvenement feedback =
FeedbackEvenement.builder()
.membre(membre)
.evenement(evenement)
.note(note)
.commentaire(commentaire)
.dateFeedback(LocalDateTime.now())
.moderationStatut(FeedbackEvenement.ModerationStatut.PUBLIE.name())
.build();
feedbackRepository.persist(feedback);
LOG.infof(
"Feedback créé: membre=%s, événement=%s, note=%d",
membre.getEmail(), evenement.getTitre(), note);
return feedback;
}
/**
* Liste les feedbacks d'un événement
*
* @param evenementId UUID de l'événement
* @return Liste des feedbacks publiés
*/
public List<FeedbackEvenement> getFeedbacks(UUID evenementId) {
return feedbackRepository.findPubliesByEvenement(evenementId);
}
/**
* Calcule les statistiques de feedback pour un événement
*
* @param evenementId UUID de l'événement
* @return Map contenant noteMovenne et nombreFeedbacks
*/
public Map<String, Object> getStatistiquesFeedback(UUID evenementId) {
Double noteMoyenne = feedbackRepository.calculateAverageNote(evenementId);
long nombreFeedbacks = feedbackRepository.countPubliesByEvenement(evenementId);
return Map.of(
"noteMoyenne", noteMoyenne,
"nombreFeedbacks", nombreFeedbacks);
}
}