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;
}
}