443 lines
16 KiB
Java
443 lines
16 KiB
Java
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.enums.solidarite.TypeAide;
|
|
import jakarta.enterprise.context.ApplicationScoped;
|
|
import jakarta.transaction.Transactional;
|
|
import jakarta.validation.Valid;
|
|
import jakarta.validation.constraints.NotBlank;
|
|
import java.math.BigDecimal;
|
|
import java.time.LocalDateTime;
|
|
import java.util.*;
|
|
import java.util.stream.Collectors;
|
|
import org.jboss.logging.Logger;
|
|
|
|
/**
|
|
* Service spécialisé pour la gestion des propositions d'aide
|
|
*
|
|
* <p>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().compareTo(BigDecimal.valueOf((Double) valeur)) < 0) 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;
|
|
}
|
|
}
|