Refactoring

This commit is contained in:
DahoudG
2025-09-17 17:54:06 +00:00
parent 12d514d866
commit 63fe107f98
165 changed files with 54220 additions and 276 deletions

View File

@@ -0,0 +1,351 @@
package dev.lions.unionflow.server.resource;
import dev.lions.unionflow.server.api.dto.analytics.AnalyticsDataDTO;
import dev.lions.unionflow.server.api.dto.analytics.KPITrendDTO;
import dev.lions.unionflow.server.api.dto.analytics.DashboardWidgetDTO;
import dev.lions.unionflow.server.api.enums.analytics.TypeMetrique;
import dev.lions.unionflow.server.api.enums.analytics.PeriodeAnalyse;
import dev.lions.unionflow.server.service.AnalyticsService;
import dev.lions.unionflow.server.service.KPICalculatorService;
import io.quarkus.security.Authenticated;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.responses.ApiResponse;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* Ressource REST pour les analytics et métriques UnionFlow
*
* Cette ressource expose les APIs pour accéder aux données analytics,
* KPI, tendances et widgets de tableau de bord.
*
* @author UnionFlow Team
* @version 1.0
* @since 2025-01-16
*/
@Path("/api/v1/analytics")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Authenticated
@Tag(name = "Analytics", description = "APIs pour les analytics et métriques")
@Slf4j
public class AnalyticsResource {
@Inject
AnalyticsService analyticsService;
@Inject
KPICalculatorService kpiCalculatorService;
/**
* Calcule une métrique analytics pour une période donnée
*/
@GET
@Path("/metriques/{typeMetrique}")
@RolesAllowed({"ADMIN", "MANAGER", "MEMBER"})
@Operation(
summary = "Calculer une métrique analytics",
description = "Calcule une métrique spécifique pour une période et organisation données"
)
@ApiResponse(responseCode = "200", description = "Métrique calculée avec succès")
@ApiResponse(responseCode = "400", description = "Paramètres invalides")
@ApiResponse(responseCode = "403", description = "Accès non autorisé")
public Response calculerMetrique(
@Parameter(description = "Type de métrique à calculer", required = true)
@PathParam("typeMetrique") TypeMetrique typeMetrique,
@Parameter(description = "Période d'analyse", required = true)
@QueryParam("periode") @NotNull PeriodeAnalyse periodeAnalyse,
@Parameter(description = "ID de l'organisation (optionnel)")
@QueryParam("organisationId") UUID organisationId) {
try {
log.info("Calcul de la métrique {} pour la période {} et l'organisation {}",
typeMetrique, periodeAnalyse, organisationId);
AnalyticsDataDTO result = analyticsService.calculerMetrique(
typeMetrique, periodeAnalyse, organisationId);
return Response.ok(result).build();
} catch (Exception e) {
log.error("Erreur lors du calcul de la métrique {}: {}", typeMetrique, e.getMessage(), e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors du calcul de la métrique",
"message", e.getMessage()))
.build();
}
}
/**
* Calcule les tendances d'un KPI sur une période
*/
@GET
@Path("/tendances/{typeMetrique}")
@RolesAllowed({"ADMIN", "MANAGER", "MEMBER"})
@Operation(
summary = "Calculer la tendance d'un KPI",
description = "Calcule l'évolution et les tendances d'un KPI sur une période donnée"
)
@ApiResponse(responseCode = "200", description = "Tendance calculée avec succès")
@ApiResponse(responseCode = "400", description = "Paramètres invalides")
@ApiResponse(responseCode = "403", description = "Accès non autorisé")
public Response calculerTendanceKPI(
@Parameter(description = "Type de métrique pour la tendance", required = true)
@PathParam("typeMetrique") TypeMetrique typeMetrique,
@Parameter(description = "Période d'analyse", required = true)
@QueryParam("periode") @NotNull PeriodeAnalyse periodeAnalyse,
@Parameter(description = "ID de l'organisation (optionnel)")
@QueryParam("organisationId") UUID organisationId) {
try {
log.info("Calcul de la tendance KPI {} pour la période {} et l'organisation {}",
typeMetrique, periodeAnalyse, organisationId);
KPITrendDTO result = analyticsService.calculerTendanceKPI(
typeMetrique, periodeAnalyse, organisationId);
return Response.ok(result).build();
} catch (Exception e) {
log.error("Erreur lors du calcul de la tendance KPI {}: {}", typeMetrique, e.getMessage(), e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors du calcul de la tendance",
"message", e.getMessage()))
.build();
}
}
/**
* Obtient tous les KPI pour une organisation
*/
@GET
@Path("/kpis")
@RolesAllowed({"ADMIN", "MANAGER", "MEMBER"})
@Operation(
summary = "Obtenir tous les KPI",
description = "Récupère tous les KPI calculés pour une organisation et période données"
)
@ApiResponse(responseCode = "200", description = "KPI récupérés avec succès")
@ApiResponse(responseCode = "400", description = "Paramètres invalides")
@ApiResponse(responseCode = "403", description = "Accès non autorisé")
public Response obtenirTousLesKPI(
@Parameter(description = "Période d'analyse", required = true)
@QueryParam("periode") @NotNull PeriodeAnalyse periodeAnalyse,
@Parameter(description = "ID de l'organisation (optionnel)")
@QueryParam("organisationId") UUID organisationId) {
try {
log.info("Récupération de tous les KPI pour la période {} et l'organisation {}",
periodeAnalyse, organisationId);
Map<TypeMetrique, BigDecimal> kpis = kpiCalculatorService.calculerTousLesKPI(
organisationId, periodeAnalyse.getDateDebut(), periodeAnalyse.getDateFin());
return Response.ok(kpis).build();
} catch (Exception e) {
log.error("Erreur lors de la récupération des KPI: {}", e.getMessage(), e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de la récupération des KPI",
"message", e.getMessage()))
.build();
}
}
/**
* Calcule le KPI de performance globale
*/
@GET
@Path("/performance-globale")
@RolesAllowed({"ADMIN", "MANAGER"})
@Operation(
summary = "Calculer la performance globale",
description = "Calcule le score de performance globale de l'organisation"
)
@ApiResponse(responseCode = "200", description = "Performance globale calculée avec succès")
@ApiResponse(responseCode = "403", description = "Accès non autorisé")
public Response calculerPerformanceGlobale(
@Parameter(description = "Période d'analyse", required = true)
@QueryParam("periode") @NotNull PeriodeAnalyse periodeAnalyse,
@Parameter(description = "ID de l'organisation (optionnel)")
@QueryParam("organisationId") UUID organisationId) {
try {
log.info("Calcul de la performance globale pour la période {} et l'organisation {}",
periodeAnalyse, organisationId);
BigDecimal performanceGlobale = kpiCalculatorService.calculerKPIPerformanceGlobale(
organisationId, periodeAnalyse.getDateDebut(), periodeAnalyse.getDateFin());
return Response.ok(Map.of(
"performanceGlobale", performanceGlobale,
"periode", periodeAnalyse,
"organisationId", organisationId,
"dateCalcul", java.time.LocalDateTime.now()
)).build();
} catch (Exception e) {
log.error("Erreur lors du calcul de la performance globale: {}", e.getMessage(), e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors du calcul de la performance globale",
"message", e.getMessage()))
.build();
}
}
/**
* Obtient les évolutions des KPI par rapport à la période précédente
*/
@GET
@Path("/evolutions")
@RolesAllowed({"ADMIN", "MANAGER", "MEMBER"})
@Operation(
summary = "Obtenir les évolutions des KPI",
description = "Récupère les évolutions des KPI par rapport à la période précédente"
)
@ApiResponse(responseCode = "200", description = "Évolutions récupérées avec succès")
@ApiResponse(responseCode = "400", description = "Paramètres invalides")
@ApiResponse(responseCode = "403", description = "Accès non autorisé")
public Response obtenirEvolutionsKPI(
@Parameter(description = "Période d'analyse", required = true)
@QueryParam("periode") @NotNull PeriodeAnalyse periodeAnalyse,
@Parameter(description = "ID de l'organisation (optionnel)")
@QueryParam("organisationId") UUID organisationId) {
try {
log.info("Récupération des évolutions KPI pour la période {} et l'organisation {}",
periodeAnalyse, organisationId);
Map<TypeMetrique, BigDecimal> evolutions = kpiCalculatorService.calculerEvolutionsKPI(
organisationId, periodeAnalyse.getDateDebut(), periodeAnalyse.getDateFin());
return Response.ok(evolutions).build();
} catch (Exception e) {
log.error("Erreur lors de la récupération des évolutions KPI: {}", e.getMessage(), e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de la récupération des évolutions",
"message", e.getMessage()))
.build();
}
}
/**
* Obtient les widgets du tableau de bord pour un utilisateur
*/
@GET
@Path("/dashboard/widgets")
@RolesAllowed({"ADMIN", "MANAGER", "MEMBER"})
@Operation(
summary = "Obtenir les widgets du tableau de bord",
description = "Récupère tous les widgets configurés pour le tableau de bord de l'utilisateur"
)
@ApiResponse(responseCode = "200", description = "Widgets récupérés avec succès")
@ApiResponse(responseCode = "403", description = "Accès non autorisé")
public Response obtenirWidgetsTableauBord(
@Parameter(description = "ID de l'organisation (optionnel)")
@QueryParam("organisationId") UUID organisationId,
@Parameter(description = "ID de l'utilisateur", required = true)
@QueryParam("utilisateurId") @NotNull UUID utilisateurId) {
try {
log.info("Récupération des widgets du tableau de bord pour l'organisation {} et l'utilisateur {}",
organisationId, utilisateurId);
List<DashboardWidgetDTO> widgets = analyticsService.obtenirMetriquesTableauBord(
organisationId, utilisateurId);
return Response.ok(widgets).build();
} catch (Exception e) {
log.error("Erreur lors de la récupération des widgets: {}", e.getMessage(), e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de la récupération des widgets",
"message", e.getMessage()))
.build();
}
}
/**
* Obtient les types de métriques disponibles
*/
@GET
@Path("/types-metriques")
@RolesAllowed({"ADMIN", "MANAGER", "MEMBER"})
@Operation(
summary = "Obtenir les types de métriques disponibles",
description = "Récupère la liste de tous les types de métriques disponibles"
)
@ApiResponse(responseCode = "200", description = "Types de métriques récupérés avec succès")
public Response obtenirTypesMetriques() {
try {
log.info("Récupération des types de métriques disponibles");
TypeMetrique[] typesMetriques = TypeMetrique.values();
return Response.ok(Map.of(
"typesMetriques", typesMetriques,
"total", typesMetriques.length
)).build();
} catch (Exception e) {
log.error("Erreur lors de la récupération des types de métriques: {}", e.getMessage(), e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de la récupération des types de métriques",
"message", e.getMessage()))
.build();
}
}
/**
* Obtient les périodes d'analyse disponibles
*/
@GET
@Path("/periodes-analyse")
@RolesAllowed({"ADMIN", "MANAGER", "MEMBER"})
@Operation(
summary = "Obtenir les périodes d'analyse disponibles",
description = "Récupère la liste de toutes les périodes d'analyse disponibles"
)
@ApiResponse(responseCode = "200", description = "Périodes d'analyse récupérées avec succès")
public Response obtenirPeriodesAnalyse() {
try {
log.info("Récupération des périodes d'analyse disponibles");
PeriodeAnalyse[] periodesAnalyse = PeriodeAnalyse.values();
return Response.ok(Map.of(
"periodesAnalyse", periodesAnalyse,
"total", periodesAnalyse.length
)).build();
} catch (Exception e) {
log.error("Erreur lors de la récupération des périodes d'analyse: {}", e.getMessage(), e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de la récupération des périodes d'analyse",
"message", e.getMessage()))
.build();
}
}
}

View File

@@ -0,0 +1,433 @@
package dev.lions.unionflow.server.resource;
import dev.lions.unionflow.server.api.dto.solidarite.DemandeAideDTO;
import dev.lions.unionflow.server.api.dto.solidarite.PropositionAideDTO;
import dev.lions.unionflow.server.api.dto.solidarite.EvaluationAideDTO;
import dev.lions.unionflow.server.api.enums.solidarite.TypeAide;
import dev.lions.unionflow.server.api.enums.solidarite.StatutAide;
import dev.lions.unionflow.server.service.SolidariteService;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.jboss.logging.Logger;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
/**
* Ressource REST pour le système de solidarité UnionFlow
*
* Cette ressource expose les endpoints pour la gestion complète
* du système de solidarité : demandes, propositions, évaluations.
*
* @author UnionFlow Team
* @version 1.0
* @since 2025-01-16
*/
@Path("/api/v1/solidarite")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Tag(name = "Solidarité", description = "API de gestion du système de solidarité")
public class SolidariteResource {
private static final Logger LOG = Logger.getLogger(SolidariteResource.class);
@Inject
SolidariteService solidariteService;
// === ENDPOINTS DEMANDES D'AIDE ===
@POST
@Path("/demandes")
@Operation(summary = "Créer une nouvelle demande d'aide",
description = "Crée une nouvelle demande d'aide dans le système")
@APIResponse(responseCode = "201", description = "Demande créée avec succès")
@APIResponse(responseCode = "400", description = "Données invalides")
@APIResponse(responseCode = "500", description = "Erreur serveur")
public Response creerDemandeAide(@Valid DemandeAideDTO demandeDTO) {
LOG.infof("Création d'une nouvelle demande d'aide: %s", demandeDTO.getTitre());
try {
DemandeAideDTO demandeCreee = solidariteService.creerDemandeAide(demandeDTO);
return Response.status(Response.Status.CREATED)
.entity(demandeCreee)
.build();
} catch (IllegalArgumentException e) {
LOG.warnf("Données invalides pour la création de demande: %s", e.getMessage());
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("erreur", e.getMessage()))
.build();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la création de demande d'aide");
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("erreur", "Erreur interne du serveur"))
.build();
}
}
@GET
@Path("/demandes/{id}")
@Operation(summary = "Obtenir une demande d'aide par ID",
description = "Récupère les détails d'une demande d'aide spécifique")
@APIResponse(responseCode = "200", description = "Demande trouvée")
@APIResponse(responseCode = "404", description = "Demande non trouvée")
public Response obtenirDemandeAide(@Parameter(description = "ID de la demande")
@PathParam("id") @NotBlank String id) {
LOG.debugf("Récupération de la demande d'aide: %s", id);
try {
DemandeAideDTO demande = solidariteService.obtenirDemandeAide(id);
if (demande == null) {
return Response.status(Response.Status.NOT_FOUND)
.entity(Map.of("erreur", "Demande non trouvée"))
.build();
}
return Response.ok(demande).build();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la récupération de la demande: %s", id);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("erreur", "Erreur interne du serveur"))
.build();
}
}
@PUT
@Path("/demandes/{id}")
@Operation(summary = "Mettre à jour une demande d'aide",
description = "Met à jour les informations d'une demande d'aide")
@APIResponse(responseCode = "200", description = "Demande mise à jour")
@APIResponse(responseCode = "404", description = "Demande non trouvée")
@APIResponse(responseCode = "400", description = "Données invalides")
public Response mettreAJourDemandeAide(@PathParam("id") @NotBlank String id,
@Valid DemandeAideDTO demandeDTO) {
LOG.infof("Mise à jour de la demande d'aide: %s", id);
try {
demandeDTO.setId(id); // S'assurer que l'ID correspond
DemandeAideDTO demandeMiseAJour = solidariteService.mettreAJourDemandeAide(demandeDTO);
return Response.ok(demandeMiseAJour).build();
} catch (IllegalArgumentException e) {
LOG.warnf("Données invalides pour la mise à jour: %s", e.getMessage());
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("erreur", e.getMessage()))
.build();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la mise à jour de la demande: %s", id);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("erreur", "Erreur interne du serveur"))
.build();
}
}
@POST
@Path("/demandes/{id}/soumettre")
@Operation(summary = "Soumettre une demande d'aide",
description = "Soumet une demande d'aide pour évaluation")
@APIResponse(responseCode = "200", description = "Demande soumise avec succès")
@APIResponse(responseCode = "404", description = "Demande non trouvée")
@APIResponse(responseCode = "400", description = "Demande ne peut pas être soumise")
public Response soumettreDemande(@PathParam("id") @NotBlank String id) {
LOG.infof("Soumission de la demande d'aide: %s", id);
try {
DemandeAideDTO demandesoumise = solidariteService.soumettreDemande(id);
return Response.ok(demandesoumise).build();
} catch (IllegalStateException e) {
LOG.warnf("Impossible de soumettre la demande %s: %s", id, e.getMessage());
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("erreur", e.getMessage()))
.build();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la soumission de la demande: %s", id);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("erreur", "Erreur interne du serveur"))
.build();
}
}
@POST
@Path("/demandes/{id}/evaluer")
@Operation(summary = "Évaluer une demande d'aide",
description = "Évalue une demande d'aide et prend une décision")
@APIResponse(responseCode = "200", description = "Demande évaluée avec succès")
@APIResponse(responseCode = "404", description = "Demande non trouvée")
@APIResponse(responseCode = "400", description = "Évaluation invalide")
public Response evaluerDemande(@PathParam("id") @NotBlank String id,
@Valid Map<String, Object> evaluationData) {
LOG.infof("Évaluation de la demande d'aide: %s", id);
try {
String evaluateurId = (String) evaluationData.get("evaluateurId");
StatutAide decision = StatutAide.valueOf((String) evaluationData.get("decision"));
String commentaire = (String) evaluationData.get("commentaire");
Double montantApprouve = evaluationData.get("montantApprouve") != null ?
((Number) evaluationData.get("montantApprouve")).doubleValue() : null;
DemandeAideDTO demandeEvaluee = solidariteService.evaluerDemande(
id, evaluateurId, decision, commentaire, montantApprouve);
return Response.ok(demandeEvaluee).build();
} catch (IllegalArgumentException | IllegalStateException e) {
LOG.warnf("Évaluation invalide pour la demande %s: %s", id, e.getMessage());
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("erreur", e.getMessage()))
.build();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de l'évaluation de la demande: %s", id);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("erreur", "Erreur interne du serveur"))
.build();
}
}
@GET
@Path("/demandes")
@Operation(summary = "Rechercher des demandes d'aide",
description = "Recherche des demandes d'aide avec filtres")
@APIResponse(responseCode = "200", description = "Liste des demandes")
public Response rechercherDemandes(@QueryParam("organisationId") String organisationId,
@QueryParam("typeAide") String typeAide,
@QueryParam("statut") String statut,
@QueryParam("demandeurId") String demandeurId,
@QueryParam("urgente") Boolean urgente,
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("taille") @DefaultValue("20") int taille) {
LOG.debugf("Recherche de demandes avec filtres");
try {
Map<String, Object> filtres = Map.of(
"organisationId", organisationId != null ? organisationId : "",
"typeAide", typeAide != null ? TypeAide.valueOf(typeAide) : "",
"statut", statut != null ? StatutAide.valueOf(statut) : "",
"demandeurId", demandeurId != null ? demandeurId : "",
"urgente", urgente != null ? urgente : false,
"page", page,
"taille", taille
);
List<DemandeAideDTO> demandes = solidariteService.rechercherDemandes(filtres);
return Response.ok(Map.of(
"demandes", demandes,
"page", page,
"taille", taille,
"total", demandes.size()
)).build();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la recherche de demandes");
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("erreur", "Erreur interne du serveur"))
.build();
}
}
// === ENDPOINTS PROPOSITIONS D'AIDE ===
@POST
@Path("/propositions")
@Operation(summary = "Créer une nouvelle proposition d'aide",
description = "Crée une nouvelle proposition d'aide")
@APIResponse(responseCode = "201", description = "Proposition créée avec succès")
@APIResponse(responseCode = "400", description = "Données invalides")
public Response creerPropositionAide(@Valid PropositionAideDTO propositionDTO) {
LOG.infof("Création d'une nouvelle proposition d'aide: %s", propositionDTO.getTitre());
try {
PropositionAideDTO propositionCreee = solidariteService.creerPropositionAide(propositionDTO);
return Response.status(Response.Status.CREATED)
.entity(propositionCreee)
.build();
} catch (IllegalArgumentException e) {
LOG.warnf("Données invalides pour la création de proposition: %s", e.getMessage());
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("erreur", e.getMessage()))
.build();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la création de proposition d'aide");
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("erreur", "Erreur interne du serveur"))
.build();
}
}
@GET
@Path("/propositions/{id}")
@Operation(summary = "Obtenir une proposition d'aide par ID",
description = "Récupère les détails d'une proposition d'aide spécifique")
@APIResponse(responseCode = "200", description = "Proposition trouvée")
@APIResponse(responseCode = "404", description = "Proposition non trouvée")
public Response obtenirPropositionAide(@PathParam("id") @NotBlank String id) {
LOG.debugf("Récupération de la proposition d'aide: %s", id);
try {
PropositionAideDTO proposition = solidariteService.obtenirPropositionAide(id);
if (proposition == null) {
return Response.status(Response.Status.NOT_FOUND)
.entity(Map.of("erreur", "Proposition non trouvée"))
.build();
}
return Response.ok(proposition).build();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la récupération de la proposition: %s", id);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("erreur", "Erreur interne du serveur"))
.build();
}
}
@GET
@Path("/propositions")
@Operation(summary = "Rechercher des propositions d'aide",
description = "Recherche des propositions d'aide avec filtres")
@APIResponse(responseCode = "200", description = "Liste des propositions")
public Response rechercherPropositions(@QueryParam("organisationId") String organisationId,
@QueryParam("typeAide") String typeAide,
@QueryParam("proposantId") String proposantId,
@QueryParam("actives") @DefaultValue("true") Boolean actives,
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("taille") @DefaultValue("20") int taille) {
LOG.debugf("Recherche de propositions avec filtres");
try {
Map<String, Object> filtres = Map.of(
"organisationId", organisationId != null ? organisationId : "",
"typeAide", typeAide != null ? TypeAide.valueOf(typeAide) : "",
"proposantId", proposantId != null ? proposantId : "",
"estDisponible", actives,
"page", page,
"taille", taille
);
List<PropositionAideDTO> propositions = solidariteService.rechercherPropositions(filtres);
return Response.ok(Map.of(
"propositions", propositions,
"page", page,
"taille", taille,
"total", propositions.size()
)).build();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la recherche de propositions");
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("erreur", "Erreur interne du serveur"))
.build();
}
}
// === ENDPOINTS MATCHING ===
@GET
@Path("/demandes/{id}/propositions-compatibles")
@Operation(summary = "Trouver des propositions compatibles",
description = "Trouve les propositions compatibles avec une demande")
@APIResponse(responseCode = "200", description = "Propositions compatibles trouvées")
@APIResponse(responseCode = "404", description = "Demande non trouvée")
public Response trouverPropositionsCompatibles(@PathParam("id") @NotBlank String demandeId) {
LOG.infof("Recherche de propositions compatibles pour la demande: %s", demandeId);
try {
List<PropositionAideDTO> propositionsCompatibles =
solidariteService.trouverPropositionsCompatibles(demandeId);
return Response.ok(Map.of(
"demandeId", demandeId,
"propositionsCompatibles", propositionsCompatibles,
"nombreResultats", propositionsCompatibles.size()
)).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity(Map.of("erreur", "Demande non trouvée"))
.build();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la recherche de propositions compatibles");
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("erreur", "Erreur interne du serveur"))
.build();
}
}
@GET
@Path("/propositions/{id}/demandes-compatibles")
@Operation(summary = "Trouver des demandes compatibles",
description = "Trouve les demandes compatibles avec une proposition")
@APIResponse(responseCode = "200", description = "Demandes compatibles trouvées")
@APIResponse(responseCode = "404", description = "Proposition non trouvée")
public Response trouverDemandesCompatibles(@PathParam("id") @NotBlank String propositionId) {
LOG.infof("Recherche de demandes compatibles pour la proposition: %s", propositionId);
try {
List<DemandeAideDTO> demandesCompatibles =
solidariteService.trouverDemandesCompatibles(propositionId);
return Response.ok(Map.of(
"propositionId", propositionId,
"demandesCompatibles", demandesCompatibles,
"nombreResultats", demandesCompatibles.size()
)).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity(Map.of("erreur", "Proposition non trouvée"))
.build();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la recherche de demandes compatibles");
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("erreur", "Erreur interne du serveur"))
.build();
}
}
// === ENDPOINTS STATISTIQUES ===
@GET
@Path("/statistiques/{organisationId}")
@Operation(summary = "Obtenir les statistiques de solidarité",
description = "Récupère les statistiques complètes du système de solidarité")
@APIResponse(responseCode = "200", description = "Statistiques récupérées")
public Response obtenirStatistiquesSolidarite(@PathParam("organisationId") @NotBlank String organisationId) {
LOG.infof("Récupération des statistiques de solidarité pour: %s", organisationId);
try {
Map<String, Object> statistiques = solidariteService.obtenirStatistiquesSolidarite(organisationId);
return Response.ok(statistiques).build();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la récupération des statistiques");
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("erreur", "Erreur interne du serveur"))
.build();
}
}
}

View File

@@ -0,0 +1,372 @@
package dev.lions.unionflow.server.service;
import dev.lions.unionflow.server.api.dto.analytics.AnalyticsDataDTO;
import dev.lions.unionflow.server.api.dto.analytics.KPITrendDTO;
import dev.lions.unionflow.server.api.dto.analytics.DashboardWidgetDTO;
import dev.lions.unionflow.server.api.enums.analytics.TypeMetrique;
import dev.lions.unionflow.server.api.enums.analytics.PeriodeAnalyse;
import dev.lions.unionflow.server.entity.Organisation;
import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.entity.Cotisation;
import dev.lions.unionflow.server.entity.Evenement;
import dev.lions.unionflow.server.entity.DemandeAide;
import dev.lions.unionflow.server.repository.OrganisationRepository;
import dev.lions.unionflow.server.repository.MembreRepository;
import dev.lions.unionflow.server.repository.CotisationRepository;
import dev.lions.unionflow.server.repository.EvenementRepository;
import dev.lions.unionflow.server.repository.DemandeAideRepository;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import lombok.extern.slf4j.Slf4j;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.util.List;
import java.util.ArrayList;
import java.util.UUID;
import java.util.Map;
import java.util.HashMap;
import java.util.stream.Collectors;
/**
* Service principal pour les analytics et métriques UnionFlow
*
* Ce service calcule et fournit toutes les métriques analytics
* pour les tableaux de bord, rapports et widgets.
*
* @author UnionFlow Team
* @version 1.0
* @since 2025-01-16
*/
@ApplicationScoped
@Slf4j
public class AnalyticsService {
@Inject
OrganisationRepository organisationRepository;
@Inject
MembreRepository membreRepository;
@Inject
CotisationRepository cotisationRepository;
@Inject
EvenementRepository evenementRepository;
@Inject
DemandeAideRepository demandeAideRepository;
@Inject
KPICalculatorService kpiCalculatorService;
@Inject
TrendAnalysisService trendAnalysisService;
/**
* Calcule une métrique analytics pour une période donnée
*
* @param typeMetrique Le type de métrique à calculer
* @param periodeAnalyse La période d'analyse
* @param organisationId L'ID de l'organisation (optionnel)
* @return Les données analytics calculées
*/
@Transactional
public AnalyticsDataDTO calculerMetrique(TypeMetrique typeMetrique,
PeriodeAnalyse periodeAnalyse,
UUID organisationId) {
log.info("Calcul de la métrique {} pour la période {} et l'organisation {}",
typeMetrique, periodeAnalyse, organisationId);
LocalDateTime dateDebut = periodeAnalyse.getDateDebut();
LocalDateTime dateFin = periodeAnalyse.getDateFin();
BigDecimal valeur = switch (typeMetrique) {
// Métriques membres
case NOMBRE_MEMBRES_ACTIFS -> calculerNombreMembresActifs(organisationId, dateDebut, dateFin);
case NOMBRE_MEMBRES_INACTIFS -> calculerNombreMembresInactifs(organisationId, dateDebut, dateFin);
case TAUX_CROISSANCE_MEMBRES -> calculerTauxCroissanceMembres(organisationId, dateDebut, dateFin);
case MOYENNE_AGE_MEMBRES -> calculerMoyenneAgeMembres(organisationId, dateDebut, dateFin);
// Métriques financières
case TOTAL_COTISATIONS_COLLECTEES -> calculerTotalCotisationsCollectees(organisationId, dateDebut, dateFin);
case COTISATIONS_EN_ATTENTE -> calculerCotisationsEnAttente(organisationId, dateDebut, dateFin);
case TAUX_RECOUVREMENT_COTISATIONS -> calculerTauxRecouvrementCotisations(organisationId, dateDebut, dateFin);
case MOYENNE_COTISATION_MEMBRE -> calculerMoyenneCotisationMembre(organisationId, dateDebut, dateFin);
// Métriques événements
case NOMBRE_EVENEMENTS_ORGANISES -> calculerNombreEvenementsOrganises(organisationId, dateDebut, dateFin);
case TAUX_PARTICIPATION_EVENEMENTS -> calculerTauxParticipationEvenements(organisationId, dateDebut, dateFin);
case MOYENNE_PARTICIPANTS_EVENEMENT -> calculerMoyenneParticipantsEvenement(organisationId, dateDebut, dateFin);
// Métriques solidarité
case NOMBRE_DEMANDES_AIDE -> calculerNombreDemandesAide(organisationId, dateDebut, dateFin);
case MONTANT_AIDES_ACCORDEES -> calculerMontantAidesAccordees(organisationId, dateDebut, dateFin);
case TAUX_APPROBATION_AIDES -> calculerTauxApprobationAides(organisationId, dateDebut, dateFin);
default -> BigDecimal.ZERO;
};
// Calcul de la valeur précédente pour comparaison
BigDecimal valeurPrecedente = calculerValeurPrecedente(typeMetrique, periodeAnalyse, organisationId);
BigDecimal pourcentageEvolution = calculerPourcentageEvolution(valeur, valeurPrecedente);
return AnalyticsDataDTO.builder()
.typeMetrique(typeMetrique)
.periodeAnalyse(periodeAnalyse)
.valeur(valeur)
.valeurPrecedente(valeurPrecedente)
.pourcentageEvolution(pourcentageEvolution)
.dateDebut(dateDebut)
.dateFin(dateFin)
.dateCalcul(LocalDateTime.now())
.organisationId(organisationId)
.nomOrganisation(obtenirNomOrganisation(organisationId))
.indicateurFiabilite(new BigDecimal("95.0"))
.niveauPriorite(3)
.tempsReel(false)
.necessiteMiseAJour(false)
.build();
}
/**
* Calcule les tendances d'un KPI sur une période
*
* @param typeMetrique Le type de métrique
* @param periodeAnalyse La période d'analyse
* @param organisationId L'ID de l'organisation (optionnel)
* @return Les données de tendance du KPI
*/
@Transactional
public KPITrendDTO calculerTendanceKPI(TypeMetrique typeMetrique,
PeriodeAnalyse periodeAnalyse,
UUID organisationId) {
log.info("Calcul de la tendance KPI {} pour la période {} et l'organisation {}",
typeMetrique, periodeAnalyse, organisationId);
return trendAnalysisService.calculerTendance(typeMetrique, periodeAnalyse, organisationId);
}
/**
* Obtient les métriques pour un tableau de bord
*
* @param organisationId L'ID de l'organisation
* @param utilisateurId L'ID de l'utilisateur
* @return La liste des widgets du tableau de bord
*/
@Transactional
public List<DashboardWidgetDTO> obtenirMetriquesTableauBord(UUID organisationId, UUID utilisateurId) {
log.info("Obtention des métriques du tableau de bord pour l'organisation {} et l'utilisateur {}",
organisationId, utilisateurId);
List<DashboardWidgetDTO> widgets = new ArrayList<>();
// Widget KPI Membres Actifs
widgets.add(creerWidgetKPI(TypeMetrique.NOMBRE_MEMBRES_ACTIFS, PeriodeAnalyse.CE_MOIS,
organisationId, utilisateurId, 0, 0, 3, 2));
// Widget KPI Cotisations
widgets.add(creerWidgetKPI(TypeMetrique.TOTAL_COTISATIONS_COLLECTEES, PeriodeAnalyse.CE_MOIS,
organisationId, utilisateurId, 3, 0, 3, 2));
// Widget KPI Événements
widgets.add(creerWidgetKPI(TypeMetrique.NOMBRE_EVENEMENTS_ORGANISES, PeriodeAnalyse.CE_MOIS,
organisationId, utilisateurId, 6, 0, 3, 2));
// Widget KPI Solidarité
widgets.add(creerWidgetKPI(TypeMetrique.NOMBRE_DEMANDES_AIDE, PeriodeAnalyse.CE_MOIS,
organisationId, utilisateurId, 9, 0, 3, 2));
// Widget Graphique Évolution Membres
widgets.add(creerWidgetGraphique(TypeMetrique.NOMBRE_MEMBRES_ACTIFS, PeriodeAnalyse.SIX_DERNIERS_MOIS,
organisationId, utilisateurId, 0, 2, 6, 4, "line"));
// Widget Graphique Évolution Financière
widgets.add(creerWidgetGraphique(TypeMetrique.TOTAL_COTISATIONS_COLLECTEES, PeriodeAnalyse.SIX_DERNIERS_MOIS,
organisationId, utilisateurId, 6, 2, 6, 4, "area"));
return widgets;
}
// === MÉTHODES PRIVÉES DE CALCUL ===
private BigDecimal calculerNombreMembresActifs(UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
Long count = membreRepository.countMembresActifs(organisationId, dateDebut, dateFin);
return new BigDecimal(count);
}
private BigDecimal calculerNombreMembresInactifs(UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
Long count = membreRepository.countMembresInactifs(organisationId, dateDebut, dateFin);
return new BigDecimal(count);
}
private BigDecimal calculerTauxCroissanceMembres(UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
Long membresActuels = membreRepository.countMembresActifs(organisationId, dateDebut, dateFin);
Long membresPrecedents = membreRepository.countMembresActifs(organisationId,
dateDebut.minusMonths(1), dateFin.minusMonths(1));
if (membresPrecedents == 0) return BigDecimal.ZERO;
BigDecimal croissance = new BigDecimal(membresActuels - membresPrecedents)
.divide(new BigDecimal(membresPrecedents), 4, RoundingMode.HALF_UP)
.multiply(new BigDecimal("100"));
return croissance;
}
private BigDecimal calculerMoyenneAgeMembres(UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
Double moyenneAge = membreRepository.calculerMoyenneAge(organisationId, dateDebut, dateFin);
return moyenneAge != null ? new BigDecimal(moyenneAge).setScale(1, RoundingMode.HALF_UP) : BigDecimal.ZERO;
}
private BigDecimal calculerTotalCotisationsCollectees(UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
BigDecimal total = cotisationRepository.sumMontantsPayes(organisationId, dateDebut, dateFin);
return total != null ? total : BigDecimal.ZERO;
}
private BigDecimal calculerCotisationsEnAttente(UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
BigDecimal total = cotisationRepository.sumMontantsEnAttente(organisationId, dateDebut, dateFin);
return total != null ? total : BigDecimal.ZERO;
}
private BigDecimal calculerTauxRecouvrementCotisations(UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
BigDecimal collectees = calculerTotalCotisationsCollectees(organisationId, dateDebut, dateFin);
BigDecimal enAttente = calculerCotisationsEnAttente(organisationId, dateDebut, dateFin);
BigDecimal total = collectees.add(enAttente);
if (total.compareTo(BigDecimal.ZERO) == 0) return BigDecimal.ZERO;
return collectees.divide(total, 4, RoundingMode.HALF_UP).multiply(new BigDecimal("100"));
}
private BigDecimal calculerMoyenneCotisationMembre(UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
BigDecimal total = calculerTotalCotisationsCollectees(organisationId, dateDebut, dateFin);
Long nombreMembres = membreRepository.countMembresActifs(organisationId, dateDebut, dateFin);
if (nombreMembres == 0) return BigDecimal.ZERO;
return total.divide(new BigDecimal(nombreMembres), 2, RoundingMode.HALF_UP);
}
private BigDecimal calculerNombreEvenementsOrganises(UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
Long count = evenementRepository.countEvenements(organisationId, dateDebut, dateFin);
return new BigDecimal(count);
}
private BigDecimal calculerTauxParticipationEvenements(UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
// Implémentation simplifiée - à enrichir selon les besoins
return new BigDecimal("75.5"); // Valeur par défaut
}
private BigDecimal calculerMoyenneParticipantsEvenement(UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
Double moyenne = evenementRepository.calculerMoyenneParticipants(organisationId, dateDebut, dateFin);
return moyenne != null ? new BigDecimal(moyenne).setScale(1, RoundingMode.HALF_UP) : BigDecimal.ZERO;
}
private BigDecimal calculerNombreDemandesAide(UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
Long count = demandeAideRepository.countDemandes(organisationId, dateDebut, dateFin);
return new BigDecimal(count);
}
private BigDecimal calculerMontantAidesAccordees(UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
BigDecimal total = demandeAideRepository.sumMontantsAccordes(organisationId, dateDebut, dateFin);
return total != null ? total : BigDecimal.ZERO;
}
private BigDecimal calculerTauxApprobationAides(UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
Long totalDemandes = demandeAideRepository.countDemandes(organisationId, dateDebut, dateFin);
Long demandesApprouvees = demandeAideRepository.countDemandesApprouvees(organisationId, dateDebut, dateFin);
if (totalDemandes == 0) return BigDecimal.ZERO;
return new BigDecimal(demandesApprouvees)
.divide(new BigDecimal(totalDemandes), 4, RoundingMode.HALF_UP)
.multiply(new BigDecimal("100"));
}
private BigDecimal calculerValeurPrecedente(TypeMetrique typeMetrique, PeriodeAnalyse periodeAnalyse, UUID organisationId) {
// Calcul de la période précédente
LocalDateTime dateDebutPrecedente = periodeAnalyse.getDateDebut().minus(periodeAnalyse.getDuree(), periodeAnalyse.getUnite());
LocalDateTime dateFinPrecedente = periodeAnalyse.getDateFin().minus(periodeAnalyse.getDuree(), periodeAnalyse.getUnite());
return switch (typeMetrique) {
case NOMBRE_MEMBRES_ACTIFS -> calculerNombreMembresActifs(organisationId, dateDebutPrecedente, dateFinPrecedente);
case TOTAL_COTISATIONS_COLLECTEES -> calculerTotalCotisationsCollectees(organisationId, dateDebutPrecedente, dateFinPrecedente);
case NOMBRE_EVENEMENTS_ORGANISES -> calculerNombreEvenementsOrganises(organisationId, dateDebutPrecedente, dateFinPrecedente);
case NOMBRE_DEMANDES_AIDE -> calculerNombreDemandesAide(organisationId, dateDebutPrecedente, dateFinPrecedente);
default -> BigDecimal.ZERO;
};
}
private BigDecimal calculerPourcentageEvolution(BigDecimal valeurActuelle, BigDecimal valeurPrecedente) {
if (valeurPrecedente == null || valeurPrecedente.compareTo(BigDecimal.ZERO) == 0) {
return BigDecimal.ZERO;
}
return valeurActuelle.subtract(valeurPrecedente)
.divide(valeurPrecedente, 4, RoundingMode.HALF_UP)
.multiply(new BigDecimal("100"));
}
private String obtenirNomOrganisation(UUID organisationId) {
if (organisationId == null) return null;
Organisation organisation = organisationRepository.findById(organisationId);
return organisation != null ? organisation.getNom() : null;
}
private DashboardWidgetDTO creerWidgetKPI(TypeMetrique typeMetrique, PeriodeAnalyse periodeAnalyse,
UUID organisationId, UUID utilisateurId,
int positionX, int positionY, int largeur, int hauteur) {
AnalyticsDataDTO data = calculerMetrique(typeMetrique, periodeAnalyse, organisationId);
return DashboardWidgetDTO.builder()
.titre(typeMetrique.getLibelle())
.typeWidget("kpi")
.typeMetrique(typeMetrique)
.periodeAnalyse(periodeAnalyse)
.organisationId(organisationId)
.utilisateurProprietaireId(utilisateurId)
.positionX(positionX)
.positionY(positionY)
.largeur(largeur)
.hauteur(hauteur)
.couleurPrincipale(typeMetrique.getCouleur())
.icone(typeMetrique.getIcone())
.donneesWidget(convertirEnJSON(data))
.dateDerniereMiseAJour(LocalDateTime.now())
.build();
}
private DashboardWidgetDTO creerWidgetGraphique(TypeMetrique typeMetrique, PeriodeAnalyse periodeAnalyse,
UUID organisationId, UUID utilisateurId,
int positionX, int positionY, int largeur, int hauteur,
String typeGraphique) {
KPITrendDTO trend = calculerTendanceKPI(typeMetrique, periodeAnalyse, organisationId);
return DashboardWidgetDTO.builder()
.titre("Évolution " + typeMetrique.getLibelle())
.typeWidget("chart")
.typeMetrique(typeMetrique)
.periodeAnalyse(periodeAnalyse)
.organisationId(organisationId)
.utilisateurProprietaireId(utilisateurId)
.positionX(positionX)
.positionY(positionY)
.largeur(largeur)
.hauteur(hauteur)
.couleurPrincipale(typeMetrique.getCouleur())
.icone(typeMetrique.getIcone())
.donneesWidget(convertirEnJSON(trend))
.configurationVisuelle("{\"type\":\"" + typeGraphique + "\",\"responsive\":true}")
.dateDerniereMiseAJour(LocalDateTime.now())
.build();
}
private String convertirEnJSON(Object data) {
// Implémentation simplifiée - utiliser Jackson en production
return "{}"; // À implémenter avec ObjectMapper
}
}

View File

@@ -0,0 +1,405 @@
package dev.lions.unionflow.server.service;
import dev.lions.unionflow.server.api.dto.solidarite.DemandeAideDTO;
import dev.lions.unionflow.server.api.dto.solidarite.HistoriqueStatutDTO;
import dev.lions.unionflow.server.api.enums.solidarite.StatutAide;
import dev.lions.unionflow.server.api.enums.solidarite.TypeAide;
import dev.lions.unionflow.server.api.enums.solidarite.PrioriteAide;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.NotBlank;
import org.jboss.logging.Logger;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
/**
* Service spécialisé pour la gestion des demandes d'aide
*
* Ce service gère le cycle de vie complet des demandes d'aide :
* création, validation, changements de statut, recherche et suivi.
*
* @author UnionFlow Team
* @version 1.0
* @since 2025-01-16
*/
@ApplicationScoped
public class DemandeAideService {
private static final Logger LOG = Logger.getLogger(DemandeAideService.class);
// Cache en mémoire pour les demandes fréquemment consultées
private final Map<String, DemandeAideDTO> cacheDemandesRecentes = new HashMap<>();
private final Map<String, LocalDateTime> cacheTimestamps = new HashMap<>();
private static final long CACHE_DURATION_MINUTES = 15;
// === OPÉRATIONS CRUD ===
/**
* Crée une nouvelle demande d'aide
*
* @param demandeDTO La demande à créer
* @return La demande créée avec ID généré
*/
@Transactional
public DemandeAideDTO creerDemande(@Valid DemandeAideDTO demandeDTO) {
LOG.infof("Création d'une nouvelle demande d'aide: %s", demandeDTO.getTitre());
// Génération des identifiants
demandeDTO.setId(UUID.randomUUID().toString());
demandeDTO.setNumeroReference(genererNumeroReference());
// Initialisation des dates
LocalDateTime maintenant = LocalDateTime.now();
demandeDTO.setDateCreation(maintenant);
demandeDTO.setDateModification(maintenant);
// Statut initial
if (demandeDTO.getStatut() == null) {
demandeDTO.setStatut(StatutAide.BROUILLON);
}
// Priorité par défaut si non définie
if (demandeDTO.getPriorite() == null) {
demandeDTO.setPriorite(PrioriteAide.NORMALE);
}
// Initialisation de l'historique
HistoriqueStatutDTO historiqueInitial = HistoriqueStatutDTO.builder()
.id(UUID.randomUUID().toString())
.ancienStatut(null)
.nouveauStatut(demandeDTO.getStatut())
.dateChangement(maintenant)
.auteurId(demandeDTO.getDemandeurId())
.motif("Création de la demande")
.estAutomatique(true)
.build();
demandeDTO.setHistoriqueStatuts(List.of(historiqueInitial));
// Calcul du score de priorité
demandeDTO.setScorePriorite(calculerScorePriorite(demandeDTO));
// Sauvegarde en cache
ajouterAuCache(demandeDTO);
LOG.infof("Demande d'aide créée avec succès: %s", demandeDTO.getId());
return demandeDTO;
}
/**
* Met à jour une demande d'aide existante
*
* @param demandeDTO La demande à mettre à jour
* @return La demande mise à jour
*/
@Transactional
public DemandeAideDTO mettreAJour(@Valid DemandeAideDTO demandeDTO) {
LOG.infof("Mise à jour de la demande d'aide: %s", demandeDTO.getId());
// Vérification que la demande peut être modifiée
if (!demandeDTO.isModifiable()) {
throw new IllegalStateException("Cette demande ne peut plus être modifiée");
}
// Mise à jour de la date de modification
demandeDTO.setDateModification(LocalDateTime.now());
demandeDTO.setVersion(demandeDTO.getVersion() + 1);
// Recalcul du score de priorité
demandeDTO.setScorePriorite(calculerScorePriorite(demandeDTO));
// Mise à jour du cache
ajouterAuCache(demandeDTO);
LOG.infof("Demande d'aide mise à jour avec succès: %s", demandeDTO.getId());
return demandeDTO;
}
/**
* Obtient une demande d'aide par son ID
*
* @param id ID de la demande
* @return La demande trouvée
*/
public DemandeAideDTO obtenirParId(@NotBlank String id) {
LOG.debugf("Récupération de la demande d'aide: %s", id);
// Vérification du cache
DemandeAideDTO demandeCachee = obtenirDuCache(id);
if (demandeCachee != null) {
LOG.debugf("Demande trouvée dans le cache: %s", id);
return demandeCachee;
}
// Simulation de récupération depuis la base de données
// Dans une vraie implémentation, ceci ferait appel au repository
DemandeAideDTO demande = simulerRecuperationBDD(id);
if (demande != null) {
ajouterAuCache(demande);
}
return demande;
}
/**
* Change le statut d'une demande d'aide
*
* @param demandeId ID de la demande
* @param nouveauStatut Nouveau statut
* @param motif Motif du changement
* @return La demande avec le nouveau statut
*/
@Transactional
public DemandeAideDTO changerStatut(@NotBlank String demandeId,
@NotNull StatutAide nouveauStatut,
String motif) {
LOG.infof("Changement de statut pour la demande %s: %s", demandeId, nouveauStatut);
DemandeAideDTO demande = obtenirParId(demandeId);
if (demande == null) {
throw new IllegalArgumentException("Demande non trouvée: " + demandeId);
}
StatutAide ancienStatut = demande.getStatut();
// Validation de la transition
if (!ancienStatut.peutTransitionnerVers(nouveauStatut)) {
throw new IllegalStateException(
String.format("Transition invalide de %s vers %s", ancienStatut, nouveauStatut));
}
// Mise à jour du statut
demande.setStatut(nouveauStatut);
demande.setDateModification(LocalDateTime.now());
// Ajout à l'historique
HistoriqueStatutDTO nouvelHistorique = HistoriqueStatutDTO.builder()
.id(UUID.randomUUID().toString())
.ancienStatut(ancienStatut)
.nouveauStatut(nouveauStatut)
.dateChangement(LocalDateTime.now())
.motif(motif)
.estAutomatique(false)
.build();
List<HistoriqueStatutDTO> historique = new ArrayList<>(demande.getHistoriqueStatuts());
historique.add(nouvelHistorique);
demande.setHistoriqueStatuts(historique);
// Actions spécifiques selon le nouveau statut
switch (nouveauStatut) {
case SOUMISE -> demande.setDateSoumission(LocalDateTime.now());
case APPROUVEE, APPROUVEE_PARTIELLEMENT -> demande.setDateApprobation(LocalDateTime.now());
case VERSEE -> demande.setDateVersement(LocalDateTime.now());
case CLOTUREE -> demande.setDateCloture(LocalDateTime.now());
}
// Mise à jour du cache
ajouterAuCache(demande);
LOG.infof("Statut changé avec succès pour la demande %s: %s -> %s",
demandeId, ancienStatut, nouveauStatut);
return demande;
}
// === RECHERCHE ET FILTRAGE ===
/**
* Recherche des demandes avec filtres
*
* @param filtres Map des critères de recherche
* @return Liste des demandes correspondantes
*/
public List<DemandeAideDTO> rechercherAvecFiltres(Map<String, Object> filtres) {
LOG.debugf("Recherche de demandes avec filtres: %s", filtres);
// Simulation de recherche - dans une vraie implémentation,
// ceci utiliserait des requêtes de base de données optimisées
List<DemandeAideDTO> toutesLesDemandes = simulerRecuperationToutesLesDemandes();
return toutesLesDemandes.stream()
.filter(demande -> correspondAuxFiltres(demande, filtres))
.sorted(this::comparerParPriorite)
.collect(Collectors.toList());
}
/**
* Obtient les demandes urgentes pour une organisation
*
* @param organisationId ID de l'organisation
* @return Liste des demandes urgentes
*/
public List<DemandeAideDTO> obtenirDemandesUrgentes(String organisationId) {
LOG.debugf("Récupération des demandes urgentes pour: %s", organisationId);
Map<String, Object> filtres = Map.of(
"organisationId", organisationId,
"priorite", List.of(PrioriteAide.CRITIQUE, PrioriteAide.URGENTE),
"statut", List.of(StatutAide.SOUMISE, StatutAide.EN_ATTENTE,
StatutAide.EN_COURS_EVALUATION, StatutAide.APPROUVEE)
);
return rechercherAvecFiltres(filtres);
}
/**
* Obtient les demandes en retard (délai dépassé)
*
* @param organisationId ID de l'organisation
* @return Liste des demandes en retard
*/
public List<DemandeAideDTO> obtenirDemandesEnRetard(String organisationId) {
LOG.debugf("Récupération des demandes en retard pour: %s", organisationId);
return simulerRecuperationToutesLesDemandes().stream()
.filter(demande -> demande.getOrganisationId().equals(organisationId))
.filter(DemandeAideDTO::isDelaiDepasse)
.filter(demande -> !demande.isTerminee())
.sorted(this::comparerParPriorite)
.collect(Collectors.toList());
}
// === MÉTHODES UTILITAIRES PRIVÉES ===
/**
* Génère un numéro de référence unique
*/
private String genererNumeroReference() {
int annee = LocalDateTime.now().getYear();
int numero = (int) (Math.random() * 999999) + 1;
return String.format("DA-%04d-%06d", annee, numero);
}
/**
* Calcule le score de priorité d'une demande
*/
private double calculerScorePriorite(DemandeAideDTO demande) {
double score = demande.getPriorite().getScorePriorite();
// Bonus pour type d'aide urgent
if (demande.getTypeAide().isUrgent()) {
score -= 1.0;
}
// Bonus pour montant élevé (aide financière)
if (demande.getTypeAide().isFinancier() && demande.getMontantDemande() != null) {
if (demande.getMontantDemande() > 50000) {
score -= 0.5;
}
}
// Malus pour ancienneté
long joursDepuisCreation = java.time.Duration.between(
demande.getDateCreation(), LocalDateTime.now()).toDays();
if (joursDepuisCreation > 7) {
score += 0.3;
}
return Math.max(0.1, score);
}
/**
* Vérifie si une demande correspond aux filtres
*/
private boolean correspondAuxFiltres(DemandeAideDTO demande, Map<String, Object> filtres) {
for (Map.Entry<String, Object> filtre : filtres.entrySet()) {
String cle = filtre.getKey();
Object valeur = filtre.getValue();
switch (cle) {
case "organisationId" -> {
if (!demande.getOrganisationId().equals(valeur)) return false;
}
case "typeAide" -> {
if (valeur instanceof List<?> liste) {
if (!liste.contains(demande.getTypeAide())) return false;
} else if (!demande.getTypeAide().equals(valeur)) {
return false;
}
}
case "statut" -> {
if (valeur instanceof List<?> liste) {
if (!liste.contains(demande.getStatut())) return false;
} else if (!demande.getStatut().equals(valeur)) {
return false;
}
}
case "priorite" -> {
if (valeur instanceof List<?> liste) {
if (!liste.contains(demande.getPriorite())) return false;
} else if (!demande.getPriorite().equals(valeur)) {
return false;
}
}
case "demandeurId" -> {
if (!demande.getDemandeurId().equals(valeur)) return false;
}
}
}
return true;
}
/**
* Compare deux demandes par priorité
*/
private int comparerParPriorite(DemandeAideDTO d1, DemandeAideDTO d2) {
// D'abord par score de priorité (plus bas = plus prioritaire)
int comparaisonScore = Double.compare(d1.getScorePriorite(), d2.getScorePriorite());
if (comparaisonScore != 0) return comparaisonScore;
// Puis par date de création (plus ancien = plus prioritaire)
return d1.getDateCreation().compareTo(d2.getDateCreation());
}
// === GESTION DU CACHE ===
private void ajouterAuCache(DemandeAideDTO demande) {
cacheDemandesRecentes.put(demande.getId(), demande);
cacheTimestamps.put(demande.getId(), LocalDateTime.now());
// Nettoyage du cache si trop volumineux
if (cacheDemandesRecentes.size() > 100) {
nettoyerCache();
}
}
private DemandeAideDTO obtenirDuCache(String id) {
LocalDateTime timestamp = cacheTimestamps.get(id);
if (timestamp == null) return null;
// Vérification de l'expiration
if (LocalDateTime.now().minusMinutes(CACHE_DURATION_MINUTES).isAfter(timestamp)) {
cacheDemandesRecentes.remove(id);
cacheTimestamps.remove(id);
return null;
}
return cacheDemandesRecentes.get(id);
}
private void nettoyerCache() {
LocalDateTime limite = LocalDateTime.now().minusMinutes(CACHE_DURATION_MINUTES);
cacheTimestamps.entrySet().removeIf(entry -> entry.getValue().isBefore(limite));
cacheDemandesRecentes.keySet().retainAll(cacheTimestamps.keySet());
}
// === MÉTHODES DE SIMULATION (À REMPLACER PAR DE VRAIS REPOSITORIES) ===
private DemandeAideDTO simulerRecuperationBDD(String id) {
// Simulation - dans une vraie implémentation, ceci ferait appel au repository
return null;
}
private List<DemandeAideDTO> simulerRecuperationToutesLesDemandes() {
// Simulation - dans une vraie implémentation, ceci ferait appel au repository
return new ArrayList<>();
}
}

View File

@@ -0,0 +1,532 @@
package dev.lions.unionflow.server.service;
import dev.lions.unionflow.server.api.dto.solidarite.EvaluationAideDTO;
import dev.lions.unionflow.server.api.dto.solidarite.DemandeAideDTO;
import dev.lions.unionflow.server.api.dto.solidarite.PropositionAideDTO;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.NotBlank;
import org.jboss.logging.Logger;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
/**
* Service de gestion des évaluations d'aide
*
* Ce service gère le cycle de vie des évaluations :
* création, validation, calcul des moyennes, détection de fraude.
*
* @author UnionFlow Team
* @version 1.0
* @since 2025-01-16
*/
@ApplicationScoped
public class EvaluationService {
private static final Logger LOG = Logger.getLogger(EvaluationService.class);
@Inject
DemandeAideService demandeAideService;
@Inject
PropositionAideService propositionAideService;
// Cache des évaluations récentes
private final Map<String, List<EvaluationAideDTO>> cacheEvaluationsParDemande = new HashMap<>();
private final Map<String, List<EvaluationAideDTO>> cacheEvaluationsParProposition = new HashMap<>();
// === CRÉATION ET GESTION DES ÉVALUATIONS ===
/**
* Crée une nouvelle évaluation d'aide
*
* @param evaluationDTO L'évaluation à créer
* @return L'évaluation créée avec ID généré
*/
@Transactional
public EvaluationAideDTO creerEvaluation(@Valid EvaluationAideDTO evaluationDTO) {
LOG.infof("Création d'une nouvelle évaluation pour la demande: %s",
evaluationDTO.getDemandeAideId());
// Validation préalable
validerEvaluationAvantCreation(evaluationDTO);
// Génération de l'ID et initialisation
evaluationDTO.setId(UUID.randomUUID().toString());
LocalDateTime maintenant = LocalDateTime.now();
evaluationDTO.setDateCreation(maintenant);
evaluationDTO.setDateModification(maintenant);
// Statut initial
if (evaluationDTO.getStatut() == null) {
evaluationDTO.setStatut(EvaluationAideDTO.StatutEvaluation.ACTIVE);
}
// Calcul du score de qualité
double scoreQualite = evaluationDTO.getScoreQualite();
// Détection de fraude potentielle
if (detecterFraudePotentielle(evaluationDTO)) {
evaluationDTO.setStatut(EvaluationAideDTO.StatutEvaluation.SIGNALEE);
LOG.warnf("Évaluation potentiellement frauduleuse détectée: %s", evaluationDTO.getId());
}
// Mise à jour du cache
ajouterAuCache(evaluationDTO);
// Mise à jour des moyennes
mettreAJourMoyennesAsync(evaluationDTO);
LOG.infof("Évaluation créée avec succès: %s (score: %.2f)",
evaluationDTO.getId(), scoreQualite);
return evaluationDTO;
}
/**
* Met à jour une évaluation existante
*
* @param evaluationDTO L'évaluation à mettre à jour
* @return L'évaluation mise à jour
*/
@Transactional
public EvaluationAideDTO mettreAJourEvaluation(@Valid EvaluationAideDTO evaluationDTO) {
LOG.infof("Mise à jour de l'évaluation: %s", evaluationDTO.getId());
// Vérification que l'évaluation peut être modifiée
if (evaluationDTO.getStatut() == EvaluationAideDTO.StatutEvaluation.SUPPRIMEE) {
throw new IllegalStateException("Impossible de modifier une évaluation supprimée");
}
// Mise à jour des dates
evaluationDTO.setDateModification(LocalDateTime.now());
evaluationDTO.setEstModifie(true);
// Nouvelle détection de fraude si changements significatifs
if (detecterChangementsSignificatifs(evaluationDTO)) {
if (detecterFraudePotentielle(evaluationDTO)) {
evaluationDTO.setStatut(EvaluationAideDTO.StatutEvaluation.SIGNALEE);
}
}
// Mise à jour du cache
ajouterAuCache(evaluationDTO);
// Recalcul des moyennes
mettreAJourMoyennesAsync(evaluationDTO);
return evaluationDTO;
}
/**
* Obtient une évaluation par son ID
*
* @param id ID de l'évaluation
* @return L'évaluation trouvée
*/
public EvaluationAideDTO obtenirParId(@NotBlank String id) {
LOG.debugf("Récupération de l'évaluation: %s", id);
// Simulation de récupération - dans une vraie implémentation,
// ceci ferait appel au repository
return simulerRecuperationBDD(id);
}
/**
* Obtient les évaluations d'une demande d'aide
*
* @param demandeId ID de la demande d'aide
* @return Liste des évaluations
*/
public List<EvaluationAideDTO> obtenirEvaluationsDemande(@NotBlank String demandeId) {
LOG.debugf("Récupération des évaluations pour la demande: %s", demandeId);
// Vérification du cache
List<EvaluationAideDTO> evaluationsCachees = cacheEvaluationsParDemande.get(demandeId);
if (evaluationsCachees != null) {
return evaluationsCachees.stream()
.filter(e -> e.getStatut() == EvaluationAideDTO.StatutEvaluation.ACTIVE)
.sorted((e1, e2) -> e2.getDateCreation().compareTo(e1.getDateCreation()))
.collect(Collectors.toList());
}
// Simulation de récupération depuis la base
return simulerRecuperationEvaluationsDemande(demandeId);
}
/**
* Obtient les évaluations d'une proposition d'aide
*
* @param propositionId ID de la proposition d'aide
* @return Liste des évaluations
*/
public List<EvaluationAideDTO> obtenirEvaluationsProposition(@NotBlank String propositionId) {
LOG.debugf("Récupération des évaluations pour la proposition: %s", propositionId);
List<EvaluationAideDTO> evaluationsCachees = cacheEvaluationsParProposition.get(propositionId);
if (evaluationsCachees != null) {
return evaluationsCachees.stream()
.filter(e -> e.getStatut() == EvaluationAideDTO.StatutEvaluation.ACTIVE)
.sorted((e1, e2) -> e2.getDateCreation().compareTo(e1.getDateCreation()))
.collect(Collectors.toList());
}
return simulerRecuperationEvaluationsProposition(propositionId);
}
// === CALCULS DE MOYENNES ET STATISTIQUES ===
/**
* Calcule la note moyenne d'une demande d'aide
*
* @param demandeId ID de la demande
* @return Note moyenne et nombre d'évaluations
*/
public Map<String, Object> calculerMoyenneDemande(@NotBlank String demandeId) {
List<EvaluationAideDTO> evaluations = obtenirEvaluationsDemande(demandeId);
if (evaluations.isEmpty()) {
return Map.of(
"noteMoyenne", 0.0,
"nombreEvaluations", 0,
"repartitionNotes", new HashMap<Integer, Integer>()
);
}
double moyenne = evaluations.stream()
.mapToDouble(EvaluationAideDTO::getNoteGlobale)
.average()
.orElse(0.0);
Map<Integer, Integer> repartition = new HashMap<>();
for (int i = 1; i <= 5; i++) {
final int note = i;
int count = (int) evaluations.stream()
.mapToDouble(EvaluationAideDTO::getNoteGlobale)
.filter(n -> Math.floor(n) == note)
.count();
repartition.put(note, count);
}
return Map.of(
"noteMoyenne", Math.round(moyenne * 100.0) / 100.0,
"nombreEvaluations", evaluations.size(),
"repartitionNotes", repartition,
"pourcentagePositives", calculerPourcentagePositives(evaluations),
"derniereMiseAJour", LocalDateTime.now()
);
}
/**
* Calcule la note moyenne d'une proposition d'aide
*
* @param propositionId ID de la proposition
* @return Note moyenne et statistiques détaillées
*/
public Map<String, Object> calculerMoyenneProposition(@NotBlank String propositionId) {
List<EvaluationAideDTO> evaluations = obtenirEvaluationsProposition(propositionId);
if (evaluations.isEmpty()) {
return Map.of(
"noteMoyenne", 0.0,
"nombreEvaluations", 0,
"notesDetaillees", new HashMap<String, Double>()
);
}
double moyenne = evaluations.stream()
.mapToDouble(EvaluationAideDTO::getNoteGlobale)
.average()
.orElse(0.0);
// Calcul des moyennes détaillées
Map<String, Double> notesDetaillees = new HashMap<>();
notesDetaillees.put("delaiReponse", calculerMoyenneNote(evaluations,
e -> e.getNoteDelaiReponse()));
notesDetaillees.put("communication", calculerMoyenneNote(evaluations,
e -> e.getNoteCommunication()));
notesDetaillees.put("professionnalisme", calculerMoyenneNote(evaluations,
e -> e.getNoteProfessionnalisme()));
notesDetaillees.put("respectEngagements", calculerMoyenneNote(evaluations,
e -> e.getNoteRespectEngagements()));
return Map.of(
"noteMoyenne", Math.round(moyenne * 100.0) / 100.0,
"nombreEvaluations", evaluations.size(),
"notesDetaillees", notesDetaillees,
"pourcentageRecommandations", calculerPourcentageRecommandations(evaluations),
"scoreQualiteMoyen", calculerScoreQualiteMoyen(evaluations)
);
}
// === MODÉRATION ET VALIDATION ===
/**
* Signale une évaluation comme inappropriée
*
* @param evaluationId ID de l'évaluation
* @param motif Motif du signalement
* @return L'évaluation mise à jour
*/
@Transactional
public EvaluationAideDTO signalerEvaluation(@NotBlank String evaluationId, String motif) {
LOG.infof("Signalement de l'évaluation: %s pour motif: %s", evaluationId, motif);
EvaluationAideDTO evaluation = obtenirParId(evaluationId);
if (evaluation == null) {
throw new IllegalArgumentException("Évaluation non trouvée: " + evaluationId);
}
evaluation.setNombreSignalements(evaluation.getNombreSignalements() + 1);
// Masquage automatique si trop de signalements
if (evaluation.getNombreSignalements() >= 3) {
evaluation.setStatut(EvaluationAideDTO.StatutEvaluation.MASQUEE);
LOG.warnf("Évaluation automatiquement masquée: %s", evaluationId);
} else {
evaluation.setStatut(EvaluationAideDTO.StatutEvaluation.SIGNALEE);
}
// Mise à jour du cache
ajouterAuCache(evaluation);
return evaluation;
}
/**
* Valide une évaluation après vérification
*
* @param evaluationId ID de l'évaluation
* @param verificateurId ID du vérificateur
* @return L'évaluation validée
*/
@Transactional
public EvaluationAideDTO validerEvaluation(@NotBlank String evaluationId,
@NotBlank String verificateurId) {
LOG.infof("Validation de l'évaluation: %s par: %s", evaluationId, verificateurId);
EvaluationAideDTO evaluation = obtenirParId(evaluationId);
if (evaluation == null) {
throw new IllegalArgumentException("Évaluation non trouvée: " + evaluationId);
}
evaluation.setEstVerifiee(true);
evaluation.setDateVerification(LocalDateTime.now());
evaluation.setVerificateurId(verificateurId);
evaluation.setStatut(EvaluationAideDTO.StatutEvaluation.ACTIVE);
// Remise à zéro des signalements si validation positive
evaluation.setNombreSignalements(0);
ajouterAuCache(evaluation);
return evaluation;
}
// === MÉTHODES UTILITAIRES PRIVÉES ===
/**
* Valide une évaluation avant création
*/
private void validerEvaluationAvantCreation(EvaluationAideDTO evaluation) {
// Vérifier que la demande existe
DemandeAideDTO demande = demandeAideService.obtenirParId(evaluation.getDemandeAideId());
if (demande == null) {
throw new IllegalArgumentException("Demande d'aide non trouvée: " +
evaluation.getDemandeAideId());
}
// Vérifier que la demande est terminée
if (!demande.isTerminee()) {
throw new IllegalStateException("Impossible d'évaluer une demande non terminée");
}
// Vérifier qu'il n'y a pas déjà une évaluation du même évaluateur
List<EvaluationAideDTO> evaluationsExistantes = obtenirEvaluationsDemande(evaluation.getDemandeAideId());
boolean dejaEvalue = evaluationsExistantes.stream()
.anyMatch(e -> e.getEvaluateurId().equals(evaluation.getEvaluateurId()));
if (dejaEvalue) {
throw new IllegalStateException("Cet évaluateur a déjà évalué cette demande");
}
}
/**
* Détecte une fraude potentielle dans une évaluation
*/
private boolean detecterFraudePotentielle(EvaluationAideDTO evaluation) {
// Critères de détection de fraude
// 1. Note extrême avec commentaire très court
if ((evaluation.getNoteGlobale() <= 1.0 || evaluation.getNoteGlobale() >= 5.0) &&
(evaluation.getCommentairePrincipal() == null ||
evaluation.getCommentairePrincipal().length() < 20)) {
return true;
}
// 2. Toutes les notes identiques (suspect)
if (evaluation.getNotesDetaillees() != null &&
evaluation.getNotesDetaillees().size() > 1) {
Set<Double> notesUniques = new HashSet<>(evaluation.getNotesDetaillees().values());
if (notesUniques.size() == 1) {
return true;
}
}
// 3. Évaluation créée trop rapidement après la fin de l'aide
// (simulation - dans une vraie implémentation, on vérifierait la date de fin réelle)
return false;
}
/**
* Détecte des changements significatifs dans une évaluation
*/
private boolean detecterChangementsSignificatifs(EvaluationAideDTO evaluation) {
// Simulation - dans une vraie implémentation, on comparerait avec la version précédente
return evaluation.getEstModifie();
}
/**
* Met à jour les moyennes de manière asynchrone
*/
private void mettreAJourMoyennesAsync(EvaluationAideDTO evaluation) {
// Simulation d'une mise à jour asynchrone
// Dans une vraie implémentation, ceci utiliserait @Async ou un message queue
try {
// Mise à jour de la moyenne de la demande
Map<String, Object> moyenneDemande = calculerMoyenneDemande(evaluation.getDemandeAideId());
// Mise à jour de la moyenne de la proposition si applicable
if (evaluation.getPropositionAideId() != null) {
Map<String, Object> moyenneProposition = calculerMoyenneProposition(evaluation.getPropositionAideId());
// Mise à jour de la proposition avec la nouvelle moyenne
PropositionAideDTO proposition = propositionAideService.obtenirParId(evaluation.getPropositionAideId());
if (proposition != null) {
proposition.setNoteMoyenne((Double) moyenneProposition.get("noteMoyenne"));
proposition.setNombreEvaluations((Integer) moyenneProposition.get("nombreEvaluations"));
propositionAideService.mettreAJour(proposition);
}
}
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la mise à jour des moyennes pour l'évaluation: %s",
evaluation.getId());
}
}
/**
* Calcule la moyenne d'une note spécifique
*/
private double calculerMoyenneNote(List<EvaluationAideDTO> evaluations,
java.util.function.Function<EvaluationAideDTO, Double> extracteur) {
return evaluations.stream()
.map(extracteur)
.filter(Objects::nonNull)
.mapToDouble(Double::doubleValue)
.average()
.orElse(0.0);
}
/**
* Calcule le pourcentage d'évaluations positives
*/
private double calculerPourcentagePositives(List<EvaluationAideDTO> evaluations) {
if (evaluations.isEmpty()) return 0.0;
long positives = evaluations.stream()
.mapToDouble(EvaluationAideDTO::getNoteGlobale)
.filter(note -> note >= 4.0)
.count();
return (positives * 100.0) / evaluations.size();
}
/**
* Calcule le pourcentage de recommandations
*/
private double calculerPourcentageRecommandations(List<EvaluationAideDTO> evaluations) {
if (evaluations.isEmpty()) return 0.0;
long recommandations = evaluations.stream()
.filter(e -> e.getRecommande() != null && e.getRecommande())
.count();
return (recommandations * 100.0) / evaluations.size();
}
/**
* Calcule le score de qualité moyen
*/
private double calculerScoreQualiteMoyen(List<EvaluationAideDTO> evaluations) {
return evaluations.stream()
.mapToDouble(EvaluationAideDTO::getScoreQualite)
.average()
.orElse(0.0);
}
// === GESTION DU CACHE ===
private void ajouterAuCache(EvaluationAideDTO evaluation) {
// Cache par demande
cacheEvaluationsParDemande.computeIfAbsent(evaluation.getDemandeAideId(),
k -> new ArrayList<>()).add(evaluation);
// Cache par proposition si applicable
if (evaluation.getPropositionAideId() != null) {
cacheEvaluationsParProposition.computeIfAbsent(evaluation.getPropositionAideId(),
k -> new ArrayList<>()).add(evaluation);
}
}
// === RECHERCHE ET FILTRAGE ===
/**
* Recherche des évaluations avec filtres
*
* @param filtres Critères de recherche
* @return Liste des évaluations correspondantes
*/
public List<EvaluationAideDTO> rechercherEvaluations(Map<String, Object> filtres) {
LOG.debugf("Recherche d'évaluations avec filtres: %s", filtres);
// Simulation de recherche - dans une vraie implémentation,
// ceci utiliserait des requêtes de base de données
return new ArrayList<>();
}
/**
* Obtient les évaluations récentes pour le tableau de bord
*
* @param organisationId ID de l'organisation
* @param limite Nombre maximum d'évaluations
* @return Liste des évaluations récentes
*/
public List<EvaluationAideDTO> obtenirEvaluationsRecentes(String organisationId, int limite) {
LOG.debugf("Récupération des %d évaluations récentes pour: %s", limite, organisationId);
// Simulation - filtrage par organisation et tri par date
return new ArrayList<>();
}
// === MÉTHODES DE SIMULATION ===
private EvaluationAideDTO simulerRecuperationBDD(String id) {
return null; // Simulation
}
private List<EvaluationAideDTO> simulerRecuperationEvaluationsDemande(String demandeId) {
return new ArrayList<>(); // Simulation
}
private List<EvaluationAideDTO> simulerRecuperationEvaluationsProposition(String propositionId) {
return new ArrayList<>(); // Simulation
}
}

View File

@@ -0,0 +1,510 @@
package dev.lions.unionflow.server.service;
import dev.lions.unionflow.server.api.dto.notification.NotificationDTO;
import dev.lions.unionflow.server.api.dto.notification.ActionNotificationDTO;
import dev.lions.unionflow.server.api.enums.notification.CanalNotification;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.annotation.PostConstruct;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.jboss.logging.Logger;
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;
import com.google.firebase.messaging.*;
import com.google.auth.oauth2.GoogleCredentials;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
/**
* Service Firebase pour l'envoi de notifications push
*
* Ce service gère l'intégration avec Firebase Cloud Messaging (FCM)
* pour l'envoi de notifications push vers les applications mobiles.
*
* @author UnionFlow Team
* @version 1.0
* @since 2025-01-16
*/
@ApplicationScoped
public class FirebaseNotificationService {
private static final Logger LOG = Logger.getLogger(FirebaseNotificationService.class);
@ConfigProperty(name = "unionflow.firebase.credentials-path")
Optional<String> firebaseCredentialsPath;
@ConfigProperty(name = "unionflow.firebase.project-id")
Optional<String> firebaseProjectId;
@ConfigProperty(name = "unionflow.firebase.enabled", defaultValue = "true")
boolean firebaseEnabled;
@ConfigProperty(name = "unionflow.firebase.dry-run", defaultValue = "false")
boolean dryRun;
@ConfigProperty(name = "unionflow.firebase.batch-size", defaultValue = "500")
int batchSize;
private FirebaseMessaging firebaseMessaging;
private boolean initialized = false;
/**
* Initialise Firebase
*/
@PostConstruct
public void init() {
if (!firebaseEnabled) {
LOG.info("Firebase désactivé par configuration");
return;
}
try {
if (firebaseCredentialsPath.isPresent() && firebaseProjectId.isPresent()) {
GoogleCredentials credentials = GoogleCredentials
.fromStream(new FileInputStream(firebaseCredentialsPath.get()));
FirebaseOptions options = FirebaseOptions.builder()
.setCredentials(credentials)
.setProjectId(firebaseProjectId.get())
.build();
if (FirebaseApp.getApps().isEmpty()) {
FirebaseApp.initializeApp(options);
}
firebaseMessaging = FirebaseMessaging.getInstance();
initialized = true;
LOG.infof("Firebase initialisé avec succès pour le projet: %s", firebaseProjectId.get());
} else {
LOG.warn("Configuration Firebase incomplète - credentials-path ou project-id manquant");
}
} catch (IOException e) {
LOG.errorf(e, "Erreur lors de l'initialisation de Firebase");
}
}
/**
* Envoie une notification push à un seul destinataire
*
* @param notification La notification à envoyer
* @return true si l'envoi a réussi
*/
public boolean envoyerNotificationPush(NotificationDTO notification) {
if (!initialized || !firebaseEnabled) {
LOG.warn("Firebase non initialisé ou désactivé");
return false;
}
try {
// Récupération du token FCM du destinataire
String tokenFCM = obtenirTokenFCM(notification.getDestinatairesIds().get(0));
if (tokenFCM == null || tokenFCM.isEmpty()) {
LOG.warnf("Token FCM non trouvé pour le destinataire: %s",
notification.getDestinatairesIds().get(0));
return false;
}
// Construction du message Firebase
Message message = construireMessage(notification, tokenFCM);
// Envoi
String response = firebaseMessaging.send(message, dryRun);
LOG.infof("Notification envoyée avec succès: %s", response);
return true;
} catch (FirebaseMessagingException e) {
LOG.errorf(e, "Erreur Firebase lors de l'envoi: %s", e.getErrorCode());
// Gestion des erreurs spécifiques
switch (e.getErrorCode()) {
case "INVALID_ARGUMENT":
notification.setMessageErreur("Token FCM invalide");
break;
case "UNREGISTERED":
notification.setMessageErreur("Token FCM non enregistré");
break;
case "SENDER_ID_MISMATCH":
notification.setMessageErreur("Sender ID incorrect");
break;
case "QUOTA_EXCEEDED":
notification.setMessageErreur("Quota Firebase dépassé");
break;
default:
notification.setMessageErreur("Erreur Firebase: " + e.getErrorCode());
}
return false;
} catch (Exception e) {
LOG.errorf(e, "Erreur inattendue lors de l'envoi de notification");
notification.setMessageErreur("Erreur technique: " + e.getMessage());
return false;
}
}
/**
* Envoie une notification push à plusieurs destinataires
*
* @param notification La notification à envoyer
* @param tokensFCM Liste des tokens FCM des destinataires
* @return Résultat de l'envoi groupé
*/
public BatchResponse envoyerNotificationGroupe(NotificationDTO notification, List<String> tokensFCM) {
if (!initialized || !firebaseEnabled) {
LOG.warn("Firebase non initialisé ou désactivé");
return null;
}
try {
// Filtrage des tokens valides
List<String> tokensValides = tokensFCM.stream()
.filter(token -> token != null && !token.isEmpty())
.collect(Collectors.toList());
if (tokensValides.isEmpty()) {
LOG.warn("Aucun token FCM valide trouvé");
return null;
}
// Construction du message multicast
MulticastMessage message = construireMessageMulticast(notification, tokensValides);
// Envoi par batch pour respecter les limites Firebase
List<BatchResponse> responses = new ArrayList<>();
for (int i = 0; i < tokensValides.size(); i += batchSize) {
int fin = Math.min(i + batchSize, tokensValides.size());
List<String> batch = tokensValides.subList(i, fin);
MulticastMessage batchMessage = MulticastMessage.builder()
.setNotification(message.getNotification())
.setAndroidConfig(message.getAndroidConfig())
.setApnsConfig(message.getApnsConfig())
.setWebpushConfig(message.getWebpushConfig())
.putAllData(message.getData())
.addAllTokens(batch)
.build();
BatchResponse response = firebaseMessaging.sendMulticast(batchMessage, dryRun);
responses.add(response);
LOG.infof("Batch envoyé: %d succès, %d échecs",
response.getSuccessCount(), response.getFailureCount());
}
// Consolidation des résultats
return consoliderResultatsBatch(responses);
} catch (FirebaseMessagingException e) {
LOG.errorf(e, "Erreur Firebase lors de l'envoi groupé: %s", e.getErrorCode());
return null;
} catch (Exception e) {
LOG.errorf(e, "Erreur inattendue lors de l'envoi groupé");
return null;
}
}
/**
* Envoie une notification à un topic Firebase
*
* @param notification La notification à envoyer
* @param topic Le topic Firebase
* @return true si l'envoi a réussi
*/
public boolean envoyerNotificationTopic(NotificationDTO notification, String topic) {
if (!initialized || !firebaseEnabled) {
LOG.warn("Firebase non initialisé ou désactivé");
return false;
}
try {
Message message = Message.builder()
.setNotification(construireNotificationFirebase(notification))
.setAndroidConfig(construireConfigAndroid(notification))
.setApnsConfig(construireConfigApns(notification))
.setWebpushConfig(construireConfigWebpush(notification))
.putAllData(construireDonneesPersonnalisees(notification))
.setTopic(topic)
.build();
String response = firebaseMessaging.send(message, dryRun);
LOG.infof("Notification topic envoyée avec succès: %s", response);
return true;
} catch (FirebaseMessagingException e) {
LOG.errorf(e, "Erreur Firebase lors de l'envoi au topic: %s", e.getErrorCode());
return false;
} catch (Exception e) {
LOG.errorf(e, "Erreur inattendue lors de l'envoi au topic");
return false;
}
}
/**
* Abonne un utilisateur à un topic
*
* @param tokenFCM Token FCM de l'utilisateur
* @param topic Topic à abonner
* @return true si l'abonnement a réussi
*/
public boolean abonnerAuTopic(String tokenFCM, String topic) {
if (!initialized || !firebaseEnabled) {
return false;
}
try {
TopicManagementResponse response = firebaseMessaging
.subscribeToTopic(List.of(tokenFCM), topic);
LOG.infof("Abonnement au topic %s: %d succès, %d échecs",
topic, response.getSuccessCount(), response.getFailureCount());
return response.getSuccessCount() > 0;
} catch (FirebaseMessagingException e) {
LOG.errorf(e, "Erreur lors de l'abonnement au topic %s", topic);
return false;
}
}
/**
* Désabonne un utilisateur d'un topic
*
* @param tokenFCM Token FCM de l'utilisateur
* @param topic Topic à désabonner
* @return true si le désabonnement a réussi
*/
public boolean desabonnerDuTopic(String tokenFCM, String topic) {
if (!initialized || !firebaseEnabled) {
return false;
}
try {
TopicManagementResponse response = firebaseMessaging
.unsubscribeFromTopic(List.of(tokenFCM), topic);
LOG.infof("Désabonnement du topic %s: %d succès, %d échecs",
topic, response.getSuccessCount(), response.getFailureCount());
return response.getSuccessCount() > 0;
} catch (FirebaseMessagingException e) {
LOG.errorf(e, "Erreur lors du désabonnement du topic %s", topic);
return false;
}
}
// === MÉTHODES PRIVÉES ===
/**
* Construit un message Firebase pour un destinataire unique
*/
private Message construireMessage(NotificationDTO notification, String tokenFCM) {
return Message.builder()
.setToken(tokenFCM)
.setNotification(construireNotificationFirebase(notification))
.setAndroidConfig(construireConfigAndroid(notification))
.setApnsConfig(construireConfigApns(notification))
.setWebpushConfig(construireConfigWebpush(notification))
.putAllData(construireDonneesPersonnalisees(notification))
.build();
}
/**
* Construit un message multicast Firebase
*/
private MulticastMessage construireMessageMulticast(NotificationDTO notification, List<String> tokens) {
return MulticastMessage.builder()
.addAllTokens(tokens)
.setNotification(construireNotificationFirebase(notification))
.setAndroidConfig(construireConfigAndroid(notification))
.setApnsConfig(construireConfigApns(notification))
.setWebpushConfig(construireConfigWebpush(notification))
.putAllData(construireDonneesPersonnalisees(notification))
.build();
}
/**
* Construit la notification Firebase de base
*/
private Notification construireNotificationFirebase(NotificationDTO notification) {
return Notification.builder()
.setTitle(notification.getTitre())
.setBody(notification.getMessageCourt() != null ?
notification.getMessageCourt() : notification.getMessage())
.setImage(notification.getImageUrl())
.build();
}
/**
* Construit la configuration Android
*/
private AndroidConfig construireConfigAndroid(NotificationDTO notification) {
CanalNotification canal = notification.getCanal();
AndroidNotification.Builder androidNotification = AndroidNotification.builder()
.setTitle(notification.getTitre())
.setBody(notification.getMessage())
.setIcon(notification.getTypeNotification().getIcone())
.setColor(notification.getTypeNotification().getCouleur())
.setChannelId(canal.getId())
.setPriority(AndroidNotification.Priority.valueOf(
canal.isCritique() ? "HIGH" : "DEFAULT"))
.setVisibility(AndroidNotification.Visibility.PUBLIC);
// Configuration du son
if (notification.getDoitEmettreSon()) {
androidNotification.setSound(notification.getSonPersonnalise() != null ?
notification.getSonPersonnalise() : canal.getSonDefaut());
}
// Configuration des actions rapides
if (notification.getActionsRapides() != null && !notification.getActionsRapides().isEmpty()) {
// Les actions rapides Android nécessitent une configuration spéciale
// Elles seront gérées côté client via les données personnalisées
}
return AndroidConfig.builder()
.setNotification(androidNotification.build())
.setPriority(canal.isCritique() ? AndroidConfig.Priority.HIGH : AndroidConfig.Priority.NORMAL)
.setTtl(canal.getDureeVieMs())
.build();
}
/**
* Construit la configuration iOS (APNs)
*/
private ApnsConfig construireConfigApns(NotificationDTO notification) {
CanalNotification canal = notification.getCanal();
Map<String, Object> apsData = new HashMap<>();
apsData.put("alert", Map.of(
"title", notification.getTitre(),
"body", notification.getMessage()
));
apsData.put("sound", notification.getDoitEmettreSon() ? "default" : null);
apsData.put("badge", 1);
if (notification.getDoitVibrer()) {
apsData.put("vibrate", Arrays.toString(canal.getPatternVibration()));
}
return ApnsConfig.builder()
.setAps(Aps.builder()
.putAllCustomData(apsData)
.build())
.putHeader("apns-priority", canal.getPrioriteIOS())
.putHeader("apns-expiration", String.valueOf(
System.currentTimeMillis() + canal.getDureeVieMs()))
.build();
}
/**
* Construit la configuration Web Push
*/
private WebpushConfig construireConfigWebpush(NotificationDTO notification) {
Map<String, String> headers = new HashMap<>();
headers.put("TTL", String.valueOf(notification.getCanal().getDureeVieMs() / 1000));
Map<String, Object> notificationData = new HashMap<>();
notificationData.put("title", notification.getTitre());
notificationData.put("body", notification.getMessage());
notificationData.put("icon", notification.getIconeUrl());
notificationData.put("image", notification.getImageUrl());
notificationData.put("badge", "/images/badge.png");
notificationData.put("vibrate", notification.getCanal().getPatternVibration());
// Actions rapides pour Web Push
if (notification.getActionsRapides() != null) {
List<Map<String, String>> actions = notification.getActionsRapides().stream()
.map(action -> Map.of(
"action", action.getId(),
"title", action.getLibelle(),
"icon", action.getIconeParDefaut()
))
.collect(Collectors.toList());
notificationData.put("actions", actions);
}
return WebpushConfig.builder()
.putAllHeaders(headers)
.setNotification(notificationData)
.build();
}
/**
* Construit les données personnalisées
*/
private Map<String, String> construireDonneesPersonnalisees(NotificationDTO notification) {
Map<String, String> data = new HashMap<>();
// Données de base
data.put("notification_id", notification.getId());
data.put("type", notification.getTypeNotification().name());
data.put("canal", notification.getCanal().getId());
data.put("action_clic", notification.getActionClic());
// Paramètres d'action
if (notification.getParametresAction() != null) {
notification.getParametresAction().forEach(data::put);
}
// Données personnalisées
if (notification.getDonneesPersonnalisees() != null) {
notification.getDonneesPersonnalisees().forEach((key, value) ->
data.put(key, String.valueOf(value)));
}
// Actions rapides (sérialisées en JSON)
if (notification.getActionsRapides() != null && !notification.getActionsRapides().isEmpty()) {
// Sérialisation simplifiée des actions
StringBuilder actionsJson = new StringBuilder("[");
for (int i = 0; i < notification.getActionsRapides().size(); i++) {
ActionNotificationDTO action = notification.getActionsRapides().get(i);
if (i > 0) actionsJson.append(",");
actionsJson.append(String.format(
"{\"id\":\"%s\",\"libelle\":\"%s\",\"type\":\"%s\"}",
action.getId(), action.getLibelle(), action.getTypeAction()
));
}
actionsJson.append("]");
data.put("actions_rapides", actionsJson.toString());
}
return data;
}
/**
* Obtient le token FCM d'un utilisateur
*/
private String obtenirTokenFCM(String utilisateurId) {
// TODO: Implémenter la récupération du token FCM depuis la base de données
// ou le service de préférences utilisateur
return "token_fcm_exemple_" + utilisateurId;
}
/**
* Consolide les résultats de plusieurs batch
*/
private BatchResponse consoliderResultatsBatch(List<BatchResponse> responses) {
// Implémentation simplifiée - dans un vrai projet, il faudrait
// créer un BatchResponse personnalisé qui agrège tous les résultats
return responses.isEmpty() ? null : responses.get(0);
}
/**
* Vérifie si Firebase est initialisé
*/
public boolean isInitialized() {
return initialized && firebaseEnabled;
}
}

View File

@@ -0,0 +1,301 @@
package dev.lions.unionflow.server.service;
import dev.lions.unionflow.server.api.enums.analytics.TypeMetrique;
import dev.lions.unionflow.server.repository.MembreRepository;
import dev.lions.unionflow.server.repository.CotisationRepository;
import dev.lions.unionflow.server.repository.EvenementRepository;
import dev.lions.unionflow.server.repository.DemandeAideRepository;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import lombok.extern.slf4j.Slf4j;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.util.UUID;
import java.util.Map;
import java.util.HashMap;
/**
* Service spécialisé dans le calcul des KPI (Key Performance Indicators)
*
* Ce service fournit des méthodes optimisées pour calculer les indicateurs
* de performance clés de l'application UnionFlow.
*
* @author UnionFlow Team
* @version 1.0
* @since 2025-01-16
*/
@ApplicationScoped
@Slf4j
public class KPICalculatorService {
@Inject
MembreRepository membreRepository;
@Inject
CotisationRepository cotisationRepository;
@Inject
EvenementRepository evenementRepository;
@Inject
DemandeAideRepository demandeAideRepository;
/**
* Calcule tous les KPI principaux pour une organisation
*
* @param organisationId L'ID de l'organisation
* @param dateDebut Date de début de la période
* @param dateFin Date de fin de la période
* @return Map contenant tous les KPI calculés
*/
public Map<TypeMetrique, BigDecimal> calculerTousLesKPI(UUID organisationId,
LocalDateTime dateDebut,
LocalDateTime dateFin) {
log.info("Calcul de tous les KPI pour l'organisation {} sur la période {} - {}",
organisationId, dateDebut, dateFin);
Map<TypeMetrique, BigDecimal> kpis = new HashMap<>();
// KPI Membres
kpis.put(TypeMetrique.NOMBRE_MEMBRES_ACTIFS, calculerKPIMembresActifs(organisationId, dateDebut, dateFin));
kpis.put(TypeMetrique.NOMBRE_MEMBRES_INACTIFS, calculerKPIMembresInactifs(organisationId, dateDebut, dateFin));
kpis.put(TypeMetrique.TAUX_CROISSANCE_MEMBRES, calculerKPITauxCroissanceMembres(organisationId, dateDebut, dateFin));
kpis.put(TypeMetrique.MOYENNE_AGE_MEMBRES, calculerKPIMoyenneAgeMembres(organisationId, dateDebut, dateFin));
// KPI Financiers
kpis.put(TypeMetrique.TOTAL_COTISATIONS_COLLECTEES, calculerKPITotalCotisations(organisationId, dateDebut, dateFin));
kpis.put(TypeMetrique.COTISATIONS_EN_ATTENTE, calculerKPICotisationsEnAttente(organisationId, dateDebut, dateFin));
kpis.put(TypeMetrique.TAUX_RECOUVREMENT_COTISATIONS, calculerKPITauxRecouvrement(organisationId, dateDebut, dateFin));
kpis.put(TypeMetrique.MOYENNE_COTISATION_MEMBRE, calculerKPIMoyenneCotisationMembre(organisationId, dateDebut, dateFin));
// KPI Événements
kpis.put(TypeMetrique.NOMBRE_EVENEMENTS_ORGANISES, calculerKPINombreEvenements(organisationId, dateDebut, dateFin));
kpis.put(TypeMetrique.TAUX_PARTICIPATION_EVENEMENTS, calculerKPITauxParticipation(organisationId, dateDebut, dateFin));
kpis.put(TypeMetrique.MOYENNE_PARTICIPANTS_EVENEMENT, calculerKPIMoyenneParticipants(organisationId, dateDebut, dateFin));
// KPI Solidarité
kpis.put(TypeMetrique.NOMBRE_DEMANDES_AIDE, calculerKPINombreDemandesAide(organisationId, dateDebut, dateFin));
kpis.put(TypeMetrique.MONTANT_AIDES_ACCORDEES, calculerKPIMontantAides(organisationId, dateDebut, dateFin));
kpis.put(TypeMetrique.TAUX_APPROBATION_AIDES, calculerKPITauxApprobationAides(organisationId, dateDebut, dateFin));
log.info("Calcul terminé : {} KPI calculés", kpis.size());
return kpis;
}
/**
* Calcule le KPI de performance globale de l'organisation
*
* @param organisationId L'ID de l'organisation
* @param dateDebut Date de début de la période
* @param dateFin Date de fin de la période
* @return Score de performance global (0-100)
*/
public BigDecimal calculerKPIPerformanceGlobale(UUID organisationId,
LocalDateTime dateDebut,
LocalDateTime dateFin) {
log.info("Calcul du KPI de performance globale pour l'organisation {}", organisationId);
Map<TypeMetrique, BigDecimal> kpis = calculerTousLesKPI(organisationId, dateDebut, dateFin);
// Pondération des différents KPI pour le score global
BigDecimal scoreMembers = calculerScoreMembres(kpis).multiply(new BigDecimal("0.30")); // 30%
BigDecimal scoreFinancier = calculerScoreFinancier(kpis).multiply(new BigDecimal("0.35")); // 35%
BigDecimal scoreEvenements = calculerScoreEvenements(kpis).multiply(new BigDecimal("0.20")); // 20%
BigDecimal scoreSolidarite = calculerScoreSolidarite(kpis).multiply(new BigDecimal("0.15")); // 15%
BigDecimal scoreGlobal = scoreMembers.add(scoreFinancier).add(scoreEvenements).add(scoreSolidarite);
log.info("Score de performance globale calculé : {}", scoreGlobal);
return scoreGlobal.setScale(1, RoundingMode.HALF_UP);
}
/**
* Calcule les KPI de comparaison avec la période précédente
*
* @param organisationId L'ID de l'organisation
* @param dateDebut Date de début de la période actuelle
* @param dateFin Date de fin de la période actuelle
* @return Map des évolutions en pourcentage
*/
public Map<TypeMetrique, BigDecimal> calculerEvolutionsKPI(UUID organisationId,
LocalDateTime dateDebut,
LocalDateTime dateFin) {
log.info("Calcul des évolutions KPI pour l'organisation {}", organisationId);
// Période actuelle
Map<TypeMetrique, BigDecimal> kpisActuels = calculerTousLesKPI(organisationId, dateDebut, dateFin);
// Période précédente (même durée, décalée)
long dureeJours = java.time.Duration.between(dateDebut, dateFin).toDays();
LocalDateTime dateDebutPrecedente = dateDebut.minusDays(dureeJours);
LocalDateTime dateFinPrecedente = dateFin.minusDays(dureeJours);
Map<TypeMetrique, BigDecimal> kpisPrecedents = calculerTousLesKPI(organisationId, dateDebutPrecedente, dateFinPrecedente);
Map<TypeMetrique, BigDecimal> evolutions = new HashMap<>();
for (TypeMetrique typeMetrique : kpisActuels.keySet()) {
BigDecimal valeurActuelle = kpisActuels.get(typeMetrique);
BigDecimal valeurPrecedente = kpisPrecedents.get(typeMetrique);
BigDecimal evolution = calculerPourcentageEvolution(valeurActuelle, valeurPrecedente);
evolutions.put(typeMetrique, evolution);
}
return evolutions;
}
// === MÉTHODES PRIVÉES DE CALCUL DES KPI ===
private BigDecimal calculerKPIMembresActifs(UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
Long count = membreRepository.countMembresActifs(organisationId, dateDebut, dateFin);
return new BigDecimal(count);
}
private BigDecimal calculerKPIMembresInactifs(UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
Long count = membreRepository.countMembresInactifs(organisationId, dateDebut, dateFin);
return new BigDecimal(count);
}
private BigDecimal calculerKPITauxCroissanceMembres(UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
Long membresActuels = membreRepository.countMembresActifs(organisationId, dateDebut, dateFin);
Long membresPrecedents = membreRepository.countMembresActifs(organisationId,
dateDebut.minusMonths(1), dateFin.minusMonths(1));
return calculerTauxCroissance(new BigDecimal(membresActuels), new BigDecimal(membresPrecedents));
}
private BigDecimal calculerKPIMoyenneAgeMembres(UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
Double moyenneAge = membreRepository.calculerMoyenneAge(organisationId, dateDebut, dateFin);
return moyenneAge != null ? new BigDecimal(moyenneAge).setScale(1, RoundingMode.HALF_UP) : BigDecimal.ZERO;
}
private BigDecimal calculerKPITotalCotisations(UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
BigDecimal total = cotisationRepository.sumMontantsPayes(organisationId, dateDebut, dateFin);
return total != null ? total : BigDecimal.ZERO;
}
private BigDecimal calculerKPICotisationsEnAttente(UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
BigDecimal total = cotisationRepository.sumMontantsEnAttente(organisationId, dateDebut, dateFin);
return total != null ? total : BigDecimal.ZERO;
}
private BigDecimal calculerKPITauxRecouvrement(UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
BigDecimal collectees = calculerKPITotalCotisations(organisationId, dateDebut, dateFin);
BigDecimal enAttente = calculerKPICotisationsEnAttente(organisationId, dateDebut, dateFin);
BigDecimal total = collectees.add(enAttente);
if (total.compareTo(BigDecimal.ZERO) == 0) return BigDecimal.ZERO;
return collectees.divide(total, 4, RoundingMode.HALF_UP).multiply(new BigDecimal("100"));
}
private BigDecimal calculerKPIMoyenneCotisationMembre(UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
BigDecimal total = calculerKPITotalCotisations(organisationId, dateDebut, dateFin);
Long nombreMembres = membreRepository.countMembresActifs(organisationId, dateDebut, dateFin);
if (nombreMembres == 0) return BigDecimal.ZERO;
return total.divide(new BigDecimal(nombreMembres), 2, RoundingMode.HALF_UP);
}
private BigDecimal calculerKPINombreEvenements(UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
Long count = evenementRepository.countEvenements(organisationId, dateDebut, dateFin);
return new BigDecimal(count);
}
private BigDecimal calculerKPITauxParticipation(UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
// Calcul basé sur les participations aux événements
Long totalParticipations = evenementRepository.countTotalParticipations(organisationId, dateDebut, dateFin);
Long nombreEvenements = evenementRepository.countEvenements(organisationId, dateDebut, dateFin);
Long nombreMembres = membreRepository.countMembresActifs(organisationId, dateDebut, dateFin);
if (nombreEvenements == 0 || nombreMembres == 0) return BigDecimal.ZERO;
BigDecimal participationsAttendues = new BigDecimal(nombreEvenements).multiply(new BigDecimal(nombreMembres));
BigDecimal tauxParticipation = new BigDecimal(totalParticipations)
.divide(participationsAttendues, 4, RoundingMode.HALF_UP)
.multiply(new BigDecimal("100"));
return tauxParticipation;
}
private BigDecimal calculerKPIMoyenneParticipants(UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
Double moyenne = evenementRepository.calculerMoyenneParticipants(organisationId, dateDebut, dateFin);
return moyenne != null ? new BigDecimal(moyenne).setScale(1, RoundingMode.HALF_UP) : BigDecimal.ZERO;
}
private BigDecimal calculerKPINombreDemandesAide(UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
Long count = demandeAideRepository.countDemandes(organisationId, dateDebut, dateFin);
return new BigDecimal(count);
}
private BigDecimal calculerKPIMontantAides(UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
BigDecimal total = demandeAideRepository.sumMontantsAccordes(organisationId, dateDebut, dateFin);
return total != null ? total : BigDecimal.ZERO;
}
private BigDecimal calculerKPITauxApprobationAides(UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
Long totalDemandes = demandeAideRepository.countDemandes(organisationId, dateDebut, dateFin);
Long demandesApprouvees = demandeAideRepository.countDemandesApprouvees(organisationId, dateDebut, dateFin);
if (totalDemandes == 0) return BigDecimal.ZERO;
return new BigDecimal(demandesApprouvees)
.divide(new BigDecimal(totalDemandes), 4, RoundingMode.HALF_UP)
.multiply(new BigDecimal("100"));
}
// === MÉTHODES UTILITAIRES ===
private BigDecimal calculerTauxCroissance(BigDecimal valeurActuelle, BigDecimal valeurPrecedente) {
if (valeurPrecedente.compareTo(BigDecimal.ZERO) == 0) return BigDecimal.ZERO;
return valeurActuelle.subtract(valeurPrecedente)
.divide(valeurPrecedente, 4, RoundingMode.HALF_UP)
.multiply(new BigDecimal("100"));
}
private BigDecimal calculerPourcentageEvolution(BigDecimal valeurActuelle, BigDecimal valeurPrecedente) {
if (valeurPrecedente == null || valeurPrecedente.compareTo(BigDecimal.ZERO) == 0) {
return BigDecimal.ZERO;
}
return valeurActuelle.subtract(valeurPrecedente)
.divide(valeurPrecedente, 4, RoundingMode.HALF_UP)
.multiply(new BigDecimal("100"));
}
private BigDecimal calculerScoreMembres(Map<TypeMetrique, BigDecimal> kpis) {
// Score basé sur la croissance et l'activité des membres
BigDecimal tauxCroissance = kpis.get(TypeMetrique.TAUX_CROISSANCE_MEMBRES);
BigDecimal nombreActifs = kpis.get(TypeMetrique.NOMBRE_MEMBRES_ACTIFS);
BigDecimal nombreInactifs = kpis.get(TypeMetrique.NOMBRE_MEMBRES_INACTIFS);
// Calcul du score (logique simplifiée)
BigDecimal scoreActivite = nombreActifs.divide(nombreActifs.add(nombreInactifs), 2, RoundingMode.HALF_UP)
.multiply(new BigDecimal("50"));
BigDecimal scoreCroissance = tauxCroissance.min(new BigDecimal("50")); // Plafonné à 50
return scoreActivite.add(scoreCroissance);
}
private BigDecimal calculerScoreFinancier(Map<TypeMetrique, BigDecimal> kpis) {
// Score basé sur le recouvrement et les montants
BigDecimal tauxRecouvrement = kpis.get(TypeMetrique.TAUX_RECOUVREMENT_COTISATIONS);
return tauxRecouvrement; // Score direct basé sur le taux de recouvrement
}
private BigDecimal calculerScoreEvenements(Map<TypeMetrique, BigDecimal> kpis) {
// Score basé sur la participation aux événements
BigDecimal tauxParticipation = kpis.get(TypeMetrique.TAUX_PARTICIPATION_EVENEMENTS);
return tauxParticipation; // Score direct basé sur le taux de participation
}
private BigDecimal calculerScoreSolidarite(Map<TypeMetrique, BigDecimal> kpis) {
// Score basé sur l'efficacité du système de solidarité
BigDecimal tauxApprobation = kpis.get(TypeMetrique.TAUX_APPROBATION_AIDES);
return tauxApprobation; // Score direct basé sur le taux d'approbation
}
}

View File

@@ -0,0 +1,418 @@
package dev.lions.unionflow.server.service;
import dev.lions.unionflow.server.api.dto.solidarite.DemandeAideDTO;
import dev.lions.unionflow.server.api.dto.solidarite.PropositionAideDTO;
import dev.lions.unionflow.server.api.dto.solidarite.CritereSelectionDTO;
import dev.lions.unionflow.server.api.enums.solidarite.TypeAide;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.jboss.logging.Logger;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
/**
* Service intelligent de matching entre demandes et propositions d'aide
*
* Ce service utilise des algorithmes avancés pour faire correspondre
* les demandes d'aide avec les propositions les plus appropriées.
*
* @author UnionFlow Team
* @version 1.0
* @since 2025-01-16
*/
@ApplicationScoped
public class MatchingService {
private static final Logger LOG = Logger.getLogger(MatchingService.class);
@Inject
PropositionAideService propositionAideService;
@Inject
DemandeAideService demandeAideService;
@ConfigProperty(name = "unionflow.matching.score-minimum", defaultValue = "30.0")
double scoreMinimumMatching;
@ConfigProperty(name = "unionflow.matching.max-resultats", defaultValue = "10")
int maxResultatsMatching;
@ConfigProperty(name = "unionflow.matching.boost-geographique", defaultValue = "10.0")
double boostGeographique;
@ConfigProperty(name = "unionflow.matching.boost-experience", defaultValue = "5.0")
double boostExperience;
// === MATCHING DEMANDES -> PROPOSITIONS ===
/**
* Trouve les propositions compatibles avec une demande d'aide
*
* @param demande La demande d'aide
* @return Liste des propositions compatibles triées par score
*/
public List<PropositionAideDTO> trouverPropositionsCompatibles(DemandeAideDTO demande) {
LOG.infof("Recherche de propositions compatibles pour la demande: %s", demande.getId());
long startTime = System.currentTimeMillis();
try {
// 1. Recherche de base par type d'aide
List<PropositionAideDTO> candidats = propositionAideService
.obtenirPropositionsActives(demande.getTypeAide());
// 2. Si pas assez de candidats, élargir à la catégorie
if (candidats.size() < 3) {
candidats.addAll(rechercherParCategorie(demande.getTypeAide().getCategorie()));
}
// 3. Filtrage et scoring
List<ResultatMatching> resultats = candidats.stream()
.filter(PropositionAideDTO::isActiveEtDisponible)
.filter(p -> p.peutAccepterBeneficiaires())
.map(proposition -> {
double score = calculerScoreCompatibilite(demande, proposition);
return new ResultatMatching(proposition, score);
})
.filter(resultat -> resultat.score >= scoreMinimumMatching)
.sorted((r1, r2) -> Double.compare(r2.score, r1.score))
.limit(maxResultatsMatching)
.collect(Collectors.toList());
// 4. Extraction des propositions
List<PropositionAideDTO> propositionsCompatibles = resultats.stream()
.map(resultat -> {
// Stocker le score dans les données personnalisées
if (resultat.proposition.getDonneesPersonnalisees() == null) {
resultat.proposition.setDonneesPersonnalisees(new HashMap<>());
}
resultat.proposition.getDonneesPersonnalisees().put("scoreMatching", resultat.score);
return resultat.proposition;
})
.collect(Collectors.toList());
long duration = System.currentTimeMillis() - startTime;
LOG.infof("Matching terminé en %d ms. Trouvé %d propositions compatibles",
duration, propositionsCompatibles.size());
return propositionsCompatibles;
} catch (Exception e) {
LOG.errorf(e, "Erreur lors du matching pour la demande: %s", demande.getId());
return new ArrayList<>();
}
}
/**
* Trouve les demandes compatibles avec une proposition d'aide
*
* @param proposition La proposition d'aide
* @return Liste des demandes compatibles triées par score
*/
public List<DemandeAideDTO> trouverDemandesCompatibles(PropositionAideDTO proposition) {
LOG.infof("Recherche de demandes compatibles pour la proposition: %s", proposition.getId());
try {
// Recherche des demandes actives du même type
Map<String, Object> filtres = Map.of(
"typeAide", proposition.getTypeAide(),
"statut", List.of(
dev.lions.unionflow.server.api.enums.solidarite.StatutAide.SOUMISE,
dev.lions.unionflow.server.api.enums.solidarite.StatutAide.EN_ATTENTE,
dev.lions.unionflow.server.api.enums.solidarite.StatutAide.EN_COURS_EVALUATION,
dev.lions.unionflow.server.api.enums.solidarite.StatutAide.APPROUVEE
)
);
List<DemandeAideDTO> candidats = demandeAideService.rechercherAvecFiltres(filtres);
// Scoring et tri
return candidats.stream()
.map(demande -> {
double score = calculerScoreCompatibilite(demande, proposition);
// Stocker le score temporairement
if (demande.getDonneesPersonnalisees() == null) {
demande.setDonneesPersonnalisees(new HashMap<>());
}
demande.getDonneesPersonnalisees().put("scoreMatching", score);
return demande;
})
.filter(demande -> (Double) demande.getDonneesPersonnalisees().get("scoreMatching") >= scoreMinimumMatching)
.sorted((d1, d2) -> {
Double score1 = (Double) d1.getDonneesPersonnalisees().get("scoreMatching");
Double score2 = (Double) d2.getDonneesPersonnalisees().get("scoreMatching");
return Double.compare(score2, score1);
})
.limit(maxResultatsMatching)
.collect(Collectors.toList());
} catch (Exception e) {
LOG.errorf(e, "Erreur lors du matching pour la proposition: %s", proposition.getId());
return new ArrayList<>();
}
}
// === MATCHING SPÉCIALISÉ ===
/**
* Recherche spécialisée de proposants financiers pour une demande approuvée
*
* @param demande La demande d'aide financière approuvée
* @return Liste des proposants financiers compatibles
*/
public List<PropositionAideDTO> rechercherProposantsFinanciers(DemandeAideDTO demande) {
LOG.infof("Recherche de proposants financiers pour la demande: %s", demande.getId());
if (!demande.getTypeAide().isFinancier()) {
LOG.warnf("La demande %s n'est pas de type financier", demande.getId());
return new ArrayList<>();
}
// Filtres spécifiques pour les aides financières
Map<String, Object> filtres = Map.of(
"typeAide", demande.getTypeAide(),
"estDisponible", true,
"montantMaximum", demande.getMontantApprouve() != null ?
demande.getMontantApprouve() : demande.getMontantDemande()
);
List<PropositionAideDTO> propositions = propositionAideService.rechercherAvecFiltres(filtres);
// Scoring spécialisé pour les aides financières
return propositions.stream()
.map(proposition -> {
double score = calculerScoreFinancier(demande, proposition);
if (proposition.getDonneesPersonnalisees() == null) {
proposition.setDonneesPersonnalisees(new HashMap<>());
}
proposition.getDonneesPersonnalisees().put("scoreFinancier", score);
return proposition;
})
.filter(p -> (Double) p.getDonneesPersonnalisees().get("scoreFinancier") >= 40.0)
.sorted((p1, p2) -> {
Double score1 = (Double) p1.getDonneesPersonnalisees().get("scoreFinancier");
Double score2 = (Double) p2.getDonneesPersonnalisees().get("scoreFinancier");
return Double.compare(score2, score1);
})
.limit(5) // Limiter à 5 pour les aides financières
.collect(Collectors.toList());
}
/**
* Matching d'urgence pour les demandes critiques
*
* @param demande La demande d'aide urgente
* @return Liste des propositions d'urgence
*/
public List<PropositionAideDTO> matchingUrgence(DemandeAideDTO demande) {
LOG.infof("Matching d'urgence pour la demande: %s", demande.getId());
// Recherche élargie pour les urgences
List<PropositionAideDTO> candidats = new ArrayList<>();
// 1. Même type d'aide
candidats.addAll(propositionAideService.obtenirPropositionsActives(demande.getTypeAide()));
// 2. Types d'aide de la même catégorie
candidats.addAll(rechercherParCategorie(demande.getTypeAide().getCategorie()));
// 3. Propositions généralistes (type AUTRE)
candidats.addAll(propositionAideService.obtenirPropositionsActives(TypeAide.AUTRE));
// Scoring avec bonus d'urgence
return candidats.stream()
.distinct()
.filter(PropositionAideDTO::isActiveEtDisponible)
.map(proposition -> {
double score = calculerScoreCompatibilite(demande, proposition);
// Bonus d'urgence
score += 20.0;
if (proposition.getDonneesPersonnalisees() == null) {
proposition.setDonneesPersonnalisees(new HashMap<>());
}
proposition.getDonneesPersonnalisees().put("scoreUrgence", score);
return proposition;
})
.filter(p -> (Double) p.getDonneesPersonnalisees().get("scoreUrgence") >= 25.0)
.sorted((p1, p2) -> {
Double score1 = (Double) p1.getDonneesPersonnalisees().get("scoreUrgence");
Double score2 = (Double) p2.getDonneesPersonnalisees().get("scoreUrgence");
return Double.compare(score2, score1);
})
.limit(15) // Plus de résultats pour les urgences
.collect(Collectors.toList());
}
// === ALGORITHMES DE SCORING ===
/**
* Calcule le score de compatibilité entre une demande et une proposition
*/
private double calculerScoreCompatibilite(DemandeAideDTO demande, PropositionAideDTO proposition) {
double score = 0.0;
// 1. Correspondance du type d'aide (40 points max)
if (demande.getTypeAide() == proposition.getTypeAide()) {
score += 40.0;
} else if (demande.getTypeAide().getCategorie().equals(proposition.getTypeAide().getCategorie())) {
score += 25.0;
} else if (proposition.getTypeAide() == TypeAide.AUTRE) {
score += 15.0;
}
// 2. Compatibilité financière (25 points max)
if (demande.getTypeAide().isNecessiteMontant() && proposition.getMontantMaximum() != null) {
Double montantDemande = demande.getMontantApprouve() != null ?
demande.getMontantApprouve() : demande.getMontantDemande();
if (montantDemande != null) {
if (montantDemande <= proposition.getMontantMaximum()) {
score += 25.0;
} else {
// Pénalité proportionnelle au dépassement
double ratio = proposition.getMontantMaximum() / montantDemande;
score += 25.0 * ratio;
}
}
} else if (!demande.getTypeAide().isNecessiteMontant()) {
score += 25.0; // Pas de contrainte financière
}
// 3. Expérience du proposant (15 points max)
if (proposition.getNombreBeneficiairesAides() > 0) {
score += Math.min(15.0, proposition.getNombreBeneficiairesAides() * boostExperience);
}
// 4. Réputation (10 points max)
if (proposition.getNoteMoyenne() != null && proposition.getNombreEvaluations() >= 3) {
score += (proposition.getNoteMoyenne() - 3.0) * 3.33; // 0 à 10 points
}
// 5. Disponibilité et capacité (10 points max)
if (proposition.peutAccepterBeneficiaires()) {
double ratioCapacite = (double) proposition.getPlacesRestantes() /
proposition.getNombreMaxBeneficiaires();
score += 10.0 * ratioCapacite;
}
// Bonus et malus additionnels
score += calculerBonusGeographique(demande, proposition);
score += calculerBonusTemporel(demande, proposition);
score -= calculerMalusDelai(demande, proposition);
return Math.max(0.0, Math.min(100.0, score));
}
/**
* Calcule le score spécialisé pour les aides financières
*/
private double calculerScoreFinancier(DemandeAideDTO demande, PropositionAideDTO proposition) {
double score = calculerScoreCompatibilite(demande, proposition);
// Bonus spécifiques aux aides financières
// 1. Historique de versements
if (proposition.getMontantTotalVerse() > 0) {
score += Math.min(10.0, proposition.getMontantTotalVerse() / 10000.0);
}
// 2. Fiabilité (ratio versements/promesses)
if (proposition.getNombreDemandesTraitees() > 0) {
// Simulation d'un ratio de fiabilité
double ratioFiabilite = 0.9; // À calculer réellement
score += ratioFiabilite * 15.0;
}
// 3. Rapidité de réponse
if (proposition.getDelaiReponseHeures() <= 24) {
score += 10.0;
} else if (proposition.getDelaiReponseHeures() <= 72) {
score += 5.0;
}
return Math.max(0.0, Math.min(100.0, score));
}
/**
* Calcule le bonus géographique
*/
private double calculerBonusGeographique(DemandeAideDTO demande, PropositionAideDTO proposition) {
// Simulation - dans une vraie implémentation, ceci utiliserait les données de localisation
if (demande.getLocalisation() != null && proposition.getZonesGeographiques() != null) {
// Logique de proximité géographique
return boostGeographique;
}
return 0.0;
}
/**
* Calcule le bonus temporel (urgence, disponibilité)
*/
private double calculerBonusTemporel(DemandeAideDTO demande, PropositionAideDTO proposition) {
double bonus = 0.0;
// Bonus pour demande urgente
if (demande.isUrgente()) {
bonus += 5.0;
}
// Bonus pour proposition récente
long joursDepuisCreation = java.time.Duration.between(
proposition.getDateCreation(), LocalDateTime.now()).toDays();
if (joursDepuisCreation <= 30) {
bonus += 3.0;
}
return bonus;
}
/**
* Calcule le malus de délai
*/
private double calculerMalusDelai(DemandeAideDTO demande, PropositionAideDTO proposition) {
double malus = 0.0;
// Malus si la demande est en retard
if (demande.isDelaiDepasse()) {
malus += 5.0;
}
// Malus si la proposition a un délai de réponse long
if (proposition.getDelaiReponseHeures() > 168) { // Plus d'une semaine
malus += 3.0;
}
return malus;
}
// === MÉTHODES UTILITAIRES ===
/**
* Recherche des propositions par catégorie
*/
private List<PropositionAideDTO> rechercherParCategorie(String categorie) {
Map<String, Object> filtres = Map.of("estDisponible", true);
return propositionAideService.rechercherAvecFiltres(filtres).stream()
.filter(p -> p.getTypeAide().getCategorie().equals(categorie))
.collect(Collectors.toList());
}
/**
* Classe interne pour stocker les résultats de matching
*/
private static class ResultatMatching {
final PropositionAideDTO proposition;
final double score;
ResultatMatching(PropositionAideDTO proposition, double score) {
this.proposition = proposition;
this.score = score;
}
}
}

View File

@@ -0,0 +1,476 @@
package dev.lions.unionflow.server.service;
import dev.lions.unionflow.server.api.dto.notification.NotificationDTO;
import dev.lions.unionflow.server.api.dto.notification.PreferencesNotificationDTO;
import dev.lions.unionflow.server.api.enums.notification.TypeNotification;
import dev.lions.unionflow.server.api.enums.notification.StatutNotification;
import dev.lions.unionflow.server.api.enums.notification.CanalNotification;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.jboss.logging.Logger;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
/**
* Service principal de gestion des notifications UnionFlow
*
* Ce service orchestre l'envoi, la gestion et le suivi des notifications
* avec intégration Firebase, templates dynamiques et préférences utilisateur.
*
* @author UnionFlow Team
* @version 1.0
* @since 2025-01-16
*/
@ApplicationScoped
public class NotificationService {
private static final Logger LOG = Logger.getLogger(NotificationService.class);
@Inject
FirebaseNotificationService firebaseService;
@Inject
NotificationTemplateService templateService;
@Inject
PreferencesNotificationService preferencesService;
@Inject
NotificationHistoryService historyService;
@Inject
NotificationSchedulerService schedulerService;
@ConfigProperty(name = "unionflow.notifications.enabled", defaultValue = "true")
boolean notificationsEnabled;
@ConfigProperty(name = "unionflow.notifications.batch-size", defaultValue = "100")
int batchSize;
@ConfigProperty(name = "unionflow.notifications.retry-attempts", defaultValue = "3")
int maxRetryAttempts;
@ConfigProperty(name = "unionflow.notifications.retry-delay-minutes", defaultValue = "5")
int retryDelayMinutes;
// Cache des préférences utilisateur pour optimiser les performances
private final Map<String, PreferencesNotificationDTO> preferencesCache = new ConcurrentHashMap<>();
// Statistiques en temps réel
private final Map<String, Long> statistiques = new ConcurrentHashMap<>();
/**
* Envoie une notification simple
*
* @param notification La notification à envoyer
* @return CompletableFuture avec le résultat de l'envoi
*/
public CompletableFuture<NotificationDTO> envoyerNotification(NotificationDTO notification) {
LOG.infof("Envoi de notification: %s", notification.getId());
return CompletableFuture.supplyAsync(() -> {
try {
// Validation des données
validerNotification(notification);
// Vérification des préférences utilisateur
if (!verifierPreferencesUtilisateur(notification)) {
notification.setStatut(StatutNotification.ANNULEE);
notification.setMessageErreur("Notification bloquée par les préférences utilisateur");
return notification;
}
// Application des templates
notification = templateService.appliquerTemplate(notification);
// Envoi via Firebase
notification.setStatut(StatutNotification.EN_COURS_ENVOI);
notification.setDateEnvoi(LocalDateTime.now());
boolean succes = firebaseService.envoyerNotificationPush(notification);
if (succes) {
notification.setStatut(StatutNotification.ENVOYEE);
incrementerStatistique("notifications_envoyees");
} else {
notification.setStatut(StatutNotification.ECHEC_ENVOI);
incrementerStatistique("notifications_echec");
}
// Sauvegarde dans l'historique
historyService.sauvegarderNotification(notification);
return notification;
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de l'envoi de la notification %s", notification.getId());
notification.setStatut(StatutNotification.ERREUR_TECHNIQUE);
notification.setMessageErreur(e.getMessage());
notification.setTraceErreur(Arrays.toString(e.getStackTrace()));
incrementerStatistique("notifications_erreur");
return notification;
}
});
}
/**
* Envoie une notification à plusieurs destinataires
*
* @param typeNotification Type de notification
* @param titre Titre de la notification
* @param message Message de la notification
* @param destinatairesIds Liste des IDs des destinataires
* @param donneesPersonnalisees Données personnalisées
* @return CompletableFuture avec la liste des résultats
*/
public CompletableFuture<List<NotificationDTO>> envoyerNotificationGroupe(
TypeNotification typeNotification,
String titre,
String message,
List<String> destinatairesIds,
Map<String, Object> donneesPersonnalisees) {
LOG.infof("Envoi de notification de groupe: %s destinataires", destinatairesIds.size());
return CompletableFuture.supplyAsync(() -> {
List<NotificationDTO> resultats = new ArrayList<>();
// Traitement par batch pour optimiser les performances
for (int i = 0; i < destinatairesIds.size(); i += batchSize) {
int fin = Math.min(i + batchSize, destinatairesIds.size());
List<String> batch = destinatairesIds.subList(i, fin);
List<CompletableFuture<NotificationDTO>> futures = batch.stream()
.map(destinataireId -> {
NotificationDTO notification = new NotificationDTO(
typeNotification, titre, message, List.of(destinataireId)
);
notification.setId(UUID.randomUUID().toString());
notification.setDonneesPersonnalisees(donneesPersonnalisees);
return envoyerNotification(notification);
})
.toList();
// Attendre que tous les envois du batch soient terminés
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.join();
// Collecter les résultats
futures.forEach(future -> {
try {
resultats.add(future.get());
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la récupération du résultat");
}
});
}
incrementerStatistique("notifications_groupe_envoyees");
return resultats;
});
}
/**
* Programme une notification pour envoi ultérieur
*
* @param notification La notification à programmer
* @param dateEnvoi Date et heure d'envoi programmé
* @return La notification programmée
*/
@Transactional
public NotificationDTO programmerNotification(NotificationDTO notification, LocalDateTime dateEnvoi) {
LOG.infof("Programmation de notification pour: %s", dateEnvoi);
notification.setId(UUID.randomUUID().toString());
notification.setStatut(StatutNotification.PROGRAMMEE);
notification.setDateEnvoiProgramme(dateEnvoi);
notification.setDateCreation(LocalDateTime.now());
// Validation
validerNotification(notification);
// Sauvegarde
historyService.sauvegarderNotification(notification);
// Programmation dans le scheduler
schedulerService.programmerNotification(notification);
incrementerStatistique("notifications_programmees");
return notification;
}
/**
* Annule une notification programmée
*
* @param notificationId ID de la notification à annuler
* @return true si l'annulation a réussi
*/
@Transactional
public boolean annulerNotificationProgrammee(String notificationId) {
LOG.infof("Annulation de notification programmée: %s", notificationId);
try {
NotificationDTO notification = historyService.obtenirNotification(notificationId);
if (notification != null && notification.getStatut().permetAnnulation()) {
notification.setStatut(StatutNotification.ANNULEE);
historyService.mettreAJourNotification(notification);
schedulerService.annulerNotificationProgrammee(notificationId);
incrementerStatistique("notifications_annulees");
return true;
}
return false;
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de l'annulation de la notification %s", notificationId);
return false;
}
}
/**
* Marque une notification comme lue
*
* @param notificationId ID de la notification
* @param utilisateurId ID de l'utilisateur
* @return true si le marquage a réussi
*/
@Transactional
public boolean marquerCommeLue(String notificationId, String utilisateurId) {
LOG.debugf("Marquage comme lue: notification=%s, utilisateur=%s", notificationId, utilisateurId);
try {
NotificationDTO notification = historyService.obtenirNotification(notificationId);
if (notification != null && notification.getDestinatairesIds().contains(utilisateurId)) {
notification.setEstLue(true);
notification.setDateDerniereLecture(LocalDateTime.now());
notification.setStatut(StatutNotification.LUE);
historyService.mettreAJourNotification(notification);
incrementerStatistique("notifications_lues");
return true;
}
return false;
} catch (Exception e) {
LOG.errorf(e, "Erreur lors du marquage comme lue: %s", notificationId);
return false;
}
}
/**
* Archive une notification
*
* @param notificationId ID de la notification
* @param utilisateurId ID de l'utilisateur
* @return true si l'archivage a réussi
*/
@Transactional
public boolean archiverNotification(String notificationId, String utilisateurId) {
LOG.debugf("Archivage: notification=%s, utilisateur=%s", notificationId, utilisateurId);
try {
NotificationDTO notification = historyService.obtenirNotification(notificationId);
if (notification != null && notification.getDestinatairesIds().contains(utilisateurId)) {
notification.setEstArchivee(true);
notification.setStatut(StatutNotification.ARCHIVEE);
historyService.mettreAJourNotification(notification);
incrementerStatistique("notifications_archivees");
return true;
}
return false;
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de l'archivage: %s", notificationId);
return false;
}
}
/**
* Obtient les notifications d'un utilisateur
*
* @param utilisateurId ID de l'utilisateur
* @param includeArchivees Inclure les notifications archivées
* @param limite Nombre maximum de notifications à retourner
* @return Liste des notifications
*/
public List<NotificationDTO> obtenirNotificationsUtilisateur(
String utilisateurId, boolean includeArchivees, int limite) {
LOG.debugf("Récupération notifications utilisateur: %s", utilisateurId);
try {
return historyService.obtenirNotificationsUtilisateur(
utilisateurId, includeArchivees, limite
);
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la récupération des notifications pour %s", utilisateurId);
return new ArrayList<>();
}
}
/**
* Obtient les statistiques des notifications
*
* @return Map des statistiques
*/
public Map<String, Long> obtenirStatistiques() {
Map<String, Long> stats = new HashMap<>(statistiques);
// Ajout des statistiques calculées
stats.put("notifications_total",
stats.getOrDefault("notifications_envoyees", 0L) +
stats.getOrDefault("notifications_echec", 0L) +
stats.getOrDefault("notifications_erreur", 0L)
);
long envoyees = stats.getOrDefault("notifications_envoyees", 0L);
long total = stats.get("notifications_total");
if (total > 0) {
stats.put("taux_succes_pct", (envoyees * 100) / total);
}
return stats;
}
/**
* Envoie une notification de test
*
* @param utilisateurId ID de l'utilisateur
* @param typeNotification Type de notification à tester
* @return La notification de test envoyée
*/
public CompletableFuture<NotificationDTO> envoyerNotificationTest(
String utilisateurId, TypeNotification typeNotification) {
LOG.infof("Envoi notification de test: utilisateur=%s, type=%s", utilisateurId, typeNotification);
NotificationDTO notification = new NotificationDTO(
typeNotification,
"Test - " + typeNotification.getLibelle(),
"Ceci est une notification de test pour vérifier vos paramètres.",
List.of(utilisateurId)
);
notification.setId("test-" + UUID.randomUUID().toString());
notification.getDonneesPersonnalisees().put("test", true);
notification.getTags().add("test");
return envoyerNotification(notification);
}
// === MÉTHODES PRIVÉES ===
/**
* Valide une notification avant envoi
*/
private void validerNotification(NotificationDTO notification) {
if (notification.getTitre() == null || notification.getTitre().trim().isEmpty()) {
throw new IllegalArgumentException("Le titre de la notification est obligatoire");
}
if (notification.getMessage() == null || notification.getMessage().trim().isEmpty()) {
throw new IllegalArgumentException("Le message de la notification est obligatoire");
}
if (notification.getDestinatairesIds() == null || notification.getDestinatairesIds().isEmpty()) {
throw new IllegalArgumentException("Au moins un destinataire est requis");
}
if (notification.getTypeNotification() == null) {
throw new IllegalArgumentException("Le type de notification est obligatoire");
}
}
/**
* Vérifie les préférences utilisateur pour une notification
*/
private boolean verifierPreferencesUtilisateur(NotificationDTO notification) {
if (!notificationsEnabled) {
return false;
}
// Vérification pour chaque destinataire
for (String destinataireId : notification.getDestinatairesIds()) {
PreferencesNotificationDTO preferences = obtenirPreferencesUtilisateur(destinataireId);
if (preferences == null || !preferences.getNotificationsActivees()) {
return false;
}
if (!preferences.isTypeActive(notification.getTypeNotification())) {
return false;
}
if (!preferences.isCanalActif(notification.getCanal())) {
return false;
}
if (preferences.isExpediteurBloque(notification.getExpediteurId())) {
return false;
}
if (preferences.isEnModeSilencieux() &&
!notification.getTypeNotification().isCritique() &&
!preferences.getUrgentesIgnorentSilencieux()) {
return false;
}
}
return true;
}
/**
* Obtient les préférences d'un utilisateur (avec cache)
*/
private PreferencesNotificationDTO obtenirPreferencesUtilisateur(String utilisateurId) {
return preferencesCache.computeIfAbsent(utilisateurId, id -> {
try {
return preferencesService.obtenirPreferences(id);
} catch (Exception e) {
LOG.warnf("Impossible de récupérer les préférences pour %s, utilisation des défauts", id);
return new PreferencesNotificationDTO(id);
}
});
}
/**
* Incrémente une statistique
*/
private void incrementerStatistique(String cle) {
statistiques.merge(cle, 1L, Long::sum);
}
/**
* Vide le cache des préférences
*/
public void viderCachePreferences() {
preferencesCache.clear();
LOG.info("Cache des préférences vidé");
}
/**
* Recharge les préférences d'un utilisateur
*/
public void rechargerPreferencesUtilisateur(String utilisateurId) {
preferencesCache.remove(utilisateurId);
LOG.debugf("Préférences rechargées pour l'utilisateur: %s", utilisateurId);
}
}

View File

@@ -0,0 +1,554 @@
package dev.lions.unionflow.server.service;
import dev.lions.unionflow.server.api.dto.solidarite.DemandeAideDTO;
import dev.lions.unionflow.server.api.dto.solidarite.PropositionAideDTO;
import dev.lions.unionflow.server.api.dto.solidarite.EvaluationAideDTO;
import dev.lions.unionflow.server.api.dto.notification.NotificationDTO;
import dev.lions.unionflow.server.api.enums.notification.TypeNotification;
import dev.lions.unionflow.server.api.enums.notification.CanalNotification;
import dev.lions.unionflow.server.api.enums.solidarite.PrioriteAide;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.jboss.logging.Logger;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.CompletableFuture;
/**
* Service spécialisé pour les notifications du système de solidarité
*
* Ce service gère toutes les notifications liées aux demandes d'aide,
* propositions, évaluations et processus de solidarité.
*
* @author UnionFlow Team
* @version 1.0
* @since 2025-01-16
*/
@ApplicationScoped
public class NotificationSolidariteService {
private static final Logger LOG = Logger.getLogger(NotificationSolidariteService.class);
@Inject
NotificationService notificationService;
@ConfigProperty(name = "unionflow.solidarite.notifications.enabled", defaultValue = "true")
boolean notificationsEnabled;
@ConfigProperty(name = "unionflow.solidarite.notifications.urgence.immediate", defaultValue = "true")
boolean notificationsUrgenceImmediate;
// === NOTIFICATIONS DEMANDES D'AIDE ===
/**
* Notifie la création d'une nouvelle demande d'aide
*
* @param demande La demande d'aide créée
*/
public CompletableFuture<Void> notifierCreationDemande(DemandeAideDTO demande) {
if (!notificationsEnabled) return CompletableFuture.completedFuture(null);
LOG.infof("Notification de création de demande: %s", demande.getId());
return CompletableFuture.runAsync(() -> {
try {
// Notification au demandeur
NotificationDTO notificationDemandeur = creerNotificationBase(
TypeNotification.DEMANDE_AIDE_CREEE,
"Demande d'aide créée",
String.format("Votre demande d'aide \"%s\" a été créée avec succès.", demande.getTitre()),
List.of(demande.getDemandeurId())
);
ajouterDonneesContexteDemande(notificationDemandeur, demande);
notificationService.envoyerNotification(notificationDemandeur);
// Notification aux administrateurs si priorité élevée
if (demande.getPriorite().getNiveau() <= 2) {
notifierAdministrateursNouvelleDemandeUrgente(demande);
}
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la notification de création de demande: %s", demande.getId());
}
});
}
/**
* Notifie la soumission d'une demande d'aide
*
* @param demande La demande soumise
*/
public CompletableFuture<Void> notifierSoumissionDemande(DemandeAideDTO demande) {
if (!notificationsEnabled) return CompletableFuture.completedFuture(null);
LOG.infof("Notification de soumission de demande: %s", demande.getId());
return CompletableFuture.runAsync(() -> {
try {
// Notification au demandeur
NotificationDTO notification = creerNotificationBase(
TypeNotification.DEMANDE_AIDE_SOUMISE,
"Demande d'aide soumise",
String.format("Votre demande \"%s\" a été soumise et sera évaluée dans les %d heures.",
demande.getTitre(), demande.getPriorite().getDelaiTraitementHeures()),
List.of(demande.getDemandeurId())
);
ajouterDonneesContexteDemande(notification, demande);
notificationService.envoyerNotification(notification);
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la notification de soumission: %s", demande.getId());
}
});
}
/**
* Notifie une décision d'évaluation
*
* @param demande La demande évaluée
*/
public CompletableFuture<Void> notifierDecisionEvaluation(DemandeAideDTO demande) {
if (!notificationsEnabled) return CompletableFuture.completedFuture(null);
LOG.infof("Notification de décision d'évaluation: %s", demande.getId());
return CompletableFuture.runAsync(() -> {
try {
TypeNotification typeNotification;
String titre;
String message;
switch (demande.getStatut()) {
case APPROUVEE -> {
typeNotification = TypeNotification.DEMANDE_AIDE_APPROUVEE;
titre = "Demande d'aide approuvée";
message = String.format("Excellente nouvelle ! Votre demande \"%s\" a été approuvée.",
demande.getTitre());
if (demande.getMontantApprouve() != null) {
message += String.format(" Montant approuvé : %.0f FCFA", demande.getMontantApprouve());
}
}
case APPROUVEE_PARTIELLEMENT -> {
typeNotification = TypeNotification.DEMANDE_AIDE_APPROUVEE;
titre = "Demande d'aide partiellement approuvée";
message = String.format("Votre demande \"%s\" a été partiellement approuvée. Montant : %.0f FCFA",
demande.getTitre(), demande.getMontantApprouve());
}
case REJETEE -> {
typeNotification = TypeNotification.DEMANDE_AIDE_REJETEE;
titre = "Demande d'aide rejetée";
message = String.format("Votre demande \"%s\" n'a pas pu être approuvée.", demande.getTitre());
if (demande.getMotifRejet() != null) {
message += " Motif : " + demande.getMotifRejet();
}
}
case INFORMATIONS_REQUISES -> {
typeNotification = TypeNotification.INFORMATIONS_REQUISES;
titre = "Informations complémentaires requises";
message = String.format("Des informations complémentaires sont nécessaires pour votre demande \"%s\".",
demande.getTitre());
}
default -> {
return; // Pas de notification pour les autres statuts
}
}
NotificationDTO notification = creerNotificationBase(
typeNotification, titre, message, List.of(demande.getDemandeurId())
);
ajouterDonneesContexteDemande(notification, demande);
notificationService.envoyerNotification(notification);
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la notification de décision: %s", demande.getId());
}
});
}
/**
* Notifie une urgence critique
*
* @param demande La demande critique
*/
public CompletableFuture<Void> notifierUrgenceCritique(DemandeAideDTO demande) {
if (!notificationsEnabled || !notificationsUrgenceImmediate) {
return CompletableFuture.completedFuture(null);
}
LOG.warnf("Notification d'urgence critique pour la demande: %s", demande.getId());
return CompletableFuture.runAsync(() -> {
try {
// Notification immédiate aux administrateurs et évaluateurs
List<String> destinataires = obtenirAdministrateursEtEvaluateurs(demande.getOrganisationId());
NotificationDTO notification = creerNotificationBase(
TypeNotification.URGENCE_CRITIQUE,
"🚨 URGENCE CRITIQUE - Demande d'aide",
String.format("ATTENTION : Demande d'aide critique \"%s\" nécessitant une intervention immédiate.",
demande.getTitre()),
destinataires
);
// Canal prioritaire pour les urgences
notification.setCanalNotification(CanalNotification.URGENCE);
notification.setPriorite(1); // Priorité maximale
ajouterDonneesContexteDemande(notification, demande);
notificationService.envoyerNotification(notification);
// Notification SMS/appel si configuré
if (demande.getContactUrgence() != null) {
notifierContactUrgence(demande);
}
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la notification d'urgence critique: %s", demande.getId());
}
});
}
// === NOTIFICATIONS PROPOSITIONS D'AIDE ===
/**
* Notifie la création d'une proposition d'aide
*
* @param proposition La proposition créée
*/
public CompletableFuture<Void> notifierCreationProposition(PropositionAideDTO proposition) {
if (!notificationsEnabled) return CompletableFuture.completedFuture(null);
LOG.infof("Notification de création de proposition: %s", proposition.getId());
return CompletableFuture.runAsync(() -> {
try {
NotificationDTO notification = creerNotificationBase(
TypeNotification.PROPOSITION_AIDE_CREEE,
"Proposition d'aide créée",
String.format("Votre proposition d'aide \"%s\" a été créée et est maintenant active.",
proposition.getTitre()),
List.of(proposition.getProposantId())
);
ajouterDonneesContexteProposition(notification, proposition);
notificationService.envoyerNotification(notification);
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la notification de création de proposition: %s",
proposition.getId());
}
});
}
/**
* Notifie les proposants compatibles d'une nouvelle demande
*
* @param demande La nouvelle demande
* @param propositionsCompatibles Les propositions compatibles
*/
public CompletableFuture<Void> notifierProposantsCompatibles(DemandeAideDTO demande,
List<PropositionAideDTO> propositionsCompatibles) {
if (!notificationsEnabled || propositionsCompatibles.isEmpty()) {
return CompletableFuture.completedFuture(null);
}
LOG.infof("Notification de %d proposants compatibles pour la demande: %s",
propositionsCompatibles.size(), demande.getId());
return CompletableFuture.runAsync(() -> {
try {
List<String> proposantsIds = propositionsCompatibles.stream()
.map(PropositionAideDTO::getProposantId)
.distinct()
.toList();
NotificationDTO notification = creerNotificationBase(
TypeNotification.DEMANDE_COMPATIBLE_TROUVEE,
"Nouvelle demande d'aide compatible",
String.format("Une nouvelle demande d'aide \"%s\" correspond à votre proposition.",
demande.getTitre()),
proposantsIds
);
ajouterDonneesContexteDemande(notification, demande);
// Ajout du score de compatibilité
Map<String, Object> donneesSupplementaires = new HashMap<>();
donneesSupplementaires.put("nombrePropositionsCompatibles", propositionsCompatibles.size());
donneesSupplementaires.put("typeAide", demande.getTypeAide().getLibelle());
notification.getDonneesPersonnalisees().putAll(donneesSupplementaires);
notificationService.envoyerNotification(notification);
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la notification aux proposants compatibles");
}
});
}
/**
* Notifie un proposant de demandes compatibles
*
* @param proposition La proposition
* @param demandesCompatibles Les demandes compatibles
*/
public CompletableFuture<Void> notifierDemandesCompatibles(PropositionAideDTO proposition,
List<DemandeAideDTO> demandesCompatibles) {
if (!notificationsEnabled || demandesCompatibles.isEmpty()) {
return CompletableFuture.completedFuture(null);
}
LOG.infof("Notification de %d demandes compatibles pour la proposition: %s",
demandesCompatibles.size(), proposition.getId());
return CompletableFuture.runAsync(() -> {
try {
String message = demandesCompatibles.size() == 1 ?
String.format("Une demande d'aide \"%s\" correspond à votre proposition.",
demandesCompatibles.get(0).getTitre()) :
String.format("%d demandes d'aide correspondent à votre proposition \"%s\".",
demandesCompatibles.size(), proposition.getTitre());
NotificationDTO notification = creerNotificationBase(
TypeNotification.PROPOSITIONS_COMPATIBLES_TROUVEES,
"Demandes d'aide compatibles trouvées",
message,
List.of(proposition.getProposantId())
);
ajouterDonneesContexteProposition(notification, proposition);
// Ajout des détails des demandes
Map<String, Object> donneesSupplementaires = new HashMap<>();
donneesSupplementaires.put("nombreDemandesCompatibles", demandesCompatibles.size());
donneesSupplementaires.put("demandesUrgentes",
demandesCompatibles.stream().filter(DemandeAideDTO::isUrgente).count());
notification.getDonneesPersonnalisees().putAll(donneesSupplementaires);
notificationService.envoyerNotification(notification);
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la notification des demandes compatibles");
}
});
}
// === NOTIFICATIONS ÉVALUATEURS ===
/**
* Notifie les évaluateurs d'une nouvelle demande à évaluer
*
* @param demande La demande à évaluer
*/
public CompletableFuture<Void> notifierEvaluateurs(DemandeAideDTO demande) {
if (!notificationsEnabled) return CompletableFuture.completedFuture(null);
LOG.infof("Notification aux évaluateurs pour la demande: %s", demande.getId());
return CompletableFuture.runAsync(() -> {
try {
List<String> evaluateurs = obtenirEvaluateursDisponibles(demande.getOrganisationId());
if (!evaluateurs.isEmpty()) {
String prioriteTexte = demande.getPriorite().isUrgente() ? " URGENTE" : "";
NotificationDTO notification = creerNotificationBase(
TypeNotification.DEMANDE_A_EVALUER,
"Nouvelle demande d'aide à évaluer" + prioriteTexte,
String.format("Une nouvelle demande d'aide%s \"%s\" nécessite votre évaluation.",
prioriteTexte.toLowerCase(), demande.getTitre()),
evaluateurs
);
if (demande.getPriorite().isUrgente()) {
notification.setCanalNotification(CanalNotification.URGENT);
notification.setPriorite(2);
}
ajouterDonneesContexteDemande(notification, demande);
notificationService.envoyerNotification(notification);
}
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la notification aux évaluateurs: %s", demande.getId());
}
});
}
// === RAPPELS ET PROGRAMMATION ===
/**
* Programme les rappels automatiques pour une demande
*
* @param demande La demande
* @param rappel50 Rappel à 50% du délai
* @param rappel80 Rappel à 80% du délai
* @param rappelDepassement Rappel de dépassement
*/
public void programmerRappels(DemandeAideDTO demande,
LocalDateTime rappel50,
LocalDateTime rappel80,
LocalDateTime rappelDepassement) {
if (!notificationsEnabled) return;
LOG.infof("Programmation des rappels pour la demande: %s", demande.getId());
try {
// Rappel à 50%
NotificationDTO notification50 = creerNotificationRappel(demande,
"Rappel : 50% du délai écoulé",
"La moitié du délai de traitement est écoulée.");
notificationService.programmerNotification(notification50, rappel50);
// Rappel à 80%
NotificationDTO notification80 = creerNotificationRappel(demande,
"Rappel urgent : 80% du délai écoulé",
"Attention : 80% du délai de traitement est écoulé !");
notification80.setCanalNotification(CanalNotification.URGENT);
notificationService.programmerNotification(notification80, rappel80);
// Rappel de dépassement
NotificationDTO notificationDepassement = creerNotificationRappel(demande,
"🚨 DÉLAI DÉPASSÉ",
"ATTENTION : Le délai de traitement est dépassé !");
notificationDepassement.setCanalNotification(CanalNotification.URGENCE);
notificationDepassement.setPriorite(1);
notificationService.programmerNotification(notificationDepassement, rappelDepassement);
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la programmation des rappels pour: %s", demande.getId());
}
}
/**
* Programme un rappel pour informations requises
*
* @param demande La demande nécessitant des informations
* @param dateRappel Date du rappel
*/
public void programmerRappelInformationsRequises(DemandeAideDTO demande, LocalDateTime dateRappel) {
if (!notificationsEnabled) return;
LOG.infof("Programmation du rappel d'informations pour la demande: %s", demande.getId());
try {
NotificationDTO notification = creerNotificationBase(
TypeNotification.RAPPEL_INFORMATIONS_REQUISES,
"Rappel : Informations complémentaires requises",
String.format("N'oubliez pas de fournir les informations complémentaires pour votre demande \"%s\".",
demande.getTitre()),
List.of(demande.getDemandeurId())
);
ajouterDonneesContexteDemande(notification, demande);
notificationService.programmerNotification(notification, dateRappel);
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la programmation du rappel d'informations: %s", demande.getId());
}
}
// === MÉTHODES UTILITAIRES PRIVÉES ===
/**
* Crée une notification de base
*/
private NotificationDTO creerNotificationBase(TypeNotification type, String titre,
String message, List<String> destinataires) {
return NotificationDTO.builder()
.id(UUID.randomUUID().toString())
.typeNotification(type)
.titre(titre)
.message(message)
.destinatairesIds(destinataires)
.canalNotification(CanalNotification.GENERAL)
.priorite(3)
.donneesPersonnalisees(new HashMap<>())
.dateCreation(LocalDateTime.now())
.build();
}
/**
* Ajoute les données de contexte d'une demande à une notification
*/
private void ajouterDonneesContexteDemande(NotificationDTO notification, DemandeAideDTO demande) {
Map<String, Object> donnees = notification.getDonneesPersonnalisees();
donnees.put("demandeId", demande.getId());
donnees.put("numeroReference", demande.getNumeroReference());
donnees.put("typeAide", demande.getTypeAide().getLibelle());
donnees.put("priorite", demande.getPriorite().getLibelle());
donnees.put("statut", demande.getStatut().getLibelle());
if (demande.getMontantDemande() != null) {
donnees.put("montant", demande.getMontantDemande());
}
}
/**
* Ajoute les données de contexte d'une proposition à une notification
*/
private void ajouterDonneesContexteProposition(NotificationDTO notification, PropositionAideDTO proposition) {
Map<String, Object> donnees = notification.getDonneesPersonnalisees();
donnees.put("propositionId", proposition.getId());
donnees.put("numeroReference", proposition.getNumeroReference());
donnees.put("typeAide", proposition.getTypeAide().getLibelle());
donnees.put("statut", proposition.getStatut().getLibelle());
if (proposition.getMontantMaximum() != null) {
donnees.put("montantMaximum", proposition.getMontantMaximum());
}
}
/**
* Crée une notification de rappel
*/
private NotificationDTO creerNotificationRappel(DemandeAideDTO demande, String titre, String messageRappel) {
List<String> destinataires = obtenirEvaluateursAssignes(demande);
String message = String.format("%s Demande : \"%s\" (%s)",
messageRappel, demande.getTitre(), demande.getNumeroReference());
NotificationDTO notification = creerNotificationBase(
TypeNotification.RAPPEL_DELAI_TRAITEMENT,
titre,
message,
destinataires
);
ajouterDonneesContexteDemande(notification, demande);
return notification;
}
// === MÉTHODES DE SIMULATION (À REMPLACER PAR DE VRAIS SERVICES) ===
private List<String> obtenirAdministrateursEtEvaluateurs(String organisationId) {
// Simulation - dans une vraie implémentation, ceci ferait appel au service utilisateur
return List.of("admin1", "evaluateur1", "evaluateur2");
}
private List<String> obtenirEvaluateursDisponibles(String organisationId) {
// Simulation
return List.of("evaluateur1", "evaluateur2", "evaluateur3");
}
private List<String> obtenirEvaluateursAssignes(DemandeAideDTO demande) {
// Simulation
return demande.getEvaluateurId() != null ?
List.of(demande.getEvaluateurId()) :
obtenirEvaluateursDisponibles(demande.getOrganisationId());
}
private void notifierAdministrateursNouvelleDemandeUrgente(DemandeAideDTO demande) {
// Simulation d'une notification spéciale aux administrateurs
LOG.infof("Notification spéciale aux administrateurs pour demande urgente: %s", demande.getId());
}
private void notifierContactUrgence(DemandeAideDTO demande) {
// Simulation d'une notification au contact d'urgence
LOG.infof("Notification au contact d'urgence pour la demande: %s", demande.getId());
}
}

View File

@@ -0,0 +1,493 @@
package dev.lions.unionflow.server.service;
import dev.lions.unionflow.server.api.dto.notification.NotificationDTO;
import dev.lions.unionflow.server.api.dto.notification.ActionNotificationDTO;
import dev.lions.unionflow.server.api.enums.notification.TypeNotification;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.jboss.logging.Logger;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Service de gestion des templates de notifications
*
* Ce service applique des templates dynamiques aux notifications
* en fonction du type, du contexte et des données personnalisées.
*
* @author UnionFlow Team
* @version 1.0
* @since 2025-01-16
*/
@ApplicationScoped
public class NotificationTemplateService {
private static final Logger LOG = Logger.getLogger(NotificationTemplateService.class);
// Pattern pour détecter les variables dans les templates
private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\{\\{([^}]+)\\}\\}");
// Formatters pour les dates
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("dd/MM/yyyy");
private static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern("dd/MM/yyyy à HH:mm");
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm");
@Inject
MembreService membreService;
@Inject
OrganisationService organisationService;
@Inject
EvenementService evenementService;
// Cache des templates pour optimiser les performances
private final Map<TypeNotification, NotificationTemplate> templatesCache = new HashMap<>();
/**
* Applique un template à une notification
*
* @param notification La notification à traiter
* @return La notification avec le template appliqué
*/
public NotificationDTO appliquerTemplate(NotificationDTO notification) {
LOG.debugf("Application du template pour: %s", notification.getTypeNotification());
try {
// Récupération du template
NotificationTemplate template = obtenirTemplate(notification.getTypeNotification());
if (template == null) {
LOG.warnf("Aucun template trouvé pour: %s", notification.getTypeNotification());
return notification;
}
// Préparation des variables de contexte
Map<String, Object> contexte = construireContexte(notification);
// Application du template au titre
if (template.getTitreTemplate() != null) {
String titrePersonnalise = appliquerVariables(template.getTitreTemplate(), contexte);
notification.setTitre(titrePersonnalise);
}
// Application du template au message
if (template.getMessageTemplate() != null) {
String messagePersonnalise = appliquerVariables(template.getMessageTemplate(), contexte);
notification.setMessage(messagePersonnalise);
}
// Application du template au message court
if (template.getMessageCourtTemplate() != null) {
String messageCourtPersonnalise = appliquerVariables(template.getMessageCourtTemplate(), contexte);
notification.setMessageCourt(messageCourtPersonnalise);
}
// Application des actions rapides du template
if (template.getActionsRapides() != null && !template.getActionsRapides().isEmpty()) {
List<ActionNotificationDTO> actionsPersonnalisees = new ArrayList<>();
for (ActionNotificationDTO actionTemplate : template.getActionsRapides()) {
ActionNotificationDTO actionPersonnalisee = new ActionNotificationDTO();
actionPersonnalisee.setId(actionTemplate.getId());
actionPersonnalisee.setTypeAction(actionTemplate.getTypeAction());
actionPersonnalisee.setLibelle(appliquerVariables(actionTemplate.getLibelle(), contexte));
actionPersonnalisee.setDescription(appliquerVariables(actionTemplate.getDescription(), contexte));
actionPersonnalisee.setIcone(actionTemplate.getIcone());
actionPersonnalisee.setCouleur(actionTemplate.getCouleur());
actionPersonnalisee.setRoute(appliquerVariables(actionTemplate.getRoute(), contexte));
actionPersonnalisee.setUrl(appliquerVariables(actionTemplate.getUrl(), contexte));
// Paramètres personnalisés
if (actionTemplate.getParametres() != null) {
Map<String, String> parametresPersonnalises = new HashMap<>();
actionTemplate.getParametres().forEach((key, value) ->
parametresPersonnalises.put(key, appliquerVariables(value, contexte)));
actionPersonnalisee.setParametres(parametresPersonnalises);
}
actionsPersonnalisees.add(actionPersonnalisee);
}
notification.setActionsRapides(actionsPersonnalisees);
}
// Application des propriétés du template
if (template.getImageUrl() != null) {
notification.setImageUrl(appliquerVariables(template.getImageUrl(), contexte));
}
if (template.getIconeUrl() != null) {
notification.setIconeUrl(appliquerVariables(template.getIconeUrl(), contexte));
}
if (template.getActionClic() != null) {
notification.setActionClic(appliquerVariables(template.getActionClic(), contexte));
}
// Fusion des données personnalisées
if (template.getDonneesPersonnalisees() != null) {
Map<String, Object> donneesPersonnalisees = notification.getDonneesPersonnalisees();
if (donneesPersonnalisees == null) {
donneesPersonnalisees = new HashMap<>();
notification.setDonneesPersonnalisees(donneesPersonnalisees);
}
template.getDonneesPersonnalisees().forEach((key, value) -> {
String valeurPersonnalisee = appliquerVariables(String.valueOf(value), contexte);
donneesPersonnalisees.put(key, valeurPersonnalisee);
});
}
LOG.debugf("Template appliqué avec succès pour: %s", notification.getTypeNotification());
return notification;
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de l'application du template pour: %s", notification.getTypeNotification());
return notification; // Retourner la notification originale en cas d'erreur
}
}
/**
* Crée une notification à partir d'un template
*
* @param typeNotification Type de notification
* @param destinatairesIds Liste des destinataires
* @param donneesContexte Données de contexte pour le template
* @return La notification créée
*/
public NotificationDTO creerDepuisTemplate(
TypeNotification typeNotification,
List<String> destinatairesIds,
Map<String, Object> donneesContexte) {
LOG.debugf("Création de notification depuis template: %s", typeNotification);
// Création de la notification de base
NotificationDTO notification = new NotificationDTO();
notification.setId(UUID.randomUUID().toString());
notification.setTypeNotification(typeNotification);
notification.setDestinatairesIds(destinatairesIds);
notification.setDonneesPersonnalisees(donneesContexte);
// Application du template
return appliquerTemplate(notification);
}
// === MÉTHODES PRIVÉES ===
/**
* Obtient le template pour un type de notification
*/
private NotificationTemplate obtenirTemplate(TypeNotification type) {
return templatesCache.computeIfAbsent(type, this::chargerTemplate);
}
/**
* Charge un template depuis la configuration
*/
private NotificationTemplate chargerTemplate(TypeNotification type) {
// Dans un vrai projet, les templates seraient stockés en base de données
// ou dans des fichiers de configuration. Ici, nous les définissons en dur.
return switch (type) {
case NOUVEL_EVENEMENT -> creerTemplateNouvelEvenement();
case RAPPEL_EVENEMENT -> creerTemplateRappelEvenement();
case COTISATION_DUE -> creerTemplateCotisationDue();
case COTISATION_PAYEE -> creerTemplateCotisationPayee();
case NOUVELLE_DEMANDE_AIDE -> creerTemplateNouvelleDemandeAide();
case NOUVEAU_MEMBRE -> creerTemplateNouveauMembre();
case ANNIVERSAIRE_MEMBRE -> creerTemplateAnniversaireMembre();
case ANNONCE_GENERALE -> creerTemplateAnnonceGenerale();
case MESSAGE_PRIVE -> creerTemplateMessagePrive();
default -> creerTemplateDefaut(type);
};
}
/**
* Construit le contexte de variables pour le template
*/
private Map<String, Object> construireContexte(NotificationDTO notification) {
Map<String, Object> contexte = new HashMap<>();
// Variables de base
contexte.put("notification_id", notification.getId());
contexte.put("type", notification.getTypeNotification().getLibelle());
contexte.put("date_creation", DATE_FORMATTER.format(notification.getDateCreation()));
contexte.put("datetime_creation", DATETIME_FORMATTER.format(notification.getDateCreation()));
contexte.put("heure_creation", TIME_FORMATTER.format(notification.getDateCreation()));
// Variables de l'expéditeur
if (notification.getExpediteurId() != null) {
try {
// Récupération des informations de l'expéditeur
var expediteur = membreService.obtenirMembre(notification.getExpediteurId());
if (expediteur != null) {
contexte.put("expediteur_nom", expediteur.getNom());
contexte.put("expediteur_prenom", expediteur.getPrenom());
contexte.put("expediteur_nom_complet", expediteur.getNom() + " " + expediteur.getPrenom());
}
} catch (Exception e) {
LOG.warnf("Impossible de récupérer les infos de l'expéditeur: %s", notification.getExpediteurId());
}
}
// Variables de l'organisation
if (notification.getOrganisationId() != null) {
try {
var organisation = organisationService.obtenirOrganisation(notification.getOrganisationId());
if (organisation != null) {
contexte.put("organisation_nom", organisation.getNom());
contexte.put("organisation_ville", organisation.getVille());
}
} catch (Exception e) {
LOG.warnf("Impossible de récupérer les infos de l'organisation: %s", notification.getOrganisationId());
}
}
// Variables des données personnalisées
if (notification.getDonneesPersonnalisees() != null) {
notification.getDonneesPersonnalisees().forEach((key, value) -> {
contexte.put(key, value);
// Formatage spécial pour les dates
if (value instanceof LocalDateTime) {
LocalDateTime dateTime = (LocalDateTime) value;
contexte.put(key + "_date", DATE_FORMATTER.format(dateTime));
contexte.put(key + "_datetime", DATETIME_FORMATTER.format(dateTime));
contexte.put(key + "_heure", TIME_FORMATTER.format(dateTime));
}
});
}
// Variables calculées
contexte.put("nombre_destinataires", notification.getDestinatairesIds().size());
contexte.put("est_groupe", notification.getDestinatairesIds().size() > 1);
return contexte;
}
/**
* Applique les variables à un template de texte
*/
private String appliquerVariables(String template, Map<String, Object> contexte) {
if (template == null) return null;
Matcher matcher = VARIABLE_PATTERN.matcher(template);
StringBuffer result = new StringBuffer();
while (matcher.find()) {
String variableName = matcher.group(1).trim();
Object value = contexte.get(variableName);
String replacement;
if (value != null) {
replacement = String.valueOf(value);
} else {
// Variable non trouvée, on garde la variable originale
replacement = "{{" + variableName + "}}";
LOG.warnf("Variable non trouvée dans le contexte: %s", variableName);
}
matcher.appendReplacement(result, Matcher.quoteReplacement(replacement));
}
matcher.appendTail(result);
return result.toString();
}
// === TEMPLATES PRÉDÉFINIS ===
private NotificationTemplate creerTemplateNouvelEvenement() {
NotificationTemplate template = new NotificationTemplate();
template.setTitreTemplate("Nouvel événement : {{evenement_titre}}");
template.setMessageTemplate("Un nouvel événement \"{{evenement_titre}}\" a été créé pour le {{evenement_date}}. Inscrivez-vous dès maintenant !");
template.setMessageCourtTemplate("Nouvel événement le {{evenement_date}}");
template.setImageUrl("{{evenement_image_url}}");
template.setActionClic("/evenements/{{evenement_id}}");
// Actions rapides
List<ActionNotificationDTO> actions = Arrays.asList(
new ActionNotificationDTO("voir", "Voir", "/evenements/{{evenement_id}}", "visibility"),
new ActionNotificationDTO("inscrire", "S'inscrire", "/evenements/{{evenement_id}}/inscription", "event_available")
);
template.setActionsRapides(actions);
return template;
}
private NotificationTemplate creerTemplateRappelEvenement() {
NotificationTemplate template = new NotificationTemplate();
template.setTitreTemplate("Rappel : {{evenement_titre}}");
template.setMessageTemplate("N'oubliez pas l'événement \"{{evenement_titre}}\" qui aura lieu {{evenement_date}} à {{evenement_heure}}.");
template.setMessageCourtTemplate("Événement dans {{temps_restant}}");
template.setActionClic("/evenements/{{evenement_id}}");
List<ActionNotificationDTO> actions = Arrays.asList(
new ActionNotificationDTO("voir", "Voir", "/evenements/{{evenement_id}}", "visibility"),
new ActionNotificationDTO("itineraire", "Itinéraire", "geo:{{evenement_latitude}},{{evenement_longitude}}", "directions")
);
template.setActionsRapides(actions);
return template;
}
private NotificationTemplate creerTemplateCotisationDue() {
NotificationTemplate template = new NotificationTemplate();
template.setTitreTemplate("Cotisation due");
template.setMessageTemplate("Votre cotisation de {{montant}} FCFA est due. Échéance : {{date_echeance}}");
template.setMessageCourtTemplate("Cotisation {{montant}} FCFA due");
template.setActionClic("/cotisations/payer/{{cotisation_id}}");
List<ActionNotificationDTO> actions = Arrays.asList(
new ActionNotificationDTO("payer", "Payer maintenant", "/cotisations/payer/{{cotisation_id}}", "payment"),
new ActionNotificationDTO("reporter", "Reporter", "/cotisations/reporter/{{cotisation_id}}", "schedule")
);
template.setActionsRapides(actions);
return template;
}
private NotificationTemplate creerTemplateCotisationPayee() {
NotificationTemplate template = new NotificationTemplate();
template.setTitreTemplate("Cotisation payée ✓");
template.setMessageTemplate("Votre cotisation de {{montant}} FCFA a été payée avec succès. Merci !");
template.setMessageCourtTemplate("Paiement {{montant}} FCFA confirmé");
template.setActionClic("/cotisations/recu/{{cotisation_id}}");
List<ActionNotificationDTO> actions = Arrays.asList(
new ActionNotificationDTO("recu", "Voir le reçu", "/cotisations/recu/{{cotisation_id}}", "receipt")
);
template.setActionsRapides(actions);
return template;
}
private NotificationTemplate creerTemplateNouvelleDemandeAide() {
NotificationTemplate template = new NotificationTemplate();
template.setTitreTemplate("Nouvelle demande d'aide");
template.setMessageTemplate("{{demandeur_nom}} a fait une demande d'aide : {{demande_titre}}");
template.setMessageCourtTemplate("Demande d'aide de {{demandeur_nom}}");
template.setActionClic("/solidarite/demandes/{{demande_id}}");
List<ActionNotificationDTO> actions = Arrays.asList(
new ActionNotificationDTO("voir", "Voir", "/solidarite/demandes/{{demande_id}}", "visibility"),
new ActionNotificationDTO("aider", "Proposer aide", "/solidarite/demandes/{{demande_id}}/aider", "volunteer_activism")
);
template.setActionsRapides(actions);
return template;
}
private NotificationTemplate creerTemplateNouveauMembre() {
NotificationTemplate template = new NotificationTemplate();
template.setTitreTemplate("Nouveau membre");
template.setMessageTemplate("{{membre_nom}} {{membre_prenom}} a rejoint notre organisation. Souhaitons-lui la bienvenue !");
template.setMessageCourtTemplate("{{membre_nom}} a rejoint l'organisation");
template.setActionClic("/membres/{{membre_id}}");
List<ActionNotificationDTO> actions = Arrays.asList(
new ActionNotificationDTO("voir", "Voir profil", "/membres/{{membre_id}}", "person"),
new ActionNotificationDTO("message", "Envoyer message", "/messages/nouveau/{{membre_id}}", "message")
);
template.setActionsRapides(actions);
return template;
}
private NotificationTemplate creerTemplateAnniversaireMembre() {
NotificationTemplate template = new NotificationTemplate();
template.setTitreTemplate("Joyeux anniversaire ! 🎉");
template.setMessageTemplate("C'est l'anniversaire de {{membre_nom}} {{membre_prenom}} aujourd'hui ! Souhaitons-lui un joyeux anniversaire.");
template.setMessageCourtTemplate("Anniversaire de {{membre_nom}}");
template.setActionClic("/membres/{{membre_id}}");
List<ActionNotificationDTO> actions = Arrays.asList(
new ActionNotificationDTO("feliciter", "Féliciter", "/membres/{{membre_id}}/feliciter", "cake"),
new ActionNotificationDTO("appeler", "Appeler", "tel:{{membre_telephone}}", "phone")
);
template.setActionsRapides(actions);
return template;
}
private NotificationTemplate creerTemplateAnnonceGenerale() {
NotificationTemplate template = new NotificationTemplate();
template.setTitreTemplate("{{annonce_titre}}");
template.setMessageTemplate("{{annonce_contenu}}");
template.setMessageCourtTemplate("Nouvelle annonce");
template.setActionClic("/annonces/{{annonce_id}}");
return template;
}
private NotificationTemplate creerTemplateMessagePrive() {
NotificationTemplate template = new NotificationTemplate();
template.setTitreTemplate("Message de {{expediteur_nom}}");
template.setMessageTemplate("{{message_contenu}}");
template.setMessageCourtTemplate("Nouveau message privé");
template.setActionClic("/messages/{{message_id}}");
List<ActionNotificationDTO> actions = Arrays.asList(
new ActionNotificationDTO("repondre", "Répondre", "/messages/repondre/{{message_id}}", "reply"),
new ActionNotificationDTO("voir", "Voir", "/messages/{{message_id}}", "visibility")
);
template.setActionsRapides(actions);
return template;
}
private NotificationTemplate creerTemplateDefaut(TypeNotification type) {
NotificationTemplate template = new NotificationTemplate();
template.setTitreTemplate(type.getLibelle());
template.setMessageTemplate("{{message}}");
template.setMessageCourtTemplate("{{message_court}}");
return template;
}
// === CLASSE INTERNE POUR LES TEMPLATES ===
private static class NotificationTemplate {
private String titreTemplate;
private String messageTemplate;
private String messageCourtTemplate;
private String imageUrl;
private String iconeUrl;
private String actionClic;
private List<ActionNotificationDTO> actionsRapides;
private Map<String, Object> donneesPersonnalisees;
// Getters et setters
public String getTitreTemplate() { return titreTemplate; }
public void setTitreTemplate(String titreTemplate) { this.titreTemplate = titreTemplate; }
public String getMessageTemplate() { return messageTemplate; }
public void setMessageTemplate(String messageTemplate) { this.messageTemplate = messageTemplate; }
public String getMessageCourtTemplate() { return messageCourtTemplate; }
public void setMessageCourtTemplate(String messageCourtTemplate) { this.messageCourtTemplate = messageCourtTemplate; }
public String getImageUrl() { return imageUrl; }
public void setImageUrl(String imageUrl) { this.imageUrl = imageUrl; }
public String getIconeUrl() { return iconeUrl; }
public void setIconeUrl(String iconeUrl) { this.iconeUrl = iconeUrl; }
public String getActionClic() { return actionClic; }
public void setActionClic(String actionClic) { this.actionClic = actionClic; }
public List<ActionNotificationDTO> getActionsRapides() { return actionsRapides; }
public void setActionsRapides(List<ActionNotificationDTO> actionsRapides) { this.actionsRapides = actionsRapides; }
public Map<String, Object> getDonneesPersonnalisees() { return donneesPersonnalisees; }
public void setDonneesPersonnalisees(Map<String, Object> donneesPersonnalisees) {
this.donneesPersonnalisees = donneesPersonnalisees;
}
}
}

View File

@@ -0,0 +1,441 @@
package dev.lions.unionflow.server.service;
import dev.lions.unionflow.server.api.dto.solidarite.PropositionAideDTO;
import dev.lions.unionflow.server.api.dto.solidarite.DemandeAideDTO;
import dev.lions.unionflow.server.api.enums.solidarite.TypeAide;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.NotBlank;
import org.jboss.logging.Logger;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
/**
* Service spécialisé pour la gestion des propositions d'aide
*
* Ce service gère le cycle de vie des propositions d'aide :
* création, activation, matching, suivi des performances.
*
* @author UnionFlow Team
* @version 1.0
* @since 2025-01-16
*/
@ApplicationScoped
public class PropositionAideService {
private static final Logger LOG = Logger.getLogger(PropositionAideService.class);
// Cache pour les propositions actives
private final Map<String, PropositionAideDTO> cachePropositionsActives = new HashMap<>();
private final Map<TypeAide, List<PropositionAideDTO>> indexParType = new HashMap<>();
// === OPÉRATIONS CRUD ===
/**
* Crée une nouvelle proposition d'aide
*
* @param propositionDTO La proposition à créer
* @return La proposition créée avec ID généré
*/
@Transactional
public PropositionAideDTO creerProposition(@Valid PropositionAideDTO propositionDTO) {
LOG.infof("Création d'une nouvelle proposition d'aide: %s", propositionDTO.getTitre());
// Génération des identifiants
propositionDTO.setId(UUID.randomUUID().toString());
propositionDTO.setNumeroReference(genererNumeroReference());
// Initialisation des dates
LocalDateTime maintenant = LocalDateTime.now();
propositionDTO.setDateCreation(maintenant);
propositionDTO.setDateModification(maintenant);
// Statut initial
if (propositionDTO.getStatut() == null) {
propositionDTO.setStatut(PropositionAideDTO.StatutProposition.ACTIVE);
}
// Calcul de la date d'expiration si non définie
if (propositionDTO.getDateExpiration() == null) {
propositionDTO.setDateExpiration(maintenant.plusMonths(6)); // 6 mois par défaut
}
// Initialisation des compteurs
propositionDTO.setNombreDemandesTraitees(0);
propositionDTO.setNombreBeneficiairesAides(0);
propositionDTO.setMontantTotalVerse(0.0);
propositionDTO.setNombreVues(0);
propositionDTO.setNombreCandidatures(0);
propositionDTO.setNombreEvaluations(0);
// Calcul du score de pertinence initial
propositionDTO.setScorePertinence(calculerScorePertinence(propositionDTO));
// Ajout au cache et index
ajouterAuCache(propositionDTO);
ajouterAIndex(propositionDTO);
LOG.infof("Proposition d'aide créée avec succès: %s", propositionDTO.getId());
return propositionDTO;
}
/**
* Met à jour une proposition d'aide existante
*
* @param propositionDTO La proposition à mettre à jour
* @return La proposition mise à jour
*/
@Transactional
public PropositionAideDTO mettreAJour(@Valid PropositionAideDTO propositionDTO) {
LOG.infof("Mise à jour de la proposition d'aide: %s", propositionDTO.getId());
// Mise à jour de la date de modification
propositionDTO.setDateModification(LocalDateTime.now());
// Recalcul du score de pertinence
propositionDTO.setScorePertinence(calculerScorePertinence(propositionDTO));
// Mise à jour du cache et index
ajouterAuCache(propositionDTO);
mettreAJourIndex(propositionDTO);
LOG.infof("Proposition d'aide mise à jour avec succès: %s", propositionDTO.getId());
return propositionDTO;
}
/**
* Obtient une proposition d'aide par son ID
*
* @param id ID de la proposition
* @return La proposition trouvée
*/
public PropositionAideDTO obtenirParId(@NotBlank String id) {
LOG.debugf("Récupération de la proposition d'aide: %s", id);
// Vérification du cache
PropositionAideDTO propositionCachee = cachePropositionsActives.get(id);
if (propositionCachee != null) {
// Incrémenter le nombre de vues
propositionCachee.setNombreVues(propositionCachee.getNombreVues() + 1);
return propositionCachee;
}
// Simulation de récupération depuis la base de données
PropositionAideDTO proposition = simulerRecuperationBDD(id);
if (proposition != null) {
ajouterAuCache(proposition);
ajouterAIndex(proposition);
}
return proposition;
}
/**
* Active ou désactive une proposition d'aide
*
* @param propositionId ID de la proposition
* @param activer true pour activer, false pour désactiver
* @return La proposition mise à jour
*/
@Transactional
public PropositionAideDTO changerStatutActivation(@NotBlank String propositionId, boolean activer) {
LOG.infof("Changement de statut d'activation pour la proposition %s: %s",
propositionId, activer ? "ACTIVE" : "SUSPENDUE");
PropositionAideDTO proposition = obtenirParId(propositionId);
if (proposition == null) {
throw new IllegalArgumentException("Proposition non trouvée: " + propositionId);
}
if (activer) {
// Vérifications avant activation
if (proposition.isExpiree()) {
throw new IllegalStateException("Impossible d'activer une proposition expirée");
}
proposition.setStatut(PropositionAideDTO.StatutProposition.ACTIVE);
proposition.setEstDisponible(true);
} else {
proposition.setStatut(PropositionAideDTO.StatutProposition.SUSPENDUE);
proposition.setEstDisponible(false);
}
proposition.setDateModification(LocalDateTime.now());
// Mise à jour du cache et index
ajouterAuCache(proposition);
mettreAJourIndex(proposition);
return proposition;
}
// === RECHERCHE ET MATCHING ===
/**
* Recherche des propositions compatibles avec une demande
*
* @param demande La demande d'aide
* @return Liste des propositions compatibles triées par score
*/
public List<PropositionAideDTO> rechercherPropositionsCompatibles(DemandeAideDTO demande) {
LOG.debugf("Recherche de propositions compatibles pour la demande: %s", demande.getId());
// Recherche par type d'aide d'abord
List<PropositionAideDTO> candidats = indexParType.getOrDefault(demande.getTypeAide(),
new ArrayList<>());
// Si pas de correspondance exacte, chercher dans la même catégorie
if (candidats.isEmpty()) {
candidats = cachePropositionsActives.values().stream()
.filter(p -> p.getTypeAide().getCategorie().equals(demande.getTypeAide().getCategorie()))
.collect(Collectors.toList());
}
// Filtrage et scoring
return candidats.stream()
.filter(PropositionAideDTO::isActiveEtDisponible)
.filter(p -> p.peutAccepterBeneficiaires())
.map(p -> {
double score = p.getScoreCompatibilite(demande);
// Stocker le score temporairement dans les données personnalisées
if (p.getDonneesPersonnalisees() == null) {
p.setDonneesPersonnalisees(new HashMap<>());
}
p.getDonneesPersonnalisees().put("scoreCompatibilite", score);
return p;
})
.filter(p -> (Double) p.getDonneesPersonnalisees().get("scoreCompatibilite") >= 30.0)
.sorted((p1, p2) -> {
Double score1 = (Double) p1.getDonneesPersonnalisees().get("scoreCompatibilite");
Double score2 = (Double) p2.getDonneesPersonnalisees().get("scoreCompatibilite");
return Double.compare(score2, score1); // Ordre décroissant
})
.limit(10) // Limiter à 10 meilleures propositions
.collect(Collectors.toList());
}
/**
* Recherche des propositions par critères
*
* @param filtres Map des critères de recherche
* @return Liste des propositions correspondantes
*/
public List<PropositionAideDTO> rechercherAvecFiltres(Map<String, Object> filtres) {
LOG.debugf("Recherche de propositions avec filtres: %s", filtres);
return cachePropositionsActives.values().stream()
.filter(proposition -> correspondAuxFiltres(proposition, filtres))
.sorted(this::comparerParPertinence)
.collect(Collectors.toList());
}
/**
* Obtient les propositions actives pour un type d'aide
*
* @param typeAide Type d'aide recherché
* @return Liste des propositions actives
*/
public List<PropositionAideDTO> obtenirPropositionsActives(TypeAide typeAide) {
LOG.debugf("Récupération des propositions actives pour le type: %s", typeAide);
return indexParType.getOrDefault(typeAide, new ArrayList<>()).stream()
.filter(PropositionAideDTO::isActiveEtDisponible)
.sorted(this::comparerParPertinence)
.collect(Collectors.toList());
}
/**
* Obtient les meilleures propositions (top performers)
*
* @param limite Nombre maximum de propositions à retourner
* @return Liste des meilleures propositions
*/
public List<PropositionAideDTO> obtenirMeilleuresPropositions(int limite) {
LOG.debugf("Récupération des %d meilleures propositions", limite);
return cachePropositionsActives.values().stream()
.filter(PropositionAideDTO::isActiveEtDisponible)
.filter(p -> p.getNombreEvaluations() >= 3) // Au moins 3 évaluations
.filter(p -> p.getNoteMoyenne() != null && p.getNoteMoyenne() >= 4.0)
.sorted((p1, p2) -> {
// Tri par note moyenne puis par nombre d'aides réalisées
int compareNote = Double.compare(p2.getNoteMoyenne(), p1.getNoteMoyenne());
if (compareNote != 0) return compareNote;
return Integer.compare(p2.getNombreBeneficiairesAides(), p1.getNombreBeneficiairesAides());
})
.limit(limite)
.collect(Collectors.toList());
}
// === GESTION DES PERFORMANCES ===
/**
* Met à jour les statistiques d'une proposition après une aide fournie
*
* @param propositionId ID de la proposition
* @param montantVerse Montant versé (si applicable)
* @param nombreBeneficiaires Nombre de bénéficiaires aidés
* @return La proposition mise à jour
*/
@Transactional
public PropositionAideDTO mettreAJourStatistiques(@NotBlank String propositionId,
Double montantVerse,
int nombreBeneficiaires) {
LOG.infof("Mise à jour des statistiques pour la proposition: %s", propositionId);
PropositionAideDTO proposition = obtenirParId(propositionId);
if (proposition == null) {
throw new IllegalArgumentException("Proposition non trouvée: " + propositionId);
}
// Mise à jour des compteurs
proposition.setNombreDemandesTraitees(proposition.getNombreDemandesTraitees() + 1);
proposition.setNombreBeneficiairesAides(proposition.getNombreBeneficiairesAides() + nombreBeneficiaires);
if (montantVerse != null) {
proposition.setMontantTotalVerse(proposition.getMontantTotalVerse() + montantVerse);
}
// Recalcul du score de pertinence
proposition.setScorePertinence(calculerScorePertinence(proposition));
// Vérification si la capacité maximale est atteinte
if (proposition.getNombreBeneficiairesAides() >= proposition.getNombreMaxBeneficiaires()) {
proposition.setEstDisponible(false);
proposition.setStatut(PropositionAideDTO.StatutProposition.TERMINEE);
}
proposition.setDateModification(LocalDateTime.now());
// Mise à jour du cache
ajouterAuCache(proposition);
return proposition;
}
// === MÉTHODES UTILITAIRES PRIVÉES ===
/**
* Génère un numéro de référence unique pour les propositions
*/
private String genererNumeroReference() {
int annee = LocalDateTime.now().getYear();
int numero = (int) (Math.random() * 999999) + 1;
return String.format("PA-%04d-%06d", annee, numero);
}
/**
* Calcule le score de pertinence d'une proposition
*/
private double calculerScorePertinence(PropositionAideDTO proposition) {
double score = 50.0; // Score de base
// Bonus pour l'expérience (nombre d'aides réalisées)
score += Math.min(20.0, proposition.getNombreBeneficiairesAides() * 2.0);
// Bonus pour la note moyenne
if (proposition.getNoteMoyenne() != null) {
score += (proposition.getNoteMoyenne() - 3.0) * 10.0; // +10 par point au-dessus de 3
}
// Bonus pour la récence
long joursDepuisCreation = java.time.Duration.between(
proposition.getDateCreation(), LocalDateTime.now()).toDays();
if (joursDepuisCreation <= 30) {
score += 10.0;
} else if (joursDepuisCreation <= 90) {
score += 5.0;
}
// Bonus pour la disponibilité
if (proposition.isActiveEtDisponible()) {
score += 15.0;
}
// Malus pour l'inactivité
if (proposition.getNombreVues() == 0) {
score -= 10.0;
}
return Math.max(0.0, Math.min(100.0, score));
}
/**
* Vérifie si une proposition correspond aux filtres
*/
private boolean correspondAuxFiltres(PropositionAideDTO proposition, Map<String, Object> filtres) {
for (Map.Entry<String, Object> filtre : filtres.entrySet()) {
String cle = filtre.getKey();
Object valeur = filtre.getValue();
switch (cle) {
case "typeAide" -> {
if (!proposition.getTypeAide().equals(valeur)) return false;
}
case "statut" -> {
if (!proposition.getStatut().equals(valeur)) return false;
}
case "proposantId" -> {
if (!proposition.getProposantId().equals(valeur)) return false;
}
case "organisationId" -> {
if (!proposition.getOrganisationId().equals(valeur)) return false;
}
case "estDisponible" -> {
if (!proposition.getEstDisponible().equals(valeur)) return false;
}
case "montantMaximum" -> {
if (proposition.getMontantMaximum() == null ||
proposition.getMontantMaximum() < (Double) valeur) return false;
}
}
}
return true;
}
/**
* Compare deux propositions par pertinence
*/
private int comparerParPertinence(PropositionAideDTO p1, PropositionAideDTO p2) {
// D'abord par score de pertinence (plus haut = meilleur)
int compareScore = Double.compare(p2.getScorePertinence(), p1.getScorePertinence());
if (compareScore != 0) return compareScore;
// Puis par date de création (plus récent = meilleur)
return p2.getDateCreation().compareTo(p1.getDateCreation());
}
// === GESTION DU CACHE ET INDEX ===
private void ajouterAuCache(PropositionAideDTO proposition) {
cachePropositionsActives.put(proposition.getId(), proposition);
}
private void ajouterAIndex(PropositionAideDTO proposition) {
indexParType.computeIfAbsent(proposition.getTypeAide(), k -> new ArrayList<>())
.add(proposition);
}
private void mettreAJourIndex(PropositionAideDTO proposition) {
// Supprimer de tous les index
indexParType.values().forEach(liste -> liste.removeIf(p -> p.getId().equals(proposition.getId())));
// Ré-ajouter si la proposition est active
if (proposition.isActiveEtDisponible()) {
ajouterAIndex(proposition);
}
}
// === MÉTHODES DE SIMULATION (À REMPLACER PAR DE VRAIS REPOSITORIES) ===
private PropositionAideDTO simulerRecuperationBDD(String id) {
// Simulation - dans une vraie implémentation, ceci ferait appel au repository
return null;
}
}

View File

@@ -0,0 +1,440 @@
package dev.lions.unionflow.server.service;
import dev.lions.unionflow.server.api.dto.solidarite.DemandeAideDTO;
import dev.lions.unionflow.server.api.dto.solidarite.PropositionAideDTO;
import dev.lions.unionflow.server.api.dto.solidarite.EvaluationAideDTO;
import dev.lions.unionflow.server.api.enums.solidarite.TypeAide;
import dev.lions.unionflow.server.api.enums.solidarite.StatutAide;
import dev.lions.unionflow.server.api.enums.solidarite.PrioriteAide;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.jboss.logging.Logger;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.stream.Collectors;
/**
* Service d'analytics spécialisé pour le système de solidarité
*
* Ce service calcule les métriques, statistiques et indicateurs
* de performance du système de solidarité.
*
* @author UnionFlow Team
* @version 1.0
* @since 2025-01-16
*/
@ApplicationScoped
public class SolidariteAnalyticsService {
private static final Logger LOG = Logger.getLogger(SolidariteAnalyticsService.class);
@Inject
DemandeAideService demandeAideService;
@Inject
PropositionAideService propositionAideService;
@Inject
EvaluationService evaluationService;
// Cache des statistiques calculées
private final Map<String, Map<String, Object>> cacheStatistiques = new HashMap<>();
private final Map<String, LocalDateTime> cacheTimestamps = new HashMap<>();
private static final long CACHE_DURATION_MINUTES = 30;
// === STATISTIQUES GÉNÉRALES ===
/**
* Calcule les statistiques générales de solidarité pour une organisation
*
* @param organisationId ID de l'organisation
* @return Map des statistiques
*/
public Map<String, Object> calculerStatistiquesSolidarite(String organisationId) {
LOG.infof("Calcul des statistiques de solidarité pour: %s", organisationId);
// Vérification du cache
String cacheKey = "stats_" + organisationId;
Map<String, Object> statsCache = obtenirDuCache(cacheKey);
if (statsCache != null) {
return statsCache;
}
try {
Map<String, Object> statistiques = new HashMap<>();
// 1. Statistiques des demandes
Map<String, Object> statsDemandes = calculerStatistiquesDemandes(organisationId);
statistiques.put("demandes", statsDemandes);
// 2. Statistiques des propositions
Map<String, Object> statsPropositions = calculerStatistiquesPropositions(organisationId);
statistiques.put("propositions", statsPropositions);
// 3. Statistiques financières
Map<String, Object> statsFinancieres = calculerStatistiquesFinancieres(organisationId);
statistiques.put("financier", statsFinancieres);
// 4. Indicateurs de performance
Map<String, Object> kpis = calculerKPIsSolidarite(organisationId);
statistiques.put("kpis", kpis);
// 5. Tendances
Map<String, Object> tendances = calculerTendances(organisationId);
statistiques.put("tendances", tendances);
// 6. Métadonnées
statistiques.put("dateCalcul", LocalDateTime.now());
statistiques.put("organisationId", organisationId);
// Mise en cache
ajouterAuCache(cacheKey, statistiques);
return statistiques;
} catch (Exception e) {
LOG.errorf(e, "Erreur lors du calcul des statistiques pour: %s", organisationId);
return new HashMap<>();
}
}
/**
* Calcule les statistiques des demandes d'aide
*/
private Map<String, Object> calculerStatistiquesDemandes(String organisationId) {
Map<String, Object> filtres = Map.of("organisationId", organisationId);
List<DemandeAideDTO> demandes = demandeAideService.rechercherAvecFiltres(filtres);
Map<String, Object> stats = new HashMap<>();
// Nombre total de demandes
stats.put("total", demandes.size());
// Répartition par statut
Map<StatutAide, Long> repartitionStatut = demandes.stream()
.collect(Collectors.groupingBy(DemandeAideDTO::getStatut, Collectors.counting()));
stats.put("parStatut", repartitionStatut);
// Répartition par type d'aide
Map<TypeAide, Long> repartitionType = demandes.stream()
.collect(Collectors.groupingBy(DemandeAideDTO::getTypeAide, Collectors.counting()));
stats.put("parType", repartitionType);
// Répartition par priorité
Map<PrioriteAide, Long> repartitionPriorite = demandes.stream()
.collect(Collectors.groupingBy(DemandeAideDTO::getPriorite, Collectors.counting()));
stats.put("parPriorite", repartitionPriorite);
// Demandes urgentes
long demandesUrgentes = demandes.stream()
.filter(DemandeAideDTO::isUrgente)
.count();
stats.put("urgentes", demandesUrgentes);
// Demandes en retard
long demandesEnRetard = demandes.stream()
.filter(DemandeAideDTO::isDelaiDepasse)
.filter(d -> !d.isTerminee())
.count();
stats.put("enRetard", demandesEnRetard);
// Taux d'approbation
long demandesEvaluees = demandes.stream()
.filter(d -> d.getStatut().isEstFinal())
.count();
long demandesApprouvees = demandes.stream()
.filter(d -> d.getStatut() == StatutAide.APPROUVEE ||
d.getStatut() == StatutAide.APPROUVEE_PARTIELLEMENT)
.count();
double tauxApprobation = demandesEvaluees > 0 ?
(demandesApprouvees * 100.0) / demandesEvaluees : 0.0;
stats.put("tauxApprobation", Math.round(tauxApprobation * 100.0) / 100.0);
// Délai moyen de traitement
double delaiMoyenHeures = demandes.stream()
.filter(d -> d.isTerminee())
.mapToLong(DemandeAideDTO::getDureeTraitementJours)
.average()
.orElse(0.0) * 24; // Conversion en heures
stats.put("delaiMoyenTraitementHeures", Math.round(delaiMoyenHeures * 100.0) / 100.0);
return stats;
}
/**
* Calcule les statistiques des propositions d'aide
*/
private Map<String, Object> calculerStatistiquesPropositions(String organisationId) {
Map<String, Object> filtres = Map.of("organisationId", organisationId);
List<PropositionAideDTO> propositions = propositionAideService.rechercherAvecFiltres(filtres);
Map<String, Object> stats = new HashMap<>();
// Nombre total de propositions
stats.put("total", propositions.size());
// Propositions actives
long propositionsActives = propositions.stream()
.filter(PropositionAideDTO::isActiveEtDisponible)
.count();
stats.put("actives", propositionsActives);
// Répartition par type d'aide
Map<TypeAide, Long> repartitionType = propositions.stream()
.collect(Collectors.groupingBy(PropositionAideDTO::getTypeAide, Collectors.counting()));
stats.put("parType", repartitionType);
// Capacité totale disponible
int capaciteTotale = propositions.stream()
.filter(PropositionAideDTO::isActiveEtDisponible)
.mapToInt(PropositionAideDTO::getPlacesRestantes)
.sum();
stats.put("capaciteDisponible", capaciteTotale);
// Taux d'utilisation moyen
double tauxUtilisationMoyen = propositions.stream()
.filter(p -> p.getNombreMaxBeneficiaires() > 0)
.mapToDouble(PropositionAideDTO::getPourcentageCapaciteUtilisee)
.average()
.orElse(0.0);
stats.put("tauxUtilisationMoyen", Math.round(tauxUtilisationMoyen * 100.0) / 100.0);
// Note moyenne des propositions
double noteMoyenne = propositions.stream()
.filter(p -> p.getNoteMoyenne() != null)
.mapToDouble(PropositionAideDTO::getNoteMoyenne)
.average()
.orElse(0.0);
stats.put("noteMoyenne", Math.round(noteMoyenne * 100.0) / 100.0);
return stats;
}
/**
* Calcule les statistiques financières
*/
private Map<String, Object> calculerStatistiquesFinancieres(String organisationId) {
Map<String, Object> filtres = Map.of("organisationId", organisationId);
List<DemandeAideDTO> demandes = demandeAideService.rechercherAvecFiltres(filtres);
List<PropositionAideDTO> propositions = propositionAideService.rechercherAvecFiltres(filtres);
Map<String, Object> stats = new HashMap<>();
// Montant total demandé
double montantTotalDemande = demandes.stream()
.filter(d -> d.getMontantDemande() != null)
.mapToDouble(DemandeAideDTO::getMontantDemande)
.sum();
stats.put("montantTotalDemande", montantTotalDemande);
// Montant total approuvé
double montantTotalApprouve = demandes.stream()
.filter(d -> d.getMontantApprouve() != null)
.mapToDouble(DemandeAideDTO::getMontantApprouve)
.sum();
stats.put("montantTotalApprouve", montantTotalApprouve);
// Montant total versé
double montantTotalVerse = demandes.stream()
.filter(d -> d.getMontantVerse() != null)
.mapToDouble(DemandeAideDTO::getMontantVerse)
.sum();
stats.put("montantTotalVerse", montantTotalVerse);
// Capacité financière disponible (propositions)
double capaciteFinanciere = propositions.stream()
.filter(p -> p.getMontantMaximum() != null)
.filter(PropositionAideDTO::isActiveEtDisponible)
.mapToDouble(PropositionAideDTO::getMontantMaximum)
.sum();
stats.put("capaciteFinanciereDisponible", capaciteFinanciere);
// Montant moyen par demande
double montantMoyenDemande = demandes.stream()
.filter(d -> d.getMontantDemande() != null)
.mapToDouble(DemandeAideDTO::getMontantDemande)
.average()
.orElse(0.0);
stats.put("montantMoyenDemande", Math.round(montantMoyenDemande * 100.0) / 100.0);
// Taux de versement
double tauxVersement = montantTotalApprouve > 0 ?
(montantTotalVerse * 100.0) / montantTotalApprouve : 0.0;
stats.put("tauxVersement", Math.round(tauxVersement * 100.0) / 100.0);
return stats;
}
/**
* Calcule les KPIs de solidarité
*/
private Map<String, Object> calculerKPIsSolidarite(String organisationId) {
Map<String, Object> kpis = new HashMap<>();
// Simulation de calculs KPI - dans une vraie implémentation,
// ces calculs seraient plus complexes et basés sur des données historiques
// Efficacité du matching
kpis.put("efficaciteMatching", 78.5); // Pourcentage de demandes matchées avec succès
// Temps de réponse moyen
kpis.put("tempsReponseMoyenHeures", 24.3);
// Satisfaction globale
kpis.put("satisfactionGlobale", 4.2); // Sur 5
// Taux de résolution
kpis.put("tauxResolution", 85.7); // Pourcentage de demandes résolues
// Impact social (nombre de personnes aidées)
kpis.put("personnesAidees", 156);
// Engagement communautaire
kpis.put("engagementCommunautaire", 67.8); // Pourcentage de membres actifs
return kpis;
}
/**
* Calcule les tendances sur les 30 derniers jours
*/
private Map<String, Object> calculerTendances(String organisationId) {
Map<String, Object> tendances = new HashMap<>();
// Simulation de calculs de tendances
// Dans une vraie implémentation, on comparerait avec la période précédente
tendances.put("evolutionDemandes", "+12.5%"); // Évolution du nombre de demandes
tendances.put("evolutionPropositions", "+8.3%"); // Évolution du nombre de propositions
tendances.put("evolutionMontants", "+15.7%"); // Évolution des montants
tendances.put("evolutionSatisfaction", "+2.1%"); // Évolution de la satisfaction
// Prédictions pour le mois prochain
Map<String, Object> predictions = new HashMap<>();
predictions.put("demandesPrevues", 45);
predictions.put("montantPrevu", 125000.0);
predictions.put("capaciteRequise", 38);
tendances.put("predictions", predictions);
return tendances;
}
// === ENREGISTREMENT D'ÉVÉNEMENTS ===
/**
* Enregistre une nouvelle demande pour les analytics
*
* @param demande La nouvelle demande
*/
public void enregistrerNouvelledemande(DemandeAideDTO demande) {
LOG.debugf("Enregistrement d'une nouvelle demande pour analytics: %s", demande.getId());
try {
// Invalidation du cache pour forcer le recalcul
invaliderCache(demande.getOrganisationId());
// Dans une vraie implémentation, on enregistrerait l'événement
// dans une base de données d'événements ou un système de métriques
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de l'enregistrement de la nouvelle demande: %s", demande.getId());
}
}
/**
* Enregistre une nouvelle proposition pour les analytics
*
* @param proposition La nouvelle proposition
*/
public void enregistrerNouvelleProposition(PropositionAideDTO proposition) {
LOG.debugf("Enregistrement d'une nouvelle proposition pour analytics: %s", proposition.getId());
try {
invaliderCache(proposition.getOrganisationId());
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de l'enregistrement de la nouvelle proposition: %s",
proposition.getId());
}
}
/**
* Enregistre l'évaluation d'une demande
*
* @param demande La demande évaluée
*/
public void enregistrerEvaluationDemande(DemandeAideDTO demande) {
LOG.debugf("Enregistrement de l'évaluation pour analytics: %s", demande.getId());
try {
invaliderCache(demande.getOrganisationId());
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de l'enregistrement de l'évaluation: %s", demande.getId());
}
}
/**
* Enregistre le rejet d'une demande avec motif
*
* @param demande La demande rejetée
* @param motif Le motif de rejet
*/
public void enregistrerRejetDemande(DemandeAideDTO demande, String motif) {
LOG.debugf("Enregistrement du rejet pour analytics: %s - motif: %s", demande.getId(), motif);
try {
invaliderCache(demande.getOrganisationId());
// Dans une vraie implémentation, on analyserait les motifs de rejet
// pour identifier les problèmes récurrents
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de l'enregistrement du rejet: %s", demande.getId());
}
}
// === GESTION DU CACHE ===
private Map<String, Object> obtenirDuCache(String cacheKey) {
LocalDateTime timestamp = cacheTimestamps.get(cacheKey);
if (timestamp == null) return null;
// Vérification de l'expiration
if (LocalDateTime.now().minusMinutes(CACHE_DURATION_MINUTES).isAfter(timestamp)) {
cacheStatistiques.remove(cacheKey);
cacheTimestamps.remove(cacheKey);
return null;
}
return cacheStatistiques.get(cacheKey);
}
private void ajouterAuCache(String cacheKey, Map<String, Object> statistiques) {
cacheStatistiques.put(cacheKey, statistiques);
cacheTimestamps.put(cacheKey, LocalDateTime.now());
// Nettoyage du cache si trop volumineux
if (cacheStatistiques.size() > 50) {
nettoyerCache();
}
}
private void invaliderCache(String organisationId) {
String cacheKey = "stats_" + organisationId;
cacheStatistiques.remove(cacheKey);
cacheTimestamps.remove(cacheKey);
}
private void nettoyerCache() {
LocalDateTime limite = LocalDateTime.now().minusMinutes(CACHE_DURATION_MINUTES);
cacheTimestamps.entrySet().removeIf(entry -> entry.getValue().isBefore(limite));
cacheStatistiques.keySet().retainAll(cacheTimestamps.keySet());
}
}

View File

@@ -0,0 +1,610 @@
package dev.lions.unionflow.server.service;
import dev.lions.unionflow.server.api.dto.solidarite.DemandeAideDTO;
import dev.lions.unionflow.server.api.dto.solidarite.PropositionAideDTO;
import dev.lions.unionflow.server.api.dto.solidarite.EvaluationAideDTO;
import dev.lions.unionflow.server.api.enums.solidarite.TypeAide;
import dev.lions.unionflow.server.api.enums.solidarite.StatutAide;
import dev.lions.unionflow.server.api.enums.solidarite.PrioriteAide;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.NotBlank;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.jboss.logging.Logger;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
/**
* Service principal de gestion de la solidarité UnionFlow
*
* Ce service orchestre toutes les opérations liées au système de solidarité :
* demandes d'aide, propositions d'aide, matching, évaluations et suivi.
*
* @author UnionFlow Team
* @version 1.0
* @since 2025-01-16
*/
@ApplicationScoped
public class SolidariteService {
private static final Logger LOG = Logger.getLogger(SolidariteService.class);
@Inject
DemandeAideService demandeAideService;
@Inject
PropositionAideService propositionAideService;
@Inject
MatchingService matchingService;
@Inject
EvaluationService evaluationService;
@Inject
NotificationSolidariteService notificationService;
@Inject
SolidariteAnalyticsService analyticsService;
@ConfigProperty(name = "unionflow.solidarite.auto-matching.enabled", defaultValue = "true")
boolean autoMatchingEnabled;
@ConfigProperty(name = "unionflow.solidarite.notification.enabled", defaultValue = "true")
boolean notificationEnabled;
@ConfigProperty(name = "unionflow.solidarite.evaluation.obligatoire", defaultValue = "false")
boolean evaluationObligatoire;
// === GESTION DES DEMANDES D'AIDE ===
/**
* Crée une nouvelle demande d'aide
*
* @param demandeDTO La demande d'aide à créer
* @return La demande d'aide créée
*/
@Transactional
public CompletableFuture<DemandeAideDTO> creerDemandeAide(@Valid DemandeAideDTO demandeDTO) {
LOG.infof("Création d'une nouvelle demande d'aide: %s", demandeDTO.getTitre());
return CompletableFuture.supplyAsync(() -> {
try {
// 1. Créer la demande
DemandeAideDTO demandeCree = demandeAideService.creerDemande(demandeDTO);
// 2. Calcul automatique de la priorité si non définie
if (demandeCree.getPriorite() == null) {
PrioriteAide prioriteCalculee = PrioriteAide.determinerPriorite(demandeCree.getTypeAide());
demandeCree.setPriorite(prioriteCalculee);
demandeCree = demandeAideService.mettreAJour(demandeCree);
}
// 3. Matching automatique si activé
if (autoMatchingEnabled) {
CompletableFuture.runAsync(() -> {
try {
List<PropositionAideDTO> propositionsCompatibles =
matchingService.trouverPropositionsCompatibles(demandeCree);
if (!propositionsCompatibles.isEmpty()) {
LOG.infof("Trouvé %d propositions compatibles pour la demande %s",
propositionsCompatibles.size(), demandeCree.getId());
// Notification aux proposants
if (notificationEnabled) {
notificationService.notifierProposantsCompatibles(
demandeCree, propositionsCompatibles);
}
}
} catch (Exception e) {
LOG.errorf(e, "Erreur lors du matching automatique pour la demande %s",
demandeCree.getId());
}
});
}
// 4. Notifications
if (notificationEnabled) {
notificationService.notifierCreationDemande(demandeCree);
// Notification d'urgence si priorité critique
if (demandeCree.getPriorite() == PrioriteAide.CRITIQUE) {
notificationService.notifierUrgenceCritique(demandeCree);
}
}
// 5. Mise à jour des analytics
analyticsService.enregistrerNouvelledemande(demandeCree);
LOG.infof("Demande d'aide créée avec succès: %s", demandeCree.getId());
return demandeCree;
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la création de la demande d'aide");
throw new RuntimeException("Erreur lors de la création de la demande d'aide", e);
}
});
}
/**
* Soumet une demande d'aide de manière asynchrone (passage de brouillon à soumise)
*
* @param demandeId ID de la demande
* @return La demande soumise
*/
@Transactional
public CompletableFuture<DemandeAideDTO> soumettreDemandeeAsync(@NotBlank String demandeId) {
LOG.infof("Soumission de la demande d'aide: %s", demandeId);
return CompletableFuture.supplyAsync(() -> {
try {
// 1. Récupérer et valider la demande
DemandeAideDTO demande = demandeAideService.obtenirParId(demandeId);
if (demande.getStatut() != StatutAide.BROUILLON) {
throw new IllegalStateException("Seules les demandes en brouillon peuvent être soumises");
}
// 2. Validation complète de la demande
validerDemandeAvantSoumission(demande);
// 3. Changement de statut
demande = demandeAideService.changerStatut(demandeId, StatutAide.SOUMISE,
"Demande soumise par le demandeur");
// 4. Calcul de la date limite de traitement
LocalDateTime dateLimite = demande.getPriorite().getDateLimiteTraitement();
demande.setDateLimiteTraitement(dateLimite);
demande = demandeAideService.mettreAJour(demande);
// 5. Notifications
if (notificationEnabled) {
notificationService.notifierSoumissionDemande(demande);
notificationService.notifierEvaluateurs(demande);
}
// 6. Programmation des rappels automatiques
programmerRappelsAutomatiques(demande);
LOG.infof("Demande soumise avec succès: %s", demandeId);
return demande;
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la soumission de la demande: %s", demandeId);
throw new RuntimeException("Erreur lors de la soumission de la demande", e);
}
});
}
/**
* Évalue une demande d'aide
*
* @param demandeId ID de la demande
* @param decision Décision d'évaluation (APPROUVEE, REJETEE, etc.)
* @param commentaires Commentaires de l'évaluateur
* @param montantApprouve Montant approuvé (si différent du montant demandé)
* @return La demande évaluée
*/
@Transactional
public CompletableFuture<DemandeAideDTO> evaluerDemande(
@NotBlank String demandeId,
@NotNull StatutAide decision,
String commentaires,
Double montantApprouve) {
LOG.infof("Évaluation de la demande: %s avec décision: %s", demandeId, decision);
return CompletableFuture.supplyAsync(() -> {
try {
// 1. Récupérer la demande
DemandeAideDTO demande = demandeAideService.obtenirParId(demandeId);
// 2. Valider que la demande peut être évaluée
if (!demande.getStatut().peutTransitionnerVers(decision)) {
throw new IllegalStateException(
String.format("Transition invalide de %s vers %s",
demande.getStatut(), decision));
}
// 3. Mise à jour de la demande
demande.setCommentairesEvaluateur(commentaires);
demande.setDateEvaluation(LocalDateTime.now());
if (montantApprouve != null) {
demande.setMontantApprouve(montantApprouve);
}
// 4. Changement de statut
demande = demandeAideService.changerStatut(demandeId, decision, commentaires);
// 5. Actions spécifiques selon la décision
switch (decision) {
case APPROUVEE, APPROUVEE_PARTIELLEMENT -> {
demande.setDateApprobation(LocalDateTime.now());
// Recherche automatique de proposants si pas de montant spécifique
if (demande.getTypeAide().isFinancier() && autoMatchingEnabled) {
CompletableFuture.runAsync(() ->
matchingService.rechercherProposantsFinanciers(demande));
}
}
case REJETEE -> {
// Enregistrement des raisons de rejet pour analytics
analyticsService.enregistrerRejetDemande(demande, commentaires);
}
case INFORMATIONS_REQUISES -> {
// Programmation d'un rappel
programmerRappelInformationsRequises(demande);
}
}
// 6. Notifications
if (notificationEnabled) {
notificationService.notifierDecisionEvaluation(demande);
}
// 7. Mise à jour des analytics
analyticsService.enregistrerEvaluationDemande(demande);
LOG.infof("Demande évaluée avec succès: %s", demandeId);
return demande;
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de l'évaluation de la demande: %s", demandeId);
throw new RuntimeException("Erreur lors de l'évaluation de la demande", e);
}
});
}
// === GESTION DES PROPOSITIONS D'AIDE ===
/**
* Crée une nouvelle proposition d'aide
*
* @param propositionDTO La proposition d'aide à créer
* @return La proposition d'aide créée
*/
@Transactional
public CompletableFuture<PropositionAideDTO> creerPropositionAide(@Valid PropositionAideDTO propositionDTO) {
LOG.infof("Création d'une nouvelle proposition d'aide: %s", propositionDTO.getTitre());
return CompletableFuture.supplyAsync(() -> {
try {
// 1. Créer la proposition
PropositionAideDTO propositionCreee = propositionAideService.creerProposition(propositionDTO);
// 2. Matching automatique avec les demandes existantes
if (autoMatchingEnabled) {
CompletableFuture.runAsync(() -> {
try {
List<DemandeAideDTO> demandesCompatibles =
matchingService.trouverDemandesCompatibles(propositionCreee);
if (!demandesCompatibles.isEmpty()) {
LOG.infof("Trouvé %d demandes compatibles pour la proposition %s",
demandesCompatibles.size(), propositionCreee.getId());
// Notification au proposant
if (notificationEnabled) {
notificationService.notifierDemandesCompatibles(
propositionCreee, demandesCompatibles);
}
}
} catch (Exception e) {
LOG.errorf(e, "Erreur lors du matching automatique pour la proposition %s",
propositionCreee.getId());
}
});
}
// 3. Notifications
if (notificationEnabled) {
notificationService.notifierCreationProposition(propositionCreee);
}
// 4. Mise à jour des analytics
analyticsService.enregistrerNouvelleProposition(propositionCreee);
LOG.infof("Proposition d'aide créée avec succès: %s", propositionCreee.getId());
return propositionCreee;
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la création de la proposition d'aide");
throw new RuntimeException("Erreur lors de la création de la proposition d'aide", e);
}
});
}
/**
* Obtient une proposition d'aide par son ID
*
* @param propositionId ID de la proposition
* @return La proposition trouvée
*/
public PropositionAideDTO obtenirPropositionAide(@NotBlank String propositionId) {
LOG.debugf("Récupération de la proposition d'aide: %s", propositionId);
try {
return propositionAideService.obtenirParId(propositionId);
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la récupération de la proposition: %s", propositionId);
return null;
}
}
/**
* Recherche des propositions d'aide avec filtres
*
* @param filtres Critères de recherche
* @return Liste des propositions correspondantes
*/
public List<PropositionAideDTO> rechercherPropositions(Map<String, Object> filtres) {
LOG.debugf("Recherche de propositions avec filtres: %s", filtres);
try {
return propositionAideService.rechercherAvecFiltres(filtres);
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la recherche de propositions");
return new ArrayList<>();
}
}
/**
* Trouve les propositions compatibles avec une demande
*
* @param demandeId ID de la demande
* @return Liste des propositions compatibles
*/
public List<PropositionAideDTO> trouverPropositionsCompatibles(@NotBlank String demandeId) {
LOG.infof("Recherche de propositions compatibles pour la demande: %s", demandeId);
try {
DemandeAideDTO demande = demandeAideService.obtenirParId(demandeId);
if (demande == null) {
throw new IllegalArgumentException("Demande non trouvée: " + demandeId);
}
return matchingService.trouverPropositionsCompatibles(demande);
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la recherche de propositions compatibles");
return new ArrayList<>();
}
}
/**
* Trouve les demandes compatibles avec une proposition
*
* @param propositionId ID de la proposition
* @return Liste des demandes compatibles
*/
public List<DemandeAideDTO> trouverDemandesCompatibles(@NotBlank String propositionId) {
LOG.infof("Recherche de demandes compatibles pour la proposition: %s", propositionId);
try {
PropositionAideDTO proposition = propositionAideService.obtenirParId(propositionId);
if (proposition == null) {
throw new IllegalArgumentException("Proposition non trouvée: " + propositionId);
}
return matchingService.trouverDemandesCompatibles(proposition);
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la recherche de demandes compatibles");
return new ArrayList<>();
}
}
// === RECHERCHE ET FILTRAGE ===
/**
* Obtient une demande d'aide par son ID
*
* @param demandeId ID de la demande
* @return La demande trouvée
*/
public DemandeAideDTO obtenirDemandeAide(@NotBlank String demandeId) {
LOG.debugf("Récupération de la demande d'aide: %s", demandeId);
try {
return demandeAideService.obtenirParId(demandeId);
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la récupération de la demande: %s", demandeId);
return null;
}
}
/**
* Met à jour une demande d'aide
*
* @param demandeDTO La demande à mettre à jour
* @return La demande mise à jour
*/
@Transactional
public DemandeAideDTO mettreAJourDemandeAide(@Valid DemandeAideDTO demandeDTO) {
LOG.infof("Mise à jour de la demande d'aide: %s", demandeDTO.getId());
try {
return demandeAideService.mettreAJour(demandeDTO);
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la mise à jour de la demande: %s", demandeDTO.getId());
throw new RuntimeException("Erreur lors de la mise à jour de la demande", e);
}
}
/**
* Évalue une demande d'aide (version synchrone pour l'API REST)
*
* @param demandeId ID de la demande
* @param evaluateurId ID de l'évaluateur
* @param decision Décision d'évaluation
* @param commentaire Commentaire de l'évaluateur
* @param montantApprouve Montant approuvé
* @return La demande évaluée
*/
@Transactional
public DemandeAideDTO evaluerDemande(@NotBlank String demandeId,
@NotBlank String evaluateurId,
@NotNull StatutAide decision,
String commentaire,
Double montantApprouve) {
LOG.infof("Évaluation synchrone de la demande: %s par: %s", demandeId, evaluateurId);
try {
// Utilisation de la version asynchrone et attente du résultat
return evaluerDemande(demandeId, decision, commentaire, montantApprouve).get();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de l'évaluation synchrone de la demande: %s", demandeId);
throw new RuntimeException("Erreur lors de l'évaluation de la demande", e);
}
}
/**
* Soumet une demande d'aide (version synchrone pour l'API REST)
*
* @param demandeId ID de la demande
* @return La demande soumise
*/
@Transactional
public DemandeAideDTO soumettreDemande(@NotBlank String demandeId) {
LOG.infof("Soumission synchrone de la demande: %s", demandeId);
try {
// Utilisation de la version asynchrone et attente du résultat
return soumettreDemandeeAsync(demandeId).get();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la soumission synchrone de la demande: %s", demandeId);
throw new RuntimeException("Erreur lors de la soumission de la demande", e);
}
}
/**
* Recherche des demandes d'aide avec filtres
*
* @param filtres Critères de recherche
* @return Liste des demandes correspondantes
*/
public List<DemandeAideDTO> rechercherDemandes(Map<String, Object> filtres) {
LOG.debugf("Recherche de demandes avec filtres: %s", filtres);
try {
return demandeAideService.rechercherAvecFiltres(filtres);
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la recherche de demandes");
return new ArrayList<>();
}
}
/**
* Obtient les demandes urgentes nécessitant une attention immédiate
*
* @param organisationId ID de l'organisation
* @return Liste des demandes urgentes
*/
public List<DemandeAideDTO> obtenirDemandesUrgentes(String organisationId) {
LOG.debugf("Récupération des demandes urgentes pour l'organisation: %s", organisationId);
try {
return demandeAideService.obtenirDemandesUrgentes(organisationId);
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la récupération des demandes urgentes");
return new ArrayList<>();
}
}
/**
* Obtient les statistiques de solidarité
*
* @param organisationId ID de l'organisation
* @return Map des statistiques
*/
public Map<String, Object> obtenirStatistiquesSolidarite(String organisationId) {
LOG.debugf("Calcul des statistiques de solidarité pour: %s", organisationId);
try {
return analyticsService.calculerStatistiquesSolidarite(organisationId);
} catch (Exception e) {
LOG.errorf(e, "Erreur lors du calcul des statistiques");
return new HashMap<>();
}
}
// === MÉTHODES UTILITAIRES PRIVÉES ===
/**
* Valide une demande avant soumission
*/
private void validerDemandeAvantSoumission(DemandeAideDTO demande) {
List<String> erreurs = new ArrayList<>();
if (demande.getTitre() == null || demande.getTitre().trim().isEmpty()) {
erreurs.add("Le titre est obligatoire");
}
if (demande.getDescription() == null || demande.getDescription().trim().isEmpty()) {
erreurs.add("La description est obligatoire");
}
if (demande.getTypeAide().isNecessiteMontant() && demande.getMontantDemande() == null) {
erreurs.add("Le montant est obligatoire pour ce type d'aide");
}
if (demande.getMontantDemande() != null &&
!demande.getTypeAide().isMontantValide(demande.getMontantDemande())) {
erreurs.add("Le montant demandé n'est pas dans la fourchette autorisée");
}
if (!erreurs.isEmpty()) {
throw new IllegalArgumentException("Erreurs de validation: " + String.join(", ", erreurs));
}
}
/**
* Programme les rappels automatiques pour une demande
*/
private void programmerRappelsAutomatiques(DemandeAideDTO demande) {
if (!notificationEnabled) return;
try {
// Rappel à 50% du délai
LocalDateTime rappel50 = demande.getDateCreation()
.plusHours(demande.getPriorite().getDelaiTraitementHeures() / 2);
// Rappel à 80% du délai
LocalDateTime rappel80 = demande.getDateCreation()
.plusHours((long) (demande.getPriorite().getDelaiTraitementHeures() * 0.8));
// Rappel de dépassement
LocalDateTime rappelDepassement = demande.getDateLimiteTraitement().plusHours(1);
notificationService.programmerRappels(demande, rappel50, rappel80, rappelDepassement);
} catch (Exception e) {
LOG.warnf(e, "Erreur lors de la programmation des rappels pour la demande %s",
demande.getId());
}
}
/**
* Programme un rappel pour les informations requises
*/
private void programmerRappelInformationsRequises(DemandeAideDTO demande) {
if (!notificationEnabled) return;
try {
// Rappel dans 48h si pas de réponse
LocalDateTime rappel = LocalDateTime.now().plusHours(48);
notificationService.programmerRappelInformationsRequises(demande, rappel);
} catch (Exception e) {
LOG.warnf(e, "Erreur lors de la programmation du rappel d'informations pour la demande %s",
demande.getId());
}
}
}

View File

@@ -0,0 +1,401 @@
package dev.lions.unionflow.server.service;
import dev.lions.unionflow.server.api.dto.analytics.KPITrendDTO;
import dev.lions.unionflow.server.api.enums.analytics.TypeMetrique;
import dev.lions.unionflow.server.api.enums.analytics.PeriodeAnalyse;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import lombok.extern.slf4j.Slf4j;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.ArrayList;
import java.util.UUID;
/**
* Service d'analyse des tendances et prédictions pour les KPI
*
* Ce service calcule les tendances, effectue des analyses statistiques
* et génère des prédictions basées sur l'historique des données.
*
* @author UnionFlow Team
* @version 1.0
* @since 2025-01-16
*/
@ApplicationScoped
@Slf4j
public class TrendAnalysisService {
@Inject
AnalyticsService analyticsService;
@Inject
KPICalculatorService kpiCalculatorService;
/**
* Calcule la tendance d'un KPI sur une période donnée
*
* @param typeMetrique Le type de métrique à analyser
* @param periodeAnalyse La période d'analyse
* @param organisationId L'ID de l'organisation (optionnel)
* @return Les données de tendance du KPI
*/
public KPITrendDTO calculerTendance(TypeMetrique typeMetrique,
PeriodeAnalyse periodeAnalyse,
UUID organisationId) {
log.info("Calcul de la tendance pour {} sur la période {} et l'organisation {}",
typeMetrique, periodeAnalyse, organisationId);
LocalDateTime dateDebut = periodeAnalyse.getDateDebut();
LocalDateTime dateFin = periodeAnalyse.getDateFin();
// Génération des points de données historiques
List<KPITrendDTO.PointDonneeDTO> pointsDonnees = genererPointsDonnees(
typeMetrique, dateDebut, dateFin, organisationId);
// Calculs statistiques
StatistiquesDTO stats = calculerStatistiques(pointsDonnees);
// Analyse de tendance (régression linéaire simple)
TendanceDTO tendance = calculerTendanceLineaire(pointsDonnees);
// Prédiction pour la prochaine période
BigDecimal prediction = calculerPrediction(pointsDonnees, tendance);
// Détection d'anomalies
detecterAnomalies(pointsDonnees, stats);
return KPITrendDTO.builder()
.typeMetrique(typeMetrique)
.periodeAnalyse(periodeAnalyse)
.organisationId(organisationId)
.nomOrganisation(obtenirNomOrganisation(organisationId))
.dateDebut(dateDebut)
.dateFin(dateFin)
.pointsDonnees(pointsDonnees)
.valeurActuelle(stats.valeurActuelle)
.valeurMinimale(stats.valeurMinimale)
.valeurMaximale(stats.valeurMaximale)
.valeurMoyenne(stats.valeurMoyenne)
.ecartType(stats.ecartType)
.coefficientVariation(stats.coefficientVariation)
.tendanceGenerale(tendance.pente)
.coefficientCorrelation(tendance.coefficientCorrelation)
.pourcentageEvolutionGlobale(calculerEvolutionGlobale(pointsDonnees))
.predictionProchainePeriode(prediction)
.margeErreurPrediction(calculerMargeErreur(tendance))
.seuilAlerteBas(calculerSeuilAlerteBas(stats))
.seuilAlerteHaut(calculerSeuilAlerteHaut(stats))
.alerteActive(verifierAlertes(stats.valeurActuelle, stats))
.intervalleRegroupement(periodeAnalyse.getIntervalleRegroupement())
.formatDate(periodeAnalyse.getFormatDate())
.dateDerniereMiseAJour(LocalDateTime.now())
.frequenceMiseAJourMinutes(determinerFrequenceMiseAJour(periodeAnalyse))
.build();
}
/**
* Génère les points de données historiques pour la période
*/
private List<KPITrendDTO.PointDonneeDTO> genererPointsDonnees(TypeMetrique typeMetrique,
LocalDateTime dateDebut,
LocalDateTime dateFin,
UUID organisationId) {
List<KPITrendDTO.PointDonneeDTO> points = new ArrayList<>();
// Déterminer l'intervalle entre les points
ChronoUnit unite = determinerUniteIntervalle(dateDebut, dateFin);
long intervalleValeur = determinerValeurIntervalle(dateDebut, dateFin, unite);
LocalDateTime dateCourante = dateDebut;
int index = 0;
while (!dateCourante.isAfter(dateFin)) {
LocalDateTime dateFinIntervalle = dateCourante.plus(intervalleValeur, unite);
if (dateFinIntervalle.isAfter(dateFin)) {
dateFinIntervalle = dateFin;
}
// Calcul de la valeur pour cet intervalle
BigDecimal valeur = calculerValeurPourIntervalle(typeMetrique, dateCourante, dateFinIntervalle, organisationId);
KPITrendDTO.PointDonneeDTO point = KPITrendDTO.PointDonneeDTO.builder()
.date(dateCourante)
.valeur(valeur)
.libelle(formaterLibellePoint(dateCourante, unite))
.anomalie(false) // Sera déterminé plus tard
.prediction(false)
.build();
points.add(point);
dateCourante = dateCourante.plus(intervalleValeur, unite);
index++;
}
log.info("Généré {} points de données pour la tendance", points.size());
return points;
}
/**
* Calcule les statistiques descriptives des points de données
*/
private StatistiquesDTO calculerStatistiques(List<KPITrendDTO.PointDonneeDTO> points) {
if (points.isEmpty()) {
return new StatistiquesDTO();
}
List<BigDecimal> valeurs = points.stream()
.map(KPITrendDTO.PointDonneeDTO::getValeur)
.toList();
BigDecimal valeurActuelle = points.get(points.size() - 1).getValeur();
BigDecimal valeurMinimale = valeurs.stream().min(BigDecimal::compareTo).orElse(BigDecimal.ZERO);
BigDecimal valeurMaximale = valeurs.stream().max(BigDecimal::compareTo).orElse(BigDecimal.ZERO);
// Calcul de la moyenne
BigDecimal somme = valeurs.stream().reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal moyenne = somme.divide(new BigDecimal(valeurs.size()), 4, RoundingMode.HALF_UP);
// Calcul de l'écart-type
BigDecimal sommeDifferencesCarrees = valeurs.stream()
.map(v -> v.subtract(moyenne).pow(2))
.reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal variance = sommeDifferencesCarrees.divide(new BigDecimal(valeurs.size()), 4, RoundingMode.HALF_UP);
BigDecimal ecartType = new BigDecimal(Math.sqrt(variance.doubleValue())).setScale(4, RoundingMode.HALF_UP);
// Coefficient de variation
BigDecimal coefficientVariation = moyenne.compareTo(BigDecimal.ZERO) != 0
? ecartType.divide(moyenne, 4, RoundingMode.HALF_UP)
: BigDecimal.ZERO;
return new StatistiquesDTO(valeurActuelle, valeurMinimale, valeurMaximale,
moyenne, ecartType, coefficientVariation);
}
/**
* Calcule la tendance linéaire (régression linéaire simple)
*/
private TendanceDTO calculerTendanceLineaire(List<KPITrendDTO.PointDonneeDTO> points) {
if (points.size() < 2) {
return new TendanceDTO(BigDecimal.ZERO, BigDecimal.ZERO);
}
int n = points.size();
BigDecimal sommeX = BigDecimal.ZERO;
BigDecimal sommeY = BigDecimal.ZERO;
BigDecimal sommeXY = BigDecimal.ZERO;
BigDecimal sommeX2 = BigDecimal.ZERO;
BigDecimal sommeY2 = BigDecimal.ZERO;
for (int i = 0; i < n; i++) {
BigDecimal x = new BigDecimal(i); // Index comme variable X
BigDecimal y = points.get(i).getValeur(); // Valeur comme variable Y
sommeX = sommeX.add(x);
sommeY = sommeY.add(y);
sommeXY = sommeXY.add(x.multiply(y));
sommeX2 = sommeX2.add(x.multiply(x));
sommeY2 = sommeY2.add(y.multiply(y));
}
// Calcul de la pente (coefficient directeur)
BigDecimal nBD = new BigDecimal(n);
BigDecimal numerateur = nBD.multiply(sommeXY).subtract(sommeX.multiply(sommeY));
BigDecimal denominateur = nBD.multiply(sommeX2).subtract(sommeX.multiply(sommeX));
BigDecimal pente = denominateur.compareTo(BigDecimal.ZERO) != 0
? numerateur.divide(denominateur, 6, RoundingMode.HALF_UP)
: BigDecimal.ZERO;
// Calcul du coefficient de corrélation R²
BigDecimal numerateurR = numerateur;
BigDecimal denominateurR1 = nBD.multiply(sommeX2).subtract(sommeX.multiply(sommeX));
BigDecimal denominateurR2 = nBD.multiply(sommeY2).subtract(sommeY.multiply(sommeY));
BigDecimal coefficientCorrelation = BigDecimal.ZERO;
if (denominateurR1.compareTo(BigDecimal.ZERO) != 0 && denominateurR2.compareTo(BigDecimal.ZERO) != 0) {
BigDecimal denominateurR = new BigDecimal(Math.sqrt(
denominateurR1.multiply(denominateurR2).doubleValue()));
if (denominateurR.compareTo(BigDecimal.ZERO) != 0) {
BigDecimal r = numerateurR.divide(denominateurR, 6, RoundingMode.HALF_UP);
coefficientCorrelation = r.multiply(r); // R²
}
}
return new TendanceDTO(pente, coefficientCorrelation);
}
/**
* Calcule une prédiction pour la prochaine période
*/
private BigDecimal calculerPrediction(List<KPITrendDTO.PointDonneeDTO> points, TendanceDTO tendance) {
if (points.isEmpty()) return BigDecimal.ZERO;
BigDecimal derniereValeur = points.get(points.size() - 1).getValeur();
BigDecimal prediction = derniereValeur.add(tendance.pente);
// S'assurer que la prédiction est positive
return prediction.max(BigDecimal.ZERO);
}
/**
* Détecte les anomalies dans les points de données
*/
private void detecterAnomalies(List<KPITrendDTO.PointDonneeDTO> points, StatistiquesDTO stats) {
BigDecimal seuilAnomalie = stats.ecartType.multiply(new BigDecimal("2")); // 2 écarts-types
for (KPITrendDTO.PointDonneeDTO point : points) {
BigDecimal ecartMoyenne = point.getValeur().subtract(stats.valeurMoyenne).abs();
if (ecartMoyenne.compareTo(seuilAnomalie) > 0) {
point.setAnomalie(true);
}
}
}
// === MÉTHODES UTILITAIRES ===
private ChronoUnit determinerUniteIntervalle(LocalDateTime dateDebut, LocalDateTime dateFin) {
long joursTotal = ChronoUnit.DAYS.between(dateDebut, dateFin);
if (joursTotal <= 7) return ChronoUnit.DAYS;
if (joursTotal <= 90) return ChronoUnit.DAYS;
if (joursTotal <= 365) return ChronoUnit.WEEKS;
return ChronoUnit.MONTHS;
}
private long determinerValeurIntervalle(LocalDateTime dateDebut, LocalDateTime dateFin, ChronoUnit unite) {
long dureeTotal = unite.between(dateDebut, dateFin);
// Viser environ 10-20 points de données
if (dureeTotal <= 20) return 1;
if (dureeTotal <= 40) return 2;
if (dureeTotal <= 100) return 5;
return dureeTotal / 15; // Environ 15 points
}
private BigDecimal calculerValeurPourIntervalle(TypeMetrique typeMetrique,
LocalDateTime dateDebut,
LocalDateTime dateFin,
UUID organisationId) {
// Utiliser le service KPI pour calculer la valeur
return switch (typeMetrique) {
case NOMBRE_MEMBRES_ACTIFS -> {
// Calcul direct via le service KPI
var kpis = kpiCalculatorService.calculerTousLesKPI(organisationId, dateDebut, dateFin);
yield kpis.getOrDefault(TypeMetrique.NOMBRE_MEMBRES_ACTIFS, BigDecimal.ZERO);
}
case TOTAL_COTISATIONS_COLLECTEES -> {
var kpis = kpiCalculatorService.calculerTousLesKPI(organisationId, dateDebut, dateFin);
yield kpis.getOrDefault(TypeMetrique.TOTAL_COTISATIONS_COLLECTEES, BigDecimal.ZERO);
}
case NOMBRE_EVENEMENTS_ORGANISES -> {
var kpis = kpiCalculatorService.calculerTousLesKPI(organisationId, dateDebut, dateFin);
yield kpis.getOrDefault(TypeMetrique.NOMBRE_EVENEMENTS_ORGANISES, BigDecimal.ZERO);
}
case NOMBRE_DEMANDES_AIDE -> {
var kpis = kpiCalculatorService.calculerTousLesKPI(organisationId, dateDebut, dateFin);
yield kpis.getOrDefault(TypeMetrique.NOMBRE_DEMANDES_AIDE, BigDecimal.ZERO);
}
default -> BigDecimal.ZERO;
};
}
private String formaterLibellePoint(LocalDateTime date, ChronoUnit unite) {
return switch (unite) {
case DAYS -> date.toLocalDate().toString();
case WEEKS -> "S" + date.get(java.time.temporal.WeekFields.ISO.weekOfYear());
case MONTHS -> date.getMonth().toString() + " " + date.getYear();
default -> date.toString();
};
}
private BigDecimal calculerEvolutionGlobale(List<KPITrendDTO.PointDonneeDTO> points) {
if (points.size() < 2) return BigDecimal.ZERO;
BigDecimal premiereValeur = points.get(0).getValeur();
BigDecimal derniereValeur = points.get(points.size() - 1).getValeur();
if (premiereValeur.compareTo(BigDecimal.ZERO) == 0) return BigDecimal.ZERO;
return derniereValeur.subtract(premiereValeur)
.divide(premiereValeur, 4, RoundingMode.HALF_UP)
.multiply(new BigDecimal("100"));
}
private BigDecimal calculerMargeErreur(TendanceDTO tendance) {
// Marge d'erreur basée sur le coefficient de corrélation
BigDecimal precision = tendance.coefficientCorrelation;
BigDecimal margeErreur = BigDecimal.ONE.subtract(precision).multiply(new BigDecimal("100"));
return margeErreur.min(new BigDecimal("50")); // Plafonnée à 50%
}
private BigDecimal calculerSeuilAlerteBas(StatistiquesDTO stats) {
return stats.valeurMoyenne.subtract(stats.ecartType.multiply(new BigDecimal("1.5")));
}
private BigDecimal calculerSeuilAlerteHaut(StatistiquesDTO stats) {
return stats.valeurMoyenne.add(stats.ecartType.multiply(new BigDecimal("1.5")));
}
private Boolean verifierAlertes(BigDecimal valeurActuelle, StatistiquesDTO stats) {
BigDecimal seuilBas = calculerSeuilAlerteBas(stats);
BigDecimal seuilHaut = calculerSeuilAlerteHaut(stats);
return valeurActuelle.compareTo(seuilBas) < 0 || valeurActuelle.compareTo(seuilHaut) > 0;
}
private Integer determinerFrequenceMiseAJour(PeriodeAnalyse periode) {
return switch (periode) {
case AUJOURD_HUI, HIER -> 15; // 15 minutes
case CETTE_SEMAINE, SEMAINE_DERNIERE -> 60; // 1 heure
case CE_MOIS, MOIS_DERNIER -> 240; // 4 heures
default -> 1440; // 24 heures
};
}
private String obtenirNomOrganisation(UUID organisationId) {
// À implémenter avec le repository
return null;
}
// === CLASSES INTERNES ===
private static class StatistiquesDTO {
final BigDecimal valeurActuelle;
final BigDecimal valeurMinimale;
final BigDecimal valeurMaximale;
final BigDecimal valeurMoyenne;
final BigDecimal ecartType;
final BigDecimal coefficientVariation;
StatistiquesDTO() {
this(BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO,
BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO);
}
StatistiquesDTO(BigDecimal valeurActuelle, BigDecimal valeurMinimale, BigDecimal valeurMaximale,
BigDecimal valeurMoyenne, BigDecimal ecartType, BigDecimal coefficientVariation) {
this.valeurActuelle = valeurActuelle;
this.valeurMinimale = valeurMinimale;
this.valeurMaximale = valeurMaximale;
this.valeurMoyenne = valeurMoyenne;
this.ecartType = ecartType;
this.coefficientVariation = coefficientVariation;
}
}
private static class TendanceDTO {
final BigDecimal pente;
final BigDecimal coefficientCorrelation;
TendanceDTO(BigDecimal pente, BigDecimal coefficientCorrelation) {
this.pente = pente;
this.coefficientCorrelation = coefficientCorrelation;
}
}
}