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 getAllCotisations(int page, int size) { log.debug("Récupération des cotisations - page: {}, size: {}", page, size); jakarta.persistence.TypedQuery query = cotisationRepository.getEntityManager().createQuery( "SELECT c FROM Cotisation c ORDER BY c.dateEcheance DESC", Cotisation.class); query.setFirstResult(page * size); query.setMaxResults(size); List 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 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 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 getCotisationsByStatut(@NotNull String statut, int page, int size) { log.debug("Récupération des cotisations avec statut: {}", statut); List 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 getCotisationsEnRetard(int page, int size) { log.debug("Récupération des cotisations en retard"); List 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 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 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 getStatistiquesPeriode(int annee, Integer mois) { return cotisationRepository.getStatistiquesPeriode(annee, mois); } /** * Statistiques globales. */ public Map 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 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 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 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 getMesCotisations(int page, int size) { String email = securiteHelper.resolveEmail(); if (email == null || email.isBlank()) { return Collections.emptyList(); } Set roles = securiteHelper.getRoles(); if (roles != null && (roles.contains("ADMIN") || roles.contains("ADMIN_ORGANISATION"))) { List 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 orgIds = orgs.stream().map(Organisation::getId).collect(Collectors.toCollection(LinkedHashSet::new)); List 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 getMesCotisationsEnAttente() { String email = securiteHelper.resolveEmail(); if (email == null || email.isBlank()) { return Collections.emptyList(); } Set roles = securiteHelper.getRoles(); if (roles != null && (roles.contains("ADMIN") || roles.contains("ADMIN_ORGANISATION"))) { List orgs = organisationService.listerOrganisationsPourUtilisateur(email); if (orgs == null || orgs.isEmpty()) { return Collections.emptyList(); } Set orgIds = orgs.stream().map(Organisation::getId).collect(Collectors.toCollection(LinkedHashSet::new)); List 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 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 getMesCotisationsSynthese() { String email = securiteHelper.resolveEmail(); if (email == null || email.isBlank()) { return syntheseVide(LocalDate.now().getYear()); } Set roles = securiteHelper.getRoles(); if (roles != null && (roles.contains("ADMIN") || roles.contains("ADMIN_ORGANISATION"))) { List orgs = organisationService.listerOrganisationsPourUtilisateur(email); if (orgs == null || orgs.isEmpty()) { return syntheseVide(LocalDate.now().getYear()); } Set 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 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 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 syntheseVide(int anneeEnCours) { Map 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; } }