883 lines
36 KiB
Java
883 lines
36 KiB
Java
package dev.lions.unionflow.server.service;
|
|
|
|
import dev.lions.unionflow.server.api.dto.cotisation.request.CreateCotisationRequest;
|
|
import dev.lions.unionflow.server.api.dto.cotisation.request.UpdateCotisationRequest;
|
|
import dev.lions.unionflow.server.api.dto.cotisation.response.CotisationResponse;
|
|
import dev.lions.unionflow.server.api.dto.cotisation.response.CotisationSummaryResponse;
|
|
import dev.lions.unionflow.server.entity.Cotisation;
|
|
import dev.lions.unionflow.server.entity.Membre;
|
|
import dev.lions.unionflow.server.entity.Organisation;
|
|
import dev.lions.unionflow.server.repository.CotisationRepository;
|
|
import dev.lions.unionflow.server.repository.MembreRepository;
|
|
import dev.lions.unionflow.server.repository.OrganisationRepository;
|
|
import dev.lions.unionflow.server.service.support.SecuriteHelper;
|
|
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.text.NumberFormat;
|
|
import java.time.LocalDate;
|
|
import java.time.format.DateTimeFormatter;
|
|
import java.util.Collections;
|
|
import java.util.LinkedHashSet;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
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;
|
|
|
|
@Inject
|
|
OrganisationRepository organisationRepository;
|
|
|
|
@Inject
|
|
DefaultsService defaultsService;
|
|
|
|
@Inject
|
|
SecuriteHelper securiteHelper;
|
|
|
|
@Inject
|
|
OrganisationService organisationService;
|
|
|
|
/**
|
|
* 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 Summary Response
|
|
*/
|
|
public List<CotisationSummaryResponse> getAllCotisations(int page, int size) {
|
|
log.debug("Récupération des cotisations - page: {}, size: {}", page, size);
|
|
|
|
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::convertToSummaryResponse).collect(Collectors.toList());
|
|
}
|
|
|
|
/**
|
|
* Récupère une cotisation par son ID.
|
|
*
|
|
* @param id identifiant UUID de la cotisation
|
|
* @return Response de la cotisation
|
|
* @throws NotFoundException si la cotisation n'existe pas
|
|
*/
|
|
public CotisationResponse 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 convertToResponse(cotisation);
|
|
}
|
|
|
|
/**
|
|
* Récupère une cotisation par son numéro de référence.
|
|
*
|
|
* @param numeroReference numéro de référence unique
|
|
* @return Response de la cotisation
|
|
* @throws NotFoundException si la cotisation n'existe pas
|
|
*/
|
|
public CotisationResponse 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 convertToResponse(cotisation);
|
|
}
|
|
|
|
/**
|
|
* Crée une nouvelle cotisation.
|
|
*
|
|
* @param request données de la cotisation à créer
|
|
* @return Response de la cotisation créée
|
|
*/
|
|
@Transactional
|
|
public CotisationResponse createCotisation(@Valid CreateCotisationRequest request) {
|
|
log.info("Création d'une nouvelle cotisation pour le membre: {}", request.membreId());
|
|
|
|
// Validation du membre
|
|
Membre membre = membreRepository
|
|
.findByIdOptional(request.membreId())
|
|
.orElseThrow(
|
|
() -> new NotFoundException(
|
|
"Membre non trouvé avec l'ID: " + request.membreId()));
|
|
|
|
// Validation de l'organisation
|
|
Organisation organisation = organisationRepository
|
|
.findByIdOptional(request.organisationId())
|
|
.orElseThrow(
|
|
() -> new NotFoundException(
|
|
"Organisation non trouvée avec l'ID: " + request.organisationId()));
|
|
|
|
// Conversion Request vers entité
|
|
Cotisation cotisation = Cotisation.builder()
|
|
.typeCotisation(request.typeCotisation())
|
|
.libelle(request.libelle())
|
|
.description(request.description())
|
|
.montantDu(request.montantDu())
|
|
.montantPaye(BigDecimal.ZERO)
|
|
.codeDevise(request.codeDevise() != null ? request.codeDevise() : defaultsService.getDevise())
|
|
.statut("EN_ATTENTE")
|
|
.dateEcheance(request.dateEcheance())
|
|
.periode(request.periode())
|
|
.annee(request.annee() != null ? request.annee() : LocalDate.now().getYear())
|
|
.mois(request.mois())
|
|
.recurrente(request.recurrente() != null ? request.recurrente() : false)
|
|
.observations(request.observations())
|
|
.membre(membre)
|
|
.organisation(organisation)
|
|
.build();
|
|
|
|
// Génération du numéro de référence (si pas encore géré par PrePersist ou
|
|
// builder)
|
|
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 convertToResponse(cotisation);
|
|
}
|
|
|
|
/**
|
|
* Met à jour une cotisation existante.
|
|
*
|
|
* @param id identifiant UUID de la cotisation
|
|
* @param request nouvelles données
|
|
* @return Response de la cotisation mise à jour
|
|
*/
|
|
@Transactional
|
|
public CotisationResponse updateCotisation(@NotNull UUID id, @Valid UpdateCotisationRequest request) {
|
|
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
|
|
if (request.libelle() != null)
|
|
cotisationExistante.setLibelle(request.libelle());
|
|
if (request.description() != null)
|
|
cotisationExistante.setDescription(request.description());
|
|
if (request.montantDu() != null)
|
|
cotisationExistante.setMontantDu(request.montantDu());
|
|
if (request.dateEcheance() != null)
|
|
cotisationExistante.setDateEcheance(request.dateEcheance());
|
|
if (request.observations() != null)
|
|
cotisationExistante.setObservations(request.observations());
|
|
if (request.statut() != null)
|
|
cotisationExistante.setStatut(request.statut());
|
|
if (request.annee() != null)
|
|
cotisationExistante.setAnnee(request.annee());
|
|
if (request.mois() != null)
|
|
cotisationExistante.setMois(request.mois());
|
|
if (request.recurrente() != null)
|
|
cotisationExistante.setRecurrente(request.recurrente());
|
|
|
|
// Validation des règles métier
|
|
validateCotisationRules(cotisationExistante);
|
|
|
|
log.info("Cotisation mise à jour avec succès - ID: {}", id);
|
|
|
|
return convertToResponse(cotisationExistante);
|
|
}
|
|
|
|
/**
|
|
* Enregistre le paiement d'une cotisation.
|
|
*/
|
|
@Transactional
|
|
public CotisationResponse enregistrerPaiement(
|
|
@NotNull UUID id,
|
|
BigDecimal montantPaye,
|
|
LocalDate datePaiement,
|
|
String modePaiement,
|
|
String reference) {
|
|
log.info("Enregistrement du paiement pour la cotisation ID: {}", id);
|
|
|
|
Cotisation cotisation = cotisationRepository
|
|
.findByIdOptional(id)
|
|
.orElseThrow(() -> new NotFoundException("Cotisation non trouvée avec l'ID: " + id));
|
|
|
|
if (montantPaye != null) {
|
|
cotisation.setMontantPaye(montantPaye);
|
|
}
|
|
if (datePaiement != null) {
|
|
cotisation.setDatePaiement(java.time.LocalDateTime.of(datePaiement, java.time.LocalTime.MIDNIGHT));
|
|
}
|
|
|
|
// Déterminer le statut en fonction du montant payé
|
|
if (cotisation.getMontantPaye() != null && cotisation.getMontantDu() != null
|
|
&& cotisation.getMontantPaye().compareTo(cotisation.getMontantDu()) >= 0) {
|
|
cotisation.setStatut("PAYEE");
|
|
} else if (cotisation.getMontantPaye() != null
|
|
&& cotisation.getMontantPaye().compareTo(BigDecimal.ZERO) > 0) {
|
|
cotisation.setStatut("PARTIELLEMENT_PAYEE");
|
|
}
|
|
|
|
log.info("Paiement enregistré - ID: {}, Statut: {}", id, cotisation.getStatut());
|
|
return convertToResponse(cotisation);
|
|
}
|
|
|
|
/**
|
|
* Supprime (annule) 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));
|
|
|
|
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.
|
|
*/
|
|
public List<CotisationSummaryResponse> getCotisationsByMembre(@NotNull UUID membreId, int page, int size) {
|
|
log.debug("Récupération des cotisations du membre: {}", membreId);
|
|
|
|
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::convertToSummaryResponse).collect(Collectors.toList());
|
|
}
|
|
|
|
/**
|
|
* Récupère les cotisations par statut.
|
|
*/
|
|
public List<CotisationSummaryResponse> 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::convertToSummaryResponse).collect(Collectors.toList());
|
|
}
|
|
|
|
/**
|
|
* Récupère les cotisations en retard.
|
|
*/
|
|
public List<CotisationSummaryResponse> 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::convertToSummaryResponse).collect(Collectors.toList());
|
|
}
|
|
|
|
/**
|
|
* Recherche avancée de cotisations.
|
|
*/
|
|
public List<CotisationSummaryResponse> 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::convertToSummaryResponse).collect(Collectors.toList());
|
|
}
|
|
|
|
/**
|
|
* Statistiques par période.
|
|
*/
|
|
public Map<String, Object> getStatistiquesPeriode(int annee, Integer mois) {
|
|
return cotisationRepository.getStatistiquesPeriode(annee, mois);
|
|
}
|
|
|
|
/**
|
|
* Statistiques globales.
|
|
*/
|
|
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();
|
|
BigDecimal montantTotalPaye = cotisationRepository.sommeMontantPayeParStatut("PAYEE");
|
|
|
|
BigDecimal totalMontant = cotisationRepository.sommeMontantDu();
|
|
Map<String, Object> map = new java.util.HashMap<>();
|
|
map.put("totalCotisations", totalCotisations);
|
|
map.put("cotisationsPayees", cotisationsPayees);
|
|
map.put("cotisationsEnRetard", cotisationsEnRetard);
|
|
map.put("tauxPaiement", totalCotisations > 0 ? (cotisationsPayees * 100.0 / totalCotisations) : 0.0);
|
|
map.put("montantTotalPaye", montantTotalPaye != null ? montantTotalPaye : BigDecimal.ZERO);
|
|
map.put("totalMontant", totalMontant != null ? totalMontant : BigDecimal.ZERO);
|
|
return map;
|
|
}
|
|
|
|
/**
|
|
* Convertit une entité Cotisation en Response DTO.
|
|
*/
|
|
private CotisationResponse convertToResponse(Cotisation cotisation) {
|
|
if (cotisation == null)
|
|
return null;
|
|
|
|
CotisationResponse response = new CotisationResponse();
|
|
response.setId(cotisation.getId());
|
|
response.setNumeroReference(cotisation.getNumeroReference());
|
|
|
|
if (cotisation.getMembre() != null) {
|
|
dev.lions.unionflow.server.entity.Membre m = cotisation.getMembre();
|
|
response.setMembreId(m.getId());
|
|
String nomComplet = m.getNomComplet();
|
|
response.setNomMembre(nomComplet);
|
|
response.setNomCompletMembre(nomComplet);
|
|
response.setNumeroMembre(m.getNumeroMembre());
|
|
response.setInitialesMembre(buildInitiales(m.getPrenom(), m.getNom()));
|
|
response.setTypeMembre(getTypeMembreLibelle(m.getStatutCompte()));
|
|
}
|
|
|
|
if (cotisation.getOrganisation() != null) {
|
|
dev.lions.unionflow.server.entity.Organisation o = cotisation.getOrganisation();
|
|
response.setOrganisationId(o.getId());
|
|
response.setNomOrganisation(o.getNom());
|
|
response.setRegionOrganisation(o.getRegion());
|
|
response.setIconeOrganisation(getIconeOrganisation(o.getTypeOrganisation()));
|
|
}
|
|
|
|
response.setTypeCotisation(cotisation.getTypeCotisation());
|
|
response.setType(cotisation.getTypeCotisation());
|
|
response.setTypeCotisationLibelle(getTypeCotisationLibelle(cotisation.getTypeCotisation()));
|
|
response.setTypeLibelle(getTypeCotisationLibelle(cotisation.getTypeCotisation()));
|
|
response.setTypeSeverity(getTypeCotisationSeverity(cotisation.getTypeCotisation()));
|
|
response.setTypeIcon(getTypeCotisationIcon(cotisation.getTypeCotisation()));
|
|
response.setLibelle(cotisation.getLibelle());
|
|
response.setDescription(cotisation.getDescription());
|
|
response.setMontantDu(cotisation.getMontantDu());
|
|
response.setMontant(cotisation.getMontantDu());
|
|
response.setMontantFormatte(formatMontant(cotisation.getMontantDu()));
|
|
response.setMontantPaye(cotisation.getMontantPaye());
|
|
response.setMontantRestant(cotisation.getMontantRestant());
|
|
response.setCodeDevise(cotisation.getCodeDevise());
|
|
response.setStatut(cotisation.getStatut());
|
|
response.setStatutLibelle(getStatutLibelle(cotisation.getStatut()));
|
|
response.setStatutSeverity(getStatutSeverity(cotisation.getStatut()));
|
|
response.setStatutIcon(getStatutIcon(cotisation.getStatut()));
|
|
response.setDateEcheance(cotisation.getDateEcheance());
|
|
response.setDateEcheanceFormattee(cotisation.getDateEcheance() != null
|
|
? cotisation.getDateEcheance().format(DateTimeFormatter.ofPattern("dd/MM/yyyy", Locale.FRANCE))
|
|
: null);
|
|
response.setDatePaiement(cotisation.getDatePaiement());
|
|
response.setDatePaiementFormattee(cotisation.getDatePaiement() != null
|
|
? cotisation.getDatePaiement().format(DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm", Locale.FRANCE))
|
|
: null);
|
|
if (cotisation.isEnRetard() && cotisation.getDateEcheance() != null) {
|
|
long jours = java.time.temporal.ChronoUnit.DAYS.between(cotisation.getDateEcheance(), LocalDate.now());
|
|
response.setRetardCouleur("text-red-500");
|
|
response.setRetardTexte(jours + " jour" + (jours > 1 ? "s" : "") + " de retard");
|
|
} else {
|
|
response.setRetardCouleur("text-green-600");
|
|
response.setRetardTexte(cotisation.getStatut() != null && "PAYEE".equals(cotisation.getStatut()) ? "Payée" : "À jour");
|
|
}
|
|
response.setModePaiementIcon(getModePaiementIcon(null));
|
|
response.setModePaiementLibelle(getModePaiementLibelle(null));
|
|
response.setPeriode(cotisation.getPeriode());
|
|
response.setAnnee(cotisation.getAnnee());
|
|
response.setMois(cotisation.getMois());
|
|
response.setObservations(cotisation.getObservations());
|
|
response.setRecurrente(cotisation.getRecurrente());
|
|
response.setNombreRappels(cotisation.getNombreRappels());
|
|
response.setDateDernierRappel(cotisation.getDateDernierRappel());
|
|
response.setValideParId(cotisation.getValideParId());
|
|
response.setNomValidateur(cotisation.getNomValidateur());
|
|
response.setDateValidation(cotisation.getDateValidation());
|
|
|
|
if (cotisation.getMontantDu() != null && cotisation.getMontantDu().compareTo(BigDecimal.ZERO) > 0) {
|
|
BigDecimal paye = cotisation.getMontantPaye() != null ? cotisation.getMontantPaye() : BigDecimal.ZERO;
|
|
response.setPourcentagePaiement(paye.multiply(BigDecimal.valueOf(100))
|
|
.divide(cotisation.getMontantDu(), 0, java.math.RoundingMode.HALF_UP).intValue());
|
|
} else {
|
|
response.setPourcentagePaiement(0);
|
|
}
|
|
|
|
response.setEnRetard(cotisation.isEnRetard());
|
|
if (cotisation.isEnRetard()) {
|
|
response
|
|
.setJoursRetard(java.time.temporal.ChronoUnit.DAYS.between(cotisation.getDateEcheance(), LocalDate.now()));
|
|
} else {
|
|
response.setJoursRetard(0L);
|
|
}
|
|
|
|
response.setDateCreation(cotisation.getDateCreation());
|
|
response.setDateModification(cotisation.getDateModification());
|
|
response.setCreePar(cotisation.getCreePar());
|
|
response.setModifiePar(cotisation.getModifiePar());
|
|
response.setVersion(cotisation.getVersion());
|
|
response.setActif(cotisation.getActif());
|
|
|
|
return response;
|
|
}
|
|
|
|
/**
|
|
* Convertit une entité Cotisation en Summary Response.
|
|
*/
|
|
private CotisationSummaryResponse convertToSummaryResponse(Cotisation cotisation) {
|
|
if (cotisation == null)
|
|
return null;
|
|
return new CotisationSummaryResponse(
|
|
cotisation.getId(),
|
|
cotisation.getNumeroReference(),
|
|
cotisation.getMembre() != null ? cotisation.getMembre().getNomComplet() : "Inconnu",
|
|
cotisation.getMontantDu(),
|
|
cotisation.getMontantPaye(),
|
|
cotisation.getStatut(),
|
|
getStatutLibelle(cotisation.getStatut()),
|
|
cotisation.getDateEcheance(),
|
|
cotisation.getAnnee(),
|
|
cotisation.getActif());
|
|
}
|
|
|
|
private String getTypeCotisationLibelle(String code) {
|
|
if (code == null)
|
|
return "Non défini";
|
|
return switch (code) {
|
|
case "MENSUELLE" -> "Mensuelle";
|
|
case "TRIMESTRIELLE" -> "Trimestrielle";
|
|
case "SEMESTRIELLE" -> "Semestrielle";
|
|
case "ANNUELLE" -> "Annuelle";
|
|
case "EXCEPTIONNELLE" -> "Exceptionnelle";
|
|
case "ADHESION" -> "Adhésion";
|
|
default -> code;
|
|
};
|
|
}
|
|
|
|
private String getStatutLibelle(String code) {
|
|
if (code == null)
|
|
return "Non défini";
|
|
return switch (code) {
|
|
case "EN_ATTENTE" -> "En attente";
|
|
case "PAYEE" -> "Payée";
|
|
case "PARTIELLEMENT_PAYEE" -> "Partiellement payée";
|
|
case "EN_RETARD" -> "En retard";
|
|
case "ANNULEE" -> "Annulée";
|
|
default -> code;
|
|
};
|
|
}
|
|
|
|
private static String buildInitiales(String prenom, String nom) {
|
|
if (prenom == null && nom == null)
|
|
return "—";
|
|
String p = prenom != null && !prenom.isEmpty() ? prenom.substring(0, 1).toUpperCase() : "";
|
|
String n = nom != null && !nom.isEmpty() ? nom.substring(0, 1).toUpperCase() : "";
|
|
return (p + n).isEmpty() ? "—" : p + n;
|
|
}
|
|
|
|
private static String getTypeMembreLibelle(String statutCompte) {
|
|
if (statutCompte == null)
|
|
return "";
|
|
return switch (statutCompte) {
|
|
case "ACTIF" -> "Actif";
|
|
case "EN_ATTENTE_VALIDATION" -> "En attente";
|
|
case "SUSPENDU" -> "Suspendu";
|
|
case "RADIE" -> "Radié";
|
|
case "INACTIF" -> "Inactif";
|
|
default -> statutCompte;
|
|
};
|
|
}
|
|
|
|
private static String getIconeOrganisation(String typeOrganisation) {
|
|
if (typeOrganisation == null)
|
|
return "pi-building";
|
|
return switch (typeOrganisation.toUpperCase()) {
|
|
case "ASSOCIATION", "ONG" -> "pi-users";
|
|
case "CLUB" -> "pi-star";
|
|
case "COOPERATIVE" -> "pi-briefcase";
|
|
default -> "pi-building";
|
|
};
|
|
}
|
|
|
|
/** Sévérité PrimeFaces pour le type de cotisation (p:tag). */
|
|
private static String getTypeCotisationSeverity(String typeCotisation) {
|
|
if (typeCotisation == null)
|
|
return "secondary";
|
|
return switch (typeCotisation.toUpperCase()) {
|
|
case "ANNUELLE", "ADHESION" -> "success";
|
|
case "MENSUELLE", "TRIMESTRIELLE" -> "info";
|
|
case "EXCEPTIONNELLE" -> "warn";
|
|
default -> "secondary";
|
|
};
|
|
}
|
|
|
|
/** Icône PrimeFaces pour le type de cotisation. */
|
|
private static String getTypeCotisationIcon(String typeCotisation) {
|
|
if (typeCotisation == null)
|
|
return "pi-tag";
|
|
return switch (typeCotisation.toUpperCase()) {
|
|
case "MENSUELLE" -> "pi-calendar";
|
|
case "ANNUELLE" -> "pi-star";
|
|
case "ADHESION" -> "pi-user-plus";
|
|
default -> "pi-tag";
|
|
};
|
|
}
|
|
|
|
/** Sévérité PrimeFaces pour le statut (p:tag). */
|
|
private static String getStatutSeverity(String statut) {
|
|
if (statut == null)
|
|
return "secondary";
|
|
return switch (statut.toUpperCase()) {
|
|
case "PAYEE" -> "success";
|
|
case "EN_ATTENTE", "PARTIELLEMENT_PAYEE" -> "info";
|
|
case "EN_RETARD" -> "error";
|
|
case "ANNULEE" -> "secondary";
|
|
default -> "secondary";
|
|
};
|
|
}
|
|
|
|
/** Icône PrimeFaces pour le statut. */
|
|
private static String getStatutIcon(String statut) {
|
|
if (statut == null)
|
|
return "pi-circle";
|
|
return switch (statut.toUpperCase()) {
|
|
case "PAYEE" -> "pi-check";
|
|
case "EN_ATTENTE" -> "pi-clock";
|
|
case "EN_RETARD" -> "pi-exclamation-triangle";
|
|
case "PARTIELLEMENT_PAYEE" -> "pi-percentage";
|
|
case "ANNULEE" -> "pi-times";
|
|
default -> "pi-circle";
|
|
};
|
|
}
|
|
|
|
private static String formatMontant(BigDecimal montant) {
|
|
if (montant == null)
|
|
return "";
|
|
return NumberFormat.getNumberInstance(Locale.FRANCE).format(montant.longValue());
|
|
}
|
|
|
|
private static String getModePaiementIcon(String methode) {
|
|
if (methode == null)
|
|
return "pi-wallet";
|
|
return switch (methode.toUpperCase()) {
|
|
case "WAVE_MONEY", "MOBILE_MONEY" -> "pi-mobile";
|
|
case "VIREMENT" -> "pi-arrow-right-arrow-left";
|
|
case "ESPECES" -> "pi-money-bill";
|
|
case "CARTE" -> "pi-credit-card";
|
|
default -> "pi-wallet";
|
|
};
|
|
}
|
|
|
|
private static String getModePaiementLibelle(String methode) {
|
|
if (methode == null)
|
|
return "—";
|
|
return switch (methode.toUpperCase()) {
|
|
case "WAVE_MONEY" -> "Wave Money";
|
|
case "MOBILE_MONEY" -> "Mobile Money";
|
|
case "VIREMENT" -> "Virement";
|
|
case "ESPECES" -> "Espèces";
|
|
case "CARTE" -> "Carte";
|
|
default -> methode;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Valide les règles métier pour une cotisation.
|
|
*/
|
|
private void validateCotisationRules(Cotisation cotisation) {
|
|
if (cotisation.getMontantDu().compareTo(BigDecimal.ZERO) <= 0) {
|
|
throw new IllegalArgumentException("Le montant dû doit être positif");
|
|
}
|
|
if (cotisation.getDateEcheance().isBefore(LocalDate.now().minusYears(1))) {
|
|
throw new IllegalArgumentException("La date d'échéance ne peut pas être antérieure à un an");
|
|
}
|
|
if (cotisation.getMontantPaye().compareTo(cotisation.getMontantDu()) > 0) {
|
|
throw new IllegalArgumentException("Le montant payé ne peut pas dépasser le montant dû");
|
|
}
|
|
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.
|
|
*/
|
|
@Transactional
|
|
public int envoyerRappelsCotisationsGroupes(List<UUID> membreIds) {
|
|
if (membreIds == null || membreIds.isEmpty()) {
|
|
throw new IllegalArgumentException("La liste des membres ne peut pas être vide");
|
|
}
|
|
log.info("Envoi de rappels de cotisations groupés à {} membres", membreIds.size());
|
|
|
|
int rappelsEnvoyes = 0;
|
|
for (UUID membreId : membreIds) {
|
|
try {
|
|
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) {
|
|
cotisationRepository.incrementerNombreRappels(cotisation.getId());
|
|
rappelsEnvoyes++;
|
|
}
|
|
} catch (Exception e) {
|
|
log.warn("Erreur lors de l'envoi du rappel pour le membre {}: {}", membreId, e.getMessage());
|
|
}
|
|
}
|
|
return rappelsEnvoyes;
|
|
}
|
|
|
|
/**
|
|
* Récupère le membre connecté via SecurityIdentity.
|
|
* Méthode helper réutilisable (Pattern DRY).
|
|
*
|
|
* @return Membre connecté
|
|
* @throws NotFoundException si le membre n'est pas trouvé
|
|
*/
|
|
private Membre getMembreConnecte() {
|
|
String email = securiteHelper.resolveEmail();
|
|
log.debug("Récupération du membre connecté: {}", email);
|
|
|
|
return membreRepository.findByEmail(email)
|
|
.orElseThrow(() -> new NotFoundException(
|
|
"Membre non trouvé pour l'email: " + email + ". Veuillez contacter l'administrateur."));
|
|
}
|
|
|
|
/**
|
|
* Toutes les cotisations du membre connecté (tous statuts), ou des organisations gérées si ADMIN/ADMIN_ORGANISATION.
|
|
* Utilisé pour les onglets Toutes / Payées / Dues / Retard.
|
|
*/
|
|
public List<CotisationSummaryResponse> getMesCotisations(int page, int size) {
|
|
String email = securiteHelper.resolveEmail();
|
|
if (email == null || email.isBlank()) {
|
|
return Collections.emptyList();
|
|
}
|
|
Set<String> roles = securiteHelper.getRoles();
|
|
if (roles != null && (roles.contains("ADMIN") || roles.contains("ADMIN_ORGANISATION"))) {
|
|
List<Organisation> orgs = organisationService.listerOrganisationsPourUtilisateur(email);
|
|
if (orgs == null || orgs.isEmpty()) {
|
|
log.info("Admin/Admin org: aucune organisation pour {}. Retour liste vide.", email);
|
|
return Collections.emptyList();
|
|
}
|
|
Set<UUID> orgIds = orgs.stream().map(Organisation::getId).collect(Collectors.toCollection(LinkedHashSet::new));
|
|
List<Cotisation> cotisations = cotisationRepository.findByOrganisationIdIn(
|
|
orgIds, Page.of(page, size), Sort.by("dateEcheance").descending());
|
|
return cotisations.stream().map(this::convertToSummaryResponse).collect(Collectors.toList());
|
|
}
|
|
Membre membreConnecte = membreRepository.findByEmail(email).orElse(null);
|
|
if (membreConnecte == null) {
|
|
log.info("Aucun membre trouvé pour l'email. Retour liste vide.");
|
|
return Collections.emptyList();
|
|
}
|
|
return getCotisationsByMembre(membreConnecte.getId(), page, size);
|
|
}
|
|
|
|
/**
|
|
* Liste les cotisations en attente du membre connecté, ou des organisations gérées si ADMIN/ADMIN_ORGANISATION.
|
|
* Auto-détection du membre via SecurityIdentity (pas de membreId en paramètre).
|
|
* Utilisé par la page personnelle "Payer mes Cotisations".
|
|
*
|
|
* @return Liste des cotisations en attente
|
|
*/
|
|
public List<CotisationSummaryResponse> getMesCotisationsEnAttente() {
|
|
String email = securiteHelper.resolveEmail();
|
|
if (email == null || email.isBlank()) {
|
|
return Collections.emptyList();
|
|
}
|
|
Set<String> roles = securiteHelper.getRoles();
|
|
if (roles != null && (roles.contains("ADMIN") || roles.contains("ADMIN_ORGANISATION"))) {
|
|
List<Organisation> orgs = organisationService.listerOrganisationsPourUtilisateur(email);
|
|
if (orgs == null || orgs.isEmpty()) {
|
|
return Collections.emptyList();
|
|
}
|
|
Set<UUID> orgIds = orgs.stream().map(Organisation::getId).collect(Collectors.toCollection(LinkedHashSet::new));
|
|
List<Cotisation> cotisations = cotisationRepository.findEnAttenteByOrganisationIdIn(orgIds);
|
|
log.info("Cotisations en attente (admin): {} pour {} organisations", cotisations.size(), orgIds.size());
|
|
return cotisations.stream().map(this::convertToSummaryResponse).collect(Collectors.toList());
|
|
}
|
|
Membre membreConnecte = membreRepository.findByEmail(email).orElse(null);
|
|
if (membreConnecte == null) {
|
|
log.info("Aucun membre trouvé pour l'email: {}. Retour d'une liste vide.", email);
|
|
return Collections.emptyList();
|
|
}
|
|
log.info("Récupération des cotisations en attente pour le membre: {} ({})",
|
|
membreConnecte.getNumeroMembre(), membreConnecte.getId());
|
|
int anneeEnCours = LocalDate.now().getYear();
|
|
List<Cotisation> cotisations = cotisationRepository.getEntityManager()
|
|
.createQuery(
|
|
"SELECT c FROM Cotisation c " +
|
|
"WHERE c.membre.id = :membreId " +
|
|
"AND c.statut = 'EN_ATTENTE' " +
|
|
"AND EXTRACT(YEAR FROM c.dateEcheance) = :annee " +
|
|
"ORDER BY c.dateEcheance ASC",
|
|
Cotisation.class)
|
|
.setParameter("membreId", membreConnecte.getId())
|
|
.setParameter("annee", anneeEnCours)
|
|
.getResultList();
|
|
log.info("Cotisations en attente trouvées: {} pour le membre {}",
|
|
cotisations.size(), membreConnecte.getNumeroMembre());
|
|
return cotisations.stream()
|
|
.map(this::convertToSummaryResponse)
|
|
.collect(Collectors.toList());
|
|
}
|
|
|
|
/**
|
|
* Récupère la synthèse des cotisations du membre connecté, ou des organisations gérées si ADMIN/ADMIN_ORGANISATION.
|
|
* KPI : cotisations en attente, montant dû, prochaine échéance, total payé année.
|
|
*
|
|
* @return Map avec les KPI
|
|
*/
|
|
public Map<String, Object> getMesCotisationsSynthese() {
|
|
String email = securiteHelper.resolveEmail();
|
|
if (email == null || email.isBlank()) {
|
|
return syntheseVide(LocalDate.now().getYear());
|
|
}
|
|
Set<String> roles = securiteHelper.getRoles();
|
|
if (roles != null && (roles.contains("ADMIN") || roles.contains("ADMIN_ORGANISATION"))) {
|
|
List<Organisation> orgs = organisationService.listerOrganisationsPourUtilisateur(email);
|
|
if (orgs == null || orgs.isEmpty()) {
|
|
return syntheseVide(LocalDate.now().getYear());
|
|
}
|
|
Set<UUID> orgIds = orgs.stream().map(Organisation::getId).collect(Collectors.toCollection(LinkedHashSet::new));
|
|
int anneeEnCours = LocalDate.now().getYear();
|
|
var em = cotisationRepository.getEntityManager();
|
|
Long cotisationsEnAttente = em.createQuery(
|
|
"SELECT COUNT(c) FROM Cotisation c WHERE c.organisation.id IN :orgIds AND c.statut = 'EN_ATTENTE'",
|
|
Long.class)
|
|
.setParameter("orgIds", orgIds)
|
|
.getSingleResult();
|
|
BigDecimal montantDu = em.createQuery(
|
|
"SELECT COALESCE(SUM(c.montantDu), 0) FROM Cotisation c WHERE c.organisation.id IN :orgIds AND c.statut = 'EN_ATTENTE'",
|
|
BigDecimal.class)
|
|
.setParameter("orgIds", orgIds)
|
|
.getSingleResult();
|
|
LocalDate prochaineEcheance = em.createQuery(
|
|
"SELECT MIN(c.dateEcheance) FROM Cotisation c WHERE c.organisation.id IN :orgIds AND c.statut = 'EN_ATTENTE'",
|
|
LocalDate.class)
|
|
.setParameter("orgIds", orgIds)
|
|
.getSingleResult();
|
|
BigDecimal totalPayeAnnee = em.createQuery(
|
|
"SELECT COALESCE(SUM(c.montantPaye), 0) FROM Cotisation c WHERE c.organisation.id IN :orgIds AND c.statut = 'PAYEE' AND c.datePaiement IS NOT NULL AND EXTRACT(YEAR FROM c.datePaiement) = :annee",
|
|
BigDecimal.class)
|
|
.setParameter("orgIds", orgIds)
|
|
.setParameter("annee", anneeEnCours)
|
|
.getSingleResult();
|
|
log.info("Synthèse (admin): {} cotisations en attente, {} FCFA dû, total payé {} FCFA", cotisationsEnAttente, montantDu, totalPayeAnnee);
|
|
Map<String, Object> result = new java.util.LinkedHashMap<>();
|
|
result.put("cotisationsEnAttente", cotisationsEnAttente != null ? cotisationsEnAttente.intValue() : 0);
|
|
result.put("montantDu", montantDu != null ? montantDu : BigDecimal.ZERO);
|
|
result.put("prochaineEcheance", prochaineEcheance);
|
|
result.put("totalPayeAnnee", totalPayeAnnee != null ? totalPayeAnnee : BigDecimal.ZERO);
|
|
result.put("anneeEnCours", anneeEnCours);
|
|
return result;
|
|
}
|
|
Membre membreConnecte = getMembreConnecte();
|
|
log.info("Récupération de la synthèse des cotisations pour le membre: {} ({})",
|
|
membreConnecte.getNumeroMembre(), membreConnecte.getId());
|
|
int anneeEnCours = LocalDate.now().getYear();
|
|
Long cotisationsEnAttente = cotisationRepository.getEntityManager()
|
|
.createQuery(
|
|
"SELECT COUNT(c) FROM Cotisation c " +
|
|
"WHERE c.membre.id = :membreId AND c.statut != 'PAYEE' AND c.statut != 'ANNULEE'",
|
|
Long.class)
|
|
.setParameter("membreId", membreConnecte.getId())
|
|
.getSingleResult();
|
|
BigDecimal montantDu = cotisationRepository.getEntityManager()
|
|
.createQuery(
|
|
"SELECT COALESCE(SUM(c.montantDu - COALESCE(c.montantPaye, 0)), 0) FROM Cotisation c " +
|
|
"WHERE c.membre.id = :membreId AND c.statut != 'PAYEE' AND c.statut != 'ANNULEE'",
|
|
BigDecimal.class)
|
|
.setParameter("membreId", membreConnecte.getId())
|
|
.getSingleResult();
|
|
LocalDate prochaineEcheance = cotisationRepository.getEntityManager()
|
|
.createQuery(
|
|
"SELECT MIN(c.dateEcheance) FROM Cotisation c " +
|
|
"WHERE c.membre.id = :membreId AND c.statut != 'PAYEE' AND c.statut != 'ANNULEE'",
|
|
LocalDate.class)
|
|
.setParameter("membreId", membreConnecte.getId())
|
|
.getSingleResult();
|
|
BigDecimal totalPayeAnnee = cotisationRepository.getEntityManager()
|
|
.createQuery(
|
|
"SELECT COALESCE(SUM(c.montantPaye), 0) FROM Cotisation c " +
|
|
"WHERE c.membre.id = :membreId " +
|
|
"AND c.statut = 'PAYEE' " +
|
|
"AND c.datePaiement IS NOT NULL " +
|
|
"AND EXTRACT(YEAR FROM c.datePaiement) = :annee",
|
|
BigDecimal.class)
|
|
.setParameter("membreId", membreConnecte.getId())
|
|
.setParameter("annee", anneeEnCours)
|
|
.getSingleResult();
|
|
log.info("Synthèse calculée pour {}: {} cotisations en attente, {} FCFA dû, total payé {} FCFA",
|
|
membreConnecte.getNumeroMembre(), cotisationsEnAttente, montantDu, totalPayeAnnee);
|
|
Map<String, Object> result = new java.util.LinkedHashMap<>();
|
|
result.put("cotisationsEnAttente", cotisationsEnAttente != null ? cotisationsEnAttente.intValue() : 0);
|
|
result.put("montantDu", montantDu != null ? montantDu : BigDecimal.ZERO);
|
|
result.put("prochaineEcheance", prochaineEcheance);
|
|
result.put("totalPayeAnnee", totalPayeAnnee != null ? totalPayeAnnee : BigDecimal.ZERO);
|
|
result.put("anneeEnCours", anneeEnCours);
|
|
return result;
|
|
}
|
|
|
|
private Map<String, Object> syntheseVide(int anneeEnCours) {
|
|
Map<String, Object> result = new java.util.LinkedHashMap<>();
|
|
result.put("cotisationsEnAttente", 0);
|
|
result.put("montantDu", BigDecimal.ZERO);
|
|
result.put("prochaineEcheance", (LocalDate) null);
|
|
result.put("totalPayeAnnee", BigDecimal.ZERO);
|
|
result.put("anneeEnCours", anneeEnCours);
|
|
return result;
|
|
}
|
|
}
|