preferences; // v2.0 - Préférences utilisateur
+
+ // Champs de compatibilité v1.0 (dépréciés)
/**
- * Constructeur qui transforme une entité Users en DTO.
+ * @deprecated Utiliser {@link #id} Ă la place.
+ */
+ @Deprecated
+ private UUID uuid;
+
+ /**
+ * @deprecated Utiliser {@link #lastName} Ă la place.
+ */
+ @Deprecated
+ private String nom;
+
+ /**
+ * @deprecated Utiliser {@link #firstName} Ă la place.
+ */
+ @Deprecated
+ private String prenoms;
+
+ /**
+ * Constructeur qui transforme une entité Users en DTO (v2.0).
*
* @param user L'utilisateur Ă convertir en DTO.
*/
public UserCreateResponseDTO(Users user) {
- this.uuid = user.getId();
- this.nom = user.getNom();
- this.prenoms = user.getPrenoms();
+ this.id = user.getId(); // v2.0
+ this.firstName = user.getFirstName(); // v2.0
+ this.lastName = user.getLastName(); // v2.0
this.email = user.getEmail();
this.role = user.getRole();
this.profileImageUrl = user.getProfileImageUrl();
+ this.bio = user.getBio(); // v2.0
+ this.loyaltyPoints = user.getLoyaltyPoints(); // v2.0
+ this.preferences = user.getPreferences(); // v2.0
+
+ // Compatibilité v1.0
+ this.uuid = this.id;
+ this.nom = this.lastName;
+ this.prenoms = this.firstName;
+
System.out.println("[LOG] DTO créé pour l'utilisateur : " + this.email);
}
}
diff --git a/src/main/java/com/lions/dev/dto/response/users/UserResponseDTO.java b/src/main/java/com/lions/dev/dto/response/users/UserResponseDTO.java
index 5bf2f12..c6d65b3 100644
--- a/src/main/java/com/lions/dev/dto/response/users/UserResponseDTO.java
+++ b/src/main/java/com/lions/dev/dto/response/users/UserResponseDTO.java
@@ -9,6 +9,10 @@ import lombok.Setter;
/**
* DTO (Data Transfer Object) pour l'utilisateur.
+ *
+ * Version 2.0 - Architecture refactorée avec nommage standardisé.
+ * Conforme à l'architecture de données AfterWork v2.0 (Ultra-Compétitive).
+ *
*
* Cette classe sert de reprĂ©sentation simplifiĂ©e d'un utilisateur, avec un ensemble d'informations nĂ©cessaires Ă
* la réponse de l'API. Elle est utilisée pour transférer des données entre le backend (serveur) et le frontend (client)
@@ -27,14 +31,14 @@ public class UserResponseDTO {
private UUID id;
/**
- * Nom de famille de l'utilisateur. C'est une donnée importante pour l'affichage du profil.
+ * Prénom de l'utilisateur (v2.0).
*/
- private String nom;
+ private String firstName;
/**
- * Prénom(s) de l'utilisateur. Représente le ou les prénoms associés à l'utilisateur.
+ * Nom de famille de l'utilisateur (v2.0).
*/
- private String prenoms;
+ private String lastName;
/**
* Adresse email de l'utilisateur. C'est une donnée souvent utilisée pour les communications.
@@ -47,13 +51,48 @@ public class UserResponseDTO {
*/
private String profileImageUrl;
+ /**
+ * Biographie courte de l'utilisateur (v2.0).
+ */
+ private String bio;
+
+ /**
+ * Points de fidélité accumulés (v2.0).
+ */
+ private Integer loyaltyPoints;
+
+ /**
+ * Préférences utilisateur en JSON (v2.0).
+ */
+ private java.util.Map preferences;
+
+ /**
+ * RĂŽle de l'utilisateur (ADMIN, USER, MANAGER, etc.).
+ */
+ private String role;
+
/**
* Indique si l'utilisateur est vérifié (compte officiel).
*/
private boolean isVerified;
/**
- * Constructeur de DTO à partir d'une entité Users.
+ * Indique si l'utilisateur est actuellement en ligne.
+ */
+ private boolean isOnline;
+
+ /**
+ * DerniÚre fois que l'utilisateur était en ligne.
+ */
+ private java.time.LocalDateTime lastSeen;
+
+ /**
+ * Date de création du compte.
+ */
+ private java.time.LocalDateTime createdAt;
+
+ /**
+ * Constructeur de DTO à partir d'une entité Users (v2.0).
*
* Ce constructeur prend une entité {@link Users} et extrait les données nécessaires pour
* peupler les champs du DTO. Cette transformation permet de transférer des données sans exposer
@@ -64,12 +103,28 @@ public class UserResponseDTO {
*/
public UserResponseDTO(Users user) {
if (user != null) {
- this.id = user.getId(); // Identifiant unique de l'utilisateur
- this.nom = user.getNom(); // Nom de famille
- this.prenoms = user.getPrenoms(); // Prénom(s)
- this.email = user.getEmail(); // Email
- this.profileImageUrl = user.getProfileImageUrl(); // URL de l'image de profil
- this.isVerified = user.isVerified(); // Statut de vérification
+ this.id = user.getId();
+ this.firstName = user.getFirstName(); // v2.0
+ this.lastName = user.getLastName(); // v2.0
+ this.email = user.getEmail();
+ this.profileImageUrl = user.getProfileImageUrl();
+ this.bio = user.getBio(); // v2.0
+ this.loyaltyPoints = user.getLoyaltyPoints(); // v2.0
+ this.preferences = user.getPreferences(); // v2.0
+ this.role = user.getRole();
+ this.isVerified = user.isVerified();
+ this.isOnline = user.isOnline();
+ this.lastSeen = user.getLastSeen();
+ this.createdAt = user.getCreatedAt();
}
}
+
+ /**
+ * Retourne le nom complet de l'utilisateur (v2.0).
+ *
+ * @return Le nom complet (firstName + lastName).
+ */
+ public String getFullName() {
+ return (firstName != null ? firstName : "") + " " + (lastName != null ? lastName : "").trim();
+ }
}
diff --git a/src/main/java/com/lions/dev/entity/booking/Booking.java b/src/main/java/com/lions/dev/entity/booking/Booking.java
new file mode 100644
index 0000000..a66bce3
--- /dev/null
+++ b/src/main/java/com/lions/dev/entity/booking/Booking.java
@@ -0,0 +1,105 @@
+package com.lions.dev.entity.booking;
+
+import com.lions.dev.entity.BaseEntity;
+import com.lions.dev.entity.establishment.Establishment;
+import com.lions.dev.entity.users.Users;
+import jakarta.persistence.*;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import lombok.ToString;
+
+import java.time.LocalDateTime;
+
+/**
+ * Entité représentant une réservation d'établissement.
+ *
+ * Version 2.0 - Architecture refactorée.
+ * Conforme à l'architecture de données AfterWork v2.0 (Ultra-Compétitive).
+ *
+ * Cette entité remplace/étend Reservation avec plus de détails et de flexibilité.
+ */
+@Entity
+@Table(name = "bookings")
+@Getter
+@Setter
+@NoArgsConstructor
+@ToString
+public class Booking extends BaseEntity {
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "establishment_id", nullable = false)
+ private Establishment establishment; // L'établissement concerné par la réservation
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "user_id", nullable = false)
+ private Users user; // L'utilisateur qui a fait la réservation
+
+ @Column(name = "reservation_time", nullable = false)
+ private LocalDateTime reservationTime; // Date et heure de la réservation
+
+ @Column(name = "guest_count", nullable = false)
+ private Integer guestCount = 1; // Nombre de convives
+
+ @Column(name = "status", nullable = false, length = 20)
+ private String status = "PENDING"; // Statut: PENDING, CONFIRMED, CANCELLED, COMPLETED
+
+ @Column(name = "special_requests", columnDefinition = "TEXT")
+ private String specialRequests; // Demandes spéciales (allergies, préférences, etc.)
+
+ @Column(name = "table_number", length = 20)
+ private String tableNumber; // Numéro de table assigné (si applicable)
+
+ /**
+ * Constructeur pour créer une réservation.
+ *
+ * @param establishment L'établissement concerné
+ * @param user L'utilisateur qui fait la réservation
+ * @param reservationTime La date et heure de la réservation
+ * @param guestCount Le nombre de convives
+ */
+ public Booking(Establishment establishment, Users user, LocalDateTime reservationTime, Integer guestCount) {
+ this.establishment = establishment;
+ this.user = user;
+ this.reservationTime = reservationTime;
+ this.guestCount = guestCount;
+ this.status = "PENDING";
+ }
+
+ /**
+ * Vérifie si la réservation est confirmée.
+ *
+ * @return true si la réservation est confirmée, false sinon.
+ */
+ public boolean isConfirmed() {
+ return "CONFIRMED".equalsIgnoreCase(this.status);
+ }
+
+ /**
+ * Vérifie si la réservation est en attente.
+ *
+ * @return true si la réservation est en attente, false sinon.
+ */
+ public boolean isPending() {
+ return "PENDING".equalsIgnoreCase(this.status);
+ }
+
+ /**
+ * Vérifie si la réservation est annulée.
+ *
+ * @return true si la réservation est annulée, false sinon.
+ */
+ public boolean isCancelled() {
+ return "CANCELLED".equalsIgnoreCase(this.status);
+ }
+
+ /**
+ * Vérifie si la réservation est complétée.
+ *
+ * @return true si la réservation est complétée, false sinon.
+ */
+ public boolean isCompleted() {
+ return "COMPLETED".equalsIgnoreCase(this.status);
+ }
+}
+
diff --git a/src/main/java/com/lions/dev/entity/establishment/BusinessHours.java b/src/main/java/com/lions/dev/entity/establishment/BusinessHours.java
new file mode 100644
index 0000000..71914f9
--- /dev/null
+++ b/src/main/java/com/lions/dev/entity/establishment/BusinessHours.java
@@ -0,0 +1,90 @@
+package com.lions.dev.entity.establishment;
+
+import com.lions.dev.entity.BaseEntity;
+import jakarta.persistence.*;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import lombok.ToString;
+
+import java.time.LocalDateTime;
+
+/**
+ * Entité représentant les horaires d'ouverture d'un établissement.
+ *
+ * Version 2.0 - Architecture refactorée.
+ * Conforme à l'architecture de données AfterWork v2.0 (Ultra-Compétitive).
+ *
+ * Cette entité permet de gérer les horaires d'ouverture par jour de la semaine,
+ * ainsi que les exceptions (fermetures temporaires, jours fériés, etc.).
+ */
+@Entity
+@Table(name = "business_hours")
+@Getter
+@Setter
+@NoArgsConstructor
+@ToString
+public class BusinessHours extends BaseEntity {
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "establishment_id", nullable = false)
+ private Establishment establishment; // L'établissement concerné
+
+ @Column(name = "day_of_week", nullable = false, length = 20)
+ private String dayOfWeek; // Jour de la semaine: MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
+
+ @Column(name = "open_time", nullable = false, length = 5)
+ private String openTime; // Heure d'ouverture (format HH:MM, ex: "09:00")
+
+ @Column(name = "close_time", nullable = false, length = 5)
+ private String closeTime; // Heure de fermeture (format HH:MM, ex: "18:00")
+
+ @Column(name = "is_closed", nullable = false)
+ private Boolean isClosed = false; // Indique si l'établissement est fermé ce jour
+
+ @Column(name = "is_exception", nullable = false)
+ private Boolean isException = false; // Indique si c'est un jour exceptionnel (fermeture temporaire, jour férié, etc.)
+
+ @Column(name = "exception_date")
+ private LocalDateTime exceptionDate; // Date de l'exception (si is_exception = true)
+
+ /**
+ * Constructeur pour créer des horaires d'ouverture standard.
+ *
+ * @param establishment L'établissement concerné
+ * @param dayOfWeek Le jour de la semaine
+ * @param openTime L'heure d'ouverture (format HH:MM)
+ * @param closeTime L'heure de fermeture (format HH:MM)
+ */
+ public BusinessHours(Establishment establishment, String dayOfWeek, String openTime, String closeTime) {
+ this.establishment = establishment;
+ this.dayOfWeek = dayOfWeek;
+ this.openTime = openTime;
+ this.closeTime = closeTime;
+ this.isClosed = false;
+ this.isException = false;
+ }
+
+ /**
+ * Constructeur pour créer un jour de fermeture.
+ *
+ * @param establishment L'établissement concerné
+ * @param dayOfWeek Le jour de la semaine
+ */
+ public BusinessHours(Establishment establishment, String dayOfWeek) {
+ this.establishment = establishment;
+ this.dayOfWeek = dayOfWeek;
+ this.isClosed = true;
+ this.isException = false;
+ }
+
+ /**
+ * Vérifie si l'établissement est ouvert ce jour.
+ *
+ * @return true si l'établissement est ouvert, false sinon.
+ */
+ public boolean isOpen() {
+ return !isClosed;
+ }
+}
+
diff --git a/src/main/java/com/lions/dev/entity/establishment/Establishment.java b/src/main/java/com/lions/dev/entity/establishment/Establishment.java
index 8485169..6ab022d 100644
--- a/src/main/java/com/lions/dev/entity/establishment/Establishment.java
+++ b/src/main/java/com/lions/dev/entity/establishment/Establishment.java
@@ -10,6 +10,10 @@ import lombok.ToString;
/**
* Entité représentant un établissement dans le systÚme AfterWork.
+ *
+ * Version 2.0 - Architecture refactorée avec nommage standardisé.
+ * Conforme à l'architecture de données AfterWork v2.0 (Ultra-Compétitive).
+ *
* Un établissement est un lieu physique (bar, restaurant, club, etc.)
* oĂč peuvent se dĂ©rouler des Ă©vĂ©nements Afterwork.
* Seuls les responsables d'établissement peuvent créer et gérer des établissements.
@@ -43,35 +47,20 @@ public class Establishment extends BaseEntity {
@Column(name = "phone_number")
private String phoneNumber; // Numéro de téléphone
- @Column(name = "email")
- private String email; // Email de contact
-
@Column(name = "website")
private String website; // Site web
- @Column(name = "image_url")
- private String imageUrl; // URL de l'image de l'établissement
-
- @Column(name = "rating")
- private Double rating; // Note moyenne sur 5 (déprécié, utiliser averageRating)
-
@Column(name = "average_rating")
private Double averageRating; // Note moyenne calculée (0.0 à 5.0)
- @Column(name = "total_ratings_count")
- private Integer totalRatingsCount; // Nombre total de notes
+ @Column(name = "total_reviews_count")
+ private Integer totalReviewsCount; // Nombre total d'avis (v2.0 - renommé depuis total_ratings_count)
@Column(name = "price_range")
- private String priceRange; // Fourchette de prix (cheap, moderate, expensive, luxury)
+ private String priceRange; // Fourchette de prix (LOW, MEDIUM, HIGH, PREMIUM)
- @Column(name = "capacity")
- private Integer capacity; // Capacité maximale
-
- @Column(name = "amenities", length = 1000)
- private String amenities; // Ăquipements (WiFi, Terrasse, Parking, etc.) - JSON ou sĂ©parĂ©s par virgule
-
- @Column(name = "opening_hours")
- private String openingHours; // Heures d'ouverture
+ @Column(name = "verification_status", nullable = false)
+ private String verificationStatus = "PENDING"; // Statut de vérification: PENDING, VERIFIED, REJECTED (v2.0)
@Column(name = "latitude")
private Double latitude; // Latitude pour la géolocalisation
@@ -88,7 +77,25 @@ public class Establishment extends BaseEntity {
private java.util.List medias = new java.util.ArrayList<>(); // Liste des médias de l'établissement
@OneToMany(mappedBy = "establishment", cascade = CascadeType.ALL, orphanRemoval = true)
- private java.util.List ratings = new java.util.ArrayList<>(); // Liste des notes de l'établissement
+ private java.util.List ratings = new java.util.ArrayList<>(); // Liste des notes de l'établissement (v1.0 - à migrer vers Review)
+
+ /**
+ * Vérifie si l'établissement est vérifié.
+ *
+ * @return true si l'établissement est vérifié, false sinon.
+ */
+ public boolean isVerified() {
+ return "VERIFIED".equalsIgnoreCase(this.verificationStatus);
+ }
+
+ /**
+ * Vérifie si l'établissement est en attente de vérification.
+ *
+ * @return true si l'établissement est en attente, false sinon.
+ */
+ public boolean isPending() {
+ return "PENDING".equalsIgnoreCase(this.verificationStatus);
+ }
/**
* Constructeur pour créer un établissement avec les informations de base.
diff --git a/src/main/java/com/lions/dev/entity/establishment/EstablishmentAmenity.java b/src/main/java/com/lions/dev/entity/establishment/EstablishmentAmenity.java
new file mode 100644
index 0000000..5dd7d8a
--- /dev/null
+++ b/src/main/java/com/lions/dev/entity/establishment/EstablishmentAmenity.java
@@ -0,0 +1,98 @@
+package com.lions.dev.entity.establishment;
+
+import jakarta.persistence.*;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import lombok.ToString;
+
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+/**
+ * Entité représentant un équipement d'un établissement.
+ *
+ * Version 2.0 - Architecture refactorée.
+ * Conforme à l'architecture de données AfterWork v2.0 (Ultra-Compétitive).
+ *
+ * Cette entité fait la liaison entre un établissement et un type d'équipement,
+ * avec la possibilité d'ajouter des détails supplémentaires.
+ */
+@Entity
+@Table(name = "establishment_amenities")
+@Getter
+@Setter
+@NoArgsConstructor
+@ToString
+@IdClass(EstablishmentAmenityId.class)
+public class EstablishmentAmenity {
+
+ @Id
+ @Column(name = "establishment_id", nullable = false)
+ private UUID establishmentId; // ID de l'établissement
+
+ @Id
+ @Column(name = "amenity_id", nullable = false)
+ private UUID amenityId; // ID du type d'équipement (référence à amenity_types)
+
+ @Column(name = "details", length = 500)
+ private String details; // Détails supplémentaires (ex: "Parking gratuit pour 20 voitures")
+
+ @Column(name = "created_at", nullable = false, updatable = false)
+ private LocalDateTime createdAt = LocalDateTime.now(); // Date de création
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "establishment_id", insertable = false, updatable = false)
+ private Establishment establishment; // L'établissement concerné
+
+ /**
+ * Constructeur pour créer une liaison établissement-équipement.
+ *
+ * @param establishmentId L'ID de l'établissement
+ * @param amenityId L'ID du type d'équipement
+ * @param details Les détails supplémentaires (optionnel)
+ */
+ public EstablishmentAmenity(UUID establishmentId, UUID amenityId, String details) {
+ this.establishmentId = establishmentId;
+ this.amenityId = amenityId;
+ this.details = details;
+ this.createdAt = LocalDateTime.now();
+ }
+
+ @PrePersist
+ protected void onCreate() {
+ if (createdAt == null) {
+ createdAt = LocalDateTime.now();
+ }
+ }
+}
+
+/**
+ * Classe composite pour la clé primaire de EstablishmentAmenity.
+ */
+@Getter
+@Setter
+@NoArgsConstructor
+class EstablishmentAmenityId implements java.io.Serializable {
+ private UUID establishmentId;
+ private UUID amenityId;
+
+ public EstablishmentAmenityId(UUID establishmentId, UUID amenityId) {
+ this.establishmentId = establishmentId;
+ this.amenityId = amenityId;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ EstablishmentAmenityId that = (EstablishmentAmenityId) o;
+ return establishmentId.equals(that.establishmentId) && amenityId.equals(that.amenityId);
+ }
+
+ @Override
+ public int hashCode() {
+ return establishmentId.hashCode() + amenityId.hashCode();
+ }
+}
+
diff --git a/src/main/java/com/lions/dev/entity/establishment/Review.java b/src/main/java/com/lions/dev/entity/establishment/Review.java
new file mode 100644
index 0000000..d088a52
--- /dev/null
+++ b/src/main/java/com/lions/dev/entity/establishment/Review.java
@@ -0,0 +1,92 @@
+package com.lions.dev.entity.establishment;
+
+import com.lions.dev.entity.BaseEntity;
+import com.lions.dev.entity.users.Users;
+import jakarta.persistence.*;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import lombok.ToString;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Entité représentant un avis (review) sur un établissement.
+ *
+ * Version 2.0 - Architecture refactorée.
+ * Conforme à l'architecture de données AfterWork v2.0 (Ultra-Compétitive).
+ *
+ * Cette entité remplace EstablishmentRating avec des fonctionnalités plus avancées,
+ * incluant des notes par critÚres et la vérification des visites.
+ */
+@Entity
+@Table(name = "reviews",
+ uniqueConstraints = @UniqueConstraint(columnNames = {"user_id", "establishment_id"}))
+@Getter
+@Setter
+@NoArgsConstructor
+@ToString
+public class Review extends BaseEntity {
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "user_id", nullable = false)
+ private Users user; // L'utilisateur qui a écrit l'avis
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "establishment_id", nullable = false)
+ private Establishment establishment; // L'établissement concerné
+
+ @Column(name = "overall_rating", nullable = false)
+ private Integer overallRating; // Note globale (1-5)
+
+ @Column(name = "comment", columnDefinition = "TEXT")
+ private String comment; // Commentaire libre de l'utilisateur
+
+ @Column(name = "criteria_ratings", nullable = false)
+ @org.hibernate.annotations.JdbcTypeCode(org.hibernate.type.SqlTypes.JSON)
+ private Map criteriaRatings = new HashMap<>(); // Notes par critĂšres (ex: {"ambiance": 4, "service": 5})
+
+ @Column(name = "is_verified_visit", nullable = false)
+ private Boolean isVerifiedVisit = false; // Indique si l'avis est lié à une visite vérifiée (réservation complétée)
+
+ /**
+ * Constructeur pour créer un avis.
+ *
+ * @param user L'utilisateur qui écrit l'avis
+ * @param establishment L'établissement concerné
+ * @param overallRating La note globale (1-5)
+ * @param comment Le commentaire (optionnel)
+ */
+ public Review(Users user, Establishment establishment, Integer overallRating, String comment) {
+ this.user = user;
+ this.establishment = establishment;
+ this.overallRating = overallRating;
+ this.comment = comment;
+ this.criteriaRatings = new HashMap<>();
+ this.isVerifiedVisit = false;
+ }
+
+ /**
+ * Ajoute une note pour un critÚre spécifique.
+ *
+ * @param criteria Le nom du critÚre (ex: "ambiance", "service", "qualité")
+ * @param rating La note (1-5)
+ */
+ public void addCriteriaRating(String criteria, Integer rating) {
+ if (rating >= 1 && rating <= 5) {
+ this.criteriaRatings.put(criteria, rating);
+ }
+ }
+
+ /**
+ * Retourne la note pour un critÚre spécifique.
+ *
+ * @param criteria Le nom du critĂšre
+ * @return La note, ou null si le critĂšre n'existe pas
+ */
+ public Integer getCriteriaRating(String criteria) {
+ return this.criteriaRatings.get(criteria);
+ }
+}
+
diff --git a/src/main/java/com/lions/dev/entity/events/Events.java b/src/main/java/com/lions/dev/entity/events/Events.java
index fa7d719..bbd37a6 100644
--- a/src/main/java/com/lions/dev/entity/events/Events.java
+++ b/src/main/java/com/lions/dev/entity/events/Events.java
@@ -16,8 +16,12 @@ import java.util.Set;
/**
* Entité représentant un événement dans le systÚme AfterWork.
+ *
+ * Version 2.0 - Architecture refactorée avec nommage standardisé.
+ * Conforme à l'architecture de données AfterWork v2.0 (Ultra-Compétitive).
+ *
* Chaque événement possÚde un titre, une description, une date de début, une date de fin,
- * un créateur, une catégorie, un lieu, une URL d'image, et des participants.
+ * un créateur, une catégorie, un établissement, une URL d'image, et des participants.
* Tous les logs et commentaires nécessaires pour la traçabilité et la documentation sont inclus.
*/
@Entity
@@ -40,8 +44,9 @@ public class Events extends BaseEntity {
@Column(name = "end_date", nullable = false)
private LocalDateTime endDate; // La date de fin de l'événement
- @Column(name = "location")
- private String location; // Le lieu de l'événement
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "establishment_id")
+ private com.lions.dev.entity.establishment.Establishment establishment; // L'Ă©tablissement oĂč se dĂ©roule l'Ă©vĂ©nement (v2.0)
@Column(name = "category")
private String category; // La catégorie de l'événement
@@ -53,7 +58,7 @@ public class Events extends BaseEntity {
private String imageUrl; // URL d'une image associée à l'événement
@Column(name = "status", nullable = false)
- private String status = "ouvert"; // Le statut de l'événement (en cours, terminé, annulé, etc.)
+ private String status = "OPEN"; // Le statut de l'événement (OPEN, CLOSED, CANCELLED, COMPLETED) (v2.0)
@Column(name = "max_participants")
private Integer maxParticipants; // Nombre maximum de participants autorisés
@@ -67,6 +72,12 @@ public class Events extends BaseEntity {
@Column(name = "participation_fee")
private Integer participationFee; // Frais de participation en centimes
+ @Column(name = "is_private", nullable = false)
+ private Boolean isPrivate = false; // Indique si l'événement est privé (v2.0)
+
+ @Column(name = "waitlist_enabled", nullable = false)
+ private Boolean waitlistEnabled = false; // Indique si la liste d'attente est activée (v2.0)
+
@Column(name = "privacy_rules", length = 1000)
private String privacyRules; // RÚgles de confidentialité de l'événement
@@ -131,13 +142,44 @@ public class Events extends BaseEntity {
}
/**
- * Ferme l'événement en changeant son statut.
+ * Ferme l'événement en changeant son statut (v2.0).
*/
public void setClosed(boolean closed) {
- this.status = closed ? "fermé" : "ouvert";
+ this.status = closed ? "CLOSED" : "OPEN";
System.out.println("[LOG] Statut de l'événement mis à jour : " + this.title + " - " + this.status);
}
+ /**
+ * Vérifie si l'événement est ouvert (v2.0).
+ *
+ * @return true si l'événement est ouvert, false sinon.
+ */
+ public boolean isOpen() {
+ return "OPEN".equalsIgnoreCase(this.status);
+ }
+
+ /**
+ * Vérifie si l'événement est fermé (v2.0).
+ *
+ * @return true si l'événement est fermé, false sinon.
+ */
+ public boolean isClosed() {
+ return "CLOSED".equalsIgnoreCase(this.status);
+ }
+
+ /**
+ * Retourne le lieu de l'événement depuis l'établissement associé (v2.0).
+ * Méthode de compatibilité pour remplacer l'ancien champ location.
+ *
+ * @return L'adresse de l'établissement, ou null si aucun établissement n'est associé.
+ */
+ public String getLocation() {
+ if (establishment != null) {
+ return establishment.getAddress() + ", " + establishment.getCity();
+ }
+ return null;
+ }
+
@OneToMany(fetch = FetchType.LAZY, mappedBy = "event")
private List comments; // Liste des commentaires associés à l'événement
diff --git a/src/main/java/com/lions/dev/entity/promotion/Promotion.java b/src/main/java/com/lions/dev/entity/promotion/Promotion.java
new file mode 100644
index 0000000..d7168f9
--- /dev/null
+++ b/src/main/java/com/lions/dev/entity/promotion/Promotion.java
@@ -0,0 +1,119 @@
+package com.lions.dev.entity.promotion;
+
+import com.lions.dev.entity.BaseEntity;
+import com.lions.dev.entity.establishment.Establishment;
+import jakarta.persistence.*;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import lombok.ToString;
+
+import java.time.LocalDateTime;
+
+/**
+ * Entité représentant une promotion ou une offre spéciale d'un établissement.
+ *
+ * Version 2.0 - Architecture refactorée.
+ * Conforme à l'architecture de données AfterWork v2.0 (Ultra-Compétitive).
+ *
+ * Cette entité permet de gérer les promotions, happy hours, et offres spéciales
+ * avec différents types de réductions et codes promo.
+ */
+@Entity
+@Table(name = "promotions")
+@Getter
+@Setter
+@NoArgsConstructor
+@ToString
+public class Promotion extends BaseEntity {
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "establishment_id", nullable = false)
+ private Establishment establishment; // L'établissement qui propose la promotion
+
+ @Column(name = "title", nullable = false, length = 200)
+ private String title; // Titre de la promotion (ex: "Happy Hour", "Menu du jour")
+
+ @Column(name = "description", columnDefinition = "TEXT")
+ private String description; // Description détaillée de la promotion
+
+ @Column(name = "promo_code", length = 50, unique = true)
+ private String promoCode; // Code promo optionnel (ex: "HAPPY2024")
+
+ @Column(name = "discount_type", nullable = false, length = 20)
+ private String discountType; // Type de réduction: PERCENTAGE, FIXED_AMOUNT, FREE_ITEM
+
+ @Column(name = "discount_value", nullable = false)
+ private java.math.BigDecimal discountValue; // Valeur de la réduction (pourcentage, montant fixe, etc.)
+
+ @Column(name = "valid_from", nullable = false)
+ private LocalDateTime validFrom; // Date de début de validité
+
+ @Column(name = "valid_until", nullable = false)
+ private LocalDateTime validUntil; // Date de fin de validité
+
+ @Column(name = "is_active", nullable = false)
+ private Boolean isActive = true; // Indique si la promotion est active
+
+ /**
+ * Constructeur pour créer une promotion.
+ *
+ * @param establishment L'établissement qui propose la promotion
+ * @param title Le titre de la promotion
+ * @param description La description
+ * @param discountType Le type de réduction
+ * @param discountValue La valeur de la réduction
+ * @param validFrom La date de début
+ * @param validUntil La date de fin
+ */
+ public Promotion(Establishment establishment, String title, String description,
+ String discountType, java.math.BigDecimal discountValue,
+ LocalDateTime validFrom, LocalDateTime validUntil) {
+ this.establishment = establishment;
+ this.title = title;
+ this.description = description;
+ this.discountType = discountType;
+ this.discountValue = discountValue;
+ this.validFrom = validFrom;
+ this.validUntil = validUntil;
+ this.isActive = true;
+ }
+
+ /**
+ * Vérifie si la promotion est actuellement valide.
+ *
+ * @return true si la promotion est active et dans sa période de validité, false sinon.
+ */
+ public boolean isValid() {
+ LocalDateTime now = LocalDateTime.now();
+ return isActive && now.isAfter(validFrom) && now.isBefore(validUntil);
+ }
+
+ /**
+ * Vérifie si la promotion est expirée.
+ *
+ * @return true si la promotion est expirée, false sinon.
+ */
+ public boolean isExpired() {
+ return LocalDateTime.now().isAfter(validUntil);
+ }
+
+ /**
+ * Vérifie si la promotion est une réduction en pourcentage.
+ *
+ * @return true si c'est un pourcentage, false sinon.
+ */
+ public boolean isPercentage() {
+ return "PERCENTAGE".equalsIgnoreCase(this.discountType);
+ }
+
+ /**
+ * Vérifie si la promotion est un montant fixe.
+ *
+ * @return true si c'est un montant fixe, false sinon.
+ */
+ public boolean isFixedAmount() {
+ return "FIXED_AMOUNT".equalsIgnoreCase(this.discountType);
+ }
+}
+
diff --git a/src/main/java/com/lions/dev/entity/reaction/Reaction.java b/src/main/java/com/lions/dev/entity/reaction/Reaction.java
index 92176f2..0621186 100644
--- a/src/main/java/com/lions/dev/entity/reaction/Reaction.java
+++ b/src/main/java/com/lions/dev/entity/reaction/Reaction.java
@@ -2,7 +2,6 @@ package com.lions.dev.entity.reaction;
import com.lions.dev.entity.BaseEntity;
import com.lions.dev.entity.users.Users;
-import com.lions.dev.entity.events.Events;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
@@ -10,58 +9,66 @@ import lombok.Setter;
import lombok.ToString;
/**
- * Entité représentant une réaction d'un utilisateur à un événement dans le systÚme AfterWork.
- * Une rĂ©action peut ĂȘtre un "like", un "dislike" ou toute autre forme de rĂ©action dĂ©finie.
- *
- * Cette entité est liée à l'utilisateur qui réagit et à l'événement auquel la réaction est associée.
- * Tous les logs et commentaires sont inclus pour la traçabilité.
+ * Entité représentant une réaction d'un utilisateur à un contenu.
+ *
+ * Version 2.0 - Architecture refactorée.
+ * Conforme à l'architecture de données AfterWork v2.0 (Ultra-Compétitive).
+ *
+ * Cette entité remplace les compteurs de réactions simples avec un systÚme
+ * plus flexible permettant différents types de réactions sur différents contenus.
*/
@Entity
-@Table(name = "reactions")
+@Table(name = "reactions",
+ uniqueConstraints = @UniqueConstraint(columnNames = {"user_id", "target_type", "target_id"}))
@Getter
@Setter
@NoArgsConstructor
@ToString
public class Reaction extends BaseEntity {
- // Types de réaction possibles
- public enum ReactionType {
- LIKE, DISLIKE, LOVE, ANGRY, SAD
- }
-
- @Enumerated(EnumType.STRING)
- @Column(name = "reaction_type", nullable = false)
- private ReactionType reactionType; // Le type de réaction (LIKE, DISLIKE, etc.)
-
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
- private Users user; // L'utilisateur qui a effectué la réaction
+ private Users user; // L'utilisateur qui a réagi
- @ManyToOne(fetch = FetchType.LAZY)
- @JoinColumn(name = "event_id", nullable = false)
- private Events event; // L'événement auquel la réaction est associée
+ @Column(name = "reaction_type", nullable = false, length = 20)
+ private String reactionType; // Type de réaction: LIKE, LOVE, HAHA, WOW, SAD, ANGRY
+
+ @Column(name = "target_type", nullable = false, length = 20)
+ private String targetType; // Type de contenu ciblé: POST, EVENT, COMMENT, REVIEW
+
+ @Column(name = "target_id", nullable = false)
+ private java.util.UUID targetId; // ID du contenu ciblé (post_id, event_id, comment_id, review_id)
/**
- * Associe une réaction à un utilisateur et un événement.
+ * Constructeur pour créer une réaction.
*
- * @param user L'utilisateur qui réagit.
- * @param event L'événement auquel la réaction est liée.
- * @param reactionType Le type de réaction.
+ * @param user L'utilisateur qui réagit
+ * @param reactionType Le type de réaction (LIKE, LOVE, HAHA, WOW, SAD, ANGRY)
+ * @param targetType Le type de contenu (POST, EVENT, COMMENT, REVIEW)
+ * @param targetId L'ID du contenu ciblé
*/
- public Reaction(Users user, Events event, ReactionType reactionType) {
+ public Reaction(Users user, String reactionType, String targetType, java.util.UUID targetId) {
this.user = user;
- this.event = event;
this.reactionType = reactionType;
- System.out.println("[LOG] Nouvelle réaction ajoutée : " + reactionType + " par l'utilisateur : " + user.getEmail() + " à l'événement : " + event.getTitle());
+ this.targetType = targetType;
+ this.targetId = targetId;
}
/**
- * Modifie le type de réaction de l'utilisateur pour cet événement.
+ * Vérifie si la réaction est un "like".
*
- * @param newReactionType Le nouveau type de réaction.
+ * @return true si c'est un like, false sinon.
*/
- public void updateReaction(ReactionType newReactionType) {
- System.out.println("[LOG] Changement de la réaction de " + this.reactionType + " à " + newReactionType + " pour l'utilisateur : " + user.getEmail() + " à l'événement : " + event.getTitle());
- this.reactionType = newReactionType;
+ public boolean isLike() {
+ return "LIKE".equalsIgnoreCase(this.reactionType);
+ }
+
+ /**
+ * Vérifie si la réaction est un "love".
+ *
+ * @return true si c'est un love, false sinon.
+ */
+ public boolean isLove() {
+ return "LOVE".equalsIgnoreCase(this.reactionType);
}
}
diff --git a/src/main/java/com/lions/dev/repository/EstablishmentRatingRepository.java b/src/main/java/com/lions/dev/repository/EstablishmentRatingRepository.java
index ed096ef..0034c24 100644
--- a/src/main/java/com/lions/dev/repository/EstablishmentRatingRepository.java
+++ b/src/main/java/com/lions/dev/repository/EstablishmentRatingRepository.java
@@ -17,13 +17,25 @@ public class EstablishmentRatingRepository implements PanacheRepositoryBase results = getEntityManager()
+ .createQuery(
+ "SELECT r FROM EstablishmentRating r " +
+ "JOIN FETCH r.establishment " +
+ "JOIN FETCH r.user " +
+ "WHERE r.establishment.id = :establishmentId AND r.user.id = :userId",
+ EstablishmentRating.class
+ )
+ .setParameter("establishmentId", establishmentId)
+ .setParameter("userId", userId)
+ .getResultList();
+ return results.isEmpty() ? null : results.get(0);
}
/**
diff --git a/src/main/java/com/lions/dev/repository/StoryRepository.java b/src/main/java/com/lions/dev/repository/StoryRepository.java
index af5a397..5691074 100644
--- a/src/main/java/com/lions/dev/repository/StoryRepository.java
+++ b/src/main/java/com/lions/dev/repository/StoryRepository.java
@@ -25,10 +25,23 @@ public class StoryRepository implements PanacheRepositoryBase {
* @return Liste des stories actives
*/
public List findAllActive() {
- System.out.println("[LOG] Récupération de toutes les stories actives");
+ return findAllActive(0, Integer.MAX_VALUE);
+ }
+
+ /**
+ * RécupÚre toutes les stories actives (non expirées) avec pagination, triées par date de création décroissante.
+ *
+ * @param page Le numéro de la page (0-indexé)
+ * @param size La taille de la page
+ * @return Liste paginée des stories actives
+ */
+ public List findAllActive(int page, int size) {
+ System.out.println("[LOG] Récupération de toutes les stories actives (page: " + page + ", size: " + size + ")");
List stories = find("isActive = true AND expiresAt > ?1",
Sort.by("createdAt", Sort.Direction.Descending),
- LocalDateTime.now()).list();
+ LocalDateTime.now())
+ .page(page, size)
+ .list();
System.out.println("[LOG] " + stories.size() + " story(ies) active(s) récupérée(s)");
return stories;
}
@@ -40,11 +53,25 @@ public class StoryRepository implements PanacheRepositoryBase {
* @return Liste des stories actives de l'utilisateur
*/
public List findActiveByUserId(UUID userId) {
- System.out.println("[LOG] Récupération des stories actives pour l'utilisateur ID : " + userId);
+ return findActiveByUserId(userId, 0, Integer.MAX_VALUE);
+ }
+
+ /**
+ * RécupÚre toutes les stories actives d'un utilisateur avec pagination.
+ *
+ * @param userId L'ID de l'utilisateur
+ * @param page Le numéro de la page (0-indexé)
+ * @param size La taille de la page
+ * @return Liste paginée des stories actives de l'utilisateur
+ */
+ public List findActiveByUserId(UUID userId, int page, int size) {
+ System.out.println("[LOG] Récupération des stories actives pour l'utilisateur ID : " + userId + " (page: " + page + ", size: " + size + ")");
List stories = find("user.id = ?1 AND isActive = true AND expiresAt > ?2",
Sort.by("createdAt", Sort.Direction.Descending),
userId,
- LocalDateTime.now()).list();
+ LocalDateTime.now())
+ .page(page, size)
+ .list();
System.out.println("[LOG] " + stories.size() + " story(ies) active(s) trouvée(s) pour l'utilisateur ID : " + userId);
return stories;
}
diff --git a/src/main/java/com/lions/dev/resource/EstablishmentMediaResource.java b/src/main/java/com/lions/dev/resource/EstablishmentMediaResource.java
index f1bf04b..6edc7ea 100644
--- a/src/main/java/com/lions/dev/resource/EstablishmentMediaResource.java
+++ b/src/main/java/com/lions/dev/resource/EstablishmentMediaResource.java
@@ -1,11 +1,13 @@
package com.lions.dev.resource;
+import com.lions.dev.dto.request.establishment.EstablishmentMediaRequestDTO;
import com.lions.dev.dto.response.establishment.EstablishmentMediaResponseDTO;
import com.lions.dev.entity.establishment.EstablishmentMedia;
import com.lions.dev.entity.establishment.MediaType;
import com.lions.dev.service.EstablishmentMediaService;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
+import jakarta.validation.Valid;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.openapi.annotations.Operation;
@@ -62,6 +64,7 @@ public class EstablishmentMediaResource {
/**
* Upload un nouveau média pour un établissement.
+ * Accepte un body JSON avec les informations du média.
*/
@POST
@Transactional
@@ -69,36 +72,56 @@ public class EstablishmentMediaResource {
description = "Upload un nouveau média (photo ou vidéo) pour un établissement")
public Response uploadMedia(
@PathParam("establishmentId") String establishmentId,
- @QueryParam("mediaUrl") String mediaUrl,
- @QueryParam("mediaType") String mediaTypeStr,
- @QueryParam("uploadedByUserId") String uploadedByUserIdStr,
- @QueryParam("thumbnailUrl") String thumbnailUrl) {
+ @Valid EstablishmentMediaRequestDTO requestDTO,
+ @QueryParam("uploadedByUserId") String uploadedByUserIdStr) {
LOG.info("Upload d'un média pour l'établissement : " + establishmentId);
try {
UUID id = UUID.fromString(establishmentId);
- UUID uploadedByUserId = UUID.fromString(uploadedByUserIdStr);
+
+ // Utiliser uploadedByUserId du query param ou du DTO
+ String userIdStr = uploadedByUserIdStr != null && !uploadedByUserIdStr.isEmpty()
+ ? uploadedByUserIdStr
+ : requestDTO.getUploadedByUserId();
+
+ if (userIdStr == null || userIdStr.isEmpty()) {
+ LOG.error("uploadedByUserId est obligatoire");
+ return Response.status(Response.Status.BAD_REQUEST)
+ .entity("L'ID de l'utilisateur (uploadedByUserId) est obligatoire")
+ .build();
+ }
+
+ UUID uploadedByUserId = UUID.fromString(userIdStr);
// Valider le type de média
MediaType mediaType;
try {
- mediaType = MediaType.valueOf(mediaTypeStr.toUpperCase());
+ mediaType = MediaType.valueOf(requestDTO.getMediaType().toUpperCase());
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Type de média invalide. Utilisez PHOTO ou VIDEO")
.build();
}
- EstablishmentMedia media = mediaService.uploadMedia(id, mediaUrl, mediaType, uploadedByUserId, thumbnailUrl);
+ // Appeler le service avec displayOrder
+ EstablishmentMedia media = mediaService.uploadMedia(
+ id,
+ requestDTO.getMediaUrl(),
+ mediaType,
+ uploadedByUserId,
+ requestDTO.getThumbnailUrl(),
+ requestDTO.getDisplayOrder() != null ? requestDTO.getDisplayOrder() : 0
+ );
+
EstablishmentMediaResponseDTO responseDTO = new EstablishmentMediaResponseDTO(media);
return Response.status(Response.Status.CREATED).entity(responseDTO).build();
} catch (IllegalArgumentException e) {
- LOG.error("ParamĂštres invalides : " + e.getMessage());
+ LOG.error("ParamĂštres invalides : " + e.getMessage(), e);
return Response.status(Response.Status.BAD_REQUEST)
.entity("ParamĂštres invalides : " + e.getMessage())
.build();
} catch (RuntimeException e) {
- LOG.error("Erreur lors de l'upload du média : " + e.getMessage());
+ LOG.error("Erreur lors de l'upload du média : " + e.getMessage(), e);
return Response.status(Response.Status.BAD_REQUEST)
.entity(e.getMessage())
.build();
diff --git a/src/main/java/com/lions/dev/resource/EstablishmentRatingResource.java b/src/main/java/com/lions/dev/resource/EstablishmentRatingResource.java
index 9a1b5a0..b4d9677 100644
--- a/src/main/java/com/lions/dev/resource/EstablishmentRatingResource.java
+++ b/src/main/java/com/lions/dev/resource/EstablishmentRatingResource.java
@@ -109,46 +109,9 @@ public class EstablishmentRatingResource {
}
}
- /**
- * RécupÚre la note d'un utilisateur pour un établissement.
- */
- @GET
- @Path("/users/{userId}")
- @Operation(summary = "Récupérer la note d'un utilisateur",
- description = "RécupÚre la note donnée par un utilisateur spécifique pour un établissement")
- public Response getUserRating(
- @PathParam("establishmentId") String establishmentId,
- @PathParam("userId") String userIdStr) {
- LOG.info("Récupération de la note de l'utilisateur " + userIdStr + " pour l'établissement " + establishmentId);
-
- try {
- UUID id = UUID.fromString(establishmentId);
- UUID userId = UUID.fromString(userIdStr);
-
- EstablishmentRating rating = ratingService.getUserRating(id, userId);
- if (rating == null) {
- return Response.status(Response.Status.NOT_FOUND)
- .entity("Note non trouvée")
- .build();
- }
-
- EstablishmentRatingResponseDTO responseDTO = new EstablishmentRatingResponseDTO(rating);
- return Response.ok(responseDTO).build();
- } catch (IllegalArgumentException e) {
- LOG.error("ID invalide : " + e.getMessage());
- return Response.status(Response.Status.BAD_REQUEST)
- .entity("ID invalide : " + e.getMessage())
- .build();
- } catch (Exception e) {
- LOG.error("Erreur inattendue lors de la récupération de la note", e);
- return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
- .entity("Erreur lors de la récupération de la note")
- .build();
- }
- }
-
/**
* RécupÚre les statistiques de notation d'un établissement.
+ * Doit ĂȘtre dĂ©clarĂ© avant les endpoints gĂ©nĂ©riques GET pour la rĂ©solution correcte par JAX-RS.
*/
@GET
@Path("/stats")
@@ -179,5 +142,92 @@ public class EstablishmentRatingResource {
.build();
}
}
+
+ /**
+ * RécupÚre la note d'un utilisateur pour un établissement (via path parameter).
+ * Endpoint alternatif pour compatibilité.
+ */
+ @GET
+ @Path("/users/{userId}")
+ @Operation(summary = "Récupérer la note d'un utilisateur (path parameter)",
+ description = "RécupÚre la note donnée par un utilisateur spécifique pour un établissement (via path parameter)")
+ public Response getUserRatingByPath(
+ @PathParam("establishmentId") String establishmentId,
+ @PathParam("userId") String userIdStr) {
+ LOG.info("Récupération de la note de l'utilisateur " + userIdStr + " pour l'établissement " + establishmentId);
+
+ try {
+ UUID id = UUID.fromString(establishmentId);
+ UUID userId = UUID.fromString(userIdStr);
+
+ EstablishmentRating rating = ratingService.getUserRating(id, userId);
+ if (rating == null) {
+ return Response.status(Response.Status.NOT_FOUND)
+ .entity("Note non trouvée")
+ .build();
+ }
+
+ EstablishmentRatingResponseDTO responseDTO = new EstablishmentRatingResponseDTO(rating);
+ return Response.ok(responseDTO).build();
+ } catch (IllegalArgumentException e) {
+ LOG.error("ID invalide : " + e.getMessage(), e);
+ return Response.status(Response.Status.BAD_REQUEST)
+ .entity("ID invalide : " + e.getMessage())
+ .build();
+ } catch (Exception e) {
+ LOG.error("Erreur inattendue lors de la récupération de la note", e);
+ return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
+ .entity("Erreur lors de la récupération de la note")
+ .build();
+ }
+ }
+
+ /**
+ * RécupÚre la note d'un utilisateur pour un établissement (via query parameter).
+ * Endpoint utilisé par le frontend Flutter.
+ * Doit ĂȘtre dĂ©clarĂ© en dernier car c'est l'endpoint le plus gĂ©nĂ©rique.
+ */
+ @GET
+ @Operation(summary = "Récupérer la note d'un utilisateur",
+ description = "RécupÚre la note donnée par un utilisateur spécifique pour un établissement (via query parameter userId)")
+ public Response getUserRatingByQuery(
+ @PathParam("establishmentId") String establishmentId,
+ @QueryParam("userId") String userIdStr) {
+ LOG.info("Récupération de la note de l'utilisateur " + userIdStr + " pour l'établissement " + establishmentId);
+
+ // Si userId n'est pas fourni, retourner une erreur
+ if (userIdStr == null || userIdStr.isEmpty()) {
+ LOG.warn("userId manquant dans la requĂȘte");
+ return Response.status(Response.Status.BAD_REQUEST)
+ .entity("Le paramĂštre userId est requis")
+ .build();
+ }
+
+ try {
+ UUID id = UUID.fromString(establishmentId);
+ UUID userId = UUID.fromString(userIdStr);
+
+ EstablishmentRating rating = ratingService.getUserRating(id, userId);
+ if (rating == null) {
+ // Retourner 404 si la note n'existe pas
+ return Response.status(Response.Status.NOT_FOUND)
+ .entity("Note non trouvée")
+ .build();
+ }
+
+ EstablishmentRatingResponseDTO responseDTO = new EstablishmentRatingResponseDTO(rating);
+ return Response.ok(responseDTO).build();
+ } catch (IllegalArgumentException e) {
+ LOG.error("ID invalide : " + e.getMessage(), e);
+ return Response.status(Response.Status.BAD_REQUEST)
+ .entity("ID invalide : " + e.getMessage())
+ .build();
+ } catch (Exception e) {
+ LOG.error("Erreur inattendue lors de la récupération de la note", e);
+ return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
+ .entity("Erreur lors de la récupération de la note")
+ .build();
+ }
+ }
}
diff --git a/src/main/java/com/lions/dev/resource/EstablishmentResource.java b/src/main/java/com/lions/dev/resource/EstablishmentResource.java
index dd1204d..f98333b 100644
--- a/src/main/java/com/lions/dev/resource/EstablishmentResource.java
+++ b/src/main/java/com/lions/dev/resource/EstablishmentResource.java
@@ -76,14 +76,8 @@ public class EstablishmentResource {
establishment.setPostalCode(requestDTO.getPostalCode());
establishment.setDescription(requestDTO.getDescription());
establishment.setPhoneNumber(requestDTO.getPhoneNumber());
- establishment.setEmail(requestDTO.getEmail());
establishment.setWebsite(requestDTO.getWebsite());
- establishment.setImageUrl(requestDTO.getImageUrl());
- establishment.setRating(requestDTO.getRating());
establishment.setPriceRange(requestDTO.getPriceRange());
- establishment.setCapacity(requestDTO.getCapacity());
- establishment.setAmenities(requestDTO.getAmenities());
- establishment.setOpeningHours(requestDTO.getOpeningHours());
establishment.setLatitude(requestDTO.getLatitude());
establishment.setLongitude(requestDTO.getLongitude());
@@ -245,14 +239,8 @@ public class EstablishmentResource {
establishment.setPostalCode(requestDTO.getPostalCode());
establishment.setDescription(requestDTO.getDescription());
establishment.setPhoneNumber(requestDTO.getPhoneNumber());
- establishment.setEmail(requestDTO.getEmail());
establishment.setWebsite(requestDTO.getWebsite());
- establishment.setImageUrl(requestDTO.getImageUrl());
- establishment.setRating(requestDTO.getRating());
establishment.setPriceRange(requestDTO.getPriceRange());
- establishment.setCapacity(requestDTO.getCapacity());
- establishment.setAmenities(requestDTO.getAmenities());
- establishment.setOpeningHours(requestDTO.getOpeningHours());
establishment.setLatitude(requestDTO.getLatitude());
establishment.setLongitude(requestDTO.getLongitude());
diff --git a/src/main/java/com/lions/dev/resource/EventsResource.java b/src/main/java/com/lions/dev/resource/EventsResource.java
index e0739ca..d2496a1 100644
--- a/src/main/java/com/lions/dev/resource/EventsResource.java
+++ b/src/main/java/com/lions/dev/resource/EventsResource.java
@@ -229,10 +229,12 @@ public class EventsResource {
@Path("/created-by-user-and-friends")
@Consumes("application/json")
@Produces("application/json")
- @Operation(summary = "Récupérer les événements créés par un utilisateur et ses amis", description = "Retourne la liste des événements créés par un utilisateur spécifique et ses amis")
+ @Operation(summary = "Récupérer les événements créés par un utilisateur et ses amis", description = "Retourne la liste paginée des événements créés par un utilisateur spécifique et ses amis")
public Response getEventsCreatedByUserAndFriends(EventReadManyByIdRequestDTO requestDTO) {
- UUID userId = requestDTO.getUserId();
- LOG.info("[LOG] Récupération des événements pour l'utilisateur avec l'ID : " + userId + " et ses amis");
+ UUID userId = requestDTO.getId();
+ int page = requestDTO.getPage() != null ? requestDTO.getPage() : 0;
+ int size = requestDTO.getSize() != null ? requestDTO.getSize() : 10;
+ LOG.info("[LOG] Récupération des événements pour l'utilisateur avec l'ID : " + userId + " et ses amis (page: " + page + ", size: " + size + ")");
Users user = usersRepository.findById(userId);
if (user == null) {
LOG.warn("[LOG] Utilisateur non trouvé avec l'ID : " + userId);
@@ -247,8 +249,10 @@ public class EventsResource {
LOG.info("[LOG] IDs d'amis + utilisateur (taille: " + friendIds.size() + ") : " + friendIds);
- // Rechercher les événements créés par l'utilisateur et ses amis
- List events = eventsRepository.find("creator.id IN ?1", friendIds).list();
+ // Rechercher les événements créés par l'utilisateur et ses amis avec pagination
+ List events = eventsRepository.find("creator.id IN ?1 ORDER BY startDate DESC", friendIds)
+ .page(page, size)
+ .list();
LOG.info("[LOG] Nombre d'Ă©vĂ©nements rĂ©cupĂ©rĂ©s dans la requĂȘte : " + events.size());
// â
Retourner avec reactionsCount et isFavorite pour l'utilisateur actuel
@@ -443,14 +447,17 @@ public class EventsResource {
@Operation(
summary = "Récupérer les événements auxquels un utilisateur est inscrit",
description = "Retourne la liste des événements auxquels un utilisateur spécifique est inscrit")
- public Response getEventsByUser(@PathParam("userId") UUID userId) {
- LOG.info("[LOG] Récupération des événements pour l'utilisateur avec l'ID : " + userId);
+ public Response getEventsByUser(
+ @PathParam("userId") UUID userId,
+ @QueryParam("page") @DefaultValue("0") int page,
+ @QueryParam("size") @DefaultValue("10") int size) {
+ LOG.info("[LOG] Récupération des événements pour l'utilisateur avec l'ID : " + userId + " (page: " + page + ", size: " + size + ")");
Users user = usersRepository.findById(userId);
if (user == null) {
LOG.warn("[LOG] Utilisateur non trouvé avec l'ID : " + userId);
return Response.status(Response.Status.NOT_FOUND).entity("Utilisateur non trouvé.").build();
}
- List events = eventService.findEventsByUser(user);
+ List events = eventService.findEventsByUser(user, page, size);
if (events.isEmpty()) {
LOG.warn("[LOG] Aucun événement trouvé pour l'utilisateur avec l'ID : " + userId);
return Response.status(Response.Status.NOT_FOUND).entity("Aucun événement trouvé.").build();
@@ -1011,7 +1018,7 @@ public class EventsResource {
public Response getEventsByFriends(
@PathParam("userId") UUID userId,
@QueryParam("page") @DefaultValue("0") int page,
- @QueryParam("size") @DefaultValue("20") int size) {
+ @QueryParam("size") @DefaultValue("10") int size) {
LOG.info("[LOG] Récupération des événements des amis pour l'utilisateur ID : " + userId);
try {
diff --git a/src/main/java/com/lions/dev/resource/FileUploadResource.java b/src/main/java/com/lions/dev/resource/FileUploadResource.java
index 6f3c8d8..6f58233 100644
--- a/src/main/java/com/lions/dev/resource/FileUploadResource.java
+++ b/src/main/java/com/lions/dev/resource/FileUploadResource.java
@@ -91,6 +91,7 @@ public class FileUploadResource {
// Construire la réponse JSON
Map response = new HashMap<>();
response.put("url", fileUrl);
+ response.put("fileName", finalFileName); // â
Ajout du fileName dans la réponse (DRY/WOU)
if (thumbnailUrl != null) {
response.put("thumbnailUrl", thumbnailUrl);
}
@@ -105,7 +106,7 @@ public class FileUploadResource {
}
}
- LOG.infof("Upload réussi, URL: %s", fileUrl);
+ LOG.infof("Upload réussi, URL: %s, FileName: %s", fileUrl, finalFileName);
return Response.status(Response.Status.CREATED)
.entity(response)
diff --git a/src/main/java/com/lions/dev/resource/NotificationResource.java b/src/main/java/com/lions/dev/resource/NotificationResource.java
index afb7422..5f42e9f 100644
--- a/src/main/java/com/lions/dev/resource/NotificationResource.java
+++ b/src/main/java/com/lions/dev/resource/NotificationResource.java
@@ -80,7 +80,7 @@ public class NotificationResource {
public Response getNotificationsByUserIdWithPagination(
@PathParam("userId") UUID userId,
@QueryParam("page") @DefaultValue("0") int page,
- @QueryParam("size") @DefaultValue("20") int size) {
+ @QueryParam("size") @DefaultValue("10") int size) {
LOG.info("[LOG] Récupération paginée des notifications pour l'utilisateur ID : " + userId);
try {
diff --git a/src/main/java/com/lions/dev/resource/SocialPostResource.java b/src/main/java/com/lions/dev/resource/SocialPostResource.java
index 8c719a9..63f6ea1 100644
--- a/src/main/java/com/lions/dev/resource/SocialPostResource.java
+++ b/src/main/java/com/lions/dev/resource/SocialPostResource.java
@@ -11,6 +11,7 @@ import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.util.List;
+import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
import org.eclipse.microprofile.openapi.annotations.Operation;
@@ -49,7 +50,7 @@ public class SocialPostResource {
description = "Retourne une liste paginée de tous les posts sociaux, triés par date de création décroissante")
public Response getAllPosts(
@QueryParam("page") @DefaultValue("0") int page,
- @QueryParam("size") @DefaultValue("20") int size) {
+ @QueryParam("size") @DefaultValue("10") int size) {
LOG.info("[LOG] Récupération de tous les posts (page: " + page + ", size: " + size + ")");
try {
@@ -110,12 +111,12 @@ public class SocialPostResource {
summary = "Créer un nouveau post",
description = "Crée un nouveau post social et retourne ses détails")
public Response createPost(@Valid SocialPostCreateRequestDTO requestDTO) {
- LOG.info("[LOG] Création d'un nouveau post par l'utilisateur ID : " + requestDTO.getUserId());
+ LOG.info("[LOG] Création d'un nouveau post par l'utilisateur ID : " + requestDTO.getCreatorId());
try {
SocialPost post = socialPostService.createPost(
requestDTO.getContent(),
- requestDTO.getUserId(),
+ requestDTO.getCreatorId(),
requestDTO.getImageUrl()
);
SocialPostResponseDTO responseDTO = new SocialPostResponseDTO(post);
@@ -244,11 +245,19 @@ public class SocialPostResource {
@Operation(
summary = "Liker un post",
description = "Incrémente le compteur de likes d'un post")
- public Response likePost(@PathParam("id") UUID postId) {
- LOG.info("[LOG] Like du post ID : " + postId);
+ public Response likePost(
+ @PathParam("id") UUID postId,
+ @QueryParam("userId") UUID userId) {
+ LOG.info("[LOG] Like du post ID : " + postId + " par utilisateur : " + userId);
+
+ if (userId == null) {
+ return Response.status(Response.Status.BAD_REQUEST)
+ .entity("{\"message\": \"userId est requis.\"}")
+ .build();
+ }
try {
- SocialPost post = socialPostService.likePost(postId);
+ SocialPost post = socialPostService.likePost(postId, userId);
SocialPostResponseDTO responseDTO = new SocialPostResponseDTO(post);
return Response.ok(responseDTO).build();
} catch (IllegalArgumentException e) {
@@ -273,14 +282,30 @@ public class SocialPostResource {
@POST
@Path("/{id}/comment")
@Transactional
+ @Consumes(MediaType.APPLICATION_JSON)
@Operation(
summary = "Commenter un post",
- description = "Incrémente le compteur de commentaires d'un post")
- public Response addComment(@PathParam("id") UUID postId) {
- LOG.info("[LOG] Ajout de commentaire au post ID : " + postId);
+ description = "Ajoute un commentaire Ă un post")
+ public Response addComment(
+ @PathParam("id") UUID postId,
+ @QueryParam("userId") UUID userId,
+ String requestBody) {
+ LOG.info("[LOG] Ajout de commentaire au post ID : " + postId + " par utilisateur : " + userId);
+
+ if (userId == null) {
+ return Response.status(Response.Status.BAD_REQUEST)
+ .entity("{\"message\": \"userId est requis.\"}")
+ .build();
+ }
try {
- SocialPost post = socialPostService.addComment(postId);
+ // Parser le body pour obtenir le contenu du commentaire
+ com.fasterxml.jackson.databind.ObjectMapper mapper =
+ new com.fasterxml.jackson.databind.ObjectMapper();
+ Map body = mapper.readValue(requestBody, Map.class);
+ String commentContent = (String) body.getOrDefault("content", "");
+
+ SocialPost post = socialPostService.addComment(postId, userId, commentContent);
SocialPostResponseDTO responseDTO = new SocialPostResponseDTO(post);
return Response.ok(responseDTO).build();
} catch (IllegalArgumentException e) {
@@ -308,11 +333,19 @@ public class SocialPostResource {
@Operation(
summary = "Partager un post",
description = "Incrémente le compteur de partages d'un post")
- public Response sharePost(@PathParam("id") UUID postId) {
- LOG.info("[LOG] Partage du post ID : " + postId);
+ public Response sharePost(
+ @PathParam("id") UUID postId,
+ @QueryParam("userId") UUID userId) {
+ LOG.info("[LOG] Partage du post ID : " + postId + " par utilisateur : " + userId);
+
+ if (userId == null) {
+ return Response.status(Response.Status.BAD_REQUEST)
+ .entity("{\"message\": \"userId est requis.\"}")
+ .build();
+ }
try {
- SocialPost post = socialPostService.sharePost(postId);
+ SocialPost post = socialPostService.sharePost(postId, userId);
SocialPostResponseDTO responseDTO = new SocialPostResponseDTO(post);
return Response.ok(responseDTO).build();
} catch (IllegalArgumentException e) {
@@ -373,7 +406,7 @@ public class SocialPostResource {
public Response getPostsByFriends(
@PathParam("userId") UUID userId,
@QueryParam("page") @DefaultValue("0") int page,
- @QueryParam("size") @DefaultValue("20") int size) {
+ @QueryParam("size") @DefaultValue("10") int size) {
LOG.info("[LOG] Récupération des posts des amis pour l'utilisateur ID : " + userId);
try {
diff --git a/src/main/java/com/lions/dev/resource/StoryResource.java b/src/main/java/com/lions/dev/resource/StoryResource.java
index 8750d34..de0d56d 100644
--- a/src/main/java/com/lions/dev/resource/StoryResource.java
+++ b/src/main/java/com/lions/dev/resource/StoryResource.java
@@ -45,12 +45,15 @@ public class StoryResource {
@GET
@Operation(
summary = "Récupérer toutes les stories actives",
- description = "Retourne une liste de toutes les stories actives (non expirées), triées par date de création décroissante")
- public Response getAllActiveStories(@QueryParam("viewerId") UUID viewerId) {
- LOG.info("[LOG] Récupération de toutes les stories actives");
+ description = "Retourne une liste paginée de toutes les stories actives (non expirées), triées par date de création décroissante")
+ public Response getAllActiveStories(
+ @QueryParam("viewerId") UUID viewerId,
+ @QueryParam("page") @DefaultValue("0") int page,
+ @QueryParam("size") @DefaultValue("10") int size) {
+ LOG.info("[LOG] Récupération de toutes les stories actives (page: " + page + ", size: " + size + ")");
try {
- List stories = storyService.getAllActiveStories();
+ List stories = storyService.getAllActiveStories(page, size);
List responseDTOs = stories.stream()
.map(story -> viewerId != null ? new StoryResponseDTO(story, viewerId) : new StoryResponseDTO(story))
.collect(Collectors.toList());
@@ -109,12 +112,16 @@ public class StoryResource {
@Path("/user/{userId}")
@Operation(
summary = "Récupérer les stories d'un utilisateur",
- description = "Retourne toutes les stories actives d'un utilisateur spécifique")
- public Response getStoriesByUserId(@PathParam("userId") UUID userId, @QueryParam("viewerId") UUID viewerId) {
- LOG.info("[LOG] Récupération des stories pour l'utilisateur ID : " + userId);
+ description = "Retourne une liste paginée des stories actives d'un utilisateur spécifique")
+ public Response getStoriesByUserId(
+ @PathParam("userId") UUID userId,
+ @QueryParam("viewerId") UUID viewerId,
+ @QueryParam("page") @DefaultValue("0") int page,
+ @QueryParam("size") @DefaultValue("10") int size) {
+ LOG.info("[LOG] Récupération des stories pour l'utilisateur ID : " + userId + " (page: " + page + ", size: " + size + ")");
try {
- List stories = storyService.getActiveStoriesByUserId(userId);
+ List stories = storyService.getActiveStoriesByUserId(userId, page, size);
List responseDTOs = stories.stream()
.map(story -> viewerId != null ? new StoryResponseDTO(story, viewerId) : new StoryResponseDTO(story))
.collect(Collectors.toList());
@@ -140,11 +147,11 @@ public class StoryResource {
summary = "Créer une nouvelle story",
description = "Crée une nouvelle story et retourne ses détails")
public Response createStory(@Valid StoryCreateRequestDTO requestDTO) {
- LOG.info("[LOG] Création d'une nouvelle story par l'utilisateur ID : " + requestDTO.getUserId());
+ LOG.info("[LOG] Création d'une nouvelle story par l'utilisateur ID : " + requestDTO.getCreatorId());
try {
Story story = storyService.createStory(
- requestDTO.getUserId(),
+ requestDTO.getCreatorId(),
requestDTO.getMediaType(),
requestDTO.getMediaUrl(),
requestDTO.getThumbnailUrl(),
diff --git a/src/main/java/com/lions/dev/resource/UsersResource.java b/src/main/java/com/lions/dev/resource/UsersResource.java
index ce567ad..877f66a 100644
--- a/src/main/java/com/lions/dev/resource/UsersResource.java
+++ b/src/main/java/com/lions/dev/resource/UsersResource.java
@@ -60,7 +60,7 @@ public class UsersResource {
}
/**
- * Endpoint pour authentifier un utilisateur.
+ * Endpoint pour authentifier un utilisateur (v2.0).
*
* @param userAuthenticateRequestDTO Le DTO contenant les informations d'authentification.
* @return Une réponse HTTP indiquant si l'authentification a réussi ou échoué.
@@ -73,10 +73,18 @@ public class UsersResource {
public Response authenticateUser(@Valid @NotNull UserAuthenticateRequestDTO userAuthenticateRequestDTO) {
LOG.info("Tentative d'authentification pour l'utilisateur avec l'email : " + userAuthenticateRequestDTO.getEmail());
- Users user = userService.authenticateUser(userAuthenticateRequestDTO.getEmail(), userAuthenticateRequestDTO.getMotDePasse());
+ // v2.0 - Utiliser getPassword() qui gÚre la compatibilité v1.0 et v2.0
+ Users user = userService.authenticateUser(userAuthenticateRequestDTO.getEmail(), userAuthenticateRequestDTO.getPassword());
LOG.info("Authentification réussie pour l'utilisateur : " + user.getEmail());
- UserAuthenticateResponseDTO responseDTO = new UserAuthenticateResponseDTO(user.getId(), user.getPrenoms(), user.getNom(), user.getEmail(), user.getRole());
+ // v2.0 - Utiliser les nouveaux noms de champs
+ UserAuthenticateResponseDTO responseDTO = new UserAuthenticateResponseDTO(
+ user.getId(),
+ user.getFirstName(), // v2.0
+ user.getLastName(), // v2.0
+ user.getEmail(),
+ user.getRole()
+ );
responseDTO.logResponseDetails();
return Response.ok(responseDTO).build();
}
diff --git a/src/main/java/com/lions/dev/service/EstablishmentMediaService.java b/src/main/java/com/lions/dev/service/EstablishmentMediaService.java
index e01995f..8baf0f3 100644
--- a/src/main/java/com/lions/dev/service/EstablishmentMediaService.java
+++ b/src/main/java/com/lions/dev/service/EstablishmentMediaService.java
@@ -52,10 +52,11 @@ public class EstablishmentMediaService {
* @param mediaType Le type de média (PHOTO ou VIDEO)
* @param uploadedByUserId L'ID de l'utilisateur qui upload
* @param thumbnailUrl L'URL de la miniature (optionnel, pour les vidéos)
+ * @param displayOrder L'ordre d'affichage (optionnel, par défaut calculé automatiquement)
* @return Le média créé
*/
@Transactional
- public EstablishmentMedia uploadMedia(UUID establishmentId, String mediaUrl, MediaType mediaType, UUID uploadedByUserId, String thumbnailUrl) {
+ public EstablishmentMedia uploadMedia(UUID establishmentId, String mediaUrl, MediaType mediaType, UUID uploadedByUserId, String thumbnailUrl, Integer displayOrder) {
LOG.info("Upload d'un média pour l'établissement : " + establishmentId);
// Vérifier que l'établissement existe
@@ -76,13 +77,18 @@ public class EstablishmentMediaService {
EstablishmentMedia media = new EstablishmentMedia(establishment, mediaUrl, mediaType, uploadedBy);
media.setThumbnailUrl(thumbnailUrl);
- // Déterminer l'ordre d'affichage (dernier média = ordre le plus élevé)
- List existingMedia = mediaRepository.findByEstablishmentId(establishmentId);
- int maxOrder = existingMedia.stream()
- .mapToInt(EstablishmentMedia::getDisplayOrder)
- .max()
- .orElse(-1);
- media.setDisplayOrder(maxOrder + 1);
+ // Utiliser le displayOrder fourni, ou calculer automatiquement si non fourni
+ if (displayOrder != null) {
+ media.setDisplayOrder(displayOrder);
+ } else {
+ // Déterminer l'ordre d'affichage (dernier média = ordre le plus élevé)
+ List existingMedia = mediaRepository.findByEstablishmentId(establishmentId);
+ int maxOrder = existingMedia.stream()
+ .mapToInt(EstablishmentMedia::getDisplayOrder)
+ .max()
+ .orElse(-1);
+ media.setDisplayOrder(maxOrder + 1);
+ }
mediaRepository.persist(media);
LOG.info("Média uploadé avec succÚs : " + media.getId());
diff --git a/src/main/java/com/lions/dev/service/EstablishmentRatingService.java b/src/main/java/com/lions/dev/service/EstablishmentRatingService.java
index b12120e..2c2d903 100644
--- a/src/main/java/com/lions/dev/service/EstablishmentRatingService.java
+++ b/src/main/java/com/lions/dev/service/EstablishmentRatingService.java
@@ -158,7 +158,8 @@ public class EstablishmentRatingService {
Long totalRatings = ratingRepository.countByEstablishmentId(establishmentId);
establishment.setAverageRating(averageRating);
- establishment.setTotalRatingsCount(totalRatings.intValue());
+ // v2.0 - Renommé depuis setTotalRatingsCount
+ establishment.setTotalReviewsCount(totalRatings.intValue());
establishmentRepository.persist(establishment);
LOG.info("Statistiques mises à jour pour l'établissement " + establishmentId + " : moyenne = " + averageRating + ", total = " + totalRatings);
diff --git a/src/main/java/com/lions/dev/service/EstablishmentService.java b/src/main/java/com/lions/dev/service/EstablishmentService.java
index 653acc6..5410674 100644
--- a/src/main/java/com/lions/dev/service/EstablishmentService.java
+++ b/src/main/java/com/lions/dev/service/EstablishmentService.java
@@ -63,12 +63,22 @@ public class EstablishmentService {
/**
* RécupÚre tous les établissements.
+ * Charge également les médias pour chaque établissement pour inclure l'image principale.
*
* @return Une liste de tous les établissements.
*/
public List getAllEstablishments() {
LOG.info("[LOG] Récupération de tous les établissements");
- List establishments = establishmentRepository.listAll();
+ // Utiliser une requĂȘte avec fetch join pour charger les mĂ©dias en une seule requĂȘte
+ List establishments = establishmentRepository.getEntityManager()
+ .createQuery(
+ "SELECT DISTINCT e FROM Establishment e " +
+ "LEFT JOIN FETCH e.medias m " +
+ "LEFT JOIN FETCH e.manager " +
+ "ORDER BY e.name ASC",
+ Establishment.class
+ )
+ .getResultList();
LOG.info("[LOG] Nombre d'établissements trouvés : " + establishments.size());
return establishments;
}
@@ -105,7 +115,7 @@ public class EstablishmentService {
throw new RuntimeException("Ătablissement non trouvĂ© avec l'ID : " + id);
}
- // Mettre Ă jour les champs
+ // v2.0 - Mettre Ă jour les champs
establishment.setName(updatedEstablishment.getName());
establishment.setType(updatedEstablishment.getType());
establishment.setAddress(updatedEstablishment.getAddress());
@@ -113,16 +123,19 @@ public class EstablishmentService {
establishment.setPostalCode(updatedEstablishment.getPostalCode());
establishment.setDescription(updatedEstablishment.getDescription());
establishment.setPhoneNumber(updatedEstablishment.getPhoneNumber());
- establishment.setEmail(updatedEstablishment.getEmail());
establishment.setWebsite(updatedEstablishment.getWebsite());
- establishment.setImageUrl(updatedEstablishment.getImageUrl());
- establishment.setRating(updatedEstablishment.getRating());
establishment.setPriceRange(updatedEstablishment.getPriceRange());
- establishment.setCapacity(updatedEstablishment.getCapacity());
- establishment.setAmenities(updatedEstablishment.getAmenities());
- establishment.setOpeningHours(updatedEstablishment.getOpeningHours());
+
+ // v2.0 - Nouveau champ
+ if (updatedEstablishment.getVerificationStatus() != null) {
+ establishment.setVerificationStatus(updatedEstablishment.getVerificationStatus());
+ }
+
establishment.setLatitude(updatedEstablishment.getLatitude());
establishment.setLongitude(updatedEstablishment.getLongitude());
+
+ // v2.0 - Champs supprimés (email, imageUrl, rating, capacity, amenities, openingHours)
+ // Ces champs ne sont plus mis à jour car ils sont dépréciés
establishmentRepository.persist(establishment);
LOG.info("[LOG] Ătablissement mis Ă jour avec succĂšs : " + establishment.getName());
diff --git a/src/main/java/com/lions/dev/service/EventService.java b/src/main/java/com/lions/dev/service/EventService.java
index 4b8cb9b..0c11e43 100644
--- a/src/main/java/com/lions/dev/service/EventService.java
+++ b/src/main/java/com/lions/dev/service/EventService.java
@@ -12,6 +12,10 @@ import com.lions.dev.exception.UserNotFoundException;
import com.lions.dev.repository.EventsRepository;
import com.lions.dev.repository.FriendshipRepository;
import com.lions.dev.repository.UsersRepository;
+import com.lions.dev.repository.EstablishmentRepository;
+import com.lions.dev.dto.events.NotificationEvent;
+import org.eclipse.microprofile.reactive.messaging.Channel;
+import org.eclipse.microprofile.reactive.messaging.Emitter;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.time.LocalDateTime;
@@ -21,6 +25,10 @@ import java.util.stream.Collectors;
/**
* Service de gestion des événements.
+ *
+ * Version 2.0 - Architecture refactorée avec nommage standardisé.
+ * Conforme à l'architecture de données AfterWork v2.0 (Ultra-Compétitive).
+ *
* Ce service contient la logique métier pour la création, récupération, mise à jour et suppression des événements.
* Chaque méthode est loguée pour assurer une traçabilité exhaustive des actions effectuées.
*/
@@ -36,13 +44,20 @@ public class EventService {
@Inject
UsersRepository usersRepository;
+ @Inject
+ EstablishmentRepository establishmentRepository; // v2.0
+
@Inject
NotificationService notificationService;
+ @Inject
+ @Channel("notifications")
+ Emitter notificationEmitter; // v2.0 - Publie dans Kafka
+
private static final Logger logger = LoggerFactory.getLogger(EventService.class);
/**
- * Crée un nouvel événement dans le systÚme.
+ * Crée un nouvel événement dans le systÚme (v2.0).
*
* @param eventCreateRequestDTO Le DTO contenant les informations de l'événement à créer.
* @param creator L'utilisateur créateur de l'événement.
@@ -55,7 +70,18 @@ public class EventService {
event.setDescription(eventCreateRequestDTO.getDescription());
event.setStartDate(eventCreateRequestDTO.getStartDate());
event.setEndDate(eventCreateRequestDTO.getEndDate());
- event.setLocation(eventCreateRequestDTO.getLocation());
+
+ // v2.0 - Ătablissement au lieu de location
+ if (eventCreateRequestDTO.getEstablishmentId() != null) {
+ com.lions.dev.entity.establishment.Establishment establishment =
+ establishmentRepository.findById(eventCreateRequestDTO.getEstablishmentId());
+ if (establishment != null) {
+ event.setEstablishment(establishment);
+ } else {
+ logger.warn("[WARN] Ătablissement non trouvĂ© avec l'ID : {}", eventCreateRequestDTO.getEstablishmentId());
+ }
+ }
+
event.setCategory(eventCreateRequestDTO.getCategory());
event.setLink(eventCreateRequestDTO.getLink());
event.setImageUrl(eventCreateRequestDTO.getImageUrl());
@@ -63,6 +89,15 @@ public class EventService {
event.setTags(eventCreateRequestDTO.getTags());
event.setOrganizer(eventCreateRequestDTO.getOrganizer());
event.setParticipationFee(eventCreateRequestDTO.getParticipationFee());
+
+ // v2.0 - Nouveaux champs
+ if (eventCreateRequestDTO.getIsPrivate() != null) {
+ event.setIsPrivate(eventCreateRequestDTO.getIsPrivate());
+ }
+ if (eventCreateRequestDTO.getWaitlistEnabled() != null) {
+ event.setWaitlistEnabled(eventCreateRequestDTO.getWaitlistEnabled());
+ }
+
event.setPrivacyRules(eventCreateRequestDTO.getPrivacyRules());
event.setTransportInfo(eventCreateRequestDTO.getTransportInfo());
event.setAccommodationInfo(eventCreateRequestDTO.getAccommodationInfo());
@@ -70,16 +105,17 @@ public class EventService {
event.setParkingInfo(eventCreateRequestDTO.getParkingInfo());
event.setSecurityProtocol(eventCreateRequestDTO.getSecurityProtocol());
event.setCreator(creator);
- event.setStatus("ouvert");
+ event.setStatus("OPEN"); // v2.0 - Statut standardisé
// Persiste l'événement dans la base de données
eventsRepository.persist(event);
logger.info("[logger] ĂvĂ©nement créé avec succĂšs : {}", event.getTitle());
- // Créer des notifications pour tous les amis
+ // Créer des notifications pour tous les amis (v2.0 - avec Kafka)
try {
List friendships = friendshipRepository.findFriendsByUser(creator, 0, Integer.MAX_VALUE);
- String creatorName = creator.getPrenoms() + " " + creator.getNom();
+ // v2.0 - Utiliser les nouveaux noms de champs
+ String creatorName = creator.getFirstName() + " " + creator.getLastName();
for (Friendship friendship : friendships) {
Users friend = friendship.getUser().equals(creator)
@@ -89,6 +125,7 @@ public class EventService {
String notificationTitle = "Nouvel événement de " + creatorName;
String notificationMessage = creatorName + " a créé un nouvel événement : " + event.getTitle();
+ // Créer notification en base
notificationService.createNotification(
notificationTitle,
notificationMessage,
@@ -96,6 +133,28 @@ public class EventService {
friend.getId(),
event.getId()
);
+
+ // TEMPS RĂEL: Publier dans Kafka (v2.0)
+ try {
+ java.util.Map notificationData = new java.util.HashMap<>();
+ notificationData.put("eventId", event.getId().toString());
+ notificationData.put("eventTitle", event.getTitle());
+ notificationData.put("creatorId", creator.getId().toString());
+ notificationData.put("creatorName", creatorName);
+ notificationData.put("startDate", event.getStartDate().toString());
+
+ NotificationEvent kafkaEvent = new NotificationEvent(
+ friend.getId().toString(), // userId destinataire
+ "event_created",
+ notificationData
+ );
+
+ notificationEmitter.send(kafkaEvent);
+ logger.debug("[logger] ĂvĂ©nement event_created publiĂ© dans Kafka pour: {}", friend.getId());
+ } catch (Exception kafkaEx) {
+ logger.error("[ERROR] Erreur publication Kafka: {}", kafkaEx.getMessage());
+ // Ne pas bloquer si Kafka échoue
+ }
}
logger.info("[logger] Notifications créées pour {} ami(s)", friendships.size());
} catch (Exception e) {
@@ -209,12 +268,17 @@ public class EventService {
throw new SecurityException("Vous n'avez pas les permissions pour modifier cet événement");
}
- // Mettre à jour les détails de l'événement
+ // v2.0 - Mettre à jour les détails de l'événement
existingEvent.setTitle(event.getTitle());
existingEvent.setDescription(event.getDescription());
existingEvent.setStartDate(event.getStartDate());
existingEvent.setEndDate(event.getEndDate());
- existingEvent.setLocation(event.getLocation());
+
+ // v2.0 - Ătablissement au lieu de location
+ if (event.getEstablishment() != null) {
+ existingEvent.setEstablishment(event.getEstablishment());
+ }
+
existingEvent.setCategory(event.getCategory());
existingEvent.setLink(event.getLink());
existingEvent.setImageUrl(event.getImageUrl());
@@ -222,6 +286,15 @@ public class EventService {
existingEvent.setTags(event.getTags());
existingEvent.setOrganizer(event.getOrganizer());
existingEvent.setParticipationFee(event.getParticipationFee());
+
+ // v2.0 - Nouveaux champs
+ if (event.getIsPrivate() != null) {
+ existingEvent.setIsPrivate(event.getIsPrivate());
+ }
+ if (event.getWaitlistEnabled() != null) {
+ existingEvent.setWaitlistEnabled(event.getWaitlistEnabled());
+ }
+
existingEvent.setPrivacyRules(event.getPrivacyRules());
existingEvent.setTransportInfo(event.getTransportInfo());
existingEvent.setAccommodationInfo(event.getAccommodationInfo());
@@ -269,8 +342,22 @@ public class EventService {
* @return La liste des événements auxquels l'utilisateur participe.
*/
public List findEventsByUser(Users user) {
- logger.info("[logger] Récupération des événements pour l'utilisateur avec l'ID : {}", user.getId());
- List events = eventsRepository.find("participants", user).list();
+ return findEventsByUser(user, 0, Integer.MAX_VALUE);
+ }
+
+ /**
+ * RécupÚre les événements auxquels un utilisateur participe avec pagination.
+ *
+ * @param user L'utilisateur pour lequel récupérer les événements.
+ * @param page Le numéro de la page (0-indexé)
+ * @param size La taille de la page
+ * @return La liste paginée des événements auxquels l'utilisateur participe.
+ */
+ public List findEventsByUser(Users user, int page, int size) {
+ logger.info("[logger] Récupération des événements pour l'utilisateur avec l'ID : {} (page: {}, size: {})", user.getId(), page, size);
+ List events = eventsRepository.find("participants", user)
+ .page(page, size)
+ .list();
logger.info("[logger] Nombre d'événements pour l'utilisateur avec l'ID {} : {}", user.getId(), events.size());
return events;
}
@@ -372,7 +459,11 @@ public class EventService {
*/
public List recommendEventsForUser(Users user) {
logger.info("[logger] Recommandation d'événements pour l'utilisateur : " + user.getEmail());
- List events = eventsRepository.find("category", user.getPreferredCategory()).list();
+ // v2.0 - Utiliser preferences pour preferredCategory
+ String preferredCategory = user.getPreferredCategory(); // Méthode qui utilise preferences JSONB
+ List events = preferredCategory != null
+ ? eventsRepository.find("category", preferredCategory).list()
+ : eventsRepository.findAll().list();
logger.info("[logger] Nombre d'événements recommandés pour l'utilisateur : " + events.size());
return events;
}
diff --git a/src/main/java/com/lions/dev/service/FriendshipService.java b/src/main/java/com/lions/dev/service/FriendshipService.java
index 9265e58..fda3ed4 100644
--- a/src/main/java/com/lions/dev/service/FriendshipService.java
+++ b/src/main/java/com/lions/dev/service/FriendshipService.java
@@ -13,7 +13,9 @@ import com.lions.dev.exception.FriendshipNotFoundException;
import com.lions.dev.exception.UserNotFoundException;
import com.lions.dev.repository.FriendshipRepository;
import com.lions.dev.repository.UsersRepository;
-import com.lions.dev.websocket.NotificationWebSocket;
+import com.lions.dev.dto.events.NotificationEvent;
+import org.eclipse.microprofile.reactive.messaging.Channel;
+import org.eclipse.microprofile.reactive.messaging.Emitter;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
@@ -40,6 +42,10 @@ public class FriendshipService {
@Inject
NotificationService notificationService; // Injecte le service de notifications
+ @Inject
+ @Channel("notifications")
+ Emitter notificationEmitter; // v2.0 - Publie dans Kafka
+
private static final Logger logger = Logger.getLogger(FriendshipService.class);
/**
@@ -79,24 +85,26 @@ public class FriendshipService {
Friendship friendship = new Friendship(user, friend, FriendshipStatus.PENDING);
friendshipRepository.persist(friendship);
- // TEMPS RĂEL: Notifier le destinataire via WebSocket
+ // TEMPS RĂEL: Publier dans Kafka (v2.0)
try {
Map notificationData = new HashMap<>();
notificationData.put("requestId", friendship.getId().toString());
notificationData.put("senderId", user.getId().toString());
- notificationData.put("senderName", user.getPrenoms() + " " + user.getNom());
+ // v2.0 - Utiliser les nouveaux noms de champs
+ notificationData.put("senderName", user.getFirstName() + " " + user.getLastName());
notificationData.put("senderProfileImage", user.getProfileImageUrl() != null ? user.getProfileImageUrl() : "");
- NotificationWebSocket.sendNotificationToUser(
- friend.getId(),
+ NotificationEvent event = new NotificationEvent(
+ friend.getId().toString(), // userId destinataire (clé Kafka)
"friend_request_received",
notificationData
);
- logger.info("[LOG] Notification WebSocket envoyée au destinataire : " + friend.getId());
+ notificationEmitter.send(event);
+ logger.info("[LOG] ĂvĂ©nement friend_request_received publiĂ© dans Kafka pour : " + friend.getId());
} catch (Exception e) {
- logger.error("[ERROR] Erreur lors de l'envoi de la notification WebSocket : " + e.getMessage(), e);
- // Ne pas bloquer la demande d'amitié si le WebSocket échoue
+ logger.error("[ERROR] Erreur lors de la publication dans Kafka : " + e.getMessage(), e);
+ // Ne pas bloquer la demande d'amitié si Kafka échoue
}
logger.info("[LOG] Demande d'amitié envoyée avec succÚs.");
@@ -139,11 +147,12 @@ public class FriendshipService {
// Log de succĂšs
logger.info(String.format("[LOG] Demande d'amitié acceptée avec succÚs pour l'ID: %s", friendshipId)); // Correctement formaté
- // TEMPS RĂEL: Notifier l'Ă©metteur de la demande via WebSocket
+ // TEMPS RĂEL: Publier dans Kafka (v2.0)
try {
Users user = friendship.getUser();
Users friend = friendship.getFriend();
- String friendName = friend.getPrenoms() + " " + friend.getNom();
+ // v2.0 - Utiliser les nouveaux noms de champs
+ String friendName = friend.getFirstName() + " " + friend.getLastName();
Map notificationData = new HashMap<>();
notificationData.put("acceptedBy", friendName);
@@ -151,24 +160,26 @@ public class FriendshipService {
notificationData.put("accepterId", friend.getId().toString());
notificationData.put("accepterProfileImage", friend.getProfileImageUrl() != null ? friend.getProfileImageUrl() : "");
- NotificationWebSocket.sendNotificationToUser(
- user.getId(),
+ NotificationEvent event = new NotificationEvent(
+ user.getId().toString(), // userId émetteur (destinataire de la notification)
"friend_request_accepted",
notificationData
);
- logger.info("[LOG] Notification WebSocket d'acceptation envoyée à : " + user.getId());
+ notificationEmitter.send(event);
+ logger.info("[LOG] ĂvĂ©nement friend_request_accepted publiĂ© dans Kafka pour : " + user.getId());
} catch (Exception e) {
- logger.error("[ERROR] Erreur lors de l'envoi de la notification WebSocket d'acceptation : " + e.getMessage(), e);
- // Ne pas bloquer l'acceptation si le WebSocket échoue
+ logger.error("[ERROR] Erreur lors de la publication dans Kafka : " + e.getMessage(), e);
+ // Ne pas bloquer l'acceptation si Kafka échoue
}
// Créer des notifications pour les deux utilisateurs
try {
Users user = friendship.getUser();
Users friend = friendship.getFriend();
- String userName = user.getPrenoms() + " " + user.getNom();
- String friendName = friend.getPrenoms() + " " + friend.getNom();
+ // v2.0 - Utiliser les nouveaux noms de champs
+ String userName = user.getFirstName() + " " + user.getLastName();
+ String friendName = friend.getFirstName() + " " + friend.getLastName();
// Notification pour l'utilisateur qui a envoyé la demande
notificationService.createNotification(
@@ -212,7 +223,7 @@ public class FriendshipService {
friendship.setStatus(FriendshipStatus.REJECTED);
friendshipRepository.persist(friendship);
- // TEMPS RĂEL: Notifier l'Ă©metteur de la demande via WebSocket (optionnel selon UX)
+ // TEMPS RĂEL: Publier dans Kafka (v2.0)
try {
Users user = friendship.getUser();
@@ -220,16 +231,17 @@ public class FriendshipService {
notificationData.put("friendshipId", friendshipId.toString());
notificationData.put("rejectedAt", System.currentTimeMillis());
- NotificationWebSocket.sendNotificationToUser(
- user.getId(),
+ NotificationEvent event = new NotificationEvent(
+ user.getId().toString(), // userId émetteur (destinataire de la notification)
"friend_request_rejected",
notificationData
);
- logger.info("[LOG] Notification WebSocket de rejet envoyée à : " + user.getId());
+ notificationEmitter.send(event);
+ logger.info("[LOG] ĂvĂ©nement friend_request_rejected publiĂ© dans Kafka pour : " + user.getId());
} catch (Exception e) {
- logger.error("[ERROR] Erreur lors de l'envoi de la notification WebSocket de rejet : " + e.getMessage(), e);
- // Ne pas bloquer le rejet si le WebSocket échoue
+ logger.error("[ERROR] Erreur lors de la publication dans Kafka : " + e.getMessage(), e);
+ // Ne pas bloquer le rejet si Kafka échoue
}
logger.info("[LOG] Demande d'amitié rejetée.");
@@ -288,11 +300,12 @@ public class FriendshipService {
+ user.getId() + ", ami ID : " + friend.getId());
// Création du DTO de réponse à partir des informations de l'ami et de la relation
+ // v2.0 - Utiliser les nouveaux noms de champs
return new FriendshipReadFriendDetailsResponseDTO(
user.getId(), // ID de l'utilisateur
friend.getId(), // ID de l'ami
- friend.getNom(), // Nom de l'ami
- friend.getPrenoms(),
+ friend.getLastName(), // Nom de famille de l'ami (v2.0)
+ friend.getFirstName(), // Prénom de l'ami (v2.0)
friend.getEmail(), // Email de l'ami
friend.getProfileImageUrl(), // URL de l'image de profil de l'ami
friendship.getStatus(), // Statut de la relation
@@ -325,11 +338,12 @@ public class FriendshipService {
return friendships.stream()
.map(friendship -> {
Users friend = friendship.getUser().equals(user) ? friendship.getFriend() : friendship.getUser();
+ // v2.0 - Utiliser les nouveaux noms de champs
return new FriendshipReadFriendDetailsResponseDTO(
user.getId(),
friend.getId(),
- friend.getNom(),
- friend.getPrenoms(),
+ friend.getLastName(), // v2.0
+ friend.getFirstName(), // v2.0
friend.getEmail(),
friend.getProfileImageUrl(),
friendship.getStatus(),
diff --git a/src/main/java/com/lions/dev/service/MessageService.java b/src/main/java/com/lions/dev/service/MessageService.java
index 88ae8bf..6a9347e 100644
--- a/src/main/java/com/lions/dev/service/MessageService.java
+++ b/src/main/java/com/lions/dev/service/MessageService.java
@@ -7,7 +7,10 @@ import com.lions.dev.exception.UserNotFoundException;
import com.lions.dev.repository.ConversationRepository;
import com.lions.dev.repository.MessageRepository;
import com.lions.dev.repository.UsersRepository;
-import com.lions.dev.websocket.ChatWebSocket;
+import com.lions.dev.dto.events.ChatMessageEvent;
+import org.eclipse.microprofile.reactive.messaging.Channel;
+import org.eclipse.microprofile.reactive.messaging.Emitter;
+import io.smallrye.reactive.messaging.kafka.api.OutgoingKafkaRecordMetadata;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
@@ -40,6 +43,10 @@ public class MessageService {
@Inject
NotificationService notificationService;
+ @Inject
+ @Channel("chat-messages")
+ Emitter chatMessageEmitter; // v2.0 - Publie dans Kafka
+
/**
* Envoie un message d'un utilisateur Ă un autre.
*
@@ -92,7 +99,8 @@ public class MessageService {
// Créer une notification pour le destinataire
try {
- String senderName = sender.getPrenoms() + " " + sender.getNom();
+ // v2.0 - Utiliser les nouveaux noms de champs
+ String senderName = sender.getFirstName() + " " + sender.getLastName();
String notificationMessage = content.length() > 50
? content.substring(0, 50) + "..."
: content;
@@ -109,43 +117,78 @@ public class MessageService {
System.out.println("[ERROR] Erreur lors de la création de la notification : " + e.getMessage());
}
- // TEMPS RĂEL : Envoyer le message via WebSocket au destinataire
+ // TEMPS RĂEL : Publier dans Kafka (v2.0)
try {
- Map messageData = new HashMap<>();
- messageData.put("id", message.getId().toString());
- messageData.put("conversationId", conversation.getId().toString());
- messageData.put("senderId", senderId.toString());
- messageData.put("senderFirstName", sender.getPrenoms());
- messageData.put("senderLastName", sender.getNom());
- messageData.put("senderProfileImageUrl", sender.getProfileImageUrl() != null ? sender.getProfileImageUrl() : "");
- messageData.put("content", content);
- messageData.put("timestamp", message.getCreatedAt().toString());
- messageData.put("isRead", message.isRead());
- messageData.put("attachmentUrl", mediaUrl != null ? mediaUrl : "");
- messageData.put("attachmentType", messageType != null ? messageType : "text");
+ // Créer l'événement pour Kafka
+ ChatMessageEvent event = new ChatMessageEvent();
+ event.setConversationId(conversation.getId().toString());
+ event.setSenderId(senderId.toString());
+ event.setRecipientId(recipientId.toString());
+ event.setContent(content);
+ event.setMessageId(message.getId().toString());
+ event.setEventType("message");
+ event.setTimestamp(System.currentTimeMillis());
+
+ // Métadonnées additionnelles
+ Map eventMetadata = new HashMap<>();
+ eventMetadata.put("senderFirstName", sender.getFirstName());
+ eventMetadata.put("senderLastName", sender.getLastName());
+ eventMetadata.put("senderProfileImageUrl", sender.getProfileImageUrl() != null ? sender.getProfileImageUrl() : "");
+ eventMetadata.put("isRead", message.isRead());
+ eventMetadata.put("attachmentUrl", mediaUrl != null ? mediaUrl : "");
+ eventMetadata.put("attachmentType", messageType != null ? messageType : "text");
+ event.setMetadata(eventMetadata);
- // Envoyer au destinataire via ChatWebSocket
- ChatWebSocket.sendMessageToUser(recipientId, messageData);
+ // Publier dans Kafka (utiliser conversationId comme clé pour garantir l'ordre)
+ OutgoingKafkaRecordMetadata kafkaMetadata =
+ OutgoingKafkaRecordMetadata.builder()
+ .withKey(conversation.getId().toString())
+ .build();
+
+ chatMessageEmitter.send(org.eclipse.microprofile.reactive.messaging.Message.of(
+ event,
+ () -> java.util.concurrent.CompletableFuture.completedFuture(null), // ack
+ throwable -> {
+ System.out.println("[ERROR] Erreur envoi Kafka: " + throwable.getMessage());
+ return java.util.concurrent.CompletableFuture.completedFuture(null); // nack
+ }
+ ).addMetadata(kafkaMetadata));
- System.out.println("[LOG] Message envoyé via WebSocket au destinataire : " + recipientId);
+ System.out.println("[LOG] Message publié dans Kafka: " + message.getId());
- // Envoyer confirmation de délivrance à l'expéditeur
+ // Envoyer confirmation de délivrance à l'expéditeur (via Kafka aussi)
try {
- Map deliveryConfirmation = new HashMap<>();
- deliveryConfirmation.put("messageId", message.getId().toString());
- deliveryConfirmation.put("isDelivered", true);
- deliveryConfirmation.put("timestamp", System.currentTimeMillis());
+ ChatMessageEvent deliveryEvent = new ChatMessageEvent();
+ deliveryEvent.setConversationId(conversation.getId().toString());
+ deliveryEvent.setSenderId(recipientId.toString()); // Le destinataire confirme
+ deliveryEvent.setRecipientId(senderId.toString()); // à l'expéditeur
+ deliveryEvent.setMessageId(message.getId().toString());
+ deliveryEvent.setEventType("delivery_confirmation");
+ deliveryEvent.setTimestamp(System.currentTimeMillis());
+
+ Map deliveryEventMetadata = new HashMap<>();
+ deliveryEventMetadata.put("isDelivered", true);
+ deliveryEvent.setMetadata(deliveryEventMetadata);
- ChatWebSocket.sendDeliveryConfirmation(senderId, deliveryConfirmation);
+ OutgoingKafkaRecordMetadata deliveryKafkaMetadata =
+ OutgoingKafkaRecordMetadata.builder()
+ .withKey(conversation.getId().toString())
+ .build();
+
+ chatMessageEmitter.send(org.eclipse.microprofile.reactive.messaging.Message.of(
+ deliveryEvent,
+ () -> java.util.concurrent.CompletableFuture.completedFuture(null),
+ throwable -> java.util.concurrent.CompletableFuture.completedFuture(null)
+ ).addMetadata(deliveryKafkaMetadata));
- System.out.println("[LOG] Confirmation de délivrance envoyée à l'expéditeur : " + senderId);
+ System.out.println("[LOG] Confirmation de délivrance publiée dans Kafka pour : " + senderId);
} catch (Exception deliveryEx) {
- System.out.println("[ERROR] Erreur envoi confirmation délivrance : " + deliveryEx.getMessage());
+ System.out.println("[ERROR] Erreur publication confirmation délivrance : " + deliveryEx.getMessage());
// Ne pas bloquer si la confirmation échoue
}
} catch (Exception e) {
- System.out.println("[ERROR] Erreur lors de l'envoi du message via WebSocket : " + e.getMessage());
- // Ne pas bloquer l'envoi du message si WebSocket échoue
+ System.out.println("[ERROR] Erreur lors de la publication dans Kafka : " + e.getMessage());
+ // Ne pas bloquer l'envoi du message si Kafka échoue
}
return message;
@@ -240,18 +283,36 @@ public class MessageService {
? conversation.getUser2().getId()
: conversation.getUser1().getId();
- Map readConfirmation = new HashMap<>();
- readConfirmation.put("messageId", message.getId().toString());
- readConfirmation.put("userId", recipientId.toString());
- readConfirmation.put("timestamp", java.time.LocalDateTime.now().toString());
+ // Publier confirmation de lecture dans Kafka (v2.0)
+ try {
+ ChatMessageEvent readEvent = new ChatMessageEvent();
+ readEvent.setConversationId(conversation.getId().toString());
+ readEvent.setSenderId(recipientId.toString()); // Celui qui a lu
+ readEvent.setRecipientId(message.getSender().getId().toString()); // L'expéditeur
+ readEvent.setMessageId(message.getId().toString());
+ readEvent.setEventType("read_confirmation");
+ readEvent.setTimestamp(System.currentTimeMillis());
+
+ Map readEventMetadata = new HashMap<>();
+ readEventMetadata.put("readBy", recipientId.toString());
+ readEventMetadata.put("readAt", System.currentTimeMillis());
+ readEvent.setMetadata(readEventMetadata);
- // Envoyer via ChatWebSocket avec type "read"
- com.lions.dev.websocket.ChatWebSocket.sendReadConfirmation(
- message.getSender().getId(),
- readConfirmation
- );
+ OutgoingKafkaRecordMetadata readKafkaMetadata =
+ OutgoingKafkaRecordMetadata.builder()
+ .withKey(conversation.getId().toString())
+ .build();
+
+ chatMessageEmitter.send(org.eclipse.microprofile.reactive.messaging.Message.of(
+ readEvent,
+ () -> java.util.concurrent.CompletableFuture.completedFuture(null),
+ throwable -> java.util.concurrent.CompletableFuture.completedFuture(null)
+ ).addMetadata(readKafkaMetadata));
- System.out.println("[LOG] Confirmation de lecture envoyée à l'expéditeur : " + message.getSender().getId());
+ System.out.println("[LOG] Confirmation de lecture publiée dans Kafka pour : " + message.getSender().getId());
+ } catch (Exception e) {
+ System.out.println("[ERROR] Erreur publication confirmation lecture : " + e.getMessage());
+ }
} catch (Exception e) {
System.out.println("[ERROR] Erreur envoi confirmation lecture : " + e.getMessage());
}
diff --git a/src/main/java/com/lions/dev/service/PresenceService.java b/src/main/java/com/lions/dev/service/PresenceService.java
index 94a84a3..53969e9 100644
--- a/src/main/java/com/lions/dev/service/PresenceService.java
+++ b/src/main/java/com/lions/dev/service/PresenceService.java
@@ -2,7 +2,7 @@ package com.lions.dev.service;
import com.lions.dev.entity.users.Users;
import com.lions.dev.repository.UsersRepository;
-import com.lions.dev.websocket.NotificationWebSocket;
+import com.lions.dev.websocket.NotificationWebSocketNext;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.control.ActivateRequestContext;
import jakarta.inject.Inject;
@@ -95,8 +95,8 @@ public class PresenceService {
presenceData.put("lastSeen", lastSeen != null ? lastSeen.toString() : null);
presenceData.put("timestamp", System.currentTimeMillis());
- // Envoyer via NotificationWebSocket
- NotificationWebSocket.broadcastPresenceUpdate(presenceData);
+ // Envoyer via NotificationWebSocketNext (v2.0)
+ NotificationWebSocketNext.broadcastPresenceUpdate(presenceData);
System.out.println("[PRESENCE] Broadcast de la présence de " + userId + " : " + isOnline);
} catch (Exception e) {
diff --git a/src/main/java/com/lions/dev/service/SocialPostService.java b/src/main/java/com/lions/dev/service/SocialPostService.java
index f788ba7..7688855 100644
--- a/src/main/java/com/lions/dev/service/SocialPostService.java
+++ b/src/main/java/com/lions/dev/service/SocialPostService.java
@@ -8,10 +8,15 @@ import com.lions.dev.exception.UserNotFoundException;
import com.lions.dev.repository.FriendshipRepository;
import com.lions.dev.repository.SocialPostRepository;
import com.lions.dev.repository.UsersRepository;
+import com.lions.dev.dto.events.ReactionEvent;
+import org.eclipse.microprofile.reactive.messaging.Channel;
+import org.eclipse.microprofile.reactive.messaging.Emitter;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
@@ -38,6 +43,10 @@ public class SocialPostService {
@Inject
NotificationService notificationService;
+ @Inject
+ @Channel("reactions")
+ Emitter reactionEmitter; // v2.0 - Publie dans Kafka
+
/**
* RécupÚre tous les posts avec pagination.
*
@@ -99,7 +108,8 @@ public class SocialPostService {
// Créer des notifications pour tous les amis
try {
List friendships = friendshipRepository.findFriendsByUser(user, 0, Integer.MAX_VALUE);
- String userName = user.getPrenoms() + " " + user.getNom();
+ // v2.0 - Utiliser les nouveaux noms de champs
+ String userName = user.getFirstName() + " " + user.getLastName();
for (Friendship friendship : friendships) {
Users friend = friendship.getUser().equals(user)
@@ -208,11 +218,12 @@ public class SocialPostService {
* Like un post (incrémente le compteur de likes).
*
* @param postId L'ID du post
+ * @param userId L'ID de l'utilisateur qui like (v2.0)
* @return Le post mis Ă jour
*/
@Transactional
- public SocialPost likePost(UUID postId) {
- System.out.println("[LOG] Like du post ID : " + postId);
+ public SocialPost likePost(UUID postId, UUID userId) {
+ System.out.println("[LOG] Like du post ID : " + postId + " par utilisateur : " + userId);
SocialPost post = socialPostRepository.findById(postId);
if (post == null) {
@@ -222,6 +233,31 @@ public class SocialPostService {
post.incrementLikes();
socialPostRepository.persist(post);
+
+ // TEMPS RĂEL: Publier dans Kafka (v2.0)
+ try {
+ Map reactionData = new HashMap<>();
+ reactionData.put("ownerId", post.getUser().getId().toString()); // Propriétaire du post
+ reactionData.put("likesCount", post.getLikesCount());
+ reactionData.put("postTitle", post.getContent().length() > 50
+ ? post.getContent().substring(0, 50) + "..."
+ : post.getContent());
+
+ ReactionEvent event = new ReactionEvent(
+ postId.toString(), // targetId
+ "post", // targetType
+ userId.toString(), // userId qui réagit
+ "like", // reactionType
+ reactionData
+ );
+
+ reactionEmitter.send(event);
+ System.out.println("[LOG] Réaction like publiée dans Kafka pour post: " + postId);
+ } catch (Exception e) {
+ System.out.println("[ERROR] Erreur publication Kafka: " + e.getMessage());
+ // Ne pas bloquer le like si Kafka échoue
+ }
+
return post;
}
@@ -229,11 +265,13 @@ public class SocialPostService {
* Ajoute un commentaire à un post (incrémente le compteur de commentaires).
*
* @param postId L'ID du post
+ * @param userId L'ID de l'utilisateur qui commente (v2.0)
+ * @param commentContent Le contenu du commentaire (v2.0)
* @return Le post mis Ă jour
*/
@Transactional
- public SocialPost addComment(UUID postId) {
- System.out.println("[LOG] Ajout de commentaire au post ID : " + postId);
+ public SocialPost addComment(UUID postId, UUID userId, String commentContent) {
+ System.out.println("[LOG] Ajout de commentaire au post ID : " + postId + " par utilisateur : " + userId);
SocialPost post = socialPostRepository.findById(postId);
if (post == null) {
@@ -243,6 +281,35 @@ public class SocialPostService {
post.incrementComments();
socialPostRepository.persist(post);
+
+ // TEMPS RĂEL: Publier dans Kafka (v2.0)
+ try {
+ Users commenter = usersRepository.findById(userId);
+ Map reactionData = new HashMap<>();
+ reactionData.put("ownerId", post.getUser().getId().toString()); // Propriétaire du post
+ reactionData.put("commentsCount", post.getCommentsCount());
+ reactionData.put("commentContent", commentContent != null && commentContent.length() > 100
+ ? commentContent.substring(0, 100) + "..."
+ : commentContent);
+ reactionData.put("commenterName", commenter != null
+ ? commenter.getFirstName() + " " + commenter.getLastName()
+ : "Utilisateur");
+
+ ReactionEvent event = new ReactionEvent(
+ postId.toString(), // targetId
+ "post", // targetType
+ userId.toString(), // userId qui commente
+ "comment", // reactionType
+ reactionData
+ );
+
+ reactionEmitter.send(event);
+ System.out.println("[LOG] Réaction comment publiée dans Kafka pour post: " + postId);
+ } catch (Exception e) {
+ System.out.println("[ERROR] Erreur publication Kafka: " + e.getMessage());
+ // Ne pas bloquer le commentaire si Kafka échoue
+ }
+
return post;
}
@@ -250,11 +317,12 @@ public class SocialPostService {
* Partage un post (incrémente le compteur de partages).
*
* @param postId L'ID du post
+ * @param userId L'ID de l'utilisateur qui partage (v2.0)
* @return Le post mis Ă jour
*/
@Transactional
- public SocialPost sharePost(UUID postId) {
- System.out.println("[LOG] Partage du post ID : " + postId);
+ public SocialPost sharePost(UUID postId, UUID userId) {
+ System.out.println("[LOG] Partage du post ID : " + postId + " par utilisateur : " + userId);
SocialPost post = socialPostRepository.findById(postId);
if (post == null) {
@@ -264,6 +332,28 @@ public class SocialPostService {
post.incrementShares();
socialPostRepository.persist(post);
+
+ // TEMPS RĂEL: Publier dans Kafka (v2.0)
+ try {
+ Map reactionData = new HashMap<>();
+ reactionData.put("ownerId", post.getUser().getId().toString()); // Propriétaire du post
+ reactionData.put("sharesCount", post.getSharesCount());
+
+ ReactionEvent event = new ReactionEvent(
+ postId.toString(), // targetId
+ "post", // targetType
+ userId.toString(), // userId qui partage
+ "share", // reactionType
+ reactionData
+ );
+
+ reactionEmitter.send(event);
+ System.out.println("[LOG] Réaction share publiée dans Kafka pour post: " + postId);
+ } catch (Exception e) {
+ System.out.println("[ERROR] Erreur publication Kafka: " + e.getMessage());
+ // Ne pas bloquer le partage si Kafka échoue
+ }
+
return post;
}
diff --git a/src/main/java/com/lions/dev/service/StoryService.java b/src/main/java/com/lions/dev/service/StoryService.java
index 48f2515..d70fdc6 100644
--- a/src/main/java/com/lions/dev/service/StoryService.java
+++ b/src/main/java/com/lions/dev/service/StoryService.java
@@ -35,8 +35,19 @@ public class StoryService {
* @return Liste des stories actives
*/
public List getAllActiveStories() {
- System.out.println("[LOG] Récupération de toutes les stories actives");
- return storyRepository.findAllActive();
+ return getAllActiveStories(0, Integer.MAX_VALUE);
+ }
+
+ /**
+ * RécupÚre toutes les stories actives avec pagination.
+ *
+ * @param page Le numéro de la page (0-indexé)
+ * @param size La taille de la page
+ * @return Liste paginée des stories actives
+ */
+ public List getAllActiveStories(int page, int size) {
+ System.out.println("[LOG] Récupération de toutes les stories actives (page: " + page + ", size: " + size + ")");
+ return storyRepository.findAllActive(page, size);
}
/**
@@ -47,7 +58,20 @@ public class StoryService {
* @throws UserNotFoundException Si l'utilisateur n'existe pas
*/
public List getActiveStoriesByUserId(UUID userId) {
- System.out.println("[LOG] Récupération des stories actives pour l'utilisateur ID : " + userId);
+ return getActiveStoriesByUserId(userId, 0, Integer.MAX_VALUE);
+ }
+
+ /**
+ * RécupÚre toutes les stories actives d'un utilisateur avec pagination.
+ *
+ * @param userId L'ID de l'utilisateur
+ * @param page Le numéro de la page (0-indexé)
+ * @param size La taille de la page
+ * @return Liste paginée des stories actives de l'utilisateur
+ * @throws UserNotFoundException Si l'utilisateur n'existe pas
+ */
+ public List getActiveStoriesByUserId(UUID userId, int page, int size) {
+ System.out.println("[LOG] Récupération des stories actives pour l'utilisateur ID : " + userId + " (page: " + page + ", size: " + size + ")");
Users user = usersRepository.findById(userId);
if (user == null) {
@@ -55,7 +79,7 @@ public class StoryService {
throw new UserNotFoundException("Utilisateur non trouvé avec l'ID : " + userId);
}
- return storyRepository.findActiveByUserId(userId);
+ return storyRepository.findActiveByUserId(userId, page, size);
}
/**
diff --git a/src/main/java/com/lions/dev/service/UsersService.java b/src/main/java/com/lions/dev/service/UsersService.java
index ac6c55b..8999bc9 100644
--- a/src/main/java/com/lions/dev/service/UsersService.java
+++ b/src/main/java/com/lions/dev/service/UsersService.java
@@ -23,7 +23,7 @@ public class UsersService {
UsersRepository usersRepository;
/**
- * Crée un nouvel utilisateur dans le systÚme.
+ * Crée un nouvel utilisateur dans le systÚme (v2.0).
*
* @param userCreateRequestDTO Le DTO contenant les informations de l'utilisateur à créer.
* @return L'utilisateur créé.
@@ -36,10 +36,22 @@ public class UsersService {
}
Users user = new Users();
- user.setNom(userCreateRequestDTO.getNom());
- user.setPrenoms(userCreateRequestDTO.getPrenoms());
+ // v2.0 - Utiliser les nouveaux noms de champs avec compatibilité v1.0
+ user.setFirstName(userCreateRequestDTO.getFirstName()); // v2.0
+ user.setLastName(userCreateRequestDTO.getLastName()); // v2.0
user.setEmail(userCreateRequestDTO.getEmail());
- user.setMotDePasse(userCreateRequestDTO.getMotDePasse()); // Hachage automatique
+ user.setPassword(userCreateRequestDTO.getPassword()); // v2.0 - Hachage automatique
+
+ // v2.0 - Nouveaux champs
+ if (userCreateRequestDTO.getBio() != null) {
+ user.setBio(userCreateRequestDTO.getBio());
+ }
+ if (userCreateRequestDTO.getLoyaltyPoints() != null) {
+ user.setLoyaltyPoints(userCreateRequestDTO.getLoyaltyPoints());
+ }
+ if (userCreateRequestDTO.getPreferences() != null) {
+ user.setPreferences(userCreateRequestDTO.getPreferences());
+ }
// Logique pour l'image et le rÎle par défaut.
user.setProfileImageUrl(
@@ -59,7 +71,7 @@ public class UsersService {
}
/**
- * Met Ă jour un utilisateur existant dans le systĂšme.
+ * Met Ă jour un utilisateur existant dans le systĂšme (v2.0).
*
* @param id L'ID de l'utilisateur Ă mettre Ă jour.
* @param userCreateRequestDTO Les nouvelles informations de l'utilisateur.
@@ -74,13 +86,35 @@ public class UsersService {
throw new UserNotFoundException("Utilisateur non trouvé avec l'ID : " + id);
}
- // Mettre Ă jour les champs de l'utilisateur existant
- existingUser.setNom(userCreateRequestDTO.getNom());
- existingUser.setPrenoms(userCreateRequestDTO.getPrenoms());
- existingUser.setEmail(userCreateRequestDTO.getEmail());
- existingUser.setMotDePasse(userCreateRequestDTO.getMotDePasse()); // Hachage automatique si nécessaire
- existingUser.setRole(userCreateRequestDTO.getRole());
- existingUser.setProfileImageUrl(userCreateRequestDTO.getProfileImageUrl());
+ // v2.0 - Mettre Ă jour les champs avec les nouveaux noms
+ if (userCreateRequestDTO.getFirstName() != null) {
+ existingUser.setFirstName(userCreateRequestDTO.getFirstName());
+ }
+ if (userCreateRequestDTO.getLastName() != null) {
+ existingUser.setLastName(userCreateRequestDTO.getLastName());
+ }
+ if (userCreateRequestDTO.getEmail() != null) {
+ existingUser.setEmail(userCreateRequestDTO.getEmail());
+ }
+ if (userCreateRequestDTO.getPassword() != null) {
+ existingUser.setPassword(userCreateRequestDTO.getPassword()); // v2.0 - Hachage automatique
+ }
+ if (userCreateRequestDTO.getRole() != null) {
+ existingUser.setRole(userCreateRequestDTO.getRole());
+ }
+ if (userCreateRequestDTO.getProfileImageUrl() != null) {
+ existingUser.setProfileImageUrl(userCreateRequestDTO.getProfileImageUrl());
+ }
+ // v2.0 - Nouveaux champs
+ if (userCreateRequestDTO.getBio() != null) {
+ existingUser.setBio(userCreateRequestDTO.getBio());
+ }
+ if (userCreateRequestDTO.getLoyaltyPoints() != null) {
+ existingUser.setLoyaltyPoints(userCreateRequestDTO.getLoyaltyPoints());
+ }
+ if (userCreateRequestDTO.getPreferences() != null) {
+ existingUser.setPreferences(userCreateRequestDTO.getPreferences());
+ }
usersRepository.persist(existingUser);
System.out.println("[LOG] Utilisateur mis Ă jour avec succĂšs : " + existingUser.getEmail());
@@ -122,16 +156,16 @@ public class UsersService {
}
/**
- * Authentifie un utilisateur avec son email et son mot de passe.
+ * Authentifie un utilisateur avec son email et son mot de passe (v2.0).
*
* @param email L'email de l'utilisateur.
- * @param motDePasse Le mot de passe de l'utilisateur.
+ * @param password Le mot de passe de l'utilisateur (v2.0).
* @return L'utilisateur authentifié.
* @throws UserNotFoundException Si l'utilisateur n'est pas trouvé ou si le mot de passe est incorrect.
*/
- public Users authenticateUser(String email, String motDePasse) {
+ public Users authenticateUser(String email, String password) {
Optional userOptional = usersRepository.findByEmail(email);
- if (userOptional.isEmpty() || !userOptional.get().verifierMotDePasse(motDePasse)) {
+ if (userOptional.isEmpty() || !userOptional.get().verifyPassword(password)) { // v2.0
System.out.println("[ERROR] Ăchec de l'authentification pour l'email : " + email);
throw new UserNotFoundException("Utilisateur ou mot de passe incorrect.");
}
@@ -139,6 +173,15 @@ public class UsersService {
return userOptional.get();
}
+ /**
+ * Méthode de compatibilité v1.0 (dépréciée).
+ * @deprecated Utiliser {@link #authenticateUser(String, String)} avec password Ă la place.
+ */
+ @Deprecated
+ public Users authenticateUser(String email, String motDePasse, boolean deprecated) {
+ return authenticateUser(email, motDePasse);
+ }
+
/**
* RécupÚre un utilisateur par son ID.
*
@@ -157,7 +200,7 @@ public class UsersService {
}
/**
- * Réinitialise le mot de passe d'un utilisateur.
+ * Réinitialise le mot de passe d'un utilisateur (v2.0).
*
* @param id L'ID de l'utilisateur.
* @param newPassword Le nouveau mot de passe à définir.
@@ -171,7 +214,7 @@ public class UsersService {
throw new UserNotFoundException("Utilisateur non trouvé.");
}
- user.setMotDePasse(newPassword); // Hachage automatique
+ user.setPassword(newPassword); // v2.0 - Hachage automatique
usersRepository.persist(user);
System.out.println("[LOG] Mot de passe réinitialisé pour l'utilisateur : " + user.getEmail());
}
diff --git a/src/main/java/com/lions/dev/websocket/ChatWebSocket.java b/src/main/java/com/lions/dev/websocket/ChatWebSocket.java
deleted file mode 100644
index 2c0cf27..0000000
--- a/src/main/java/com/lions/dev/websocket/ChatWebSocket.java
+++ /dev/null
@@ -1,361 +0,0 @@
-package com.lions.dev.websocket;
-
-import com.lions.dev.dto.request.chat.SendMessageRequestDTO;
-import com.lions.dev.dto.response.chat.MessageResponseDTO;
-import com.lions.dev.entity.chat.Message;
-import com.lions.dev.service.MessageService;
-import io.quarkus.logging.Log;
-import jakarta.enterprise.context.ApplicationScoped;
-import jakarta.inject.Inject;
-import jakarta.websocket.*;
-import jakarta.websocket.server.PathParam;
-import jakarta.websocket.server.ServerEndpoint;
-
-import java.io.IOException;
-import java.util.Map;
-import java.util.UUID;
-import java.util.concurrent.ConcurrentHashMap;
-
-/**
- * WebSocket endpoint pour le chat en temps réel.
- *
- * Ce endpoint gĂšre:
- * - La connexion/déconnexion des utilisateurs
- * - L'envoi et la réception de messages en temps réel
- * - Les indicateurs de frappe (typing indicators)
- * - Les confirmations de lecture (read receipts)
- *
- * URL: ws://localhost:8080/chat/ws/{userId}
- */
-@ServerEndpoint("/chat/ws/{userId}")
-@ApplicationScoped
-public class ChatWebSocket {
-
- @Inject
- MessageService messageService;
-
- // Map pour stocker les sessions WebSocket des utilisateurs connectés
- private static final Map sessions = new ConcurrentHashMap<>();
-
- /**
- * Appelé lorsqu'un utilisateur se connecte.
- *
- * @param session La session WebSocket
- * @param userId L'ID de l'utilisateur
- */
- @OnOpen
- public void onOpen(Session session, @PathParam("userId") String userId) {
- try {
- UUID userUUID = UUID.fromString(userId);
- sessions.put(userUUID, session);
- Log.info("[LOG] WebSocket ouvert pour l'utilisateur ID : " + userId);
-
- // Envoyer un message de confirmation
- sendToUser(userUUID, "{\"type\":\"connected\",\"message\":\"Connecté au chat\"}");
-
- } catch (Exception e) {
- Log.error("[ERROR] Erreur lors de la connexion WebSocket : " + e.getMessage(), e);
- }
- }
-
- /**
- * Appelé lorsqu'un message est reçu.
- *
- * @param message Le message reçu (au format JSON)
- * @param userId L'ID de l'utilisateur qui envoie le message
- */
- @OnMessage
- public void onMessage(String message, @PathParam("userId") String userId) {
- try {
- Log.info("[LOG] Message reçu de l'utilisateur " + userId + " : " + message);
-
- // Parser le message JSON
- com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
- Map messageData = mapper.readValue(message, Map.class);
-
- String type = (String) messageData.get("type");
-
- switch (type) {
- case "message":
- handleChatMessage(messageData, userId);
- break;
- case "typing":
- handleTypingIndicator(messageData, userId);
- break;
- case "read":
- handleReadReceipt(messageData, userId);
- break;
- default:
- Log.warn("[WARN] Type de message inconnu : " + type);
- }
-
- } catch (Exception e) {
- Log.error("[ERROR] Erreur lors du traitement du message : " + e.getMessage(), e);
- }
- }
-
- /**
- * Appelé lorsqu'une erreur se produit.
- *
- * @param session La session WebSocket
- * @param error L'erreur
- */
- @OnError
- public void onError(Session session, Throwable error) {
- Log.error("[ERROR] Erreur WebSocket : " + error.getMessage(), error);
- }
-
- /**
- * Appelé lorsqu'un utilisateur se déconnecte.
- *
- * @param session La session WebSocket
- * @param userId L'ID de l'utilisateur
- */
- @OnClose
- public void onClose(Session session, @PathParam("userId") String userId) {
- try {
- UUID userUUID = UUID.fromString(userId);
- sessions.remove(userUUID);
- Log.info("[LOG] WebSocket fermé pour l'utilisateur ID : " + userId);
-
- } catch (Exception e) {
- Log.error("[ERROR] Erreur lors de la fermeture WebSocket : " + e.getMessage(), e);
- }
- }
-
- /**
- * GĂšre l'envoi d'un message de chat.
- */
- private void handleChatMessage(Map messageData, String senderId) {
- try {
- UUID senderUUID = UUID.fromString(senderId);
- UUID recipientUUID = UUID.fromString((String) messageData.get("recipientId"));
- String content = (String) messageData.get("content");
- String messageType = messageData.getOrDefault("messageType", "text").toString();
- String mediaUrl = (String) messageData.get("mediaUrl");
-
- // Enregistrer le message dans la base de données
- Message message = messageService.sendMessage(
- senderUUID,
- recipientUUID,
- content,
- messageType,
- mediaUrl
- );
-
- // Créer le DTO de réponse
- MessageResponseDTO response = new MessageResponseDTO(message);
- com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
- String responseJson = mapper.writeValueAsString(Map.of(
- "type", "message",
- "data", response
- ));
-
- // Envoyer le message au destinataire s'il est connecté
- sendToUser(recipientUUID, responseJson);
-
- // Envoyer une confirmation à l'expéditeur
- sendToUser(senderUUID, responseJson);
-
- Log.info("[LOG] Message envoyé de " + senderId + " à " + recipientUUID);
-
- } catch (Exception e) {
- Log.error("[ERROR] Erreur lors de l'envoi du message : " + e.getMessage(), e);
- }
- }
-
- /**
- * GĂšre les indicateurs de frappe.
- */
- private void handleTypingIndicator(Map messageData, String userId) {
- try {
- UUID recipientUUID = UUID.fromString((String) messageData.get("recipientId"));
- boolean isTyping = (boolean) messageData.getOrDefault("isTyping", false);
-
- com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
- String response = mapper.writeValueAsString(Map.of(
- "type", "typing",
- "userId", userId,
- "isTyping", isTyping
- ));
-
- sendToUser(recipientUUID, response);
-
- Log.info("[LOG] Indicateur de frappe envoyé de " + userId + " à " + recipientUUID);
-
- } catch (Exception e) {
- Log.error("[ERROR] Erreur lors de l'envoi de l'indicateur de frappe : " + e.getMessage(), e);
- }
- }
-
- /**
- * GĂšre les confirmations de lecture.
- */
- private void handleReadReceipt(Map messageData, String userId) {
- try {
- UUID messageUUID = UUID.fromString((String) messageData.get("messageId"));
-
- // Marquer le message comme lu
- Message message = messageService.markMessageAsRead(messageUUID);
-
- // Notifier l'expéditeur que le message a été lu
- UUID senderUUID = message.getSender().getId();
-
- com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
- String response = mapper.writeValueAsString(Map.of(
- "type", "read",
- "messageId", messageUUID.toString(),
- "readBy", userId
- ));
-
- sendToUser(senderUUID, response);
-
- Log.info("[LOG] Confirmation de lecture envoyée pour le message " + messageUUID);
-
- } catch (Exception e) {
- Log.error("[ERROR] Erreur lors de l'envoi de la confirmation de lecture : " + e.getMessage(), e);
- }
- }
-
- /**
- * Envoie un message à un utilisateur spécifique.
- *
- * @param userId L'ID de l'utilisateur
- * @param message Le message Ă envoyer
- */
- private void sendToUser(UUID userId, String message) {
- Session session = sessions.get(userId);
- if (session != null && session.isOpen()) {
- try {
- session.getAsyncRemote().sendText(message);
- } catch (Exception e) {
- Log.error("[ERROR] Erreur lors de l'envoi du message Ă l'utilisateur " + userId + " : " + e.getMessage(), e);
- }
- } else {
- Log.warn("[WARN] Utilisateur " + userId + " non connecté ou session fermée");
- }
- }
-
- /**
- * Diffuse un message à tous les utilisateurs connectés.
- *
- * @param message Le message Ă diffuser
- */
- public void broadcast(String message) {
- sessions.values().forEach(session -> {
- if (session.isOpen()) {
- try {
- session.getAsyncRemote().sendText(message);
- } catch (Exception e) {
- Log.error("[ERROR] Erreur lors de la diffusion : " + e.getMessage(), e);
- }
- }
- });
- }
-
- /**
- * Envoie un message chat à un utilisateur spécifique via WebSocket.
- *
- * Cette méthode est statique pour permettre son appel depuis les services
- * (comme MessageService) sans nécessiter une instance de ChatWebSocket.
- *
- * @param userId L'ID de l'utilisateur destinataire
- * @param messageData Les données du message (id, conversationId, content, etc.)
- */
- public static void sendMessageToUser(UUID userId, Map messageData) {
- Session session = sessions.get(userId);
-
- if (session == null || !session.isOpen()) {
- Log.warn("[CHAT-WS] Utilisateur " + userId + " non connecté, message non envoyé");
- return;
- }
-
- try {
- // Construire le message JSON au format attendu par le frontend
- com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
- Map envelope = Map.of(
- "type", "message",
- "data", messageData
- );
- String json = mapper.writeValueAsString(envelope);
-
- session.getAsyncRemote().sendText(json);
-
- Log.info("[CHAT-WS] Message envoyé à l'utilisateur " + userId);
- } catch (Exception e) {
- Log.error("[CHAT-WS] Erreur lors de l'envoi du message Ă " + userId + " : " + e.getMessage(), e);
- }
- }
-
- /**
- * Envoie une confirmation de délivrance à l'expéditeur via WebSocket.
- *
- * Cette méthode est appelée lorsqu'un message est délivré au destinataire
- * pour notifier l'expéditeur que le message a bien été reçu.
- *
- * @param userId L'ID de l'utilisateur (expéditeur) à notifier
- * @param deliveryData Les données de confirmation (messageId, isDelivered, timestamp)
- */
- public static void sendDeliveryConfirmation(UUID userId, Map deliveryData) {
- Session session = sessions.get(userId);
-
- if (session == null || !session.isOpen()) {
- Log.warn("[CHAT-WS] Utilisateur " + userId + " non connecté, confirmation de délivrance non envoyée");
- return;
- }
-
- try {
- com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
- Map envelope = Map.of(
- "type", "delivered",
- "data", deliveryData
- );
- String json = mapper.writeValueAsString(envelope);
-
- session.getAsyncRemote().sendText(json);
-
- Log.info("[CHAT-WS] Confirmation de délivrance envoyée à l'utilisateur " + userId);
- } catch (Exception e) {
- Log.error("[CHAT-WS] Erreur lors de l'envoi de la confirmation de délivrance : " + e.getMessage(), e);
- }
- }
-
- /**
- * Envoie une confirmation de lecture à l'expéditeur via WebSocket.
- *
- * @param userId L'ID de l'utilisateur expéditeur
- * @param readData Les données de confirmation de lecture
- */
- public static void sendReadConfirmation(UUID userId, Map readData) {
- Session session = sessions.get(userId);
-
- if (session == null || !session.isOpen()) {
- Log.warn("[CHAT-WS] Utilisateur " + userId + " non connecté, confirmation de lecture non envoyée");
- return;
- }
-
- try {
- com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
- Map envelope = Map.of(
- "type", "read",
- "data", readData
- );
- String json = mapper.writeValueAsString(envelope);
-
- session.getAsyncRemote().sendText(json);
-
- Log.info("[CHAT-WS] Confirmation de lecture envoyée à l'utilisateur " + userId);
- } catch (Exception e) {
- Log.error("[CHAT-WS] Erreur lors de l'envoi de la confirmation de lecture : " + e.getMessage(), e);
- }
- }
-
- /**
- * RécupÚre le nombre d'utilisateurs connectés.
- *
- * @return Le nombre d'utilisateurs connectés
- */
- public static int getConnectedUsersCount() {
- return sessions.size();
- }
-}
diff --git a/src/main/java/com/lions/dev/websocket/ChatWebSocketNext.java b/src/main/java/com/lions/dev/websocket/ChatWebSocketNext.java
new file mode 100644
index 0000000..06e6ae8
--- /dev/null
+++ b/src/main/java/com/lions/dev/websocket/ChatWebSocketNext.java
@@ -0,0 +1,301 @@
+package com.lions.dev.websocket;
+
+import com.lions.dev.dto.response.chat.MessageResponseDTO;
+import com.lions.dev.entity.chat.Message;
+import com.lions.dev.service.MessageService;
+import io.quarkus.logging.Log;
+import io.quarkus.websockets.next.OnClose;
+import io.quarkus.websockets.next.OnOpen;
+import io.quarkus.websockets.next.OnTextMessage;
+import io.quarkus.websockets.next.WebSocket;
+import io.quarkus.websockets.next.WebSocketConnection;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * WebSocket endpoint pour le chat en temps réel (WebSockets Next).
+ *
+ * Architecture v2.0:
+ * Client â WebSocket â MessageService â Kafka â Bridge â WebSocket â Destinataire
+ *
+ * GĂšre:
+ * - La connexion/déconnexion des utilisateurs
+ * - L'envoi et la réception de messages en temps réel
+ * - Les indicateurs de frappe (typing indicators)
+ * - Les confirmations de lecture (read receipts)
+ * - Les confirmations de délivrance
+ *
+ * URL: ws://localhost:8080/chat/{userId}
+ */
+@WebSocket(path = "/chat/{userId}")
+@ApplicationScoped
+public class ChatWebSocketNext {
+
+ @Inject
+ MessageService messageService;
+
+ // Map pour stocker les sessions WebSocket des utilisateurs connectés
+ private static final Map sessions = new ConcurrentHashMap<>();
+
+ @OnOpen
+ public void onOpen(WebSocketConnection connection) {
+ String userId = connection.pathParam("userId");
+ try {
+ UUID userUUID = UUID.fromString(userId);
+ sessions.put(userUUID, connection);
+ Log.info("[CHAT-WS-NEXT] WebSocket ouvert pour l'utilisateur ID : " + userId);
+
+ // Envoyer un message de confirmation
+ String confirmation = buildJsonMessage("connected",
+ Map.of("message", "Connecté au chat"));
+ connection.sendText(confirmation);
+
+ } catch (IllegalArgumentException e) {
+ Log.error("[CHAT-WS-NEXT] UUID invalide: " + userId, e);
+ connection.close();
+ } catch (Exception e) {
+ Log.error("[CHAT-WS-NEXT] Erreur lors de la connexion", e);
+ connection.close();
+ }
+ }
+
+ @OnClose
+ public void onClose(WebSocketConnection connection) {
+ try {
+ String userId = connection.pathParam("userId");
+ UUID userUUID = UUID.fromString(userId);
+ sessions.remove(userUUID);
+ Log.info("[CHAT-WS-NEXT] WebSocket fermé pour l'utilisateur ID : " + userId);
+ } catch (Exception e) {
+ Log.error("[CHAT-WS-NEXT] Erreur lors de la fermeture", e);
+ }
+ }
+
+ @OnTextMessage
+ public void onMessage(String message, WebSocketConnection connection) {
+ try {
+ String userId = connection.pathParam("userId");
+ Log.debug("[CHAT-WS-NEXT] Message reçu de " + userId + ": " + message);
+
+ // Parser le message JSON
+ com.fasterxml.jackson.databind.ObjectMapper mapper =
+ new com.fasterxml.jackson.databind.ObjectMapper();
+ Map messageData = mapper.readValue(message, Map.class);
+
+ String type = (String) messageData.get("type");
+
+ switch (type) {
+ case "message":
+ handleChatMessage(messageData, userId);
+ break;
+ case "typing":
+ handleTypingIndicator(messageData, userId);
+ break;
+ case "read":
+ handleReadReceipt(messageData, userId);
+ break;
+ default:
+ Log.warn("[CHAT-WS-NEXT] Type de message inconnu: " + type);
+ }
+
+ } catch (Exception e) {
+ Log.error("[CHAT-WS-NEXT] Erreur lors du traitement du message", e);
+ }
+ }
+
+ /**
+ * GĂšre l'envoi d'un message de chat.
+ * Le message est traité par MessageService qui publiera dans Kafka.
+ */
+ private void handleChatMessage(Map messageData, String senderId) {
+ try {
+ UUID senderUUID = UUID.fromString(senderId);
+ UUID recipientUUID = UUID.fromString((String) messageData.get("recipientId"));
+ String content = (String) messageData.get("content");
+ String messageType = messageData.getOrDefault("messageType", "text").toString();
+ String mediaUrl = (String) messageData.get("mediaUrl");
+
+ // Enregistrer le message dans la base de données
+ // MessageService publiera automatiquement dans Kafka
+ Message message = messageService.sendMessage(
+ senderUUID,
+ recipientUUID,
+ content,
+ messageType,
+ mediaUrl
+ );
+
+ // Créer le DTO de réponse
+ MessageResponseDTO response = new MessageResponseDTO(message);
+ String responseJson = buildJsonMessage("message",
+ Map.of("message", response));
+
+ // Envoyer confirmation à l'expéditeur
+ sendToUser(senderUUID, responseJson);
+
+ Log.info("[CHAT-WS-NEXT] Message traité de " + senderId + " à " + recipientUUID);
+
+ } catch (Exception e) {
+ Log.error("[CHAT-WS-NEXT] Erreur lors de l'envoi du message", e);
+ }
+ }
+
+ /**
+ * GĂšre les indicateurs de frappe.
+ */
+ private void handleTypingIndicator(Map messageData, String userId) {
+ try {
+ UUID recipientUUID = UUID.fromString((String) messageData.get("recipientId"));
+ boolean isTyping = (boolean) messageData.getOrDefault("isTyping", false);
+
+ String response = buildJsonMessage("typing", Map.of(
+ "userId", userId,
+ "isTyping", isTyping
+ ));
+
+ sendToUser(recipientUUID, response);
+
+ Log.debug("[CHAT-WS-NEXT] Indicateur de frappe envoyé de " + userId + " à " + recipientUUID);
+
+ } catch (Exception e) {
+ Log.error("[CHAT-WS-NEXT] Erreur lors de l'envoi de l'indicateur de frappe", e);
+ }
+ }
+
+ /**
+ * GĂšre les confirmations de lecture.
+ */
+ private void handleReadReceipt(Map messageData, String userId) {
+ try {
+ UUID messageUUID = UUID.fromString((String) messageData.get("messageId"));
+
+ // Marquer le message comme lu
+ Message message = messageService.markMessageAsRead(messageUUID);
+
+ if (message != null) {
+ // Envoyer confirmation de lecture à l'expéditeur via WebSocket
+ // (sera aussi publié dans Kafka par MessageService)
+ UUID senderUUID = message.getSender().getId();
+ String response = buildJsonMessage("read_receipt", Map.of(
+ "messageId", messageUUID.toString(),
+ "readBy", userId,
+ "readAt", System.currentTimeMillis()
+ ));
+
+ sendToUser(senderUUID, response);
+ Log.info("[CHAT-WS-NEXT] Confirmation de lecture envoyée pour message " + messageUUID);
+ }
+
+ } catch (Exception e) {
+ Log.error("[CHAT-WS-NEXT] Erreur lors du traitement de la confirmation de lecture", e);
+ }
+ }
+
+ /**
+ * Envoie un message chat à un utilisateur spécifique via WebSocket.
+ * AppelĂ© par le bridge Kafka â WebSocket.
+ *
+ * @param userId ID de l'utilisateur destinataire
+ * @param message Message JSON Ă envoyer
+ */
+ public static void sendMessageToUser(UUID userId, String message) {
+ WebSocketConnection connection = sessions.get(userId);
+
+ if (connection == null || !connection.isOpen()) {
+ Log.debug("[CHAT-WS-NEXT] Utilisateur " + userId + " non connecté");
+ return;
+ }
+
+ try {
+ connection.sendText(message);
+ Log.debug("[CHAT-WS-NEXT] Message envoyé à l'utilisateur: " + userId);
+ } catch (Exception e) {
+ Log.error("[CHAT-WS-NEXT] Erreur lors de l'envoi Ă " + userId, e);
+ }
+ }
+
+ /**
+ * Envoie une confirmation de délivrance à l'expéditeur via WebSocket.
+ */
+ public static void sendDeliveryConfirmation(UUID senderId, Map confirmationData) {
+ WebSocketConnection connection = sessions.get(senderId);
+
+ if (connection == null || !connection.isOpen()) {
+ Log.debug("[CHAT-WS-NEXT] Expéditeur " + senderId + " non connecté pour confirmation");
+ return;
+ }
+
+ try {
+ String response = buildJsonMessage("delivery_confirmation", confirmationData);
+ connection.sendText(response);
+ Log.debug("[CHAT-WS-NEXT] Confirmation de délivrance envoyée à : " + senderId);
+ } catch (Exception e) {
+ Log.error("[CHAT-WS-NEXT] Erreur envoi confirmation Ă " + senderId, e);
+ }
+ }
+
+ /**
+ * Envoie une confirmation de lecture à l'expéditeur via WebSocket.
+ */
+ public static void sendReadConfirmation(UUID senderId, Map readData) {
+ WebSocketConnection connection = sessions.get(senderId);
+
+ if (connection == null || !connection.isOpen()) {
+ Log.debug("[CHAT-WS-NEXT] Expéditeur " + senderId + " non connecté pour confirmation de lecture");
+ return;
+ }
+
+ try {
+ String response = buildJsonMessage("read_confirmation", readData);
+ connection.sendText(response);
+ Log.debug("[CHAT-WS-NEXT] Confirmation de lecture envoyée à : " + senderId);
+ } catch (Exception e) {
+ Log.error("[CHAT-WS-NEXT] Erreur envoi confirmation lecture Ă " + senderId, e);
+ }
+ }
+
+ /**
+ * Envoie un message à un utilisateur (méthode privée pour usage interne).
+ */
+ private void sendToUser(UUID userId, String message) {
+ WebSocketConnection connection = sessions.get(userId);
+
+ if (connection != null && connection.isOpen()) {
+ try {
+ connection.sendText(message);
+ } catch (Exception e) {
+ Log.error("[CHAT-WS-NEXT] Erreur lors de l'envoi Ă " + userId, e);
+ }
+ }
+ }
+
+ /**
+ * Construit un message JSON.
+ */
+ private static String buildJsonMessage(String type, Map data) {
+ try {
+ com.fasterxml.jackson.databind.ObjectMapper mapper =
+ new com.fasterxml.jackson.databind.ObjectMapper();
+ Map message = Map.of(
+ "type", type,
+ "data", data,
+ "timestamp", System.currentTimeMillis()
+ );
+ return mapper.writeValueAsString(message);
+ } catch (Exception e) {
+ Log.error("[CHAT-WS-NEXT] Erreur construction JSON", e);
+ return "{\"type\":\"error\",\"data\":{\"message\":\"Erreur de construction\"}}";
+ }
+ }
+
+ /**
+ * RécupÚre le nombre d'utilisateurs connectés au chat.
+ */
+ public static int getConnectedUsersCount() {
+ return sessions.size();
+ }
+}
diff --git a/src/main/java/com/lions/dev/websocket/NotificationWebSocket.java b/src/main/java/com/lions/dev/websocket/NotificationWebSocket.java
deleted file mode 100644
index 7f8afe8..0000000
--- a/src/main/java/com/lions/dev/websocket/NotificationWebSocket.java
+++ /dev/null
@@ -1,370 +0,0 @@
-package com.lions.dev.websocket;
-
-import com.lions.dev.service.NotificationService;
-import com.lions.dev.service.FriendshipService;
-import io.quarkus.logging.Log;
-import jakarta.enterprise.context.ApplicationScoped;
-import jakarta.inject.Inject;
-import jakarta.websocket.*;
-import jakarta.websocket.server.PathParam;
-import jakarta.websocket.server.ServerEndpoint;
-
-import java.io.IOException;
-import java.util.Map;
-import java.util.Set;
-import java.util.UUID;
-import java.util.concurrent.ConcurrentHashMap;
-
-/**
- * WebSocket endpoint pour les notifications en temps réel.
- *
- * Ce endpoint gĂšre:
- * - Les notifications de demandes d'amitié (envoi, acceptation, rejet)
- * - Les notifications systÚme (événements, rappels)
- * - Les alertes de messages
- *
- * URL: ws://localhost:8080/notifications/ws/{userId}
- */
-@ServerEndpoint("/notifications/ws/{userId}")
-@ApplicationScoped
-public class NotificationWebSocket {
-
- @Inject
- NotificationService notificationService;
-
- @Inject
- FriendshipService friendshipService;
-
- @Inject
- com.lions.dev.service.PresenceService presenceService;
-
- // Map pour stocker les sessions WebSocket par utilisateur
- // Support de plusieurs sessions par utilisateur (multi-device)
- private static final Map> userSessions = new ConcurrentHashMap<>();
-
- /**
- * Appelé lorsqu'un utilisateur se connecte.
- *
- * @param session La session WebSocket
- * @param userId L'ID de l'utilisateur
- */
- @OnOpen
- public void onOpen(Session session, @PathParam("userId") String userId) {
- try {
- UUID userUUID = UUID.fromString(userId);
-
- // Ajouter la session Ă l'ensemble des sessions de l'utilisateur
- userSessions.computeIfAbsent(userUUID, k -> ConcurrentHashMap.newKeySet()).add(session);
-
- Log.info("[NOTIFICATION-WS] Connexion ouverte pour l'utilisateur ID : " + userId +
- " (Total sessions: " + userSessions.get(userUUID).size() + ")");
-
- // Envoyer un message de confirmation
- String confirmationMessage = buildNotificationJson("connected",
- Map.of("message", "Connecté au service de notifications en temps réel"));
-
- session.getAsyncRemote().sendText(confirmationMessage);
-
- // Marquer l'utilisateur comme en ligne
- presenceService.setUserOnline(userUUID);
-
- } catch (IllegalArgumentException e) {
- Log.error("[NOTIFICATION-WS] UUID invalide : " + userId, e);
- try {
- session.close(new CloseReason(CloseReason.CloseCodes.CANNOT_ACCEPT, "UUID invalide"));
- } catch (IOException ioException) {
- Log.error("[NOTIFICATION-WS] Erreur lors de la fermeture de session", ioException);
- }
- } catch (Exception e) {
- Log.error("[NOTIFICATION-WS] Erreur lors de la connexion : " + e.getMessage(), e);
- }
- }
-
- /**
- * Appelé lorsqu'un message est reçu.
- *
- * GĂšre les messages de type ping, ack, etc.
- *
- * @param message Le message reçu (au format JSON)
- * @param userId L'ID de l'utilisateur qui envoie le message
- */
- @OnMessage
- public void onMessage(String message, @PathParam("userId") String userId) {
- try {
- Log.info("[NOTIFICATION-WS] Message reçu de l'utilisateur " + userId + " : " + message);
-
- // Parser le message JSON
- com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
- Map messageData = mapper.readValue(message, Map.class);
-
- String type = (String) messageData.get("type");
-
- switch (type) {
- case "ping":
- handlePing(userId);
- break;
- case "ack":
- handleAcknowledgement(messageData, userId);
- break;
- default:
- Log.warn("[NOTIFICATION-WS] Type de message inconnu : " + type);
- }
-
- } catch (Exception e) {
- Log.error("[NOTIFICATION-WS] Erreur lors du traitement du message : " + e.getMessage(), e);
- }
- }
-
- /**
- * Appelé lorsqu'une erreur se produit.
- *
- * @param session La session WebSocket
- * @param error L'erreur
- */
- @OnError
- public void onError(Session session, Throwable error) {
- Log.error("[NOTIFICATION-WS] Erreur WebSocket : " + error.getMessage(), error);
- }
-
- /**
- * Appelé lorsqu'un utilisateur se déconnecte.
- *
- * @param session La session WebSocket
- * @param userId L'ID de l'utilisateur
- */
- @OnClose
- public void onClose(Session session, @PathParam("userId") String userId) {
- try {
- UUID userUUID = UUID.fromString(userId);
-
- // Supprimer la session de l'ensemble
- Set sessions = userSessions.get(userUUID);
- if (sessions != null) {
- sessions.remove(session);
-
- // Si l'utilisateur n'a plus de sessions, supprimer l'entrée et marquer hors ligne
- if (sessions.isEmpty()) {
- userSessions.remove(userUUID);
- presenceService.setUserOffline(userUUID);
- Log.info("[NOTIFICATION-WS] Toutes les sessions fermées pour l'utilisateur ID : " + userId);
- } else {
- Log.info("[NOTIFICATION-WS] Session fermée pour l'utilisateur ID : " + userId +
- " (Sessions restantes: " + sessions.size() + ")");
- }
- }
-
- } catch (Exception e) {
- Log.error("[NOTIFICATION-WS] Erreur lors de la fermeture : " + e.getMessage(), e);
- }
- }
-
- /**
- * GĂšre les messages de type ping (keep-alive).
- * Exécuté de maniÚre asynchrone sur un thread worker pour permettre les transactions JTA.
- */
- private void handlePing(String userId) {
- // Exécuter le heartbeat de maniÚre asynchrone sur un thread worker
- java.util.concurrent.CompletableFuture.runAsync(() -> {
- try {
- UUID userUUID = UUID.fromString(userId);
-
- // Mettre à jour le heartbeat de présence (exécuté sur thread worker)
- presenceService.heartbeat(userUUID);
-
- // Envoyer le pong depuis le thread worker
- String pongMessage = buildNotificationJson("pong", Map.of("timestamp", System.currentTimeMillis()));
- sendToUser(userUUID, pongMessage);
-
- Log.debug("[NOTIFICATION-WS] Pong envoyé à l'utilisateur : " + userId);
- } catch (Exception e) {
- Log.error("[NOTIFICATION-WS] Erreur lors de l'envoi du pong : " + e.getMessage(), e);
- }
- });
- }
-
- /**
- * GÚre les accusés de réception des notifications.
- */
- private void handleAcknowledgement(Map messageData, String userId) {
- try {
- String notificationId = (String) messageData.get("notificationId");
- Log.info("[NOTIFICATION-WS] ACK reçu pour la notification " + notificationId + " de l'utilisateur " + userId);
-
- // Optionnel: marquer la notification comme délivrée en base de données
-
- } catch (Exception e) {
- Log.error("[NOTIFICATION-WS] Erreur lors du traitement de l'ACK : " + e.getMessage(), e);
- }
- }
-
- /**
- * Envoie une notification à toutes les sessions d'un utilisateur spécifique.
- *
- * Cette méthode est statique pour permettre son appel depuis les services
- * sans nécessiter une instance de NotificationWebSocket.
- *
- * @param userId L'ID de l'utilisateur
- * @param notificationType Le type de notification
- * @param data Les données de la notification
- */
- public static void sendNotificationToUser(UUID userId, String notificationType, Map data) {
- Set sessions = userSessions.get(userId);
-
- if (sessions == null || sessions.isEmpty()) {
- Log.warn("[NOTIFICATION-WS] Utilisateur " + userId + " non connecté ou aucune session active");
- return;
- }
-
- String json = buildNotificationJson(notificationType, data);
-
- int successCount = 0;
- int failCount = 0;
-
- for (Session session : sessions) {
- if (session.isOpen()) {
- try {
- session.getAsyncRemote().sendText(json);
- successCount++;
- } catch (Exception e) {
- failCount++;
- Log.error("[NOTIFICATION-WS] Erreur lors de l'envoi Ă une session de l'utilisateur " + userId + " : " + e.getMessage(), e);
- }
- } else {
- failCount++;
- }
- }
-
- Log.info("[NOTIFICATION-WS] Notification " + notificationType + " envoyée à l'utilisateur " + userId +
- " (SuccĂšs: " + successCount + ", Ăchec: " + failCount + ")");
- }
-
- /**
- * Envoie un message Ă toutes les sessions d'un utilisateur.
- *
- * Version privée pour usage interne (ping/pong, etc.)
- *
- * @param userId L'ID de l'utilisateur
- * @param message Le message Ă envoyer
- */
- private void sendToUser(UUID userId, String message) {
- Set sessions = userSessions.get(userId);
-
- if (sessions != null) {
- for (Session session : sessions) {
- if (session.isOpen()) {
- try {
- session.getAsyncRemote().sendText(message);
- } catch (Exception e) {
- Log.error("[NOTIFICATION-WS] Erreur lors de l'envoi Ă l'utilisateur " + userId + " : " + e.getMessage(), e);
- }
- }
- }
- }
- }
-
- /**
- * Diffuse une notification à tous les utilisateurs connectés.
- *
- * @param notificationType Le type de notification
- * @param data Les données de la notification
- */
- public static void broadcastNotification(String notificationType, Map data) {
- String json = buildNotificationJson(notificationType, data);
-
- int totalSessions = 0;
- int successCount = 0;
-
- for (Set sessions : userSessions.values()) {
- for (Session session : sessions) {
- totalSessions++;
- if (session.isOpen()) {
- try {
- session.getAsyncRemote().sendText(json);
- successCount++;
- } catch (Exception e) {
- Log.error("[NOTIFICATION-WS] Erreur lors de la diffusion : " + e.getMessage(), e);
- }
- }
- }
- }
-
- Log.info("[NOTIFICATION-WS] Notification diffusée à " + successCount + " sessions sur " + totalSessions);
- }
-
- /**
- * Construit un message JSON pour les notifications.
- *
- * Format: {"type": "notification_type", "data": {...}}
- *
- * @param type Le type de notification
- * @param data Les données de la notification
- * @return Le JSON sous forme de String
- */
- private static String buildNotificationJson(String type, Map data) {
- try {
- com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
- Map message = Map.of(
- "type", type,
- "data", data,
- "timestamp", System.currentTimeMillis()
- );
- return mapper.writeValueAsString(message);
- } catch (Exception e) {
- Log.error("[NOTIFICATION-WS] Erreur lors de la construction du JSON : " + e.getMessage(), e);
- return "{\"type\":\"error\",\"data\":{\"message\":\"Erreur de construction du message\"}}";
- }
- }
-
- /**
- * RécupÚre le nombre total d'utilisateurs connectés.
- *
- * @return Le nombre d'utilisateurs connectés
- */
- public static int getConnectedUsersCount() {
- return userSessions.size();
- }
-
- /**
- * RécupÚre le nombre total de sessions actives.
- *
- * @return Le nombre total de sessions
- */
- public static int getTotalSessionsCount() {
- return userSessions.values().stream()
- .mapToInt(Set::size)
- .sum();
- }
-
- /**
- * Broadcast une mise à jour de présence à tous les utilisateurs connectés.
- *
- * @param presenceData Les données de présence (userId, isOnline, lastSeen)
- */
- public static void broadcastPresenceUpdate(Map presenceData) {
- try {
- com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
- Map envelope = Map.of(
- "type", "presence",
- "data", presenceData
- );
- String json = mapper.writeValueAsString(envelope);
-
- // Envoyer à tous les utilisateurs connectés
- for (Set sessions : userSessions.values()) {
- for (Session session : sessions) {
- if (session.isOpen()) {
- try {
- session.getAsyncRemote().sendText(json);
- } catch (Exception e) {
- Log.error("[NOTIFICATION-WS] Erreur broadcast présence : " + e.getMessage());
- }
- }
- }
- }
-
- Log.debug("[NOTIFICATION-WS] Présence broadcastée : " + presenceData.get("userId"));
- } catch (Exception e) {
- Log.error("[NOTIFICATION-WS] Erreur lors du broadcast de présence : " + e.getMessage(), e);
- }
- }
-}
diff --git a/src/main/java/com/lions/dev/websocket/NotificationWebSocketNext.java b/src/main/java/com/lions/dev/websocket/NotificationWebSocketNext.java
new file mode 100644
index 0000000..abcb69e
--- /dev/null
+++ b/src/main/java/com/lions/dev/websocket/NotificationWebSocketNext.java
@@ -0,0 +1,282 @@
+package com.lions.dev.websocket;
+
+import io.quarkus.logging.Log;
+import io.quarkus.websockets.next.OnClose;
+import io.quarkus.websockets.next.OnOpen;
+import io.quarkus.websockets.next.OnTextMessage;
+import io.quarkus.websockets.next.WebSocket;
+import io.quarkus.websockets.next.WebSocketConnection;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+
+import com.lions.dev.service.PresenceService;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * WebSocket endpoint pour les notifications en temps réel (WebSockets Next).
+ *
+ * Architecture v2.0:
+ * Services mĂ©tier â Kafka Topic â Kafka Bridge â WebSocket â Client
+ *
+ * Avantages:
+ * - Scalabilité horizontale (plusieurs instances Quarkus)
+ * - Durabilité (événements persistés dans Kafka)
+ * - Découplage (services indépendants des WebSockets)
+ *
+ * URL: ws://localhost:8080/notifications/{userId}
+ */
+@WebSocket(path = "/notifications/{userId}")
+@ApplicationScoped
+public class NotificationWebSocketNext {
+
+ @Inject
+ PresenceService presenceService;
+
+ // Stockage des connexions actives par utilisateur (multi-device support)
+ private static final Map> userConnections = new ConcurrentHashMap<>();
+
+ @OnOpen
+ public void onOpen(WebSocketConnection connection) {
+ String userId = connection.pathParam("userId");
+ try {
+ UUID userUUID = UUID.fromString(userId);
+
+ // Ajouter la connexion Ă l'ensemble des connexions de l'utilisateur
+ userConnections.computeIfAbsent(userUUID, k -> ConcurrentHashMap.newKeySet())
+ .add(connection);
+
+ Log.info("[WS-NEXT] Connexion ouverte pour l'utilisateur: " + userId +
+ " (Total: " + userConnections.get(userUUID).size() + ")");
+
+ // Envoyer confirmation
+ String confirmation = buildJsonMessage("connected",
+ Map.of("message", "Connecté au service de notifications en temps réel"));
+ connection.sendText(confirmation);
+
+ // Marquer l'utilisateur comme en ligne
+ presenceService.setUserOnline(userUUID);
+
+ } catch (IllegalArgumentException e) {
+ Log.error("[WS-NEXT] UUID invalide: " + userId, e);
+ connection.close();
+ } catch (Exception e) {
+ Log.error("[WS-NEXT] Erreur lors de la connexion", e);
+ connection.close();
+ }
+ }
+
+ @OnClose
+ public void onClose(WebSocketConnection connection) {
+ try {
+ String userId = connection.pathParam("userId");
+ UUID userUUID = UUID.fromString(userId);
+ Set connections = userConnections.get(userUUID);
+
+ if (connections != null) {
+ connections.removeIf(conn -> !conn.isOpen());
+
+ if (connections.isEmpty()) {
+ userConnections.remove(userUUID);
+ presenceService.setUserOffline(userUUID);
+ Log.info("[WS-NEXT] Toutes les connexions fermées pour: " + userId);
+ } else {
+ Log.info("[WS-NEXT] Connexion fermée pour: " + userId +
+ " (Restantes: " + connections.size() + ")");
+ }
+ }
+ } catch (Exception e) {
+ Log.error("[WS-NEXT] Erreur lors de la fermeture", e);
+ }
+ }
+
+ @OnTextMessage
+ public void onMessage(String message, WebSocketConnection connection) {
+ try {
+ String userId = connection.pathParam("userId");
+ Log.debug("[WS-NEXT] Message reçu de " + userId + ": " + message);
+
+ // Parser le message JSON
+ com.fasterxml.jackson.databind.ObjectMapper mapper =
+ new com.fasterxml.jackson.databind.ObjectMapper();
+ Map messageData = mapper.readValue(message, Map.class);
+
+ String type = (String) messageData.get("type");
+
+ switch (type) {
+ case "ping":
+ handlePing(userId);
+ break;
+ case "ack":
+ handleAck(messageData, userId);
+ break;
+ default:
+ Log.warn("[WS-NEXT] Type de message inconnu: " + type);
+ }
+ } catch (Exception e) {
+ Log.error("[WS-NEXT] Erreur traitement message", e);
+ }
+ }
+
+ private void handlePing(String userId) {
+ try {
+ UUID userUUID = UUID.fromString(userId);
+
+ // Mettre à jour le heartbeat de présence
+ presenceService.heartbeat(userUUID);
+
+ // Envoyer pong
+ String pong = buildJsonMessage("pong",
+ Map.of("timestamp", System.currentTimeMillis()));
+ sendToUser(userUUID, pong);
+
+ Log.debug("[WS-NEXT] Pong envoyé à : " + userId);
+ } catch (Exception e) {
+ Log.error("[WS-NEXT] Erreur lors de l'envoi du pong", e);
+ }
+ }
+
+ private void handleAck(Map messageData, String userId) {
+ try {
+ String notificationId = (String) messageData.get("notificationId");
+ Log.debug("[WS-NEXT] ACK reçu pour notification " + notificationId +
+ " de " + userId);
+ // Optionnel: marquer la notification comme délivrée en base
+ } catch (Exception e) {
+ Log.error("[WS-NEXT] Erreur lors du traitement de l'ACK", e);
+ }
+ }
+
+ /**
+ * Envoie une notification à un utilisateur spécifique.
+ * AppelĂ© par le bridge Kafka â WebSocket.
+ *
+ * @param userId ID de l'utilisateur destinataire
+ * @param message Message JSON Ă envoyer
+ */
+ public static void sendToUser(UUID userId, String message) {
+ Set connections = userConnections.get(userId);
+
+ if (connections == null || connections.isEmpty()) {
+ Log.debug("[WS-NEXT] Utilisateur " + userId + " non connecté");
+ return;
+ }
+
+ int success = 0;
+ int failed = 0;
+
+ for (WebSocketConnection conn : connections) {
+ if (conn.isOpen()) {
+ try {
+ conn.sendText(message);
+ success++;
+ } catch (Exception e) {
+ failed++;
+ Log.error("[WS-NEXT] Erreur envoi Ă " + userId, e);
+ }
+ } else {
+ failed++;
+ }
+ }
+
+ Log.info("[WS-NEXT] Notification envoyée à " + userId +
+ " (SuccĂšs: " + success + ", Ăchec: " + failed + ")");
+ }
+
+ /**
+ * Diffuse une notification à tous les utilisateurs connectés.
+ *
+ * @param notificationType Type de notification
+ * @param data Données de la notification
+ */
+ public static void broadcastNotification(String notificationType, Map data) {
+ String json = buildJsonMessage(notificationType, data);
+
+ int totalSessions = 0;
+ int successCount = 0;
+
+ for (Set sessions : userConnections.values()) {
+ for (WebSocketConnection session : sessions) {
+ totalSessions++;
+ if (session.isOpen()) {
+ try {
+ session.sendText(json);
+ successCount++;
+ } catch (Exception e) {
+ Log.error("[WS-NEXT] Erreur lors de la diffusion", e);
+ }
+ }
+ }
+ }
+
+ Log.info("[WS-NEXT] Notification diffusée à " + successCount +
+ " sessions sur " + totalSessions);
+ }
+
+ /**
+ * Broadcast une mise à jour de présence à tous les utilisateurs connectés.
+ *
+ * @param presenceData Données de présence (userId, isOnline, lastSeen)
+ */
+ public static void broadcastPresenceUpdate(Map presenceData) {
+ try {
+ String json = buildJsonMessage("presence", presenceData);
+
+ for (Set sessions : userConnections.values()) {
+ for (WebSocketConnection session : sessions) {
+ if (session.isOpen()) {
+ try {
+ session.sendText(json);
+ } catch (Exception e) {
+ Log.error("[WS-NEXT] Erreur broadcast présence", e);
+ }
+ }
+ }
+ }
+
+ Log.debug("[WS-NEXT] Présence broadcastée: " + presenceData.get("userId"));
+ } catch (Exception e) {
+ Log.error("[WS-NEXT] Erreur lors du broadcast de présence", e);
+ }
+ }
+
+ /**
+ * Construit un message JSON pour les notifications.
+ *
+ * Format: {"type": "notification_type", "data": {...}, "timestamp": ...}
+ */
+ private static String buildJsonMessage(String type, Map data) {
+ try {
+ com.fasterxml.jackson.databind.ObjectMapper mapper =
+ new com.fasterxml.jackson.databind.ObjectMapper();
+ Map message = Map.of(
+ "type", type,
+ "data", data,
+ "timestamp", System.currentTimeMillis()
+ );
+ return mapper.writeValueAsString(message);
+ } catch (Exception e) {
+ Log.error("[WS-NEXT] Erreur construction JSON", e);
+ return "{\"type\":\"error\",\"data\":{\"message\":\"Erreur de construction du message\"}}";
+ }
+ }
+
+ /**
+ * RécupÚre le nombre total d'utilisateurs connectés.
+ */
+ public static int getConnectedUsersCount() {
+ return userConnections.size();
+ }
+
+ /**
+ * RécupÚre le nombre total de sessions actives.
+ */
+ public static int getTotalSessionsCount() {
+ return userConnections.values().stream()
+ .mapToInt(Set::size)
+ .sum();
+ }
+}
diff --git a/src/main/java/com/lions/dev/websocket/bridge/ChatKafkaBridge.java b/src/main/java/com/lions/dev/websocket/bridge/ChatKafkaBridge.java
new file mode 100644
index 0000000..be16a52
--- /dev/null
+++ b/src/main/java/com/lions/dev/websocket/bridge/ChatKafkaBridge.java
@@ -0,0 +1,86 @@
+package com.lions.dev.websocket.bridge;
+
+import com.lions.dev.dto.events.ChatMessageEvent;
+import com.lions.dev.websocket.ChatWebSocketNext;
+import io.quarkus.logging.Log;
+import jakarta.enterprise.context.ApplicationScoped;
+import org.eclipse.microprofile.reactive.messaging.Incoming;
+import org.eclipse.microprofile.reactive.messaging.Message;
+
+import java.util.UUID;
+import java.util.Map;
+
+/**
+ * Bridge qui consomme depuis Kafka et envoie via WebSocket pour le chat.
+ *
+ * Architecture:
+ * MessageService â Kafka Topic (chat.messages) â Bridge â WebSocket â Client
+ */
+@ApplicationScoped
+public class ChatKafkaBridge {
+
+ /**
+ * Consomme les messages chat depuis Kafka et les route vers WebSocket.
+ */
+ @Incoming("kafka-chat")
+ public void processChatMessage(Message message) {
+ try {
+ ChatMessageEvent event = message.getPayload();
+
+ Log.debug("[CHAT-BRIDGE] Message reçu: " + event.getEventType() +
+ " de " + event.getSenderId() + " Ă " + event.getRecipientId());
+
+ UUID recipientId = UUID.fromString(event.getRecipientId());
+
+ // Construire le message JSON pour WebSocket
+ String wsMessage = buildWebSocketMessage(event);
+
+ // Envoyer via WebSocket au destinataire
+ ChatWebSocketNext.sendMessageToUser(recipientId, wsMessage);
+
+ // Acknowledger le message Kafka
+ message.ack();
+
+ Log.debug("[CHAT-BRIDGE] Message routé vers WebSocket pour: " + event.getRecipientId());
+
+ } catch (IllegalArgumentException e) {
+ Log.error("[CHAT-BRIDGE] UUID invalide dans l'événement", e);
+ message.nack(e);
+ } catch (Exception e) {
+ Log.error("[CHAT-BRIDGE] Erreur traitement événement", e);
+ message.nack(e);
+ }
+ }
+
+ /**
+ * Construit le message JSON pour WebSocket à partir de l'événement Kafka.
+ */
+ private String buildWebSocketMessage(ChatMessageEvent event) {
+ try {
+ com.fasterxml.jackson.databind.ObjectMapper mapper =
+ new com.fasterxml.jackson.databind.ObjectMapper();
+
+ Map messageData = new java.util.HashMap<>();
+ messageData.put("id", event.getMessageId());
+ messageData.put("conversationId", event.getConversationId());
+ messageData.put("senderId", event.getSenderId());
+ messageData.put("recipientId", event.getRecipientId());
+ messageData.put("content", event.getContent());
+ messageData.put("timestamp", event.getTimestamp());
+ if (event.getMetadata() != null) {
+ messageData.putAll(event.getMetadata());
+ }
+
+ java.util.Map wsMessage = java.util.Map.of(
+ "type", event.getEventType() != null ? event.getEventType() : "message",
+ "data", messageData,
+ "timestamp", event.getTimestamp() != null ? event.getTimestamp() : System.currentTimeMillis()
+ );
+
+ return mapper.writeValueAsString(wsMessage);
+ } catch (Exception e) {
+ Log.error("[CHAT-BRIDGE] Erreur construction message WebSocket", e);
+ return "{\"type\":\"error\",\"data\":{\"message\":\"Erreur de traitement\"}}";
+ }
+ }
+}
diff --git a/src/main/java/com/lions/dev/websocket/bridge/NotificationKafkaBridge.java b/src/main/java/com/lions/dev/websocket/bridge/NotificationKafkaBridge.java
new file mode 100644
index 0000000..77e07ff
--- /dev/null
+++ b/src/main/java/com/lions/dev/websocket/bridge/NotificationKafkaBridge.java
@@ -0,0 +1,81 @@
+package com.lions.dev.websocket.bridge;
+
+import com.lions.dev.dto.events.NotificationEvent;
+import com.lions.dev.websocket.NotificationWebSocketNext;
+import io.quarkus.logging.Log;
+import jakarta.enterprise.context.ApplicationScoped;
+import org.eclipse.microprofile.reactive.messaging.Incoming;
+import org.eclipse.microprofile.reactive.messaging.Message;
+
+import java.util.UUID;
+
+/**
+ * Bridge qui consomme depuis Kafka et envoie via WebSocket pour les notifications.
+ *
+ * Architecture:
+ * Services mĂ©tier â Kafka Topic (notifications) â Bridge â WebSocket â Client
+ *
+ * Avantages:
+ * - Découplage: Services ne connaissent pas les WebSockets
+ * - Scalabilité: Plusieurs instances peuvent consommer depuis Kafka
+ * - DurabilitĂ©: Messages persistĂ©s dans Kafka mĂȘme si client dĂ©connectĂ©
+ */
+@ApplicationScoped
+public class NotificationKafkaBridge {
+
+ /**
+ * Consomme les événements depuis Kafka et les route vers WebSocket.
+ *
+ * @param message Message Kafka contenant un NotificationEvent
+ */
+ @Incoming("kafka-notifications")
+ public void processNotification(Message message) {
+ try {
+ NotificationEvent event = message.getPayload();
+
+ Log.debug("[KAFKA-BRIDGE] ĂvĂ©nement reçu: " + event.getType() +
+ " pour utilisateur: " + event.getUserId());
+
+ UUID userId = UUID.fromString(event.getUserId());
+
+ // Construire le message JSON pour WebSocket
+ String wsMessage = buildWebSocketMessage(event);
+
+ // Envoyer via WebSocket
+ NotificationWebSocketNext.sendToUser(userId, wsMessage);
+
+ // Acknowledger le message Kafka
+ message.ack();
+
+ Log.debug("[KAFKA-BRIDGE] Notification routée vers WebSocket pour: " + event.getUserId());
+
+ } catch (IllegalArgumentException e) {
+ Log.error("[KAFKA-BRIDGE] UUID invalide dans l'événement", e);
+ message.nack(e);
+ } catch (Exception e) {
+ Log.error("[KAFKA-BRIDGE] Erreur traitement événement", e);
+ message.nack(e);
+ }
+ }
+
+ /**
+ * Construit le message JSON pour WebSocket à partir de l'événement Kafka.
+ */
+ private String buildWebSocketMessage(NotificationEvent event) {
+ try {
+ com.fasterxml.jackson.databind.ObjectMapper mapper =
+ new com.fasterxml.jackson.databind.ObjectMapper();
+
+ java.util.Map wsMessage = java.util.Map.of(
+ "type", event.getType(),
+ "data", event.getData() != null ? event.getData() : java.util.Map.of(),
+ "timestamp", event.getTimestamp() != null ? event.getTimestamp() : System.currentTimeMillis()
+ );
+
+ return mapper.writeValueAsString(wsMessage);
+ } catch (Exception e) {
+ Log.error("[KAFKA-BRIDGE] Erreur construction message WebSocket", e);
+ return "{\"type\":\"error\",\"data\":{\"message\":\"Erreur de traitement\"}}";
+ }
+ }
+}
diff --git a/src/main/java/com/lions/dev/websocket/bridge/ReactionKafkaBridge.java b/src/main/java/com/lions/dev/websocket/bridge/ReactionKafkaBridge.java
new file mode 100644
index 0000000..89e4af1
--- /dev/null
+++ b/src/main/java/com/lions/dev/websocket/bridge/ReactionKafkaBridge.java
@@ -0,0 +1,93 @@
+package com.lions.dev.websocket.bridge;
+
+import com.lions.dev.dto.events.ReactionEvent;
+import com.lions.dev.websocket.NotificationWebSocketNext;
+import io.quarkus.logging.Log;
+import jakarta.enterprise.context.ApplicationScoped;
+import org.eclipse.microprofile.reactive.messaging.Incoming;
+import org.eclipse.microprofile.reactive.messaging.Message;
+
+import java.util.UUID;
+
+/**
+ * Bridge qui consomme depuis Kafka et envoie via WebSocket pour les réactions.
+ *
+ * Architecture:
+ * SocialPostService/EventService â Kafka Topic (reactions) â Bridge â WebSocket â Client
+ *
+ * Les réactions (likes, comments) sont notifiées en temps réel aux propriétaires
+ * des posts/stories/événements.
+ */
+@ApplicationScoped
+public class ReactionKafkaBridge {
+
+ /**
+ * Consomme les réactions depuis Kafka et les route vers WebSocket.
+ */
+ @Incoming("kafka-reactions")
+ public void processReaction(Message message) {
+ try {
+ ReactionEvent event = message.getPayload();
+
+ Log.debug("[REACTION-BRIDGE] Réaction reçue: " + event.getReactionType() +
+ " sur " + event.getTargetType() + ":" + event.getTargetId());
+
+ // Pour les réactions, on doit notifier le propriétaire du post/story/event
+ // L'ID du propriĂ©taire doit ĂȘtre dans event.getData() sous la clĂ© "ownerId"
+ if (event.getData() != null && event.getData().containsKey("ownerId")) {
+ String ownerId = event.getData().get("ownerId").toString();
+ UUID ownerUUID = UUID.fromString(ownerId);
+
+ // Construire le message JSON pour WebSocket
+ String wsMessage = buildWebSocketMessage(event);
+
+ // Envoyer via WebSocket au propriétaire
+ NotificationWebSocketNext.sendToUser(ownerUUID, wsMessage);
+
+ Log.debug("[REACTION-BRIDGE] Réaction routée vers WebSocket pour propriétaire: " + ownerId);
+ } else {
+ Log.warn("[REACTION-BRIDGE] ownerId manquant dans les données de l'événement");
+ }
+
+ // Acknowledger le message Kafka
+ message.ack();
+
+ } catch (IllegalArgumentException e) {
+ Log.error("[REACTION-BRIDGE] UUID invalide dans l'événement", e);
+ message.nack(e);
+ } catch (Exception e) {
+ Log.error("[REACTION-BRIDGE] Erreur traitement événement", e);
+ message.nack(e);
+ }
+ }
+
+ /**
+ * Construit le message JSON pour WebSocket à partir de l'événement Kafka.
+ */
+ private String buildWebSocketMessage(ReactionEvent event) {
+ try {
+ com.fasterxml.jackson.databind.ObjectMapper mapper =
+ new com.fasterxml.jackson.databind.ObjectMapper();
+
+ java.util.Map reactionData = new java.util.HashMap<>();
+ reactionData.put("targetId", event.getTargetId());
+ reactionData.put("targetType", event.getTargetType());
+ reactionData.put("userId", event.getUserId());
+ reactionData.put("reactionType", event.getReactionType());
+ if (event.getData() != null) {
+ reactionData.putAll(event.getData());
+ }
+
+ java.util.Map wsMessage = java.util.Map.of(
+ "type", "reaction",
+ "data", reactionData,
+ "timestamp", event.getTimestamp() != null ? event.getTimestamp() : System.currentTimeMillis()
+ );
+
+ return mapper.writeValueAsString(wsMessage);
+ } catch (Exception e) {
+ Log.error("[REACTION-BRIDGE] Erreur construction message WebSocket", e);
+ return "{\"type\":\"error\",\"data\":{\"message\":\"Erreur de traitement\"}}";
+ }
+ }
+}
diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties
index 8240e41..490eb5c 100644
--- a/src/main/resources/application-dev.properties
+++ b/src/main/resources/application-dev.properties
@@ -20,6 +20,9 @@ quarkus.datasource.devservices.enabled=false
quarkus.hibernate-orm.database.generation=drop-and-create
quarkus.hibernate-orm.log.sql=true
quarkus.hibernate-orm.format_sql=true
+quarkus.hibernate-orm.packages=com.lions.dev.entity
+# Forcer la création du schéma au démarrage
+quarkus.hibernate-orm.schema-generation.scripts.action=drop-and-create
# ====================================================================
# Logging
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index b3f066a..07e62d5 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -24,3 +24,67 @@ quarkus.http.port=8080
# ====================================================================
quarkus.http.body.uploads-directory=/tmp/uploads
quarkus.http.limits.max-body-size=10M
+
+# ====================================================================
+# WebSockets Next (commun Ă tous les environnements)
+# ====================================================================
+quarkus.websockets-next.server.enabled=true
+
+# ====================================================================
+# Kafka Configuration (commun Ă tous les environnements)
+# ====================================================================
+kafka.bootstrap.servers=${KAFKA_BOOTSTRAP_SERVERS:localhost:9092}
+
+# ====================================================================
+# Kafka Topics - Outgoing (Services â Kafka)
+# ====================================================================
+# Topic: Notifications
+mp.messaging.outgoing.notifications.connector=smallrye-kafka
+mp.messaging.outgoing.notifications.topic=notifications
+mp.messaging.outgoing.notifications.key.serializer=org.apache.kafka.common.serialization.StringSerializer
+mp.messaging.outgoing.notifications.value.serializer=io.quarkus.kafka.client.serialization.JsonbSerializer
+
+# Topic: Chat Messages
+mp.messaging.outgoing.chat-messages.connector=smallrye-kafka
+mp.messaging.outgoing.chat-messages.topic=chat.messages
+mp.messaging.outgoing.chat-messages.key.serializer=org.apache.kafka.common.serialization.StringSerializer
+mp.messaging.outgoing.chat-messages.value.serializer=io.quarkus.kafka.client.serialization.JsonbSerializer
+
+# Topic: Reactions (likes, comments, shares)
+mp.messaging.outgoing.reactions.connector=smallrye-kafka
+mp.messaging.outgoing.reactions.topic=reactions
+mp.messaging.outgoing.reactions.key.serializer=org.apache.kafka.common.serialization.StringSerializer
+mp.messaging.outgoing.reactions.value.serializer=io.quarkus.kafka.client.serialization.JsonbSerializer
+
+# Topic: Presence Updates
+mp.messaging.outgoing.presence.connector=smallrye-kafka
+mp.messaging.outgoing.presence.topic=presence.updates
+mp.messaging.outgoing.presence.key.serializer=org.apache.kafka.common.serialization.StringSerializer
+mp.messaging.outgoing.presence.value.serializer=io.quarkus.kafka.client.serialization.JsonbSerializer
+
+# ====================================================================
+# Kafka Topics - Incoming (Kafka â WebSocket Bridge)
+# ====================================================================
+# Consommer depuis Kafka et router vers WebSocket pour notifications
+mp.messaging.incoming.kafka-notifications.connector=smallrye-kafka
+mp.messaging.incoming.kafka-notifications.topic=notifications
+mp.messaging.incoming.kafka-notifications.group.id=websocket-notifications-bridge
+mp.messaging.incoming.kafka-notifications.key.deserializer=org.apache.kafka.common.serialization.StringDeserializer
+mp.messaging.incoming.kafka-notifications.value.deserializer=io.quarkus.kafka.client.serialization.JsonbDeserializer
+mp.messaging.incoming.kafka-notifications.enable.auto.commit=true
+
+# Consommer depuis Kafka et router vers WebSocket pour chat
+mp.messaging.incoming.kafka-chat.connector=smallrye-kafka
+mp.messaging.incoming.kafka-chat.topic=chat.messages
+mp.messaging.incoming.kafka-chat.group.id=websocket-chat-bridge
+mp.messaging.incoming.kafka-chat.key.deserializer=org.apache.kafka.common.serialization.StringDeserializer
+mp.messaging.incoming.kafka-chat.value.deserializer=io.quarkus.kafka.client.serialization.JsonbDeserializer
+mp.messaging.incoming.kafka-chat.enable.auto.commit=true
+
+# Consommer depuis Kafka et router vers WebSocket pour réactions
+mp.messaging.incoming.kafka-reactions.connector=smallrye-kafka
+mp.messaging.incoming.kafka-reactions.topic=reactions
+mp.messaging.incoming.kafka-reactions.group.id=websocket-reactions-bridge
+mp.messaging.incoming.kafka-reactions.key.deserializer=org.apache.kafka.common.serialization.StringDeserializer
+mp.messaging.incoming.kafka-reactions.value.deserializer=io.quarkus.kafka.client.serialization.JsonbDeserializer
+mp.messaging.incoming.kafka-reactions.enable.auto.commit=true
diff --git a/src/main/resources/db/migration/V10__Create_Bookings_Table.sql b/src/main/resources/db/migration/V10__Create_Bookings_Table.sql
new file mode 100644
index 0000000..19d91e5
--- /dev/null
+++ b/src/main/resources/db/migration/V10__Create_Bookings_Table.sql
@@ -0,0 +1,81 @@
+-- Migration V10: Création de la table bookings pour remplacer/étendre reservations
+-- Date: 2026-01-15
+-- Description: Nouvelle table pour les réservations avec plus de détails (v2.0)
+
+-- Note: Cette migration crée une nouvelle table "bookings" qui remplace/étend "reservations"
+-- La table reservations peut ĂȘtre conservĂ©e pour compatibilitĂ© ou migrĂ©e progressivement
+
+CREATE TABLE IF NOT EXISTS bookings (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ establishment_id UUID NOT NULL,
+ user_id UUID NOT NULL,
+ reservation_time TIMESTAMP NOT NULL, -- Date et heure de la réservation
+ guest_count INTEGER NOT NULL DEFAULT 1, -- Nombre de convives
+ status VARCHAR(20) NOT NULL DEFAULT 'PENDING', -- PENDING, CONFIRMED, CANCELLED, COMPLETED
+ special_requests TEXT, -- Demandes spéciales (allergies, préférences, etc.)
+ table_number VARCHAR(20), -- Numéro de table assigné (si applicable)
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
+
+ CONSTRAINT fk_bookings_establishment
+ FOREIGN KEY (establishment_id)
+ REFERENCES establishments(id)
+ ON DELETE CASCADE,
+
+ CONSTRAINT fk_bookings_user
+ FOREIGN KEY (user_id)
+ REFERENCES users(id)
+ ON DELETE CASCADE,
+
+ CONSTRAINT chk_bookings_guest_count
+ CHECK (guest_count > 0),
+
+ CONSTRAINT chk_bookings_status
+ CHECK (status IN ('PENDING', 'CONFIRMED', 'CANCELLED', 'COMPLETED'))
+);
+
+-- Créer les index pour améliorer les performances
+CREATE INDEX IF NOT EXISTS idx_bookings_establishment
+ON bookings(establishment_id);
+
+CREATE INDEX IF NOT EXISTS idx_bookings_user
+ON bookings(user_id);
+
+CREATE INDEX IF NOT EXISTS idx_bookings_status
+ON bookings(status);
+
+CREATE INDEX IF NOT EXISTS idx_bookings_reservation_time
+ON bookings(reservation_time);
+
+CREATE INDEX IF NOT EXISTS idx_bookings_establishment_time
+ON bookings(establishment_id, reservation_time);
+
+-- Index pour les requĂȘtes de rĂ©servations Ă venir
+CREATE INDEX IF NOT EXISTS idx_bookings_upcoming
+ON bookings(establishment_id, reservation_time)
+WHERE reservation_time >= CURRENT_TIMESTAMP AND status IN ('PENDING', 'CONFIRMED');
+
+-- Créer un trigger pour mettre à jour updated_at automatiquement
+CREATE OR REPLACE FUNCTION update_bookings_updated_at()
+RETURNS TRIGGER AS $$
+BEGIN
+ NEW.updated_at = CURRENT_TIMESTAMP;
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE TRIGGER trigger_update_bookings_updated_at
+ BEFORE UPDATE ON bookings
+ FOR EACH ROW
+ EXECUTE FUNCTION update_bookings_updated_at();
+
+-- Commentaires pour documentation
+COMMENT ON TABLE bookings IS 'Réservations d''établissements (v2.0 - remplace/étend reservations)';
+COMMENT ON COLUMN bookings.establishment_id IS 'Ătablissement concernĂ© par la rĂ©servation';
+COMMENT ON COLUMN bookings.user_id IS 'Utilisateur qui a fait la réservation';
+COMMENT ON COLUMN bookings.reservation_time IS 'Date et heure de la réservation';
+COMMENT ON COLUMN bookings.guest_count IS 'Nombre de convives';
+COMMENT ON COLUMN bookings.status IS 'Statut: PENDING, CONFIRMED, CANCELLED, COMPLETED';
+COMMENT ON COLUMN bookings.special_requests IS 'Demandes spéciales (allergies, préférences, etc.)';
+COMMENT ON COLUMN bookings.table_number IS 'Numéro de table assigné (si applicable)';
+
diff --git a/src/main/resources/db/migration/V11__Create_Promotions_Table.sql b/src/main/resources/db/migration/V11__Create_Promotions_Table.sql
new file mode 100644
index 0000000..a2c6d96
--- /dev/null
+++ b/src/main/resources/db/migration/V11__Create_Promotions_Table.sql
@@ -0,0 +1,80 @@
+-- Migration V11: Création de la table promotions pour les offres spéciales
+-- Date: 2026-01-15
+-- Description: Table pour gérer les promotions et happy hours des établissements (v2.0)
+
+CREATE TABLE IF NOT EXISTS promotions (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ establishment_id UUID NOT NULL,
+ title VARCHAR(200) NOT NULL, -- Titre de la promotion (ex: "Happy Hour", "Menu du jour")
+ description TEXT, -- Description détaillée de la promotion
+ promo_code VARCHAR(50) UNIQUE, -- Code promo optionnel (ex: "HAPPY2024")
+ discount_type VARCHAR(20) NOT NULL, -- PERCENTAGE, FIXED_AMOUNT, FREE_ITEM
+ discount_value NUMERIC(10,2) NOT NULL, -- Valeur de la réduction (pourcentage, montant fixe, etc.)
+ valid_from TIMESTAMP NOT NULL, -- Date de début de validité
+ valid_until TIMESTAMP NOT NULL, -- Date de fin de validité
+ is_active BOOLEAN DEFAULT true NOT NULL, -- Indique si la promotion est active
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
+
+ CONSTRAINT fk_promotions_establishment
+ FOREIGN KEY (establishment_id)
+ REFERENCES establishments(id)
+ ON DELETE CASCADE,
+
+ CONSTRAINT chk_promotions_discount_type
+ CHECK (discount_type IN ('PERCENTAGE', 'FIXED_AMOUNT', 'FREE_ITEM')),
+
+ CONSTRAINT chk_promotions_discount_value
+ CHECK (discount_value >= 0),
+
+ CONSTRAINT chk_promotions_valid_dates
+ CHECK (valid_until > valid_from)
+);
+
+-- Créer les index pour améliorer les performances
+CREATE INDEX IF NOT EXISTS idx_promotions_establishment
+ON promotions(establishment_id);
+
+CREATE INDEX IF NOT EXISTS idx_promotions_active
+ON promotions(establishment_id, is_active)
+WHERE is_active = true;
+
+CREATE INDEX IF NOT EXISTS idx_promotions_valid_dates
+ON promotions(establishment_id, valid_from, valid_until);
+
+-- Index pour les promotions actives et valides
+CREATE INDEX IF NOT EXISTS idx_promotions_active_valid
+ON promotions(establishment_id, valid_from, valid_until)
+WHERE is_active = true AND valid_from <= CURRENT_TIMESTAMP AND valid_until >= CURRENT_TIMESTAMP;
+
+-- Index pour les recherches par code promo
+CREATE INDEX IF NOT EXISTS idx_promotions_promo_code
+ON promotions(promo_code)
+WHERE promo_code IS NOT NULL;
+
+-- Créer un trigger pour mettre à jour updated_at automatiquement
+CREATE OR REPLACE FUNCTION update_promotions_updated_at()
+RETURNS TRIGGER AS $$
+BEGIN
+ NEW.updated_at = CURRENT_TIMESTAMP;
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE TRIGGER trigger_update_promotions_updated_at
+ BEFORE UPDATE ON promotions
+ FOR EACH ROW
+ EXECUTE FUNCTION update_promotions_updated_at();
+
+-- Commentaires pour documentation
+COMMENT ON TABLE promotions IS 'Promotions et offres spéciales des établissements (v2.0)';
+COMMENT ON COLUMN promotions.establishment_id IS 'Ătablissement qui propose la promotion';
+COMMENT ON COLUMN promotions.title IS 'Titre de la promotion';
+COMMENT ON COLUMN promotions.description IS 'Description détaillée de la promotion';
+COMMENT ON COLUMN promotions.promo_code IS 'Code promo optionnel pour activer la promotion';
+COMMENT ON COLUMN promotions.discount_type IS 'Type de réduction: PERCENTAGE, FIXED_AMOUNT, FREE_ITEM';
+COMMENT ON COLUMN promotions.discount_value IS 'Valeur de la réduction (pourcentage, montant fixe, etc.)';
+COMMENT ON COLUMN promotions.valid_from IS 'Date de début de validité';
+COMMENT ON COLUMN promotions.valid_until IS 'Date de fin de validité';
+COMMENT ON COLUMN promotions.is_active IS 'Indique si la promotion est active';
+
diff --git a/src/main/resources/db/migration/V3__Migrate_Users_To_V2.sql b/src/main/resources/db/migration/V3__Migrate_Users_To_V2.sql
new file mode 100644
index 0000000..b8523de
--- /dev/null
+++ b/src/main/resources/db/migration/V3__Migrate_Users_To_V2.sql
@@ -0,0 +1,48 @@
+-- Migration V3: Migration de la table users vers l'architecture v2.0
+-- Date: 2026-01-15
+-- Description: Renommage des colonnes et ajout des nouveaux champs pour l'entité User
+
+-- Renommer les colonnes existantes
+ALTER TABLE users RENAME COLUMN nom TO first_name;
+ALTER TABLE users RENAME COLUMN prenoms TO last_name;
+ALTER TABLE users RENAME COLUMN mot_de_passe TO password_hash;
+
+-- Ajouter les nouvelles colonnes
+ALTER TABLE users ADD COLUMN IF NOT EXISTS bio VARCHAR(500);
+ALTER TABLE users ADD COLUMN IF NOT EXISTS loyalty_points INTEGER DEFAULT 0 NOT NULL;
+
+-- Ajouter la colonne preferences en JSONB (PostgreSQL)
+ALTER TABLE users ADD COLUMN IF NOT EXISTS preferences JSONB DEFAULT '{}'::jsonb NOT NULL;
+
+-- Migrer les données de preferred_category vers preferences si nécessaire
+-- Note: Cette migration préserve les données existantes
+UPDATE users
+SET preferences = jsonb_build_object('preferred_category', preferred_category)
+WHERE preferred_category IS NOT NULL
+ AND (preferences IS NULL OR preferences = '{}'::jsonb);
+
+-- Supprimer la colonne obsolĂšte preferred_category
+-- Note: On vérifie d'abord si la colonne existe pour éviter les erreurs
+DO $$
+BEGIN
+ IF EXISTS (
+ SELECT 1
+ FROM information_schema.columns
+ WHERE table_name = 'users'
+ AND column_name = 'preferred_category'
+ ) THEN
+ ALTER TABLE users DROP COLUMN preferred_category;
+ END IF;
+END $$;
+
+-- CrĂ©er un index sur preferences si nĂ©cessaire (pour les requĂȘtes JSONB)
+CREATE INDEX IF NOT EXISTS idx_users_preferences ON users USING GIN (preferences);
+
+-- Commentaires pour documentation
+COMMENT ON COLUMN users.first_name IS 'Prénom de l''utilisateur (v2.0)';
+COMMENT ON COLUMN users.last_name IS 'Nom de famille de l''utilisateur (v2.0)';
+COMMENT ON COLUMN users.password_hash IS 'Mot de passe hashé avec BCrypt (v2.0)';
+COMMENT ON COLUMN users.bio IS 'Biographie courte de l''utilisateur (v2.0)';
+COMMENT ON COLUMN users.loyalty_points IS 'Points de fidélité accumulés (v2.0)';
+COMMENT ON COLUMN users.preferences IS 'Préférences utilisateur en JSONB (v2.0)';
+
diff --git a/src/main/resources/db/migration/V4__Migrate_Establishments_To_V2.sql b/src/main/resources/db/migration/V4__Migrate_Establishments_To_V2.sql
new file mode 100644
index 0000000..2d64cf9
--- /dev/null
+++ b/src/main/resources/db/migration/V4__Migrate_Establishments_To_V2.sql
@@ -0,0 +1,74 @@
+-- Migration V4: Migration de la table establishments vers l'architecture v2.0
+-- Date: 2026-01-15
+-- Description: Renommage des colonnes, ajout de verification_status, suppression des colonnes obsolĂštes
+
+-- Renommer la colonne total_ratings_count vers total_reviews_count
+ALTER TABLE establishments RENAME COLUMN total_ratings_count TO total_reviews_count;
+
+-- Ajouter la colonne verification_status
+ALTER TABLE establishments ADD COLUMN IF NOT EXISTS verification_status VARCHAR(20) DEFAULT 'PENDING' NOT NULL;
+
+-- Supprimer les colonnes obsolÚtes (avec vérification pour éviter les erreurs)
+DO $$
+BEGIN
+ -- Supprimer rating (déprécié, utiliser average_rating)
+ IF EXISTS (
+ SELECT 1 FROM information_schema.columns
+ WHERE table_name = 'establishments' AND column_name = 'rating'
+ ) THEN
+ ALTER TABLE establishments DROP COLUMN rating;
+ END IF;
+
+ -- Supprimer email (délégué à la table users via manager_id)
+ IF EXISTS (
+ SELECT 1 FROM information_schema.columns
+ WHERE table_name = 'establishments' AND column_name = 'email'
+ ) THEN
+ ALTER TABLE establishments DROP COLUMN email;
+ END IF;
+
+ -- Supprimer image_url (utiliser establishment_media Ă la place)
+ IF EXISTS (
+ SELECT 1 FROM information_schema.columns
+ WHERE table_name = 'establishments' AND column_name = 'image_url'
+ ) THEN
+ ALTER TABLE establishments DROP COLUMN image_url;
+ END IF;
+
+ -- Supprimer capacity (non utilisé dans v2.0)
+ IF EXISTS (
+ SELECT 1 FROM information_schema.columns
+ WHERE table_name = 'establishments' AND column_name = 'capacity'
+ ) THEN
+ ALTER TABLE establishments DROP COLUMN capacity;
+ END IF;
+
+ -- Supprimer amenities (délégué à la table establishment_amenities)
+ IF EXISTS (
+ SELECT 1 FROM information_schema.columns
+ WHERE table_name = 'establishments' AND column_name = 'amenities'
+ ) THEN
+ -- Optionnel: Migrer les données vers establishment_amenities avant suppression
+ -- (à implémenter si nécessaire)
+ ALTER TABLE establishments DROP COLUMN amenities;
+ END IF;
+
+ -- Supprimer opening_hours (délégué à la table business_hours)
+ IF EXISTS (
+ SELECT 1 FROM information_schema.columns
+ WHERE table_name = 'establishments' AND column_name = 'opening_hours'
+ ) THEN
+ -- Optionnel: Migrer les données vers business_hours avant suppression
+ -- (à implémenter si nécessaire)
+ ALTER TABLE establishments DROP COLUMN opening_hours;
+ END IF;
+END $$;
+
+-- CrĂ©er un index sur verification_status pour les requĂȘtes de filtrage
+CREATE INDEX IF NOT EXISTS idx_establishments_verification_status
+ON establishments(verification_status);
+
+-- Commentaires pour documentation
+COMMENT ON COLUMN establishments.total_reviews_count IS 'Nombre total d''avis (v2.0 - renommé depuis total_ratings_count)';
+COMMENT ON COLUMN establishments.verification_status IS 'Statut de vérification: PENDING, VERIFIED, REJECTED (v2.0)';
+
diff --git a/src/main/resources/db/migration/V5__Create_Business_Hours_Table.sql b/src/main/resources/db/migration/V5__Create_Business_Hours_Table.sql
new file mode 100644
index 0000000..b9be2bc
--- /dev/null
+++ b/src/main/resources/db/migration/V5__Create_Business_Hours_Table.sql
@@ -0,0 +1,64 @@
+-- Migration V5: Création de la table business_hours pour gérer les horaires d'ouverture
+-- Date: 2026-01-15
+-- Description: Table dédiée pour les horaires d'ouverture des établissements (v2.0)
+
+CREATE TABLE IF NOT EXISTS business_hours (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ establishment_id UUID NOT NULL,
+ day_of_week VARCHAR(20) NOT NULL, -- MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
+ open_time VARCHAR(5) NOT NULL, -- Format HH:MM (ex: "09:00")
+ close_time VARCHAR(5) NOT NULL, -- Format HH:MM (ex: "18:00")
+ is_closed BOOLEAN DEFAULT false NOT NULL,
+ is_exception BOOLEAN DEFAULT false NOT NULL, -- Pour les jours exceptionnels (fermetures temporaires)
+ exception_date TIMESTAMP, -- Date de l'exception (si is_exception = true)
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
+
+ CONSTRAINT fk_business_hours_establishment
+ FOREIGN KEY (establishment_id)
+ REFERENCES establishments(id)
+ ON DELETE CASCADE,
+
+ CONSTRAINT chk_business_hours_time_format
+ CHECK (open_time ~ '^([0-1][0-9]|2[0-3]):[0-5][0-9]$'
+ AND close_time ~ '^([0-1][0-9]|2[0-3]):[0-5][0-9]$'),
+
+ CONSTRAINT chk_business_hours_day_of_week
+ CHECK (day_of_week IN ('MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY', 'SUNDAY'))
+);
+
+-- Créer les index pour améliorer les performances
+CREATE INDEX IF NOT EXISTS idx_business_hours_establishment
+ON business_hours(establishment_id);
+
+CREATE INDEX IF NOT EXISTS idx_business_hours_day_of_week
+ON business_hours(day_of_week);
+
+CREATE INDEX IF NOT EXISTS idx_business_hours_exception
+ON business_hours(establishment_id, exception_date)
+WHERE is_exception = true;
+
+-- Créer un trigger pour mettre à jour updated_at automatiquement
+CREATE OR REPLACE FUNCTION update_business_hours_updated_at()
+RETURNS TRIGGER AS $$
+BEGIN
+ NEW.updated_at = CURRENT_TIMESTAMP;
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE TRIGGER trigger_update_business_hours_updated_at
+ BEFORE UPDATE ON business_hours
+ FOR EACH ROW
+ EXECUTE FUNCTION update_business_hours_updated_at();
+
+-- Commentaires pour documentation
+COMMENT ON TABLE business_hours IS 'Horaires d''ouverture des établissements (v2.0)';
+COMMENT ON COLUMN business_hours.establishment_id IS 'Référence vers l''établissement';
+COMMENT ON COLUMN business_hours.day_of_week IS 'Jour de la semaine (MONDAY Ă SUNDAY)';
+COMMENT ON COLUMN business_hours.open_time IS 'Heure d''ouverture (format HH:MM)';
+COMMENT ON COLUMN business_hours.close_time IS 'Heure de fermeture (format HH:MM)';
+COMMENT ON COLUMN business_hours.is_closed IS 'Indique si l''établissement est fermé ce jour';
+COMMENT ON COLUMN business_hours.is_exception IS 'Indique si c''est un jour exceptionnel';
+COMMENT ON COLUMN business_hours.exception_date IS 'Date de l''exception (si is_exception = true)';
+
diff --git a/src/main/resources/db/migration/V6__Create_Establishment_Amenities_Table.sql b/src/main/resources/db/migration/V6__Create_Establishment_Amenities_Table.sql
new file mode 100644
index 0000000..1b80c90
--- /dev/null
+++ b/src/main/resources/db/migration/V6__Create_Establishment_Amenities_Table.sql
@@ -0,0 +1,65 @@
+-- Migration V6: Création de la table establishment_amenities pour gérer les équipements
+-- Date: 2026-01-15
+-- Description: Table de liaison pour les équipements des établissements (v2.0)
+
+-- Créer d'abord une table de référence pour les types d'équipements (optionnel mais recommandé)
+CREATE TABLE IF NOT EXISTS amenity_types (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ name VARCHAR(100) NOT NULL UNIQUE, -- Ex: "WiFi", "Parking", "Terrasse", "Climatisation"
+ category VARCHAR(50), -- Ex: "Comfort", "Accessibility", "Entertainment"
+ icon VARCHAR(50), -- Nom de l'icĂŽne Ă utiliser dans l'UI
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
+
+ CONSTRAINT chk_amenity_types_name_not_empty CHECK (LENGTH(TRIM(name)) > 0)
+);
+
+-- Créer la table de liaison establishment_amenities
+CREATE TABLE IF NOT EXISTS establishment_amenities (
+ establishment_id UUID NOT NULL,
+ amenity_id UUID NOT NULL,
+ details VARCHAR(500), -- Détails supplémentaires (ex: "Parking gratuit pour 20 voitures")
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
+
+ PRIMARY KEY (establishment_id, amenity_id),
+
+ CONSTRAINT fk_establishment_amenities_establishment
+ FOREIGN KEY (establishment_id)
+ REFERENCES establishments(id)
+ ON DELETE CASCADE,
+
+ CONSTRAINT fk_establishment_amenities_amenity
+ FOREIGN KEY (amenity_id)
+ REFERENCES amenity_types(id)
+ ON DELETE CASCADE
+);
+
+-- Créer les index pour améliorer les performances
+CREATE INDEX IF NOT EXISTS idx_establishment_amenities_establishment
+ON establishment_amenities(establishment_id);
+
+CREATE INDEX IF NOT EXISTS idx_establishment_amenities_amenity
+ON establishment_amenities(amenity_id);
+
+-- Insérer quelques types d'équipements courants (optionnel)
+INSERT INTO amenity_types (name, category, icon) VALUES
+ ('WiFi', 'Comfort', 'wifi'),
+ ('Parking', 'Accessibility', 'parking'),
+ ('Terrasse', 'Comfort', 'terrace'),
+ ('Climatisation', 'Comfort', 'ac'),
+ ('Chauffage', 'Comfort', 'heating'),
+ ('Accessible PMR', 'Accessibility', 'accessible'),
+ ('Animaux acceptés', 'Comfort', 'pets'),
+ ('Réservation en ligne', 'Service', 'online_booking'),
+ ('Service de livraison', 'Service', 'delivery'),
+ ('Bar', 'Entertainment', 'bar'),
+ ('Musique live', 'Entertainment', 'live_music'),
+ ('Ăcrans TV', 'Entertainment', 'tv')
+ON CONFLICT (name) DO NOTHING; -- Ăviter les doublons
+
+-- Commentaires pour documentation
+COMMENT ON TABLE amenity_types IS 'Types d''équipements disponibles pour les établissements (v2.0)';
+COMMENT ON TABLE establishment_amenities IS 'Liaison entre établissements et équipements (v2.0)';
+COMMENT ON COLUMN establishment_amenities.establishment_id IS 'Référence vers l''établissement';
+COMMENT ON COLUMN establishment_amenities.amenity_id IS 'Référence vers le type d''équipement';
+COMMENT ON COLUMN establishment_amenities.details IS 'Détails supplémentaires sur l''équipement';
+
diff --git a/src/main/resources/db/migration/V7__Migrate_Events_To_V2.sql b/src/main/resources/db/migration/V7__Migrate_Events_To_V2.sql
new file mode 100644
index 0000000..8eeece1
--- /dev/null
+++ b/src/main/resources/db/migration/V7__Migrate_Events_To_V2.sql
@@ -0,0 +1,55 @@
+-- Migration V7: Migration de la table events vers l'architecture v2.0
+-- Date: 2026-01-15
+-- Description: Ajout de establishment_id, is_private, waitlist_enabled et suppression de location
+
+-- Ajouter la colonne establishment_id (FK vers establishments)
+ALTER TABLE events ADD COLUMN IF NOT EXISTS establishment_id UUID;
+
+-- Créer la contrainte de clé étrangÚre pour establishment_id
+DO $$
+BEGIN
+ IF NOT EXISTS (
+ SELECT 1 FROM pg_constraint
+ WHERE conname = 'fk_events_establishment'
+ ) THEN
+ ALTER TABLE events
+ ADD CONSTRAINT fk_events_establishment
+ FOREIGN KEY (establishment_id)
+ REFERENCES establishments(id)
+ ON DELETE SET NULL; -- Si l'établissement est supprimé, mettre NULL plutÎt que supprimer l'événement
+ END IF;
+END $$;
+
+-- Ajouter la colonne is_private
+ALTER TABLE events ADD COLUMN IF NOT EXISTS is_private BOOLEAN DEFAULT false NOT NULL;
+
+-- Ajouter la colonne waitlist_enabled
+ALTER TABLE events ADD COLUMN IF NOT EXISTS waitlist_enabled BOOLEAN DEFAULT false NOT NULL;
+
+-- Créer un index sur establishment_id pour améliorer les performances
+CREATE INDEX IF NOT EXISTS idx_events_establishment
+ON events(establishment_id);
+
+-- CrĂ©er un index sur is_private pour les requĂȘtes de filtrage
+CREATE INDEX IF NOT EXISTS idx_events_is_private
+ON events(is_private);
+
+-- Supprimer la colonne location (déléguée à Establishment via establishment_id)
+DO $$
+BEGIN
+ -- Optionnel: Migrer les données de location vers establishment_id si possible
+ -- (nécessite une logique de mapping personnalisée)
+
+ IF EXISTS (
+ SELECT 1 FROM information_schema.columns
+ WHERE table_name = 'events' AND column_name = 'location'
+ ) THEN
+ ALTER TABLE events DROP COLUMN location;
+ END IF;
+END $$;
+
+-- Commentaires pour documentation
+COMMENT ON COLUMN events.establishment_id IS 'RĂ©fĂ©rence vers l''Ă©tablissement oĂč se dĂ©roule l''Ă©vĂ©nement (v2.0)';
+COMMENT ON COLUMN events.is_private IS 'Indique si l''événement est privé (v2.0)';
+COMMENT ON COLUMN events.waitlist_enabled IS 'Indique si la liste d''attente est activée (v2.0)';
+
diff --git a/src/main/resources/db/migration/V8__Create_Reviews_Table.sql b/src/main/resources/db/migration/V8__Create_Reviews_Table.sql
new file mode 100644
index 0000000..dbf4100
--- /dev/null
+++ b/src/main/resources/db/migration/V8__Create_Reviews_Table.sql
@@ -0,0 +1,97 @@
+-- Migration V8: Création de la table reviews pour remplacer establishment_ratings
+-- Date: 2026-01-15
+-- Description: Nouvelle table pour les avis détaillés avec critÚres (v2.0)
+
+-- Note: Cette migration crée une nouvelle table "reviews" qui remplace "establishment_ratings"
+-- La table establishment_ratings peut ĂȘtre conservĂ©e pour compatibilitĂ© ou migrĂ©e progressivement
+
+CREATE TABLE IF NOT EXISTS reviews (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ user_id UUID NOT NULL,
+ establishment_id UUID NOT NULL,
+ overall_rating INTEGER NOT NULL, -- Note globale (1-5)
+ comment TEXT, -- Commentaire libre
+ criteria_ratings JSONB DEFAULT '{}'::jsonb NOT NULL, -- Notes par critĂšres (ex: {"ambiance": 4, "service": 5, "qualite": 4})
+ is_verified_visit BOOLEAN DEFAULT false NOT NULL, -- Indique si l'avis est lié à une visite vérifiée (réservation complétée)
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
+
+ CONSTRAINT fk_reviews_user
+ FOREIGN KEY (user_id)
+ REFERENCES users(id)
+ ON DELETE CASCADE,
+
+ CONSTRAINT fk_reviews_establishment
+ FOREIGN KEY (establishment_id)
+ REFERENCES establishments(id)
+ ON DELETE CASCADE,
+
+ CONSTRAINT chk_reviews_overall_rating_range
+ CHECK (overall_rating >= 1 AND overall_rating <= 5),
+
+ -- Un utilisateur ne peut avoir qu'un seul avis par établissement
+ CONSTRAINT uq_reviews_user_establishment
+ UNIQUE (user_id, establishment_id)
+);
+
+-- Créer les index pour améliorer les performances
+CREATE INDEX IF NOT EXISTS idx_reviews_user
+ON reviews(user_id);
+
+CREATE INDEX IF NOT EXISTS idx_reviews_establishment
+ON reviews(establishment_id);
+
+CREATE INDEX IF NOT EXISTS idx_reviews_overall_rating
+ON reviews(establishment_id, overall_rating);
+
+CREATE INDEX IF NOT EXISTS idx_reviews_created_at
+ON reviews(created_at DESC);
+
+-- Index GIN pour les requĂȘtes JSONB sur criteria_ratings
+CREATE INDEX IF NOT EXISTS idx_reviews_criteria_ratings
+ON reviews USING GIN (criteria_ratings);
+
+-- Index pour les avis vérifiés
+CREATE INDEX IF NOT EXISTS idx_reviews_verified_visit
+ON reviews(establishment_id, is_verified_visit)
+WHERE is_verified_visit = true;
+
+-- Créer un trigger pour mettre à jour updated_at automatiquement
+CREATE OR REPLACE FUNCTION update_reviews_updated_at()
+RETURNS TRIGGER AS $$
+BEGIN
+ NEW.updated_at = CURRENT_TIMESTAMP;
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE TRIGGER trigger_update_reviews_updated_at
+ BEFORE UPDATE ON reviews
+ FOR EACH ROW
+ EXECUTE FUNCTION update_reviews_updated_at();
+
+-- Créer une vue pour calculer les statistiques d'avis par établissement
+CREATE OR REPLACE VIEW establishment_review_stats AS
+SELECT
+ establishment_id,
+ COUNT(*) as total_reviews,
+ AVG(overall_rating)::NUMERIC(3,2) as average_rating,
+ COUNT(*) FILTER (WHERE is_verified_visit = true) as verified_reviews_count,
+ COUNT(*) FILTER (WHERE overall_rating = 5) as five_star_count,
+ COUNT(*) FILTER (WHERE overall_rating = 4) as four_star_count,
+ COUNT(*) FILTER (WHERE overall_rating = 3) as three_star_count,
+ COUNT(*) FILTER (WHERE overall_rating = 2) as two_star_count,
+ COUNT(*) FILTER (WHERE overall_rating = 1) as one_star_count
+FROM reviews
+GROUP BY establishment_id;
+
+-- Commentaires pour documentation
+COMMENT ON TABLE reviews IS 'Avis détaillés sur les établissements (v2.0 - remplace establishment_ratings)';
+COMMENT ON COLUMN reviews.user_id IS 'Utilisateur qui a écrit l''avis';
+COMMENT ON COLUMN reviews.establishment_id IS 'Ătablissement concernĂ© par l''avis';
+COMMENT ON COLUMN reviews.overall_rating IS 'Note globale sur 5';
+COMMENT ON COLUMN reviews.comment IS 'Commentaire libre de l''utilisateur';
+COMMENT ON COLUMN reviews.criteria_ratings IS 'Notes par critĂšres en JSONB (ex: {"ambiance": 4, "service": 5})';
+COMMENT ON COLUMN reviews.is_verified_visit IS 'Indique si l''avis est lié à une visite vérifiée (réservation complétée)';
+COMMENT ON VIEW establishment_review_stats IS 'Statistiques d''avis par établissement (calculées automatiquement)';
+
diff --git a/src/main/resources/db/migration/V9__Create_Reactions_Table.sql b/src/main/resources/db/migration/V9__Create_Reactions_Table.sql
new file mode 100644
index 0000000..cc776e8
--- /dev/null
+++ b/src/main/resources/db/migration/V9__Create_Reactions_Table.sql
@@ -0,0 +1,64 @@
+-- Migration V9: Création de la table reactions pour remplacer les compteurs de réactions
+-- Date: 2026-01-15
+-- Description: Table pour gérer les réactions des utilisateurs (like, love, etc.) sur différents contenus (v2.0)
+
+CREATE TABLE IF NOT EXISTS reactions (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ user_id UUID NOT NULL,
+ reaction_type VARCHAR(20) NOT NULL, -- LIKE, LOVE, HAHA, WOW, SAD, ANGRY
+ target_type VARCHAR(20) NOT NULL, -- POST, EVENT, COMMENT, REVIEW
+ target_id UUID NOT NULL, -- ID du contenu ciblé (post_id, event_id, comment_id, review_id)
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
+
+ CONSTRAINT fk_reactions_user
+ FOREIGN KEY (user_id)
+ REFERENCES users(id)
+ ON DELETE CASCADE,
+
+ CONSTRAINT chk_reactions_type
+ CHECK (reaction_type IN ('LIKE', 'LOVE', 'HAHA', 'WOW', 'SAD', 'ANGRY')),
+
+ CONSTRAINT chk_reactions_target_type
+ CHECK (target_type IN ('POST', 'EVENT', 'COMMENT', 'REVIEW')),
+
+ -- Un utilisateur ne peut avoir qu'une seule réaction par contenu
+ -- (mais peut changer de type de réaction)
+ CONSTRAINT uq_reactions_user_target
+ UNIQUE (user_id, target_type, target_id)
+);
+
+-- Créer les index pour améliorer les performances
+CREATE INDEX IF NOT EXISTS idx_reactions_user
+ON reactions(user_id);
+
+CREATE INDEX IF NOT EXISTS idx_reactions_target
+ON reactions(target_type, target_id);
+
+CREATE INDEX IF NOT EXISTS idx_reactions_type
+ON reactions(reaction_type);
+
+CREATE INDEX IF NOT EXISTS idx_reactions_created_at
+ON reactions(created_at DESC);
+
+-- Index composite pour les requĂȘtes frĂ©quentes (compter les rĂ©actions par type et cible)
+CREATE INDEX IF NOT EXISTS idx_reactions_target_type
+ON reactions(target_type, target_id, reaction_type);
+
+-- Créer une vue pour compter les réactions par type et cible
+CREATE OR REPLACE VIEW reaction_counts AS
+SELECT
+ target_type,
+ target_id,
+ reaction_type,
+ COUNT(*) as count
+FROM reactions
+GROUP BY target_type, target_id, reaction_type;
+
+-- Commentaires pour documentation
+COMMENT ON TABLE reactions IS 'Réactions des utilisateurs sur différents contenus (v2.0 - remplace les compteurs)';
+COMMENT ON COLUMN reactions.user_id IS 'Utilisateur qui a réagi';
+COMMENT ON COLUMN reactions.reaction_type IS 'Type de réaction: LIKE, LOVE, HAHA, WOW, SAD, ANGRY';
+COMMENT ON COLUMN reactions.target_type IS 'Type de contenu ciblé: POST, EVENT, COMMENT, REVIEW';
+COMMENT ON COLUMN reactions.target_id IS 'ID du contenu ciblé';
+COMMENT ON VIEW reaction_counts IS 'Compteurs de réactions par type et cible (calculés automatiquement)';
+