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 * *

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 cachePropositionsActives = new HashMap<>(); private final Map> 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 rechercherPropositionsCompatibles(DemandeAideDTO demande) { LOG.debugf("Recherche de propositions compatibles pour la demande: %s", demande.getId()); // Recherche par type d'aide d'abord List 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 rechercherAvecFiltres(Map 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 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 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 filtres) { for (Map.Entry 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; } }