Refactoring
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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<>();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user