package dev.lions.unionflow.server.service; import dev.lions.unionflow.server.api.dto.finance.request.CreateAdhesionRequest; import dev.lions.unionflow.server.api.dto.finance.request.UpdateAdhesionRequest; import dev.lions.unionflow.server.api.dto.finance.response.AdhesionResponse; import dev.lions.unionflow.server.entity.DemandeAdhesion; 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.MembreOrganisationRepository; import dev.lions.unionflow.server.repository.MembreRepository; import dev.lions.unionflow.server.repository.OrganisationRepository; import io.quarkus.security.identity.SecurityIdentity; 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.ForbiddenException; import jakarta.ws.rs.NotFoundException; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import org.eclipse.microprofile.jwt.JsonWebToken; /** * Service métier pour la gestion des demandes d'adhésion. * * @author UnionFlow Team * @version 2.0 * @since 2025-02-18 */ @ApplicationScoped @Slf4j public class AdhesionService { @Inject AdhesionRepository adhesionRepository; @Inject MembreRepository membreRepository; @Inject OrganisationRepository organisationRepository; @Inject MembreOrganisationRepository membreOrganisationRepository; @Inject MembreKeycloakSyncService keycloakSyncService; @Inject DefaultsService defaultsService; @Inject SecurityIdentity securityIdentity; @Inject JsonWebToken jwt; public List getAllAdhesions(int page, int size) { log.debug("Récupération des adhésions - page: {}, size: {}", page, size); jakarta.persistence.TypedQuery query = adhesionRepository .getEntityManager() .createQuery( "SELECT a FROM DemandeAdhesion a ORDER BY a.dateDemande DESC", DemandeAdhesion.class); query.setFirstResult(page * size); query.setMaxResults(size); return query.getResultList().stream().map(this::convertToDTO).collect(Collectors.toList()); } public AdhesionResponse getAdhesionById(@NotNull UUID id) { log.debug("Récupération de l'adhésion avec ID: {}", id); DemandeAdhesion adhesion = adhesionRepository .findByIdOptional(id) .orElseThrow(() -> new NotFoundException("Adhésion non trouvée avec l'ID: " + id)); return convertToDTO(adhesion); } public AdhesionResponse getAdhesionByReference(@NotNull String numeroReference) { log.debug("Récupération de l'adhésion avec référence: {}", numeroReference); DemandeAdhesion adhesion = adhesionRepository .findByNumeroReference(numeroReference) .orElseThrow( () -> new NotFoundException( "Adhésion non trouvée avec la référence: " + numeroReference)); return convertToDTO(adhesion); } @Transactional public AdhesionResponse createAdhesion(@Valid CreateAdhesionRequest request) { log.info( "Création d'une nouvelle adhésion pour le membre: {} et l'organisation: {}", request.membreId(), request.organisationId()); Membre membre = membreRepository .findByIdOptional(request.membreId()) .orElseThrow( () -> new NotFoundException( "Membre non trouvé avec l'ID: " + request.membreId())); Organisation organisation = organisationRepository .findByIdOptional(request.organisationId()) .orElseThrow( () -> new NotFoundException( "Organisation non trouvée avec l'ID: " + request.organisationId())); DemandeAdhesion adhesion = convertToEntity(request); adhesion.setUtilisateur(membre); adhesion.setOrganisation(organisation); adhesionRepository.persist(adhesion); log.info( "Adhésion créée avec succès - ID: {}, Référence: {}", adhesion.getId(), adhesion.getNumeroReference()); return convertToDTO(adhesion); } @Transactional public AdhesionResponse updateAdhesion(@NotNull UUID id, @Valid UpdateAdhesionRequest request) { log.info("Mise à jour de l'adhésion avec ID: {}", id); DemandeAdhesion adhesionExistante = adhesionRepository .findByIdOptional(id) .orElseThrow(() -> new NotFoundException("Adhésion non trouvée avec l'ID: " + id)); updateAdhesionFields(adhesionExistante, request); log.info("Adhésion mise à jour avec succès - ID: {}", id); return convertToDTO(adhesionExistante); } @Transactional public void deleteAdhesion(@NotNull UUID id) { log.info("Suppression de l'adhésion avec ID: {}", id); DemandeAdhesion adhesion = adhesionRepository .findByIdOptional(id) .orElseThrow(() -> new NotFoundException("Adhésion non trouvée avec l'ID: " + id)); if ("APPROUVEE".equals(adhesion.getStatut()) && adhesion.isPayeeIntegralement()) { throw new IllegalStateException("Impossible de supprimer une adhésion déjà payée intégralement"); } adhesion.setStatut("ANNULEE"); log.info("Adhésion annulée avec succès - ID: {}", id); } @Transactional public AdhesionResponse approuverAdhesion(@NotNull UUID id, String approuvePar) { log.info("Approbation de l'adhésion avec ID: {}", id); DemandeAdhesion adhesion = adhesionRepository .findByIdOptional(id) .orElseThrow(() -> new NotFoundException("Adhésion non trouvée avec l'ID: " + id)); verifierAccesOrganisation(adhesion); if (!adhesion.isEnAttente()) { throw new IllegalStateException("Seules les adhésions en attente peuvent être approuvées"); } adhesion.setStatut("APPROUVEE"); adhesion.setDateTraitement(LocalDateTime.now()); adhesion.setObservations( approuvePar != null ? "Approuvée par : " + approuvePar : adhesion.getObservations()); // Activer le compte membre et provisionner son accès Keycloak Membre membre = adhesion.getUtilisateur(); if (membre != null) { membre.setStatutCompte("ACTIF"); membre.setActif(true); try { keycloakSyncService.provisionKeycloakUser(membre.getId()); log.info("Compte Keycloak provisionné pour le membre: {}", membre.getEmail()); } catch (Exception e) { log.warn("Provisionnement Keycloak non bloquant pour {} : {}", membre.getEmail(), e.getMessage()); } } log.info("Adhésion approuvée avec succès - ID: {}", id); return convertToDTO(adhesion); } @Transactional public AdhesionResponse rejeterAdhesion(@NotNull UUID id, String motifRejet) { log.info("Rejet de l'adhésion avec ID: {}", id); DemandeAdhesion adhesion = adhesionRepository .findByIdOptional(id) .orElseThrow(() -> new NotFoundException("Adhésion non trouvée avec l'ID: " + id)); verifierAccesOrganisation(adhesion); if (!adhesion.isEnAttente()) { throw new IllegalStateException("Seules les adhésions en attente peuvent être rejetées"); } adhesion.setStatut("REJETEE"); adhesion.setMotifRejet(motifRejet); adhesion.setDateTraitement(LocalDateTime.now()); // Désactiver le compte membre Membre membre = adhesion.getUtilisateur(); if (membre != null) { membre.setStatutCompte("DESACTIVE"); membre.setActif(false); } log.info("Adhésion rejetée avec succès - ID: {}", id); return convertToDTO(adhesion); } @Transactional public AdhesionResponse enregistrerPaiement( @NotNull UUID id, BigDecimal montantPaye, String methodePaiement, String referencePaiement) { log.info("Enregistrement du paiement pour l'adhésion avec ID: {}", id); DemandeAdhesion 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 receive un paiement"); } adhesion.setMontantPaye(adhesion.getMontantPaye().add(montantPaye)); if (adhesion.isPayeeIntegralement()) { adhesion.setStatut("APPROUVEE"); } log.info("Paiement enregistré avec succès pour l'adhésion - ID: {}", id); return convertToDTO(adhesion); } public List 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); } return adhesionRepository.findByMembreId(membreId).stream() .skip((long) page * size) .limit(size) .map(this::convertToDTO) .collect(Collectors.toList()); } public List 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); } return adhesionRepository.findByOrganisationId(organisationId).stream() .skip((long) page * size) .limit(size) .map(this::convertToDTO) .collect(Collectors.toList()); } public List getAdhesionsByStatut(@NotNull String statut, int page, int size) { log.debug("Récupération des adhésions avec statut: {}", statut); return adhesionRepository.findByStatut(statut).stream() .skip((long) page * size) .limit(size) .map(this::convertToDTO) .collect(Collectors.toList()); } public List getAdhesionsEnAttente(int page, int size) { log.debug("Récupération des adhésions en attente"); return adhesionRepository.findEnAttente().stream() .skip((long) page * size) .limit(size) .map(this::convertToDTO) .collect(Collectors.toList()); } public Map getStatistiquesAdhesions() { log.debug("Calcul des statistiques des adhésions"); long total = adhesionRepository.count(); long approuvees = adhesionRepository.findByStatut("APPROUVEE").size(); long enAttente = adhesionRepository.findEnAttente().size(); long rejetees = adhesionRepository.findByStatut("REJETEE").size(); return Map.of( "totalAdhesions", total, "adhesionsApprouvees", approuvees, "adhesionsEnAttente", enAttente, "adhesionsRejetees", rejetees, "tauxApprobation", total > 0 ? (approuvees * 100.0 / total) : 0.0, "tauxRejet", total > 0 ? (rejetees * 100.0 / total) : 0.0); } /** * Vérifie que l'ADMIN_ORGANISATION n'agit que sur les adhésions de sa propre organisation. * Les rôles SUPER_ADMIN et ADMIN ont accès sans restriction. */ private void verifierAccesOrganisation(DemandeAdhesion adhesion) { if (!securityIdentity.hasRole("ADMIN_ORGANISATION")) { return; // SUPER_ADMIN / ADMIN : accès libre } UUID adhesionOrgId = adhesion.getOrganisation() != null ? adhesion.getOrganisation().getId() : null; if (adhesionOrgId == null) { throw new ForbiddenException("L'adhésion n'est rattachée à aucune organisation"); } String keycloakSubject = jwt.getSubject(); Membre adminMembre = membreRepository.findByKeycloakUserId(keycloakSubject) .orElseThrow(() -> new ForbiddenException("Compte admin introuvable pour le sujet JWT: " + keycloakSubject)); boolean appartient = membreOrganisationRepository .findByMembreIdAndOrganisationId(adminMembre.getId(), adhesionOrgId) .isPresent(); if (!appartient) { log.warn("ADMIN_ORGANISATION {} tente d'agir sur une adhésion de l'organisation {} qui n'est pas la sienne", keycloakSubject, adhesionOrgId); throw new ForbiddenException("Vous ne pouvez gérer que les adhésions de votre organisation"); } } private AdhesionResponse convertToDTO(DemandeAdhesion adhesion) { AdhesionResponse response = new AdhesionResponse(); response.setId(adhesion.getId()); response.setNumeroReference(adhesion.getNumeroReference()); if (adhesion.getUtilisateur() != null) { response.setMembreId(adhesion.getUtilisateur().getId()); response.setNomMembre(adhesion.getUtilisateur().getNomComplet()); response.setNumeroMembre(adhesion.getUtilisateur().getNumeroMembre()); response.setEmailMembre(adhesion.getUtilisateur().getEmail()); } if (adhesion.getOrganisation() != null) { response.setOrganisationId(adhesion.getOrganisation().getId()); response.setNomOrganisation(adhesion.getOrganisation().getNom()); } response.setDateDemande( adhesion.getDateDemande() != null ? adhesion.getDateDemande().toLocalDate() : null); response.setFraisAdhesion(adhesion.getFraisAdhesion()); response.setMontantPaye(adhesion.getMontantPaye()); response.setCodeDevise(adhesion.getCodeDevise()); response.setStatut(adhesion.getStatut()); response.setMotifRejet(adhesion.getMotifRejet()); response.setObservations(adhesion.getObservations()); if (adhesion.getDateTraitement() != null) { response.setDateApprobation(adhesion.getDateTraitement().toLocalDate()); } if (adhesion.getTraitePar() != null) { response.setApprouvePar(adhesion.getTraitePar().getNomComplet()); } response.setDateCreation(adhesion.getDateCreation()); response.setDateModification(adhesion.getDateModification()); response.setCreePar(adhesion.getCreePar()); response.setModifiePar(adhesion.getModifiePar()); response.setActif(adhesion.getActif()); return response; } private DemandeAdhesion convertToEntity(CreateAdhesionRequest request) { return DemandeAdhesion.builder() .numeroReference(request.numeroReference()) .dateDemande(request.dateDemande().atStartOfDay()) .fraisAdhesion(request.fraisAdhesion()) .montantPaye(BigDecimal.ZERO) .codeDevise(request.codeDevise()) .statut("EN_ATTENTE") .observations(request.observations()) .build(); } private void updateAdhesionFields(DemandeAdhesion adhesion, UpdateAdhesionRequest request) { if (request.statut() != null) adhesion.setStatut(request.statut()); if (request.montantPaye() != null) adhesion.setMontantPaye(request.montantPaye()); if (request.motifRejet() != null) adhesion.setMotifRejet(request.motifRejet()); if (request.observations() != null) adhesion.setObservations(request.observations()); if (request.dateApprobation() != null) { adhesion.setDateTraitement(request.dateApprobation().atStartOfDay()); } if (request.dateValidation() != null) { // Logic for validation date if needed } } }