Configure Maven repository for unionflow-server-api dependency
This commit is contained in:
@@ -0,0 +1,559 @@
|
||||
package dev.lions.unionflow.server.service;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.finance.AdhesionDTO;
|
||||
import dev.lions.unionflow.server.entity.Adhesion;
|
||||
import dev.lions.unionflow.server.entity.Membre;
|
||||
import dev.lions.unionflow.server.entity.Organisation;
|
||||
import dev.lions.unionflow.server.repository.AdhesionRepository;
|
||||
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 jakarta.ws.rs.NotFoundException;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Service métier pour la gestion des adhésions
|
||||
* Contient la logique métier et les règles de validation
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 1.0
|
||||
* @since 2025-01-17
|
||||
*/
|
||||
@ApplicationScoped
|
||||
@Slf4j
|
||||
public class AdhesionService {
|
||||
|
||||
@Inject AdhesionRepository adhesionRepository;
|
||||
|
||||
@Inject MembreRepository membreRepository;
|
||||
|
||||
@Inject OrganisationRepository organisationRepository;
|
||||
|
||||
/**
|
||||
* Récupère toutes les adhésions avec pagination
|
||||
*
|
||||
* @param page numéro de page (0-based)
|
||||
* @param size taille de la page
|
||||
* @return liste des adhésions converties en DTO
|
||||
*/
|
||||
public List<AdhesionDTO> getAllAdhesions(int page, int size) {
|
||||
log.debug("Récupération des adhésions - page: {}, size: {}", page, size);
|
||||
|
||||
jakarta.persistence.TypedQuery<Adhesion> query =
|
||||
adhesionRepository
|
||||
.getEntityManager()
|
||||
.createQuery(
|
||||
"SELECT a FROM Adhesion a ORDER BY a.dateDemande DESC", Adhesion.class);
|
||||
query.setFirstResult(page * size);
|
||||
query.setMaxResults(size);
|
||||
List<Adhesion> adhesions = query.getResultList();
|
||||
|
||||
return adhesions.stream().map(this::convertToDTO).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère une adhésion par son ID
|
||||
*
|
||||
* @param id identifiant UUID de l'adhésion
|
||||
* @return DTO de l'adhésion
|
||||
* @throws NotFoundException si l'adhésion n'existe pas
|
||||
*/
|
||||
public AdhesionDTO getAdhesionById(@NotNull UUID id) {
|
||||
log.debug("Récupération de l'adhésion avec ID: {}", id);
|
||||
|
||||
Adhesion adhesion =
|
||||
adhesionRepository
|
||||
.findByIdOptional(id)
|
||||
.orElseThrow(() -> new NotFoundException("Adhésion non trouvée avec l'ID: " + id));
|
||||
|
||||
return convertToDTO(adhesion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère une adhésion par son numéro de référence
|
||||
*
|
||||
* @param numeroReference numéro de référence unique
|
||||
* @return DTO de l'adhésion
|
||||
* @throws NotFoundException si l'adhésion n'existe pas
|
||||
*/
|
||||
public AdhesionDTO getAdhesionByReference(@NotNull String numeroReference) {
|
||||
log.debug("Récupération de l'adhésion avec référence: {}", numeroReference);
|
||||
|
||||
Adhesion adhesion =
|
||||
adhesionRepository
|
||||
.findByNumeroReference(numeroReference)
|
||||
.orElseThrow(
|
||||
() ->
|
||||
new NotFoundException(
|
||||
"Adhésion non trouvée avec la référence: " + numeroReference));
|
||||
|
||||
return convertToDTO(adhesion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée une nouvelle adhésion
|
||||
*
|
||||
* @param adhesionDTO données de l'adhésion à créer
|
||||
* @return DTO de l'adhésion créée
|
||||
*/
|
||||
@Transactional
|
||||
public AdhesionDTO createAdhesion(@Valid AdhesionDTO adhesionDTO) {
|
||||
log.info(
|
||||
"Création d'une nouvelle adhésion pour le membre: {} et l'organisation: {}",
|
||||
adhesionDTO.getMembreId(),
|
||||
adhesionDTO.getOrganisationId());
|
||||
|
||||
// Validation du membre
|
||||
Membre membre =
|
||||
membreRepository
|
||||
.findByIdOptional(adhesionDTO.getMembreId())
|
||||
.orElseThrow(
|
||||
() ->
|
||||
new NotFoundException(
|
||||
"Membre non trouvé avec l'ID: " + adhesionDTO.getMembreId()));
|
||||
|
||||
// Validation de l'organisation
|
||||
Organisation organisation =
|
||||
organisationRepository
|
||||
.findByIdOptional(adhesionDTO.getOrganisationId())
|
||||
.orElseThrow(
|
||||
() ->
|
||||
new NotFoundException(
|
||||
"Organisation non trouvée avec l'ID: " + adhesionDTO.getOrganisationId()));
|
||||
|
||||
// Conversion DTO vers entité
|
||||
Adhesion adhesion = convertToEntity(adhesionDTO);
|
||||
adhesion.setMembre(membre);
|
||||
adhesion.setOrganisation(organisation);
|
||||
|
||||
// Génération automatique du numéro de référence si absent
|
||||
if (adhesion.getNumeroReference() == null || adhesion.getNumeroReference().isEmpty()) {
|
||||
adhesion.setNumeroReference(genererNumeroReference());
|
||||
}
|
||||
|
||||
// Initialisation par défaut
|
||||
if (adhesion.getDateDemande() == null) {
|
||||
adhesion.setDateDemande(LocalDate.now());
|
||||
}
|
||||
if (adhesion.getStatut() == null || adhesion.getStatut().isEmpty()) {
|
||||
adhesion.setStatut("EN_ATTENTE");
|
||||
}
|
||||
if (adhesion.getMontantPaye() == null) {
|
||||
adhesion.setMontantPaye(BigDecimal.ZERO);
|
||||
}
|
||||
if (adhesion.getCodeDevise() == null || adhesion.getCodeDevise().isEmpty()) {
|
||||
adhesion.setCodeDevise("XOF");
|
||||
}
|
||||
|
||||
// Persistance
|
||||
adhesionRepository.persist(adhesion);
|
||||
|
||||
log.info(
|
||||
"Adhésion créée avec succès - ID: {}, Référence: {}",
|
||||
adhesion.getId(),
|
||||
adhesion.getNumeroReference());
|
||||
|
||||
return convertToDTO(adhesion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour une adhésion existante
|
||||
*
|
||||
* @param id identifiant UUID de l'adhésion
|
||||
* @param adhesionDTO nouvelles données
|
||||
* @return DTO de l'adhésion mise à jour
|
||||
*/
|
||||
@Transactional
|
||||
public AdhesionDTO updateAdhesion(@NotNull UUID id, @Valid AdhesionDTO adhesionDTO) {
|
||||
log.info("Mise à jour de l'adhésion avec ID: {}", id);
|
||||
|
||||
Adhesion adhesionExistante =
|
||||
adhesionRepository
|
||||
.findByIdOptional(id)
|
||||
.orElseThrow(() -> new NotFoundException("Adhésion non trouvée avec l'ID: " + id));
|
||||
|
||||
// Mise à jour des champs modifiables
|
||||
updateAdhesionFields(adhesionExistante, adhesionDTO);
|
||||
|
||||
log.info("Adhésion mise à jour avec succès - ID: {}", id);
|
||||
|
||||
return convertToDTO(adhesionExistante);
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime (désactive) une adhésion
|
||||
*
|
||||
* @param id identifiant UUID de l'adhésion
|
||||
*/
|
||||
@Transactional
|
||||
public void deleteAdhesion(@NotNull UUID id) {
|
||||
log.info("Suppression de l'adhésion avec ID: {}", id);
|
||||
|
||||
Adhesion adhesion =
|
||||
adhesionRepository
|
||||
.findByIdOptional(id)
|
||||
.orElseThrow(() -> new NotFoundException("Adhésion non trouvée avec l'ID: " + id));
|
||||
|
||||
// Vérification si l'adhésion peut être supprimée
|
||||
if ("PAYEE".equals(adhesion.getStatut())) {
|
||||
throw new IllegalStateException("Impossible de supprimer une adhésion déjà payée");
|
||||
}
|
||||
|
||||
adhesion.setStatut("ANNULEE");
|
||||
|
||||
log.info("Adhésion supprimée avec succès - ID: {}", id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Approuve une adhésion
|
||||
*
|
||||
* @param id identifiant UUID de l'adhésion
|
||||
* @param approuvePar nom de l'utilisateur qui approuve
|
||||
* @return DTO de l'adhésion approuvée
|
||||
*/
|
||||
@Transactional
|
||||
public AdhesionDTO approuverAdhesion(@NotNull UUID id, String approuvePar) {
|
||||
log.info("Approbation de l'adhésion avec ID: {}", id);
|
||||
|
||||
Adhesion adhesion =
|
||||
adhesionRepository
|
||||
.findByIdOptional(id)
|
||||
.orElseThrow(() -> new NotFoundException("Adhésion non trouvée avec l'ID: " + id));
|
||||
|
||||
if (!"EN_ATTENTE".equals(adhesion.getStatut())) {
|
||||
throw new IllegalStateException(
|
||||
"Seules les adhésions en attente peuvent être approuvées");
|
||||
}
|
||||
|
||||
adhesion.setStatut("APPROUVEE");
|
||||
adhesion.setDateApprobation(LocalDate.now());
|
||||
adhesion.setApprouvePar(approuvePar);
|
||||
adhesion.setDateValidation(LocalDate.now());
|
||||
|
||||
log.info("Adhésion approuvée avec succès - ID: {}", id);
|
||||
|
||||
return convertToDTO(adhesion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rejette une adhésion
|
||||
*
|
||||
* @param id identifiant UUID de l'adhésion
|
||||
* @param motifRejet motif du rejet
|
||||
* @return DTO de l'adhésion rejetée
|
||||
*/
|
||||
@Transactional
|
||||
public AdhesionDTO rejeterAdhesion(@NotNull UUID id, String motifRejet) {
|
||||
log.info("Rejet de l'adhésion avec ID: {}", id);
|
||||
|
||||
Adhesion adhesion =
|
||||
adhesionRepository
|
||||
.findByIdOptional(id)
|
||||
.orElseThrow(() -> new NotFoundException("Adhésion non trouvée avec l'ID: " + id));
|
||||
|
||||
if (!"EN_ATTENTE".equals(adhesion.getStatut())) {
|
||||
throw new IllegalStateException("Seules les adhésions en attente peuvent être rejetées");
|
||||
}
|
||||
|
||||
adhesion.setStatut("REJETEE");
|
||||
adhesion.setMotifRejet(motifRejet);
|
||||
|
||||
log.info("Adhésion rejetée avec succès - ID: {}", id);
|
||||
|
||||
return convertToDTO(adhesion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enregistre un paiement pour une adhésion
|
||||
*
|
||||
* @param id identifiant UUID de l'adhésion
|
||||
* @param montantPaye montant payé
|
||||
* @param methodePaiement méthode de paiement
|
||||
* @param referencePaiement référence du paiement
|
||||
* @return DTO de l'adhésion mise à jour
|
||||
*/
|
||||
@Transactional
|
||||
public AdhesionDTO enregistrerPaiement(
|
||||
@NotNull UUID id,
|
||||
BigDecimal montantPaye,
|
||||
String methodePaiement,
|
||||
String referencePaiement) {
|
||||
log.info("Enregistrement du paiement pour l'adhésion avec ID: {}", id);
|
||||
|
||||
Adhesion adhesion =
|
||||
adhesionRepository
|
||||
.findByIdOptional(id)
|
||||
.orElseThrow(() -> new NotFoundException("Adhésion non trouvée avec l'ID: " + id));
|
||||
|
||||
if (!"APPROUVEE".equals(adhesion.getStatut()) && !"EN_PAIEMENT".equals(adhesion.getStatut())) {
|
||||
throw new IllegalStateException(
|
||||
"Seules les adhésions approuvées peuvent recevoir un paiement");
|
||||
}
|
||||
|
||||
BigDecimal nouveauMontantPaye =
|
||||
adhesion.getMontantPaye() != null
|
||||
? adhesion.getMontantPaye().add(montantPaye)
|
||||
: montantPaye;
|
||||
|
||||
adhesion.setMontantPaye(nouveauMontantPaye);
|
||||
adhesion.setMethodePaiement(methodePaiement);
|
||||
adhesion.setReferencePaiement(referencePaiement);
|
||||
adhesion.setDatePaiement(java.time.LocalDateTime.now());
|
||||
|
||||
// Mise à jour du statut si payée intégralement
|
||||
if (adhesion.isPayeeIntegralement()) {
|
||||
adhesion.setStatut("PAYEE");
|
||||
} else {
|
||||
adhesion.setStatut("EN_PAIEMENT");
|
||||
}
|
||||
|
||||
log.info("Paiement enregistré avec succès pour l'adhésion - ID: {}", id);
|
||||
|
||||
return convertToDTO(adhesion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les adhésions d'un membre
|
||||
*
|
||||
* @param membreId identifiant UUID du membre
|
||||
* @param page numéro de page
|
||||
* @param size taille de la page
|
||||
* @return liste des adhésions du membre
|
||||
*/
|
||||
public List<AdhesionDTO> getAdhesionsByMembre(@NotNull UUID membreId, int page, int size) {
|
||||
log.debug("Récupération des adhésions du membre: {}", membreId);
|
||||
|
||||
if (!membreRepository.findByIdOptional(membreId).isPresent()) {
|
||||
throw new NotFoundException("Membre non trouvé avec l'ID: " + membreId);
|
||||
}
|
||||
|
||||
List<Adhesion> adhesions =
|
||||
adhesionRepository.findByMembreId(membreId).stream()
|
||||
.skip(page * size)
|
||||
.limit(size)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return adhesions.stream().map(this::convertToDTO).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les adhésions d'une organisation
|
||||
*
|
||||
* @param organisationId identifiant UUID de l'organisation
|
||||
* @param page numéro de page
|
||||
* @param size taille de la page
|
||||
* @return liste des adhésions de l'organisation
|
||||
*/
|
||||
public List<AdhesionDTO> getAdhesionsByOrganisation(
|
||||
@NotNull UUID organisationId, int page, int size) {
|
||||
log.debug("Récupération des adhésions de l'organisation: {}", organisationId);
|
||||
|
||||
if (!organisationRepository.findByIdOptional(organisationId).isPresent()) {
|
||||
throw new NotFoundException("Organisation non trouvée avec l'ID: " + organisationId);
|
||||
}
|
||||
|
||||
List<Adhesion> adhesions =
|
||||
adhesionRepository.findByOrganisationId(organisationId).stream()
|
||||
.skip(page * size)
|
||||
.limit(size)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return adhesions.stream().map(this::convertToDTO).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les adhésions par statut
|
||||
*
|
||||
* @param statut statut recherché
|
||||
* @param page numéro de page
|
||||
* @param size taille de la page
|
||||
* @return liste des adhésions avec le statut spécifié
|
||||
*/
|
||||
public List<AdhesionDTO> getAdhesionsByStatut(@NotNull String statut, int page, int size) {
|
||||
log.debug("Récupération des adhésions avec statut: {}", statut);
|
||||
|
||||
List<Adhesion> adhesions =
|
||||
adhesionRepository.findByStatut(statut).stream()
|
||||
.skip(page * size)
|
||||
.limit(size)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return adhesions.stream().map(this::convertToDTO).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les adhésions en attente
|
||||
*
|
||||
* @param page numéro de page
|
||||
* @param size taille de la page
|
||||
* @return liste des adhésions en attente
|
||||
*/
|
||||
public List<AdhesionDTO> getAdhesionsEnAttente(int page, int size) {
|
||||
log.debug("Récupération des adhésions en attente");
|
||||
|
||||
List<Adhesion> adhesions =
|
||||
adhesionRepository.findEnAttente().stream()
|
||||
.skip(page * size)
|
||||
.limit(size)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return adhesions.stream().map(this::convertToDTO).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les statistiques des adhésions
|
||||
*
|
||||
* @return map contenant les statistiques
|
||||
*/
|
||||
public Map<String, Object> getStatistiquesAdhesions() {
|
||||
log.debug("Calcul des statistiques des adhésions");
|
||||
|
||||
long totalAdhesions = adhesionRepository.count();
|
||||
long adhesionsApprouvees = adhesionRepository.findByStatut("APPROUVEE").size();
|
||||
long adhesionsEnAttente = adhesionRepository.findEnAttente().size();
|
||||
long adhesionsPayees = adhesionRepository.findByStatut("PAYEE").size();
|
||||
|
||||
return Map.of(
|
||||
"totalAdhesions", totalAdhesions,
|
||||
"adhesionsApprouvees", adhesionsApprouvees,
|
||||
"adhesionsEnAttente", adhesionsEnAttente,
|
||||
"adhesionsPayees", adhesionsPayees,
|
||||
"tauxApprobation",
|
||||
totalAdhesions > 0 ? (adhesionsApprouvees * 100.0 / totalAdhesions) : 0.0,
|
||||
"tauxPaiement",
|
||||
adhesionsApprouvees > 0
|
||||
? (adhesionsPayees * 100.0 / adhesionsApprouvees)
|
||||
: 0.0);
|
||||
}
|
||||
|
||||
/** Génère un numéro de référence unique pour une adhésion */
|
||||
private String genererNumeroReference() {
|
||||
return "ADH-" + System.currentTimeMillis() + "-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
|
||||
}
|
||||
|
||||
/** Convertit une entité Adhesion en DTO */
|
||||
private AdhesionDTO convertToDTO(Adhesion adhesion) {
|
||||
if (adhesion == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
AdhesionDTO dto = new AdhesionDTO();
|
||||
|
||||
dto.setId(adhesion.getId());
|
||||
dto.setNumeroReference(adhesion.getNumeroReference());
|
||||
|
||||
// Conversion du membre associé
|
||||
if (adhesion.getMembre() != null) {
|
||||
dto.setMembreId(adhesion.getMembre().getId());
|
||||
dto.setNomMembre(adhesion.getMembre().getNomComplet());
|
||||
dto.setNumeroMembre(adhesion.getMembre().getNumeroMembre());
|
||||
dto.setEmailMembre(adhesion.getMembre().getEmail());
|
||||
}
|
||||
|
||||
// Conversion de l'organisation
|
||||
if (adhesion.getOrganisation() != null) {
|
||||
dto.setOrganisationId(adhesion.getOrganisation().getId());
|
||||
dto.setNomOrganisation(adhesion.getOrganisation().getNom());
|
||||
}
|
||||
|
||||
// Propriétés de l'adhésion
|
||||
dto.setDateDemande(adhesion.getDateDemande());
|
||||
dto.setFraisAdhesion(adhesion.getFraisAdhesion());
|
||||
dto.setMontantPaye(adhesion.getMontantPaye());
|
||||
dto.setCodeDevise(adhesion.getCodeDevise());
|
||||
dto.setStatut(adhesion.getStatut());
|
||||
dto.setDateApprobation(adhesion.getDateApprobation());
|
||||
dto.setDatePaiement(adhesion.getDatePaiement());
|
||||
dto.setMethodePaiement(adhesion.getMethodePaiement());
|
||||
dto.setReferencePaiement(adhesion.getReferencePaiement());
|
||||
dto.setMotifRejet(adhesion.getMotifRejet());
|
||||
dto.setObservations(adhesion.getObservations());
|
||||
dto.setApprouvePar(adhesion.getApprouvePar());
|
||||
dto.setDateValidation(adhesion.getDateValidation());
|
||||
|
||||
// Métadonnées de BaseEntity
|
||||
dto.setDateCreation(adhesion.getDateCreation());
|
||||
dto.setDateModification(adhesion.getDateModification());
|
||||
dto.setCreePar(adhesion.getCreePar());
|
||||
dto.setModifiePar(adhesion.getModifiePar());
|
||||
dto.setActif(adhesion.getActif());
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
/** Convertit un DTO en entité Adhesion */
|
||||
private Adhesion convertToEntity(AdhesionDTO dto) {
|
||||
if (dto == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Adhesion adhesion = new Adhesion();
|
||||
|
||||
adhesion.setNumeroReference(dto.getNumeroReference());
|
||||
adhesion.setDateDemande(dto.getDateDemande());
|
||||
adhesion.setFraisAdhesion(dto.getFraisAdhesion());
|
||||
adhesion.setMontantPaye(dto.getMontantPaye() != null ? dto.getMontantPaye() : BigDecimal.ZERO);
|
||||
adhesion.setCodeDevise(dto.getCodeDevise());
|
||||
adhesion.setStatut(dto.getStatut());
|
||||
adhesion.setDateApprobation(dto.getDateApprobation());
|
||||
adhesion.setDatePaiement(dto.getDatePaiement());
|
||||
adhesion.setMethodePaiement(dto.getMethodePaiement());
|
||||
adhesion.setReferencePaiement(dto.getReferencePaiement());
|
||||
adhesion.setMotifRejet(dto.getMotifRejet());
|
||||
adhesion.setObservations(dto.getObservations());
|
||||
adhesion.setApprouvePar(dto.getApprouvePar());
|
||||
adhesion.setDateValidation(dto.getDateValidation());
|
||||
|
||||
return adhesion;
|
||||
}
|
||||
|
||||
/** Met à jour les champs modifiables d'une adhésion existante */
|
||||
private void updateAdhesionFields(Adhesion adhesion, AdhesionDTO dto) {
|
||||
if (dto.getFraisAdhesion() != null) {
|
||||
adhesion.setFraisAdhesion(dto.getFraisAdhesion());
|
||||
}
|
||||
if (dto.getMontantPaye() != null) {
|
||||
adhesion.setMontantPaye(dto.getMontantPaye());
|
||||
}
|
||||
if (dto.getStatut() != null) {
|
||||
adhesion.setStatut(dto.getStatut());
|
||||
}
|
||||
if (dto.getDateApprobation() != null) {
|
||||
adhesion.setDateApprobation(dto.getDateApprobation());
|
||||
}
|
||||
if (dto.getDatePaiement() != null) {
|
||||
adhesion.setDatePaiement(dto.getDatePaiement());
|
||||
}
|
||||
if (dto.getMethodePaiement() != null) {
|
||||
adhesion.setMethodePaiement(dto.getMethodePaiement());
|
||||
}
|
||||
if (dto.getReferencePaiement() != null) {
|
||||
adhesion.setReferencePaiement(dto.getReferencePaiement());
|
||||
}
|
||||
if (dto.getMotifRejet() != null) {
|
||||
adhesion.setMotifRejet(dto.getMotifRejet());
|
||||
}
|
||||
if (dto.getObservations() != null) {
|
||||
adhesion.setObservations(dto.getObservations());
|
||||
}
|
||||
if (dto.getApprouvePar() != null) {
|
||||
adhesion.setApprouvePar(dto.getApprouvePar());
|
||||
}
|
||||
if (dto.getDateValidation() != null) {
|
||||
adhesion.setDateValidation(dto.getDateValidation());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,353 @@
|
||||
package dev.lions.unionflow.server.service;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.adresse.AdresseDTO;
|
||||
import dev.lions.unionflow.server.api.enums.adresse.TypeAdresse;
|
||||
import dev.lions.unionflow.server.entity.Adresse;
|
||||
import dev.lions.unionflow.server.entity.Evenement;
|
||||
import dev.lions.unionflow.server.entity.Membre;
|
||||
import dev.lions.unionflow.server.entity.Organisation;
|
||||
import dev.lions.unionflow.server.repository.AdresseRepository;
|
||||
import dev.lions.unionflow.server.repository.EvenementRepository;
|
||||
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.ws.rs.NotFoundException;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
/**
|
||||
* Service métier pour la gestion des adresses
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 3.0
|
||||
* @since 2025-01-29
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class AdresseService {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(AdresseService.class);
|
||||
|
||||
@Inject AdresseRepository adresseRepository;
|
||||
|
||||
@Inject OrganisationRepository organisationRepository;
|
||||
|
||||
@Inject MembreRepository membreRepository;
|
||||
|
||||
@Inject EvenementRepository evenementRepository;
|
||||
|
||||
/**
|
||||
* Crée une nouvelle adresse
|
||||
*
|
||||
* @param adresseDTO DTO de l'adresse à créer
|
||||
* @return DTO de l'adresse créée
|
||||
*/
|
||||
@Transactional
|
||||
public AdresseDTO creerAdresse(AdresseDTO adresseDTO) {
|
||||
LOG.infof("Création d'une nouvelle adresse de type: %s", adresseDTO.getTypeAdresse());
|
||||
|
||||
Adresse adresse = convertToEntity(adresseDTO);
|
||||
|
||||
// Gestion de l'adresse principale
|
||||
if (Boolean.TRUE.equals(adresseDTO.getPrincipale())) {
|
||||
desactiverAutresPrincipales(adresseDTO);
|
||||
}
|
||||
|
||||
adresseRepository.persist(adresse);
|
||||
LOG.infof("Adresse créée avec succès: ID=%s", adresse.getId());
|
||||
|
||||
return convertToDTO(adresse);
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour une adresse existante
|
||||
*
|
||||
* @param id ID de l'adresse
|
||||
* @param adresseDTO DTO avec les nouvelles données
|
||||
* @return DTO de l'adresse mise à jour
|
||||
*/
|
||||
@Transactional
|
||||
public AdresseDTO mettreAJourAdresse(UUID id, AdresseDTO adresseDTO) {
|
||||
LOG.infof("Mise à jour de l'adresse ID: %s", id);
|
||||
|
||||
Adresse adresse =
|
||||
adresseRepository
|
||||
.findAdresseById(id)
|
||||
.orElseThrow(() -> new NotFoundException("Adresse non trouvée avec l'ID: " + id));
|
||||
|
||||
// Mise à jour des champs
|
||||
updateFromDTO(adresse, adresseDTO);
|
||||
|
||||
// Gestion de l'adresse principale
|
||||
if (Boolean.TRUE.equals(adresseDTO.getPrincipale())) {
|
||||
desactiverAutresPrincipales(adresseDTO);
|
||||
}
|
||||
|
||||
adresseRepository.persist(adresse);
|
||||
LOG.infof("Adresse mise à jour avec succès: ID=%s", id);
|
||||
|
||||
return convertToDTO(adresse);
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime une adresse
|
||||
*
|
||||
* @param id ID de l'adresse
|
||||
*/
|
||||
@Transactional
|
||||
public void supprimerAdresse(UUID id) {
|
||||
LOG.infof("Suppression de l'adresse ID: %s", id);
|
||||
|
||||
Adresse adresse =
|
||||
adresseRepository
|
||||
.findAdresseById(id)
|
||||
.orElseThrow(() -> new NotFoundException("Adresse non trouvée avec l'ID: " + id));
|
||||
|
||||
adresseRepository.delete(adresse);
|
||||
LOG.infof("Adresse supprimée avec succès: ID=%s", id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve une adresse par son ID
|
||||
*
|
||||
* @param id ID de l'adresse
|
||||
* @return DTO de l'adresse
|
||||
*/
|
||||
public AdresseDTO trouverParId(UUID id) {
|
||||
return adresseRepository
|
||||
.findAdresseById(id)
|
||||
.map(this::convertToDTO)
|
||||
.orElseThrow(() -> new NotFoundException("Adresse non trouvée avec l'ID: " + id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve toutes les adresses d'une organisation
|
||||
*
|
||||
* @param organisationId ID de l'organisation
|
||||
* @return Liste des adresses
|
||||
*/
|
||||
public List<AdresseDTO> trouverParOrganisation(UUID organisationId) {
|
||||
return adresseRepository.findByOrganisationId(organisationId).stream()
|
||||
.map(this::convertToDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve toutes les adresses d'un membre
|
||||
*
|
||||
* @param membreId ID du membre
|
||||
* @return Liste des adresses
|
||||
*/
|
||||
public List<AdresseDTO> trouverParMembre(UUID membreId) {
|
||||
return adresseRepository.findByMembreId(membreId).stream()
|
||||
.map(this::convertToDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve l'adresse d'un événement
|
||||
*
|
||||
* @param evenementId ID de l'événement
|
||||
* @return DTO de l'adresse ou null
|
||||
*/
|
||||
public AdresseDTO trouverParEvenement(UUID evenementId) {
|
||||
return adresseRepository
|
||||
.findByEvenementId(evenementId)
|
||||
.map(this::convertToDTO)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve l'adresse principale d'une organisation
|
||||
*
|
||||
* @param organisationId ID de l'organisation
|
||||
* @return DTO de l'adresse principale ou null
|
||||
*/
|
||||
public AdresseDTO trouverPrincipaleParOrganisation(UUID organisationId) {
|
||||
return adresseRepository
|
||||
.findPrincipaleByOrganisationId(organisationId)
|
||||
.map(this::convertToDTO)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve l'adresse principale d'un membre
|
||||
*
|
||||
* @param membreId ID du membre
|
||||
* @return DTO de l'adresse principale ou null
|
||||
*/
|
||||
public AdresseDTO trouverPrincipaleParMembre(UUID membreId) {
|
||||
return adresseRepository
|
||||
.findPrincipaleByMembreId(membreId)
|
||||
.map(this::convertToDTO)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// MÉTHODES PRIVÉES
|
||||
// ========================================
|
||||
|
||||
/** Désactive les autres adresses principales pour la même entité */
|
||||
private void desactiverAutresPrincipales(AdresseDTO adresseDTO) {
|
||||
List<Adresse> autresPrincipales;
|
||||
|
||||
if (adresseDTO.getOrganisationId() != null) {
|
||||
autresPrincipales =
|
||||
adresseRepository
|
||||
.find("organisation.id = ?1 AND principale = true", adresseDTO.getOrganisationId())
|
||||
.list();
|
||||
} else if (adresseDTO.getMembreId() != null) {
|
||||
autresPrincipales =
|
||||
adresseRepository
|
||||
.find("membre.id = ?1 AND principale = true", adresseDTO.getMembreId())
|
||||
.list();
|
||||
} else {
|
||||
return; // Pas d'entité associée
|
||||
}
|
||||
|
||||
autresPrincipales.forEach(adr -> adr.setPrincipale(false));
|
||||
}
|
||||
|
||||
/** Convertit une entité en DTO */
|
||||
private AdresseDTO convertToDTO(Adresse adresse) {
|
||||
if (adresse == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
AdresseDTO dto = new AdresseDTO();
|
||||
dto.setId(adresse.getId());
|
||||
dto.setTypeAdresse(convertTypeAdresse(adresse.getTypeAdresse()));
|
||||
dto.setAdresse(adresse.getAdresse());
|
||||
dto.setComplementAdresse(adresse.getComplementAdresse());
|
||||
dto.setCodePostal(adresse.getCodePostal());
|
||||
dto.setVille(adresse.getVille());
|
||||
dto.setRegion(adresse.getRegion());
|
||||
dto.setPays(adresse.getPays());
|
||||
dto.setLatitude(adresse.getLatitude());
|
||||
dto.setLongitude(adresse.getLongitude());
|
||||
dto.setPrincipale(adresse.getPrincipale());
|
||||
dto.setLibelle(adresse.getLibelle());
|
||||
dto.setNotes(adresse.getNotes());
|
||||
|
||||
if (adresse.getOrganisation() != null) {
|
||||
dto.setOrganisationId(adresse.getOrganisation().getId());
|
||||
}
|
||||
if (adresse.getMembre() != null) {
|
||||
dto.setMembreId(adresse.getMembre().getId());
|
||||
}
|
||||
if (adresse.getEvenement() != null) {
|
||||
dto.setEvenementId(adresse.getEvenement().getId());
|
||||
}
|
||||
|
||||
dto.setAdresseComplete(adresse.getAdresseComplete());
|
||||
dto.setDateCreation(adresse.getDateCreation());
|
||||
dto.setDateModification(adresse.getDateModification());
|
||||
dto.setActif(adresse.getActif());
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
/** Convertit un DTO en entité */
|
||||
private Adresse convertToEntity(AdresseDTO dto) {
|
||||
if (dto == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Adresse adresse = new Adresse();
|
||||
adresse.setTypeAdresse(convertTypeAdresse(dto.getTypeAdresse()));
|
||||
adresse.setAdresse(dto.getAdresse());
|
||||
adresse.setComplementAdresse(dto.getComplementAdresse());
|
||||
adresse.setCodePostal(dto.getCodePostal());
|
||||
adresse.setVille(dto.getVille());
|
||||
adresse.setRegion(dto.getRegion());
|
||||
adresse.setPays(dto.getPays());
|
||||
adresse.setLatitude(dto.getLatitude());
|
||||
adresse.setLongitude(dto.getLongitude());
|
||||
adresse.setPrincipale(dto.getPrincipale() != null ? dto.getPrincipale() : false);
|
||||
adresse.setLibelle(dto.getLibelle());
|
||||
adresse.setNotes(dto.getNotes());
|
||||
|
||||
// Relations
|
||||
if (dto.getOrganisationId() != null) {
|
||||
Organisation org =
|
||||
organisationRepository
|
||||
.findByIdOptional(dto.getOrganisationId())
|
||||
.orElseThrow(
|
||||
() ->
|
||||
new NotFoundException(
|
||||
"Organisation non trouvée avec l'ID: " + dto.getOrganisationId()));
|
||||
adresse.setOrganisation(org);
|
||||
}
|
||||
|
||||
if (dto.getMembreId() != null) {
|
||||
Membre membre =
|
||||
membreRepository
|
||||
.findByIdOptional(dto.getMembreId())
|
||||
.orElseThrow(
|
||||
() -> new NotFoundException("Membre non trouvé avec l'ID: " + dto.getMembreId()));
|
||||
adresse.setMembre(membre);
|
||||
}
|
||||
|
||||
if (dto.getEvenementId() != null) {
|
||||
Evenement evenement =
|
||||
evenementRepository
|
||||
.findByIdOptional(dto.getEvenementId())
|
||||
.orElseThrow(
|
||||
() ->
|
||||
new NotFoundException(
|
||||
"Événement non trouvé avec l'ID: " + dto.getEvenementId()));
|
||||
adresse.setEvenement(evenement);
|
||||
}
|
||||
|
||||
return adresse;
|
||||
}
|
||||
|
||||
/** Met à jour une entité à partir d'un DTO */
|
||||
private void updateFromDTO(Adresse adresse, AdresseDTO dto) {
|
||||
if (dto.getTypeAdresse() != null) {
|
||||
adresse.setTypeAdresse(convertTypeAdresse(dto.getTypeAdresse()));
|
||||
}
|
||||
if (dto.getAdresse() != null) {
|
||||
adresse.setAdresse(dto.getAdresse());
|
||||
}
|
||||
if (dto.getComplementAdresse() != null) {
|
||||
adresse.setComplementAdresse(dto.getComplementAdresse());
|
||||
}
|
||||
if (dto.getCodePostal() != null) {
|
||||
adresse.setCodePostal(dto.getCodePostal());
|
||||
}
|
||||
if (dto.getVille() != null) {
|
||||
adresse.setVille(dto.getVille());
|
||||
}
|
||||
if (dto.getRegion() != null) {
|
||||
adresse.setRegion(dto.getRegion());
|
||||
}
|
||||
if (dto.getPays() != null) {
|
||||
adresse.setPays(dto.getPays());
|
||||
}
|
||||
if (dto.getLatitude() != null) {
|
||||
adresse.setLatitude(dto.getLatitude());
|
||||
}
|
||||
if (dto.getLongitude() != null) {
|
||||
adresse.setLongitude(dto.getLongitude());
|
||||
}
|
||||
if (dto.getPrincipale() != null) {
|
||||
adresse.setPrincipale(dto.getPrincipale());
|
||||
}
|
||||
if (dto.getLibelle() != null) {
|
||||
adresse.setLibelle(dto.getLibelle());
|
||||
}
|
||||
if (dto.getNotes() != null) {
|
||||
adresse.setNotes(dto.getNotes());
|
||||
}
|
||||
}
|
||||
|
||||
/** Convertit TypeAdresse (entité) vers TypeAdresse (DTO) - même enum, pas de conversion nécessaire */
|
||||
private TypeAdresse convertTypeAdresse(TypeAdresse type) {
|
||||
return type != null ? type : TypeAdresse.AUTRE; // Même enum, valeur par défaut si null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,478 @@
|
||||
package dev.lions.unionflow.server.service;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.analytics.AnalyticsDataDTO;
|
||||
import dev.lions.unionflow.server.api.dto.analytics.DashboardWidgetDTO;
|
||||
import dev.lions.unionflow.server.api.dto.analytics.KPITrendDTO;
|
||||
import dev.lions.unionflow.server.api.enums.analytics.PeriodeAnalyse;
|
||||
// import dev.lions.unionflow.server.entity.DemandeAide;
|
||||
import dev.lions.unionflow.server.api.enums.analytics.TypeMetrique;
|
||||
import dev.lions.unionflow.server.repository.CotisationRepository;
|
||||
import dev.lions.unionflow.server.repository.DemandeAideRepository;
|
||||
import dev.lions.unionflow.server.repository.EvenementRepository;
|
||||
// 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 java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Service principal pour les analytics et métriques UnionFlow
|
||||
*
|
||||
* <p>Ce service calcule et fournit toutes les métriques analytics pour les tableaux de bord,
|
||||
* rapports et widgets.
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 1.0
|
||||
* @since 2025-01-16
|
||||
*/
|
||||
@ApplicationScoped
|
||||
@Slf4j
|
||||
public class AnalyticsService {
|
||||
|
||||
@Inject OrganisationRepository organisationRepository;
|
||||
|
||||
@Inject MembreRepository membreRepository;
|
||||
|
||||
@Inject CotisationRepository cotisationRepository;
|
||||
|
||||
@Inject DemandeAideRepository demandeAideRepository;
|
||||
|
||||
@Inject EvenementRepository evenementRepository;
|
||||
|
||||
// @Inject
|
||||
// DemandeAideRepository demandeAideRepository;
|
||||
|
||||
@Inject KPICalculatorService kpiCalculatorService;
|
||||
|
||||
@Inject TrendAnalysisService trendAnalysisService;
|
||||
|
||||
/**
|
||||
* Calcule une métrique analytics pour une période donnée
|
||||
*
|
||||
* @param typeMetrique Le type de métrique à calculer
|
||||
* @param periodeAnalyse La période d'analyse
|
||||
* @param organisationId L'ID de l'organisation (optionnel)
|
||||
* @return Les données analytics calculées
|
||||
*/
|
||||
@Transactional
|
||||
public AnalyticsDataDTO calculerMetrique(
|
||||
TypeMetrique typeMetrique, PeriodeAnalyse periodeAnalyse, UUID organisationId) {
|
||||
log.info(
|
||||
"Calcul de la métrique {} pour la période {} et l'organisation {}",
|
||||
typeMetrique,
|
||||
periodeAnalyse,
|
||||
organisationId);
|
||||
|
||||
LocalDateTime dateDebut = periodeAnalyse.getDateDebut();
|
||||
LocalDateTime dateFin = periodeAnalyse.getDateFin();
|
||||
|
||||
BigDecimal valeur =
|
||||
switch (typeMetrique) {
|
||||
// Métriques membres
|
||||
case NOMBRE_MEMBRES_ACTIFS ->
|
||||
calculerNombreMembresActifs(organisationId, dateDebut, dateFin);
|
||||
case NOMBRE_MEMBRES_INACTIFS ->
|
||||
calculerNombreMembresInactifs(organisationId, dateDebut, dateFin);
|
||||
case TAUX_CROISSANCE_MEMBRES ->
|
||||
calculerTauxCroissanceMembres(organisationId, dateDebut, dateFin);
|
||||
case MOYENNE_AGE_MEMBRES -> calculerMoyenneAgeMembres(organisationId, dateDebut, dateFin);
|
||||
|
||||
// Métriques financières
|
||||
case TOTAL_COTISATIONS_COLLECTEES ->
|
||||
calculerTotalCotisationsCollectees(organisationId, dateDebut, dateFin);
|
||||
case COTISATIONS_EN_ATTENTE ->
|
||||
calculerCotisationsEnAttente(organisationId, dateDebut, dateFin);
|
||||
case TAUX_RECOUVREMENT_COTISATIONS ->
|
||||
calculerTauxRecouvrementCotisations(organisationId, dateDebut, dateFin);
|
||||
case MOYENNE_COTISATION_MEMBRE ->
|
||||
calculerMoyenneCotisationMembre(organisationId, dateDebut, dateFin);
|
||||
|
||||
// Métriques événements
|
||||
case NOMBRE_EVENEMENTS_ORGANISES ->
|
||||
calculerNombreEvenementsOrganises(organisationId, dateDebut, dateFin);
|
||||
case TAUX_PARTICIPATION_EVENEMENTS ->
|
||||
calculerTauxParticipationEvenements(organisationId, dateDebut, dateFin);
|
||||
case MOYENNE_PARTICIPANTS_EVENEMENT ->
|
||||
calculerMoyenneParticipantsEvenement(organisationId, dateDebut, dateFin);
|
||||
|
||||
// Métriques solidarité
|
||||
case NOMBRE_DEMANDES_AIDE ->
|
||||
calculerNombreDemandesAide(organisationId, dateDebut, dateFin);
|
||||
case MONTANT_AIDES_ACCORDEES ->
|
||||
calculerMontantAidesAccordees(organisationId, dateDebut, dateFin);
|
||||
case TAUX_APPROBATION_AIDES ->
|
||||
calculerTauxApprobationAides(organisationId, dateDebut, dateFin);
|
||||
|
||||
default -> BigDecimal.ZERO;
|
||||
};
|
||||
|
||||
// Calcul de la valeur précédente pour comparaison
|
||||
BigDecimal valeurPrecedente =
|
||||
calculerValeurPrecedente(typeMetrique, periodeAnalyse, organisationId);
|
||||
BigDecimal pourcentageEvolution = calculerPourcentageEvolution(valeur, valeurPrecedente);
|
||||
|
||||
return AnalyticsDataDTO.builder()
|
||||
.typeMetrique(typeMetrique)
|
||||
.periodeAnalyse(periodeAnalyse)
|
||||
.valeur(valeur)
|
||||
.valeurPrecedente(valeurPrecedente)
|
||||
.pourcentageEvolution(pourcentageEvolution)
|
||||
.dateDebut(dateDebut)
|
||||
.dateFin(dateFin)
|
||||
.dateCalcul(LocalDateTime.now())
|
||||
.organisationId(organisationId)
|
||||
.nomOrganisation(obtenirNomOrganisation(organisationId))
|
||||
.indicateurFiabilite(new BigDecimal("95.0"))
|
||||
.niveauPriorite(3)
|
||||
.tempsReel(false)
|
||||
.necessiteMiseAJour(false)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule les tendances d'un KPI sur une période
|
||||
*
|
||||
* @param typeMetrique Le type de métrique
|
||||
* @param periodeAnalyse La période d'analyse
|
||||
* @param organisationId L'ID de l'organisation (optionnel)
|
||||
* @return Les données de tendance du KPI
|
||||
*/
|
||||
@Transactional
|
||||
public KPITrendDTO calculerTendanceKPI(
|
||||
TypeMetrique typeMetrique, PeriodeAnalyse periodeAnalyse, UUID organisationId) {
|
||||
log.info(
|
||||
"Calcul de la tendance KPI {} pour la période {} et l'organisation {}",
|
||||
typeMetrique,
|
||||
periodeAnalyse,
|
||||
organisationId);
|
||||
|
||||
return trendAnalysisService.calculerTendance(typeMetrique, periodeAnalyse, organisationId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtient les métriques pour un tableau de bord
|
||||
*
|
||||
* @param organisationId L'ID de l'organisation
|
||||
* @param utilisateurId L'ID de l'utilisateur
|
||||
* @return La liste des widgets du tableau de bord
|
||||
*/
|
||||
@Transactional
|
||||
public List<DashboardWidgetDTO> obtenirMetriquesTableauBord(
|
||||
UUID organisationId, UUID utilisateurId) {
|
||||
log.info(
|
||||
"Obtention des métriques du tableau de bord pour l'organisation {} et l'utilisateur {}",
|
||||
organisationId,
|
||||
utilisateurId);
|
||||
|
||||
List<DashboardWidgetDTO> widgets = new ArrayList<>();
|
||||
|
||||
// Widget KPI Membres Actifs
|
||||
widgets.add(
|
||||
creerWidgetKPI(
|
||||
TypeMetrique.NOMBRE_MEMBRES_ACTIFS,
|
||||
PeriodeAnalyse.CE_MOIS,
|
||||
organisationId,
|
||||
utilisateurId,
|
||||
0,
|
||||
0,
|
||||
3,
|
||||
2));
|
||||
|
||||
// Widget KPI Cotisations
|
||||
widgets.add(
|
||||
creerWidgetKPI(
|
||||
TypeMetrique.TOTAL_COTISATIONS_COLLECTEES,
|
||||
PeriodeAnalyse.CE_MOIS,
|
||||
organisationId,
|
||||
utilisateurId,
|
||||
3,
|
||||
0,
|
||||
3,
|
||||
2));
|
||||
|
||||
// Widget KPI Événements
|
||||
widgets.add(
|
||||
creerWidgetKPI(
|
||||
TypeMetrique.NOMBRE_EVENEMENTS_ORGANISES,
|
||||
PeriodeAnalyse.CE_MOIS,
|
||||
organisationId,
|
||||
utilisateurId,
|
||||
6,
|
||||
0,
|
||||
3,
|
||||
2));
|
||||
|
||||
// Widget KPI Solidarité
|
||||
widgets.add(
|
||||
creerWidgetKPI(
|
||||
TypeMetrique.NOMBRE_DEMANDES_AIDE,
|
||||
PeriodeAnalyse.CE_MOIS,
|
||||
organisationId,
|
||||
utilisateurId,
|
||||
9,
|
||||
0,
|
||||
3,
|
||||
2));
|
||||
|
||||
// Widget Graphique Évolution Membres
|
||||
widgets.add(
|
||||
creerWidgetGraphique(
|
||||
TypeMetrique.NOMBRE_MEMBRES_ACTIFS,
|
||||
PeriodeAnalyse.SIX_DERNIERS_MOIS,
|
||||
organisationId,
|
||||
utilisateurId,
|
||||
0,
|
||||
2,
|
||||
6,
|
||||
4,
|
||||
"line"));
|
||||
|
||||
// Widget Graphique Évolution Financière
|
||||
widgets.add(
|
||||
creerWidgetGraphique(
|
||||
TypeMetrique.TOTAL_COTISATIONS_COLLECTEES,
|
||||
PeriodeAnalyse.SIX_DERNIERS_MOIS,
|
||||
organisationId,
|
||||
utilisateurId,
|
||||
6,
|
||||
2,
|
||||
6,
|
||||
4,
|
||||
"area"));
|
||||
|
||||
return widgets;
|
||||
}
|
||||
|
||||
// === MÉTHODES PRIVÉES DE CALCUL ===
|
||||
|
||||
private BigDecimal calculerNombreMembresActifs(
|
||||
UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||
Long count = membreRepository.countMembresActifs(organisationId, dateDebut, dateFin);
|
||||
return new BigDecimal(count);
|
||||
}
|
||||
|
||||
private BigDecimal calculerNombreMembresInactifs(
|
||||
UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||
Long count = membreRepository.countMembresInactifs(organisationId, dateDebut, dateFin);
|
||||
return new BigDecimal(count);
|
||||
}
|
||||
|
||||
private BigDecimal calculerTauxCroissanceMembres(
|
||||
UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||
Long membresActuels = membreRepository.countMembresActifs(organisationId, dateDebut, dateFin);
|
||||
Long membresPrecedents =
|
||||
membreRepository.countMembresActifs(
|
||||
organisationId, dateDebut.minusMonths(1), dateFin.minusMonths(1));
|
||||
|
||||
if (membresPrecedents == 0) return BigDecimal.ZERO;
|
||||
|
||||
BigDecimal croissance =
|
||||
new BigDecimal(membresActuels - membresPrecedents)
|
||||
.divide(new BigDecimal(membresPrecedents), 4, RoundingMode.HALF_UP)
|
||||
.multiply(new BigDecimal("100"));
|
||||
|
||||
return croissance;
|
||||
}
|
||||
|
||||
private BigDecimal calculerMoyenneAgeMembres(
|
||||
UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||
Double moyenneAge = membreRepository.calculerMoyenneAge(organisationId, dateDebut, dateFin);
|
||||
return moyenneAge != null
|
||||
? new BigDecimal(moyenneAge).setScale(1, RoundingMode.HALF_UP)
|
||||
: BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
private BigDecimal calculerTotalCotisationsCollectees(
|
||||
UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||
BigDecimal total = cotisationRepository.sumMontantsPayes(organisationId, dateDebut, dateFin);
|
||||
return total != null ? total : BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
private BigDecimal calculerCotisationsEnAttente(
|
||||
UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||
BigDecimal total =
|
||||
cotisationRepository.sumMontantsEnAttente(organisationId, dateDebut, dateFin);
|
||||
return total != null ? total : BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
private BigDecimal calculerTauxRecouvrementCotisations(
|
||||
UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||
BigDecimal collectees = calculerTotalCotisationsCollectees(organisationId, dateDebut, dateFin);
|
||||
BigDecimal enAttente = calculerCotisationsEnAttente(organisationId, dateDebut, dateFin);
|
||||
BigDecimal total = collectees.add(enAttente);
|
||||
|
||||
if (total.compareTo(BigDecimal.ZERO) == 0) return BigDecimal.ZERO;
|
||||
|
||||
return collectees.divide(total, 4, RoundingMode.HALF_UP).multiply(new BigDecimal("100"));
|
||||
}
|
||||
|
||||
private BigDecimal calculerMoyenneCotisationMembre(
|
||||
UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||
BigDecimal total = calculerTotalCotisationsCollectees(organisationId, dateDebut, dateFin);
|
||||
Long nombreMembres = membreRepository.countMembresActifs(organisationId, dateDebut, dateFin);
|
||||
|
||||
if (nombreMembres == 0) return BigDecimal.ZERO;
|
||||
|
||||
return total.divide(new BigDecimal(nombreMembres), 2, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
private BigDecimal calculerNombreEvenementsOrganises(
|
||||
UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||
Long count = evenementRepository.countEvenements(organisationId, dateDebut, dateFin);
|
||||
return new BigDecimal(count);
|
||||
}
|
||||
|
||||
private BigDecimal calculerTauxParticipationEvenements(
|
||||
UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||
// Implémentation simplifiée - à enrichir selon les besoins
|
||||
return new BigDecimal("75.5"); // Valeur par défaut
|
||||
}
|
||||
|
||||
private BigDecimal calculerMoyenneParticipantsEvenement(
|
||||
UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||
Double moyenne =
|
||||
evenementRepository.calculerMoyenneParticipants(organisationId, dateDebut, dateFin);
|
||||
return moyenne != null
|
||||
? new BigDecimal(moyenne).setScale(1, RoundingMode.HALF_UP)
|
||||
: BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
private BigDecimal calculerNombreDemandesAide(
|
||||
UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||
Long count = demandeAideRepository.countDemandes(organisationId, dateDebut, dateFin);
|
||||
return new BigDecimal(count);
|
||||
}
|
||||
|
||||
private BigDecimal calculerMontantAidesAccordees(
|
||||
UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||
BigDecimal total =
|
||||
demandeAideRepository.sumMontantsAccordes(organisationId, dateDebut, dateFin);
|
||||
return total != null ? total : BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
private BigDecimal calculerTauxApprobationAides(
|
||||
UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||
Long totalDemandes = demandeAideRepository.countDemandes(organisationId, dateDebut, dateFin);
|
||||
Long demandesApprouvees =
|
||||
demandeAideRepository.countDemandesApprouvees(organisationId, dateDebut, dateFin);
|
||||
|
||||
if (totalDemandes == 0) return BigDecimal.ZERO;
|
||||
|
||||
return new BigDecimal(demandesApprouvees)
|
||||
.divide(new BigDecimal(totalDemandes), 4, RoundingMode.HALF_UP)
|
||||
.multiply(new BigDecimal("100"));
|
||||
}
|
||||
|
||||
private BigDecimal calculerValeurPrecedente(
|
||||
TypeMetrique typeMetrique, PeriodeAnalyse periodeAnalyse, UUID organisationId) {
|
||||
// Calcul de la période précédente
|
||||
LocalDateTime dateDebutPrecedente =
|
||||
periodeAnalyse.getDateDebut().minus(periodeAnalyse.getDuree(), periodeAnalyse.getUnite());
|
||||
LocalDateTime dateFinPrecedente =
|
||||
periodeAnalyse.getDateFin().minus(periodeAnalyse.getDuree(), periodeAnalyse.getUnite());
|
||||
|
||||
return switch (typeMetrique) {
|
||||
case NOMBRE_MEMBRES_ACTIFS ->
|
||||
calculerNombreMembresActifs(organisationId, dateDebutPrecedente, dateFinPrecedente);
|
||||
case TOTAL_COTISATIONS_COLLECTEES ->
|
||||
calculerTotalCotisationsCollectees(
|
||||
organisationId, dateDebutPrecedente, dateFinPrecedente);
|
||||
case NOMBRE_EVENEMENTS_ORGANISES ->
|
||||
calculerNombreEvenementsOrganises(organisationId, dateDebutPrecedente, dateFinPrecedente);
|
||||
case NOMBRE_DEMANDES_AIDE ->
|
||||
calculerNombreDemandesAide(organisationId, dateDebutPrecedente, dateFinPrecedente);
|
||||
default -> BigDecimal.ZERO;
|
||||
};
|
||||
}
|
||||
|
||||
private BigDecimal calculerPourcentageEvolution(
|
||||
BigDecimal valeurActuelle, BigDecimal valeurPrecedente) {
|
||||
if (valeurPrecedente == null || valeurPrecedente.compareTo(BigDecimal.ZERO) == 0) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
return valeurActuelle
|
||||
.subtract(valeurPrecedente)
|
||||
.divide(valeurPrecedente, 4, RoundingMode.HALF_UP)
|
||||
.multiply(new BigDecimal("100"));
|
||||
}
|
||||
|
||||
private String obtenirNomOrganisation(UUID organisationId) {
|
||||
// Temporairement désactivé pour éviter les erreurs de compilation
|
||||
return "Organisation "
|
||||
+ (organisationId != null ? organisationId.toString().substring(0, 8) : "inconnue");
|
||||
}
|
||||
|
||||
private DashboardWidgetDTO creerWidgetKPI(
|
||||
TypeMetrique typeMetrique,
|
||||
PeriodeAnalyse periodeAnalyse,
|
||||
UUID organisationId,
|
||||
UUID utilisateurId,
|
||||
int positionX,
|
||||
int positionY,
|
||||
int largeur,
|
||||
int hauteur) {
|
||||
AnalyticsDataDTO data = calculerMetrique(typeMetrique, periodeAnalyse, organisationId);
|
||||
|
||||
return DashboardWidgetDTO.builder()
|
||||
.titre(typeMetrique.getLibelle())
|
||||
.typeWidget("kpi")
|
||||
.typeMetrique(typeMetrique)
|
||||
.periodeAnalyse(periodeAnalyse)
|
||||
.organisationId(organisationId)
|
||||
.utilisateurProprietaireId(utilisateurId)
|
||||
.positionX(positionX)
|
||||
.positionY(positionY)
|
||||
.largeur(largeur)
|
||||
.hauteur(hauteur)
|
||||
.couleurPrincipale(typeMetrique.getCouleur())
|
||||
.icone(typeMetrique.getIcone())
|
||||
.donneesWidget(convertirEnJSON(data))
|
||||
.dateDerniereMiseAJour(LocalDateTime.now())
|
||||
.build();
|
||||
}
|
||||
|
||||
private DashboardWidgetDTO creerWidgetGraphique(
|
||||
TypeMetrique typeMetrique,
|
||||
PeriodeAnalyse periodeAnalyse,
|
||||
UUID organisationId,
|
||||
UUID utilisateurId,
|
||||
int positionX,
|
||||
int positionY,
|
||||
int largeur,
|
||||
int hauteur,
|
||||
String typeGraphique) {
|
||||
KPITrendDTO trend = calculerTendanceKPI(typeMetrique, periodeAnalyse, organisationId);
|
||||
|
||||
return DashboardWidgetDTO.builder()
|
||||
.titre("Évolution " + typeMetrique.getLibelle())
|
||||
.typeWidget("chart")
|
||||
.typeMetrique(typeMetrique)
|
||||
.periodeAnalyse(periodeAnalyse)
|
||||
.organisationId(organisationId)
|
||||
.utilisateurProprietaireId(utilisateurId)
|
||||
.positionX(positionX)
|
||||
.positionY(positionY)
|
||||
.largeur(largeur)
|
||||
.hauteur(hauteur)
|
||||
.couleurPrincipale(typeMetrique.getCouleur())
|
||||
.icone(typeMetrique.getIcone())
|
||||
.donneesWidget(convertirEnJSON(trend))
|
||||
.configurationVisuelle("{\"type\":\"" + typeGraphique + "\",\"responsive\":true}")
|
||||
.dateDerniereMiseAJour(LocalDateTime.now())
|
||||
.build();
|
||||
}
|
||||
|
||||
private String convertirEnJSON(Object data) {
|
||||
// Implémentation simplifiée - utiliser Jackson en production
|
||||
return "{}"; // À implémenter avec ObjectMapper
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
package dev.lions.unionflow.server.service;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.admin.AuditLogDTO;
|
||||
import dev.lions.unionflow.server.entity.AuditLog;
|
||||
import dev.lions.unionflow.server.repository.AuditLogRepository;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Service pour la gestion des logs d'audit
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 1.0
|
||||
* @since 2025-01-17
|
||||
*/
|
||||
@ApplicationScoped
|
||||
@Slf4j
|
||||
public class AuditService {
|
||||
|
||||
@Inject
|
||||
AuditLogRepository auditLogRepository;
|
||||
|
||||
/**
|
||||
* Enregistre un nouveau log d'audit
|
||||
*/
|
||||
@Transactional
|
||||
public AuditLogDTO enregistrerLog(AuditLogDTO dto) {
|
||||
log.debug("Enregistrement d'un log d'audit: {}", dto.getTypeAction());
|
||||
|
||||
AuditLog auditLog = convertToEntity(dto);
|
||||
auditLogRepository.persist(auditLog);
|
||||
|
||||
return convertToDTO(auditLog);
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère tous les logs avec pagination
|
||||
*/
|
||||
public Map<String, Object> listerTous(int page, int size, String sortBy, String sortOrder) {
|
||||
log.debug("Récupération des logs d'audit - page: {}, size: {}", page, size);
|
||||
|
||||
String orderBy = sortBy != null ? sortBy : "dateHeure";
|
||||
String order = "desc".equalsIgnoreCase(sortOrder) ? "DESC" : "ASC";
|
||||
|
||||
var entityManager = auditLogRepository.getEntityManager();
|
||||
|
||||
// Compter le total
|
||||
long total = auditLogRepository.count();
|
||||
|
||||
// Récupérer les logs avec pagination
|
||||
var query = entityManager.createQuery(
|
||||
"SELECT a FROM AuditLog a ORDER BY a." + orderBy + " " + order, AuditLog.class);
|
||||
query.setFirstResult(page * size);
|
||||
query.setMaxResults(size);
|
||||
|
||||
List<AuditLog> logs = query.getResultList();
|
||||
List<AuditLogDTO> dtos = logs.stream()
|
||||
.map(this::convertToDTO)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return Map.of(
|
||||
"data", dtos,
|
||||
"total", total,
|
||||
"page", page,
|
||||
"size", size,
|
||||
"totalPages", (int) Math.ceil((double) total / size)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recherche les logs avec filtres
|
||||
*/
|
||||
public Map<String, Object> rechercher(
|
||||
LocalDateTime dateDebut, LocalDateTime dateFin,
|
||||
String typeAction, String severite, String utilisateur,
|
||||
String module, String ipAddress,
|
||||
int page, int size) {
|
||||
|
||||
log.debug("Recherche de logs d'audit avec filtres");
|
||||
|
||||
// Construire la requête dynamique avec Criteria API
|
||||
var entityManager = auditLogRepository.getEntityManager();
|
||||
var cb = entityManager.getCriteriaBuilder();
|
||||
var query = cb.createQuery(AuditLog.class);
|
||||
var root = query.from(AuditLog.class);
|
||||
|
||||
var predicates = new ArrayList<jakarta.persistence.criteria.Predicate>();
|
||||
|
||||
if (dateDebut != null) {
|
||||
predicates.add(cb.greaterThanOrEqualTo(root.get("dateHeure"), dateDebut));
|
||||
}
|
||||
if (dateFin != null) {
|
||||
predicates.add(cb.lessThanOrEqualTo(root.get("dateHeure"), dateFin));
|
||||
}
|
||||
if (typeAction != null && !typeAction.isEmpty()) {
|
||||
predicates.add(cb.equal(root.get("typeAction"), typeAction));
|
||||
}
|
||||
if (severite != null && !severite.isEmpty()) {
|
||||
predicates.add(cb.equal(root.get("severite"), severite));
|
||||
}
|
||||
if (utilisateur != null && !utilisateur.isEmpty()) {
|
||||
predicates.add(cb.like(cb.lower(root.get("utilisateur")),
|
||||
"%" + utilisateur.toLowerCase() + "%"));
|
||||
}
|
||||
if (module != null && !module.isEmpty()) {
|
||||
predicates.add(cb.equal(root.get("module"), module));
|
||||
}
|
||||
if (ipAddress != null && !ipAddress.isEmpty()) {
|
||||
predicates.add(cb.like(root.get("ipAddress"), "%" + ipAddress + "%"));
|
||||
}
|
||||
|
||||
query.where(predicates.toArray(new jakarta.persistence.criteria.Predicate[0]));
|
||||
query.orderBy(cb.desc(root.get("dateHeure")));
|
||||
|
||||
// Compter le total
|
||||
var countQuery = cb.createQuery(Long.class);
|
||||
countQuery.select(cb.count(countQuery.from(AuditLog.class)));
|
||||
countQuery.where(predicates.toArray(new jakarta.persistence.criteria.Predicate[0]));
|
||||
long total = entityManager.createQuery(countQuery).getSingleResult();
|
||||
|
||||
// Récupérer les résultats avec pagination
|
||||
var typedQuery = entityManager.createQuery(query);
|
||||
typedQuery.setFirstResult(page * size);
|
||||
typedQuery.setMaxResults(size);
|
||||
|
||||
List<AuditLog> logs = typedQuery.getResultList();
|
||||
List<AuditLogDTO> dtos = logs.stream()
|
||||
.map(this::convertToDTO)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return Map.of(
|
||||
"data", dtos,
|
||||
"total", total,
|
||||
"page", page,
|
||||
"size", size,
|
||||
"totalPages", (int) Math.ceil((double) total / size)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les statistiques d'audit
|
||||
*/
|
||||
public Map<String, Object> getStatistiques() {
|
||||
long total = auditLogRepository.count();
|
||||
|
||||
var entityManager = auditLogRepository.getEntityManager();
|
||||
|
||||
long success = entityManager.createQuery(
|
||||
"SELECT COUNT(a) FROM AuditLog a WHERE a.severite = :severite", Long.class)
|
||||
.setParameter("severite", "SUCCESS")
|
||||
.getSingleResult();
|
||||
|
||||
long errors = entityManager.createQuery(
|
||||
"SELECT COUNT(a) FROM AuditLog a WHERE a.severite IN :severites", Long.class)
|
||||
.setParameter("severites", List.of("ERROR", "CRITICAL"))
|
||||
.getSingleResult();
|
||||
|
||||
long warnings = entityManager.createQuery(
|
||||
"SELECT COUNT(a) FROM AuditLog a WHERE a.severite = :severite", Long.class)
|
||||
.setParameter("severite", "WARNING")
|
||||
.getSingleResult();
|
||||
|
||||
return Map.of(
|
||||
"total", total,
|
||||
"success", success,
|
||||
"errors", errors,
|
||||
"warnings", warnings
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit une entité en DTO
|
||||
*/
|
||||
private AuditLogDTO convertToDTO(AuditLog auditLog) {
|
||||
AuditLogDTO dto = new AuditLogDTO();
|
||||
dto.setId(auditLog.getId());
|
||||
dto.setTypeAction(auditLog.getTypeAction());
|
||||
dto.setSeverite(auditLog.getSeverite());
|
||||
dto.setUtilisateur(auditLog.getUtilisateur());
|
||||
dto.setRole(auditLog.getRole());
|
||||
dto.setModule(auditLog.getModule());
|
||||
dto.setDescription(auditLog.getDescription());
|
||||
dto.setDetails(auditLog.getDetails());
|
||||
dto.setIpAddress(auditLog.getIpAddress());
|
||||
dto.setUserAgent(auditLog.getUserAgent());
|
||||
dto.setSessionId(auditLog.getSessionId());
|
||||
dto.setDateHeure(auditLog.getDateHeure());
|
||||
dto.setDonneesAvant(auditLog.getDonneesAvant());
|
||||
dto.setDonneesApres(auditLog.getDonneesApres());
|
||||
dto.setEntiteId(auditLog.getEntiteId());
|
||||
dto.setEntiteType(auditLog.getEntiteType());
|
||||
return dto;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit un DTO en entité
|
||||
*/
|
||||
private AuditLog convertToEntity(AuditLogDTO dto) {
|
||||
AuditLog auditLog = new AuditLog();
|
||||
if (dto.getId() != null) {
|
||||
auditLog.setId(dto.getId());
|
||||
}
|
||||
auditLog.setTypeAction(dto.getTypeAction());
|
||||
auditLog.setSeverite(dto.getSeverite());
|
||||
auditLog.setUtilisateur(dto.getUtilisateur());
|
||||
auditLog.setRole(dto.getRole());
|
||||
auditLog.setModule(dto.getModule());
|
||||
auditLog.setDescription(dto.getDescription());
|
||||
auditLog.setDetails(dto.getDetails());
|
||||
auditLog.setIpAddress(dto.getIpAddress());
|
||||
auditLog.setUserAgent(dto.getUserAgent());
|
||||
auditLog.setSessionId(dto.getSessionId());
|
||||
auditLog.setDateHeure(dto.getDateHeure() != null ? dto.getDateHeure() : LocalDateTime.now());
|
||||
auditLog.setDonneesAvant(dto.getDonneesAvant());
|
||||
auditLog.setDonneesApres(dto.getDonneesApres());
|
||||
auditLog.setEntiteId(dto.getEntiteId());
|
||||
auditLog.setEntiteType(dto.getEntiteType());
|
||||
return auditLog;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,479 @@
|
||||
package dev.lions.unionflow.server.service;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.comptabilite.*;
|
||||
import dev.lions.unionflow.server.entity.*;
|
||||
import dev.lions.unionflow.server.repository.*;
|
||||
import dev.lions.unionflow.server.service.KeycloakService;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.ws.rs.NotFoundException;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
/**
|
||||
* Service métier pour la gestion comptable
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 3.0
|
||||
* @since 2025-01-29
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class ComptabiliteService {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(ComptabiliteService.class);
|
||||
|
||||
@Inject CompteComptableRepository compteComptableRepository;
|
||||
|
||||
@Inject JournalComptableRepository journalComptableRepository;
|
||||
|
||||
@Inject EcritureComptableRepository ecritureComptableRepository;
|
||||
|
||||
@Inject LigneEcritureRepository ligneEcritureRepository;
|
||||
|
||||
@Inject OrganisationRepository organisationRepository;
|
||||
|
||||
@Inject PaiementRepository paiementRepository;
|
||||
|
||||
@Inject KeycloakService keycloakService;
|
||||
|
||||
// ========================================
|
||||
// COMPTES COMPTABLES
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Crée un nouveau compte comptable
|
||||
*
|
||||
* @param compteDTO DTO du compte à créer
|
||||
* @return DTO du compte créé
|
||||
*/
|
||||
@Transactional
|
||||
public CompteComptableDTO creerCompteComptable(CompteComptableDTO compteDTO) {
|
||||
LOG.infof("Création d'un nouveau compte comptable: %s", compteDTO.getNumeroCompte());
|
||||
|
||||
// Vérifier l'unicité du numéro
|
||||
if (compteComptableRepository.findByNumeroCompte(compteDTO.getNumeroCompte()).isPresent()) {
|
||||
throw new IllegalArgumentException("Un compte avec ce numéro existe déjà: " + compteDTO.getNumeroCompte());
|
||||
}
|
||||
|
||||
CompteComptable compte = convertToEntity(compteDTO);
|
||||
compte.setCreePar(keycloakService.getCurrentUserEmail());
|
||||
|
||||
compteComptableRepository.persist(compte);
|
||||
LOG.infof("Compte comptable créé avec succès: ID=%s, Numéro=%s", compte.getId(), compte.getNumeroCompte());
|
||||
|
||||
return convertToDTO(compte);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve un compte comptable par son ID
|
||||
*
|
||||
* @param id ID du compte
|
||||
* @return DTO du compte
|
||||
*/
|
||||
public CompteComptableDTO trouverCompteParId(UUID id) {
|
||||
return compteComptableRepository
|
||||
.findCompteComptableById(id)
|
||||
.map(this::convertToDTO)
|
||||
.orElseThrow(() -> new NotFoundException("Compte comptable non trouvé avec l'ID: " + id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste tous les comptes comptables actifs
|
||||
*
|
||||
* @return Liste des comptes
|
||||
*/
|
||||
public List<CompteComptableDTO> listerTousLesComptes() {
|
||||
return compteComptableRepository.findAllActifs().stream()
|
||||
.map(this::convertToDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// JOURNAUX COMPTABLES
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Crée un nouveau journal comptable
|
||||
*
|
||||
* @param journalDTO DTO du journal à créer
|
||||
* @return DTO du journal créé
|
||||
*/
|
||||
@Transactional
|
||||
public JournalComptableDTO creerJournalComptable(JournalComptableDTO journalDTO) {
|
||||
LOG.infof("Création d'un nouveau journal comptable: %s", journalDTO.getCode());
|
||||
|
||||
// Vérifier l'unicité du code
|
||||
if (journalComptableRepository.findByCode(journalDTO.getCode()).isPresent()) {
|
||||
throw new IllegalArgumentException("Un journal avec ce code existe déjà: " + journalDTO.getCode());
|
||||
}
|
||||
|
||||
JournalComptable journal = convertToEntity(journalDTO);
|
||||
journal.setCreePar(keycloakService.getCurrentUserEmail());
|
||||
|
||||
journalComptableRepository.persist(journal);
|
||||
LOG.infof("Journal comptable créé avec succès: ID=%s, Code=%s", journal.getId(), journal.getCode());
|
||||
|
||||
return convertToDTO(journal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve un journal comptable par son ID
|
||||
*
|
||||
* @param id ID du journal
|
||||
* @return DTO du journal
|
||||
*/
|
||||
public JournalComptableDTO trouverJournalParId(UUID id) {
|
||||
return journalComptableRepository
|
||||
.findJournalComptableById(id)
|
||||
.map(this::convertToDTO)
|
||||
.orElseThrow(() -> new NotFoundException("Journal comptable non trouvé avec l'ID: " + id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste tous les journaux comptables actifs
|
||||
*
|
||||
* @return Liste des journaux
|
||||
*/
|
||||
public List<JournalComptableDTO> listerTousLesJournaux() {
|
||||
return journalComptableRepository.findAllActifs().stream()
|
||||
.map(this::convertToDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// ÉCRITURES COMPTABLES
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Crée une nouvelle écriture comptable avec validation de l'équilibre
|
||||
*
|
||||
* @param ecritureDTO DTO de l'écriture à créer
|
||||
* @return DTO de l'écriture créée
|
||||
*/
|
||||
@Transactional
|
||||
public EcritureComptableDTO creerEcritureComptable(EcritureComptableDTO ecritureDTO) {
|
||||
LOG.infof("Création d'une nouvelle écriture comptable: %s", ecritureDTO.getNumeroPiece());
|
||||
|
||||
// Vérifier l'équilibre
|
||||
if (!isEcritureEquilibree(ecritureDTO)) {
|
||||
throw new IllegalArgumentException("L'écriture n'est pas équilibrée (Débit ≠ Crédit)");
|
||||
}
|
||||
|
||||
EcritureComptable ecriture = convertToEntity(ecritureDTO);
|
||||
ecriture.setCreePar(keycloakService.getCurrentUserEmail());
|
||||
|
||||
// Calculer les totaux
|
||||
ecriture.calculerTotaux();
|
||||
|
||||
ecritureComptableRepository.persist(ecriture);
|
||||
LOG.infof("Écriture comptable créée avec succès: ID=%s, Numéro=%s", ecriture.getId(), ecriture.getNumeroPiece());
|
||||
|
||||
return convertToDTO(ecriture);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve une écriture comptable par son ID
|
||||
*
|
||||
* @param id ID de l'écriture
|
||||
* @return DTO de l'écriture
|
||||
*/
|
||||
public EcritureComptableDTO trouverEcritureParId(UUID id) {
|
||||
return ecritureComptableRepository
|
||||
.findEcritureComptableById(id)
|
||||
.map(this::convertToDTO)
|
||||
.orElseThrow(() -> new NotFoundException("Écriture comptable non trouvée avec l'ID: " + id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste les écritures d'un journal
|
||||
*
|
||||
* @param journalId ID du journal
|
||||
* @return Liste des écritures
|
||||
*/
|
||||
public List<EcritureComptableDTO> listerEcrituresParJournal(UUID journalId) {
|
||||
return ecritureComptableRepository.findByJournalId(journalId).stream()
|
||||
.map(this::convertToDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste les écritures d'une organisation
|
||||
*
|
||||
* @param organisationId ID de l'organisation
|
||||
* @return Liste des écritures
|
||||
*/
|
||||
public List<EcritureComptableDTO> listerEcrituresParOrganisation(UUID organisationId) {
|
||||
return ecritureComptableRepository.findByOrganisationId(organisationId).stream()
|
||||
.map(this::convertToDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// MÉTHODES PRIVÉES - CONVERSIONS
|
||||
// ========================================
|
||||
|
||||
/** Vérifie si une écriture est équilibrée */
|
||||
private boolean isEcritureEquilibree(EcritureComptableDTO ecritureDTO) {
|
||||
if (ecritureDTO.getLignes() == null || ecritureDTO.getLignes().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
BigDecimal totalDebit =
|
||||
ecritureDTO.getLignes().stream()
|
||||
.map(LigneEcritureDTO::getMontantDebit)
|
||||
.filter(amount -> amount != null)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
BigDecimal totalCredit =
|
||||
ecritureDTO.getLignes().stream()
|
||||
.map(LigneEcritureDTO::getMontantCredit)
|
||||
.filter(amount -> amount != null)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
return totalDebit.compareTo(totalCredit) == 0;
|
||||
}
|
||||
|
||||
/** Convertit une entité CompteComptable en DTO */
|
||||
private CompteComptableDTO convertToDTO(CompteComptable compte) {
|
||||
if (compte == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
CompteComptableDTO dto = new CompteComptableDTO();
|
||||
dto.setId(compte.getId());
|
||||
dto.setNumeroCompte(compte.getNumeroCompte());
|
||||
dto.setLibelle(compte.getLibelle());
|
||||
dto.setTypeCompte(compte.getTypeCompte());
|
||||
dto.setClasseComptable(compte.getClasseComptable());
|
||||
dto.setSoldeInitial(compte.getSoldeInitial());
|
||||
dto.setSoldeActuel(compte.getSoldeActuel());
|
||||
dto.setCompteCollectif(compte.getCompteCollectif());
|
||||
dto.setCompteAnalytique(compte.getCompteAnalytique());
|
||||
dto.setDescription(compte.getDescription());
|
||||
dto.setDateCreation(compte.getDateCreation());
|
||||
dto.setDateModification(compte.getDateModification());
|
||||
dto.setActif(compte.getActif());
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
/** Convertit un DTO en entité CompteComptable */
|
||||
private CompteComptable convertToEntity(CompteComptableDTO dto) {
|
||||
if (dto == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
CompteComptable compte = new CompteComptable();
|
||||
compte.setNumeroCompte(dto.getNumeroCompte());
|
||||
compte.setLibelle(dto.getLibelle());
|
||||
compte.setTypeCompte(dto.getTypeCompte());
|
||||
compte.setClasseComptable(dto.getClasseComptable());
|
||||
compte.setSoldeInitial(dto.getSoldeInitial() != null ? dto.getSoldeInitial() : BigDecimal.ZERO);
|
||||
compte.setSoldeActuel(dto.getSoldeActuel() != null ? dto.getSoldeActuel() : dto.getSoldeInitial());
|
||||
compte.setCompteCollectif(dto.getCompteCollectif() != null ? dto.getCompteCollectif() : false);
|
||||
compte.setCompteAnalytique(dto.getCompteAnalytique() != null ? dto.getCompteAnalytique() : false);
|
||||
compte.setDescription(dto.getDescription());
|
||||
|
||||
return compte;
|
||||
}
|
||||
|
||||
/** Convertit une entité JournalComptable en DTO */
|
||||
private JournalComptableDTO convertToDTO(JournalComptable journal) {
|
||||
if (journal == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
JournalComptableDTO dto = new JournalComptableDTO();
|
||||
dto.setId(journal.getId());
|
||||
dto.setCode(journal.getCode());
|
||||
dto.setLibelle(journal.getLibelle());
|
||||
dto.setTypeJournal(journal.getTypeJournal());
|
||||
dto.setDateDebut(journal.getDateDebut());
|
||||
dto.setDateFin(journal.getDateFin());
|
||||
dto.setStatut(journal.getStatut());
|
||||
dto.setDescription(journal.getDescription());
|
||||
dto.setDateCreation(journal.getDateCreation());
|
||||
dto.setDateModification(journal.getDateModification());
|
||||
dto.setActif(journal.getActif());
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
/** Convertit un DTO en entité JournalComptable */
|
||||
private JournalComptable convertToEntity(JournalComptableDTO dto) {
|
||||
if (dto == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
JournalComptable journal = new JournalComptable();
|
||||
journal.setCode(dto.getCode());
|
||||
journal.setLibelle(dto.getLibelle());
|
||||
journal.setTypeJournal(dto.getTypeJournal());
|
||||
journal.setDateDebut(dto.getDateDebut());
|
||||
journal.setDateFin(dto.getDateFin());
|
||||
journal.setStatut(dto.getStatut() != null ? dto.getStatut() : "OUVERT");
|
||||
journal.setDescription(dto.getDescription());
|
||||
|
||||
return journal;
|
||||
}
|
||||
|
||||
/** Convertit une entité EcritureComptable en DTO */
|
||||
private EcritureComptableDTO convertToDTO(EcritureComptable ecriture) {
|
||||
if (ecriture == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
EcritureComptableDTO dto = new EcritureComptableDTO();
|
||||
dto.setId(ecriture.getId());
|
||||
dto.setNumeroPiece(ecriture.getNumeroPiece());
|
||||
dto.setDateEcriture(ecriture.getDateEcriture());
|
||||
dto.setLibelle(ecriture.getLibelle());
|
||||
dto.setReference(ecriture.getReference());
|
||||
dto.setLettrage(ecriture.getLettrage());
|
||||
dto.setPointe(ecriture.getPointe());
|
||||
dto.setMontantDebit(ecriture.getMontantDebit());
|
||||
dto.setMontantCredit(ecriture.getMontantCredit());
|
||||
dto.setCommentaire(ecriture.getCommentaire());
|
||||
|
||||
if (ecriture.getJournal() != null) {
|
||||
dto.setJournalId(ecriture.getJournal().getId());
|
||||
}
|
||||
if (ecriture.getOrganisation() != null) {
|
||||
dto.setOrganisationId(ecriture.getOrganisation().getId());
|
||||
}
|
||||
if (ecriture.getPaiement() != null) {
|
||||
dto.setPaiementId(ecriture.getPaiement().getId());
|
||||
}
|
||||
|
||||
// Convertir les lignes
|
||||
if (ecriture.getLignes() != null) {
|
||||
dto.setLignes(
|
||||
ecriture.getLignes().stream().map(this::convertToDTO).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
dto.setDateCreation(ecriture.getDateCreation());
|
||||
dto.setDateModification(ecriture.getDateModification());
|
||||
dto.setActif(ecriture.getActif());
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
/** Convertit un DTO en entité EcritureComptable */
|
||||
private EcritureComptable convertToEntity(EcritureComptableDTO dto) {
|
||||
if (dto == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
EcritureComptable ecriture = new EcritureComptable();
|
||||
ecriture.setNumeroPiece(dto.getNumeroPiece());
|
||||
ecriture.setDateEcriture(dto.getDateEcriture() != null ? dto.getDateEcriture() : LocalDate.now());
|
||||
ecriture.setLibelle(dto.getLibelle());
|
||||
ecriture.setReference(dto.getReference());
|
||||
ecriture.setLettrage(dto.getLettrage());
|
||||
ecriture.setPointe(dto.getPointe() != null ? dto.getPointe() : false);
|
||||
ecriture.setCommentaire(dto.getCommentaire());
|
||||
|
||||
// Relations
|
||||
if (dto.getJournalId() != null) {
|
||||
JournalComptable journal =
|
||||
journalComptableRepository
|
||||
.findJournalComptableById(dto.getJournalId())
|
||||
.orElseThrow(
|
||||
() -> new NotFoundException("Journal comptable non trouvé avec l'ID: " + dto.getJournalId()));
|
||||
ecriture.setJournal(journal);
|
||||
}
|
||||
|
||||
if (dto.getOrganisationId() != null) {
|
||||
Organisation org =
|
||||
organisationRepository
|
||||
.findByIdOptional(dto.getOrganisationId())
|
||||
.orElseThrow(
|
||||
() ->
|
||||
new NotFoundException(
|
||||
"Organisation non trouvée avec l'ID: " + dto.getOrganisationId()));
|
||||
ecriture.setOrganisation(org);
|
||||
}
|
||||
|
||||
if (dto.getPaiementId() != null) {
|
||||
Paiement paiement =
|
||||
paiementRepository
|
||||
.findPaiementById(dto.getPaiementId())
|
||||
.orElseThrow(
|
||||
() -> new NotFoundException("Paiement non trouvé avec l'ID: " + dto.getPaiementId()));
|
||||
ecriture.setPaiement(paiement);
|
||||
}
|
||||
|
||||
// Convertir les lignes
|
||||
if (dto.getLignes() != null) {
|
||||
for (LigneEcritureDTO ligneDTO : dto.getLignes()) {
|
||||
LigneEcriture ligne = convertToEntity(ligneDTO);
|
||||
ligne.setEcriture(ecriture);
|
||||
ecriture.getLignes().add(ligne);
|
||||
}
|
||||
}
|
||||
|
||||
return ecriture;
|
||||
}
|
||||
|
||||
/** Convertit une entité LigneEcriture en DTO */
|
||||
private LigneEcritureDTO convertToDTO(LigneEcriture ligne) {
|
||||
if (ligne == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
LigneEcritureDTO dto = new LigneEcritureDTO();
|
||||
dto.setId(ligne.getId());
|
||||
dto.setNumeroLigne(ligne.getNumeroLigne());
|
||||
dto.setMontantDebit(ligne.getMontantDebit());
|
||||
dto.setMontantCredit(ligne.getMontantCredit());
|
||||
dto.setLibelle(ligne.getLibelle());
|
||||
dto.setReference(ligne.getReference());
|
||||
|
||||
if (ligne.getEcriture() != null) {
|
||||
dto.setEcritureId(ligne.getEcriture().getId());
|
||||
}
|
||||
if (ligne.getCompteComptable() != null) {
|
||||
dto.setCompteComptableId(ligne.getCompteComptable().getId());
|
||||
}
|
||||
|
||||
dto.setDateCreation(ligne.getDateCreation());
|
||||
dto.setDateModification(ligne.getDateModification());
|
||||
dto.setActif(ligne.getActif());
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
/** Convertit un DTO en entité LigneEcriture */
|
||||
private LigneEcriture convertToEntity(LigneEcritureDTO dto) {
|
||||
if (dto == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
LigneEcriture ligne = new LigneEcriture();
|
||||
ligne.setNumeroLigne(dto.getNumeroLigne());
|
||||
ligne.setMontantDebit(dto.getMontantDebit() != null ? dto.getMontantDebit() : BigDecimal.ZERO);
|
||||
ligne.setMontantCredit(dto.getMontantCredit() != null ? dto.getMontantCredit() : BigDecimal.ZERO);
|
||||
ligne.setLibelle(dto.getLibelle());
|
||||
ligne.setReference(dto.getReference());
|
||||
|
||||
// Relation CompteComptable
|
||||
if (dto.getCompteComptableId() != null) {
|
||||
CompteComptable compte =
|
||||
compteComptableRepository
|
||||
.findCompteComptableById(dto.getCompteComptableId())
|
||||
.orElseThrow(
|
||||
() ->
|
||||
new NotFoundException(
|
||||
"Compte comptable non trouvé avec l'ID: " + dto.getCompteComptableId()));
|
||||
ligne.setCompteComptable(compte);
|
||||
}
|
||||
|
||||
return ligne;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,493 @@
|
||||
package dev.lions.unionflow.server.service;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.finance.CotisationDTO;
|
||||
import dev.lions.unionflow.server.entity.Cotisation;
|
||||
import dev.lions.unionflow.server.entity.Membre;
|
||||
import dev.lions.unionflow.server.repository.CotisationRepository;
|
||||
import dev.lions.unionflow.server.repository.MembreRepository;
|
||||
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 jakarta.ws.rs.NotFoundException;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Service métier pour la gestion des cotisations Contient la logique métier et les règles de
|
||||
* validation
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 1.0
|
||||
* @since 2025-01-15
|
||||
*/
|
||||
@ApplicationScoped
|
||||
@Slf4j
|
||||
public class CotisationService {
|
||||
|
||||
@Inject CotisationRepository cotisationRepository;
|
||||
|
||||
@Inject MembreRepository membreRepository;
|
||||
|
||||
/**
|
||||
* Récupère toutes les cotisations avec pagination
|
||||
*
|
||||
* @param page numéro de page (0-based)
|
||||
* @param size taille de la page
|
||||
* @return liste des cotisations converties en DTO
|
||||
*/
|
||||
public List<CotisationDTO> getAllCotisations(int page, int size) {
|
||||
log.debug("Récupération des cotisations - page: {}, size: {}", page, size);
|
||||
|
||||
// Utilisation de EntityManager pour la pagination
|
||||
jakarta.persistence.TypedQuery<Cotisation> query =
|
||||
cotisationRepository.getEntityManager().createQuery(
|
||||
"SELECT c FROM Cotisation c ORDER BY c.dateEcheance DESC",
|
||||
Cotisation.class);
|
||||
query.setFirstResult(page * size);
|
||||
query.setMaxResults(size);
|
||||
List<Cotisation> cotisations = query.getResultList();
|
||||
|
||||
return cotisations.stream().map(this::convertToDTO).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère une cotisation par son ID
|
||||
*
|
||||
* @param id identifiant UUID de la cotisation
|
||||
* @return DTO de la cotisation
|
||||
* @throws NotFoundException si la cotisation n'existe pas
|
||||
*/
|
||||
public CotisationDTO getCotisationById(@NotNull UUID id) {
|
||||
log.debug("Récupération de la cotisation avec ID: {}", id);
|
||||
|
||||
Cotisation cotisation =
|
||||
cotisationRepository
|
||||
.findByIdOptional(id)
|
||||
.orElseThrow(() -> new NotFoundException("Cotisation non trouvée avec l'ID: " + id));
|
||||
|
||||
return convertToDTO(cotisation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère une cotisation par son numéro de référence
|
||||
*
|
||||
* @param numeroReference numéro de référence unique
|
||||
* @return DTO de la cotisation
|
||||
* @throws NotFoundException si la cotisation n'existe pas
|
||||
*/
|
||||
public CotisationDTO getCotisationByReference(@NotNull String numeroReference) {
|
||||
log.debug("Récupération de la cotisation avec référence: {}", numeroReference);
|
||||
|
||||
Cotisation cotisation =
|
||||
cotisationRepository
|
||||
.findByNumeroReference(numeroReference)
|
||||
.orElseThrow(
|
||||
() ->
|
||||
new NotFoundException(
|
||||
"Cotisation non trouvée avec la référence: " + numeroReference));
|
||||
|
||||
return convertToDTO(cotisation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée une nouvelle cotisation
|
||||
*
|
||||
* @param cotisationDTO données de la cotisation à créer
|
||||
* @return DTO de la cotisation créée
|
||||
*/
|
||||
@Transactional
|
||||
public CotisationDTO createCotisation(@Valid CotisationDTO cotisationDTO) {
|
||||
log.info("Création d'une nouvelle cotisation pour le membre: {}", cotisationDTO.getMembreId());
|
||||
|
||||
// Validation du membre - UUID direct maintenant
|
||||
Membre membre =
|
||||
membreRepository
|
||||
.findByIdOptional(cotisationDTO.getMembreId())
|
||||
.orElseThrow(
|
||||
() ->
|
||||
new NotFoundException(
|
||||
"Membre non trouvé avec l'ID: " + cotisationDTO.getMembreId()));
|
||||
|
||||
// Conversion DTO vers entité
|
||||
Cotisation cotisation = convertToEntity(cotisationDTO);
|
||||
cotisation.setMembre(membre);
|
||||
|
||||
// Génération automatique du numéro de référence si absent
|
||||
if (cotisation.getNumeroReference() == null || cotisation.getNumeroReference().isEmpty()) {
|
||||
cotisation.setNumeroReference(Cotisation.genererNumeroReference());
|
||||
}
|
||||
|
||||
// Validation des règles métier
|
||||
validateCotisationRules(cotisation);
|
||||
|
||||
// Persistance
|
||||
cotisationRepository.persist(cotisation);
|
||||
|
||||
log.info(
|
||||
"Cotisation créée avec succès - ID: {}, Référence: {}",
|
||||
cotisation.getId(),
|
||||
cotisation.getNumeroReference());
|
||||
|
||||
return convertToDTO(cotisation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour une cotisation existante
|
||||
*
|
||||
* @param id identifiant UUID de la cotisation
|
||||
* @param cotisationDTO nouvelles données
|
||||
* @return DTO de la cotisation mise à jour
|
||||
*/
|
||||
@Transactional
|
||||
public CotisationDTO updateCotisation(@NotNull UUID id, @Valid CotisationDTO cotisationDTO) {
|
||||
log.info("Mise à jour de la cotisation avec ID: {}", id);
|
||||
|
||||
Cotisation cotisationExistante =
|
||||
cotisationRepository
|
||||
.findByIdOptional(id)
|
||||
.orElseThrow(() -> new NotFoundException("Cotisation non trouvée avec l'ID: " + id));
|
||||
|
||||
// Mise à jour des champs modifiables
|
||||
updateCotisationFields(cotisationExistante, cotisationDTO);
|
||||
|
||||
// Validation des règles métier
|
||||
validateCotisationRules(cotisationExistante);
|
||||
|
||||
log.info("Cotisation mise à jour avec succès - ID: {}", id);
|
||||
|
||||
return convertToDTO(cotisationExistante);
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime (désactive) une cotisation
|
||||
*
|
||||
* @param id identifiant UUID de la cotisation
|
||||
*/
|
||||
@Transactional
|
||||
public void deleteCotisation(@NotNull UUID id) {
|
||||
log.info("Suppression de la cotisation avec ID: {}", id);
|
||||
|
||||
Cotisation cotisation =
|
||||
cotisationRepository
|
||||
.findByIdOptional(id)
|
||||
.orElseThrow(() -> new NotFoundException("Cotisation non trouvée avec l'ID: " + id));
|
||||
|
||||
// Vérification si la cotisation peut être supprimée
|
||||
if ("PAYEE".equals(cotisation.getStatut())) {
|
||||
throw new IllegalStateException("Impossible de supprimer une cotisation déjà payée");
|
||||
}
|
||||
|
||||
cotisation.setStatut("ANNULEE");
|
||||
|
||||
log.info("Cotisation supprimée avec succès - ID: {}", id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les cotisations d'un membre
|
||||
*
|
||||
* @param membreId identifiant UUID du membre
|
||||
* @param page numéro de page
|
||||
* @param size taille de la page
|
||||
* @return liste des cotisations du membre
|
||||
*/
|
||||
public List<CotisationDTO> getCotisationsByMembre(@NotNull UUID membreId, int page, int size) {
|
||||
log.debug("Récupération des cotisations du membre: {}", membreId);
|
||||
|
||||
// Vérification de l'existence du membre
|
||||
if (!membreRepository.findByIdOptional(membreId).isPresent()) {
|
||||
throw new NotFoundException("Membre non trouvé avec l'ID: " + membreId);
|
||||
}
|
||||
|
||||
List<Cotisation> cotisations =
|
||||
cotisationRepository.findByMembreId(
|
||||
membreId, Page.of(page, size), Sort.by("dateEcheance").descending());
|
||||
|
||||
return cotisations.stream().map(this::convertToDTO).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les cotisations par statut
|
||||
*
|
||||
* @param statut statut recherché
|
||||
* @param page numéro de page
|
||||
* @param size taille de la page
|
||||
* @return liste des cotisations avec le statut spécifié
|
||||
*/
|
||||
public List<CotisationDTO> getCotisationsByStatut(@NotNull String statut, int page, int size) {
|
||||
log.debug("Récupération des cotisations avec statut: {}", statut);
|
||||
|
||||
List<Cotisation> cotisations = cotisationRepository.findByStatut(statut, Page.of(page, size));
|
||||
|
||||
return cotisations.stream().map(this::convertToDTO).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les cotisations en retard
|
||||
*
|
||||
* @param page numéro de page
|
||||
* @param size taille de la page
|
||||
* @return liste des cotisations en retard
|
||||
*/
|
||||
public List<CotisationDTO> getCotisationsEnRetard(int page, int size) {
|
||||
log.debug("Récupération des cotisations en retard");
|
||||
|
||||
List<Cotisation> cotisations =
|
||||
cotisationRepository.findCotisationsEnRetard(LocalDate.now(), Page.of(page, size));
|
||||
|
||||
return cotisations.stream().map(this::convertToDTO).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Recherche avancée de cotisations
|
||||
*
|
||||
* @param membreId identifiant du membre (optionnel)
|
||||
* @param statut statut (optionnel)
|
||||
* @param typeCotisation type (optionnel)
|
||||
* @param annee année (optionnel)
|
||||
* @param mois mois (optionnel)
|
||||
* @param page numéro de page
|
||||
* @param size taille de la page
|
||||
* @return liste filtrée des cotisations
|
||||
*/
|
||||
public List<CotisationDTO> rechercherCotisations(
|
||||
UUID membreId,
|
||||
String statut,
|
||||
String typeCotisation,
|
||||
Integer annee,
|
||||
Integer mois,
|
||||
int page,
|
||||
int size) {
|
||||
log.debug("Recherche avancée de cotisations avec filtres");
|
||||
|
||||
List<Cotisation> cotisations =
|
||||
cotisationRepository.rechercheAvancee(
|
||||
membreId, statut, typeCotisation, annee, mois, Page.of(page, size));
|
||||
|
||||
return cotisations.stream().map(this::convertToDTO).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les statistiques des cotisations
|
||||
*
|
||||
* @return map contenant les statistiques
|
||||
*/
|
||||
public Map<String, Object> getStatistiquesCotisations() {
|
||||
log.debug("Calcul des statistiques des cotisations");
|
||||
|
||||
long totalCotisations = cotisationRepository.count();
|
||||
long cotisationsPayees = cotisationRepository.compterParStatut("PAYEE");
|
||||
long cotisationsEnRetard =
|
||||
cotisationRepository
|
||||
.findCotisationsEnRetard(LocalDate.now(), Page.of(0, Integer.MAX_VALUE))
|
||||
.size();
|
||||
|
||||
return Map.of(
|
||||
"totalCotisations", totalCotisations,
|
||||
"cotisationsPayees", cotisationsPayees,
|
||||
"cotisationsEnRetard", cotisationsEnRetard,
|
||||
"tauxPaiement",
|
||||
totalCotisations > 0 ? (cotisationsPayees * 100.0 / totalCotisations) : 0.0);
|
||||
}
|
||||
|
||||
/** Convertit une entité Cotisation en DTO */
|
||||
private CotisationDTO convertToDTO(Cotisation cotisation) {
|
||||
if (cotisation == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
CotisationDTO dto = new CotisationDTO();
|
||||
|
||||
// Conversion de l'ID UUID vers UUID (pas de conversion nécessaire maintenant)
|
||||
dto.setId(cotisation.getId());
|
||||
dto.setNumeroReference(cotisation.getNumeroReference());
|
||||
|
||||
// Conversion du membre associé
|
||||
if (cotisation.getMembre() != null) {
|
||||
dto.setMembreId(cotisation.getMembre().getId());
|
||||
dto.setNomMembre(cotisation.getMembre().getNomComplet());
|
||||
dto.setNumeroMembre(cotisation.getMembre().getNumeroMembre());
|
||||
|
||||
// Conversion de l'organisation du membre (associationId)
|
||||
if (cotisation.getMembre().getOrganisation() != null
|
||||
&& cotisation.getMembre().getOrganisation().getId() != null) {
|
||||
dto.setAssociationId(cotisation.getMembre().getOrganisation().getId());
|
||||
dto.setNomAssociation(cotisation.getMembre().getOrganisation().getNom());
|
||||
}
|
||||
}
|
||||
|
||||
// Propriétés de la cotisation
|
||||
dto.setTypeCotisation(cotisation.getTypeCotisation());
|
||||
dto.setMontantDu(cotisation.getMontantDu());
|
||||
dto.setMontantPaye(cotisation.getMontantPaye());
|
||||
dto.setCodeDevise(cotisation.getCodeDevise());
|
||||
dto.setStatut(cotisation.getStatut());
|
||||
dto.setDateEcheance(cotisation.getDateEcheance());
|
||||
dto.setDatePaiement(cotisation.getDatePaiement());
|
||||
dto.setDescription(cotisation.getDescription());
|
||||
dto.setPeriode(cotisation.getPeriode());
|
||||
dto.setAnnee(cotisation.getAnnee());
|
||||
dto.setMois(cotisation.getMois());
|
||||
dto.setObservations(cotisation.getObservations());
|
||||
dto.setRecurrente(cotisation.getRecurrente());
|
||||
dto.setNombreRappels(cotisation.getNombreRappels());
|
||||
dto.setDateDernierRappel(cotisation.getDateDernierRappel());
|
||||
|
||||
// Conversion du validateur
|
||||
dto.setValidePar(
|
||||
cotisation.getValideParId() != null
|
||||
? cotisation.getValideParId()
|
||||
: null);
|
||||
dto.setNomValidateur(cotisation.getNomValidateur());
|
||||
|
||||
dto.setMethodePaiement(cotisation.getMethodePaiement());
|
||||
dto.setReferencePaiement(cotisation.getReferencePaiement());
|
||||
dto.setDateCreation(cotisation.getDateCreation());
|
||||
dto.setDateModification(cotisation.getDateModification());
|
||||
|
||||
// Propriétés héritées de BaseDTO
|
||||
dto.setActif(true); // Les cotisations sont toujours actives
|
||||
dto.setVersion(0L); // Version par défaut
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
/** Convertit un DTO en entité Cotisation */
|
||||
private Cotisation convertToEntity(CotisationDTO dto) {
|
||||
return Cotisation.builder()
|
||||
.numeroReference(dto.getNumeroReference())
|
||||
.typeCotisation(dto.getTypeCotisation())
|
||||
.montantDu(dto.getMontantDu())
|
||||
.montantPaye(dto.getMontantPaye() != null ? dto.getMontantPaye() : BigDecimal.ZERO)
|
||||
.codeDevise(dto.getCodeDevise() != null ? dto.getCodeDevise() : "XOF")
|
||||
.statut(dto.getStatut() != null ? dto.getStatut() : "EN_ATTENTE")
|
||||
.dateEcheance(dto.getDateEcheance())
|
||||
.datePaiement(dto.getDatePaiement())
|
||||
.description(dto.getDescription())
|
||||
.periode(dto.getPeriode())
|
||||
.annee(dto.getAnnee())
|
||||
.mois(dto.getMois())
|
||||
.observations(dto.getObservations())
|
||||
.recurrente(dto.getRecurrente() != null ? dto.getRecurrente() : false)
|
||||
.nombreRappels(dto.getNombreRappels() != null ? dto.getNombreRappels() : 0)
|
||||
.dateDernierRappel(dto.getDateDernierRappel())
|
||||
.methodePaiement(dto.getMethodePaiement())
|
||||
.referencePaiement(dto.getReferencePaiement())
|
||||
.build();
|
||||
}
|
||||
|
||||
/** Met à jour les champs d'une cotisation existante */
|
||||
private void updateCotisationFields(Cotisation cotisation, CotisationDTO dto) {
|
||||
if (dto.getTypeCotisation() != null) {
|
||||
cotisation.setTypeCotisation(dto.getTypeCotisation());
|
||||
}
|
||||
if (dto.getMontantDu() != null) {
|
||||
cotisation.setMontantDu(dto.getMontantDu());
|
||||
}
|
||||
if (dto.getMontantPaye() != null) {
|
||||
cotisation.setMontantPaye(dto.getMontantPaye());
|
||||
}
|
||||
if (dto.getStatut() != null) {
|
||||
cotisation.setStatut(dto.getStatut());
|
||||
}
|
||||
if (dto.getDateEcheance() != null) {
|
||||
cotisation.setDateEcheance(dto.getDateEcheance());
|
||||
}
|
||||
if (dto.getDatePaiement() != null) {
|
||||
cotisation.setDatePaiement(dto.getDatePaiement());
|
||||
}
|
||||
if (dto.getDescription() != null) {
|
||||
cotisation.setDescription(dto.getDescription());
|
||||
}
|
||||
if (dto.getObservations() != null) {
|
||||
cotisation.setObservations(dto.getObservations());
|
||||
}
|
||||
if (dto.getMethodePaiement() != null) {
|
||||
cotisation.setMethodePaiement(dto.getMethodePaiement());
|
||||
}
|
||||
if (dto.getReferencePaiement() != null) {
|
||||
cotisation.setReferencePaiement(dto.getReferencePaiement());
|
||||
}
|
||||
}
|
||||
|
||||
/** Valide les règles métier pour une cotisation */
|
||||
private void validateCotisationRules(Cotisation cotisation) {
|
||||
// Validation du montant
|
||||
if (cotisation.getMontantDu().compareTo(BigDecimal.ZERO) <= 0) {
|
||||
throw new IllegalArgumentException("Le montant dû doit être positif");
|
||||
}
|
||||
|
||||
// Validation de la date d'échéance
|
||||
if (cotisation.getDateEcheance().isBefore(LocalDate.now().minusYears(1))) {
|
||||
throw new IllegalArgumentException("La date d'échéance ne peut pas être antérieure à un an");
|
||||
}
|
||||
|
||||
// Validation du montant payé
|
||||
if (cotisation.getMontantPaye().compareTo(cotisation.getMontantDu()) > 0) {
|
||||
throw new IllegalArgumentException("Le montant payé ne peut pas dépasser le montant dû");
|
||||
}
|
||||
|
||||
// Validation de la cohérence statut/paiement
|
||||
if ("PAYEE".equals(cotisation.getStatut())
|
||||
&& cotisation.getMontantPaye().compareTo(cotisation.getMontantDu()) < 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"Une cotisation marquée comme payée doit avoir un montant payé égal au montant dû");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Envoie des rappels de cotisations groupés à plusieurs membres (WOU/DRY)
|
||||
*
|
||||
* @param membreIds Liste des IDs des membres destinataires
|
||||
* @return Nombre de rappels envoyés
|
||||
*/
|
||||
@Transactional
|
||||
public int envoyerRappelsCotisationsGroupes(List<UUID> membreIds) {
|
||||
log.info("Envoi de rappels de cotisations groupés à {} membres", membreIds.size());
|
||||
|
||||
if (membreIds == null || membreIds.isEmpty()) {
|
||||
throw new IllegalArgumentException("La liste des membres ne peut pas être vide");
|
||||
}
|
||||
|
||||
int rappelsEnvoyes = 0;
|
||||
for (UUID membreId : membreIds) {
|
||||
try {
|
||||
Membre membre =
|
||||
membreRepository
|
||||
.findByIdOptional(membreId)
|
||||
.orElseThrow(
|
||||
() ->
|
||||
new IllegalArgumentException(
|
||||
"Membre non trouvé avec l'ID: " + membreId));
|
||||
|
||||
// Trouver les cotisations en retard pour ce membre
|
||||
List<Cotisation> cotisationsEnRetard =
|
||||
cotisationRepository.findCotisationsAuRappel(7, 3).stream()
|
||||
.filter(c -> c.getMembre() != null && c.getMembre().getId().equals(membreId))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
for (Cotisation cotisation : cotisationsEnRetard) {
|
||||
// Incrémenter le nombre de rappels
|
||||
cotisationRepository.incrementerNombreRappels(cotisation.getId());
|
||||
rappelsEnvoyes++;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn(
|
||||
"Erreur lors de l'envoi du rappel de cotisation pour le membre {}: {}",
|
||||
membreId,
|
||||
e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
log.info("{} rappels envoyés sur {} membres demandés", rappelsEnvoyes, membreIds.size());
|
||||
return rappelsEnvoyes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
package dev.lions.unionflow.server.service;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.dashboard.DashboardDataDTO;
|
||||
import dev.lions.unionflow.server.api.dto.dashboard.DashboardStatsDTO;
|
||||
import dev.lions.unionflow.server.api.dto.dashboard.RecentActivityDTO;
|
||||
import dev.lions.unionflow.server.api.dto.dashboard.UpcomingEventDTO;
|
||||
import dev.lions.unionflow.server.api.enums.solidarite.StatutAide;
|
||||
import dev.lions.unionflow.server.api.service.dashboard.DashboardService;
|
||||
import dev.lions.unionflow.server.entity.Cotisation;
|
||||
import dev.lions.unionflow.server.entity.DemandeAide;
|
||||
import dev.lions.unionflow.server.entity.Evenement;
|
||||
import dev.lions.unionflow.server.entity.Membre;
|
||||
import dev.lions.unionflow.server.repository.CotisationRepository;
|
||||
import dev.lions.unionflow.server.repository.DemandeAideRepository;
|
||||
import dev.lions.unionflow.server.repository.EvenementRepository;
|
||||
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.persistence.TypedQuery;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Implémentation du service Dashboard pour Quarkus
|
||||
*
|
||||
* <p>Cette implémentation récupère les données réelles depuis la base de données
|
||||
* via les repositories.
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 2.0
|
||||
* @since 2025-01-17
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class DashboardServiceImpl implements DashboardService {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(DashboardServiceImpl.class);
|
||||
|
||||
@Inject
|
||||
MembreRepository membreRepository;
|
||||
|
||||
@Inject
|
||||
EvenementRepository evenementRepository;
|
||||
|
||||
@Inject
|
||||
CotisationRepository cotisationRepository;
|
||||
|
||||
@Inject
|
||||
DemandeAideRepository demandeAideRepository;
|
||||
|
||||
@Inject
|
||||
OrganisationRepository organisationRepository;
|
||||
|
||||
@Override
|
||||
public DashboardDataDTO getDashboardData(String organizationId, String userId) {
|
||||
LOG.infof("Récupération des données dashboard pour org: %s et user: %s", organizationId, userId);
|
||||
|
||||
UUID orgId = UUID.fromString(organizationId);
|
||||
|
||||
return DashboardDataDTO.builder()
|
||||
.stats(getDashboardStats(organizationId, userId))
|
||||
.recentActivities(getRecentActivities(organizationId, userId, 10))
|
||||
.upcomingEvents(getUpcomingEvents(organizationId, userId, 5))
|
||||
.userPreferences(getUserPreferences(userId))
|
||||
.organizationId(organizationId)
|
||||
.userId(userId)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DashboardStatsDTO getDashboardStats(String organizationId, String userId) {
|
||||
LOG.infof("Récupération des stats dashboard pour org: %s et user: %s", organizationId, userId);
|
||||
|
||||
UUID orgId = UUID.fromString(organizationId);
|
||||
|
||||
// Compter les membres
|
||||
long totalMembers = membreRepository.count();
|
||||
long activeMembers = membreRepository.countActifs();
|
||||
|
||||
// Compter les événements
|
||||
long totalEvents = evenementRepository.count();
|
||||
long upcomingEvents = evenementRepository.findEvenementsAVenir().size();
|
||||
|
||||
// Compter les cotisations
|
||||
long totalContributions = cotisationRepository.count();
|
||||
BigDecimal totalContributionAmount = calculateTotalContributionAmount(orgId);
|
||||
|
||||
// Compter les demandes en attente
|
||||
List<DemandeAide> pendingRequests = demandeAideRepository.findByStatut(StatutAide.EN_ATTENTE);
|
||||
long pendingRequestsCount = pendingRequests.stream()
|
||||
.filter(d -> d.getOrganisation() != null && d.getOrganisation().getId().equals(orgId))
|
||||
.count();
|
||||
|
||||
// Calculer la croissance mensuelle (membres ajoutés ce mois)
|
||||
LocalDate debutMois = LocalDate.now().withDayOfMonth(1);
|
||||
long nouveauxMembresMois = membreRepository.countNouveauxMembres(debutMois);
|
||||
long totalMembresAvant = totalMembers - nouveauxMembresMois;
|
||||
double monthlyGrowth = totalMembresAvant > 0
|
||||
? (double) nouveauxMembresMois / totalMembresAvant * 100.0
|
||||
: 0.0;
|
||||
|
||||
// Calculer le taux d'engagement (membres actifs / total)
|
||||
double engagementRate = totalMembers > 0
|
||||
? (double) activeMembers / totalMembers
|
||||
: 0.0;
|
||||
|
||||
return DashboardStatsDTO.builder()
|
||||
.totalMembers((int) totalMembers)
|
||||
.activeMembers((int) activeMembers)
|
||||
.totalEvents((int) totalEvents)
|
||||
.upcomingEvents((int) upcomingEvents)
|
||||
.totalContributions((int) totalContributions)
|
||||
.totalContributionAmount(totalContributionAmount.doubleValue())
|
||||
.pendingRequests((int) pendingRequestsCount)
|
||||
.completedProjects(0) // À implémenter si nécessaire
|
||||
.monthlyGrowth(monthlyGrowth)
|
||||
.engagementRate(engagementRate)
|
||||
.lastUpdated(LocalDateTime.now())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RecentActivityDTO> getRecentActivities(String organizationId, String userId, int limit) {
|
||||
LOG.infof("Récupération de %d activités récentes pour org: %s et user: %s", limit, organizationId, userId);
|
||||
|
||||
UUID orgId = UUID.fromString(organizationId);
|
||||
List<RecentActivityDTO> activities = new ArrayList<>();
|
||||
|
||||
// Récupérer les membres récemment créés
|
||||
List<Membre> nouveauxMembres = membreRepository.rechercheAvancee(
|
||||
null, true, null, null, Page.of(0, limit), Sort.by("dateCreation", Sort.Direction.Descending));
|
||||
|
||||
for (Membre membre : nouveauxMembres) {
|
||||
if (membre.getOrganisation() != null && membre.getOrganisation().getId().equals(orgId)) {
|
||||
activities.add(RecentActivityDTO.builder()
|
||||
.id(membre.getId().toString())
|
||||
.type("member")
|
||||
.title("Nouveau membre inscrit")
|
||||
.description(membre.getNomComplet() + " a rejoint l'organisation")
|
||||
.userName(membre.getNomComplet())
|
||||
.timestamp(membre.getDateCreation())
|
||||
.userAvatar(null)
|
||||
.actionUrl("/members/" + membre.getId())
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
// Récupérer les événements récemment créés
|
||||
List<Evenement> tousEvenements = evenementRepository.listAll();
|
||||
List<Evenement> nouveauxEvenements = tousEvenements.stream()
|
||||
.filter(e -> e.getOrganisation() != null && e.getOrganisation().getId().equals(orgId))
|
||||
.sorted(Comparator.comparing(Evenement::getDateCreation).reversed())
|
||||
.limit(limit)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
for (Evenement evenement : nouveauxEvenements) {
|
||||
activities.add(RecentActivityDTO.builder()
|
||||
.id(evenement.getId().toString())
|
||||
.type("event")
|
||||
.title("Événement créé")
|
||||
.description(evenement.getTitre() + " a été programmé")
|
||||
.userName(evenement.getOrganisation() != null ? evenement.getOrganisation().getNom() : "Système")
|
||||
.timestamp(evenement.getDateCreation())
|
||||
.userAvatar(null)
|
||||
.actionUrl("/events/" + evenement.getId())
|
||||
.build());
|
||||
}
|
||||
|
||||
// Récupérer les cotisations récentes
|
||||
List<Cotisation> cotisationsRecentes = cotisationRepository.rechercheAvancee(
|
||||
null, "PAYEE", null, null, null, Page.of(0, limit));
|
||||
|
||||
for (Cotisation cotisation : cotisationsRecentes) {
|
||||
if (cotisation.getMembre() != null &&
|
||||
cotisation.getMembre().getOrganisation() != null &&
|
||||
cotisation.getMembre().getOrganisation().getId().equals(orgId)) {
|
||||
activities.add(RecentActivityDTO.builder()
|
||||
.id(cotisation.getId().toString())
|
||||
.type("contribution")
|
||||
.title("Cotisation reçue")
|
||||
.description("Paiement de " + cotisation.getMontantPaye() + " " + cotisation.getCodeDevise() + " reçu")
|
||||
.userName(cotisation.getMembre().getNomComplet())
|
||||
.timestamp(cotisation.getDatePaiement() != null ? cotisation.getDatePaiement() : cotisation.getDateCreation())
|
||||
.userAvatar(null)
|
||||
.actionUrl("/contributions/" + cotisation.getId())
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
// Trier par timestamp décroissant et limiter
|
||||
return activities.stream()
|
||||
.sorted(Comparator.comparing(RecentActivityDTO::getTimestamp).reversed())
|
||||
.limit(limit)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<UpcomingEventDTO> getUpcomingEvents(String organizationId, String userId, int limit) {
|
||||
LOG.infof("Récupération de %d événements à venir pour org: %s et user: %s", limit, organizationId, userId);
|
||||
|
||||
UUID orgId = UUID.fromString(organizationId);
|
||||
|
||||
List<Evenement> evenements = evenementRepository.findEvenementsAVenir(
|
||||
Page.of(0, limit), Sort.by("dateDebut", Sort.Direction.Ascending));
|
||||
|
||||
return evenements.stream()
|
||||
.filter(e -> e.getOrganisation() == null || e.getOrganisation().getId().equals(orgId))
|
||||
.map(this::convertToUpcomingEventDTO)
|
||||
.limit(limit)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private UpcomingEventDTO convertToUpcomingEventDTO(Evenement evenement) {
|
||||
return UpcomingEventDTO.builder()
|
||||
.id(evenement.getId().toString())
|
||||
.title(evenement.getTitre())
|
||||
.description(evenement.getDescription())
|
||||
.startDate(evenement.getDateDebut())
|
||||
.endDate(evenement.getDateFin())
|
||||
.location(evenement.getLieu())
|
||||
.maxParticipants(evenement.getCapaciteMax())
|
||||
.currentParticipants(evenement.getNombreInscrits())
|
||||
.status(evenement.getStatut() != null ? evenement.getStatut().name() : "PLANIFIE")
|
||||
.imageUrl(null)
|
||||
.tags(Collections.emptyList())
|
||||
.build();
|
||||
}
|
||||
|
||||
private BigDecimal calculateTotalContributionAmount(UUID organisationId) {
|
||||
TypedQuery<BigDecimal> query = cotisationRepository.getEntityManager().createQuery(
|
||||
"SELECT COALESCE(SUM(c.montantDu), 0) FROM Cotisation c WHERE c.membre.organisation.id = :organisationId",
|
||||
BigDecimal.class);
|
||||
query.setParameter("organisationId", organisationId);
|
||||
BigDecimal result = query.getSingleResult();
|
||||
return result != null ? result : BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
private Map<String, Object> getUserPreferences(String userId) {
|
||||
Map<String, Object> preferences = new HashMap<>();
|
||||
preferences.put("theme", "royal_teal");
|
||||
preferences.put("language", "fr");
|
||||
preferences.put("notifications", true);
|
||||
preferences.put("autoRefresh", true);
|
||||
preferences.put("refreshInterval", 300);
|
||||
return preferences;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,400 @@
|
||||
package dev.lions.unionflow.server.service;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.solidarite.DemandeAideDTO;
|
||||
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 jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
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.
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 1.0
|
||||
* @since 2025-01-16
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class DemandeAideService {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(DemandeAideService.class);
|
||||
|
||||
// Cache en mémoire pour les demandes fréquemment consultées
|
||||
private final Map<UUID, DemandeAideDTO> 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 demandeDTO La demande à créer
|
||||
* @return La demande créée avec ID généré
|
||||
*/
|
||||
@Transactional
|
||||
public DemandeAideDTO creerDemande(@Valid DemandeAideDTO demandeDTO) {
|
||||
LOG.infof("Création d'une nouvelle demande d'aide: %s", demandeDTO.getTitre());
|
||||
|
||||
// Génération des identifiants
|
||||
demandeDTO.setId(UUID.randomUUID());
|
||||
demandeDTO.setNumeroReference(genererNumeroReference());
|
||||
|
||||
// Initialisation des dates
|
||||
LocalDateTime maintenant = LocalDateTime.now();
|
||||
demandeDTO.setDateCreation(maintenant);
|
||||
demandeDTO.setDateModification(maintenant);
|
||||
|
||||
// Statut initial
|
||||
if (demandeDTO.getStatut() == null) {
|
||||
demandeDTO.setStatut(StatutAide.BROUILLON);
|
||||
}
|
||||
|
||||
// Priorité par défaut si non définie
|
||||
if (demandeDTO.getPriorite() == null) {
|
||||
demandeDTO.setPriorite(PrioriteAide.NORMALE);
|
||||
}
|
||||
|
||||
// Initialisation de l'historique
|
||||
HistoriqueStatutDTO historiqueInitial =
|
||||
HistoriqueStatutDTO.builder()
|
||||
.id(UUID.randomUUID().toString())
|
||||
.ancienStatut(null)
|
||||
.nouveauStatut(demandeDTO.getStatut())
|
||||
.dateChangement(maintenant)
|
||||
.auteurId(demandeDTO.getMembreDemandeurId() != null ? demandeDTO.getMembreDemandeurId().toString() : null)
|
||||
.motif("Création de la demande")
|
||||
.estAutomatique(true)
|
||||
.build();
|
||||
|
||||
demandeDTO.setHistoriqueStatuts(List.of(historiqueInitial));
|
||||
|
||||
// Calcul du score de priorité
|
||||
demandeDTO.setScorePriorite(calculerScorePriorite(demandeDTO));
|
||||
|
||||
// Sauvegarde en cache
|
||||
ajouterAuCache(demandeDTO);
|
||||
|
||||
LOG.infof("Demande d'aide créée avec succès: %s", demandeDTO.getId());
|
||||
return demandeDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour une demande d'aide existante
|
||||
*
|
||||
* @param demandeDTO La demande à mettre à jour
|
||||
* @return La demande mise à jour
|
||||
*/
|
||||
@Transactional
|
||||
public DemandeAideDTO mettreAJour(@Valid DemandeAideDTO demandeDTO) {
|
||||
LOG.infof("Mise à jour de la demande d'aide: %s", demandeDTO.getId());
|
||||
|
||||
// Vérification que la demande peut être modifiée
|
||||
if (!demandeDTO.estModifiable()) {
|
||||
throw new IllegalStateException("Cette demande ne peut plus être modifiée");
|
||||
}
|
||||
|
||||
// Mise à jour de la date de modification
|
||||
demandeDTO.setDateModification(LocalDateTime.now());
|
||||
demandeDTO.setVersion(demandeDTO.getVersion() + 1);
|
||||
|
||||
// Recalcul du score de priorité
|
||||
demandeDTO.setScorePriorite(calculerScorePriorite(demandeDTO));
|
||||
|
||||
// Mise à jour du cache
|
||||
ajouterAuCache(demandeDTO);
|
||||
|
||||
LOG.infof("Demande d'aide mise à jour avec succès: %s", demandeDTO.getId());
|
||||
return demandeDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtient une demande d'aide par son ID
|
||||
*
|
||||
* @param id UUID de la demande
|
||||
* @return La demande trouvée
|
||||
*/
|
||||
public DemandeAideDTO obtenirParId(@NotNull UUID id) {
|
||||
LOG.debugf("Récupération de la demande d'aide: %s", id);
|
||||
|
||||
// Vérification du cache
|
||||
DemandeAideDTO demandeCachee = obtenirDuCache(id);
|
||||
if (demandeCachee != null) {
|
||||
LOG.debugf("Demande trouvée dans le cache: %s", id);
|
||||
return demandeCachee;
|
||||
}
|
||||
|
||||
// Simulation de récupération depuis la base de données
|
||||
// Dans une vraie implémentation, ceci ferait appel au repository
|
||||
DemandeAideDTO demande = simulerRecuperationBDD(id);
|
||||
|
||||
if (demande != null) {
|
||||
ajouterAuCache(demande);
|
||||
}
|
||||
|
||||
return demande;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 DemandeAideDTO changerStatut(
|
||||
@NotNull UUID demandeId, @NotNull StatutAide nouveauStatut, String motif) {
|
||||
LOG.infof("Changement de statut pour la demande %s: %s", demandeId, nouveauStatut);
|
||||
|
||||
DemandeAideDTO demande = obtenirParId(demandeId);
|
||||
if (demande == null) {
|
||||
throw new IllegalArgumentException("Demande non trouvée: " + demandeId);
|
||||
}
|
||||
|
||||
StatutAide ancienStatut = demande.getStatut();
|
||||
|
||||
// Validation de la transition
|
||||
if (!ancienStatut.peutTransitionnerVers(nouveauStatut)) {
|
||||
throw new IllegalStateException(
|
||||
String.format("Transition invalide de %s vers %s", ancienStatut, nouveauStatut));
|
||||
}
|
||||
|
||||
// Mise à jour du statut
|
||||
demande.setStatut(nouveauStatut);
|
||||
demande.setDateModification(LocalDateTime.now());
|
||||
|
||||
// Ajout à l'historique
|
||||
HistoriqueStatutDTO nouvelHistorique =
|
||||
HistoriqueStatutDTO.builder()
|
||||
.id(UUID.randomUUID().toString())
|
||||
.ancienStatut(ancienStatut)
|
||||
.nouveauStatut(nouveauStatut)
|
||||
.dateChangement(LocalDateTime.now())
|
||||
.motif(motif)
|
||||
.estAutomatique(false)
|
||||
.build();
|
||||
|
||||
List<HistoriqueStatutDTO> historique = new ArrayList<>(demande.getHistoriqueStatuts());
|
||||
historique.add(nouvelHistorique);
|
||||
demande.setHistoriqueStatuts(historique);
|
||||
|
||||
// Actions spécifiques selon le nouveau statut
|
||||
switch (nouveauStatut) {
|
||||
case SOUMISE -> demande.setDateSoumission(LocalDateTime.now());
|
||||
case APPROUVEE, APPROUVEE_PARTIELLEMENT -> demande.setDateApprobation(LocalDateTime.now());
|
||||
case VERSEE -> demande.setDateVersement(LocalDateTime.now());
|
||||
case CLOTUREE -> demande.setDateCloture(LocalDateTime.now());
|
||||
}
|
||||
|
||||
// Mise à jour du cache
|
||||
ajouterAuCache(demande);
|
||||
|
||||
LOG.infof(
|
||||
"Statut changé avec succès pour la demande %s: %s -> %s",
|
||||
demandeId, ancienStatut, nouveauStatut);
|
||||
return demande;
|
||||
}
|
||||
|
||||
// === RECHERCHE ET FILTRAGE ===
|
||||
|
||||
/**
|
||||
* Recherche des demandes avec filtres
|
||||
*
|
||||
* @param filtres Map des critères de recherche
|
||||
* @return Liste des demandes correspondantes
|
||||
*/
|
||||
public List<DemandeAideDTO> rechercherAvecFiltres(Map<String, Object> filtres) {
|
||||
LOG.debugf("Recherche de demandes avec filtres: %s", filtres);
|
||||
|
||||
// Simulation de recherche - dans une vraie implémentation,
|
||||
// ceci utiliserait des requêtes de base de données optimisées
|
||||
List<DemandeAideDTO> toutesLesDemandes = simulerRecuperationToutesLesDemandes();
|
||||
|
||||
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<DemandeAideDTO> 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<DemandeAideDTO> obtenirDemandesEnRetard(UUID organisationId) {
|
||||
LOG.debugf("Récupération des demandes en retard pour: %s", organisationId);
|
||||
|
||||
return simulerRecuperationToutesLesDemandes().stream()
|
||||
.filter(demande -> demande.getAssociationId().equals(organisationId))
|
||||
.filter(DemandeAideDTO::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(DemandeAideDTO 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(DemandeAideDTO 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().equals(valeur)) return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Compare deux demandes par priorité */
|
||||
private int comparerParPriorite(DemandeAideDTO d1, DemandeAideDTO d2) {
|
||||
// D'abord par score de priorité (plus bas = plus prioritaire)
|
||||
int comparaisonScore = Double.compare(d1.getScorePriorite(), d2.getScorePriorite());
|
||||
if (comparaisonScore != 0) return comparaisonScore;
|
||||
|
||||
// Puis par date de création (plus ancien = plus prioritaire)
|
||||
return d1.getDateCreation().compareTo(d2.getDateCreation());
|
||||
}
|
||||
|
||||
// === GESTION DU CACHE ===
|
||||
|
||||
private void ajouterAuCache(DemandeAideDTO demande) {
|
||||
cacheDemandesRecentes.put(demande.getId(), demande);
|
||||
cacheTimestamps.put(demande.getId(), LocalDateTime.now());
|
||||
|
||||
// Nettoyage du cache si trop volumineux
|
||||
if (cacheDemandesRecentes.size() > 100) {
|
||||
nettoyerCache();
|
||||
}
|
||||
}
|
||||
|
||||
private DemandeAideDTO 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());
|
||||
}
|
||||
|
||||
// === MÉTHODES DE SIMULATION (À REMPLACER PAR DE VRAIS REPOSITORIES) ===
|
||||
|
||||
private DemandeAideDTO simulerRecuperationBDD(UUID id) {
|
||||
// Simulation - dans une vraie implémentation, ceci ferait appel au repository
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<DemandeAideDTO> simulerRecuperationToutesLesDemandes() {
|
||||
// Simulation - dans une vraie implémentation, ceci ferait appel au repository
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
package dev.lions.unionflow.server.service;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.document.DocumentDTO;
|
||||
import dev.lions.unionflow.server.api.dto.document.PieceJointeDTO;
|
||||
import dev.lions.unionflow.server.entity.*;
|
||||
import dev.lions.unionflow.server.repository.*;
|
||||
import dev.lions.unionflow.server.service.KeycloakService;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.ws.rs.NotFoundException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
/**
|
||||
* Service métier pour la gestion documentaire
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 3.0
|
||||
* @since 2025-01-29
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class DocumentService {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(DocumentService.class);
|
||||
|
||||
@Inject DocumentRepository documentRepository;
|
||||
|
||||
@Inject PieceJointeRepository pieceJointeRepository;
|
||||
|
||||
@Inject MembreRepository membreRepository;
|
||||
|
||||
@Inject OrganisationRepository organisationRepository;
|
||||
|
||||
@Inject CotisationRepository cotisationRepository;
|
||||
|
||||
@Inject AdhesionRepository adhesionRepository;
|
||||
|
||||
@Inject DemandeAideRepository demandeAideRepository;
|
||||
|
||||
@Inject TransactionWaveRepository transactionWaveRepository;
|
||||
|
||||
@Inject KeycloakService keycloakService;
|
||||
|
||||
/**
|
||||
* Crée un nouveau document
|
||||
*
|
||||
* @param documentDTO DTO du document à créer
|
||||
* @return DTO du document créé
|
||||
*/
|
||||
@Transactional
|
||||
public DocumentDTO creerDocument(DocumentDTO documentDTO) {
|
||||
LOG.infof("Création d'un nouveau document: %s", documentDTO.getNomFichier());
|
||||
|
||||
Document document = convertToEntity(documentDTO);
|
||||
document.setCreePar(keycloakService.getCurrentUserEmail());
|
||||
|
||||
documentRepository.persist(document);
|
||||
LOG.infof("Document créé avec succès: ID=%s, Fichier=%s", document.getId(), document.getNomFichier());
|
||||
|
||||
return convertToDTO(document);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve un document par son ID
|
||||
*
|
||||
* @param id ID du document
|
||||
* @return DTO du document
|
||||
*/
|
||||
public DocumentDTO trouverParId(UUID id) {
|
||||
return documentRepository
|
||||
.findDocumentById(id)
|
||||
.map(this::convertToDTO)
|
||||
.orElseThrow(() -> new NotFoundException("Document non trouvé avec l'ID: " + id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Enregistre un téléchargement de document
|
||||
*
|
||||
* @param id ID du document
|
||||
*/
|
||||
@Transactional
|
||||
public void enregistrerTelechargement(UUID id) {
|
||||
Document document =
|
||||
documentRepository
|
||||
.findDocumentById(id)
|
||||
.orElseThrow(() -> new NotFoundException("Document non trouvé avec l'ID: " + id));
|
||||
|
||||
document.setNombreTelechargements(
|
||||
(document.getNombreTelechargements() != null ? document.getNombreTelechargements() : 0) + 1);
|
||||
document.setDateDernierTelechargement(LocalDateTime.now());
|
||||
document.setModifiePar(keycloakService.getCurrentUserEmail());
|
||||
|
||||
documentRepository.persist(document);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée une pièce jointe
|
||||
*
|
||||
* @param pieceJointeDTO DTO de la pièce jointe à créer
|
||||
* @return DTO de la pièce jointe créée
|
||||
*/
|
||||
@Transactional
|
||||
public PieceJointeDTO creerPieceJointe(PieceJointeDTO pieceJointeDTO) {
|
||||
LOG.infof("Création d'une nouvelle pièce jointe");
|
||||
|
||||
PieceJointe pieceJointe = convertToEntity(pieceJointeDTO);
|
||||
|
||||
// Vérifier qu'une seule relation est renseignée
|
||||
if (!pieceJointe.isValide()) {
|
||||
throw new IllegalArgumentException("Une seule relation doit être renseignée pour une pièce jointe");
|
||||
}
|
||||
|
||||
pieceJointe.setCreePar(keycloakService.getCurrentUserEmail());
|
||||
pieceJointeRepository.persist(pieceJointe);
|
||||
|
||||
LOG.infof("Pièce jointe créée avec succès: ID=%s", pieceJointe.getId());
|
||||
return convertToDTO(pieceJointe);
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste toutes les pièces jointes d'un document
|
||||
*
|
||||
* @param documentId ID du document
|
||||
* @return Liste des pièces jointes
|
||||
*/
|
||||
public List<PieceJointeDTO> listerPiecesJointesParDocument(UUID documentId) {
|
||||
return pieceJointeRepository.findByDocumentId(documentId).stream()
|
||||
.map(this::convertToDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// MÉTHODES PRIVÉES
|
||||
// ========================================
|
||||
|
||||
/** Convertit une entité Document en DTO */
|
||||
private DocumentDTO convertToDTO(Document document) {
|
||||
if (document == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
DocumentDTO dto = new DocumentDTO();
|
||||
dto.setId(document.getId());
|
||||
dto.setNomFichier(document.getNomFichier());
|
||||
dto.setNomOriginal(document.getNomOriginal());
|
||||
dto.setCheminStockage(document.getCheminStockage());
|
||||
dto.setTypeMime(document.getTypeMime());
|
||||
dto.setTailleOctets(document.getTailleOctets());
|
||||
dto.setTypeDocument(document.getTypeDocument());
|
||||
dto.setHashMd5(document.getHashMd5());
|
||||
dto.setHashSha256(document.getHashSha256());
|
||||
dto.setDescription(document.getDescription());
|
||||
dto.setNombreTelechargements(document.getNombreTelechargements());
|
||||
dto.setTailleFormatee(document.getTailleFormatee());
|
||||
dto.setDateCreation(document.getDateCreation());
|
||||
dto.setDateModification(document.getDateModification());
|
||||
dto.setActif(document.getActif());
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
/** Convertit un DTO en entité Document */
|
||||
private Document convertToEntity(DocumentDTO dto) {
|
||||
if (dto == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Document document = new Document();
|
||||
document.setNomFichier(dto.getNomFichier());
|
||||
document.setNomOriginal(dto.getNomOriginal());
|
||||
document.setCheminStockage(dto.getCheminStockage());
|
||||
document.setTypeMime(dto.getTypeMime());
|
||||
document.setTailleOctets(dto.getTailleOctets());
|
||||
document.setTypeDocument(dto.getTypeDocument() != null ? dto.getTypeDocument() : dev.lions.unionflow.server.api.enums.document.TypeDocument.AUTRE);
|
||||
document.setHashMd5(dto.getHashMd5());
|
||||
document.setHashSha256(dto.getHashSha256());
|
||||
document.setDescription(dto.getDescription());
|
||||
document.setNombreTelechargements(dto.getNombreTelechargements() != null ? dto.getNombreTelechargements() : 0);
|
||||
|
||||
return document;
|
||||
}
|
||||
|
||||
/** Convertit une entité PieceJointe en DTO */
|
||||
private PieceJointeDTO convertToDTO(PieceJointe pieceJointe) {
|
||||
if (pieceJointe == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
PieceJointeDTO dto = new PieceJointeDTO();
|
||||
dto.setId(pieceJointe.getId());
|
||||
dto.setOrdre(pieceJointe.getOrdre());
|
||||
dto.setLibelle(pieceJointe.getLibelle());
|
||||
dto.setCommentaire(pieceJointe.getCommentaire());
|
||||
|
||||
if (pieceJointe.getDocument() != null) {
|
||||
dto.setDocumentId(pieceJointe.getDocument().getId());
|
||||
}
|
||||
if (pieceJointe.getMembre() != null) {
|
||||
dto.setMembreId(pieceJointe.getMembre().getId());
|
||||
}
|
||||
if (pieceJointe.getOrganisation() != null) {
|
||||
dto.setOrganisationId(pieceJointe.getOrganisation().getId());
|
||||
}
|
||||
if (pieceJointe.getCotisation() != null) {
|
||||
dto.setCotisationId(pieceJointe.getCotisation().getId());
|
||||
}
|
||||
if (pieceJointe.getAdhesion() != null) {
|
||||
dto.setAdhesionId(pieceJointe.getAdhesion().getId());
|
||||
}
|
||||
if (pieceJointe.getDemandeAide() != null) {
|
||||
dto.setDemandeAideId(pieceJointe.getDemandeAide().getId());
|
||||
}
|
||||
if (pieceJointe.getTransactionWave() != null) {
|
||||
dto.setTransactionWaveId(pieceJointe.getTransactionWave().getId());
|
||||
}
|
||||
|
||||
dto.setDateCreation(pieceJointe.getDateCreation());
|
||||
dto.setDateModification(pieceJointe.getDateModification());
|
||||
dto.setActif(pieceJointe.getActif());
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
/** Convertit un DTO en entité PieceJointe */
|
||||
private PieceJointe convertToEntity(PieceJointeDTO dto) {
|
||||
if (dto == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
PieceJointe pieceJointe = new PieceJointe();
|
||||
pieceJointe.setOrdre(dto.getOrdre() != null ? dto.getOrdre() : 1);
|
||||
pieceJointe.setLibelle(dto.getLibelle());
|
||||
pieceJointe.setCommentaire(dto.getCommentaire());
|
||||
|
||||
// Relation Document
|
||||
if (dto.getDocumentId() != null) {
|
||||
Document document =
|
||||
documentRepository
|
||||
.findDocumentById(dto.getDocumentId())
|
||||
.orElseThrow(() -> new NotFoundException("Document non trouvé avec l'ID: " + dto.getDocumentId()));
|
||||
pieceJointe.setDocument(document);
|
||||
}
|
||||
|
||||
// Relations flexibles (une seule doit être renseignée)
|
||||
if (dto.getMembreId() != null) {
|
||||
Membre membre =
|
||||
membreRepository
|
||||
.findByIdOptional(dto.getMembreId())
|
||||
.orElseThrow(() -> new NotFoundException("Membre non trouvé avec l'ID: " + dto.getMembreId()));
|
||||
pieceJointe.setMembre(membre);
|
||||
}
|
||||
|
||||
if (dto.getOrganisationId() != null) {
|
||||
Organisation org =
|
||||
organisationRepository
|
||||
.findByIdOptional(dto.getOrganisationId())
|
||||
.orElseThrow(
|
||||
() ->
|
||||
new NotFoundException(
|
||||
"Organisation non trouvée avec l'ID: " + dto.getOrganisationId()));
|
||||
pieceJointe.setOrganisation(org);
|
||||
}
|
||||
|
||||
if (dto.getCotisationId() != null) {
|
||||
Cotisation cotisation =
|
||||
cotisationRepository
|
||||
.findByIdOptional(dto.getCotisationId())
|
||||
.orElseThrow(
|
||||
() -> new NotFoundException("Cotisation non trouvée avec l'ID: " + dto.getCotisationId()));
|
||||
pieceJointe.setCotisation(cotisation);
|
||||
}
|
||||
|
||||
if (dto.getAdhesionId() != null) {
|
||||
Adhesion adhesion =
|
||||
adhesionRepository
|
||||
.findByIdOptional(dto.getAdhesionId())
|
||||
.orElseThrow(
|
||||
() -> new NotFoundException("Adhésion non trouvée avec l'ID: " + dto.getAdhesionId()));
|
||||
pieceJointe.setAdhesion(adhesion);
|
||||
}
|
||||
|
||||
if (dto.getDemandeAideId() != null) {
|
||||
DemandeAide demandeAide =
|
||||
demandeAideRepository
|
||||
.findByIdOptional(dto.getDemandeAideId())
|
||||
.orElseThrow(
|
||||
() ->
|
||||
new NotFoundException(
|
||||
"Demande d'aide non trouvée avec l'ID: " + dto.getDemandeAideId()));
|
||||
pieceJointe.setDemandeAide(demandeAide);
|
||||
}
|
||||
|
||||
if (dto.getTransactionWaveId() != null) {
|
||||
TransactionWave transactionWave =
|
||||
transactionWaveRepository
|
||||
.findTransactionWaveById(dto.getTransactionWaveId())
|
||||
.orElseThrow(
|
||||
() ->
|
||||
new NotFoundException(
|
||||
"Transaction Wave non trouvée avec l'ID: " + dto.getTransactionWaveId()));
|
||||
pieceJointe.setTransactionWave(transactionWave);
|
||||
}
|
||||
|
||||
return pieceJointe;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,340 @@
|
||||
package dev.lions.unionflow.server.service;
|
||||
|
||||
import dev.lions.unionflow.server.entity.Evenement;
|
||||
import dev.lions.unionflow.server.entity.Evenement.StatutEvenement;
|
||||
import dev.lions.unionflow.server.entity.Evenement.TypeEvenement;
|
||||
import dev.lions.unionflow.server.repository.EvenementRepository;
|
||||
import dev.lions.unionflow.server.repository.MembreRepository;
|
||||
import dev.lions.unionflow.server.repository.OrganisationRepository;
|
||||
import dev.lions.unionflow.server.service.KeycloakService;
|
||||
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 java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
/**
|
||||
* Service métier pour la gestion des événements Version simplifiée pour tester les imports et
|
||||
* Lombok
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 1.0
|
||||
* @since 2025-01-15
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class EvenementService {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(EvenementService.class);
|
||||
|
||||
@Inject EvenementRepository evenementRepository;
|
||||
|
||||
@Inject MembreRepository membreRepository;
|
||||
|
||||
@Inject OrganisationRepository organisationRepository;
|
||||
|
||||
@Inject KeycloakService keycloakService;
|
||||
|
||||
/**
|
||||
* Crée un nouvel événement
|
||||
*
|
||||
* @param evenement l'événement à créer
|
||||
* @return l'événement créé
|
||||
* @throws IllegalArgumentException si les données sont invalides
|
||||
*/
|
||||
@Transactional
|
||||
public Evenement creerEvenement(Evenement evenement) {
|
||||
LOG.infof("Création événement: %s", evenement.getTitre());
|
||||
|
||||
// Validation des données
|
||||
validerEvenement(evenement);
|
||||
|
||||
// Vérifier l'unicité du titre dans l'organisation
|
||||
if (evenement.getOrganisation() != null) {
|
||||
Optional<Evenement> existant = evenementRepository.findByTitre(evenement.getTitre());
|
||||
if (existant.isPresent()
|
||||
&& existant.get().getOrganisation().getId().equals(evenement.getOrganisation().getId())) {
|
||||
throw new IllegalArgumentException(
|
||||
"Un événement avec ce titre existe déjà dans cette organisation");
|
||||
}
|
||||
}
|
||||
|
||||
// Métadonnées de création
|
||||
evenement.setCreePar(keycloakService.getCurrentUserEmail());
|
||||
|
||||
// Valeurs par défaut
|
||||
if (evenement.getStatut() == null) {
|
||||
evenement.setStatut(StatutEvenement.PLANIFIE);
|
||||
}
|
||||
if (evenement.getActif() == null) {
|
||||
evenement.setActif(true);
|
||||
}
|
||||
if (evenement.getVisiblePublic() == null) {
|
||||
evenement.setVisiblePublic(true);
|
||||
}
|
||||
if (evenement.getInscriptionRequise() == null) {
|
||||
evenement.setInscriptionRequise(true);
|
||||
}
|
||||
|
||||
evenementRepository.persist(evenement);
|
||||
|
||||
LOG.infof("Événement créé avec succès: ID=%s, Titre=%s", evenement.getId(), evenement.getTitre());
|
||||
return evenement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour un événement existant
|
||||
*
|
||||
* @param id l'UUID de l'événement
|
||||
* @param evenementMisAJour les nouvelles données
|
||||
* @return l'événement mis à jour
|
||||
* @throws IllegalArgumentException si l'événement n'existe pas
|
||||
*/
|
||||
@Transactional
|
||||
public Evenement mettreAJourEvenement(UUID id, Evenement evenementMisAJour) {
|
||||
LOG.infof("Mise à jour événement ID: %s", id);
|
||||
|
||||
Evenement evenementExistant =
|
||||
evenementRepository
|
||||
.findByIdOptional(id)
|
||||
.orElseThrow(
|
||||
() -> new IllegalArgumentException("Événement non trouvé avec l'ID: " + id));
|
||||
|
||||
// Vérifier les permissions
|
||||
if (!peutModifierEvenement(evenementExistant)) {
|
||||
throw new SecurityException("Vous n'avez pas les permissions pour modifier cet événement");
|
||||
}
|
||||
|
||||
// Validation des nouvelles données
|
||||
validerEvenement(evenementMisAJour);
|
||||
|
||||
// Mise à jour des champs
|
||||
evenementExistant.setTitre(evenementMisAJour.getTitre());
|
||||
evenementExistant.setDescription(evenementMisAJour.getDescription());
|
||||
evenementExistant.setDateDebut(evenementMisAJour.getDateDebut());
|
||||
evenementExistant.setDateFin(evenementMisAJour.getDateFin());
|
||||
evenementExistant.setLieu(evenementMisAJour.getLieu());
|
||||
evenementExistant.setAdresse(evenementMisAJour.getAdresse());
|
||||
evenementExistant.setTypeEvenement(evenementMisAJour.getTypeEvenement());
|
||||
evenementExistant.setCapaciteMax(evenementMisAJour.getCapaciteMax());
|
||||
evenementExistant.setPrix(evenementMisAJour.getPrix());
|
||||
evenementExistant.setInscriptionRequise(evenementMisAJour.getInscriptionRequise());
|
||||
evenementExistant.setDateLimiteInscription(evenementMisAJour.getDateLimiteInscription());
|
||||
evenementExistant.setInstructionsParticulieres(
|
||||
evenementMisAJour.getInstructionsParticulieres());
|
||||
evenementExistant.setContactOrganisateur(evenementMisAJour.getContactOrganisateur());
|
||||
evenementExistant.setMaterielRequis(evenementMisAJour.getMaterielRequis());
|
||||
evenementExistant.setVisiblePublic(evenementMisAJour.getVisiblePublic());
|
||||
|
||||
// Métadonnées de modification
|
||||
evenementExistant.setModifiePar(keycloakService.getCurrentUserEmail());
|
||||
|
||||
evenementRepository.update(evenementExistant);
|
||||
|
||||
LOG.infof("Événement mis à jour avec succès: ID=%s", id);
|
||||
return evenementExistant;
|
||||
}
|
||||
|
||||
/** Trouve un événement par ID */
|
||||
public Optional<Evenement> trouverParId(UUID id) {
|
||||
return evenementRepository.findByIdOptional(id);
|
||||
}
|
||||
|
||||
/** Liste tous les événements actifs avec pagination */
|
||||
public List<Evenement> listerEvenementsActifs(Page page, Sort sort) {
|
||||
return evenementRepository.findAllActifs(page, sort);
|
||||
}
|
||||
|
||||
/** Liste les événements à venir */
|
||||
public List<Evenement> listerEvenementsAVenir(Page page, Sort sort) {
|
||||
return evenementRepository.findEvenementsAVenir(page, sort);
|
||||
}
|
||||
|
||||
/** Liste les événements publics */
|
||||
public List<Evenement> listerEvenementsPublics(Page page, Sort sort) {
|
||||
return evenementRepository.findEvenementsPublics(page, sort);
|
||||
}
|
||||
|
||||
/** Recherche d'événements par terme */
|
||||
public List<Evenement> rechercherEvenements(String terme, Page page, Sort sort) {
|
||||
return evenementRepository.rechercheAvancee(
|
||||
terme, null, null, null, null, null, null, null, null, null, page, sort);
|
||||
}
|
||||
|
||||
/** Liste les événements par type */
|
||||
public List<Evenement> listerParType(TypeEvenement type, Page page, Sort sort) {
|
||||
return evenementRepository.findByType(type, page, sort);
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime logiquement un événement
|
||||
*
|
||||
* @param id l'UUID de l'événement à supprimer
|
||||
* @throws IllegalArgumentException si l'événement n'existe pas
|
||||
*/
|
||||
@Transactional
|
||||
public void supprimerEvenement(UUID id) {
|
||||
LOG.infof("Suppression événement ID: %s", id);
|
||||
|
||||
Evenement evenement =
|
||||
evenementRepository
|
||||
.findByIdOptional(id)
|
||||
.orElseThrow(
|
||||
() -> new IllegalArgumentException("Événement non trouvé avec l'ID: " + id));
|
||||
|
||||
// Vérifier les permissions
|
||||
if (!peutModifierEvenement(evenement)) {
|
||||
throw new SecurityException("Vous n'avez pas les permissions pour supprimer cet événement");
|
||||
}
|
||||
|
||||
// Vérifier s'il y a des inscriptions
|
||||
if (evenement.getNombreInscrits() > 0) {
|
||||
throw new IllegalStateException("Impossible de supprimer un événement avec des inscriptions");
|
||||
}
|
||||
|
||||
// Suppression logique
|
||||
evenement.setActif(false);
|
||||
evenement.setModifiePar(keycloakService.getCurrentUserEmail());
|
||||
|
||||
evenementRepository.update(evenement);
|
||||
|
||||
LOG.infof("Événement supprimé avec succès: ID=%s", id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change le statut d'un événement
|
||||
*
|
||||
* @param id l'UUID de l'événement
|
||||
* @param nouveauStatut le nouveau statut
|
||||
* @return l'événement mis à jour
|
||||
*/
|
||||
@Transactional
|
||||
public Evenement changerStatut(UUID id, StatutEvenement nouveauStatut) {
|
||||
LOG.infof("Changement statut événement ID: %s vers %s", id, nouveauStatut);
|
||||
|
||||
Evenement evenement =
|
||||
evenementRepository
|
||||
.findByIdOptional(id)
|
||||
.orElseThrow(
|
||||
() -> new IllegalArgumentException("Événement non trouvé avec l'ID: " + id));
|
||||
|
||||
// Vérifier les permissions
|
||||
if (!peutModifierEvenement(evenement)) {
|
||||
throw new SecurityException("Vous n'avez pas les permissions pour modifier cet événement");
|
||||
}
|
||||
|
||||
// Valider le changement de statut
|
||||
validerChangementStatut(evenement.getStatut(), nouveauStatut);
|
||||
|
||||
evenement.setStatut(nouveauStatut);
|
||||
evenement.setModifiePar(keycloakService.getCurrentUserEmail());
|
||||
|
||||
evenementRepository.update(evenement);
|
||||
|
||||
LOG.infof("Statut événement changé avec succès: ID=%s, Nouveau statut=%s", id, nouveauStatut);
|
||||
return evenement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compte le nombre total d'événements
|
||||
*
|
||||
* @return le nombre total d'événements
|
||||
*/
|
||||
public long countEvenements() {
|
||||
return evenementRepository.count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compte le nombre d'événements actifs
|
||||
*
|
||||
* @return le nombre d'événements actifs
|
||||
*/
|
||||
public long countEvenementsActifs() {
|
||||
return evenementRepository.countActifs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtient les statistiques des événements
|
||||
*
|
||||
* @return les statistiques sous forme de Map
|
||||
*/
|
||||
public Map<String, Object> obtenirStatistiques() {
|
||||
Map<String, Long> statsBase = evenementRepository.getStatistiques();
|
||||
|
||||
long total = statsBase.getOrDefault("total", 0L);
|
||||
long actifs = statsBase.getOrDefault("actifs", 0L);
|
||||
long aVenir = statsBase.getOrDefault("aVenir", 0L);
|
||||
long enCours = statsBase.getOrDefault("enCours", 0L);
|
||||
|
||||
Map<String, Object> result = new java.util.HashMap<>();
|
||||
result.put("total", total);
|
||||
result.put("actifs", actifs);
|
||||
result.put("aVenir", aVenir);
|
||||
result.put("enCours", enCours);
|
||||
result.put("passes", statsBase.getOrDefault("passes", 0L));
|
||||
result.put("publics", statsBase.getOrDefault("publics", 0L));
|
||||
result.put("avecInscription", statsBase.getOrDefault("avecInscription", 0L));
|
||||
result.put("tauxActivite", total > 0 ? (actifs * 100.0 / total) : 0.0);
|
||||
result.put("tauxEvenementsAVenir", total > 0 ? (aVenir * 100.0 / total) : 0.0);
|
||||
result.put("tauxEvenementsEnCours", total > 0 ? (enCours * 100.0 / total) : 0.0);
|
||||
result.put("timestamp", LocalDateTime.now());
|
||||
return result;
|
||||
}
|
||||
|
||||
// Méthodes privées de validation et permissions
|
||||
|
||||
/** Valide les données d'un événement */
|
||||
private void validerEvenement(Evenement evenement) {
|
||||
if (evenement.getTitre() == null || evenement.getTitre().trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("Le titre de l'événement est obligatoire");
|
||||
}
|
||||
|
||||
if (evenement.getDateDebut() == null) {
|
||||
throw new IllegalArgumentException("La date de début est obligatoire");
|
||||
}
|
||||
|
||||
if (evenement.getDateDebut().isBefore(LocalDateTime.now().minusHours(1))) {
|
||||
throw new IllegalArgumentException("La date de début ne peut pas être dans le passé");
|
||||
}
|
||||
|
||||
if (evenement.getDateFin() != null
|
||||
&& evenement.getDateFin().isBefore(evenement.getDateDebut())) {
|
||||
throw new IllegalArgumentException(
|
||||
"La date de fin ne peut pas être antérieure à la date de début");
|
||||
}
|
||||
|
||||
if (evenement.getCapaciteMax() != null && evenement.getCapaciteMax() <= 0) {
|
||||
throw new IllegalArgumentException("La capacité maximale doit être positive");
|
||||
}
|
||||
|
||||
if (evenement.getPrix() != null
|
||||
&& evenement.getPrix().compareTo(java.math.BigDecimal.ZERO) < 0) {
|
||||
throw new IllegalArgumentException("Le prix ne peut pas être négatif");
|
||||
}
|
||||
}
|
||||
|
||||
/** Valide un changement de statut */
|
||||
private void validerChangementStatut(
|
||||
StatutEvenement statutActuel, StatutEvenement nouveauStatut) {
|
||||
// Règles de transition simplifiées pour la version mobile
|
||||
if (statutActuel == StatutEvenement.TERMINE || statutActuel == StatutEvenement.ANNULE) {
|
||||
throw new IllegalArgumentException(
|
||||
"Impossible de changer le statut d'un événement terminé ou annulé");
|
||||
}
|
||||
}
|
||||
|
||||
/** Vérifie les permissions de modification pour l'application mobile */
|
||||
private boolean peutModifierEvenement(Evenement evenement) {
|
||||
if (keycloakService.hasRole("ADMIN") || keycloakService.hasRole("ORGANISATEUR_EVENEMENT")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String utilisateurActuel = keycloakService.getCurrentUserEmail();
|
||||
return utilisateurActuel != null && utilisateurActuel.equals(evenement.getCreePar());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
package dev.lions.unionflow.server.service;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.finance.CotisationDTO;
|
||||
import dev.lions.unionflow.server.entity.Cotisation;
|
||||
import dev.lions.unionflow.server.repository.CotisationRepository;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Service d'export des données en Excel et PDF
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 1.0
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class ExportService {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(ExportService.class);
|
||||
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("dd/MM/yyyy");
|
||||
private static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm");
|
||||
|
||||
@Inject
|
||||
CotisationRepository cotisationRepository;
|
||||
|
||||
@Inject
|
||||
CotisationService cotisationService;
|
||||
|
||||
/**
|
||||
* Exporte les cotisations en format CSV (compatible Excel)
|
||||
*/
|
||||
public byte[] exporterCotisationsCSV(List<UUID> cotisationIds) {
|
||||
LOG.infof("Export CSV de %d cotisations", cotisationIds.size());
|
||||
|
||||
StringBuilder csv = new StringBuilder();
|
||||
csv.append("Numéro Référence;Membre;Type;Montant Dû;Montant Payé;Statut;Date Échéance;Date Paiement;Méthode Paiement\n");
|
||||
|
||||
for (UUID id : cotisationIds) {
|
||||
Optional<Cotisation> cotisationOpt = cotisationRepository.findByIdOptional(id);
|
||||
if (cotisationOpt.isPresent()) {
|
||||
Cotisation c = cotisationOpt.get();
|
||||
String nomMembre = c.getMembre() != null
|
||||
? c.getMembre().getNom() + " " + c.getMembre().getPrenom()
|
||||
: "";
|
||||
csv.append(String.format("%s;%s;%s;%s;%s;%s;%s;%s;%s\n",
|
||||
c.getNumeroReference() != null ? c.getNumeroReference() : "",
|
||||
nomMembre,
|
||||
c.getTypeCotisation() != null ? c.getTypeCotisation() : "",
|
||||
c.getMontantDu() != null ? c.getMontantDu().toString() : "0",
|
||||
c.getMontantPaye() != null ? c.getMontantPaye().toString() : "0",
|
||||
c.getStatut() != null ? c.getStatut() : "",
|
||||
c.getDateEcheance() != null ? c.getDateEcheance().format(DATE_FORMATTER) : "",
|
||||
c.getDatePaiement() != null ? c.getDatePaiement().format(DATETIME_FORMATTER) : "",
|
||||
c.getMethodePaiement() != null ? c.getMethodePaiement() : ""
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return csv.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exporte toutes les cotisations filtrées en CSV
|
||||
*/
|
||||
public byte[] exporterToutesCotisationsCSV(String statut, String type, UUID associationId) {
|
||||
LOG.info("Export CSV de toutes les cotisations");
|
||||
|
||||
List<Cotisation> cotisations = cotisationRepository.listAll();
|
||||
|
||||
// Filtrer
|
||||
if (statut != null && !statut.isEmpty()) {
|
||||
cotisations = cotisations.stream()
|
||||
.filter(c -> c.getStatut() != null && c.getStatut().equals(statut))
|
||||
.toList();
|
||||
}
|
||||
if (type != null && !type.isEmpty()) {
|
||||
cotisations = cotisations.stream()
|
||||
.filter(c -> c.getTypeCotisation() != null && c.getTypeCotisation().equals(type))
|
||||
.toList();
|
||||
}
|
||||
// Note: le filtrage par association n'est pas disponible car Membre n'a pas de lien direct
|
||||
// avec Association dans cette version du modèle
|
||||
|
||||
List<UUID> ids = cotisations.stream().map(Cotisation::getId).toList();
|
||||
return exporterCotisationsCSV(ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère un reçu de paiement en format texte (pour impression)
|
||||
*/
|
||||
public byte[] genererRecuPaiement(UUID cotisationId) {
|
||||
LOG.infof("Génération reçu pour cotisation: %s", cotisationId);
|
||||
|
||||
Optional<Cotisation> cotisationOpt = cotisationRepository.findByIdOptional(cotisationId);
|
||||
if (cotisationOpt.isEmpty()) {
|
||||
return "Cotisation non trouvée".getBytes();
|
||||
}
|
||||
|
||||
Cotisation c = cotisationOpt.get();
|
||||
|
||||
StringBuilder recu = new StringBuilder();
|
||||
recu.append("═══════════════════════════════════════════════════════════════\n");
|
||||
recu.append(" REÇU DE PAIEMENT\n");
|
||||
recu.append("═══════════════════════════════════════════════════════════════\n\n");
|
||||
|
||||
recu.append("Numéro de reçu : ").append(c.getNumeroReference()).append("\n");
|
||||
recu.append("Date : ").append(LocalDateTime.now().format(DATETIME_FORMATTER)).append("\n\n");
|
||||
|
||||
recu.append("───────────────────────────────────────────────────────────────\n");
|
||||
recu.append(" INFORMATIONS MEMBRE\n");
|
||||
recu.append("───────────────────────────────────────────────────────────────\n");
|
||||
|
||||
if (c.getMembre() != null) {
|
||||
recu.append("Nom : ").append(c.getMembre().getNom()).append(" ").append(c.getMembre().getPrenom()).append("\n");
|
||||
recu.append("Numéro membre : ").append(c.getMembre().getNumeroMembre()).append("\n");
|
||||
}
|
||||
|
||||
recu.append("\n───────────────────────────────────────────────────────────────\n");
|
||||
recu.append(" DÉTAILS DU PAIEMENT\n");
|
||||
recu.append("───────────────────────────────────────────────────────────────\n");
|
||||
|
||||
recu.append("Type cotisation : ").append(c.getTypeCotisation() != null ? c.getTypeCotisation() : "").append("\n");
|
||||
recu.append("Période : ").append(c.getPeriode() != null ? c.getPeriode() : "").append("\n");
|
||||
recu.append("Montant dû : ").append(formatMontant(c.getMontantDu())).append("\n");
|
||||
recu.append("Montant payé : ").append(formatMontant(c.getMontantPaye())).append("\n");
|
||||
recu.append("Mode de paiement : ").append(c.getMethodePaiement() != null ? c.getMethodePaiement() : "").append("\n");
|
||||
recu.append("Date de paiement : ").append(c.getDatePaiement() != null ? c.getDatePaiement().format(DATETIME_FORMATTER) : "").append("\n");
|
||||
recu.append("Statut : ").append(c.getStatut() != null ? c.getStatut() : "").append("\n");
|
||||
|
||||
recu.append("\n═══════════════════════════════════════════════════════════════\n");
|
||||
recu.append(" Ce document fait foi de paiement de cotisation\n");
|
||||
recu.append(" Merci de votre confiance !\n");
|
||||
recu.append("═══════════════════════════════════════════════════════════════\n");
|
||||
|
||||
return recu.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère plusieurs reçus de paiement
|
||||
*/
|
||||
public byte[] genererRecusGroupes(List<UUID> cotisationIds) {
|
||||
LOG.infof("Génération de %d reçus groupés", cotisationIds.size());
|
||||
|
||||
StringBuilder allRecus = new StringBuilder();
|
||||
for (int i = 0; i < cotisationIds.size(); i++) {
|
||||
byte[] recu = genererRecuPaiement(cotisationIds.get(i));
|
||||
allRecus.append(new String(recu, java.nio.charset.StandardCharsets.UTF_8));
|
||||
if (i < cotisationIds.size() - 1) {
|
||||
allRecus.append("\n\n════════════════════════ PAGE SUIVANTE ════════════════════════\n\n");
|
||||
}
|
||||
}
|
||||
|
||||
return allRecus.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère un rapport mensuel
|
||||
*/
|
||||
public byte[] genererRapportMensuel(int annee, int mois, UUID associationId) {
|
||||
LOG.infof("Génération rapport mensuel: %d/%d", mois, annee);
|
||||
|
||||
List<Cotisation> cotisations = cotisationRepository.listAll();
|
||||
|
||||
// Filtrer par mois/année et association
|
||||
LocalDate debut = LocalDate.of(annee, mois, 1);
|
||||
LocalDate fin = debut.plusMonths(1).minusDays(1);
|
||||
|
||||
cotisations = cotisations.stream()
|
||||
.filter(c -> {
|
||||
if (c.getDateCreation() == null) return false;
|
||||
LocalDate dateCot = c.getDateCreation().toLocalDate();
|
||||
return !dateCot.isBefore(debut) && !dateCot.isAfter(fin);
|
||||
})
|
||||
// Note: le filtrage par association n'est pas implémenté ici
|
||||
.toList();
|
||||
|
||||
// Calculer les statistiques
|
||||
long total = cotisations.size();
|
||||
long payees = cotisations.stream().filter(c -> "PAYEE".equals(c.getStatut())).count();
|
||||
long enAttente = cotisations.stream().filter(c -> "EN_ATTENTE".equals(c.getStatut())).count();
|
||||
long enRetard = cotisations.stream().filter(c -> "EN_RETARD".equals(c.getStatut())).count();
|
||||
|
||||
BigDecimal montantTotal = cotisations.stream()
|
||||
.map(c -> c.getMontantDu() != null ? c.getMontantDu() : BigDecimal.ZERO)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
BigDecimal montantCollecte = cotisations.stream()
|
||||
.filter(c -> "PAYEE".equals(c.getStatut()) || "PARTIELLEMENT_PAYEE".equals(c.getStatut()))
|
||||
.map(c -> c.getMontantPaye() != null ? c.getMontantPaye() : BigDecimal.ZERO)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
double tauxRecouvrement = montantTotal.compareTo(BigDecimal.ZERO) > 0
|
||||
? montantCollecte.multiply(BigDecimal.valueOf(100)).divide(montantTotal, 2, java.math.RoundingMode.HALF_UP).doubleValue()
|
||||
: 0;
|
||||
|
||||
// Construire le rapport
|
||||
StringBuilder rapport = new StringBuilder();
|
||||
rapport.append("═══════════════════════════════════════════════════════════════\n");
|
||||
rapport.append(" RAPPORT MENSUEL DES COTISATIONS\n");
|
||||
rapport.append("═══════════════════════════════════════════════════════════════\n\n");
|
||||
|
||||
rapport.append("Période : ").append(String.format("%02d/%d", mois, annee)).append("\n");
|
||||
rapport.append("Date de génération: ").append(LocalDateTime.now().format(DATETIME_FORMATTER)).append("\n\n");
|
||||
|
||||
rapport.append("───────────────────────────────────────────────────────────────\n");
|
||||
rapport.append(" RÉSUMÉ\n");
|
||||
rapport.append("───────────────────────────────────────────────────────────────\n\n");
|
||||
|
||||
rapport.append("Total cotisations : ").append(total).append("\n");
|
||||
rapport.append("Cotisations payées : ").append(payees).append("\n");
|
||||
rapport.append("Cotisations en attente: ").append(enAttente).append("\n");
|
||||
rapport.append("Cotisations en retard : ").append(enRetard).append("\n\n");
|
||||
|
||||
rapport.append("───────────────────────────────────────────────────────────────\n");
|
||||
rapport.append(" FINANCIER\n");
|
||||
rapport.append("───────────────────────────────────────────────────────────────\n\n");
|
||||
|
||||
rapport.append("Montant total attendu : ").append(formatMontant(montantTotal)).append("\n");
|
||||
rapport.append("Montant collecté : ").append(formatMontant(montantCollecte)).append("\n");
|
||||
rapport.append("Taux de recouvrement : ").append(String.format("%.1f%%", tauxRecouvrement)).append("\n\n");
|
||||
|
||||
rapport.append("═══════════════════════════════════════════════════════════════\n");
|
||||
|
||||
return rapport.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private String formatMontant(BigDecimal montant) {
|
||||
if (montant == null) return "0 FCFA";
|
||||
return String.format("%,.0f FCFA", montant.doubleValue());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
package dev.lions.unionflow.server.service;
|
||||
|
||||
import dev.lions.unionflow.server.api.enums.analytics.TypeMetrique;
|
||||
import dev.lions.unionflow.server.repository.CotisationRepository;
|
||||
import dev.lions.unionflow.server.repository.DemandeAideRepository;
|
||||
import dev.lions.unionflow.server.repository.EvenementRepository;
|
||||
import dev.lions.unionflow.server.repository.MembreRepository;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Service spécialisé dans le calcul des KPI (Key Performance Indicators)
|
||||
*
|
||||
* <p>Ce service fournit des méthodes optimisées pour calculer les indicateurs de performance clés
|
||||
* de l'application UnionFlow.
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 1.0
|
||||
* @since 2025-01-16
|
||||
*/
|
||||
@ApplicationScoped
|
||||
@Slf4j
|
||||
public class KPICalculatorService {
|
||||
|
||||
@Inject MembreRepository membreRepository;
|
||||
|
||||
@Inject CotisationRepository cotisationRepository;
|
||||
|
||||
@Inject EvenementRepository evenementRepository;
|
||||
|
||||
@Inject DemandeAideRepository demandeAideRepository;
|
||||
|
||||
/**
|
||||
* Calcule tous les KPI principaux pour une organisation
|
||||
*
|
||||
* @param organisationId L'ID de l'organisation
|
||||
* @param dateDebut Date de début de la période
|
||||
* @param dateFin Date de fin de la période
|
||||
* @return Map contenant tous les KPI calculés
|
||||
*/
|
||||
public Map<TypeMetrique, BigDecimal> calculerTousLesKPI(
|
||||
UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||
log.info(
|
||||
"Calcul de tous les KPI pour l'organisation {} sur la période {} - {}",
|
||||
organisationId,
|
||||
dateDebut,
|
||||
dateFin);
|
||||
|
||||
Map<TypeMetrique, BigDecimal> kpis = new HashMap<>();
|
||||
|
||||
// KPI Membres
|
||||
kpis.put(
|
||||
TypeMetrique.NOMBRE_MEMBRES_ACTIFS,
|
||||
calculerKPIMembresActifs(organisationId, dateDebut, dateFin));
|
||||
kpis.put(
|
||||
TypeMetrique.NOMBRE_MEMBRES_INACTIFS,
|
||||
calculerKPIMembresInactifs(organisationId, dateDebut, dateFin));
|
||||
kpis.put(
|
||||
TypeMetrique.TAUX_CROISSANCE_MEMBRES,
|
||||
calculerKPITauxCroissanceMembres(organisationId, dateDebut, dateFin));
|
||||
kpis.put(
|
||||
TypeMetrique.MOYENNE_AGE_MEMBRES,
|
||||
calculerKPIMoyenneAgeMembres(organisationId, dateDebut, dateFin));
|
||||
|
||||
// KPI Financiers
|
||||
kpis.put(
|
||||
TypeMetrique.TOTAL_COTISATIONS_COLLECTEES,
|
||||
calculerKPITotalCotisations(organisationId, dateDebut, dateFin));
|
||||
kpis.put(
|
||||
TypeMetrique.COTISATIONS_EN_ATTENTE,
|
||||
calculerKPICotisationsEnAttente(organisationId, dateDebut, dateFin));
|
||||
kpis.put(
|
||||
TypeMetrique.TAUX_RECOUVREMENT_COTISATIONS,
|
||||
calculerKPITauxRecouvrement(organisationId, dateDebut, dateFin));
|
||||
kpis.put(
|
||||
TypeMetrique.MOYENNE_COTISATION_MEMBRE,
|
||||
calculerKPIMoyenneCotisationMembre(organisationId, dateDebut, dateFin));
|
||||
|
||||
// KPI Événements
|
||||
kpis.put(
|
||||
TypeMetrique.NOMBRE_EVENEMENTS_ORGANISES,
|
||||
calculerKPINombreEvenements(organisationId, dateDebut, dateFin));
|
||||
kpis.put(
|
||||
TypeMetrique.TAUX_PARTICIPATION_EVENEMENTS,
|
||||
calculerKPITauxParticipation(organisationId, dateDebut, dateFin));
|
||||
kpis.put(
|
||||
TypeMetrique.MOYENNE_PARTICIPANTS_EVENEMENT,
|
||||
calculerKPIMoyenneParticipants(organisationId, dateDebut, dateFin));
|
||||
|
||||
// KPI Solidarité
|
||||
kpis.put(
|
||||
TypeMetrique.NOMBRE_DEMANDES_AIDE,
|
||||
calculerKPINombreDemandesAide(organisationId, dateDebut, dateFin));
|
||||
kpis.put(
|
||||
TypeMetrique.MONTANT_AIDES_ACCORDEES,
|
||||
calculerKPIMontantAides(organisationId, dateDebut, dateFin));
|
||||
kpis.put(
|
||||
TypeMetrique.TAUX_APPROBATION_AIDES,
|
||||
calculerKPITauxApprobationAides(organisationId, dateDebut, dateFin));
|
||||
|
||||
log.info("Calcul terminé : {} KPI calculés", kpis.size());
|
||||
return kpis;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule le KPI de performance globale de l'organisation
|
||||
*
|
||||
* @param organisationId L'ID de l'organisation
|
||||
* @param dateDebut Date de début de la période
|
||||
* @param dateFin Date de fin de la période
|
||||
* @return Score de performance global (0-100)
|
||||
*/
|
||||
public BigDecimal calculerKPIPerformanceGlobale(
|
||||
UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||
log.info("Calcul du KPI de performance globale pour l'organisation {}", organisationId);
|
||||
|
||||
Map<TypeMetrique, BigDecimal> kpis = calculerTousLesKPI(organisationId, dateDebut, dateFin);
|
||||
|
||||
// Pondération des différents KPI pour le score global
|
||||
BigDecimal scoreMembers = calculerScoreMembres(kpis).multiply(new BigDecimal("0.30")); // 30%
|
||||
BigDecimal scoreFinancier =
|
||||
calculerScoreFinancier(kpis).multiply(new BigDecimal("0.35")); // 35%
|
||||
BigDecimal scoreEvenements =
|
||||
calculerScoreEvenements(kpis).multiply(new BigDecimal("0.20")); // 20%
|
||||
BigDecimal scoreSolidarite =
|
||||
calculerScoreSolidarite(kpis).multiply(new BigDecimal("0.15")); // 15%
|
||||
|
||||
BigDecimal scoreGlobal =
|
||||
scoreMembers.add(scoreFinancier).add(scoreEvenements).add(scoreSolidarite);
|
||||
|
||||
log.info("Score de performance globale calculé : {}", scoreGlobal);
|
||||
return scoreGlobal.setScale(1, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule les KPI de comparaison avec la période précédente
|
||||
*
|
||||
* @param organisationId L'ID de l'organisation
|
||||
* @param dateDebut Date de début de la période actuelle
|
||||
* @param dateFin Date de fin de la période actuelle
|
||||
* @return Map des évolutions en pourcentage
|
||||
*/
|
||||
public Map<TypeMetrique, BigDecimal> calculerEvolutionsKPI(
|
||||
UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||
log.info("Calcul des évolutions KPI pour l'organisation {}", organisationId);
|
||||
|
||||
// Période actuelle
|
||||
Map<TypeMetrique, BigDecimal> kpisActuels =
|
||||
calculerTousLesKPI(organisationId, dateDebut, dateFin);
|
||||
|
||||
// Période précédente (même durée, décalée)
|
||||
long dureeJours = java.time.Duration.between(dateDebut, dateFin).toDays();
|
||||
LocalDateTime dateDebutPrecedente = dateDebut.minusDays(dureeJours);
|
||||
LocalDateTime dateFinPrecedente = dateFin.minusDays(dureeJours);
|
||||
Map<TypeMetrique, BigDecimal> kpisPrecedents =
|
||||
calculerTousLesKPI(organisationId, dateDebutPrecedente, dateFinPrecedente);
|
||||
|
||||
Map<TypeMetrique, BigDecimal> evolutions = new HashMap<>();
|
||||
|
||||
for (TypeMetrique typeMetrique : kpisActuels.keySet()) {
|
||||
BigDecimal valeurActuelle = kpisActuels.get(typeMetrique);
|
||||
BigDecimal valeurPrecedente = kpisPrecedents.get(typeMetrique);
|
||||
|
||||
BigDecimal evolution = calculerPourcentageEvolution(valeurActuelle, valeurPrecedente);
|
||||
evolutions.put(typeMetrique, evolution);
|
||||
}
|
||||
|
||||
return evolutions;
|
||||
}
|
||||
|
||||
// === MÉTHODES PRIVÉES DE CALCUL DES KPI ===
|
||||
|
||||
private BigDecimal calculerKPIMembresActifs(
|
||||
UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||
Long count = membreRepository.countMembresActifs(organisationId, dateDebut, dateFin);
|
||||
return new BigDecimal(count);
|
||||
}
|
||||
|
||||
private BigDecimal calculerKPIMembresInactifs(
|
||||
UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||
Long count = membreRepository.countMembresInactifs(organisationId, dateDebut, dateFin);
|
||||
return new BigDecimal(count);
|
||||
}
|
||||
|
||||
private BigDecimal calculerKPITauxCroissanceMembres(
|
||||
UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||
Long membresActuels = membreRepository.countMembresActifs(organisationId, dateDebut, dateFin);
|
||||
Long membresPrecedents =
|
||||
membreRepository.countMembresActifs(
|
||||
organisationId, dateDebut.minusMonths(1), dateFin.minusMonths(1));
|
||||
|
||||
return calculerTauxCroissance(
|
||||
new BigDecimal(membresActuels), new BigDecimal(membresPrecedents));
|
||||
}
|
||||
|
||||
private BigDecimal calculerKPIMoyenneAgeMembres(
|
||||
UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||
Double moyenneAge = membreRepository.calculerMoyenneAge(organisationId, dateDebut, dateFin);
|
||||
return moyenneAge != null
|
||||
? new BigDecimal(moyenneAge).setScale(1, RoundingMode.HALF_UP)
|
||||
: BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
private BigDecimal calculerKPITotalCotisations(
|
||||
UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||
BigDecimal total = cotisationRepository.sumMontantsPayes(organisationId, dateDebut, dateFin);
|
||||
return total != null ? total : BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
private BigDecimal calculerKPICotisationsEnAttente(
|
||||
UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||
BigDecimal total =
|
||||
cotisationRepository.sumMontantsEnAttente(organisationId, dateDebut, dateFin);
|
||||
return total != null ? total : BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
private BigDecimal calculerKPITauxRecouvrement(
|
||||
UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||
BigDecimal collectees = calculerKPITotalCotisations(organisationId, dateDebut, dateFin);
|
||||
BigDecimal enAttente = calculerKPICotisationsEnAttente(organisationId, dateDebut, dateFin);
|
||||
BigDecimal total = collectees.add(enAttente);
|
||||
|
||||
if (total.compareTo(BigDecimal.ZERO) == 0) return BigDecimal.ZERO;
|
||||
|
||||
return collectees.divide(total, 4, RoundingMode.HALF_UP).multiply(new BigDecimal("100"));
|
||||
}
|
||||
|
||||
private BigDecimal calculerKPIMoyenneCotisationMembre(
|
||||
UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||
BigDecimal total = calculerKPITotalCotisations(organisationId, dateDebut, dateFin);
|
||||
Long nombreMembres = membreRepository.countMembresActifs(organisationId, dateDebut, dateFin);
|
||||
|
||||
if (nombreMembres == 0) return BigDecimal.ZERO;
|
||||
|
||||
return total.divide(new BigDecimal(nombreMembres), 2, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
private BigDecimal calculerKPINombreEvenements(
|
||||
UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||
Long count = evenementRepository.countEvenements(organisationId, dateDebut, dateFin);
|
||||
return new BigDecimal(count);
|
||||
}
|
||||
|
||||
private BigDecimal calculerKPITauxParticipation(
|
||||
UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||
// Calcul basé sur les participations aux événements
|
||||
Long totalParticipations =
|
||||
evenementRepository.countTotalParticipations(organisationId, dateDebut, dateFin);
|
||||
Long nombreEvenements = evenementRepository.countEvenements(organisationId, dateDebut, dateFin);
|
||||
Long nombreMembres = membreRepository.countMembresActifs(organisationId, dateDebut, dateFin);
|
||||
|
||||
if (nombreEvenements == 0 || nombreMembres == 0) return BigDecimal.ZERO;
|
||||
|
||||
BigDecimal participationsAttendues =
|
||||
new BigDecimal(nombreEvenements).multiply(new BigDecimal(nombreMembres));
|
||||
BigDecimal tauxParticipation =
|
||||
new BigDecimal(totalParticipations)
|
||||
.divide(participationsAttendues, 4, RoundingMode.HALF_UP)
|
||||
.multiply(new BigDecimal("100"));
|
||||
|
||||
return tauxParticipation;
|
||||
}
|
||||
|
||||
private BigDecimal calculerKPIMoyenneParticipants(
|
||||
UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||
Double moyenne =
|
||||
evenementRepository.calculerMoyenneParticipants(organisationId, dateDebut, dateFin);
|
||||
return moyenne != null
|
||||
? new BigDecimal(moyenne).setScale(1, RoundingMode.HALF_UP)
|
||||
: BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
private BigDecimal calculerKPINombreDemandesAide(
|
||||
UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||
Long count = demandeAideRepository.countDemandes(organisationId, dateDebut, dateFin);
|
||||
return new BigDecimal(count);
|
||||
}
|
||||
|
||||
private BigDecimal calculerKPIMontantAides(
|
||||
UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||
BigDecimal total =
|
||||
demandeAideRepository.sumMontantsAccordes(organisationId, dateDebut, dateFin);
|
||||
return total != null ? total : BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
private BigDecimal calculerKPITauxApprobationAides(
|
||||
UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||
Long totalDemandes = demandeAideRepository.countDemandes(organisationId, dateDebut, dateFin);
|
||||
Long demandesApprouvees =
|
||||
demandeAideRepository.countDemandesApprouvees(organisationId, dateDebut, dateFin);
|
||||
|
||||
if (totalDemandes == 0) return BigDecimal.ZERO;
|
||||
|
||||
return new BigDecimal(demandesApprouvees)
|
||||
.divide(new BigDecimal(totalDemandes), 4, RoundingMode.HALF_UP)
|
||||
.multiply(new BigDecimal("100"));
|
||||
}
|
||||
|
||||
// === MÉTHODES UTILITAIRES ===
|
||||
|
||||
private BigDecimal calculerTauxCroissance(
|
||||
BigDecimal valeurActuelle, BigDecimal valeurPrecedente) {
|
||||
if (valeurPrecedente.compareTo(BigDecimal.ZERO) == 0) return BigDecimal.ZERO;
|
||||
|
||||
return valeurActuelle
|
||||
.subtract(valeurPrecedente)
|
||||
.divide(valeurPrecedente, 4, RoundingMode.HALF_UP)
|
||||
.multiply(new BigDecimal("100"));
|
||||
}
|
||||
|
||||
private BigDecimal calculerPourcentageEvolution(
|
||||
BigDecimal valeurActuelle, BigDecimal valeurPrecedente) {
|
||||
if (valeurPrecedente == null || valeurPrecedente.compareTo(BigDecimal.ZERO) == 0) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
return valeurActuelle
|
||||
.subtract(valeurPrecedente)
|
||||
.divide(valeurPrecedente, 4, RoundingMode.HALF_UP)
|
||||
.multiply(new BigDecimal("100"));
|
||||
}
|
||||
|
||||
private BigDecimal calculerScoreMembres(Map<TypeMetrique, BigDecimal> kpis) {
|
||||
// Score basé sur la croissance et l'activité des membres
|
||||
BigDecimal tauxCroissance = kpis.get(TypeMetrique.TAUX_CROISSANCE_MEMBRES);
|
||||
BigDecimal nombreActifs = kpis.get(TypeMetrique.NOMBRE_MEMBRES_ACTIFS);
|
||||
BigDecimal nombreInactifs = kpis.get(TypeMetrique.NOMBRE_MEMBRES_INACTIFS);
|
||||
|
||||
// Calcul du score (logique simplifiée)
|
||||
BigDecimal scoreActivite =
|
||||
nombreActifs
|
||||
.divide(nombreActifs.add(nombreInactifs), 2, RoundingMode.HALF_UP)
|
||||
.multiply(new BigDecimal("50"));
|
||||
BigDecimal scoreCroissance = tauxCroissance.min(new BigDecimal("50")); // Plafonné à 50
|
||||
|
||||
return scoreActivite.add(scoreCroissance);
|
||||
}
|
||||
|
||||
private BigDecimal calculerScoreFinancier(Map<TypeMetrique, BigDecimal> kpis) {
|
||||
// Score basé sur le recouvrement et les montants
|
||||
BigDecimal tauxRecouvrement = kpis.get(TypeMetrique.TAUX_RECOUVREMENT_COTISATIONS);
|
||||
return tauxRecouvrement; // Score direct basé sur le taux de recouvrement
|
||||
}
|
||||
|
||||
private BigDecimal calculerScoreEvenements(Map<TypeMetrique, BigDecimal> kpis) {
|
||||
// Score basé sur la participation aux événements
|
||||
BigDecimal tauxParticipation = kpis.get(TypeMetrique.TAUX_PARTICIPATION_EVENEMENTS);
|
||||
return tauxParticipation; // Score direct basé sur le taux de participation
|
||||
}
|
||||
|
||||
private BigDecimal calculerScoreSolidarite(Map<TypeMetrique, BigDecimal> kpis) {
|
||||
// Score basé sur l'efficacité du système de solidarité
|
||||
BigDecimal tauxApprobation = kpis.get(TypeMetrique.TAUX_APPROBATION_AIDES);
|
||||
return tauxApprobation; // Score direct basé sur le taux d'approbation
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
package dev.lions.unionflow.server.service;
|
||||
|
||||
import io.quarkus.oidc.runtime.OidcJwtCallerPrincipal;
|
||||
import io.quarkus.security.identity.SecurityIdentity;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import java.util.Set;
|
||||
import org.eclipse.microprofile.jwt.JsonWebToken;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
/**
|
||||
* Service pour l'intégration avec Keycloak
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 1.0
|
||||
* @since 2025-01-15
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class KeycloakService {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(KeycloakService.class);
|
||||
|
||||
@Inject SecurityIdentity securityIdentity;
|
||||
|
||||
@Inject JsonWebToken jwt;
|
||||
|
||||
/**
|
||||
* Vérifie si l'utilisateur actuel est authentifié
|
||||
*
|
||||
* @return true si l'utilisateur est authentifié
|
||||
*/
|
||||
public boolean isAuthenticated() {
|
||||
return securityIdentity != null && !securityIdentity.isAnonymous();
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtient l'ID de l'utilisateur actuel depuis Keycloak
|
||||
*
|
||||
* @return l'ID de l'utilisateur ou null si non authentifié
|
||||
*/
|
||||
public String getCurrentUserId() {
|
||||
if (!isAuthenticated()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return jwt.getSubject();
|
||||
} catch (Exception e) {
|
||||
LOG.warnf("Erreur lors de la récupération de l'ID utilisateur: %s", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtient l'email de l'utilisateur actuel
|
||||
*
|
||||
* @return l'email de l'utilisateur ou null si non authentifié
|
||||
*/
|
||||
public String getCurrentUserEmail() {
|
||||
if (!isAuthenticated()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return jwt.getClaim("email");
|
||||
} catch (Exception e) {
|
||||
LOG.warnf("Erreur lors de la récupération de l'email utilisateur: %s", e.getMessage());
|
||||
return securityIdentity.getPrincipal().getName();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtient le nom complet de l'utilisateur actuel
|
||||
*
|
||||
* @return le nom complet ou null si non disponible
|
||||
*/
|
||||
public String getCurrentUserFullName() {
|
||||
if (!isAuthenticated()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
String firstName = jwt.getClaim("given_name");
|
||||
String lastName = jwt.getClaim("family_name");
|
||||
|
||||
if (firstName != null && lastName != null) {
|
||||
return firstName + " " + lastName;
|
||||
} else if (firstName != null) {
|
||||
return firstName;
|
||||
} else if (lastName != null) {
|
||||
return lastName;
|
||||
}
|
||||
|
||||
// Fallback sur le nom d'utilisateur
|
||||
return jwt.getClaim("preferred_username");
|
||||
} catch (Exception e) {
|
||||
LOG.warnf("Erreur lors de la récupération du nom utilisateur: %s", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtient tous les rôles de l'utilisateur actuel
|
||||
*
|
||||
* @return les rôles de l'utilisateur
|
||||
*/
|
||||
public Set<String> getCurrentUserRoles() {
|
||||
if (!isAuthenticated()) {
|
||||
return Set.of();
|
||||
}
|
||||
|
||||
return securityIdentity.getRoles();
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si l'utilisateur actuel a un rôle spécifique
|
||||
*
|
||||
* @param role le rôle à vérifier
|
||||
* @return true si l'utilisateur a le rôle
|
||||
*/
|
||||
public boolean hasRole(String role) {
|
||||
if (!isAuthenticated()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return securityIdentity.hasRole(role);
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si l'utilisateur actuel a au moins un des rôles spécifiés
|
||||
*
|
||||
* @param roles les rôles à vérifier
|
||||
* @return true si l'utilisateur a au moins un des rôles
|
||||
*/
|
||||
public boolean hasAnyRole(String... roles) {
|
||||
if (!isAuthenticated()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (String role : roles) {
|
||||
if (securityIdentity.hasRole(role)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si l'utilisateur actuel a tous les rôles spécifiés
|
||||
*
|
||||
* @param roles les rôles à vérifier
|
||||
* @return true si l'utilisateur a tous les rôles
|
||||
*/
|
||||
public boolean hasAllRoles(String... roles) {
|
||||
if (!isAuthenticated()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (String role : roles) {
|
||||
if (!securityIdentity.hasRole(role)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtient une claim spécifique du JWT
|
||||
*
|
||||
* @param claimName le nom de la claim
|
||||
* @return la valeur de la claim ou null si non trouvée
|
||||
*/
|
||||
public <T> T getClaim(String claimName) {
|
||||
if (!isAuthenticated()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return jwt.getClaim(claimName);
|
||||
} catch (Exception e) {
|
||||
LOG.warnf("Erreur lors de la récupération de la claim %s: %s", claimName, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtient toutes les claims du JWT
|
||||
*
|
||||
* @return toutes les claims ou une map vide si non authentifié
|
||||
*/
|
||||
public Set<String> getAllClaimNames() {
|
||||
if (!isAuthenticated()) {
|
||||
return Set.of();
|
||||
}
|
||||
|
||||
try {
|
||||
return jwt.getClaimNames();
|
||||
} catch (Exception e) {
|
||||
LOG.warnf("Erreur lors de la récupération des claims: %s", e.getMessage());
|
||||
return Set.of();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtient les informations utilisateur pour les logs
|
||||
*
|
||||
* @return informations utilisateur formatées
|
||||
*/
|
||||
public String getUserInfoForLogging() {
|
||||
if (!isAuthenticated()) {
|
||||
return "Utilisateur non authentifié";
|
||||
}
|
||||
|
||||
String email = getCurrentUserEmail();
|
||||
String fullName = getCurrentUserFullName();
|
||||
Set<String> roles = getCurrentUserRoles();
|
||||
|
||||
return String.format(
|
||||
"Utilisateur: %s (%s), Rôles: %s",
|
||||
fullName != null ? fullName : "N/A", email != null ? email : "N/A", roles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si l'utilisateur actuel est un administrateur
|
||||
*
|
||||
* @return true si l'utilisateur est administrateur
|
||||
*/
|
||||
public boolean isAdmin() {
|
||||
return hasRole("ADMIN") || hasRole("admin");
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si l'utilisateur actuel peut gérer les membres
|
||||
*
|
||||
* @return true si l'utilisateur peut gérer les membres
|
||||
*/
|
||||
public boolean canManageMembers() {
|
||||
return hasAnyRole(
|
||||
"ADMIN",
|
||||
"GESTIONNAIRE_MEMBRE",
|
||||
"PRESIDENT",
|
||||
"SECRETAIRE",
|
||||
"admin",
|
||||
"gestionnaire_membre",
|
||||
"president",
|
||||
"secretaire");
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si l'utilisateur actuel peut gérer les finances
|
||||
*
|
||||
* @return true si l'utilisateur peut gérer les finances
|
||||
*/
|
||||
public boolean canManageFinances() {
|
||||
return hasAnyRole("ADMIN", "TRESORIER", "PRESIDENT", "admin", "tresorier", "president");
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si l'utilisateur actuel peut gérer les événements
|
||||
*
|
||||
* @return true si l'utilisateur peut gérer les événements
|
||||
*/
|
||||
public boolean canManageEvents() {
|
||||
return hasAnyRole(
|
||||
"ADMIN",
|
||||
"ORGANISATEUR_EVENEMENT",
|
||||
"PRESIDENT",
|
||||
"SECRETAIRE",
|
||||
"admin",
|
||||
"organisateur_evenement",
|
||||
"president",
|
||||
"secretaire");
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si l'utilisateur actuel peut gérer les organisations
|
||||
*
|
||||
* @return true si l'utilisateur peut gérer les organisations
|
||||
*/
|
||||
public boolean canManageOrganizations() {
|
||||
return hasAnyRole("ADMIN", "PRESIDENT", "admin", "president");
|
||||
}
|
||||
|
||||
/** Log les informations de sécurité pour debug */
|
||||
public void logSecurityInfo() {
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debugf("Informations de sécurité: %s", getUserInfoForLogging());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtient le token d'accès brut
|
||||
*
|
||||
* @return le token JWT brut ou null si non disponible
|
||||
*/
|
||||
public String getRawAccessToken() {
|
||||
if (!isAuthenticated()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
if (jwt instanceof OidcJwtCallerPrincipal) {
|
||||
return ((OidcJwtCallerPrincipal) jwt).getRawToken();
|
||||
}
|
||||
return jwt.getRawToken();
|
||||
} catch (Exception e) {
|
||||
LOG.warnf("Erreur lors de la récupération du token brut: %s", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,428 @@
|
||||
package dev.lions.unionflow.server.service;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.solidarite.DemandeAideDTO;
|
||||
import dev.lions.unionflow.server.api.dto.solidarite.PropositionAideDTO;
|
||||
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<PropositionAideDTO> trouverPropositionsCompatibles(DemandeAideDTO 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<PropositionAideDTO> candidats =
|
||||
propositionAideService.obtenirPropositionsActives(demande.getTypeAide());
|
||||
|
||||
// 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(PropositionAideDTO::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<PropositionAideDTO> 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<DemandeAideDTO> trouverDemandesCompatibles(PropositionAideDTO 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<DemandeAideDTO> 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<PropositionAideDTO> rechercherProposantsFinanciers(DemandeAideDTO 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());
|
||||
|
||||
List<PropositionAideDTO> 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<PropositionAideDTO> matchingUrgence(DemandeAideDTO demande) {
|
||||
LOG.infof("Matching d'urgence pour la demande: %s", demande.getId());
|
||||
|
||||
// Recherche élargie pour les urgences
|
||||
List<PropositionAideDTO> 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(PropositionAideDTO::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(
|
||||
DemandeAideDTO demande, PropositionAideDTO 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() > 0) {
|
||||
score += Math.min(15.0, proposition.getNombreBeneficiairesAides() * boostExperience);
|
||||
}
|
||||
|
||||
// 4. Réputation (10 points max)
|
||||
if (proposition.getNoteMoyenne() != 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(DemandeAideDTO demande, PropositionAideDTO proposition) {
|
||||
double score = calculerScoreCompatibilite(demande, proposition);
|
||||
|
||||
// Bonus spécifiques aux aides financières
|
||||
|
||||
// 1. Historique de versements
|
||||
if (proposition.getMontantTotalVerse() > 0) {
|
||||
score += Math.min(10.0, proposition.getMontantTotalVerse() / 10000.0);
|
||||
}
|
||||
|
||||
// 2. Fiabilité (ratio versements/promesses)
|
||||
if (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() <= 24) {
|
||||
score += 10.0;
|
||||
} else if (proposition.getDelaiReponseHeures() <= 72) {
|
||||
score += 5.0;
|
||||
}
|
||||
|
||||
return Math.max(0.0, Math.min(100.0, score));
|
||||
}
|
||||
|
||||
/** Calcule le bonus géographique */
|
||||
private double calculerBonusGeographique(DemandeAideDTO demande, PropositionAideDTO 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(DemandeAideDTO demande, PropositionAideDTO proposition) {
|
||||
double bonus = 0.0;
|
||||
|
||||
// Bonus pour demande urgente
|
||||
if (demande.estUrgente()) {
|
||||
bonus += 5.0;
|
||||
}
|
||||
|
||||
// Bonus pour proposition récente
|
||||
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(DemandeAideDTO demande, PropositionAideDTO 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() > 168) { // Plus d'une semaine
|
||||
malus += 3.0;
|
||||
}
|
||||
|
||||
return malus;
|
||||
}
|
||||
|
||||
// === MÉTHODES UTILITAIRES ===
|
||||
|
||||
/** Recherche des propositions par catégorie */
|
||||
private List<PropositionAideDTO> 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 PropositionAideDTO proposition;
|
||||
final double score;
|
||||
|
||||
ResultatMatching(PropositionAideDTO proposition, double score) {
|
||||
this.proposition = proposition;
|
||||
this.score = score;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,842 @@
|
||||
package dev.lions.unionflow.server.service;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.membre.MembreDTO;
|
||||
import dev.lions.unionflow.server.entity.Membre;
|
||||
import dev.lions.unionflow.server.entity.Organisation;
|
||||
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 org.apache.commons.csv.CSVFormat;
|
||||
import org.apache.commons.csv.CSVPrinter;
|
||||
import org.apache.commons.csv.CSVRecord;
|
||||
import org.apache.poi.ss.usermodel.*;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Service pour l'import et l'export de membres depuis/vers Excel et CSV
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class MembreImportExportService {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(MembreImportExportService.class);
|
||||
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("dd/MM/yyyy");
|
||||
|
||||
@Inject
|
||||
MembreRepository membreRepository;
|
||||
|
||||
@Inject
|
||||
OrganisationRepository organisationRepository;
|
||||
|
||||
@Inject
|
||||
MembreService membreService;
|
||||
|
||||
/**
|
||||
* Importe des membres depuis un fichier Excel ou CSV
|
||||
*/
|
||||
@Transactional
|
||||
public ResultatImport importerMembres(
|
||||
InputStream fileInputStream,
|
||||
String fileName,
|
||||
UUID organisationId,
|
||||
String typeMembreDefaut,
|
||||
boolean mettreAJourExistants,
|
||||
boolean ignorerErreurs) {
|
||||
|
||||
LOG.infof("Import de membres depuis le fichier: %s", fileName);
|
||||
|
||||
ResultatImport resultat = new ResultatImport();
|
||||
resultat.erreurs = new ArrayList<>();
|
||||
resultat.membresImportes = new ArrayList<>();
|
||||
|
||||
try {
|
||||
if (fileName.toLowerCase().endsWith(".csv")) {
|
||||
return importerDepuisCSV(fileInputStream, organisationId, typeMembreDefaut, mettreAJourExistants, ignorerErreurs);
|
||||
} else if (fileName.toLowerCase().endsWith(".xlsx") || fileName.toLowerCase().endsWith(".xls")) {
|
||||
return importerDepuisExcel(fileInputStream, organisationId, typeMembreDefaut, mettreAJourExistants, ignorerErreurs);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Format de fichier non supporté. Formats acceptés: .xlsx, .xls, .csv");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Erreur lors de l'import");
|
||||
resultat.erreurs.add("Erreur générale: " + e.getMessage());
|
||||
return resultat;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Importe depuis un fichier Excel
|
||||
*/
|
||||
private ResultatImport importerDepuisExcel(
|
||||
InputStream fileInputStream,
|
||||
UUID organisationId,
|
||||
String typeMembreDefaut,
|
||||
boolean mettreAJourExistants,
|
||||
boolean ignorerErreurs) throws IOException {
|
||||
|
||||
ResultatImport resultat = new ResultatImport();
|
||||
resultat.erreurs = new ArrayList<>();
|
||||
resultat.membresImportes = new ArrayList<>();
|
||||
int ligneNum = 0;
|
||||
|
||||
try (Workbook workbook = new XSSFWorkbook(fileInputStream)) {
|
||||
Sheet sheet = workbook.getSheetAt(0);
|
||||
Row headerRow = sheet.getRow(0);
|
||||
|
||||
if (headerRow == null) {
|
||||
throw new IllegalArgumentException("Le fichier Excel est vide ou n'a pas d'en-têtes");
|
||||
}
|
||||
|
||||
// Mapper les colonnes
|
||||
Map<String, Integer> colonnes = mapperColonnes(headerRow);
|
||||
|
||||
// Vérifier les colonnes obligatoires
|
||||
if (!colonnes.containsKey("nom") || !colonnes.containsKey("prenom") ||
|
||||
!colonnes.containsKey("email") || !colonnes.containsKey("telephone")) {
|
||||
throw new IllegalArgumentException("Colonnes obligatoires manquantes: nom, prenom, email, telephone");
|
||||
}
|
||||
|
||||
// Lire les données
|
||||
for (int i = 1; i <= sheet.getLastRowNum(); i++) {
|
||||
ligneNum = i + 1;
|
||||
Row row = sheet.getRow(i);
|
||||
|
||||
if (row == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
Membre membre = lireLigneExcel(row, colonnes, organisationId, typeMembreDefaut);
|
||||
|
||||
// Vérifier si le membre existe déjà
|
||||
Optional<Membre> membreExistant = membreRepository.findByEmail(membre.getEmail());
|
||||
|
||||
if (membreExistant.isPresent()) {
|
||||
if (mettreAJourExistants) {
|
||||
Membre existant = membreExistant.get();
|
||||
existant.setNom(membre.getNom());
|
||||
existant.setPrenom(membre.getPrenom());
|
||||
existant.setTelephone(membre.getTelephone());
|
||||
existant.setDateNaissance(membre.getDateNaissance());
|
||||
if (membre.getOrganisation() != null) {
|
||||
existant.setOrganisation(membre.getOrganisation());
|
||||
}
|
||||
membreRepository.persist(existant);
|
||||
resultat.membresImportes.add(membreService.convertToDTO(existant));
|
||||
resultat.lignesTraitees++;
|
||||
} else {
|
||||
resultat.erreurs.add(String.format("Ligne %d: Membre avec email %s existe déjà", ligneNum, membre.getEmail()));
|
||||
if (!ignorerErreurs) {
|
||||
throw new IllegalArgumentException("Membre existant trouvé et mise à jour désactivée");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
membre = membreService.creerMembre(membre);
|
||||
resultat.membresImportes.add(membreService.convertToDTO(membre));
|
||||
resultat.lignesTraitees++;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
String erreur = String.format("Ligne %d: %s", ligneNum, e.getMessage());
|
||||
resultat.erreurs.add(erreur);
|
||||
resultat.lignesErreur++;
|
||||
|
||||
if (!ignorerErreurs) {
|
||||
throw new RuntimeException(erreur, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resultat.totalLignes = sheet.getLastRowNum();
|
||||
}
|
||||
|
||||
LOG.infof("Import terminé: %d lignes traitées, %d erreurs", resultat.lignesTraitees, resultat.lignesErreur);
|
||||
return resultat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Importe depuis un fichier CSV
|
||||
*/
|
||||
private ResultatImport importerDepuisCSV(
|
||||
InputStream fileInputStream,
|
||||
UUID organisationId,
|
||||
String typeMembreDefaut,
|
||||
boolean mettreAJourExistants,
|
||||
boolean ignorerErreurs) throws IOException {
|
||||
|
||||
ResultatImport resultat = new ResultatImport();
|
||||
resultat.erreurs = new ArrayList<>();
|
||||
resultat.membresImportes = new ArrayList<>();
|
||||
|
||||
try (InputStreamReader reader = new InputStreamReader(fileInputStream, StandardCharsets.UTF_8)) {
|
||||
Iterable<CSVRecord> records = CSVFormat.DEFAULT.builder().setHeader().setSkipHeaderRecord(true).build().parse(reader);
|
||||
|
||||
int ligneNum = 0;
|
||||
for (CSVRecord record : records) {
|
||||
ligneNum++;
|
||||
|
||||
try {
|
||||
Membre membre = lireLigneCSV(record, organisationId, typeMembreDefaut);
|
||||
|
||||
// Vérifier si le membre existe déjà
|
||||
Optional<Membre> membreExistant = membreRepository.findByEmail(membre.getEmail());
|
||||
|
||||
if (membreExistant.isPresent()) {
|
||||
if (mettreAJourExistants) {
|
||||
Membre existant = membreExistant.get();
|
||||
existant.setNom(membre.getNom());
|
||||
existant.setPrenom(membre.getPrenom());
|
||||
existant.setTelephone(membre.getTelephone());
|
||||
existant.setDateNaissance(membre.getDateNaissance());
|
||||
if (membre.getOrganisation() != null) {
|
||||
existant.setOrganisation(membre.getOrganisation());
|
||||
}
|
||||
membreRepository.persist(existant);
|
||||
resultat.membresImportes.add(membreService.convertToDTO(existant));
|
||||
resultat.lignesTraitees++;
|
||||
} else {
|
||||
resultat.erreurs.add(String.format("Ligne %d: Membre avec email %s existe déjà", ligneNum, membre.getEmail()));
|
||||
if (!ignorerErreurs) {
|
||||
throw new IllegalArgumentException("Membre existant trouvé et mise à jour désactivée");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
membre = membreService.creerMembre(membre);
|
||||
resultat.membresImportes.add(membreService.convertToDTO(membre));
|
||||
resultat.lignesTraitees++;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
String erreur = String.format("Ligne %d: %s", ligneNum, e.getMessage());
|
||||
resultat.erreurs.add(erreur);
|
||||
resultat.lignesErreur++;
|
||||
|
||||
if (!ignorerErreurs) {
|
||||
throw new RuntimeException(erreur, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resultat.totalLignes = ligneNum;
|
||||
}
|
||||
|
||||
LOG.infof("Import CSV terminé: %d lignes traitées, %d erreurs", resultat.lignesTraitees, resultat.lignesErreur);
|
||||
return resultat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lit une ligne Excel et crée un membre
|
||||
*/
|
||||
private Membre lireLigneExcel(Row row, Map<String, Integer> colonnes, UUID organisationId, String typeMembreDefaut) {
|
||||
Membre membre = new Membre();
|
||||
|
||||
// Colonnes obligatoires
|
||||
String nom = getCellValueAsString(row, colonnes.get("nom"));
|
||||
String prenom = getCellValueAsString(row, colonnes.get("prenom"));
|
||||
String email = getCellValueAsString(row, colonnes.get("email"));
|
||||
String telephone = getCellValueAsString(row, colonnes.get("telephone"));
|
||||
|
||||
if (nom == null || nom.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("Le nom est obligatoire");
|
||||
}
|
||||
if (prenom == null || prenom.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("Le prénom est obligatoire");
|
||||
}
|
||||
if (email == null || email.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("L'email est obligatoire");
|
||||
}
|
||||
if (telephone == null || telephone.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("Le téléphone est obligatoire");
|
||||
}
|
||||
|
||||
membre.setNom(nom.trim());
|
||||
membre.setPrenom(prenom.trim());
|
||||
membre.setEmail(email.trim().toLowerCase());
|
||||
membre.setTelephone(telephone.trim());
|
||||
|
||||
// Colonnes optionnelles
|
||||
if (colonnes.containsKey("date_naissance")) {
|
||||
LocalDate dateNaissance = getCellValueAsDate(row, colonnes.get("date_naissance"));
|
||||
if (dateNaissance != null) {
|
||||
membre.setDateNaissance(dateNaissance);
|
||||
}
|
||||
}
|
||||
if (membre.getDateNaissance() == null) {
|
||||
membre.setDateNaissance(LocalDate.now().minusYears(18));
|
||||
}
|
||||
|
||||
if (colonnes.containsKey("date_adhesion")) {
|
||||
LocalDate dateAdhesion = getCellValueAsDate(row, colonnes.get("date_adhesion"));
|
||||
if (dateAdhesion != null) {
|
||||
membre.setDateAdhesion(dateAdhesion);
|
||||
}
|
||||
}
|
||||
if (membre.getDateAdhesion() == null) {
|
||||
membre.setDateAdhesion(LocalDate.now());
|
||||
}
|
||||
|
||||
// Organisation
|
||||
if (organisationId != null) {
|
||||
Optional<Organisation> org = organisationRepository.findByIdOptional(organisationId);
|
||||
if (org.isPresent()) {
|
||||
membre.setOrganisation(org.get());
|
||||
}
|
||||
}
|
||||
|
||||
// Statut par défaut
|
||||
membre.setActif(typeMembreDefaut == null || typeMembreDefaut.isEmpty() || "ACTIF".equals(typeMembreDefaut));
|
||||
|
||||
return membre;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lit une ligne CSV et crée un membre
|
||||
*/
|
||||
private Membre lireLigneCSV(CSVRecord record, UUID organisationId, String typeMembreDefaut) {
|
||||
Membre membre = new Membre();
|
||||
|
||||
// Colonnes obligatoires
|
||||
String nom = record.get("nom");
|
||||
String prenom = record.get("prenom");
|
||||
String email = record.get("email");
|
||||
String telephone = record.get("telephone");
|
||||
|
||||
if (nom == null || nom.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("Le nom est obligatoire");
|
||||
}
|
||||
if (prenom == null || prenom.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("Le prénom est obligatoire");
|
||||
}
|
||||
if (email == null || email.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("L'email est obligatoire");
|
||||
}
|
||||
if (telephone == null || telephone.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("Le téléphone est obligatoire");
|
||||
}
|
||||
|
||||
membre.setNom(nom.trim());
|
||||
membre.setPrenom(prenom.trim());
|
||||
membre.setEmail(email.trim().toLowerCase());
|
||||
membre.setTelephone(telephone.trim());
|
||||
|
||||
// Colonnes optionnelles
|
||||
try {
|
||||
String dateNaissanceStr = record.get("date_naissance");
|
||||
if (dateNaissanceStr != null && !dateNaissanceStr.trim().isEmpty()) {
|
||||
membre.setDateNaissance(parseDate(dateNaissanceStr));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Ignorer si la date est invalide
|
||||
}
|
||||
if (membre.getDateNaissance() == null) {
|
||||
membre.setDateNaissance(LocalDate.now().minusYears(18));
|
||||
}
|
||||
|
||||
try {
|
||||
String dateAdhesionStr = record.get("date_adhesion");
|
||||
if (dateAdhesionStr != null && !dateAdhesionStr.trim().isEmpty()) {
|
||||
membre.setDateAdhesion(parseDate(dateAdhesionStr));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Ignorer si la date est invalide
|
||||
}
|
||||
if (membre.getDateAdhesion() == null) {
|
||||
membre.setDateAdhesion(LocalDate.now());
|
||||
}
|
||||
|
||||
// Organisation
|
||||
if (organisationId != null) {
|
||||
Optional<Organisation> org = organisationRepository.findByIdOptional(organisationId);
|
||||
if (org.isPresent()) {
|
||||
membre.setOrganisation(org.get());
|
||||
}
|
||||
}
|
||||
|
||||
// Statut par défaut
|
||||
membre.setActif(typeMembreDefaut == null || typeMembreDefaut.isEmpty() || "ACTIF".equals(typeMembreDefaut));
|
||||
|
||||
return membre;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mappe les colonnes Excel
|
||||
*/
|
||||
private Map<String, Integer> mapperColonnes(Row headerRow) {
|
||||
Map<String, Integer> colonnes = new HashMap<>();
|
||||
for (Cell cell : headerRow) {
|
||||
String headerName = getCellValueAsString(headerRow, cell.getColumnIndex()).toLowerCase()
|
||||
.replace(" ", "_")
|
||||
.replace("é", "e")
|
||||
.replace("è", "e")
|
||||
.replace("ê", "e");
|
||||
colonnes.put(headerName, cell.getColumnIndex());
|
||||
}
|
||||
return colonnes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtient la valeur d'une cellule comme String
|
||||
*/
|
||||
private String getCellValueAsString(Row row, Integer columnIndex) {
|
||||
if (columnIndex == null || row == null) {
|
||||
return null;
|
||||
}
|
||||
Cell cell = row.getCell(columnIndex);
|
||||
if (cell == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (cell.getCellType()) {
|
||||
case STRING:
|
||||
return cell.getStringCellValue();
|
||||
case NUMERIC:
|
||||
if (DateUtil.isCellDateFormatted(cell)) {
|
||||
return cell.getDateCellValue().toString();
|
||||
} else {
|
||||
return String.valueOf((long) cell.getNumericCellValue());
|
||||
}
|
||||
case BOOLEAN:
|
||||
return String.valueOf(cell.getBooleanCellValue());
|
||||
case FORMULA:
|
||||
return cell.getCellFormula();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtient la valeur d'une cellule comme Date
|
||||
*/
|
||||
private LocalDate getCellValueAsDate(Row row, Integer columnIndex) {
|
||||
if (columnIndex == null || row == null) {
|
||||
return null;
|
||||
}
|
||||
Cell cell = row.getCell(columnIndex);
|
||||
if (cell == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
if (cell.getCellType() == CellType.NUMERIC && DateUtil.isCellDateFormatted(cell)) {
|
||||
return cell.getDateCellValue().toInstant()
|
||||
.atZone(java.time.ZoneId.systemDefault())
|
||||
.toLocalDate();
|
||||
} else if (cell.getCellType() == CellType.STRING) {
|
||||
return parseDate(cell.getStringCellValue());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warnf("Erreur lors de la lecture de la date: %s", e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse une date depuis une String
|
||||
*/
|
||||
private LocalDate parseDate(String dateStr) {
|
||||
if (dateStr == null || dateStr.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
dateStr = dateStr.trim();
|
||||
|
||||
// Essayer différents formats
|
||||
String[] formats = {
|
||||
"dd/MM/yyyy",
|
||||
"yyyy-MM-dd",
|
||||
"dd-MM-yyyy",
|
||||
"dd.MM.yyyy"
|
||||
};
|
||||
|
||||
for (String format : formats) {
|
||||
try {
|
||||
return LocalDate.parse(dateStr, DateTimeFormatter.ofPattern(format));
|
||||
} catch (DateTimeParseException e) {
|
||||
// Continuer avec le format suivant
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Format de date non reconnu: " + dateStr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exporte des membres vers Excel
|
||||
*/
|
||||
public byte[] exporterVersExcel(List<MembreDTO> membres, List<String> colonnesExport, boolean inclureHeaders, boolean formaterDates, boolean inclureStatistiques, String motDePasse) throws IOException {
|
||||
try (Workbook workbook = new XSSFWorkbook()) {
|
||||
Sheet sheet = workbook.createSheet("Membres");
|
||||
|
||||
int rowNum = 0;
|
||||
|
||||
// En-têtes
|
||||
if (inclureHeaders) {
|
||||
Row headerRow = sheet.createRow(rowNum++);
|
||||
int colNum = 0;
|
||||
|
||||
if (colonnesExport.contains("PERSO") || colonnesExport.isEmpty()) {
|
||||
headerRow.createCell(colNum++).setCellValue("Nom");
|
||||
headerRow.createCell(colNum++).setCellValue("Prénom");
|
||||
headerRow.createCell(colNum++).setCellValue("Date de naissance");
|
||||
}
|
||||
if (colonnesExport.contains("CONTACT") || colonnesExport.isEmpty()) {
|
||||
headerRow.createCell(colNum++).setCellValue("Email");
|
||||
headerRow.createCell(colNum++).setCellValue("Téléphone");
|
||||
}
|
||||
if (colonnesExport.contains("ADHESION") || colonnesExport.isEmpty()) {
|
||||
headerRow.createCell(colNum++).setCellValue("Date adhésion");
|
||||
headerRow.createCell(colNum++).setCellValue("Statut");
|
||||
}
|
||||
if (colonnesExport.contains("ORGANISATION") || colonnesExport.isEmpty()) {
|
||||
headerRow.createCell(colNum++).setCellValue("Organisation");
|
||||
}
|
||||
}
|
||||
|
||||
// Données
|
||||
for (MembreDTO membre : membres) {
|
||||
Row row = sheet.createRow(rowNum++);
|
||||
int colNum = 0;
|
||||
|
||||
if (colonnesExport.contains("PERSO") || colonnesExport.isEmpty()) {
|
||||
row.createCell(colNum++).setCellValue(membre.getNom() != null ? membre.getNom() : "");
|
||||
row.createCell(colNum++).setCellValue(membre.getPrenom() != null ? membre.getPrenom() : "");
|
||||
if (membre.getDateNaissance() != null) {
|
||||
Cell dateCell = row.createCell(colNum++);
|
||||
if (formaterDates) {
|
||||
dateCell.setCellValue(membre.getDateNaissance().format(DATE_FORMATTER));
|
||||
} else {
|
||||
dateCell.setCellValue(membre.getDateNaissance().toString());
|
||||
}
|
||||
} else {
|
||||
row.createCell(colNum++).setCellValue("");
|
||||
}
|
||||
}
|
||||
if (colonnesExport.contains("CONTACT") || colonnesExport.isEmpty()) {
|
||||
row.createCell(colNum++).setCellValue(membre.getEmail() != null ? membre.getEmail() : "");
|
||||
row.createCell(colNum++).setCellValue(membre.getTelephone() != null ? membre.getTelephone() : "");
|
||||
}
|
||||
if (colonnesExport.contains("ADHESION") || colonnesExport.isEmpty()) {
|
||||
if (membre.getDateAdhesion() != null) {
|
||||
Cell dateCell = row.createCell(colNum++);
|
||||
if (formaterDates) {
|
||||
dateCell.setCellValue(membre.getDateAdhesion().format(DATE_FORMATTER));
|
||||
} else {
|
||||
dateCell.setCellValue(membre.getDateAdhesion().toString());
|
||||
}
|
||||
} else {
|
||||
row.createCell(colNum++).setCellValue("");
|
||||
}
|
||||
row.createCell(colNum++).setCellValue(membre.getStatut() != null ? membre.getStatut().toString() : "");
|
||||
}
|
||||
if (colonnesExport.contains("ORGANISATION") || colonnesExport.isEmpty()) {
|
||||
row.createCell(colNum++).setCellValue(membre.getAssociationNom() != null ? membre.getAssociationNom() : "");
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-size columns
|
||||
for (int i = 0; i < 10; i++) {
|
||||
sheet.autoSizeColumn(i);
|
||||
}
|
||||
|
||||
// Ajouter un onglet statistiques si demandé
|
||||
if (inclureStatistiques && !membres.isEmpty()) {
|
||||
Sheet statsSheet = workbook.createSheet("Statistiques");
|
||||
creerOngletStatistiques(statsSheet, membres);
|
||||
}
|
||||
|
||||
// Écrire dans un ByteArrayOutputStream
|
||||
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
|
||||
workbook.write(outputStream);
|
||||
byte[] excelData = outputStream.toByteArray();
|
||||
|
||||
// Chiffrer le fichier si un mot de passe est fourni
|
||||
if (motDePasse != null && !motDePasse.trim().isEmpty()) {
|
||||
return chiffrerExcel(excelData, motDePasse);
|
||||
}
|
||||
|
||||
return excelData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un onglet statistiques dans le classeur Excel
|
||||
*/
|
||||
private void creerOngletStatistiques(Sheet sheet, List<MembreDTO> membres) {
|
||||
int rowNum = 0;
|
||||
|
||||
// Titre
|
||||
Row titleRow = sheet.createRow(rowNum++);
|
||||
Cell titleCell = titleRow.createCell(0);
|
||||
titleCell.setCellValue("Statistiques des Membres");
|
||||
CellStyle titleStyle = sheet.getWorkbook().createCellStyle();
|
||||
Font titleFont = sheet.getWorkbook().createFont();
|
||||
titleFont.setBold(true);
|
||||
titleFont.setFontHeightInPoints((short) 14);
|
||||
titleStyle.setFont(titleFont);
|
||||
titleCell.setCellStyle(titleStyle);
|
||||
|
||||
rowNum++; // Ligne vide
|
||||
|
||||
// Statistiques générales
|
||||
Row headerRow = sheet.createRow(rowNum++);
|
||||
headerRow.createCell(0).setCellValue("Indicateur");
|
||||
headerRow.createCell(1).setCellValue("Valeur");
|
||||
|
||||
// Style pour les en-têtes
|
||||
CellStyle headerStyle = sheet.getWorkbook().createCellStyle();
|
||||
Font headerFont = sheet.getWorkbook().createFont();
|
||||
headerFont.setBold(true);
|
||||
headerStyle.setFont(headerFont);
|
||||
headerStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
|
||||
headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
|
||||
headerRow.getCell(0).setCellStyle(headerStyle);
|
||||
headerRow.getCell(1).setCellStyle(headerStyle);
|
||||
|
||||
// Calcul des statistiques
|
||||
long totalMembres = membres.size();
|
||||
long membresActifs = membres.stream().filter(m -> "ACTIF".equals(m.getStatut())).count();
|
||||
long membresInactifs = membres.stream().filter(m -> "INACTIF".equals(m.getStatut())).count();
|
||||
long membresSuspendus = membres.stream().filter(m -> "SUSPENDU".equals(m.getStatut())).count();
|
||||
|
||||
// Organisations distinctes
|
||||
long organisationsDistinctes = membres.stream()
|
||||
.filter(m -> m.getAssociationNom() != null)
|
||||
.map(MembreDTO::getAssociationNom)
|
||||
.distinct()
|
||||
.count();
|
||||
|
||||
// Statistiques par type (si disponible dans le DTO)
|
||||
// Note: Le type de membre peut ne pas être disponible dans MembreDTO
|
||||
// Pour l'instant, on utilise le statut comme indicateur
|
||||
long typeActif = membresActifs;
|
||||
long typeAssocie = 0;
|
||||
long typeBienfaiteur = 0;
|
||||
long typeHonoraire = 0;
|
||||
|
||||
// Ajout des statistiques
|
||||
int currentRow = rowNum;
|
||||
sheet.createRow(currentRow++).createCell(0).setCellValue("Total Membres");
|
||||
sheet.getRow(currentRow - 1).createCell(1).setCellValue(totalMembres);
|
||||
|
||||
sheet.createRow(currentRow++).createCell(0).setCellValue("Membres Actifs");
|
||||
sheet.getRow(currentRow - 1).createCell(1).setCellValue(membresActifs);
|
||||
|
||||
sheet.createRow(currentRow++).createCell(0).setCellValue("Membres Inactifs");
|
||||
sheet.getRow(currentRow - 1).createCell(1).setCellValue(membresInactifs);
|
||||
|
||||
sheet.createRow(currentRow++).createCell(0).setCellValue("Membres Suspendus");
|
||||
sheet.getRow(currentRow - 1).createCell(1).setCellValue(membresSuspendus);
|
||||
|
||||
sheet.createRow(currentRow++).createCell(0).setCellValue("Organisations Distinctes");
|
||||
sheet.getRow(currentRow - 1).createCell(1).setCellValue(organisationsDistinctes);
|
||||
|
||||
currentRow++; // Ligne vide
|
||||
|
||||
// Section par type
|
||||
sheet.createRow(currentRow++).createCell(0).setCellValue("Répartition par Type");
|
||||
CellStyle sectionStyle = sheet.getWorkbook().createCellStyle();
|
||||
Font sectionFont = sheet.getWorkbook().createFont();
|
||||
sectionFont.setBold(true);
|
||||
sectionStyle.setFont(sectionFont);
|
||||
sheet.getRow(currentRow - 1).getCell(0).setCellStyle(sectionStyle);
|
||||
|
||||
sheet.createRow(currentRow++).createCell(0).setCellValue("Type Actif");
|
||||
sheet.getRow(currentRow - 1).createCell(1).setCellValue(typeActif);
|
||||
|
||||
sheet.createRow(currentRow++).createCell(0).setCellValue("Type Associé");
|
||||
sheet.getRow(currentRow - 1).createCell(1).setCellValue(typeAssocie);
|
||||
|
||||
sheet.createRow(currentRow++).createCell(0).setCellValue("Type Bienfaiteur");
|
||||
sheet.getRow(currentRow - 1).createCell(1).setCellValue(typeBienfaiteur);
|
||||
|
||||
sheet.createRow(currentRow++).createCell(0).setCellValue("Type Honoraire");
|
||||
sheet.getRow(currentRow - 1).createCell(1).setCellValue(typeHonoraire);
|
||||
|
||||
// Auto-size columns
|
||||
sheet.autoSizeColumn(0);
|
||||
sheet.autoSizeColumn(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Protège un fichier Excel avec un mot de passe
|
||||
* Utilise Apache POI pour protéger les feuilles et la structure du workbook
|
||||
* Note: Ceci protège contre la modification, pas un chiffrement complet du fichier
|
||||
*/
|
||||
private byte[] chiffrerExcel(byte[] excelData, String motDePasse) throws IOException {
|
||||
try {
|
||||
// Pour XLSX, on protège les feuilles et la structure du workbook
|
||||
// Note: POI 5.2.5 ne supporte pas le chiffrement complet XLSX (nécessite des bibliothèques externes)
|
||||
// On utilise la protection par mot de passe qui empêche la modification sans le mot de passe
|
||||
|
||||
try (java.io.ByteArrayInputStream inputStream = new java.io.ByteArrayInputStream(excelData);
|
||||
XSSFWorkbook workbook = new XSSFWorkbook(inputStream);
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
|
||||
|
||||
// Protéger toutes les feuilles avec un mot de passe (empêche la modification des cellules)
|
||||
for (int i = 0; i < workbook.getNumberOfSheets(); i++) {
|
||||
Sheet sheet = workbook.getSheetAt(i);
|
||||
sheet.protectSheet(motDePasse);
|
||||
}
|
||||
|
||||
// Protéger la structure du workbook (empêche l'ajout/suppression de feuilles)
|
||||
org.openxmlformats.schemas.spreadsheetml.x2006.main.CTWorkbookProtection protection =
|
||||
workbook.getCTWorkbook().getWorkbookProtection();
|
||||
if (protection == null) {
|
||||
protection = workbook.getCTWorkbook().addNewWorkbookProtection();
|
||||
}
|
||||
protection.setLockStructure(true);
|
||||
// Le mot de passe doit être haché selon le format Excel
|
||||
// Pour simplifier, on utilise le hash MD5 du mot de passe
|
||||
try {
|
||||
java.security.MessageDigest md = java.security.MessageDigest.getInstance("MD5");
|
||||
byte[] passwordHash = md.digest(motDePasse.getBytes(java.nio.charset.StandardCharsets.UTF_16LE));
|
||||
protection.setWorkbookPassword(passwordHash);
|
||||
} catch (java.security.NoSuchAlgorithmException e) {
|
||||
LOG.warnf("Impossible de hasher le mot de passe, protection partielle uniquement");
|
||||
}
|
||||
|
||||
workbook.write(outputStream);
|
||||
return outputStream.toByteArray();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Erreur lors de la protection du fichier Excel");
|
||||
// En cas d'erreur, retourner le fichier non protégé avec un avertissement
|
||||
LOG.warnf("Le fichier sera exporté sans protection en raison d'une erreur");
|
||||
return excelData;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exporte des membres vers CSV
|
||||
*/
|
||||
public byte[] exporterVersCSV(List<MembreDTO> membres, List<String> colonnesExport, boolean inclureHeaders, boolean formaterDates) throws IOException {
|
||||
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
CSVPrinter printer = new CSVPrinter(
|
||||
new java.io.OutputStreamWriter(outputStream, StandardCharsets.UTF_8),
|
||||
CSVFormat.DEFAULT)) {
|
||||
|
||||
// En-têtes
|
||||
if (inclureHeaders) {
|
||||
List<String> headers = new ArrayList<>();
|
||||
if (colonnesExport.contains("PERSO") || colonnesExport.isEmpty()) {
|
||||
headers.add("Nom");
|
||||
headers.add("Prénom");
|
||||
headers.add("Date de naissance");
|
||||
}
|
||||
if (colonnesExport.contains("CONTACT") || colonnesExport.isEmpty()) {
|
||||
headers.add("Email");
|
||||
headers.add("Téléphone");
|
||||
}
|
||||
if (colonnesExport.contains("ADHESION") || colonnesExport.isEmpty()) {
|
||||
headers.add("Date adhésion");
|
||||
headers.add("Statut");
|
||||
}
|
||||
if (colonnesExport.contains("ORGANISATION") || colonnesExport.isEmpty()) {
|
||||
headers.add("Organisation");
|
||||
}
|
||||
printer.printRecord(headers);
|
||||
}
|
||||
|
||||
// Données
|
||||
for (MembreDTO membre : membres) {
|
||||
List<String> values = new ArrayList<>();
|
||||
|
||||
if (colonnesExport.contains("PERSO") || colonnesExport.isEmpty()) {
|
||||
values.add(membre.getNom() != null ? membre.getNom() : "");
|
||||
values.add(membre.getPrenom() != null ? membre.getPrenom() : "");
|
||||
if (membre.getDateNaissance() != null) {
|
||||
values.add(formaterDates ? membre.getDateNaissance().format(DATE_FORMATTER) : membre.getDateNaissance().toString());
|
||||
} else {
|
||||
values.add("");
|
||||
}
|
||||
}
|
||||
if (colonnesExport.contains("CONTACT") || colonnesExport.isEmpty()) {
|
||||
values.add(membre.getEmail() != null ? membre.getEmail() : "");
|
||||
values.add(membre.getTelephone() != null ? membre.getTelephone() : "");
|
||||
}
|
||||
if (colonnesExport.contains("ADHESION") || colonnesExport.isEmpty()) {
|
||||
if (membre.getDateAdhesion() != null) {
|
||||
values.add(formaterDates ? membre.getDateAdhesion().format(DATE_FORMATTER) : membre.getDateAdhesion().toString());
|
||||
} else {
|
||||
values.add("");
|
||||
}
|
||||
values.add(membre.getStatut() != null ? membre.getStatut().toString() : "");
|
||||
}
|
||||
if (colonnesExport.contains("ORGANISATION") || colonnesExport.isEmpty()) {
|
||||
values.add(membre.getAssociationNom() != null ? membre.getAssociationNom() : "");
|
||||
}
|
||||
|
||||
printer.printRecord(values);
|
||||
}
|
||||
|
||||
printer.flush();
|
||||
return outputStream.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère un modèle Excel pour l'import
|
||||
*/
|
||||
public byte[] genererModeleImport() throws IOException {
|
||||
try (Workbook workbook = new XSSFWorkbook()) {
|
||||
Sheet sheet = workbook.createSheet("Modèle");
|
||||
|
||||
// En-têtes
|
||||
Row headerRow = sheet.createRow(0);
|
||||
headerRow.createCell(0).setCellValue("Nom");
|
||||
headerRow.createCell(1).setCellValue("Prénom");
|
||||
headerRow.createCell(2).setCellValue("Email");
|
||||
headerRow.createCell(3).setCellValue("Téléphone");
|
||||
headerRow.createCell(4).setCellValue("Date naissance");
|
||||
headerRow.createCell(5).setCellValue("Date adhésion");
|
||||
headerRow.createCell(6).setCellValue("Adresse");
|
||||
headerRow.createCell(7).setCellValue("Profession");
|
||||
headerRow.createCell(8).setCellValue("Type membre");
|
||||
|
||||
// Exemple de ligne
|
||||
Row exampleRow = sheet.createRow(1);
|
||||
exampleRow.createCell(0).setCellValue("DUPONT");
|
||||
exampleRow.createCell(1).setCellValue("Jean");
|
||||
exampleRow.createCell(2).setCellValue("jean.dupont@example.com");
|
||||
exampleRow.createCell(3).setCellValue("+225 07 12 34 56 78");
|
||||
exampleRow.createCell(4).setCellValue("15/01/1990");
|
||||
exampleRow.createCell(5).setCellValue("01/01/2024");
|
||||
exampleRow.createCell(6).setCellValue("Abidjan, Cocody");
|
||||
exampleRow.createCell(7).setCellValue("Ingénieur");
|
||||
exampleRow.createCell(8).setCellValue("ACTIF");
|
||||
|
||||
// Auto-size columns
|
||||
for (int i = 0; i < 9; i++) {
|
||||
sheet.autoSizeColumn(i);
|
||||
}
|
||||
|
||||
// Écrire dans un ByteArrayOutputStream
|
||||
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
|
||||
workbook.write(outputStream);
|
||||
return outputStream.toByteArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Classe pour le résultat de l'import
|
||||
*/
|
||||
public static class ResultatImport {
|
||||
public int totalLignes;
|
||||
public int lignesTraitees;
|
||||
public int lignesErreur;
|
||||
public List<String> erreurs;
|
||||
public List<MembreDTO> membresImportes;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,740 @@
|
||||
package dev.lions.unionflow.server.service;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.membre.MembreDTO;
|
||||
import dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria;
|
||||
import dev.lions.unionflow.server.api.dto.membre.MembreSearchResultDTO;
|
||||
import dev.lions.unionflow.server.api.enums.membre.StatutMembre;
|
||||
import dev.lions.unionflow.server.entity.Membre;
|
||||
import dev.lions.unionflow.server.repository.MembreRepository;
|
||||
import io.quarkus.panache.common.Page;
|
||||
import io.quarkus.panache.common.Sort;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.PersistenceContext;
|
||||
import jakarta.persistence.TypedQuery;
|
||||
import jakarta.transaction.Transactional;
|
||||
import java.io.InputStream;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.Period;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
/** Service métier pour les membres */
|
||||
@ApplicationScoped
|
||||
public class MembreService {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(MembreService.class);
|
||||
|
||||
@Inject MembreRepository membreRepository;
|
||||
|
||||
@Inject
|
||||
MembreImportExportService membreImportExportService;
|
||||
|
||||
@PersistenceContext
|
||||
EntityManager entityManager;
|
||||
|
||||
/** Crée un nouveau membre */
|
||||
@Transactional
|
||||
public Membre creerMembre(Membre membre) {
|
||||
LOG.infof("Création d'un nouveau membre: %s", membre.getEmail());
|
||||
|
||||
// Générer un numéro de membre unique
|
||||
if (membre.getNumeroMembre() == null || membre.getNumeroMembre().isEmpty()) {
|
||||
membre.setNumeroMembre(genererNumeroMembre());
|
||||
}
|
||||
|
||||
// Définir la date d'adhésion si non fournie
|
||||
if (membre.getDateAdhesion() == null) {
|
||||
membre.setDateAdhesion(LocalDate.now());
|
||||
LOG.infof("Date d'adhésion automatiquement définie à: %s", membre.getDateAdhesion());
|
||||
}
|
||||
|
||||
// Définir la date de naissance par défaut si non fournie (pour éviter @NotNull)
|
||||
if (membre.getDateNaissance() == null) {
|
||||
membre.setDateNaissance(LocalDate.now().minusYears(18)); // Majeur par défaut
|
||||
LOG.warn("Date de naissance non fournie, définie par défaut à il y a 18 ans");
|
||||
}
|
||||
|
||||
// Vérifier l'unicité de l'email
|
||||
if (membreRepository.findByEmail(membre.getEmail()).isPresent()) {
|
||||
throw new IllegalArgumentException("Un membre avec cet email existe déjà");
|
||||
}
|
||||
|
||||
// Vérifier l'unicité du numéro de membre
|
||||
if (membreRepository.findByNumeroMembre(membre.getNumeroMembre()).isPresent()) {
|
||||
throw new IllegalArgumentException("Un membre avec ce numéro existe déjà");
|
||||
}
|
||||
|
||||
membreRepository.persist(membre);
|
||||
|
||||
// Mettre à jour le compteur de membres de l'organisation
|
||||
if (membre.getOrganisation() != null) {
|
||||
membre.getOrganisation().ajouterMembre();
|
||||
LOG.infof("Compteur de membres mis à jour pour l'organisation: %s", membre.getOrganisation().getNom());
|
||||
}
|
||||
|
||||
LOG.infof("Membre créé avec succès: %s (ID: %s)", membre.getNomComplet(), membre.getId());
|
||||
return membre;
|
||||
}
|
||||
|
||||
/** Met à jour un membre existant */
|
||||
@Transactional
|
||||
public Membre mettreAJourMembre(UUID id, Membre membreModifie) {
|
||||
LOG.infof("Mise à jour du membre ID: %s", id);
|
||||
|
||||
Membre membre = membreRepository.findById(id);
|
||||
if (membre == null) {
|
||||
throw new IllegalArgumentException("Membre non trouvé avec l'ID: " + id);
|
||||
}
|
||||
|
||||
// Vérifier l'unicité de l'email si modifié
|
||||
if (!membre.getEmail().equals(membreModifie.getEmail())) {
|
||||
if (membreRepository.findByEmail(membreModifie.getEmail()).isPresent()) {
|
||||
throw new IllegalArgumentException("Un membre avec cet email existe déjà");
|
||||
}
|
||||
}
|
||||
|
||||
// Mettre à jour les champs
|
||||
membre.setPrenom(membreModifie.getPrenom());
|
||||
membre.setNom(membreModifie.getNom());
|
||||
membre.setEmail(membreModifie.getEmail());
|
||||
membre.setTelephone(membreModifie.getTelephone());
|
||||
membre.setDateNaissance(membreModifie.getDateNaissance());
|
||||
membre.setActif(membreModifie.getActif());
|
||||
|
||||
LOG.infof("Membre mis à jour avec succès: %s", membre.getNomComplet());
|
||||
return membre;
|
||||
}
|
||||
|
||||
/** Trouve un membre par son ID */
|
||||
public Optional<Membre> trouverParId(UUID id) {
|
||||
return Optional.ofNullable(membreRepository.findById(id));
|
||||
}
|
||||
|
||||
/** Trouve un membre par son email */
|
||||
public Optional<Membre> trouverParEmail(String email) {
|
||||
return membreRepository.findByEmail(email);
|
||||
}
|
||||
|
||||
/** Liste tous les membres actifs */
|
||||
public List<Membre> listerMembresActifs() {
|
||||
return membreRepository.findAllActifs();
|
||||
}
|
||||
|
||||
/** Recherche des membres par nom ou prénom */
|
||||
public List<Membre> rechercherMembres(String recherche) {
|
||||
return membreRepository.findByNomOrPrenom(recherche);
|
||||
}
|
||||
|
||||
/** Désactive un membre */
|
||||
@Transactional
|
||||
public void desactiverMembre(UUID id) {
|
||||
LOG.infof("Désactivation du membre ID: %s", id);
|
||||
|
||||
Membre membre = membreRepository.findById(id);
|
||||
if (membre == null) {
|
||||
throw new IllegalArgumentException("Membre non trouvé avec l'ID: " + id);
|
||||
}
|
||||
|
||||
membre.setActif(false);
|
||||
LOG.infof("Membre désactivé: %s", membre.getNomComplet());
|
||||
}
|
||||
|
||||
/** Génère un numéro de membre unique */
|
||||
private String genererNumeroMembre() {
|
||||
String prefix = "UF" + LocalDate.now().getYear();
|
||||
String suffix = UUID.randomUUID().toString().substring(0, 8).toUpperCase();
|
||||
return prefix + "-" + suffix;
|
||||
}
|
||||
|
||||
/** Compte le nombre total de membres actifs */
|
||||
public long compterMembresActifs() {
|
||||
return membreRepository.countActifs();
|
||||
}
|
||||
|
||||
/** Liste tous les membres actifs avec pagination */
|
||||
public List<Membre> listerMembresActifs(Page page, Sort sort) {
|
||||
return membreRepository.findAllActifs(page, sort);
|
||||
}
|
||||
|
||||
/** Recherche des membres avec pagination */
|
||||
public List<Membre> rechercherMembres(String recherche, Page page, Sort sort) {
|
||||
return membreRepository.findByNomOrPrenom(recherche, page, sort);
|
||||
}
|
||||
|
||||
/** Obtient les statistiques avancées des membres */
|
||||
public Map<String, Object> obtenirStatistiquesAvancees() {
|
||||
LOG.info("Calcul des statistiques avancées des membres");
|
||||
|
||||
long totalMembres = membreRepository.count();
|
||||
long membresActifs = membreRepository.countActifs();
|
||||
long membresInactifs = totalMembres - membresActifs;
|
||||
long nouveauxMembres30Jours =
|
||||
membreRepository.countNouveauxMembres(LocalDate.now().minusDays(30));
|
||||
|
||||
return Map.of(
|
||||
"totalMembres", totalMembres,
|
||||
"membresActifs", membresActifs,
|
||||
"membresInactifs", membresInactifs,
|
||||
"nouveauxMembres30Jours", nouveauxMembres30Jours,
|
||||
"tauxActivite", totalMembres > 0 ? (membresActifs * 100.0 / totalMembres) : 0.0,
|
||||
"timestamp", LocalDateTime.now());
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// MÉTHODES DE CONVERSION DTO
|
||||
// ========================================
|
||||
|
||||
/** Convertit une entité Membre en MembreDTO */
|
||||
public MembreDTO convertToDTO(Membre membre) {
|
||||
if (membre == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
MembreDTO dto = new MembreDTO();
|
||||
|
||||
// Conversion de l'ID UUID vers UUID (pas de conversion nécessaire maintenant)
|
||||
dto.setId(membre.getId());
|
||||
|
||||
// Copie des champs de base
|
||||
dto.setNumeroMembre(membre.getNumeroMembre());
|
||||
dto.setNom(membre.getNom());
|
||||
dto.setPrenom(membre.getPrenom());
|
||||
dto.setEmail(membre.getEmail());
|
||||
dto.setTelephone(membre.getTelephone());
|
||||
dto.setDateNaissance(membre.getDateNaissance());
|
||||
dto.setDateAdhesion(membre.getDateAdhesion());
|
||||
|
||||
// Conversion du statut boolean vers enum StatutMembre
|
||||
// Règle métier: actif=true → ACTIF, actif=false → INACTIF
|
||||
if (membre.getActif() == null || Boolean.TRUE.equals(membre.getActif())) {
|
||||
dto.setStatut(StatutMembre.ACTIF);
|
||||
} else {
|
||||
dto.setStatut(StatutMembre.INACTIF);
|
||||
}
|
||||
|
||||
// Conversion de l'organisation (associationId)
|
||||
// Utilisation directe de l'UUID de l'organisation
|
||||
if (membre.getOrganisation() != null && membre.getOrganisation().getId() != null) {
|
||||
dto.setAssociationId(membre.getOrganisation().getId());
|
||||
dto.setAssociationNom(membre.getOrganisation().getNom());
|
||||
}
|
||||
|
||||
// Champs de base DTO
|
||||
dto.setDateCreation(membre.getDateCreation());
|
||||
dto.setDateModification(membre.getDateModification());
|
||||
dto.setVersion(0L); // Version par défaut
|
||||
|
||||
// Champs par défaut pour les champs manquants dans l'entité
|
||||
dto.setMembreBureau(false);
|
||||
dto.setResponsable(false);
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
/** Convertit un MembreDTO en entité Membre */
|
||||
public Membre convertFromDTO(MembreDTO dto) {
|
||||
if (dto == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Membre membre = new Membre();
|
||||
|
||||
// Copie des champs
|
||||
membre.setNumeroMembre(dto.getNumeroMembre());
|
||||
membre.setNom(dto.getNom());
|
||||
membre.setPrenom(dto.getPrenom());
|
||||
membre.setEmail(dto.getEmail());
|
||||
membre.setTelephone(dto.getTelephone());
|
||||
membre.setDateNaissance(dto.getDateNaissance());
|
||||
membre.setDateAdhesion(dto.getDateAdhesion());
|
||||
|
||||
// Conversion du statut enum vers boolean
|
||||
// Règle métier: ACTIF → true, autres statuts → false
|
||||
membre.setActif(dto.getStatut() != null && StatutMembre.ACTIF.equals(dto.getStatut()));
|
||||
|
||||
// Champs de base
|
||||
if (dto.getDateCreation() != null) {
|
||||
membre.setDateCreation(dto.getDateCreation());
|
||||
}
|
||||
if (dto.getDateModification() != null) {
|
||||
membre.setDateModification(dto.getDateModification());
|
||||
}
|
||||
|
||||
return membre;
|
||||
}
|
||||
|
||||
/** Convertit une liste d'entités en liste de DTOs */
|
||||
public List<MembreDTO> convertToDTOList(List<Membre> membres) {
|
||||
return membres.stream().map(this::convertToDTO).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/** Met à jour une entité Membre à partir d'un MembreDTO */
|
||||
public void updateFromDTO(Membre membre, MembreDTO dto) {
|
||||
if (membre == null || dto == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Mise à jour des champs modifiables
|
||||
membre.setPrenom(dto.getPrenom());
|
||||
membre.setNom(dto.getNom());
|
||||
membre.setEmail(dto.getEmail());
|
||||
membre.setTelephone(dto.getTelephone());
|
||||
membre.setDateNaissance(dto.getDateNaissance());
|
||||
// Conversion du statut enum vers boolean
|
||||
membre.setActif(dto.getStatut() != null && StatutMembre.ACTIF.equals(dto.getStatut()));
|
||||
membre.setDateModification(LocalDateTime.now());
|
||||
}
|
||||
|
||||
/** Recherche avancée de membres avec filtres multiples (DEPRECATED) */
|
||||
public List<Membre> rechercheAvancee(
|
||||
String recherche,
|
||||
Boolean actif,
|
||||
LocalDate dateAdhesionMin,
|
||||
LocalDate dateAdhesionMax,
|
||||
Page page,
|
||||
Sort sort) {
|
||||
LOG.infof(
|
||||
"Recherche avancée (DEPRECATED) - recherche: %s, actif: %s, dateMin: %s, dateMax: %s",
|
||||
recherche, actif, dateAdhesionMin, dateAdhesionMax);
|
||||
|
||||
return membreRepository.rechercheAvancee(
|
||||
recherche, actif, dateAdhesionMin, dateAdhesionMax, page, sort);
|
||||
}
|
||||
|
||||
/**
|
||||
* Nouvelle recherche avancée de membres avec critères complets Retourne des résultats paginés
|
||||
* avec statistiques
|
||||
*
|
||||
* @param criteria Critères de recherche
|
||||
* @param page Pagination
|
||||
* @param sort Tri
|
||||
* @return Résultats de recherche avec métadonnées
|
||||
*/
|
||||
public MembreSearchResultDTO searchMembresAdvanced(
|
||||
MembreSearchCriteria criteria, Page page, Sort sort) {
|
||||
LOG.infof("Recherche avancée de membres - critères: %s", criteria.getDescription());
|
||||
|
||||
try {
|
||||
// Construction de la requête dynamique
|
||||
StringBuilder queryBuilder = new StringBuilder("SELECT m FROM Membre m WHERE 1=1");
|
||||
Map<String, Object> parameters = new HashMap<>();
|
||||
|
||||
// Ajout des critères de recherche
|
||||
addSearchCriteria(queryBuilder, parameters, criteria);
|
||||
|
||||
// Requête pour compter le total
|
||||
String countQuery =
|
||||
queryBuilder
|
||||
.toString()
|
||||
.replace("SELECT m FROM Membre m", "SELECT COUNT(m) FROM Membre m");
|
||||
|
||||
// Exécution de la requête de comptage
|
||||
TypedQuery<Long> countQueryTyped = entityManager.createQuery(countQuery, Long.class);
|
||||
for (Map.Entry<String, Object> param : parameters.entrySet()) {
|
||||
countQueryTyped.setParameter(param.getKey(), param.getValue());
|
||||
}
|
||||
long totalElements = countQueryTyped.getSingleResult();
|
||||
|
||||
if (totalElements == 0) {
|
||||
return MembreSearchResultDTO.empty(criteria, page.size, page.index);
|
||||
}
|
||||
|
||||
// Ajout du tri et pagination
|
||||
String finalQuery = queryBuilder.toString();
|
||||
if (sort != null) {
|
||||
finalQuery += " ORDER BY " + buildOrderByClause(sort);
|
||||
}
|
||||
|
||||
// Exécution de la requête principale
|
||||
TypedQuery<Membre> queryTyped = entityManager.createQuery(finalQuery, Membre.class);
|
||||
for (Map.Entry<String, Object> param : parameters.entrySet()) {
|
||||
queryTyped.setParameter(param.getKey(), param.getValue());
|
||||
}
|
||||
queryTyped.setFirstResult(page.index * page.size);
|
||||
queryTyped.setMaxResults(page.size);
|
||||
List<Membre> membres = queryTyped.getResultList();
|
||||
|
||||
// Conversion en DTOs
|
||||
List<MembreDTO> membresDTO = convertToDTOList(membres);
|
||||
|
||||
// Calcul des statistiques
|
||||
MembreSearchResultDTO.SearchStatistics statistics = calculateSearchStatistics(membres);
|
||||
|
||||
// Construction du résultat
|
||||
MembreSearchResultDTO result =
|
||||
MembreSearchResultDTO.builder()
|
||||
.membres(membresDTO)
|
||||
.totalElements(totalElements)
|
||||
.totalPages((int) Math.ceil((double) totalElements / page.size))
|
||||
.currentPage(page.index)
|
||||
.pageSize(page.size)
|
||||
.criteria(criteria)
|
||||
.statistics(statistics)
|
||||
.build();
|
||||
|
||||
// Calcul des indicateurs de pagination
|
||||
result.calculatePaginationFlags();
|
||||
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Erreur lors de la recherche avancée de membres");
|
||||
throw new RuntimeException("Erreur lors de la recherche avancée", e);
|
||||
}
|
||||
}
|
||||
|
||||
/** Ajoute les critères de recherche à la requête */
|
||||
private void addSearchCriteria(
|
||||
StringBuilder queryBuilder, Map<String, Object> parameters, MembreSearchCriteria criteria) {
|
||||
|
||||
// Recherche générale dans nom, prénom, email
|
||||
if (criteria.getQuery() != null) {
|
||||
queryBuilder.append(
|
||||
" AND (LOWER(m.nom) LIKE LOWER(:query) OR LOWER(m.prenom) LIKE LOWER(:query) OR"
|
||||
+ " LOWER(m.email) LIKE LOWER(:query))");
|
||||
parameters.put("query", "%" + criteria.getQuery() + "%");
|
||||
}
|
||||
|
||||
// Recherche par nom
|
||||
if (criteria.getNom() != null) {
|
||||
queryBuilder.append(" AND LOWER(m.nom) LIKE LOWER(:nom)");
|
||||
parameters.put("nom", "%" + criteria.getNom() + "%");
|
||||
}
|
||||
|
||||
// Recherche par prénom
|
||||
if (criteria.getPrenom() != null) {
|
||||
queryBuilder.append(" AND LOWER(m.prenom) LIKE LOWER(:prenom)");
|
||||
parameters.put("prenom", "%" + criteria.getPrenom() + "%");
|
||||
}
|
||||
|
||||
// Recherche par email
|
||||
if (criteria.getEmail() != null) {
|
||||
queryBuilder.append(" AND LOWER(m.email) LIKE LOWER(:email)");
|
||||
parameters.put("email", "%" + criteria.getEmail() + "%");
|
||||
}
|
||||
|
||||
// Recherche par téléphone
|
||||
if (criteria.getTelephone() != null) {
|
||||
queryBuilder.append(" AND m.telephone LIKE :telephone");
|
||||
parameters.put("telephone", "%" + criteria.getTelephone() + "%");
|
||||
}
|
||||
|
||||
// Filtre par statut
|
||||
if (criteria.getStatut() != null) {
|
||||
boolean isActif = "ACTIF".equals(criteria.getStatut());
|
||||
queryBuilder.append(" AND m.actif = :actif");
|
||||
parameters.put("actif", isActif);
|
||||
} else if (!Boolean.TRUE.equals(criteria.getIncludeInactifs())) {
|
||||
// Par défaut, exclure les inactifs
|
||||
queryBuilder.append(" AND m.actif = true");
|
||||
}
|
||||
|
||||
// Filtre par dates d'adhésion
|
||||
if (criteria.getDateAdhesionMin() != null) {
|
||||
queryBuilder.append(" AND m.dateAdhesion >= :dateAdhesionMin");
|
||||
parameters.put("dateAdhesionMin", criteria.getDateAdhesionMin());
|
||||
}
|
||||
|
||||
if (criteria.getDateAdhesionMax() != null) {
|
||||
queryBuilder.append(" AND m.dateAdhesion <= :dateAdhesionMax");
|
||||
parameters.put("dateAdhesionMax", criteria.getDateAdhesionMax());
|
||||
}
|
||||
|
||||
// Filtre par âge (calculé à partir de la date de naissance)
|
||||
if (criteria.getAgeMin() != null) {
|
||||
LocalDate maxBirthDate = LocalDate.now().minusYears(criteria.getAgeMin());
|
||||
queryBuilder.append(" AND m.dateNaissance <= :maxBirthDateForMinAge");
|
||||
parameters.put("maxBirthDateForMinAge", maxBirthDate);
|
||||
}
|
||||
|
||||
if (criteria.getAgeMax() != null) {
|
||||
LocalDate minBirthDate = LocalDate.now().minusYears(criteria.getAgeMax() + 1).plusDays(1);
|
||||
queryBuilder.append(" AND m.dateNaissance >= :minBirthDateForMaxAge");
|
||||
parameters.put("minBirthDateForMaxAge", minBirthDate);
|
||||
}
|
||||
|
||||
// Filtre par organisations (si implémenté dans l'entité)
|
||||
if (criteria.getOrganisationIds() != null && !criteria.getOrganisationIds().isEmpty()) {
|
||||
queryBuilder.append(" AND m.organisation.id IN :organisationIds");
|
||||
parameters.put("organisationIds", criteria.getOrganisationIds());
|
||||
}
|
||||
|
||||
// Filtre par rôles (recherche via la relation MembreRole -> Role)
|
||||
if (criteria.getRoles() != null && !criteria.getRoles().isEmpty()) {
|
||||
// Utiliser EXISTS avec une sous-requête pour vérifier les rôles
|
||||
queryBuilder.append(" AND EXISTS (");
|
||||
queryBuilder.append(" SELECT 1 FROM MembreRole mr WHERE mr.membre = m");
|
||||
queryBuilder.append(" AND mr.actif = true");
|
||||
queryBuilder.append(" AND mr.role.code IN :roleCodes");
|
||||
queryBuilder.append(")");
|
||||
// Convertir les noms de rôles en codes (supposant que criteria.getRoles() contient des codes)
|
||||
parameters.put("roleCodes", criteria.getRoles());
|
||||
}
|
||||
}
|
||||
|
||||
/** Construit la clause ORDER BY à partir du Sort */
|
||||
private String buildOrderByClause(Sort sort) {
|
||||
if (sort == null || sort.getColumns().isEmpty()) {
|
||||
return "m.nom ASC";
|
||||
}
|
||||
|
||||
return sort.getColumns().stream()
|
||||
.map(column -> {
|
||||
String direction = column.getDirection() == Sort.Direction.Descending ? "DESC" : "ASC";
|
||||
return "m." + column.getName() + " " + direction;
|
||||
})
|
||||
.collect(Collectors.joining(", "));
|
||||
}
|
||||
|
||||
/** Calcule les statistiques sur les résultats de recherche */
|
||||
private MembreSearchResultDTO.SearchStatistics calculateSearchStatistics(List<Membre> membres) {
|
||||
if (membres.isEmpty()) {
|
||||
return MembreSearchResultDTO.SearchStatistics.builder()
|
||||
.membresActifs(0)
|
||||
.membresInactifs(0)
|
||||
.ageMoyen(0.0)
|
||||
.ageMin(0)
|
||||
.ageMax(0)
|
||||
.nombreOrganisations(0)
|
||||
.nombreRegions(0)
|
||||
.ancienneteMoyenne(0.0)
|
||||
.build();
|
||||
}
|
||||
|
||||
long membresActifs =
|
||||
membres.stream().mapToLong(m -> Boolean.TRUE.equals(m.getActif()) ? 1 : 0).sum();
|
||||
long membresInactifs = membres.size() - membresActifs;
|
||||
|
||||
// Calcul des âges
|
||||
List<Integer> ages =
|
||||
membres.stream()
|
||||
.filter(m -> m.getDateNaissance() != null)
|
||||
.map(m -> Period.between(m.getDateNaissance(), LocalDate.now()).getYears())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
double ageMoyen = ages.stream().mapToInt(Integer::intValue).average().orElse(0.0);
|
||||
int ageMin = ages.stream().mapToInt(Integer::intValue).min().orElse(0);
|
||||
int ageMax = ages.stream().mapToInt(Integer::intValue).max().orElse(0);
|
||||
|
||||
// Calcul de l'ancienneté moyenne
|
||||
double ancienneteMoyenne =
|
||||
membres.stream()
|
||||
.filter(m -> m.getDateAdhesion() != null)
|
||||
.mapToDouble(m -> Period.between(m.getDateAdhesion(), LocalDate.now()).getYears())
|
||||
.average()
|
||||
.orElse(0.0);
|
||||
|
||||
// Nombre d'organisations (si relation disponible)
|
||||
long nombreOrganisations =
|
||||
membres.stream()
|
||||
.filter(m -> m.getOrganisation() != null)
|
||||
.map(m -> m.getOrganisation().getId())
|
||||
.distinct()
|
||||
.count();
|
||||
|
||||
return MembreSearchResultDTO.SearchStatistics.builder()
|
||||
.membresActifs(membresActifs)
|
||||
.membresInactifs(membresInactifs)
|
||||
.ageMoyen(ageMoyen)
|
||||
.ageMin(ageMin)
|
||||
.ageMax(ageMax)
|
||||
.nombreOrganisations(nombreOrganisations)
|
||||
.nombreRegions(0) // TODO: Calculer depuis les adresses
|
||||
.ancienneteMoyenne(ancienneteMoyenne)
|
||||
.build();
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// MÉTHODES D'AUTOCOMPLÉTION (WOU/DRY)
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Obtient la liste des villes distinctes depuis les adresses des membres
|
||||
* Réutilisable pour autocomplétion (WOU/DRY)
|
||||
*/
|
||||
public List<String> obtenirVillesDistinctes(String query) {
|
||||
LOG.infof("Récupération des villes distinctes - query: %s", query);
|
||||
|
||||
String jpql = "SELECT DISTINCT a.ville FROM Adresse a WHERE a.ville IS NOT NULL AND a.ville != ''";
|
||||
if (query != null && !query.trim().isEmpty()) {
|
||||
jpql += " AND LOWER(a.ville) LIKE LOWER(:query)";
|
||||
}
|
||||
jpql += " ORDER BY a.ville ASC";
|
||||
|
||||
TypedQuery<String> typedQuery = entityManager.createQuery(jpql, String.class);
|
||||
if (query != null && !query.trim().isEmpty()) {
|
||||
typedQuery.setParameter("query", "%" + query.trim() + "%");
|
||||
}
|
||||
typedQuery.setMaxResults(50); // Limiter à 50 résultats pour performance
|
||||
|
||||
List<String> villes = typedQuery.getResultList();
|
||||
LOG.infof("Trouvé %d villes distinctes", villes.size());
|
||||
return villes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtient la liste des professions distinctes depuis les membres
|
||||
* Note: Si le champ profession n'existe pas dans Membre, retourne une liste vide
|
||||
* Réutilisable pour autocomplétion (WOU/DRY)
|
||||
*/
|
||||
public List<String> obtenirProfessionsDistinctes(String query) {
|
||||
LOG.infof("Récupération des professions distinctes - query: %s", query);
|
||||
|
||||
// TODO: Vérifier si le champ profession existe dans Membre
|
||||
// Pour l'instant, retourner une liste vide car le champ n'existe pas
|
||||
// Cette méthode peut être étendue si un champ profession est ajouté plus tard
|
||||
LOG.warn("Le champ profession n'existe pas dans l'entité Membre. Retour d'une liste vide.");
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Exporte une sélection de membres en Excel (WOU/DRY - réutilise la logique d'export)
|
||||
*
|
||||
* @param membreIds Liste des IDs des membres à exporter
|
||||
* @param format Format d'export (EXCEL, CSV, etc.)
|
||||
* @return Données binaires du fichier Excel
|
||||
*/
|
||||
public byte[] exporterMembresSelectionnes(List<UUID> membreIds, String format) {
|
||||
LOG.infof("Export de %d membres sélectionnés - format: %s", membreIds.size(), format);
|
||||
|
||||
if (membreIds == null || membreIds.isEmpty()) {
|
||||
throw new IllegalArgumentException("La liste des membres ne peut pas être vide");
|
||||
}
|
||||
|
||||
// Récupérer les membres
|
||||
List<Membre> membres =
|
||||
membreIds.stream()
|
||||
.map(id -> membreRepository.findByIdOptional(id))
|
||||
.filter(opt -> opt.isPresent())
|
||||
.map(java.util.Optional::get)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// Convertir en DTOs
|
||||
List<MembreDTO> membresDTO = convertToDTOList(membres);
|
||||
|
||||
// Générer le fichier Excel (simplifié - à améliorer avec Apache POI)
|
||||
// Pour l'instant, générer un CSV simple
|
||||
StringBuilder csv = new StringBuilder();
|
||||
csv.append("Numéro;Nom;Prénom;Email;Téléphone;Statut;Date Adhésion\n");
|
||||
for (MembreDTO m : membresDTO) {
|
||||
csv.append(
|
||||
String.format(
|
||||
"%s;%s;%s;%s;%s;%s;%s\n",
|
||||
m.getNumeroMembre() != null ? m.getNumeroMembre() : "",
|
||||
m.getNom() != null ? m.getNom() : "",
|
||||
m.getPrenom() != null ? m.getPrenom() : "",
|
||||
m.getEmail() != null ? m.getEmail() : "",
|
||||
m.getTelephone() != null ? m.getTelephone() : "",
|
||||
m.getStatut() != null ? m.getStatut() : "",
|
||||
m.getDateAdhesion() != null ? m.getDateAdhesion().toString() : ""));
|
||||
}
|
||||
|
||||
return csv.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Importe des membres depuis un fichier Excel ou CSV
|
||||
*/
|
||||
public MembreImportExportService.ResultatImport importerMembres(
|
||||
InputStream fileInputStream,
|
||||
String fileName,
|
||||
UUID organisationId,
|
||||
String typeMembreDefaut,
|
||||
boolean mettreAJourExistants,
|
||||
boolean ignorerErreurs) {
|
||||
return membreImportExportService.importerMembres(
|
||||
fileInputStream, fileName, organisationId, typeMembreDefaut, mettreAJourExistants, ignorerErreurs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exporte des membres vers Excel
|
||||
*/
|
||||
public byte[] exporterVersExcel(List<MembreDTO> membres, List<String> colonnesExport, boolean inclureHeaders, boolean formaterDates, boolean inclureStatistiques, String motDePasse) {
|
||||
try {
|
||||
return membreImportExportService.exporterVersExcel(membres, colonnesExport, inclureHeaders, formaterDates, inclureStatistiques, motDePasse);
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Erreur lors de l'export Excel");
|
||||
throw new RuntimeException("Erreur lors de l'export Excel: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exporte des membres vers CSV
|
||||
*/
|
||||
public byte[] exporterVersCSV(List<MembreDTO> membres, List<String> colonnesExport, boolean inclureHeaders, boolean formaterDates) {
|
||||
try {
|
||||
return membreImportExportService.exporterVersCSV(membres, colonnesExport, inclureHeaders, formaterDates);
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Erreur lors de l'export CSV");
|
||||
throw new RuntimeException("Erreur lors de l'export CSV: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère un modèle Excel pour l'import
|
||||
*/
|
||||
public byte[] genererModeleImport() {
|
||||
try {
|
||||
return membreImportExportService.genererModeleImport();
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Erreur lors de la génération du modèle");
|
||||
throw new RuntimeException("Erreur lors de la génération du modèle: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste les membres pour l'export selon les filtres
|
||||
*/
|
||||
public List<MembreDTO> listerMembresPourExport(
|
||||
UUID associationId,
|
||||
String statut,
|
||||
String type,
|
||||
String dateAdhesionDebut,
|
||||
String dateAdhesionFin) {
|
||||
|
||||
List<Membre> membres;
|
||||
|
||||
if (associationId != null) {
|
||||
TypedQuery<Membre> query = entityManager.createQuery(
|
||||
"SELECT m FROM Membre m WHERE m.organisation.id = :associationId", Membre.class);
|
||||
query.setParameter("associationId", associationId);
|
||||
membres = query.getResultList();
|
||||
} else {
|
||||
membres = membreRepository.listAll();
|
||||
}
|
||||
|
||||
// Filtrer par statut
|
||||
if (statut != null && !statut.isEmpty()) {
|
||||
boolean actif = "ACTIF".equals(statut);
|
||||
membres = membres.stream()
|
||||
.filter(m -> m.getActif() == actif)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// Filtrer par dates d'adhésion
|
||||
if (dateAdhesionDebut != null && !dateAdhesionDebut.isEmpty()) {
|
||||
LocalDate dateDebut = LocalDate.parse(dateAdhesionDebut);
|
||||
membres = membres.stream()
|
||||
.filter(m -> m.getDateAdhesion() != null && !m.getDateAdhesion().isBefore(dateDebut))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
if (dateAdhesionFin != null && !dateAdhesionFin.isEmpty()) {
|
||||
LocalDate dateFin = LocalDate.parse(dateAdhesionFin);
|
||||
membres = membres.stream()
|
||||
.filter(m -> m.getDateAdhesion() != null && !m.getDateAdhesion().isAfter(dateFin))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
return convertToDTOList(membres);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
package dev.lions.unionflow.server.service;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
/** Service pour gérer l'historique des notifications */
|
||||
@ApplicationScoped
|
||||
public class NotificationHistoryService {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(NotificationHistoryService.class);
|
||||
|
||||
// Stockage temporaire en mémoire (à remplacer par une base de données)
|
||||
private final Map<UUID, List<NotificationHistoryEntry>> historiqueNotifications =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
/** Enregistre une notification dans l'historique */
|
||||
public void enregistrerNotification(
|
||||
UUID utilisateurId, String type, String titre, String message, String canal, boolean succes) {
|
||||
LOG.infof("Enregistrement de la notification %s pour l'utilisateur %s", type, utilisateurId);
|
||||
|
||||
NotificationHistoryEntry entry =
|
||||
NotificationHistoryEntry.builder()
|
||||
.id(UUID.randomUUID())
|
||||
.utilisateurId(utilisateurId)
|
||||
.type(type)
|
||||
.titre(titre)
|
||||
.message(message)
|
||||
.canal(canal)
|
||||
.dateEnvoi(LocalDateTime.now())
|
||||
.succes(succes)
|
||||
.lu(false)
|
||||
.build();
|
||||
|
||||
historiqueNotifications.computeIfAbsent(utilisateurId, k -> new ArrayList<>()).add(entry);
|
||||
|
||||
// Limiter l'historique à 1000 notifications par utilisateur
|
||||
List<NotificationHistoryEntry> historique = historiqueNotifications.get(utilisateurId);
|
||||
if (historique.size() > 1000) {
|
||||
historique.sort(Comparator.comparing(NotificationHistoryEntry::getDateEnvoi).reversed());
|
||||
historiqueNotifications.put(utilisateurId, historique.subList(0, 1000));
|
||||
}
|
||||
}
|
||||
|
||||
/** Obtient l'historique des notifications d'un utilisateur */
|
||||
public List<NotificationHistoryEntry> obtenirHistorique(UUID utilisateurId) {
|
||||
LOG.infof(
|
||||
"Récupération de l'historique des notifications pour l'utilisateur %s", utilisateurId);
|
||||
|
||||
return historiqueNotifications.getOrDefault(utilisateurId, new ArrayList<>()).stream()
|
||||
.sorted(Comparator.comparing(NotificationHistoryEntry::getDateEnvoi).reversed())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/** Obtient l'historique des notifications d'un utilisateur avec pagination */
|
||||
public List<NotificationHistoryEntry> obtenirHistorique(
|
||||
UUID utilisateurId, int page, int taille) {
|
||||
List<NotificationHistoryEntry> historique = obtenirHistorique(utilisateurId);
|
||||
|
||||
int debut = page * taille;
|
||||
int fin = Math.min(debut + taille, historique.size());
|
||||
|
||||
if (debut >= historique.size()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
return historique.subList(debut, fin);
|
||||
}
|
||||
|
||||
/** Marque une notification comme lue */
|
||||
public void marquerCommeLue(UUID utilisateurId, UUID notificationId) {
|
||||
LOG.infof(
|
||||
"Marquage de la notification %s comme lue pour l'utilisateur %s",
|
||||
notificationId, utilisateurId);
|
||||
|
||||
List<NotificationHistoryEntry> historique = historiqueNotifications.get(utilisateurId);
|
||||
if (historique != null) {
|
||||
historique.stream()
|
||||
.filter(entry -> entry.getId().equals(notificationId))
|
||||
.findFirst()
|
||||
.ifPresent(entry -> entry.setLu(true));
|
||||
}
|
||||
}
|
||||
|
||||
/** Marque toutes les notifications comme lues */
|
||||
public void marquerToutesCommeLues(UUID utilisateurId) {
|
||||
LOG.infof(
|
||||
"Marquage de toutes les notifications comme lues pour l'utilisateur %s", utilisateurId);
|
||||
|
||||
List<NotificationHistoryEntry> historique = historiqueNotifications.get(utilisateurId);
|
||||
if (historique != null) {
|
||||
historique.forEach(entry -> entry.setLu(true));
|
||||
}
|
||||
}
|
||||
|
||||
/** Compte le nombre de notifications non lues */
|
||||
public long compterNotificationsNonLues(UUID utilisateurId) {
|
||||
return obtenirHistorique(utilisateurId).stream().filter(entry -> !entry.isLu()).count();
|
||||
}
|
||||
|
||||
/** Obtient les notifications non lues */
|
||||
public List<NotificationHistoryEntry> obtenirNotificationsNonLues(UUID utilisateurId) {
|
||||
return obtenirHistorique(utilisateurId).stream()
|
||||
.filter(entry -> !entry.isLu())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/** Supprime les notifications anciennes (plus de 90 jours) */
|
||||
public void nettoyerHistorique() {
|
||||
LOG.info("Nettoyage de l'historique des notifications");
|
||||
|
||||
LocalDateTime dateLimit = LocalDateTime.now().minusDays(90);
|
||||
|
||||
for (Map.Entry<UUID, List<NotificationHistoryEntry>> entry :
|
||||
historiqueNotifications.entrySet()) {
|
||||
List<NotificationHistoryEntry> historique = entry.getValue();
|
||||
List<NotificationHistoryEntry> historiqueFiltre =
|
||||
historique.stream()
|
||||
.filter(notification -> notification.getDateEnvoi().isAfter(dateLimit))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
entry.setValue(historiqueFiltre);
|
||||
}
|
||||
}
|
||||
|
||||
/** Obtient les statistiques des notifications pour un utilisateur */
|
||||
public Map<String, Object> obtenirStatistiques(UUID utilisateurId) {
|
||||
List<NotificationHistoryEntry> historique = obtenirHistorique(utilisateurId);
|
||||
|
||||
Map<String, Object> stats = new HashMap<>();
|
||||
stats.put("total", historique.size());
|
||||
stats.put("nonLues", historique.stream().filter(entry -> !entry.isLu()).count());
|
||||
stats.put("succes", historique.stream().filter(NotificationHistoryEntry::isSucces).count());
|
||||
stats.put("echecs", historique.stream().filter(entry -> !entry.isSucces()).count());
|
||||
|
||||
// Statistiques par type
|
||||
Map<String, Long> parType =
|
||||
historique.stream()
|
||||
.collect(
|
||||
Collectors.groupingBy(NotificationHistoryEntry::getType, Collectors.counting()));
|
||||
stats.put("parType", parType);
|
||||
|
||||
// Statistiques par canal
|
||||
Map<String, Long> parCanal =
|
||||
historique.stream()
|
||||
.collect(
|
||||
Collectors.groupingBy(NotificationHistoryEntry::getCanal, Collectors.counting()));
|
||||
stats.put("parCanal", parCanal);
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/** Classe interne pour représenter une entrée d'historique */
|
||||
public static class NotificationHistoryEntry {
|
||||
private UUID id;
|
||||
private UUID utilisateurId;
|
||||
private String type;
|
||||
private String titre;
|
||||
private String message;
|
||||
private String canal;
|
||||
private LocalDateTime dateEnvoi;
|
||||
private boolean succes;
|
||||
private boolean lu;
|
||||
|
||||
// Constructeurs
|
||||
public NotificationHistoryEntry() {}
|
||||
|
||||
private NotificationHistoryEntry(Builder builder) {
|
||||
this.id = builder.id;
|
||||
this.utilisateurId = builder.utilisateurId;
|
||||
this.type = builder.type;
|
||||
this.titre = builder.titre;
|
||||
this.message = builder.message;
|
||||
this.canal = builder.canal;
|
||||
this.dateEnvoi = builder.dateEnvoi;
|
||||
this.succes = builder.succes;
|
||||
this.lu = builder.lu;
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
// Getters et Setters
|
||||
public UUID getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(UUID id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public UUID getUtilisateurId() {
|
||||
return utilisateurId;
|
||||
}
|
||||
|
||||
public void setUtilisateurId(UUID utilisateurId) {
|
||||
this.utilisateurId = utilisateurId;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public String getTitre() {
|
||||
return titre;
|
||||
}
|
||||
|
||||
public void setTitre(String titre) {
|
||||
this.titre = titre;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public String getCanal() {
|
||||
return canal;
|
||||
}
|
||||
|
||||
public void setCanal(String canal) {
|
||||
this.canal = canal;
|
||||
}
|
||||
|
||||
public LocalDateTime getDateEnvoi() {
|
||||
return dateEnvoi;
|
||||
}
|
||||
|
||||
public void setDateEnvoi(LocalDateTime dateEnvoi) {
|
||||
this.dateEnvoi = dateEnvoi;
|
||||
}
|
||||
|
||||
public boolean isSucces() {
|
||||
return succes;
|
||||
}
|
||||
|
||||
public void setSucces(boolean succes) {
|
||||
this.succes = succes;
|
||||
}
|
||||
|
||||
public boolean isLu() {
|
||||
return lu;
|
||||
}
|
||||
|
||||
public void setLu(boolean lu) {
|
||||
this.lu = lu;
|
||||
}
|
||||
|
||||
// Builder
|
||||
public static class Builder {
|
||||
private UUID id;
|
||||
private UUID utilisateurId;
|
||||
private String type;
|
||||
private String titre;
|
||||
private String message;
|
||||
private String canal;
|
||||
private LocalDateTime dateEnvoi;
|
||||
private boolean succes;
|
||||
private boolean lu;
|
||||
|
||||
public Builder id(UUID id) {
|
||||
this.id = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder utilisateurId(UUID utilisateurId) {
|
||||
this.utilisateurId = utilisateurId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder type(String type) {
|
||||
this.type = type;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder titre(String titre) {
|
||||
this.titre = titre;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder message(String message) {
|
||||
this.message = message;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder canal(String canal) {
|
||||
this.canal = canal;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder dateEnvoi(LocalDateTime dateEnvoi) {
|
||||
this.dateEnvoi = dateEnvoi;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder succes(boolean succes) {
|
||||
this.succes = succes;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder lu(boolean lu) {
|
||||
this.lu = lu;
|
||||
return this;
|
||||
}
|
||||
|
||||
public NotificationHistoryEntry build() {
|
||||
return new NotificationHistoryEntry(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
package dev.lions.unionflow.server.service;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.notification.NotificationDTO;
|
||||
import dev.lions.unionflow.server.api.dto.notification.TemplateNotificationDTO;
|
||||
import dev.lions.unionflow.server.api.enums.notification.PrioriteNotification;
|
||||
import dev.lions.unionflow.server.api.enums.notification.StatutNotification;
|
||||
import dev.lions.unionflow.server.entity.*;
|
||||
import dev.lions.unionflow.server.repository.*;
|
||||
import dev.lions.unionflow.server.service.KeycloakService;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.ws.rs.NotFoundException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
/**
|
||||
* Service métier pour la gestion des notifications
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 3.0
|
||||
* @since 2025-01-29
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class NotificationService {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(NotificationService.class);
|
||||
|
||||
@Inject NotificationRepository notificationRepository;
|
||||
|
||||
@Inject TemplateNotificationRepository templateNotificationRepository;
|
||||
|
||||
@Inject MembreRepository membreRepository;
|
||||
|
||||
@Inject OrganisationRepository organisationRepository;
|
||||
|
||||
@Inject KeycloakService keycloakService;
|
||||
|
||||
/**
|
||||
* Crée un nouveau template de notification
|
||||
*
|
||||
* @param templateDTO DTO du template à créer
|
||||
* @return DTO du template créé
|
||||
*/
|
||||
@Transactional
|
||||
public TemplateNotificationDTO creerTemplate(TemplateNotificationDTO templateDTO) {
|
||||
LOG.infof("Création d'un nouveau template: %s", templateDTO.getCode());
|
||||
|
||||
// Vérifier l'unicité du code
|
||||
if (templateNotificationRepository.findByCode(templateDTO.getCode()).isPresent()) {
|
||||
throw new IllegalArgumentException("Un template avec ce code existe déjà: " + templateDTO.getCode());
|
||||
}
|
||||
|
||||
TemplateNotification template = convertToEntity(templateDTO);
|
||||
template.setCreePar(keycloakService.getCurrentUserEmail());
|
||||
|
||||
templateNotificationRepository.persist(template);
|
||||
LOG.infof("Template créé avec succès: ID=%s, Code=%s", template.getId(), template.getCode());
|
||||
|
||||
return convertToDTO(template);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée une nouvelle notification
|
||||
*
|
||||
* @param notificationDTO DTO de la notification à créer
|
||||
* @return DTO de la notification créée
|
||||
*/
|
||||
@Transactional
|
||||
public NotificationDTO creerNotification(NotificationDTO notificationDTO) {
|
||||
LOG.infof("Création d'une nouvelle notification: %s", notificationDTO.getTypeNotification());
|
||||
|
||||
Notification notification = convertToEntity(notificationDTO);
|
||||
notification.setCreePar(keycloakService.getCurrentUserEmail());
|
||||
|
||||
notificationRepository.persist(notification);
|
||||
LOG.infof("Notification créée avec succès: ID=%s", notification.getId());
|
||||
|
||||
return convertToDTO(notification);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marque une notification comme lue
|
||||
*
|
||||
* @param id ID de la notification
|
||||
* @return DTO de la notification mise à jour
|
||||
*/
|
||||
@Transactional
|
||||
public NotificationDTO marquerCommeLue(UUID id) {
|
||||
LOG.infof("Marquage de la notification comme lue: ID=%s", id);
|
||||
|
||||
Notification notification =
|
||||
notificationRepository
|
||||
.findNotificationById(id)
|
||||
.orElseThrow(() -> new NotFoundException("Notification non trouvée avec l'ID: " + id));
|
||||
|
||||
notification.setStatut(StatutNotification.LUE);
|
||||
notification.setDateLecture(LocalDateTime.now());
|
||||
notification.setModifiePar(keycloakService.getCurrentUserEmail());
|
||||
|
||||
notificationRepository.persist(notification);
|
||||
LOG.infof("Notification marquée comme lue: ID=%s", id);
|
||||
|
||||
return convertToDTO(notification);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve une notification par son ID
|
||||
*
|
||||
* @param id ID de la notification
|
||||
* @return DTO de la notification
|
||||
*/
|
||||
public NotificationDTO trouverNotificationParId(UUID id) {
|
||||
return notificationRepository
|
||||
.findNotificationById(id)
|
||||
.map(this::convertToDTO)
|
||||
.orElseThrow(() -> new NotFoundException("Notification non trouvée avec l'ID: " + id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste toutes les notifications d'un membre
|
||||
*
|
||||
* @param membreId ID du membre
|
||||
* @return Liste des notifications
|
||||
*/
|
||||
public List<NotificationDTO> listerNotificationsParMembre(UUID membreId) {
|
||||
return notificationRepository.findByMembreId(membreId).stream()
|
||||
.map(this::convertToDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste les notifications non lues d'un membre
|
||||
*
|
||||
* @param membreId ID du membre
|
||||
* @return Liste des notifications non lues
|
||||
*/
|
||||
public List<NotificationDTO> listerNotificationsNonLuesParMembre(UUID membreId) {
|
||||
return notificationRepository.findNonLuesByMembreId(membreId).stream()
|
||||
.map(this::convertToDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste les notifications en attente d'envoi
|
||||
*
|
||||
* @return Liste des notifications en attente
|
||||
*/
|
||||
public List<NotificationDTO> listerNotificationsEnAttenteEnvoi() {
|
||||
return notificationRepository.findEnAttenteEnvoi().stream()
|
||||
.map(this::convertToDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Envoie des notifications groupées à plusieurs membres (WOU/DRY)
|
||||
*
|
||||
* @param membreIds Liste des IDs des membres destinataires
|
||||
* @param sujet Sujet de la notification
|
||||
* @param corps Corps du message
|
||||
* @param canaux Canaux d'envoi (EMAIL, SMS, etc.)
|
||||
* @return Nombre de notifications créées
|
||||
*/
|
||||
@Transactional
|
||||
public int envoyerNotificationsGroupees(
|
||||
List<UUID> membreIds, String sujet, String corps, List<String> canaux) {
|
||||
LOG.infof(
|
||||
"Envoi de notifications groupées à %d membres - sujet: %s", membreIds.size(), sujet);
|
||||
|
||||
if (membreIds == null || membreIds.isEmpty()) {
|
||||
throw new IllegalArgumentException("La liste des membres ne peut pas être vide");
|
||||
}
|
||||
|
||||
int notificationsCreees = 0;
|
||||
for (UUID membreId : membreIds) {
|
||||
try {
|
||||
Membre membre =
|
||||
membreRepository
|
||||
.findByIdOptional(membreId)
|
||||
.orElseThrow(
|
||||
() ->
|
||||
new IllegalArgumentException(
|
||||
"Membre non trouvé avec l'ID: " + membreId));
|
||||
|
||||
Notification notification = new Notification();
|
||||
notification.setMembre(membre);
|
||||
notification.setSujet(sujet);
|
||||
notification.setCorps(corps);
|
||||
notification.setTypeNotification(
|
||||
dev.lions.unionflow.server.api.enums.notification.TypeNotification.IN_APP);
|
||||
notification.setPriorite(PrioriteNotification.NORMALE);
|
||||
notification.setStatut(StatutNotification.EN_ATTENTE);
|
||||
notification.setDateEnvoiPrevue(java.time.LocalDateTime.now());
|
||||
notification.setCreePar(keycloakService.getCurrentUserEmail());
|
||||
|
||||
notificationRepository.persist(notification);
|
||||
notificationsCreees++;
|
||||
} catch (Exception e) {
|
||||
LOG.warnf(
|
||||
"Erreur lors de la création de la notification pour le membre %s: %s",
|
||||
membreId, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
LOG.infof(
|
||||
"%d notifications créées sur %d membres demandés", notificationsCreees, membreIds.size());
|
||||
return notificationsCreees;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// MÉTHODES PRIVÉES
|
||||
// ========================================
|
||||
|
||||
/** Convertit une entité TemplateNotification en DTO */
|
||||
private TemplateNotificationDTO convertToDTO(TemplateNotification template) {
|
||||
if (template == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
TemplateNotificationDTO dto = new TemplateNotificationDTO();
|
||||
dto.setId(template.getId());
|
||||
dto.setCode(template.getCode());
|
||||
dto.setSujet(template.getSujet());
|
||||
dto.setCorpsTexte(template.getCorpsTexte());
|
||||
dto.setCorpsHtml(template.getCorpsHtml());
|
||||
dto.setVariablesDisponibles(template.getVariablesDisponibles());
|
||||
dto.setCanauxSupportes(template.getCanauxSupportes());
|
||||
dto.setLangue(template.getLangue());
|
||||
dto.setDescription(template.getDescription());
|
||||
dto.setDateCreation(template.getDateCreation());
|
||||
dto.setDateModification(template.getDateModification());
|
||||
dto.setActif(template.getActif());
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
/** Convertit un DTO en entité TemplateNotification */
|
||||
private TemplateNotification convertToEntity(TemplateNotificationDTO dto) {
|
||||
if (dto == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
TemplateNotification template = new TemplateNotification();
|
||||
template.setCode(dto.getCode());
|
||||
template.setSujet(dto.getSujet());
|
||||
template.setCorpsTexte(dto.getCorpsTexte());
|
||||
template.setCorpsHtml(dto.getCorpsHtml());
|
||||
template.setVariablesDisponibles(dto.getVariablesDisponibles());
|
||||
template.setCanauxSupportes(dto.getCanauxSupportes());
|
||||
template.setLangue(dto.getLangue() != null ? dto.getLangue() : "fr");
|
||||
template.setDescription(dto.getDescription());
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
/** Convertit une entité Notification en DTO */
|
||||
private NotificationDTO convertToDTO(Notification notification) {
|
||||
if (notification == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
NotificationDTO dto = new NotificationDTO();
|
||||
dto.setId(notification.getId());
|
||||
dto.setTypeNotification(notification.getTypeNotification());
|
||||
dto.setPriorite(notification.getPriorite());
|
||||
dto.setStatut(notification.getStatut());
|
||||
dto.setSujet(notification.getSujet());
|
||||
dto.setCorps(notification.getCorps());
|
||||
dto.setDateEnvoiPrevue(notification.getDateEnvoiPrevue());
|
||||
dto.setDateEnvoi(notification.getDateEnvoi());
|
||||
dto.setDateLecture(notification.getDateLecture());
|
||||
dto.setNombreTentatives(notification.getNombreTentatives());
|
||||
dto.setMessageErreur(notification.getMessageErreur());
|
||||
dto.setDonneesAdditionnelles(notification.getDonneesAdditionnelles());
|
||||
|
||||
if (notification.getMembre() != null) {
|
||||
dto.setMembreId(notification.getMembre().getId());
|
||||
}
|
||||
if (notification.getOrganisation() != null) {
|
||||
dto.setOrganisationId(notification.getOrganisation().getId());
|
||||
}
|
||||
if (notification.getTemplate() != null) {
|
||||
dto.setTemplateId(notification.getTemplate().getId());
|
||||
}
|
||||
|
||||
dto.setDateCreation(notification.getDateCreation());
|
||||
dto.setDateModification(notification.getDateModification());
|
||||
dto.setActif(notification.getActif());
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
/** Convertit un DTO en entité Notification */
|
||||
private Notification convertToEntity(NotificationDTO dto) {
|
||||
if (dto == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Notification notification = new Notification();
|
||||
notification.setTypeNotification(dto.getTypeNotification());
|
||||
notification.setPriorite(
|
||||
dto.getPriorite() != null ? dto.getPriorite() : PrioriteNotification.NORMALE);
|
||||
notification.setStatut(
|
||||
dto.getStatut() != null ? dto.getStatut() : StatutNotification.EN_ATTENTE);
|
||||
notification.setSujet(dto.getSujet());
|
||||
notification.setCorps(dto.getCorps());
|
||||
notification.setDateEnvoiPrevue(
|
||||
dto.getDateEnvoiPrevue() != null ? dto.getDateEnvoiPrevue() : LocalDateTime.now());
|
||||
notification.setDateEnvoi(dto.getDateEnvoi());
|
||||
notification.setDateLecture(dto.getDateLecture());
|
||||
notification.setNombreTentatives(dto.getNombreTentatives() != null ? dto.getNombreTentatives() : 0);
|
||||
notification.setMessageErreur(dto.getMessageErreur());
|
||||
notification.setDonneesAdditionnelles(dto.getDonneesAdditionnelles());
|
||||
|
||||
// Relations
|
||||
if (dto.getMembreId() != null) {
|
||||
Membre membre =
|
||||
membreRepository
|
||||
.findByIdOptional(dto.getMembreId())
|
||||
.orElseThrow(
|
||||
() -> new NotFoundException("Membre non trouvé avec l'ID: " + dto.getMembreId()));
|
||||
notification.setMembre(membre);
|
||||
}
|
||||
|
||||
if (dto.getOrganisationId() != null) {
|
||||
Organisation org =
|
||||
organisationRepository
|
||||
.findByIdOptional(dto.getOrganisationId())
|
||||
.orElseThrow(
|
||||
() ->
|
||||
new NotFoundException(
|
||||
"Organisation non trouvée avec l'ID: " + dto.getOrganisationId()));
|
||||
notification.setOrganisation(org);
|
||||
}
|
||||
|
||||
if (dto.getTemplateId() != null) {
|
||||
TemplateNotification template =
|
||||
templateNotificationRepository
|
||||
.findTemplateNotificationById(dto.getTemplateId())
|
||||
.orElseThrow(
|
||||
() ->
|
||||
new NotFoundException(
|
||||
"Template non trouvé avec l'ID: " + dto.getTemplateId()));
|
||||
notification.setTemplate(template);
|
||||
}
|
||||
|
||||
return notification;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,443 @@
|
||||
package dev.lions.unionflow.server.service;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.organisation.OrganisationDTO;
|
||||
import dev.lions.unionflow.server.api.enums.organisation.StatutOrganisation;
|
||||
import dev.lions.unionflow.server.api.enums.organisation.TypeOrganisation;
|
||||
import dev.lions.unionflow.server.entity.Organisation;
|
||||
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.ws.rs.NotFoundException;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
/**
|
||||
* Service métier pour la gestion des organisations
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 1.0
|
||||
* @since 2025-01-15
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class OrganisationService {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(OrganisationService.class);
|
||||
|
||||
@Inject OrganisationRepository organisationRepository;
|
||||
|
||||
/**
|
||||
* Crée une nouvelle organisation
|
||||
*
|
||||
* @param organisation l'organisation à créer
|
||||
* @return l'organisation créée
|
||||
*/
|
||||
@Transactional
|
||||
public Organisation creerOrganisation(Organisation organisation) {
|
||||
LOG.infof("Création d'une nouvelle organisation: %s", organisation.getNom());
|
||||
|
||||
// Vérifier l'unicité de l'email
|
||||
if (organisationRepository.findByEmail(organisation.getEmail()).isPresent()) {
|
||||
throw new IllegalArgumentException("Une organisation avec cet email existe déjà");
|
||||
}
|
||||
|
||||
// Vérifier l'unicité du nom
|
||||
if (organisationRepository.findByNom(organisation.getNom()).isPresent()) {
|
||||
throw new IllegalArgumentException("Une organisation avec ce nom existe déjà");
|
||||
}
|
||||
|
||||
// Vérifier l'unicité du numéro d'enregistrement si fourni
|
||||
if (organisation.getNumeroEnregistrement() != null
|
||||
&& !organisation.getNumeroEnregistrement().isEmpty()) {
|
||||
if (organisationRepository
|
||||
.findByNumeroEnregistrement(organisation.getNumeroEnregistrement())
|
||||
.isPresent()) {
|
||||
throw new IllegalArgumentException(
|
||||
"Une organisation avec ce numéro d'enregistrement existe déjà");
|
||||
}
|
||||
}
|
||||
|
||||
// Définir les valeurs par défaut
|
||||
if (organisation.getStatut() == null) {
|
||||
organisation.setStatut("ACTIVE");
|
||||
}
|
||||
if (organisation.getTypeOrganisation() == null) {
|
||||
organisation.setTypeOrganisation("ASSOCIATION");
|
||||
}
|
||||
|
||||
organisationRepository.persist(organisation);
|
||||
LOG.infof(
|
||||
"Organisation créée avec succès: ID=%s, Nom=%s", organisation.getId(), organisation.getNom());
|
||||
|
||||
return organisation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour une organisation existante
|
||||
*
|
||||
* @param id l'ID de l'organisation
|
||||
* @param organisationMiseAJour les données de mise à jour
|
||||
* @param utilisateur l'utilisateur effectuant la modification
|
||||
* @return l'organisation mise à jour
|
||||
*/
|
||||
@Transactional
|
||||
public Organisation mettreAJourOrganisation(
|
||||
UUID id, Organisation organisationMiseAJour, String utilisateur) {
|
||||
LOG.infof("Mise à jour de l'organisation ID: %s", id);
|
||||
|
||||
Organisation organisation =
|
||||
organisationRepository
|
||||
.findByIdOptional(id)
|
||||
.orElseThrow(() -> new NotFoundException("Organisation non trouvée avec l'ID: " + id));
|
||||
|
||||
// Vérifier l'unicité de l'email si modifié
|
||||
if (!organisation.getEmail().equals(organisationMiseAJour.getEmail())) {
|
||||
if (organisationRepository.findByEmail(organisationMiseAJour.getEmail()).isPresent()) {
|
||||
throw new IllegalArgumentException("Une organisation avec cet email existe déjà");
|
||||
}
|
||||
organisation.setEmail(organisationMiseAJour.getEmail());
|
||||
}
|
||||
|
||||
// Vérifier l'unicité du nom si modifié
|
||||
if (!organisation.getNom().equals(organisationMiseAJour.getNom())) {
|
||||
if (organisationRepository.findByNom(organisationMiseAJour.getNom()).isPresent()) {
|
||||
throw new IllegalArgumentException("Une organisation avec ce nom existe déjà");
|
||||
}
|
||||
organisation.setNom(organisationMiseAJour.getNom());
|
||||
}
|
||||
|
||||
// Mettre à jour les autres champs
|
||||
organisation.setNomCourt(organisationMiseAJour.getNomCourt());
|
||||
organisation.setDescription(organisationMiseAJour.getDescription());
|
||||
organisation.setTelephone(organisationMiseAJour.getTelephone());
|
||||
organisation.setAdresse(organisationMiseAJour.getAdresse());
|
||||
organisation.setVille(organisationMiseAJour.getVille());
|
||||
organisation.setCodePostal(organisationMiseAJour.getCodePostal());
|
||||
organisation.setRegion(organisationMiseAJour.getRegion());
|
||||
organisation.setPays(organisationMiseAJour.getPays());
|
||||
organisation.setSiteWeb(organisationMiseAJour.getSiteWeb());
|
||||
organisation.setObjectifs(organisationMiseAJour.getObjectifs());
|
||||
organisation.setActivitesPrincipales(organisationMiseAJour.getActivitesPrincipales());
|
||||
|
||||
organisation.marquerCommeModifie(utilisateur);
|
||||
|
||||
LOG.infof("Organisation mise à jour avec succès: ID=%s", id);
|
||||
return organisation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime une organisation
|
||||
*
|
||||
* @param id l'UUID de l'organisation
|
||||
* @param utilisateur l'utilisateur effectuant la suppression
|
||||
*/
|
||||
@Transactional
|
||||
public void supprimerOrganisation(UUID id, String utilisateur) {
|
||||
LOG.infof("Suppression de l'organisation ID: %s", id);
|
||||
|
||||
Organisation organisation =
|
||||
organisationRepository
|
||||
.findByIdOptional(id)
|
||||
.orElseThrow(() -> new NotFoundException("Organisation non trouvée avec l'ID: " + id));
|
||||
|
||||
// Vérifier qu'il n'y a pas de membres actifs
|
||||
if (organisation.getNombreMembres() > 0) {
|
||||
throw new IllegalStateException(
|
||||
"Impossible de supprimer une organisation avec des membres actifs");
|
||||
}
|
||||
|
||||
// Soft delete - marquer comme inactive
|
||||
organisation.setActif(false);
|
||||
organisation.setStatut("DISSOUTE");
|
||||
organisation.marquerCommeModifie(utilisateur);
|
||||
|
||||
LOG.infof("Organisation supprimée (soft delete) avec succès: ID=%s", id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve une organisation par son ID
|
||||
*
|
||||
* @param id l'UUID de l'organisation
|
||||
* @return Optional contenant l'organisation si trouvée
|
||||
*/
|
||||
public Optional<Organisation> trouverParId(UUID id) {
|
||||
return organisationRepository.findByIdOptional(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve une organisation par son email
|
||||
*
|
||||
* @param email l'email de l'organisation
|
||||
* @return Optional contenant l'organisation si trouvée
|
||||
*/
|
||||
public Optional<Organisation> trouverParEmail(String email) {
|
||||
return organisationRepository.findByEmail(email);
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste toutes les organisations actives
|
||||
*
|
||||
* @return liste des organisations actives
|
||||
*/
|
||||
public List<Organisation> listerOrganisationsActives() {
|
||||
return organisationRepository.findAllActives();
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste toutes les organisations actives avec pagination
|
||||
*
|
||||
* @param page numéro de page
|
||||
* @param size taille de la page
|
||||
* @return liste paginée des organisations actives
|
||||
*/
|
||||
public List<Organisation> listerOrganisationsActives(int page, int size) {
|
||||
return organisationRepository.findAllActives(Page.of(page, size), Sort.by("nom").ascending());
|
||||
}
|
||||
|
||||
/**
|
||||
* Recherche d'organisations par nom
|
||||
*
|
||||
* @param recherche terme de recherche
|
||||
* @param page numéro de page
|
||||
* @param size taille de la page
|
||||
* @return liste paginée des organisations correspondantes
|
||||
*/
|
||||
public List<Organisation> rechercherOrganisations(String recherche, int page, int size) {
|
||||
return organisationRepository.findByNomOrNomCourt(
|
||||
recherche, Page.of(page, size), Sort.by("nom").ascending());
|
||||
}
|
||||
|
||||
/**
|
||||
* Recherche avancée d'organisations
|
||||
*
|
||||
* @param nom nom (optionnel)
|
||||
* @param typeOrganisation type (optionnel)
|
||||
* @param statut statut (optionnel)
|
||||
* @param ville ville (optionnel)
|
||||
* @param region région (optionnel)
|
||||
* @param pays pays (optionnel)
|
||||
* @param page numéro de page
|
||||
* @param size taille de la page
|
||||
* @return liste filtrée des organisations
|
||||
*/
|
||||
public List<Organisation> rechercheAvancee(
|
||||
String nom,
|
||||
String typeOrganisation,
|
||||
String statut,
|
||||
String ville,
|
||||
String region,
|
||||
String pays,
|
||||
int page,
|
||||
int size) {
|
||||
return organisationRepository.rechercheAvancee(
|
||||
nom, typeOrganisation, statut, ville, region, pays, Page.of(page, size));
|
||||
}
|
||||
|
||||
/**
|
||||
* Active une organisation
|
||||
*
|
||||
* @param id l'ID de l'organisation
|
||||
* @param utilisateur l'utilisateur effectuant l'activation
|
||||
* @return l'organisation activée
|
||||
*/
|
||||
@Transactional
|
||||
public Organisation activerOrganisation(UUID id, String utilisateur) {
|
||||
LOG.infof("Activation de l'organisation ID: %s", id);
|
||||
|
||||
Organisation organisation =
|
||||
organisationRepository
|
||||
.findByIdOptional(id)
|
||||
.orElseThrow(() -> new NotFoundException("Organisation non trouvée avec l'ID: " + id));
|
||||
|
||||
organisation.activer(utilisateur);
|
||||
|
||||
LOG.infof("Organisation activée avec succès: ID=%s", id);
|
||||
return organisation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Suspend une organisation
|
||||
*
|
||||
* @param id l'UUID de l'organisation
|
||||
* @param utilisateur l'utilisateur effectuant la suspension
|
||||
* @return l'organisation suspendue
|
||||
*/
|
||||
@Transactional
|
||||
public Organisation suspendreOrganisation(UUID id, String utilisateur) {
|
||||
LOG.infof("Suspension de l'organisation ID: %s", id);
|
||||
|
||||
Organisation organisation =
|
||||
organisationRepository
|
||||
.findByIdOptional(id)
|
||||
.orElseThrow(() -> new NotFoundException("Organisation non trouvée avec l'ID: " + id));
|
||||
|
||||
organisation.suspendre(utilisateur);
|
||||
|
||||
LOG.infof("Organisation suspendue avec succès: ID=%s", id);
|
||||
return organisation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtient les statistiques des organisations
|
||||
*
|
||||
* @return map contenant les statistiques
|
||||
*/
|
||||
public Map<String, Object> obtenirStatistiques() {
|
||||
LOG.info("Calcul des statistiques des organisations");
|
||||
|
||||
long totalOrganisations = organisationRepository.count();
|
||||
long organisationsActives = organisationRepository.countActives();
|
||||
long organisationsInactives = totalOrganisations - organisationsActives;
|
||||
long nouvellesOrganisations30Jours =
|
||||
organisationRepository.countNouvellesOrganisations(LocalDate.now().minusDays(30));
|
||||
|
||||
return Map.of(
|
||||
"totalOrganisations", totalOrganisations,
|
||||
"organisationsActives", organisationsActives,
|
||||
"organisationsInactives", organisationsInactives,
|
||||
"nouvellesOrganisations30Jours", nouvellesOrganisations30Jours,
|
||||
"tauxActivite",
|
||||
totalOrganisations > 0 ? (organisationsActives * 100.0 / totalOrganisations) : 0.0,
|
||||
"timestamp", LocalDateTime.now());
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit une entité Organisation en DTO
|
||||
*
|
||||
* @param organisation l'entité à convertir
|
||||
* @return le DTO correspondant
|
||||
*/
|
||||
public OrganisationDTO convertToDTO(Organisation organisation) {
|
||||
if (organisation == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
OrganisationDTO dto = new OrganisationDTO();
|
||||
|
||||
// Conversion de l'ID UUID vers UUID (pas de conversion nécessaire maintenant)
|
||||
dto.setId(organisation.getId());
|
||||
|
||||
// Informations de base
|
||||
dto.setNom(organisation.getNom());
|
||||
dto.setNomCourt(organisation.getNomCourt());
|
||||
dto.setDescription(organisation.getDescription());
|
||||
dto.setEmail(organisation.getEmail());
|
||||
dto.setTelephone(organisation.getTelephone());
|
||||
dto.setTelephoneSecondaire(organisation.getTelephoneSecondaire());
|
||||
dto.setEmailSecondaire(organisation.getEmailSecondaire());
|
||||
dto.setAdresse(organisation.getAdresse());
|
||||
dto.setVille(organisation.getVille());
|
||||
dto.setCodePostal(organisation.getCodePostal());
|
||||
dto.setRegion(organisation.getRegion());
|
||||
dto.setPays(organisation.getPays());
|
||||
dto.setLatitude(organisation.getLatitude());
|
||||
dto.setLongitude(organisation.getLongitude());
|
||||
dto.setSiteWeb(organisation.getSiteWeb());
|
||||
dto.setLogo(organisation.getLogo());
|
||||
dto.setReseauxSociaux(organisation.getReseauxSociaux());
|
||||
dto.setObjectifs(organisation.getObjectifs());
|
||||
dto.setActivitesPrincipales(organisation.getActivitesPrincipales());
|
||||
dto.setNombreMembres(organisation.getNombreMembres());
|
||||
dto.setNombreAdministrateurs(organisation.getNombreAdministrateurs());
|
||||
dto.setBudgetAnnuel(organisation.getBudgetAnnuel());
|
||||
dto.setDevise(organisation.getDevise());
|
||||
dto.setDateFondation(organisation.getDateFondation());
|
||||
dto.setNumeroEnregistrement(organisation.getNumeroEnregistrement());
|
||||
dto.setNiveauHierarchique(organisation.getNiveauHierarchique());
|
||||
|
||||
// Conversion de l'organisation parente (UUID → UUID, pas de conversion nécessaire)
|
||||
if (organisation.getOrganisationParenteId() != null) {
|
||||
dto.setOrganisationParenteId(organisation.getOrganisationParenteId());
|
||||
}
|
||||
|
||||
// Conversion du type d'organisation (String → Enum)
|
||||
if (organisation.getTypeOrganisation() != null) {
|
||||
try {
|
||||
dto.setTypeOrganisation(
|
||||
TypeOrganisation.valueOf(organisation.getTypeOrganisation().toUpperCase()));
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Valeur par défaut si la conversion échoue
|
||||
LOG.warnf(
|
||||
"Type d'organisation inconnu: %s, utilisation de ASSOCIATION par défaut",
|
||||
organisation.getTypeOrganisation());
|
||||
dto.setTypeOrganisation(TypeOrganisation.ASSOCIATION);
|
||||
}
|
||||
} else {
|
||||
dto.setTypeOrganisation(TypeOrganisation.ASSOCIATION);
|
||||
}
|
||||
|
||||
// Conversion du statut (String → Enum)
|
||||
if (organisation.getStatut() != null) {
|
||||
try {
|
||||
dto.setStatut(
|
||||
StatutOrganisation.valueOf(organisation.getStatut().toUpperCase()));
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Valeur par défaut si la conversion échoue
|
||||
LOG.warnf(
|
||||
"Statut d'organisation inconnu: %s, utilisation de ACTIVE par défaut",
|
||||
organisation.getStatut());
|
||||
dto.setStatut(StatutOrganisation.ACTIVE);
|
||||
}
|
||||
} else {
|
||||
dto.setStatut(StatutOrganisation.ACTIVE);
|
||||
}
|
||||
|
||||
// Champs de base DTO
|
||||
dto.setDateCreation(organisation.getDateCreation());
|
||||
dto.setDateModification(organisation.getDateModification());
|
||||
dto.setActif(organisation.getActif());
|
||||
dto.setVersion(organisation.getVersion() != null ? organisation.getVersion() : 0L);
|
||||
|
||||
// Champs par défaut
|
||||
dto.setOrganisationPublique(
|
||||
organisation.getOrganisationPublique() != null
|
||||
? organisation.getOrganisationPublique()
|
||||
: true);
|
||||
dto.setAccepteNouveauxMembres(
|
||||
organisation.getAccepteNouveauxMembres() != null
|
||||
? organisation.getAccepteNouveauxMembres()
|
||||
: true);
|
||||
dto.setCotisationObligatoire(
|
||||
organisation.getCotisationObligatoire() != null
|
||||
? organisation.getCotisationObligatoire()
|
||||
: false);
|
||||
dto.setMontantCotisationAnnuelle(organisation.getMontantCotisationAnnuelle());
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit un DTO en entité Organisation
|
||||
*
|
||||
* @param dto le DTO à convertir
|
||||
* @return l'entité correspondante
|
||||
*/
|
||||
public Organisation convertFromDTO(OrganisationDTO dto) {
|
||||
if (dto == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Organisation.builder()
|
||||
.nom(dto.getNom())
|
||||
.nomCourt(dto.getNomCourt())
|
||||
.description(dto.getDescription())
|
||||
.email(dto.getEmail())
|
||||
.telephone(dto.getTelephone())
|
||||
.adresse(dto.getAdresse())
|
||||
.ville(dto.getVille())
|
||||
.codePostal(dto.getCodePostal())
|
||||
.region(dto.getRegion())
|
||||
.pays(dto.getPays())
|
||||
.siteWeb(dto.getSiteWeb())
|
||||
.objectifs(dto.getObjectifs())
|
||||
.activitesPrincipales(dto.getActivitesPrincipales())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
package dev.lions.unionflow.server.service;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.paiement.PaiementDTO;
|
||||
import dev.lions.unionflow.server.api.enums.paiement.StatutPaiement;
|
||||
import dev.lions.unionflow.server.entity.Membre;
|
||||
import dev.lions.unionflow.server.entity.Paiement;
|
||||
import dev.lions.unionflow.server.repository.MembreRepository;
|
||||
import dev.lions.unionflow.server.repository.PaiementRepository;
|
||||
import dev.lions.unionflow.server.service.KeycloakService;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.ws.rs.NotFoundException;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
/**
|
||||
* Service métier pour la gestion des paiements
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 3.0
|
||||
* @since 2025-01-29
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class PaiementService {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(PaiementService.class);
|
||||
|
||||
@Inject PaiementRepository paiementRepository;
|
||||
|
||||
@Inject MembreRepository membreRepository;
|
||||
|
||||
@Inject KeycloakService keycloakService;
|
||||
|
||||
/**
|
||||
* Crée un nouveau paiement
|
||||
*
|
||||
* @param paiementDTO DTO du paiement à créer
|
||||
* @return DTO du paiement créé
|
||||
*/
|
||||
@Transactional
|
||||
public PaiementDTO creerPaiement(PaiementDTO paiementDTO) {
|
||||
LOG.infof("Création d'un nouveau paiement: %s", paiementDTO.getNumeroReference());
|
||||
|
||||
Paiement paiement = convertToEntity(paiementDTO);
|
||||
paiement.setCreePar(keycloakService.getCurrentUserEmail());
|
||||
|
||||
paiementRepository.persist(paiement);
|
||||
LOG.infof("Paiement créé avec succès: ID=%s, Référence=%s", paiement.getId(), paiement.getNumeroReference());
|
||||
|
||||
return convertToDTO(paiement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour un paiement existant
|
||||
*
|
||||
* @param id ID du paiement
|
||||
* @param paiementDTO DTO avec les modifications
|
||||
* @return DTO du paiement mis à jour
|
||||
*/
|
||||
@Transactional
|
||||
public PaiementDTO mettreAJourPaiement(UUID id, PaiementDTO paiementDTO) {
|
||||
LOG.infof("Mise à jour du paiement ID: %s", id);
|
||||
|
||||
Paiement paiement =
|
||||
paiementRepository
|
||||
.findPaiementById(id)
|
||||
.orElseThrow(() -> new NotFoundException("Paiement non trouvé avec l'ID: " + id));
|
||||
|
||||
if (!paiement.peutEtreModifie()) {
|
||||
throw new IllegalStateException("Le paiement ne peut plus être modifié (statut finalisé)");
|
||||
}
|
||||
|
||||
updateFromDTO(paiement, paiementDTO);
|
||||
paiement.setModifiePar(keycloakService.getCurrentUserEmail());
|
||||
|
||||
paiementRepository.persist(paiement);
|
||||
LOG.infof("Paiement mis à jour avec succès: ID=%s", id);
|
||||
|
||||
return convertToDTO(paiement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Valide un paiement
|
||||
*
|
||||
* @param id ID du paiement
|
||||
* @return DTO du paiement validé
|
||||
*/
|
||||
@Transactional
|
||||
public PaiementDTO validerPaiement(UUID id) {
|
||||
LOG.infof("Validation du paiement ID: %s", id);
|
||||
|
||||
Paiement paiement =
|
||||
paiementRepository
|
||||
.findPaiementById(id)
|
||||
.orElseThrow(() -> new NotFoundException("Paiement non trouvé avec l'ID: " + id));
|
||||
|
||||
if (paiement.isValide()) {
|
||||
LOG.warnf("Le paiement ID=%s est déjà validé", id);
|
||||
return convertToDTO(paiement);
|
||||
}
|
||||
|
||||
paiement.setStatutPaiement(StatutPaiement.VALIDE);
|
||||
paiement.setDateValidation(LocalDateTime.now());
|
||||
paiement.setValidateur(keycloakService.getCurrentUserEmail());
|
||||
paiement.setModifiePar(keycloakService.getCurrentUserEmail());
|
||||
|
||||
paiementRepository.persist(paiement);
|
||||
LOG.infof("Paiement validé avec succès: ID=%s", id);
|
||||
|
||||
return convertToDTO(paiement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Annule un paiement
|
||||
*
|
||||
* @param id ID du paiement
|
||||
* @return DTO du paiement annulé
|
||||
*/
|
||||
@Transactional
|
||||
public PaiementDTO annulerPaiement(UUID id) {
|
||||
LOG.infof("Annulation du paiement ID: %s", id);
|
||||
|
||||
Paiement paiement =
|
||||
paiementRepository
|
||||
.findPaiementById(id)
|
||||
.orElseThrow(() -> new NotFoundException("Paiement non trouvé avec l'ID: " + id));
|
||||
|
||||
if (!paiement.peutEtreModifie()) {
|
||||
throw new IllegalStateException("Le paiement ne peut plus être annulé (statut finalisé)");
|
||||
}
|
||||
|
||||
paiement.setStatutPaiement(StatutPaiement.ANNULE);
|
||||
paiement.setModifiePar(keycloakService.getCurrentUserEmail());
|
||||
|
||||
paiementRepository.persist(paiement);
|
||||
LOG.infof("Paiement annulé avec succès: ID=%s", id);
|
||||
|
||||
return convertToDTO(paiement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve un paiement par son ID
|
||||
*
|
||||
* @param id ID du paiement
|
||||
* @return DTO du paiement
|
||||
*/
|
||||
public PaiementDTO trouverParId(UUID id) {
|
||||
return paiementRepository
|
||||
.findPaiementById(id)
|
||||
.map(this::convertToDTO)
|
||||
.orElseThrow(() -> new NotFoundException("Paiement non trouvé avec l'ID: " + id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve un paiement par son numéro de référence
|
||||
*
|
||||
* @param numeroReference Numéro de référence
|
||||
* @return DTO du paiement
|
||||
*/
|
||||
public PaiementDTO trouverParNumeroReference(String numeroReference) {
|
||||
return paiementRepository
|
||||
.findByNumeroReference(numeroReference)
|
||||
.map(this::convertToDTO)
|
||||
.orElseThrow(() -> new NotFoundException("Paiement non trouvé avec la référence: " + numeroReference));
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste tous les paiements d'un membre
|
||||
*
|
||||
* @param membreId ID du membre
|
||||
* @return Liste des paiements
|
||||
*/
|
||||
public List<PaiementDTO> listerParMembre(UUID membreId) {
|
||||
return paiementRepository.findByMembreId(membreId).stream()
|
||||
.map(this::convertToDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule le montant total des paiements validés dans une période
|
||||
*
|
||||
* @param dateDebut Date de début
|
||||
* @param dateFin Date de fin
|
||||
* @return Montant total
|
||||
*/
|
||||
public BigDecimal calculerMontantTotalValides(LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||
return paiementRepository.calculerMontantTotalValides(dateDebut, dateFin);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// MÉTHODES PRIVÉES
|
||||
// ========================================
|
||||
|
||||
/** Convertit une entité en DTO */
|
||||
private PaiementDTO convertToDTO(Paiement paiement) {
|
||||
if (paiement == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
PaiementDTO dto = new PaiementDTO();
|
||||
dto.setId(paiement.getId());
|
||||
dto.setNumeroReference(paiement.getNumeroReference());
|
||||
dto.setMontant(paiement.getMontant());
|
||||
dto.setCodeDevise(paiement.getCodeDevise());
|
||||
dto.setMethodePaiement(paiement.getMethodePaiement());
|
||||
dto.setStatutPaiement(paiement.getStatutPaiement());
|
||||
dto.setDatePaiement(paiement.getDatePaiement());
|
||||
dto.setDateValidation(paiement.getDateValidation());
|
||||
dto.setValidateur(paiement.getValidateur());
|
||||
dto.setReferenceExterne(paiement.getReferenceExterne());
|
||||
dto.setUrlPreuve(paiement.getUrlPreuve());
|
||||
dto.setCommentaire(paiement.getCommentaire());
|
||||
dto.setIpAddress(paiement.getIpAddress());
|
||||
dto.setUserAgent(paiement.getUserAgent());
|
||||
|
||||
if (paiement.getMembre() != null) {
|
||||
dto.setMembreId(paiement.getMembre().getId());
|
||||
}
|
||||
if (paiement.getTransactionWave() != null) {
|
||||
dto.setTransactionWaveId(paiement.getTransactionWave().getId());
|
||||
}
|
||||
|
||||
dto.setDateCreation(paiement.getDateCreation());
|
||||
dto.setDateModification(paiement.getDateModification());
|
||||
dto.setActif(paiement.getActif());
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
/** Convertit un DTO en entité */
|
||||
private Paiement convertToEntity(PaiementDTO dto) {
|
||||
if (dto == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Paiement paiement = new Paiement();
|
||||
paiement.setNumeroReference(dto.getNumeroReference());
|
||||
paiement.setMontant(dto.getMontant());
|
||||
paiement.setCodeDevise(dto.getCodeDevise());
|
||||
paiement.setMethodePaiement(dto.getMethodePaiement());
|
||||
paiement.setStatutPaiement(dto.getStatutPaiement() != null ? dto.getStatutPaiement() : StatutPaiement.EN_ATTENTE);
|
||||
paiement.setDatePaiement(dto.getDatePaiement());
|
||||
paiement.setDateValidation(dto.getDateValidation());
|
||||
paiement.setValidateur(dto.getValidateur());
|
||||
paiement.setReferenceExterne(dto.getReferenceExterne());
|
||||
paiement.setUrlPreuve(dto.getUrlPreuve());
|
||||
paiement.setCommentaire(dto.getCommentaire());
|
||||
paiement.setIpAddress(dto.getIpAddress());
|
||||
paiement.setUserAgent(dto.getUserAgent());
|
||||
|
||||
// Relation Membre
|
||||
if (dto.getMembreId() != null) {
|
||||
Membre membre =
|
||||
membreRepository
|
||||
.findByIdOptional(dto.getMembreId())
|
||||
.orElseThrow(() -> new NotFoundException("Membre non trouvé avec l'ID: " + dto.getMembreId()));
|
||||
paiement.setMembre(membre);
|
||||
}
|
||||
|
||||
// Relation TransactionWave sera gérée par WaveService
|
||||
|
||||
return paiement;
|
||||
}
|
||||
|
||||
/** Met à jour une entité à partir d'un DTO */
|
||||
private void updateFromDTO(Paiement paiement, PaiementDTO dto) {
|
||||
if (dto.getMontant() != null) {
|
||||
paiement.setMontant(dto.getMontant());
|
||||
}
|
||||
if (dto.getCodeDevise() != null) {
|
||||
paiement.setCodeDevise(dto.getCodeDevise());
|
||||
}
|
||||
if (dto.getMethodePaiement() != null) {
|
||||
paiement.setMethodePaiement(dto.getMethodePaiement());
|
||||
}
|
||||
if (dto.getStatutPaiement() != null) {
|
||||
paiement.setStatutPaiement(dto.getStatutPaiement());
|
||||
}
|
||||
if (dto.getDatePaiement() != null) {
|
||||
paiement.setDatePaiement(dto.getDatePaiement());
|
||||
}
|
||||
if (dto.getDateValidation() != null) {
|
||||
paiement.setDateValidation(dto.getDateValidation());
|
||||
}
|
||||
if (dto.getValidateur() != null) {
|
||||
paiement.setValidateur(dto.getValidateur());
|
||||
}
|
||||
if (dto.getReferenceExterne() != null) {
|
||||
paiement.setReferenceExterne(dto.getReferenceExterne());
|
||||
}
|
||||
if (dto.getUrlPreuve() != null) {
|
||||
paiement.setUrlPreuve(dto.getUrlPreuve());
|
||||
}
|
||||
if (dto.getCommentaire() != null) {
|
||||
paiement.setCommentaire(dto.getCommentaire());
|
||||
}
|
||||
if (dto.getIpAddress() != null) {
|
||||
paiement.setIpAddress(dto.getIpAddress());
|
||||
}
|
||||
if (dto.getUserAgent() != null) {
|
||||
paiement.setUserAgent(dto.getUserAgent());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package dev.lions.unionflow.server.service;
|
||||
|
||||
import dev.lions.unionflow.server.entity.Permission;
|
||||
import dev.lions.unionflow.server.repository.PermissionRepository;
|
||||
import dev.lions.unionflow.server.service.KeycloakService;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.ws.rs.NotFoundException;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
/**
|
||||
* Service métier pour la gestion des permissions
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 3.0
|
||||
* @since 2025-01-29
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class PermissionService {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(PermissionService.class);
|
||||
|
||||
@Inject PermissionRepository permissionRepository;
|
||||
|
||||
@Inject KeycloakService keycloakService;
|
||||
|
||||
/**
|
||||
* Crée une nouvelle permission
|
||||
*
|
||||
* @param permission Permission à créer
|
||||
* @return Permission créée
|
||||
*/
|
||||
@Transactional
|
||||
public Permission creerPermission(Permission permission) {
|
||||
LOG.infof("Création d'une nouvelle permission: %s", permission.getCode());
|
||||
|
||||
// Vérifier l'unicité du code
|
||||
if (permissionRepository.findByCode(permission.getCode()).isPresent()) {
|
||||
throw new IllegalArgumentException(
|
||||
"Une permission avec ce code existe déjà: " + permission.getCode());
|
||||
}
|
||||
|
||||
// Générer le code si non fourni
|
||||
if (permission.getCode() == null || permission.getCode().isEmpty()) {
|
||||
permission.setCode(
|
||||
Permission.genererCode(
|
||||
permission.getModule(), permission.getRessource(), permission.getAction()));
|
||||
}
|
||||
|
||||
// Métadonnées
|
||||
permission.setCreePar(keycloakService.getCurrentUserEmail());
|
||||
|
||||
permissionRepository.persist(permission);
|
||||
LOG.infof(
|
||||
"Permission créée avec succès: ID=%s, Code=%s",
|
||||
permission.getId(), permission.getCode());
|
||||
|
||||
return permission;
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour une permission existante
|
||||
*
|
||||
* @param id ID de la permission
|
||||
* @param permissionModifiee Permission avec les modifications
|
||||
* @return Permission mise à jour
|
||||
*/
|
||||
@Transactional
|
||||
public Permission mettreAJourPermission(UUID id, Permission permissionModifiee) {
|
||||
LOG.infof("Mise à jour de la permission ID: %s", id);
|
||||
|
||||
Permission permission =
|
||||
permissionRepository
|
||||
.findPermissionById(id)
|
||||
.orElseThrow(() -> new NotFoundException("Permission non trouvée avec l'ID: " + id));
|
||||
|
||||
// Mise à jour
|
||||
permission.setCode(permissionModifiee.getCode());
|
||||
permission.setModule(permissionModifiee.getModule());
|
||||
permission.setRessource(permissionModifiee.getRessource());
|
||||
permission.setAction(permissionModifiee.getAction());
|
||||
permission.setLibelle(permissionModifiee.getLibelle());
|
||||
permission.setDescription(permissionModifiee.getDescription());
|
||||
permission.setModifiePar(keycloakService.getCurrentUserEmail());
|
||||
|
||||
permissionRepository.persist(permission);
|
||||
LOG.infof("Permission mise à jour avec succès: ID=%s", id);
|
||||
|
||||
return permission;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve une permission par son ID
|
||||
*
|
||||
* @param id ID de la permission
|
||||
* @return Permission ou null
|
||||
*/
|
||||
public Permission trouverParId(UUID id) {
|
||||
return permissionRepository.findPermissionById(id).orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve une permission par son code
|
||||
*
|
||||
* @param code Code de la permission
|
||||
* @return Permission ou null
|
||||
*/
|
||||
public Permission trouverParCode(String code) {
|
||||
return permissionRepository.findByCode(code).orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste les permissions par module
|
||||
*
|
||||
* @param module Nom du module
|
||||
* @return Liste des permissions
|
||||
*/
|
||||
public List<Permission> listerParModule(String module) {
|
||||
return permissionRepository.findByModule(module);
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste les permissions par ressource
|
||||
*
|
||||
* @param ressource Nom de la ressource
|
||||
* @return Liste des permissions
|
||||
*/
|
||||
public List<Permission> listerParRessource(String ressource) {
|
||||
return permissionRepository.findByRessource(ressource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste toutes les permissions actives
|
||||
*
|
||||
* @return Liste des permissions actives
|
||||
*/
|
||||
public List<Permission> listerToutesActives() {
|
||||
return permissionRepository.findAllActives();
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime (désactive) une permission
|
||||
*
|
||||
* @param id ID de la permission
|
||||
*/
|
||||
@Transactional
|
||||
public void supprimerPermission(UUID id) {
|
||||
LOG.infof("Suppression de la permission ID: %s", id);
|
||||
|
||||
Permission permission =
|
||||
permissionRepository
|
||||
.findPermissionById(id)
|
||||
.orElseThrow(() -> new NotFoundException("Permission non trouvée avec l'ID: " + id));
|
||||
|
||||
permission.setActif(false);
|
||||
permission.setModifiePar(keycloakService.getCurrentUserEmail());
|
||||
|
||||
permissionRepository.persist(permission);
|
||||
LOG.infof("Permission supprimée avec succès: ID=%s", id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
package dev.lions.unionflow.server.service;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
/** Service pour gérer les préférences de notification des utilisateurs */
|
||||
@ApplicationScoped
|
||||
public class PreferencesNotificationService {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(PreferencesNotificationService.class);
|
||||
|
||||
// Stockage temporaire en mémoire (à remplacer par une base de données)
|
||||
private final Map<UUID, Map<String, Boolean>> preferencesUtilisateurs = new HashMap<>();
|
||||
|
||||
/** Obtient les préférences de notification d'un utilisateur */
|
||||
public Map<String, Boolean> obtenirPreferences(UUID utilisateurId) {
|
||||
LOG.infof("Récupération des préférences de notification pour l'utilisateur %s", utilisateurId);
|
||||
|
||||
return preferencesUtilisateurs.getOrDefault(utilisateurId, getPreferencesParDefaut());
|
||||
}
|
||||
|
||||
/** Met à jour les préférences de notification d'un utilisateur */
|
||||
public void mettreAJourPreferences(UUID utilisateurId, Map<String, Boolean> preferences) {
|
||||
LOG.infof("Mise à jour des préférences de notification pour l'utilisateur %s", utilisateurId);
|
||||
|
||||
preferencesUtilisateurs.put(utilisateurId, new HashMap<>(preferences));
|
||||
}
|
||||
|
||||
/** Vérifie si un utilisateur souhaite recevoir un type de notification */
|
||||
public boolean accepteNotification(UUID utilisateurId, String typeNotification) {
|
||||
Map<String, Boolean> preferences = obtenirPreferences(utilisateurId);
|
||||
return preferences.getOrDefault(typeNotification, true);
|
||||
}
|
||||
|
||||
/** Active un type de notification pour un utilisateur */
|
||||
public void activerNotification(UUID utilisateurId, String typeNotification) {
|
||||
LOG.infof(
|
||||
"Activation de la notification %s pour l'utilisateur %s", typeNotification, utilisateurId);
|
||||
|
||||
Map<String, Boolean> preferences = obtenirPreferences(utilisateurId);
|
||||
preferences.put(typeNotification, true);
|
||||
mettreAJourPreferences(utilisateurId, preferences);
|
||||
}
|
||||
|
||||
/** Désactive un type de notification pour un utilisateur */
|
||||
public void desactiverNotification(UUID utilisateurId, String typeNotification) {
|
||||
LOG.infof(
|
||||
"Désactivation de la notification %s pour l'utilisateur %s",
|
||||
typeNotification, utilisateurId);
|
||||
|
||||
Map<String, Boolean> preferences = obtenirPreferences(utilisateurId);
|
||||
preferences.put(typeNotification, false);
|
||||
mettreAJourPreferences(utilisateurId, preferences);
|
||||
}
|
||||
|
||||
/** Réinitialise les préférences d'un utilisateur aux valeurs par défaut */
|
||||
public void reinitialiserPreferences(UUID utilisateurId) {
|
||||
LOG.infof("Réinitialisation des préférences pour l'utilisateur %s", utilisateurId);
|
||||
|
||||
mettreAJourPreferences(utilisateurId, getPreferencesParDefaut());
|
||||
}
|
||||
|
||||
/** Obtient les préférences par défaut */
|
||||
private Map<String, Boolean> getPreferencesParDefaut() {
|
||||
Map<String, Boolean> preferences = new HashMap<>();
|
||||
|
||||
// Notifications générales
|
||||
preferences.put("NOUVELLE_COTISATION", true);
|
||||
preferences.put("RAPPEL_COTISATION", true);
|
||||
preferences.put("COTISATION_RETARD", true);
|
||||
|
||||
// Notifications d'événements
|
||||
preferences.put("NOUVEL_EVENEMENT", true);
|
||||
preferences.put("RAPPEL_EVENEMENT", true);
|
||||
preferences.put("MODIFICATION_EVENEMENT", true);
|
||||
preferences.put("ANNULATION_EVENEMENT", true);
|
||||
|
||||
// Notifications de solidarité
|
||||
preferences.put("NOUVELLE_DEMANDE_AIDE", true);
|
||||
preferences.put("DEMANDE_AIDE_APPROUVEE", true);
|
||||
preferences.put("DEMANDE_AIDE_REJETEE", true);
|
||||
preferences.put("NOUVELLE_PROPOSITION_AIDE", true);
|
||||
|
||||
// Notifications administratives
|
||||
preferences.put("NOUVEAU_MEMBRE", false);
|
||||
preferences.put("MODIFICATION_PROFIL", false);
|
||||
preferences.put("RAPPORT_MENSUEL", true);
|
||||
|
||||
// Notifications push
|
||||
preferences.put("PUSH_MOBILE", true);
|
||||
preferences.put("EMAIL", true);
|
||||
preferences.put("SMS", false);
|
||||
|
||||
return preferences;
|
||||
}
|
||||
|
||||
/** Obtient tous les utilisateurs qui acceptent un type de notification */
|
||||
public Map<UUID, Boolean> obtenirUtilisateursAcceptantNotification(String typeNotification) {
|
||||
LOG.infof("Recherche des utilisateurs acceptant la notification %s", typeNotification);
|
||||
|
||||
Map<UUID, Boolean> utilisateursAcceptant = new HashMap<>();
|
||||
|
||||
for (Map.Entry<UUID, Map<String, Boolean>> entry : preferencesUtilisateurs.entrySet()) {
|
||||
UUID utilisateurId = entry.getKey();
|
||||
Map<String, Boolean> preferences = entry.getValue();
|
||||
|
||||
if (preferences.getOrDefault(typeNotification, true)) {
|
||||
utilisateursAcceptant.put(utilisateurId, true);
|
||||
}
|
||||
}
|
||||
|
||||
return utilisateursAcceptant;
|
||||
}
|
||||
|
||||
/** Exporte les préférences d'un utilisateur */
|
||||
public Map<String, Object> exporterPreferences(UUID utilisateurId) {
|
||||
LOG.infof("Export des préférences pour l'utilisateur %s", utilisateurId);
|
||||
|
||||
Map<String, Object> export = new HashMap<>();
|
||||
export.put("utilisateurId", utilisateurId);
|
||||
export.put("preferences", obtenirPreferences(utilisateurId));
|
||||
export.put("dateExport", java.time.LocalDateTime.now());
|
||||
|
||||
return export;
|
||||
}
|
||||
|
||||
/** Importe les préférences d'un utilisateur */
|
||||
@SuppressWarnings("unchecked")
|
||||
public void importerPreferences(UUID utilisateurId, Map<String, Object> donnees) {
|
||||
LOG.infof("Import des préférences pour l'utilisateur %s", utilisateurId);
|
||||
|
||||
if (donnees.containsKey("preferences")) {
|
||||
Map<String, Boolean> preferences = (Map<String, Boolean>) donnees.get("preferences");
|
||||
mettreAJourPreferences(utilisateurId, preferences);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,442 @@
|
||||
package dev.lions.unionflow.server.service;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.solidarite.DemandeAideDTO;
|
||||
import dev.lions.unionflow.server.api.dto.solidarite.PropositionAideDTO;
|
||||
import dev.lions.unionflow.server.api.enums.solidarite.TypeAide;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
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 propositions d'aide
|
||||
*
|
||||
* <p>Ce service gère le cycle de vie des propositions d'aide : création, activation, matching,
|
||||
* suivi des performances.
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 1.0
|
||||
* @since 2025-01-16
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class PropositionAideService {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(PropositionAideService.class);
|
||||
|
||||
// Cache pour les propositions actives
|
||||
private final Map<String, PropositionAideDTO> cachePropositionsActives = new HashMap<>();
|
||||
private final Map<TypeAide, List<PropositionAideDTO>> indexParType = new HashMap<>();
|
||||
|
||||
// === OPÉRATIONS CRUD ===
|
||||
|
||||
/**
|
||||
* Crée une nouvelle proposition d'aide
|
||||
*
|
||||
* @param propositionDTO La proposition à créer
|
||||
* @return La proposition créée avec ID généré
|
||||
*/
|
||||
@Transactional
|
||||
public PropositionAideDTO creerProposition(@Valid PropositionAideDTO propositionDTO) {
|
||||
LOG.infof("Création d'une nouvelle proposition d'aide: %s", propositionDTO.getTitre());
|
||||
|
||||
// Génération des identifiants
|
||||
propositionDTO.setId(UUID.randomUUID().toString());
|
||||
propositionDTO.setNumeroReference(genererNumeroReference());
|
||||
|
||||
// Initialisation des dates
|
||||
LocalDateTime maintenant = LocalDateTime.now();
|
||||
propositionDTO.setDateCreation(maintenant);
|
||||
propositionDTO.setDateModification(maintenant);
|
||||
|
||||
// Statut initial
|
||||
if (propositionDTO.getStatut() == null) {
|
||||
propositionDTO.setStatut(PropositionAideDTO.StatutProposition.ACTIVE);
|
||||
}
|
||||
|
||||
// Calcul de la date d'expiration si non définie
|
||||
if (propositionDTO.getDateExpiration() == null) {
|
||||
propositionDTO.setDateExpiration(maintenant.plusMonths(6)); // 6 mois par défaut
|
||||
}
|
||||
|
||||
// Initialisation des compteurs
|
||||
propositionDTO.setNombreDemandesTraitees(0);
|
||||
propositionDTO.setNombreBeneficiairesAides(0);
|
||||
propositionDTO.setMontantTotalVerse(0.0);
|
||||
propositionDTO.setNombreVues(0);
|
||||
propositionDTO.setNombreCandidatures(0);
|
||||
propositionDTO.setNombreEvaluations(0);
|
||||
|
||||
// Calcul du score de pertinence initial
|
||||
propositionDTO.setScorePertinence(calculerScorePertinence(propositionDTO));
|
||||
|
||||
// Ajout au cache et index
|
||||
ajouterAuCache(propositionDTO);
|
||||
ajouterAIndex(propositionDTO);
|
||||
|
||||
LOG.infof("Proposition d'aide créée avec succès: %s", propositionDTO.getId());
|
||||
return propositionDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour une proposition d'aide existante
|
||||
*
|
||||
* @param propositionDTO La proposition à mettre à jour
|
||||
* @return La proposition mise à jour
|
||||
*/
|
||||
@Transactional
|
||||
public PropositionAideDTO mettreAJour(@Valid PropositionAideDTO propositionDTO) {
|
||||
LOG.infof("Mise à jour de la proposition d'aide: %s", propositionDTO.getId());
|
||||
|
||||
// Mise à jour de la date de modification
|
||||
propositionDTO.setDateModification(LocalDateTime.now());
|
||||
|
||||
// Recalcul du score de pertinence
|
||||
propositionDTO.setScorePertinence(calculerScorePertinence(propositionDTO));
|
||||
|
||||
// Mise à jour du cache et index
|
||||
ajouterAuCache(propositionDTO);
|
||||
mettreAJourIndex(propositionDTO);
|
||||
|
||||
LOG.infof("Proposition d'aide mise à jour avec succès: %s", propositionDTO.getId());
|
||||
return propositionDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtient une proposition d'aide par son ID
|
||||
*
|
||||
* @param id ID de la proposition
|
||||
* @return La proposition trouvée
|
||||
*/
|
||||
public PropositionAideDTO obtenirParId(@NotBlank String id) {
|
||||
LOG.debugf("Récupération de la proposition d'aide: %s", id);
|
||||
|
||||
// Vérification du cache
|
||||
PropositionAideDTO propositionCachee = cachePropositionsActives.get(id);
|
||||
if (propositionCachee != null) {
|
||||
// Incrémenter le nombre de vues
|
||||
propositionCachee.setNombreVues(propositionCachee.getNombreVues() + 1);
|
||||
return propositionCachee;
|
||||
}
|
||||
|
||||
// Simulation de récupération depuis la base de données
|
||||
PropositionAideDTO proposition = simulerRecuperationBDD(id);
|
||||
|
||||
if (proposition != null) {
|
||||
ajouterAuCache(proposition);
|
||||
ajouterAIndex(proposition);
|
||||
}
|
||||
|
||||
return proposition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Active ou désactive une proposition d'aide
|
||||
*
|
||||
* @param propositionId ID de la proposition
|
||||
* @param activer true pour activer, false pour désactiver
|
||||
* @return La proposition mise à jour
|
||||
*/
|
||||
@Transactional
|
||||
public PropositionAideDTO changerStatutActivation(
|
||||
@NotBlank String propositionId, boolean activer) {
|
||||
LOG.infof(
|
||||
"Changement de statut d'activation pour la proposition %s: %s",
|
||||
propositionId, activer ? "ACTIVE" : "SUSPENDUE");
|
||||
|
||||
PropositionAideDTO proposition = obtenirParId(propositionId);
|
||||
if (proposition == null) {
|
||||
throw new IllegalArgumentException("Proposition non trouvée: " + propositionId);
|
||||
}
|
||||
|
||||
if (activer) {
|
||||
// Vérifications avant activation
|
||||
if (proposition.isExpiree()) {
|
||||
throw new IllegalStateException("Impossible d'activer une proposition expirée");
|
||||
}
|
||||
proposition.setStatut(PropositionAideDTO.StatutProposition.ACTIVE);
|
||||
proposition.setEstDisponible(true);
|
||||
} else {
|
||||
proposition.setStatut(PropositionAideDTO.StatutProposition.SUSPENDUE);
|
||||
proposition.setEstDisponible(false);
|
||||
}
|
||||
|
||||
proposition.setDateModification(LocalDateTime.now());
|
||||
|
||||
// Mise à jour du cache et index
|
||||
ajouterAuCache(proposition);
|
||||
mettreAJourIndex(proposition);
|
||||
|
||||
return proposition;
|
||||
}
|
||||
|
||||
// === RECHERCHE ET MATCHING ===
|
||||
|
||||
/**
|
||||
* Recherche des propositions compatibles avec une demande
|
||||
*
|
||||
* @param demande La demande d'aide
|
||||
* @return Liste des propositions compatibles triées par score
|
||||
*/
|
||||
public List<PropositionAideDTO> rechercherPropositionsCompatibles(DemandeAideDTO demande) {
|
||||
LOG.debugf("Recherche de propositions compatibles pour la demande: %s", demande.getId());
|
||||
|
||||
// Recherche par type d'aide d'abord
|
||||
List<PropositionAideDTO> candidats =
|
||||
indexParType.getOrDefault(demande.getTypeAide(), new ArrayList<>());
|
||||
|
||||
// Si pas de correspondance exacte, chercher dans la même catégorie
|
||||
if (candidats.isEmpty()) {
|
||||
candidats =
|
||||
cachePropositionsActives.values().stream()
|
||||
.filter(
|
||||
p -> p.getTypeAide().getCategorie().equals(demande.getTypeAide().getCategorie()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// Filtrage et scoring
|
||||
return candidats.stream()
|
||||
.filter(PropositionAideDTO::isActiveEtDisponible)
|
||||
.filter(p -> p.peutAccepterBeneficiaires())
|
||||
.map(
|
||||
p -> {
|
||||
double score = p.getScoreCompatibilite(demande);
|
||||
// Stocker le score temporairement dans les données personnalisées
|
||||
if (p.getDonneesPersonnalisees() == null) {
|
||||
p.setDonneesPersonnalisees(new HashMap<>());
|
||||
}
|
||||
p.getDonneesPersonnalisees().put("scoreCompatibilite", score);
|
||||
return p;
|
||||
})
|
||||
.filter(p -> (Double) p.getDonneesPersonnalisees().get("scoreCompatibilite") >= 30.0)
|
||||
.sorted(
|
||||
(p1, p2) -> {
|
||||
Double score1 = (Double) p1.getDonneesPersonnalisees().get("scoreCompatibilite");
|
||||
Double score2 = (Double) p2.getDonneesPersonnalisees().get("scoreCompatibilite");
|
||||
return Double.compare(score2, score1); // Ordre décroissant
|
||||
})
|
||||
.limit(10) // Limiter à 10 meilleures propositions
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Recherche des propositions par critères
|
||||
*
|
||||
* @param filtres Map des critères de recherche
|
||||
* @return Liste des propositions correspondantes
|
||||
*/
|
||||
public List<PropositionAideDTO> rechercherAvecFiltres(Map<String, Object> filtres) {
|
||||
LOG.debugf("Recherche de propositions avec filtres: %s", filtres);
|
||||
|
||||
return cachePropositionsActives.values().stream()
|
||||
.filter(proposition -> correspondAuxFiltres(proposition, filtres))
|
||||
.sorted(this::comparerParPertinence)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtient les propositions actives pour un type d'aide
|
||||
*
|
||||
* @param typeAide Type d'aide recherché
|
||||
* @return Liste des propositions actives
|
||||
*/
|
||||
public List<PropositionAideDTO> obtenirPropositionsActives(TypeAide typeAide) {
|
||||
LOG.debugf("Récupération des propositions actives pour le type: %s", typeAide);
|
||||
|
||||
return indexParType.getOrDefault(typeAide, new ArrayList<>()).stream()
|
||||
.filter(PropositionAideDTO::isActiveEtDisponible)
|
||||
.sorted(this::comparerParPertinence)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtient les meilleures propositions (top performers)
|
||||
*
|
||||
* @param limite Nombre maximum de propositions à retourner
|
||||
* @return Liste des meilleures propositions
|
||||
*/
|
||||
public List<PropositionAideDTO> obtenirMeilleuresPropositions(int limite) {
|
||||
LOG.debugf("Récupération des %d meilleures propositions", limite);
|
||||
|
||||
return cachePropositionsActives.values().stream()
|
||||
.filter(PropositionAideDTO::isActiveEtDisponible)
|
||||
.filter(p -> p.getNombreEvaluations() >= 3) // Au moins 3 évaluations
|
||||
.filter(p -> p.getNoteMoyenne() != null && p.getNoteMoyenne() >= 4.0)
|
||||
.sorted(
|
||||
(p1, p2) -> {
|
||||
// Tri par note moyenne puis par nombre d'aides réalisées
|
||||
int compareNote = Double.compare(p2.getNoteMoyenne(), p1.getNoteMoyenne());
|
||||
if (compareNote != 0) return compareNote;
|
||||
return Integer.compare(
|
||||
p2.getNombreBeneficiairesAides(), p1.getNombreBeneficiairesAides());
|
||||
})
|
||||
.limit(limite)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// === GESTION DES PERFORMANCES ===
|
||||
|
||||
/**
|
||||
* Met à jour les statistiques d'une proposition après une aide fournie
|
||||
*
|
||||
* @param propositionId ID de la proposition
|
||||
* @param montantVerse Montant versé (si applicable)
|
||||
* @param nombreBeneficiaires Nombre de bénéficiaires aidés
|
||||
* @return La proposition mise à jour
|
||||
*/
|
||||
@Transactional
|
||||
public PropositionAideDTO mettreAJourStatistiques(
|
||||
@NotBlank String propositionId, Double montantVerse, int nombreBeneficiaires) {
|
||||
LOG.infof("Mise à jour des statistiques pour la proposition: %s", propositionId);
|
||||
|
||||
PropositionAideDTO proposition = obtenirParId(propositionId);
|
||||
if (proposition == null) {
|
||||
throw new IllegalArgumentException("Proposition non trouvée: " + propositionId);
|
||||
}
|
||||
|
||||
// Mise à jour des compteurs
|
||||
proposition.setNombreDemandesTraitees(proposition.getNombreDemandesTraitees() + 1);
|
||||
proposition.setNombreBeneficiairesAides(
|
||||
proposition.getNombreBeneficiairesAides() + nombreBeneficiaires);
|
||||
|
||||
if (montantVerse != null) {
|
||||
proposition.setMontantTotalVerse(proposition.getMontantTotalVerse() + montantVerse);
|
||||
}
|
||||
|
||||
// Recalcul du score de pertinence
|
||||
proposition.setScorePertinence(calculerScorePertinence(proposition));
|
||||
|
||||
// Vérification si la capacité maximale est atteinte
|
||||
if (proposition.getNombreBeneficiairesAides() >= proposition.getNombreMaxBeneficiaires()) {
|
||||
proposition.setEstDisponible(false);
|
||||
proposition.setStatut(PropositionAideDTO.StatutProposition.TERMINEE);
|
||||
}
|
||||
|
||||
proposition.setDateModification(LocalDateTime.now());
|
||||
|
||||
// Mise à jour du cache
|
||||
ajouterAuCache(proposition);
|
||||
|
||||
return proposition;
|
||||
}
|
||||
|
||||
// === MÉTHODES UTILITAIRES PRIVÉES ===
|
||||
|
||||
/** Génère un numéro de référence unique pour les propositions */
|
||||
private String genererNumeroReference() {
|
||||
int annee = LocalDateTime.now().getYear();
|
||||
int numero = (int) (Math.random() * 999999) + 1;
|
||||
return String.format("PA-%04d-%06d", annee, numero);
|
||||
}
|
||||
|
||||
/** Calcule le score de pertinence d'une proposition */
|
||||
private double calculerScorePertinence(PropositionAideDTO proposition) {
|
||||
double score = 50.0; // Score de base
|
||||
|
||||
// Bonus pour l'expérience (nombre d'aides réalisées)
|
||||
score += Math.min(20.0, proposition.getNombreBeneficiairesAides() * 2.0);
|
||||
|
||||
// Bonus pour la note moyenne
|
||||
if (proposition.getNoteMoyenne() != null) {
|
||||
score += (proposition.getNoteMoyenne() - 3.0) * 10.0; // +10 par point au-dessus de 3
|
||||
}
|
||||
|
||||
// Bonus pour la récence
|
||||
long joursDepuisCreation =
|
||||
java.time.Duration.between(proposition.getDateCreation(), LocalDateTime.now()).toDays();
|
||||
if (joursDepuisCreation <= 30) {
|
||||
score += 10.0;
|
||||
} else if (joursDepuisCreation <= 90) {
|
||||
score += 5.0;
|
||||
}
|
||||
|
||||
// Bonus pour la disponibilité
|
||||
if (proposition.isActiveEtDisponible()) {
|
||||
score += 15.0;
|
||||
}
|
||||
|
||||
// Malus pour l'inactivité
|
||||
if (proposition.getNombreVues() == 0) {
|
||||
score -= 10.0;
|
||||
}
|
||||
|
||||
return Math.max(0.0, Math.min(100.0, score));
|
||||
}
|
||||
|
||||
/** Vérifie si une proposition correspond aux filtres */
|
||||
private boolean correspondAuxFiltres(
|
||||
PropositionAideDTO proposition, Map<String, Object> filtres) {
|
||||
for (Map.Entry<String, Object> filtre : filtres.entrySet()) {
|
||||
String cle = filtre.getKey();
|
||||
Object valeur = filtre.getValue();
|
||||
|
||||
switch (cle) {
|
||||
case "typeAide" -> {
|
||||
if (!proposition.getTypeAide().equals(valeur)) return false;
|
||||
}
|
||||
case "statut" -> {
|
||||
if (!proposition.getStatut().equals(valeur)) return false;
|
||||
}
|
||||
case "proposantId" -> {
|
||||
if (!proposition.getProposantId().equals(valeur)) return false;
|
||||
}
|
||||
case "organisationId" -> {
|
||||
if (!proposition.getOrganisationId().equals(valeur)) return false;
|
||||
}
|
||||
case "estDisponible" -> {
|
||||
if (!proposition.getEstDisponible().equals(valeur)) return false;
|
||||
}
|
||||
case "montantMaximum" -> {
|
||||
if (proposition.getMontantMaximum() == null
|
||||
|| proposition.getMontantMaximum().compareTo(BigDecimal.valueOf((Double) valeur)) < 0) return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Compare deux propositions par pertinence */
|
||||
private int comparerParPertinence(PropositionAideDTO p1, PropositionAideDTO p2) {
|
||||
// D'abord par score de pertinence (plus haut = meilleur)
|
||||
int compareScore = Double.compare(p2.getScorePertinence(), p1.getScorePertinence());
|
||||
if (compareScore != 0) return compareScore;
|
||||
|
||||
// Puis par date de création (plus récent = meilleur)
|
||||
return p2.getDateCreation().compareTo(p1.getDateCreation());
|
||||
}
|
||||
|
||||
// === GESTION DU CACHE ET INDEX ===
|
||||
|
||||
private void ajouterAuCache(PropositionAideDTO proposition) {
|
||||
cachePropositionsActives.put(proposition.getId(), proposition);
|
||||
}
|
||||
|
||||
private void ajouterAIndex(PropositionAideDTO proposition) {
|
||||
indexParType
|
||||
.computeIfAbsent(proposition.getTypeAide(), k -> new ArrayList<>())
|
||||
.add(proposition);
|
||||
}
|
||||
|
||||
private void mettreAJourIndex(PropositionAideDTO proposition) {
|
||||
// Supprimer de tous les index
|
||||
indexParType
|
||||
.values()
|
||||
.forEach(liste -> liste.removeIf(p -> p.getId().equals(proposition.getId())));
|
||||
|
||||
// Ré-ajouter si la proposition est active
|
||||
if (proposition.isActiveEtDisponible()) {
|
||||
ajouterAIndex(proposition);
|
||||
}
|
||||
}
|
||||
|
||||
// === MÉTHODES DE SIMULATION (À REMPLACER PAR DE VRAIS REPOSITORIES) ===
|
||||
|
||||
private PropositionAideDTO simulerRecuperationBDD(String id) {
|
||||
// Simulation - dans une vraie implémentation, ceci ferait appel au repository
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package dev.lions.unionflow.server.service;
|
||||
|
||||
import dev.lions.unionflow.server.entity.Organisation;
|
||||
import dev.lions.unionflow.server.entity.Role;
|
||||
import dev.lions.unionflow.server.entity.Role.TypeRole;
|
||||
import dev.lions.unionflow.server.repository.OrganisationRepository;
|
||||
import dev.lions.unionflow.server.repository.RoleRepository;
|
||||
import dev.lions.unionflow.server.service.KeycloakService;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.ws.rs.NotFoundException;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
/**
|
||||
* Service métier pour la gestion des rôles
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 3.0
|
||||
* @since 2025-01-29
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class RoleService {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(RoleService.class);
|
||||
|
||||
@Inject RoleRepository roleRepository;
|
||||
|
||||
@Inject OrganisationRepository organisationRepository;
|
||||
|
||||
@Inject KeycloakService keycloakService;
|
||||
|
||||
/**
|
||||
* Crée un nouveau rôle
|
||||
*
|
||||
* @param role Rôle à créer
|
||||
* @return Rôle créé
|
||||
*/
|
||||
@Transactional
|
||||
public Role creerRole(Role role) {
|
||||
LOG.infof("Création d'un nouveau rôle: %s", role.getCode());
|
||||
|
||||
// Vérifier l'unicité du code
|
||||
if (roleRepository.findByCode(role.getCode()).isPresent()) {
|
||||
throw new IllegalArgumentException("Un rôle avec ce code existe déjà: " + role.getCode());
|
||||
}
|
||||
|
||||
// Métadonnées
|
||||
role.setCreePar(keycloakService.getCurrentUserEmail());
|
||||
|
||||
roleRepository.persist(role);
|
||||
LOG.infof("Rôle créé avec succès: ID=%s, Code=%s", role.getId(), role.getCode());
|
||||
|
||||
return role;
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour un rôle existant
|
||||
*
|
||||
* @param id ID du rôle
|
||||
* @param roleModifie Rôle avec les modifications
|
||||
* @return Rôle mis à jour
|
||||
*/
|
||||
@Transactional
|
||||
public Role mettreAJourRole(UUID id, Role roleModifie) {
|
||||
LOG.infof("Mise à jour du rôle ID: %s", id);
|
||||
|
||||
Role role =
|
||||
roleRepository
|
||||
.findRoleById(id)
|
||||
.orElseThrow(() -> new NotFoundException("Rôle non trouvé avec l'ID: " + id));
|
||||
|
||||
// Vérifier l'unicité du code si modifié
|
||||
if (!role.getCode().equals(roleModifie.getCode())) {
|
||||
if (roleRepository.findByCode(roleModifie.getCode()).isPresent()) {
|
||||
throw new IllegalArgumentException("Un rôle avec ce code existe déjà: " + roleModifie.getCode());
|
||||
}
|
||||
}
|
||||
|
||||
// Mise à jour
|
||||
role.setCode(roleModifie.getCode());
|
||||
role.setLibelle(roleModifie.getLibelle());
|
||||
role.setDescription(roleModifie.getDescription());
|
||||
role.setNiveauHierarchique(roleModifie.getNiveauHierarchique());
|
||||
role.setTypeRole(roleModifie.getTypeRole());
|
||||
role.setOrganisation(roleModifie.getOrganisation());
|
||||
role.setModifiePar(keycloakService.getCurrentUserEmail());
|
||||
|
||||
roleRepository.persist(role);
|
||||
LOG.infof("Rôle mis à jour avec succès: ID=%s", id);
|
||||
|
||||
return role;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve un rôle par son ID
|
||||
*
|
||||
* @param id ID du rôle
|
||||
* @return Rôle ou null
|
||||
*/
|
||||
public Role trouverParId(UUID id) {
|
||||
return roleRepository.findRoleById(id).orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve un rôle par son code
|
||||
*
|
||||
* @param code Code du rôle
|
||||
* @return Rôle ou null
|
||||
*/
|
||||
public Role trouverParCode(String code) {
|
||||
return roleRepository.findByCode(code).orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste tous les rôles système
|
||||
*
|
||||
* @return Liste des rôles système
|
||||
*/
|
||||
public List<Role> listerRolesSysteme() {
|
||||
return roleRepository.findRolesSysteme();
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste tous les rôles d'une organisation
|
||||
*
|
||||
* @param organisationId ID de l'organisation
|
||||
* @return Liste des rôles
|
||||
*/
|
||||
public List<Role> listerParOrganisation(UUID organisationId) {
|
||||
return roleRepository.findByOrganisationId(organisationId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste tous les rôles actifs
|
||||
*
|
||||
* @return Liste des rôles actifs
|
||||
*/
|
||||
public List<Role> listerTousActifs() {
|
||||
return roleRepository.findAllActifs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime (désactive) un rôle
|
||||
*
|
||||
* @param id ID du rôle
|
||||
*/
|
||||
@Transactional
|
||||
public void supprimerRole(UUID id) {
|
||||
LOG.infof("Suppression du rôle ID: %s", id);
|
||||
|
||||
Role role =
|
||||
roleRepository
|
||||
.findRoleById(id)
|
||||
.orElseThrow(() -> new NotFoundException("Rôle non trouvé avec l'ID: " + id));
|
||||
|
||||
// Vérifier si c'est un rôle système
|
||||
if (role.isRoleSysteme()) {
|
||||
throw new IllegalStateException("Impossible de supprimer un rôle système");
|
||||
}
|
||||
|
||||
role.setActif(false);
|
||||
role.setModifiePar(keycloakService.getCurrentUserEmail());
|
||||
|
||||
roleRepository.persist(role);
|
||||
LOG.infof("Rôle supprimé avec succès: ID=%s", id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,412 @@
|
||||
package dev.lions.unionflow.server.service;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.analytics.KPITrendDTO;
|
||||
import dev.lions.unionflow.server.api.enums.analytics.PeriodeAnalyse;
|
||||
import dev.lions.unionflow.server.api.enums.analytics.TypeMetrique;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Service d'analyse des tendances et prédictions pour les KPI
|
||||
*
|
||||
* <p>Ce service calcule les tendances, effectue des analyses statistiques et génère des prédictions
|
||||
* basées sur l'historique des données.
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 1.0
|
||||
* @since 2025-01-16
|
||||
*/
|
||||
@ApplicationScoped
|
||||
@Slf4j
|
||||
public class TrendAnalysisService {
|
||||
|
||||
@Inject AnalyticsService analyticsService;
|
||||
|
||||
@Inject KPICalculatorService kpiCalculatorService;
|
||||
|
||||
/**
|
||||
* Calcule la tendance d'un KPI sur une période donnée
|
||||
*
|
||||
* @param typeMetrique Le type de métrique à analyser
|
||||
* @param periodeAnalyse La période d'analyse
|
||||
* @param organisationId L'ID de l'organisation (optionnel)
|
||||
* @return Les données de tendance du KPI
|
||||
*/
|
||||
public KPITrendDTO calculerTendance(
|
||||
TypeMetrique typeMetrique, PeriodeAnalyse periodeAnalyse, UUID organisationId) {
|
||||
log.info(
|
||||
"Calcul de la tendance pour {} sur la période {} et l'organisation {}",
|
||||
typeMetrique,
|
||||
periodeAnalyse,
|
||||
organisationId);
|
||||
|
||||
LocalDateTime dateDebut = periodeAnalyse.getDateDebut();
|
||||
LocalDateTime dateFin = periodeAnalyse.getDateFin();
|
||||
|
||||
// Génération des points de données historiques
|
||||
List<KPITrendDTO.PointDonneeDTO> pointsDonnees =
|
||||
genererPointsDonnees(typeMetrique, dateDebut, dateFin, organisationId);
|
||||
|
||||
// Calculs statistiques
|
||||
StatistiquesDTO stats = calculerStatistiques(pointsDonnees);
|
||||
|
||||
// Analyse de tendance (régression linéaire simple)
|
||||
TendanceDTO tendance = calculerTendanceLineaire(pointsDonnees);
|
||||
|
||||
// Prédiction pour la prochaine période
|
||||
BigDecimal prediction = calculerPrediction(pointsDonnees, tendance);
|
||||
|
||||
// Détection d'anomalies
|
||||
detecterAnomalies(pointsDonnees, stats);
|
||||
|
||||
return KPITrendDTO.builder()
|
||||
.typeMetrique(typeMetrique)
|
||||
.periodeAnalyse(periodeAnalyse)
|
||||
.organisationId(organisationId)
|
||||
.nomOrganisation(obtenirNomOrganisation(organisationId))
|
||||
.dateDebut(dateDebut)
|
||||
.dateFin(dateFin)
|
||||
.pointsDonnees(pointsDonnees)
|
||||
.valeurActuelle(stats.valeurActuelle)
|
||||
.valeurMinimale(stats.valeurMinimale)
|
||||
.valeurMaximale(stats.valeurMaximale)
|
||||
.valeurMoyenne(stats.valeurMoyenne)
|
||||
.ecartType(stats.ecartType)
|
||||
.coefficientVariation(stats.coefficientVariation)
|
||||
.tendanceGenerale(tendance.pente)
|
||||
.coefficientCorrelation(tendance.coefficientCorrelation)
|
||||
.pourcentageEvolutionGlobale(calculerEvolutionGlobale(pointsDonnees))
|
||||
.predictionProchainePeriode(prediction)
|
||||
.margeErreurPrediction(calculerMargeErreur(tendance))
|
||||
.seuilAlerteBas(calculerSeuilAlerteBas(stats))
|
||||
.seuilAlerteHaut(calculerSeuilAlerteHaut(stats))
|
||||
.alerteActive(verifierAlertes(stats.valeurActuelle, stats))
|
||||
.intervalleRegroupement(periodeAnalyse.getIntervalleRegroupement())
|
||||
.formatDate(periodeAnalyse.getFormatDate())
|
||||
.dateDerniereMiseAJour(LocalDateTime.now())
|
||||
.frequenceMiseAJourMinutes(determinerFrequenceMiseAJour(periodeAnalyse))
|
||||
.build();
|
||||
}
|
||||
|
||||
/** Génère les points de données historiques pour la période */
|
||||
private List<KPITrendDTO.PointDonneeDTO> genererPointsDonnees(
|
||||
TypeMetrique typeMetrique,
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin,
|
||||
UUID organisationId) {
|
||||
List<KPITrendDTO.PointDonneeDTO> points = new ArrayList<>();
|
||||
|
||||
// Déterminer l'intervalle entre les points
|
||||
ChronoUnit unite = determinerUniteIntervalle(dateDebut, dateFin);
|
||||
long intervalleValeur = determinerValeurIntervalle(dateDebut, dateFin, unite);
|
||||
|
||||
LocalDateTime dateCourante = dateDebut;
|
||||
int index = 0;
|
||||
|
||||
while (!dateCourante.isAfter(dateFin)) {
|
||||
LocalDateTime dateFinIntervalle = dateCourante.plus(intervalleValeur, unite);
|
||||
if (dateFinIntervalle.isAfter(dateFin)) {
|
||||
dateFinIntervalle = dateFin;
|
||||
}
|
||||
|
||||
// Calcul de la valeur pour cet intervalle
|
||||
BigDecimal valeur =
|
||||
calculerValeurPourIntervalle(
|
||||
typeMetrique, dateCourante, dateFinIntervalle, organisationId);
|
||||
|
||||
KPITrendDTO.PointDonneeDTO point =
|
||||
KPITrendDTO.PointDonneeDTO.builder()
|
||||
.date(dateCourante)
|
||||
.valeur(valeur)
|
||||
.libelle(formaterLibellePoint(dateCourante, unite))
|
||||
.anomalie(false) // Sera déterminé plus tard
|
||||
.prediction(false)
|
||||
.build();
|
||||
|
||||
points.add(point);
|
||||
dateCourante = dateCourante.plus(intervalleValeur, unite);
|
||||
index++;
|
||||
}
|
||||
|
||||
log.info("Généré {} points de données pour la tendance", points.size());
|
||||
return points;
|
||||
}
|
||||
|
||||
/** Calcule les statistiques descriptives des points de données */
|
||||
private StatistiquesDTO calculerStatistiques(List<KPITrendDTO.PointDonneeDTO> points) {
|
||||
if (points.isEmpty()) {
|
||||
return new StatistiquesDTO();
|
||||
}
|
||||
|
||||
List<BigDecimal> valeurs = points.stream().map(KPITrendDTO.PointDonneeDTO::getValeur).toList();
|
||||
|
||||
BigDecimal valeurActuelle = points.get(points.size() - 1).getValeur();
|
||||
BigDecimal valeurMinimale = valeurs.stream().min(BigDecimal::compareTo).orElse(BigDecimal.ZERO);
|
||||
BigDecimal valeurMaximale = valeurs.stream().max(BigDecimal::compareTo).orElse(BigDecimal.ZERO);
|
||||
|
||||
// Calcul de la moyenne
|
||||
BigDecimal somme = valeurs.stream().reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
BigDecimal moyenne = somme.divide(new BigDecimal(valeurs.size()), 4, RoundingMode.HALF_UP);
|
||||
|
||||
// Calcul de l'écart-type
|
||||
BigDecimal sommeDifferencesCarrees =
|
||||
valeurs.stream()
|
||||
.map(v -> v.subtract(moyenne).pow(2))
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
BigDecimal variance =
|
||||
sommeDifferencesCarrees.divide(new BigDecimal(valeurs.size()), 4, RoundingMode.HALF_UP);
|
||||
BigDecimal ecartType =
|
||||
new BigDecimal(Math.sqrt(variance.doubleValue())).setScale(4, RoundingMode.HALF_UP);
|
||||
|
||||
// Coefficient de variation
|
||||
BigDecimal coefficientVariation =
|
||||
moyenne.compareTo(BigDecimal.ZERO) != 0
|
||||
? ecartType.divide(moyenne, 4, RoundingMode.HALF_UP)
|
||||
: BigDecimal.ZERO;
|
||||
|
||||
return new StatistiquesDTO(
|
||||
valeurActuelle, valeurMinimale, valeurMaximale, moyenne, ecartType, coefficientVariation);
|
||||
}
|
||||
|
||||
/** Calcule la tendance linéaire (régression linéaire simple) */
|
||||
private TendanceDTO calculerTendanceLineaire(List<KPITrendDTO.PointDonneeDTO> points) {
|
||||
if (points.size() < 2) {
|
||||
return new TendanceDTO(BigDecimal.ZERO, BigDecimal.ZERO);
|
||||
}
|
||||
|
||||
int n = points.size();
|
||||
BigDecimal sommeX = BigDecimal.ZERO;
|
||||
BigDecimal sommeY = BigDecimal.ZERO;
|
||||
BigDecimal sommeXY = BigDecimal.ZERO;
|
||||
BigDecimal sommeX2 = BigDecimal.ZERO;
|
||||
BigDecimal sommeY2 = BigDecimal.ZERO;
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
BigDecimal x = new BigDecimal(i); // Index comme variable X
|
||||
BigDecimal y = points.get(i).getValeur(); // Valeur comme variable Y
|
||||
|
||||
sommeX = sommeX.add(x);
|
||||
sommeY = sommeY.add(y);
|
||||
sommeXY = sommeXY.add(x.multiply(y));
|
||||
sommeX2 = sommeX2.add(x.multiply(x));
|
||||
sommeY2 = sommeY2.add(y.multiply(y));
|
||||
}
|
||||
|
||||
// Calcul de la pente (coefficient directeur)
|
||||
BigDecimal nBD = new BigDecimal(n);
|
||||
BigDecimal numerateur = nBD.multiply(sommeXY).subtract(sommeX.multiply(sommeY));
|
||||
BigDecimal denominateur = nBD.multiply(sommeX2).subtract(sommeX.multiply(sommeX));
|
||||
|
||||
BigDecimal pente =
|
||||
denominateur.compareTo(BigDecimal.ZERO) != 0
|
||||
? numerateur.divide(denominateur, 6, RoundingMode.HALF_UP)
|
||||
: BigDecimal.ZERO;
|
||||
|
||||
// Calcul du coefficient de corrélation R²
|
||||
BigDecimal numerateurR = numerateur;
|
||||
BigDecimal denominateurR1 = nBD.multiply(sommeX2).subtract(sommeX.multiply(sommeX));
|
||||
BigDecimal denominateurR2 = nBD.multiply(sommeY2).subtract(sommeY.multiply(sommeY));
|
||||
|
||||
BigDecimal coefficientCorrelation = BigDecimal.ZERO;
|
||||
if (denominateurR1.compareTo(BigDecimal.ZERO) != 0
|
||||
&& denominateurR2.compareTo(BigDecimal.ZERO) != 0) {
|
||||
BigDecimal denominateurR =
|
||||
new BigDecimal(Math.sqrt(denominateurR1.multiply(denominateurR2).doubleValue()));
|
||||
|
||||
if (denominateurR.compareTo(BigDecimal.ZERO) != 0) {
|
||||
BigDecimal r = numerateurR.divide(denominateurR, 6, RoundingMode.HALF_UP);
|
||||
coefficientCorrelation = r.multiply(r); // R²
|
||||
}
|
||||
}
|
||||
|
||||
return new TendanceDTO(pente, coefficientCorrelation);
|
||||
}
|
||||
|
||||
/** Calcule une prédiction pour la prochaine période */
|
||||
private BigDecimal calculerPrediction(
|
||||
List<KPITrendDTO.PointDonneeDTO> points, TendanceDTO tendance) {
|
||||
if (points.isEmpty()) return BigDecimal.ZERO;
|
||||
|
||||
BigDecimal derniereValeur = points.get(points.size() - 1).getValeur();
|
||||
BigDecimal prediction = derniereValeur.add(tendance.pente);
|
||||
|
||||
// S'assurer que la prédiction est positive
|
||||
return prediction.max(BigDecimal.ZERO);
|
||||
}
|
||||
|
||||
/** Détecte les anomalies dans les points de données */
|
||||
private void detecterAnomalies(List<KPITrendDTO.PointDonneeDTO> points, StatistiquesDTO stats) {
|
||||
BigDecimal seuilAnomalie = stats.ecartType.multiply(new BigDecimal("2")); // 2 écarts-types
|
||||
|
||||
for (KPITrendDTO.PointDonneeDTO point : points) {
|
||||
BigDecimal ecartMoyenne = point.getValeur().subtract(stats.valeurMoyenne).abs();
|
||||
if (ecartMoyenne.compareTo(seuilAnomalie) > 0) {
|
||||
point.setAnomalie(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === MÉTHODES UTILITAIRES ===
|
||||
|
||||
private ChronoUnit determinerUniteIntervalle(LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||
long joursTotal = ChronoUnit.DAYS.between(dateDebut, dateFin);
|
||||
|
||||
if (joursTotal <= 7) return ChronoUnit.DAYS;
|
||||
if (joursTotal <= 90) return ChronoUnit.DAYS;
|
||||
if (joursTotal <= 365) return ChronoUnit.WEEKS;
|
||||
return ChronoUnit.MONTHS;
|
||||
}
|
||||
|
||||
private long determinerValeurIntervalle(
|
||||
LocalDateTime dateDebut, LocalDateTime dateFin, ChronoUnit unite) {
|
||||
long dureeTotal = unite.between(dateDebut, dateFin);
|
||||
|
||||
// Viser environ 10-20 points de données
|
||||
if (dureeTotal <= 20) return 1;
|
||||
if (dureeTotal <= 40) return 2;
|
||||
if (dureeTotal <= 100) return 5;
|
||||
return dureeTotal / 15; // Environ 15 points
|
||||
}
|
||||
|
||||
private BigDecimal calculerValeurPourIntervalle(
|
||||
TypeMetrique typeMetrique,
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin,
|
||||
UUID organisationId) {
|
||||
// Utiliser le service KPI pour calculer la valeur
|
||||
return switch (typeMetrique) {
|
||||
case NOMBRE_MEMBRES_ACTIFS -> {
|
||||
// Calcul direct via le service KPI
|
||||
var kpis = kpiCalculatorService.calculerTousLesKPI(organisationId, dateDebut, dateFin);
|
||||
yield kpis.getOrDefault(TypeMetrique.NOMBRE_MEMBRES_ACTIFS, BigDecimal.ZERO);
|
||||
}
|
||||
case TOTAL_COTISATIONS_COLLECTEES -> {
|
||||
var kpis = kpiCalculatorService.calculerTousLesKPI(organisationId, dateDebut, dateFin);
|
||||
yield kpis.getOrDefault(TypeMetrique.TOTAL_COTISATIONS_COLLECTEES, BigDecimal.ZERO);
|
||||
}
|
||||
case NOMBRE_EVENEMENTS_ORGANISES -> {
|
||||
var kpis = kpiCalculatorService.calculerTousLesKPI(organisationId, dateDebut, dateFin);
|
||||
yield kpis.getOrDefault(TypeMetrique.NOMBRE_EVENEMENTS_ORGANISES, BigDecimal.ZERO);
|
||||
}
|
||||
case NOMBRE_DEMANDES_AIDE -> {
|
||||
var kpis = kpiCalculatorService.calculerTousLesKPI(organisationId, dateDebut, dateFin);
|
||||
yield kpis.getOrDefault(TypeMetrique.NOMBRE_DEMANDES_AIDE, BigDecimal.ZERO);
|
||||
}
|
||||
default -> BigDecimal.ZERO;
|
||||
};
|
||||
}
|
||||
|
||||
private String formaterLibellePoint(LocalDateTime date, ChronoUnit unite) {
|
||||
return switch (unite) {
|
||||
case DAYS -> date.toLocalDate().toString();
|
||||
case WEEKS -> "S" + date.get(java.time.temporal.WeekFields.ISO.weekOfYear());
|
||||
case MONTHS -> date.getMonth().toString() + " " + date.getYear();
|
||||
default -> date.toString();
|
||||
};
|
||||
}
|
||||
|
||||
private BigDecimal calculerEvolutionGlobale(List<KPITrendDTO.PointDonneeDTO> points) {
|
||||
if (points.size() < 2) return BigDecimal.ZERO;
|
||||
|
||||
BigDecimal premiereValeur = points.get(0).getValeur();
|
||||
BigDecimal derniereValeur = points.get(points.size() - 1).getValeur();
|
||||
|
||||
if (premiereValeur.compareTo(BigDecimal.ZERO) == 0) return BigDecimal.ZERO;
|
||||
|
||||
return derniereValeur
|
||||
.subtract(premiereValeur)
|
||||
.divide(premiereValeur, 4, RoundingMode.HALF_UP)
|
||||
.multiply(new BigDecimal("100"));
|
||||
}
|
||||
|
||||
private BigDecimal calculerMargeErreur(TendanceDTO tendance) {
|
||||
// Marge d'erreur basée sur le coefficient de corrélation
|
||||
BigDecimal precision = tendance.coefficientCorrelation;
|
||||
BigDecimal margeErreur = BigDecimal.ONE.subtract(precision).multiply(new BigDecimal("100"));
|
||||
return margeErreur.min(new BigDecimal("50")); // Plafonnée à 50%
|
||||
}
|
||||
|
||||
private BigDecimal calculerSeuilAlerteBas(StatistiquesDTO stats) {
|
||||
return stats.valeurMoyenne.subtract(stats.ecartType.multiply(new BigDecimal("1.5")));
|
||||
}
|
||||
|
||||
private BigDecimal calculerSeuilAlerteHaut(StatistiquesDTO stats) {
|
||||
return stats.valeurMoyenne.add(stats.ecartType.multiply(new BigDecimal("1.5")));
|
||||
}
|
||||
|
||||
private Boolean verifierAlertes(BigDecimal valeurActuelle, StatistiquesDTO stats) {
|
||||
BigDecimal seuilBas = calculerSeuilAlerteBas(stats);
|
||||
BigDecimal seuilHaut = calculerSeuilAlerteHaut(stats);
|
||||
|
||||
return valeurActuelle.compareTo(seuilBas) < 0 || valeurActuelle.compareTo(seuilHaut) > 0;
|
||||
}
|
||||
|
||||
private Integer determinerFrequenceMiseAJour(PeriodeAnalyse periode) {
|
||||
return switch (periode) {
|
||||
case AUJOURD_HUI, HIER -> 15; // 15 minutes
|
||||
case CETTE_SEMAINE, SEMAINE_DERNIERE -> 60; // 1 heure
|
||||
case CE_MOIS, MOIS_DERNIER -> 240; // 4 heures
|
||||
default -> 1440; // 24 heures
|
||||
};
|
||||
}
|
||||
|
||||
private String obtenirNomOrganisation(UUID organisationId) {
|
||||
// À implémenter avec le repository
|
||||
return null;
|
||||
}
|
||||
|
||||
// === CLASSES INTERNES ===
|
||||
|
||||
private static class StatistiquesDTO {
|
||||
final BigDecimal valeurActuelle;
|
||||
final BigDecimal valeurMinimale;
|
||||
final BigDecimal valeurMaximale;
|
||||
final BigDecimal valeurMoyenne;
|
||||
final BigDecimal ecartType;
|
||||
final BigDecimal coefficientVariation;
|
||||
|
||||
StatistiquesDTO() {
|
||||
this(
|
||||
BigDecimal.ZERO,
|
||||
BigDecimal.ZERO,
|
||||
BigDecimal.ZERO,
|
||||
BigDecimal.ZERO,
|
||||
BigDecimal.ZERO,
|
||||
BigDecimal.ZERO);
|
||||
}
|
||||
|
||||
StatistiquesDTO(
|
||||
BigDecimal valeurActuelle,
|
||||
BigDecimal valeurMinimale,
|
||||
BigDecimal valeurMaximale,
|
||||
BigDecimal valeurMoyenne,
|
||||
BigDecimal ecartType,
|
||||
BigDecimal coefficientVariation) {
|
||||
this.valeurActuelle = valeurActuelle;
|
||||
this.valeurMinimale = valeurMinimale;
|
||||
this.valeurMaximale = valeurMaximale;
|
||||
this.valeurMoyenne = valeurMoyenne;
|
||||
this.ecartType = ecartType;
|
||||
this.coefficientVariation = coefficientVariation;
|
||||
}
|
||||
}
|
||||
|
||||
private static class TendanceDTO {
|
||||
final BigDecimal pente;
|
||||
final BigDecimal coefficientCorrelation;
|
||||
|
||||
TendanceDTO(BigDecimal pente, BigDecimal coefficientCorrelation) {
|
||||
this.pente = pente;
|
||||
this.coefficientCorrelation = coefficientCorrelation;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package dev.lions.unionflow.server.service;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.organisation.TypeOrganisationDTO;
|
||||
import dev.lions.unionflow.server.entity.TypeOrganisationEntity;
|
||||
import dev.lions.unionflow.server.repository.TypeOrganisationRepository;
|
||||
import dev.lions.unionflow.server.service.KeycloakService;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
/**
|
||||
* Service de gestion du catalogue des types d'organisation.
|
||||
*
|
||||
* <p>Synchronise les types persistés avec l'enum {@link TypeOrganisation} pour les valeurs
|
||||
* par défaut, puis permet un CRUD entièrement dynamique (les nouveaux codes ne sont plus
|
||||
* limités aux valeurs de l'enum).
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class TypeOrganisationService {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(TypeOrganisationService.class);
|
||||
|
||||
@Inject TypeOrganisationRepository repository;
|
||||
@Inject KeycloakService keycloakService;
|
||||
|
||||
// Plus d'initialisation automatique : le catalogue des types est désormais entièrement
|
||||
// géré en mode CRUD via l'UI d'administration. Aucune donnée fictive n'est injectée
|
||||
// au démarrage ; si nécessaire, utilisez des scripts de migration (Flyway) ou l'UI.
|
||||
|
||||
/** Retourne la liste de tous les types (optionnellement seulement actifs). */
|
||||
public List<TypeOrganisationDTO> listAll(boolean onlyActifs) {
|
||||
List<TypeOrganisationEntity> entities =
|
||||
onlyActifs ? repository.listActifsOrdennes() : repository.listAll();
|
||||
return entities.stream().map(this::toDTO).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/** Crée un nouveau type. Le code doit être non vide et unique. */
|
||||
@Transactional
|
||||
public TypeOrganisationDTO create(TypeOrganisationDTO dto) {
|
||||
validateCode(dto.getCode());
|
||||
|
||||
// Si un type existe déjà pour ce code, on retourne simplement l'existant
|
||||
// (comportement idempotent côté API) plutôt que de remonter une 400.
|
||||
// Le CRUD complet reste possible via l'écran d'édition.
|
||||
var existingOpt = repository.findByCode(dto.getCode());
|
||||
if (existingOpt.isPresent()) {
|
||||
LOG.infof(
|
||||
"Type d'organisation déjà existant pour le code %s, retour de l'entrée existante.",
|
||||
dto.getCode());
|
||||
return toDTO(existingOpt.get());
|
||||
}
|
||||
|
||||
TypeOrganisationEntity entity = new TypeOrganisationEntity();
|
||||
// métadonnées de création
|
||||
entity.setCreePar(keycloakService.getCurrentUserEmail());
|
||||
applyToEntity(dto, entity);
|
||||
repository.persist(entity);
|
||||
return toDTO(entity);
|
||||
}
|
||||
|
||||
/** Met à jour un type existant. L'ID est utilisé comme identifiant principal. */
|
||||
@Transactional
|
||||
public TypeOrganisationDTO update(UUID id, TypeOrganisationDTO dto) {
|
||||
TypeOrganisationEntity entity =
|
||||
repository
|
||||
.findByIdOptional(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Type d'organisation introuvable"));
|
||||
|
||||
if (dto.getCode() != null && !dto.getCode().equalsIgnoreCase(entity.getCode())) {
|
||||
validateCode(dto.getCode());
|
||||
repository
|
||||
.findByCode(dto.getCode())
|
||||
.ifPresent(
|
||||
existing -> {
|
||||
if (!existing.getId().equals(id)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Un autre type d'organisation utilise déjà le code: " + dto.getCode());
|
||||
}
|
||||
});
|
||||
entity.setCode(dto.getCode());
|
||||
}
|
||||
|
||||
// métadonnées de modification
|
||||
entity.setModifiePar(keycloakService.getCurrentUserEmail());
|
||||
applyToEntity(dto, entity);
|
||||
repository.update(entity);
|
||||
return toDTO(entity);
|
||||
}
|
||||
|
||||
/** Désactive logiquement un type. */
|
||||
@Transactional
|
||||
public void disable(UUID id) {
|
||||
TypeOrganisationEntity entity =
|
||||
repository
|
||||
.findByIdOptional(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Type d'organisation introuvable"));
|
||||
entity.setActif(false);
|
||||
repository.update(entity);
|
||||
}
|
||||
|
||||
private void validateCode(String code) {
|
||||
if (code == null || code.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("Le code du type d'organisation est obligatoire");
|
||||
}
|
||||
// Plus aucune contrainte de format technique côté backend pour éviter les 400 inutiles.
|
||||
// Le code est simplement normalisé en majuscules dans applyToEntity, ce qui suffit
|
||||
// pour garantir la cohérence métier et la clé fonctionnelle.
|
||||
}
|
||||
|
||||
private TypeOrganisationDTO toDTO(TypeOrganisationEntity entity) {
|
||||
TypeOrganisationDTO dto = new TypeOrganisationDTO();
|
||||
dto.setId(entity.getId());
|
||||
dto.setDateCreation(entity.getDateCreation());
|
||||
dto.setDateModification(entity.getDateModification());
|
||||
dto.setActif(entity.getActif());
|
||||
dto.setVersion(entity.getVersion());
|
||||
|
||||
dto.setCode(entity.getCode());
|
||||
dto.setLibelle(entity.getLibelle());
|
||||
dto.setDescription(entity.getDescription());
|
||||
dto.setOrdreAffichage(entity.getOrdreAffichage());
|
||||
return dto;
|
||||
}
|
||||
|
||||
private void applyToEntity(TypeOrganisationDTO dto, TypeOrganisationEntity entity) {
|
||||
if (dto.getCode() != null) {
|
||||
entity.setCode(dto.getCode().toUpperCase());
|
||||
}
|
||||
if (dto.getLibelle() != null) {
|
||||
entity.setLibelle(dto.getLibelle());
|
||||
}
|
||||
entity.setDescription(dto.getDescription());
|
||||
entity.setOrdreAffichage(dto.getOrdreAffichage());
|
||||
if (dto.getActif() != null) {
|
||||
entity.setActif(dto.getActif());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,393 @@
|
||||
package dev.lions.unionflow.server.service;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.wave.CompteWaveDTO;
|
||||
import dev.lions.unionflow.server.api.dto.wave.TransactionWaveDTO;
|
||||
import dev.lions.unionflow.server.api.enums.wave.StatutCompteWave;
|
||||
import dev.lions.unionflow.server.api.enums.wave.StatutTransactionWave;
|
||||
import dev.lions.unionflow.server.api.enums.wave.TypeTransactionWave;
|
||||
import dev.lions.unionflow.server.entity.CompteWave;
|
||||
import dev.lions.unionflow.server.entity.Organisation;
|
||||
import dev.lions.unionflow.server.entity.Membre;
|
||||
import dev.lions.unionflow.server.entity.TransactionWave;
|
||||
import dev.lions.unionflow.server.repository.CompteWaveRepository;
|
||||
import dev.lions.unionflow.server.repository.OrganisationRepository;
|
||||
import dev.lions.unionflow.server.repository.MembreRepository;
|
||||
import dev.lions.unionflow.server.repository.TransactionWaveRepository;
|
||||
import dev.lions.unionflow.server.service.KeycloakService;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.ws.rs.NotFoundException;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
/**
|
||||
* Service métier pour l'intégration Wave Mobile Money
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 3.0
|
||||
* @since 2025-01-29
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class WaveService {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(WaveService.class);
|
||||
|
||||
@Inject CompteWaveRepository compteWaveRepository;
|
||||
|
||||
@Inject TransactionWaveRepository transactionWaveRepository;
|
||||
|
||||
@Inject OrganisationRepository organisationRepository;
|
||||
|
||||
@Inject MembreRepository membreRepository;
|
||||
|
||||
@Inject KeycloakService keycloakService;
|
||||
|
||||
/**
|
||||
* Crée un nouveau compte Wave
|
||||
*
|
||||
* @param compteWaveDTO DTO du compte à créer
|
||||
* @return DTO du compte créé
|
||||
*/
|
||||
@Transactional
|
||||
public CompteWaveDTO creerCompteWave(CompteWaveDTO compteWaveDTO) {
|
||||
LOG.infof("Création d'un nouveau compte Wave: %s", compteWaveDTO.getNumeroTelephone());
|
||||
|
||||
// Vérifier l'unicité du numéro de téléphone
|
||||
if (compteWaveRepository.findByNumeroTelephone(compteWaveDTO.getNumeroTelephone()).isPresent()) {
|
||||
throw new IllegalArgumentException(
|
||||
"Un compte Wave existe déjà pour ce numéro: " + compteWaveDTO.getNumeroTelephone());
|
||||
}
|
||||
|
||||
CompteWave compteWave = convertToEntity(compteWaveDTO);
|
||||
compteWave.setCreePar(keycloakService.getCurrentUserEmail());
|
||||
|
||||
compteWaveRepository.persist(compteWave);
|
||||
LOG.infof("Compte Wave créé avec succès: ID=%s, Téléphone=%s", compteWave.getId(), compteWave.getNumeroTelephone());
|
||||
|
||||
return convertToDTO(compteWave);
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour un compte Wave
|
||||
*
|
||||
* @param id ID du compte
|
||||
* @param compteWaveDTO DTO avec les modifications
|
||||
* @return DTO du compte mis à jour
|
||||
*/
|
||||
@Transactional
|
||||
public CompteWaveDTO mettreAJourCompteWave(UUID id, CompteWaveDTO compteWaveDTO) {
|
||||
LOG.infof("Mise à jour du compte Wave ID: %s", id);
|
||||
|
||||
CompteWave compteWave =
|
||||
compteWaveRepository
|
||||
.findCompteWaveById(id)
|
||||
.orElseThrow(() -> new NotFoundException("Compte Wave non trouvé avec l'ID: " + id));
|
||||
|
||||
updateFromDTO(compteWave, compteWaveDTO);
|
||||
compteWave.setModifiePar(keycloakService.getCurrentUserEmail());
|
||||
|
||||
compteWaveRepository.persist(compteWave);
|
||||
LOG.infof("Compte Wave mis à jour avec succès: ID=%s", id);
|
||||
|
||||
return convertToDTO(compteWave);
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie un compte Wave
|
||||
*
|
||||
* @param id ID du compte
|
||||
* @return DTO du compte vérifié
|
||||
*/
|
||||
@Transactional
|
||||
public CompteWaveDTO verifierCompteWave(UUID id) {
|
||||
LOG.infof("Vérification du compte Wave ID: %s", id);
|
||||
|
||||
CompteWave compteWave =
|
||||
compteWaveRepository
|
||||
.findCompteWaveById(id)
|
||||
.orElseThrow(() -> new NotFoundException("Compte Wave non trouvé avec l'ID: " + id));
|
||||
|
||||
compteWave.setStatutCompte(StatutCompteWave.VERIFIE);
|
||||
compteWave.setDateDerniereVerification(LocalDateTime.now());
|
||||
compteWave.setModifiePar(keycloakService.getCurrentUserEmail());
|
||||
|
||||
compteWaveRepository.persist(compteWave);
|
||||
LOG.infof("Compte Wave vérifié avec succès: ID=%s", id);
|
||||
|
||||
return convertToDTO(compteWave);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve un compte Wave par son ID
|
||||
*
|
||||
* @param id ID du compte
|
||||
* @return DTO du compte
|
||||
*/
|
||||
public CompteWaveDTO trouverCompteWaveParId(UUID id) {
|
||||
return compteWaveRepository
|
||||
.findCompteWaveById(id)
|
||||
.map(this::convertToDTO)
|
||||
.orElseThrow(() -> new NotFoundException("Compte Wave non trouvé avec l'ID: " + id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve un compte Wave par numéro de téléphone
|
||||
*
|
||||
* @param numeroTelephone Numéro de téléphone
|
||||
* @return DTO du compte ou null
|
||||
*/
|
||||
public CompteWaveDTO trouverCompteWaveParTelephone(String numeroTelephone) {
|
||||
return compteWaveRepository
|
||||
.findByNumeroTelephone(numeroTelephone)
|
||||
.map(this::convertToDTO)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste tous les comptes Wave d'une organisation
|
||||
*
|
||||
* @param organisationId ID de l'organisation
|
||||
* @return Liste des comptes Wave
|
||||
*/
|
||||
public List<CompteWaveDTO> listerComptesWaveParOrganisation(UUID organisationId) {
|
||||
return compteWaveRepository.findByOrganisationId(organisationId).stream()
|
||||
.map(this::convertToDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée une nouvelle transaction Wave
|
||||
*
|
||||
* @param transactionWaveDTO DTO de la transaction à créer
|
||||
* @return DTO de la transaction créée
|
||||
*/
|
||||
@Transactional
|
||||
public TransactionWaveDTO creerTransactionWave(TransactionWaveDTO transactionWaveDTO) {
|
||||
LOG.infof("Création d'une nouvelle transaction Wave: %s", transactionWaveDTO.getWaveTransactionId());
|
||||
|
||||
TransactionWave transactionWave = convertToEntity(transactionWaveDTO);
|
||||
transactionWave.setCreePar(keycloakService.getCurrentUserEmail());
|
||||
|
||||
transactionWaveRepository.persist(transactionWave);
|
||||
LOG.infof(
|
||||
"Transaction Wave créée avec succès: ID=%s, WaveTransactionId=%s",
|
||||
transactionWave.getId(), transactionWave.getWaveTransactionId());
|
||||
|
||||
return convertToDTO(transactionWave);
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour le statut d'une transaction Wave
|
||||
*
|
||||
* @param waveTransactionId Identifiant Wave de la transaction
|
||||
* @param nouveauStatut Nouveau statut
|
||||
* @return DTO de la transaction mise à jour
|
||||
*/
|
||||
@Transactional
|
||||
public TransactionWaveDTO mettreAJourStatutTransaction(
|
||||
String waveTransactionId, StatutTransactionWave nouveauStatut) {
|
||||
LOG.infof("Mise à jour du statut de la transaction Wave: %s -> %s", waveTransactionId, nouveauStatut);
|
||||
|
||||
TransactionWave transactionWave =
|
||||
transactionWaveRepository
|
||||
.findByWaveTransactionId(waveTransactionId)
|
||||
.orElseThrow(
|
||||
() ->
|
||||
new NotFoundException(
|
||||
"Transaction Wave non trouvée avec l'ID: " + waveTransactionId));
|
||||
|
||||
transactionWave.setStatutTransaction(nouveauStatut);
|
||||
transactionWave.setDateDerniereTentative(LocalDateTime.now());
|
||||
transactionWave.setModifiePar(keycloakService.getCurrentUserEmail());
|
||||
|
||||
transactionWaveRepository.persist(transactionWave);
|
||||
LOG.infof("Statut de la transaction Wave mis à jour: %s", waveTransactionId);
|
||||
|
||||
return convertToDTO(transactionWave);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve une transaction Wave par son identifiant Wave
|
||||
*
|
||||
* @param waveTransactionId Identifiant Wave
|
||||
* @return DTO de la transaction
|
||||
*/
|
||||
public TransactionWaveDTO trouverTransactionWaveParId(String waveTransactionId) {
|
||||
return transactionWaveRepository
|
||||
.findByWaveTransactionId(waveTransactionId)
|
||||
.map(this::convertToDTO)
|
||||
.orElseThrow(
|
||||
() -> new NotFoundException("Transaction Wave non trouvée avec l'ID: " + waveTransactionId));
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// MÉTHODES PRIVÉES
|
||||
// ========================================
|
||||
|
||||
/** Convertit une entité CompteWave en DTO */
|
||||
private CompteWaveDTO convertToDTO(CompteWave compteWave) {
|
||||
if (compteWave == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
CompteWaveDTO dto = new CompteWaveDTO();
|
||||
dto.setId(compteWave.getId());
|
||||
dto.setNumeroTelephone(compteWave.getNumeroTelephone());
|
||||
dto.setStatutCompte(compteWave.getStatutCompte());
|
||||
dto.setWaveAccountId(compteWave.getWaveAccountId());
|
||||
dto.setEnvironnement(compteWave.getEnvironnement());
|
||||
dto.setDateDerniereVerification(compteWave.getDateDerniereVerification());
|
||||
dto.setCommentaire(compteWave.getCommentaire());
|
||||
|
||||
if (compteWave.getOrganisation() != null) {
|
||||
dto.setOrganisationId(compteWave.getOrganisation().getId());
|
||||
}
|
||||
if (compteWave.getMembre() != null) {
|
||||
dto.setMembreId(compteWave.getMembre().getId());
|
||||
}
|
||||
|
||||
dto.setDateCreation(compteWave.getDateCreation());
|
||||
dto.setDateModification(compteWave.getDateModification());
|
||||
dto.setActif(compteWave.getActif());
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
/** Convertit un DTO en entité CompteWave */
|
||||
private CompteWave convertToEntity(CompteWaveDTO dto) {
|
||||
if (dto == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
CompteWave compteWave = new CompteWave();
|
||||
compteWave.setNumeroTelephone(dto.getNumeroTelephone());
|
||||
compteWave.setStatutCompte(dto.getStatutCompte() != null ? dto.getStatutCompte() : StatutCompteWave.NON_VERIFIE);
|
||||
compteWave.setWaveAccountId(dto.getWaveAccountId());
|
||||
compteWave.setEnvironnement(dto.getEnvironnement() != null ? dto.getEnvironnement() : "SANDBOX");
|
||||
compteWave.setDateDerniereVerification(dto.getDateDerniereVerification());
|
||||
compteWave.setCommentaire(dto.getCommentaire());
|
||||
|
||||
// Relations
|
||||
if (dto.getOrganisationId() != null) {
|
||||
Organisation org =
|
||||
organisationRepository
|
||||
.findByIdOptional(dto.getOrganisationId())
|
||||
.orElseThrow(
|
||||
() ->
|
||||
new NotFoundException(
|
||||
"Organisation non trouvée avec l'ID: " + dto.getOrganisationId()));
|
||||
compteWave.setOrganisation(org);
|
||||
}
|
||||
|
||||
if (dto.getMembreId() != null) {
|
||||
Membre membre =
|
||||
membreRepository
|
||||
.findByIdOptional(dto.getMembreId())
|
||||
.orElseThrow(
|
||||
() -> new NotFoundException("Membre non trouvé avec l'ID: " + dto.getMembreId()));
|
||||
compteWave.setMembre(membre);
|
||||
}
|
||||
|
||||
return compteWave;
|
||||
}
|
||||
|
||||
/** Met à jour une entité CompteWave à partir d'un DTO */
|
||||
private void updateFromDTO(CompteWave compteWave, CompteWaveDTO dto) {
|
||||
if (dto.getStatutCompte() != null) {
|
||||
compteWave.setStatutCompte(dto.getStatutCompte());
|
||||
}
|
||||
if (dto.getWaveAccountId() != null) {
|
||||
compteWave.setWaveAccountId(dto.getWaveAccountId());
|
||||
}
|
||||
if (dto.getEnvironnement() != null) {
|
||||
compteWave.setEnvironnement(dto.getEnvironnement());
|
||||
}
|
||||
if (dto.getDateDerniereVerification() != null) {
|
||||
compteWave.setDateDerniereVerification(dto.getDateDerniereVerification());
|
||||
}
|
||||
if (dto.getCommentaire() != null) {
|
||||
compteWave.setCommentaire(dto.getCommentaire());
|
||||
}
|
||||
}
|
||||
|
||||
/** Convertit une entité TransactionWave en DTO */
|
||||
private TransactionWaveDTO convertToDTO(TransactionWave transactionWave) {
|
||||
if (transactionWave == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
TransactionWaveDTO dto = new TransactionWaveDTO();
|
||||
dto.setId(transactionWave.getId());
|
||||
dto.setWaveTransactionId(transactionWave.getWaveTransactionId());
|
||||
dto.setWaveRequestId(transactionWave.getWaveRequestId());
|
||||
dto.setWaveReference(transactionWave.getWaveReference());
|
||||
dto.setTypeTransaction(transactionWave.getTypeTransaction());
|
||||
dto.setStatutTransaction(transactionWave.getStatutTransaction());
|
||||
dto.setMontant(transactionWave.getMontant());
|
||||
dto.setFrais(transactionWave.getFrais());
|
||||
dto.setMontantNet(transactionWave.getMontantNet());
|
||||
dto.setCodeDevise(transactionWave.getCodeDevise());
|
||||
dto.setTelephonePayeur(transactionWave.getTelephonePayeur());
|
||||
dto.setTelephoneBeneficiaire(transactionWave.getTelephoneBeneficiaire());
|
||||
dto.setMetadonnees(transactionWave.getMetadonnees());
|
||||
dto.setNombreTentatives(transactionWave.getNombreTentatives());
|
||||
dto.setDateDerniereTentative(transactionWave.getDateDerniereTentative());
|
||||
dto.setMessageErreur(transactionWave.getMessageErreur());
|
||||
|
||||
if (transactionWave.getCompteWave() != null) {
|
||||
dto.setCompteWaveId(transactionWave.getCompteWave().getId());
|
||||
}
|
||||
|
||||
dto.setDateCreation(transactionWave.getDateCreation());
|
||||
dto.setDateModification(transactionWave.getDateModification());
|
||||
dto.setActif(transactionWave.getActif());
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
/** Convertit un DTO en entité TransactionWave */
|
||||
private TransactionWave convertToEntity(TransactionWaveDTO dto) {
|
||||
if (dto == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
TransactionWave transactionWave = new TransactionWave();
|
||||
transactionWave.setWaveTransactionId(dto.getWaveTransactionId());
|
||||
transactionWave.setWaveRequestId(dto.getWaveRequestId());
|
||||
transactionWave.setWaveReference(dto.getWaveReference());
|
||||
transactionWave.setTypeTransaction(dto.getTypeTransaction());
|
||||
transactionWave.setStatutTransaction(
|
||||
dto.getStatutTransaction() != null
|
||||
? dto.getStatutTransaction()
|
||||
: StatutTransactionWave.INITIALISE);
|
||||
transactionWave.setMontant(dto.getMontant());
|
||||
transactionWave.setFrais(dto.getFrais());
|
||||
transactionWave.setMontantNet(dto.getMontantNet());
|
||||
transactionWave.setCodeDevise(dto.getCodeDevise() != null ? dto.getCodeDevise() : "XOF");
|
||||
transactionWave.setTelephonePayeur(dto.getTelephonePayeur());
|
||||
transactionWave.setTelephoneBeneficiaire(dto.getTelephoneBeneficiaire());
|
||||
transactionWave.setMetadonnees(dto.getMetadonnees());
|
||||
transactionWave.setNombreTentatives(dto.getNombreTentatives() != null ? dto.getNombreTentatives() : 0);
|
||||
transactionWave.setDateDerniereTentative(dto.getDateDerniereTentative());
|
||||
transactionWave.setMessageErreur(dto.getMessageErreur());
|
||||
|
||||
// Relation CompteWave
|
||||
if (dto.getCompteWaveId() != null) {
|
||||
CompteWave compteWave =
|
||||
compteWaveRepository
|
||||
.findCompteWaveById(dto.getCompteWaveId())
|
||||
.orElseThrow(
|
||||
() ->
|
||||
new NotFoundException(
|
||||
"Compte Wave non trouvé avec l'ID: " + dto.getCompteWaveId()));
|
||||
transactionWave.setCompteWave(compteWave);
|
||||
}
|
||||
|
||||
return transactionWave;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user