package dev.lions.unionflow.server.service; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.when; import dev.lions.unionflow.server.api.dto.dashboard.MembreDashboardSyntheseResponse; import dev.lions.unionflow.server.api.enums.solidarite.StatutAide; import dev.lions.unionflow.server.entity.DemandeAide; import dev.lions.unionflow.server.entity.Membre; import dev.lions.unionflow.server.repository.CotisationRepository; import dev.lions.unionflow.server.repository.DemandeAideRepository; import dev.lions.unionflow.server.repository.MembreRepository; import dev.lions.unionflow.server.repository.mutuelle.epargne.CompteEpargneRepository; import dev.lions.unionflow.server.service.support.SecuriteHelper; import io.quarkus.test.InjectMock; import io.quarkus.test.junit.QuarkusTest; import jakarta.inject.Inject; import jakarta.ws.rs.NotFoundException; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.UUID; @QuarkusTest class MembreDashboardServiceTest { @Inject MembreDashboardService service; @InjectMock SecuriteHelper securiteHelper; @InjectMock MembreRepository membreRepository; @InjectMock CotisationRepository cotisationRepository; @InjectMock CompteEpargneRepository compteEpargneRepository; @InjectMock DemandeAideRepository demandeAideRepository; // ------------------------------------------------------------------------- // Helper // ------------------------------------------------------------------------- private Membre buildMembre(boolean actif) { Membre m = new Membre(); m.setId(UUID.randomUUID()); m.setPrenom("Alice"); m.setNom("Dupont"); m.setEmail("alice@test.com"); m.setActif(actif); m.setDateCreation(LocalDateTime.of(2024, 1, 15, 10, 0)); return m; } private void stubbingCotisationsBase(UUID membreId, BigDecimal paiementsMois, long enRetardCount, BigDecimal totalDu, BigDecimal totalPaye, BigDecimal totalToutTemps, long countAll, long countPayees) { when(cotisationRepository.calculerTotalCotisationsPayeesCeMois(membreId)) .thenReturn(paiementsMois); when(cotisationRepository.countRetardByMembreId(membreId)).thenReturn(enRetardCount); when(cotisationRepository.calculerTotalCotisationsAnneeEnCours(membreId)) .thenReturn(totalDu); when(cotisationRepository.calculerTotalCotisationsPayeesAnneeEnCours(membreId)) .thenReturn(totalPaye); when(cotisationRepository.calculerTotalCotisationsPayeesToutTemps(membreId)) .thenReturn(totalToutTemps); when(cotisationRepository.countByMembreId(membreId)).thenReturn(countAll); when(cotisationRepository.countPayeesByMembreId(membreId)).thenReturn(countPayees); } // ------------------------------------------------------------------------- // emailNull / blank → NotFoundException // ------------------------------------------------------------------------- @Test @DisplayName("getDashboardData - email null → NotFoundException") void getDashboardData_emailNull_throws() { when(securiteHelper.resolveEmail()).thenReturn(null); assertThatThrownBy(() -> service.getDashboardData()) .isInstanceOf(NotFoundException.class) .hasMessageContaining("Identité non disponible"); } @Test @DisplayName("getDashboardData - email blank → NotFoundException") void getDashboardData_emailBlank_throws() { when(securiteHelper.resolveEmail()).thenReturn(" "); assertThatThrownBy(() -> service.getDashboardData()) .isInstanceOf(NotFoundException.class) .hasMessageContaining("Identité non disponible"); } // ------------------------------------------------------------------------- // Membre introuvable // ------------------------------------------------------------------------- @Test @DisplayName("getDashboardData - membre absent en base → NotFoundException") void getDashboardData_membreInexistant_throws() { when(securiteHelper.resolveEmail()).thenReturn("inconnu@test.com"); when(membreRepository.findByEmail(anyString())).thenReturn(Optional.empty()); assertThatThrownBy(() -> service.getDashboardData()) .isInstanceOf(NotFoundException.class) .hasMessageContaining("inconnu@test.com"); } @Test @DisplayName("getDashboardData - membre inactif → NotFoundException") void getDashboardData_membreInactif_throws() { Membre inactif = buildMembre(false); inactif.setActif(false); when(securiteHelper.resolveEmail()).thenReturn("inactif@test.com"); when(membreRepository.findByEmail("inactif@test.com")).thenReturn(Optional.of(inactif)); when(membreRepository.findByEmail("inactif@test.com")).thenReturn(Optional.of(inactif)); assertThatThrownBy(() -> service.getDashboardData()) .isInstanceOf(NotFoundException.class); } // ------------------------------------------------------------------------- // Happy path - cotisations normales avec totalAnneeDu > 0 // ------------------------------------------------------------------------- @Test @DisplayName("getDashboardData - happy path - cotisations normales (taux calculé sur l'année)") void getDashboardData_happyPath_cotisationsNormales() { Membre membre = buildMembre(true); UUID membreId = membre.getId(); when(securiteHelper.resolveEmail()).thenReturn("alice@test.com"); when(membreRepository.findByEmail("alice@test.com")).thenReturn(Optional.of(membre)); stubbingCotisationsBase(membreId, new BigDecimal("5000"), // paiementsMois 0L, // enRetardCount → "À jour" new BigDecimal("12000"), // totalAnneeDu new BigDecimal("6000"), // totalAnneePaye new BigDecimal("20000"), // totalToutTemps 10L, // countAll 5L // countPayees ); when(compteEpargneRepository.sumSoldeActuelByMembreId(membreId)) .thenReturn(new BigDecimal("50000")); when(demandeAideRepository.findByDemandeurId(membreId)).thenReturn(Collections.emptyList()); MembreDashboardSyntheseResponse result = service.getDashboardData(); assertThat(result).isNotNull(); assertThat(result.prenom()).isEqualTo("Alice"); assertThat(result.nom()).isEqualTo("Dupont"); assertThat(result.dateInscription()).isEqualTo(LocalDate.of(2024, 1, 15)); assertThat(result.statutCotisations()).isEqualTo("À jour"); assertThat(result.tauxCotisationsPerso()).isEqualTo(50); // 6000/12000*100 assertThat(result.mesCotisationsPaiement()).isEqualByComparingTo("5000"); assertThat(result.monSoldeEpargne()).isEqualByComparingTo("50000"); assertThat(result.mesDemandesAide()).isEqualTo(0); assertThat(result.aidesEnCours()).isEqualTo(0); assertThat(result.tauxAidesApprouvees()).isNull(); } @Test @DisplayName("getDashboardData - statut En retard quand enRetardCount > 0") void getDashboardData_enRetard_statutEnRetard() { Membre membre = buildMembre(true); UUID membreId = membre.getId(); when(securiteHelper.resolveEmail()).thenReturn("alice@test.com"); when(membreRepository.findByEmail("alice@test.com")).thenReturn(Optional.of(membre)); stubbingCotisationsBase(membreId, BigDecimal.ZERO, // paiementsMois 2L, // enRetardCount > 0 → "En retard" new BigDecimal("12000"), // totalAnneeDu new BigDecimal("3000"), // totalAnneePaye new BigDecimal("5000"), // totalToutTemps 10L, 3L ); when(compteEpargneRepository.sumSoldeActuelByMembreId(membreId)) .thenReturn(BigDecimal.ZERO); when(demandeAideRepository.findByDemandeurId(membreId)).thenReturn(Collections.emptyList()); MembreDashboardSyntheseResponse result = service.getDashboardData(); assertThat(result.statutCotisations()).isEqualTo("En retard"); assertThat(result.tauxCotisationsPerso()).isEqualTo(25); // 3000/12000*100 } // ------------------------------------------------------------------------- // Fallback: totalAnneeDu == 0 → branches du else // ------------------------------------------------------------------------- @Test @DisplayName("getDashboardData - fallback (aucune cotisation en cours) → tauxCotisations null") void getDashboardData_fallback_aucuneCotisation() { Membre membre = buildMembre(true); UUID membreId = membre.getId(); when(securiteHelper.resolveEmail()).thenReturn("alice@test.com"); when(membreRepository.findByEmail("alice@test.com")).thenReturn(Optional.of(membre)); stubbingCotisationsBase(membreId, BigDecimal.ZERO, 0L, null, // totalAnneeDu null → fallback null, BigDecimal.ZERO, 0L, // totalToutesAnneesCount = 0 → taux null 0L ); when(compteEpargneRepository.sumSoldeActuelByMembreId(membreId)) .thenReturn(BigDecimal.ZERO); when(demandeAideRepository.findByDemandeurId(membreId)).thenReturn(Collections.emptyList()); MembreDashboardSyntheseResponse result = service.getDashboardData(); assertThat(result.tauxCotisationsPerso()).isNull(); } @Test @DisplayName("getDashboardData - fallback avec cotisations en retard → taux partiel") void getDashboardData_fallback_avecRetard_tauxPartiel() { Membre membre = buildMembre(true); UUID membreId = membre.getId(); when(securiteHelper.resolveEmail()).thenReturn("alice@test.com"); when(membreRepository.findByEmail("alice@test.com")).thenReturn(Optional.of(membre)); when(cotisationRepository.calculerTotalCotisationsPayeesCeMois(membreId)) .thenReturn(BigDecimal.ZERO); when(cotisationRepository.countRetardByMembreId(membreId)).thenReturn(1L); when(cotisationRepository.calculerTotalCotisationsAnneeEnCours(membreId)) .thenReturn(null); // totalAnneeDu null → fallback when(cotisationRepository.calculerTotalCotisationsPayeesAnneeEnCours(membreId)) .thenReturn(null); when(cotisationRepository.calculerTotalCotisationsPayeesToutTemps(membreId)) .thenReturn(new BigDecimal("2000")); when(cotisationRepository.countByMembreId(membreId)).thenReturn(4L); when(cotisationRepository.countPayeesByMembreId(membreId)).thenReturn(2L); when(compteEpargneRepository.sumSoldeActuelByMembreId(membreId)) .thenReturn(BigDecimal.ZERO); when(demandeAideRepository.findByDemandeurId(membreId)).thenReturn(Collections.emptyList()); MembreDashboardSyntheseResponse result = service.getDashboardData(); // 2 payées / 4 totales = 50% assertThat(result.tauxCotisationsPerso()).isEqualTo(50); } @Test @DisplayName("getDashboardData - fallback sans retard → taux 100%") void getDashboardData_fallback_sansRetard_taux100() { Membre membre = buildMembre(true); UUID membreId = membre.getId(); when(securiteHelper.resolveEmail()).thenReturn("alice@test.com"); when(membreRepository.findByEmail("alice@test.com")).thenReturn(Optional.of(membre)); when(cotisationRepository.calculerTotalCotisationsPayeesCeMois(membreId)) .thenReturn(BigDecimal.ZERO); when(cotisationRepository.countRetardByMembreId(membreId)).thenReturn(0L); when(cotisationRepository.calculerTotalCotisationsAnneeEnCours(membreId)) .thenReturn(null); // fallback when(cotisationRepository.calculerTotalCotisationsPayeesAnneeEnCours(membreId)) .thenReturn(null); when(cotisationRepository.calculerTotalCotisationsPayeesToutTemps(membreId)) .thenReturn(new BigDecimal("3000")); when(cotisationRepository.countByMembreId(membreId)).thenReturn(3L); when(cotisationRepository.countPayeesByMembreId(membreId)).thenReturn(3L); when(compteEpargneRepository.sumSoldeActuelByMembreId(membreId)) .thenReturn(new BigDecimal("10000")); when(demandeAideRepository.findByDemandeurId(membreId)).thenReturn(Collections.emptyList()); MembreDashboardSyntheseResponse result = service.getDashboardData(); assertThat(result.tauxCotisationsPerso()).isEqualTo(100); } // ------------------------------------------------------------------------- // Aides // ------------------------------------------------------------------------- @Test @DisplayName("getDashboardData - demandes d'aide avec approbations") void getDashboardData_avecDemandes_tauxAidesCalcule() { Membre membre = buildMembre(true); UUID membreId = membre.getId(); when(securiteHelper.resolveEmail()).thenReturn("alice@test.com"); when(membreRepository.findByEmail("alice@test.com")).thenReturn(Optional.of(membre)); stubbingCotisationsBase(membreId, new BigDecimal("1000"), 0L, new BigDecimal("5000"), new BigDecimal("5000"), new BigDecimal("10000"), 5L, 5L); when(compteEpargneRepository.sumSoldeActuelByMembreId(membreId)) .thenReturn(BigDecimal.ZERO); DemandeAide approuvee = new DemandeAide(); approuvee.setStatut(StatutAide.APPROUVEE); DemandeAide enCours = new DemandeAide(); enCours.setStatut(StatutAide.EN_COURS_EVALUATION); DemandeAide rejetee = new DemandeAide(); rejetee.setStatut(StatutAide.REJETEE); when(demandeAideRepository.findByDemandeurId(membreId)) .thenReturn(List.of(approuvee, enCours, rejetee)); MembreDashboardSyntheseResponse result = service.getDashboardData(); assertThat(result.mesDemandesAide()).isEqualTo(3); assertThat(result.aidesEnCours()).isEqualTo(1); // seulement EN_COURS_EXAMEN assertThat(result.tauxAidesApprouvees()).isEqualTo(33); // 1/3 * 100 } @Test @DisplayName("getDashboardData - demande avec statut null ne bloque pas") void getDashboardData_demande_statutNull() { Membre membre = buildMembre(true); UUID membreId = membre.getId(); when(securiteHelper.resolveEmail()).thenReturn("alice@test.com"); when(membreRepository.findByEmail("alice@test.com")).thenReturn(Optional.of(membre)); stubbingCotisationsBase(membreId, BigDecimal.ZERO, 0L, new BigDecimal("1000"), new BigDecimal("500"), new BigDecimal("500"), 1L, 1L); when(compteEpargneRepository.sumSoldeActuelByMembreId(membreId)) .thenReturn(BigDecimal.ZERO); DemandeAide avecStatutNull = new DemandeAide(); avecStatutNull.setStatut(null); when(demandeAideRepository.findByDemandeurId(membreId)) .thenReturn(List.of(avecStatutNull)); MembreDashboardSyntheseResponse result = service.getDashboardData(); assertThat(result.mesDemandesAide()).isEqualTo(1); // statut null → exclut du filtre aidesEnCours (condition: d.getStatut() != null && ...) assertThat(result.aidesEnCours()).isEqualTo(0); assertThat(result.tauxAidesApprouvees()).isEqualTo(0); } // ------------------------------------------------------------------------- // Données nulles des repositories // ------------------------------------------------------------------------- @Test @DisplayName("getDashboardData - paiementsMois null → remplacé par ZERO") void getDashboardData_paiementsMoisNull_treatedAsZero() { Membre membre = buildMembre(true); UUID membreId = membre.getId(); when(securiteHelper.resolveEmail()).thenReturn("alice@test.com"); when(membreRepository.findByEmail("alice@test.com")).thenReturn(Optional.of(membre)); stubbingCotisationsBase(membreId, null, // paiementsMois null 0L, new BigDecimal("1000"), new BigDecimal("500"), null, // totalToutTemps null 1L, 1L ); when(compteEpargneRepository.sumSoldeActuelByMembreId(membreId)) .thenReturn(BigDecimal.ZERO); when(demandeAideRepository.findByDemandeurId(membreId)).thenReturn(Collections.emptyList()); MembreDashboardSyntheseResponse result = service.getDashboardData(); assertThat(result.mesCotisationsPaiement()).isEqualByComparingTo(BigDecimal.ZERO); assertThat(result.totalCotisationsPayeesToutTemps()).isEqualByComparingTo(BigDecimal.ZERO); } @Test @DisplayName("getDashboardData - membre sans dateCreation → dateInscription null") void getDashboardData_sansDates_dateInscriptionNull() { Membre membre = buildMembre(true); membre.setDateCreation(null); UUID membreId = membre.getId(); when(securiteHelper.resolveEmail()).thenReturn("alice@test.com"); when(membreRepository.findByEmail("alice@test.com")).thenReturn(Optional.of(membre)); stubbingCotisationsBase(membreId, BigDecimal.ZERO, 0L, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, 0L, 0L); when(compteEpargneRepository.sumSoldeActuelByMembreId(membreId)) .thenReturn(BigDecimal.ZERO); when(demandeAideRepository.findByDemandeurId(membreId)).thenReturn(Collections.emptyList()); MembreDashboardSyntheseResponse result = service.getDashboardData(); assertThat(result.dateInscription()).isNull(); } @Test @DisplayName("getDashboardData - membre actif null (traité comme actif)") void getDashboardData_actifNull_treatedAsActif() { Membre membre = buildMembre(true); membre.setActif(null); // actif null → filtre passe (actif == null || actif) UUID membreId = membre.getId(); when(securiteHelper.resolveEmail()).thenReturn("alice@test.com"); when(membreRepository.findByEmail("alice@test.com")).thenReturn(Optional.of(membre)); stubbingCotisationsBase(membreId, BigDecimal.ZERO, 0L, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, 0L, 0L); when(compteEpargneRepository.sumSoldeActuelByMembreId(membreId)) .thenReturn(BigDecimal.ZERO); when(demandeAideRepository.findByDemandeurId(membreId)).thenReturn(Collections.emptyList()); // Membre avec actif=null doit passer le filtre MembreDashboardSyntheseResponse result = service.getDashboardData(); assertThat(result).isNotNull(); } @Test @DisplayName("getDashboardData - totalAnneePaye null avec totalAnneeDu > 0 → traité comme ZERO") void getDashboardData_totalAnneePayeNull_treatedAsZero() { Membre membre = buildMembre(true); UUID membreId = membre.getId(); when(securiteHelper.resolveEmail()).thenReturn("alice@test.com"); when(membreRepository.findByEmail("alice@test.com")).thenReturn(Optional.of(membre)); when(cotisationRepository.calculerTotalCotisationsPayeesCeMois(membreId)) .thenReturn(BigDecimal.ZERO); when(cotisationRepository.countRetardByMembreId(membreId)).thenReturn(0L); when(cotisationRepository.calculerTotalCotisationsAnneeEnCours(membreId)) .thenReturn(new BigDecimal("10000")); // > 0 when(cotisationRepository.calculerTotalCotisationsPayeesAnneeEnCours(membreId)) .thenReturn(null); // null → ZERO when(cotisationRepository.calculerTotalCotisationsPayeesToutTemps(membreId)) .thenReturn(BigDecimal.ZERO); when(cotisationRepository.countByMembreId(membreId)).thenReturn(5L); when(cotisationRepository.countPayeesByMembreId(membreId)).thenReturn(0L); when(compteEpargneRepository.sumSoldeActuelByMembreId(membreId)) .thenReturn(BigDecimal.ZERO); when(demandeAideRepository.findByDemandeurId(membreId)).thenReturn(Collections.emptyList()); MembreDashboardSyntheseResponse result = service.getDashboardData(); assertThat(result.tauxCotisationsPerso()).isEqualTo(0); assertThat(result.totalCotisationsPayeesAnnee()).isEqualByComparingTo(BigDecimal.ZERO); } @Test @DisplayName("getDashboardData - demande ANNULEE n'est pas comptée dans aidesEnCours (branche != ANNULEE false)") void getDashboardData_demandeAnnulee_nonComptee() { Membre membre = buildMembre(true); UUID membreId = membre.getId(); when(securiteHelper.resolveEmail()).thenReturn("alice@test.com"); when(membreRepository.findByEmail("alice@test.com")).thenReturn(Optional.of(membre)); stubbingCotisationsBase(membreId, BigDecimal.ZERO, 0L, new BigDecimal("1000"), new BigDecimal("1000"), new BigDecimal("1000"), 1L, 1L); when(compteEpargneRepository.sumSoldeActuelByMembreId(membreId)) .thenReturn(BigDecimal.ZERO); DemandeAide annulee = new DemandeAide(); annulee.setStatut(StatutAide.ANNULEE); // != ANNULEE → false → exclu when(demandeAideRepository.findByDemandeurId(membreId)) .thenReturn(List.of(annulee)); MembreDashboardSyntheseResponse result = service.getDashboardData(); assertThat(result.mesDemandesAide()).isEqualTo(1); assertThat(result.aidesEnCours()).isEqualTo(0); // ANNULEE exclue } @Test @DisplayName("getDashboardData - demande EN_COURS_EVALUATION non APPROUVEE/REJETEE/ANNULEE → comptée dans aidesEnCours (L129 lambda branche)") void getDashboardData_demandeEnCoursEvaluation_compteeAidesEnCours() { Membre membre = buildMembre(true); UUID membreId = membre.getId(); when(securiteHelper.resolveEmail()).thenReturn("alice@test.com"); when(membreRepository.findByEmail("alice@test.com")).thenReturn(Optional.of(membre)); stubbingCotisationsBase(membreId, BigDecimal.ZERO, 0L, new BigDecimal("1000"), new BigDecimal("1000"), new BigDecimal("1000"), 1L, 1L); when(compteEpargneRepository.sumSoldeActuelByMembreId(membreId)).thenReturn(BigDecimal.ZERO); // Demande EN_COURS_EVALUATION : statut != null, != APPROUVEE, != REJETEE, != ANNULEE → comptée DemandeAide enCoursEval = new DemandeAide(); enCoursEval.setStatut(StatutAide.EN_COURS_EVALUATION); // Demande REJETEE : exclue de aidesEnCours (statut != REJETEE = false) DemandeAide rejetee = new DemandeAide(); rejetee.setStatut(StatutAide.REJETEE); when(demandeAideRepository.findByDemandeurId(membreId)) .thenReturn(List.of(enCoursEval, rejetee)); MembreDashboardSyntheseResponse result = service.getDashboardData(); // EN_COURS_EVALUATION comptée + REJETEE non comptée → aidesEnCours = 1 assertThat(result.aidesEnCours()).isEqualTo(1); assertThat(result.mesDemandesAide()).isEqualTo(2); // 0 APPROUVEE / 2 total → tauxAidesApprouvees = 0 assertThat(result.tauxAidesApprouvees()).isEqualTo(0); } @Test @DisplayName("getDashboardData - demandeAideRepository retourne null → demandes traitées comme 0") void getDashboardData_demandesNull_returnsZero() { Membre membre = buildMembre(true); UUID membreId = membre.getId(); when(securiteHelper.resolveEmail()).thenReturn("alice@test.com"); when(membreRepository.findByEmail("alice@test.com")).thenReturn(Optional.of(membre)); stubbingCotisationsBase(membreId, BigDecimal.ZERO, 0L, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, 0L, 0L); when(compteEpargneRepository.sumSoldeActuelByMembreId(membreId)).thenReturn(BigDecimal.ZERO); // null au lieu de liste vide → couvre la branche demandes == null when(demandeAideRepository.findByDemandeurId(membreId)).thenReturn(null); MembreDashboardSyntheseResponse result = service.getDashboardData(); assertThat(result.mesDemandesAide()).isEqualTo(0); assertThat(result.aidesEnCours()).isEqualTo(0); assertThat(result.tauxAidesApprouvees()).isNull(); } }