RBAC:
- HealthResource: @PermitAll
- RoleResource: @RolesAllowed ADMIN/SUPER_ADMIN/ADMIN_ORGANISATION class-level
- PropositionAideResource: @RolesAllowed MEMBRE/USER class-level
- AuthCallbackResource: @PermitAll
- EvenementResource: @PermitAll /publics et /test, count restreint
- BackupResource/LogsMonitoringResource/SystemResource: MODERATOR → MODERATEUR
- AnalyticsResource: MANAGER/MEMBER → ADMIN_ORGANISATION/MEMBRE
- RoleConstant.java: constantes de rôles centralisées
Cycle de vie membres:
- MemberLifecycleService: ajouterMembre()/retirerMembre() sur activation/radiation/archivage
- MembreResource: endpoint GET /numero/{numeroMembre}
- MembreService: méthode trouverParNumeroMembre()
Changement mot de passe:
- CompteAdherentResource: endpoint POST /auth/change-password (mobile)
- MembreKeycloakSyncService: changerMotDePasseDirectKeycloak() via API Admin Keycloak directe
- Fallback automatique si lions-user-manager indisponible
Workflow:
- Flyway V17-V23: rôles, types org, formules Option C, lifecycle columns, bareme cotisation
- Nouvelles classes: MemberLifecycleService, OrganisationModuleService, scheduler
- Security: OrganisationContextFilter, OrganisationContextHolder, ModuleAccessFilter
426 lines
15 KiB
Java
426 lines
15 KiB
Java
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 io.quarkus.panache.common.Page;
|
|
import io.quarkus.panache.common.Sort;
|
|
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
|
|
*
|
|
* <p>
|
|
* 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<UUID, DemandeAideResponse> cacheDemandesRecentes = new HashMap<>();
|
|
private final Map<UUID, LocalDateTime> 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<HistoriqueStatutDTO> 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<DemandeAideResponse> rechercherAvecFiltres(Map<String, Object> filtres) {
|
|
LOG.debugf("Recherche de demandes avec filtres: %s", filtres);
|
|
|
|
List<DemandeAideResponse> 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<DemandeAideResponse> obtenirDemandesUrgentes(UUID organisationId) {
|
|
LOG.debugf("Récupération des demandes urgentes pour: %s", organisationId);
|
|
|
|
Map<String, Object> 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<DemandeAideResponse> 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<String, Object> filtres) {
|
|
for (Map.Entry<String, Object> 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 les demandes depuis la base avec une limite de 1000 enregistrements.
|
|
* Log un avertissement si le résultat dépasse 500 éléments pour anticiper les risques OOM.
|
|
*/
|
|
private List<DemandeAideResponse> chargerToutesLesDemandesDepuisBDD() {
|
|
int limite = 1000;
|
|
List<DemandeAide> entities = demandeAideRepository.findAll(
|
|
Page.ofSize(limite),
|
|
Sort.by("dateDemande", Sort.Direction.Descending)
|
|
);
|
|
if (entities.size() > 500) {
|
|
LOG.warnf("chargerToutesLesDemandesDepuisBDD : %d demandes chargées en mémoire — risque OOM si la volumétrie continue de croître", entities.size());
|
|
}
|
|
return entities.stream()
|
|
.map(demandeAideMapper::toDTO)
|
|
.collect(Collectors.toList());
|
|
}
|
|
}
|