Files
unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/service/CotisationService.java
dahoud 75a19988b0 Sync: code local unifié
Synchronisation du code source local (fait foi).

Signed-off-by: lions dev Team
2026-03-15 16:25:40 +00:00

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;
}
}