Files
unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/service/MatchingService.java
2026-03-28 14:21:30 +00:00

430 lines
16 KiB
Java

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
*
* <p>
* 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<PropositionAideResponse> 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<PropositionAideResponse> candidatsOriginal = propositionAideService
.obtenirPropositionsActives(demande.getTypeAide());
List<PropositionAideResponse> 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<ResultatMatching> 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<PropositionAideResponse> 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<DemandeAideResponse> 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<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<DemandeAideResponse> 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<PropositionAideResponse> 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<String, Object> filtres = Map.of(
"typeAide",
demande.getTypeAide(),
"estDisponible",
true,
"montantMaximum",
demande.getMontantApprouve() != null
? demande.getMontantApprouve()
: (demande.getMontantDemande() != null ? demande.getMontantDemande() : BigDecimal.ZERO));
List<PropositionAideResponse> 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<PropositionAideResponse> matchingUrgence(DemandeAideResponse demande) {
LOG.infof("Matching d'urgence pour la demande: %s", demande.getId());
// Recherche élargie pour les urgences
List<PropositionAideResponse> 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<PropositionAideResponse> 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 PropositionAideResponse proposition;
final double score;
ResultatMatching(PropositionAideResponse proposition, double score) {
this.proposition = proposition;
this.score = score;
}
}
}