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 <noreply@anthropic.com>
This commit is contained in:
dahoud
2026-01-31 20:27:27 +00:00
parent 9dc9ca591c
commit 0240442671
19 changed files with 574 additions and 62 deletions

View File

@@ -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);
}
}
}

View File

@@ -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());

View File

@@ -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

View File

@@ -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();
}
}

View File

@@ -179,6 +179,9 @@ public class Events extends BaseEntity {
@OneToMany(fetch = FetchType.LAZY, mappedBy = "event")
private List<Comment> comments; // Liste des commentaires associés à l'événement
@OneToMany(fetch = FetchType.LAZY, mappedBy = "event", cascade = CascadeType.ALL, orphanRemoval = true)
private List<EventShare> 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<EventShare> getShares() {
return shares != null ? shares : new java.util.ArrayList<>();
}
}

View File

@@ -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<EventShare, UUID> {
}

View File

@@ -108,4 +108,16 @@ public class EventsRepository implements PanacheRepositoryBase<Events, UUID> {
public List<Events> 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> 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();
}
}

View File

@@ -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());

View File

@@ -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.
*

View File

@@ -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());

View File

@@ -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<NotificationEvent> 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<String, Object> 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<String, Object> 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<String, Object> 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.");

View File

@@ -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);
}
}

View File

@@ -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<String, Object> 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;
}