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