package dev.lions.unionflow.server.service; import dev.lions.unionflow.server.api.dto.solidarite.response.DemandeAideResponse; import dev.lions.unionflow.server.api.dto.solidarite.response.PropositionAideResponse; import dev.lions.unionflow.server.api.enums.solidarite.TypeAide; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.*; import java.util.stream.Collectors; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.jboss.logging.Logger; /** * 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 trouverPropositionsCompatibles(DemandeAideResponse 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 candidatsOriginal = propositionAideService .obtenirPropositionsActives(demande.getTypeAide()); List candidats = new ArrayList<>(candidatsOriginal); // 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 resultats = candidats.stream() .filter(PropositionAideResponse::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 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 trouverDemandesCompatibles(PropositionAideResponse proposition) { LOG.infof("Recherche de demandes compatibles pour la proposition: %s", proposition.getId()); try { // Recherche des demandes actives du même type Map 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 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 rechercherProposantsFinanciers(DemandeAideResponse 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 filtres = Map.of( "typeAide", demande.getTypeAide(), "estDisponible", true, "montantMaximum", demande.getMontantApprouve() != null ? demande.getMontantApprouve() : (demande.getMontantDemande() != null ? demande.getMontantDemande() : BigDecimal.ZERO)); List 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 matchingUrgence(DemandeAideResponse demande) { LOG.infof("Matching d'urgence pour la demande: %s", demande.getId()); // Recherche élargie pour les urgences List 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(PropositionAideResponse::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( DemandeAideResponse demande, PropositionAideResponse 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) { BigDecimal montantDemande = demande.getMontantApprouve() != null ? demande.getMontantApprouve() : demande.getMontantDemande(); if (montantDemande != null) { if (montantDemande.compareTo(proposition.getMontantMaximum()) <= 0) { score += 25.0; } else { // Pénalité proportionnelle au dépassement double ratio = proposition.getMontantMaximum().divide(montantDemande, 4, java.math.RoundingMode.HALF_UP) .doubleValue(); 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() != null && proposition.getNombreBeneficiairesAides() > 0) { score += Math.min(15.0, proposition.getNombreBeneficiairesAides() * boostExperience); } // 4. Réputation (10 points max) if (proposition.getNoteMoyenne() != null && proposition.getNombreEvaluations() != 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(DemandeAideResponse demande, PropositionAideResponse proposition) { double score = calculerScoreCompatibilite(demande, proposition); // Bonus spécifiques aux aides financières // 1. Historique de versements if (proposition.getMontantTotalVerse() != null && proposition.getMontantTotalVerse() > 0) { score += Math.min(10.0, proposition.getMontantTotalVerse() / 10000.0); } // 2. Fiabilité (ratio versements/promesses) if (proposition.getNombreDemandesTraitees() != null && 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() != null && proposition.getDelaiReponseHeures() <= 24) { score += 10.0; } else if (proposition.getDelaiReponseHeures() != null && proposition.getDelaiReponseHeures() <= 72) { score += 5.0; } return Math.max(0.0, Math.min(100.0, score)); } /** Calcule le bonus géographique */ private double calculerBonusGeographique(DemandeAideResponse demande, PropositionAideResponse 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(DemandeAideResponse demande, PropositionAideResponse proposition) { double bonus = 0.0; // Bonus pour demande urgente if (demande.estUrgente()) { bonus += 5.0; } // Bonus pour proposition récente if (proposition.getDateCreation() != null) { 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(DemandeAideResponse demande, PropositionAideResponse proposition) { double malus = 0.0; // Malus si la demande est en retard if (demande.estDelaiDepasse()) { malus += 5.0; } // Malus si la proposition a un délai de réponse long if (proposition.getDelaiReponseHeures() != null && proposition.getDelaiReponseHeures() > 168) { // Plus d'une // semaine malus += 3.0; } return malus; } // === MÉTHODES UTILITAIRES === /** Recherche des propositions par catégorie */ private List rechercherParCategorie(String categorie) { Map 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 PropositionAideResponse proposition; final double score; ResultatMatching(PropositionAideResponse proposition, double score) { this.proposition = proposition; this.score = score; } } }