Refactoring

This commit is contained in:
dahoud
2026-03-01 22:00:28 +00:00
parent c0e2c4da45
commit 6b28cf751e
469 changed files with 26866 additions and 14768 deletions

View File

@@ -1,6 +1,8 @@
package dev.lions.unionflow.client.service;
import dev.lions.unionflow.client.dto.AdhesionDTO;
import dev.lions.unionflow.server.api.dto.finance.request.*;
import dev.lions.unionflow.server.api.dto.finance.response.*;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
@@ -17,6 +19,7 @@ import java.util.UUID;
* @version 1.0
*/
@RegisterRestClient(configKey = "unionflow-api")
@RegisterClientHeaders(dev.lions.unionflow.client.security.AuthHeaderFactory.class)
@Path("/api/adhesions")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@@ -26,7 +29,7 @@ public interface AdhesionService {
* Récupère toutes les adhésions avec pagination
*/
@GET
List<AdhesionDTO> listerToutes(
List<AdhesionResponse> listerToutes(
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("20") int size
);
@@ -36,27 +39,27 @@ public interface AdhesionService {
*/
@GET
@Path("/{id}")
AdhesionDTO obtenirParId(@PathParam("id") UUID id);
AdhesionResponse obtenirParId(@PathParam("id") UUID id);
/**
* Récupère une adhésion par son numéro de référence
*/
@GET
@Path("/reference/{numeroReference}")
AdhesionDTO obtenirParReference(@PathParam("numeroReference") String numeroReference);
AdhesionResponse obtenirParReference(@PathParam("numeroReference") String numeroReference);
/**
* Crée une nouvelle adhésion
*/
@POST
AdhesionDTO creer(AdhesionDTO adhesion);
AdhesionResponse creer(CreateAdhesionRequest adhesion);
/**
* Met à jour une adhésion existante
*/
@PUT
@Path("/{id}")
AdhesionDTO modifier(@PathParam("id") UUID id, AdhesionDTO adhesion);
AdhesionResponse modifier(@PathParam("id") UUID id, UpdateAdhesionRequest adhesion);
/**
* Supprime une adhésion
@@ -70,7 +73,7 @@ public interface AdhesionService {
*/
@POST
@Path("/{id}/approuver")
AdhesionDTO approuver(
AdhesionResponse approuver(
@PathParam("id") UUID id,
@QueryParam("approuvePar") String approuvePar
);
@@ -80,7 +83,7 @@ public interface AdhesionService {
*/
@POST
@Path("/{id}/rejeter")
AdhesionDTO rejeter(
AdhesionResponse rejeter(
@PathParam("id") UUID id,
@QueryParam("motifRejet") String motifRejet
);
@@ -90,7 +93,7 @@ public interface AdhesionService {
*/
@POST
@Path("/{id}/paiement")
AdhesionDTO enregistrerPaiement(
AdhesionResponse enregistrerPaiement(
@PathParam("id") UUID id,
@QueryParam("montantPaye") BigDecimal montantPaye,
@QueryParam("methodePaiement") String methodePaiement,
@@ -102,7 +105,7 @@ public interface AdhesionService {
*/
@GET
@Path("/membre/{membreId}")
List<AdhesionDTO> obtenirParMembre(
List<AdhesionResponse> obtenirParMembre(
@PathParam("membreId") UUID membreId,
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("20") int size
@@ -113,7 +116,7 @@ public interface AdhesionService {
*/
@GET
@Path("/organisation/{organisationId}")
List<AdhesionDTO> obtenirParOrganisation(
List<AdhesionResponse> obtenirParOrganisation(
@PathParam("organisationId") UUID organisationId,
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("20") int size
@@ -124,7 +127,7 @@ public interface AdhesionService {
*/
@GET
@Path("/statut/{statut}")
List<AdhesionDTO> obtenirParStatut(
List<AdhesionResponse> obtenirParStatut(
@PathParam("statut") String statut,
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("20") int size
@@ -135,7 +138,7 @@ public interface AdhesionService {
*/
@GET
@Path("/en-attente")
List<AdhesionDTO> obtenirEnAttente(
List<AdhesionResponse> obtenirEnAttente(
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("20") int size
);

View File

@@ -0,0 +1,54 @@
package dev.lions.unionflow.client.service;
import dev.lions.unionflow.server.api.dto.role.response.RoleResponse;
import dev.lions.unionflow.server.api.dto.user.request.CreateUserRequest;
import dev.lions.unionflow.server.api.dto.user.request.UpdateUserRequest;
import dev.lions.unionflow.server.api.dto.user.response.UserResponse;
import dev.lions.unionflow.server.api.dto.base.PageResponse;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
/**
* Client REST pour l'API admin utilisateurs Keycloak (/api/admin/users).
* Réservé aux utilisateurs avec rôle SUPER_ADMIN.
*/
@RegisterRestClient(configKey = "unionflow-api")
@RegisterClientHeaders(dev.lions.unionflow.client.security.AuthHeaderFactory.class)
@Path("/api/admin/users")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public interface AdminUserService {
@GET
PageResponse<UserResponse> lister(
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("20") int size,
@QueryParam("search") String search
);
@GET
@Path("/{id}")
UserResponse obtenirParId(@PathParam("id") String id);
@GET
@Path("/roles")
List<RoleResponse> getRealmRoles();
@GET
@Path("/{id}/roles")
List<RoleResponse> getUserRoles(@PathParam("id") String id);
@PUT
@Path("/{id}/roles")
void setUserRoles(@PathParam("id") String id, List<String> roleNames);
@POST
UserResponse creer(CreateUserRequest request);
@PUT
@Path("/{id}")
UserResponse mettreAJour(@PathParam("id") String id, UpdateUserRequest request);
}

View File

@@ -1,13 +1,17 @@
package dev.lions.unionflow.client.service;
import dev.lions.unionflow.client.dto.AnalyticsDataDTO;
import dev.lions.unionflow.server.api.dto.analytics.AnalyticsDataResponse;
import dev.lions.unionflow.server.api.dto.analytics.DashboardWidgetResponse;
import dev.lions.unionflow.server.api.dto.analytics.KPITrendResponse;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
import java.util.Map;
@RegisterRestClient(configKey = "unionflow-api")
@RegisterClientHeaders(dev.lions.unionflow.client.security.AuthHeaderFactory.class)
@Path("/api/v1/analytics")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@@ -15,7 +19,7 @@ public interface AnalyticsService {
@GET
@Path("/metriques/{typeMetrique}")
AnalyticsDataDTO calculerMetrique(
AnalyticsDataResponse calculerMetrique(
@PathParam("typeMetrique") String typeMetrique,
@QueryParam("periode") String periode,
@QueryParam("organisationId") String organisationId

View File

@@ -1,6 +1,7 @@
package dev.lions.unionflow.client.service;
import dev.lions.unionflow.client.dto.AssociationDTO;
import dev.lions.unionflow.server.api.dto.organisation.response.OrganisationResponse;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
@@ -8,78 +9,101 @@ import java.util.List;
import java.util.UUID;
@RegisterRestClient(configKey = "unionflow-api")
@RegisterClientHeaders(dev.lions.unionflow.client.security.AuthHeaderFactory.class)
@Path("/api/organisations")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public interface AssociationService {
@GET
List<AssociationDTO> listerToutes(
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("1000") int size
);
PagedResponseDTO<OrganisationResponse> listerToutes(
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("1000") int size);
class PagedResponseDTO<T> {
public List<T> data;
public Long total;
public Integer page;
public Integer size;
public Integer totalPages;
public List<T> getData() {
return data;
}
public void setData(List<T> data) {
this.data = data;
}
public Long getTotal() {
return total;
}
public void setTotal(Long total) {
this.total = total;
}
}
@GET
@Path("/{id}")
AssociationDTO obtenirParId(@PathParam("id") UUID id);
OrganisationResponse obtenirParId(@PathParam("id") UUID id);
@GET
@Path("/recherche")
List<AssociationDTO> rechercher(
@QueryParam("nom") String nom,
@QueryParam("type") String type,
@QueryParam("statut") String statut,
@QueryParam("region") String region,
@QueryParam("ville") String ville,
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("20") int size
);
PagedResponseDTO<OrganisationResponse> rechercher(
@QueryParam("nom") String nom,
@QueryParam("type") String type,
@QueryParam("statut") String statut,
@QueryParam("region") String region,
@QueryParam("ville") String ville,
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("20") int size);
@GET
@Path("/type/{type}")
List<AssociationDTO> listerParType(@PathParam("type") String type);
List<OrganisationResponse> listerParType(@PathParam("type") String type);
@GET
@Path("/region/{region}")
List<AssociationDTO> listerParRegion(@PathParam("region") String region);
List<OrganisationResponse> listerParRegion(@PathParam("region") String region);
@POST
AssociationDTO creer(AssociationDTO association);
OrganisationResponse creer(OrganisationResponse association);
@PUT
@Path("/{id}")
AssociationDTO modifier(@PathParam("id") UUID id, AssociationDTO association);
OrganisationResponse modifier(@PathParam("id") UUID id, OrganisationResponse association);
@DELETE
@Path("/{id}")
void supprimer(@PathParam("id") UUID id);
// Côté serveur: POST /{id}/activer
@POST
@Path("/{id}/activer")
AssociationDTO activer(@PathParam("id") UUID id);
OrganisationResponse activer(@PathParam("id") UUID id);
// Suspension: POST /{id}/suspendre (alias historique "désactiver")
@POST
@Path("/{id}/suspendre")
AssociationDTO suspendre(@PathParam("id") UUID id);
OrganisationResponse suspendre(@PathParam("id") UUID id);
@PUT
@Path("/{id}/dissoudre")
AssociationDTO dissoudre(@PathParam("id") UUID id);
OrganisationResponse dissoudre(@PathParam("id") UUID id);
@GET
@Path("/statistiques")
StatistiquesAssociationDTO obtenirStatistiques();
@GET
@Path("/{id}/membres/count")
Long compterMembres(@PathParam("id") UUID id);
@GET
@Path("/{id}/performance")
PerformanceAssociationDTO obtenirPerformance(@PathParam("id") UUID id);
// Classes DTO internes
class StatistiquesAssociationDTO {
public Long totalAssociations;
@@ -91,39 +115,85 @@ public interface AssociationService {
public Double tauxActivite;
public java.util.Map<String, Long> repartitionParType;
public java.util.Map<String, Long> repartitionParRegion;
// Constructeurs
public StatistiquesAssociationDTO() {}
public StatistiquesAssociationDTO() {
}
// Getters et setters
public Long getTotalAssociations() { return totalAssociations; }
public void setTotalAssociations(Long totalAssociations) { this.totalAssociations = totalAssociations; }
public Long getAssociationsActives() { return associationsActives; }
public void setAssociationsActives(Long associationsActives) { this.associationsActives = associationsActives; }
public Long getAssociationsInactives() { return associationsInactives; }
public void setAssociationsInactives(Long associationsInactives) { this.associationsInactives = associationsInactives; }
public Long getAssociationsSuspendues() { return associationsSuspendues; }
public void setAssociationsSuspendues(Long associationsSuspendues) { this.associationsSuspendues = associationsSuspendues; }
public Long getAssociationsDissoutes() { return associationsDissoutes; }
public void setAssociationsDissoutes(Long associationsDissoutes) { this.associationsDissoutes = associationsDissoutes; }
public Long getNouvellesAssociations30Jours() { return nouvellesAssociations30Jours; }
public void setNouvellesAssociations30Jours(Long nouvellesAssociations30Jours) { this.nouvellesAssociations30Jours = nouvellesAssociations30Jours; }
public Double getTauxActivite() { return tauxActivite; }
public void setTauxActivite(Double tauxActivite) { this.tauxActivite = tauxActivite; }
public java.util.Map<String, Long> getRepartitionParType() { return repartitionParType; }
public void setRepartitionParType(java.util.Map<String, Long> repartitionParType) { this.repartitionParType = repartitionParType; }
public java.util.Map<String, Long> getRepartitionParRegion() { return repartitionParRegion; }
public void setRepartitionParRegion(java.util.Map<String, Long> repartitionParRegion) { this.repartitionParRegion = repartitionParRegion; }
public Long getTotalAssociations() {
return totalAssociations;
}
public void setTotalAssociations(Long totalAssociations) {
this.totalAssociations = totalAssociations;
}
public Long getAssociationsActives() {
return associationsActives;
}
public void setAssociationsActives(Long associationsActives) {
this.associationsActives = associationsActives;
}
public Long getAssociationsInactives() {
return associationsInactives;
}
public void setAssociationsInactives(Long associationsInactives) {
this.associationsInactives = associationsInactives;
}
public Long getAssociationsSuspendues() {
return associationsSuspendues;
}
public void setAssociationsSuspendues(Long associationsSuspendues) {
this.associationsSuspendues = associationsSuspendues;
}
public Long getAssociationsDissoutes() {
return associationsDissoutes;
}
public void setAssociationsDissoutes(Long associationsDissoutes) {
this.associationsDissoutes = associationsDissoutes;
}
public Long getNouvellesAssociations30Jours() {
return nouvellesAssociations30Jours;
}
public void setNouvellesAssociations30Jours(Long nouvellesAssociations30Jours) {
this.nouvellesAssociations30Jours = nouvellesAssociations30Jours;
}
public Double getTauxActivite() {
return tauxActivite;
}
public void setTauxActivite(Double tauxActivite) {
this.tauxActivite = tauxActivite;
}
public java.util.Map<String, Long> getRepartitionParType() {
return repartitionParType;
}
public void setRepartitionParType(java.util.Map<String, Long> repartitionParType) {
this.repartitionParType = repartitionParType;
}
public java.util.Map<String, Long> getRepartitionParRegion() {
return repartitionParRegion;
}
public void setRepartitionParRegion(java.util.Map<String, Long> repartitionParRegion) {
this.repartitionParRegion = repartitionParRegion;
}
}
class PerformanceAssociationDTO {
public UUID associationId;
public String nom;
@@ -133,33 +203,74 @@ public interface AssociationService {
public Integer scoreFinances;
public String tendance;
public java.time.LocalDateTime derniereMiseAJour;
// Constructeurs
public PerformanceAssociationDTO() {}
public PerformanceAssociationDTO() {
}
// Getters et setters
public UUID getAssociationId() { return associationId; }
public void setAssociationId(UUID associationId) { this.associationId = associationId; }
public String getNom() { return nom; }
public void setNom(String nom) { this.nom = nom; }
public Integer getScoreGlobal() { return scoreGlobal; }
public void setScoreGlobal(Integer scoreGlobal) { this.scoreGlobal = scoreGlobal; }
public Integer getScoreMembres() { return scoreMembres; }
public void setScoreMembres(Integer scoreMembres) { this.scoreMembres = scoreMembres; }
public Integer getScoreActivites() { return scoreActivites; }
public void setScoreActivites(Integer scoreActivites) { this.scoreActivites = scoreActivites; }
public Integer getScoreFinances() { return scoreFinances; }
public void setScoreFinances(Integer scoreFinances) { this.scoreFinances = scoreFinances; }
public String getTendance() { return tendance; }
public void setTendance(String tendance) { this.tendance = tendance; }
public java.time.LocalDateTime getDerniereMiseAJour() { return derniereMiseAJour; }
public void setDerniereMiseAJour(java.time.LocalDateTime derniereMiseAJour) { this.derniereMiseAJour = derniereMiseAJour; }
public UUID getAssociationId() {
return associationId;
}
public void setAssociationId(UUID associationId) {
this.associationId = associationId;
}
public String getNom() {
return nom;
}
public void setNom(String nom) {
this.nom = nom;
}
public Integer getScoreGlobal() {
return scoreGlobal;
}
public void setScoreGlobal(Integer scoreGlobal) {
this.scoreGlobal = scoreGlobal;
}
public Integer getScoreMembres() {
return scoreMembres;
}
public void setScoreMembres(Integer scoreMembres) {
this.scoreMembres = scoreMembres;
}
public Integer getScoreActivites() {
return scoreActivites;
}
public void setScoreActivites(Integer scoreActivites) {
this.scoreActivites = scoreActivites;
}
public Integer getScoreFinances() {
return scoreFinances;
}
public void setScoreFinances(Integer scoreFinances) {
this.scoreFinances = scoreFinances;
}
public String getTendance() {
return tendance;
}
public void setTendance(String tendance) {
this.tendance = tendance;
}
public java.time.LocalDateTime getDerniereMiseAJour() {
return derniereMiseAJour;
}
public void setDerniereMiseAJour(java.time.LocalDateTime derniereMiseAJour) {
this.derniereMiseAJour = derniereMiseAJour;
}
}
}
}

View File

@@ -1,6 +1,6 @@
package dev.lions.unionflow.client.service;
import dev.lions.unionflow.client.dto.AuditLogDTO;
import dev.lions.unionflow.server.api.dto.admin.response.AuditLogResponse;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import java.util.Map;
@@ -13,8 +13,8 @@ import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
* @author UnionFlow Team
* @version 1.0
*/
@RegisterRestClient(baseUri = "http://localhost:8085")
@RegisterClientHeaders
@RegisterRestClient(configKey = "unionflow-api")
@RegisterClientHeaders(dev.lions.unionflow.client.security.AuthHeaderFactory.class)
@Path("/api/audit")
public interface AuditService {
@@ -43,7 +43,7 @@ public interface AuditService {
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
AuditLogDTO enregistrerLog(AuditLogDTO dto);
AuditLogResponse enregistrerLog(AuditLogResponse dto);
@GET
@Path("/statistiques")

View File

@@ -1,7 +1,7 @@
package dev.lions.unionflow.client.service;
import dev.lions.unionflow.client.dto.auth.LoginRequest;
import dev.lions.unionflow.client.dto.auth.LoginResponse;
import dev.lions.unionflow.server.api.dto.auth.request.LoginRequest;
import dev.lions.unionflow.server.api.dto.auth.response.LoginResponse;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.client.ClientBuilder;
@@ -9,6 +9,7 @@ import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import java.time.LocalDateTime;
import java.util.UUID;
import java.util.logging.Logger;
@@ -38,18 +39,18 @@ public class AuthenticationService {
if (response.getStatus() == 200) {
LoginResponse loginResponse = response.readEntity(LoginResponse.class);
LOGGER.info("Authentification réussie pour l'utilisateur: " + loginRequest.getUsername());
LOGGER.info("Authentification réussie pour l'utilisateur: " + loginRequest.username());
return loginResponse;
} else {
LOGGER.warning("Échec de l'authentification. Code de statut: " + response.getStatus());
throw new AuthenticationException("Nom d'utilisateur ou mot de passe incorrect");
}
} catch (Exception e) {
LOGGER.severe("Erreur lors de l'authentification: " + e.getMessage());
// Mode simulation pour le développement
if ("demo".equals(loginRequest.getUsername()) || isValidDemoCredentials(loginRequest)) {
if ("demo".equals(loginRequest.username()) || isValidDemoCredentials(loginRequest)) {
return createDemoLoginResponse(loginRequest);
}
@@ -93,21 +94,21 @@ public class AuthenticationService {
}
private boolean isValidDemoCredentials(LoginRequest request) {
return ("admin".equals(request.getUsername()) && "admin".equals(request.getPassword())) ||
("superadmin".equals(request.getUsername()) && "admin".equals(request.getPassword())) ||
("membre".equals(request.getUsername()) && "membre".equals(request.getPassword()));
return ("admin".equals(request.username()) && "admin".equals(request.password())) ||
("superadmin".equals(request.username()) && "admin".equals(request.password())) ||
("membre".equals(request.username()) && "membre".equals(request.password()));
}
private LoginResponse createDemoLoginResponse(LoginRequest request) {
LoginResponse.UserInfo userInfo = new LoginResponse.UserInfo();
// UUIDs fixes pour la démonstration (pour cohérence entre les sessions)
UUID superAdminId = UUID.fromString("00000000-0000-0000-0000-000000000001");
UUID adminId = UUID.fromString("00000000-0000-0000-0000-000000000002");
UUID membreId = UUID.fromString("00000000-0000-0000-0000-000000000003");
UUID entiteId = UUID.fromString("00000000-0000-0000-0000-000000000010");
switch (request.getUsername()) {
switch (request.username()) {
case "superadmin":
userInfo.setId(superAdminId);
userInfo.setNom("Diallo");
@@ -156,13 +157,18 @@ public class AuthenticationService {
userInfo.setEntite(entiteMembre);
break;
}
return new LoginResponse(
"demo_access_token_" + System.currentTimeMillis(),
"demo_refresh_token_" + System.currentTimeMillis(),
3600L, // 1 heure
userInfo
);
long expiresIn = 3600L; // 1 heure en secondes
LocalDateTime expirationDate = LocalDateTime.now().plusSeconds(expiresIn);
return LoginResponse.builder()
.accessToken("demo_access_token_" + System.currentTimeMillis())
.refreshToken("demo_refresh_token_" + System.currentTimeMillis())
.tokenType("Bearer")
.expiresIn(expiresIn)
.expirationDate(expirationDate)
.user(userInfo)
.build();
}
public static class AuthenticationException extends RuntimeException {

View File

@@ -0,0 +1,265 @@
package dev.lions.unionflow.client.service;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.jboss.logging.Logger;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
/**
* Service de cache pour optimiser les performances de l'application.
*
* <p>Ce service fournit un cache en mémoire pour les données fréquemment accédées,
* avec support de l'expiration automatique et de l'invalidation manuelle.
*
* <p><strong>Production-ready:</strong> Cache thread-safe, gestion de l'expiration,
* invalidation sélective, et métriques de performance.
*
* <p><strong>Usage:</strong>
* <pre>{@code
* @Inject
* CacheService cacheService;
*
* // Obtenir une valeur du cache ou la charger si absente
* List<TypeOrganisationDTO> types = cacheService.getOrLoad(
* "types-organisation",
* () -> typeOrganisationService.list(true),
* 300 // Expire après 5 minutes
* );
*
* // Invalider le cache
* cacheService.invalidate("types-organisation");
* }</pre>
*
* @author UnionFlow Team
* @version 1.0
* @since 2025-12-24
*/
@ApplicationScoped
public class CacheService {
private static final Logger LOG = Logger.getLogger(CacheService.class);
@Inject
MetricsService metricsService;
/**
* Cache thread-safe pour stocker les données.
*/
private final Map<String, CacheEntry<?>> cache = new ConcurrentHashMap<>();
/**
* Nombre maximum d'entrées dans le cache (pour éviter les fuites mémoire).
*/
private static final int MAX_CACHE_SIZE = 1000;
/**
* Obtient une valeur du cache ou la charge si absente.
*
* @param key Clé du cache
* @param loader Fonction pour charger la valeur si absente
* @param ttlSeconds Durée de vie en secondes (0 = pas d'expiration)
* @return La valeur du cache ou chargée
*/
@SuppressWarnings("unchecked")
public <T> T getOrLoad(String key, Supplier<T> loader, int ttlSeconds) {
if (key == null || key.trim().isEmpty()) {
LOG.warn("Tentative d'accès au cache avec une clé null ou vide");
return loader.get();
}
// Vérifier si la valeur est en cache et valide
CacheEntry<?> entry = cache.get(key);
if (entry != null && entry.isValid(ttlSeconds)) {
LOG.debugf("Cache hit pour la clé: %s", key);
if (metricsService != null) {
metricsService.recordCacheHit();
}
return (T) entry.getValue();
}
// Charger la valeur
LOG.debugf("Cache miss pour la clé: %s - chargement depuis le backend", key);
if (metricsService != null) {
metricsService.recordCacheMiss();
}
try {
T value = loader.get();
// Vérifier la taille du cache avant d'ajouter
if (cache.size() >= MAX_CACHE_SIZE) {
LOG.warnf("Cache plein (%d entrées) - nettoyage des entrées expirées", cache.size());
cleanupExpiredEntries();
// Si toujours plein, supprimer les entrées les plus anciennes
if (cache.size() >= MAX_CACHE_SIZE) {
evictOldestEntries(MAX_CACHE_SIZE / 10); // Supprimer 10% des entrées
}
}
// Mettre en cache
cache.put(key, new CacheEntry<>(value, LocalDateTime.now()));
return value;
} catch (Exception e) {
LOG.errorf(e, "Erreur lors du chargement de la valeur pour la clé: %s", key);
throw new RuntimeException("Erreur lors du chargement de la valeur du cache", e);
}
}
/**
* Obtient une valeur du cache sans la charger si absente.
*
* @param key Clé du cache
* @return La valeur ou null si absente ou expirée
*/
@SuppressWarnings("unchecked")
public <T> T get(String key) {
if (key == null || key.trim().isEmpty()) {
return null;
}
CacheEntry<?> entry = cache.get(key);
if (entry != null && entry.isValid(0)) { // 0 = pas d'expiration
return (T) entry.getValue();
}
return null;
}
/**
* Met une valeur dans le cache.
*
* @param key Clé du cache
* @param value Valeur à mettre en cache
* @param ttlSeconds Durée de vie en secondes (0 = pas d'expiration)
*/
public <T> void put(String key, T value, int ttlSeconds) {
if (key == null || key.trim().isEmpty()) {
LOG.warn("Tentative de mise en cache avec une clé null ou vide");
return;
}
if (cache.size() >= MAX_CACHE_SIZE) {
cleanupExpiredEntries();
}
cache.put(key, new CacheEntry<>(value, LocalDateTime.now()));
LOG.debugf("Valeur mise en cache pour la clé: %s (TTL: %d secondes)", key, ttlSeconds);
}
/**
* Invalide une entrée du cache.
*
* @param key Clé à invalider
*/
public void invalidate(String key) {
if (key != null && cache.remove(key) != null) {
LOG.debugf("Cache invalidé pour la clé: %s", key);
}
}
/**
* Invalide toutes les entrées du cache correspondant à un préfixe.
*
* @param prefix Préfixe des clés à invalider
*/
public void invalidateByPrefix(String prefix) {
if (prefix == null || prefix.trim().isEmpty()) {
return;
}
int count = 0;
for (String key : cache.keySet()) {
if (key.startsWith(prefix)) {
cache.remove(key);
count++;
}
}
if (count > 0) {
LOG.debugf("Cache invalidé pour %d entrées avec le préfixe: %s", count, prefix);
}
}
/**
* Vide complètement le cache.
*/
public void clear() {
int size = cache.size();
cache.clear();
LOG.infof("Cache vidé (%d entrées supprimées)", size);
}
/**
* Nettoie les entrées expirées du cache.
*/
public void cleanupExpiredEntries() {
int initialSize = cache.size();
cache.entrySet().removeIf(entry -> !entry.getValue().isValid(0));
int removed = initialSize - cache.size();
if (removed > 0) {
LOG.debugf("Nettoyage du cache: %d entrées expirées supprimées", removed);
}
}
/**
* Supprime les entrées les plus anciennes du cache.
*/
private void evictOldestEntries(int count) {
cache.entrySet().stream()
.sorted((e1, e2) -> e1.getValue().getTimestamp().compareTo(e2.getValue().getTimestamp()))
.limit(count)
.forEach(entry -> cache.remove(entry.getKey()));
LOG.debugf("Éviction de %d entrées les plus anciennes du cache", count);
}
/**
* Obtient la taille actuelle du cache.
*/
public int size() {
return cache.size();
}
/**
* Entrée du cache avec timestamp.
*/
private static class CacheEntry<T> {
private final T value;
private final LocalDateTime timestamp;
public CacheEntry(T value, LocalDateTime timestamp) {
this.value = value;
this.timestamp = timestamp;
}
public T getValue() {
return value;
}
public LocalDateTime getTimestamp() {
return timestamp;
}
/**
* Vérifie si l'entrée est valide (non expirée).
*
* @param ttlSeconds Durée de vie en secondes (0 = pas d'expiration)
*/
public boolean isValid(int ttlSeconds) {
if (ttlSeconds <= 0) {
return true; // Pas d'expiration
}
LocalDateTime expiration = timestamp.plusSeconds(ttlSeconds);
return LocalDateTime.now().isBefore(expiration);
}
}
}

View File

@@ -0,0 +1,107 @@
package dev.lions.unionflow.client.service;
import dev.lions.unionflow.server.api.dto.comptabilite.request.*;
import dev.lions.unionflow.server.api.dto.comptabilite.response.*;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
import java.util.UUID;
/**
* Service REST Client pour la gestion comptable
*
* @author UnionFlow Team
* @version 1.0
*/
@RegisterRestClient(configKey = "unionflow-api")
@RegisterClientHeaders(dev.lions.unionflow.client.security.AuthHeaderFactory.class)
@Path("/api/comptabilite")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public interface ComptabiliteService {
// ========================================
// COMPTES COMPTABLES
// ========================================
/**
* Liste tous les comptes comptables actifs
*/
@GET
@Path("/comptes")
List<CompteComptableResponse> listerComptes();
/**
* Crée un nouveau compte comptable
*/
@POST
@Path("/comptes")
CompteComptableResponse creerCompte(CreateCompteComptableRequest request);
/**
* Trouve un compte comptable par son ID
*/
@GET
@Path("/comptes/{id}")
CompteComptableResponse obtenirCompte(@PathParam("id") UUID id);
// ========================================
// JOURNAUX COMPTABLES
// ========================================
/**
* Liste tous les journaux comptables actifs
*/
@GET
@Path("/journaux")
List<JournalComptableResponse> listerJournaux();
/**
* Crée un nouveau journal comptable
*/
@POST
@Path("/journaux")
JournalComptableResponse creerJournal(CreateJournalComptableRequest request);
/**
* Trouve un journal comptable par son ID
*/
@GET
@Path("/journaux/{id}")
JournalComptableResponse obtenirJournal(@PathParam("id") UUID id);
// ========================================
// ÉCRITURES COMPTABLES
// ========================================
/**
* Crée une nouvelle écriture comptable
*/
@POST
@Path("/ecritures")
EcritureComptableResponse creerEcriture(CreateEcritureComptableRequest request);
/**
* Trouve une écriture comptable par son ID
*/
@GET
@Path("/ecritures/{id}")
EcritureComptableResponse obtenirEcriture(@PathParam("id") UUID id);
/**
* Liste les écritures d'un journal
*/
@GET
@Path("/ecritures/journal/{journalId}")
List<EcritureComptableResponse> listerEcrituresParJournal(@PathParam("journalId") UUID journalId);
/**
* Liste les écritures d'une organisation
*/
@GET
@Path("/ecritures/organisation/{organisationId}")
List<EcritureComptableResponse> listerEcrituresParOrganisation(@PathParam("organisationId") UUID organisationId);
}

View File

@@ -0,0 +1,35 @@
package dev.lions.unionflow.client.service;
import dev.lions.unionflow.server.api.dto.config.request.*;
import dev.lions.unionflow.server.api.dto.config.response.*;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
/**
* Service REST client pour la gestion de la configuration système
*
* @author UnionFlow Team
* @version 1.0
*/
@RegisterRestClient(configKey = "unionflow-api")
@RegisterClientHeaders(dev.lions.unionflow.client.security.AuthHeaderFactory.class)
@Path("/api/configuration")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public interface ConfigurationService {
@GET
List<ConfigurationResponse> listerConfigurations();
@GET
@Path("/{cle}")
ConfigurationResponse obtenirConfiguration(@PathParam("cle") String cle);
@PUT
@Path("/{cle}")
ConfigurationResponse mettreAJourConfiguration(@PathParam("cle") String cle, UpdateConfigurationRequest request);
}

View File

@@ -1,6 +1,8 @@
package dev.lions.unionflow.client.service;
import dev.lions.unionflow.client.dto.CotisationDTO;
import dev.lions.unionflow.server.api.dto.cotisation.request.CreateCotisationRequest;
import dev.lions.unionflow.server.api.dto.cotisation.response.CotisationResponse;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
@@ -16,6 +18,7 @@ import java.util.UUID;
* @version 1.0
*/
@RegisterRestClient(configKey = "unionflow-api")
@RegisterClientHeaders(dev.lions.unionflow.client.security.AuthHeaderFactory.class)
@Path("/api/cotisations")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@@ -25,7 +28,7 @@ public interface CotisationService {
* Récupère toutes les cotisations avec pagination
*/
@GET
List<CotisationDTO> listerToutes(
List<CotisationResponse> listerToutes(
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("20") int size
);
@@ -35,21 +38,21 @@ public interface CotisationService {
*/
@GET
@Path("/{id}")
CotisationDTO obtenirParId(@PathParam("id") UUID id);
CotisationResponse obtenirParId(@PathParam("id") UUID id);
/**
* Récupère une cotisation par son numéro de référence
*/
@GET
@Path("/reference/{numeroReference}")
CotisationDTO obtenirParReference(@PathParam("numeroReference") String numeroReference);
CotisationResponse obtenirParReference(@PathParam("numeroReference") String numeroReference);
/**
* Récupère les cotisations d'un membre
*/
@GET
@Path("/membre/{membreId}")
List<CotisationDTO> obtenirParMembre(
List<CotisationResponse> obtenirParMembre(
@PathParam("membreId") UUID membreId,
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("20") int size
@@ -60,7 +63,7 @@ public interface CotisationService {
*/
@GET
@Path("/statut/{statut}")
List<CotisationDTO> obtenirParStatut(
List<CotisationResponse> obtenirParStatut(
@PathParam("statut") String statut,
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("20") int size
@@ -71,7 +74,7 @@ public interface CotisationService {
*/
@GET
@Path("/en-retard")
List<CotisationDTO> obtenirEnRetard(
List<CotisationResponse> obtenirEnRetard(
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("20") int size
);
@@ -81,7 +84,7 @@ public interface CotisationService {
*/
@GET
@Path("/recherche")
List<CotisationDTO> rechercher(
List<CotisationResponse> rechercher(
@QueryParam("membreId") UUID membreId,
@QueryParam("statut") String statut,
@QueryParam("typeCotisation") String typeCotisation,
@@ -102,14 +105,14 @@ public interface CotisationService {
* Crée une nouvelle cotisation
*/
@POST
CotisationDTO creer(CotisationDTO cotisation);
CotisationResponse creer(CreateCotisationRequest request);
/**
* Met à jour une cotisation existante
*/
@PUT
@Path("/{id}")
CotisationDTO modifier(@PathParam("id") UUID id, CotisationDTO cotisation);
CotisationResponse modifier(@PathParam("id") UUID id, CotisationResponse cotisation);
/**
* Supprime une cotisation

View File

@@ -0,0 +1,76 @@
package dev.lions.unionflow.client.service;
import dev.lions.unionflow.server.api.dto.dashboard.DashboardDataResponse;
import dev.lions.unionflow.server.api.dto.dashboard.DashboardStatsResponse;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import java.util.Map;
/**
* Service REST Client pour les APIs du dashboard
*
* @author UnionFlow Team
* @version 1.0
*/
@RegisterRestClient(configKey = "unionflow-api")
@RegisterClientHeaders(dev.lions.unionflow.client.security.AuthHeaderFactory.class)
@Path("/api/v1/dashboard")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public interface DashboardService {
/**
* Récupère toutes les données du dashboard
*/
@GET
@Path("/data")
DashboardDataResponse getDashboardData(
@QueryParam("organizationId") String organizationId,
@QueryParam("userId") String userId
);
/**
* Récupère uniquement les statistiques du dashboard
*/
@GET
@Path("/stats")
DashboardStatsResponse getDashboardStats(
@QueryParam("organizationId") String organizationId,
@QueryParam("userId") String userId
);
/**
* Récupère les activités récentes
*/
@GET
@Path("/activities")
Map<String, Object> getRecentActivities(
@QueryParam("organizationId") String organizationId,
@QueryParam("userId") String userId,
@QueryParam("limit") @DefaultValue("10") int limit
);
/**
* Récupère les événements à venir
*/
@GET
@Path("/events/upcoming")
Map<String, Object> getUpcomingEvents(
@QueryParam("organizationId") String organizationId,
@QueryParam("userId") String userId,
@QueryParam("limit") @DefaultValue("5") int limit
);
/**
* Rafraîchit les données du dashboard
*/
@POST
@Path("/refresh")
Map<String, Object> refreshDashboard(
@QueryParam("organizationId") String organizationId,
@QueryParam("userId") String userId
);
}

View File

@@ -1,31 +1,34 @@
package dev.lions.unionflow.client.service;
import dev.lions.unionflow.client.dto.DemandeAideDTO;
import dev.lions.unionflow.server.api.dto.solidarite.request.CreateDemandeAideRequest;
import dev.lions.unionflow.server.api.dto.solidarite.response.DemandeAideResponse;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
import java.util.UUID;
@RegisterRestClient(configKey = "unionflow-api")
@RegisterClientHeaders(dev.lions.unionflow.client.security.AuthHeaderFactory.class)
@Path("/api/demandes-aide")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public interface DemandeAideService {
@GET
List<DemandeAideDTO> listerToutes(
List<DemandeAideResponse> listerToutes(
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("20") int size
);
@GET
@Path("/{id}")
DemandeAideDTO obtenirParId(@PathParam("id") UUID id);
DemandeAideResponse obtenirParId(@PathParam("id") UUID id);
@GET
@Path("/search")
List<DemandeAideDTO> rechercher(
List<DemandeAideResponse> rechercher(
@QueryParam("statut") String statut,
@QueryParam("type") String type,
@QueryParam("urgence") String urgence,
@@ -34,11 +37,11 @@ public interface DemandeAideService {
);
@POST
DemandeAideDTO creer(DemandeAideDTO demande);
DemandeAideResponse creer(CreateDemandeAideRequest request);
@PUT
@Path("/{id}")
DemandeAideDTO modifier(@PathParam("id") UUID id, DemandeAideDTO demande);
DemandeAideResponse modifier(@PathParam("id") UUID id, DemandeAideResponse demande);
@DELETE
@Path("/{id}")
@@ -46,10 +49,10 @@ public interface DemandeAideService {
@PUT
@Path("/{id}/approuver")
DemandeAideDTO approuver(@PathParam("id") UUID id);
DemandeAideResponse approuver(@PathParam("id") UUID id);
@PUT
@Path("/{id}/rejeter")
DemandeAideDTO rejeter(@PathParam("id") UUID id);
DemandeAideResponse rejeter(@PathParam("id") UUID id);
}

View File

@@ -0,0 +1,59 @@
package dev.lions.unionflow.client.service;
import dev.lions.unionflow.server.api.dto.document.request.*;
import dev.lions.unionflow.server.api.dto.document.response.*;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
import java.util.UUID;
/**
* Service REST Client pour la gestion documentaire
*
* @author UnionFlow Team
* @version 1.0
*/
@RegisterRestClient(configKey = "unionflow-api")
@RegisterClientHeaders(dev.lions.unionflow.client.security.AuthHeaderFactory.class)
@Path("/api/documents")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public interface DocumentService {
/**
* Crée un nouveau document
*/
@POST
DocumentResponse creerDocument(CreateDocumentRequest request);
/**
* Trouve un document par son ID
*/
@GET
@Path("/{id}")
DocumentResponse obtenirDocument(@PathParam("id") UUID id);
/**
* Enregistre un téléchargement de document
*/
@POST
@Path("/{id}/telechargement")
void enregistrerTelechargement(@PathParam("id") UUID id);
/**
* Crée une pièce jointe
*/
@POST
@Path("/pieces-jointes")
PieceJointeResponse creerPieceJointe(CreatePieceJointeRequest request);
/**
* Liste toutes les pièces jointes d'un document
*/
@GET
@Path("/{documentId}/pieces-jointes")
List<PieceJointeResponse> listerPiecesJointes(@PathParam("documentId") UUID documentId);
}

View File

@@ -0,0 +1,292 @@
package dev.lions.unionflow.client.service;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import jakarta.inject.Inject;
import org.jboss.logging.Logger;
/**
* Service centralisé de gestion des erreurs et messages utilisateur.
*
* <p>Ce service fournit une interface unifiée pour :
* <ul>
* <li>Gérer les erreurs backend de manière cohérente</li>
* <li>Afficher des messages utilisateur appropriés</li>
* <li>Logger les erreurs de manière structurée</li>
* <li>Gérer les cas d'erreur spécifiques (connexion, autorisation, validation, etc.)</li>
* </ul>
*
* <p><strong>Production-ready:</strong> Gestion complète des erreurs avec logging structuré,
* messages utilisateur appropriés, et gestion des cas limites.
*
* @author UnionFlow Team
* @version 1.0
* @since 2025-12-24
*/
@ApplicationScoped
public class ErrorHandlerService {
private static final Logger LOG = Logger.getLogger(ErrorHandlerService.class);
@Inject
FacesContext facesContext;
/**
* Gère une exception backend et affiche un message approprié à l'utilisateur.
*
* @param exception L'exception à gérer
* @param contextMessage Message contextuel pour le logging (ex: "lors de la création d'une organisation")
* @param userFriendlyMessage Message affiché à l'utilisateur (peut être null pour utiliser le message par défaut)
*/
public void handleException(Exception exception, String contextMessage, String userFriendlyMessage) {
if (exception == null) {
LOG.warn("handleException appelé avec exception null pour: " + contextMessage);
return;
}
// Logging structuré de l'erreur
logError(exception, contextMessage);
// Déterminer le type d'erreur et le message approprié
FacesMessage.Severity severity = FacesMessage.SEVERITY_ERROR;
String message = userFriendlyMessage != null ? userFriendlyMessage : getDefaultErrorMessage(exception);
String detail = getErrorDetail(exception);
// Gestion spécifique selon le type d'exception
if (exception instanceof RestClientExceptionMapper.UnauthorizedException) {
severity = FacesMessage.SEVERITY_ERROR;
message = "Erreur d'autorisation";
detail = "Vous n'êtes pas autorisé à effectuer cette action. Veuillez vérifier vos permissions.";
} else if (exception instanceof RestClientExceptionMapper.ForbiddenException) {
severity = FacesMessage.SEVERITY_ERROR;
message = "Accès interdit";
detail = "Vous n'avez pas les permissions nécessaires pour accéder à cette ressource.";
} else if (exception instanceof RestClientExceptionMapper.BadRequestException) {
severity = FacesMessage.SEVERITY_ERROR;
message = "Données invalides";
detail = extractValidationErrors(exception.getMessage());
} else if (exception instanceof RestClientExceptionMapper.ConflictException) {
severity = FacesMessage.SEVERITY_WARN;
message = "Conflit";
detail = "Cette ressource existe déjà ou est en conflit avec une autre.";
} else if (exception instanceof RestClientExceptionMapper.NotFoundException) {
severity = FacesMessage.SEVERITY_WARN;
message = "Ressource introuvable";
detail = "La ressource demandée n'existe pas ou a été supprimée.";
} else if (exception instanceof RestClientExceptionMapper.UnprocessableEntityException) {
severity = FacesMessage.SEVERITY_ERROR;
message = "Données non valides";
detail = extractValidationErrors(exception.getMessage());
} else if (exception instanceof jakarta.ws.rs.ProcessingException) {
severity = FacesMessage.SEVERITY_ERROR;
message = "Erreur de connexion";
detail = "Impossible de se connecter au serveur. Vérifiez votre connexion réseau et que le serveur est démarré.";
} else if (exception instanceof java.net.ConnectException ||
exception.getCause() instanceof java.net.ConnectException) {
severity = FacesMessage.SEVERITY_ERROR;
message = "Serveur inaccessible";
detail = "Le serveur backend n'est pas accessible. Vérifiez qu'il est démarré et accessible.";
} else if (exception instanceof java.util.concurrent.TimeoutException ||
exception.getCause() instanceof java.util.concurrent.TimeoutException) {
severity = FacesMessage.SEVERITY_WARN;
message = "Délai d'attente dépassé";
detail = "La requête a pris trop de temps. Veuillez réessayer.";
}
// Afficher le message à l'utilisateur
addFacesMessage(severity, message, detail);
}
/**
* Gère une erreur de validation et affiche les messages appropriés.
*
* @param validationErrors Liste des erreurs de validation
* @param contextMessage Message contextuel pour le logging
*/
public void handleValidationErrors(java.util.List<String> validationErrors, String contextMessage) {
if (validationErrors == null || validationErrors.isEmpty()) {
return;
}
LOG.warnf("Erreurs de validation %s: %d erreur(s)", contextMessage, validationErrors.size());
// Afficher chaque erreur de validation
for (String error : validationErrors) {
addFacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur de validation", error);
}
}
/**
* Affiche un message de succès à l'utilisateur.
*
* @param summary Résumé du message
* @param detail Détail du message
*/
public void showSuccess(String summary, String detail) {
addFacesMessage(FacesMessage.SEVERITY_INFO, summary, detail);
}
/**
* Affiche un message d'information à l'utilisateur.
*
* @param summary Résumé du message
* @param detail Détail du message
*/
public void showInfo(String summary, String detail) {
addFacesMessage(FacesMessage.SEVERITY_INFO, summary, detail);
}
/**
* Affiche un message d'avertissement à l'utilisateur.
*
* @param summary Résumé du message
* @param detail Détail du message
*/
public void showWarning(String summary, String detail) {
addFacesMessage(FacesMessage.SEVERITY_WARN, summary, detail);
}
/**
* Log l'erreur de manière structurée.
*/
private void logError(Exception exception, String contextMessage) {
String errorId = java.util.UUID.randomUUID().toString().substring(0, 8);
LOG.errorf("=== ERREUR [%s] ===", errorId);
LOG.errorf("Contexte: %s", contextMessage);
LOG.errorf("Type: %s", exception.getClass().getName());
LOG.errorf("Message: %s", exception.getMessage());
if (exception.getCause() != null) {
LOG.errorf("Cause: %s - %s",
exception.getCause().getClass().getName(),
exception.getCause().getMessage());
}
// Stack trace complet en mode DEBUG seulement
if (LOG.isDebugEnabled()) {
LOG.errorf("Stack trace:", exception);
} else {
// En production, logger seulement les 5 premières lignes du stack trace
StackTraceElement[] stackTrace = exception.getStackTrace();
int maxLines = Math.min(5, stackTrace.length);
for (int i = 0; i < maxLines; i++) {
LOG.errorf(" at %s", stackTrace[i]);
}
}
LOG.errorf("=== FIN ERREUR [%s] ===", errorId);
}
/**
* Obtient le message d'erreur par défaut selon le type d'exception.
*/
private String getDefaultErrorMessage(Exception exception) {
if (exception.getMessage() != null && !exception.getMessage().trim().isEmpty()) {
return exception.getMessage();
}
return "Une erreur inattendue s'est produite. Veuillez réessayer.";
}
/**
* Extrait les détails d'erreur de validation depuis le message d'exception.
*/
private String extractValidationErrors(String errorMessage) {
if (errorMessage == null || errorMessage.trim().isEmpty()) {
return "Les données fournies ne sont pas valides.";
}
// Si le message contient du JSON (erreur de validation Bean Validation)
if (errorMessage.contains("\"objectName\"") || errorMessage.contains("\"attributeName\"")) {
try {
// Essayer d'extraire les informations de validation
if (errorMessage.contains("\"attributeName\"")) {
// Extraire le nom de l'attribut
int attrIndex = errorMessage.indexOf("\"attributeName\"");
if (attrIndex > 0) {
int start = errorMessage.indexOf("\"", attrIndex + 16) + 1;
int end = errorMessage.indexOf("\"", start);
if (start > 0 && end > start) {
String attributeName = errorMessage.substring(start, end);
return "Le champ '" + attributeName + "' contient une valeur invalide.";
}
}
}
} catch (Exception e) {
LOG.debugf("Impossible d'extraire les détails de validation: %s", e.getMessage());
}
}
return errorMessage;
}
/**
* Obtient le détail de l'erreur pour l'affichage à l'utilisateur.
*/
private String getErrorDetail(Exception exception) {
String message = exception.getMessage();
// Pour les erreurs de validation, extraire les détails
if (exception instanceof RestClientExceptionMapper.BadRequestException ||
exception instanceof RestClientExceptionMapper.UnprocessableEntityException) {
return extractValidationErrors(message);
}
// Pour les autres erreurs, utiliser le message de l'exception
if (message != null && !message.trim().isEmpty()) {
// Limiter la longueur du message pour l'utilisateur
if (message.length() > 200) {
return message.substring(0, 197) + "...";
}
return message;
}
return "Une erreur technique s'est produite. Veuillez contacter le support si le problème persiste.";
}
/**
* Ajoute un message Faces avec gestion du Flash Scope pour les redirections.
*/
private void addFacesMessage(FacesMessage.Severity severity, String summary, String detail) {
if (facesContext == null) {
facesContext = FacesContext.getCurrentInstance();
}
if (facesContext != null) {
FacesMessage message = new FacesMessage(severity, summary, detail);
facesContext.addMessage(null, message);
// Activer le Flash Scope pour que le message survive à une redirection
facesContext.getExternalContext().getFlash().setKeepMessages(true);
}
}
/**
* Vérifie si une exception est une erreur de connexion au backend.
*/
public boolean isConnectionError(Exception exception) {
return exception instanceof jakarta.ws.rs.ProcessingException ||
exception instanceof java.net.ConnectException ||
(exception.getCause() != null && exception.getCause() instanceof java.net.ConnectException) ||
exception instanceof java.util.concurrent.TimeoutException ||
(exception.getCause() != null && exception.getCause() instanceof java.util.concurrent.TimeoutException);
}
/**
* Vérifie si une exception est une erreur d'autorisation.
*/
public boolean isAuthorizationError(Exception exception) {
return exception instanceof RestClientExceptionMapper.UnauthorizedException ||
exception instanceof RestClientExceptionMapper.ForbiddenException;
}
/**
* Vérifie si une exception est une erreur de validation.
*/
public boolean isValidationError(Exception exception) {
return exception instanceof RestClientExceptionMapper.BadRequestException ||
exception instanceof RestClientExceptionMapper.UnprocessableEntityException;
}
}

View File

@@ -1,6 +1,7 @@
package dev.lions.unionflow.client.service;
import dev.lions.unionflow.client.dto.EvenementDTO;
import dev.lions.unionflow.server.api.dto.evenement.response.EvenementResponse;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
@@ -16,6 +17,7 @@ import java.util.UUID;
* @version 2.0
*/
@RegisterRestClient(configKey = "unionflow-api")
@RegisterClientHeaders(dev.lions.unionflow.client.security.AuthHeaderFactory.class)
@Path("/api/evenements")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@@ -37,20 +39,20 @@ public interface EvenementService {
*/
@GET
@Path("/{id}")
EvenementDTO obtenirParId(@PathParam("id") UUID id);
EvenementResponse obtenirParId(@PathParam("id") UUID id);
/**
* Crée un nouvel événement
*/
@POST
EvenementDTO creer(EvenementDTO evenement);
EvenementResponse creer(EvenementResponse evenement);
/**
* Met à jour un événement existant
*/
@PUT
@Path("/{id}")
EvenementDTO modifier(@PathParam("id") UUID id, EvenementDTO evenement);
EvenementResponse modifier(@PathParam("id") UUID id, EvenementResponse evenement);
/**
* Supprime un événement

View File

@@ -1,6 +1,7 @@
package dev.lions.unionflow.client.service;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
@@ -10,6 +11,7 @@ import java.util.UUID;
* Service REST client pour l'export des données
*/
@RegisterRestClient(configKey = "unionflow-api")
@RegisterClientHeaders(dev.lions.unionflow.client.security.AuthHeaderFactory.class)
@Path("/api/export")
@Consumes(MediaType.APPLICATION_JSON)
public interface ExportClientService {

View File

@@ -0,0 +1,41 @@
package dev.lions.unionflow.client.service;
import dev.lions.unionflow.server.api.dto.favoris.request.*;
import dev.lions.unionflow.server.api.dto.favoris.response.*;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* Service REST client pour la gestion des favoris
*
* @author UnionFlow Team
* @version 1.0
*/
@RegisterRestClient(configKey = "unionflow-api")
@RegisterClientHeaders(dev.lions.unionflow.client.security.AuthHeaderFactory.class)
@Path("/api/favoris")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public interface FavorisService {
@GET
@Path("/utilisateur/{utilisateurId}")
List<FavoriResponse> listerFavoris(@PathParam("utilisateurId") UUID utilisateurId);
@POST
FavoriResponse creerFavori(CreateFavoriRequest request);
@DELETE
@Path("/{id}")
void supprimerFavori(@PathParam("id") UUID id);
@GET
@Path("/utilisateur/{utilisateurId}/statistiques")
Map<String, Object> obtenirStatistiques(@PathParam("utilisateurId") UUID utilisateurId);
}

View File

@@ -1,39 +1,41 @@
package dev.lions.unionflow.client.service;
import dev.lions.unionflow.client.dto.FormulaireDTO;
import dev.lions.unionflow.server.api.dto.formuleabonnement.response.FormuleAbonnementResponse;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
import java.util.UUID;
@RegisterRestClient(configKey = "unionflow-api")
@RegisterClientHeaders(dev.lions.unionflow.client.security.AuthHeaderFactory.class)
@Path("/api/formulaires")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public interface FormulaireService {
@GET
List<FormulaireDTO> listerTous();
List<FormuleAbonnementResponse> listerTous();
@GET
@Path("/{id}")
FormulaireDTO obtenirParId(@PathParam("id") UUID id);
FormuleAbonnementResponse obtenirParId(@PathParam("id") UUID id);
@GET
@Path("/actifs")
List<FormulaireDTO> listerActifs();
List<FormuleAbonnementResponse> listerActifs();
@GET
@Path("/populaires")
List<FormulaireDTO> listerPopulaires();
List<FormuleAbonnementResponse> listerPopulaires();
@POST
FormulaireDTO creer(FormulaireDTO formulaire);
FormuleAbonnementResponse creer(FormuleAbonnementResponse formulaire);
@PUT
@Path("/{id}")
FormulaireDTO modifier(@PathParam("id") UUID id, FormulaireDTO formulaire);
FormuleAbonnementResponse modifier(@PathParam("id") UUID id, FormuleAbonnementResponse formulaire);
@DELETE
@Path("/{id}")
@@ -41,10 +43,10 @@ public interface FormulaireService {
@PUT
@Path("/{id}/activer")
FormulaireDTO activer(@PathParam("id") UUID id);
FormuleAbonnementResponse activer(@PathParam("id") UUID id);
@PUT
@Path("/{id}/desactiver")
FormulaireDTO desactiver(@PathParam("id") UUID id);
FormuleAbonnementResponse desactiver(@PathParam("id") UUID id);
}

View File

@@ -2,7 +2,6 @@ package dev.lions.unionflow.client.service;
import jakarta.ws.rs.FormParam;
import jakarta.ws.rs.core.MediaType;
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.jboss.resteasy.reactive.PartType;
import java.util.UUID;

View File

@@ -1,6 +1,7 @@
package dev.lions.unionflow.client.service;
import dev.lions.unionflow.client.dto.MembreDTO;
import dev.lions.unionflow.server.api.dto.membre.response.MembreResponse;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
@@ -8,112 +9,120 @@ import java.util.List;
import java.util.UUID;
@RegisterRestClient(configKey = "unionflow-api")
@RegisterClientHeaders(dev.lions.unionflow.client.security.AuthHeaderFactory.class)
@Path("/api/membres")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public interface MembreService {
@GET
List<MembreDTO> listerTous();
List<MembreResponse> listerTous();
@GET
@Path("/{id}")
MembreDTO obtenirParId(@PathParam("id") UUID id);
MembreResponse obtenirParId(@PathParam("id") UUID id);
@GET
@Path("/numero/{numeroMembre}")
MembreDTO obtenirParNumero(@PathParam("numeroMembre") String numeroMembre);
MembreResponse obtenirParNumero(@PathParam("numeroMembre") String numeroMembre);
@GET
@Path("/search")
List<MembreDTO> rechercher(
@QueryParam("nom") String nom,
@QueryParam("prenom") String prenom,
@QueryParam("email") String email,
@QueryParam("telephone") String telephone,
@QueryParam("statut") String statut,
@QueryParam("associationId") UUID associationId,
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("20") int size
);
List<MembreResponse> rechercher(
@QueryParam("nom") String nom,
@QueryParam("prenom") String prenom,
@QueryParam("email") String email,
@QueryParam("telephone") String telephone,
@QueryParam("statut") String statut,
@QueryParam("associationId") UUID associationId,
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("20") int size);
@POST
@Path("/search/advanced")
dev.lions.unionflow.server.api.dto.membre.MembreSearchResultDTO rechercherAvance(
dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria criteria,
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("20") int size,
@QueryParam("sort") @DefaultValue("nom") String sortField,
@QueryParam("direction") @DefaultValue("asc") String sortDirection);
@GET
@Path("/association/{associationId}")
List<MembreDTO> listerParAssociation(@PathParam("associationId") UUID associationId);
List<MembreResponse> listerParAssociation(@PathParam("associationId") UUID associationId);
@GET
@Path("/actifs")
List<MembreDTO> listerActifs();
List<MembreResponse> listerActifs();
@GET
@Path("/inactifs")
List<MembreDTO> listerInactifs();
List<MembreResponse> listerInactifs();
@POST
MembreDTO creer(MembreDTO membre);
MembreResponse creer(MembreResponse membre);
@PUT
@Path("/{id}")
MembreDTO modifier(@PathParam("id") UUID id, MembreDTO membre);
MembreResponse modifier(@PathParam("id") UUID id, MembreResponse membre);
@DELETE
@Path("/{id}")
void supprimer(@PathParam("id") UUID id);
@PUT
@Path("/{id}/activer")
MembreDTO activer(@PathParam("id") UUID id);
MembreResponse activer(@PathParam("id") UUID id);
@PUT
@Path("/{id}/desactiver")
MembreDTO desactiver(@PathParam("id") UUID id);
MembreResponse desactiver(@PathParam("id") UUID id);
@PUT
@Path("/{id}/suspendre")
MembreDTO suspendre(@PathParam("id") UUID id);
MembreResponse suspendre(@PathParam("id") UUID id);
@PUT
@Path("/{id}/radier")
MembreDTO radier(@PathParam("id") UUID id);
MembreResponse radier(@PathParam("id") UUID id);
@GET
@Path("/statistiques")
@Path("/stats")
StatistiquesMembreDTO obtenirStatistiques();
@GET
@Path("/export")
@Produces({"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "text/csv", "application/pdf", "application/json"})
@Produces({ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "text/csv", "application/pdf",
"application/json" })
byte[] exporterExcel(
@QueryParam("format") @DefaultValue("EXCEL") String format,
@QueryParam("associationId") UUID associationId,
@QueryParam("statut") String statut,
@QueryParam("type") String type,
@QueryParam("dateAdhesionDebut") String dateAdhesionDebut,
@QueryParam("dateAdhesionFin") String dateAdhesionFin,
@QueryParam("colonnes") List<String> colonnesExport,
@QueryParam("inclureHeaders") @DefaultValue("true") boolean inclureHeaders,
@QueryParam("formaterDates") @DefaultValue("true") boolean formaterDates,
@QueryParam("inclureStatistiques") @DefaultValue("false") boolean inclureStatistiques,
@QueryParam("motDePasse") String motDePasse
);
@QueryParam("format") @DefaultValue("EXCEL") String format,
@QueryParam("associationId") UUID associationId,
@QueryParam("statut") String statut,
@QueryParam("type") String type,
@QueryParam("dateAdhesionDebut") String dateAdhesionDebut,
@QueryParam("dateAdhesionFin") String dateAdhesionFin,
@QueryParam("colonnes") List<String> colonnesExport,
@QueryParam("inclureHeaders") @DefaultValue("true") boolean inclureHeaders,
@QueryParam("formaterDates") @DefaultValue("true") boolean formaterDates,
@QueryParam("inclureStatistiques") @DefaultValue("false") boolean inclureStatistiques,
@QueryParam("motDePasse") String motDePasse);
@GET
@Path("/export/count")
@Produces(MediaType.APPLICATION_JSON)
Long compterMembresPourExport(
@QueryParam("associationId") UUID associationId,
@QueryParam("statut") String statut,
@QueryParam("type") String type,
@QueryParam("dateAdhesionDebut") String dateAdhesionDebut,
@QueryParam("dateAdhesionFin") String dateAdhesionFin
);
@QueryParam("associationId") UUID associationId,
@QueryParam("statut") String statut,
@QueryParam("type") String type,
@QueryParam("dateAdhesionDebut") String dateAdhesionDebut,
@QueryParam("dateAdhesionFin") String dateAdhesionFin);
@POST
@Path("/import")
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces(MediaType.APPLICATION_JSON)
ResultatImportDTO importerDonnees(MembreImportMultipartForm form);
@GET
@Path("/import/modele")
@Produces("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
@@ -132,9 +141,9 @@ public interface MembreService {
@Consumes(MediaType.APPLICATION_JSON)
@Produces("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
byte[] exporterSelection(
List<UUID> membreIds,
@QueryParam("format") @DefaultValue("EXCEL") String format);
List<UUID> membreIds,
@QueryParam("format") @DefaultValue("EXCEL") String format);
// Classes DTO internes pour les réponses spécialisées
class StatistiquesMembreDTO {
public Long totalMembres;
@@ -145,60 +154,127 @@ public interface MembreService {
public Long nouveauxMembres30Jours;
public Double tauxActivite;
public Double tauxCroissance;
// Constructeurs
public StatistiquesMembreDTO() {}
public StatistiquesMembreDTO() {
}
// Getters et setters
public Long getTotalMembres() { return totalMembres; }
public void setTotalMembres(Long totalMembres) { this.totalMembres = totalMembres; }
public Long getMembresActifs() { return membresActifs; }
public void setMembresActifs(Long membresActifs) { this.membresActifs = membresActifs; }
public Long getMembresInactifs() { return membresInactifs; }
public void setMembresInactifs(Long membresInactifs) { this.membresInactifs = membresInactifs; }
public Long getMembresSuspendus() { return membresSuspendus; }
public void setMembresSuspendus(Long membresSuspendus) { this.membresSuspendus = membresSuspendus; }
public Long getMembresRadies() { return membresRadies; }
public void setMembresRadies(Long membresRadies) { this.membresRadies = membresRadies; }
public Long getNouveauxMembres30Jours() { return nouveauxMembres30Jours; }
public void setNouveauxMembres30Jours(Long nouveauxMembres30Jours) { this.nouveauxMembres30Jours = nouveauxMembres30Jours; }
public Double getTauxActivite() { return tauxActivite; }
public void setTauxActivite(Double tauxActivite) { this.tauxActivite = tauxActivite; }
public Double getTauxCroissance() { return tauxCroissance; }
public void setTauxCroissance(Double tauxCroissance) { this.tauxCroissance = tauxCroissance; }
public Long getTotalMembres() {
return totalMembres;
}
public void setTotalMembres(Long totalMembres) {
this.totalMembres = totalMembres;
}
public Long getMembresActifs() {
return membresActifs;
}
public void setMembresActifs(Long membresActifs) {
this.membresActifs = membresActifs;
}
public Long getMembresInactifs() {
return membresInactifs;
}
public void setMembresInactifs(Long membresInactifs) {
this.membresInactifs = membresInactifs;
}
public Long getMembresSuspendus() {
return membresSuspendus;
}
public void setMembresSuspendus(Long membresSuspendus) {
this.membresSuspendus = membresSuspendus;
}
public Long getMembresRadies() {
return membresRadies;
}
public void setMembresRadies(Long membresRadies) {
this.membresRadies = membresRadies;
}
public Long getNouveauxMembres30Jours() {
return nouveauxMembres30Jours;
}
public void setNouveauxMembres30Jours(Long nouveauxMembres30Jours) {
this.nouveauxMembres30Jours = nouveauxMembres30Jours;
}
public Double getTauxActivite() {
return tauxActivite;
}
public void setTauxActivite(Double tauxActivite) {
this.tauxActivite = tauxActivite;
}
public Double getTauxCroissance() {
return tauxCroissance;
}
public void setTauxCroissance(Double tauxCroissance) {
this.tauxCroissance = tauxCroissance;
}
}
class ResultatImportDTO {
public Integer totalLignes;
public Integer lignesTraitees;
public Integer lignesErreur;
public List<String> erreurs;
public List<MembreDTO> membresImportes;
public List<MembreResponse> membresImportes;
// Constructeurs
public ResultatImportDTO() {}
public ResultatImportDTO() {
}
// Getters et setters
public Integer getTotalLignes() { return totalLignes; }
public void setTotalLignes(Integer totalLignes) { this.totalLignes = totalLignes; }
public Integer getLignesTraitees() { return lignesTraitees; }
public void setLignesTraitees(Integer lignesTraitees) { this.lignesTraitees = lignesTraitees; }
public Integer getLignesErreur() { return lignesErreur; }
public void setLignesErreur(Integer lignesErreur) { this.lignesErreur = lignesErreur; }
public List<String> getErreurs() { return erreurs; }
public void setErreurs(List<String> erreurs) { this.erreurs = erreurs; }
public List<MembreDTO> getMembresImportes() { return membresImportes; }
public void setMembresImportes(List<MembreDTO> membresImportes) { this.membresImportes = membresImportes; }
public Integer getTotalLignes() {
return totalLignes;
}
public void setTotalLignes(Integer totalLignes) {
this.totalLignes = totalLignes;
}
public Integer getLignesTraitees() {
return lignesTraitees;
}
public void setLignesTraitees(Integer lignesTraitees) {
this.lignesTraitees = lignesTraitees;
}
public Integer getLignesErreur() {
return lignesErreur;
}
public void setLignesErreur(Integer lignesErreur) {
this.lignesErreur = lignesErreur;
}
public List<String> getErreurs() {
return erreurs;
}
public void setErreurs(List<String> erreurs) {
this.erreurs = erreurs;
}
public List<MembreResponse> getMembresImportes() {
return membresImportes;
}
public void setMembresImportes(List<MembreResponse> membresImportes) {
this.membresImportes = membresImportes;
}
}
}
}

View File

@@ -0,0 +1,304 @@
package dev.lions.unionflow.client.service;
import jakarta.enterprise.context.ApplicationScoped;
import org.jboss.logging.Logger;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
/**
* Service de monitoring et métriques pour la production
*
* Collecte et expose des métriques applicatives :
* - Compteurs d'appels backend
* - Temps de réponse (min, max, moyenne)
* - Taux d'erreurs
* - Utilisation du cache
* - Statistiques de retry
*
* @author UnionFlow Team
* @version 3.0
* @since 2026-01-04
*/
@ApplicationScoped
public class MetricsService {
private static final Logger LOG = Logger.getLogger(MetricsService.class);
// Compteurs d'appels backend
private final Map<String, AtomicLong> backendCallCounters = new ConcurrentHashMap<>();
private final Map<String, AtomicLong> backendSuccessCounters = new ConcurrentHashMap<>();
private final Map<String, AtomicLong> backendErrorCounters = new ConcurrentHashMap<>();
// Temps de réponse
private final Map<String, ResponseTimeStats> responseTimeStats = new ConcurrentHashMap<>();
// Cache metrics
private final AtomicLong cacheHits = new AtomicLong(0);
private final AtomicLong cacheMisses = new AtomicLong(0);
// Retry metrics
private final AtomicLong retryAttempts = new AtomicLong(0);
private final AtomicLong retrySuccesses = new AtomicLong(0);
private final AtomicLong retryFailures = new AtomicLong(0);
// Validation metrics
private final AtomicLong validationSuccesses = new AtomicLong(0);
private final AtomicLong validationFailures = new AtomicLong(0);
// Timestamp de démarrage
private final Instant startTime = Instant.now();
/**
* Enregistre un appel backend
*/
public void recordBackendCall(String serviceName, boolean success, long durationMs) {
backendCallCounters.computeIfAbsent(serviceName, k -> new AtomicLong(0)).incrementAndGet();
if (success) {
backendSuccessCounters.computeIfAbsent(serviceName, k -> new AtomicLong(0)).incrementAndGet();
} else {
backendErrorCounters.computeIfAbsent(serviceName, k -> new AtomicLong(0)).incrementAndGet();
}
// Enregistrer le temps de réponse
ResponseTimeStats stats = responseTimeStats.computeIfAbsent(serviceName, k -> new ResponseTimeStats());
stats.record(durationMs);
LOG.debugf("Métrique backend: %s - success=%b, durée=%dms", serviceName, success, durationMs);
}
/**
* Enregistre un hit de cache
*/
public void recordCacheHit() {
cacheHits.incrementAndGet();
}
/**
* Enregistre un miss de cache
*/
public void recordCacheMiss() {
cacheMisses.incrementAndGet();
}
/**
* Enregistre une tentative de retry
*/
public void recordRetryAttempt() {
retryAttempts.incrementAndGet();
}
/**
* Enregistre un retry réussi
*/
public void recordRetrySuccess() {
retrySuccesses.incrementAndGet();
}
/**
* Enregistre un retry échoué
*/
public void recordRetryFailure() {
retryFailures.incrementAndGet();
}
/**
* Enregistre une validation
*/
public void recordValidation(boolean success) {
if (success) {
validationSuccesses.incrementAndGet();
} else {
validationFailures.incrementAndGet();
}
}
/**
* Obtient les métriques globales
*/
public Map<String, Object> getGlobalMetrics() {
Map<String, Object> metrics = new HashMap<>();
// Uptime
Duration uptime = Duration.between(startTime, Instant.now());
metrics.put("uptime_seconds", uptime.getSeconds());
metrics.put("uptime_hours", uptime.toHours());
// Backend calls
long totalCalls = backendCallCounters.values().stream()
.mapToLong(AtomicLong::get)
.sum();
long totalSuccesses = backendSuccessCounters.values().stream()
.mapToLong(AtomicLong::get)
.sum();
long totalErrors = backendErrorCounters.values().stream()
.mapToLong(AtomicLong::get)
.sum();
metrics.put("backend_calls_total", totalCalls);
metrics.put("backend_calls_success", totalSuccesses);
metrics.put("backend_calls_errors", totalErrors);
metrics.put("backend_success_rate", totalCalls > 0 ? (double) totalSuccesses / totalCalls * 100 : 0);
// Cache
long totalCacheAccess = cacheHits.get() + cacheMisses.get();
metrics.put("cache_hits", cacheHits.get());
metrics.put("cache_misses", cacheMisses.get());
metrics.put("cache_hit_rate", totalCacheAccess > 0 ? (double) cacheHits.get() / totalCacheAccess * 100 : 0);
// Retry
metrics.put("retry_attempts", retryAttempts.get());
metrics.put("retry_successes", retrySuccesses.get());
metrics.put("retry_failures", retryFailures.get());
metrics.put("retry_success_rate", retryAttempts.get() > 0 ? (double) retrySuccesses.get() / retryAttempts.get() * 100 : 0);
// Validation
long totalValidations = validationSuccesses.get() + validationFailures.get();
metrics.put("validation_successes", validationSuccesses.get());
metrics.put("validation_failures", validationFailures.get());
metrics.put("validation_success_rate", totalValidations > 0 ? (double) validationSuccesses.get() / totalValidations * 100 : 0);
return metrics;
}
/**
* Obtient les métriques par service
*/
public Map<String, Map<String, Object>> getServiceMetrics() {
Map<String, Map<String, Object>> serviceMetrics = new HashMap<>();
for (String serviceName : backendCallCounters.keySet()) {
Map<String, Object> metrics = new HashMap<>();
long calls = backendCallCounters.getOrDefault(serviceName, new AtomicLong(0)).get();
long successes = backendSuccessCounters.getOrDefault(serviceName, new AtomicLong(0)).get();
long errors = backendErrorCounters.getOrDefault(serviceName, new AtomicLong(0)).get();
metrics.put("calls", calls);
metrics.put("successes", successes);
metrics.put("errors", errors);
metrics.put("success_rate", calls > 0 ? (double) successes / calls * 100 : 0);
ResponseTimeStats stats = responseTimeStats.get(serviceName);
if (stats != null) {
metrics.put("response_time_min_ms", stats.getMin());
metrics.put("response_time_max_ms", stats.getMax());
metrics.put("response_time_avg_ms", stats.getAverage());
metrics.put("response_time_p95_ms", stats.getP95());
}
serviceMetrics.put(serviceName, metrics);
}
return serviceMetrics;
}
/**
* Obtient un résumé des métriques sous forme de String (pour logging)
*/
public String getMetricsSummary() {
Map<String, Object> metrics = getGlobalMetrics();
return String.format(
"Métriques UnionFlow - Uptime: %dh, Backend: %d appels (%d%% succès), " +
"Cache: %d hits (%d%% hit rate), Retry: %d tentatives (%d%% succès)",
metrics.get("uptime_hours"),
metrics.get("backend_calls_total"),
Math.round((Double) metrics.get("backend_success_rate")),
metrics.get("cache_hits"),
Math.round((Double) metrics.get("cache_hit_rate")),
metrics.get("retry_attempts"),
Math.round((Double) metrics.get("retry_success_rate"))
);
}
/**
* Réinitialise toutes les métriques
*/
public void resetMetrics() {
LOG.info("Réinitialisation de toutes les métriques");
backendCallCounters.clear();
backendSuccessCounters.clear();
backendErrorCounters.clear();
responseTimeStats.clear();
cacheHits.set(0);
cacheMisses.set(0);
retryAttempts.set(0);
retrySuccesses.set(0);
retryFailures.set(0);
validationSuccesses.set(0);
validationFailures.set(0);
}
/**
* Log les métriques actuelles
*/
public void logMetrics() {
LOG.info(getMetricsSummary());
// Détail par service
Map<String, Map<String, Object>> serviceMetrics = getServiceMetrics();
for (Map.Entry<String, Map<String, Object>> entry : serviceMetrics.entrySet()) {
Map<String, Object> metrics = entry.getValue();
LOG.infof("Service %s: %d appels, %d ms avg, %d%% succès",
entry.getKey(),
metrics.get("calls"),
metrics.getOrDefault("response_time_avg_ms", 0L),
Math.round((Double) metrics.getOrDefault("success_rate", 0.0)));
}
}
/**
* Classe interne pour statistiques de temps de réponse
*/
private static class ResponseTimeStats {
private long min = Long.MAX_VALUE;
private long max = Long.MIN_VALUE;
private long sum = 0;
private long count = 0;
private final List<Long> samples = new ArrayList<>();
private static final int MAX_SAMPLES = 100; // Garder les 100 derniers pour P95
public synchronized void record(long durationMs) {
min = Math.min(min, durationMs);
max = Math.max(max, durationMs);
sum += durationMs;
count++;
samples.add(durationMs);
if (samples.size() > MAX_SAMPLES) {
samples.remove(0);
}
}
public long getMin() {
return min == Long.MAX_VALUE ? 0 : min;
}
public long getMax() {
return max == Long.MIN_VALUE ? 0 : max;
}
public long getAverage() {
return count > 0 ? sum / count : 0;
}
public long getP95() {
if (samples.isEmpty()) return 0;
List<Long> sorted = new ArrayList<>(samples);
sorted.sort(Long::compareTo);
int index = (int) Math.ceil(sorted.size() * 0.95) - 1;
return sorted.get(Math.max(0, index));
}
}
}

View File

@@ -1,6 +1,7 @@
package dev.lions.unionflow.client.service;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
@@ -10,6 +11,7 @@ import java.util.Map;
* Service REST client pour les notifications
*/
@RegisterRestClient(configKey = "unionflow-api")
@RegisterClientHeaders(dev.lions.unionflow.client.security.AuthHeaderFactory.class)
@Path("/api/notifications")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)

View File

@@ -1,6 +1,9 @@
package dev.lions.unionflow.client.service;
import dev.lions.unionflow.server.api.dto.notification.request.*;
import dev.lions.unionflow.server.api.dto.notification.response.*;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
@@ -8,17 +11,74 @@ import java.util.Map;
import java.util.UUID;
/**
* Service REST Client pour la gestion des notifications (WOU/DRY)
* Service REST Client pour la gestion des notifications
*
* @author UnionFlow Team
* @version 3.0
*/
@RegisterRestClient(configKey = "unionflow-api")
@RegisterClientHeaders(dev.lions.unionflow.client.security.AuthHeaderFactory.class)
@Path("/api/notifications")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public interface NotificationService {
// ========================================
// TEMPLATES
// ========================================
/**
* Crée un nouveau template de notification
*/
@POST
@Path("/templates")
TemplateNotificationResponse creerTemplate(CreateTemplateNotificationRequest request);
// ========================================
// NOTIFICATIONS
// ========================================
/**
* Crée une nouvelle notification
*/
@POST
NotificationResponse creerNotification(CreateNotificationRequest request);
/**
* Marque une notification comme lue
*/
@POST
@Path("/{id}/marquer-lue")
NotificationResponse marquerCommeLue(@PathParam("id") UUID id);
/**
* Trouve une notification par son ID
*/
@GET
@Path("/{id}")
NotificationResponse obtenirNotification(@PathParam("id") UUID id);
/**
* Liste toutes les notifications d'un membre
*/
@GET
@Path("/membre/{membreId}")
List<NotificationResponse> listerNotificationsParMembre(@PathParam("membreId") UUID membreId);
/**
* Liste les notifications non lues d'un membre
*/
@GET
@Path("/membre/{membreId}/non-lues")
List<NotificationResponse> listerNotificationsNonLuesParMembre(@PathParam("membreId") UUID membreId);
/**
* Liste les notifications en attente d'envoi
*/
@GET
@Path("/en-attente-envoi")
List<NotificationResponse> listerNotificationsEnAttenteEnvoi();
/**
* Envoie des notifications groupées à plusieurs membres (WOU/DRY)
*

View File

@@ -1,12 +1,14 @@
package dev.lions.unionflow.client.service;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import java.util.Map;
import java.util.UUID;
@RegisterRestClient(configKey = "unionflow-api")
@RegisterClientHeaders(dev.lions.unionflow.client.security.AuthHeaderFactory.class)
@Path("/api/preferences")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)

View File

@@ -1,41 +1,58 @@
package dev.lions.unionflow.client.service;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.rest.client.ext.ResponseExceptionMapper;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
public class RestClientExceptionMapper implements ResponseExceptionMapper<RuntimeException> {
@Override
public RuntimeException toThrowable(Response response) {
int status = response.getStatus();
String reasonPhrase = response.getStatusInfo().getReasonPhrase();
// Lire le corps de la réponse pour plus de détails
String body = "";
java.util.logging.Logger logger = java.util.logging.Logger.getLogger(RestClientExceptionMapper.class.getName());
// Logger l'URL et le statut pour debugging
try {
if (response.hasEntity()) {
body = response.readEntity(String.class);
if (response.getLocation() != null) {
logger.severe("Erreur backend - URL: " + response.getLocation() + " - Status: " + status);
}
} catch (Exception e) {
body = "Impossible de lire le détail de l'erreur";
// Ignorer si on ne peut pas obtenir l'URL
}
// Lire le corps de la réponse pour plus de détails
// SÉCURITÉ: Ne pas exposer les détails des erreurs serveur (5xx) au client
String body = "";
boolean shouldIncludeBody = status >= 400 && status < 500; // Seulement pour erreurs client (4xx)
if (shouldIncludeBody) {
try {
if (response.hasEntity()) {
body = response.readEntity(String.class);
logger.severe("Corps de la réponse (4xx): " + body);
}
} catch (Exception e) {
body = "Impossible de lire le détail de l'erreur";
logger.warning("Impossible de lire le corps de la réponse: " + e.getMessage());
}
}
// Logger toutes les erreurs pour debugging
logger.severe("Erreur backend - HTTP " + status + " (" + reasonPhrase + ")");
return switch (status) {
case 400 -> new BadRequestException("Requête invalide: " + body);
case 401 -> new UnauthorizedException("Non autorisé: " + reasonPhrase);
case 403 -> new ForbiddenException("Accès interdit: " + reasonPhrase);
case 404 -> new NotFoundException("Ressource non trouvée: " + reasonPhrase);
case 401 -> new UnauthorizedException("Non autorisé");
case 403 -> new ForbiddenException("Accès interdit");
case 404 -> new NotFoundException("Ressource non trouvée");
case 409 -> new ConflictException("Conflit: " + body);
case 422 -> new UnprocessableEntityException("Données non valides: " + body);
case 500 -> new InternalServerErrorException("Erreur serveur interne: " + body);
case 502 -> new BadGatewayException("Erreur de passerelle: " + reasonPhrase);
case 503 -> new ServiceUnavailableException("Service indisponible: " + reasonPhrase);
case 504 -> new GatewayTimeoutException("Timeout de passerelle: " + reasonPhrase);
default -> new UnknownHttpStatusException("Erreur HTTP " + status + ": " + reasonPhrase + (body.isEmpty() ? "" : " - " + body));
// SÉCURITÉ: Erreurs 5xx - Messages génériques sans détails backend
case 500 -> new InternalServerErrorException("Erreur serveur interne. Veuillez réessayer ultérieurement.");
case 502 -> new BadGatewayException("Service temporairement indisponible");
case 503 -> new ServiceUnavailableException("Service indisponible. Veuillez réessayer ultérieurement.");
case 504 -> new GatewayTimeoutException("Délai d'attente dépassé. Veuillez réessayer.");
default -> new UnknownHttpStatusException("Une erreur est survenue. Veuillez réessayer.");
};
}
@@ -83,4 +100,4 @@ public class RestClientExceptionMapper implements ResponseExceptionMapper<Runtim
public static class UnknownHttpStatusException extends RuntimeException {
public UnknownHttpStatusException(String message) { super(message); }
}
}
}

View File

@@ -0,0 +1,196 @@
package dev.lions.unionflow.client.service;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.jboss.logging.Logger;
import java.util.concurrent.Callable;
import java.util.function.Supplier;
/**
* Service de retry automatique pour les appels backend en cas d'échec temporaire.
*
* <p>Ce service implémente une stratégie de retry intelligente pour gérer:
* - Erreurs temporaires (503 Service Unavailable, 504 Gateway Timeout)
* - Timeouts réseau
* - Erreurs de connexion
*
* <p><strong>Production-ready:</strong> Gestion complète des retries avec backoff exponentiel,
* limite de tentatives, et logging approprié.
*
* <p><strong>Usage:</strong>
* <pre>{@code
* @Inject
* RetryService retryService;
*
* OrganisationResponse org = retryService.executeWithRetry(
* () -> associationService.creer(nouvelleOrganisation),
* "création d'une organisation"
* );
* }</pre>
*
* @author UnionFlow Team
* @version 1.0
* @since 2025-12-24
*/
@ApplicationScoped
public class RetryService {
private static final Logger LOG = Logger.getLogger(RetryService.class);
@Inject
MetricsService metricsService;
/**
* Nombre maximum de tentatives (incluant la première).
*/
private static final int MAX_ATTEMPTS = 3;
/**
* Délai initial entre les tentatives en millisecondes.
*/
private static final long INITIAL_DELAY_MS = 1000;
/**
* Multiplicateur pour le backoff exponentiel.
*/
private static final double BACKOFF_MULTIPLIER = 2.0;
/**
* Exécute une opération avec retry automatique en cas d'échec temporaire.
*
* @param operation L'opération à exécuter (Supplier)
* @param contextMessage Message contextuel pour le logging (ex: "création d'une organisation")
* @return Le résultat de l'opération
* @throws Exception Si toutes les tentatives échouent ou si l'erreur n'est pas retryable
*/
public <T> T executeWithRetrySupplier(Supplier<T> operation, String contextMessage) throws Exception {
return executeWithRetry(() -> operation.get(), contextMessage);
}
/**
* Exécute une opération avec retry automatique en cas d'échec temporaire.
*
* @param operation L'opération à exécuter (Callable)
* @param contextMessage Message contextuel pour le logging
* @return Le résultat de l'opération
* @throws Exception Si toutes les tentatives échouent ou si l'erreur n'est pas retryable
*/
public <T> T executeWithRetry(Callable<T> operation, String contextMessage) throws Exception {
Exception lastException = null;
for (int attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
try {
LOG.debugf("Tentative %d/%d pour: %s", attempt, MAX_ATTEMPTS, contextMessage);
T result = operation.call();
if (attempt > 1) {
LOG.infof("Succès après %d tentative(s) pour: %s", attempt, contextMessage);
}
return result;
} catch (Exception e) {
lastException = e;
// Vérifier si l'erreur est retryable
if (!isRetryable(e)) {
LOG.debugf("Erreur non retryable pour: %s - %s", contextMessage, e.getClass().getSimpleName());
throw e; // Ne pas retryer
}
// Si c'est la dernière tentative, re-lancer l'exception
if (attempt == MAX_ATTEMPTS) {
LOG.errorf(e, "Échec après %d tentative(s) pour: %s", MAX_ATTEMPTS, contextMessage);
throw e;
}
// Calculer le délai avant la prochaine tentative (backoff exponentiel)
long delay = calculateDelay(attempt);
LOG.warnf("Tentative %d/%d échouée pour: %s - Retry dans %d ms - Erreur: %s",
attempt, MAX_ATTEMPTS, contextMessage, delay, e.getMessage());
// Attendre avant la prochaine tentative
try {
Thread.sleep(delay);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException("Retry interrompu", ie);
}
}
}
// Ne devrait jamais arriver ici, mais au cas où
throw lastException != null ? lastException : new RuntimeException("Échec inattendu");
}
/**
* Vérifie si une exception est retryable (erreur temporaire).
*/
private boolean isRetryable(Exception e) {
// Erreurs de connexion (retryable)
if (e instanceof java.net.ConnectException ||
e instanceof java.net.SocketTimeoutException ||
e instanceof java.util.concurrent.TimeoutException ||
e.getCause() instanceof java.net.ConnectException ||
e.getCause() instanceof java.net.SocketTimeoutException ||
e.getCause() instanceof java.util.concurrent.TimeoutException) {
return true;
}
// Erreurs REST Client spécifiques
if (e instanceof RestClientExceptionMapper.ServiceUnavailableException ||
e instanceof RestClientExceptionMapper.GatewayTimeoutException ||
e instanceof RestClientExceptionMapper.BadGatewayException) {
return true;
}
// Erreurs ProcessingException (connexion, timeout, etc.)
if (e instanceof jakarta.ws.rs.ProcessingException) {
String message = e.getMessage();
if (message != null && (
message.contains("Connection") ||
message.contains("timeout") ||
message.contains("refused") ||
message.contains("unreachable"))) {
return true;
}
}
// Erreurs 5xx (sauf 500 qui peut être une erreur permanente)
if (e instanceof RestClientExceptionMapper.InternalServerErrorException) {
// 500 peut être retryable si c'est une erreur temporaire du serveur
return true;
}
// Par défaut, ne pas retryer (erreurs 4xx, validation, etc.)
return false;
}
/**
* Calcule le délai avant la prochaine tentative (backoff exponentiel).
*/
private long calculateDelay(int attempt) {
// Backoff exponentiel: INITIAL_DELAY * (BACKOFF_MULTIPLIER ^ (attempt - 1))
double delay = INITIAL_DELAY_MS * Math.pow(BACKOFF_MULTIPLIER, attempt - 1);
// Limiter à 10 secondes maximum
return Math.min((long) delay, 10000);
}
/**
* Exécute une opération avec retry, mais sans propagation d'exception (retourne null en cas d'échec).
*
* <p>Utile pour les opérations non critiques où on peut continuer même en cas d'échec.
*/
public <T> T executeWithRetryOrNull(Callable<T> operation, String contextMessage) {
try {
return executeWithRetry(operation, contextMessage);
} catch (Exception e) {
LOG.warnf(e, "Échec définitif pour: %s - Retour de null", contextMessage);
return null;
}
}
}

View File

@@ -1,20 +1,22 @@
package dev.lions.unionflow.client.service;
import dev.lions.unionflow.client.dto.SouscriptionDTO;
import dev.lions.unionflow.server.api.dto.abonnement.response.AbonnementResponse;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
import java.util.UUID;
@RegisterRestClient(configKey = "unionflow-api")
@RegisterClientHeaders(dev.lions.unionflow.client.security.AuthHeaderFactory.class)
@Path("/api/souscriptions")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public interface SouscriptionService {
@GET
List<SouscriptionDTO> listerToutes(
List<AbonnementResponse> listerToutes(
@QueryParam("organisationId") UUID organisationId,
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("20") int size
@@ -22,18 +24,18 @@ public interface SouscriptionService {
@GET
@Path("/{id}")
SouscriptionDTO obtenirParId(@PathParam("id") UUID id);
AbonnementResponse obtenirParId(@PathParam("id") UUID id);
@GET
@Path("/organisation/{organisationId}/active")
SouscriptionDTO obtenirActive(@PathParam("organisationId") UUID organisationId);
AbonnementResponse obtenirActive(@PathParam("organisationId") UUID organisationId);
@POST
SouscriptionDTO creer(SouscriptionDTO souscription);
AbonnementResponse creer(AbonnementResponse souscription);
@PUT
@Path("/{id}")
SouscriptionDTO modifier(@PathParam("id") UUID id, SouscriptionDTO souscription);
AbonnementResponse modifier(@PathParam("id") UUID id, AbonnementResponse souscription);
@DELETE
@Path("/{id}")
@@ -41,6 +43,6 @@ public interface SouscriptionService {
@PUT
@Path("/{id}/renouveler")
SouscriptionDTO renouveler(@PathParam("id") UUID id);
AbonnementResponse renouveler(@PathParam("id") UUID id);
}

View File

@@ -0,0 +1,40 @@
package dev.lions.unionflow.client.service;
import dev.lions.unionflow.server.api.dto.suggestion.request.*;
import dev.lions.unionflow.server.api.dto.suggestion.response.*;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* Service REST client pour la gestion des suggestions
*
* @author UnionFlow Team
* @version 1.0
*/
@RegisterRestClient(configKey = "unionflow-api")
@RegisterClientHeaders(dev.lions.unionflow.client.security.AuthHeaderFactory.class)
@Path("/api/suggestions")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public interface SuggestionService {
@GET
List<SuggestionResponse> listerSuggestions();
@POST
SuggestionResponse creerSuggestion(CreateSuggestionRequest request);
@POST
@Path("/{id}/voter")
void voterPourSuggestion(@PathParam("id") UUID id, @QueryParam("utilisateurId") UUID utilisateurId);
@GET
@Path("/statistiques")
Map<String, Object> obtenirStatistiques();
}

View File

@@ -0,0 +1,41 @@
package dev.lions.unionflow.client.service;
import dev.lions.unionflow.server.api.dto.ticket.request.*;
import dev.lions.unionflow.server.api.dto.ticket.response.*;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* Service REST client pour la gestion des tickets support
*
* @author UnionFlow Team
* @version 1.0
*/
@RegisterRestClient(configKey = "unionflow-api")
@RegisterClientHeaders(dev.lions.unionflow.client.security.AuthHeaderFactory.class)
@Path("/api/tickets")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public interface TicketService {
@GET
@Path("/utilisateur/{utilisateurId}")
List<TicketResponse> listerTickets(@PathParam("utilisateurId") UUID utilisateurId);
@GET
@Path("/{id}")
TicketResponse obtenirTicket(@PathParam("id") UUID id);
@POST
TicketResponse creerTicket(CreateTicketRequest request);
@GET
@Path("/utilisateur/{utilisateurId}/statistiques")
Map<String, Object> obtenirStatistiques(@PathParam("utilisateurId") UUID utilisateurId);
}

View File

@@ -0,0 +1,115 @@
package dev.lions.unionflow.client.service;
import dev.lions.unionflow.server.api.dto.organisation.response.OrganisationResponse;
import dev.lions.unionflow.server.api.dto.reference.response.TypeReferenceResponse;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.faces.model.SelectItem;
import jakarta.inject.Inject;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.jboss.logging.Logger;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Service applicatif (singleton) résolvant les libellés des types d'organisation
* à partir du catalogue dynamique stocké en base.
*
* <p>Remplace tous les {@code switch} hardcodés répandus dans les beans JSF.
* Le cache est invalidé explicitement après chaque mutation du catalogue
* ({@link #recharger()}).
*/
@ApplicationScoped
public class TypeCatalogueService {
private static final Logger LOG = Logger.getLogger(TypeCatalogueService.class);
@Inject
@RestClient
TypeOrganisationClientService typeOrganisationClientService;
/** code → libelle, chargé paresseusement. */
private volatile Map<String, String> libelleCache;
/** liste complète (actifs + inactifs) pour les formulaires admin. */
private volatile List<TypeReferenceResponse> catalogueCache;
// ── Chargement ──────────────────────────────────────────────────────────
private synchronized void charger() {
if (libelleCache != null) return; // double-checked locking
try {
List<TypeReferenceResponse> types = typeOrganisationClientService.list(false);
Map<String, String> map = new LinkedHashMap<>();
for (TypeReferenceResponse t : types) {
if (t.getCode() != null && t.getLibelle() != null) {
map.put(t.getCode(), t.getLibelle());
}
}
catalogueCache = List.copyOf(types);
libelleCache = Collections.unmodifiableMap(map);
LOG.infof("Catalogue types chargé : %d entrées", map.size());
} catch (Exception e) {
LOG.errorf(e, "Impossible de charger le catalogue des types d'organisation");
catalogueCache = List.of();
libelleCache = Map.of();
}
}
/** Invalide le cache — à appeler après toute mutation du catalogue. */
public void recharger() {
libelleCache = null;
catalogueCache = null;
LOG.debug("Cache catalogue types invalidé");
}
// ── Résolution ──────────────────────────────────────────────────────────
/**
* Retourne le libellé correspondant au code, ou le code brut si inconnu.
* Ne retourne jamais {@code null}.
*/
public String resolveLibelle(String code) {
if (code == null || code.isBlank()) return "";
if (libelleCache == null) charger();
return libelleCache.getOrDefault(code, code);
}
/**
* Renseigne {@code typeOrganisationLibelle} sur chaque DTO de la liste d'après le catalogue.
* Opération in-place, O(n).
*/
public void enrichir(List<OrganisationResponse> dtos) {
if (dtos == null || dtos.isEmpty()) return;
if (libelleCache == null) charger();
for (OrganisationResponse dto : dtos) {
dto.setTypeOrganisationLibelle(resolveLibelle(dto.getTypeOrganisation()));
}
}
// ── Listes pour les composants UI ────────────────────────────────────────
/**
* Retourne les types actifs sous forme de {@link SelectItem} avec un premier
* élément vide portant le {@code placeholder} fourni.
*/
public List<SelectItem> getSelectItems(String placeholder) {
if (libelleCache == null) charger();
List<SelectItem> items = new ArrayList<>();
items.add(new SelectItem("", placeholder));
for (TypeReferenceResponse t : catalogueCache) {
if (!Boolean.FALSE.equals(t.getActif())) {
items.add(new SelectItem(t.getCode(), t.getLibelle()));
}
}
return items;
}
/** Retourne tous les types (actifs + inactifs) — usage admin. */
public List<TypeReferenceResponse> getCatalogueComplet() {
if (catalogueCache == null) charger();
return catalogueCache;
}
}

View File

@@ -1,30 +1,33 @@
package dev.lions.unionflow.client.service;
import dev.lions.unionflow.client.dto.TypeOrganisationClientDTO;
import dev.lions.unionflow.server.api.dto.reference.request.*;
import dev.lions.unionflow.server.api.dto.reference.response.*;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
import java.util.UUID;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
/**
* REST client pour le catalogue des types d'organisation.
* REST client pour le catalogue des types d'organisation (références).
*/
@RegisterRestClient(configKey = "unionflow-api")
@Path("/api/types-organisations")
@RegisterClientHeaders(dev.lions.unionflow.client.security.AuthHeaderFactory.class)
@Path("/api/references/types-organisation")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public interface TypeOrganisationClientService {
@GET
List<TypeOrganisationClientDTO> list(@QueryParam("onlyActifs") @DefaultValue("true") boolean onlyActifs);
List<TypeReferenceResponse> list(@QueryParam("onlyActifs") @DefaultValue("true") boolean onlyActifs);
@POST
TypeOrganisationClientDTO create(TypeOrganisationClientDTO dto);
TypeReferenceResponse create(CreateTypeReferenceRequest request);
@PUT
@Path("/{id}")
TypeOrganisationClientDTO update(@PathParam("id") UUID id, TypeOrganisationClientDTO dto);
TypeReferenceResponse update(@PathParam("id") UUID id, UpdateTypeReferenceRequest request);
@DELETE
@Path("/{id}")

View File

@@ -4,76 +4,251 @@ import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validator;
import jakarta.validation.groups.Default;
import org.jboss.logging.Logger;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
* Service de validation centralisé avec feedback en temps réel
*
* Fournit des méthodes de validation pour les beans avec :
* - Validation complète d'objets
* - Validation de propriétés individuelles (temps réel)
* - Validation de valeurs
* - Support des groupes de validation
* - Intégration avec ErrorHandlerService
*
* @author UnionFlow Team
* @version 3.0
* @since 2026-01-04
*/
@ApplicationScoped
public class ValidationService {
private static final Logger LOG = Logger.getLogger(ValidationService.class);
@Inject
Validator validator;
@Inject
ErrorHandlerService errorHandler;
@Inject
MetricsService metricsService;
/**
* Valide un objet et retourne la liste des erreurs
*
* @param object L'objet à valider
* @return Résultat de validation avec détails des erreurs
*/
public <T> ValidationResult validate(T object) {
if (object == null) {
LOG.warn("Tentative de validation d'un objet null");
ValidationResult result = new ValidationResult();
result.setValid(false);
result.addErrorMessage("L'objet à valider est null");
return result;
}
Set<ConstraintViolation<T>> violations = validator.validate(object);
LOG.debugf("Validation de %s : %d violation(s)", object.getClass().getSimpleName(), violations.size());
ValidationResult result = new ValidationResult();
result.setValid(violations.isEmpty());
List<String> messages = new ArrayList<>();
for (ConstraintViolation<T> violation : violations) {
messages.add(violation.getPropertyPath() + ": " + violation.getMessage());
String message = violation.getPropertyPath() + ": " + violation.getMessage();
result.addErrorMessage(message);
LOG.debugf("Violation: %s", message);
}
// Enregistrer les métriques de validation
if (metricsService != null) {
metricsService.recordValidation(result.isValid());
}
result.setErrorMessages(messages);
return result;
}
/**
* Valide une propriété spécifique d'un objet
* Valide un objet avec groupes de validation spécifiques
*
* @param object L'objet à valider
* @param groups Groupes de validation à appliquer
* @return Résultat de validation
*/
public <T> ValidationResult validateProperty(T object, String propertyName) {
Set<ConstraintViolation<T>> violations = validator.validateProperty(object, propertyName);
public <T> ValidationResult validate(T object, Class<?>... groups) {
if (object == null) {
ValidationResult result = new ValidationResult();
result.setValid(false);
result.addErrorMessage("L'objet à valider est null");
return result;
}
Set<ConstraintViolation<T>> violations = validator.validate(object, groups);
LOG.debugf("Validation de %s avec groupes : %d violation(s)",
object.getClass().getSimpleName(), violations.size());
ValidationResult result = new ValidationResult();
result.setValid(violations.isEmpty());
List<String> messages = new ArrayList<>();
for (ConstraintViolation<T> violation : violations) {
messages.add(violation.getMessage());
result.addErrorMessage(violation.getPropertyPath() + ": " + violation.getMessage());
}
result.setErrorMessages(messages);
return result;
}
/**
* Valide un objet et affiche les erreurs automatiquement via ErrorHandlerService
*
* @param object L'objet à valider
* @param showMessagesToUser Si true, affiche les erreurs à l'utilisateur
* @return true si valide, false sinon
*/
public <T> boolean validateAndShow(T object, boolean showMessagesToUser) {
ValidationResult result = validate(object);
if (!result.isValid() && showMessagesToUser) {
for (String message : result.getErrorMessages()) {
errorHandler.showWarning("Validation", message);
}
}
return result.isValid();
}
/**
* Valide une propriété spécifique d'un objet (utile pour validation en temps réel)
*
* @param object L'objet contenant la propriété
* @param propertyName Nom de la propriété à valider
* @return Résultat de validation
*/
public <T> ValidationResult validateProperty(T object, String propertyName) {
if (object == null || propertyName == null || propertyName.isEmpty()) {
ValidationResult result = new ValidationResult();
result.setValid(false);
result.addErrorMessage("Paramètres de validation invalides");
return result;
}
Set<ConstraintViolation<T>> violations = validator.validateProperty(object, propertyName);
LOG.debugf("Validation propriété %s.%s : %d violation(s)",
object.getClass().getSimpleName(), propertyName, violations.size());
ValidationResult result = new ValidationResult();
result.setValid(violations.isEmpty());
result.setPropertyName(propertyName);
for (ConstraintViolation<T> violation : violations) {
result.addErrorMessage(violation.getMessage());
}
return result;
}
/**
* Valide une propriété et affiche l'erreur si invalide
* Utilisé pour validation AJAX en temps réel
*
* @param object L'objet
* @param propertyName Propriété à valider
* @param componentId ID du composant JSF pour cibler le message
* @return true si valide
*/
public <T> boolean validatePropertyAndShow(T object, String propertyName, String componentId) {
ValidationResult result = validateProperty(object, propertyName);
if (!result.isValid()) {
String message = result.getFirstErrorMessage();
if (message != null) {
errorHandler.showWarning(propertyName, message);
}
}
return result.isValid();
}
/**
* Valide une valeur contre les contraintes d'une propriété
* Utile pour valider une valeur avant de l'assigner
*
* @param beanType Classe du bean
* @param propertyName Nom de la propriété
* @param value Valeur à valider
* @return Résultat de validation
*/
public <T> ValidationResult validateValue(Class<T> beanType, String propertyName, Object value) {
Set<ConstraintViolation<T>> violations = validator.validateValue(beanType, propertyName, value);
LOG.debugf("Validation valeur %s.%s = %s : %d violation(s)",
beanType.getSimpleName(), propertyName, value, violations.size());
ValidationResult result = new ValidationResult();
result.setValid(violations.isEmpty());
result.setPropertyName(propertyName);
List<String> messages = new ArrayList<>();
for (ConstraintViolation<T> violation : violations) {
messages.add(violation.getMessage());
result.addErrorMessage(violation.getMessage());
}
result.setErrorMessages(messages);
return result;
}
/**
* Valide plusieurs objets en une seule opération
*
* @param objects Liste d'objets à valider
* @return Résultat de validation global
*/
public ValidationResult validateMultiple(Object... objects) {
ValidationResult globalResult = new ValidationResult();
globalResult.setValid(true);
for (Object object : objects) {
ValidationResult result = validate(object);
if (!result.isValid()) {
globalResult.setValid(false);
globalResult.getErrorMessages().addAll(result.getErrorMessages());
}
}
return globalResult;
}
/**
* Vérifie si un objet est valide (méthode simplifiée)
*
* @param object L'objet à valider
* @return true si valide, false sinon
*/
public <T> boolean isValid(T object) {
return object != null && validator.validate(object).isEmpty();
}
/**
* Vérifie si une propriété est valide (méthode simplifiée)
*
* @param object L'objet
* @param propertyName Propriété à vérifier
* @return true si valide
*/
public <T> boolean isPropertyValid(T object, String propertyName) {
return object != null &&
propertyName != null &&
validator.validateProperty(object, propertyName).isEmpty();
}
/**
* Classe pour encapsuler le résultat de validation
*/
public static class ValidationResult {
private boolean valid;
private boolean valid = true;
private List<String> errorMessages = new ArrayList<>();
private String propertyName;
public boolean isValid() {
return valid;
@@ -91,6 +266,18 @@ public class ValidationService {
this.errorMessages = errorMessages;
}
public void addErrorMessage(String message) {
this.errorMessages.add(message);
}
public String getPropertyName() {
return propertyName;
}
public void setPropertyName(String propertyName) {
this.propertyName = propertyName;
}
public String getFirstErrorMessage() {
return errorMessages.isEmpty() ? null : errorMessages.get(0);
}
@@ -98,5 +285,22 @@ public class ValidationService {
public String getAllErrorMessages() {
return String.join(", ", errorMessages);
}
public int getErrorCount() {
return errorMessages.size();
}
public boolean hasErrors() {
return !valid || !errorMessages.isEmpty();
}
@Override
public String toString() {
return "ValidationResult{" +
"valid=" + valid +
", errorCount=" + errorMessages.size() +
", propertyName='" + propertyName + '\'' +
'}';
}
}
}
}

View File

@@ -1,7 +1,7 @@
package dev.lions.unionflow.client.service;
import dev.lions.unionflow.client.dto.WaveCheckoutSessionDTO;
import dev.lions.unionflow.client.dto.WaveBalanceDTO;
import dev.lions.unionflow.server.api.dto.paiement.WaveCheckoutSessionDTO;
import dev.lions.unionflow.server.api.dto.paiement.WaveBalanceDTO;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
@@ -14,6 +14,7 @@ import java.math.BigDecimal;
import java.util.Map;
import java.util.UUID;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
/**
* Service REST client pour l'intégration Wave Money
@@ -22,7 +23,8 @@ import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
* @version 1.0
* @since 2025-01-17
*/
@RegisterRestClient(baseUri = "http://localhost:8085")
@RegisterRestClient(configKey = "unionflow-api")
@RegisterClientHeaders(dev.lions.unionflow.client.security.AuthHeaderFactory.class)
@Path("/api/wave")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)