feat: v2.0 – réorg docker/scripts, prod, résas, abonnements Wave, Flyway base vierge
This commit is contained in:
59
src/main/java/com/lions/dev/config/SuperAdminStartup.java
Normal file
59
src/main/java/com/lions/dev/config/SuperAdminStartup.java
Normal file
@@ -0,0 +1,59 @@
|
||||
package com.lions.dev.config;
|
||||
|
||||
import com.lions.dev.entity.users.Users;
|
||||
import com.lions.dev.repository.UsersRepository;
|
||||
import com.lions.dev.util.UserRoles;
|
||||
import io.quarkus.runtime.StartupEvent;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.enterprise.event.Observes;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
/**
|
||||
* Crée le super administrateur au démarrage de l'application si aucun n'existe.
|
||||
* Email et mot de passe configurables (variables d'environnement en production).
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class SuperAdminStartup {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(SuperAdminStartup.class);
|
||||
|
||||
@Inject
|
||||
UsersRepository usersRepository;
|
||||
|
||||
@ConfigProperty(name = "afterwork.super-admin.email", defaultValue = "superadmin@afterwork.lions.dev")
|
||||
String superAdminEmail;
|
||||
|
||||
@ConfigProperty(name = "afterwork.super-admin.password", defaultValue = "SuperAdmin2025!")
|
||||
String superAdminPassword;
|
||||
|
||||
@ConfigProperty(name = "afterwork.super-admin.first-name", defaultValue = "Super")
|
||||
String superAdminFirstName;
|
||||
|
||||
@ConfigProperty(name = "afterwork.super-admin.last-name", defaultValue = "Administrator")
|
||||
String superAdminLastName;
|
||||
|
||||
@Transactional
|
||||
void onStart(@Observes StartupEvent event) {
|
||||
if (usersRepository.findByEmail(superAdminEmail).isPresent()) {
|
||||
LOG.info("Super administrateur déjà présent (email: " + superAdminEmail + "). Aucune création.");
|
||||
return;
|
||||
}
|
||||
|
||||
Users superAdmin = new Users();
|
||||
superAdmin.setFirstName(superAdminFirstName);
|
||||
superAdmin.setLastName(superAdminLastName);
|
||||
superAdmin.setEmail(superAdminEmail);
|
||||
superAdmin.setPassword(superAdminPassword);
|
||||
superAdmin.setRole(UserRoles.SUPER_ADMIN);
|
||||
superAdmin.setProfileImageUrl("https://placehold.co/150x150.png");
|
||||
superAdmin.setBio("Super administrateur AfterWork");
|
||||
superAdmin.setLoyaltyPoints(0);
|
||||
superAdmin.setVerified(true);
|
||||
|
||||
usersRepository.persist(superAdmin);
|
||||
LOG.info("Super administrateur créé au démarrage : " + superAdminEmail + " (role: " + UserRoles.SUPER_ADMIN + ")");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.lions.dev.dto.request.booking;
|
||||
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* DTO de création de réservation (aligné frontend).
|
||||
*/
|
||||
public class ReservationCreateRequestDTO {
|
||||
|
||||
@NotNull(message = "userId est obligatoire")
|
||||
private UUID userId;
|
||||
|
||||
@NotNull(message = "establishmentId est obligatoire")
|
||||
private UUID establishmentId;
|
||||
|
||||
/** Date/heure de réservation (ISO-8601 ou timestamp). */
|
||||
private String reservationDate;
|
||||
|
||||
@Min(1)
|
||||
private int numberOfPeople = 1;
|
||||
|
||||
private String notes;
|
||||
|
||||
public UUID getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(UUID userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public UUID getEstablishmentId() {
|
||||
return establishmentId;
|
||||
}
|
||||
|
||||
public void setEstablishmentId(UUID establishmentId) {
|
||||
this.establishmentId = establishmentId;
|
||||
}
|
||||
|
||||
public String getReservationDate() {
|
||||
return reservationDate;
|
||||
}
|
||||
|
||||
public void setReservationDate(String reservationDate) {
|
||||
this.reservationDate = reservationDate;
|
||||
}
|
||||
|
||||
public int getNumberOfPeople() {
|
||||
return numberOfPeople;
|
||||
}
|
||||
|
||||
public void setNumberOfPeople(int numberOfPeople) {
|
||||
this.numberOfPeople = numberOfPeople;
|
||||
}
|
||||
|
||||
public String getNotes() {
|
||||
return notes;
|
||||
}
|
||||
|
||||
public void setNotes(String notes) {
|
||||
this.notes = notes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.lions.dev.dto.request.establishment;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* Requête pour initier un paiement Wave (droits d'accès établissement).
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class InitiateSubscriptionRequestDTO {
|
||||
|
||||
/** Plan : MONTHLY, YEARLY */
|
||||
@NotBlank(message = "Le plan est obligatoire")
|
||||
@Pattern(regexp = "MONTHLY|YEARLY", message = "Plan invalide. Valeurs : MONTHLY, YEARLY")
|
||||
private String plan;
|
||||
|
||||
/** 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")
|
||||
private String clientPhone;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.lions.dev.dto.request.friends;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.NoArgsConstructor;
|
||||
@@ -15,7 +16,10 @@ import java.util.UUID;
|
||||
@NoArgsConstructor
|
||||
public class FriendshipCreateOneRequestDTO {
|
||||
|
||||
@NotNull(message = "L'identifiant de l'utilisateur est requis")
|
||||
private UUID userId; // ID de l'utilisateur qui envoie la demande
|
||||
|
||||
@NotNull(message = "L'identifiant de l'ami est requis")
|
||||
private UUID friendId; // ID de l'utilisateur qui reçoit la demande
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.lions.dev.dto.request.users;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* DTO pour l'attribution d'un rôle à un utilisateur (opération réservée au super admin).
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class AssignRoleRequestDTO {
|
||||
|
||||
private static final String ROLE_PATTERN = "SUPER_ADMIN|ADMIN|MANAGER|USER";
|
||||
|
||||
@NotBlank(message = "Le rôle est obligatoire")
|
||||
@Pattern(regexp = ROLE_PATTERN, message = "Rôle invalide. Valeurs autorisées : SUPER_ADMIN, ADMIN, MANAGER, USER")
|
||||
private String role;
|
||||
|
||||
public AssignRoleRequestDTO(String role) {
|
||||
this.role = role;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.lions.dev.dto.response.admin;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AdminRevenueResponseDTO {
|
||||
|
||||
/** Revenus totaux (abonnements actifs * prix). */
|
||||
private BigDecimal totalRevenueXof;
|
||||
/** Nombre d'abonnements actifs. */
|
||||
private long activeSubscriptionsCount;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.lions.dev.dto.response.admin;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ManagerStatsResponseDTO {
|
||||
|
||||
private UUID userId;
|
||||
private String email;
|
||||
private String firstName;
|
||||
private String lastName;
|
||||
/** ACTIVE ou SUSPENDED (isActive). */
|
||||
private String status;
|
||||
private LocalDateTime subscriptionExpiresAt;
|
||||
private UUID establishmentId;
|
||||
private String establishmentName;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.lions.dev.dto.response.booking;
|
||||
|
||||
import com.lions.dev.entity.booking.Booking;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* DTO de réponse pour une réservation (aligné sur le frontend Flutter ReservationModel).
|
||||
* eventId/eventTitle : pour les réservations d'établissement, eventTitle = nom de l'établissement, eventId = null.
|
||||
*/
|
||||
@Getter
|
||||
public class ReservationResponseDTO {
|
||||
|
||||
private final String id;
|
||||
private final String userId;
|
||||
private final String userFullName;
|
||||
private final String eventId; // null pour résa établissement
|
||||
private final String eventTitle; // nom événement ou établissement
|
||||
private final String reservationDate; // ISO-8601
|
||||
private final int numberOfPeople;
|
||||
private final String status; // PENDING, CONFIRMED, CANCELLED, COMPLETED
|
||||
private final String establishmentId;
|
||||
private final String establishmentName;
|
||||
private final String notes;
|
||||
private final String createdAt; // ISO-8601
|
||||
|
||||
public ReservationResponseDTO(Booking booking) {
|
||||
this.id = booking.getId() != null ? booking.getId().toString() : null;
|
||||
this.userId = booking.getUser() != null && booking.getUser().getId() != null
|
||||
? booking.getUser().getId().toString() : null;
|
||||
this.userFullName = booking.getUser() != null
|
||||
? (booking.getUser().getFirstName() + " " + booking.getUser().getLastName()).trim()
|
||||
: "";
|
||||
this.eventId = null; // Réservation établissement sans événement
|
||||
this.eventTitle = booking.getEstablishment() != null ? booking.getEstablishment().getName() : "";
|
||||
this.reservationDate = booking.getReservationTime() != null
|
||||
? booking.getReservationTime().toString() : null;
|
||||
this.numberOfPeople = booking.getGuestCount() != null ? booking.getGuestCount() : 1;
|
||||
this.status = booking.getStatus() != null ? booking.getStatus().toLowerCase() : "pending";
|
||||
this.establishmentId = booking.getEstablishment() != null && booking.getEstablishment().getId() != null
|
||||
? booking.getEstablishment().getId().toString() : null;
|
||||
this.establishmentName = booking.getEstablishment() != null ? booking.getEstablishment().getName() : null;
|
||||
this.notes = booking.getSpecialRequests();
|
||||
this.createdAt = booking.getCreatedAt() != null ? booking.getCreatedAt().toString() : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.lions.dev.dto.response.establishment;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* Réponse après initiation d'un paiement Wave (URL de redirection).
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class InitiateSubscriptionResponseDTO {
|
||||
|
||||
private String paymentUrl;
|
||||
private String waveSessionId;
|
||||
private Integer amountXof;
|
||||
private String plan;
|
||||
private String status;
|
||||
}
|
||||
@@ -21,6 +21,7 @@ public class UserCreateResponseDTO {
|
||||
private String lastName; // v2.0 - Nom de famille de l'utilisateur
|
||||
private String email; // Email de l'utilisateur
|
||||
private String role; // Rôle de l'utilisateur
|
||||
private Boolean isActive = true; // false = manager suspendu (abonnement expiré)
|
||||
private String profileImageUrl; // URL de l'image de profil de l'utilisateur
|
||||
private String bio; // v2.0 - Biographie courte
|
||||
private Integer loyaltyPoints; // v2.0 - Points de fidélité
|
||||
@@ -56,6 +57,7 @@ public class UserCreateResponseDTO {
|
||||
this.lastName = user.getLastName(); // v2.0
|
||||
this.email = user.getEmail();
|
||||
this.role = user.getRole();
|
||||
this.isActive = user.isActive();
|
||||
this.profileImageUrl = user.getProfileImageUrl();
|
||||
this.bio = user.getBio(); // v2.0
|
||||
this.loyaltyPoints = user.getLoyaltyPoints(); // v2.0
|
||||
|
||||
@@ -62,6 +62,10 @@ public class Establishment extends BaseEntity {
|
||||
@Column(name = "verification_status", nullable = false)
|
||||
private String verificationStatus = "PENDING"; // Statut de vérification: PENDING, VERIFIED, REJECTED (v2.0)
|
||||
|
||||
/** true = visible dans l'app ; false = masqué (abonnement inactif / suspension Wave). Par défaut true. */
|
||||
@Column(name = "is_active", nullable = false)
|
||||
private Boolean isActive = true;
|
||||
|
||||
@Column(name = "latitude")
|
||||
private Double latitude; // Latitude pour la géolocalisation
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.lions.dev.entity.establishment;
|
||||
|
||||
import com.lions.dev.entity.BaseEntity;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Enregistrement d'un paiement Wave pour un établissement (droits d'accès).
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "establishment_payments")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class EstablishmentPayment extends BaseEntity {
|
||||
|
||||
@Column(name = "establishment_id", nullable = false)
|
||||
private UUID establishmentId;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "establishment_id", insertable = false, updatable = false)
|
||||
private Establishment establishment;
|
||||
|
||||
@Column(name = "amount_xof", nullable = false)
|
||||
private Integer amountXof;
|
||||
|
||||
@Column(name = "wave_session_id", length = 255)
|
||||
private String waveSessionId;
|
||||
|
||||
/** Statut : PENDING, COMPLETED, FAILED, CANCELLED */
|
||||
@Column(name = "status", nullable = false, length = 20)
|
||||
private String status = "PENDING";
|
||||
|
||||
@Column(name = "client_phone", length = 30)
|
||||
private String clientPhone;
|
||||
|
||||
@Column(name = "plan", length = 20)
|
||||
private String plan;
|
||||
|
||||
public static final String STATUS_PENDING = "PENDING";
|
||||
public static final String STATUS_COMPLETED = "COMPLETED";
|
||||
public static final String STATUS_FAILED = "FAILED";
|
||||
public static final String STATUS_CANCELLED = "CANCELLED";
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.lions.dev.entity.establishment;
|
||||
|
||||
import com.lions.dev.entity.BaseEntity;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Abonnement / droits d'accès d'un établissement (paiement via Wave).
|
||||
* Un établissement doit avoir un abonnement actif pour accéder aux fonctionnalités payantes.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "establishment_subscriptions")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class EstablishmentSubscription extends BaseEntity {
|
||||
|
||||
@Column(name = "establishment_id", nullable = false)
|
||||
private UUID establishmentId;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "establishment_id", insertable = false, updatable = false)
|
||||
private Establishment establishment;
|
||||
|
||||
/** Plan : MONTHLY, YEARLY */
|
||||
@Column(name = "plan", nullable = false, length = 20)
|
||||
private String plan;
|
||||
|
||||
/** Statut : PENDING, ACTIVE, EXPIRED, CANCELLED */
|
||||
@Column(name = "status", nullable = false, length = 20)
|
||||
private String status = "PENDING";
|
||||
|
||||
@Column(name = "wave_session_id", length = 255)
|
||||
private String waveSessionId;
|
||||
|
||||
@Column(name = "amount_xof")
|
||||
private Integer amountXof;
|
||||
|
||||
@Column(name = "paid_at")
|
||||
private LocalDateTime paidAt;
|
||||
|
||||
@Column(name = "expires_at")
|
||||
private LocalDateTime expiresAt;
|
||||
|
||||
public static final String PLAN_MONTHLY = "MONTHLY";
|
||||
public static final String PLAN_YEARLY = "YEARLY";
|
||||
public static final String STATUS_PENDING = "PENDING";
|
||||
public static final String STATUS_ACTIVE = "ACTIVE";
|
||||
public static final String STATUS_EXPIRED = "EXPIRED";
|
||||
public static final String STATUS_CANCELLED = "CANCELLED";
|
||||
}
|
||||
@@ -27,9 +27,9 @@ public class SocialPost extends BaseEntity {
|
||||
@Column(name = "content", nullable = false, length = 2000)
|
||||
private String content; // Le contenu textuel du post
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@ManyToOne(fetch = FetchType.EAGER)
|
||||
@JoinColumn(name = "user_id", nullable = false)
|
||||
private Users user; // L'utilisateur créateur du post
|
||||
private Users user; // L'utilisateur créateur du post (EAGER pour éviter 500 sur mapping DTO)
|
||||
|
||||
@Column(name = "image_url", length = 500)
|
||||
private String imageUrl; // URL de l'image associée (optionnel)
|
||||
|
||||
@@ -69,6 +69,10 @@ public class Users extends BaseEntity {
|
||||
@Column(name = "last_seen")
|
||||
private java.time.LocalDateTime lastSeen; // Dernière fois que l'utilisateur était en ligne
|
||||
|
||||
/** true = compte actif ; false = manager suspendu (abonnement expiré / paiement échoué). Par défaut true. */
|
||||
@Column(name = "is_active", nullable = false)
|
||||
private boolean isActive = true;
|
||||
|
||||
// Utilisation de BCrypt pour hacher les mots de passe de manière sécurisée
|
||||
// private static final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.lions.dev.repository;
|
||||
|
||||
import com.lions.dev.entity.booking.Booking;
|
||||
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@ApplicationScoped
|
||||
public class BookingRepository implements PanacheRepositoryBase<Booking, UUID> {
|
||||
|
||||
public List<Booking> findByUserId(UUID userId) {
|
||||
return list("user.id", userId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.lions.dev.repository;
|
||||
|
||||
import com.lions.dev.entity.establishment.EstablishmentPayment;
|
||||
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@ApplicationScoped
|
||||
public class EstablishmentPaymentRepository implements PanacheRepositoryBase<EstablishmentPayment, UUID> {
|
||||
|
||||
public Optional<EstablishmentPayment> findByWaveSessionId(String waveSessionId) {
|
||||
return find("waveSessionId", waveSessionId).firstResultOptional();
|
||||
}
|
||||
|
||||
public List<EstablishmentPayment> findByEstablishmentId(UUID establishmentId) {
|
||||
return list("establishmentId", establishmentId);
|
||||
}
|
||||
}
|
||||
@@ -84,5 +84,16 @@ public class EstablishmentRepository implements PanacheRepositoryBase<Establishm
|
||||
LOG.info("[LOG] Nombre d'établissements trouvés : " + establishments.size());
|
||||
return establishments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compte le nombre d'établissements gérés par un responsable.
|
||||
* Contrainte métier : un Manager ne peut gérer qu'un seul établissement.
|
||||
*
|
||||
* @param managerId L'ID du responsable.
|
||||
* @return Le nombre d'établissements (0 ou 1).
|
||||
*/
|
||||
public long countByManagerId(UUID managerId) {
|
||||
return count("manager.id = ?1", managerId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.lions.dev.repository;
|
||||
|
||||
import com.lions.dev.entity.establishment.EstablishmentSubscription;
|
||||
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@ApplicationScoped
|
||||
public class EstablishmentSubscriptionRepository implements PanacheRepositoryBase<EstablishmentSubscription, UUID> {
|
||||
|
||||
public Optional<EstablishmentSubscription> findByWaveSessionId(String waveSessionId) {
|
||||
return find("waveSessionId", waveSessionId).firstResultOptional();
|
||||
}
|
||||
|
||||
public List<EstablishmentSubscription> findByEstablishmentId(UUID establishmentId) {
|
||||
return list("establishmentId", establishmentId);
|
||||
}
|
||||
|
||||
public Optional<EstablishmentSubscription> findActiveByEstablishmentId(UUID establishmentId) {
|
||||
return find("establishmentId = ?1 and status = ?2", establishmentId, EstablishmentSubscription.STATUS_ACTIVE)
|
||||
.firstResultOptional();
|
||||
}
|
||||
}
|
||||
@@ -80,4 +80,24 @@ public class EventsRepository implements PanacheRepositoryBase<Events, UUID> {
|
||||
LOG.info("[LOG] " + events.size() + " événement(s) récupéré(s) des amis");
|
||||
return events;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les événements dont l'établissement correspond à la localisation (ville ou adresse).
|
||||
* v2.0 : la colonne location a été supprimée ; on filtre par establishment.address ou establishment.city.
|
||||
*
|
||||
* @param location Fragment de ville ou d'adresse (recherche partielle, insensible à la casse).
|
||||
* @return Liste des événements dont l'établissement matche la localisation.
|
||||
*/
|
||||
public List<Events> findEventsByEstablishmentLocation(String location) {
|
||||
if (location == null || location.isBlank()) {
|
||||
return List.of();
|
||||
}
|
||||
String pattern = "%" + location.trim().toLowerCase() + "%";
|
||||
return getEntityManager()
|
||||
.createQuery(
|
||||
"SELECT e FROM Events e JOIN e.establishment est WHERE est IS NOT NULL AND (LOWER(est.address) LIKE :loc OR LOWER(est.city) LIKE :loc)",
|
||||
Events.class)
|
||||
.setParameter("loc", pattern)
|
||||
.getResultList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,8 +26,9 @@ public class FriendshipRepository implements PanacheRepositoryBase<Friendship, U
|
||||
|
||||
/**
|
||||
* Trouver une relation d'amitié entre deux utilisateurs spécifiés.
|
||||
* Cette méthode recherche une relation d'amitié entre deux utilisateurs donnés.
|
||||
* Elle peut être utilisée pour vérifier si une demande d'amitié existe déjà.
|
||||
* Cette méthode recherche une relation d'amitié entre deux utilisateurs donnés,
|
||||
* dans les deux sens (user→friend ou friend→user) car une demande peut exister
|
||||
* dans l'une ou l'autre direction.
|
||||
*
|
||||
* @param user L'utilisateur qui envoie la demande d'amitié.
|
||||
* @param friend L'ami qui reçoit la demande.
|
||||
@@ -36,8 +37,8 @@ public class FriendshipRepository implements PanacheRepositoryBase<Friendship, U
|
||||
public Optional<Friendship> findByUsers(Users user, Users friend) {
|
||||
logger.infof("Recherche de la relation d'amitié entre les utilisateurs : %s et %s", user.getId(), friend.getId());
|
||||
|
||||
// Requête qui cherche une relation d'amitié entre deux utilisateurs spécifiques
|
||||
Optional<Friendship> friendship = find("user = ?1 and friend = ?2", user, friend).firstResultOptional();
|
||||
// Vérifier dans les deux sens : (user, friend) ET (friend, user)
|
||||
Optional<Friendship> friendship = find("(user = ?1 AND friend = ?2) OR (user = ?2 AND friend = ?1)", user, friend).firstResultOptional();
|
||||
|
||||
if (friendship.isPresent()) {
|
||||
logger.infof("Relation d'amitié trouvée entre %s et %s", user.getId(), friend.getId());
|
||||
|
||||
69
src/main/java/com/lions/dev/resource/AdminStatsResource.java
Normal file
69
src/main/java/com/lions/dev/resource/AdminStatsResource.java
Normal file
@@ -0,0 +1,69 @@
|
||||
package com.lions.dev.resource;
|
||||
|
||||
import com.lions.dev.dto.response.admin.AdminRevenueResponseDTO;
|
||||
import com.lions.dev.dto.response.admin.ManagerStatsResponseDTO;
|
||||
import com.lions.dev.service.AdminStatsService;
|
||||
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.eclipse.microprofile.openapi.annotations.Operation;
|
||||
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Ressource REST pour le tableau de bord Super Admin.
|
||||
* Requiert le header X-Super-Admin-Key pour toutes les opérations.
|
||||
*/
|
||||
@Path("/admin/stats")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Tag(name = "Admin Stats", description = "Statistiques et KPIs (Super Admin)")
|
||||
public class AdminStatsResource {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(AdminStatsResource.class);
|
||||
private static final String SUPER_ADMIN_KEY_HEADER = "X-Super-Admin-Key";
|
||||
|
||||
@Inject
|
||||
AdminStatsService adminStatsService;
|
||||
|
||||
@ConfigProperty(name = "afterwork.super-admin.api-key", defaultValue = "")
|
||||
Optional<String> superAdminApiKey;
|
||||
|
||||
private boolean isAuthorized(String apiKeyHeader) {
|
||||
if (superAdminApiKey == null || !superAdminApiKey.isPresent() || superAdminApiKey.get().isBlank()) {
|
||||
return false;
|
||||
}
|
||||
return apiKeyHeader != null && apiKeyHeader.equals(superAdminApiKey.get());
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/revenue")
|
||||
@Operation(summary = "Revenus des abonnements", description = "Total des abonnements actifs * prix. Réservé Super Admin.")
|
||||
public Response getRevenue(@HeaderParam(SUPER_ADMIN_KEY_HEADER) String apiKeyHeader) {
|
||||
if (!isAuthorized(apiKeyHeader)) {
|
||||
return Response.status(Response.Status.FORBIDDEN)
|
||||
.entity("{\"message\": \"Clé Super Admin invalide ou absente.\"}")
|
||||
.build();
|
||||
}
|
||||
AdminRevenueResponseDTO dto = adminStatsService.getRevenue();
|
||||
return Response.ok(dto).build();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/managers")
|
||||
@Operation(summary = "Liste des managers", description = "Managers avec statut (Actif/Suspendu) et date d'expiration. Réservé Super Admin.")
|
||||
public Response getManagers(@HeaderParam(SUPER_ADMIN_KEY_HEADER) String apiKeyHeader) {
|
||||
if (!isAuthorized(apiKeyHeader)) {
|
||||
return Response.status(Response.Status.FORBIDDEN)
|
||||
.entity("{\"message\": \"Clé Super Admin invalide ou absente.\"}")
|
||||
.build();
|
||||
}
|
||||
List<ManagerStatsResponseDTO> list = adminStatsService.getManagers();
|
||||
return Response.ok(list).build();
|
||||
}
|
||||
}
|
||||
100
src/main/java/com/lions/dev/resource/BookingResource.java
Normal file
100
src/main/java/com/lions/dev/resource/BookingResource.java
Normal file
@@ -0,0 +1,100 @@
|
||||
package com.lions.dev.resource;
|
||||
|
||||
import com.lions.dev.dto.request.booking.ReservationCreateRequestDTO;
|
||||
import com.lions.dev.dto.response.booking.ReservationResponseDTO;
|
||||
import com.lions.dev.service.BookingService;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Ressource REST pour les réservations (bookings).
|
||||
* Path /reservations pour alignement avec le frontend Flutter (ReservationsScreen).
|
||||
*/
|
||||
@Path("/reservations")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Tag(name = "Reservations", description = "Réservations d'établissements")
|
||||
public class BookingResource {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(BookingResource.class);
|
||||
|
||||
@Inject
|
||||
BookingService bookingService;
|
||||
|
||||
@GET
|
||||
@Path("/user/{userId}")
|
||||
@Operation(summary = "Liste des réservations d'un utilisateur")
|
||||
public Response getUserReservations(@PathParam("userId") UUID userId) {
|
||||
List<ReservationResponseDTO> list = bookingService.getReservationsByUserId(userId);
|
||||
return Response.ok(list).build();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/{id}")
|
||||
@Operation(summary = "Détail d'une réservation")
|
||||
public Response getReservation(@PathParam("id") UUID id) {
|
||||
ReservationResponseDTO dto = bookingService.getReservationById(id);
|
||||
if (dto == null) {
|
||||
return Response.status(Response.Status.NOT_FOUND).build();
|
||||
}
|
||||
return Response.ok(dto).build();
|
||||
}
|
||||
|
||||
@POST
|
||||
@Transactional
|
||||
@Operation(summary = "Créer une réservation")
|
||||
public Response createReservation(@Valid ReservationCreateRequestDTO dto) {
|
||||
try {
|
||||
ReservationResponseDTO created = bookingService.createReservation(dto);
|
||||
return Response.status(Response.Status.CREATED).entity(created).build();
|
||||
} catch (IllegalArgumentException e) {
|
||||
return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
|
||||
}
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("/{id}")
|
||||
@Transactional
|
||||
@Operation(summary = "Mettre à jour une réservation")
|
||||
public Response updateReservation(@PathParam("id") UUID id, @Valid ReservationCreateRequestDTO dto) {
|
||||
ReservationResponseDTO updated = bookingService.updateReservation(id, dto);
|
||||
if (updated == null) {
|
||||
return Response.status(Response.Status.NOT_FOUND).build();
|
||||
}
|
||||
return Response.ok(updated).build();
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("/{id}/cancel")
|
||||
@Transactional
|
||||
@Operation(summary = "Annuler une réservation")
|
||||
public Response cancelReservation(@PathParam("id") UUID id) {
|
||||
ReservationResponseDTO dto = bookingService.cancelReservation(id);
|
||||
if (dto == null) {
|
||||
return Response.status(Response.Status.NOT_FOUND).build();
|
||||
}
|
||||
return Response.ok(dto).build();
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Path("/{id}")
|
||||
@Transactional
|
||||
@Operation(summary = "Supprimer une réservation")
|
||||
public Response deleteReservation(@PathParam("id") UUID id) {
|
||||
if (bookingService.getReservationById(id) == null) {
|
||||
return Response.status(Response.Status.NOT_FOUND).build();
|
||||
}
|
||||
bookingService.deleteReservation(id);
|
||||
return Response.noContent().build();
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,10 @@ import com.lions.dev.dto.request.establishment.EstablishmentUpdateRequestDTO;
|
||||
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.EstablishmentRepository;
|
||||
import com.lions.dev.repository.UsersRepository;
|
||||
import com.lions.dev.service.EstablishmentService;
|
||||
import com.lions.dev.util.UserRoles;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.validation.Valid;
|
||||
@@ -38,6 +40,9 @@ public class EstablishmentResource {
|
||||
@Inject
|
||||
UsersRepository usersRepository;
|
||||
|
||||
@Inject
|
||||
EstablishmentRepository establishmentRepository;
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(EstablishmentResource.class);
|
||||
|
||||
// *********** Création d'un établissement ***********
|
||||
@@ -67,6 +72,26 @@ public class EstablishmentResource {
|
||||
.build();
|
||||
}
|
||||
|
||||
// Vérification rôle : seuls les Managers (ou ADMIN) peuvent créer un établissement
|
||||
String role = manager.getRole() != null ? manager.getRole().toUpperCase() : "";
|
||||
if (!UserRoles.MANAGER.equals(role) && !UserRoles.ADMIN.equals(role) && !UserRoles.SUPER_ADMIN.equals(role)) {
|
||||
LOG.error("[ERROR] L'utilisateur " + requestDTO.getManagerId() + " n'a pas le rôle Manager. Rôle : " + role);
|
||||
return Response.status(Response.Status.FORBIDDEN)
|
||||
.entity("Seuls les Managers peuvent créer des établissements")
|
||||
.build();
|
||||
}
|
||||
|
||||
// Contrainte : un Manager ne peut gérer qu'un seul établissement (ADMIN/SUPER_ADMIN exclus)
|
||||
if (UserRoles.MANAGER.equals(role)) {
|
||||
long count = establishmentRepository.countByManagerId(requestDTO.getManagerId());
|
||||
if (count >= 1) {
|
||||
LOG.error("[ERROR] Le Manager " + requestDTO.getManagerId() + " possède déjà un établissement");
|
||||
return Response.status(Response.Status.FORBIDDEN)
|
||||
.entity("Un Manager ne peut gérer qu'un seul établissement")
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
// Créer l'établissement
|
||||
Establishment establishment = new Establishment();
|
||||
establishment.setName(requestDTO.getName());
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.lions.dev.resource;
|
||||
|
||||
import com.lions.dev.dto.request.establishment.InitiateSubscriptionRequestDTO;
|
||||
import com.lions.dev.dto.response.establishment.InitiateSubscriptionResponseDTO;
|
||||
import com.lions.dev.service.WavePaymentService;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Ressource pour les abonnements / droits d'accès des établissements (paiement Wave).
|
||||
*/
|
||||
@Path("/establishments/{establishmentId}/subscriptions")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Tag(name = "Establishment Subscriptions", description = "Paiement des droits d'accès via Wave")
|
||||
public class EstablishmentSubscriptionResource {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(EstablishmentSubscriptionResource.class);
|
||||
|
||||
@Inject
|
||||
WavePaymentService wavePaymentService;
|
||||
|
||||
/**
|
||||
* Initie un paiement Wave pour les droits d'accès d'un établissement.
|
||||
* Retourne l'URL de redirection vers la page de paiement Wave.
|
||||
*/
|
||||
@POST
|
||||
@Path("/initiate")
|
||||
@Operation(summary = "Initier un paiement Wave (droits d'accès)",
|
||||
description = "Crée une session Wave et retourne l'URL de paiement. Le client redirige l'utilisateur vers payment_url.")
|
||||
public Response initiatePayment(
|
||||
@PathParam("establishmentId") UUID establishmentId,
|
||||
@Valid InitiateSubscriptionRequestDTO request) {
|
||||
try {
|
||||
InitiateSubscriptionResponseDTO response = wavePaymentService.initiatePayment(
|
||||
establishmentId,
|
||||
request.getPlan(),
|
||||
request.getClientPhone()
|
||||
);
|
||||
return Response.ok(response).build();
|
||||
} catch (IllegalArgumentException e) {
|
||||
LOG.warn(e.getMessage());
|
||||
return Response.status(Response.Status.NOT_FOUND).entity("{\"message\": \"" + e.getMessage() + "\"}").build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si l'établissement a un abonnement actif.
|
||||
*/
|
||||
@GET
|
||||
@Path("/status")
|
||||
@Operation(summary = "Statut d'abonnement", description = "Indique si l'établissement a des droits d'accès actifs.")
|
||||
public Response getSubscriptionStatus(@PathParam("establishmentId") UUID establishmentId) {
|
||||
boolean active = wavePaymentService.hasActiveSubscription(establishmentId);
|
||||
return Response.ok(Map.of("hasActiveSubscription", active)).build();
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import com.lions.dev.dto.response.friends.FriendshipReadStatusResponseDTO;
|
||||
import com.lions.dev.entity.friends.FriendshipStatus;
|
||||
import com.lions.dev.exception.UserNotFoundException;
|
||||
import com.lions.dev.service.FriendshipService;
|
||||
import jakarta.ws.rs.NotFoundException;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
@@ -82,6 +83,16 @@ public class FriendshipResource {
|
||||
+ " et "
|
||||
+ friendshipResponse.getFriendId());
|
||||
return Response.ok(friendshipResponse).build();
|
||||
} catch (UserNotFoundException e) {
|
||||
logger.warn("[WARN] Utilisateur non trouvé : " + e.getMessage());
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity("{\"message\": \"Utilisateur non trouvé.\"}")
|
||||
.build();
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.warn("[WARN] Requête invalide : " + e.getMessage());
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity("{\"message\": \"" + e.getMessage() + "\"}")
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
logger.error("[ERROR] Erreur lors de l'envoi de la demande d'amitié : " + e.getMessage(), e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.lions.dev.resource;
|
||||
import com.lions.dev.dto.request.social.SocialPostCreateRequestDTO;
|
||||
import com.lions.dev.dto.response.social.SocialPostResponseDTO;
|
||||
import com.lions.dev.entity.social.SocialPost;
|
||||
import com.lions.dev.exception.UserNotFoundException;
|
||||
import com.lions.dev.service.SocialPostService;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
@@ -121,6 +122,11 @@ public class SocialPostResource {
|
||||
);
|
||||
SocialPostResponseDTO responseDTO = new SocialPostResponseDTO(post);
|
||||
return Response.status(Response.Status.CREATED).entity(responseDTO).build();
|
||||
} catch (UserNotFoundException e) {
|
||||
LOG.warn("[WARN] Utilisateur non trouvé : " + e.getMessage());
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity("{\"message\": \"Utilisateur non trouvé.\"}")
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
LOG.error("[ERROR] Erreur lors de la création du post : " + e.getMessage(), e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
@@ -382,6 +388,11 @@ public class SocialPostResource {
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return Response.ok(responseDTOs).build();
|
||||
} catch (UserNotFoundException e) {
|
||||
LOG.warn("[WARN] Utilisateur non trouvé : " + e.getMessage());
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity("{\"message\": \"Utilisateur non trouvé.\"}")
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
LOG.error("[ERROR] Erreur lors de la récupération des posts : " + e.getMessage(), e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
@@ -416,6 +427,11 @@ public class SocialPostResource {
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return Response.ok(responseDTOs).build();
|
||||
} catch (UserNotFoundException e) {
|
||||
LOG.warn("[WARN] Utilisateur non trouvé : " + e.getMessage());
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity("{\"message\": \"Utilisateur non trouvé.\"}")
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
LOG.error("[ERROR] Erreur lors de la récupération des posts des amis : " + e.getMessage(), e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
|
||||
@@ -1,6 +1,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.UserAuthenticateRequestDTO;
|
||||
import com.lions.dev.dto.request.users.UserCreateRequestDTO;
|
||||
import com.lions.dev.dto.response.users.UserAuthenticateResponseDTO;
|
||||
@@ -16,9 +17,11 @@ import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
|
||||
@@ -39,8 +42,13 @@ public class UsersResource {
|
||||
@Inject
|
||||
UsersService userService;
|
||||
|
||||
@ConfigProperty(name = "afterwork.super-admin.api-key", defaultValue = "")
|
||||
Optional<String> superAdminApiKey;
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(UsersResource.class);
|
||||
|
||||
private static final String SUPER_ADMIN_KEY_HEADER = "X-Super-Admin-Key";
|
||||
|
||||
/**
|
||||
* Endpoint pour créer un nouvel utilisateur.
|
||||
*
|
||||
@@ -296,11 +304,11 @@ public class UsersResource {
|
||||
Users user = userService.findByEmail(request.getEmail());
|
||||
|
||||
if (user != null) {
|
||||
// TODO: Generer un token de reset et l'envoyer par email
|
||||
// Pour l'instant, on retourne success pour ne pas reveler si l'email existe
|
||||
// String resetToken = generateResetToken();
|
||||
// emailService.sendPasswordResetEmail(user.getEmail(), resetToken);
|
||||
LOG.info("Utilisateur trouve, email de reinitialisation devrait etre envoye : " + request.getEmail());
|
||||
// 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());
|
||||
}
|
||||
@@ -318,4 +326,98 @@ public class UsersResource {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribue un rôle à un utilisateur (réservé au super administrateur).
|
||||
* Requiert le header X-Super-Admin-Key correspondant à afterwork.super-admin.api-key.
|
||||
*
|
||||
* @param id L'ID de l'utilisateur.
|
||||
* @param request Le DTO contenant le nouveau rôle.
|
||||
* @param apiKeyHeader Valeur du header X-Super-Admin-Key (injecté via @HeaderParam).
|
||||
* @return L'utilisateur mis à jour.
|
||||
*/
|
||||
@PUT
|
||||
@Path("/{id}/role")
|
||||
@Transactional
|
||||
@Operation(summary = "Attribuer un rôle à un utilisateur (super admin)",
|
||||
description = "Modifie le rôle d'un utilisateur. Réservé au super administrateur (header X-Super-Admin-Key).")
|
||||
@APIResponse(responseCode = "200", description = "Rôle mis à jour")
|
||||
@APIResponse(responseCode = "403", description = "Clé super admin invalide ou absente")
|
||||
@APIResponse(responseCode = "404", description = "Utilisateur non trouvé")
|
||||
public Response assignRole(
|
||||
@PathParam("id") UUID id,
|
||||
@Valid AssignRoleRequestDTO request,
|
||||
@HeaderParam(SUPER_ADMIN_KEY_HEADER) String apiKeyHeader) {
|
||||
|
||||
String key = superAdminApiKey.orElse("");
|
||||
if (key.isBlank()) {
|
||||
LOG.warn("Opération assignRole 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 assignRole 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.assignRole(id, request.getRole());
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère un token temporaire d'impersonation (Super Admin se connecte en tant qu'un autre utilisateur).
|
||||
* Le client envoie ce token (ex: Authorization: Bearer <token>) pour les requêtes suivantes.
|
||||
* La validation du token côté backend (filter) est à implémenter si nécessaire.
|
||||
*
|
||||
* @param id L'ID de l'utilisateur à impersonner.
|
||||
* @param apiKeyHeader X-Super-Admin-Key.
|
||||
* @return JSON avec impersonationToken, expiresInSeconds, userId.
|
||||
*/
|
||||
@POST
|
||||
@Path("/{id}/impersonate")
|
||||
@Operation(summary = "Impersonation (Super Admin)",
|
||||
description = "Génère un token temporaire pour se connecter en tant que cet utilisateur. Header X-Super-Admin-Key requis.")
|
||||
public Response impersonate(
|
||||
@PathParam("id") UUID id,
|
||||
@HeaderParam(SUPER_ADMIN_KEY_HEADER) String apiKeyHeader) {
|
||||
|
||||
String key = superAdminApiKey.orElse("");
|
||||
if (key.isBlank()) {
|
||||
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)) {
|
||||
return Response.status(Response.Status.FORBIDDEN)
|
||||
.entity("{\"message\": \"Clé super administrateur invalide ou absente.\"}")
|
||||
.build();
|
||||
}
|
||||
|
||||
try {
|
||||
Users user = userService.getUserById(id);
|
||||
// Token temporaire (UUID) — à valider côté backend via un filter si besoin
|
||||
String token = UUID.randomUUID().toString();
|
||||
int expiresInSeconds = 900; // 15 min
|
||||
return Response.ok(Map.of(
|
||||
"impersonationToken", token,
|
||||
"expiresInSeconds", expiresInSeconds,
|
||||
"userId", user.getId().toString(),
|
||||
"email", user.getEmail()
|
||||
)).build();
|
||||
} catch (UserNotFoundException e) {
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity("{\"message\": \"Utilisateur non trouvé.\"}")
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.lions.dev.resource;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.lions.dev.service.WavePaymentService;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
/**
|
||||
* Webhook Wave pour les notifications de paiement (payment.completed, payment.cancelled, etc.).
|
||||
*/
|
||||
@Path("/webhooks/wave")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public class WaveWebhookResource {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(WaveWebhookResource.class);
|
||||
|
||||
@Inject
|
||||
WavePaymentService wavePaymentService;
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@POST
|
||||
public Response handleWebhook(String payload) {
|
||||
try {
|
||||
JsonNode node = objectMapper.readTree(payload);
|
||||
wavePaymentService.handleWebhook(node);
|
||||
return Response.ok().build();
|
||||
} catch (Exception e) {
|
||||
LOG.error("Erreur traitement webhook Wave", e);
|
||||
return Response.status(Response.Status.BAD_REQUEST).build();
|
||||
}
|
||||
}
|
||||
}
|
||||
78
src/main/java/com/lions/dev/service/AdminStatsService.java
Normal file
78
src/main/java/com/lions/dev/service/AdminStatsService.java
Normal file
@@ -0,0 +1,78 @@
|
||||
package com.lions.dev.service;
|
||||
|
||||
import com.lions.dev.dto.response.admin.AdminRevenueResponseDTO;
|
||||
import com.lions.dev.dto.response.admin.ManagerStatsResponseDTO;
|
||||
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.UsersRepository;
|
||||
import com.lions.dev.util.UserRoles;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@ApplicationScoped
|
||||
public class AdminStatsService {
|
||||
|
||||
@Inject
|
||||
EstablishmentSubscriptionRepository subscriptionRepository;
|
||||
@Inject
|
||||
EstablishmentRepository establishmentRepository;
|
||||
@Inject
|
||||
UsersRepository usersRepository;
|
||||
|
||||
/**
|
||||
* Revenus totaux : somme des montants des abonnements actifs.
|
||||
*/
|
||||
public AdminRevenueResponseDTO getRevenue() {
|
||||
List<EstablishmentSubscription> active = subscriptionRepository.list(
|
||||
"status", EstablishmentSubscription.STATUS_ACTIVE);
|
||||
long count = active.size();
|
||||
BigDecimal total = active.stream()
|
||||
.map(s -> s.getAmountXof() != null ? BigDecimal.valueOf(s.getAmountXof()) : BigDecimal.ZERO)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
return new AdminRevenueResponseDTO(total, count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste des managers avec statut (Actif/Suspendu) et date d'expiration d'abonnement.
|
||||
*/
|
||||
public List<ManagerStatsResponseDTO> getManagers() {
|
||||
List<Users> managers = usersRepository.list("role", UserRoles.MANAGER);
|
||||
List<ManagerStatsResponseDTO> result = new ArrayList<>();
|
||||
for (Users manager : managers) {
|
||||
List<Establishment> establishments = establishmentRepository.findByManagerId(manager.getId());
|
||||
UUID establishmentId = null;
|
||||
String establishmentName = null;
|
||||
java.time.LocalDateTime expiresAt = null;
|
||||
String status = manager.isActive() ? "ACTIVE" : "SUSPENDED";
|
||||
if (!establishments.isEmpty()) {
|
||||
Establishment est = establishments.get(0);
|
||||
establishmentId = est.getId();
|
||||
establishmentName = est.getName();
|
||||
Optional<EstablishmentSubscription> sub = subscriptionRepository.findActiveByEstablishmentId(est.getId());
|
||||
if (sub.isPresent()) {
|
||||
expiresAt = sub.get().getExpiresAt();
|
||||
}
|
||||
}
|
||||
result.add(new ManagerStatsResponseDTO(
|
||||
manager.getId(),
|
||||
manager.getEmail(),
|
||||
manager.getFirstName(),
|
||||
manager.getLastName(),
|
||||
status,
|
||||
expiresAt,
|
||||
establishmentId,
|
||||
establishmentName
|
||||
));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
108
src/main/java/com/lions/dev/service/BookingService.java
Normal file
108
src/main/java/com/lions/dev/service/BookingService.java
Normal file
@@ -0,0 +1,108 @@
|
||||
package com.lions.dev.service;
|
||||
|
||||
import com.lions.dev.dto.request.booking.ReservationCreateRequestDTO;
|
||||
import com.lions.dev.dto.response.booking.ReservationResponseDTO;
|
||||
import com.lions.dev.entity.booking.Booking;
|
||||
import com.lions.dev.entity.establishment.Establishment;
|
||||
import com.lions.dev.entity.users.Users;
|
||||
import com.lions.dev.repository.BookingRepository;
|
||||
import com.lions.dev.repository.EstablishmentRepository;
|
||||
import com.lions.dev.repository.UsersRepository;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@ApplicationScoped
|
||||
public class BookingService {
|
||||
|
||||
@Inject
|
||||
BookingRepository bookingRepository;
|
||||
@Inject
|
||||
EstablishmentRepository establishmentRepository;
|
||||
@Inject
|
||||
UsersRepository usersRepository;
|
||||
|
||||
private static final DateTimeFormatter ISO = DateTimeFormatter.ISO_DATE_TIME;
|
||||
|
||||
public List<ReservationResponseDTO> getReservationsByUserId(UUID userId) {
|
||||
return bookingRepository.findByUserId(userId).stream()
|
||||
.map(ReservationResponseDTO::new)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public ReservationResponseDTO getReservationById(UUID id) {
|
||||
Booking booking = bookingRepository.findById(id);
|
||||
if (booking == null) return null;
|
||||
return new ReservationResponseDTO(booking);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ReservationResponseDTO createReservation(ReservationCreateRequestDTO dto) {
|
||||
Establishment establishment = establishmentRepository.findById(dto.getEstablishmentId());
|
||||
Users user = usersRepository.findById(dto.getUserId());
|
||||
if (establishment == null || user == null) {
|
||||
throw new IllegalArgumentException("Établissement ou utilisateur non trouvé");
|
||||
}
|
||||
LocalDateTime reservationTime = parseReservationDate(dto.getReservationDate());
|
||||
Booking booking = new Booking();
|
||||
booking.setEstablishment(establishment);
|
||||
booking.setUser(user);
|
||||
booking.setReservationTime(reservationTime);
|
||||
booking.setGuestCount(dto.getNumberOfPeople() > 0 ? dto.getNumberOfPeople() : 1);
|
||||
booking.setStatus("PENDING");
|
||||
booking.setSpecialRequests(dto.getNotes());
|
||||
bookingRepository.persist(booking);
|
||||
return new ReservationResponseDTO(booking);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ReservationResponseDTO cancelReservation(UUID id) {
|
||||
Booking booking = bookingRepository.findById(id);
|
||||
if (booking == null) return null;
|
||||
booking.setStatus("CANCELLED");
|
||||
bookingRepository.persist(booking);
|
||||
return new ReservationResponseDTO(booking);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteReservation(UUID id) {
|
||||
bookingRepository.deleteById(id);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ReservationResponseDTO updateReservation(UUID id, ReservationCreateRequestDTO dto) {
|
||||
Booking booking = bookingRepository.findById(id);
|
||||
if (booking == null) return null;
|
||||
if (dto.getReservationDate() != null) {
|
||||
booking.setReservationTime(parseReservationDate(dto.getReservationDate()));
|
||||
}
|
||||
if (dto.getNumberOfPeople() > 0) {
|
||||
booking.setGuestCount(dto.getNumberOfPeople());
|
||||
}
|
||||
if (dto.getNotes() != null) {
|
||||
booking.setSpecialRequests(dto.getNotes());
|
||||
}
|
||||
bookingRepository.persist(booking);
|
||||
return new ReservationResponseDTO(booking);
|
||||
}
|
||||
|
||||
private static LocalDateTime parseReservationDate(String value) {
|
||||
if (value == null || value.isBlank()) return LocalDateTime.now();
|
||||
try {
|
||||
return LocalDateTime.parse(value, ISO);
|
||||
} catch (Exception e) {
|
||||
try {
|
||||
int len = Math.min(19, value.length());
|
||||
return LocalDateTime.parse(value.substring(0, len));
|
||||
} catch (Exception e2) {
|
||||
return LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,6 +75,7 @@ public class EstablishmentService {
|
||||
"SELECT DISTINCT e FROM Establishment e " +
|
||||
"LEFT JOIN FETCH e.medias m " +
|
||||
"LEFT JOIN FETCH e.manager " +
|
||||
"WHERE (e.isActive IS NULL OR e.isActive = true) " +
|
||||
"ORDER BY e.name ASC",
|
||||
Establishment.class
|
||||
)
|
||||
|
||||
@@ -152,7 +152,7 @@ public class EventService {
|
||||
notificationEmitter.send(kafkaEvent);
|
||||
logger.debug("[logger] Événement event_created publié dans Kafka pour: {}", friend.getId());
|
||||
} catch (Exception kafkaEx) {
|
||||
logger.error("[ERROR] Erreur publication Kafka: {}", kafkaEx.getMessage());
|
||||
logger.error("[ERROR] Erreur publication Kafka pour événement {}", event.getId(), kafkaEx);
|
||||
// Ne pas bloquer si Kafka échoue
|
||||
}
|
||||
}
|
||||
@@ -424,14 +424,15 @@ public class EventService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les événements par localisation.
|
||||
* Récupère les événements par localisation (ville ou adresse de l'établissement).
|
||||
* v2.0 : plus de colonne location ; recherche sur establishment.address et establishment.city.
|
||||
*
|
||||
* @param location La localisation des événements.
|
||||
* @return La liste des événements situés à cette localisation.
|
||||
* @param location Fragment de localisation (ville ou adresse).
|
||||
* @return La liste des événements dont l'établissement matche.
|
||||
*/
|
||||
public List<Events> findEventsByLocation(String location) {
|
||||
logger.info("[logger] Récupération des événements pour la localisation : " + location);
|
||||
List<Events> events = eventsRepository.find("location", location).list();
|
||||
List<Events> events = eventsRepository.findEventsByEstablishmentLocation(location);
|
||||
logger.info("[logger] Nombre d'événements trouvés pour la localisation '" + location + "' : " + events.size());
|
||||
return events;
|
||||
}
|
||||
|
||||
@@ -56,6 +56,11 @@ public class FriendshipService {
|
||||
*/
|
||||
@Transactional
|
||||
public FriendshipCreateOneResponseDTO sendFriendRequest(FriendshipCreateOneRequestDTO request) {
|
||||
if (request == null || request.getUserId() == null || request.getFriendId() == null) {
|
||||
logger.error("[ERROR] Requête invalide : userId ou friendId manquant");
|
||||
throw new IllegalArgumentException("L'identifiant de l'utilisateur et de l'ami sont requis.");
|
||||
}
|
||||
|
||||
logger.info("[LOG] Envoi d'une demande d'amitié de l'utilisateur " + request.getUserId() + " à l'utilisateur " + request.getFriendId());
|
||||
|
||||
// Récupérer les utilisateurs concernés
|
||||
|
||||
@@ -187,7 +187,7 @@ public class MessageService {
|
||||
// Ne pas bloquer si la confirmation échoue
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.out.println("[ERROR] Erreur lors de la publication dans Kafka : " + e.getMessage());
|
||||
io.quarkus.logging.Log.error("[ERROR] Erreur lors de la publication dans Kafka pour message " + message.getId(), e);
|
||||
// Ne pas bloquer l'envoi du message si Kafka échoue
|
||||
}
|
||||
|
||||
|
||||
@@ -263,4 +263,24 @@ public class UsersService {
|
||||
Optional<Users> userOptional = usersRepository.findByEmail(email);
|
||||
return userOptional.orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribue un rôle à un utilisateur (réservé au super administrateur).
|
||||
*
|
||||
* @param userId L'ID de l'utilisateur à modifier.
|
||||
* @param newRole Le nouveau rôle (SUPER_ADMIN, ADMIN, MANAGER, USER).
|
||||
* @return L'utilisateur mis à jour.
|
||||
* @throws UserNotFoundException Si l'utilisateur n'existe pas.
|
||||
*/
|
||||
@Transactional
|
||||
public Users assignRole(UUID userId, String newRole) {
|
||||
Users user = usersRepository.findById(userId);
|
||||
if (user == null) {
|
||||
throw new UserNotFoundException("Utilisateur non trouvé avec l'ID : " + userId);
|
||||
}
|
||||
user.setRole(newRole);
|
||||
usersRepository.persist(user);
|
||||
System.out.println("[LOG] Rôle attribué à " + user.getEmail() + " : " + newRole);
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
216
src/main/java/com/lions/dev/service/WavePaymentService.java
Normal file
216
src/main/java/com/lions/dev/service/WavePaymentService.java
Normal file
@@ -0,0 +1,216 @@
|
||||
package com.lions.dev.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
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.EstablishmentPayment;
|
||||
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 jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Service d'intégration Wave pour le paiement des droits d'accès des établissements.
|
||||
* Crée une session de paiement Wave et traite les webhooks (payment.completed, etc.).
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class WavePaymentService {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(WavePaymentService.class);
|
||||
|
||||
private static final int MONTHLY_AMOUNT_XOF = 15_000;
|
||||
private static final int YEARLY_AMOUNT_XOF = 150_000;
|
||||
|
||||
@Inject
|
||||
EstablishmentRepository establishmentRepository;
|
||||
@Inject
|
||||
EstablishmentSubscriptionRepository subscriptionRepository;
|
||||
@Inject
|
||||
EstablishmentPaymentRepository paymentRepository;
|
||||
@Inject
|
||||
UsersRepository usersRepository;
|
||||
|
||||
@ConfigProperty(name = "wave.api.url", defaultValue = "https://api.wave.com")
|
||||
String waveApiUrl;
|
||||
|
||||
@ConfigProperty(name = "wave.api.key", defaultValue = "")
|
||||
Optional<String> waveApiKey;
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private final HttpClient httpClient = HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(10))
|
||||
.build();
|
||||
|
||||
/**
|
||||
* Initie un paiement Wave pour un établissement (droits d'accès).
|
||||
* Crée une session Wave et retourne l'URL de redirection.
|
||||
*/
|
||||
@Transactional
|
||||
public InitiateSubscriptionResponseDTO initiatePayment(UUID establishmentId, String plan, String clientPhone) {
|
||||
Establishment establishment = establishmentRepository.findById(establishmentId);
|
||||
if (establishment == null) {
|
||||
throw new IllegalArgumentException("Établissement non trouvé : " + establishmentId);
|
||||
}
|
||||
|
||||
int amountXof = EstablishmentSubscription.PLAN_MONTHLY.equals(plan) ? MONTHLY_AMOUNT_XOF : YEARLY_AMOUNT_XOF;
|
||||
String description = "AfterWork - Abonnement " + plan + " - " + establishment.getName();
|
||||
|
||||
EstablishmentSubscription subscription = new EstablishmentSubscription();
|
||||
subscription.setEstablishmentId(establishmentId);
|
||||
subscription.setPlan(plan);
|
||||
subscription.setStatus(EstablishmentSubscription.STATUS_PENDING);
|
||||
subscription.setAmountXof(amountXof);
|
||||
subscriptionRepository.persist(subscription);
|
||||
|
||||
EstablishmentPayment payment = new EstablishmentPayment();
|
||||
payment.setEstablishmentId(establishmentId);
|
||||
payment.setAmountXof(amountXof);
|
||||
payment.setStatus(EstablishmentPayment.STATUS_PENDING);
|
||||
payment.setClientPhone(clientPhone);
|
||||
payment.setPlan(plan);
|
||||
paymentRepository.persist(payment);
|
||||
|
||||
String waveSessionId = null;
|
||||
String paymentUrl = null;
|
||||
|
||||
String apiKey = waveApiKey.orElse("");
|
||||
if (!apiKey.isBlank()) {
|
||||
try {
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("amount", amountXof);
|
||||
body.put("currency", "XOF");
|
||||
body.put("description", description);
|
||||
body.put("client_reference", subscription.getId().toString());
|
||||
body.put("customer_phone_number", clientPhone.startsWith("+") ? clientPhone : "+" + clientPhone);
|
||||
|
||||
String bodyJson = objectMapper.writeValueAsString(body);
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(waveApiUrl + "/wave/api/v1/checkout/sessions"))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", "Bearer " + apiKey)
|
||||
.timeout(Duration.ofSeconds(15))
|
||||
.POST(HttpRequest.BodyPublishers.ofString(bodyJson))
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
if (response.statusCode() >= 200 && response.statusCode() < 300) {
|
||||
JsonNode node = objectMapper.readTree(response.body());
|
||||
waveSessionId = node.has("id") ? node.get("id").asText() : null;
|
||||
paymentUrl = node.has("payment_url") ? node.get("payment_url").asText() : null;
|
||||
} else {
|
||||
LOG.warn("Wave API error: " + response.statusCode() + " " + response.body());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.error("Erreur appel Wave API", e);
|
||||
}
|
||||
} else {
|
||||
LOG.warn("Wave API key non configurée : utilisation d'une URL de test");
|
||||
paymentUrl = "https://checkout.wave.com/session/test?ref=" + subscription.getId();
|
||||
waveSessionId = "test-" + subscription.getId();
|
||||
}
|
||||
|
||||
subscription.setWaveSessionId(waveSessionId);
|
||||
payment.setWaveSessionId(waveSessionId);
|
||||
subscriptionRepository.persist(subscription);
|
||||
paymentRepository.persist(payment);
|
||||
|
||||
return new InitiateSubscriptionResponseDTO(
|
||||
paymentUrl != null ? paymentUrl : "",
|
||||
waveSessionId,
|
||||
amountXof,
|
||||
plan,
|
||||
EstablishmentSubscription.STATUS_PENDING
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si un établissement a un abonnement actif (droits d'accès payés).
|
||||
*/
|
||||
public boolean hasActiveSubscription(UUID establishmentId) {
|
||||
return subscriptionRepository.findActiveByEstablishmentId(establishmentId).isPresent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Traite un webhook Wave (payment.completed, payment.cancelled, etc.).
|
||||
* Met à jour l'abonnement et le paiement.
|
||||
*/
|
||||
@Transactional
|
||||
public void handleWebhook(JsonNode payload) {
|
||||
String eventType = payload.has("type") ? payload.get("type").asText() : null;
|
||||
JsonNode data = payload.has("data") ? payload.get("data") : null;
|
||||
if (data == null) return;
|
||||
|
||||
String sessionId = data.has("id") ? data.get("id").asText() : (data.has("session_id") ? data.get("session_id").asText() : null);
|
||||
if (sessionId == null) return;
|
||||
|
||||
subscriptionRepository.findByWaveSessionId(sessionId).ifPresent(sub -> {
|
||||
UUID establishmentId = sub.getEstablishmentId();
|
||||
Establishment establishment = establishmentRepository.findById(establishmentId);
|
||||
|
||||
if ("payment.completed".equals(eventType)) {
|
||||
sub.setStatus(EstablishmentSubscription.STATUS_ACTIVE);
|
||||
sub.setPaidAt(LocalDateTime.now());
|
||||
if (EstablishmentSubscription.PLAN_MONTHLY.equals(sub.getPlan())) {
|
||||
sub.setExpiresAt(LocalDateTime.now().plusMonths(1));
|
||||
} else {
|
||||
sub.setExpiresAt(LocalDateTime.now().plusYears(1));
|
||||
}
|
||||
subscriptionRepository.persist(sub);
|
||||
// Activer l'établissement et le manager
|
||||
if (establishment != null) {
|
||||
establishment.setIsActive(true);
|
||||
establishmentRepository.persist(establishment);
|
||||
Users manager = establishment.getManager();
|
||||
if (manager != null) {
|
||||
manager.setActive(true);
|
||||
usersRepository.persist(manager);
|
||||
LOG.info("Webhook Wave: établissement et manager activés pour " + establishmentId);
|
||||
}
|
||||
}
|
||||
} else if ("payment.cancelled".equals(eventType) || "payment.expired".equals(eventType)
|
||||
|| "payment.failed".equals(eventType)) {
|
||||
sub.setStatus(EstablishmentSubscription.STATUS_CANCELLED);
|
||||
subscriptionRepository.persist(sub);
|
||||
// Suspendre l'établissement et le manager
|
||||
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: établissement et manager suspendus pour " + establishmentId);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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)) {
|
||||
payment.setStatus(EstablishmentPayment.STATUS_CANCELLED);
|
||||
}
|
||||
paymentRepository.persist(payment);
|
||||
});
|
||||
}
|
||||
}
|
||||
46
src/main/java/com/lions/dev/util/UserRole.java
Normal file
46
src/main/java/com/lions/dev/util/UserRole.java
Normal file
@@ -0,0 +1,46 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
36
src/main/java/com/lions/dev/util/UserRoles.java
Normal file
36
src/main/java/com/lions/dev/util/UserRoles.java
Normal file
@@ -0,0 +1,36 @@
|
||||
package com.lions.dev.util;
|
||||
|
||||
/**
|
||||
* Rôles utilisateur de l'application AfterWork.
|
||||
* Hiérarchie : SUPER_ADMIN > ADMIN > MANAGER > USER.
|
||||
*/
|
||||
public final class UserRoles {
|
||||
|
||||
private UserRoles() {}
|
||||
|
||||
/** Super administrateur : tous les droits (gestion utilisateurs, attribution de rôles, etc.). */
|
||||
public static final String SUPER_ADMIN = "SUPER_ADMIN";
|
||||
|
||||
/** Administrateur : gestion courante de l'application. */
|
||||
public static final String ADMIN = "ADMIN";
|
||||
|
||||
/** Manager : gestion d'établissements, événements, etc. */
|
||||
public static final String MANAGER = "MANAGER";
|
||||
|
||||
/** Utilisateur standard. */
|
||||
public static final String USER = "USER";
|
||||
|
||||
/**
|
||||
* Indique si le rôle a les droits super administrateur (ou est SUPER_ADMIN).
|
||||
*/
|
||||
public static boolean isSuperAdmin(String role) {
|
||||
return SUPER_ADMIN.equals(role);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indique 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);
|
||||
}
|
||||
}
|
||||
@@ -81,25 +81,30 @@ public class ChatWebSocketNext {
|
||||
String userId = connection.pathParam("userId");
|
||||
Log.debug("[CHAT-WS-NEXT] Message reçu de " + userId + ": " + message);
|
||||
|
||||
// Parser le message JSON
|
||||
com.fasterxml.jackson.databind.ObjectMapper mapper =
|
||||
new com.fasterxml.jackson.databind.ObjectMapper();
|
||||
Map<String, Object> messageData = mapper.readValue(message, Map.class);
|
||||
|
||||
String type = (String) messageData.get("type");
|
||||
Map<String, Object> raw = mapper.readValue(message, Map.class);
|
||||
String type = (String) raw.get("type");
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> data = (Map<String, Object>) raw.get("data");
|
||||
|
||||
switch (type) {
|
||||
case "message":
|
||||
handleChatMessage(messageData, userId);
|
||||
if (data != null) handleChatMessage(data, userId);
|
||||
else Log.warn("[CHAT-WS-NEXT] Message sans 'data'");
|
||||
break;
|
||||
case "typing":
|
||||
handleTypingIndicator(messageData, userId);
|
||||
if (data != null) handleTypingIndicator(data, userId);
|
||||
break;
|
||||
case "read":
|
||||
handleReadReceipt(messageData, userId);
|
||||
if (data != null) handleReadReceipt(data, userId);
|
||||
else Log.warn("[CHAT-WS-NEXT] Read receipt sans 'data'");
|
||||
break;
|
||||
case "ping":
|
||||
// Heartbeat - ignorer
|
||||
break;
|
||||
default:
|
||||
Log.warn("[CHAT-WS-NEXT] Type de message inconnu: " + type);
|
||||
Log.warn("[CHAT-WS-NEXT] Type inconnu: " + type);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
@@ -108,16 +113,17 @@ public class ChatWebSocketNext {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gère l'envoi d'un message de chat.
|
||||
* Le message est traité par MessageService qui publiera dans Kafka.
|
||||
* Gère l'envoi d'un message de chat via WebSocket.
|
||||
* Note: L'envoi principal passe par REST (POST /messages). Cette méthode
|
||||
* est pour compatibilité si le client envoie via WebSocket.
|
||||
*/
|
||||
private void handleChatMessage(Map<String, Object> messageData, String senderId) {
|
||||
private void handleChatMessage(Map<String, Object> data, String senderId) {
|
||||
try {
|
||||
UUID senderUUID = UUID.fromString(senderId);
|
||||
UUID recipientUUID = UUID.fromString((String) messageData.get("recipientId"));
|
||||
String content = (String) messageData.get("content");
|
||||
String messageType = messageData.getOrDefault("messageType", "text").toString();
|
||||
String mediaUrl = (String) messageData.get("mediaUrl");
|
||||
UUID recipientUUID = UUID.fromString((String) data.get("recipientId"));
|
||||
String content = (String) data.get("content");
|
||||
String messageType = data.getOrDefault("messageType", "text").toString();
|
||||
String mediaUrl = (String) data.get("mediaUrl");
|
||||
|
||||
// Enregistrer le message dans la base de données
|
||||
// MessageService publiera automatiquement dans Kafka
|
||||
@@ -146,13 +152,21 @@ public class ChatWebSocketNext {
|
||||
|
||||
/**
|
||||
* Gère les indicateurs de frappe.
|
||||
* data doit contenir recipientId (ID du destinataire) et isTyping.
|
||||
*/
|
||||
private void handleTypingIndicator(Map<String, Object> messageData, String userId) {
|
||||
private void handleTypingIndicator(Map<String, Object> data, String userId) {
|
||||
try {
|
||||
UUID recipientUUID = UUID.fromString((String) messageData.get("recipientId"));
|
||||
boolean isTyping = (boolean) messageData.getOrDefault("isTyping", false);
|
||||
Object recipientIdObj = data.get("recipientId");
|
||||
if (recipientIdObj == null) {
|
||||
Log.warn("[CHAT-WS-NEXT] Typing sans recipientId - ignoré");
|
||||
return;
|
||||
}
|
||||
UUID recipientUUID = UUID.fromString(recipientIdObj.toString());
|
||||
Object isTypingObj = data.get("isTyping");
|
||||
boolean isTyping = isTypingObj instanceof Boolean ? (Boolean) isTypingObj : Boolean.parseBoolean(String.valueOf(isTypingObj));
|
||||
|
||||
String response = buildJsonMessage("typing", Map.of(
|
||||
"conversationId", data.getOrDefault("conversationId", ""),
|
||||
"userId", userId,
|
||||
"isTyping", isTyping
|
||||
));
|
||||
@@ -168,22 +182,22 @@ public class ChatWebSocketNext {
|
||||
|
||||
/**
|
||||
* Gère les confirmations de lecture.
|
||||
* Envoie type "read" (format attendu par le client Flutter).
|
||||
*/
|
||||
private void handleReadReceipt(Map<String, Object> messageData, String userId) {
|
||||
private void handleReadReceipt(Map<String, Object> data, String userId) {
|
||||
try {
|
||||
UUID messageUUID = UUID.fromString((String) messageData.get("messageId"));
|
||||
UUID messageUUID = UUID.fromString((String) data.get("messageId"));
|
||||
|
||||
// Marquer le message comme lu
|
||||
Message message = messageService.markMessageAsRead(messageUUID);
|
||||
|
||||
if (message != null) {
|
||||
// Envoyer confirmation de lecture à l'expéditeur via WebSocket
|
||||
// (sera aussi publié dans Kafka par MessageService)
|
||||
UUID senderUUID = message.getSender().getId();
|
||||
String response = buildJsonMessage("read_receipt", Map.of(
|
||||
long now = System.currentTimeMillis();
|
||||
String timestampIso = java.time.Instant.ofEpochMilli(now).toString();
|
||||
String response = buildJsonMessage("read", Map.of(
|
||||
"messageId", messageUUID.toString(),
|
||||
"readBy", userId,
|
||||
"readAt", System.currentTimeMillis()
|
||||
"userId", userId,
|
||||
"timestamp", timestampIso
|
||||
));
|
||||
|
||||
sendToUser(senderUUID, response);
|
||||
|
||||
@@ -3,48 +3,51 @@ package com.lions.dev.websocket.bridge;
|
||||
import com.lions.dev.dto.events.ChatMessageEvent;
|
||||
import com.lions.dev.websocket.ChatWebSocketNext;
|
||||
import io.quarkus.logging.Log;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import org.eclipse.microprofile.reactive.messaging.Incoming;
|
||||
import org.eclipse.microprofile.reactive.messaging.Message;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.time.Instant;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
|
||||
/**
|
||||
* Bridge qui consomme depuis Kafka et envoie via WebSocket pour le chat.
|
||||
*
|
||||
* Architecture:
|
||||
* Architecture (best practice):
|
||||
* MessageService → Kafka Topic (chat.messages) → Bridge → WebSocket → Client
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class ChatKafkaBridge {
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
Log.info("[KAFKA-BRIDGE] Bridge démarré pour topic: chat.messages");
|
||||
}
|
||||
|
||||
/**
|
||||
* Consomme les messages chat depuis Kafka et les route vers WebSocket.
|
||||
*
|
||||
* @param message Message Kafka contenant un ChatMessageEvent
|
||||
* @return CompletionStage pour gérer l'ack/nack asynchrone
|
||||
*/
|
||||
@Incoming("kafka-chat")
|
||||
public CompletionStage<Void> processChatMessage(Message<ChatMessageEvent> message) {
|
||||
try {
|
||||
ChatMessageEvent event = message.getPayload();
|
||||
|
||||
Log.debug("[CHAT-BRIDGE] Message reçu: " + event.getEventType() +
|
||||
Log.debug("[CHAT-BRIDGE] Événement reçu: " + event.getEventType() +
|
||||
" de " + event.getSenderId() + " à " + event.getRecipientId());
|
||||
|
||||
UUID recipientId = UUID.fromString(event.getRecipientId());
|
||||
|
||||
// Construire le message JSON pour WebSocket
|
||||
String wsMessage = buildWebSocketMessage(event);
|
||||
|
||||
// Envoyer via WebSocket au destinataire
|
||||
ChatWebSocketNext.sendMessageToUser(recipientId, wsMessage);
|
||||
|
||||
Log.debug("[CHAT-BRIDGE] Message routé vers WebSocket pour: " + event.getRecipientId());
|
||||
|
||||
// Acknowledger le message Kafka
|
||||
return message.ack();
|
||||
|
||||
} catch (IllegalArgumentException e) {
|
||||
@@ -57,29 +60,66 @@ public class ChatKafkaBridge {
|
||||
}
|
||||
|
||||
/**
|
||||
* Construit le message JSON pour WebSocket à partir de l'événement Kafka.
|
||||
* Construit le message JSON pour WebSocket (format attendu par le client Flutter).
|
||||
*/
|
||||
private String buildWebSocketMessage(ChatMessageEvent event) {
|
||||
try {
|
||||
com.fasterxml.jackson.databind.ObjectMapper mapper =
|
||||
new com.fasterxml.jackson.databind.ObjectMapper();
|
||||
|
||||
Map<String, Object> messageData = new java.util.HashMap<>();
|
||||
messageData.put("id", event.getMessageId());
|
||||
messageData.put("conversationId", event.getConversationId());
|
||||
messageData.put("senderId", event.getSenderId());
|
||||
messageData.put("recipientId", event.getRecipientId());
|
||||
messageData.put("content", event.getContent());
|
||||
messageData.put("timestamp", event.getTimestamp());
|
||||
if (event.getMetadata() != null) {
|
||||
messageData.putAll(event.getMetadata());
|
||||
String eventType = event.getEventType() != null ? event.getEventType() : "message";
|
||||
long ts = event.getTimestamp() != null ? event.getTimestamp() : System.currentTimeMillis();
|
||||
String timestampIso = DateTimeFormatter.ISO_INSTANT.format(Instant.ofEpochMilli(ts));
|
||||
|
||||
Map<String, Object> data;
|
||||
String type;
|
||||
|
||||
switch (eventType) {
|
||||
case "delivery_confirmation":
|
||||
type = "delivered";
|
||||
data = new HashMap<>();
|
||||
data.put("messageId", event.getMessageId());
|
||||
data.put("isDelivered", event.getMetadata() != null &&
|
||||
Boolean.TRUE.equals(event.getMetadata().get("isDelivered")));
|
||||
data.put("timestamp", ts);
|
||||
break;
|
||||
|
||||
case "read_confirmation":
|
||||
type = "read";
|
||||
data = new HashMap<>();
|
||||
data.put("messageId", event.getMessageId());
|
||||
data.put("userId", event.getMetadata() != null ?
|
||||
event.getMetadata().get("readBy") : event.getSenderId());
|
||||
data.put("timestamp", timestampIso);
|
||||
break;
|
||||
|
||||
case "message":
|
||||
default:
|
||||
type = "message";
|
||||
data = new HashMap<>();
|
||||
data.put("id", event.getMessageId());
|
||||
data.put("conversationId", event.getConversationId());
|
||||
data.put("senderId", event.getSenderId());
|
||||
data.put("recipientId", event.getRecipientId());
|
||||
data.put("content", event.getContent() != null ? event.getContent() : "");
|
||||
data.put("timestamp", timestampIso);
|
||||
data.put("isRead", event.getMetadata() != null &&
|
||||
Boolean.TRUE.equals(event.getMetadata().get("isRead")));
|
||||
data.put("isDelivered", true);
|
||||
if (event.getMetadata() != null) {
|
||||
data.put("senderFirstName", event.getMetadata().getOrDefault("senderFirstName", ""));
|
||||
data.put("senderLastName", event.getMetadata().getOrDefault("senderLastName", ""));
|
||||
data.put("senderProfileImageUrl", event.getMetadata().getOrDefault("senderProfileImageUrl", ""));
|
||||
data.put("attachmentUrl", event.getMetadata().getOrDefault("attachmentUrl", ""));
|
||||
data.put("attachmentType", event.getMetadata().getOrDefault("attachmentType", "text"));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
java.util.Map<String, Object> wsMessage = java.util.Map.of(
|
||||
"type", event.getEventType() != null ? event.getEventType() : "message",
|
||||
"data", messageData,
|
||||
"timestamp", event.getTimestamp() != null ? event.getTimestamp() : System.currentTimeMillis()
|
||||
);
|
||||
Map<String, Object> wsMessage = new HashMap<>();
|
||||
wsMessage.put("type", type);
|
||||
wsMessage.put("data", data);
|
||||
wsMessage.put("timestamp", ts);
|
||||
|
||||
return mapper.writeValueAsString(wsMessage);
|
||||
} catch (Exception e) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.lions.dev.websocket.bridge;
|
||||
import com.lions.dev.dto.events.NotificationEvent;
|
||||
import com.lions.dev.websocket.NotificationWebSocketNext;
|
||||
import io.quarkus.logging.Log;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import org.eclipse.microprofile.reactive.messaging.Incoming;
|
||||
import org.eclipse.microprofile.reactive.messaging.Message;
|
||||
@@ -25,6 +26,11 @@ import java.util.concurrent.CompletionStage;
|
||||
@ApplicationScoped
|
||||
public class NotificationKafkaBridge {
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
Log.info("[KAFKA-BRIDGE] Bridge démarré pour topic: notifications");
|
||||
}
|
||||
|
||||
/**
|
||||
* Consomme les événements depuis Kafka et les route vers WebSocket.
|
||||
*
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.lions.dev.websocket.bridge;
|
||||
import com.lions.dev.dto.events.PresenceEvent;
|
||||
import com.lions.dev.websocket.NotificationWebSocketNext;
|
||||
import io.quarkus.logging.Log;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import org.eclipse.microprofile.reactive.messaging.Incoming;
|
||||
import org.eclipse.microprofile.reactive.messaging.Message;
|
||||
@@ -23,6 +24,11 @@ import java.util.concurrent.CompletionStage;
|
||||
@ApplicationScoped
|
||||
public class PresenceKafkaBridge {
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
Log.info("[KAFKA-BRIDGE] Bridge démarré pour topic: presence.updates");
|
||||
}
|
||||
|
||||
/**
|
||||
* Consomme les événements de présence depuis Kafka et les route vers WebSocket.
|
||||
*
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.lions.dev.websocket.bridge;
|
||||
import com.lions.dev.dto.events.ReactionEvent;
|
||||
import com.lions.dev.websocket.NotificationWebSocketNext;
|
||||
import io.quarkus.logging.Log;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import org.eclipse.microprofile.reactive.messaging.Incoming;
|
||||
import org.eclipse.microprofile.reactive.messaging.Message;
|
||||
@@ -22,6 +23,11 @@ import java.util.concurrent.CompletionStage;
|
||||
@ApplicationScoped
|
||||
public class ReactionKafkaBridge {
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
Log.info("[KAFKA-BRIDGE] Bridge démarré pour topic: reactions");
|
||||
}
|
||||
|
||||
/**
|
||||
* Consomme les réactions depuis Kafka et les route vers WebSocket.
|
||||
*
|
||||
|
||||
@@ -4,6 +4,13 @@
|
||||
# Ce fichier est automatiquement chargé avec: mvn quarkus:dev
|
||||
# Les configurations ici surchargent celles de application.properties
|
||||
|
||||
# ====================================================================
|
||||
# Super administrateur (dev)
|
||||
# ====================================================================
|
||||
# En dev, clé par défaut pour PUT /users/{id}/role (header X-Super-Admin-Key).
|
||||
# Saisir "dev-super-admin-key" dans l'app (Paramètres → Super Admin) pour attribuer des rôles.
|
||||
afterwork.super-admin.api-key=${SUPER_ADMIN_API_KEY:dev-super-admin-key}
|
||||
|
||||
# ====================================================================
|
||||
# Base de données H2 (en mémoire)
|
||||
# ====================================================================
|
||||
@@ -24,6 +31,12 @@ quarkus.hibernate-orm.packages=com.lions.dev.entity
|
||||
# Forcer la création du schéma au démarrage
|
||||
quarkus.hibernate-orm.schema-generation.scripts.action=drop-and-create
|
||||
|
||||
# ====================================================================
|
||||
# Kafka (développement local)
|
||||
# ====================================================================
|
||||
# En dev, défaut localhost:9092. Définir KAFKA_BOOTSTRAP_SERVERS si Kafka tourne ailleurs.
|
||||
kafka.bootstrap.servers=${KAFKA_BOOTSTRAP_SERVERS:localhost:9092}
|
||||
|
||||
# ====================================================================
|
||||
# Logging
|
||||
# ====================================================================
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# ====================================================================
|
||||
# AfterWork Server - Configuration PRODUCTION
|
||||
# AfterWork Server - Configuration PRODUCTION (profil prod)
|
||||
# ====================================================================
|
||||
# Ce fichier est automatiquement chargé avec: java -jar app.jar
|
||||
# Les configurations ici surchargent celles de application.properties
|
||||
# Chargé avec QUARKUS_PROFILE=prod (Kubernetes ConfigMap).
|
||||
# Ce fichier remplace application-production.properties pour cohérence
|
||||
# avec le déploiement (QUARKUS_PROFILE=prod).
|
||||
|
||||
# ====================================================================
|
||||
# HTTP - Chemin de base de l'API
|
||||
@@ -19,34 +20,26 @@
|
||||
# - pathType: Prefix
|
||||
# - PAS d'annotation rewrite-target
|
||||
#
|
||||
# Pourquoi cette approche ?
|
||||
# - Swagger UI nécessite que l'application connaisse son contexte
|
||||
# - Les URLs générées (OpenAPI, WebSocket) sont correctes
|
||||
# - Cohérent avec les applications context-aware (btpxpress, etc.)
|
||||
quarkus.http.root-path=/afterwork
|
||||
|
||||
# ====================================================================
|
||||
# Swagger/OpenAPI (Production)
|
||||
# ====================================================================
|
||||
# Configuration pour que Swagger UI fonctionne avec root-path
|
||||
quarkus.swagger-ui.enable=true
|
||||
quarkus.swagger-ui.always-include=true
|
||||
quarkus.swagger-ui.path=/q/swagger-ui
|
||||
# Configuration du chemin OpenAPI (relatif au root-path)
|
||||
quarkus.smallrye-openapi.path=/openapi
|
||||
# Configuration des serveurs OpenAPI pour que Swagger UI génère les bonnes URLs
|
||||
quarkus.smallrye-openapi.servers=https://api.lions.dev/afterwork
|
||||
# Configuration explicite de l'URL OpenAPI pour Swagger UI
|
||||
# Essayer avec URL absolue pour forcer le bon chemin
|
||||
quarkus.swagger-ui.urls.default=https://api.lions.dev/afterwork/openapi
|
||||
quarkus.smallrye-openapi.servers=https://api.lions.dev
|
||||
|
||||
# ====================================================================
|
||||
# Base de données PostgreSQL
|
||||
# ====================================================================
|
||||
# IMPORTANT: Les credentials doivent être fournis via variables d'environnement
|
||||
# en production (Kubernetes Secrets ou Vault).
|
||||
quarkus.datasource.db-kind=postgresql
|
||||
quarkus.datasource.jdbc.url=jdbc:postgresql://${DB_HOST:postgresql}:${DB_PORT:5432}/${DB_NAME:mic-after-work-server-impl-quarkus-main}
|
||||
quarkus.datasource.jdbc.url=jdbc:postgresql://${DB_HOST:postgresql-service.postgresql.svc.cluster.local}:${DB_PORT:5432}/${DB_NAME:mic-after-work-server-impl-quarkus-main}
|
||||
quarkus.datasource.username=${DB_USERNAME:lionsuser}
|
||||
quarkus.datasource.password=${DB_PASSWORD:LionsUser2025!}
|
||||
quarkus.datasource.password=${DB_PASSWORD}
|
||||
quarkus.datasource.jdbc.driver=org.postgresql.Driver
|
||||
quarkus.datasource.jdbc.max-size=20
|
||||
quarkus.datasource.jdbc.min-size=5
|
||||
@@ -94,42 +87,18 @@ quarkus.log.category."io.quarkus".level=INFO
|
||||
# ====================================================================
|
||||
# Kafka Configuration (Production)
|
||||
# ====================================================================
|
||||
# Kafka est deploye dans le namespace 'kafka' du cluster Kubernetes
|
||||
# Service: kafka-service.kafka.svc.cluster.local:9092
|
||||
kafka.bootstrap.servers=${KAFKA_BOOTSTRAP_SERVERS:kafka-service.kafka.svc.cluster.local:9092}
|
||||
|
||||
# Configuration de resilience Kafka
|
||||
mp.messaging.connector.smallrye-kafka.health-enabled=true
|
||||
mp.messaging.connector.smallrye-kafka.health-readiness-enabled=true
|
||||
|
||||
# Topics auto-crees par Quarkus SmallRye Kafka:
|
||||
# - notifications
|
||||
# - chat.messages
|
||||
# - reactions
|
||||
# - presence.updates
|
||||
|
||||
# ====================================================================
|
||||
# WebSocket
|
||||
# ====================================================================
|
||||
# Note: La propriété quarkus.websocket.max-frame-size n'existe pas dans Quarkus 3.16
|
||||
# Les WebSockets Next utilisent une configuration différente si nécessaire
|
||||
|
||||
# ====================================================================
|
||||
# SSL/TLS (géré par le reverse proxy)
|
||||
# WebSocket / SSL / Performance / Localisation
|
||||
# ====================================================================
|
||||
quarkus.http.ssl.certificate.files=
|
||||
quarkus.http.ssl.certificate.key-files=
|
||||
quarkus.http.insecure-requests=enabled
|
||||
|
||||
# ====================================================================
|
||||
# Performance
|
||||
# ====================================================================
|
||||
quarkus.thread-pool.core-threads=2
|
||||
quarkus.thread-pool.max-threads=16
|
||||
quarkus.thread-pool.queue-size=100
|
||||
|
||||
# ====================================================================
|
||||
# Localisation
|
||||
# ====================================================================
|
||||
quarkus.locales=fr-FR,en-US
|
||||
quarkus.default-locale=fr-FR
|
||||
@@ -17,6 +17,23 @@ quarkus.swagger-ui.always-include=true
|
||||
quarkus.swagger-ui.path=/q/swagger-ui
|
||||
quarkus.smallrye-openapi.path=/openapi
|
||||
|
||||
# ====================================================================
|
||||
# Super administrateur (créé au démarrage si absent)
|
||||
# ====================================================================
|
||||
# En production, définir via variables d'environnement (SUPER_ADMIN_EMAIL, SUPER_ADMIN_PASSWORD).
|
||||
afterwork.super-admin.email=${SUPER_ADMIN_EMAIL:superadmin@afterwork.lions.dev}
|
||||
afterwork.super-admin.password=${SUPER_ADMIN_PASSWORD:SuperAdmin2025!}
|
||||
afterwork.super-admin.first-name=${SUPER_ADMIN_FIRST_NAME:Super}
|
||||
afterwork.super-admin.last-name=${SUPER_ADMIN_LAST_NAME:Administrator}
|
||||
# Clé secrète pour les opérations admin (header X-Super-Admin-Key sur PUT /users/{id}/role, etc.)
|
||||
afterwork.super-admin.api-key=${SUPER_ADMIN_API_KEY:}
|
||||
|
||||
# ====================================================================
|
||||
# Wave API (paiement droits d'accès établissements)
|
||||
# ====================================================================
|
||||
wave.api.url=${WAVE_API_URL:https://api.wave.com}
|
||||
wave.api.key=${WAVE_API_KEY:}
|
||||
|
||||
# ====================================================================
|
||||
# HTTP (commun à tous les environnements)
|
||||
# ====================================================================
|
||||
@@ -62,24 +79,28 @@ kafka.bootstrap.servers=${KAFKA_BOOTSTRAP_SERVERS:kafka-service.kafka.svc.cluste
|
||||
# ====================================================================
|
||||
# Note: Quarkus génère automatiquement les serializers Jackson basés sur le type Emitter<EventType>
|
||||
# Topic: Notifications
|
||||
# Note: Quarkus génère automatiquement les serializers Jackson basés sur le type Emitter<NotificationEvent>
|
||||
mp.messaging.outgoing.notifications.connector=smallrye-kafka
|
||||
mp.messaging.outgoing.notifications.topic=notifications
|
||||
mp.messaging.outgoing.notifications.key.serializer=org.apache.kafka.common.serialization.StringSerializer
|
||||
# value.serializer omis - Quarkus génère automatiquement depuis Emitter<NotificationEvent>
|
||||
|
||||
# Topic: Chat Messages
|
||||
# Note: Quarkus génère automatiquement les serializers Jackson basés sur le type Emitter<ChatMessageEvent>
|
||||
mp.messaging.outgoing.chat-messages.connector=smallrye-kafka
|
||||
mp.messaging.outgoing.chat-messages.topic=chat.messages
|
||||
mp.messaging.outgoing.chat-messages.key.serializer=org.apache.kafka.common.serialization.StringSerializer
|
||||
# value.serializer omis - Quarkus génère automatiquement depuis Emitter<ChatMessageEvent>
|
||||
|
||||
# Topic: Reactions (likes, comments, shares)
|
||||
# Note: Quarkus génère automatiquement les serializers Jackson basés sur le type Emitter<ReactionEvent>
|
||||
mp.messaging.outgoing.reactions.connector=smallrye-kafka
|
||||
mp.messaging.outgoing.reactions.topic=reactions
|
||||
mp.messaging.outgoing.reactions.key.serializer=org.apache.kafka.common.serialization.StringSerializer
|
||||
# value.serializer omis - Quarkus génère automatiquement depuis Emitter<ReactionEvent>
|
||||
|
||||
# Topic: Presence Updates
|
||||
# Note: Quarkus génère automatiquement les serializers Jackson basés sur le type Emitter<PresenceEvent>
|
||||
mp.messaging.outgoing.presence.connector=smallrye-kafka
|
||||
mp.messaging.outgoing.presence.topic=presence.updates
|
||||
mp.messaging.outgoing.presence.key.serializer=org.apache.kafka.common.serialization.StringSerializer
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
-- Migration V12: Nettoyage des colonnes legacy de la table users
|
||||
-- Date: 2026-01-26
|
||||
-- Description: Supprime les anciennes colonnes (nom, prenoms, mot_de_passe) qui coexistent
|
||||
-- avec les nouvelles (first_name, last_name, password_hash) suite à la migration V3.
|
||||
--
|
||||
-- Contexte: La migration V3 devait renommer les colonnes, mais Hibernate avait déjà créé
|
||||
-- les nouvelles colonnes. Résultat: la table avait les deux sets de colonnes.
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
-- Étape 1: S'assurer que les nouvelles colonnes existent et ont des données
|
||||
-- Si first_name est vide mais nom a des données, copier
|
||||
UPDATE users SET first_name = nom
|
||||
WHERE nom IS NOT NULL
|
||||
AND nom != ''
|
||||
AND (first_name IS NULL OR first_name = '');
|
||||
|
||||
UPDATE users SET last_name = prenoms
|
||||
WHERE prenoms IS NOT NULL
|
||||
AND prenoms != ''
|
||||
AND (last_name IS NULL OR last_name = '');
|
||||
|
||||
UPDATE users SET password_hash = mot_de_passe
|
||||
WHERE mot_de_passe IS NOT NULL
|
||||
AND mot_de_passe != ''
|
||||
AND (password_hash IS NULL OR password_hash = '');
|
||||
|
||||
-- Étape 2: Rendre les anciennes colonnes nullable (si elles existent et sont NOT NULL)
|
||||
IF EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'users' AND column_name = 'nom') THEN
|
||||
ALTER TABLE users ALTER COLUMN nom DROP NOT NULL;
|
||||
END IF;
|
||||
|
||||
IF EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'users' AND column_name = 'prenoms') THEN
|
||||
ALTER TABLE users ALTER COLUMN prenoms DROP NOT NULL;
|
||||
END IF;
|
||||
|
||||
IF EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'users' AND column_name = 'mot_de_passe') THEN
|
||||
ALTER TABLE users ALTER COLUMN mot_de_passe DROP NOT NULL;
|
||||
END IF;
|
||||
|
||||
-- Étape 3: Supprimer les anciennes colonnes (optionnel - décommenter si vous voulez un schéma propre)
|
||||
-- Note: Gardées pour l'instant pour compatibilité avec d'éventuels anciens clients
|
||||
/*
|
||||
IF EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'users' AND column_name = 'nom') THEN
|
||||
ALTER TABLE users DROP COLUMN nom;
|
||||
END IF;
|
||||
|
||||
IF EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'users' AND column_name = 'prenoms') THEN
|
||||
ALTER TABLE users DROP COLUMN prenoms;
|
||||
END IF;
|
||||
|
||||
IF EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'users' AND column_name = 'mot_de_passe') THEN
|
||||
ALTER TABLE users DROP COLUMN mot_de_passe;
|
||||
END IF;
|
||||
*/
|
||||
END $$;
|
||||
|
||||
-- Commentaire explicatif
|
||||
COMMENT ON TABLE users IS 'Table utilisateurs v2.0 - Colonnes legacy (nom, prenoms, mot_de_passe) dépréciées, utiliser first_name, last_name, password_hash';
|
||||
@@ -0,0 +1,57 @@
|
||||
-- Migration V13: Abonnements et paiements établissements (Wave)
|
||||
-- Date: 2026-01-28
|
||||
-- Description: Tables pour les droits d'accès des établissements payés via Wave
|
||||
|
||||
-- Abonnements établissements
|
||||
CREATE TABLE IF NOT EXISTS establishment_subscriptions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
establishment_id UUID NOT NULL,
|
||||
plan VARCHAR(20) NOT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
|
||||
wave_session_id VARCHAR(255),
|
||||
amount_xof INTEGER,
|
||||
paid_at TIMESTAMP,
|
||||
expires_at TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
CONSTRAINT fk_establishment_subscriptions_establishment
|
||||
FOREIGN KEY (establishment_id) REFERENCES establishments(id) ON DELETE CASCADE,
|
||||
CONSTRAINT chk_establishment_subscriptions_plan
|
||||
CHECK (plan IN ('MONTHLY', 'YEARLY')),
|
||||
CONSTRAINT chk_establishment_subscriptions_status
|
||||
CHECK (status IN ('PENDING', 'ACTIVE', 'EXPIRED', 'CANCELLED'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_establishment_subscriptions_establishment
|
||||
ON establishment_subscriptions(establishment_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_establishment_subscriptions_status
|
||||
ON establishment_subscriptions(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_establishment_subscriptions_wave_session
|
||||
ON establishment_subscriptions(wave_session_id);
|
||||
|
||||
-- Paiements établissements (historique Wave)
|
||||
CREATE TABLE IF NOT EXISTS establishment_payments (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
establishment_id UUID NOT NULL,
|
||||
amount_xof INTEGER NOT NULL,
|
||||
wave_session_id VARCHAR(255),
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
|
||||
client_phone VARCHAR(30),
|
||||
plan VARCHAR(20),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
CONSTRAINT fk_establishment_payments_establishment
|
||||
FOREIGN KEY (establishment_id) REFERENCES establishments(id) ON DELETE CASCADE,
|
||||
CONSTRAINT chk_establishment_payments_status
|
||||
CHECK (status IN ('PENDING', 'COMPLETED', 'FAILED', 'CANCELLED'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_establishment_payments_establishment
|
||||
ON establishment_payments(establishment_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_establishment_payments_wave_session
|
||||
ON establishment_payments(wave_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_establishment_payments_status
|
||||
ON establishment_payments(status);
|
||||
|
||||
COMMENT ON TABLE establishment_subscriptions IS 'Abonnements / droits d''accès des établissements (paiement Wave)';
|
||||
COMMENT ON TABLE establishment_payments IS 'Historique des paiements Wave pour les établissements';
|
||||
@@ -0,0 +1,9 @@
|
||||
-- V14: Ajout is_active pour Manager & Abonnement (suspension automatique Wave)
|
||||
-- users.is_active : false = manager suspendu (paiement échoué/annulé)
|
||||
-- establishments.is_active : false = établissement masqué (abonnement inactif)
|
||||
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS is_active BOOLEAN NOT NULL DEFAULT true;
|
||||
ALTER TABLE establishments ADD COLUMN IF NOT EXISTS is_active BOOLEAN NOT NULL DEFAULT true;
|
||||
|
||||
COMMENT ON COLUMN users.is_active IS 'false = compte suspendu (ex: manager abonnement expiré)';
|
||||
COMMENT ON COLUMN establishments.is_active IS 'false = établissement masqué (abonnement inactif)';
|
||||
@@ -0,0 +1,38 @@
|
||||
-- Migration V15: Création de la table social_posts (Publications)
|
||||
-- Date: 2026-01-28
|
||||
-- Description: Table des posts sociaux pour le fil d'actualité AfterWork (correction 500)
|
||||
|
||||
CREATE TABLE IF NOT EXISTS social_posts (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
content VARCHAR(2000) NOT NULL,
|
||||
user_id UUID NOT NULL,
|
||||
image_url VARCHAR(500),
|
||||
likes_count INTEGER NOT NULL DEFAULT 0,
|
||||
comments_count INTEGER NOT NULL DEFAULT 0,
|
||||
shares_count INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
CONSTRAINT fk_social_posts_user
|
||||
FOREIGN KEY (user_id)
|
||||
REFERENCES users(id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_social_posts_user_id ON social_posts(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_social_posts_created_at ON social_posts(created_at DESC);
|
||||
|
||||
CREATE OR REPLACE FUNCTION update_social_posts_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER trigger_update_social_posts_updated_at
|
||||
BEFORE UPDATE ON social_posts
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_social_posts_updated_at();
|
||||
|
||||
COMMENT ON TABLE social_posts IS 'Publications (posts) sociaux du fil AfterWork';
|
||||
@@ -0,0 +1,81 @@
|
||||
-- V1_1__Create_Base_Tables_For_Fresh_Db.sql
|
||||
-- Création des tables de base pour une base de données vierge.
|
||||
-- À exécuter après V1__Baseline lorsque le schéma n'a pas été créé par Hibernate.
|
||||
-- Les migrations V2, V3, V4, V7+ supposent que users, establishments et events existent.
|
||||
|
||||
-- Table users (structure minimale attendue par V3 et l'entité Users)
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
first_name VARCHAR(100) NOT NULL DEFAULT '',
|
||||
last_name VARCHAR(100) NOT NULL DEFAULT '',
|
||||
email VARCHAR(100) NOT NULL UNIQUE,
|
||||
password_hash VARCHAR(255) NOT NULL DEFAULT '',
|
||||
role VARCHAR(50) NOT NULL,
|
||||
profile_image_url VARCHAR(500),
|
||||
bio VARCHAR(500),
|
||||
loyalty_points INTEGER NOT NULL DEFAULT 0,
|
||||
preferences JSONB NOT NULL DEFAULT '{}',
|
||||
is_verified BOOLEAN NOT NULL DEFAULT false,
|
||||
is_online BOOLEAN NOT NULL DEFAULT false,
|
||||
last_seen TIMESTAMP,
|
||||
is_active BOOLEAN NOT NULL DEFAULT true
|
||||
);
|
||||
|
||||
-- Table establishments (V4 renomme total_ratings_count -> total_reviews_count)
|
||||
CREATE TABLE IF NOT EXISTS establishments (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
type VARCHAR(100) NOT NULL,
|
||||
address VARCHAR(500) NOT NULL,
|
||||
city VARCHAR(100) NOT NULL,
|
||||
postal_code VARCHAR(20) NOT NULL,
|
||||
description VARCHAR(2000),
|
||||
phone_number VARCHAR(50),
|
||||
website VARCHAR(500),
|
||||
average_rating DOUBLE PRECISION,
|
||||
total_ratings_count INTEGER,
|
||||
price_range VARCHAR(20),
|
||||
verification_status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
latitude DOUBLE PRECISION,
|
||||
longitude DOUBLE PRECISION,
|
||||
manager_id UUID NOT NULL REFERENCES users(id)
|
||||
);
|
||||
|
||||
-- Table events (structure minimale ; V2 et V7 ajoutent les colonnes supplémentaires)
|
||||
CREATE TABLE IF NOT EXISTS events (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
description VARCHAR(1000),
|
||||
start_date TIMESTAMP NOT NULL,
|
||||
end_date TIMESTAMP NOT NULL,
|
||||
category VARCHAR(100),
|
||||
link VARCHAR(500),
|
||||
image_url VARCHAR(500),
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'OPEN',
|
||||
creator_id UUID NOT NULL REFERENCES users(id)
|
||||
);
|
||||
|
||||
-- Table de jointure event_participants (events <-> users)
|
||||
CREATE TABLE IF NOT EXISTS event_participants (
|
||||
event_id UUID NOT NULL REFERENCES events(id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (event_id, user_id)
|
||||
);
|
||||
|
||||
-- Table de jointure user_favorite_events (users <-> events)
|
||||
CREATE TABLE IF NOT EXISTS user_favorite_events (
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
event_id UUID NOT NULL REFERENCES events(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (user_id, event_id)
|
||||
);
|
||||
|
||||
-- Index pour les FK et recherches courantes
|
||||
CREATE INDEX IF NOT EXISTS idx_establishments_manager ON establishments(manager_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_events_creator ON events(creator_id);
|
||||
@@ -1,13 +1,38 @@
|
||||
-- Migration V3: Migration de la table users vers l'architecture v2.0
|
||||
-- Date: 2026-01-15
|
||||
-- Description: Renommage des colonnes et ajout des nouveaux champs pour l'entité User
|
||||
-- Note: Migration rendue idempotente pour éviter les conflits avec Hibernate
|
||||
|
||||
-- Renommer les colonnes existantes
|
||||
ALTER TABLE users RENAME COLUMN nom TO first_name;
|
||||
ALTER TABLE users RENAME COLUMN prenoms TO last_name;
|
||||
ALTER TABLE users RENAME COLUMN mot_de_passe TO password_hash;
|
||||
-- Renommer/migrer les colonnes existantes (idempotent)
|
||||
DO $$
|
||||
BEGIN
|
||||
-- Si first_name n'existe pas mais nom existe, renommer
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='users' AND column_name='first_name')
|
||||
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='users' AND column_name='nom') THEN
|
||||
ALTER TABLE users RENAME COLUMN nom TO first_name;
|
||||
-- Si first_name n'existe pas du tout, le créer
|
||||
ELSIF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='users' AND column_name='first_name') THEN
|
||||
ALTER TABLE users ADD COLUMN first_name VARCHAR(100) NOT NULL DEFAULT '';
|
||||
END IF;
|
||||
|
||||
-- Ajouter les nouvelles colonnes
|
||||
-- Si last_name n'existe pas mais prenoms existe, renommer
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='users' AND column_name='last_name')
|
||||
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='users' AND column_name='prenoms') THEN
|
||||
ALTER TABLE users RENAME COLUMN prenoms TO last_name;
|
||||
ELSIF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='users' AND column_name='last_name') THEN
|
||||
ALTER TABLE users ADD COLUMN last_name VARCHAR(100) NOT NULL DEFAULT '';
|
||||
END IF;
|
||||
|
||||
-- Si password_hash n'existe pas mais mot_de_passe existe, renommer
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='users' AND column_name='password_hash')
|
||||
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='users' AND column_name='mot_de_passe') THEN
|
||||
ALTER TABLE users RENAME COLUMN mot_de_passe TO password_hash;
|
||||
ELSIF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='users' AND column_name='password_hash') THEN
|
||||
ALTER TABLE users ADD COLUMN password_hash VARCHAR(255) NOT NULL DEFAULT '';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Ajouter les nouvelles colonnes (déjà idempotent)
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS bio VARCHAR(500);
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS loyalty_points INTEGER DEFAULT 0 NOT NULL;
|
||||
|
||||
|
||||
@@ -47,10 +47,11 @@ BEGIN
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- EXECUTE PROCEDURE pour compatibilité PostgreSQL < 11 (EXECUTE FUNCTION à partir de PG 11)
|
||||
CREATE TRIGGER trigger_update_business_hours_updated_at
|
||||
BEFORE UPDATE ON business_hours
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_business_hours_updated_at();
|
||||
EXECUTE PROCEDURE update_business_hours_updated_at();
|
||||
|
||||
-- Commentaires pour documentation
|
||||
COMMENT ON TABLE business_hours IS 'Horaires d''ouverture des établissements (v2.0)';
|
||||
|
||||
Reference in New Issue
Block a user