- BackupService: DB-persisted metadata (BackupRecord/BackupConfig entities + V16 Flyway migration), real pg_dump execution via ProcessBuilder, soft-delete on deleteBackup, pg_restore manual guidance - OrganisationService: repartitionRegion now queries Adresse entities (was Map.of() stub) - SystemConfigService: in-memory config overrides via AtomicReference (no DB dependency) - SystemMetricsService: null-guard on MemoryMXBean in getSystemStatus() (fixes test NPE) - Souscription workflow: SouscriptionService, SouscriptionResource, FormuleAbonnementRepository, V11 Flyway migration, admin REST clients - Flyway V8-V15: notes membres, types référence, type orga constraint, seed roles, première connexion, Wave checkout URL, Wave telephone column length fix - .gitignore: added uploads/ and .claude/ Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
396 lines
15 KiB
Java
396 lines
15 KiB
Java
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<AdhesionResponse> getAllAdhesions(int page, int size) {
|
|
log.debug("Récupération des adhésions - page: {}, size: {}", page, size);
|
|
jakarta.persistence.TypedQuery<DemandeAdhesion> 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<AdhesionResponse> 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<AdhesionResponse> 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<AdhesionResponse> 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<AdhesionResponse> 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<String, Object> 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
|
|
}
|
|
}
|
|
}
|