From 0240442671e21fd8f5b0d6b7353f514178e7f7a6 Mon Sep 17 00:00:00 2001 From: dahoud Date: Sat, 31 Jan 2026 20:27:27 +0000 Subject: [PATCH] fix(build): switch from uber-jar to fast-jar for Docker compatibility - Change quarkus.package.type from uber-jar to fast-jar - Add EventShare entity and migration for share tracking - Add establishment capacity field - Improve event and establishment services - Add comprehensive tests Co-Authored-By: Claude Opus 4.5 --- pom.xml | 2 +- .../EstablishmentResponseDTO.java | 25 +- .../events/EventCreateResponseDTO.java | 8 +- .../entity/establishment/Establishment.java | 4 + .../lions/dev/entity/events/EventShare.java | 41 ++++ .../com/lions/dev/entity/events/Events.java | 10 + .../dev/repository/EventShareRepository.java | 15 ++ .../dev/repository/EventsRepository.java | 12 + .../dev/resource/EstablishmentResource.java | 12 +- .../lions/dev/resource/EventsResource.java | 38 ++- .../dev/service/EstablishmentService.java | 4 +- .../lions/dev/service/FriendshipService.java | 80 ++++--- .../dev/websocket/ChatWebSocketNext.java | 19 +- .../dev/websocket/bridge/ChatKafkaBridge.java | 13 +- .../V18__Create_Event_Shares_Table.sql | 21 ++ .../V19__Add_Establishment_Capacity.sql | 6 + .../dev/repository/EventsRepositoryTest.java | 55 +++++ .../dev/resource/EventsResourceTest.java | 51 ++++ .../EstablishmentRatingServiceTest.java | 220 ++++++++++++++++++ 19 files changed, 574 insertions(+), 62 deletions(-) create mode 100644 src/main/java/com/lions/dev/entity/events/EventShare.java create mode 100644 src/main/java/com/lions/dev/repository/EventShareRepository.java create mode 100644 src/main/resources/db/migration/V18__Create_Event_Shares_Table.sql create mode 100644 src/main/resources/db/migration/V19__Add_Establishment_Capacity.sql create mode 100644 src/test/java/com/lions/dev/repository/EventsRepositoryTest.java create mode 100644 src/test/java/com/lions/dev/resource/EventsResourceTest.java create mode 100644 src/test/java/com/lions/dev/service/EstablishmentRatingServiceTest.java diff --git a/pom.xml b/pom.xml index 01466b2..b38e6af 100644 --- a/pom.xml +++ b/pom.xml @@ -13,7 +13,7 @@ quarkus-bom io.quarkus.platform 3.16.3 - uber-jar + fast-jar true 3.5.0 diff --git a/src/main/java/com/lions/dev/dto/response/establishment/EstablishmentResponseDTO.java b/src/main/java/com/lions/dev/dto/response/establishment/EstablishmentResponseDTO.java index 4179a31..194db3f 100644 --- a/src/main/java/com/lions/dev/dto/response/establishment/EstablishmentResponseDTO.java +++ b/src/main/java/com/lions/dev/dto/response/establishment/EstablishmentResponseDTO.java @@ -41,6 +41,11 @@ public class EstablishmentResponseDTO { private LocalDateTime createdAt; private LocalDateTime updatedAt; + /** Nombre maximum de places dans l'établissement (optionnel). */ + private Integer capacity; + /** Places restantes (capacity - participants des événements ouverts/à venir). Null si capacity non défini. */ + private Integer remainingPlaces; + // Champs dépréciés (v1.0) - conservés pour compatibilité /** * @deprecated Utiliser {@link #averageRating} à la place. @@ -60,12 +65,6 @@ public class EstablishmentResponseDTO { @Deprecated private String imageUrl; - /** - * @deprecated Supprimé en v2.0. - */ - @Deprecated - private Integer capacity; - /** * @deprecated Supprimé en v2.0 (utiliser establishment_amenities à la place). */ @@ -130,14 +129,26 @@ public class EstablishmentResponseDTO { this.createdAt = establishment.getCreatedAt(); this.updatedAt = establishment.getUpdatedAt(); + this.capacity = establishment.getCapacity(); + this.remainingPlaces = null; // Sera renseigné via le constructeur avec occupiedPlaces si besoin + // Compatibilité v1.0 - valeurs null pour les champs dépréciés this.rating = null; this.email = null; this.imageUrl = null; - this.capacity = null; this.amenities = null; this.openingHours = null; this.totalRatingsCount = this.totalReviewsCount; // Alias pour compatibilité } + + /** + * Constructeur avec calcul des places restantes (capacity - participants des événements ouverts/à venir). + */ + public EstablishmentResponseDTO(Establishment establishment, Integer occupiedPlaces) { + this(establishment); + if (this.capacity != null && occupiedPlaces != null) { + this.remainingPlaces = Math.max(0, this.capacity - occupiedPlaces); + } + } } diff --git a/src/main/java/com/lions/dev/dto/response/events/EventCreateResponseDTO.java b/src/main/java/com/lions/dev/dto/response/events/EventCreateResponseDTO.java index 87c69c0..23aa066 100644 --- a/src/main/java/com/lions/dev/dto/response/events/EventCreateResponseDTO.java +++ b/src/main/java/com/lions/dev/dto/response/events/EventCreateResponseDTO.java @@ -36,6 +36,9 @@ public class EventCreateResponseDTO { private Boolean waitlistEnabled; // v2.0 - Indique si la liste d'attente est activée private Integer maxParticipants; // Nombre maximum de participants autorisés private Integer participationFee; // Frais de participation en centimes + private Integer participantsCount; // ✅ Nombre actuel de participants (event.getParticipants().size()) + private Integer commentsCount; // ✅ Nombre de commentaires (event.getComments().size()) + private Integer sharesCount; // ✅ Nombre de partages (event.getShares().size()) private Long reactionsCount; // ✅ Nombre de réactions (utilisateurs qui ont cet événement en favori) private Boolean isFavorite; // ✅ Indique si l'utilisateur actuel a cet événement en favori (optionnel, dépend du contexte) @@ -68,7 +71,10 @@ public class EventCreateResponseDTO { this.waitlistEnabled = event.getWaitlistEnabled(); // v2.0 this.maxParticipants = event.getMaxParticipants(); this.participationFee = event.getParticipationFee(); - + this.participantsCount = event.getParticipants() != null ? event.getParticipants().size() : 0; + this.commentsCount = event.getComments() != null ? event.getComments().size() : 0; + this.sharesCount = event.getShares() != null ? event.getShares().size() : 0; + // ✅ Calculer reactionsCount si usersRepository est fourni if (usersRepository != null) { this.reactionsCount = usersRepository.countUsersWithFavoriteEvent(event.getId()); 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 dc2387e..4d7dfd3 100644 --- a/src/main/java/com/lions/dev/entity/establishment/Establishment.java +++ b/src/main/java/com/lions/dev/entity/establishment/Establishment.java @@ -72,6 +72,10 @@ public class Establishment extends BaseEntity { @Column(name = "longitude") private Double longitude; // Longitude pour la géolocalisation + /** Nombre maximum de places dans l'établissement (optionnel). Utilisé pour le calcul des places restantes. */ + @Column(name = "capacity") + private Integer capacity; + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "manager_id", nullable = false) private Users manager; // Le responsable de l'établissement diff --git a/src/main/java/com/lions/dev/entity/events/EventShare.java b/src/main/java/com/lions/dev/entity/events/EventShare.java new file mode 100644 index 0000000..6b1292c --- /dev/null +++ b/src/main/java/com/lions/dev/entity/events/EventShare.java @@ -0,0 +1,41 @@ +package com.lions.dev.entity.events; + +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.time.LocalDateTime; + +/** + * Enregistrement d'un partage d'événement par un utilisateur. + * Chaque partage est lié à un utilisateur et à un événement. + */ +@Entity +@Table(name = "event_shares") +@Getter +@Setter +@NoArgsConstructor +@ToString +public class EventShare extends BaseEntity { + + @Column(name = "shared_at", nullable = false) + private LocalDateTime sharedAt; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private Users user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "event_id", nullable = false) + private Events event; + + public EventShare(Users user, Events event) { + this.user = user; + this.event = event; + this.sharedAt = LocalDateTime.now(); + } +} 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 b118d02..3ecac15 100644 --- a/src/main/java/com/lions/dev/entity/events/Events.java +++ b/src/main/java/com/lions/dev/entity/events/Events.java @@ -179,6 +179,9 @@ public class Events extends BaseEntity { @OneToMany(fetch = FetchType.LAZY, mappedBy = "event") private List comments; // Liste des commentaires associés à l'événement + @OneToMany(fetch = FetchType.LAZY, mappedBy = "event", cascade = CascadeType.ALL, orphanRemoval = true) + private List shares = new java.util.ArrayList<>(); // Partages de l'événement + /** * Retourne la liste des commentaires associés à cet événement. * @@ -188,4 +191,11 @@ public class Events extends BaseEntity { return comments; } + /** + * Retourne la liste des partages de cet événement. + */ + public List getShares() { + return shares != null ? shares : new java.util.ArrayList<>(); + } + } diff --git a/src/main/java/com/lions/dev/repository/EventShareRepository.java b/src/main/java/com/lions/dev/repository/EventShareRepository.java new file mode 100644 index 0000000..1e28327 --- /dev/null +++ b/src/main/java/com/lions/dev/repository/EventShareRepository.java @@ -0,0 +1,15 @@ +package com.lions.dev.repository; + +import com.lions.dev.entity.events.EventShare; +import com.lions.dev.entity.events.Events; +import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase; +import jakarta.enterprise.context.ApplicationScoped; + +import java.util.UUID; + +/** + * Repository pour l'entité EventShare (partages d'événements). + */ +@ApplicationScoped +public class EventShareRepository implements PanacheRepositoryBase { +} diff --git a/src/main/java/com/lions/dev/repository/EventsRepository.java b/src/main/java/com/lions/dev/repository/EventsRepository.java index 51f02b2..77efeb2 100644 --- a/src/main/java/com/lions/dev/repository/EventsRepository.java +++ b/src/main/java/com/lions/dev/repository/EventsRepository.java @@ -108,4 +108,16 @@ public class EventsRepository implements PanacheRepositoryBase { public List findEventsStartingBetween(LocalDateTime from, LocalDateTime to) { return list("startDate >= ?1 AND startDate <= ?2", from, to); } + + /** + * Compte le nombre total de participants dans les événements ouverts et à venir + * d'un établissement (pour calcul des places restantes). + */ + public long countParticipantsForEstablishment(UUID establishmentId) { + List events = find("establishment.id = ?1 and status = ?2 and startDate >= ?3", + establishmentId, "OPEN", LocalDateTime.now()).list(); + return events.stream() + .mapToLong(e -> e.getParticipants() != null ? e.getParticipants().size() : 0L) + .sum(); + } } diff --git a/src/main/java/com/lions/dev/resource/EstablishmentResource.java b/src/main/java/com/lions/dev/resource/EstablishmentResource.java index 357d56f..0acc7f1 100644 --- a/src/main/java/com/lions/dev/resource/EstablishmentResource.java +++ b/src/main/java/com/lions/dev/resource/EstablishmentResource.java @@ -10,6 +10,7 @@ import com.lions.dev.entity.users.Users; import com.lions.dev.repository.BusinessHoursRepository; import com.lions.dev.repository.EstablishmentAmenityRepository; import com.lions.dev.repository.EstablishmentRepository; +import com.lions.dev.repository.EventsRepository; import com.lions.dev.repository.UsersRepository; import com.lions.dev.service.EstablishmentService; import com.lions.dev.util.UserRoles; @@ -53,6 +54,9 @@ public class EstablishmentResource { @Inject EstablishmentAmenityRepository establishmentAmenityRepository; + @Inject + EventsRepository eventsRepository; + private static final Logger LOG = Logger.getLogger(EstablishmentResource.class); // *********** Création d'un établissement *********** @@ -115,6 +119,7 @@ public class EstablishmentResource { establishment.setPriceRange(requestDTO.getPriceRange()); establishment.setLatitude(requestDTO.getLatitude()); establishment.setLongitude(requestDTO.getLongitude()); + establishment.setCapacity(requestDTO.getCapacity()); Establishment createdEstablishment = establishmentService.createEstablishment(establishment, requestDTO.getManagerId()); LOG.info("[LOG] Établissement créé avec succès : " + createdEstablishment.getName()); @@ -217,7 +222,8 @@ public class EstablishmentResource { LOG.info("[LOG] Récupération de l'établissement avec l'ID : " + id); try { Establishment establishment = establishmentService.getEstablishmentById(id); - EstablishmentResponseDTO responseDTO = new EstablishmentResponseDTO(establishment); + int occupiedPlaces = (int) eventsRepository.countParticipantsForEstablishment(id); + EstablishmentResponseDTO responseDTO = new EstablishmentResponseDTO(establishment, occupiedPlaces); return Response.ok(responseDTO).build(); } catch (RuntimeException e) { LOG.warn("[WARN] " + e.getMessage()); @@ -330,9 +336,11 @@ public class EstablishmentResource { establishment.setPriceRange(requestDTO.getPriceRange()); establishment.setLatitude(requestDTO.getLatitude()); establishment.setLongitude(requestDTO.getLongitude()); + establishment.setCapacity(requestDTO.getCapacity()); Establishment updatedEstablishment = establishmentService.updateEstablishment(id, establishment); - EstablishmentResponseDTO responseDTO = new EstablishmentResponseDTO(updatedEstablishment); + int occupiedPlaces = (int) eventsRepository.countParticipantsForEstablishment(id); + EstablishmentResponseDTO responseDTO = new EstablishmentResponseDTO(updatedEstablishment, occupiedPlaces); return Response.ok(responseDTO).build(); } catch (RuntimeException e) { LOG.error("[ERROR] " + e.getMessage()); diff --git a/src/main/java/com/lions/dev/resource/EventsResource.java b/src/main/java/com/lions/dev/resource/EventsResource.java index e014b99..83f24cf 100644 --- a/src/main/java/com/lions/dev/resource/EventsResource.java +++ b/src/main/java/com/lions/dev/resource/EventsResource.java @@ -11,8 +11,10 @@ import com.lions.dev.dto.response.events.EventUpdateResponseDTO; import com.lions.dev.dto.response.friends.FriendshipReadFriendDetailsResponseDTO; import com.lions.dev.dto.response.users.UserResponseDTO; import com.lions.dev.entity.comment.Comment; +import com.lions.dev.entity.events.EventShare; import com.lions.dev.entity.events.Events; import com.lions.dev.entity.users.Users; +import com.lions.dev.repository.EventShareRepository; import com.lions.dev.repository.EventsRepository; import com.lions.dev.repository.UsersRepository; import com.lions.dev.service.EventService; @@ -48,6 +50,9 @@ public class EventsResource { @Inject EventsRepository eventsRepository; + @Inject + EventShareRepository eventShareRepository; + @Inject UsersRepository usersRepository; @@ -914,10 +919,41 @@ public class EventsResource { return Response.status(Response.Status.NOT_FOUND).entity("Événement non trouvé.").build(); } - String shareLink = "https://lions.dev /events/" + eventId; + String shareLink = "https://lions.dev/events/" + eventId; return Response.ok(Map.of("shareLink", shareLink)).build(); } + @POST + @Path("/{id}/share") + @Transactional + @Operation(summary = "Enregistrer un partage d'événement", description = "Enregistre qu'un utilisateur a partagé l'événement (incrémente le compteur).") + public Response shareEvent(@PathParam("id") UUID eventId, @QueryParam("userId") UUID userId) { + LOG.info("[LOG] Partage de l'événement ID : " + eventId + " par l'utilisateur ID : " + userId); + + Events event = eventsRepository.findById(eventId); + if (event == null) { + LOG.warn("[LOG] Événement non trouvé avec l'ID : " + eventId); + return Response.status(Response.Status.NOT_FOUND).entity("Événement non trouvé.").build(); + } + + 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(); + } + + EventShare share = new EventShare(user, event); + eventShareRepository.persist(share); + event.getShares().add(share); + + String shareLink = "https://lions.dev/events/" + eventId; + LOG.info("[LOG] Partage enregistré pour l'événement : " + event.getTitle()); + return Response.status(Response.Status.CREATED).entity(Map.of( + "shareLink", shareLink, + "sharesCount", event.getShares().size() + )).build(); + } + /** * Endpoint pour fermer un événement. * diff --git a/src/main/java/com/lions/dev/service/EstablishmentService.java b/src/main/java/com/lions/dev/service/EstablishmentService.java index c83e426..50492ec 100644 --- a/src/main/java/com/lions/dev/service/EstablishmentService.java +++ b/src/main/java/com/lions/dev/service/EstablishmentService.java @@ -134,9 +134,7 @@ public class EstablishmentService { 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 + establishment.setCapacity(updatedEstablishment.getCapacity()); establishmentRepository.persist(establishment); LOG.info("[LOG] Établissement mis à jour avec succès : " + establishment.getName()); diff --git a/src/main/java/com/lions/dev/service/FriendshipService.java b/src/main/java/com/lions/dev/service/FriendshipService.java index 9e56c5f..90c3bcf 100644 --- a/src/main/java/com/lions/dev/service/FriendshipService.java +++ b/src/main/java/com/lions/dev/service/FriendshipService.java @@ -18,7 +18,10 @@ 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.Status; +import jakarta.transaction.Synchronization; import jakarta.transaction.Transactional; +import jakarta.transaction.TransactionManager; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -46,8 +49,38 @@ public class FriendshipService { @Channel("notifications") Emitter notificationEmitter; // v2.0 - Publie dans Kafka + @Inject + TransactionManager transactionManager; + private static final Logger logger = Logger.getLogger(FriendshipService.class); + /** + * Envoie un événement Kafka après le commit de la transaction courante. + * Évite "Commit invoked while multiple threads active" quand Kafka publie sur un autre thread. + */ + private void sendToKafkaAfterCommit(NotificationEvent event) { + try { + transactionManager.getTransaction().registerSynchronization(new Synchronization() { + @Override + public void beforeCompletion() {} + + @Override + public void afterCompletion(int status) { + if (status == Status.STATUS_COMMITTED) { + try { + notificationEmitter.send(event); + logger.info("[LOG] Événement publié dans Kafka après commit pour : " + event.getUserId()); + } catch (Exception e) { + logger.error("[ERROR] Publication Kafka après commit : " + e.getMessage(), e); + } + } + } + }); + } catch (Exception e) { + logger.error("[ERROR] Enregistrement synchronisation JTA : " + e.getMessage(), e); + } + } + /** * Envoie une demande d'amitié entre deux utilisateurs. * @@ -105,26 +138,22 @@ public class FriendshipService { logger.error("[ERROR] Erreur création notification demande d'amitié : " + e.getMessage()); } - // TEMPS RÉEL: Publier dans Kafka (v2.0) + // TEMPS RÉEL: Publier dans Kafka après commit (évite "multiple threads active" JTA) try { Map notificationData = new HashMap<>(); notificationData.put("requestId", friendship.getId().toString()); notificationData.put("senderId", user.getId().toString()); - // v2.0 - Utiliser les nouveaux noms de champs notificationData.put("senderName", user.getFirstName() + " " + user.getLastName()); notificationData.put("senderProfileImage", user.getProfileImageUrl() != null ? user.getProfileImageUrl() : ""); NotificationEvent event = new NotificationEvent( - friend.getId().toString(), // userId destinataire (clé Kafka) + friend.getId().toString(), "friend_request_received", notificationData ); - - notificationEmitter.send(event); - logger.info("[LOG] Événement friend_request_received publié dans Kafka pour : " + friend.getId()); + sendToKafkaAfterCommit(event); } catch (Exception e) { - logger.error("[ERROR] Erreur lors de la publication dans Kafka : " + e.getMessage(), e); - // Ne pas bloquer la demande d'amitié si Kafka échoue + logger.error("[ERROR] Préparation publication Kafka : " + e.getMessage(), e); } logger.info("[LOG] Demande d'amitié envoyée avec succès."); @@ -168,30 +197,20 @@ 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: Publier dans Kafka (v2.0) + // TEMPS RÉEL: Publier dans Kafka après commit try { Users user = friendship.getUser(); Users friend = friendship.getFriend(); - // v2.0 - Utiliser les nouveaux noms de champs String friendName = friend.getFirstName() + " " + friend.getLastName(); - Map notificationData = new HashMap<>(); notificationData.put("acceptedBy", friendName); notificationData.put("friendshipId", friendshipId.toString()); notificationData.put("accepterId", friend.getId().toString()); notificationData.put("accepterProfileImage", friend.getProfileImageUrl() != null ? friend.getProfileImageUrl() : ""); - - NotificationEvent event = new NotificationEvent( - user.getId().toString(), // userId émetteur (destinataire de la notification) - "friend_request_accepted", - notificationData - ); - - notificationEmitter.send(event); - logger.info("[LOG] Événement friend_request_accepted publié dans Kafka pour : " + user.getId()); + NotificationEvent event = new NotificationEvent(user.getId().toString(), "friend_request_accepted", notificationData); + sendToKafkaAfterCommit(event); } catch (Exception e) { - logger.error("[ERROR] Erreur lors de la publication dans Kafka : " + e.getMessage(), e); - // Ne pas bloquer l'acceptation si Kafka échoue + logger.error("[ERROR] Préparation publication Kafka : " + e.getMessage(), e); } // Créer des notifications pour les deux utilisateurs @@ -246,25 +265,16 @@ public class FriendshipService { friendship.setStatus(FriendshipStatus.REJECTED); friendshipRepository.persist(friendship); - // TEMPS RÉEL: Publier dans Kafka (v2.0) + // TEMPS RÉEL: Publier dans Kafka après commit try { Users user = friendship.getUser(); - Map notificationData = new HashMap<>(); notificationData.put("friendshipId", friendshipId.toString()); notificationData.put("rejectedAt", System.currentTimeMillis()); - - NotificationEvent event = new NotificationEvent( - user.getId().toString(), // userId émetteur (destinataire de la notification) - "friend_request_rejected", - notificationData - ); - - notificationEmitter.send(event); - logger.info("[LOG] Événement friend_request_rejected publié dans Kafka pour : " + user.getId()); + NotificationEvent event = new NotificationEvent(user.getId().toString(), "friend_request_rejected", notificationData); + sendToKafkaAfterCommit(event); } catch (Exception e) { - logger.error("[ERROR] Erreur lors de la publication dans Kafka : " + e.getMessage(), e); - // Ne pas bloquer le rejet si Kafka échoue + logger.error("[ERROR] Préparation publication Kafka : " + e.getMessage(), e); } logger.info("[LOG] Demande d'amitié rejetée."); diff --git a/src/main/java/com/lions/dev/websocket/ChatWebSocketNext.java b/src/main/java/com/lions/dev/websocket/ChatWebSocketNext.java index a7bcc82..40157c3 100644 --- a/src/main/java/com/lions/dev/websocket/ChatWebSocketNext.java +++ b/src/main/java/com/lions/dev/websocket/ChatWebSocketNext.java @@ -47,7 +47,7 @@ public class ChatWebSocketNext { try { UUID userUUID = UUID.fromString(userId); sessions.put(userUUID, connection); - Log.info("[CHAT-WS-NEXT] WebSocket ouvert pour l'utilisateur ID : " + userId); + Log.info("[CHAT-WS-NEXT] WebSocket ouvert pour l'utilisateur ID : " + userId + " (sessions actives: " + sessions.size() + ")"); // Envoyer un message de confirmation String confirmation = buildJsonMessage("connected", @@ -220,7 +220,11 @@ public class ChatWebSocketNext { WebSocketConnection connection = sessions.get(userId); if (connection == null || !connection.isOpen()) { - Log.debug("[CHAT-WS-NEXT] Utilisateur " + userId + " non connecté"); + if (connection != null) { + sessions.remove(userId); + Log.debug("[CHAT-WS-NEXT] Connexion périmée supprimée pour " + userId); + } + Log.debug("[CHAT-WS-NEXT] Utilisateur " + userId + " non connecté (sessions actives: " + sessions.size() + ")"); return; } @@ -228,7 +232,8 @@ public class ChatWebSocketNext { 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); + Log.error("[CHAT-WS-NEXT] Erreur lors de l'envoi à " + userId + ", connexion supprimée", e); + sessions.remove(userId); } } @@ -239,6 +244,7 @@ public class ChatWebSocketNext { WebSocketConnection connection = sessions.get(senderId); if (connection == null || !connection.isOpen()) { + if (connection != null) sessions.remove(senderId); Log.debug("[CHAT-WS-NEXT] Expéditeur " + senderId + " non connecté pour confirmation"); return; } @@ -248,7 +254,8 @@ public class ChatWebSocketNext { 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); + Log.error("[CHAT-WS-NEXT] Erreur envoi confirmation à " + senderId + ", connexion supprimée", e); + sessions.remove(senderId); } } @@ -259,6 +266,7 @@ public class ChatWebSocketNext { WebSocketConnection connection = sessions.get(senderId); if (connection == null || !connection.isOpen()) { + if (connection != null) sessions.remove(senderId); Log.debug("[CHAT-WS-NEXT] Expéditeur " + senderId + " non connecté pour confirmation de lecture"); return; } @@ -268,7 +276,8 @@ public class ChatWebSocketNext { 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); + Log.error("[CHAT-WS-NEXT] Erreur envoi confirmation lecture à " + senderId + ", connexion supprimée", e); + sessions.remove(senderId); } } diff --git a/src/main/java/com/lions/dev/websocket/bridge/ChatKafkaBridge.java b/src/main/java/com/lions/dev/websocket/bridge/ChatKafkaBridge.java index a15aaa7..d755377 100644 --- a/src/main/java/com/lions/dev/websocket/bridge/ChatKafkaBridge.java +++ b/src/main/java/com/lions/dev/websocket/bridge/ChatKafkaBridge.java @@ -106,13 +106,12 @@ public class ChatKafkaBridge { data.put("isRead", event.getMetadata() != null && Boolean.TRUE.equals(event.getMetadata().get("isRead"))); data.put("isDelivered", true); - if (event.getMetadata() != null) { - data.put("senderFirstName", event.getMetadata().getOrDefault("senderFirstName", "")); - data.put("senderLastName", event.getMetadata().getOrDefault("senderLastName", "")); - data.put("senderProfileImageUrl", event.getMetadata().getOrDefault("senderProfileImageUrl", "")); - data.put("attachmentUrl", event.getMetadata().getOrDefault("attachmentUrl", "")); - data.put("attachmentType", event.getMetadata().getOrDefault("attachmentType", "text")); - } + java.util.Map meta = event.getMetadata(); + data.put("senderFirstName", meta != null ? meta.getOrDefault("senderFirstName", "") : ""); + data.put("senderLastName", meta != null ? meta.getOrDefault("senderLastName", "") : ""); + data.put("senderProfileImageUrl", meta != null ? meta.getOrDefault("senderProfileImageUrl", "") : ""); + data.put("attachmentUrl", meta != null ? meta.getOrDefault("attachmentUrl", "") : ""); + data.put("attachmentType", meta != null ? meta.getOrDefault("attachmentType", "text") : "text"); break; } diff --git a/src/main/resources/db/migration/V18__Create_Event_Shares_Table.sql b/src/main/resources/db/migration/V18__Create_Event_Shares_Table.sql new file mode 100644 index 0000000..7aa691c --- /dev/null +++ b/src/main/resources/db/migration/V18__Create_Event_Shares_Table.sql @@ -0,0 +1,21 @@ +-- Migration V18: Création de la table event_shares pour les partages d'événements +-- Description: Enregistre chaque partage d'un événement par un utilisateur (compteur de partages) + +CREATE TABLE IF NOT EXISTS event_shares ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL, + event_id UUID NOT NULL, + shared_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + + CONSTRAINT fk_event_shares_user + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + CONSTRAINT fk_event_shares_event + FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE +); + +CREATE INDEX IF NOT EXISTS idx_event_shares_event_id ON event_shares(event_id); +CREATE INDEX IF NOT EXISTS idx_event_shares_user_id ON event_shares(user_id); + +COMMENT ON TABLE event_shares IS 'Partages d''événements par les utilisateurs (compteur)'; diff --git a/src/main/resources/db/migration/V19__Add_Establishment_Capacity.sql b/src/main/resources/db/migration/V19__Add_Establishment_Capacity.sql new file mode 100644 index 0000000..badb069 --- /dev/null +++ b/src/main/resources/db/migration/V19__Add_Establishment_Capacity.sql @@ -0,0 +1,6 @@ +-- Migration V19: Ajout de la capacité (places) pour les établissements +-- Description: Permet au manager de définir le nombre max de places ; places restantes calculées ou gérées manuellement + +ALTER TABLE establishments ADD COLUMN IF NOT EXISTS capacity INTEGER NULL; + +COMMENT ON COLUMN establishments.capacity IS 'Nombre maximum de places dans l''établissement (optionnel). Utilisé pour calculer les places restantes.'; diff --git a/src/test/java/com/lions/dev/repository/EventsRepositoryTest.java b/src/test/java/com/lions/dev/repository/EventsRepositoryTest.java new file mode 100644 index 0000000..f9b8953 --- /dev/null +++ b/src/test/java/com/lions/dev/repository/EventsRepositoryTest.java @@ -0,0 +1,55 @@ +package com.lions.dev.repository; + +import com.lions.dev.entity.establishment.Establishment; +import com.lions.dev.entity.events.Events; +import com.lions.dev.entity.users.Users; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.util.HashSet; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +@QuarkusTest +class EventsRepositoryTest { + + @Inject + EventsRepository eventsRepository; + + @Test + @DisplayName("countParticipantsForEstablishment sans événements retourne 0") + void countParticipantsForEstablishment_noEvents_returnsZero() { + UUID establishmentId = UUID.randomUUID(); + + long count = eventsRepository.countParticipantsForEstablishment(establishmentId); + + assertEquals(0L, count); + } + + @Test + @DisplayName("findEventsAfterDate retourne une liste (éventuellement vide)") + void findEventsAfterDate_returnsList() { + LocalDateTime future = LocalDateTime.now().plusDays(1); + + var result = eventsRepository.findEventsAfterDate(future); + + assertNotNull(result); + assertTrue(result.isEmpty() || result.stream().allMatch(e -> e.getStartDate().isAfter(future))); + } + + @Test + @DisplayName("findEventsBetweenDates avec fin avant début retourne liste vide") + void findEventsBetweenDates_endBeforeStart_returnsEmpty() { + LocalDateTime start = LocalDateTime.now().plusDays(2); + LocalDateTime end = LocalDateTime.now().plusDays(1); + + var result = eventsRepository.findEventsBetweenDates(start, end); + + assertNotNull(result); + assertTrue(result.isEmpty()); + } +} diff --git a/src/test/java/com/lions/dev/resource/EventsResourceTest.java b/src/test/java/com/lions/dev/resource/EventsResourceTest.java new file mode 100644 index 0000000..8a2e9e2 --- /dev/null +++ b/src/test/java/com/lions/dev/resource/EventsResourceTest.java @@ -0,0 +1,51 @@ +package com.lions.dev.resource; + +import io.quarkus.test.junit.QuarkusTest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.UUID; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.*; + +@QuarkusTest +class EventsResourceTest { + + @Test + @DisplayName("POST /events/{id}/share sans événement existant retourne 404") + void shareEvent_eventNotFound_returns404() { + String eventId = UUID.randomUUID().toString(); + String userId = UUID.randomUUID().toString(); + + given() + .queryParam("userId", userId) + .when() + .post("/events/" + eventId + "/share") + .then() + .statusCode(404); + } + + @Test + @DisplayName("GET /events/share-link avec id invalide retourne 404") + void getShareLink_eventNotFound_returns404() { + String eventId = UUID.randomUUID().toString(); + + given() + .when() + .get("/events/" + eventId + "/share-link") + .then() + .statusCode(404); + } + + @Test + @DisplayName("GET /events retourne une liste (200)") + void getEvents_returnsOk() { + given() + .when() + .get("/events") + .then() + .statusCode(200) + .body(anyOf(is("[]"), startsWith("["))); + } +} diff --git a/src/test/java/com/lions/dev/service/EstablishmentRatingServiceTest.java b/src/test/java/com/lions/dev/service/EstablishmentRatingServiceTest.java new file mode 100644 index 0000000..dbac6bd --- /dev/null +++ b/src/test/java/com/lions/dev/service/EstablishmentRatingServiceTest.java @@ -0,0 +1,220 @@ +package com.lions.dev.service; + +import com.lions.dev.dto.request.establishment.EstablishmentRatingRequestDTO; +import com.lions.dev.entity.establishment.Establishment; +import com.lions.dev.entity.establishment.EstablishmentRating; +import com.lions.dev.entity.users.Users; +import com.lions.dev.repository.EstablishmentRatingRepository; +import com.lions.dev.repository.EstablishmentRepository; +import com.lions.dev.repository.UsersRepository; +import io.quarkus.test.InjectMock; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Map; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@QuarkusTest +class EstablishmentRatingServiceTest { + + @Inject + EstablishmentRatingService establishmentRatingService; + + @InjectMock + EstablishmentRatingRepository ratingRepository; + + @InjectMock + EstablishmentRepository establishmentRepository; + + @InjectMock + UsersRepository usersRepository; + + @InjectMock + NotificationService notificationService; + + @Test + @DisplayName("submitRating avec données valides crée la note et met à jour les stats") + void submitRating_validData_createsRatingAndUpdatesStats() { + UUID establishmentId = UUID.randomUUID(); + UUID userId = UUID.randomUUID(); + Establishment establishment = new Establishment("Bar", "BAR", "1 rue Test", "Paris", "75001", new Users()); + establishment.setId(establishmentId); + Users user = new Users(); + user.setId(userId); + user.setFirstName("Jean"); + user.setLastName("Dupont"); + EstablishmentRatingRequestDTO requestDTO = new EstablishmentRatingRequestDTO(); + requestDTO.setRating(4); + requestDTO.setComment("Très bien"); + + when(establishmentRepository.findById(establishmentId)).thenReturn(establishment); + when(usersRepository.findById(userId)).thenReturn(user); + when(ratingRepository.findByEstablishmentIdAndUserId(establishmentId, userId)).thenReturn(null); + when(ratingRepository.calculateAverageRating(establishmentId)).thenReturn(4.0); + when(ratingRepository.countByEstablishmentId(establishmentId)).thenReturn(1L); + when(ratingRepository.calculateRatingDistribution(establishmentId)).thenReturn(Map.of(4, 1)); + Notification notif = new Notification("Nouvelle note", "Message", "rating", user); + when(notificationService.createNotification(anyString(), anyString(), anyString(), any(UUID.class), any())).thenReturn(notif); + + EstablishmentRating result = establishmentRatingService.submitRating(establishmentId, userId, requestDTO); + + assertNotNull(result); + verify(ratingRepository).persist(any(EstablishmentRating.class)); + verify(establishmentRepository).persist(establishment); + } + + @Test + @DisplayName("submitRating avec établissement inexistant lance RuntimeException") + void submitRating_establishmentNotFound_throws() { + UUID establishmentId = UUID.randomUUID(); + UUID userId = UUID.randomUUID(); + EstablishmentRatingRequestDTO requestDTO = new EstablishmentRatingRequestDTO(); + requestDTO.setRating(5); + + when(establishmentRepository.findById(establishmentId)).thenReturn(null); + + assertThrows(RuntimeException.class, () -> + establishmentRatingService.submitRating(establishmentId, userId, requestDTO)); + verify(ratingRepository, never()).persist(any()); + } + + @Test + @DisplayName("submitRating avec utilisateur inexistant lance RuntimeException") + void submitRating_userNotFound_throws() { + UUID establishmentId = UUID.randomUUID(); + UUID userId = UUID.randomUUID(); + Establishment establishment = new Establishment("Bar", "BAR", "1 rue Test", "Paris", "75001", new Users()); + establishment.setId(establishmentId); + EstablishmentRatingRequestDTO requestDTO = new EstablishmentRatingRequestDTO(); + requestDTO.setRating(5); + + when(establishmentRepository.findById(establishmentId)).thenReturn(establishment); + when(usersRepository.findById(userId)).thenReturn(null); + + assertThrows(RuntimeException.class, () -> + establishmentRatingService.submitRating(establishmentId, userId, requestDTO)); + verify(ratingRepository, never()).persist(any()); + } + + @Test + @DisplayName("submitRating quand l'utilisateur a déjà noté lance RuntimeException") + void submitRating_alreadyRated_throws() { + UUID establishmentId = UUID.randomUUID(); + UUID userId = UUID.randomUUID(); + Establishment establishment = new Establishment("Bar", "BAR", "1 rue Test", "Paris", "75001", new Users()); + establishment.setId(establishmentId); + Users user = new Users(); + user.setId(userId); + EstablishmentRating existing = new EstablishmentRating(establishment, user, 3); + EstablishmentRatingRequestDTO requestDTO = new EstablishmentRatingRequestDTO(); + requestDTO.setRating(5); + + when(establishmentRepository.findById(establishmentId)).thenReturn(establishment); + when(usersRepository.findById(userId)).thenReturn(user); + when(ratingRepository.findByEstablishmentIdAndUserId(establishmentId, userId)).thenReturn(existing); + + assertThrows(RuntimeException.class, () -> + establishmentRatingService.submitRating(establishmentId, userId, requestDTO)); + verify(ratingRepository, never()).persist(any()); + } + + @Test + @DisplayName("getUserRating retourne la note si elle existe") + void getUserRating_exists_returnsRating() { + UUID establishmentId = UUID.randomUUID(); + UUID userId = UUID.randomUUID(); + Establishment establishment = new Establishment("Bar", "BAR", "1 rue Test", "Paris", "75001", new Users()); + establishment.setId(establishmentId); + Users user = new Users(); + user.setId(userId); + EstablishmentRating rating = new EstablishmentRating(establishment, user, 4); + + when(ratingRepository.findByEstablishmentIdAndUserId(establishmentId, userId)).thenReturn(rating); + + EstablishmentRating result = establishmentRatingService.getUserRating(establishmentId, userId); + + assertNotNull(result); + assertEquals(4, result.getRating()); + } + + @Test + @DisplayName("getUserRating retourne null si pas de note") + void getUserRating_notExists_returnsNull() { + UUID establishmentId = UUID.randomUUID(); + UUID userId = UUID.randomUUID(); + + when(ratingRepository.findByEstablishmentIdAndUserId(establishmentId, userId)).thenReturn(null); + + EstablishmentRating result = establishmentRatingService.getUserRating(establishmentId, userId); + + assertNull(result); + } + + @Test + @DisplayName("getRatingStats retourne moyenne, total et distribution") + void getRatingStats_returnsMap() { + UUID establishmentId = UUID.randomUUID(); + + when(ratingRepository.calculateAverageRating(establishmentId)).thenReturn(4.2); + when(ratingRepository.countByEstablishmentId(establishmentId)).thenReturn(10L); + when(ratingRepository.calculateRatingDistribution(establishmentId)).thenReturn(Map.of(4, 6, 5, 4)); + + Map result = establishmentRatingService.getRatingStats(establishmentId); + + assertNotNull(result); + assertEquals(4.2, result.get("averageRating")); + assertEquals(10, result.get("totalRatingsCount")); + @SuppressWarnings("unchecked") + Map dist = (Map) result.get("ratingDistribution"); + assertNotNull(dist); + assertEquals(6, dist.get(4)); + assertEquals(4, dist.get(5)); + } + + @Test + @DisplayName("updateRating avec note existante met à jour et met à jour les stats") + void updateRating_exists_updatesAndRefreshesStats() { + UUID establishmentId = UUID.randomUUID(); + UUID userId = UUID.randomUUID(); + Establishment establishment = new Establishment("Bar", "BAR", "1 rue Test", "Paris", "75001", new Users()); + establishment.setId(establishmentId); + Users user = new Users(); + user.setId(userId); + EstablishmentRating rating = new EstablishmentRating(establishment, user, 3); + EstablishmentRatingRequestDTO requestDTO = new EstablishmentRatingRequestDTO(); + requestDTO.setRating(5); + requestDTO.setComment("Parfait"); + + when(ratingRepository.findByEstablishmentIdAndUserId(establishmentId, userId)).thenReturn(rating); + when(ratingRepository.calculateAverageRating(establishmentId)).thenReturn(5.0); + when(ratingRepository.countByEstablishmentId(establishmentId)).thenReturn(1L); + when(ratingRepository.calculateRatingDistribution(establishmentId)).thenReturn(Map.of(5, 1)); + + EstablishmentRating result = establishmentRatingService.updateRating(establishmentId, userId, requestDTO); + + assertNotNull(result); + verify(ratingRepository).persist(rating); + verify(establishmentRepository).persist(establishment); + } + + @Test + @DisplayName("updateRating quand la note n'existe pas lance RuntimeException") + void updateRating_notExists_throws() { + UUID establishmentId = UUID.randomUUID(); + UUID userId = UUID.randomUUID(); + EstablishmentRatingRequestDTO requestDTO = new EstablishmentRatingRequestDTO(); + requestDTO.setRating(5); + + when(ratingRepository.findByEstablishmentIdAndUserId(establishmentId, userId)).thenReturn(null); + + assertThrows(RuntimeException.class, () -> + establishmentRatingService.updateRating(establishmentId, userId, requestDTO)); + verify(ratingRepository, never()).persist(any()); + } +}