diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 055604a..2a961d4 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -2,7 +2,7 @@ ## 📋 Vue d'Ensemble -Ce guide dĂ©crit le processus de dĂ©ploiement de l'API AfterWork sur le VPS via `lionesctl pipeline`. +Ce guide dĂ©crit le processus de dĂ©ploiement de l'API AfterWork sur le VPS via `lionsctl pipeline`. **URL de l'API** : `https://api.lions.dev/afterwork` @@ -14,7 +14,7 @@ Ce guide dĂ©crit le processus de dĂ©ploiement de l'API AfterWork sur le VPS via - Java 17 (JDK) - Maven 3.9+ - Docker 20.10+ -- `lionesctl` CLI installĂ© et configurĂ© +- `lionsctl` CLI installĂ© et configurĂ© ### Environnement Serveur - PostgreSQL 15+ @@ -86,31 +86,66 @@ docker push registry.lions.dev/afterwork-api:latest --- -## 🚱 DĂ©ploiement avec lionesctl +## 🚱 DĂ©ploiement avec lionsctl pipeline -### Commande de DĂ©ploiement +La commande **`lionsctl pipeline`** clone le repo Git, compile (Maven), construit l'image Docker, dĂ©ploie sur Kubernetes et envoie une notification email. Il n'y a pas de sous-commande `deploy` : tout est inclus dans `lionsctl pipeline`. + +### Commande de dĂ©ploiement + +Remplacez `` par votre organisation Git (ex. `lionsdev`, `developer`) et `` par l'adresse de notification. ```bash -# DĂ©ploiement via lionesctl pipeline -lionesctl pipeline deploy \ - --app afterwork-api \ - --image registry.lions.dev/afterwork-api:1.0.0 \ - --namespace applications \ - --port 8080 \ - --replicas 2 +# DĂ©ploiement en dev (clone + build + image + dĂ©ploiement K8s) +lionsctl pipeline \ + -u https://git.lions.dev//mic-after-work-server-impl-quarkus-main \ + -b develop \ + -j 17 \ + -e dev \ + -c k1 \ + -m -# Ou avec le fichier de configuration -lionesctl pipeline deploy -f kubernetes/afterwork-deployment.yaml +# DĂ©ploiement en production sur le cluster k2 +lionsctl pipeline \ + -u https://git.lions.dev//mic-after-work-server-impl-quarkus-main \ + -b main \ + -j 17 \ + -e production \ + -c k2 \ + -m \ + -p prod + +# Avec dĂ©ploiement Helm (charts gĂ©nĂ©rĂ©s automatiquement) +lionsctl pipeline \ + -u https://git.lions.dev//mic-after-work-server-impl-quarkus-main \ + -b develop \ + -j 17 \ + -e dev \ + -c k1 \ + -m \ + --use-helm ``` -### VĂ©rification du DĂ©ploiement +**Options principales :** + +| Option | Description | Exemple | +|--------|-------------|---------| +| `-u`, `--url` | URL du repo Git (obligatoire) | `https://git.lions.dev/.../mic-after-work-server-impl-quarkus-main` | +| `-b`, `--branch` | Branche Ă  dĂ©ployer | `develop`, `main` | +| `-j`, `--java-version` | Version Java (8–21) | `17` | +| `-e`, `--environment` | Environnement (dev / staging / production) | `dev`, `production` | +| `-c`, `--cluster` | Cluster Kubernetes (k1 ou k2) (obligatoire) | `k1`, `k2` | +| `-m`, `--mail` | Email(s) pour les notifications | `admin@lions.dev` | +| `-p`, `--profile` | Profil Maven | `prod` pour production | +| `--use-helm` | DĂ©ployer via Helm | — | + +### VĂ©rification du dĂ©ploiement ```bash -# Status du dĂ©ploiement -lionesctl pipeline status --app afterwork-api +# Pods et statut (nom d'app dĂ©rivĂ© du repo, ex. mic-after-work-server-impl-quarkus-main) +kubectl get pods -n applications -l app=mic-after-work-server-impl-quarkus-main # Logs en temps rĂ©el -lionesctl pipeline logs --app afterwork-api --follow +kubectl logs -n applications -l app=mic-after-work-server-impl-quarkus-main -f # Health check curl https://api.lions.dev/afterwork/q/health/ready @@ -325,8 +360,8 @@ kubectl apply -f kubernetes/afterwork-deployment.yaml kubectl apply -f kubernetes/afterwork-service.yaml kubectl apply -f kubernetes/afterwork-ingress.yaml -# Ou via lionesctl pipeline -lionesctl pipeline deploy -f kubernetes/ +# Ou via lionsctl pipeline (clone + build + dĂ©ploiement) +lionsctl pipeline -u https://git.lions.dev//mic-after-work-server-impl-quarkus-main -b develop -j 17 -e dev -c k1 -m ``` ### Étape 5 : VĂ©rification diff --git a/DEPLOYMENT_STATUS.md b/DEPLOYMENT_STATUS.md index b21f447..cba6e64 100644 --- a/DEPLOYMENT_STATUS.md +++ b/DEPLOYMENT_STATUS.md @@ -88,7 +88,7 @@ cd C:\Users\dadyo\PersonalProjects\mic-after-work-server-impl-quarkus-main .\deploy.ps1 -Action deploy # DĂ©ploiement K8s ``` -### 3ïžâƒŁ DĂ©ployer via lionesctl (Alternative) +### 3ïžâƒŁ DĂ©ployer via lionsctl pipeline (Alternative) ```bash cd C:\Users\dadyo\PersonalProjects\mic-after-work-server-impl-quarkus-main @@ -99,7 +99,7 @@ docker build -f Dockerfile.prod -t registry.lions.dev/afterwork-api:1.0.0 . docker push registry.lions.dev/afterwork-api:1.0.0 # DĂ©ploiement -lionesctl pipeline deploy -f kubernetes/ +lionsctl pipeline -u https://git.lions.dev//mic-after-work-server-impl-quarkus-main -b develop -j 17 -e dev -c k1 -m ``` ### 4ïžâƒŁ VĂ©rifier le DĂ©ploiement diff --git a/QUICK_DEPLOY.md b/QUICK_DEPLOY.md index bd005b1..3484aa3 100644 --- a/QUICK_DEPLOY.md +++ b/QUICK_DEPLOY.md @@ -48,18 +48,15 @@ kubectl get pods -n applications -l app=afterwork-api kubectl logs -n applications -l app=afterwork-api -f ``` -### Option 3 : DĂ©ploiement via lionesctl +### Option 3 : DĂ©ploiement via lionsctl pipeline ```bash cd C:\Users\dadyo\PersonalProjects\mic-after-work-server-impl-quarkus-main -# Build local -mvn clean package -DskipTests -docker build -f docker/Dockerfile.prod -t registry.lions.dev/afterwork-api:1.0.0 . -docker push registry.lions.dev/afterwork-api:1.0.0 +# Le pipeline clone le repo, build Maven, construit l’image Docker et dĂ©ploie sur K8s. Remplacer et . # DĂ©ploiement -lionesctl pipeline deploy -f kubernetes/ +lionsctl pipeline -u https://git.lions.dev//mic-after-work-server-impl-quarkus-main -b develop -j 17 -e dev -c k1 -m ``` --- diff --git a/README.md b/README.md index f5a7b18..ab48e16 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,36 @@ You can then execute your native executable with: `./target/mic-after-work-serve If you want to learn more about building native executables, please consult . +## FonctionnalitĂ©s mĂ©tier (AfterWork) + +### Notifications + +- **Service** : `NotificationService` — crĂ©ation, lecture, pagination, marquage lu/suppression des notifications en base. +- **DĂ©clencheurs** : Notifications créées automatiquement pour les demandes d’amitiĂ© (destinataire), les likes/commentaires sur les posts (auteur du post), les nouvelles notes d’établissement (manager). +- **API** : `GET/POST /notifications/user/{userId}`, pagination, marquer lu, supprimer. Voir [SECURITY.md](SECURITY.md) pour l’usage en production (userId issu de l’auth). + +### Jobs planifiĂ©s (Quarkus Scheduler) + +- **Stories** : DĂ©sactivation des stories expirĂ©es (cron : toutes les heures). +- **Tokens** : Suppression des tokens de rĂ©initialisation de mot de passe expirĂ©s (tous les jours Ă  3h). +- **Abonnements** : Expiration des abonnements Ă©tablissements et dĂ©sactivation des Ă©tablissements non payĂ©s (toutes les heures). +- **Rappels Ă©vĂ©nements** : Notifications en base pour les participants (J-1 et H-1), exĂ©cution toutes les 15 minutes. +- **Avertissement abonnement** : Envoi d’emails J-3 avant expiration aux managers (tous les jours Ă  9h). + +Configuration : `quarkus.scheduler.enabled=true` (dĂ©sactivĂ© en test via `%test.quarkus.scheduler.enabled=false`). + +### Emails transactionnels + +- **EmailService** : RĂ©initialisation mot de passe, bienvenue, confirmation de paiement Wave, rappel Ă©vĂ©nement, avertissement expiration abonnement, confirmation de rĂ©servation, Ă©chec de paiement Wave. +- Configuration SMTP via variables d’environnement (`MAILER_HOST`, `MAILER_USERNAME`, `MAILER_PASSWORD`, etc.) ; en test le mailer peut ĂȘtre en mode mock. + +### Paiement Wave (Ă©tablissements) + +- Initiation de paiement (abonnement mensuel/annuel), webhook `POST /webhooks/wave` pour `payment.completed`, `payment.refunded`, `payment.failed`, etc. +- VĂ©rification optionnelle de la signature du webhook (header `X-Wave-Signature`, HMAC-SHA256) si `wave.webhook.secret` est configurĂ©. Voir [SECURITY.md](SECURITY.md). + +--- + ## Related Guides - Hibernate ORM ([guide](https://quarkus.io/guides/hibernate-orm)): Define your persistent model with Hibernate ORM and Jakarta Persistence @@ -61,6 +91,11 @@ If you want to learn more about building native executables, please consult io.quarkus quarkus-flyway + + + io.quarkus + quarkus-scheduler + org.projectlombok lombok @@ -107,11 +112,21 @@ bcrypt 0.10.2 + + + io.quarkus + quarkus-mailer + io.quarkus quarkus-junit5 test + + io.quarkus + quarkus-junit5-mockito + test + io.rest-assured rest-assured diff --git a/src/main/java/com/lions/dev/config/ScheduledJobs.java b/src/main/java/com/lions/dev/config/ScheduledJobs.java new file mode 100644 index 0000000..3a83bfc --- /dev/null +++ b/src/main/java/com/lions/dev/config/ScheduledJobs.java @@ -0,0 +1,171 @@ +package com.lions.dev.config; + +import com.lions.dev.entity.events.Events; +import com.lions.dev.entity.establishment.Establishment; +import com.lions.dev.entity.establishment.EstablishmentSubscription; +import com.lions.dev.entity.users.Users; +import com.lions.dev.repository.EstablishmentRepository; +import com.lions.dev.repository.EstablishmentSubscriptionRepository; +import com.lions.dev.repository.EventsRepository; +import com.lions.dev.repository.PasswordResetTokenRepository; +import com.lions.dev.repository.StoryRepository; +import com.lions.dev.service.EmailService; +import com.lions.dev.service.NotificationService; +import io.quarkus.scheduler.Scheduled; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import org.jboss.logging.Logger; + +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.Set; + +/** + * Jobs planifiĂ©s (Quarkus Scheduler) pour : + * - Nettoyage des stories expirĂ©es (24h) + * - Nettoyage des tokens de reset password expirĂ©s + * - Expiration des abonnements Ă©tablissements + * - DĂ©sactivation des Ă©tablissements non payĂ©s + * - Rappels d'Ă©vĂ©nements (J-1, H-1) + */ +@ApplicationScoped +public class ScheduledJobs { + + private static final Logger LOG = Logger.getLogger(ScheduledJobs.class); + + @Inject + StoryRepository storyRepository; + + @Inject + PasswordResetTokenRepository passwordResetTokenRepository; + + @Inject + EstablishmentSubscriptionRepository subscriptionRepository; + + @Inject + EstablishmentRepository establishmentRepository; + + @Inject + EventsRepository eventsRepository; + + @Inject + NotificationService notificationService; + + @Inject + EmailService emailService; + + /** Nettoyage des stories expirĂ©es : toutes les heures. */ + @Scheduled(cron = "0 0 * * * ?") + @Transactional + public void deactivateExpiredStories() { + int count = storyRepository.deactivateExpiredStories(); + if (count > 0) { + LOG.info("[ScheduledJobs] Stories expirĂ©es dĂ©sactivĂ©es : " + count); + } + } + + /** Nettoyage des tokens de reset password expirĂ©s : tous les jours Ă  3h. */ + @Scheduled(cron = "0 0 3 * * ?") + @Transactional + public void deleteExpiredPasswordResetTokens() { + long count = passwordResetTokenRepository.deleteExpiredTokens(); + if (count > 0) { + LOG.info("[ScheduledJobs] Tokens de reset password supprimĂ©s : " + count); + } + } + + /** Expiration des abonnements et dĂ©sactivation des Ă©tablissements non payĂ©s : toutes les heures. */ + @Scheduled(cron = "0 5 * * * ?") + @Transactional + public void expireSubscriptionsAndDisableEstablishments() { + List expired = subscriptionRepository.findExpiredActiveSubscriptions(); + for (EstablishmentSubscription sub : expired) { + sub.setStatus(EstablishmentSubscription.STATUS_EXPIRED); + subscriptionRepository.persist(sub); + Establishment est = establishmentRepository.findById(sub.getEstablishmentId()); + if (est != null && Boolean.TRUE.equals(est.getIsActive())) { + est.setIsActive(false); + establishmentRepository.persist(est); + LOG.info("[ScheduledJobs] Établissement dĂ©sactivĂ© (abonnement expirĂ©) : " + est.getId()); + } + } + if (!expired.isEmpty()) { + LOG.info("[ScheduledJobs] Abonnements expirĂ©s traitĂ©s : " + expired.size()); + } + } + + /** Rappels d'Ă©vĂ©nements J-1 (dans ~24h) et H-1 (dans ~1h) : toutes les 15 minutes. */ + @Scheduled(cron = "0 */15 * * * ?") + @Transactional + public void sendEventReminders() { + LocalDateTime now = LocalDateTime.now(); + // FenĂȘtre J-1 : dĂ©but entre 23h30 et 24h30 + LocalDateTime j1From = now.plus(23, ChronoUnit.HOURS).plus(30, ChronoUnit.MINUTES); + LocalDateTime j1To = now.plus(24, ChronoUnit.HOURS).plus(30, ChronoUnit.MINUTES); + List eventsJ1 = eventsRepository.findEventsStartingBetween(j1From, j1To); + for (Events event : eventsJ1) { + sendReminderToParticipants(event, "J-1", "demain"); + } + // FenĂȘtre H-1 : dĂ©but entre 50 min et 1h10 + LocalDateTime h1From = now.plus(50, ChronoUnit.MINUTES); + LocalDateTime h1To = now.plus(70, ChronoUnit.MINUTES); + List eventsH1 = eventsRepository.findEventsStartingBetween(h1From, h1To); + for (Events event : eventsH1) { + sendReminderToParticipants(event, "H-1", "dans 1 heure"); + } + } + + /** Avertissement expiration abonnement (J-3) : email au manager. */ + @Scheduled(cron = "0 0 9 * * ?") + @Transactional + public void sendSubscriptionExpirationWarningEmails() { + LocalDateTime now = LocalDateTime.now(); + LocalDateTime in3DaysStart = now.plusDays(3); + LocalDateTime in3DaysEnd = now.plusDays(3).plusHours(23).plusMinutes(59); + List expiring = subscriptionRepository.findActiveSubscriptionsExpiringBetween(in3DaysStart, in3DaysEnd); + for (EstablishmentSubscription sub : expiring) { + Establishment est = establishmentRepository.findById(sub.getEstablishmentId()); + if (est == null) continue; + Users manager = est.getManager(); + if (manager == null || manager.getEmail() == null) continue; + try { + emailService.sendSubscriptionExpirationWarningEmail( + manager.getEmail(), + manager.getFirstName(), + est.getName(), + sub.getExpiresAt() + ); + } catch (Exception e) { + LOG.warn("[ScheduledJobs] Email expiration abonnement Ă©chouĂ© pour " + est.getId() + ": " + e.getMessage()); + } + } + if (!expiring.isEmpty()) { + LOG.info("[ScheduledJobs] Emails avertissement expiration envoyĂ©s : " + expiring.size()); + } + } + + private void sendReminderToParticipants(Events event, String reminderType, String whenText) { + Set participants = event.getParticipants(); + if (participants == null) return; + Users creator = event.getCreator(); + String title = "Rappel Ă©vĂ©nement " + reminderType + " : " + event.getTitle(); + String message = "L'Ă©vĂ©nement « " + event.getTitle() + " » commence " + whenText + "."; + for (Users participant : participants) { + if (participant == null || participant.getId() == null) continue; + try { + notificationService.createNotification(title, message, "reminder", participant.getId(), event.getId()); + } catch (Exception e) { + LOG.warn("[ScheduledJobs] Impossible de crĂ©er rappel pour participant " + participant.getId() + ": " + e.getMessage()); + } + } + if (creator != null && creator.getId() != null && (participants.isEmpty() || !participants.stream().anyMatch(p -> p.getId().equals(creator.getId())))) { + try { + notificationService.createNotification(title, message, "reminder", creator.getId(), event.getId()); + } catch (Exception e) { + LOG.warn("[ScheduledJobs] Impossible de crĂ©er rappel pour crĂ©ateur: " + e.getMessage()); + } + } + } +} diff --git a/src/main/java/com/lions/dev/core/errors/Exceptions.java b/src/main/java/com/lions/dev/core/errors/Exceptions.java index d7c2165..fd28090 100644 --- a/src/main/java/com/lions/dev/core/errors/Exceptions.java +++ b/src/main/java/com/lions/dev/core/errors/Exceptions.java @@ -13,6 +13,5 @@ public abstract class Exceptions extends Exception { */ public Exceptions(String message) { super(message); - System.out.println("[ERROR] Exception dĂ©clenchĂ©e : " + message); } } diff --git a/src/main/java/com/lions/dev/core/errors/Failures.java b/src/main/java/com/lions/dev/core/errors/Failures.java index 2dc591e..2bb6147 100644 --- a/src/main/java/com/lions/dev/core/errors/Failures.java +++ b/src/main/java/com/lions/dev/core/errors/Failures.java @@ -15,7 +15,6 @@ public class Failures { */ public Failures(String failureMessage) { this.failureMessage = failureMessage; - System.out.println("[FAILURE] Échec dĂ©tectĂ© : " + failureMessage); } /** diff --git a/src/main/java/com/lions/dev/core/errors/exceptions/BadRequestException.java b/src/main/java/com/lions/dev/core/errors/exceptions/BadRequestException.java index cf57486..07a371e 100644 --- a/src/main/java/com/lions/dev/core/errors/exceptions/BadRequestException.java +++ b/src/main/java/com/lions/dev/core/errors/exceptions/BadRequestException.java @@ -16,7 +16,6 @@ public class BadRequestException extends WebApplicationException { */ public BadRequestException(String message) { super(message, Response.Status.BAD_REQUEST); - System.out.println("[ERROR] RequĂȘte invalide : " + message); } } diff --git a/src/main/java/com/lions/dev/core/errors/exceptions/NotFoundException.java b/src/main/java/com/lions/dev/core/errors/exceptions/NotFoundException.java index 1c042bf..29c0692 100644 --- a/src/main/java/com/lions/dev/core/errors/exceptions/NotFoundException.java +++ b/src/main/java/com/lions/dev/core/errors/exceptions/NotFoundException.java @@ -16,6 +16,5 @@ public class NotFoundException extends WebApplicationException { */ public NotFoundException(String message) { super(message, Response.Status.NOT_FOUND); - System.out.println("[ERROR] Ressource non trouvĂ©e : " + message); } } diff --git a/src/main/java/com/lions/dev/core/errors/exceptions/ServerException.java b/src/main/java/com/lions/dev/core/errors/exceptions/ServerException.java index 297d537..2c8c85c 100644 --- a/src/main/java/com/lions/dev/core/errors/exceptions/ServerException.java +++ b/src/main/java/com/lions/dev/core/errors/exceptions/ServerException.java @@ -13,6 +13,5 @@ public class ServerException extends RuntimeException { */ public ServerException(String message) { super(message); - System.out.println("[ERROR] Erreur serveur : " + message); } } diff --git a/src/main/java/com/lions/dev/core/errors/exceptions/UnauthorizedException.java b/src/main/java/com/lions/dev/core/errors/exceptions/UnauthorizedException.java index 0cbc2fe..b94ab34 100644 --- a/src/main/java/com/lions/dev/core/errors/exceptions/UnauthorizedException.java +++ b/src/main/java/com/lions/dev/core/errors/exceptions/UnauthorizedException.java @@ -16,6 +16,5 @@ public class UnauthorizedException extends WebApplicationException { */ public UnauthorizedException(String message) { super(message, Response.Status.UNAUTHORIZED); - System.out.println("[ERROR] AccĂšs non autorisĂ© : " + message); } } diff --git a/src/main/java/com/lions/dev/dto/request/establishment/InitiateSubscriptionRequestDTO.java b/src/main/java/com/lions/dev/dto/request/establishment/InitiateSubscriptionRequestDTO.java index 787e098..22070b4 100644 --- a/src/main/java/com/lions/dev/dto/request/establishment/InitiateSubscriptionRequestDTO.java +++ b/src/main/java/com/lions/dev/dto/request/establishment/InitiateSubscriptionRequestDTO.java @@ -2,6 +2,7 @@ package com.lions.dev.dto.request.establishment; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -21,5 +22,6 @@ public class InitiateSubscriptionRequestDTO { /** NumĂ©ro de tĂ©lĂ©phone client au format international (ex. 221771234567). */ @NotBlank(message = "Le numĂ©ro de tĂ©lĂ©phone client est obligatoire pour Wave") + @Size(max = 25, message = "Le numĂ©ro de tĂ©lĂ©phone ne peut pas dĂ©passer 25 caractĂšres") private String clientPhone; } diff --git a/src/main/java/com/lions/dev/dto/request/events/EventCreateRequestDTO.java b/src/main/java/com/lions/dev/dto/request/events/EventCreateRequestDTO.java index d6b64a7..6357d2e 100644 --- a/src/main/java/com/lions/dev/dto/request/events/EventCreateRequestDTO.java +++ b/src/main/java/com/lions/dev/dto/request/events/EventCreateRequestDTO.java @@ -6,6 +6,7 @@ import lombok.Setter; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; import java.time.LocalDateTime; +import org.jboss.logging.Logger; /** * DTO pour la crĂ©ation d'un Ă©vĂ©nement. @@ -61,7 +62,6 @@ public class EventCreateRequestDTO { private String location; public EventCreateRequestDTO() { - System.out.println("[LOG] DTO de requĂȘte de crĂ©ation d'Ă©vĂ©nement initialisĂ©."); } /** diff --git a/src/main/java/com/lions/dev/dto/request/events/EventDeleteRequestDTO.java b/src/main/java/com/lions/dev/dto/request/events/EventDeleteRequestDTO.java index 57c59c7..3b4c66a 100644 --- a/src/main/java/com/lions/dev/dto/request/events/EventDeleteRequestDTO.java +++ b/src/main/java/com/lions/dev/dto/request/events/EventDeleteRequestDTO.java @@ -2,6 +2,7 @@ package com.lions.dev.dto.request.events; import jakarta.validation.constraints.NotNull; import java.util.UUID; +import org.jboss.logging.Logger; /** * DTO pour la suppression d'un Ă©vĂ©nement. @@ -15,6 +16,5 @@ public class EventDeleteRequestDTO { private UUID eventId; // ID de l'Ă©vĂ©nement Ă  supprimer public EventDeleteRequestDTO() { - System.out.println("[LOG] DTO de requĂȘte de suppression d'Ă©vĂ©nement initialisĂ©."); } } diff --git a/src/main/java/com/lions/dev/dto/request/events/EventReadOneByIdRequestDTO.java b/src/main/java/com/lions/dev/dto/request/events/EventReadOneByIdRequestDTO.java index 7765d9b..9e18e1c 100644 --- a/src/main/java/com/lions/dev/dto/request/events/EventReadOneByIdRequestDTO.java +++ b/src/main/java/com/lions/dev/dto/request/events/EventReadOneByIdRequestDTO.java @@ -2,6 +2,7 @@ package com.lions.dev.dto.request.events; import jakarta.validation.constraints.NotNull; import java.util.UUID; +import org.jboss.logging.Logger; /** * DTO pour lire un Ă©vĂ©nement par son ID. @@ -15,6 +16,5 @@ public class EventReadOneByIdRequestDTO { private UUID eventId; // ID de l'Ă©vĂ©nement Ă  lire public EventReadOneByIdRequestDTO() { - System.out.println("[LOG] DTO de requĂȘte de lecture d'Ă©vĂ©nement initialisĂ©."); } } diff --git a/src/main/java/com/lions/dev/dto/request/social/SocialPostCreateRequestDTO.java b/src/main/java/com/lions/dev/dto/request/social/SocialPostCreateRequestDTO.java index 1fec60e..3171d16 100644 --- a/src/main/java/com/lions/dev/dto/request/social/SocialPostCreateRequestDTO.java +++ b/src/main/java/com/lions/dev/dto/request/social/SocialPostCreateRequestDTO.java @@ -6,6 +6,7 @@ import jakarta.validation.constraints.Size; import java.util.UUID; import lombok.Getter; import lombok.Setter; +import org.jboss.logging.Logger; /** * DTO pour la crĂ©ation d'un post social. @@ -29,7 +30,6 @@ public class SocialPostCreateRequestDTO { private String imageUrl; // URL de l'image (optionnel) public SocialPostCreateRequestDTO() { - System.out.println("[LOG] DTO de requĂȘte de crĂ©ation de post social initialisĂ©."); } } diff --git a/src/main/java/com/lions/dev/dto/request/story/StoryCreateRequestDTO.java b/src/main/java/com/lions/dev/dto/request/story/StoryCreateRequestDTO.java index 0898689..cd4f364 100644 --- a/src/main/java/com/lions/dev/dto/request/story/StoryCreateRequestDTO.java +++ b/src/main/java/com/lions/dev/dto/request/story/StoryCreateRequestDTO.java @@ -7,6 +7,7 @@ import jakarta.validation.constraints.Size; import java.util.UUID; import lombok.Getter; import lombok.Setter; +import org.jboss.logging.Logger; /** * DTO pour la crĂ©ation d'une story. @@ -34,6 +35,5 @@ public class StoryCreateRequestDTO { private Integer durationSeconds; // DurĂ©e en secondes (optionnel, pour les vidĂ©os) public StoryCreateRequestDTO() { - System.out.println("[LOG] DTO de requĂȘte de crĂ©ation de story initialisĂ©."); } } diff --git a/src/main/java/com/lions/dev/dto/request/users/SetUserActiveRequestDTO.java b/src/main/java/com/lions/dev/dto/request/users/SetUserActiveRequestDTO.java new file mode 100644 index 0000000..7e6a7d6 --- /dev/null +++ b/src/main/java/com/lions/dev/dto/request/users/SetUserActiveRequestDTO.java @@ -0,0 +1,22 @@ +package com.lions.dev.dto.request.users; + +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** + * DTO pour forcer l'activation ou la suspension d'un utilisateur (opĂ©ration rĂ©servĂ©e au super admin). + */ +@Getter +@Setter +@NoArgsConstructor +public class SetUserActiveRequestDTO { + + @NotNull(message = "Le champ active est obligatoire") + private Boolean active; + + public SetUserActiveRequestDTO(Boolean active) { + this.active = active; + } +} diff --git a/src/main/java/com/lions/dev/dto/response/establishment/BusinessHoursResponseDTO.java b/src/main/java/com/lions/dev/dto/response/establishment/BusinessHoursResponseDTO.java new file mode 100644 index 0000000..78b12dc --- /dev/null +++ b/src/main/java/com/lions/dev/dto/response/establishment/BusinessHoursResponseDTO.java @@ -0,0 +1,46 @@ +package com.lions.dev.dto.response.establishment; + +import com.lions.dev.entity.establishment.BusinessHours; +import lombok.Getter; + +import java.time.LocalDateTime; +import java.util.UUID; + +/** + * DTO pour renvoyer les horaires d'ouverture d'un Ă©tablissement. + * Conforme Ă  l'architecture AfterWork v2.0. + */ +@Getter +public class BusinessHoursResponseDTO { + + private String id; + private String establishmentId; + private String dayOfWeek; + private String openTime; + private String closeTime; + private Boolean isClosed; + private Boolean isException; + private LocalDateTime exceptionDate; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + /** + * Constructeur qui transforme une entitĂ© BusinessHours en DTO. + * Utilise establishmentId fourni pour Ă©viter LazyInitializationException. + * + * @param businessHours L'entitĂ© Ă  convertir. + * @param establishmentId ID de l'Ă©tablissement (dĂ©jĂ  connu par l'appelant). + */ + public BusinessHoursResponseDTO(BusinessHours businessHours, UUID establishmentId) { + this.id = businessHours.getId() != null ? businessHours.getId().toString() : null; + this.establishmentId = establishmentId != null ? establishmentId.toString() : null; + this.dayOfWeek = businessHours.getDayOfWeek(); + this.openTime = businessHours.getOpenTime(); + this.closeTime = businessHours.getCloseTime(); + this.isClosed = businessHours.getIsClosed(); + this.isException = businessHours.getIsException(); + this.exceptionDate = businessHours.getExceptionDate(); + this.createdAt = businessHours.getCreatedAt(); + this.updatedAt = businessHours.getUpdatedAt(); + } +} diff --git a/src/main/java/com/lions/dev/dto/response/establishment/EstablishmentAmenityResponseDTO.java b/src/main/java/com/lions/dev/dto/response/establishment/EstablishmentAmenityResponseDTO.java new file mode 100644 index 0000000..d781f76 --- /dev/null +++ b/src/main/java/com/lions/dev/dto/response/establishment/EstablishmentAmenityResponseDTO.java @@ -0,0 +1,44 @@ +package com.lions.dev.dto.response.establishment; + +import com.lions.dev.entity.establishment.EstablishmentAmenity; +import lombok.Getter; + +import java.time.LocalDateTime; +import java.util.UUID; + +/** + * DTO pour renvoyer un Ă©quipement d'Ă©tablissement (avec nom du type). + * Conforme Ă  l'architecture AfterWork v2.0. + * Utilise des paramĂštres explicites pour Ă©viter LazyInitializationException. + */ +@Getter +public class EstablishmentAmenityResponseDTO { + + private String establishmentId; + private String amenityId; + private String amenityName; + private String category; + private String icon; + private String details; + private LocalDateTime createdAt; + + /** + * Constructeur qui transforme une entitĂ© EstablishmentAmenity en DTO. + * Les champs du type (name, category, icon) sont passĂ©s en paramĂštres car ils peuvent + * provenir d'un JOIN FETCH dĂ©jĂ  rĂ©solu ou ĂȘtre null si le type n'est pas chargĂ©. + * + * @param ea L'entitĂ© Ă  convertir. + * @param amenityName Nom du type d'Ă©quipement (ex: "WiFi", "Parking"). + * @param category CatĂ©gorie du type (ex: "Comfort", "Accessibility"). + * @param icon Nom de l'icĂŽne (ex: "wifi", "parking"). + */ + public EstablishmentAmenityResponseDTO(EstablishmentAmenity ea, String amenityName, String category, String icon) { + this.establishmentId = ea.getEstablishmentId() != null ? ea.getEstablishmentId().toString() : null; + this.amenityId = ea.getAmenityId() != null ? ea.getAmenityId().toString() : null; + this.amenityName = amenityName; + this.category = category; + this.icon = icon; + this.details = ea.getDetails(); + this.createdAt = ea.getCreatedAt(); + } +} diff --git a/src/main/java/com/lions/dev/dto/response/friends/FriendshipCreateOneResponseDTO.java b/src/main/java/com/lions/dev/dto/response/friends/FriendshipCreateOneResponseDTO.java index 368d5d0..5c3b7d8 100644 --- a/src/main/java/com/lions/dev/dto/response/friends/FriendshipCreateOneResponseDTO.java +++ b/src/main/java/com/lions/dev/dto/response/friends/FriendshipCreateOneResponseDTO.java @@ -27,8 +27,24 @@ public class FriendshipCreateOneResponseDTO { /** * Constructeur pour mapper l'entitĂ© `Friendship` Ă  ce DTO. + * Utilise les IDs fournis pour Ă©viter LazyInitializationException sur user/friend. * * @param friendship L'entitĂ© `Friendship` Ă  convertir en DTO. + * @param userId ID de l'utilisateur qui envoie la demande (dĂ©jĂ  chargĂ©). + * @param friendId ID de l'utilisateur qui reçoit la demande (dĂ©jĂ  chargĂ©). + */ + public FriendshipCreateOneResponseDTO(Friendship friendship, UUID userId, UUID friendId) { + this.id = friendship.getId(); + this.userId = userId; + this.friendId = friendId; + this.status = friendship.getStatus(); + this.createdAt = friendship.getCreatedAt(); + this.updatedAt = friendship.getUpdatedAt(); + } + + /** + * Constructeur pour mapper l'entitĂ© `Friendship` Ă  ce DTO (charge les associations lazy). + * PrĂ©fĂ©rer {@link #FriendshipCreateOneResponseDTO(Friendship, UUID, UUID)} en fin de transaction. */ public FriendshipCreateOneResponseDTO(Friendship friendship) { this.id = friendship.getId(); diff --git a/src/main/java/com/lions/dev/dto/response/users/UserCreateResponseDTO.java b/src/main/java/com/lions/dev/dto/response/users/UserCreateResponseDTO.java index 05fa409..b2f31d5 100644 --- a/src/main/java/com/lions/dev/dto/response/users/UserCreateResponseDTO.java +++ b/src/main/java/com/lions/dev/dto/response/users/UserCreateResponseDTO.java @@ -3,6 +3,7 @@ package com.lions.dev.dto.response.users; import com.lions.dev.entity.users.Users; import java.util.UUID; import lombok.Getter; +import org.jboss.logging.Logger; /** * DTO pour renvoyer les informations d'un utilisateur. @@ -68,6 +69,5 @@ public class UserCreateResponseDTO { this.nom = this.lastName; this.prenoms = this.firstName; - System.out.println("[LOG] DTO créé pour l'utilisateur : " + this.email); } } diff --git a/src/main/java/com/lions/dev/dto/response/users/UserResponseDTO.java b/src/main/java/com/lions/dev/dto/response/users/UserResponseDTO.java index c6d65b3..01737f4 100644 --- a/src/main/java/com/lions/dev/dto/response/users/UserResponseDTO.java +++ b/src/main/java/com/lions/dev/dto/response/users/UserResponseDTO.java @@ -1,4 +1,4 @@ -package com.lions.dev.dto; +package com.lions.dev.dto.response.users; import com.lions.dev.entity.users.Users; // Import de l'entitĂ© Users import java.util.UUID; diff --git a/src/main/java/com/lions/dev/entity/BaseEntity.java b/src/main/java/com/lions/dev/entity/BaseEntity.java index a7c0aa6..f72768a 100644 --- a/src/main/java/com/lions/dev/entity/BaseEntity.java +++ b/src/main/java/com/lions/dev/entity/BaseEntity.java @@ -37,7 +37,6 @@ public abstract class BaseEntity { protected void onCreate() { this.createdAt = LocalDateTime.now(); this.updatedAt = LocalDateTime.now(); - System.out.println("[LOG] Nouvelle entitĂ© créée avec ID : " + this.id + " Ă  " + this.createdAt); } /** @@ -47,6 +46,5 @@ public abstract class BaseEntity { @PreUpdate protected void onUpdate() { this.updatedAt = LocalDateTime.now(); - System.out.println("[LOG] EntitĂ© mise Ă  jour avec ID : " + this.id + " Ă  " + this.updatedAt); } } diff --git a/src/main/java/com/lions/dev/entity/auth/PasswordResetToken.java b/src/main/java/com/lions/dev/entity/auth/PasswordResetToken.java new file mode 100644 index 0000000..4732d72 --- /dev/null +++ b/src/main/java/com/lions/dev/entity/auth/PasswordResetToken.java @@ -0,0 +1,79 @@ +package com.lions.dev.entity.auth; + +import com.lions.dev.entity.users.Users; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.LocalDateTime; +import java.util.UUID; + +/** + * EntitĂ© reprĂ©sentant un token de rĂ©initialisation de mot de passe. + * + * Le token est valide pendant 1 heure aprĂšs sa crĂ©ation. + */ +@Entity +@Table(name = "password_reset_tokens") +@Getter +@Setter +@NoArgsConstructor +public class PasswordResetToken { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private UUID id; + + @Column(name = "token", nullable = false, unique = true, length = 64) + private String token; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private Users user; + + @Column(name = "expires_at", nullable = false) + private LocalDateTime expiresAt; + + @Column(name = "used", nullable = false) + private boolean used = false; + + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; + + /** + * CrĂ©e un nouveau token de rĂ©initialisation pour un utilisateur. + * Le token expire aprĂšs 1 heure. + * + * @param user L'utilisateur pour lequel crĂ©er le token + */ + public PasswordResetToken(Users user) { + this.user = user; + this.token = generateToken(); + this.createdAt = LocalDateTime.now(); + this.expiresAt = this.createdAt.plusHours(1); + this.used = false; + } + + /** + * GĂ©nĂšre un token alĂ©atoire sĂ©curisĂ©. + */ + private String generateToken() { + return UUID.randomUUID().toString().replace("-", "") + + UUID.randomUUID().toString().replace("-", "").substring(0, 32); + } + + /** + * VĂ©rifie si le token est valide (non expirĂ© et non utilisĂ©). + */ + public boolean isValid() { + return !used && LocalDateTime.now().isBefore(expiresAt); + } + + /** + * Marque le token comme utilisĂ©. + */ + public void markAsUsed() { + this.used = true; + } +} diff --git a/src/main/java/com/lions/dev/entity/chat/Conversation.java b/src/main/java/com/lions/dev/entity/chat/Conversation.java index 251e1b2..69aac9e 100644 --- a/src/main/java/com/lions/dev/entity/chat/Conversation.java +++ b/src/main/java/com/lions/dev/entity/chat/Conversation.java @@ -62,7 +62,6 @@ public class Conversation extends BaseEntity { this.user1 = user1; this.user2 = user2; this.lastMessageTimestamp = LocalDateTime.now(); - System.out.println("[LOG] Conversation créée entre " + user1.getEmail() + " et " + user2.getEmail()); } /** @@ -80,7 +79,6 @@ public class Conversation extends BaseEntity { unreadCountUser1++; } - System.out.println("[LOG] Dernier message mis Ă  jour pour la conversation " + this.getId()); } /** @@ -89,10 +87,8 @@ public class Conversation extends BaseEntity { public void markAllAsReadForUser(Users user) { if (user != null && user1 != null && user.getId().equals(user1.getId())) { unreadCountUser1 = 0; - System.out.println("[LOG] Messages marquĂ©s comme lus pour user1 dans la conversation " + this.getId()); } else if (user != null && user2 != null && user.getId().equals(user2.getId())) { unreadCountUser2 = 0; - System.out.println("[LOG] Messages marquĂ©s comme lus pour user2 dans la conversation " + this.getId()); } } diff --git a/src/main/java/com/lions/dev/entity/chat/Message.java b/src/main/java/com/lions/dev/entity/chat/Message.java index 00f14de..63722c8 100644 --- a/src/main/java/com/lions/dev/entity/chat/Message.java +++ b/src/main/java/com/lions/dev/entity/chat/Message.java @@ -57,7 +57,6 @@ public class Message extends BaseEntity { this.messageType = "text"; this.isRead = false; this.isDelivered = false; - System.out.println("[LOG] Message créé par " + sender.getEmail() + " dans la conversation " + conversation.getId()); } /** @@ -66,7 +65,6 @@ public class Message extends BaseEntity { public void markAsRead() { if (!this.isRead) { this.isRead = true; - System.out.println("[LOG] Message " + this.getId() + " marquĂ© comme lu"); } } @@ -76,7 +74,6 @@ public class Message extends BaseEntity { public void markAsDelivered() { if (!this.isDelivered) { this.isDelivered = true; - System.out.println("[LOG] Message " + this.getId() + " marquĂ© comme dĂ©livrĂ©"); } } diff --git a/src/main/java/com/lions/dev/entity/comment/Comment.java b/src/main/java/com/lions/dev/entity/comment/Comment.java index 397128a..c292869 100644 --- a/src/main/java/com/lions/dev/entity/comment/Comment.java +++ b/src/main/java/com/lions/dev/entity/comment/Comment.java @@ -53,15 +53,7 @@ public class Comment extends BaseEntity { this.user = user; this.event = event; this.text = text; - this.commentDate = - LocalDateTime.now(); // La date est dĂ©finie automatiquement lors de la crĂ©ation - System.out.println( - "[LOG] Nouveau commentaire ajoutĂ© par " - + user.getEmail() - + " sur l'Ă©vĂ©nement : " - + event.getTitle() - + " - Texte : " - + text); + this.commentDate = LocalDateTime.now(); } /** @@ -73,14 +65,7 @@ public class Comment extends BaseEntity { * @param newText Le nouveau texte du commentaire. */ public void updateComment(String newText) { - System.out.println( - "[LOG] Modification du commentaire de " - + user.getEmail() - + " sur l'Ă©vĂ©nement : " - + event.getTitle() - + " - Nouveau texte : " - + newText); this.text = newText; - this.commentDate = LocalDateTime.now(); // Mise Ă  jour de la date de modification + this.commentDate = LocalDateTime.now(); } } diff --git a/src/main/java/com/lions/dev/entity/establishment/AmenityType.java b/src/main/java/com/lions/dev/entity/establishment/AmenityType.java new file mode 100644 index 0000000..117823a --- /dev/null +++ b/src/main/java/com/lions/dev/entity/establishment/AmenityType.java @@ -0,0 +1,37 @@ +package com.lions.dev.entity.establishment; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.LocalDateTime; +import java.util.UUID; + +/** + * EntitĂ© reprĂ©sentant un type d'Ă©quipement (rĂ©fĂ©rence amenity_types). + * UtilisĂ©e pour afficher le nom/catĂ©gorie des Ă©quipements d'un Ă©tablissement. + */ +@Entity +@Table(name = "amenity_types") +@Getter +@Setter +@NoArgsConstructor +public class AmenityType { + + @Id + @Column(name = "id", nullable = false) + private UUID id; + + @Column(name = "name", nullable = false, unique = true, length = 100) + private String name; + + @Column(name = "category", length = 50) + private String category; + + @Column(name = "icon", length = 50) + private String icon; + + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt = LocalDateTime.now(); +} diff --git a/src/main/java/com/lions/dev/entity/establishment/EstablishmentAmenity.java b/src/main/java/com/lions/dev/entity/establishment/EstablishmentAmenity.java index 5dd7d8a..9c5daee 100644 --- a/src/main/java/com/lions/dev/entity/establishment/EstablishmentAmenity.java +++ b/src/main/java/com/lions/dev/entity/establishment/EstablishmentAmenity.java @@ -45,6 +45,10 @@ public class EstablishmentAmenity { @JoinColumn(name = "establishment_id", insertable = false, updatable = false) private Establishment establishment; // L'Ă©tablissement concernĂ© + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "amenity_id", insertable = false, updatable = false) + private AmenityType amenityType; // Type d'Ă©quipement (nom, catĂ©gorie, icĂŽne) + /** * Constructeur pour crĂ©er une liaison Ă©tablissement-Ă©quipement. * @@ -67,32 +71,3 @@ public class EstablishmentAmenity { } } -/** - * Classe composite pour la clĂ© primaire de EstablishmentAmenity. - */ -@Getter -@Setter -@NoArgsConstructor -class EstablishmentAmenityId implements java.io.Serializable { - private UUID establishmentId; - private UUID amenityId; - - public EstablishmentAmenityId(UUID establishmentId, UUID amenityId) { - this.establishmentId = establishmentId; - this.amenityId = amenityId; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - EstablishmentAmenityId that = (EstablishmentAmenityId) o; - return establishmentId.equals(that.establishmentId) && amenityId.equals(that.amenityId); - } - - @Override - public int hashCode() { - return establishmentId.hashCode() + amenityId.hashCode(); - } -} - diff --git a/src/main/java/com/lions/dev/entity/establishment/EstablishmentAmenityId.java b/src/main/java/com/lions/dev/entity/establishment/EstablishmentAmenityId.java new file mode 100644 index 0000000..51d901e --- /dev/null +++ b/src/main/java/com/lions/dev/entity/establishment/EstablishmentAmenityId.java @@ -0,0 +1,38 @@ +package com.lions.dev.entity.establishment; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.io.Serializable; +import java.util.UUID; + +/** + * Classe composite pour la clĂ© primaire de EstablishmentAmenity. + */ +@Getter +@Setter +@NoArgsConstructor +public class EstablishmentAmenityId implements Serializable { + + private UUID establishmentId; + private UUID amenityId; + + public EstablishmentAmenityId(UUID establishmentId, UUID amenityId) { + this.establishmentId = establishmentId; + this.amenityId = amenityId; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + EstablishmentAmenityId that = (EstablishmentAmenityId) o; + return establishmentId.equals(that.establishmentId) && amenityId.equals(that.amenityId); + } + + @Override + public int hashCode() { + return establishmentId.hashCode() + amenityId.hashCode(); + } +} diff --git a/src/main/java/com/lions/dev/entity/events/Events.java b/src/main/java/com/lions/dev/entity/events/Events.java index bbd37a6..b118d02 100644 --- a/src/main/java/com/lions/dev/entity/events/Events.java +++ b/src/main/java/com/lions/dev/entity/events/Events.java @@ -117,7 +117,6 @@ public class Events extends BaseEntity { */ public void addParticipant(Users user) { participants.add(user); - System.out.println("[LOG] Participant ajoutĂ© : " + user.getEmail() + " Ă  l'Ă©vĂ©nement : " + this.title); } /** @@ -127,7 +126,6 @@ public class Events extends BaseEntity { */ public void removeParticipant(Users user) { participants.remove(user); - System.out.println("[LOG] Participant supprimĂ© : " + user.getEmail() + " de l'Ă©vĂ©nement : " + this.title); } /** @@ -137,7 +135,6 @@ public class Events extends BaseEntity { */ public int getNumberOfParticipants() { int count = participants.size(); - System.out.println("[LOG] Nombre de participants Ă  l'Ă©vĂ©nement : " + this.title + " - " + count); return count; } @@ -146,7 +143,6 @@ public class Events extends BaseEntity { */ public void setClosed(boolean closed) { this.status = closed ? "CLOSED" : "OPEN"; - System.out.println("[LOG] Statut de l'Ă©vĂ©nement mis Ă  jour : " + this.title + " - " + this.status); } /** @@ -189,7 +185,6 @@ public class Events extends BaseEntity { * @return Une liste de commentaires. */ public List getComments() { - System.out.println("[LOG] RĂ©cupĂ©ration des commentaires pour l'Ă©vĂ©nement : " + this.title); return comments; } diff --git a/src/main/java/com/lions/dev/entity/friends/Friendship.java b/src/main/java/com/lions/dev/entity/friends/Friendship.java index 475f4ae..4727ca4 100644 --- a/src/main/java/com/lions/dev/entity/friends/Friendship.java +++ b/src/main/java/com/lions/dev/entity/friends/Friendship.java @@ -38,12 +38,5 @@ public class Friendship extends BaseEntity { */ public void setStatus(FriendshipStatus newStatus) { this.status = newStatus; - System.out.println( - "[LOG] Statut changĂ© pour l'amitiĂ© entre " - + this.user.getEmail() - + " et " - + this.friend.getEmail() - + " - Nouveau statut : " - + this.status); } } diff --git a/src/main/java/com/lions/dev/entity/notification/Notification.java b/src/main/java/com/lions/dev/entity/notification/Notification.java index 13d3d26..2143af7 100644 --- a/src/main/java/com/lions/dev/entity/notification/Notification.java +++ b/src/main/java/com/lions/dev/entity/notification/Notification.java @@ -60,7 +60,6 @@ public class Notification extends BaseEntity { this.type = type; this.user = user; this.isRead = false; - System.out.println("[LOG] Notification créée : " + title + " pour l'utilisateur " + user.getEmail()); } /** @@ -69,7 +68,6 @@ public class Notification extends BaseEntity { public void markAsRead() { if (!this.isRead) { this.isRead = true; - System.out.println("[LOG] Notification marquĂ©e comme lue : " + this.title); } } diff --git a/src/main/java/com/lions/dev/entity/social/SocialPost.java b/src/main/java/com/lions/dev/entity/social/SocialPost.java index 48ca873..ea5a0fe 100644 --- a/src/main/java/com/lions/dev/entity/social/SocialPost.java +++ b/src/main/java/com/lions/dev/entity/social/SocialPost.java @@ -52,7 +52,6 @@ public class SocialPost extends BaseEntity { this.likesCount = 0; this.commentsCount = 0; this.sharesCount = 0; - System.out.println("[LOG] Post social créé par l'utilisateur : " + user.getEmail()); } /** @@ -60,7 +59,6 @@ public class SocialPost extends BaseEntity { */ public void incrementLikes() { this.likesCount++; - System.out.println("[LOG] Like ajoutĂ© au post ID : " + this.getId() + " (total: " + this.likesCount + ")"); } /** @@ -69,7 +67,6 @@ public class SocialPost extends BaseEntity { public void decrementLikes() { if (this.likesCount > 0) { this.likesCount--; - System.out.println("[LOG] Like retirĂ© du post ID : " + this.getId() + " (total: " + this.likesCount + ")"); } } @@ -78,7 +75,6 @@ public class SocialPost extends BaseEntity { */ public void incrementComments() { this.commentsCount++; - System.out.println("[LOG] Commentaire ajoutĂ© au post ID : " + this.getId() + " (total: " + this.commentsCount + ")"); } /** @@ -86,7 +82,6 @@ public class SocialPost extends BaseEntity { */ public void incrementShares() { this.sharesCount++; - System.out.println("[LOG] Partage ajoutĂ© au post ID : " + this.getId() + " (total: " + this.sharesCount + ")"); } } diff --git a/src/main/java/com/lions/dev/entity/story/Story.java b/src/main/java/com/lions/dev/entity/story/Story.java index e1c0349..78b134d 100644 --- a/src/main/java/com/lions/dev/entity/story/Story.java +++ b/src/main/java/com/lions/dev/entity/story/Story.java @@ -70,7 +70,6 @@ public class Story extends BaseEntity { this.expiresAt = LocalDateTime.now().plusHours(24); // Expire aprĂšs 24h this.isActive = true; this.viewsCount = 0; - System.out.println("[LOG] Story créée par l'utilisateur : " + user.getEmail()); } /** @@ -82,7 +81,6 @@ public class Story extends BaseEntity { public boolean markAsViewed(UUID viewerId) { if (viewerIds.add(viewerId)) { this.viewsCount++; - System.out.println("[LOG] Story ID : " + this.getId() + " vue par l'utilisateur ID : " + viewerId + " (total: " + this.viewsCount + ")"); return true; } return false; @@ -102,7 +100,6 @@ public class Story extends BaseEntity { */ public void deactivate() { this.isActive = false; - System.out.println("[LOG] Story ID : " + this.getId() + " dĂ©sactivĂ©e"); } /** diff --git a/src/main/java/com/lions/dev/entity/users/Users.java b/src/main/java/com/lions/dev/entity/users/Users.java index d0b1984..90cce82 100644 --- a/src/main/java/com/lions/dev/entity/users/Users.java +++ b/src/main/java/com/lions/dev/entity/users/Users.java @@ -84,7 +84,6 @@ public class Users extends BaseEntity { */ public void setPassword(String password) { this.passwordHash = BCrypt.withDefaults().hashToString(12, password.toCharArray()); - System.out.println("[LOG] Mot de passe hachĂ© pour l'utilisateur : " + this.email); } /** @@ -105,9 +104,7 @@ public class Users extends BaseEntity { */ public boolean verifyPassword(String password) { BCrypt.Result result = BCrypt.verifyer().verify(password.toCharArray(), this.passwordHash); - boolean isValid = result.verified; - System.out.println("[LOG] VĂ©rification du mot de passe pour l'utilisateur : " + this.email + " - RĂ©sultat : " + isValid); - return isValid; + return result.verified; } /** @@ -134,9 +131,7 @@ public class Users extends BaseEntity { * @return true si l'utilisateur est un administrateur, false sinon. */ public boolean isAdmin() { - boolean isAdmin = "ADMIN".equalsIgnoreCase(this.role); - System.out.println("[LOG] VĂ©rification du rĂŽle ADMIN pour l'utilisateur : " + this.email + " - RĂ©sultat : " + isAdmin); - return isAdmin; + return "ADMIN".equalsIgnoreCase(this.role); } @ManyToMany(fetch = FetchType.LAZY) @@ -154,7 +149,6 @@ public class Users extends BaseEntity { */ public void addFavoriteEvent(Events event) { favoriteEvents.add(event); - System.out.println("[LOG] ÉvĂ©nement ajoutĂ© aux favoris pour l'utilisateur : " + this.email); } /** @@ -164,7 +158,6 @@ public class Users extends BaseEntity { */ public void removeFavoriteEvent(Events event) { favoriteEvents.remove(event); - System.out.println("[LOG] ÉvĂ©nement retirĂ© des favoris pour l'utilisateur : " + this.email); } /** @@ -183,7 +176,6 @@ public class Users extends BaseEntity { * @return Une liste d'Ă©vĂ©nements favoris. */ public Set getFavoriteEvents() { - System.out.println("[LOG] RĂ©cupĂ©ration des Ă©vĂ©nements favoris pour l'utilisateur : " + this.email); return favoriteEvents; } @@ -214,7 +206,6 @@ public class Users extends BaseEntity { } else { preferences.remove("preferred_category"); } - System.out.println("[LOG] CatĂ©gorie prĂ©fĂ©rĂ©e dĂ©finie pour l'utilisateur : " + this.email + " - CatĂ©gorie : " + category); } /** @@ -232,7 +223,6 @@ public class Users extends BaseEntity { public void updatePresence() { this.isOnline = true; this.lastSeen = java.time.LocalDateTime.now(); - System.out.println("[LOG] PrĂ©sence mise Ă  jour pour l'utilisateur : " + this.email + " - Online: true"); } /** @@ -241,7 +231,6 @@ public class Users extends BaseEntity { public void setOffline() { this.isOnline = false; this.lastSeen = java.time.LocalDateTime.now(); - System.out.println("[LOG] Utilisateur marquĂ© hors ligne : " + this.email); } } diff --git a/src/main/java/com/lions/dev/exception/EventNotFoundException.java b/src/main/java/com/lions/dev/exception/EventNotFoundException.java index e310b71..10f2c1a 100644 --- a/src/main/java/com/lions/dev/exception/EventNotFoundException.java +++ b/src/main/java/com/lions/dev/exception/EventNotFoundException.java @@ -17,6 +17,5 @@ public class EventNotFoundException extends WebApplicationException { */ public EventNotFoundException(UUID eventId) { super("ÉvĂ©nement non trouvĂ© avec l'ID : " + eventId.toString(), Response.Status.NOT_FOUND); - System.out.println("[ERROR] ÉvĂ©nement non trouvĂ© avec l'ID : " + eventId.toString()); } } diff --git a/src/main/java/com/lions/dev/exception/UserNotFoundException.java b/src/main/java/com/lions/dev/exception/UserNotFoundException.java index 958e514..f0616d6 100644 --- a/src/main/java/com/lions/dev/exception/UserNotFoundException.java +++ b/src/main/java/com/lions/dev/exception/UserNotFoundException.java @@ -16,6 +16,5 @@ public class UserNotFoundException extends WebApplicationException { */ public UserNotFoundException(String message) { super(message, Response.Status.NOT_FOUND); - System.out.println("[ERROR] Utilisateur non trouvĂ© : " + message); } } diff --git a/src/main/java/com/lions/dev/repository/BusinessHoursRepository.java b/src/main/java/com/lions/dev/repository/BusinessHoursRepository.java new file mode 100644 index 0000000..b5f7135 --- /dev/null +++ b/src/main/java/com/lions/dev/repository/BusinessHoursRepository.java @@ -0,0 +1,31 @@ +package com.lions.dev.repository; + +import com.lions.dev.entity.establishment.BusinessHours; +import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase; +import jakarta.enterprise.context.ApplicationScoped; +import java.util.List; +import java.util.UUID; +import org.jboss.logging.Logger; + +/** + * Repository pour l'entitĂ© BusinessHours. + * GĂšre les opĂ©rations de lecture sur les horaires d'ouverture des Ă©tablissements. + */ +@ApplicationScoped +public class BusinessHoursRepository implements PanacheRepositoryBase { + + private static final Logger LOG = Logger.getLogger(BusinessHoursRepository.class); + + /** + * RĂ©cupĂšre tous les horaires d'ouverture d'un Ă©tablissement. + * + * @param establishmentId L'ID de l'Ă©tablissement. + * @return La liste des horaires (triĂ©s par jour de la semaine). + */ + public List findByEstablishmentId(UUID establishmentId) { + LOG.infof("[LOG] RĂ©cupĂ©ration des horaires pour l'Ă©tablissement : %s", establishmentId); + List list = list("establishment.id = ?1 ORDER BY dayOfWeek", establishmentId); + LOG.infof("[LOG] Nombre d'horaires trouvĂ©s pour l'Ă©tablissement %s : %d", establishmentId, list.size()); + return list; + } +} diff --git a/src/main/java/com/lions/dev/repository/ConversationRepository.java b/src/main/java/com/lions/dev/repository/ConversationRepository.java index 829299c..62229a8 100644 --- a/src/main/java/com/lions/dev/repository/ConversationRepository.java +++ b/src/main/java/com/lions/dev/repository/ConversationRepository.java @@ -5,6 +5,7 @@ import com.lions.dev.entity.users.Users; import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase; import io.quarkus.panache.common.Sort; import jakarta.enterprise.context.ApplicationScoped; +import org.jboss.logging.Logger; import java.util.List; import java.util.Optional; @@ -27,7 +28,6 @@ public class ConversationRepository implements PanacheRepositoryBase findByUser(Users user) { - System.out.println("[LOG] RĂ©cupĂ©ration des conversations pour l'utilisateur : " + user.getEmail()); return find("user1 = ?1 or user2 = ?1", Sort.by("lastMessageTimestamp", Sort.Direction.Descending), user) .list(); } @@ -74,7 +71,6 @@ public class ConversationRepository implements PanacheRepositoryBase findConversationsWithUnreadMessages(UUID userId) { - System.out.println("[LOG] RĂ©cupĂ©ration des conversations avec messages non lus pour l'utilisateur ID : " + userId); return find( "(user1.id = ?1 and unreadCountUser1 > 0) or (user2.id = ?1 and unreadCountUser2 > 0)", @@ -101,7 +97,6 @@ public class ConversationRepository implements PanacheRepositoryBase { + + @PersistenceContext + EntityManager entityManager; + + private static final Logger LOG = Logger.getLogger(EstablishmentAmenityRepository.class); + + /** + * RĂ©cupĂšre tous les Ă©quipements d'un Ă©tablissement avec le type chargĂ© (nom, catĂ©gorie, icĂŽne). + * Utilise JOIN FETCH pour Ă©viter LazyInitializationException. + * + * @param establishmentId L'ID de l'Ă©tablissement. + * @return La liste des Ă©quipements (avec amenityType chargĂ©). + */ + public List findByEstablishmentIdWithType(UUID establishmentId) { + LOG.infof("[LOG] RĂ©cupĂ©ration des Ă©quipements pour l'Ă©tablissement : %s", establishmentId); + List list = entityManager + .createQuery( + "SELECT ea FROM EstablishmentAmenity ea LEFT JOIN FETCH ea.amenityType WHERE ea.establishmentId = :eid ORDER BY ea.amenityId", + EstablishmentAmenity.class) + .setParameter("eid", establishmentId) + .getResultList(); + LOG.infof("[LOG] Nombre d'Ă©quipements trouvĂ©s pour l'Ă©tablissement %s : %d", establishmentId, list.size()); + return list; + } +} diff --git a/src/main/java/com/lions/dev/repository/EstablishmentSubscriptionRepository.java b/src/main/java/com/lions/dev/repository/EstablishmentSubscriptionRepository.java index 23fbc58..982113e 100644 --- a/src/main/java/com/lions/dev/repository/EstablishmentSubscriptionRepository.java +++ b/src/main/java/com/lions/dev/repository/EstablishmentSubscriptionRepository.java @@ -4,6 +4,7 @@ import com.lions.dev.entity.establishment.EstablishmentSubscription; import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase; import jakarta.enterprise.context.ApplicationScoped; +import java.time.LocalDateTime; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -23,4 +24,16 @@ public class EstablishmentSubscriptionRepository implements PanacheRepositoryBas return find("establishmentId = ?1 and status = ?2", establishmentId, EstablishmentSubscription.STATUS_ACTIVE) .firstResultOptional(); } + + /** Abonnements actifs dont la date d'expiration est dĂ©passĂ©e (pour job de nettoyage). */ + public List findExpiredActiveSubscriptions() { + return list("status = ?1 AND expiresAt IS NOT NULL AND expiresAt <= ?2", + EstablishmentSubscription.STATUS_ACTIVE, LocalDateTime.now()); + } + + /** Abonnements actifs dont l'expiration est dans la fenĂȘtre [from, to] (pour avertissement J-3). */ + public List findActiveSubscriptionsExpiringBetween(LocalDateTime from, LocalDateTime to) { + return list("status = ?1 AND expiresAt IS NOT NULL AND expiresAt >= ?2 AND expiresAt <= ?3", + EstablishmentSubscription.STATUS_ACTIVE, from, to); + } } diff --git a/src/main/java/com/lions/dev/repository/EventsRepository.java b/src/main/java/com/lions/dev/repository/EventsRepository.java index 1e6ae3c..51f02b2 100644 --- a/src/main/java/com/lions/dev/repository/EventsRepository.java +++ b/src/main/java/com/lions/dev/repository/EventsRepository.java @@ -100,4 +100,12 @@ public class EventsRepository implements PanacheRepositoryBase { .setParameter("loc", pattern) .getResultList(); } + + /** + * RĂ©cupĂšre les Ă©vĂ©nements dont la date de dĂ©but est dans la fenĂȘtre [from, to]. + * UtilisĂ© pour les rappels (J-1, H-1). + */ + public List findEventsStartingBetween(LocalDateTime from, LocalDateTime to) { + return list("startDate >= ?1 AND startDate <= ?2", from, to); + } } diff --git a/src/main/java/com/lions/dev/repository/FriendshipRepository.java b/src/main/java/com/lions/dev/repository/FriendshipRepository.java index c988423..a693a3a 100644 --- a/src/main/java/com/lions/dev/repository/FriendshipRepository.java +++ b/src/main/java/com/lions/dev/repository/FriendshipRepository.java @@ -136,4 +136,16 @@ public class FriendshipRepository implements PanacheRepositoryBase findAcceptedFriendships(UUID userId) { + return find("(user.id = ?1 OR friend.id = ?1) AND status = ?2", userId, FriendshipStatus.ACCEPTED) + .list(); + } } diff --git a/src/main/java/com/lions/dev/repository/MessageRepository.java b/src/main/java/com/lions/dev/repository/MessageRepository.java index 67e49ae..8788c5c 100644 --- a/src/main/java/com/lions/dev/repository/MessageRepository.java +++ b/src/main/java/com/lions/dev/repository/MessageRepository.java @@ -6,6 +6,7 @@ import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase; import io.quarkus.panache.common.Page; import io.quarkus.panache.common.Sort; import jakarta.enterprise.context.ApplicationScoped; +import org.jboss.logging.Logger; import java.util.List; import java.util.UUID; @@ -28,7 +29,6 @@ public class MessageRepository implements PanacheRepositoryBase { * @return Liste paginĂ©e des messages triĂ©s par date (du plus rĂ©cent au plus ancien) */ public List findByConversationId(UUID conversationId, int page, int size) { - System.out.println("[LOG] RĂ©cupĂ©ration des messages pour la conversation ID : " + conversationId); return find("conversation.id = ?1", Sort.by("createdAt", Sort.Direction.Descending), conversationId) .page(Page.of(page, size)) .list(); @@ -41,7 +41,6 @@ public class MessageRepository implements PanacheRepositoryBase { * @return Liste de tous les messages triĂ©s par date */ public List findByConversation(Conversation conversation) { - System.out.println("[LOG] RĂ©cupĂ©ration de tous les messages pour la conversation ID : " + conversation.getId()); return find("conversation = ?1", Sort.by("createdAt", Sort.Direction.Ascending), conversation).list(); } @@ -64,7 +63,6 @@ public class MessageRepository implements PanacheRepositoryBase { * @return Le nombre de messages mis Ă  jour */ public int markAllAsRead(UUID conversationId, UUID recipientId) { - System.out.println("[LOG] Marquage de tous les messages comme lus pour la conversation " + conversationId); return update("isRead = true where conversation.id = ?1 and sender.id != ?2 and isRead = false", conversationId, recipientId); } @@ -86,7 +84,6 @@ public class MessageRepository implements PanacheRepositoryBase { * @return Le nombre de messages supprimĂ©s */ public long deleteByConversationId(UUID conversationId) { - System.out.println("[LOG] Suppression de tous les messages pour la conversation " + conversationId); return delete("conversation.id = ?1", conversationId); } } diff --git a/src/main/java/com/lions/dev/repository/NotificationRepository.java b/src/main/java/com/lions/dev/repository/NotificationRepository.java index 70e3053..1b68c53 100644 --- a/src/main/java/com/lions/dev/repository/NotificationRepository.java +++ b/src/main/java/com/lions/dev/repository/NotificationRepository.java @@ -7,6 +7,7 @@ import io.quarkus.panache.common.Sort; import jakarta.enterprise.context.ApplicationScoped; import java.util.List; import java.util.UUID; +import org.jboss.logging.Logger; /** * Repository pour l'entitĂ© Notification. @@ -27,9 +28,7 @@ public class NotificationRepository implements PanacheRepositoryBase findByUserId(UUID userId) { - System.out.println("[LOG] RĂ©cupĂ©ration des notifications pour l'utilisateur ID : " + userId); List notifications = find("user.id", Sort.by("createdAt", Sort.Direction.Descending), userId).list(); - System.out.println("[LOG] " + notifications.size() + " notification(s) trouvĂ©e(s) pour l'utilisateur ID : " + userId); return notifications; } @@ -40,9 +39,7 @@ public class NotificationRepository implements PanacheRepositoryBase findUnreadByUserId(UUID userId) { - System.out.println("[LOG] RĂ©cupĂ©ration des notifications non lues pour l'utilisateur ID : " + userId); List notifications = find("user.id = ?1 AND isRead = false", Sort.by("createdAt", Sort.Direction.Descending), userId).list(); - System.out.println("[LOG] " + notifications.size() + " notification(s) non lue(s) trouvĂ©e(s) pour l'utilisateur ID : " + userId); return notifications; } @@ -55,11 +52,9 @@ public class NotificationRepository implements PanacheRepositoryBase findByUserIdWithPagination(UUID userId, int page, int size) { - System.out.println("[LOG] RĂ©cupĂ©ration paginĂ©e des notifications pour l'utilisateur ID : " + userId + " (page: " + page + ", size: " + size + ")"); List notifications = find("user.id", Sort.by("createdAt", Sort.Direction.Descending), userId) .page(Page.of(page, size)) .list(); - System.out.println("[LOG] " + notifications.size() + " notification(s) rĂ©cupĂ©rĂ©e(s) pour la page " + page); return notifications; } @@ -71,7 +66,6 @@ public class NotificationRepository implements PanacheRepositoryBase { + + private static final Logger logger = Logger.getLogger(PasswordResetTokenRepository.class); + + /** + * Trouve un token par sa valeur. + * + * @param token La valeur du token + * @return Le token s'il existe + */ + public Optional findByToken(String token) { + return find("token", token).firstResultOptional(); + } + + /** + * Trouve un token valide par sa valeur. + * + * @param token La valeur du token + * @return Le token s'il existe, n'est pas expirĂ© et n'a pas Ă©tĂ© utilisĂ© + */ + public Optional findValidToken(String token) { + return find("token = ?1 AND used = false AND expiresAt > ?2", token, LocalDateTime.now()) + .firstResultOptional(); + } + + /** + * Invalide tous les tokens existants pour un utilisateur. + * + * @param user L'utilisateur dont les tokens doivent ĂȘtre invalidĂ©s + * @return Le nombre de tokens invalidĂ©s + */ + public long invalidateAllForUser(Users user) { + long count = update("used = true WHERE user = ?1 AND used = false", user); + if (count > 0) { + logger.info("InvalidĂ© " + count + " token(s) existant(s) pour l'utilisateur: " + user.getEmail()); + } + return count; + } + + /** + * Supprime les tokens expirĂ©s (nettoyage). + * + * @return Le nombre de tokens supprimĂ©s + */ + public long deleteExpiredTokens() { + long count = delete("expiresAt < ?1 OR used = true", LocalDateTime.now().minusDays(7)); + if (count > 0) { + logger.info("SupprimĂ© " + count + " token(s) expirĂ©(s)"); + } + return count; + } +} diff --git a/src/main/java/com/lions/dev/repository/SocialPostRepository.java b/src/main/java/com/lions/dev/repository/SocialPostRepository.java index 1db00f5..478dd2c 100644 --- a/src/main/java/com/lions/dev/repository/SocialPostRepository.java +++ b/src/main/java/com/lions/dev/repository/SocialPostRepository.java @@ -7,6 +7,7 @@ import io.quarkus.panache.common.Sort; import jakarta.enterprise.context.ApplicationScoped; import java.util.List; import java.util.UUID; +import org.jboss.logging.Logger; /** * Repository pour l'entitĂ© SocialPost. @@ -27,11 +28,9 @@ public class SocialPostRepository implements PanacheRepositoryBase findAllWithPagination(int page, int size) { - System.out.println("[LOG] RĂ©cupĂ©ration paginĂ©e des posts (page: " + page + ", size: " + size + ")"); List posts = findAll(Sort.by("createdAt", Sort.Direction.Descending)) .page(Page.of(page, size)) .list(); - System.out.println("[LOG] " + posts.size() + " post(s) rĂ©cupĂ©rĂ©(s)"); return posts; } @@ -42,9 +41,7 @@ public class SocialPostRepository implements PanacheRepositoryBase findByUserId(UUID userId) { - System.out.println("[LOG] RĂ©cupĂ©ration des posts pour l'utilisateur ID : " + userId); List posts = find("user.id", Sort.by("createdAt", Sort.Direction.Descending), userId).list(); - System.out.println("[LOG] " + posts.size() + " post(s) trouvĂ©(s) pour l'utilisateur ID : " + userId); return posts; } @@ -55,10 +52,8 @@ public class SocialPostRepository implements PanacheRepositoryBase searchByContent(String query) { - System.out.println("[LOG] Recherche de posts avec la requĂȘte : " + query); String searchPattern = "%" + query.toLowerCase() + "%"; List posts = find("LOWER(content) LIKE ?1", Sort.by("createdAt", Sort.Direction.Descending), searchPattern).list(); - System.out.println("[LOG] " + posts.size() + " post(s) trouvĂ©(s) pour la requĂȘte : " + query); return posts; } @@ -69,11 +64,9 @@ public class SocialPostRepository implements PanacheRepositoryBase findPopularPosts(int limit) { - System.out.println("[LOG] RĂ©cupĂ©ration des " + limit + " posts les plus populaires"); List posts = findAll(Sort.by("likesCount", Sort.Direction.Descending)) .page(0, limit) .list(); - System.out.println("[LOG] " + posts.size() + " post(s) populaire(s) rĂ©cupĂ©rĂ©(s)"); return posts; } @@ -87,10 +80,8 @@ public class SocialPostRepository implements PanacheRepositoryBase findPostsByFriends(UUID userId, List friendIds, int page, int size) { - System.out.println("[LOG] RĂ©cupĂ©ration des posts des amis pour l'utilisateur ID : " + userId); if (friendIds == null || friendIds.isEmpty()) { - System.out.println("[LOG] Aucun ami trouvĂ© pour l'utilisateur ID : " + userId); return List.of(); } @@ -102,7 +93,6 @@ public class SocialPostRepository implements PanacheRepositoryBase { * @return Liste paginĂ©e des stories actives */ public List findAllActive(int page, int size) { - System.out.println("[LOG] RĂ©cupĂ©ration de toutes les stories actives (page: " + page + ", size: " + size + ")"); List stories = find("isActive = true AND expiresAt > ?1", Sort.by("createdAt", Sort.Direction.Descending), LocalDateTime.now()) .page(page, size) .list(); - System.out.println("[LOG] " + stories.size() + " story(ies) active(s) rĂ©cupĂ©rĂ©e(s)"); return stories; } @@ -65,14 +64,12 @@ public class StoryRepository implements PanacheRepositoryBase { * @return Liste paginĂ©e des stories actives de l'utilisateur */ public List findActiveByUserId(UUID userId, int page, int size) { - System.out.println("[LOG] RĂ©cupĂ©ration des stories actives pour l'utilisateur ID : " + userId + " (page: " + page + ", size: " + size + ")"); List stories = find("user.id = ?1 AND isActive = true AND expiresAt > ?2", Sort.by("createdAt", Sort.Direction.Descending), userId, LocalDateTime.now()) .page(page, size) .list(); - System.out.println("[LOG] " + stories.size() + " story(ies) active(s) trouvĂ©e(s) pour l'utilisateur ID : " + userId); return stories; } @@ -83,9 +80,7 @@ public class StoryRepository implements PanacheRepositoryBase { * @return Liste de toutes les stories de l'utilisateur */ public List findAllByUserId(UUID userId) { - System.out.println("[LOG] RĂ©cupĂ©ration de toutes les stories pour l'utilisateur ID : " + userId); List stories = find("user.id", Sort.by("createdAt", Sort.Direction.Descending), userId).list(); - System.out.println("[LOG] " + stories.size() + " story(ies) trouvĂ©e(s) pour l'utilisateur ID : " + userId); return stories; } @@ -96,9 +91,7 @@ public class StoryRepository implements PanacheRepositoryBase { * @return Liste des stories expirĂ©es */ public List findExpiredStories() { - System.out.println("[LOG] RĂ©cupĂ©ration des stories expirĂ©es"); List stories = find("isActive = true AND expiresAt <= ?1", LocalDateTime.now()).list(); - System.out.println("[LOG] " + stories.size() + " story(ies) expirĂ©e(s) trouvĂ©e(s)"); return stories; } @@ -109,9 +102,7 @@ public class StoryRepository implements PanacheRepositoryBase { * @return Le nombre de stories dĂ©sactivĂ©es */ public int deactivateExpiredStories() { - System.out.println("[LOG] DĂ©sactivation des stories expirĂ©es"); int updated = update("isActive = false WHERE isActive = true AND expiresAt <= ?1", LocalDateTime.now()); - System.out.println("[LOG] " + updated + " story(ies) dĂ©sactivĂ©e(s)"); return updated; } @@ -122,13 +113,11 @@ public class StoryRepository implements PanacheRepositoryBase { * @return Liste des stories les plus vues */ public List findMostViewedStories(int limit) { - System.out.println("[LOG] RĂ©cupĂ©ration des " + limit + " stories les plus vues"); List stories = find("isActive = true AND expiresAt > ?1", Sort.by("viewsCount", Sort.Direction.Descending), LocalDateTime.now()) .page(0, limit) .list(); - System.out.println("[LOG] " + stories.size() + " story(ies) populaire(s) rĂ©cupĂ©rĂ©e(s)"); return stories; } } diff --git a/src/main/java/com/lions/dev/repository/UsersRepository.java b/src/main/java/com/lions/dev/repository/UsersRepository.java index 4099e36..4e09805 100644 --- a/src/main/java/com/lions/dev/repository/UsersRepository.java +++ b/src/main/java/com/lions/dev/repository/UsersRepository.java @@ -8,6 +8,7 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; import java.util.Optional; import java.util.UUID; +import org.jboss.logging.Logger; /** * Repository pour l'entitĂ© Users. @@ -62,12 +63,9 @@ public class UsersRepository implements PanacheRepositoryBase { * @return Un Optional contenant l'utilisateur s'il est trouvĂ©, sinon un Optional vide. */ public Optional findByEmail(String email) { - System.out.println("[LOG] Recherche de l'utilisateur avec l'email : " + email); Users user = find("email", email).firstResult(); if (user != null) { - System.out.println("[LOG] Utilisateur trouvĂ© : " + user.getEmail()); } else { - System.out.println("[LOG] Aucun utilisateur trouvĂ© avec l'email : " + email); } return Optional.ofNullable(user); } @@ -79,10 +77,8 @@ public class UsersRepository implements PanacheRepositoryBase { * @return true si un utilisateur avec cet email existe, sinon false. */ public boolean existsByEmail(String email) { - System.out.println("[LOG] VĂ©rification de l'existence de l'utilisateur avec l'email : " + email); long count = find("email", email).count(); boolean exists = count > 0; - System.out.println("[LOG] Utilisateur existe : " + exists); return exists; } @@ -93,13 +89,10 @@ public class UsersRepository implements PanacheRepositoryBase { * @return true si l'utilisateur a Ă©tĂ© supprimĂ©, sinon false. */ public boolean deleteById(UUID id) { - System.out.println("[LOG] Suppression de l'utilisateur avec l'ID : " + id); long deletedCount = delete("id", id); // Utiliser long pour rĂ©cupĂ©rer le nombre d'enregistrements supprimĂ©s boolean deleted = deletedCount > 0; // Convertir en boolean if (deleted) { - System.out.println("[LOG] Utilisateur avec l'ID " + id + " supprimĂ© avec succĂšs."); } else { - System.out.println("[LOG] Aucune suppression, utilisateur avec l'ID " + id + " introuvable."); } return deleted; } diff --git a/src/main/java/com/lions/dev/resource/EstablishmentRatingResource.java b/src/main/java/com/lions/dev/resource/EstablishmentRatingResource.java index b4d9677..3b1fba6 100644 --- a/src/main/java/com/lions/dev/resource/EstablishmentRatingResource.java +++ b/src/main/java/com/lions/dev/resource/EstablishmentRatingResource.java @@ -44,6 +44,12 @@ public class EstablishmentRatingResource { @PathParam("establishmentId") String establishmentId, @QueryParam("userId") String userIdStr, @Valid EstablishmentRatingRequestDTO requestDTO) { + if (userIdStr == null || userIdStr.isBlank()) { + LOG.warn("Soumission de note sans userId pour l'Ă©tablissement " + establishmentId); + return Response.status(Response.Status.BAD_REQUEST) + .entity("Le paramĂštre userId est requis") + .build(); + } LOG.info("Soumission d'une note pour l'Ă©tablissement " + establishmentId + " par l'utilisateur " + userIdStr); try { @@ -82,6 +88,12 @@ public class EstablishmentRatingResource { @PathParam("establishmentId") String establishmentId, @QueryParam("userId") String userIdStr, @Valid EstablishmentRatingRequestDTO requestDTO) { + if (userIdStr == null || userIdStr.isBlank()) { + LOG.warn("Mise Ă  jour de note sans userId pour l'Ă©tablissement " + establishmentId); + return Response.status(Response.Status.BAD_REQUEST) + .entity("Le paramĂštre userId est requis") + .build(); + } LOG.info("Mise Ă  jour de la note pour l'Ă©tablissement " + establishmentId + " par l'utilisateur " + userIdStr); try { diff --git a/src/main/java/com/lions/dev/resource/EstablishmentResource.java b/src/main/java/com/lions/dev/resource/EstablishmentResource.java index 4d6d41e..357d56f 100644 --- a/src/main/java/com/lions/dev/resource/EstablishmentResource.java +++ b/src/main/java/com/lions/dev/resource/EstablishmentResource.java @@ -2,9 +2,13 @@ package com.lions.dev.resource; import com.lions.dev.dto.request.establishment.EstablishmentCreateRequestDTO; import com.lions.dev.dto.request.establishment.EstablishmentUpdateRequestDTO; +import com.lions.dev.dto.response.establishment.BusinessHoursResponseDTO; +import com.lions.dev.dto.response.establishment.EstablishmentAmenityResponseDTO; import com.lions.dev.dto.response.establishment.EstablishmentResponseDTO; import com.lions.dev.entity.establishment.Establishment; 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.UsersRepository; import com.lions.dev.service.EstablishmentService; @@ -43,6 +47,12 @@ public class EstablishmentResource { @Inject EstablishmentRepository establishmentRepository; + @Inject + BusinessHoursRepository businessHoursRepository; + + @Inject + EstablishmentAmenityRepository establishmentAmenityRepository; + private static final Logger LOG = Logger.getLogger(EstablishmentResource.class); // *********** CrĂ©ation d'un Ă©tablissement *********** @@ -145,6 +155,58 @@ public class EstablishmentResource { } } + // *********** Horaires d'ouverture d'un Ă©tablissement *********** + + @GET + @Path("/{id}/business-hours") + @Operation(summary = "RĂ©cupĂ©rer les horaires d'ouverture d'un Ă©tablissement", + description = "Retourne la liste des horaires d'ouverture pour l'Ă©tablissement donnĂ©") + public Response getBusinessHoursByEstablishmentId(@PathParam("id") UUID id) { + LOG.info("[LOG] RĂ©cupĂ©ration des horaires pour l'Ă©tablissement : " + id); + try { + establishmentService.getEstablishmentById(id); + var list = businessHoursRepository.findByEstablishmentId(id).stream() + .map(bh -> new BusinessHoursResponseDTO(bh, id)) + .collect(Collectors.toList()); + return Response.ok(list).build(); + } catch (RuntimeException e) { + LOG.warn("[WARN] " + e.getMessage()); + return Response.status(Response.Status.NOT_FOUND).entity(e.getMessage()).build(); + } catch (Exception e) { + LOG.error("[ERROR] Erreur lors de la rĂ©cupĂ©ration des horaires", e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity("Erreur lors de la rĂ©cupĂ©ration des horaires").build(); + } + } + + // *********** Équipements d'un Ă©tablissement *********** + + @GET + @Path("/{id}/amenities") + @Operation(summary = "RĂ©cupĂ©rer les Ă©quipements d'un Ă©tablissement", + description = "Retourne la liste des Ă©quipements (amenities) pour l'Ă©tablissement donnĂ©") + public Response getAmenitiesByEstablishmentId(@PathParam("id") UUID id) { + LOG.info("[LOG] RĂ©cupĂ©ration des Ă©quipements pour l'Ă©tablissement : " + id); + try { + establishmentService.getEstablishmentById(id); + var list = establishmentAmenityRepository.findByEstablishmentIdWithType(id).stream() + .map(ea -> new EstablishmentAmenityResponseDTO( + ea, + ea.getAmenityType() != null ? ea.getAmenityType().getName() : null, + ea.getAmenityType() != null ? ea.getAmenityType().getCategory() : null, + ea.getAmenityType() != null ? ea.getAmenityType().getIcon() : null)) + .collect(Collectors.toList()); + return Response.ok(list).build(); + } catch (RuntimeException e) { + LOG.warn("[WARN] " + e.getMessage()); + return Response.status(Response.Status.NOT_FOUND).entity(e.getMessage()).build(); + } catch (Exception e) { + LOG.error("[ERROR] Erreur lors de la rĂ©cupĂ©ration des Ă©quipements", e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity("Erreur lors de la rĂ©cupĂ©ration des Ă©quipements").build(); + } + } + // *********** RĂ©cupĂ©ration d'un Ă©tablissement par ID *********** @GET diff --git a/src/main/java/com/lions/dev/resource/EventsResource.java b/src/main/java/com/lions/dev/resource/EventsResource.java index d2496a1..e014b99 100644 --- a/src/main/java/com/lions/dev/resource/EventsResource.java +++ b/src/main/java/com/lions/dev/resource/EventsResource.java @@ -1,7 +1,6 @@ package com.lions.dev.resource; import com.lions.dev.core.errors.exceptions.EventNotFoundException; -import com.lions.dev.dto.UserResponseDTO; import com.lions.dev.dto.request.events.EventCreateRequestDTO; import com.lions.dev.dto.request.events.EventReadManyByIdRequestDTO; import com.lions.dev.dto.request.events.EventUpdateRequestDTO; @@ -10,6 +9,7 @@ import com.lions.dev.dto.response.events.EventCreateResponseDTO; import com.lions.dev.dto.response.events.EventReadManyByIdResponseDTO; 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.Events; import com.lions.dev.entity.users.Users; diff --git a/src/main/java/com/lions/dev/resource/FileUploadResource.java b/src/main/java/com/lions/dev/resource/FileUploadResource.java index 6f58233..e2a56f8 100644 --- a/src/main/java/com/lions/dev/resource/FileUploadResource.java +++ b/src/main/java/com/lions/dev/resource/FileUploadResource.java @@ -18,6 +18,7 @@ import jakarta.inject.Inject; import java.io.IOException; import java.util.HashMap; import java.util.Map; +import java.util.Optional; import java.util.UUID; @Path("/media") @@ -81,11 +82,16 @@ public class FileUploadResource { // Note: En production, vous devriez utiliser une URL publique (CDN, S3, etc.) String fileUrl = buildFileUrl(finalFileName, uriInfo); String thumbnailUrl = null; - - // Pour les vidĂ©os, on pourrait gĂ©nĂ©rer un thumbnail ici + if ("video".equalsIgnoreCase(mediaType)) { - // TODO: GĂ©nĂ©rer un thumbnail pour les vidĂ©os - thumbnailUrl = fileUrl; // Placeholder + Optional thumbPath = fileService.generateVideoThumbnail(savedFilePath); + if (thumbPath.isPresent()) { + thumbnailUrl = buildFileUrl(thumbPath.get().getFileName().toString(), uriInfo); + LOG.infof("Thumbnail vidĂ©o gĂ©nĂ©rĂ© : %s", thumbnailUrl); + } else { + thumbnailUrl = fileUrl; + LOG.infof("Thumbnail vidĂ©o non gĂ©nĂ©rĂ© (FFmpeg absent ou erreur), utilisation de l'URL vidĂ©o en placeholder"); + } } // Construire la rĂ©ponse JSON diff --git a/src/main/java/com/lions/dev/resource/NotificationResource.java b/src/main/java/com/lions/dev/resource/NotificationResource.java index 5f42e9f..b6c19d7 100644 --- a/src/main/java/com/lions/dev/resource/NotificationResource.java +++ b/src/main/java/com/lions/dev/resource/NotificationResource.java @@ -36,6 +36,7 @@ public class NotificationResource { /** * RĂ©cupĂšre toutes les notifications d'un utilisateur. + * En production, le userId doit ĂȘtre dĂ©rivĂ© du contexte d'authentification (JWT/session), pas de l'URL. * * @param userId L'ID de l'utilisateur * @return Liste des notifications de l'utilisateur diff --git a/src/main/java/com/lions/dev/resource/UsersResource.java b/src/main/java/com/lions/dev/resource/UsersResource.java index 06e7260..45e1f6a 100644 --- a/src/main/java/com/lions/dev/resource/UsersResource.java +++ b/src/main/java/com/lions/dev/resource/UsersResource.java @@ -2,6 +2,7 @@ package com.lions.dev.resource; import com.lions.dev.dto.PasswordResetRequest; import com.lions.dev.dto.request.users.AssignRoleRequestDTO; +import com.lions.dev.dto.request.users.SetUserActiveRequestDTO; import com.lions.dev.dto.request.users.UserAuthenticateRequestDTO; import com.lions.dev.dto.request.users.UserCreateRequestDTO; import com.lions.dev.dto.response.users.UserAuthenticateResponseDTO; @@ -9,6 +10,7 @@ import com.lions.dev.dto.response.users.UserCreateResponseDTO; import com.lions.dev.dto.response.users.UserDeleteResponseDto; import com.lions.dev.entity.users.Users; import com.lions.dev.exception.UserNotFoundException; +import com.lions.dev.service.PasswordResetService; import com.lions.dev.service.UsersService; import jakarta.inject.Inject; import jakarta.transaction.Transactional; @@ -42,6 +44,9 @@ public class UsersResource { @Inject UsersService userService; + @Inject + PasswordResetService passwordResetService; + @ConfigProperty(name = "afterwork.super-admin.api-key", defaultValue = "") Optional superAdminApiKey; @@ -300,18 +305,8 @@ public class UsersResource { LOG.info("Demande de reinitialisation de mot de passe pour l'email : " + request.getEmail()); try { - // Rechercher l'utilisateur par email - Users user = userService.findByEmail(request.getEmail()); - - if (user != null) { - // En standby : pas encore de service d'envoi de mail. Quand disponible : - // - generer un token de reset (table dĂ©diĂ©e ou champ user avec expiration) - // - appeler emailService.sendPasswordResetEmail(user.getEmail(), resetToken) - // Pour l'instant, on retourne success pour ne pas reveler si l'email existe. - LOG.info("Utilisateur trouve, email de reinitialisation (en standby - pas de mail service) : " + request.getEmail()); - } else { - LOG.info("Aucun utilisateur trouve avec cet email (ne pas reveler) : " + request.getEmail()); - } + // Le service gĂšre tout : crĂ©ation du token et envoi de l'email + passwordResetService.initiatePasswordReset(request.getEmail()); // Toujours retourner 200 pour ne pas reveler si l'email existe return Response.ok() @@ -326,6 +321,50 @@ public class UsersResource { } } + /** + * Endpoint pour rĂ©initialiser le mot de passe avec un token valide. + * + * @param token Le token de rĂ©initialisation reçu par email. + * @param newPassword Le nouveau mot de passe. + * @return Une rĂ©ponse HTTP indiquant si la rĂ©initialisation a rĂ©ussi. + */ + @POST + @Path("/reset-password") + @Operation(summary = "RĂ©initialiser le mot de passe avec un token", + description = "RĂ©initialise le mot de passe en utilisant le token reçu par email") + @APIResponse(responseCode = "200", description = "Mot de passe rĂ©initialisĂ© avec succĂšs") + @APIResponse(responseCode = "400", description = "Token invalide ou expirĂ©") + public Response resetPasswordWithToken( + @QueryParam("token") String token, + @QueryParam("newPassword") String newPassword) { + + if (token == null || token.isBlank()) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("message", "Le token est requis")) + .build(); + } + + if (newPassword == null || newPassword.length() < 8) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("message", "Le mot de passe doit contenir au moins 8 caractĂšres")) + .build(); + } + + boolean success = passwordResetService.resetPassword(token, newPassword); + + if (success) { + LOG.info("Mot de passe rĂ©initialisĂ© avec succĂšs via token"); + return Response.ok() + .entity(Map.of("message", "Votre mot de passe a Ă©tĂ© rĂ©initialisĂ© avec succĂšs")) + .build(); + } else { + LOG.warn("Échec de rĂ©initialisation : token invalide ou expirĂ©"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("message", "Le lien de rĂ©initialisation est invalide ou a expirĂ©")) + .build(); + } + } + /** * Attribue un rĂŽle Ă  un utilisateur (rĂ©servĂ© au super administrateur). * Requiert le header X-Super-Admin-Key correspondant Ă  afterwork.super-admin.api-key. @@ -420,4 +459,51 @@ public class UsersResource { } } + /** + * Force l'activation ou la suspension d'un utilisateur (rĂ©servĂ© au super administrateur). + * UtilisĂ© pour les managers : active = false = suspendu. + * + * @param id L'ID de l'utilisateur. + * @param request Le DTO contenant active (true = forcer l'activation, false = suspendre). + * @param apiKeyHeader Valeur du header X-Super-Admin-Key. + * @return L'utilisateur mis Ă  jour. + */ + @PATCH + @Path("/{id}/active") + @Transactional + @Operation(summary = "Forcer activation ou suspendre un utilisateur (super admin)", + description = "Modifie le statut actif (isActive) d'un utilisateur. RĂ©servĂ© au super administrateur (header X-Super-Admin-Key).") + @APIResponse(responseCode = "200", description = "Statut actif mis Ă  jour") + @APIResponse(responseCode = "403", description = "ClĂ© super admin invalide ou absente") + @APIResponse(responseCode = "404", description = "Utilisateur non trouvĂ©") + public Response setUserActive( + @PathParam("id") UUID id, + @Valid SetUserActiveRequestDTO request, + @HeaderParam(SUPER_ADMIN_KEY_HEADER) String apiKeyHeader) { + + String key = superAdminApiKey.orElse(""); + if (key.isBlank()) { + LOG.warn("OpĂ©ration setUserActive refusĂ©e : afterwork.super-admin.api-key non configurĂ©e"); + return Response.status(Response.Status.FORBIDDEN) + .entity("{\"message\": \"OpĂ©ration non autorisĂ©e : clĂ© super admin non configurĂ©e.\"}") + .build(); + } + if (apiKeyHeader == null || !apiKeyHeader.equals(key)) { + LOG.warn("OpĂ©ration setUserActive refusĂ©e : clĂ© super admin invalide ou absente"); + return Response.status(Response.Status.FORBIDDEN) + .entity("{\"message\": \"ClĂ© super administrateur invalide ou absente.\"}") + .build(); + } + + try { + Users user = userService.setUserActive(id, request.getActive()); + UserCreateResponseDTO responseDTO = new UserCreateResponseDTO(user); + return Response.ok(responseDTO).build(); + } catch (UserNotFoundException e) { + return Response.status(Response.Status.NOT_FOUND) + .entity("{\"message\": \"" + e.getMessage() + "\"}") + .build(); + } + } + } diff --git a/src/main/java/com/lions/dev/resource/WaveWebhookResource.java b/src/main/java/com/lions/dev/resource/WaveWebhookResource.java index 6e2772e..8af5582 100644 --- a/src/main/java/com/lions/dev/resource/WaveWebhookResource.java +++ b/src/main/java/com/lions/dev/resource/WaveWebhookResource.java @@ -7,10 +7,17 @@ import jakarta.inject.Inject; import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.config.inject.ConfigProperty; import org.jboss.logging.Logger; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.util.Optional; + /** * Webhook Wave pour les notifications de paiement (payment.completed, payment.cancelled, etc.). + * Si wave.webhook.secret est configurĂ©, la requĂȘte doit contenir le header X-Wave-Signature (HMAC-SHA256 du body). */ @Path("/webhooks/wave") @Consumes(MediaType.APPLICATION_JSON) @@ -18,15 +25,32 @@ import org.jboss.logging.Logger; public class WaveWebhookResource { private static final Logger LOG = Logger.getLogger(WaveWebhookResource.class); + private static final String SIGNATURE_HEADER = "X-Wave-Signature"; @Inject WavePaymentService wavePaymentService; + @ConfigProperty(name = "wave.webhook.secret", defaultValue = "") + Optional webhookSecret; + private final ObjectMapper objectMapper = new ObjectMapper(); @POST - public Response handleWebhook(String payload) { + public Response handleWebhook( + @HeaderParam(SIGNATURE_HEADER) String signature, + String payload) { try { + if (webhookSecret.isPresent() && !webhookSecret.get().isBlank()) { + if (signature == null || signature.isBlank()) { + LOG.warn("Webhook Wave rejetĂ© : header " + SIGNATURE_HEADER + " absent"); + return Response.status(Response.Status.UNAUTHORIZED).build(); + } + String expected = hmacSha256(webhookSecret.get(), payload); + if (!constantTimeEquals(signature.trim(), expected)) { + LOG.warn("Webhook Wave rejetĂ© : signature invalide"); + return Response.status(Response.Status.UNAUTHORIZED).build(); + } + } JsonNode node = objectMapper.readTree(payload); wavePaymentService.handleWebhook(node); return Response.ok().build(); @@ -35,4 +59,28 @@ public class WaveWebhookResource { return Response.status(Response.Status.BAD_REQUEST).build(); } } + + private static String hmacSha256(String secret, String payload) { + try { + Mac mac = Mac.getInstance("HmacSHA256"); + mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256")); + byte[] hash = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8)); + StringBuilder sb = new StringBuilder(hash.length * 2); + for (byte b : hash) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } catch (Exception e) { + throw new RuntimeException("HMAC computation failed", e); + } + } + + private static boolean constantTimeEquals(String a, String b) { + if (a.length() != b.length()) return false; + int diff = 0; + for (int i = 0; i < a.length(); i++) { + diff |= a.charAt(i) ^ b.charAt(i); + } + return diff == 0; + } } diff --git a/src/main/java/com/lions/dev/service/BookingService.java b/src/main/java/com/lions/dev/service/BookingService.java index de02601..9a3a7be 100644 --- a/src/main/java/com/lions/dev/service/BookingService.java +++ b/src/main/java/com/lions/dev/service/BookingService.java @@ -1,5 +1,6 @@ package com.lions.dev.service; +import com.lions.dev.dto.events.NotificationEvent; import com.lions.dev.dto.request.booking.ReservationCreateRequestDTO; import com.lions.dev.dto.response.booking.ReservationResponseDTO; import com.lions.dev.entity.booking.Booking; @@ -11,22 +12,35 @@ import com.lions.dev.repository.UsersRepository; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import jakarta.transaction.Transactional; +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Emitter; +import org.jboss.logging.Logger; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.UUID; import java.util.stream.Collectors; @ApplicationScoped public class BookingService { + private static final Logger LOG = Logger.getLogger(BookingService.class); + @Inject BookingRepository bookingRepository; @Inject EstablishmentRepository establishmentRepository; @Inject UsersRepository usersRepository; + @Inject + EmailService emailService; + + @Inject + @Channel("notifications") + Emitter notificationEmitter; private static final DateTimeFormatter ISO = DateTimeFormatter.ISO_DATE_TIME; @@ -58,18 +72,121 @@ public class BookingService { booking.setStatus("PENDING"); booking.setSpecialRequests(dto.getNotes()); bookingRepository.persist(booking); + + try { + emailService.sendBookingConfirmationEmail( + user.getEmail(), + user.getFirstName(), + establishment.getName(), + booking.getReservationTime(), + booking.getGuestCount() != null ? booking.getGuestCount() : 1 + ); + } catch (Exception e) { + // Ne pas faire Ă©chouer la rĂ©servation si l'email Ă©choue + } + + // Notifier le manager en temps rĂ©el + notifyManagerOfNewBooking(booking, establishment, user); + return new ReservationResponseDTO(booking); } + /** + * Notifie le manager d'un Ă©tablissement d'une nouvelle rĂ©servation. + */ + private void notifyManagerOfNewBooking(Booking booking, Establishment establishment, Users customer) { + try { + Users manager = establishment.getManager(); + if (manager == null) return; + + String customerName = customer.getFirstName() + " " + customer.getLastName(); + String dateStr = booking.getReservationTime() != null + ? booking.getReservationTime().format(DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm")) + : ""; + + Map data = new HashMap<>(); + data.put("bookingId", booking.getId().toString()); + data.put("establishmentId", establishment.getId().toString()); + data.put("establishmentName", establishment.getName()); + data.put("customerId", customer.getId().toString()); + data.put("customerName", customerName); + data.put("customerEmail", customer.getEmail()); + data.put("reservationTime", dateStr); + data.put("guestCount", booking.getGuestCount()); + data.put("specialRequests", booking.getSpecialRequests()); + data.put("status", booking.getStatus()); + + NotificationEvent event = new NotificationEvent( + manager.getId().toString(), + "booking_created", + data + ); + + notificationEmitter.send(event); + LOG.debug("[BookingService] Notification nouvelle rĂ©servation envoyĂ©e au manager : " + manager.getId()); + } catch (Exception e) { + LOG.warn("[BookingService] Échec notification Kafka rĂ©servation (non bloquant) : " + e.getMessage()); + } + } + @Transactional public ReservationResponseDTO cancelReservation(UUID id) { Booking booking = bookingRepository.findById(id); if (booking == null) return null; booking.setStatus("CANCELLED"); bookingRepository.persist(booking); + + // Notifier l'utilisateur et le manager + notifyBookingCancellation(booking); + return new ReservationResponseDTO(booking); } + /** + * Notifie l'utilisateur et le manager d'une annulation de rĂ©servation. + */ + private void notifyBookingCancellation(Booking booking) { + try { + Establishment establishment = booking.getEstablishment(); + Users customer = booking.getUser(); + if (establishment == null || customer == null) return; + + String dateStr = booking.getReservationTime() != null + ? booking.getReservationTime().format(DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm")) + : ""; + + Map data = new HashMap<>(); + data.put("bookingId", booking.getId().toString()); + data.put("establishmentName", establishment.getName()); + data.put("reservationTime", dateStr); + data.put("status", "CANCELLED"); + + // Notifier le client + NotificationEvent customerEvent = new NotificationEvent( + customer.getId().toString(), + "booking_cancelled", + data + ); + notificationEmitter.send(customerEvent); + + // Notifier le manager + Users manager = establishment.getManager(); + if (manager != null) { + data.put("customerName", customer.getFirstName() + " " + customer.getLastName()); + NotificationEvent managerEvent = new NotificationEvent( + manager.getId().toString(), + "booking_cancelled", + data + ); + notificationEmitter.send(managerEvent); + } + + LOG.debug("[BookingService] Notifications annulation envoyĂ©es"); + } catch (Exception e) { + LOG.warn("[BookingService] Échec notification annulation (non bloquant) : " + e.getMessage()); + } + } + @Transactional public void deleteReservation(UUID id) { bookingRepository.deleteById(id); diff --git a/src/main/java/com/lions/dev/service/EmailService.java b/src/main/java/com/lions/dev/service/EmailService.java new file mode 100644 index 0000000..b20cf34 --- /dev/null +++ b/src/main/java/com/lions/dev/service/EmailService.java @@ -0,0 +1,301 @@ +package com.lions.dev.service; + +import io.quarkus.mailer.Mail; +import io.quarkus.mailer.Mailer; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.jboss.logging.Logger; + +/** + * Service d'envoi d'emails pour l'application AfterWork. + * + * GĂšre l'envoi des emails de rĂ©initialisation de mot de passe, + * notifications, et autres communications. + */ +@ApplicationScoped +public class EmailService { + + private static final Logger logger = Logger.getLogger(EmailService.class); + + @Inject + Mailer mailer; + + @ConfigProperty(name = "afterwork.app.name", defaultValue = "AfterWork") + String appName; + + @ConfigProperty(name = "afterwork.app.url", defaultValue = "https://afterwork.lions.dev") + String appUrl; + + @ConfigProperty(name = "afterwork.email.from", defaultValue = "noreply@lions.dev") + String fromEmail; + + /** + * Envoie un email de rĂ©initialisation de mot de passe. + * + * @param toEmail L'adresse email du destinataire + * @param firstName Le prĂ©nom de l'utilisateur + * @param resetToken Le token de rĂ©initialisation + */ + public void sendPasswordResetEmail(String toEmail, String firstName, String resetToken) { + String resetLink = appUrl + "/reset-password?token=" + resetToken; + + String subject = appName + " - RĂ©initialisation de votre mot de passe"; + + String htmlContent = buildPasswordResetHtml(firstName, resetLink); + String textContent = buildPasswordResetText(firstName, resetLink); + + try { + mailer.send( + Mail.withHtml(toEmail, subject, htmlContent) + .setText(textContent) + ); + logger.info("Email de rĂ©initialisation envoyĂ© Ă : " + toEmail); + } catch (Exception e) { + logger.error("Erreur lors de l'envoi de l'email de rĂ©initialisation Ă : " + toEmail, e); + throw new RuntimeException("Impossible d'envoyer l'email de rĂ©initialisation", e); + } + } + + /** + * Construit le contenu HTML de l'email de rĂ©initialisation. + */ + private String buildPasswordResetHtml(String firstName, String resetLink) { + return """ + + + + + + + +
+
+

%s

+
+
+

Bonjour %s,

+

Vous avez demandé la réinitialisation de votre mot de passe.

+

Cliquez sur le bouton ci-dessous pour créer un nouveau mot de passe :

+

+ Réinitialiser mon mot de passe +

+
+ ⚠ Ce lien expire dans 1 heure. +

Si vous n'avez pas demandé cette réinitialisation, ignorez cet email.

+
+

Si le bouton ne fonctionne pas, copiez ce lien dans votre navigateur :

+

%s

+
+ +
+ + + """.formatted(appName, firstName, resetLink, resetLink, appName); + } + + /** + * Construit le contenu texte de l'email de rĂ©initialisation (fallback). + */ + private String buildPasswordResetText(String firstName, String resetLink) { + return """ + Bonjour %s, + + Vous avez demandĂ© la rĂ©initialisation de votre mot de passe sur %s. + + Pour crĂ©er un nouveau mot de passe, cliquez sur ce lien : + %s + + ⚠ Ce lien expire dans 1 heure. + + Si vous n'avez pas demandĂ© cette rĂ©initialisation, ignorez simplement cet email. + + Cordialement, + L'Ă©quipe %s + """.formatted(firstName, appName, resetLink, appName); + } + + /** + * Envoie un email de bienvenue aprĂšs inscription. + * + * @param toEmail L'adresse email du destinataire + * @param firstName Le prĂ©nom de l'utilisateur + */ + public void sendWelcomeEmail(String toEmail, String firstName) { + String subject = "Bienvenue sur " + appName + " !"; + + String htmlContent = """ + + + + + + + +
+
+

Bienvenue sur %s !

+
+
+

Bonjour %s,

+

Nous sommes ravis de vous accueillir sur %s !

+

Découvrez les meilleurs événements et établissements prÚs de chez vous.

+

+ Découvrir l'application +

+
+ +
+ + + """.formatted(appName, firstName, appName, appUrl, appName); + + try { + mailer.send(Mail.withHtml(toEmail, subject, htmlContent)); + logger.info("Email de bienvenue envoyĂ© Ă : " + toEmail); + } catch (Exception e) { + logger.error("Erreur lors de l'envoi de l'email de bienvenue Ă : " + toEmail, e); + } + } + + /** + * Confirmation de paiement / facture (abonnement Ă©tablissement Wave). + */ + public void sendPaymentConfirmationEmail(String toEmail, String firstName, String establishmentName, int amountXof, String plan) { + String subject = appName + " - Confirmation de paiement - " + establishmentName; + String amount = String.format("%d XOF", amountXof); + String planLabel = "MONTHLY".equalsIgnoreCase(plan) ? "Mensuel" : "Annuel"; + String html = buildTransactionalHtml( + "Paiement confirmĂ©", + "Bonjour " + firstName + ",", + "Votre paiement pour l'abonnement " + planLabel + " de l'Ă©tablissement " + establishmentName + " a bien Ă©tĂ© enregistrĂ©.", + "Montant : " + amount + ".", + "Votre Ă©tablissement est actif sur " + appName + "." + ); + sendSafe(toEmail, subject, html, "confirmation paiement"); + } + + /** + * Rappel d'Ă©vĂ©nement (J-1 ou H-1). + */ + public void sendEventReminderEmail(String toEmail, String firstName, String eventTitle, java.time.LocalDateTime startDate) { + String subject = appName + " - Rappel : " + eventTitle; + String dateStr = startDate != null ? startDate.format(java.time.format.DateTimeFormatter.ofPattern("EEEE d MMMM Ă  HH'h'mm")) : ""; + String html = buildTransactionalHtml( + "Rappel Ă©vĂ©nement", + "Bonjour " + firstName + ",", + "L'Ă©vĂ©nement " + eventTitle + " commence bientĂŽt.", + "Date et heure : " + dateStr + ".", + "Rendez-vous sur l'application pour plus de dĂ©tails." + ); + sendSafe(toEmail, subject, html, "rappel Ă©vĂ©nement"); + } + + /** + * Avertissement expiration abonnement (envoi J-3 avant expiration). + */ + public void sendSubscriptionExpirationWarningEmail(String toEmail, String firstName, String establishmentName, java.time.LocalDateTime expiresAt) { + String subject = appName + " - Votre abonnement expire bientĂŽt - " + establishmentName; + String dateStr = expiresAt != null ? expiresAt.format(java.time.format.DateTimeFormatter.ofPattern("d MMMM yyyy")) : ""; + String html = buildTransactionalHtml( + "Abonnement bientĂŽt expirĂ©", + "Bonjour " + firstName + ",", + "L'abonnement de votre Ă©tablissement " + establishmentName + " expire le " + dateStr + ".", + "Renouvelez votre abonnement pour continuer Ă  bĂ©nĂ©ficier des fonctionnalitĂ©s.", + "Renouveler sur " + appName + "" + ); + sendSafe(toEmail, subject, html, "expiration abonnement"); + } + + /** + * Confirmation de rĂ©servation (Ă©tablissement). + */ + public void sendBookingConfirmationEmail(String toEmail, String firstName, String establishmentName, java.time.LocalDateTime reservationTime, int guestCount) { + String subject = appName + " - Confirmation de rĂ©servation - " + establishmentName; + String dateStr = reservationTime != null ? reservationTime.format(java.time.format.DateTimeFormatter.ofPattern("EEEE d MMMM Ă  HH'h'mm")) : ""; + String html = buildTransactionalHtml( + "RĂ©servation confirmĂ©e", + "Bonjour " + firstName + ",", + "Votre rĂ©servation chez " + establishmentName + " est confirmĂ©e.", + "Date et heure : " + dateStr + ". Nombre de convives : " + guestCount + ".", + "À bientĂŽt !" + ); + sendSafe(toEmail, subject, html, "confirmation rĂ©servation"); + } + + /** + * Notification Ă©chec paiement Wave (destinĂ© au manager). + */ + public void sendPaymentFailureEmail(String toEmail, String firstName, String establishmentName) { + String subject = appName + " - Échec du paiement - " + establishmentName; + String html = buildTransactionalHtml( + "Échec du paiement", + "Bonjour " + firstName + ",", + "Le paiement pour l'Ă©tablissement " + establishmentName + " n'a pas abouti.", + "Veuillez rĂ©essayer ou contacter le support si le problĂšme persiste.", + "RĂ©essayer sur " + appName + "" + ); + sendSafe(toEmail, subject, html, "Ă©chec paiement"); + } + + private String buildTransactionalHtml(String title, String greeting, String paragraph1, String paragraph2, String paragraph3) { + return """ + + + + + + +
+

%s

+
+

%s

+

%s

+

%s

+

%s

+
+ +
+ + + """.formatted(title, greeting, paragraph1, paragraph2, paragraph3, appName); + } + + private void sendSafe(String toEmail, String subject, String htmlContent, String logLabel) { + try { + mailer.send(Mail.withHtml(toEmail, subject, htmlContent)); + logger.info("Email " + logLabel + " envoyĂ© Ă : " + toEmail); + } catch (Exception e) { + logger.error("Erreur envoi email " + logLabel + " Ă  " + toEmail, e); + } + } +} diff --git a/src/main/java/com/lions/dev/service/EstablishmentRatingService.java b/src/main/java/com/lions/dev/service/EstablishmentRatingService.java index 2c2d903..9c5e5ab 100644 --- a/src/main/java/com/lions/dev/service/EstablishmentRatingService.java +++ b/src/main/java/com/lions/dev/service/EstablishmentRatingService.java @@ -31,6 +31,9 @@ public class EstablishmentRatingService { @Inject UsersRepository usersRepository; + @Inject + com.lions.dev.service.NotificationService notificationService; + private static final Logger LOG = Logger.getLogger(EstablishmentRatingService.class); /** @@ -74,6 +77,24 @@ public class EstablishmentRatingService { // Mettre Ă  jour les statistiques de l'Ă©tablissement updateEstablishmentRatingStats(establishmentId); + // Notification au manager de l'Ă©tablissement (dĂ©clencheur automatique) + try { + Users manager = establishment.getManager(); + if (manager != null && !manager.getId().equals(userId)) { + String raterName = user.getFirstName() + " " + user.getLastName(); + notificationService.createNotification( + "Nouvelle note", + raterName + " a notĂ© votre Ă©tablissement « " + establishment.getName() + " » (" + requestDTO.getRating() + "/5)", + "rating", + manager.getId(), + null + ); + LOG.info("Notification nouvelle note envoyĂ©e au manager : " + manager.getId()); + } + } catch (Exception e) { + LOG.error("Erreur crĂ©ation notification note Ă©tablissement : " + e.getMessage()); + } + LOG.info("Note soumise avec succĂšs : " + rating.getId()); return rating; } diff --git a/src/main/java/com/lions/dev/service/FileService.java b/src/main/java/com/lions/dev/service/FileService.java index 1f0d52e..a8b8c3a 100644 --- a/src/main/java/com/lions/dev/service/FileService.java +++ b/src/main/java/com/lions/dev/service/FileService.java @@ -9,6 +9,8 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; +import java.util.Optional; +import java.util.concurrent.TimeUnit; import org.jboss.logging.Logger; /** @@ -64,5 +66,63 @@ public class FileService { return destinationPath; } - + + /** + * GĂ©nĂšre une miniature (thumbnail) Ă  partir d'un fichier vidĂ©o via FFmpeg. + * Extrait une frame Ă  1 seconde. Si FFmpeg n'est pas disponible ou en cas d'erreur, + * retourne Optional.empty() sans lever d'exception. + * + * @param videoPath Chemin absolu du fichier vidĂ©o. + * @return Chemin du fichier JPEG gĂ©nĂ©rĂ©, ou empty si indisponible. + */ + public Optional generateVideoThumbnail(Path videoPath) { + if (videoPath == null || !Files.exists(videoPath)) { + LOG.warnf("[WARN] GĂ©nĂ©ration thumbnail : fichier vidĂ©o absent ou invalide : %s", videoPath); + return Optional.empty(); + } + String baseName = getBaseNameWithoutExtension(videoPath.getFileName().toString()); + Path parentDir = videoPath.getParent(); + String thumbFileName = baseName + "_thumb.jpg"; + Path thumbPath = parentDir != null ? parentDir.resolve(thumbFileName) : Paths.get(thumbFileName); + ProcessBuilder pb = new ProcessBuilder( + "ffmpeg", + "-y", + "-i", videoPath.toAbsolutePath().toString(), + "-ss", "00:00:01", + "-vframes", "1", + "-q:v", "2", + "-f", "image2", + thumbPath.toAbsolutePath().toString() + ); + pb.redirectErrorStream(true); + try { + Process process = pb.start(); + boolean finished = process.waitFor(30, TimeUnit.SECONDS); + if (!finished) { + process.destroyForcibly(); + LOG.warnf("[WARN] GĂ©nĂ©ration thumbnail : timeout FFmpeg pour %s", videoPath); + return Optional.empty(); + } + if (process.exitValue() != 0) { + LOG.warnf("[WARN] GĂ©nĂ©ration thumbnail : FFmpeg a Ă©chouĂ© (code %d) pour %s", process.exitValue(), videoPath); + return Optional.empty(); + } + if (Files.exists(thumbPath)) { + LOG.infof("[LOG] Thumbnail vidĂ©o gĂ©nĂ©rĂ© : %s", thumbPath); + return Optional.of(thumbPath); + } + } catch (IOException e) { + LOG.warnf("[WARN] FFmpeg non disponible ou erreur I/O pour thumbnail : %s", e.getMessage()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + LOG.warnf("[WARN] GĂ©nĂ©ration thumbnail interrompue pour %s", videoPath); + } + return Optional.empty(); + } + + private static String getBaseNameWithoutExtension(String fileName) { + if (fileName == null) return "video"; + int lastDot = fileName.lastIndexOf('.'); + return lastDot > 0 ? fileName.substring(0, lastDot) : fileName; + } } diff --git a/src/main/java/com/lions/dev/service/FriendshipService.java b/src/main/java/com/lions/dev/service/FriendshipService.java index 3bbbd9a..9e56c5f 100644 --- a/src/main/java/com/lions/dev/service/FriendshipService.java +++ b/src/main/java/com/lions/dev/service/FriendshipService.java @@ -90,6 +90,21 @@ public class FriendshipService { Friendship friendship = new Friendship(user, friend, FriendshipStatus.PENDING); friendshipRepository.persist(friendship); + // Notification en base pour le destinataire (dĂ©clencheur automatique) + try { + String senderName = user.getFirstName() + " " + user.getLastName(); + notificationService.createNotification( + "Demande d'amitiĂ©", + senderName + " vous a envoyĂ© une demande d'amitiĂ©", + "friend", + friend.getId(), + null + ); + logger.info("[LOG] Notification demande d'amitiĂ© créée pour : " + friend.getId()); + } catch (Exception e) { + logger.error("[ERROR] Erreur crĂ©ation notification demande d'amitiĂ© : " + e.getMessage()); + } + // TEMPS RÉEL: Publier dans Kafka (v2.0) try { Map notificationData = new HashMap<>(); @@ -113,7 +128,8 @@ public class FriendshipService { } logger.info("[LOG] Demande d'amitiĂ© envoyĂ©e avec succĂšs."); - return new FriendshipCreateOneResponseDTO(friendship); + // Construire le DTO avec les IDs dĂ©jĂ  chargĂ©s pour Ă©viter LazyInitializationException + return new FriendshipCreateOneResponseDTO(friendship, user.getId(), friend.getId()); } /** @@ -209,8 +225,10 @@ public class FriendshipService { logger.error("[ERROR] Erreur lors de la crĂ©ation des notifications d'amitiĂ© : " + e.getMessage()); } - // Retourner la rĂ©ponse avec les informations de la relation d'amitiĂ© - return new FriendshipCreateOneResponseDTO(friendship); + // Retourner la rĂ©ponse avec les IDs dĂ©jĂ  chargĂ©s (Ă©vite LazyInitializationException) + Users user = friendship.getUser(); + Users friendUser = friendship.getFriend(); + return new FriendshipCreateOneResponseDTO(friendship, user.getId(), friendUser.getId()); } /** diff --git a/src/main/java/com/lions/dev/service/MessageService.java b/src/main/java/com/lions/dev/service/MessageService.java index 5a10e65..8e9084d 100644 --- a/src/main/java/com/lions/dev/service/MessageService.java +++ b/src/main/java/com/lions/dev/service/MessageService.java @@ -14,6 +14,7 @@ import io.smallrye.reactive.messaging.kafka.api.OutgoingKafkaRecordMetadata; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import jakarta.transaction.Transactional; +import org.jboss.logging.Logger; import java.util.HashMap; import java.util.List; @@ -25,12 +26,12 @@ import java.util.UUID; * * Ce service contient la logique mĂ©tier pour l'envoi de messages, * la rĂ©cupĂ©ration de conversations, et la gestion des messages non lus. - * - * Tous les logs nĂ©cessaires pour la traçabilitĂ© sont intĂ©grĂ©s. */ @ApplicationScoped public class MessageService { + private static final Logger logger = Logger.getLogger(MessageService.class); + @Inject MessageRepository messageRepository; @@ -65,7 +66,7 @@ public class MessageService { String content, String messageType, String mediaUrl) { - System.out.println("[LOG] Envoi de message de " + senderId + " Ă  " + recipientId); + logger.info("[MessageService] Envoi de message de " + senderId + " Ă  " + recipientId); // RĂ©cupĂ©rer les utilisateurs Users sender = usersRepository.findById(senderId); @@ -91,7 +92,7 @@ public class MessageService { // Persister le message messageRepository.persist(message); - System.out.println("[LOG] Message créé avec l'ID : " + message.getId()); + logger.info("[MessageService] Message créé avec l'ID : " + message.getId()); // Mettre Ă  jour la conversation conversation.updateLastMessage(message); @@ -112,9 +113,9 @@ public class MessageService { recipientId, null ); - System.out.println("[LOG] Notification créée pour le destinataire"); + logger.info("[MessageService] Notification créée pour le destinataire"); } catch (Exception e) { - System.out.println("[ERROR] Erreur lors de la crĂ©ation de la notification : " + e.getMessage()); + logger.error("[MessageService] Erreur lors de la crĂ©ation de la notification : " + e.getMessage()); } // TEMPS RÉEL : Publier dans Kafka (v2.0) @@ -149,12 +150,12 @@ public class MessageService { event, () -> java.util.concurrent.CompletableFuture.completedFuture(null), // ack throwable -> { - System.out.println("[ERROR] Erreur envoi Kafka: " + throwable.getMessage()); + logger.error("[MessageService] Erreur envoi Kafka: " + throwable.getMessage()); return java.util.concurrent.CompletableFuture.completedFuture(null); // nack } ).addMetadata(kafkaMetadata)); - System.out.println("[LOG] Message publiĂ© dans Kafka: " + message.getId()); + logger.info("[MessageService] Message publiĂ© dans Kafka: " + message.getId()); // Envoyer confirmation de dĂ©livrance Ă  l'expĂ©diteur (via Kafka aussi) try { @@ -181,9 +182,9 @@ public class MessageService { throwable -> java.util.concurrent.CompletableFuture.completedFuture(null) ).addMetadata(deliveryKafkaMetadata)); - System.out.println("[LOG] Confirmation de dĂ©livrance publiĂ©e dans Kafka pour : " + senderId); + logger.info("[MessageService] Confirmation de dĂ©livrance publiĂ©e dans Kafka pour : " + senderId); } catch (Exception deliveryEx) { - System.out.println("[ERROR] Erreur publication confirmation dĂ©livrance : " + deliveryEx.getMessage()); + logger.error("[MessageService] Erreur publication confirmation dĂ©livrance : " + deliveryEx.getMessage()); // Ne pas bloquer si la confirmation Ă©choue } } catch (Exception e) { @@ -202,7 +203,7 @@ public class MessageService { * @throws UserNotFoundException Si l'utilisateur n'existe pas */ public List getUserConversations(UUID userId) { - System.out.println("[LOG] RĂ©cupĂ©ration des conversations pour l'utilisateur ID : " + userId); + logger.info("[MessageService] RĂ©cupĂ©ration des conversations pour l'utilisateur ID : " + userId); Users user = usersRepository.findById(userId); if (user == null) { @@ -221,7 +222,7 @@ public class MessageService { * @return Liste paginĂ©e des messages */ public List getConversationMessages(UUID conversationId, int page, int size) { - System.out.println("[LOG] RĂ©cupĂ©ration des messages pour la conversation ID : " + conversationId); + logger.info("[MessageService] RĂ©cupĂ©ration des messages pour la conversation ID : " + conversationId); return messageRepository.findByConversationId(conversationId, page, size); } @@ -232,7 +233,7 @@ public class MessageService { * @return La conversation */ public Conversation getConversation(UUID conversationId) { - System.out.println("[LOG] RĂ©cupĂ©ration de la conversation ID : " + conversationId); + logger.info("[MessageService] RĂ©cupĂ©ration de la conversation ID : " + conversationId); return conversationRepository.findById(conversationId); } @@ -245,7 +246,7 @@ public class MessageService { * @throws UserNotFoundException Si l'un des utilisateurs n'existe pas */ public Conversation getConversationBetweenUsers(UUID user1Id, UUID user2Id) { - System.out.println("[LOG] Recherche de conversation entre " + user1Id + " et " + user2Id); + logger.info("[MessageService] Recherche de conversation entre " + user1Id + " et " + user2Id); Users user1 = usersRepository.findById(user1Id); Users user2 = usersRepository.findById(user2Id); @@ -265,7 +266,7 @@ public class MessageService { */ @Transactional public Message markMessageAsRead(UUID messageId) { - System.out.println("[LOG] Marquage du message comme lu : " + messageId); + logger.info("[MessageService] Marquage du message comme lu : " + messageId); Message message = messageRepository.findById(messageId); if (message == null) { @@ -309,12 +310,12 @@ public class MessageService { throwable -> java.util.concurrent.CompletableFuture.completedFuture(null) ).addMetadata(readKafkaMetadata)); - System.out.println("[LOG] Confirmation de lecture publiĂ©e dans Kafka pour : " + message.getSender().getId()); + logger.info("[MessageService] Confirmation de lecture publiĂ©e dans Kafka pour : " + message.getSender().getId()); } catch (Exception e) { - System.out.println("[ERROR] Erreur publication confirmation lecture : " + e.getMessage()); + logger.error("[MessageService] Erreur publication confirmation lecture : " + e.getMessage()); } } catch (Exception e) { - System.out.println("[ERROR] Erreur envoi confirmation lecture : " + e.getMessage()); + logger.error("[MessageService] Erreur envoi confirmation lecture : " + e.getMessage()); } return message; @@ -329,7 +330,7 @@ public class MessageService { */ @Transactional public int markAllMessagesAsRead(UUID conversationId, UUID userId) { - System.out.println("[LOG] Marquage de tous les messages comme lus pour la conversation " + conversationId); + logger.info("[MessageService] Marquage de tous les messages comme lus pour la conversation " + conversationId); Conversation conversation = conversationRepository.findById(conversationId); if (conversation == null) { @@ -358,7 +359,7 @@ public class MessageService { * @return Le nombre de messages non lus */ public long getTotalUnreadCount(UUID userId) { - System.out.println("[LOG] RĂ©cupĂ©ration du nombre total de messages non lus pour l'utilisateur " + userId); + logger.info("[MessageService] RĂ©cupĂ©ration du nombre total de messages non lus pour l'utilisateur " + userId); return conversationRepository.countTotalUnreadMessages(userId); } @@ -370,7 +371,7 @@ public class MessageService { */ @Transactional public boolean deleteMessage(UUID messageId) { - System.out.println("[LOG] Suppression du message ID : " + messageId); + logger.info("[MessageService] Suppression du message ID : " + messageId); Message message = messageRepository.findById(messageId); if (message == null) { @@ -389,7 +390,7 @@ public class MessageService { */ @Transactional public boolean deleteConversation(UUID conversationId) { - System.out.println("[LOG] Suppression de la conversation ID : " + conversationId); + logger.info("[MessageService] Suppression de la conversation ID : " + conversationId); // Supprimer d'abord tous les messages messageRepository.deleteByConversationId(conversationId); diff --git a/src/main/java/com/lions/dev/service/NotificationService.java b/src/main/java/com/lions/dev/service/NotificationService.java index b214fee..333a4fd 100644 --- a/src/main/java/com/lions/dev/service/NotificationService.java +++ b/src/main/java/com/lions/dev/service/NotificationService.java @@ -1,5 +1,6 @@ package com.lions.dev.service; +import com.lions.dev.dto.events.NotificationEvent; import com.lions.dev.entity.events.Events; import com.lions.dev.entity.notification.Notification; import com.lions.dev.entity.users.Users; @@ -10,20 +11,25 @@ import com.lions.dev.repository.UsersRepository; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import jakarta.transaction.Transactional; +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Emitter; +import org.jboss.logging.Logger; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.UUID; /** * Service de gestion des notifications. - * + * * Ce service contient la logique mĂ©tier pour la crĂ©ation, rĂ©cupĂ©ration, * mise Ă  jour et suppression des notifications. - * - * Tous les logs nĂ©cessaires pour la traçabilitĂ© sont intĂ©grĂ©s. */ @ApplicationScoped public class NotificationService { + private static final Logger logger = Logger.getLogger(NotificationService.class); + @Inject NotificationRepository notificationRepository; @@ -33,6 +39,10 @@ public class NotificationService { @Inject EventsRepository eventsRepository; + @Inject + @Channel("notifications") + Emitter notificationEmitter; + /** * RĂ©cupĂšre toutes les notifications d'un utilisateur. * @@ -41,16 +51,16 @@ public class NotificationService { * @throws UserNotFoundException Si l'utilisateur n'existe pas */ public List getNotificationsByUserId(UUID userId) { - System.out.println("[LOG] RĂ©cupĂ©ration des notifications pour l'utilisateur ID : " + userId); + logger.info("[NotificationService] RĂ©cupĂ©ration des notifications pour l'utilisateur ID : " + userId); Users user = usersRepository.findById(userId); if (user == null) { - System.out.println("[ERROR] Utilisateur non trouvĂ© avec l'ID : " + userId); + logger.error("[NotificationService] Utilisateur non trouvĂ© avec l'ID : " + userId); throw new UserNotFoundException("Utilisateur non trouvĂ© avec l'ID : " + userId); } List notifications = notificationRepository.findByUserId(userId); - System.out.println("[LOG] " + notifications.size() + " notification(s) rĂ©cupĂ©rĂ©e(s) pour l'utilisateur ID : " + userId); + logger.info("[NotificationService] " + notifications.size() + " notification(s) rĂ©cupĂ©rĂ©e(s) pour l'utilisateur ID : " + userId); return notifications; } @@ -64,11 +74,11 @@ public class NotificationService { * @throws UserNotFoundException Si l'utilisateur n'existe pas */ public List getNotificationsByUserIdWithPagination(UUID userId, int page, int size) { - System.out.println("[LOG] RĂ©cupĂ©ration paginĂ©e des notifications pour l'utilisateur ID : " + userId); + logger.info("[NotificationService] RĂ©cupĂ©ration paginĂ©e des notifications pour l'utilisateur ID : " + userId); Users user = usersRepository.findById(userId); if (user == null) { - System.out.println("[ERROR] Utilisateur non trouvĂ© avec l'ID : " + userId); + logger.error("[NotificationService] Utilisateur non trouvĂ© avec l'ID : " + userId); throw new UserNotFoundException("Utilisateur non trouvĂ© avec l'ID : " + userId); } @@ -93,11 +103,11 @@ public class NotificationService { String type, UUID userId, UUID eventId) { - System.out.println("[LOG] CrĂ©ation d'une notification : " + title + " pour l'utilisateur ID : " + userId); + logger.info("[NotificationService] CrĂ©ation d'une notification : " + title + " pour l'utilisateur ID : " + userId); Users user = usersRepository.findById(userId); if (user == null) { - System.out.println("[ERROR] Utilisateur non trouvĂ© avec l'ID : " + userId); + logger.error("[NotificationService] Utilisateur non trouvĂ© avec l'ID : " + userId); throw new UserNotFoundException("Utilisateur non trouvĂ© avec l'ID : " + userId); } @@ -107,15 +117,50 @@ public class NotificationService { Events event = eventsRepository.findById(eventId); if (event != null) { notification.setEvent(event); - System.out.println("[LOG] Notification associĂ©e Ă  l'Ă©vĂ©nement ID : " + eventId); + logger.info("[NotificationService] Notification associĂ©e Ă  l'Ă©vĂ©nement ID : " + eventId); } } notificationRepository.persist(notification); - System.out.println("[LOG] 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 + 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 data = new HashMap<>(); + data.put("notificationId", notification.getId().toString()); + data.put("title", notification.getTitle()); + data.put("message", notification.getMessage()); + data.put("isRead", notification.isRead()); + data.put("createdAt", notification.getCreatedAt() != null + ? notification.getCreatedAt().toString() : null); + + if (notification.getEvent() != null) { + data.put("eventId", notification.getEvent().getId().toString()); + data.put("eventTitle", notification.getEvent().getTitle()); + } + + NotificationEvent event = new NotificationEvent( + user.getId().toString(), + notification.getType(), + data + ); + + notificationEmitter.send(event); + 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()); + } + } + /** * Marque une notification comme lue. * @@ -124,17 +169,17 @@ public class NotificationService { */ @Transactional public Notification markAsRead(UUID notificationId) { - System.out.println("[LOG] Marquage de la notification ID : " + notificationId + " comme lue"); + logger.info("[NotificationService] Marquage de la notification ID : " + notificationId + " comme lue"); Notification notification = notificationRepository.findById(notificationId); if (notification == null) { - System.out.println("[ERROR] Notification non trouvĂ©e avec l'ID : " + notificationId); + logger.error("[NotificationService] Notification non trouvĂ©e avec l'ID : " + notificationId); throw new IllegalArgumentException("Notification non trouvĂ©e avec l'ID : " + notificationId); } notification.markAsRead(); notificationRepository.persist(notification); - System.out.println("[LOG] Notification marquĂ©e comme lue avec succĂšs"); + logger.info("[NotificationService] Notification marquĂ©e comme lue avec succĂšs"); return notification; } @@ -147,16 +192,16 @@ public class NotificationService { */ @Transactional public int markAllAsRead(UUID userId) { - System.out.println("[LOG] Marquage de toutes les notifications comme lues pour l'utilisateur ID : " + userId); + logger.info("[NotificationService] Marquage de toutes les notifications comme lues pour l'utilisateur ID : " + userId); Users user = usersRepository.findById(userId); if (user == null) { - System.out.println("[ERROR] Utilisateur non trouvĂ© avec l'ID : " + userId); + logger.error("[NotificationService] Utilisateur non trouvĂ© avec l'ID : " + userId); throw new UserNotFoundException("Utilisateur non trouvĂ© avec l'ID : " + userId); } int updated = notificationRepository.markAllAsReadByUserId(userId); - System.out.println("[LOG] " + updated + " notification(s) marquĂ©e(s) comme lue(s)"); + logger.info("[NotificationService] " + updated + " notification(s) marquĂ©e(s) comme lue(s)"); return updated; } @@ -168,16 +213,16 @@ public class NotificationService { */ @Transactional public boolean deleteNotification(UUID notificationId) { - System.out.println("[LOG] Suppression de la notification ID : " + notificationId); + logger.info("[NotificationService] Suppression de la notification ID : " + notificationId); Notification notification = notificationRepository.findById(notificationId); if (notification == null) { - System.out.println("[ERROR] Notification non trouvĂ©e avec l'ID : " + notificationId); + logger.error("[NotificationService] Notification non trouvĂ©e avec l'ID : " + notificationId); return false; } notificationRepository.delete(notification); - System.out.println("[LOG] Notification supprimĂ©e avec succĂšs"); + logger.info("[NotificationService] Notification supprimĂ©e avec succĂšs"); return true; } @@ -188,11 +233,11 @@ public class NotificationService { * @return La notification trouvĂ©e */ public Notification getNotificationById(UUID notificationId) { - System.out.println("[LOG] RĂ©cupĂ©ration de la notification ID : " + notificationId); + logger.info("[NotificationService] RĂ©cupĂ©ration de la notification ID : " + notificationId); Notification notification = notificationRepository.findById(notificationId); if (notification == null) { - System.out.println("[ERROR] Notification non trouvĂ©e avec l'ID : " + notificationId); + logger.error("[NotificationService] Notification non trouvĂ©e avec l'ID : " + notificationId); throw new IllegalArgumentException("Notification non trouvĂ©e avec l'ID : " + notificationId); } diff --git a/src/main/java/com/lions/dev/service/PasswordResetService.java b/src/main/java/com/lions/dev/service/PasswordResetService.java new file mode 100644 index 0000000..76b746d --- /dev/null +++ b/src/main/java/com/lions/dev/service/PasswordResetService.java @@ -0,0 +1,133 @@ +package com.lions.dev.service; + +import com.lions.dev.entity.auth.PasswordResetToken; +import com.lions.dev.entity.users.Users; +import com.lions.dev.repository.PasswordResetTokenRepository; +import com.lions.dev.repository.UsersRepository; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import org.jboss.logging.Logger; + +import java.util.Optional; + +/** + * Service de gestion de la rĂ©initialisation de mot de passe. + * + * Coordonne la crĂ©ation de tokens, l'envoi d'emails et la validation. + */ +@ApplicationScoped +public class PasswordResetService { + + private static final Logger logger = Logger.getLogger(PasswordResetService.class); + + @Inject + PasswordResetTokenRepository tokenRepository; + + @Inject + UsersRepository usersRepository; + + @Inject + EmailService emailService; + + /** + * Initie le processus de rĂ©initialisation de mot de passe. + * CrĂ©e un token et envoie un email Ă  l'utilisateur. + * + * @param email L'email de l'utilisateur + * @return true si l'email a Ă©tĂ© envoyĂ© (ou simulĂ©), false si l'utilisateur n'existe pas + */ + @Transactional + public boolean initiatePasswordReset(String email) { + logger.info("Initiation de rĂ©initialisation de mot de passe pour: " + email); + + // Rechercher l'utilisateur + Optional userOpt = usersRepository.findByEmail(email); + if (userOpt.isEmpty()) { + logger.info("Aucun utilisateur trouvĂ© avec l'email: " + email); + return false; + } + + Users user = userOpt.get(); + + // Invalider les tokens existants pour cet utilisateur + tokenRepository.invalidateAllForUser(user); + + // CrĂ©er un nouveau token + PasswordResetToken token = new PasswordResetToken(user); + tokenRepository.persist(token); + + logger.info("Token de rĂ©initialisation créé pour: " + email + " (expire: " + token.getExpiresAt() + ")"); + + // Envoyer l'email + try { + emailService.sendPasswordResetEmail(user.getEmail(), user.getFirstName(), token.getToken()); + return true; + } catch (Exception e) { + logger.error("Erreur lors de l'envoi de l'email de rĂ©initialisation", e); + // On ne rĂ©vĂšle pas l'erreur Ă  l'utilisateur pour des raisons de sĂ©curitĂ© + return true; // Retourne true quand mĂȘme pour ne pas rĂ©vĂ©ler si l'email existe + } + } + + /** + * Valide un token de rĂ©initialisation. + * + * @param tokenValue La valeur du token + * @return Le token s'il est valide, empty sinon + */ + public Optional validateToken(String tokenValue) { + Optional tokenOpt = tokenRepository.findValidToken(tokenValue); + + if (tokenOpt.isEmpty()) { + logger.warn("Token de rĂ©initialisation invalide ou expirĂ©: " + tokenValue.substring(0, 8) + "..."); + } + + return tokenOpt; + } + + /** + * RĂ©initialise le mot de passe avec un token valide. + * + * @param tokenValue La valeur du token + * @param newPassword Le nouveau mot de passe + * @return true si le mot de passe a Ă©tĂ© rĂ©initialisĂ©, false sinon + */ + @Transactional + public boolean resetPassword(String tokenValue, String newPassword) { + Optional tokenOpt = tokenRepository.findValidToken(tokenValue); + + if (tokenOpt.isEmpty()) { + logger.warn("Tentative de rĂ©initialisation avec un token invalide"); + return false; + } + + PasswordResetToken token = tokenOpt.get(); + Users user = token.getUser(); + + // Mettre Ă  jour le mot de passe + user.setPassword(newPassword); + usersRepository.persist(user); + + // Marquer le token comme utilisĂ© + token.markAsUsed(); + tokenRepository.persist(token); + + // Invalider tous les autres tokens pour cet utilisateur + tokenRepository.invalidateAllForUser(user); + + logger.info("Mot de passe rĂ©initialisĂ© avec succĂšs pour: " + user.getEmail()); + + return true; + } + + /** + * Nettoie les tokens expirĂ©s (Ă  appeler via un scheduler). + * + * @return Le nombre de tokens supprimĂ©s + */ + @Transactional + public long cleanupExpiredTokens() { + return tokenRepository.deleteExpiredTokens(); + } +} diff --git a/src/main/java/com/lions/dev/service/PresenceService.java b/src/main/java/com/lions/dev/service/PresenceService.java index 1823e44..d5cfb81 100644 --- a/src/main/java/com/lions/dev/service/PresenceService.java +++ b/src/main/java/com/lions/dev/service/PresenceService.java @@ -51,7 +51,7 @@ public class PresenceService { // Broadcast prĂ©sence aux autres utilisateurs broadcastPresenceToAll(userId, true, user.getLastSeen()); - System.out.println("[PRESENCE] Utilisateur " + userId + " marquĂ© online"); + Log.debug("[PRESENCE] Utilisateur " + userId + " marquĂ© online"); } } @@ -70,7 +70,7 @@ public class PresenceService { // Broadcast prĂ©sence aux autres utilisateurs broadcastPresenceToAll(userId, false, user.getLastSeen()); - System.out.println("[PRESENCE] Utilisateur " + userId + " marquĂ© offline"); + Log.debug("[PRESENCE] Utilisateur " + userId + " marquĂ© offline"); } } @@ -87,7 +87,7 @@ public class PresenceService { if (user != null) { user.updatePresence(); usersRepository.persist(user); - System.out.println("[PRESENCE] Heartbeat reçu pour utilisateur " + userId); + Log.debug("[PRESENCE] Heartbeat reçu pour utilisateur " + userId); } } diff --git a/src/main/java/com/lions/dev/service/SocialPostService.java b/src/main/java/com/lions/dev/service/SocialPostService.java index 7688855..704347d 100644 --- a/src/main/java/com/lions/dev/service/SocialPostService.java +++ b/src/main/java/com/lions/dev/service/SocialPostService.java @@ -1,7 +1,6 @@ package com.lions.dev.service; import com.lions.dev.entity.friends.Friendship; -import com.lions.dev.entity.friends.FriendshipStatus; import com.lions.dev.entity.social.SocialPost; import com.lions.dev.entity.users.Users; import com.lions.dev.exception.UserNotFoundException; @@ -14,6 +13,7 @@ import org.eclipse.microprofile.reactive.messaging.Emitter; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import jakarta.transaction.Transactional; +import org.jboss.logging.Logger; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -22,15 +22,15 @@ import java.util.stream.Collectors; /** * Service de gestion des posts sociaux. - * + * * Ce service contient la logique mĂ©tier pour la crĂ©ation, rĂ©cupĂ©ration, * mise Ă  jour et suppression des posts sociaux. - * - * Tous les logs nĂ©cessaires pour la traçabilitĂ© sont intĂ©grĂ©s. */ @ApplicationScoped public class SocialPostService { + private static final Logger logger = Logger.getLogger(SocialPostService.class); + @Inject SocialPostRepository socialPostRepository; @@ -55,7 +55,7 @@ public class SocialPostService { * @return Liste paginĂ©e des posts */ public List getAllPosts(int page, int size) { - System.out.println("[LOG] RĂ©cupĂ©ration de tous les posts (page: " + page + ", size: " + size + ")"); + logger.info("[SocialPostService] RĂ©cupĂ©ration de tous les posts (page: " + page + ", size: " + size + ")"); return socialPostRepository.findAllWithPagination(page, size); } @@ -67,11 +67,11 @@ public class SocialPostService { * @throws UserNotFoundException Si l'utilisateur n'existe pas */ public List getPostsByUserId(UUID userId) { - System.out.println("[LOG] RĂ©cupĂ©ration des posts pour l'utilisateur ID : " + userId); + logger.info("[SocialPostService] RĂ©cupĂ©ration des posts pour l'utilisateur ID : " + userId); Users user = usersRepository.findById(userId); if (user == null) { - System.out.println("[ERROR] Utilisateur non trouvĂ© avec l'ID : " + userId); + logger.error("[SocialPostService] Utilisateur non trouvĂ© avec l'ID : " + userId); throw new UserNotFoundException("Utilisateur non trouvĂ© avec l'ID : " + userId); } @@ -89,11 +89,11 @@ public class SocialPostService { */ @Transactional public SocialPost createPost(String content, UUID userId, String imageUrl) { - System.out.println("[LOG] CrĂ©ation d'un post par l'utilisateur ID : " + userId); + logger.info("[SocialPostService] CrĂ©ation d'un post par l'utilisateur ID : " + userId); Users user = usersRepository.findById(userId); if (user == null) { - System.out.println("[ERROR] Utilisateur non trouvĂ© avec l'ID : " + userId); + logger.error("[SocialPostService] Utilisateur non trouvĂ© avec l'ID : " + userId); throw new UserNotFoundException("Utilisateur non trouvĂ© avec l'ID : " + userId); } @@ -103,7 +103,7 @@ public class SocialPostService { } socialPostRepository.persist(post); - System.out.println("[LOG] Post créé avec succĂšs : " + post.getId()); + logger.info("[SocialPostService] Post créé avec succĂšs : " + post.getId()); // CrĂ©er des notifications pour tous les amis try { @@ -128,9 +128,9 @@ public class SocialPostService { null ); } - System.out.println("[LOG] Notifications créées pour " + friendships.size() + " ami(s)"); + logger.info("[SocialPostService] Notifications créées pour " + friendships.size() + " ami(s)"); } catch (Exception e) { - System.out.println("[ERROR] Erreur lors de la crĂ©ation des notifications : " + e.getMessage()); + logger.error("[SocialPostService] Erreur lors de la crĂ©ation des notifications : " + e.getMessage()); } return post; @@ -143,11 +143,11 @@ public class SocialPostService { * @return Le post trouvĂ© */ public SocialPost getPostById(UUID postId) { - System.out.println("[LOG] RĂ©cupĂ©ration du post ID : " + postId); + logger.info("[SocialPostService] RĂ©cupĂ©ration du post ID : " + postId); SocialPost post = socialPostRepository.findById(postId); if (post == null) { - System.out.println("[ERROR] Post non trouvĂ© avec l'ID : " + postId); + logger.error("[SocialPostService] Post non trouvĂ© avec l'ID : " + postId); throw new IllegalArgumentException("Post non trouvĂ© avec l'ID : " + postId); } @@ -164,11 +164,11 @@ public class SocialPostService { */ @Transactional public SocialPost updatePost(UUID postId, String content, String imageUrl) { - System.out.println("[LOG] Mise Ă  jour du post ID : " + postId); + logger.info("[SocialPostService] Mise Ă  jour du post ID : " + postId); SocialPost post = socialPostRepository.findById(postId); if (post == null) { - System.out.println("[ERROR] Post non trouvĂ© avec l'ID : " + postId); + logger.error("[SocialPostService] Post non trouvĂ© avec l'ID : " + postId); throw new IllegalArgumentException("Post non trouvĂ© avec l'ID : " + postId); } @@ -178,7 +178,7 @@ public class SocialPostService { } socialPostRepository.persist(post); - System.out.println("[LOG] Post mis Ă  jour avec succĂšs"); + logger.info("[SocialPostService] Post mis Ă  jour avec succĂšs"); return post; } @@ -190,16 +190,16 @@ public class SocialPostService { */ @Transactional public boolean deletePost(UUID postId) { - System.out.println("[LOG] Suppression du post ID : " + postId); + logger.info("[SocialPostService] Suppression du post ID : " + postId); SocialPost post = socialPostRepository.findById(postId); if (post == null) { - System.out.println("[ERROR] Post non trouvĂ© avec l'ID : " + postId); + logger.error("[SocialPostService] Post non trouvĂ© avec l'ID : " + postId); return false; } socialPostRepository.delete(post); - System.out.println("[LOG] Post supprimĂ© avec succĂšs"); + logger.info("[SocialPostService] Post supprimĂ© avec succĂšs"); return true; } @@ -210,7 +210,7 @@ public class SocialPostService { * @return Liste des posts correspondant Ă  la recherche */ public List searchPosts(String query) { - System.out.println("[LOG] Recherche de posts avec la requĂȘte : " + query); + logger.info("[SocialPostService] Recherche de posts avec la requĂȘte : " + query); return socialPostRepository.searchByContent(query); } @@ -223,17 +223,35 @@ public class SocialPostService { */ @Transactional public SocialPost likePost(UUID postId, UUID userId) { - System.out.println("[LOG] Like du post ID : " + postId + " par utilisateur : " + userId); + logger.info("[SocialPostService] Like du post ID : " + postId + " par utilisateur : " + userId); SocialPost post = socialPostRepository.findById(postId); if (post == null) { - System.out.println("[ERROR] Post non trouvĂ© avec l'ID : " + postId); + logger.error("[SocialPostService] Post non trouvĂ© avec l'ID : " + postId); throw new IllegalArgumentException("Post non trouvĂ© avec l'ID : " + postId); } post.incrementLikes(); socialPostRepository.persist(post); + // Notification pour l'auteur du post (sauf auto-like) + try { + Users author = post.getUser(); + if (author != null && !author.getId().equals(userId)) { + Users liker = usersRepository.findById(userId); + String likerName = liker != null ? liker.getFirstName() + " " + liker.getLastName() : "Quelqu'un"; + notificationService.createNotification( + "Nouveau like", + likerName + " a aimĂ© votre post", + "post", + author.getId(), + null + ); + } + } catch (Exception e) { + logger.error("[SocialPostService] Erreur crĂ©ation notification like : " + e.getMessage()); + } + // TEMPS RÉEL: Publier dans Kafka (v2.0) try { Map reactionData = new HashMap<>(); @@ -252,9 +270,9 @@ public class SocialPostService { ); reactionEmitter.send(event); - System.out.println("[LOG] RĂ©action like publiĂ©e dans Kafka pour post: " + postId); + logger.info("[SocialPostService] RĂ©action like publiĂ©e dans Kafka pour post: " + postId); } catch (Exception e) { - System.out.println("[ERROR] Erreur publication Kafka: " + e.getMessage()); + logger.error("[SocialPostService] Erreur publication Kafka: " + e.getMessage()); // Ne pas bloquer le like si Kafka Ă©choue } @@ -271,17 +289,38 @@ public class SocialPostService { */ @Transactional public SocialPost addComment(UUID postId, UUID userId, String commentContent) { - System.out.println("[LOG] Ajout de commentaire au post ID : " + postId + " par utilisateur : " + userId); + logger.info("[SocialPostService] Ajout de commentaire au post ID : " + postId + " par utilisateur : " + userId); SocialPost post = socialPostRepository.findById(postId); if (post == null) { - System.out.println("[ERROR] Post non trouvĂ© avec l'ID : " + postId); + logger.error("[SocialPostService] Post non trouvĂ© avec l'ID : " + postId); throw new IllegalArgumentException("Post non trouvĂ© avec l'ID : " + postId); } post.incrementComments(); socialPostRepository.persist(post); + // Notification pour l'auteur du post (sauf auto-commentaire) + try { + Users author = post.getUser(); + if (author != null && !author.getId().equals(userId)) { + Users commenter = usersRepository.findById(userId); + String commenterName = commenter != null ? commenter.getFirstName() + " " + commenter.getLastName() : "Quelqu'un"; + String preview = commentContent != null && commentContent.length() > 60 + ? commentContent.substring(0, 60) + "..." + : (commentContent != null ? commentContent : ""); + notificationService.createNotification( + "Nouveau commentaire", + commenterName + " a commentĂ© votre post : " + preview, + "post", + author.getId(), + null + ); + } + } catch (Exception e) { + logger.error("[SocialPostService] Erreur crĂ©ation notification commentaire : " + e.getMessage()); + } + // TEMPS RÉEL: Publier dans Kafka (v2.0) try { Users commenter = usersRepository.findById(userId); @@ -304,9 +343,9 @@ public class SocialPostService { ); reactionEmitter.send(event); - System.out.println("[LOG] RĂ©action comment publiĂ©e dans Kafka pour post: " + postId); + logger.info("[SocialPostService] RĂ©action comment publiĂ©e dans Kafka pour post: " + postId); } catch (Exception e) { - System.out.println("[ERROR] Erreur publication Kafka: " + e.getMessage()); + logger.error("[SocialPostService] Erreur publication Kafka: " + e.getMessage()); // Ne pas bloquer le commentaire si Kafka Ă©choue } @@ -322,11 +361,11 @@ public class SocialPostService { */ @Transactional public SocialPost sharePost(UUID postId, UUID userId) { - System.out.println("[LOG] Partage du post ID : " + postId + " par utilisateur : " + userId); + logger.info("[SocialPostService] Partage du post ID : " + postId + " par utilisateur : " + userId); SocialPost post = socialPostRepository.findById(postId); if (post == null) { - System.out.println("[ERROR] Post non trouvĂ© avec l'ID : " + postId); + logger.error("[SocialPostService] Post non trouvĂ© avec l'ID : " + postId); throw new IllegalArgumentException("Post non trouvĂ© avec l'ID : " + postId); } @@ -348,9 +387,9 @@ public class SocialPostService { ); reactionEmitter.send(event); - System.out.println("[LOG] RĂ©action share publiĂ©e dans Kafka pour post: " + postId); + logger.info("[SocialPostService] RĂ©action share publiĂ©e dans Kafka pour post: " + postId); } catch (Exception e) { - System.out.println("[ERROR] Erreur publication Kafka: " + e.getMessage()); + logger.error("[SocialPostService] Erreur publication Kafka: " + e.getMessage()); // Ne pas bloquer le partage si Kafka Ă©choue } @@ -367,11 +406,11 @@ public class SocialPostService { * @throws UserNotFoundException Si l'utilisateur n'existe pas */ public List getPostsByFriends(UUID userId, int page, int size) { - System.out.println("[LOG] RĂ©cupĂ©ration des posts des amis pour l'utilisateur ID : " + userId); + logger.info("[SocialPostService] RĂ©cupĂ©ration des posts des amis pour l'utilisateur ID : " + userId); Users user = usersRepository.findById(userId); if (user == null) { - System.out.println("[ERROR] Utilisateur non trouvĂ© avec l'ID : " + userId); + logger.error("[SocialPostService] Utilisateur non trouvĂ© avec l'ID : " + userId); throw new UserNotFoundException("Utilisateur non trouvĂ© avec l'ID : " + userId); } @@ -389,7 +428,7 @@ public class SocialPostService { .distinct() .collect(Collectors.toList()); - System.out.println("[LOG] " + friendIds.size() + " ami(s) trouvĂ©(s) pour l'utilisateur ID : " + userId); + logger.info("[SocialPostService] " + friendIds.size() + " ami(s) trouvĂ©(s) pour l'utilisateur ID : " + userId); // RĂ©cupĂ©rer les posts de l'utilisateur et de ses amis return socialPostRepository.findPostsByFriends(userId, friendIds, page, size); diff --git a/src/main/java/com/lions/dev/service/StoryService.java b/src/main/java/com/lions/dev/service/StoryService.java index d70fdc6..0fcb296 100644 --- a/src/main/java/com/lions/dev/service/StoryService.java +++ b/src/main/java/com/lions/dev/service/StoryService.java @@ -1,15 +1,24 @@ package com.lions.dev.service; +import com.lions.dev.dto.events.NotificationEvent; +import com.lions.dev.entity.friends.Friendship; +import com.lions.dev.entity.friends.FriendshipStatus; import com.lions.dev.entity.story.MediaType; import com.lions.dev.entity.story.Story; import com.lions.dev.entity.users.Users; import com.lions.dev.exception.UserNotFoundException; +import com.lions.dev.repository.FriendshipRepository; import com.lions.dev.repository.StoryRepository; import com.lions.dev.repository.UsersRepository; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import jakarta.transaction.Transactional; +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Emitter; +import org.jboss.logging.Logger; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.UUID; /** @@ -17,18 +26,25 @@ import java.util.UUID; * * Ce service contient la logique mĂ©tier pour la crĂ©ation, rĂ©cupĂ©ration, * mise Ă  jour et suppression des stories. - * - * Tous les logs nĂ©cessaires pour la traçabilitĂ© sont intĂ©grĂ©s. */ @ApplicationScoped public class StoryService { + private static final Logger logger = Logger.getLogger(StoryService.class); + @Inject StoryRepository storyRepository; @Inject UsersRepository usersRepository; + @Inject + FriendshipRepository friendshipRepository; + + @Inject + @Channel("notifications") + Emitter notificationEmitter; + /** * RĂ©cupĂšre toutes les stories actives (non expirĂ©es). * @@ -46,7 +62,7 @@ public class StoryService { * @return Liste paginĂ©e des stories actives */ public List getAllActiveStories(int page, int size) { - System.out.println("[LOG] RĂ©cupĂ©ration de toutes les stories actives (page: " + page + ", size: " + size + ")"); + logger.info("[StoryService] RĂ©cupĂ©ration de toutes les stories actives (page: " + page + ", size: " + size + ")"); return storyRepository.findAllActive(page, size); } @@ -71,11 +87,11 @@ public class StoryService { * @throws UserNotFoundException Si l'utilisateur n'existe pas */ public List getActiveStoriesByUserId(UUID userId, int page, int size) { - System.out.println("[LOG] RĂ©cupĂ©ration des stories actives pour l'utilisateur ID : " + userId + " (page: " + page + ", size: " + size + ")"); + logger.info("[StoryService] RĂ©cupĂ©ration des stories actives pour l'utilisateur ID : " + userId + " (page: " + page + ", size: " + size + ")"); Users user = usersRepository.findById(userId); if (user == null) { - System.out.println("[ERROR] Utilisateur non trouvĂ© avec l'ID : " + userId); + logger.error("[StoryService] Utilisateur non trouvĂ© avec l'ID : " + userId); throw new UserNotFoundException("Utilisateur non trouvĂ© avec l'ID : " + userId); } @@ -96,23 +112,23 @@ public class StoryService { */ @Transactional public Story createStory(UUID userId, MediaType mediaType, String mediaUrl, String thumbnailUrl, Integer durationSeconds) { - System.out.println("[LOG] CrĂ©ation d'une story par l'utilisateur ID : " + userId); + logger.info("[StoryService] CrĂ©ation d'une story par l'utilisateur ID : " + userId); // Validation de l'utilisateur Users user = usersRepository.findById(userId); if (user == null) { - System.out.println("[ERROR] Utilisateur non trouvĂ© avec l'ID : " + userId); + logger.error("[StoryService] Utilisateur non trouvĂ© avec l'ID : " + userId); throw new UserNotFoundException("Utilisateur non trouvĂ© avec l'ID : " + userId); } // Validation des paramĂštres if (mediaUrl == null || mediaUrl.trim().isEmpty()) { - System.out.println("[ERROR] L'URL du mĂ©dia est obligatoire"); + logger.error("[StoryService] L'URL du mĂ©dia est obligatoire"); throw new IllegalArgumentException("L'URL du mĂ©dia est obligatoire"); } if (mediaType == null) { - System.out.println("[ERROR] Le type de mĂ©dia est obligatoire"); + logger.error("[StoryService] Le type de mĂ©dia est obligatoire"); throw new IllegalArgumentException("Le type de mĂ©dia est obligatoire"); } @@ -128,10 +144,52 @@ public class StoryService { } storyRepository.persist(story); - System.out.println("[LOG] Story créée avec succĂšs : " + story.getId()); + logger.info("[StoryService] Story créée avec succĂšs : " + story.getId()); + + // Notifier les amis en temps rĂ©el via Kafka + notifyFriendsOfNewStory(user, story); + return story; } + /** + * Notifie les amis d'un utilisateur qu'une nouvelle story a Ă©tĂ© publiĂ©e. + */ + private void notifyFriendsOfNewStory(Users creator, Story story) { + try { + List friendships = friendshipRepository.findAcceptedFriendships(creator.getId()); + String creatorName = creator.getFirstName() + " " + creator.getLastName(); + + for (Friendship friendship : friendships) { + Users friend = friendship.getUser().getId().equals(creator.getId()) + ? friendship.getFriend() + : friendship.getUser(); + + if (friend == null || friend.getId() == null) continue; + + Map data = new HashMap<>(); + data.put("storyId", story.getId().toString()); + data.put("creatorId", creator.getId().toString()); + data.put("creatorName", creatorName); + data.put("creatorProfileImage", creator.getProfileImageUrl()); + data.put("mediaType", story.getMediaType().toString()); + data.put("thumbnailUrl", story.getThumbnailUrl()); + + NotificationEvent event = new NotificationEvent( + friend.getId().toString(), + "story_created", + data + ); + + notificationEmitter.send(event); + } + + logger.debug("[StoryService] Notification story envoyĂ©e Ă  " + friendships.size() + " amis"); + } catch (Exception e) { + logger.warn("[StoryService] Échec notification Kafka story (non bloquant) : " + e.getMessage()); + } + } + /** * RĂ©cupĂšre une story par son ID. * @@ -140,11 +198,11 @@ public class StoryService { * @throws IllegalArgumentException Si la story n'existe pas */ public Story getStoryById(UUID storyId) { - System.out.println("[LOG] RĂ©cupĂ©ration de la story ID : " + storyId); + logger.info("[StoryService] RĂ©cupĂ©ration de la story ID : " + storyId); Story story = storyRepository.findById(storyId); if (story == null) { - System.out.println("[ERROR] Story non trouvĂ©e avec l'ID : " + storyId); + logger.error("[StoryService] Story non trouvĂ©e avec l'ID : " + storyId); throw new IllegalArgumentException("Story non trouvĂ©e avec l'ID : " + storyId); } @@ -162,19 +220,19 @@ public class StoryService { */ @Transactional public Story markStoryAsViewed(UUID storyId, UUID viewerId) { - System.out.println("[LOG] Marquage de la story ID : " + storyId + " comme vue par l'utilisateur ID : " + viewerId); + logger.info("[StoryService] Marquage de la story ID : " + storyId + " comme vue par l'utilisateur ID : " + viewerId); // Validation de l'utilisateur Users viewer = usersRepository.findById(viewerId); if (viewer == null) { - System.out.println("[ERROR] Utilisateur non trouvĂ© avec l'ID : " + viewerId); + logger.error("[StoryService] Utilisateur non trouvĂ© avec l'ID : " + viewerId); throw new UserNotFoundException("Utilisateur non trouvĂ© avec l'ID : " + viewerId); } // Validation de la story Story story = storyRepository.findById(storyId); if (story == null) { - System.out.println("[ERROR] Story non trouvĂ©e avec l'ID : " + storyId); + logger.error("[StoryService] Story non trouvĂ©e avec l'ID : " + storyId); throw new IllegalArgumentException("Story non trouvĂ©e avec l'ID : " + storyId); } @@ -182,14 +240,47 @@ public class StoryService { boolean isNewView = story.markAsViewed(viewerId); if (isNewView) { storyRepository.persist(story); - System.out.println("[LOG] Story marquĂ©e comme vue (nouvelle vue)"); + logger.info("[StoryService] Story marquĂ©e comme vue (nouvelle vue)"); + + // Notifier le crĂ©ateur en temps rĂ©el + notifyCreatorOfStoryView(story, viewer); } else { - System.out.println("[LOG] Story dĂ©jĂ  vue par cet utilisateur"); + logger.info("[StoryService] Story dĂ©jĂ  vue par cet utilisateur"); } return story; } + /** + * Notifie le crĂ©ateur d'une story qu'elle a Ă©tĂ© vue. + */ + private void notifyCreatorOfStoryView(Story story, Users viewer) { + try { + Users creator = story.getUser(); + if (creator == null || creator.getId().equals(viewer.getId())) return; + + String viewerName = viewer.getFirstName() + " " + viewer.getLastName(); + + Map data = new HashMap<>(); + data.put("storyId", story.getId().toString()); + data.put("viewerId", viewer.getId().toString()); + data.put("viewerName", viewerName); + data.put("viewerProfileImage", viewer.getProfileImageUrl()); + data.put("viewsCount", story.getViewsCount()); + + NotificationEvent event = new NotificationEvent( + creator.getId().toString(), + "story_viewed", + data + ); + + notificationEmitter.send(event); + logger.debug("[StoryService] Notification vue story envoyĂ©e au crĂ©ateur : " + creator.getId()); + } catch (Exception e) { + logger.warn("[StoryService] Échec notification vue story (non bloquant) : " + e.getMessage()); + } + } + /** * Supprime une story. * @@ -198,16 +289,16 @@ public class StoryService { */ @Transactional public boolean deleteStory(UUID storyId) { - System.out.println("[LOG] Suppression de la story ID : " + storyId); + logger.info("[StoryService] Suppression de la story ID : " + storyId); Story story = storyRepository.findById(storyId); if (story == null) { - System.out.println("[ERROR] Story non trouvĂ©e avec l'ID : " + storyId); + logger.error("[StoryService] Story non trouvĂ©e avec l'ID : " + storyId); return false; } storyRepository.delete(story); - System.out.println("[LOG] Story supprimĂ©e avec succĂšs"); + logger.info("[StoryService] Story supprimĂ©e avec succĂšs"); return true; } @@ -219,7 +310,7 @@ public class StoryService { */ @Transactional public int deactivateExpiredStories() { - System.out.println("[LOG] DĂ©sactivation des stories expirĂ©es"); + logger.info("[StoryService] DĂ©sactivation des stories expirĂ©es"); return storyRepository.deactivateExpiredStories(); } @@ -230,7 +321,7 @@ public class StoryService { * @return Liste des stories les plus vues */ public List getMostViewedStories(int limit) { - System.out.println("[LOG] RĂ©cupĂ©ration des " + limit + " stories les plus vues"); + logger.info("[StoryService] RĂ©cupĂ©ration des " + limit + " stories les plus vues"); return storyRepository.findMostViewedStories(limit); } } diff --git a/src/main/java/com/lions/dev/service/UsersService.java b/src/main/java/com/lions/dev/service/UsersService.java index af36eb4..49e5551 100644 --- a/src/main/java/com/lions/dev/service/UsersService.java +++ b/src/main/java/com/lions/dev/service/UsersService.java @@ -7,6 +7,7 @@ import com.lions.dev.repository.UsersRepository; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import jakarta.transaction.Transactional; +import org.jboss.logging.Logger; import java.util.List; import java.util.Optional; @@ -19,6 +20,8 @@ import java.util.UUID; @ApplicationScoped public class UsersService { + private static final Logger logger = Logger.getLogger(UsersService.class); + @Inject UsersRepository usersRepository; @@ -66,7 +69,7 @@ public class UsersService { ); usersRepository.persist(user); - System.out.println("[LOG] Utilisateur créé : " + user.getEmail()); + logger.info("Utilisateur créé : " + user.getEmail()); return user; } @@ -82,7 +85,7 @@ public class UsersService { public Users updateUser(UUID id, UserCreateRequestDTO userCreateRequestDTO) { Users existingUser = usersRepository.findById(id); if (existingUser == null) { - System.out.println("[ERROR] Utilisateur non trouvĂ© avec l'ID : " + id); + logger.error("Utilisateur non trouvĂ© avec l'ID : " + id); throw new UserNotFoundException("Utilisateur non trouvĂ© avec l'ID : " + id); } @@ -117,7 +120,7 @@ public class UsersService { } usersRepository.persist(existingUser); - System.out.println("[LOG] Utilisateur mis Ă  jour avec succĂšs : " + existingUser.getEmail()); + logger.info("Utilisateur mis Ă  jour avec succĂšs : " + existingUser.getEmail()); return existingUser; } /** @@ -132,7 +135,7 @@ public class UsersService { public Users updateUserProfileImage(UUID id, String profileImageUrl) { Users existingUser = usersRepository.findById(id); if (existingUser == null) { - System.out.println("[ERROR] Utilisateur non trouvĂ© avec l'ID : " + id); + logger.error("Utilisateur non trouvĂ© avec l'ID : " + id); throw new UserNotFoundException("Utilisateur non trouvĂ© avec l'ID : " + id); } @@ -140,7 +143,7 @@ public class UsersService { existingUser.setProfileImageUrl(profileImageUrl); usersRepository.persist(existingUser); - System.out.println("[LOG] L'image de profile de l\'Utilisateur mis Ă  jour avec succĂšs : " + existingUser.getEmail()); + logger.info("Image de profil de l'utilisateur mise Ă  jour avec succĂšs : " + existingUser.getEmail()); return existingUser; } @@ -166,10 +169,10 @@ public class UsersService { public Users authenticateUser(String email, String password) { Optional userOptional = usersRepository.findByEmail(email); if (userOptional.isEmpty() || !userOptional.get().verifyPassword(password)) { // v2.0 - System.out.println("[ERROR] Échec de l'authentification pour l'email : " + email); + logger.warn("Échec de l'authentification pour l'email : " + email); throw new UserNotFoundException("Utilisateur ou mot de passe incorrect."); } - System.out.println("[LOG] Utilisateur authentifiĂ© : " + email); + logger.info("Utilisateur authentifiĂ© : " + email); return userOptional.get(); } @@ -192,10 +195,10 @@ public class UsersService { public Users getUserById(UUID id) { Users user = usersRepository.findById(id); if (user == null) { - System.out.println("[ERROR] Utilisateur non trouvĂ© avec l'ID : " + id); + logger.error("Utilisateur non trouvĂ© avec l'ID : " + id); throw new UserNotFoundException("Utilisateur non trouvĂ© avec l'ID : " + id); } - System.out.println("[LOG] Utilisateur trouvĂ© avec l'ID : " + id); + logger.debug("Utilisateur trouvĂ© avec l'ID : " + id); return user; } @@ -210,13 +213,13 @@ public class UsersService { public void resetPassword(UUID id, String newPassword) { Users user = usersRepository.findById(id); if (user == null) { - System.out.println("[ERROR] Utilisateur non trouvĂ© avec l'ID : " + id); + logger.error("Utilisateur non trouvĂ© avec l'ID : " + id); throw new UserNotFoundException("Utilisateur non trouvĂ©."); } user.setPassword(newPassword); // v2.0 - Hachage automatique usersRepository.persist(user); - System.out.println("[LOG] Mot de passe rĂ©initialisĂ© pour l'utilisateur : " + user.getEmail()); + logger.info("Mot de passe rĂ©initialisĂ© pour l'utilisateur : " + user.getEmail()); } /** @@ -228,9 +231,9 @@ public class UsersService { public boolean deleteUser(UUID id) { boolean deleted = usersRepository.deleteById(id); if (deleted) { - System.out.println("[LOG] Utilisateur supprimĂ© avec succĂšs : " + id); + logger.info("Utilisateur supprimĂ© avec succĂšs : " + id); } else { - System.out.println("[ERROR] Échec de la suppression de l'utilisateur avec l'ID : " + id); + logger.error("Échec de la suppression de l'utilisateur avec l'ID : " + id); } return deleted; } @@ -245,10 +248,10 @@ public class UsersService { public Users getUserByEmail(String email) { Optional userOptional = usersRepository.findByEmail(email); if (userOptional.isEmpty()) { - System.out.println("[ERROR] Utilisateur non trouvĂ© avec l'email : " + email); + logger.error("Utilisateur non trouvĂ© avec l'email : " + email); throw new UserNotFoundException("Utilisateur non trouvĂ© avec l'email : " + email); } - System.out.println("[LOG] Utilisateur trouvĂ© avec l'email : " + email); + logger.debug("Utilisateur trouvĂ© avec l'email : " + email); return userOptional.get(); } @@ -280,7 +283,28 @@ public class UsersService { } user.setRole(newRole); usersRepository.persist(user); - System.out.println("[LOG] RĂŽle attribuĂ© Ă  " + user.getEmail() + " : " + newRole); + logger.info("RĂŽle attribuĂ© Ă  " + user.getEmail() + " : " + newRole); + return user; + } + + /** + * Force ou suspend le compte d'un utilisateur (rĂ©servĂ© au super administrateur). + * UtilisĂ© pour les managers : isActive = false = suspendu (abonnement expirĂ© / sanction). + * + * @param userId L'ID de l'utilisateur Ă  modifier. + * @param active true = forcer l'activation, false = suspendre. + * @return L'utilisateur mis Ă  jour. + * @throws UserNotFoundException Si l'utilisateur n'existe pas. + */ + @Transactional + public Users setUserActive(UUID userId, boolean active) { + Users user = usersRepository.findById(userId); + if (user == null) { + throw new UserNotFoundException("Utilisateur non trouvĂ© avec l'ID : " + userId); + } + user.setActive(active); + usersRepository.persist(user); + logger.info("Statut actif modifiĂ© pour " + user.getEmail() + " : " + active); return user; } } diff --git a/src/main/java/com/lions/dev/service/WavePaymentService.java b/src/main/java/com/lions/dev/service/WavePaymentService.java index 67807b7..9c7b4f3 100644 --- a/src/main/java/com/lions/dev/service/WavePaymentService.java +++ b/src/main/java/com/lions/dev/service/WavePaymentService.java @@ -48,6 +48,8 @@ public class WavePaymentService { EstablishmentPaymentRepository paymentRepository; @Inject UsersRepository usersRepository; + @Inject + EmailService emailService; @ConfigProperty(name = "wave.api.url", defaultValue = "https://api.wave.com") String waveApiUrl; @@ -184,8 +186,32 @@ public class WavePaymentService { manager.setActive(true); usersRepository.persist(manager); LOG.info("Webhook Wave: Ă©tablissement et manager activĂ©s pour " + establishmentId); + try { + emailService.sendPaymentConfirmationEmail( + manager.getEmail(), + manager.getFirstName(), + establishment.getName(), + sub.getAmountXof() != null ? sub.getAmountXof() : 0, + sub.getPlan() != null ? sub.getPlan() : "MONTHLY" + ); + } catch (Exception e) { + LOG.warn("Envoi email confirmation paiement Ă©chouĂ©: " + e.getMessage()); + } } } + } else if ("payment.refunded".equals(eventType)) { + sub.setStatus(EstablishmentSubscription.STATUS_CANCELLED); + subscriptionRepository.persist(sub); + if (establishment != null) { + establishment.setIsActive(false); + establishmentRepository.persist(establishment); + Users manager = establishment.getManager(); + if (manager != null) { + manager.setActive(false); + usersRepository.persist(manager); + } + LOG.info("Webhook Wave: remboursement traitĂ©, Ă©tablissement dĂ©sactivĂ© pour " + establishmentId); + } } else if ("payment.cancelled".equals(eventType) || "payment.expired".equals(eventType) || "payment.failed".equals(eventType)) { sub.setStatus(EstablishmentSubscription.STATUS_CANCELLED); @@ -199,6 +225,17 @@ public class WavePaymentService { manager.setActive(false); usersRepository.persist(manager); LOG.info("Webhook Wave: Ă©tablissement et manager suspendus pour " + establishmentId); + if ("payment.failed".equals(eventType)) { + try { + emailService.sendPaymentFailureEmail( + manager.getEmail(), + manager.getFirstName(), + establishment.getName() + ); + } catch (Exception e) { + LOG.warn("Envoi email Ă©chec paiement Ă©chouĂ©: " + e.getMessage()); + } + } } } } @@ -207,8 +244,11 @@ public class WavePaymentService { paymentRepository.findByWaveSessionId(sessionId).ifPresent(payment -> { if ("payment.completed".equals(eventType)) { payment.setStatus(EstablishmentPayment.STATUS_COMPLETED); - } else if ("payment.cancelled".equals(eventType) || "payment.expired".equals(eventType)) { + } else if ("payment.cancelled".equals(eventType) || "payment.expired".equals(eventType) + || "payment.refunded".equals(eventType)) { payment.setStatus(EstablishmentPayment.STATUS_CANCELLED); + } else if ("payment.failed".equals(eventType)) { + payment.setStatus(EstablishmentPayment.STATUS_FAILED); } paymentRepository.persist(payment); }); diff --git a/src/main/java/com/lions/dev/util/InputConverter.java b/src/main/java/com/lions/dev/util/InputConverter.java index 89cb2a8..d970fa2 100644 --- a/src/main/java/com/lions/dev/util/InputConverter.java +++ b/src/main/java/com/lions/dev/util/InputConverter.java @@ -16,7 +16,6 @@ public class InputConverter { try { return Integer.parseInt(value); } catch (NumberFormatException e) { - System.out.println("[ERROR] Impossible de convertir la valeur en entier : " + value); return null; } } @@ -33,7 +32,6 @@ public class InputConverter { } else if ("false".equalsIgnoreCase(value)) { return false; } - System.out.println("[ERROR] Valeur non valide pour un boolĂ©en : " + value); return false; } } diff --git a/src/main/java/com/lions/dev/util/SecureStorage.java b/src/main/java/com/lions/dev/util/SecureStorage.java index ca718ff..66896e5 100644 --- a/src/main/java/com/lions/dev/util/SecureStorage.java +++ b/src/main/java/com/lions/dev/util/SecureStorage.java @@ -42,7 +42,6 @@ public class SecureStorage { * @param data Les donnĂ©es Ă  effacer. */ public void clearData(String data) { - System.out.println("[LOG] Les donnĂ©es ont Ă©tĂ© effacĂ©es de maniĂšre sĂ©curisĂ©e."); // En Java, il n'y a pas de suppression directe des donnĂ©es en mĂ©moire. On peut ici // gĂ©rer la suppression logique ou l'effacement de donnĂ©es dans un fichier sĂ©curisĂ©. } diff --git a/src/main/java/com/lions/dev/util/UserRole.java b/src/main/java/com/lions/dev/util/UserRole.java deleted file mode 100644 index 3eabaef..0000000 --- a/src/main/java/com/lions/dev/util/UserRole.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.lions.dev.util; - -/** - * RĂŽles utilisateur de l'application AfterWork. - * HiĂ©rarchie : SUPER_ADMIN > ADMIN > MANAGER > USER. - */ -public final class UserRole { - - private UserRole() {} - - /** Utilisateur standard (participation aux Ă©vĂ©nements, profil, amis). */ - public static final String USER = "USER"; - - /** Responsable d'Ă©tablissement (gestion de son Ă©tablissement). */ - public static final String MANAGER = "MANAGER"; - - /** Administrateur (gestion des Ă©tablissements, modĂ©ration). */ - public static final String ADMIN = "ADMIN"; - - /** - * Super administrateur : tous les droits (gestion des utilisateurs, attribution des rĂŽles, - * gestion des Ă©tablissements, accĂšs aux paiements Wave, etc.). - */ - public static final String SUPER_ADMIN = "SUPER_ADMIN"; - - /** - * VĂ©rifie si le rĂŽle a les droits super admin (ou est super admin). - */ - public static boolean isSuperAdmin(String role) { - return SUPER_ADMIN.equals(role); - } - - /** - * VĂ©rifie si le rĂŽle peut gĂ©rer les utilisateurs (attribution de rĂŽles, etc.). - */ - public static boolean canManageUsers(String role) { - return SUPER_ADMIN.equals(role) || ADMIN.equals(role); - } - - /** - * VĂ©rifie si le rĂŽle peut gĂ©rer les Ă©tablissements (vĂ©rification, modĂ©ration). - */ - public static boolean canManageEstablishments(String role) { - return SUPER_ADMIN.equals(role) || ADMIN.equals(role); - } -} diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties index eb07216..77252fd 100644 --- a/src/main/resources/application-dev.properties +++ b/src/main/resources/application-dev.properties @@ -34,9 +34,14 @@ quarkus.hibernate-orm.schema-generation.scripts.action=drop-and-create # ==================================================================== # Kafka (dĂ©veloppement local) # ==================================================================== -# En dev, dĂ©faut localhost:9092. DĂ©finir KAFKA_BOOTSTRAP_SERVERS si Kafka tourne ailleurs. +# En dev, mĂȘme connecteur que la base (smallrye-kafka). DĂ©marrer Kafka en local +# (ex. docker-compose) ou dĂ©finir KAFKA_BOOTSTRAP_SERVERS si Kafka est ailleurs. +afterwork.kafka.enabled=${KAFKA_ENABLED:true} kafka.bootstrap.servers=${KAFKA_BOOTSTRAP_SERVERS:localhost:9092} +# SmallRye Reactive Messaging - Utiliser smallrye-kafka (pas d'extension in-memory en runtime). +# Les canaux sont dĂ©finis dans application.properties. + # ==================================================================== # Logging # ==================================================================== diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 9fc713d..5cacfc3 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -33,6 +33,26 @@ afterwork.super-admin.api-key=${SUPER_ADMIN_API_KEY:} # ==================================================================== wave.api.url=${WAVE_API_URL:https://api.wave.com} wave.api.key=${WAVE_API_KEY:} +# Secret pour vĂ©rifier la signature des webhooks (X-Wave-Signature). Si vide, la vĂ©rification est dĂ©sactivĂ©e. +wave.webhook.secret=${WAVE_WEBHOOK_SECRET:} + +# ==================================================================== +# Configuration Email (Quarkus Mailer) +# ==================================================================== +# En production, configurer via variables d'environnement +quarkus.mailer.from=${MAILER_FROM:AfterWork } +quarkus.mailer.host=${MAILER_HOST:smtp.gmail.com} +quarkus.mailer.port=${MAILER_PORT:587} +quarkus.mailer.start-tls=REQUIRED +quarkus.mailer.username=${MAILER_USERNAME:} +quarkus.mailer.password=${MAILER_PASSWORD:} +# Mode mock pour les tests (pas d'envoi rĂ©el) +quarkus.mailer.mock=${MAILER_MOCK:true} + +# Configuration application pour les emails +afterwork.app.name=AfterWork +afterwork.app.url=${APP_URL:https://afterwork.lions.dev} +afterwork.email.from=${MAILER_FROM:noreply@lions.dev} # ==================================================================== # HTTP (commun Ă  tous les environnements) @@ -60,7 +80,8 @@ quarkus.hibernate-orm.log.sql=false # Upload de fichiers (commun Ă  tous les environnements) # ==================================================================== quarkus.http.body.uploads-directory=/tmp/uploads -quarkus.http.limits.max-body-size=10M +# Limite augmentĂ©e pour Ă©viter "Broken pipe" sur gros fichiers (ex. photos > 10M) +quarkus.http.limits.max-body-size=25M # ==================================================================== # WebSockets Next (commun Ă  tous les environnements) diff --git a/src/main/resources/db/migration/V16__Create_Password_Reset_Tokens_Table.sql b/src/main/resources/db/migration/V16__Create_Password_Reset_Tokens_Table.sql new file mode 100644 index 0000000..6e45f2e --- /dev/null +++ b/src/main/resources/db/migration/V16__Create_Password_Reset_Tokens_Table.sql @@ -0,0 +1,27 @@ +-- V16: CrĂ©ation de la table password_reset_tokens pour la rĂ©initialisation de mot de passe +-- AfterWork Platform - Lions Dev + +-- Table des tokens de rĂ©initialisation de mot de passe +CREATE TABLE IF NOT EXISTS password_reset_tokens ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + token VARCHAR(64) NOT NULL UNIQUE, + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + expires_at TIMESTAMP NOT NULL, + used BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- Index pour la recherche rapide par token +CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_token ON password_reset_tokens(token); + +-- Index pour la recherche par utilisateur +CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_user_id ON password_reset_tokens(user_id); + +-- Index pour le nettoyage des tokens expirĂ©s +CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_expires_at ON password_reset_tokens(expires_at); + +COMMENT ON TABLE password_reset_tokens IS 'Tokens de rĂ©initialisation de mot de passe (expire aprĂšs 1h)'; +COMMENT ON COLUMN password_reset_tokens.token IS 'Token unique de 64 caractĂšres'; +COMMENT ON COLUMN password_reset_tokens.user_id IS 'RĂ©fĂ©rence vers l''utilisateur'; +COMMENT ON COLUMN password_reset_tokens.expires_at IS 'Date d''expiration du token (1h aprĂšs crĂ©ation)'; +COMMENT ON COLUMN password_reset_tokens.used IS 'Indique si le token a Ă©tĂ© utilisĂ©'; diff --git a/src/main/resources/db/migration/V17__Create_Friendships_Table.sql b/src/main/resources/db/migration/V17__Create_Friendships_Table.sql new file mode 100644 index 0000000..170b610 --- /dev/null +++ b/src/main/resources/db/migration/V17__Create_Friendships_Table.sql @@ -0,0 +1,42 @@ +-- Migration V17: CrĂ©ation de la table friendships (demandes d'amitiĂ©) +-- Description: Table des relations d'amitiĂ© entre utilisateurs (PENDING, ACCEPTED, REJECTED) + +CREATE TABLE IF NOT EXISTS friendships ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + user_id UUID NOT NULL, + friend_id UUID NOT NULL, + status VARCHAR(20) NOT NULL, + + CONSTRAINT fk_friendships_user + FOREIGN KEY (user_id) + REFERENCES users(id) + ON DELETE CASCADE, + CONSTRAINT fk_friendships_friend + FOREIGN KEY (friend_id) + REFERENCES users(id) + ON DELETE CASCADE, + CONSTRAINT chk_friendships_user_friend_diff + CHECK (user_id != friend_id), + CONSTRAINT uq_friendships_user_friend + UNIQUE (user_id, friend_id) +); + +CREATE INDEX IF NOT EXISTS idx_friendships_user_id ON friendships(user_id); +CREATE INDEX IF NOT EXISTS idx_friendships_friend_id ON friendships(friend_id); +CREATE INDEX IF NOT EXISTS idx_friendships_status ON friendships(status); + +-- Trigger pour mettre Ă  jour updated_at (alignĂ© avec BaseEntity, nomenclature comme V8/V10/V11/V15) +CREATE OR REPLACE FUNCTION update_friendships_updated_at() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER trigger_update_friendships_updated_at + BEFORE UPDATE ON friendships + FOR EACH ROW + EXECUTE FUNCTION update_friendships_updated_at(); diff --git a/src/test/java/com/lions/dev/config/ScheduledJobsTest.java b/src/test/java/com/lions/dev/config/ScheduledJobsTest.java new file mode 100644 index 0000000..01eaaf0 --- /dev/null +++ b/src/test/java/com/lions/dev/config/ScheduledJobsTest.java @@ -0,0 +1,103 @@ +package com.lions.dev.config; + +import com.lions.dev.repository.EstablishmentRepository; +import com.lions.dev.repository.EstablishmentSubscriptionRepository; +import com.lions.dev.repository.EventsRepository; +import com.lions.dev.repository.PasswordResetTokenRepository; +import com.lions.dev.repository.StoryRepository; +import com.lions.dev.service.EmailService; +import com.lions.dev.service.NotificationService; +import io.quarkus.test.InjectMock; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.DisplayName; + +import java.util.Collections; +import java.util.List; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +/** + * Tests des jobs planifiĂ©s (ScheduledJobs). + * Les dĂ©pendances sont mockĂ©es pour Ă©viter DB/Kafka ; on vĂ©rifie que les mĂ©thodes ne lĂšvent pas et appellent les repos. + */ +@QuarkusTest +class ScheduledJobsTest { + + @Inject + ScheduledJobs scheduledJobs; + + @InjectMock + StoryRepository storyRepository; + + @InjectMock + PasswordResetTokenRepository passwordResetTokenRepository; + + @InjectMock + EstablishmentSubscriptionRepository subscriptionRepository; + + @InjectMock + EstablishmentRepository establishmentRepository; + + @InjectMock + EventsRepository eventsRepository; + + @InjectMock + NotificationService notificationService; + + @InjectMock + EmailService emailService; + + @Test + @DisplayName("deactivateExpiredStories appelle le repository et ne lance pas") + void deactivateExpiredStories_callsRepository() { + when(storyRepository.deactivateExpiredStories()).thenReturn(0); + + scheduledJobs.deactivateExpiredStories(); + + verify(storyRepository).deactivateExpiredStories(); + } + + @Test + @DisplayName("deleteExpiredPasswordResetTokens appelle le repository et ne lance pas") + void deleteExpiredPasswordResetTokens_callsRepository() { + when(passwordResetTokenRepository.deleteExpiredTokens()).thenReturn(0L); + + scheduledJobs.deleteExpiredPasswordResetTokens(); + + verify(passwordResetTokenRepository).deleteExpiredTokens(); + } + + @Test + @DisplayName("expireSubscriptionsAndDisableEstablishments sans abonnements expirĂ©s ne lance pas") + void expireSubscriptionsAndDisableEstablishments_emptyList_doesNotThrow() { + when(subscriptionRepository.findExpiredActiveSubscriptions()).thenReturn(Collections.emptyList()); + + scheduledJobs.expireSubscriptionsAndDisableEstablishments(); + + verify(subscriptionRepository).findExpiredActiveSubscriptions(); + } + + @Test + @DisplayName("sendEventReminders sans Ă©vĂ©nements dans les fenĂȘtres ne lance pas") + void sendEventReminders_emptyWindows_doesNotThrow() { + when(eventsRepository.findEventsStartingBetween(any(), any())).thenReturn(List.of()); + + scheduledJobs.sendEventReminders(); + + verify(eventsRepository, atLeast(1)).findEventsStartingBetween(any(), any()); + } + + @Test + @DisplayName("sendSubscriptionExpirationWarningEmails sans abonnements J-3 ne lance pas") + void sendSubscriptionExpirationWarningEmails_emptyList_doesNotThrow() { + when(subscriptionRepository.findActiveSubscriptionsExpiringBetween(any(), any())) + .thenReturn(Collections.emptyList()); + + scheduledJobs.sendSubscriptionExpirationWarningEmails(); + + verify(subscriptionRepository).findActiveSubscriptionsExpiringBetween(any(), any()); + } +} diff --git a/src/test/java/com/lions/dev/service/EmailServiceTest.java b/src/test/java/com/lions/dev/service/EmailServiceTest.java new file mode 100644 index 0000000..6b5699e --- /dev/null +++ b/src/test/java/com/lions/dev/service/EmailServiceTest.java @@ -0,0 +1,69 @@ +package com.lions.dev.service; + +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.DisplayName; + +import java.time.LocalDateTime; + +/** + * Tests du service email (mode mock : pas d'envoi rĂ©el). + * VĂ©rifie que les mĂ©thodes n'Ă©chouent pas et que le contenu est cohĂ©rent. + */ +@QuarkusTest +class EmailServiceTest { + + @Inject + EmailService emailService; + + @Test + @DisplayName("sendPasswordResetEmail ne lance pas d'exception") + void sendPasswordResetEmail_doesNotThrow() { + emailService.sendPasswordResetEmail("test@example.com", "Jean", "token-123"); + } + + @Test + @DisplayName("sendWelcomeEmail ne lance pas d'exception") + void sendWelcomeEmail_doesNotThrow() { + emailService.sendWelcomeEmail("welcome@example.com", "Marie"); + } + + @Test + @DisplayName("sendPaymentConfirmationEmail ne lance pas d'exception") + void sendPaymentConfirmationEmail_doesNotThrow() { + emailService.sendPaymentConfirmationEmail( + "manager@example.com", "Paul", "Le Bar du coin", 15_000, "MONTHLY"); + } + + @Test + @DisplayName("sendEventReminderEmail ne lance pas d'exception") + void sendEventReminderEmail_doesNotThrow() { + emailService.sendEventReminderEmail( + "user@example.com", "Sophie", "Afterwork Jeudi", + LocalDateTime.now().plusHours(1)); + } + + @Test + @DisplayName("sendSubscriptionExpirationWarningEmail ne lance pas d'exception") + void sendSubscriptionExpirationWarningEmail_doesNotThrow() { + emailService.sendSubscriptionExpirationWarningEmail( + "manager@example.com", "Luc", "Mon Établissement", + LocalDateTime.now().plusDays(3)); + } + + @Test + @DisplayName("sendBookingConfirmationEmail ne lance pas d'exception") + void sendBookingConfirmationEmail_doesNotThrow() { + emailService.sendBookingConfirmationEmail( + "client@example.com", "Emma", "Le Restaurant", + LocalDateTime.now().plusDays(1), 4); + } + + @Test + @DisplayName("sendPaymentFailureEmail ne lance pas d'exception") + void sendPaymentFailureEmail_doesNotThrow() { + emailService.sendPaymentFailureEmail( + "manager@example.com", "Marc", "Établissement XYZ"); + } +} diff --git a/src/test/java/com/lions/dev/service/NotificationServiceTest.java b/src/test/java/com/lions/dev/service/NotificationServiceTest.java new file mode 100644 index 0000000..ec8555f --- /dev/null +++ b/src/test/java/com/lions/dev/service/NotificationServiceTest.java @@ -0,0 +1,138 @@ +package com.lions.dev.service; + +import com.lions.dev.entity.notification.Notification; +import com.lions.dev.entity.users.Users; +import com.lions.dev.exception.UserNotFoundException; +import com.lions.dev.repository.EventsRepository; +import com.lions.dev.repository.NotificationRepository; +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.Test; +import org.junit.jupiter.api.DisplayName; + +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@QuarkusTest +class NotificationServiceTest { + + @Inject + NotificationService notificationService; + + @InjectMock + NotificationRepository notificationRepository; + + @InjectMock + UsersRepository usersRepository; + + @InjectMock + EventsRepository eventsRepository; + + @Test + @DisplayName("getNotificationsByUserId avec utilisateur existant retourne la liste") + void getNotificationsByUserId_userExists_returnsList() { + UUID userId = UUID.randomUUID(); + Users user = new Users(); + user.setId(userId); + Notification notif = new Notification("Titre", "Message", "event", user); + + when(usersRepository.findById(userId)).thenReturn(user); + when(notificationRepository.findByUserId(userId)).thenReturn(List.of(notif)); + + List result = notificationService.getNotificationsByUserId(userId); + + assertNotNull(result); + assertEquals(1, result.size()); + assertEquals("Titre", result.get(0).getTitle()); + verify(usersRepository).findById(userId); + verify(notificationRepository).findByUserId(userId); + } + + @Test + @DisplayName("getNotificationsByUserId avec utilisateur inconnu lance UserNotFoundException") + void getNotificationsByUserId_userNotFound_throws() { + UUID userId = UUID.randomUUID(); + when(usersRepository.findById(userId)).thenReturn(null); + + assertThrows(UserNotFoundException.class, () -> + notificationService.getNotificationsByUserId(userId)); + verify(notificationRepository, never()).findByUserId(any()); + } + + @Test + @DisplayName("createNotification avec donnĂ©es valides persiste et retourne la notification") + void createNotification_validData_persistsAndReturns() { + UUID userId = UUID.randomUUID(); + UUID eventId = UUID.randomUUID(); + Users user = new Users(); + user.setId(userId); + + when(usersRepository.findById(userId)).thenReturn(user); + + Notification created = notificationService.createNotification( + "Rappel", "L'Ă©vĂ©nement commence bientĂŽt", "reminder", userId, eventId); + + assertNotNull(created); + assertEquals("Rappel", created.getTitle()); + assertEquals("reminder", created.getType()); + } + + @Test + @DisplayName("createNotification avec userId inconnu lance UserNotFoundException") + void createNotification_unknownUser_throws() { + UUID userId = UUID.randomUUID(); + when(usersRepository.findById(userId)).thenReturn(null); + + assertThrows(UserNotFoundException.class, () -> + notificationService.createNotification("T", "M", "event", userId, null)); + } + + @Test + @DisplayName("countUnreadNotifications avec utilisateur existant retourne le compte") + void countUnreadNotifications_userExists_returnsCount() { + UUID userId = UUID.randomUUID(); + Users user = new Users(); + user.setId(userId); + when(usersRepository.findById(userId)).thenReturn(user); + when(notificationRepository.countUnreadByUserId(userId)).thenReturn(3L); + + long count = notificationService.countUnreadNotifications(userId); + + assertEquals(3L, count); + verify(notificationRepository).countUnreadByUserId(userId); + } + + @Test + @DisplayName("countUnreadNotifications avec utilisateur inconnu lance UserNotFoundException") + void countUnreadNotifications_userNotFound_throws() { + UUID userId = UUID.randomUUID(); + when(usersRepository.findById(userId)).thenReturn(null); + + assertThrows(UserNotFoundException.class, () -> + notificationService.countUnreadNotifications(userId)); + } + + @Test + @DisplayName("getNotificationsByUserIdWithPagination avec utilisateur existant retourne la page") + void getNotificationsByUserIdWithPagination_userExists_returnsPage() { + UUID userId = UUID.randomUUID(); + Users user = new Users(); + user.setId(userId); + when(usersRepository.findById(userId)).thenReturn(user); + when(notificationRepository.findByUserIdWithPagination(userId, 0, 10)) + .thenReturn(Collections.emptyList()); + + List result = notificationService.getNotificationsByUserIdWithPagination(userId, 0, 10); + + assertNotNull(result); + assertTrue(result.isEmpty()); + verify(notificationRepository).findByUserIdWithPagination(userId, 0, 10); + } +} diff --git a/src/test/java/com/lions/dev/service/WavePaymentServiceTest.java b/src/test/java/com/lions/dev/service/WavePaymentServiceTest.java new file mode 100644 index 0000000..2a88bad --- /dev/null +++ b/src/test/java/com/lions/dev/service/WavePaymentServiceTest.java @@ -0,0 +1,183 @@ +package com.lions.dev.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.lions.dev.dto.response.establishment.InitiateSubscriptionResponseDTO; +import com.lions.dev.entity.establishment.Establishment; +import com.lions.dev.entity.establishment.EstablishmentSubscription; +import com.lions.dev.entity.users.Users; +import com.lions.dev.repository.EstablishmentPaymentRepository; +import com.lions.dev.repository.EstablishmentRepository; +import com.lions.dev.repository.EstablishmentSubscriptionRepository; +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.Test; +import org.junit.jupiter.api.DisplayName; + +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@QuarkusTest +class WavePaymentServiceTest { + + @Inject + WavePaymentService wavePaymentService; + + @InjectMock + EstablishmentRepository establishmentRepository; + + @InjectMock + EstablishmentSubscriptionRepository subscriptionRepository; + + @InjectMock + EstablishmentPaymentRepository paymentRepository; + + @InjectMock + UsersRepository usersRepository; + + @InjectMock + EmailService emailService; + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + @Test + @DisplayName("initiatePayment avec Ă©tablissement inconnu lance IllegalArgumentException") + void initiatePayment_unknownEstablishment_throws() { + UUID establishmentId = UUID.randomUUID(); + when(establishmentRepository.findById(establishmentId)).thenReturn(null); + + assertThrows(IllegalArgumentException.class, () -> + wavePaymentService.initiatePayment(establishmentId, "MONTHLY", "+221771234567")); + } + + @Test + @DisplayName("initiatePayment avec Ă©tablissement valide retourne un DTO avec paymentUrl") + void initiatePayment_validEstablishment_returnsDto() { + UUID establishmentId = UUID.randomUUID(); + Establishment establishment = new Establishment(); + establishment.setId(establishmentId); + establishment.setName("Le Bar Test"); + + when(establishmentRepository.findById(establishmentId)).thenReturn(establishment); + + InitiateSubscriptionResponseDTO result = wavePaymentService.initiatePayment( + establishmentId, "MONTHLY", "+221771234567"); + + assertNotNull(result); + assertNotNull(result.getPaymentUrl()); + assertTrue(result.getPaymentUrl().contains("checkout") || result.getPaymentUrl().contains("test")); + assertEquals("MONTHLY", result.getPlan()); + } + + @Test + @DisplayName("hasActiveSubscription retourne false quand aucun abonnement actif") + void hasActiveSubscription_noActive_returnsFalse() { + UUID establishmentId = UUID.randomUUID(); + when(subscriptionRepository.findActiveByEstablishmentId(establishmentId)).thenReturn(Optional.empty()); + + boolean result = wavePaymentService.hasActiveSubscription(establishmentId); + + assertFalse(result); + } + + @Test + @DisplayName("hasActiveSubscription retourne true quand abonnement actif existe") + void hasActiveSubscription_hasActive_returnsTrue() { + UUID establishmentId = UUID.randomUUID(); + EstablishmentSubscription sub = new EstablishmentSubscription(); + sub.setEstablishmentId(establishmentId); + sub.setStatus(EstablishmentSubscription.STATUS_ACTIVE); + when(subscriptionRepository.findActiveByEstablishmentId(establishmentId)).thenReturn(Optional.of(sub)); + + boolean result = wavePaymentService.hasActiveSubscription(establishmentId); + + assertTrue(result); + } + + @Test + @DisplayName("handleWebhook payment.completed active l'Ă©tablissement et envoie l'email de confirmation") + void handleWebhook_paymentCompleted_activatesAndSendsEmail() throws Exception { + String sessionId = "wave-session-123"; + UUID establishmentId = UUID.randomUUID(); + Establishment establishment = new Establishment(); + establishment.setId(establishmentId); + establishment.setName("Bar Test"); + establishment.setIsActive(false); + Users manager = new Users(); + manager.setId(UUID.randomUUID()); + manager.setEmail("manager@test.com"); + manager.setFirstName("Jean"); + manager.setActive(false); + establishment.setManager(manager); + + EstablishmentSubscription sub = new EstablishmentSubscription(); + sub.setId(UUID.randomUUID()); + sub.setEstablishmentId(establishmentId); + sub.setPlan(EstablishmentSubscription.PLAN_MONTHLY); + sub.setStatus(EstablishmentSubscription.STATUS_PENDING); + sub.setAmountXof(15_000); + + when(subscriptionRepository.findByWaveSessionId(sessionId)).thenReturn(Optional.of(sub)); + when(establishmentRepository.findById(establishmentId)).thenReturn(establishment); + when(usersRepository.findById(manager.getId())).thenReturn(manager); + when(paymentRepository.findByWaveSessionId(sessionId)).thenReturn(Optional.of(new com.lions.dev.entity.establishment.EstablishmentPayment())); + + String payload = """ + {"type":"payment.completed","data":{"id":"%s"}} + """.formatted(sessionId); + var node = objectMapper.readTree(payload); + + wavePaymentService.handleWebhook(node); + + verify(emailService).sendPaymentConfirmationEmail( + eq("manager@test.com"), + eq("Jean"), + eq("Bar Test"), + eq(15_000), + eq("MONTHLY") + ); + } + + @Test + @DisplayName("handleWebhook payment.failed envoie l'email d'Ă©chec") + void handleWebhook_paymentFailed_sendsFailureEmail() throws Exception { + String sessionId = "wave-session-fail"; + UUID establishmentId = UUID.randomUUID(); + Establishment establishment = new Establishment(); + establishment.setId(establishmentId); + establishment.setName("Bar Fail"); + Users manager = new Users(); + manager.setId(UUID.randomUUID()); + manager.setEmail("manager@fail.com"); + manager.setFirstName("Paul"); + establishment.setManager(manager); + + EstablishmentSubscription sub = new EstablishmentSubscription(); + sub.setId(UUID.randomUUID()); + sub.setEstablishmentId(establishmentId); + sub.setStatus(EstablishmentSubscription.STATUS_PENDING); + + when(subscriptionRepository.findByWaveSessionId(sessionId)).thenReturn(Optional.of(sub)); + when(establishmentRepository.findById(establishmentId)).thenReturn(establishment); + when(usersRepository.findById(manager.getId())).thenReturn(manager); + when(paymentRepository.findByWaveSessionId(sessionId)).thenReturn(Optional.of(new com.lions.dev.entity.establishment.EstablishmentPayment())); + + String payload = """ + {"type":"payment.failed","data":{"id":"%s"}} + """.formatted(sessionId); + var node = objectMapper.readTree(payload); + + wavePaymentService.handleWebhook(node); + + verify(emailService).sendPaymentFailureEmail( + eq("manager@fail.com"), + eq("Paul"), + eq("Bar Fail") + ); + } +} diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties new file mode 100644 index 0000000..c006cf9 --- /dev/null +++ b/src/test/resources/application.properties @@ -0,0 +1,9 @@ +# Profil test : bootstrap Kafka rĂ©solvable (localhost) + consumers dĂ©sactivĂ©s +%test.kafka.bootstrap.servers=localhost:9092 +%test.mp.messaging.incoming.kafka-notifications.enabled=false +%test.mp.messaging.incoming.kafka-chat.enabled=false +%test.mp.messaging.incoming.kafka-reactions.enabled=false +%test.mp.messaging.incoming.kafka-presence.enabled=false + +# Scheduler dĂ©sactivĂ© en test pour Ă©viter exĂ©cution des jobs pendant les tests +%test.quarkus.scheduler.enabled=false