package dev.lions.unionflow.server.service;
import dev.lions.unionflow.server.api.dto.solidarite.request.CreateDemandeAideRequest;
import dev.lions.unionflow.server.api.dto.solidarite.request.UpdateDemandeAideRequest;
import dev.lions.unionflow.server.api.dto.solidarite.response.DemandeAideResponse;
import dev.lions.unionflow.server.api.dto.solidarite.HistoriqueStatutDTO;
import dev.lions.unionflow.server.api.enums.solidarite.PrioriteAide;
import dev.lions.unionflow.server.api.enums.solidarite.StatutAide;
import dev.lions.unionflow.server.entity.DemandeAide;
import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.entity.Organisation;
import dev.lions.unionflow.server.mapper.DemandeAideMapper;
import dev.lions.unionflow.server.repository.DemandeAideRepository;
import dev.lions.unionflow.server.repository.MembreRepository;
import dev.lions.unionflow.server.repository.OrganisationRepository;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
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 demandes d'aide
*
*
* Ce service gère le cycle de vie complet des demandes d'aide : création,
* validation,
* changements de statut, recherche et suivi. Persistance via
* DemandeAideRepository.
*
* @author UnionFlow Team
* @version 1.0
* @since 2025-01-16
*/
@ApplicationScoped
public class DemandeAideService {
private static final Logger LOG = Logger.getLogger(DemandeAideService.class);
@Inject
DemandeAideRepository demandeAideRepository;
@Inject
DemandeAideMapper demandeAideMapper;
@Inject
MembreRepository membreRepository;
@Inject
OrganisationRepository organisationRepository;
// Cache en mémoire pour les demandes fréquemment consultées
private final Map cacheDemandesRecentes = new HashMap<>();
private final Map cacheTimestamps = new HashMap<>();
private static final long CACHE_DURATION_MINUTES = 15;
// === OPÉRATIONS CRUD ===
/**
* Crée une nouvelle demande d'aide
*
* @param request La requête de création
* @return La demande créée
*/
@Transactional
public DemandeAideResponse creerDemande(@Valid CreateDemandeAideRequest request) {
LOG.infof("Création d'une nouvelle demande d'aide: %s", request.titre());
Membre demandeur = null;
if (request.membreDemandeurId() != null) {
demandeur = membreRepository.findById(request.membreDemandeurId());
if (demandeur == null) {
throw new IllegalArgumentException("Membre demandeur non trouvé: " + request.membreDemandeurId());
}
}
Organisation organisation = null;
if (request.associationId() != null) {
organisation = organisationRepository.findById(request.associationId());
if (organisation == null) {
throw new IllegalArgumentException("Organisation non trouvée: " + request.associationId());
}
}
DemandeAide entity = demandeAideMapper.toEntity(request, demandeur, null, organisation);
demandeAideRepository.persist(entity);
DemandeAideResponse response = demandeAideMapper.toDTO(entity);
response.setNumeroReference(genererNumeroReference());
response.setScorePriorite(calculerScorePriorite(response));
LocalDateTime maintenant = LocalDateTime.now();
HistoriqueStatutDTO historiqueInitial = HistoriqueStatutDTO.builder()
.id(UUID.randomUUID().toString())
.ancienStatut(null)
.nouveauStatut(response.getStatut())
.dateChangement(maintenant)
.auteurId(response.getMembreDemandeurId() != null ? response.getMembreDemandeurId().toString() : null)
.motif("Création de la demande")
.estAutomatique(true)
.build();
response.setHistoriqueStatuts(List.of(historiqueInitial));
ajouterAuCache(response);
LOG.infof("Demande d'aide créée avec succès: %s", response.getId());
return response;
}
/**
* Met à jour une demande d'aide existante
*
* @param id Identifiant de la demande
* @param request La requête de mise à jour
* @return La demande mise à jour
*/
@Transactional
public DemandeAideResponse mettreAJour(@NotNull UUID id, @Valid UpdateDemandeAideRequest request) {
LOG.infof("Mise à jour de la demande d'aide: %s", id);
DemandeAide entity = demandeAideRepository.findById(id);
if (entity == null) {
throw new IllegalArgumentException("Demande non trouvée: " + id);
}
if (!entity.getStatut().permetModification()) {
throw new IllegalStateException("Cette demande ne peut plus être modifiée");
}
demandeAideMapper.updateEntityFromDTO(entity, request);
entity = demandeAideRepository.update(entity);
DemandeAideResponse response = demandeAideMapper.toDTO(entity);
response.setScorePriorite(calculerScorePriorite(response));
ajouterAuCache(response);
LOG.infof("Demande d'aide mise à jour avec succès: %s", response.getId());
return response;
}
/**
* Obtient une demande d'aide par son ID
*
* @param id UUID de la demande
* @return La demande trouvée
*/
public DemandeAideResponse obtenirParId(@NotNull UUID id) {
LOG.debugf("Récupération de la demande d'aide: %s", id);
DemandeAideResponse demandeCachee = obtenirDuCache(id);
if (demandeCachee != null) {
LOG.debugf("Demande trouvée dans le cache: %s", id);
return demandeCachee;
}
DemandeAide entity = demandeAideRepository.findById(id);
DemandeAideResponse response = entity != null ? demandeAideMapper.toDTO(entity) : null;
if (response != null) {
ajouterAuCache(response);
}
return response;
}
/**
* Change le statut d'une demande d'aide
*
* @param demandeId UUID de la demande
* @param nouveauStatut Nouveau statut
* @param motif Motif du changement
* @return La demande avec le nouveau statut
*/
@Transactional
public DemandeAideResponse changerStatut(
@NotNull UUID demandeId, @NotNull StatutAide nouveauStatut, String motif) {
LOG.infof("Changement de statut pour la demande %s: %s", demandeId, nouveauStatut);
DemandeAide entity = demandeAideRepository.findById(demandeId);
if (entity == null) {
throw new IllegalArgumentException("Demande non trouvée: " + demandeId);
}
StatutAide ancienStatut = entity.getStatut();
if (!ancienStatut.peutTransitionnerVers(nouveauStatut)) {
throw new IllegalStateException(
String.format("Transition invalide de %s vers %s", ancienStatut, nouveauStatut));
}
entity.setStatut(nouveauStatut);
if (motif != null && !motif.isBlank()) {
entity.setCommentaireEvaluation(
entity.getCommentaireEvaluation() != null
? entity.getCommentaireEvaluation() + "\n" + motif
: motif);
}
LocalDateTime now = LocalDateTime.now();
entity.setDateModification(now);
switch (nouveauStatut) {
case SOUMISE -> entity.setDateDemande(now);
case APPROUVEE, APPROUVEE_PARTIELLEMENT -> entity.setDateEvaluation(now);
case VERSEE -> entity.setDateVersement(now);
default -> {
}
}
entity = demandeAideRepository.update(entity);
DemandeAideResponse response = demandeAideMapper.toDTO(entity);
HistoriqueStatutDTO nouvelHistorique = HistoriqueStatutDTO.builder()
.id(UUID.randomUUID().toString())
.ancienStatut(ancienStatut)
.nouveauStatut(nouveauStatut)
.dateChangement(now)
.motif(motif)
.estAutomatique(false)
.build();
List historique = new ArrayList<>(
response.getHistoriqueStatuts() != null ? response.getHistoriqueStatuts() : List.of());
historique.add(nouvelHistorique);
response.setHistoriqueStatuts(historique);
ajouterAuCache(response);
LOG.infof(
"Statut changé avec succès pour la demande %s: %s -> %s",
demandeId, ancienStatut, nouveauStatut);
return response;
}
// === RECHERCHE ET FILTRAGE ===
/**
* Recherche des demandes avec filtres
*
* @param filtres Map des critères de recherche
* @return Liste des demandes correspondantes
*/
public List rechercherAvecFiltres(Map filtres) {
LOG.debugf("Recherche de demandes avec filtres: %s", filtres);
List toutesLesDemandes = chargerToutesLesDemandesDepuisBDD();
return toutesLesDemandes.stream()
.filter(demande -> correspondAuxFiltres(demande, filtres))
.sorted(this::comparerParPriorite)
.collect(Collectors.toList());
}
/**
* Obtient les demandes urgentes pour une organisation
*
* @param organisationId UUID de l'organisation
* @return Liste des demandes urgentes
*/
public List obtenirDemandesUrgentes(UUID organisationId) {
LOG.debugf("Récupération des demandes urgentes pour: %s", organisationId);
Map filtres = Map.of(
"organisationId", organisationId,
"priorite", List.of(PrioriteAide.CRITIQUE, PrioriteAide.URGENTE),
"statut",
List.of(
StatutAide.SOUMISE,
StatutAide.EN_ATTENTE,
StatutAide.EN_COURS_EVALUATION,
StatutAide.APPROUVEE));
return rechercherAvecFiltres(filtres);
}
/**
* Obtient les demandes en retard (délai dépassé)
*
* @param organisationId ID de l'organisation
* @return Liste des demandes en retard
*/
public List obtenirDemandesEnRetard(UUID organisationId) {
LOG.debugf("Récupération des demandes en retard pour: %s", organisationId);
return chargerToutesLesDemandesDepuisBDD().stream()
.filter(demande -> demande.getAssociationId() != null && demande.getAssociationId().equals(organisationId))
.filter(DemandeAideResponse::estDelaiDepasse)
.filter(demande -> !demande.estTerminee())
.sorted(this::comparerParPriorite)
.collect(Collectors.toList());
}
// === MÉTHODES UTILITAIRES PRIVÉES ===
/** Génère un numéro de référence unique */
private String genererNumeroReference() {
int annee = LocalDateTime.now().getYear();
int numero = (int) (Math.random() * 999999) + 1;
return String.format("DA-%04d-%06d", annee, numero);
}
/** Calcule le score de priorité d'une demande */
private double calculerScorePriorite(DemandeAideResponse demande) {
double score = demande.getPriorite().getScorePriorite();
// Bonus pour type d'aide urgent
if (demande.getTypeAide().isUrgent()) {
score -= 1.0;
}
// Bonus pour montant élevé (aide financière)
if (demande.getTypeAide().isFinancier() && demande.getMontantDemande() != null) {
if (demande.getMontantDemande().compareTo(new BigDecimal("50000")) > 0) {
score -= 0.5;
}
}
// Malus pour ancienneté
long joursDepuisCreation = java.time.Duration.between(demande.getDateCreation(), LocalDateTime.now()).toDays();
if (joursDepuisCreation > 7) {
score += 0.3;
}
return Math.max(0.1, score);
}
/** Vérifie si une demande correspond aux filtres */
private boolean correspondAuxFiltres(DemandeAideResponse demande, Map filtres) {
for (Map.Entry filtre : filtres.entrySet()) {
String cle = filtre.getKey();
Object valeur = filtre.getValue();
switch (cle) {
case "organisationId" -> {
if (!demande.getAssociationId().equals(valeur))
return false;
}
case "typeAide" -> {
if (valeur instanceof List> liste) {
if (!liste.contains(demande.getTypeAide()))
return false;
} else if (!demande.getTypeAide().equals(valeur)) {
return false;
}
}
case "statut" -> {
if (valeur instanceof List> liste) {
if (!liste.contains(demande.getStatut()))
return false;
} else if (!demande.getStatut().equals(valeur)) {
return false;
}
}
case "priorite" -> {
if (valeur instanceof List> liste) {
if (!liste.contains(demande.getPriorite()))
return false;
} else if (!demande.getPriorite().equals(valeur)) {
return false;
}
}
case "demandeurId" -> {
if (demande.getMembreDemandeurId() == null || !demande.getMembreDemandeurId().equals(valeur))
return false;
}
}
}
return true;
}
/** Compare deux demandes par priorité */
private int comparerParPriorite(DemandeAideResponse d1, DemandeAideResponse d2) {
double s1 = d1.getScorePriorite() != null ? d1.getScorePriorite() : Double.MAX_VALUE;
double s2 = d2.getScorePriorite() != null ? d2.getScorePriorite() : Double.MAX_VALUE;
int comparaisonScore = Double.compare(s1, s2);
if (comparaisonScore != 0)
return comparaisonScore;
LocalDateTime c1 = d1.getDateCreation() != null ? d1.getDateCreation() : LocalDateTime.MIN;
LocalDateTime c2 = d2.getDateCreation() != null ? d2.getDateCreation() : LocalDateTime.MIN;
return c1.compareTo(c2);
}
// === GESTION DU CACHE ===
private void ajouterAuCache(DemandeAideResponse demande) {
cacheDemandesRecentes.put(demande.getId(), demande);
cacheTimestamps.put(demande.getId(), LocalDateTime.now());
// Nettoyage du cache si trop volumineux
if (cacheDemandesRecentes.size() > 100) {
nettoyerCache();
}
}
private DemandeAideResponse obtenirDuCache(UUID id) {
LocalDateTime timestamp = cacheTimestamps.get(id);
if (timestamp == null)
return null;
// Vérification de l'expiration
if (LocalDateTime.now().minusMinutes(CACHE_DURATION_MINUTES).isAfter(timestamp)) {
cacheDemandesRecentes.remove(id);
cacheTimestamps.remove(id);
return null;
}
return cacheDemandesRecentes.get(id);
}
private void nettoyerCache() {
LocalDateTime limite = LocalDateTime.now().minusMinutes(CACHE_DURATION_MINUTES);
cacheTimestamps.entrySet().removeIf(entry -> entry.getValue().isBefore(limite));
cacheDemandesRecentes.keySet().retainAll(cacheTimestamps.keySet());
}
/** Charge toutes les demandes depuis la base et les mappe en DTO. */
private List chargerToutesLesDemandesDepuisBDD() {
List entities = demandeAideRepository.listAll();
return entities.stream()
.map(demandeAideMapper::toDTO)
.collect(Collectors.toList());
}
}