diff --git a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/entity/Aide.java b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/entity/Aide.java new file mode 100644 index 0000000..180e961 --- /dev/null +++ b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/entity/Aide.java @@ -0,0 +1,380 @@ +package dev.lions.unionflow.server.entity; + +import dev.lions.unionflow.server.api.enums.solidarite.StatutAide; +import dev.lions.unionflow.server.api.enums.solidarite.TypeAide; +import io.quarkus.hibernate.orm.panache.PanacheEntity; +import jakarta.persistence.*; +import jakarta.validation.constraints.*; +import lombok.*; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * Entité JPA pour la gestion des demandes d'aide et de solidarité + * Représente les demandes d'assistance mutuelle entre membres + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-15 + */ +@Entity +@Table(name = "aides", indexes = { + @Index(name = "idx_aide_numero_reference", columnList = "numero_reference", unique = true), + @Index(name = "idx_aide_membre_demandeur", columnList = "membre_demandeur_id"), + @Index(name = "idx_aide_organisation", columnList = "organisation_id"), + @Index(name = "idx_aide_statut", columnList = "statut"), + @Index(name = "idx_aide_type", columnList = "type_aide"), + @Index(name = "idx_aide_priorite", columnList = "priorite"), + @Index(name = "idx_aide_date_creation", columnList = "date_creation"), + @Index(name = "idx_aide_actif", columnList = "actif") +}) +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@EqualsAndHashCode(callSuper = false) +public class Aide extends PanacheEntity { + + /** Numéro de référence unique de la demande (format: AIDE-YYYY-XXXXXX) */ + @NotBlank(message = "Le numéro de référence est obligatoire") + @Pattern(regexp = "^AIDE-\\d{4}-[A-Z0-9]{6}$", + message = "Format de référence invalide (AIDE-YYYY-XXXXXX)") + @Column(name = "numero_reference", unique = true, nullable = false, length = 20) + private String numeroReference; + + /** Membre demandeur de l'aide */ + @NotNull(message = "Le membre demandeur est obligatoire") + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "membre_demandeur_id", nullable = false) + private Membre membreDemandeur; + + /** Organisation à laquelle appartient la demande */ + @NotNull(message = "L'organisation est obligatoire") + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "organisation_id", nullable = false) + private Organisation organisation; + + /** Type d'aide demandée */ + @NotNull(message = "Le type d'aide est obligatoire") + @Enumerated(EnumType.STRING) + @Column(name = "type_aide", nullable = false, length = 30) + private TypeAide typeAide; + + /** Titre de la demande d'aide */ + @NotBlank(message = "Le titre est obligatoire") + @Size(min = 5, max = 200, message = "Le titre doit contenir entre 5 et 200 caractères") + @Column(name = "titre", nullable = false, length = 200) + private String titre; + + /** Description détaillée de la demande */ + @NotBlank(message = "La description est obligatoire") + @Size(min = 20, max = 2000, message = "La description doit contenir entre 20 et 2000 caractères") + @Column(name = "description", nullable = false, columnDefinition = "TEXT") + private String description; + + /** Montant demandé */ + @DecimalMin(value = "0.0", inclusive = false, message = "Le montant demandé doit être positif") + @Digits(integer = 12, fraction = 2, message = "Format de montant invalide") + @Column(name = "montant_demande", precision = 15, scale = 2) + private BigDecimal montantDemande; + + /** Montant approuvé par l'organisation */ + @DecimalMin(value = "0.0", inclusive = false, message = "Le montant approuvé doit être positif") + @Digits(integer = 12, fraction = 2, message = "Format de montant invalide") + @Column(name = "montant_approuve", precision = 15, scale = 2) + private BigDecimal montantApprouve; + + /** Montant effectivement versé */ + @DecimalMin(value = "0.0", inclusive = false, message = "Le montant versé doit être positif") + @Digits(integer = 12, fraction = 2, message = "Format de montant invalide") + @Column(name = "montant_verse", precision = 15, scale = 2) + private BigDecimal montantVerse; + + /** Devise du montant (par défaut XOF) */ + @Pattern(regexp = "^[A-Z]{3}$", message = "La devise doit être un code ISO à 3 lettres") + @Builder.Default + @Column(name = "devise", length = 3) + private String devise = "XOF"; + + /** Statut de la demande */ + @NotNull(message = "Le statut est obligatoire") + @Enumerated(EnumType.STRING) + @Builder.Default + @Column(name = "statut", nullable = false, length = 30) + private StatutAide statut = StatutAide.EN_ATTENTE; + + /** Priorité de la demande */ + @NotBlank(message = "La priorité est obligatoire") + @Pattern(regexp = "^(BASSE|NORMALE|HAUTE|URGENTE)$", + message = "La priorité doit être BASSE, NORMALE, HAUTE ou URGENTE") + @Builder.Default + @Column(name = "priorite", nullable = false, length = 10) + private String priorite = "NORMALE"; + + /** Date limite pour la demande */ + @Column(name = "date_limite") + private LocalDate dateLimite; + + /** Date de début de l'aide (si approuvée) */ + @Column(name = "date_debut_aide") + private LocalDate dateDebutAide; + + /** Date de fin de l'aide */ + @Column(name = "date_fin_aide") + private LocalDate dateFinAide; + + /** Justificatifs fournis */ + @Builder.Default + @Column(name = "justificatifs_fournis", nullable = false) + private Boolean justificatifsFournis = false; + + /** Documents joints (URLs ou chemins) */ + @Size(max = 1000, message = "Les documents joints ne peuvent pas dépasser 1000 caractères") + @Column(name = "documents_joints", columnDefinition = "TEXT") + private String documentsJoints; + + /** Commentaires de l'évaluateur */ + @Size(max = 1000, message = "Les commentaires ne peuvent pas dépasser 1000 caractères") + @Column(name = "commentaires_evaluateur", columnDefinition = "TEXT") + private String commentairesEvaluateur; + + /** Membre qui a évalué la demande */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "evalue_par_id") + private Membre evaluePar; + + /** Date d'évaluation */ + @Column(name = "date_evaluation") + private LocalDateTime dateEvaluation; + + /** Mode de versement (ESPECES, VIREMENT, MOBILE_MONEY, CHEQUE) */ + @Pattern(regexp = "^(ESPECES|VIREMENT|MOBILE_MONEY|CHEQUE|AUTRE)$", + message = "Mode de versement invalide") + @Column(name = "mode_versement", length = 20) + private String modeVersement; + + /** Numéro de transaction (pour les paiements mobiles) */ + @Size(max = 50, message = "Le numéro de transaction ne peut pas dépasser 50 caractères") + @Column(name = "numero_transaction", length = 50) + private String numeroTransaction; + + /** Date de versement */ + @Column(name = "date_versement") + private LocalDateTime dateVersement; + + /** Commentaires du bénéficiaire */ + @Size(max = 1000, message = "Les commentaires ne peuvent pas dépasser 1000 caractères") + @Column(name = "commentaires_beneficiaire", columnDefinition = "TEXT") + private String commentairesBeneficiaire; + + /** Note de satisfaction (1-5) */ + @Min(value = 1, message = "La note de satisfaction doit être entre 1 et 5") + @Max(value = 5, message = "La note de satisfaction doit être entre 1 et 5") + @Column(name = "note_satisfaction") + private Integer noteSatisfaction; + + /** Aide publique (visible par tous les membres) */ + @Builder.Default + @Column(name = "aide_publique", nullable = false) + private Boolean aidePublique = true; + + /** Aide anonyme (demandeur anonyme) */ + @Builder.Default + @Column(name = "aide_anonyme", nullable = false) + private Boolean aideAnonyme = false; + + /** Nombre de vues de la demande */ + @Builder.Default + @Column(name = "nombre_vues", nullable = false) + private Integer nombreVues = 0; + + /** Raison du rejet (si applicable) */ + @Size(max = 500, message = "La raison du rejet ne peut pas dépasser 500 caractères") + @Column(name = "raison_rejet", length = 500) + private String raisonRejet; + + /** Date de rejet */ + @Column(name = "date_rejet") + private LocalDateTime dateRejet; + + /** Membre qui a rejeté la demande */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "rejete_par_id") + private Membre rejetePar; + + // Champs d'audit + @Builder.Default + @Column(name = "actif", nullable = false) + private Boolean actif = true; + + @Builder.Default + @Column(name = "date_creation", nullable = false) + private LocalDateTime dateCreation = LocalDateTime.now(); + + @Column(name = "date_modification") + private LocalDateTime dateModification; + + @Column(name = "cree_par", length = 100) + private String creePar; + + @Column(name = "modifie_par", length = 100) + private String modifiePar; + + @Version + @Column(name = "version") + private Long version; + + // ===== MÉTHODES MÉTIER ===== + + /** + * Génère un numéro de référence unique pour la demande d'aide + * Format: AIDE-YYYY-XXXXXX + */ + public static String genererNumeroReference() { + return "AIDE-" + LocalDate.now().getYear() + "-" + + String.format("%06d", (int) (Math.random() * 1000000)); + } + + /** + * Approuve la demande d'aide avec un montant spécifique + * + * @param montantApprouve Montant approuvé + * @param evaluateur Membre qui évalue + * @param commentaires Commentaires d'évaluation + */ + public void approuver(BigDecimal montantApprouve, Membre evaluateur, String commentaires) { + this.statut = StatutAide.APPROUVEE; + this.montantApprouve = montantApprouve; + this.evaluePar = evaluateur; + this.commentairesEvaluateur = commentaires; + this.dateEvaluation = LocalDateTime.now(); + this.dateDebutAide = LocalDate.now(); + this.dateModification = LocalDateTime.now(); + } + + /** + * Rejette la demande d'aide + * + * @param raison Raison du rejet + * @param evaluateur Membre qui rejette + */ + public void rejeter(String raison, Membre evaluateur) { + this.statut = StatutAide.REJETEE; + this.raisonRejet = raison; + this.rejetePar = evaluateur; + this.dateRejet = LocalDateTime.now(); + this.dateEvaluation = LocalDateTime.now(); + this.dateModification = LocalDateTime.now(); + } + + /** + * Marque l'aide comme versée + * + * @param montantVerse Montant effectivement versé + * @param modeVersement Mode de versement + * @param numeroTransaction Numéro de transaction + */ + public void marquerCommeVersee(BigDecimal montantVerse, String modeVersement, String numeroTransaction) { + this.statut = StatutAide.VERSEE; + this.montantVerse = montantVerse; + this.modeVersement = modeVersement; + this.numeroTransaction = numeroTransaction; + this.dateVersement = LocalDateTime.now(); + this.dateFinAide = LocalDate.now(); + this.dateModification = LocalDateTime.now(); + } + + /** + * Incrémente le nombre de vues de la demande + */ + public void incrementerVues() { + if (this.nombreVues == null) { + this.nombreVues = 1; + } else { + this.nombreVues++; + } + this.dateModification = LocalDateTime.now(); + } + + /** + * Vérifie si la demande est en cours de traitement + */ + public boolean isEnCoursDeTraitement() { + return this.statut == StatutAide.EN_COURS || + this.statut == StatutAide.EN_COURS_VERSEMENT; + } + + /** + * Vérifie si la demande est terminée (versée ou rejetée) + */ + public boolean isTerminee() { + return this.statut == StatutAide.VERSEE || + this.statut == StatutAide.REJETEE || + this.statut == StatutAide.ANNULEE; + } + + /** + * Vérifie si la demande peut être modifiée + */ + public boolean isPeutEtreModifiee() { + return this.statut == StatutAide.EN_ATTENTE; + } + + /** + * Calcule le pourcentage d'aide accordée par rapport à la demande + */ + public double getPourcentageAideAccordee() { + if (montantDemande == null || montantDemande.compareTo(BigDecimal.ZERO) == 0) { + return 0.0; + } + if (montantApprouve == null) { + return 0.0; + } + return montantApprouve.divide(montantDemande, 4, java.math.RoundingMode.HALF_UP) + .multiply(BigDecimal.valueOf(100)) + .doubleValue(); + } + + /** + * Retourne le nom complet du demandeur (si pas anonyme) + */ + public String getNomDemandeur() { + if (aideAnonyme != null && aideAnonyme) { + return "Demandeur anonyme"; + } + return membreDemandeur != null ? membreDemandeur.getNomComplet() : "Inconnu"; + } + + // ===== CALLBACKS JPA ===== + + @PrePersist + public void prePersist() { + if (numeroReference == null || numeroReference.isEmpty()) { + numeroReference = genererNumeroReference(); + } + if (dateCreation == null) { + dateCreation = LocalDateTime.now(); + } + } + + @PreUpdate + public void preUpdate() { + this.dateModification = LocalDateTime.now(); + } + + @Override + public String toString() { + return "Aide{" + + "id=" + id + + ", numeroReference='" + numeroReference + '\'' + + ", titre='" + titre + '\'' + + ", typeAide=" + typeAide + + ", statut=" + statut + + ", montantDemande=" + montantDemande + + ", devise='" + devise + '\'' + + ", priorite='" + priorite + '\'' + + '}'; + } +} diff --git a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/repository/AideRepository.java b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/repository/AideRepository.java new file mode 100644 index 0000000..69da99d --- /dev/null +++ b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/repository/AideRepository.java @@ -0,0 +1,435 @@ +package dev.lions.unionflow.server.repository; + +import dev.lions.unionflow.server.api.enums.solidarite.StatutAide; +import dev.lions.unionflow.server.api.enums.solidarite.TypeAide; +import dev.lions.unionflow.server.entity.Aide; +import io.quarkus.hibernate.orm.panache.PanacheRepository; +import io.quarkus.panache.common.Page; +import io.quarkus.panache.common.Sort; +import jakarta.enterprise.context.ApplicationScoped; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * Repository pour la gestion des demandes d'aide et de solidarité + * Utilise Panache pour simplifier les opérations JPA + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-15 + */ +@ApplicationScoped +public class AideRepository implements PanacheRepository { + + /** + * Trouve une aide par son numéro de référence + * + * @param numeroReference le numéro de référence unique + * @return Optional contenant l'aide si trouvée + */ + public Optional findByNumeroReference(String numeroReference) { + return find("numeroReference = ?1", numeroReference).firstResultOptional(); + } + + /** + * Trouve toutes les aides actives + * + * @return liste des aides actives + */ + public List findAllActives() { + return find("actif = true", Sort.by("dateCreation").descending()).list(); + } + + /** + * Trouve les aides par statut + * + * @param statut le statut recherché + * @return liste des aides avec ce statut + */ + public List findByStatut(StatutAide statut) { + return find("statut = ?1 and actif = true", Sort.by("dateCreation").descending(), statut).list(); + } + + /** + * Trouve les aides par type + * + * @param typeAide le type d'aide recherché + * @return liste des aides de ce type + */ + public List findByTypeAide(TypeAide typeAide) { + return find("typeAide = ?1 and actif = true", Sort.by("dateCreation").descending(), typeAide).list(); + } + + /** + * Trouve les aides d'un membre demandeur + * + * @param membreId identifiant du membre demandeur + * @return liste des aides du membre + */ + public List findByMembreDemandeur(Long membreId) { + return find("membreDemandeur.id = ?1 and actif = true", + Sort.by("dateCreation").descending(), membreId).list(); + } + + /** + * Trouve les aides d'une organisation + * + * @param organisationId identifiant de l'organisation + * @return liste des aides de l'organisation + */ + public List findByOrganisation(Long organisationId) { + return find("organisation.id = ?1 and actif = true", + Sort.by("dateCreation").descending(), organisationId).list(); + } + + /** + * Trouve les aides par priorité + * + * @param priorite la priorité recherchée + * @return liste des aides avec cette priorité + */ + public List findByPriorite(String priorite) { + return find("priorite = ?1 and actif = true", + Sort.by("dateCreation").descending(), priorite).list(); + } + + /** + * Trouve les aides urgentes en attente + * + * @return liste des aides urgentes en attente + */ + public List findAidesUrgentesEnAttente() { + return find("priorite = 'URGENTE' and statut = ?1 and actif = true", + Sort.by("dateCreation").ascending(), StatutAide.EN_ATTENTE).list(); + } + + /** + * Trouve les aides publiques (visibles par tous) + * + * @param page pagination + * @param sort tri + * @return liste paginée des aides publiques + */ + public List findAidesPubliques(Page page, Sort sort) { + return find("aidePublique = true and actif = true", sort).page(page).list(); + } + + /** + * Trouve les aides en attente d'évaluation + * + * @param page pagination + * @param sort tri + * @return liste paginée des aides en attente + */ + public List findAidesEnAttente(Page page, Sort sort) { + return find("statut = ?1 and actif = true", sort, StatutAide.EN_ATTENTE).page(page).list(); + } + + /** + * Trouve les aides approuvées non encore versées + * + * @return liste des aides approuvées + */ + public List findAidesApprouveesNonVersees() { + return find("statut = ?1 and actif = true", + Sort.by("dateEvaluation").ascending(), StatutAide.APPROUVEE).list(); + } + + /** + * Trouve les aides avec date limite proche + * + * @param joursAvantLimite nombre de jours avant la limite + * @return liste des aides avec date limite proche + */ + public List findAidesAvecDateLimiteProche(int joursAvantLimite) { + LocalDate dateLimite = LocalDate.now().plusDays(joursAvantLimite); + return find("dateLimite <= ?1 and statut = ?2 and actif = true", + Sort.by("dateLimite").ascending(), dateLimite, StatutAide.EN_ATTENTE).list(); + } + + /** + * Recherche textuelle dans les titres et descriptions + * + * @param recherche terme de recherche + * @param page pagination + * @param sort tri + * @return liste paginée des aides correspondantes + */ + public List rechercheTextuelle(String recherche, Page page, Sort sort) { + String pattern = "%" + recherche.toLowerCase() + "%"; + return find("(lower(titre) like ?1 or lower(description) like ?1) and actif = true", + sort, pattern).page(page).list(); + } + + /** + * Recherche avancée avec filtres multiples + * + * @param membreId identifiant du membre (optionnel) + * @param organisationId identifiant de l'organisation (optionnel) + * @param statut statut (optionnel) + * @param typeAide type d'aide (optionnel) + * @param priorite priorité (optionnel) + * @param dateCreationMin date de création minimum (optionnel) + * @param dateCreationMax date de création maximum (optionnel) + * @param montantMin montant minimum (optionnel) + * @param montantMax montant maximum (optionnel) + * @param page pagination + * @param sort tri + * @return liste filtrée des aides + */ + public List rechercheAvancee(Long membreId, Long organisationId, StatutAide statut, + TypeAide typeAide, String priorite, LocalDate dateCreationMin, + LocalDate dateCreationMax, BigDecimal montantMin, + BigDecimal montantMax, Page page, Sort sort) { + StringBuilder query = new StringBuilder("actif = true"); + Map params = new java.util.HashMap<>(); + + if (membreId != null) { + query.append(" and membreDemandeur.id = :membreId"); + params.put("membreId", membreId); + } + + if (organisationId != null) { + query.append(" and organisation.id = :organisationId"); + params.put("organisationId", organisationId); + } + + if (statut != null) { + query.append(" and statut = :statut"); + params.put("statut", statut); + } + + if (typeAide != null) { + query.append(" and typeAide = :typeAide"); + params.put("typeAide", typeAide); + } + + if (priorite != null && !priorite.isEmpty()) { + query.append(" and priorite = :priorite"); + params.put("priorite", priorite); + } + + if (dateCreationMin != null) { + query.append(" and date(dateCreation) >= :dateCreationMin"); + params.put("dateCreationMin", dateCreationMin); + } + + if (dateCreationMax != null) { + query.append(" and date(dateCreation) <= :dateCreationMax"); + params.put("dateCreationMax", dateCreationMax); + } + + if (montantMin != null) { + query.append(" and montantDemande >= :montantMin"); + params.put("montantMin", montantMin); + } + + if (montantMax != null) { + query.append(" and montantDemande <= :montantMax"); + params.put("montantMax", montantMax); + } + + return find(query.toString(), sort, params).page(page).list(); + } + + /** + * Compte les aides par statut + * + * @param statut le statut + * @return nombre d'aides avec ce statut + */ + public long countByStatut(StatutAide statut) { + return count("statut = ?1 and actif = true", statut); + } + + /** + * Compte les aides par type + * + * @param typeAide le type d'aide + * @return nombre d'aides de ce type + */ + public long countByTypeAide(TypeAide typeAide) { + return count("typeAide = ?1 and actif = true", typeAide); + } + + /** + * Compte les aides d'un membre + * + * @param membreId identifiant du membre + * @return nombre d'aides du membre + */ + public long countByMembreDemandeur(Long membreId) { + return count("membreDemandeur.id = ?1 and actif = true", membreId); + } + + /** + * Calcule le montant total demandé par statut + * + * @param statut le statut + * @return montant total demandé + */ + public BigDecimal sumMontantDemandeByStatut(StatutAide statut) { + BigDecimal result = find("select sum(a.montantDemande) from Aide a where a.statut = ?1 and a.actif = true", statut) + .project(BigDecimal.class) + .firstResult(); + return result != null ? result : BigDecimal.ZERO; + } + + /** + * Calcule le montant total versé + * + * @return montant total versé + */ + public BigDecimal sumMontantVerse() { + BigDecimal result = find("select sum(a.montantVerse) from Aide a where a.montantVerse is not null and a.actif = true") + .project(BigDecimal.class) + .firstResult(); + return result != null ? result : BigDecimal.ZERO; + } + + /** + * Trouve les aides nécessitant un suivi + * (approuvées depuis plus de X jours sans versement) + * + * @param joursDepuisApprobation nombre de jours depuis l'approbation + * @return liste des aides nécessitant un suivi + */ + public List findAidesNecessitantSuivi(int joursDepuisApprobation) { + LocalDateTime dateLimit = LocalDateTime.now().minusDays(joursDepuisApprobation); + return find("statut = ?1 and dateEvaluation <= ?2 and actif = true", + Sort.by("dateEvaluation").ascending(), StatutAide.APPROUVEE, dateLimit).list(); + } + + /** + * Trouve les aides les plus consultées + * + * @param limite nombre maximum d'aides à retourner + * @return liste des aides les plus consultées + */ + public List findAidesLesPlusConsultees(int limite) { + return find("aidePublique = true and actif = true", + Sort.by("nombreVues").descending()) + .page(Page.ofSize(limite)) + .list(); + } + + /** + * Trouve les aides récentes (créées dans les X derniers jours) + * + * @param nombreJours nombre de jours + * @param page pagination + * @param sort tri + * @return liste paginée des aides récentes + */ + public List findAidesRecentes(int nombreJours, Page page, Sort sort) { + LocalDateTime dateLimit = LocalDateTime.now().minusDays(nombreJours); + return find("dateCreation >= ?1 and actif = true", sort, dateLimit).page(page).list(); + } + + /** + * Statistiques globales des aides + * + * @return map contenant les statistiques + */ + public Map getStatistiquesGlobales() { + Map stats = new java.util.HashMap<>(); + + // Compteurs par statut + stats.put("total", count("actif = true")); + stats.put("enAttente", count("statut = ?1 and actif = true", StatutAide.EN_ATTENTE)); + stats.put("enCours", count("statut = ?1 and actif = true", StatutAide.EN_COURS)); + stats.put("approuvees", count("statut = ?1 and actif = true", StatutAide.APPROUVEE)); + stats.put("versees", count("statut = ?1 and actif = true", StatutAide.VERSEE)); + stats.put("rejetees", count("statut = ?1 and actif = true", StatutAide.REJETEE)); + stats.put("annulees", count("statut = ?1 and actif = true", StatutAide.ANNULEE)); + + // Compteurs par priorité + stats.put("urgentes", count("priorite = 'URGENTE' and actif = true")); + stats.put("hautePriorite", count("priorite = 'HAUTE' and actif = true")); + stats.put("prioriteNormale", count("priorite = 'NORMALE' and actif = true")); + stats.put("bassePriorite", count("priorite = 'BASSE' and actif = true")); + + // Montants + stats.put("montantTotalDemande", sumMontantDemandeByStatut(null)); + stats.put("montantTotalVerse", sumMontantVerse()); + stats.put("montantEnAttente", sumMontantDemandeByStatut(StatutAide.EN_ATTENTE)); + stats.put("montantApprouve", sumMontantDemandeByStatut(StatutAide.APPROUVEE)); + + // Aides publiques vs privées + stats.put("aidesPubliques", count("aidePublique = true and actif = true")); + stats.put("aidesPrivees", count("aidePublique = false and actif = true")); + stats.put("aidesAnonymes", count("aideAnonyme = true and actif = true")); + + return stats; + } + + /** + * Statistiques par période + * + * @param dateDebut date de début + * @param dateFin date de fin + * @return map contenant les statistiques de la période + */ + public Map getStatistiquesPeriode(LocalDate dateDebut, LocalDate dateFin) { + Map stats = new java.util.HashMap<>(); + + LocalDateTime dateDebutTime = dateDebut.atStartOfDay(); + LocalDateTime dateFinTime = dateFin.atTime(23, 59, 59); + + String baseQuery = "dateCreation >= ?1 and dateCreation <= ?2 and actif = true"; + + stats.put("totalPeriode", count(baseQuery, dateDebutTime, dateFinTime)); + stats.put("enAttentePeriode", count(baseQuery + " and statut = ?3", + dateDebutTime, dateFinTime, StatutAide.EN_ATTENTE)); + stats.put("approuveesPeriode", count(baseQuery + " and statut = ?3", + dateDebutTime, dateFinTime, StatutAide.APPROUVEE)); + stats.put("verseesPeriode", count(baseQuery + " and statut = ?3", + dateDebutTime, dateFinTime, StatutAide.VERSEE)); + + // Montant total demandé sur la période + BigDecimal montantPeriode = find("select sum(a.montantDemande) from Aide a where " + baseQuery, + dateDebutTime, dateFinTime) + .project(BigDecimal.class) + .firstResult(); + stats.put("montantTotalPeriode", montantPeriode != null ? montantPeriode : BigDecimal.ZERO); + + return stats; + } + + /** + * Trouve les aides par évaluateur + * + * @param evaluateurId identifiant de l'évaluateur + * @param page pagination + * @param sort tri + * @return liste paginée des aides évaluées par ce membre + */ + public List findByEvaluateur(Long evaluateurId, Page page, Sort sort) { + return find("evaluePar.id = ?1 and actif = true", sort, evaluateurId).page(page).list(); + } + + /** + * Trouve les aides avec justificatifs manquants + * + * @return liste des aides sans justificatifs + */ + public List findAidesSansJustificatifs() { + return find("justificatifsFournis = false and statut = ?1 and actif = true", + Sort.by("dateCreation").ascending(), StatutAide.EN_ATTENTE).list(); + } + + /** + * Met à jour le nombre de vues d'une aide + * + * @param aideId identifiant de l'aide + */ + public void incrementerNombreVues(Long aideId) { + update("nombreVues = nombreVues + 1, dateModification = ?1 where id = ?2", + LocalDateTime.now(), aideId); + } +} diff --git a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/AideResource.java b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/AideResource.java new file mode 100644 index 0000000..c39a911 --- /dev/null +++ b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/AideResource.java @@ -0,0 +1,625 @@ +package dev.lions.unionflow.server.resource; + +import dev.lions.unionflow.server.api.dto.solidarite.aide.AideDTO; +import dev.lions.unionflow.server.api.enums.solidarite.StatutAide; +import dev.lions.unionflow.server.api.enums.solidarite.TypeAide; +import dev.lions.unionflow.server.service.AideService; +import jakarta.annotation.security.RolesAllowed; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; +import org.jboss.logging.Logger; + +import java.math.BigDecimal; +import java.net.URI; +import java.time.LocalDate; +import java.util.List; +import java.util.Map; + +/** + * Resource REST pour la gestion des demandes d'aide et de solidarité + * Expose les endpoints API pour les opérations CRUD et métier sur les aides + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-15 + */ +@Path("/api/aides") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +@ApplicationScoped +@Tag(name = "Aides", description = "Gestion des demandes d'aide et de solidarité") +public class AideResource { + + private static final Logger LOG = Logger.getLogger(AideResource.class); + + @Inject + AideService aideService; + + // ===== OPÉRATIONS CRUD ===== + + /** + * Liste toutes les demandes d'aide actives avec pagination + */ + @GET + @Operation(summary = "Lister toutes les demandes d'aide actives", + description = "Récupère la liste paginée des demandes d'aide actives") + @APIResponse(responseCode = "200", description = "Liste des demandes d'aide actives") + @APIResponse(responseCode = "401", description = "Non authentifié") + @RolesAllowed({"admin", "gestionnaire_aide", "evaluateur_aide", "membre"}) + public Response listerAides( + @Parameter(description = "Numéro de page (0-based)", example = "0") + @QueryParam("page") @DefaultValue("0") @Min(0) int page, + + @Parameter(description = "Taille de la page", example = "20") + @QueryParam("size") @DefaultValue("20") @Min(1) int size) { + + try { + LOG.infof("GET /api/aides - page: %d, size: %d", page, size); + + List aides = aideService.listerAidesActives(page, size); + + LOG.infof("Récupération réussie de %d demandes d'aide", aides.size()); + return Response.ok(aides).build(); + + } catch (Exception e) { + LOG.errorf("Erreur lors de la récupération des demandes d'aide: %s", e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Erreur lors de la récupération des demandes d'aide")) + .build(); + } + } + + /** + * Récupère une demande d'aide par son ID + */ + @GET + @Path("/{id}") + @Operation(summary = "Récupérer une demande d'aide par ID") + @APIResponse(responseCode = "200", description = "Demande d'aide trouvée") + @APIResponse(responseCode = "404", description = "Demande d'aide non trouvée") + @RolesAllowed({"admin", "gestionnaire_aide", "evaluateur_aide", "membre"}) + public Response obtenirAide( + @Parameter(description = "ID de la demande d'aide", required = true) + @PathParam("id") Long id) { + + try { + LOG.infof("GET /api/aides/%d", id); + + AideDTO aide = aideService.obtenirAideParId(id); + + return Response.ok(aide).build(); + + } catch (jakarta.ws.rs.NotFoundException e) { + LOG.warnf("Demande d'aide non trouvée: ID %d", id); + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of("error", "Demande d'aide non trouvée")) + .build(); + } catch (Exception e) { + LOG.errorf("Erreur lors de la récupération de la demande d'aide %d: %s", id, e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Erreur lors de la récupération de la demande d'aide")) + .build(); + } + } + + /** + * Récupère une demande d'aide par son numéro de référence + */ + @GET + @Path("/reference/{numeroReference}") + @Operation(summary = "Récupérer une demande d'aide par numéro de référence") + @APIResponse(responseCode = "200", description = "Demande d'aide trouvée") + @APIResponse(responseCode = "404", description = "Demande d'aide non trouvée") + @RolesAllowed({"admin", "gestionnaire_aide", "evaluateur_aide", "membre"}) + public Response obtenirAideParReference( + @Parameter(description = "Numéro de référence de la demande", required = true) + @PathParam("numeroReference") String numeroReference) { + + try { + LOG.infof("GET /api/aides/reference/%s", numeroReference); + + AideDTO aide = aideService.obtenirAideParReference(numeroReference); + + return Response.ok(aide).build(); + + } catch (jakarta.ws.rs.NotFoundException e) { + LOG.warnf("Demande d'aide non trouvée: référence %s", numeroReference); + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of("error", "Demande d'aide non trouvée")) + .build(); + } catch (Exception e) { + LOG.errorf("Erreur lors de la récupération de la demande d'aide %s: %s", numeroReference, e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Erreur lors de la récupération de la demande d'aide")) + .build(); + } + } + + /** + * Crée une nouvelle demande d'aide + */ + @POST + @Operation(summary = "Créer une nouvelle demande d'aide") + @APIResponses({ + @APIResponse(responseCode = "201", description = "Demande d'aide créée avec succès", + content = @Content(schema = @Schema(implementation = AideDTO.class))), + @APIResponse(responseCode = "400", description = "Données invalides"), + @APIResponse(responseCode = "404", description = "Membre ou organisation non trouvé") + }) + @RolesAllowed({"admin", "gestionnaire_aide", "membre"}) + public Response creerAide( + @Parameter(description = "Données de la demande d'aide à créer", required = true) + @Valid AideDTO aideDTO) { + + try { + LOG.infof("POST /api/aides - Création demande d'aide: %s", aideDTO.getTitre()); + + AideDTO aideCree = aideService.creerAide(aideDTO); + + LOG.infof("Demande d'aide créée avec succès: %s", aideCree.getNumeroReference()); + return Response.status(Response.Status.CREATED) + .location(URI.create("/api/aides/" + aideCree.getId())) + .entity(aideCree) + .build(); + + } catch (jakarta.ws.rs.NotFoundException e) { + LOG.warnf("Membre ou organisation non trouvé lors de la création: %s", e.getMessage()); + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of("error", "Membre ou organisation non trouvé")) + .build(); + } catch (IllegalArgumentException e) { + LOG.warnf("Données invalides pour la création: %s", e.getMessage()); + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("error", "Données invalides", "message", e.getMessage())) + .build(); + } catch (Exception e) { + LOG.errorf("Erreur lors de la création de la demande d'aide: %s", e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Erreur lors de la création de la demande d'aide")) + .build(); + } + } + + /** + * Met à jour une demande d'aide existante + */ + @PUT + @Path("/{id}") + @Operation(summary = "Mettre à jour une demande d'aide") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Demande d'aide mise à jour avec succès"), + @APIResponse(responseCode = "400", description = "Données invalides"), + @APIResponse(responseCode = "403", description = "Accès non autorisé"), + @APIResponse(responseCode = "404", description = "Demande d'aide non trouvée") + }) + @RolesAllowed({"admin", "gestionnaire_aide", "membre"}) + public Response mettreAJourAide( + @Parameter(description = "ID de la demande d'aide", required = true) + @PathParam("id") Long id, + + @Parameter(description = "Nouvelles données de la demande d'aide", required = true) + @Valid AideDTO aideDTO) { + + try { + LOG.infof("PUT /api/aides/%d", id); + + AideDTO aideMiseAJour = aideService.mettreAJourAide(id, aideDTO); + + LOG.infof("Demande d'aide mise à jour avec succès: %s", aideMiseAJour.getNumeroReference()); + return Response.ok(aideMiseAJour).build(); + + } catch (jakarta.ws.rs.NotFoundException e) { + LOG.warnf("Demande d'aide non trouvée pour mise à jour: ID %d", id); + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of("error", "Demande d'aide non trouvée")) + .build(); + } catch (SecurityException e) { + LOG.warnf("Accès non autorisé pour mise à jour: ID %d", id); + return Response.status(Response.Status.FORBIDDEN) + .entity(Map.of("error", "Accès non autorisé")) + .build(); + } catch (IllegalStateException e) { + LOG.warnf("État invalide pour mise à jour: %s", e.getMessage()); + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("error", "État invalide", "message", e.getMessage())) + .build(); + } catch (IllegalArgumentException e) { + LOG.warnf("Données invalides pour mise à jour: %s", e.getMessage()); + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("error", "Données invalides", "message", e.getMessage())) + .build(); + } catch (Exception e) { + LOG.errorf("Erreur lors de la mise à jour de la demande d'aide %d: %s", id, e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Erreur lors de la mise à jour")) + .build(); + } + } + + // ===== OPÉRATIONS MÉTIER SPÉCIALISÉES ===== + + /** + * Approuve une demande d'aide + */ + @POST + @Path("/{id}/approuver") + @Operation(summary = "Approuver une demande d'aide") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Demande d'aide approuvée avec succès"), + @APIResponse(responseCode = "400", description = "Données invalides"), + @APIResponse(responseCode = "403", description = "Accès non autorisé"), + @APIResponse(responseCode = "404", description = "Demande d'aide non trouvée") + }) + @RolesAllowed({"admin", "gestionnaire_aide", "evaluateur_aide"}) + public Response approuverAide( + @Parameter(description = "ID de la demande d'aide", required = true) + @PathParam("id") Long id, + + @Parameter(description = "Données d'approbation", required = true) + Map approbationData) { + + try { + LOG.infof("POST /api/aides/%d/approuver", id); + + // Extraction des données d'approbation + BigDecimal montantApprouve = new BigDecimal(approbationData.get("montantApprouve").toString()); + String commentaires = (String) approbationData.get("commentaires"); + + AideDTO aideApprouvee = aideService.approuverAide(id, montantApprouve, commentaires); + + LOG.infof("Demande d'aide approuvée avec succès: %s", aideApprouvee.getNumeroReference()); + return Response.ok(aideApprouvee).build(); + + } catch (jakarta.ws.rs.NotFoundException e) { + LOG.warnf("Demande d'aide non trouvée pour approbation: ID %d", id); + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of("error", "Demande d'aide non trouvée")) + .build(); + } catch (SecurityException e) { + LOG.warnf("Accès non autorisé pour approbation: ID %d", id); + return Response.status(Response.Status.FORBIDDEN) + .entity(Map.of("error", "Accès non autorisé")) + .build(); + } catch (IllegalStateException e) { + LOG.warnf("État invalide pour approbation: %s", e.getMessage()); + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("error", "État invalide", "message", e.getMessage())) + .build(); + } catch (IllegalArgumentException e) { + LOG.warnf("Données invalides pour approbation: %s", e.getMessage()); + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("error", "Données invalides", "message", e.getMessage())) + .build(); + } catch (Exception e) { + LOG.errorf("Erreur lors de l'approbation de la demande d'aide %d: %s", id, e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Erreur lors de l'approbation")) + .build(); + } + } + + /** + * Rejette une demande d'aide + */ + @POST + @Path("/{id}/rejeter") + @Operation(summary = "Rejeter une demande d'aide") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Demande d'aide rejetée avec succès"), + @APIResponse(responseCode = "400", description = "Données invalides"), + @APIResponse(responseCode = "403", description = "Accès non autorisé"), + @APIResponse(responseCode = "404", description = "Demande d'aide non trouvée") + }) + @RolesAllowed({"admin", "gestionnaire_aide", "evaluateur_aide"}) + public Response rejeterAide( + @Parameter(description = "ID de la demande d'aide", required = true) + @PathParam("id") Long id, + + @Parameter(description = "Données de rejet", required = true) + Map rejetData) { + + try { + LOG.infof("POST /api/aides/%d/rejeter", id); + + String raisonRejet = rejetData.get("raisonRejet"); + if (raisonRejet == null || raisonRejet.trim().isEmpty()) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("error", "La raison du rejet est obligatoire")) + .build(); + } + + AideDTO aideRejetee = aideService.rejeterAide(id, raisonRejet); + + LOG.infof("Demande d'aide rejetée avec succès: %s", aideRejetee.getNumeroReference()); + return Response.ok(aideRejetee).build(); + + } catch (jakarta.ws.rs.NotFoundException e) { + LOG.warnf("Demande d'aide non trouvée pour rejet: ID %d", id); + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of("error", "Demande d'aide non trouvée")) + .build(); + } catch (SecurityException e) { + LOG.warnf("Accès non autorisé pour rejet: ID %d", id); + return Response.status(Response.Status.FORBIDDEN) + .entity(Map.of("error", "Accès non autorisé")) + .build(); + } catch (IllegalStateException e) { + LOG.warnf("État invalide pour rejet: %s", e.getMessage()); + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("error", "État invalide", "message", e.getMessage())) + .build(); + } catch (Exception e) { + LOG.errorf("Erreur lors du rejet de la demande d'aide %d: %s", id, e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Erreur lors du rejet")) + .build(); + } + } + + /** + * Marque une aide comme versée + */ + @POST + @Path("/{id}/verser") + @Operation(summary = "Marquer une aide comme versée") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Aide marquée comme versée avec succès"), + @APIResponse(responseCode = "400", description = "Données invalides"), + @APIResponse(responseCode = "403", description = "Accès non autorisé"), + @APIResponse(responseCode = "404", description = "Demande d'aide non trouvée") + }) + @RolesAllowed({"admin", "gestionnaire_aide", "tresorier"}) + public Response marquerCommeVersee( + @Parameter(description = "ID de la demande d'aide", required = true) + @PathParam("id") Long id, + + @Parameter(description = "Données de versement", required = true) + Map versementData) { + + try { + LOG.infof("POST /api/aides/%d/verser", id); + + // Extraction des données de versement + BigDecimal montantVerse = new BigDecimal(versementData.get("montantVerse").toString()); + String modeVersement = (String) versementData.get("modeVersement"); + String numeroTransaction = (String) versementData.get("numeroTransaction"); + + AideDTO aideVersee = aideService.marquerCommeVersee(id, montantVerse, modeVersement, numeroTransaction); + + LOG.infof("Aide marquée comme versée avec succès: %s", aideVersee.getNumeroReference()); + return Response.ok(aideVersee).build(); + + } catch (jakarta.ws.rs.NotFoundException e) { + LOG.warnf("Demande d'aide non trouvée pour versement: ID %d", id); + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of("error", "Demande d'aide non trouvée")) + .build(); + } catch (SecurityException e) { + LOG.warnf("Accès non autorisé pour versement: ID %d", id); + return Response.status(Response.Status.FORBIDDEN) + .entity(Map.of("error", "Accès non autorisé")) + .build(); + } catch (IllegalStateException e) { + LOG.warnf("État invalide pour versement: %s", e.getMessage()); + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("error", "État invalide", "message", e.getMessage())) + .build(); + } catch (IllegalArgumentException e) { + LOG.warnf("Données invalides pour versement: %s", e.getMessage()); + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("error", "Données invalides", "message", e.getMessage())) + .build(); + } catch (Exception e) { + LOG.errorf("Erreur lors du versement de l'aide %d: %s", id, e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Erreur lors du versement")) + .build(); + } + } + + // ===== ENDPOINTS DE RECHERCHE ET FILTRAGE ===== + + /** + * Liste les demandes d'aide par statut + */ + @GET + @Path("/statut/{statut}") + @Operation(summary = "Lister les demandes d'aide par statut") + @APIResponse(responseCode = "200", description = "Liste des demandes d'aide par statut") + @RolesAllowed({"admin", "gestionnaire_aide", "evaluateur_aide", "membre"}) + public Response listerAidesParStatut( + @Parameter(description = "Statut des demandes d'aide", required = true) + @PathParam("statut") StatutAide statut, + + @Parameter(description = "Numéro de page (0-based)", example = "0") + @QueryParam("page") @DefaultValue("0") @Min(0) int page, + + @Parameter(description = "Taille de la page", example = "20") + @QueryParam("size") @DefaultValue("20") @Min(1) int size) { + + try { + LOG.infof("GET /api/aides/statut/%s - page: %d, size: %d", statut, page, size); + + List aides = aideService.listerAidesParStatut(statut, page, size); + + LOG.infof("Récupération réussie de %d demandes d'aide avec statut %s", aides.size(), statut); + return Response.ok(aides).build(); + + } catch (Exception e) { + LOG.errorf("Erreur lors de la récupération des demandes d'aide par statut %s: %s", statut, e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Erreur lors de la récupération")) + .build(); + } + } + + /** + * Liste les demandes d'aide d'un membre + */ + @GET + @Path("/membre/{membreId}") + @Operation(summary = "Lister les demandes d'aide d'un membre") + @APIResponse(responseCode = "200", description = "Liste des demandes d'aide du membre") + @APIResponse(responseCode = "404", description = "Membre non trouvé") + @RolesAllowed({"admin", "gestionnaire_aide", "evaluateur_aide", "membre"}) + public Response listerAidesParMembre( + @Parameter(description = "ID du membre", required = true) + @PathParam("membreId") Long membreId, + + @Parameter(description = "Numéro de page (0-based)", example = "0") + @QueryParam("page") @DefaultValue("0") @Min(0) int page, + + @Parameter(description = "Taille de la page", example = "20") + @QueryParam("size") @DefaultValue("20") @Min(1) int size) { + + try { + LOG.infof("GET /api/aides/membre/%d - page: %d, size: %d", membreId, page, size); + + List aides = aideService.listerAidesParMembre(membreId, page, size); + + LOG.infof("Récupération réussie de %d demandes d'aide pour le membre %d", aides.size(), membreId); + return Response.ok(aides).build(); + + } catch (jakarta.ws.rs.NotFoundException e) { + LOG.warnf("Membre non trouvé: ID %d", membreId); + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of("error", "Membre non trouvé")) + .build(); + } catch (Exception e) { + LOG.errorf("Erreur lors de la récupération des demandes d'aide du membre %d: %s", membreId, e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Erreur lors de la récupération")) + .build(); + } + } + + /** + * Liste les demandes d'aide publiques + */ + @GET + @Path("/publiques") + @Operation(summary = "Lister les demandes d'aide publiques") + @APIResponse(responseCode = "200", description = "Liste des demandes d'aide publiques") + @RolesAllowed({"admin", "gestionnaire_aide", "evaluateur_aide", "membre"}) + public Response listerAidesPubliques( + @Parameter(description = "Numéro de page (0-based)", example = "0") + @QueryParam("page") @DefaultValue("0") @Min(0) int page, + + @Parameter(description = "Taille de la page", example = "20") + @QueryParam("size") @DefaultValue("20") @Min(1) int size) { + + try { + LOG.infof("GET /api/aides/publiques - page: %d, size: %d", page, size); + + List aides = aideService.listerAidesPubliques(page, size); + + LOG.infof("Récupération réussie de %d demandes d'aide publiques", aides.size()); + return Response.ok(aides).build(); + + } catch (Exception e) { + LOG.errorf("Erreur lors de la récupération des demandes d'aide publiques: %s", e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Erreur lors de la récupération")) + .build(); + } + } + + /** + * Recherche textuelle dans les demandes d'aide + */ + @GET + @Path("/recherche") + @Operation(summary = "Rechercher des demandes d'aide par texte") + @APIResponse(responseCode = "200", description = "Résultats de la recherche") + @RolesAllowed({"admin", "gestionnaire_aide", "evaluateur_aide", "membre"}) + public Response rechercherAides( + @Parameter(description = "Terme de recherche", required = true) + @QueryParam("q") @NotNull String recherche, + + @Parameter(description = "Numéro de page (0-based)", example = "0") + @QueryParam("page") @DefaultValue("0") @Min(0) int page, + + @Parameter(description = "Taille de la page", example = "20") + @QueryParam("size") @DefaultValue("20") @Min(1) int size) { + + try { + LOG.infof("GET /api/aides/recherche?q=%s - page: %d, size: %d", recherche, page, size); + + List aides = aideService.rechercherAides(recherche, page, size); + + LOG.infof("Recherche réussie: %d demandes d'aide trouvées pour '%s'", aides.size(), recherche); + return Response.ok(aides).build(); + + } catch (Exception e) { + LOG.errorf("Erreur lors de la recherche de demandes d'aide: %s", e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Erreur lors de la recherche")) + .build(); + } + } + + /** + * Statistiques globales des demandes d'aide + */ + @GET + @Path("/statistiques") + @Operation(summary = "Obtenir les statistiques des demandes d'aide") + @APIResponse(responseCode = "200", description = "Statistiques des demandes d'aide") + @RolesAllowed({"admin", "gestionnaire_aide", "evaluateur_aide"}) + public Response obtenirStatistiques() { + + try { + LOG.info("GET /api/aides/statistiques"); + + Map statistiques = aideService.obtenirStatistiquesGlobales(); + + LOG.info("Statistiques calculées avec succès"); + return Response.ok(statistiques).build(); + + } catch (Exception e) { + LOG.errorf("Erreur lors du calcul des statistiques: %s", e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Erreur lors du calcul des statistiques")) + .build(); + } + } + + /** + * Demandes d'aide urgentes en attente + */ + @GET + @Path("/urgentes") + @Operation(summary = "Lister les demandes d'aide urgentes en attente") + @APIResponse(responseCode = "200", description = "Liste des demandes d'aide urgentes") + @RolesAllowed({"admin", "gestionnaire_aide", "evaluateur_aide"}) + public Response listerAidesUrgentes() { + + try { + LOG.info("GET /api/aides/urgentes"); + + List aides = aideService.listerAidesUrgentesEnAttente(); + + LOG.infof("Récupération réussie de %d demandes d'aide urgentes", aides.size()); + return Response.ok(aides).build(); + + } catch (Exception e) { + LOG.errorf("Erreur lors de la récupération des demandes d'aide urgentes: %s", e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Erreur lors de la récupération")) + .build(); + } + } +} diff --git a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/service/AideService.java b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/service/AideService.java new file mode 100644 index 0000000..43ad56e --- /dev/null +++ b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/service/AideService.java @@ -0,0 +1,865 @@ +package dev.lions.unionflow.server.service; + +import dev.lions.unionflow.server.api.dto.solidarite.aide.AideDTO; +import dev.lions.unionflow.server.api.enums.solidarite.StatutAide; +import dev.lions.unionflow.server.api.enums.solidarite.TypeAide; +import dev.lions.unionflow.server.entity.Aide; +import dev.lions.unionflow.server.entity.Membre; +import dev.lions.unionflow.server.entity.Organisation; +import dev.lions.unionflow.server.repository.AideRepository; +import dev.lions.unionflow.server.repository.MembreRepository; +import dev.lions.unionflow.server.repository.OrganisationRepository; +import dev.lions.unionflow.server.security.KeycloakService; +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 org.jboss.logging.Logger; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * Service métier pour la gestion des demandes d'aide et de solidarité + * Implémente la logique métier complète avec validation, sécurité et gestion d'erreurs + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-15 + */ +@ApplicationScoped +public class AideService { + + private static final Logger LOG = Logger.getLogger(AideService.class); + + @Inject + AideRepository aideRepository; + + @Inject + MembreRepository membreRepository; + + @Inject + OrganisationRepository organisationRepository; + + @Inject + KeycloakService keycloakService; + + // ===== OPÉRATIONS CRUD ===== + + /** + * Crée une nouvelle demande d'aide + * + * @param aideDTO données de la demande d'aide + * @return DTO de l'aide créée + */ + @Transactional + public AideDTO creerAide(@Valid AideDTO aideDTO) { + LOG.infof("Création d'une nouvelle demande d'aide: %s", aideDTO.getTitre()); + + // Validation du membre demandeur + Membre membreDemandeur = membreRepository.findByIdOptional(Long.valueOf(aideDTO.getMembreDemandeurId().toString())) + .orElseThrow(() -> new NotFoundException("Membre demandeur non trouvé avec l'ID: " + aideDTO.getMembreDemandeurId())); + + // Validation de l'organisation + Organisation organisation = organisationRepository.findByIdOptional(Long.valueOf(aideDTO.getAssociationId().toString())) + .orElseThrow(() -> new NotFoundException("Organisation non trouvée avec l'ID: " + aideDTO.getAssociationId())); + + // Conversion DTO vers entité + Aide aide = convertFromDTO(aideDTO); + aide.setMembreDemandeur(membreDemandeur); + aide.setOrganisation(organisation); + + // Génération automatique du numéro de référence si absent + if (aide.getNumeroReference() == null || aide.getNumeroReference().isEmpty()) { + aide.setNumeroReference(Aide.genererNumeroReference()); + } + + // Métadonnées de création + aide.setCreePar(keycloakService.getCurrentUserEmail()); + aide.setDateCreation(LocalDateTime.now()); + + // Validation des règles métier + validerReglesMétier(aide); + + // Persistance + aideRepository.persist(aide); + + LOG.infof("Demande d'aide créée avec succès - ID: %d, Référence: %s", aide.id, aide.getNumeroReference()); + return convertToDTO(aide); + } + + /** + * Met à jour une demande d'aide existante + * + * @param id identifiant de l'aide + * @param aideDTO nouvelles données + * @return DTO de l'aide mise à jour + */ + @Transactional + public AideDTO mettreAJourAide(@NotNull Long id, @Valid AideDTO aideDTO) { + LOG.infof("Mise à jour de la demande d'aide ID: %d", id); + + Aide aide = aideRepository.findByIdOptional(id) + .orElseThrow(() -> new NotFoundException("Demande d'aide non trouvée avec l'ID: " + id)); + + // Vérifier les permissions de modification + if (!peutModifierAide(aide)) { + throw new SecurityException("Vous n'avez pas les permissions pour modifier cette demande d'aide"); + } + + // Vérifier si la demande peut être modifiée + if (!aide.isPeutEtreModifiee()) { + throw new IllegalStateException("Cette demande d'aide ne peut plus être modifiée (statut: " + aide.getStatut() + ")"); + } + + // Mise à jour des champs modifiables + aide.setTitre(aideDTO.getTitre()); + aide.setDescription(aideDTO.getDescription()); + aide.setMontantDemande(aideDTO.getMontantDemande()); + aide.setDateLimite(aideDTO.getDateLimite()); + aide.setPriorite(aideDTO.getPriorite()); + aide.setDocumentsJoints(aideDTO.getDocumentsJoints()); + aide.setJustificatifsFournis(aideDTO.getJustificatifsFournis()); + + // Métadonnées de modification + aide.setModifiePar(keycloakService.getCurrentUserEmail()); + aide.setDateModification(LocalDateTime.now()); + + // Validation des règles métier + validerReglesMétier(aide); + + LOG.infof("Demande d'aide mise à jour avec succès: %s", aide.getNumeroReference()); + return convertToDTO(aide); + } + + /** + * Récupère une demande d'aide par son ID + * + * @param id identifiant de l'aide + * @return DTO de l'aide + */ + public AideDTO obtenirAideParId(@NotNull Long id) { + LOG.debugf("Récupération de la demande d'aide ID: %d", id); + + Aide aide = aideRepository.findByIdOptional(id) + .orElseThrow(() -> new NotFoundException("Demande d'aide non trouvée avec l'ID: " + id)); + + // Incrémenter le nombre de vues si l'aide est publique + if (aide.getAidePublique() && !aide.getMembreDemandeur().getEmail().equals(keycloakService.getCurrentUserEmail())) { + aide.incrementerVues(); + } + + return convertToDTO(aide); + } + + /** + * Récupère une demande d'aide par son numéro de référence + * + * @param numeroReference numéro de référence unique + * @return DTO de l'aide + */ + public AideDTO obtenirAideParReference(@NotNull String numeroReference) { + LOG.debugf("Récupération de la demande d'aide par référence: %s", numeroReference); + + Aide aide = aideRepository.findByNumeroReference(numeroReference) + .orElseThrow(() -> new NotFoundException("Demande d'aide non trouvée avec la référence: " + numeroReference)); + + // Incrémenter le nombre de vues si l'aide est publique + if (aide.getAidePublique() && !aide.getMembreDemandeur().getEmail().equals(keycloakService.getCurrentUserEmail())) { + aide.incrementerVues(); + } + + return convertToDTO(aide); + } + + /** + * Liste toutes les demandes d'aide actives avec pagination + * + * @param page numéro de page + * @param size taille de la page + * @return liste des demandes d'aide + */ + public List listerAidesActives(int page, int size) { + LOG.debugf("Récupération des demandes d'aide actives - page: %d, size: %d", page, size); + + List aides = aideRepository.findAllActives(); + + return aides.stream() + .skip((long) page * size) + .limit(size) + .map(this::convertToDTO) + .collect(Collectors.toList()); + } + + /** + * Liste les demandes d'aide par statut + * + * @param statut statut recherché + * @param page numéro de page + * @param size taille de la page + * @return liste des demandes d'aide + */ + public List listerAidesParStatut(@NotNull StatutAide statut, int page, int size) { + LOG.debugf("Récupération des demandes d'aide par statut: %s", statut); + + List aides = aideRepository.findByStatut(statut); + + return aides.stream() + .skip((long) page * size) + .limit(size) + .map(this::convertToDTO) + .collect(Collectors.toList()); + } + + /** + * Liste les demandes d'aide d'un membre + * + * @param membreId identifiant du membre + * @param page numéro de page + * @param size taille de la page + * @return liste des demandes d'aide du membre + */ + public List listerAidesParMembre(@NotNull Long membreId, int page, int size) { + LOG.debugf("Récupération des demandes d'aide du membre: %d", membreId); + + // Vérification de l'existence du membre + if (!membreRepository.findByIdOptional(membreId).isPresent()) { + throw new NotFoundException("Membre non trouvé avec l'ID: " + membreId); + } + + List aides = aideRepository.findByMembreDemandeur(membreId); + + return aides.stream() + .skip((long) page * size) + .limit(size) + .map(this::convertToDTO) + .collect(Collectors.toList()); + } + + /** + * Liste les demandes d'aide publiques + * + * @param page numéro de page + * @param size taille de la page + * @return liste des demandes d'aide publiques + */ + public List listerAidesPubliques(int page, int size) { + LOG.debugf("Récupération des demandes d'aide publiques"); + + List aides = aideRepository.findAidesPubliques( + Page.of(page, size), + Sort.by("dateCreation").descending() + ); + + return aides.stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + } + + /** + * Recherche textuelle dans les demandes d'aide + * + * @param recherche terme de recherche + * @param page numéro de page + * @param size taille de la page + * @return liste des demandes d'aide correspondantes + */ + public List rechercherAides(@NotNull String recherche, int page, int size) { + LOG.debugf("Recherche textuelle dans les demandes d'aide: %s", recherche); + + List aides = aideRepository.rechercheTextuelle( + recherche, + Page.of(page, size), + Sort.by("dateCreation").descending() + ); + + return aides.stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + } + + // ===== OPÉRATIONS MÉTIER SPÉCIALISÉES ===== + + /** + * Approuve une demande d'aide + * + * @param id identifiant de l'aide + * @param montantApprouve montant approuvé + * @param commentaires commentaires d'évaluation + * @return DTO de l'aide approuvée + */ + @Transactional + public AideDTO approuverAide(@NotNull Long id, @NotNull BigDecimal montantApprouve, String commentaires) { + LOG.infof("Approbation de la demande d'aide ID: %d avec montant: %s", id, montantApprouve); + + Aide aide = aideRepository.findByIdOptional(id) + .orElseThrow(() -> new NotFoundException("Demande d'aide non trouvée avec l'ID: " + id)); + + // Vérifier les permissions d'évaluation + if (!peutEvaluerAide(aide)) { + throw new SecurityException("Vous n'avez pas les permissions pour évaluer cette demande d'aide"); + } + + // Vérifier le statut + if (aide.getStatut() != StatutAide.EN_ATTENTE && aide.getStatut() != StatutAide.EN_COURS) { + throw new IllegalStateException("Cette demande d'aide ne peut pas être approuvée (statut: " + aide.getStatut() + ")"); + } + + // Validation du montant approuvé + if (montantApprouve.compareTo(BigDecimal.ZERO) <= 0) { + throw new IllegalArgumentException("Le montant approuvé doit être positif"); + } + + if (montantApprouve.compareTo(aide.getMontantDemande()) > 0) { + LOG.warnf("Montant approuvé (%s) supérieur au montant demandé (%s) pour l'aide %s", + montantApprouve, aide.getMontantDemande(), aide.getNumeroReference()); + } + + // Récupérer l'évaluateur + String emailEvaluateur = keycloakService.getCurrentUserEmail(); + Membre evaluateur = membreRepository.findByEmail(emailEvaluateur) + .orElseThrow(() -> new NotFoundException("Évaluateur non trouvé: " + emailEvaluateur)); + + // Approuver l'aide + aide.approuver(montantApprouve, evaluateur, commentaires); + + LOG.infof("Demande d'aide approuvée avec succès: %s", aide.getNumeroReference()); + return convertToDTO(aide); + } + + /** + * Rejette une demande d'aide + * + * @param id identifiant de l'aide + * @param raisonRejet raison du rejet + * @return DTO de l'aide rejetée + */ + @Transactional + public AideDTO rejeterAide(@NotNull Long id, @NotNull String raisonRejet) { + LOG.infof("Rejet de la demande d'aide ID: %d", id); + + Aide aide = aideRepository.findByIdOptional(id) + .orElseThrow(() -> new NotFoundException("Demande d'aide non trouvée avec l'ID: " + id)); + + // Vérifier les permissions d'évaluation + if (!peutEvaluerAide(aide)) { + throw new SecurityException("Vous n'avez pas les permissions pour évaluer cette demande d'aide"); + } + + // Vérifier le statut + if (aide.getStatut() != StatutAide.EN_ATTENTE && aide.getStatut() != StatutAide.EN_COURS) { + throw new IllegalStateException("Cette demande d'aide ne peut pas être rejetée (statut: " + aide.getStatut() + ")"); + } + + // Récupérer l'évaluateur + String emailEvaluateur = keycloakService.getCurrentUserEmail(); + Membre evaluateur = membreRepository.findByEmail(emailEvaluateur) + .orElseThrow(() -> new NotFoundException("Évaluateur non trouvé: " + emailEvaluateur)); + + // Rejeter l'aide + aide.rejeter(raisonRejet, evaluateur); + + LOG.infof("Demande d'aide rejetée avec succès: %s", aide.getNumeroReference()); + return convertToDTO(aide); + } + + /** + * Marque une aide comme versée + * + * @param id identifiant de l'aide + * @param montantVerse montant effectivement versé + * @param modeVersement mode de versement + * @param numeroTransaction numéro de transaction + * @return DTO de l'aide versée + */ + @Transactional + public AideDTO marquerCommeVersee(@NotNull Long id, @NotNull BigDecimal montantVerse, + @NotNull String modeVersement, String numeroTransaction) { + LOG.infof("Marquage comme versée de la demande d'aide ID: %d", id); + + Aide aide = aideRepository.findByIdOptional(id) + .orElseThrow(() -> new NotFoundException("Demande d'aide non trouvée avec l'ID: " + id)); + + // Vérifier les permissions + if (!peutGererVersement(aide)) { + throw new SecurityException("Vous n'avez pas les permissions pour gérer les versements"); + } + + // Vérifier le statut + if (aide.getStatut() != StatutAide.APPROUVEE && aide.getStatut() != StatutAide.EN_COURS_VERSEMENT) { + throw new IllegalStateException("Cette demande d'aide ne peut pas être marquée comme versée (statut: " + aide.getStatut() + ")"); + } + + // Validation du montant versé + if (montantVerse.compareTo(BigDecimal.ZERO) <= 0) { + throw new IllegalArgumentException("Le montant versé doit être positif"); + } + + if (montantVerse.compareTo(aide.getMontantApprouve()) > 0) { + throw new IllegalArgumentException("Le montant versé ne peut pas être supérieur au montant approuvé"); + } + + // Marquer comme versée + aide.marquerCommeVersee(montantVerse, modeVersement, numeroTransaction); + + LOG.infof("Demande d'aide marquée comme versée avec succès: %s", aide.getNumeroReference()); + return convertToDTO(aide); + } + + /** + * Annule une demande d'aide + * + * @param id identifiant de l'aide + * @param raisonAnnulation raison de l'annulation + * @return DTO de l'aide annulée + */ + @Transactional + public AideDTO annulerAide(@NotNull Long id, String raisonAnnulation) { + LOG.infof("Annulation de la demande d'aide ID: %d", id); + + Aide aide = aideRepository.findByIdOptional(id) + .orElseThrow(() -> new NotFoundException("Demande d'aide non trouvée avec l'ID: " + id)); + + // Vérifier les permissions d'annulation + if (!peutAnnulerAide(aide)) { + throw new SecurityException("Vous n'avez pas les permissions pour annuler cette demande d'aide"); + } + + // Vérifier si l'aide peut être annulée + if (aide.getStatut() == StatutAide.VERSEE) { + throw new IllegalStateException("Une aide déjà versée ne peut pas être annulée"); + } + + // Annuler l'aide + aide.setStatut(StatutAide.ANNULEE); + aide.setRaisonRejet(raisonAnnulation); + aide.setDateModification(LocalDateTime.now()); + aide.setModifiePar(keycloakService.getCurrentUserEmail()); + + LOG.infof("Demande d'aide annulée avec succès: %s", aide.getNumeroReference()); + return convertToDTO(aide); + } + + // ===== MÉTHODES DE RECHERCHE ET STATISTIQUES ===== + + /** + * Recherche avancée avec filtres multiples + * + * @param membreId identifiant du membre (optionnel) + * @param organisationId identifiant de l'organisation (optionnel) + * @param statut statut (optionnel) + * @param typeAide type d'aide (optionnel) + * @param priorite priorité (optionnel) + * @param dateCreationMin date de création minimum (optionnel) + * @param dateCreationMax date de création maximum (optionnel) + * @param montantMin montant minimum (optionnel) + * @param montantMax montant maximum (optionnel) + * @param page numéro de page + * @param size taille de la page + * @return liste filtrée des demandes d'aide + */ + public List rechercheAvancee(Long membreId, Long organisationId, StatutAide statut, + TypeAide typeAide, String priorite, LocalDate dateCreationMin, + LocalDate dateCreationMax, BigDecimal montantMin, + BigDecimal montantMax, int page, int size) { + LOG.debugf("Recherche avancée de demandes d'aide avec filtres multiples"); + + List aides = aideRepository.rechercheAvancee( + membreId, organisationId, statut, typeAide, priorite, + dateCreationMin, dateCreationMax, montantMin, montantMax, + Page.of(page, size), Sort.by("dateCreation").descending() + ); + + return aides.stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + } + + /** + * Obtient les statistiques globales des demandes d'aide + * + * @return map contenant les statistiques + */ + public Map obtenirStatistiquesGlobales() { + LOG.debug("Récupération des statistiques globales des demandes d'aide"); + return aideRepository.getStatistiquesGlobales(); + } + + /** + * Obtient les statistiques pour une période donnée + * + * @param dateDebut date de début + * @param dateFin date de fin + * @return map contenant les statistiques de la période + */ + public Map obtenirStatistiquesPeriode(@NotNull LocalDate dateDebut, @NotNull LocalDate dateFin) { + LOG.debugf("Récupération des statistiques pour la période: %s - %s", dateDebut, dateFin); + + if (dateDebut.isAfter(dateFin)) { + throw new IllegalArgumentException("La date de début doit être antérieure à la date de fin"); + } + + return aideRepository.getStatistiquesPeriode(dateDebut, dateFin); + } + + /** + * Liste les demandes d'aide urgentes en attente + * + * @return liste des demandes d'aide urgentes + */ + public List listerAidesUrgentesEnAttente() { + LOG.debug("Récupération des demandes d'aide urgentes en attente"); + + List aides = aideRepository.findAidesUrgentesEnAttente(); + + return aides.stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + } + + /** + * Liste les demandes d'aide nécessitant un suivi + * + * @param joursDepuisApprobation nombre de jours depuis l'approbation + * @return liste des demandes d'aide nécessitant un suivi + */ + public List listerAidesNecessitantSuivi(int joursDepuisApprobation) { + LOG.debugf("Récupération des demandes d'aide nécessitant un suivi (%d jours)", joursDepuisApprobation); + + List aides = aideRepository.findAidesNecessitantSuivi(joursDepuisApprobation); + + return aides.stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + } + + /** + * Liste les demandes d'aide les plus consultées + * + * @param limite nombre maximum d'aides à retourner + * @return liste des demandes d'aide les plus consultées + */ + public List listerAidesLesPlusConsultees(int limite) { + LOG.debugf("Récupération des %d demandes d'aide les plus consultées", limite); + + List aides = aideRepository.findAidesLesPlusConsultees(limite); + + return aides.stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + } + + /** + * Liste les demandes d'aide récentes + * + * @param nombreJours nombre de jours + * @param page numéro de page + * @param size taille de la page + * @return liste des demandes d'aide récentes + */ + public List listerAidesRecentes(int nombreJours, int page, int size) { + LOG.debugf("Récupération des demandes d'aide récentes (%d jours)", nombreJours); + + List aides = aideRepository.findAidesRecentes( + nombreJours, + Page.of(page, size), + Sort.by("dateCreation").descending() + ); + + return aides.stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + } + + // ===== MÉTHODES DE VALIDATION ET SÉCURITÉ ===== + + /** + * Valide les règles métier pour une demande d'aide + * + * @param aide l'aide à valider + */ + private void validerReglesMétier(Aide aide) { + // Validation du montant demandé + if (aide.getMontantDemande() != null && aide.getMontantDemande().compareTo(BigDecimal.ZERO) <= 0) { + throw new IllegalArgumentException("Le montant demandé doit être positif"); + } + + // Validation de la date limite + if (aide.getDateLimite() != null && aide.getDateLimite().isBefore(LocalDate.now())) { + throw new IllegalArgumentException("La date limite ne peut pas être dans le passé"); + } + + // Validation du type d'aide et du montant + if (aide.getTypeAide() == TypeAide.AIDE_FINANCIERE && aide.getMontantDemande() == null) { + throw new IllegalArgumentException("Le montant demandé est obligatoire pour une aide financière"); + } + + // Validation des justificatifs pour certains types d'aide + if ((aide.getTypeAide() == TypeAide.AIDE_MEDICALE || aide.getTypeAide() == TypeAide.AIDE_JURIDIQUE) + && !aide.getJustificatifsFournis()) { + LOG.warnf("Justificatifs recommandés pour le type d'aide: %s", aide.getTypeAide()); + } + } + + /** + * Vérifie si l'utilisateur actuel peut modifier une demande d'aide + * + * @param aide l'aide à vérifier + * @return true si l'utilisateur peut modifier l'aide + */ + private boolean peutModifierAide(Aide aide) { + String emailUtilisateur = keycloakService.getCurrentUserEmail(); + + // Le demandeur peut modifier sa propre demande + if (aide.getMembreDemandeur().getEmail().equals(emailUtilisateur)) { + return true; + } + + // Les administrateurs peuvent modifier toutes les demandes + return keycloakService.hasRole("admin") || keycloakService.hasRole("gestionnaire_aide"); + } + + /** + * Vérifie si l'utilisateur actuel peut évaluer une demande d'aide + * + * @param aide l'aide à vérifier + * @return true si l'utilisateur peut évaluer l'aide + */ + private boolean peutEvaluerAide(Aide aide) { + String emailUtilisateur = keycloakService.getCurrentUserEmail(); + + // Le demandeur ne peut pas évaluer sa propre demande + if (aide.getMembreDemandeur().getEmail().equals(emailUtilisateur)) { + return false; + } + + // Seuls les évaluateurs autorisés peuvent évaluer + return keycloakService.hasRole("admin") || + keycloakService.hasRole("evaluateur_aide") || + keycloakService.hasRole("gestionnaire_aide"); + } + + /** + * Vérifie si l'utilisateur actuel peut gérer les versements + * + * @param aide l'aide à vérifier + * @return true si l'utilisateur peut gérer les versements + */ + private boolean peutGererVersement(Aide aide) { + return keycloakService.hasRole("admin") || + keycloakService.hasRole("tresorier") || + keycloakService.hasRole("gestionnaire_aide"); + } + + /** + * Vérifie si l'utilisateur actuel peut annuler une demande d'aide + * + * @param aide l'aide à vérifier + * @return true si l'utilisateur peut annuler l'aide + */ + private boolean peutAnnulerAide(Aide aide) { + String emailUtilisateur = keycloakService.getCurrentUserEmail(); + + // Le demandeur peut annuler sa propre demande si elle n'est pas encore approuvée + if (aide.getMembreDemandeur().getEmail().equals(emailUtilisateur) && + aide.getStatut() == StatutAide.EN_ATTENTE) { + return true; + } + + // Les administrateurs peuvent annuler toutes les demandes + return keycloakService.hasRole("admin") || keycloakService.hasRole("gestionnaire_aide"); + } + + // ===== MÉTHODES DE CONVERSION DTO/ENTITY ===== + + /** + * Convertit une entité Aide en DTO + * + * @param aide l'entité à convertir + * @return le DTO correspondant + */ + public AideDTO convertToDTO(Aide aide) { + if (aide == null) { + return null; + } + + AideDTO dto = new AideDTO(); + + // Génération d'UUID basé sur l'ID numérique pour compatibilité + dto.setId(UUID.nameUUIDFromBytes(("aide-" + aide.id).getBytes())); + + // Copie des champs de base + dto.setNumeroReference(aide.getNumeroReference()); + dto.setTitre(aide.getTitre()); + dto.setDescription(aide.getDescription()); + dto.setMontantDemande(aide.getMontantDemande()); + dto.setMontantApprouve(aide.getMontantApprouve()); + dto.setMontantVerse(aide.getMontantVerse()); + dto.setDevise(aide.getDevise()); + dto.setPriorite(aide.getPriorite()); + dto.setDateLimite(aide.getDateLimite()); + dto.setDateDebutAide(aide.getDateDebutAide()); + dto.setDateFinAide(aide.getDateFinAide()); + dto.setJustificatifsFournis(aide.getJustificatifsFournis()); + dto.setDocumentsJoints(aide.getDocumentsJoints()); + dto.setCommentairesEvaluateur(aide.getCommentairesEvaluateur()); + dto.setDateEvaluation(aide.getDateEvaluation()); + dto.setModeVersement(aide.getModeVersement()); + dto.setNumeroTransaction(aide.getNumeroTransaction()); + dto.setDateVersement(aide.getDateVersement()); + dto.setCommentairesBeneficiaire(aide.getCommentairesBeneficiaire()); + dto.setNoteSatisfaction(aide.getNoteSatisfaction()); + dto.setAidePublique(aide.getAidePublique()); + dto.setAideAnonyme(aide.getAideAnonyme()); + dto.setNombreVues(aide.getNombreVues()); + dto.setRaisonRejet(aide.getRaisonRejet()); + dto.setDateRejet(aide.getDateRejet()); + + // Conversion des énumérations vers String + if (aide.getStatut() != null) { + dto.setStatut(aide.getStatut().name()); + } + if (aide.getTypeAide() != null) { + dto.setTypeAide(aide.getTypeAide().name()); + } + + // Informations du membre demandeur + if (aide.getMembreDemandeur() != null) { + dto.setMembreDemandeurId(UUID.nameUUIDFromBytes(("membre-" + aide.getMembreDemandeur().id).getBytes())); + dto.setNomDemandeur(aide.getNomDemandeur()); + dto.setNumeroMembreDemandeur(aide.getMembreDemandeur().getNumeroMembre()); + } + + // Informations de l'organisation + if (aide.getOrganisation() != null) { + dto.setAssociationId(UUID.nameUUIDFromBytes(("organisation-" + aide.getOrganisation().id).getBytes())); + dto.setNomAssociation(aide.getOrganisation().getNom()); + } + + // Informations de l'évaluateur (pas de champs spécifiques dans AideDTO) + // Les informations d'évaluation sont dans dateEvaluation et commentairesEvaluateur + + // Informations de rejet + if (aide.getRejetePar() != null) { + dto.setRejeteParId(UUID.nameUUIDFromBytes(("membre-" + aide.getRejetePar().id).getBytes())); + dto.setRejetePar(aide.getRejetePar().getNomComplet()); + } + + // Champs d'audit (hérités de BaseDTO) + dto.setActif(aide.getActif()); + dto.setDateCreation(aide.getDateCreation()); + dto.setDateModification(aide.getDateModification()); + // Les champs creePar, modifiePar et version sont gérés par BaseDTO + + return dto; + } + + /** + * Convertit un DTO en entité Aide + * + * @param dto le DTO à convertir + * @return l'entité correspondante + */ + public Aide convertFromDTO(AideDTO dto) { + if (dto == null) { + return null; + } + + Aide aide = new Aide(); + + // Copie des champs de base + aide.setNumeroReference(dto.getNumeroReference()); + aide.setTitre(dto.getTitre()); + aide.setDescription(dto.getDescription()); + aide.setMontantDemande(dto.getMontantDemande()); + aide.setMontantApprouve(dto.getMontantApprouve()); + aide.setMontantVerse(dto.getMontantVerse()); + aide.setDevise(dto.getDevise()); + aide.setPriorite(dto.getPriorite()); + aide.setDateLimite(dto.getDateLimite()); + aide.setDateDebutAide(dto.getDateDebutAide()); + aide.setDateFinAide(dto.getDateFinAide()); + aide.setJustificatifsFournis(dto.getJustificatifsFournis()); + aide.setDocumentsJoints(dto.getDocumentsJoints()); + aide.setCommentairesEvaluateur(dto.getCommentairesEvaluateur()); + aide.setDateEvaluation(dto.getDateEvaluation()); + aide.setModeVersement(dto.getModeVersement()); + aide.setNumeroTransaction(dto.getNumeroTransaction()); + aide.setDateVersement(dto.getDateVersement()); + aide.setCommentairesBeneficiaire(dto.getCommentairesBeneficiaire()); + aide.setNoteSatisfaction(dto.getNoteSatisfaction()); + aide.setAidePublique(dto.getAidePublique()); + aide.setAideAnonyme(dto.getAideAnonyme()); + aide.setNombreVues(dto.getNombreVues()); + aide.setRaisonRejet(dto.getRaisonRejet()); + aide.setDateRejet(dto.getDateRejet()); + + // Champs d'audit + aide.setActif(dto.isActif()); + aide.setDateCreation(dto.getDateCreation()); + aide.setDateModification(dto.getDateModification()); + + // Conversion des énumérations depuis String + if (dto.getStatut() != null && !dto.getStatut().isEmpty()) { + try { + aide.setStatut(StatutAide.valueOf(dto.getStatut())); + } catch (IllegalArgumentException e) { + LOG.warnf("Statut invalide: %s, utilisation de EN_ATTENTE par défaut", dto.getStatut()); + aide.setStatut(StatutAide.EN_ATTENTE); + } + } + + if (dto.getTypeAide() != null && !dto.getTypeAide().isEmpty()) { + try { + // Conversion du String vers l'énumération TypeAide + String typeAideStr = dto.getTypeAide(); + // Mapping des valeurs du DTO vers l'énumération + TypeAide typeAide = switch (typeAideStr) { + case "FINANCIERE" -> TypeAide.AIDE_FINANCIERE; + case "MATERIELLE" -> TypeAide.AIDE_FINANCIERE; // Pas d'équivalent exact + case "MEDICALE" -> TypeAide.AIDE_MEDICALE; + case "JURIDIQUE" -> TypeAide.AIDE_JURIDIQUE; + case "LOGEMENT" -> TypeAide.AIDE_LOGEMENT; + case "EDUCATION" -> TypeAide.AIDE_EDUCATIVE; + case "AUTRE" -> TypeAide.AUTRE; + default -> { + LOG.warnf("Type d'aide non mappé: %s, utilisation de AUTRE", typeAideStr); + yield TypeAide.AUTRE; + } + }; + aide.setTypeAide(typeAide); + } catch (Exception e) { + LOG.warnf("Erreur lors de la conversion du type d'aide: %s", dto.getTypeAide()); + aide.setTypeAide(TypeAide.AUTRE); + } + } + + return aide; + } + + /** + * Convertit une liste d'entités en liste de DTOs + * + * @param aides liste des entités + * @return liste des DTOs + */ + public List convertToDTOList(List aides) { + if (aides == null) { + return null; + } + + return aides.stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + } +} diff --git a/unionflow-server-impl-quarkus/src/test/java/dev/lions/unionflow/server/resource/AideResourceTest.java b/unionflow-server-impl-quarkus/src/test/java/dev/lions/unionflow/server/resource/AideResourceTest.java new file mode 100644 index 0000000..79fb2bf --- /dev/null +++ b/unionflow-server-impl-quarkus/src/test/java/dev/lions/unionflow/server/resource/AideResourceTest.java @@ -0,0 +1,375 @@ +package dev.lions.unionflow.server.resource; + +import dev.lions.unionflow.server.api.dto.solidarite.aide.AideDTO; +import dev.lions.unionflow.server.api.enums.solidarite.StatutAide; +import dev.lions.unionflow.server.service.AideService; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.mockito.InjectMock; +import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; +import jakarta.ws.rs.NotFoundException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.when; + +/** + * Tests d'intégration pour AideResource + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-15 + */ +@QuarkusTest +@DisplayName("AideResource - Tests d'intégration") +class AideResourceTest { + + @InjectMock + AideService aideService; + + private AideDTO aideDTOTest; + private List listeAidesTest; + + @BeforeEach + void setUp() { + // DTO de test + aideDTOTest = new AideDTO(); + aideDTOTest.setId(UUID.randomUUID()); + aideDTOTest.setNumeroReference("AIDE-2025-TEST01"); + aideDTOTest.setTitre("Aide médicale urgente"); + aideDTOTest.setDescription("Demande d'aide pour frais médicaux urgents"); + aideDTOTest.setTypeAide("MEDICALE"); + aideDTOTest.setMontantDemande(new BigDecimal("500000.00")); + aideDTOTest.setStatut("EN_ATTENTE"); + aideDTOTest.setPriorite("URGENTE"); + aideDTOTest.setMembreDemandeurId(UUID.randomUUID()); + aideDTOTest.setAssociationId(UUID.randomUUID()); + aideDTOTest.setActif(true); + + // Liste de test + listeAidesTest = Arrays.asList(aideDTOTest); + } + + @Nested + @DisplayName("Tests des endpoints CRUD") + class CrudEndpointsTests { + + @Test + @TestSecurity(user = "admin", roles = {"admin"}) + @DisplayName("GET /api/aides - Liste des aides") + void testListerAides() { + // Given + when(aideService.listerAidesActives(0, 20)).thenReturn(listeAidesTest); + + // When & Then + given() + .when() + .get("/api/aides") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("size()", is(1)) + .body("[0].titre", equalTo("Aide médicale urgente")) + .body("[0].statut", equalTo("EN_ATTENTE")); + } + + @Test + @TestSecurity(user = "admin", roles = {"admin"}) + @DisplayName("GET /api/aides/{id} - Récupération par ID") + void testObtenirAideParId() { + // Given + when(aideService.obtenirAideParId(1L)).thenReturn(aideDTOTest); + + // When & Then + given() + .when() + .get("/api/aides/1") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("titre", equalTo("Aide médicale urgente")) + .body("numeroReference", equalTo("AIDE-2025-TEST01")); + } + + @Test + @TestSecurity(user = "admin", roles = {"admin"}) + @DisplayName("GET /api/aides/{id} - Aide non trouvée") + void testObtenirAideParId_NonTrouvee() { + // Given + when(aideService.obtenirAideParId(999L)).thenThrow(new NotFoundException("Demande d'aide non trouvée")); + + // When & Then + given() + .when() + .get("/api/aides/999") + .then() + .statusCode(404) + .contentType(ContentType.JSON) + .body("error", equalTo("Demande d'aide non trouvée")); + } + + @Test + @TestSecurity(user = "admin", roles = {"admin"}) + @DisplayName("POST /api/aides - Création d'aide") + void testCreerAide() { + // Given + when(aideService.creerAide(any(AideDTO.class))).thenReturn(aideDTOTest); + + // When & Then + given() + .contentType(ContentType.JSON) + .body(aideDTOTest) + .when() + .post("/api/aides") + .then() + .statusCode(201) + .contentType(ContentType.JSON) + .body("titre", equalTo("Aide médicale urgente")) + .body("numeroReference", equalTo("AIDE-2025-TEST01")); + } + + @Test + @TestSecurity(user = "admin", roles = {"admin"}) + @DisplayName("PUT /api/aides/{id} - Mise à jour d'aide") + void testMettreAJourAide() { + // Given + AideDTO aideMiseAJour = new AideDTO(); + aideMiseAJour.setTitre("Titre modifié"); + aideMiseAJour.setDescription("Description modifiée"); + + when(aideService.mettreAJourAide(eq(1L), any(AideDTO.class))).thenReturn(aideMiseAJour); + + // When & Then + given() + .contentType(ContentType.JSON) + .body(aideMiseAJour) + .when() + .put("/api/aides/1") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("titre", equalTo("Titre modifié")); + } + } + + @Nested + @DisplayName("Tests des endpoints métier") + class EndpointsMetierTests { + + @Test + @TestSecurity(user = "evaluateur", roles = {"evaluateur_aide"}) + @DisplayName("POST /api/aides/{id}/approuver - Approbation d'aide") + void testApprouverAide() { + // Given + AideDTO aideApprouvee = new AideDTO(); + aideApprouvee.setStatut("APPROUVEE"); + aideApprouvee.setMontantApprouve(new BigDecimal("400000.00")); + + when(aideService.approuverAide(eq(1L), any(BigDecimal.class), anyString())) + .thenReturn(aideApprouvee); + + Map approbationData = Map.of( + "montantApprouve", "400000.00", + "commentaires", "Aide approuvée après évaluation" + ); + + // When & Then + given() + .contentType(ContentType.JSON) + .body(approbationData) + .when() + .post("/api/aides/1/approuver") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("statut", equalTo("APPROUVEE")); + } + + @Test + @TestSecurity(user = "evaluateur", roles = {"evaluateur_aide"}) + @DisplayName("POST /api/aides/{id}/rejeter - Rejet d'aide") + void testRejeterAide() { + // Given + AideDTO aideRejetee = new AideDTO(); + aideRejetee.setStatut("REJETEE"); + aideRejetee.setRaisonRejet("Dossier incomplet"); + + when(aideService.rejeterAide(eq(1L), anyString())).thenReturn(aideRejetee); + + Map rejetData = Map.of( + "raisonRejet", "Dossier incomplet" + ); + + // When & Then + given() + .contentType(ContentType.JSON) + .body(rejetData) + .when() + .post("/api/aides/1/rejeter") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("statut", equalTo("REJETEE")); + } + + @Test + @TestSecurity(user = "tresorier", roles = {"tresorier"}) + @DisplayName("POST /api/aides/{id}/verser - Versement d'aide") + void testMarquerCommeVersee() { + // Given + AideDTO aideVersee = new AideDTO(); + aideVersee.setStatut("VERSEE"); + aideVersee.setMontantVerse(new BigDecimal("400000.00")); + + when(aideService.marquerCommeVersee(eq(1L), any(BigDecimal.class), anyString(), anyString())) + .thenReturn(aideVersee); + + Map versementData = Map.of( + "montantVerse", "400000.00", + "modeVersement", "MOBILE_MONEY", + "numeroTransaction", "TXN123456789" + ); + + // When & Then + given() + .contentType(ContentType.JSON) + .body(versementData) + .when() + .post("/api/aides/1/verser") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("statut", equalTo("VERSEE")); + } + } + + @Nested + @DisplayName("Tests des endpoints de recherche") + class EndpointsRechercheTests { + + @Test + @TestSecurity(user = "membre", roles = {"membre"}) + @DisplayName("GET /api/aides/statut/{statut} - Filtrage par statut") + void testListerAidesParStatut() { + // Given + when(aideService.listerAidesParStatut(StatutAide.EN_ATTENTE, 0, 20)) + .thenReturn(listeAidesTest); + + // When & Then + given() + .when() + .get("/api/aides/statut/EN_ATTENTE") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("size()", is(1)) + .body("[0].statut", equalTo("EN_ATTENTE")); + } + + @Test + @TestSecurity(user = "membre", roles = {"membre"}) + @DisplayName("GET /api/aides/membre/{membreId} - Aides d'un membre") + void testListerAidesParMembre() { + // Given + when(aideService.listerAidesParMembre(1L, 0, 20)).thenReturn(listeAidesTest); + + // When & Then + given() + .when() + .get("/api/aides/membre/1") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("size()", is(1)); + } + + @Test + @TestSecurity(user = "membre", roles = {"membre"}) + @DisplayName("GET /api/aides/recherche - Recherche textuelle") + void testRechercherAides() { + // Given + when(aideService.rechercherAides("médical", 0, 20)).thenReturn(listeAidesTest); + + // When & Then + given() + .queryParam("q", "médical") + .when() + .get("/api/aides/recherche") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("size()", is(1)); + } + + @Test + @TestSecurity(user = "admin", roles = {"admin"}) + @DisplayName("GET /api/aides/statistiques - Statistiques") + void testObtenirStatistiques() { + // Given + Map statistiques = Map.of( + "total", 100L, + "enAttente", 25L, + "approuvees", 50L, + "versees", 20L + ); + when(aideService.obtenirStatistiquesGlobales()).thenReturn(statistiques); + + // When & Then + given() + .when() + .get("/api/aides/statistiques") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("total", equalTo(100)) + .body("enAttente", equalTo(25)) + .body("approuvees", equalTo(50)) + .body("versees", equalTo(20)); + } + } + + @Nested + @DisplayName("Tests de sécurité") + class SecurityTests { + + @Test + @DisplayName("Accès non authentifié - 401") + void testAccesNonAuthentifie() { + given() + .when() + .get("/api/aides") + .then() + .statusCode(401); + } + + @Test + @TestSecurity(user = "membre", roles = {"membre"}) + @DisplayName("Accès non autorisé pour approbation - 403") + void testAccesNonAutorisePourApprobation() { + Map approbationData = Map.of( + "montantApprouve", "400000.00", + "commentaires", "Test" + ); + + given() + .contentType(ContentType.JSON) + .body(approbationData) + .when() + .post("/api/aides/1/approuver") + .then() + .statusCode(403); + } + } +} diff --git a/unionflow-server-impl-quarkus/src/test/java/dev/lions/unionflow/server/service/AideServiceTest.java b/unionflow-server-impl-quarkus/src/test/java/dev/lions/unionflow/server/service/AideServiceTest.java new file mode 100644 index 0000000..1c7790c --- /dev/null +++ b/unionflow-server-impl-quarkus/src/test/java/dev/lions/unionflow/server/service/AideServiceTest.java @@ -0,0 +1,332 @@ +package dev.lions.unionflow.server.service; + +import dev.lions.unionflow.server.api.dto.solidarite.aide.AideDTO; +import dev.lions.unionflow.server.api.enums.solidarite.StatutAide; +import dev.lions.unionflow.server.api.enums.solidarite.TypeAide; +import dev.lions.unionflow.server.entity.Aide; +import dev.lions.unionflow.server.entity.Membre; +import dev.lions.unionflow.server.entity.Organisation; +import dev.lions.unionflow.server.repository.AideRepository; +import dev.lions.unionflow.server.repository.MembreRepository; +import dev.lions.unionflow.server.repository.OrganisationRepository; +import dev.lions.unionflow.server.security.KeycloakService; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.mockito.InjectMock; +import jakarta.inject.Inject; +import jakarta.ws.rs.NotFoundException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.*; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +/** + * Tests unitaires pour AideService + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-15 + */ +@QuarkusTest +@DisplayName("AideService - Tests unitaires") +class AideServiceTest { + + @Inject + AideService aideService; + + @InjectMock + AideRepository aideRepository; + + @InjectMock + MembreRepository membreRepository; + + @InjectMock + OrganisationRepository organisationRepository; + + @InjectMock + KeycloakService keycloakService; + + private Membre membreTest; + private Organisation organisationTest; + private Aide aideTest; + private AideDTO aideDTOTest; + + @BeforeEach + void setUp() { + // Membre de test + membreTest = new Membre(); + membreTest.id = 1L; + membreTest.setNumeroMembre("UF-2025-TEST001"); + membreTest.setNom("Dupont"); + membreTest.setPrenom("Jean"); + membreTest.setEmail("jean.dupont@test.com"); + membreTest.setActif(true); + + // Organisation de test + organisationTest = new Organisation(); + organisationTest.id = 1L; + organisationTest.setNom("Lions Club Test"); + organisationTest.setEmail("contact@lionstest.com"); + organisationTest.setActif(true); + + // Aide de test + aideTest = new Aide(); + aideTest.id = 1L; + aideTest.setNumeroReference("AIDE-2025-TEST01"); + aideTest.setTitre("Aide médicale urgente"); + aideTest.setDescription("Demande d'aide pour frais médicaux urgents"); + aideTest.setTypeAide(TypeAide.AIDE_MEDICALE); + aideTest.setMontantDemande(new BigDecimal("500000.00")); + aideTest.setStatut(StatutAide.EN_ATTENTE); + aideTest.setPriorite("URGENTE"); + aideTest.setMembreDemandeur(membreTest); + aideTest.setOrganisation(organisationTest); + aideTest.setActif(true); + aideTest.setDateCreation(LocalDateTime.now()); + + // DTO de test + aideDTOTest = new AideDTO(); + aideDTOTest.setId(UUID.randomUUID()); + aideDTOTest.setNumeroReference("AIDE-2025-TEST01"); + aideDTOTest.setTitre("Aide médicale urgente"); + aideDTOTest.setDescription("Demande d'aide pour frais médicaux urgents"); + aideDTOTest.setTypeAide("MEDICALE"); + aideDTOTest.setMontantDemande(new BigDecimal("500000.00")); + aideDTOTest.setStatut("EN_ATTENTE"); + aideDTOTest.setPriorite("URGENTE"); + aideDTOTest.setMembreDemandeurId(UUID.randomUUID()); + aideDTOTest.setAssociationId(UUID.randomUUID()); + aideDTOTest.setActif(true); + } + + @Nested + @DisplayName("Tests de création d'aide") + class CreationAideTests { + + @Test + @DisplayName("Création d'aide réussie") + void testCreerAide_Success() { + // Given + when(membreRepository.findByIdOptional(anyLong())).thenReturn(Optional.of(membreTest)); + when(organisationRepository.findByIdOptional(anyLong())).thenReturn(Optional.of(organisationTest)); + when(keycloakService.getCurrentUserEmail()).thenReturn("admin@test.com"); + + ArgumentCaptor aideCaptor = ArgumentCaptor.forClass(Aide.class); + doNothing().when(aideRepository).persist(aideCaptor.capture()); + + // When + AideDTO result = aideService.creerAide(aideDTOTest); + + // Then + assertThat(result).isNotNull(); + assertThat(result.getTitre()).isEqualTo(aideDTOTest.getTitre()); + assertThat(result.getDescription()).isEqualTo(aideDTOTest.getDescription()); + + Aide aidePersistee = aideCaptor.getValue(); + assertThat(aidePersistee.getTitre()).isEqualTo(aideDTOTest.getTitre()); + assertThat(aidePersistee.getMembreDemandeur()).isEqualTo(membreTest); + assertThat(aidePersistee.getOrganisation()).isEqualTo(organisationTest); + assertThat(aidePersistee.getCreePar()).isEqualTo("admin@test.com"); + + verify(aideRepository).persist(any(Aide.class)); + } + + @Test + @DisplayName("Création d'aide - Membre non trouvé") + void testCreerAide_MembreNonTrouve() { + // Given + when(membreRepository.findByIdOptional(anyLong())).thenReturn(Optional.empty()); + + // When & Then + assertThatThrownBy(() -> aideService.creerAide(aideDTOTest)) + .isInstanceOf(NotFoundException.class) + .hasMessageContaining("Membre demandeur non trouvé"); + + verify(aideRepository, never()).persist(any(Aide.class)); + } + + @Test + @DisplayName("Création d'aide - Organisation non trouvée") + void testCreerAide_OrganisationNonTrouvee() { + // Given + when(membreRepository.findByIdOptional(anyLong())).thenReturn(Optional.of(membreTest)); + when(organisationRepository.findByIdOptional(anyLong())).thenReturn(Optional.empty()); + + // When & Then + assertThatThrownBy(() -> aideService.creerAide(aideDTOTest)) + .isInstanceOf(NotFoundException.class) + .hasMessageContaining("Organisation non trouvée"); + + verify(aideRepository, never()).persist(any(Aide.class)); + } + + @Test + @DisplayName("Création d'aide - Montant invalide") + void testCreerAide_MontantInvalide() { + // Given + aideDTOTest.setMontantDemande(new BigDecimal("-100.00")); + when(membreRepository.findByIdOptional(anyLong())).thenReturn(Optional.of(membreTest)); + when(organisationRepository.findByIdOptional(anyLong())).thenReturn(Optional.of(organisationTest)); + + // When & Then + assertThatThrownBy(() -> aideService.creerAide(aideDTOTest)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Le montant demandé doit être positif"); + + verify(aideRepository, never()).persist(any(Aide.class)); + } + } + + @Nested + @DisplayName("Tests de récupération d'aide") + class RecuperationAideTests { + + @Test + @DisplayName("Récupération d'aide par ID réussie") + void testObtenirAideParId_Success() { + // Given + when(aideRepository.findByIdOptional(1L)).thenReturn(Optional.of(aideTest)); + when(keycloakService.getCurrentUserEmail()).thenReturn("autre@test.com"); + + // When + AideDTO result = aideService.obtenirAideParId(1L); + + // Then + assertThat(result).isNotNull(); + assertThat(result.getTitre()).isEqualTo(aideTest.getTitre()); + assertThat(result.getDescription()).isEqualTo(aideTest.getDescription()); + assertThat(result.getStatut()).isEqualTo(aideTest.getStatut().name()); + } + + @Test + @DisplayName("Récupération d'aide par ID - Non trouvée") + void testObtenirAideParId_NonTrouvee() { + // Given + when(aideRepository.findByIdOptional(999L)).thenReturn(Optional.empty()); + + // When & Then + assertThatThrownBy(() -> aideService.obtenirAideParId(999L)) + .isInstanceOf(NotFoundException.class) + .hasMessageContaining("Demande d'aide non trouvée"); + } + + @Test + @DisplayName("Récupération d'aide par référence réussie") + void testObtenirAideParReference_Success() { + // Given + String reference = "AIDE-2025-TEST01"; + when(aideRepository.findByNumeroReference(reference)).thenReturn(Optional.of(aideTest)); + when(keycloakService.getCurrentUserEmail()).thenReturn("autre@test.com"); + + // When + AideDTO result = aideService.obtenirAideParReference(reference); + + // Then + assertThat(result).isNotNull(); + assertThat(result.getNumeroReference()).isEqualTo(reference); + } + } + + @Nested + @DisplayName("Tests de mise à jour d'aide") + class MiseAJourAideTests { + + @Test + @DisplayName("Mise à jour d'aide réussie") + void testMettreAJourAide_Success() { + // Given + when(aideRepository.findByIdOptional(1L)).thenReturn(Optional.of(aideTest)); + when(keycloakService.getCurrentUserEmail()).thenReturn("jean.dupont@test.com"); + when(keycloakService.hasRole("admin")).thenReturn(false); + when(keycloakService.hasRole("gestionnaire_aide")).thenReturn(false); + + AideDTO aideMiseAJour = new AideDTO(); + aideMiseAJour.setTitre("Titre modifié"); + aideMiseAJour.setDescription("Description modifiée"); + aideMiseAJour.setMontantDemande(new BigDecimal("600000.00")); + aideMiseAJour.setPriorite("HAUTE"); + + // When + AideDTO result = aideService.mettreAJourAide(1L, aideMiseAJour); + + // Then + assertThat(result).isNotNull(); + assertThat(aideTest.getTitre()).isEqualTo("Titre modifié"); + assertThat(aideTest.getDescription()).isEqualTo("Description modifiée"); + assertThat(aideTest.getMontantDemande()).isEqualTo(new BigDecimal("600000.00")); + assertThat(aideTest.getPriorite()).isEqualTo("HAUTE"); + } + + @Test + @DisplayName("Mise à jour d'aide - Accès non autorisé") + void testMettreAJourAide_AccesNonAutorise() { + // Given + when(aideRepository.findByIdOptional(1L)).thenReturn(Optional.of(aideTest)); + when(keycloakService.getCurrentUserEmail()).thenReturn("autre@test.com"); + when(keycloakService.hasRole("admin")).thenReturn(false); + when(keycloakService.hasRole("gestionnaire_aide")).thenReturn(false); + + AideDTO aideMiseAJour = new AideDTO(); + aideMiseAJour.setTitre("Titre modifié"); + + // When & Then + assertThatThrownBy(() -> aideService.mettreAJourAide(1L, aideMiseAJour)) + .isInstanceOf(SecurityException.class) + .hasMessageContaining("Vous n'avez pas les permissions"); + } + } + + @Nested + @DisplayName("Tests de conversion DTO/Entity") + class ConversionTests { + + @Test + @DisplayName("Conversion Entity vers DTO") + void testConvertToDTO() { + // When + AideDTO result = aideService.convertToDTO(aideTest); + + // Then + assertThat(result).isNotNull(); + assertThat(result.getTitre()).isEqualTo(aideTest.getTitre()); + assertThat(result.getDescription()).isEqualTo(aideTest.getDescription()); + assertThat(result.getMontantDemande()).isEqualTo(aideTest.getMontantDemande()); + assertThat(result.getStatut()).isEqualTo(aideTest.getStatut().name()); + assertThat(result.getTypeAide()).isEqualTo(aideTest.getTypeAide().name()); + } + + @Test + @DisplayName("Conversion DTO vers Entity") + void testConvertFromDTO() { + // When + Aide result = aideService.convertFromDTO(aideDTOTest); + + // Then + assertThat(result).isNotNull(); + assertThat(result.getTitre()).isEqualTo(aideDTOTest.getTitre()); + assertThat(result.getDescription()).isEqualTo(aideDTOTest.getDescription()); + assertThat(result.getMontantDemande()).isEqualTo(aideDTOTest.getMontantDemande()); + assertThat(result.getStatut()).isEqualTo(StatutAide.EN_ATTENTE); + assertThat(result.getTypeAide()).isEqualTo(TypeAide.AIDE_MEDICALE); + } + + @Test + @DisplayName("Conversion DTO null") + void testConvertFromDTO_Null() { + // When + Aide result = aideService.convertFromDTO(null); + + // Then + assertThat(result).isNull(); + } + } +}