Refactoring
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
-Xmx2048m
|
-Xmx2048m
|
||||||
-Xms1024m
|
-Xms1024m
|
||||||
-XX:MaxMetaspaceSize=512m
|
-XX:MaxMetaspaceSize=512m
|
||||||
|
-Dfile.encoding=UTF-8
|
||||||
|
|||||||
119
REALTIME_DEV.md
Normal file
119
REALTIME_DEV.md
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
# Temps réel en développement (Kafka + WebSocket)
|
||||||
|
|
||||||
|
Ce guide permet de faire fonctionner les **notifications / présence / réactions / chat** en temps réel en environnement de développement.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
Services métier → Kafka (topics) → Bridges → WebSocket → Client Flutter
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Topics Kafka** : `notifications`, `chat.messages`, `reactions`, `presence.updates`
|
||||||
|
- **WebSocket** : `ws://<backend>/notifications/<userId>` (et `/chat/<userId>` pour le chat)
|
||||||
|
|
||||||
|
## 1. Démarrer Kafka en local
|
||||||
|
|
||||||
|
Un conteneur Kafka doit être joignable sur le **port 9092** depuis la machine où tourne Quarkus.
|
||||||
|
|
||||||
|
### Option A : Conteneur existant
|
||||||
|
|
||||||
|
Si vous avez déjà un conteneur Kafka (ex. ID `e100552d0da2...`) :
|
||||||
|
|
||||||
|
- Vérifiez que le port **9092** est exposé vers l’hôte :
|
||||||
|
```bash
|
||||||
|
docker port <container_id_or_name> 9092
|
||||||
|
```
|
||||||
|
- Si rien n’est mappé, recréez le conteneur avec `-p 9092:9092` ou dans un `docker-compose` :
|
||||||
|
```yaml
|
||||||
|
kafka:
|
||||||
|
image: apache/kafka-native:latest # ou quay.io/strimzi/kafka:latest, etc.
|
||||||
|
ports:
|
||||||
|
- "9092:9092"
|
||||||
|
# ... reste de la config (KAFKA_CFG_..., etc.)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option B : Lancer Kafka avec Docker (exemple minimal)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d --name kafka-dev -p 9092:9092 \
|
||||||
|
-e KAFKA_NODE_ID=1 \
|
||||||
|
-e KAFKA_PROCESS_ROLES=broker,controller \
|
||||||
|
-e KAFKA_LISTENERS=PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093 \
|
||||||
|
apache/kafka-native:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
(Adaptez l’image et les variables à votre setup si vous en utilisez un autre.)
|
||||||
|
|
||||||
|
### Depuis une autre machine / Docker
|
||||||
|
|
||||||
|
- **Quarkus sur l’hôte, Kafka dans Docker** : `localhost:9092` suffit si le port est mappé (`-p 9092:9092`).
|
||||||
|
- **Quarkus dans Docker, Kafka sur l’hôte** : utilisez `host.docker.internal:9092` (Windows/Mac) ou l’IP de l’hôte.
|
||||||
|
- Définir alors :
|
||||||
|
```bash
|
||||||
|
export KAFKA_BOOTSTRAP_SERVERS=localhost:9092
|
||||||
|
```
|
||||||
|
(ou `host.docker.internal:9092` selon le cas).
|
||||||
|
|
||||||
|
## 2. Démarrer le backend Quarkus (profil dev)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd mic-after-work-server-impl-quarkus-main
|
||||||
|
mvn quarkus:dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Le fichier `application-dev.properties` utilise par défaut `localhost:9092`.
|
||||||
|
En cas d’erreur de connexion Kafka au démarrage, vérifiez que Kafka écoute bien sur 9092 et que `KAFKA_BOOTSTRAP_SERVERS` pointe vers ce broker.
|
||||||
|
|
||||||
|
Logs utiles au démarrage :
|
||||||
|
|
||||||
|
- `[KAFKA-BRIDGE] Bridge démarré pour topic: notifications`
|
||||||
|
- Pas d’exception type `ConfigException` / « No resolvable bootstrap urls »
|
||||||
|
|
||||||
|
Quand une notification est publiée et consommée :
|
||||||
|
|
||||||
|
- `[KAFKA-BRIDGE] Événement reçu: type=... userId=...`
|
||||||
|
- `[WS-NEXT] Notification envoyée à <userId> (Succès: 1, Échec: 0)`
|
||||||
|
|
||||||
|
## 3. Configurer l’app Flutter (URL du backend)
|
||||||
|
|
||||||
|
Le client doit pouvoir joindre le **HTTP** et le **WebSocket** du même backend.
|
||||||
|
|
||||||
|
- **Émulateur Android** : souvent `http://10.0.2.2:8080` (puis WebSocket `ws://10.0.2.2:8080/notifications/<userId>`).
|
||||||
|
- **Appareil physique / même réseau** : IP de la machine qui fait tourner Quarkus, ex. `http://192.168.1.103:8080`.
|
||||||
|
- **Chrome / web** : `http://localhost:8080` si Flutter web et Quarkus sont sur la même machine.
|
||||||
|
|
||||||
|
Définir cette URL comme base API (elle est aussi utilisée pour le WebSocket) :
|
||||||
|
|
||||||
|
- Au run :
|
||||||
|
```bash
|
||||||
|
flutter run --dart-define=API_BASE_URL=http://<VOTRE_IP_OU_HOST>:8080
|
||||||
|
```
|
||||||
|
- Ou dans `lib/core/constants/env_config.dart` (valeur par défaut en dev).
|
||||||
|
|
||||||
|
Important : **pas de slash final** dans `API_BASE_URL` (ex. `http://192.168.1.103:8080`).
|
||||||
|
|
||||||
|
## 4. Vérifier que le temps réel fonctionne
|
||||||
|
|
||||||
|
1. **Connexion WebSocket**
|
||||||
|
- Se connecter dans l’app avec un utilisateur.
|
||||||
|
- Côté Flutter : log du type « Connecté avec succès au service de notifications ».
|
||||||
|
- Côté Quarkus : `[WS-NEXT] Connexion ouverte pour l'utilisateur: <userId>`.
|
||||||
|
|
||||||
|
2. **Notification (ex. demande d’ami / post)**
|
||||||
|
- Déclencher une action qui crée une notification (autre compte ou service).
|
||||||
|
- Côté Quarkus : `[KAFKA-BRIDGE] Événement reçu` puis `[WS-NEXT] Notification envoyée à ...`.
|
||||||
|
- Côté Flutter : la notification doit apparaître sans recharger (si l’écran écoute le stream temps réel).
|
||||||
|
|
||||||
|
3. **Si rien n’arrive**
|
||||||
|
- Kafka : le broker est-il bien sur le port 9092 ? `KAFKA_BOOTSTRAP_SERVERS` correct ?
|
||||||
|
- WebSocket : l’URL dans l’app est-elle exactement celle du backend (même hôte/port) ?
|
||||||
|
- CORS : pour Flutter web, le backend doit autoriser l’origine de l’app (déjà géré dans la config actuelle si vous n’avez pas changé l’origine).
|
||||||
|
|
||||||
|
## 5. Résumé des variables utiles (dev)
|
||||||
|
|
||||||
|
| Variable | Rôle | Exemple |
|
||||||
|
|----------|------|--------|
|
||||||
|
| `KAFKA_BOOTSTRAP_SERVERS` | Broker Kafka pour Quarkus | `localhost:9092` ou `host.docker.internal:9092` |
|
||||||
|
| `API_BASE_URL` (Flutter) | Base HTTP + WS du backend | `http://192.168.1.103:8080` |
|
||||||
|
|
||||||
|
Aucune régression fonctionnelle n’est introduite par ce guide : seules la configuration dev et le format des messages WebSocket (timestamp/type dans `data`) ont été alignés pour le client.
|
||||||
@@ -5,6 +5,7 @@ import com.lions.dev.core.errors.exceptions.EventNotFoundException;
|
|||||||
import com.lions.dev.core.errors.exceptions.NotFoundException;
|
import com.lions.dev.core.errors.exceptions.NotFoundException;
|
||||||
import com.lions.dev.core.errors.exceptions.ServerException;
|
import com.lions.dev.core.errors.exceptions.ServerException;
|
||||||
import com.lions.dev.core.errors.exceptions.UnauthorizedException;
|
import com.lions.dev.core.errors.exceptions.UnauthorizedException;
|
||||||
|
import com.lions.dev.exception.UserNotFoundException;
|
||||||
import jakarta.ws.rs.core.Response;
|
import jakarta.ws.rs.core.Response;
|
||||||
import jakarta.ws.rs.ext.ExceptionMapper;
|
import jakarta.ws.rs.ext.ExceptionMapper;
|
||||||
import jakarta.ws.rs.ext.Provider;
|
import jakarta.ws.rs.ext.Provider;
|
||||||
@@ -30,6 +31,9 @@ public class GlobalExceptionHandler implements ExceptionMapper<Throwable> {
|
|||||||
if (exception instanceof BadRequestException) {
|
if (exception instanceof BadRequestException) {
|
||||||
logger.warn("BadRequestException intercepted: " + exception.getMessage());
|
logger.warn("BadRequestException intercepted: " + exception.getMessage());
|
||||||
return buildResponse(Response.Status.BAD_REQUEST, exception.getMessage());
|
return buildResponse(Response.Status.BAD_REQUEST, exception.getMessage());
|
||||||
|
} else if (exception instanceof UserNotFoundException) {
|
||||||
|
logger.warn("UserNotFoundException (404): " + exception.getMessage());
|
||||||
|
return buildResponse(Response.Status.NOT_FOUND, exception.getMessage());
|
||||||
} else if (exception instanceof EventNotFoundException || exception instanceof NotFoundException) {
|
} else if (exception instanceof EventNotFoundException || exception instanceof NotFoundException) {
|
||||||
logger.warn("NotFoundException intercepted: " + exception.getMessage());
|
logger.warn("NotFoundException intercepted: " + exception.getMessage());
|
||||||
return buildResponse(Response.Status.NOT_FOUND, exception.getMessage());
|
return buildResponse(Response.Status.NOT_FOUND, exception.getMessage());
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ public class ConversationResponseDTO {
|
|||||||
private LocalDateTime lastMessageTimestamp;
|
private LocalDateTime lastMessageTimestamp;
|
||||||
private int unreadCount;
|
private int unreadCount;
|
||||||
private boolean isTyping;
|
private boolean isTyping;
|
||||||
|
/** Indique si le participant (l'autre utilisateur) est actuellement en ligne (WebSocket notifications). */
|
||||||
|
private boolean participantIsOnline;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructeur depuis une entité Conversation.
|
* Constructeur depuis une entité Conversation.
|
||||||
@@ -44,6 +46,7 @@ public class ConversationResponseDTO {
|
|||||||
this.participantFirstName = otherUser.getFirstName();
|
this.participantFirstName = otherUser.getFirstName();
|
||||||
this.participantLastName = otherUser.getLastName();
|
this.participantLastName = otherUser.getLastName();
|
||||||
this.participantProfileImageUrl = otherUser.getProfileImageUrl();
|
this.participantProfileImageUrl = otherUser.getProfileImageUrl();
|
||||||
|
this.participantIsOnline = otherUser.isOnline();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.lastMessage = conversation.getLastMessageContent();
|
this.lastMessage = conversation.getLastMessageContent();
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.lions.dev.exception;
|
||||||
|
|
||||||
|
import jakarta.ws.rs.WebApplicationException;
|
||||||
|
import jakarta.ws.rs.core.Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception levée lorsqu'on tente de supprimer un établissement qui a encore
|
||||||
|
* des dépendances (événements, réservations). Renvoie une réponse HTTP 409 (Conflict).
|
||||||
|
*/
|
||||||
|
public class EstablishmentHasDependenciesException extends WebApplicationException {
|
||||||
|
|
||||||
|
public EstablishmentHasDependenciesException(String message) {
|
||||||
|
super(message, Response.Status.CONFLICT);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,4 +13,15 @@ public class BookingRepository implements PanacheRepositoryBase<Booking, UUID> {
|
|||||||
public List<Booking> findByUserId(UUID userId) {
|
public List<Booking> findByUserId(UUID userId) {
|
||||||
return list("user.id", userId);
|
return list("user.id", userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compte le nombre de réservations d'un établissement.
|
||||||
|
* Utilisé pour refuser la suppression si des réservations existent (règle métier).
|
||||||
|
*
|
||||||
|
* @param establishmentId L'ID de l'établissement
|
||||||
|
* @return Nombre de réservations
|
||||||
|
*/
|
||||||
|
public long countByEstablishmentId(UUID establishmentId) {
|
||||||
|
return count("establishment.id", establishmentId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,6 +109,27 @@ public class EventsRepository implements PanacheRepositoryBase<Events, UUID> {
|
|||||||
return list("startDate >= ?1 AND startDate <= ?2", from, to);
|
return list("startDate >= ?1 AND startDate <= ?2", from, to);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compte le nombre d'événements d'un établissement.
|
||||||
|
* Utilisé pour refuser la suppression si des événements existent (règle métier).
|
||||||
|
*
|
||||||
|
* @param establishmentId L'ID de l'établissement
|
||||||
|
* @return Nombre d'événements
|
||||||
|
*/
|
||||||
|
public long countByEstablishmentId(UUID establishmentId) {
|
||||||
|
return count("establishment.id", establishmentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprime tous les événements d'un établissement (réservé à un usage interne / migration).
|
||||||
|
*
|
||||||
|
* @param establishmentId L'ID de l'établissement
|
||||||
|
* @return Nombre d'événements supprimés
|
||||||
|
*/
|
||||||
|
public long deleteByEstablishmentId(UUID establishmentId) {
|
||||||
|
return delete("establishment.id = ?1", establishmentId);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compte le nombre total de participants dans les événements ouverts et à venir
|
* Compte le nombre total de participants dans les événements ouverts et à venir
|
||||||
* d'un établissement (pour calcul des places restantes).
|
* d'un établissement (pour calcul des places restantes).
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import com.lions.dev.dto.response.establishment.BusinessHoursResponseDTO;
|
|||||||
import com.lions.dev.dto.response.establishment.EstablishmentAmenityResponseDTO;
|
import com.lions.dev.dto.response.establishment.EstablishmentAmenityResponseDTO;
|
||||||
import com.lions.dev.dto.response.establishment.EstablishmentResponseDTO;
|
import com.lions.dev.dto.response.establishment.EstablishmentResponseDTO;
|
||||||
import com.lions.dev.entity.establishment.Establishment;
|
import com.lions.dev.entity.establishment.Establishment;
|
||||||
|
import com.lions.dev.exception.EstablishmentHasDependenciesException;
|
||||||
import com.lions.dev.entity.users.Users;
|
import com.lions.dev.entity.users.Users;
|
||||||
import com.lions.dev.repository.BusinessHoursRepository;
|
import com.lions.dev.repository.BusinessHoursRepository;
|
||||||
import com.lions.dev.repository.EstablishmentAmenityRepository;
|
import com.lions.dev.repository.EstablishmentAmenityRepository;
|
||||||
@@ -25,6 +26,7 @@ import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
|||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@@ -367,15 +369,20 @@ public class EstablishmentResource {
|
|||||||
try {
|
try {
|
||||||
establishmentService.deleteEstablishment(id);
|
establishmentService.deleteEstablishment(id);
|
||||||
return Response.noContent().build();
|
return Response.noContent().build();
|
||||||
|
} catch (EstablishmentHasDependenciesException e) {
|
||||||
|
LOG.warn("[WARN] " + e.getMessage());
|
||||||
|
return Response.status(Response.Status.CONFLICT)
|
||||||
|
.entity(Map.of("message", e.getMessage()))
|
||||||
|
.build();
|
||||||
} catch (RuntimeException e) {
|
} catch (RuntimeException e) {
|
||||||
LOG.error("[ERROR] " + e.getMessage());
|
LOG.error("[ERROR] " + e.getMessage());
|
||||||
return Response.status(Response.Status.NOT_FOUND)
|
return Response.status(Response.Status.NOT_FOUND)
|
||||||
.entity(e.getMessage())
|
.entity(Map.of("message", e.getMessage() != null ? e.getMessage() : "Établissement non trouvé"))
|
||||||
.build();
|
.build();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LOG.error("[ERROR] Erreur lors de la suppression de l'établissement", e);
|
LOG.error("[ERROR] Erreur lors de la suppression de l'établissement", e);
|
||||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||||
.entity("Erreur lors de la suppression de l'établissement")
|
.entity(Map.of("message", "Erreur lors de la suppression de l'établissement"))
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import org.jboss.resteasy.reactive.RestForm;
|
|||||||
import org.jboss.resteasy.reactive.multipart.FileUpload;
|
import org.jboss.resteasy.reactive.multipart.FileUpload;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Base64;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@@ -176,6 +177,20 @@ public class FileUploadResource {
|
|||||||
return baseUri + "/media/files/" + fileName;
|
return baseUri + "/media/files/" + fileName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** PNG 1x1 transparent (placeholder quand le fichier image est introuvable). */
|
||||||
|
private static final byte[] PLACEHOLDER_IMAGE_PNG = Base64.getDecoder().decode(
|
||||||
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indique si le nom de fichier correspond à une image (extension).
|
||||||
|
*/
|
||||||
|
private boolean isImageFileName(String fileName) {
|
||||||
|
if (fileName == null) return false;
|
||||||
|
String lower = fileName.toLowerCase();
|
||||||
|
return lower.endsWith(".jpg") || lower.endsWith(".jpeg")
|
||||||
|
|| lower.endsWith(".png") || lower.endsWith(".gif") || lower.endsWith(".webp");
|
||||||
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/files/{fileName}")
|
@Path("/files/{fileName}")
|
||||||
@Produces(MediaType.APPLICATION_OCTET_STREAM)
|
@Produces(MediaType.APPLICATION_OCTET_STREAM)
|
||||||
@@ -185,6 +200,14 @@ public class FileUploadResource {
|
|||||||
|
|
||||||
if (!java.nio.file.Files.exists(filePath)) {
|
if (!java.nio.file.Files.exists(filePath)) {
|
||||||
LOG.warnf("Fichier non trouvé: %s", fileName);
|
LOG.warnf("Fichier non trouvé: %s", fileName);
|
||||||
|
// Pour les images : retourner un placeholder pour que l'affichage reste correct (pas de 404)
|
||||||
|
if (isImageFileName(fileName)) {
|
||||||
|
return Response.ok(PLACEHOLDER_IMAGE_PNG)
|
||||||
|
.type("image/png")
|
||||||
|
.header("Content-Disposition", "inline; filename=\"" + fileName + "\"")
|
||||||
|
.header("X-Placeholder", "true")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
return Response.status(Response.Status.NOT_FOUND)
|
return Response.status(Response.Status.NOT_FOUND)
|
||||||
.entity(createErrorResponse("Fichier non trouvé"))
|
.entity(createErrorResponse("Fichier non trouvé"))
|
||||||
.build();
|
.build();
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ package com.lions.dev.service;
|
|||||||
|
|
||||||
import com.lions.dev.entity.establishment.Establishment;
|
import com.lions.dev.entity.establishment.Establishment;
|
||||||
import com.lions.dev.entity.users.Users;
|
import com.lions.dev.entity.users.Users;
|
||||||
|
import com.lions.dev.exception.EstablishmentHasDependenciesException;
|
||||||
|
import com.lions.dev.repository.BookingRepository;
|
||||||
import com.lions.dev.repository.EstablishmentRepository;
|
import com.lions.dev.repository.EstablishmentRepository;
|
||||||
|
import com.lions.dev.repository.EventsRepository;
|
||||||
import com.lions.dev.repository.UsersRepository;
|
import com.lions.dev.repository.UsersRepository;
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
@@ -23,6 +26,12 @@ public class EstablishmentService {
|
|||||||
@Inject
|
@Inject
|
||||||
EstablishmentRepository establishmentRepository;
|
EstablishmentRepository establishmentRepository;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
EventsRepository eventsRepository;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
BookingRepository bookingRepository;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
UsersRepository usersRepository;
|
UsersRepository usersRepository;
|
||||||
|
|
||||||
@@ -154,6 +163,19 @@ public class EstablishmentService {
|
|||||||
LOG.error("[ERROR] Établissement non trouvé avec l'ID : " + id);
|
LOG.error("[ERROR] Établissement non trouvé avec l'ID : " + id);
|
||||||
throw new RuntimeException("Établissement non trouvé avec l'ID : " + id);
|
throw new RuntimeException("Établissement non trouvé avec l'ID : " + id);
|
||||||
}
|
}
|
||||||
|
// Règle métier : refuser la suppression si des événements ou réservations existent
|
||||||
|
long eventsCount = eventsRepository.countByEstablishmentId(id);
|
||||||
|
if (eventsCount > 0) {
|
||||||
|
LOG.warn("[WARN] Impossible de supprimer l'établissement : " + eventsCount + " événement(s) associé(s)");
|
||||||
|
throw new EstablishmentHasDependenciesException(
|
||||||
|
"Impossible de supprimer l'établissement : des événements y sont encore associés. Annulez ou déplacez les événements avant de supprimer l'établissement.");
|
||||||
|
}
|
||||||
|
long bookingsCount = bookingRepository.countByEstablishmentId(id);
|
||||||
|
if (bookingsCount > 0) {
|
||||||
|
LOG.warn("[WARN] Impossible de supprimer l'établissement : " + bookingsCount + " réservation(s) associée(s)");
|
||||||
|
throw new EstablishmentHasDependenciesException(
|
||||||
|
"Impossible de supprimer l'établissement : des réservations y sont encore associées.");
|
||||||
|
}
|
||||||
establishmentRepository.delete(establishment);
|
establishmentRepository.delete(establishment);
|
||||||
LOG.info("[LOG] Établissement supprimé avec succès : " + establishment.getName());
|
LOG.info("[LOG] Établissement supprimé avec succès : " + establishment.getName());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ import com.lions.dev.repository.NotificationRepository;
|
|||||||
import com.lions.dev.repository.UsersRepository;
|
import com.lions.dev.repository.UsersRepository;
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.transaction.Status;
|
||||||
|
import jakarta.transaction.Synchronization;
|
||||||
import jakarta.transaction.Transactional;
|
import jakarta.transaction.Transactional;
|
||||||
|
import jakarta.transaction.TransactionManager;
|
||||||
import org.eclipse.microprofile.reactive.messaging.Channel;
|
import org.eclipse.microprofile.reactive.messaging.Channel;
|
||||||
import org.eclipse.microprofile.reactive.messaging.Emitter;
|
import org.eclipse.microprofile.reactive.messaging.Emitter;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
@@ -43,6 +46,36 @@ public class NotificationService {
|
|||||||
@Channel("notifications")
|
@Channel("notifications")
|
||||||
Emitter<NotificationEvent> notificationEmitter;
|
Emitter<NotificationEvent> notificationEmitter;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
TransactionManager transactionManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 publishToKafkaAfterCommit(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.debug("[NotificationService] Événement publié dans Kafka après commit pour : " + event.getUserId());
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("[NotificationService] Publication Kafka après commit : " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("[NotificationService] Enregistrement synchronisation JTA : " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Récupère toutes les notifications d'un utilisateur.
|
* Récupère toutes les notifications d'un utilisateur.
|
||||||
*
|
*
|
||||||
@@ -124,17 +157,7 @@ public class NotificationService {
|
|||||||
notificationRepository.persist(notification);
|
notificationRepository.persist(notification);
|
||||||
logger.info("[NotificationService] Notification créée avec succès : " + notification.getId());
|
logger.info("[NotificationService] Notification créée avec succès : " + notification.getId());
|
||||||
|
|
||||||
// Publication temps réel via Kafka → WebSocket
|
// Publication temps réel via Kafka → WebSocket après commit (évite "multiple threads active" JTA)
|
||||||
publishToKafka(notification, user);
|
|
||||||
|
|
||||||
return notification;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Publie une notification dans Kafka pour livraison temps réel via WebSocket.
|
|
||||||
*/
|
|
||||||
private void publishToKafka(Notification notification, Users user) {
|
|
||||||
try {
|
|
||||||
Map<String, Object> data = new HashMap<>();
|
Map<String, Object> data = new HashMap<>();
|
||||||
data.put("notificationId", notification.getId().toString());
|
data.put("notificationId", notification.getId().toString());
|
||||||
data.put("title", notification.getTitle());
|
data.put("title", notification.getTitle());
|
||||||
@@ -142,23 +165,18 @@ public class NotificationService {
|
|||||||
data.put("isRead", notification.isRead());
|
data.put("isRead", notification.isRead());
|
||||||
data.put("createdAt", notification.getCreatedAt() != null
|
data.put("createdAt", notification.getCreatedAt() != null
|
||||||
? notification.getCreatedAt().toString() : null);
|
? notification.getCreatedAt().toString() : null);
|
||||||
|
|
||||||
if (notification.getEvent() != null) {
|
if (notification.getEvent() != null) {
|
||||||
data.put("eventId", notification.getEvent().getId().toString());
|
data.put("eventId", notification.getEvent().getId().toString());
|
||||||
data.put("eventTitle", notification.getEvent().getTitle());
|
data.put("eventTitle", notification.getEvent().getTitle());
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationEvent event = new NotificationEvent(
|
NotificationEvent event = new NotificationEvent(
|
||||||
user.getId().toString(),
|
user.getId().toString(),
|
||||||
notification.getType(),
|
notification.getType(),
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
|
publishToKafkaAfterCommit(event);
|
||||||
|
|
||||||
notificationEmitter.send(event);
|
return notification;
|
||||||
logger.debug("[NotificationService] Notification publiée dans Kafka pour temps réel : " + notification.getId());
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.warn("[NotificationService] Échec publication Kafka (non bloquant) : " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ public class UsersService {
|
|||||||
public Users updateUser(UUID id, UserCreateRequestDTO userCreateRequestDTO) {
|
public Users updateUser(UUID id, UserCreateRequestDTO userCreateRequestDTO) {
|
||||||
Users existingUser = usersRepository.findById(id);
|
Users existingUser = usersRepository.findById(id);
|
||||||
if (existingUser == null) {
|
if (existingUser == null) {
|
||||||
logger.error("Utilisateur non trouvé avec l'ID : " + id);
|
logger.warn("Utilisateur non trouvé avec l'ID : " + id);
|
||||||
throw new UserNotFoundException("Utilisateur non trouvé avec l'ID : " + id);
|
throw new UserNotFoundException("Utilisateur non trouvé avec l'ID : " + id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,7 +135,7 @@ public class UsersService {
|
|||||||
public Users updateUserProfileImage(UUID id, String profileImageUrl) {
|
public Users updateUserProfileImage(UUID id, String profileImageUrl) {
|
||||||
Users existingUser = usersRepository.findById(id);
|
Users existingUser = usersRepository.findById(id);
|
||||||
if (existingUser == null) {
|
if (existingUser == null) {
|
||||||
logger.error("Utilisateur non trouvé avec l'ID : " + id);
|
logger.warn("Utilisateur non trouvé avec l'ID : " + id);
|
||||||
throw new UserNotFoundException("Utilisateur non trouvé avec l'ID : " + id);
|
throw new UserNotFoundException("Utilisateur non trouvé avec l'ID : " + id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,7 +195,7 @@ public class UsersService {
|
|||||||
public Users getUserById(UUID id) {
|
public Users getUserById(UUID id) {
|
||||||
Users user = usersRepository.findById(id);
|
Users user = usersRepository.findById(id);
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
logger.error("Utilisateur non trouvé avec l'ID : " + id);
|
logger.warn("Utilisateur non trouvé avec l'ID : " + id);
|
||||||
throw new UserNotFoundException("Utilisateur non trouvé avec l'ID : " + id);
|
throw new UserNotFoundException("Utilisateur non trouvé avec l'ID : " + id);
|
||||||
}
|
}
|
||||||
logger.debug("Utilisateur trouvé avec l'ID : " + id);
|
logger.debug("Utilisateur trouvé avec l'ID : " + id);
|
||||||
@@ -213,7 +213,7 @@ public class UsersService {
|
|||||||
public void resetPassword(UUID id, String newPassword) {
|
public void resetPassword(UUID id, String newPassword) {
|
||||||
Users user = usersRepository.findById(id);
|
Users user = usersRepository.findById(id);
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
logger.error("Utilisateur non trouvé avec l'ID : " + id);
|
logger.warn("Utilisateur non trouvé avec l'ID : " + id);
|
||||||
throw new UserNotFoundException("Utilisateur non trouvé.");
|
throw new UserNotFoundException("Utilisateur non trouvé.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,7 +248,7 @@ public class UsersService {
|
|||||||
public Users getUserByEmail(String email) {
|
public Users getUserByEmail(String email) {
|
||||||
Optional<Users> userOptional = usersRepository.findByEmail(email);
|
Optional<Users> userOptional = usersRepository.findByEmail(email);
|
||||||
if (userOptional.isEmpty()) {
|
if (userOptional.isEmpty()) {
|
||||||
logger.error("Utilisateur non trouvé avec l'email : " + email);
|
logger.warn("Utilisateur non trouvé avec l'email : " + email);
|
||||||
throw new UserNotFoundException("Utilisateur non trouvé avec l'email : " + email);
|
throw new UserNotFoundException("Utilisateur non trouvé avec l'email : " + email);
|
||||||
}
|
}
|
||||||
logger.debug("Utilisateur trouvé avec l'email : " + email);
|
logger.debug("Utilisateur trouvé avec l'email : " + email);
|
||||||
@@ -279,6 +279,7 @@ public class UsersService {
|
|||||||
public Users assignRole(UUID userId, String newRole) {
|
public Users assignRole(UUID userId, String newRole) {
|
||||||
Users user = usersRepository.findById(userId);
|
Users user = usersRepository.findById(userId);
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
|
logger.warn("Utilisateur non trouvé avec l'ID : " + userId);
|
||||||
throw new UserNotFoundException("Utilisateur non trouvé avec l'ID : " + userId);
|
throw new UserNotFoundException("Utilisateur non trouvé avec l'ID : " + userId);
|
||||||
}
|
}
|
||||||
user.setRole(newRole);
|
user.setRole(newRole);
|
||||||
@@ -300,6 +301,7 @@ public class UsersService {
|
|||||||
public Users setUserActive(UUID userId, boolean active) {
|
public Users setUserActive(UUID userId, boolean active) {
|
||||||
Users user = usersRepository.findById(userId);
|
Users user = usersRepository.findById(userId);
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
|
logger.warn("Utilisateur non trouvé avec l'ID : " + userId);
|
||||||
throw new UserNotFoundException("Utilisateur non trouvé avec l'ID : " + userId);
|
throw new UserNotFoundException("Utilisateur non trouvé avec l'ID : " + userId);
|
||||||
}
|
}
|
||||||
user.setActive(active);
|
user.setActive(active);
|
||||||
|
|||||||
@@ -224,13 +224,15 @@ public class ChatWebSocketNext {
|
|||||||
sessions.remove(userId);
|
sessions.remove(userId);
|
||||||
Log.debug("[CHAT-WS-NEXT] Connexion périmée supprimée pour " + 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() + ")");
|
Log.info("[CHAT-WS-NEXT] Destinataire " + userId + " non connecté, message non délivré (sessions: " + sessions.size() + ")");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
String preview = message.length() > 300 ? message.substring(0, 300) + "..." : message;
|
||||||
|
Log.info("[CHAT-WS-NEXT] Envoi vers " + userId + " (" + message.length() + " car): " + preview);
|
||||||
connection.sendText(message);
|
connection.sendText(message);
|
||||||
Log.debug("[CHAT-WS-NEXT] Message envoyé à l'utilisateur: " + userId);
|
Log.info("[CHAT-WS-NEXT] Message délivré à l'utilisateur: " + userId);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.error("[CHAT-WS-NEXT] Erreur lors de l'envoi à " + userId + ", connexion supprimée", e);
|
Log.error("[CHAT-WS-NEXT] Erreur lors de l'envoi à " + userId + ", connexion supprimée", e);
|
||||||
sessions.remove(userId);
|
sessions.remove(userId);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import jakarta.inject.Inject;
|
|||||||
|
|
||||||
import com.lions.dev.service.PresenceService;
|
import com.lions.dev.service.PresenceService;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@@ -60,6 +61,10 @@ public class NotificationWebSocketNext {
|
|||||||
// Marquer l'utilisateur comme en ligne
|
// Marquer l'utilisateur comme en ligne
|
||||||
presenceService.setUserOnline(userUUID);
|
presenceService.setUserOnline(userUUID);
|
||||||
|
|
||||||
|
// Envoyer au client la liste des utilisateurs déjà en ligne (snapshot)
|
||||||
|
// pour qu'il affiche correctement "En ligne" sans attendre les events Kafka
|
||||||
|
sendPresenceSnapshotTo(connection);
|
||||||
|
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
Log.error("[WS-NEXT] UUID invalide: " + userId, e);
|
Log.error("[WS-NEXT] UUID invalide: " + userId, e);
|
||||||
connection.close();
|
connection.close();
|
||||||
@@ -216,6 +221,27 @@ public class NotificationWebSocketNext {
|
|||||||
" sessions sur " + totalSessions);
|
" sessions sur " + totalSessions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Envoie à une connexion la liste de tous les utilisateurs actuellement en ligne.
|
||||||
|
* Appelé à l'onOpen pour que le client affiche tout de suite le bon statut.
|
||||||
|
*/
|
||||||
|
private static void sendPresenceSnapshotTo(WebSocketConnection connection) {
|
||||||
|
if (!connection.isOpen()) return;
|
||||||
|
try {
|
||||||
|
for (UUID onlineUserId : userConnections.keySet()) {
|
||||||
|
Map<String, Object> presenceData = new HashMap<>();
|
||||||
|
presenceData.put("userId", onlineUserId.toString());
|
||||||
|
presenceData.put("isOnline", true);
|
||||||
|
presenceData.put("timestamp", System.currentTimeMillis());
|
||||||
|
String json = buildJsonMessage("presence", presenceData);
|
||||||
|
connection.sendText(json);
|
||||||
|
}
|
||||||
|
Log.debug("[WS-NEXT] Snapshot présence envoyé (" + userConnections.size() + " utilisateur(s) en ligne)");
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.error("[WS-NEXT] Erreur envoi snapshot présence", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Broadcast une mise à jour de présence à tous les utilisateurs connectés.
|
* Broadcast une mise à jour de présence à tous les utilisateurs connectés.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -36,9 +36,9 @@ public class ChatKafkaBridge {
|
|||||||
public CompletionStage<Void> processChatMessage(Message<ChatMessageEvent> message) {
|
public CompletionStage<Void> processChatMessage(Message<ChatMessageEvent> message) {
|
||||||
try {
|
try {
|
||||||
ChatMessageEvent event = message.getPayload();
|
ChatMessageEvent event = message.getPayload();
|
||||||
|
// Log INFO pour confirmer la consommation Kafka (diagnostic "aucun log dans kafka")
|
||||||
Log.debug("[CHAT-BRIDGE] Événement reçu: " + event.getEventType() +
|
Log.info("[CHAT-BRIDGE] Kafka consommé: type=" + event.getEventType() +
|
||||||
" de " + event.getSenderId() + " à " + event.getRecipientId());
|
" de " + event.getSenderId() + " vers " + event.getRecipientId());
|
||||||
|
|
||||||
UUID recipientId = UUID.fromString(event.getRecipientId());
|
UUID recipientId = UUID.fromString(event.getRecipientId());
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ import jakarta.enterprise.context.ApplicationScoped;
|
|||||||
import org.eclipse.microprofile.reactive.messaging.Incoming;
|
import org.eclipse.microprofile.reactive.messaging.Incoming;
|
||||||
import org.eclipse.microprofile.reactive.messaging.Message;
|
import org.eclipse.microprofile.reactive.messaging.Message;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.CompletableFuture;
|
|
||||||
import java.util.concurrent.CompletionStage;
|
import java.util.concurrent.CompletionStage;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -42,8 +44,8 @@ public class NotificationKafkaBridge {
|
|||||||
try {
|
try {
|
||||||
NotificationEvent event = message.getPayload();
|
NotificationEvent event = message.getPayload();
|
||||||
|
|
||||||
Log.debug("[KAFKA-BRIDGE] Événement reçu: " + event.getType() +
|
Log.info("[KAFKA-BRIDGE] Événement reçu: type=" + event.getType() +
|
||||||
" pour utilisateur: " + event.getUserId());
|
" userId=" + event.getUserId());
|
||||||
|
|
||||||
UUID userId = UUID.fromString(event.getUserId());
|
UUID userId = UUID.fromString(event.getUserId());
|
||||||
|
|
||||||
@@ -69,16 +71,24 @@ public class NotificationKafkaBridge {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Construit le message JSON pour WebSocket à partir de l'événement Kafka.
|
* Construit le message JSON pour WebSocket à partir de l'événement Kafka.
|
||||||
|
* Le champ "data" inclut "timestamp" et "type" pour compatibilité client Flutter (SystemNotification.fromJson).
|
||||||
*/
|
*/
|
||||||
private String buildWebSocketMessage(NotificationEvent event) {
|
private String buildWebSocketMessage(NotificationEvent event) {
|
||||||
try {
|
try {
|
||||||
com.fasterxml.jackson.databind.ObjectMapper mapper =
|
com.fasterxml.jackson.databind.ObjectMapper mapper =
|
||||||
new com.fasterxml.jackson.databind.ObjectMapper();
|
new com.fasterxml.jackson.databind.ObjectMapper();
|
||||||
|
|
||||||
java.util.Map<String, Object> wsMessage = java.util.Map.of(
|
long ts = event.getTimestamp() != null ? event.getTimestamp() : System.currentTimeMillis();
|
||||||
|
Map<String, Object> data = event.getData() != null
|
||||||
|
? new HashMap<>(event.getData())
|
||||||
|
: new HashMap<>();
|
||||||
|
data.put("timestamp", Instant.ofEpochMilli(ts).toString());
|
||||||
|
data.put("type", event.getType());
|
||||||
|
|
||||||
|
Map<String, Object> wsMessage = Map.of(
|
||||||
"type", event.getType(),
|
"type", event.getType(),
|
||||||
"data", event.getData() != null ? event.getData() : java.util.Map.of(),
|
"data", data,
|
||||||
"timestamp", event.getTimestamp() != null ? event.getTimestamp() : System.currentTimeMillis()
|
"timestamp", ts
|
||||||
);
|
);
|
||||||
|
|
||||||
return mapper.writeValueAsString(wsMessage);
|
return mapper.writeValueAsString(wsMessage);
|
||||||
|
|||||||
@@ -12,13 +12,16 @@
|
|||||||
afterwork.super-admin.api-key=${SUPER_ADMIN_API_KEY:dev-super-admin-key}
|
afterwork.super-admin.api-key=${SUPER_ADMIN_API_KEY:dev-super-admin-key}
|
||||||
|
|
||||||
# ====================================================================
|
# ====================================================================
|
||||||
# Base de données H2 (en mémoire)
|
# Base de données PostgreSQL (développement local)
|
||||||
# ====================================================================
|
# ====================================================================
|
||||||
quarkus.datasource.db-kind=h2
|
# H2 ne supporte pas LISTEN/NOTIFY ni certaines fonctionnalités temps réel.
|
||||||
quarkus.datasource.jdbc.url=jdbc:h2:mem:afterwork_db;DB_CLOSE_DELAY=-1
|
# Utiliser PostgreSQL en dev avec les identifiants ci-dessous.
|
||||||
quarkus.datasource.username=sa
|
quarkus.datasource.db-kind=postgresql
|
||||||
quarkus.datasource.password=
|
# En dev local (mvn quarkus:dev sur l'hôte) : localhost. En conteneur Docker : définir DB_HOST=host.docker.internal
|
||||||
quarkus.datasource.jdbc.driver=org.h2.Driver
|
quarkus.datasource.jdbc.url=jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:afterwork_dev}?connectTimeout=10000
|
||||||
|
quarkus.datasource.username=${DB_USERNAME:skyfile}
|
||||||
|
quarkus.datasource.password=${DB_PASSWORD:skyfile}
|
||||||
|
quarkus.datasource.jdbc.driver=org.postgresql.Driver
|
||||||
quarkus.datasource.devservices.enabled=false
|
quarkus.datasource.devservices.enabled=false
|
||||||
|
|
||||||
# ====================================================================
|
# ====================================================================
|
||||||
@@ -34,13 +37,16 @@ quarkus.hibernate-orm.schema-generation.scripts.action=drop-and-create
|
|||||||
# ====================================================================
|
# ====================================================================
|
||||||
# Kafka (développement local)
|
# Kafka (développement local)
|
||||||
# ====================================================================
|
# ====================================================================
|
||||||
# En dev, même connecteur que la base (smallrye-kafka). Démarrer Kafka en local
|
# En dev, Kafka doit être joignable sur le port 9092 (conteneur Docker avec -p 9092:9092).
|
||||||
# (ex. docker-compose) ou définir KAFKA_BOOTSTRAP_SERVERS si Kafka est ailleurs.
|
# Si Kafka est ailleurs, définir KAFKA_BOOTSTRAP_SERVERS (ex: host.docker.internal:9092).
|
||||||
afterwork.kafka.enabled=${KAFKA_ENABLED:true}
|
afterwork.kafka.enabled=${KAFKA_ENABLED:true}
|
||||||
kafka.bootstrap.servers=${KAFKA_BOOTSTRAP_SERVERS:localhost:9092}
|
kafka.bootstrap.servers=${KAFKA_BOOTSTRAP_SERVERS:localhost:9092}
|
||||||
|
|
||||||
# SmallRye Reactive Messaging - Utiliser smallrye-kafka (pas d'extension in-memory en runtime).
|
# Propager explicitement bootstrap.servers au connecteur SmallRye Kafka (évite les soucis de résolution).
|
||||||
# Les canaux sont définis dans application.properties.
|
mp.messaging.connector.smallrye-kafka.bootstrap.servers=${KAFKA_BOOTSTRAP_SERVERS:localhost:9092}
|
||||||
|
|
||||||
|
# SmallRye Reactive Messaging - Les canaux sont définis dans application.properties.
|
||||||
|
# Voir REALTIME_DEV.md pour faire fonctionner le temps réel en local.
|
||||||
|
|
||||||
# ====================================================================
|
# ====================================================================
|
||||||
# Logging
|
# Logging
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ quarkus.http.root-path=/afterwork
|
|||||||
# ====================================================================
|
# ====================================================================
|
||||||
# Quarkus fixe db-kind, jdbc.driver et root-path au BUILD. Si on met H2 ici, le JAR
|
# Quarkus fixe db-kind, jdbc.driver et root-path au BUILD. Si on met H2 ici, le JAR
|
||||||
# ne pourra pas utiliser PostgreSQL en prod. Donc on met la config prod par défaut ;
|
# ne pourra pas utiliser PostgreSQL en prod. Donc on met la config prod par défaut ;
|
||||||
# les profils dev/test surchargent avec H2 (application-dev.properties et %test en test).
|
# le profil dev surcharge avec PostgreSQL local (application-dev.properties).
|
||||||
quarkus.datasource.db-kind=postgresql
|
quarkus.datasource.db-kind=postgresql
|
||||||
quarkus.datasource.jdbc.url=jdbc:postgresql://${DB_HOST:postgresql-service.postgresql.svc.cluster.local}:${DB_PORT:5432}/${DB_NAME:mic-after-work-server-impl-quarkus-main}
|
quarkus.datasource.jdbc.url=jdbc:postgresql://${DB_HOST:postgresql-service.postgresql.svc.cluster.local}:${DB_PORT:5432}/${DB_NAME:mic-after-work-server-impl-quarkus-main}
|
||||||
quarkus.datasource.username=${DB_USERNAME:lionsuser}
|
quarkus.datasource.username=${DB_USERNAME:lionsuser}
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ class EstablishmentRatingServiceTest {
|
|||||||
|
|
||||||
assertThrows(RuntimeException.class, () ->
|
assertThrows(RuntimeException.class, () ->
|
||||||
establishmentRatingService.submitRating(establishmentId, userId, requestDTO));
|
establishmentRatingService.submitRating(establishmentId, userId, requestDTO));
|
||||||
verify(ratingRepository, never()).persist(any());
|
verify(ratingRepository, never()).persist(any(EstablishmentRating.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Reference in New Issue
Block a user