430 lines
16 KiB
Java
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;
|
|
}
|
|
}
|
|
}
|